diff --git a/rio/components/app_root.py b/rio/components/app_root.py
index 1549efb5..2925d55f 100644
--- a/rio/components/app_root.py
+++ b/rio/components/app_root.py
@@ -70,12 +70,12 @@ class Sidebar(component.Component):
rio.Text(
self.session.app.name,
style="heading2",
- justify='left',
+ justify="left",
),
rio.Text(
"TODO: Subtext",
style="dim",
- justify='left',
+ justify="left",
),
align_y=0,
),
diff --git a/rio/components/labeled_column.py b/rio/components/labeled_column.py
index 575f7e10..2a6b4369 100644
--- a/rio/components/labeled_column.py
+++ b/rio/components/labeled_column.py
@@ -102,7 +102,7 @@ class LabeledColumn(Component):
def build(self) -> Component:
rows = [
[
- rio.Text(label, justify='right'),
+ rio.Text(label, justify="right"),
child,
]
for label, child in self.content.items()
diff --git a/rio/components/list_items.py b/rio/components/list_items.py
index 62e017da..3b5e5c86 100644
--- a/rio/components/list_items.py
+++ b/rio/components/list_items.py
@@ -195,7 +195,7 @@ class SimpleListItem(Component):
text_children = [
rio.Text(
self.text,
- justify='left',
+ justify="left",
)
]
@@ -205,7 +205,7 @@ class SimpleListItem(Component):
self.secondary_text,
wrap=True,
style="dim",
- justify='left',
+ justify="left",
)
)
diff --git a/rio/debug/client_side_debugger/component_details.py b/rio/debug/client_side_debugger/component_details.py
index 0d88a90b..b2ab9aee 100644
--- a/rio/debug/client_side_debugger/component_details.py
+++ b/rio/debug/client_side_debugger/component_details.py
@@ -106,7 +106,7 @@ class ComponentDetails(rio.Component):
rio.Text(
text,
style="dim" if is_label else "text",
- justify='right' if is_label else 'left',
+ justify="right" if is_label else "left",
),
row_index,
column_index,
@@ -126,7 +126,7 @@ class ComponentDetails(rio.Component):
rio.Text(
target.key,
style="dim",
- justify='left',
+ justify="left",
),
]
@@ -160,7 +160,7 @@ class ComponentDetails(rio.Component):
rio.Text(
f"{file} line {line}",
style="dim",
- justify='left',
+ justify="left",
),
row_index,
0,
@@ -227,8 +227,8 @@ class ComponentDetails(rio.Component):
row_index += 1
# Header
- result.add(rio.Text("width", style="dim", justify='left'), row_index, 1)
- result.add(rio.Text("height", style="dim", justify='left'), row_index, 2)
+ result.add(rio.Text("width", style="dim", justify="left"), row_index, 1)
+ result.add(rio.Text("height", style="dim", justify="left"), row_index, 2)
row_index += 1
# The size as specified in Python
diff --git a/rio/debug/client_side_debugger/deploy_page.py b/rio/debug/client_side_debugger/deploy_page.py
index ad2b2079..6d5c414e 100644
--- a/rio/debug/client_side_debugger/deploy_page.py
+++ b/rio/debug/client_side_debugger/deploy_page.py
@@ -8,7 +8,7 @@ class DeployPage(rio.Component):
"Deploy",
style="heading2",
margin=1,
- justify='left',
+ justify="left",
),
rio.Column(
rio.Icon(
diff --git a/rio/debug/client_side_debugger/docs_page.py b/rio/debug/client_side_debugger/docs_page.py
index 3c7db26e..4dc42371 100644
--- a/rio/debug/client_side_debugger/docs_page.py
+++ b/rio/debug/client_side_debugger/docs_page.py
@@ -8,7 +8,7 @@ class DocsPage(rio.Component):
"Documentation",
style="heading2",
margin=1,
- justify='left',
+ justify="left",
),
rio.Column(
rio.Text(
diff --git a/rio/debug/client_side_debugger/icons_page.py b/rio/debug/client_side_debugger/icons_page.py
index 45c14321..c0f53ff8 100644
--- a/rio/debug/client_side_debugger/icons_page.py
+++ b/rio/debug/client_side_debugger/icons_page.py
@@ -162,9 +162,11 @@ class IconsPage(rio.Component):
async def _on_select_icon_by_name(self, icon_name: str) -> None:
# Parse the icon name
- icon_set, icon_name, icon_variant = (
- rio.icon_registry.IconRegistry.parse_icon_name(icon_name)
- )
+ (
+ icon_set,
+ icon_name,
+ icon_variant,
+ ) = rio.icon_registry.IconRegistry.parse_icon_name(icon_name)
# Find all available variants
for (
@@ -210,7 +212,7 @@ class IconsPage(rio.Component):
rio.Text(
"Configure",
style="heading3",
- justify='left',
+ justify="left",
)
)
@@ -285,7 +287,7 @@ class IconsPage(rio.Component):
rio.Text(
"Example Code",
style="heading3",
- justify='left',
+ justify="left",
margin_top=1,
)
)
diff --git a/rio/debug/client_side_debugger/project_page.py b/rio/debug/client_side_debugger/project_page.py
index c1ae6a6f..52d13911 100644
--- a/rio/debug/client_side_debugger/project_page.py
+++ b/rio/debug/client_side_debugger/project_page.py
@@ -36,13 +36,13 @@ class ProjectPage(rio.Component):
rio.Text(
project_name,
style="heading2",
- justify='left',
+ justify="left",
),
rio.Text(
str(self.project.project_directory),
style="dim",
margin_bottom=2,
- justify='left',
+ justify="left",
),
rio.Text(
"To launch your project, Rio needs to know the name of your python module and in which variable you've stored your app. You can configure those here.",
diff --git a/rio/debug/client_side_debugger/theme_picker_page.py b/rio/debug/client_side_debugger/theme_picker_page.py
index 4cff9338..31333998 100644
--- a/rio/debug/client_side_debugger/theme_picker_page.py
+++ b/rio/debug/client_side_debugger/theme_picker_page.py
@@ -201,7 +201,7 @@ class PalettePicker(rio.Component): #
fill=palette.foreground,
),
selectable=False,
- justify='left',
+ justify="left",
),
rio.Text(
f"#{palette.background.hex}",
@@ -209,7 +209,7 @@ class PalettePicker(rio.Component): #
font_size=1,
fill=palette.foreground.replace(opacity=0.5),
),
- justify='left',
+ justify="left",
),
spacing=0.2,
margin_x=1,
@@ -419,7 +419,7 @@ class ThemePickerPage(rio.Component):
style="heading3",
margin_top=1,
margin_bottom=1,
- justify='left',
+ justify="left",
),
radius_sliders,
# Theme Variants
@@ -428,7 +428,7 @@ class ThemePickerPage(rio.Component):
style="heading3",
margin_top=1,
margin_bottom=1,
- justify='left',
+ justify="left",
),
rio.Row(
rio.Spacer(),
@@ -451,7 +451,7 @@ class ThemePickerPage(rio.Component):
style="heading3",
margin_top=1,
# margin_bottom=1, Not used for now, since markdown has an oddly large margin anyway
- justify='left',
+ justify="left",
),
rio.Markdown(
f"""
diff --git a/rio/snippets/snippet-files/old-tutorial-biography/__init__.py b/rio/snippets/snippet-files/old-tutorial-biography/__init__.py
deleted file mode 100644
index e56c4eb7..00000000
--- a/rio/snippets/snippet-files/old-tutorial-biography/__init__.py
+++ /dev/null
@@ -1,13 +0,0 @@
-import rio
-
-from . import pages
-
-app = rio.App(
- name="Biography",
- pages=[
- rio.Page("", pages.BiographyPage),
- ],
-)
-
-
-fastapi_app = app.as_fastapi()
diff --git a/rio/snippets/snippet-files/old-tutorial-biography/assets/jane-doe.jpg b/rio/snippets/snippet-files/old-tutorial-biography/assets/jane-doe.jpg
deleted file mode 100644
index aec4e550..00000000
Binary files a/rio/snippets/snippet-files/old-tutorial-biography/assets/jane-doe.jpg and /dev/null differ
diff --git a/rio/snippets/snippet-files/old-tutorial-biography/assets/john-doe.jpg b/rio/snippets/snippet-files/old-tutorial-biography/assets/john-doe.jpg
deleted file mode 100644
index d38610ad..00000000
Binary files a/rio/snippets/snippet-files/old-tutorial-biography/assets/john-doe.jpg and /dev/null differ
diff --git a/rio/snippets/snippet-files/old-tutorial-biography/components/__init__.py b/rio/snippets/snippet-files/old-tutorial-biography/components/__init__.py
deleted file mode 100644
index 2b535bcb..00000000
--- a/rio/snippets/snippet-files/old-tutorial-biography/components/__init__.py
+++ /dev/null
@@ -1,5 +0,0 @@
-from .about_me import AboutMe
-from .contact import Contact
-from .history import History
-from .projects import Projects
-from .skill_bars import SkillBars
diff --git a/rio/snippets/snippet-files/old-tutorial-biography/components/about_me.py b/rio/snippets/snippet-files/old-tutorial-biography/components/about_me.py
deleted file mode 100644
index b6bbbb86..00000000
--- a/rio/snippets/snippet-files/old-tutorial-biography/components/about_me.py
+++ /dev/null
@@ -1,41 +0,0 @@
-from pathlib import Path
-from typing import * # type: ignore
-
-import rio
-
-
-class AboutMe(rio.Component):
- def build(self) -> rio.Component:
- return rio.Row(
- rio.Image(
- Path("me.jpg"),
- width=11,
- height=11,
- corner_radius=99999,
- fill_mode="zoom",
- margin_right=2,
- ),
- rio.Column(
- rio.Text(
- "Jane Doe",
- style="heading1",
- justify='left',
- ),
- rio.Text(
- "Data Analyst",
- justify='left',
- ),
- rio.Text(
- "AI Researcher",
- justify='left',
- ),
- rio.Text(
- "Python Developer",
- justify='left',
- ),
- spacing=0.5,
- align_y=0,
- width="grow",
- ),
- width="grow",
- )
diff --git a/rio/snippets/snippet-files/old-tutorial-biography/components/contact.py b/rio/snippets/snippet-files/old-tutorial-biography/components/contact.py
deleted file mode 100644
index 8ca0c109..00000000
--- a/rio/snippets/snippet-files/old-tutorial-biography/components/contact.py
+++ /dev/null
@@ -1,50 +0,0 @@
-from dataclasses import KW_ONLY
-from typing import * # type: ignore
-
-import rio
-
-
-class Contact(rio.Component):
- text: str
- icon_name_or_url: str | rio.URL
- _: KW_ONLY
- link: rio.URL | None = None
-
- def build(self) -> rio.Component:
- # Prepare the image. This varies depending on whether an icon or an
- # image should be displayed.
- if isinstance(self.icon_name_or_url, str):
- image = rio.Icon(
- self.icon_name_or_url,
- width=2,
- height=2,
- margin_x=0.5,
- )
- else:
- image = rio.Image(
- self.icon_name_or_url,
- width=2,
- height=2,
- margin_x=0.5,
- )
-
- # Combine everything
- result = rio.Row(
- image,
- rio.Text(
- self.text,
- justify='left',
- width="grow",
- ),
- width="grow",
- margin=0.2,
- )
-
- # If a link is provided, wrap the result in a link
- if self.link is not None:
- result = rio.Link(
- result,
- self.link,
- )
-
- return result
diff --git a/rio/snippets/snippet-files/old-tutorial-biography/components/history.py b/rio/snippets/snippet-files/old-tutorial-biography/components/history.py
deleted file mode 100644
index a6c5233e..00000000
--- a/rio/snippets/snippet-files/old-tutorial-biography/components/history.py
+++ /dev/null
@@ -1,114 +0,0 @@
-from typing import * # type: ignore
-
-import rio
-
-
-class HistoryItem(rio.Component):
- from_string: str
- to_string: str
- title: str
- organization: str
- details: str
- icon: str
-
- is_open: bool = False
-
- def on_press(self) -> None:
- self.is_open = not self.is_open
-
- def build(self) -> rio.Component:
- return rio.Card(
- rio.Row(
- rio.Icon(
- self.icon,
- width=3,
- height=3,
- margin_right=2,
- align_x=0,
- align_y=0,
- ),
- rio.Column(
- rio.Text(
- f"{self.from_string} - {self.to_string}",
- justify='left',
- style=rio.TextStyle(fill=rio.Color.GREY),
- ),
- rio.Text(
- f"{self.title} at {self.organization}",
- style="heading3",
- justify='left',
- ),
- rio.Revealer(
- None,
- rio.Text(
- self.details,
- wrap=True,
- justify='left',
- ),
- is_open=self.is_open,
- ),
- spacing=0.3,
- width="grow",
- align_y=0,
- ),
- ),
- on_press=self.on_press,
- )
-
-
-class History(rio.Component):
- def build(self) -> rio.Component:
- return rio.Column(
- rio.Text(
- "Professional Experience",
- style="heading2",
- margin_top=3,
- ),
- HistoryItem(
- "2021",
- "Present",
- "AI Researcher",
- "DataWorks Inc.",
- "Developed and deployed machine learning models to improve the "
- "accuracy of the company's recommender system. Collaborated "
- "with a team of 10 researchers to implement a novel algorithm "
- "for predicting user behavior.",
- "science",
- ),
- HistoryItem(
- "2018",
- "2021",
- "Data Analyst",
- "Insight Technologies",
- "Conducted in-depth data analysis and implemented machine "
- "learning algorithms to improve predictive models, resulting "
- "in a 15% increase in accuracy. Collaborated with "
- "cross-functional teams to develop and deploy scalable AI "
- "solutions for optimizing business operations.",
- "monitoring",
- ),
- rio.Text(
- "Education",
- style="heading2",
- margin_top=3,
- ),
- HistoryItem(
- "2015",
- "2018",
- "Master in Data Science",
- "Carnegie Mellon University, Pittsburgh, PA",
- 'Thesis: "Predictive Modeling in Financial Markets using '
- 'Machine Learning Techniques"',
- "school",
- ),
- HistoryItem(
- "2012",
- "2015",
- "Bachelor in Software Engineering",
- "University of Texas, Austin, TX",
- 'Thesis: "Design and Implementation of an Intelligent Traffic '
- 'Control System"',
- "school",
- ),
- spacing=2,
- )
diff --git a/rio/snippets/snippet-files/old-tutorial-biography/components/projects.py b/rio/snippets/snippet-files/old-tutorial-biography/components/projects.py
deleted file mode 100644
index d67e89c6..00000000
--- a/rio/snippets/snippet-files/old-tutorial-biography/components/projects.py
+++ /dev/null
@@ -1,70 +0,0 @@
-from typing import * # type: ignore
-
-import rio
-
-
-class Project(rio.Component):
- name: str
- details: str
- link: rio.URL
-
- def on_press(self) -> None:
- self.session.navigate_to(self.link)
-
- def build(self) -> rio.Component:
- return rio.Card(
- rio.Column(
- rio.Text(
- self.name,
- style="heading3",
- ),
- rio.Text(
- self.details,
- wrap=True,
- ),
- spacing=0.5,
- ),
- on_press=self.on_press,
- )
-
-
-class Projects(rio.Component):
- def build(self) -> rio.Component:
- return rio.Grid(
- (
- Project(
- "uniserde",
- "Pure Python, easy to use Serializer",
- rio.URL("https://gitlab.com/Vivern/uniserde"),
- ),
- Project(
- "uniserde",
- "Pure Python, easy to use Serializer",
- rio.URL("https://gitlab.com/Vivern/uniserde"),
- ),
- Project(
- "uniserde",
- "Pure Python, easy to use Serializer",
- rio.URL("https://gitlab.com/Vivern/uniserde"),
- ),
- ),
- (
- Project(
- "uniserde",
- "Pure Python, easy to use Serializer",
- rio.URL("https://gitlab.com/Vivern/uniserde"),
- ),
- Project(
- "uniserde",
- "Pure Python, easy to use Serializer",
- rio.URL("https://gitlab.com/Vivern/uniserde"),
- ),
- Project(
- "uniserde",
- "Pure Python, easy to use Serializer",
- rio.URL("https://gitlab.com/Vivern/uniserde"),
- ),
- ),
- row_spacing=2,
- column_spacing=2,
- )
diff --git a/rio/snippets/snippet-files/old-tutorial-biography/components/skill_bars.py b/rio/snippets/snippet-files/old-tutorial-biography/components/skill_bars.py
deleted file mode 100644
index 5dcc2e36..00000000
--- a/rio/snippets/snippet-files/old-tutorial-biography/components/skill_bars.py
+++ /dev/null
@@ -1,30 +0,0 @@
-from typing import * # type: ignore
-
-import rio
-
-
-class SkillBars(rio.Component):
- skills: dict[str, int]
-
- def build(self) -> rio.Component:
- rows = []
-
- for name, level in self.skills.items():
- rows.append(
- rio.Column(
- rio.Text(
- name,
- justify='left',
- ),
- rio.ProgressBar(
- level / 10,
- ),
- )
- )
-
- return rio.Card(
- rio.Column(
- *rows,
- spacing=0.5,
- ),
- )
diff --git a/rio/snippets/snippet-files/old-tutorial-biography/pages/__init__.py b/rio/snippets/snippet-files/old-tutorial-biography/pages/__init__.py
deleted file mode 100644
index f575379d..00000000
--- a/rio/snippets/snippet-files/old-tutorial-biography/pages/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-from .biography_page import BiographyPage
diff --git a/rio/snippets/snippet-files/old-tutorial-biography/pages/biography_page.py b/rio/snippets/snippet-files/old-tutorial-biography/pages/biography_page.py
deleted file mode 100644
index 9305a143..00000000
--- a/rio/snippets/snippet-files/old-tutorial-biography/pages/biography_page.py
+++ /dev/null
@@ -1,125 +0,0 @@
-from typing import * # type: ignore
-
-import rio
-
-from .. import components as comps
-
-
-class BiographyPage(rio.Component):
- def build(self) -> rio.Component:
- thm = self.session.theme
-
- return rio.Column(
- rio.Rectangle(
- fill=rio.LinearGradientFill(
- (thm.primary_palette.background, 0),
- (thm.primary_palette.background.brighter(0.2), 1),
- ),
- height=1.0,
- ),
- rio.Column(
- comps.AboutMe(),
- rio.Grid(
- (
- comps.Contact(
- "123 Main Street, Anytown, USA 12345",
- "location-on",
- ),
- comps.Contact(
- "Gitlab",
- rio.URL(
- "https://about.gitlab.com/images/press/logo/svg/gitlab-logo-500.svg"
- ),
- link=rio.URL("https://gitlab.com"),
- ),
- ),
- (
- comps.Contact(
- "(123) 456-7890",
- "call:fill",
- ),
- comps.Contact(
- "GitHub",
- rio.URL(
- "https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png"
- ),
- link=rio.URL("https://github.com"),
- ),
- ),
- (
- comps.Contact(
- "janedoe@email.com",
- "mail",
- link=rio.URL("mailto:janedoe@email.com"),
- ),
- comps.Contact(
- "LinkedIn",
- rio.URL(
- "https://upload.wikimedia.org/wikipedia/commons/thumb/c/ca/LinkedIn_logo_initials.png/768px-LinkedIn_logo_initials.png"
- ),
- link=rio.URL("https://linkedin.com"),
- ),
- ),
- row_spacing=1.5,
- column_spacing=5,
- width="grow",
- margin_y=2,
- ),
- rio.Row(
- comps.SkillBars(
- {
- "Python": 5,
- "Rust": 3,
- "SQL": 4,
- "R": 3,
- },
- width="grow",
- align_y=0,
- ),
- comps.SkillBars(
- {
- "Pandas": 5,
- "Numpy": 5,
- "SciPy": 3,
- "Matplotlib": 4,
- "Seaborn": 4,
- },
- width="grow",
- align_y=0,
- ),
- comps.SkillBars(
- {
- "English": 5,
- "Spanish": 3,
- "French": 2,
- },
- width="grow",
- align_y=0,
- ),
- width="grow",
- spacing=5,
- ),
- comps.History(margin_x=10),
- rio.Text(
- "Personal Projects",
- style="heading2",
- margin_x=8,
- margin_top=3,
- ),
- comps.Projects(),
- rio.Overlay(
- rio.IconButton(
- icon="material/mail:fill",
- margin_right=3,
- margin_bottom=3,
- align_x=1,
- align_y=1,
- )
- ),
- margin_y=4,
- spacing=2,
- width=40,
- align_x=0.5,
- ),
- align_y=0,
- )
diff --git a/rio/snippets/snippet-files/old-tutorial-biography/pages/biography_page_after_tutorial_4.py b/rio/snippets/snippet-files/old-tutorial-biography/pages/biography_page_after_tutorial_4.py
deleted file mode 100644
index 21fb342a..00000000
--- a/rio/snippets/snippet-files/old-tutorial-biography/pages/biography_page_after_tutorial_4.py
+++ /dev/null
@@ -1,10 +0,0 @@
-from typing import * # type: ignore
-
-import rio
-
-from .. import components as comps
-
-
-class BiographyPage(rio.Component):
- def build(self) -> rio.Component:
- return comps.AboutMe()
diff --git a/rio/snippets/snippet-files/project-template-Empty/components/__init__.py b/rio/snippets/snippet-files/project-template-Empty/components/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/rio/snippets/snippet-files/project-template-Tic-Tac-Toe/components/__init__.py b/rio/snippets/snippet-files/project-template-Tic-Tac-Toe/components/__init__.py
index e69de29b..847f1bbe 100644
--- a/rio/snippets/snippet-files/project-template-Tic-Tac-Toe/components/__init__.py
+++ b/rio/snippets/snippet-files/project-template-Tic-Tac-Toe/components/__init__.py
@@ -0,0 +1 @@
+from .field import Field as Field
diff --git a/rio/snippets/snippet-files/project-template-Tic-Tac-Toe/components/field.py b/rio/snippets/snippet-files/project-template-Tic-Tac-Toe/components/field.py
new file mode 100644
index 00000000..86825cde
--- /dev/null
+++ b/rio/snippets/snippet-files/project-template-Tic-Tac-Toe/components/field.py
@@ -0,0 +1,64 @@
+from __future__ import annotations
+
+from typing import * # type: ignore
+
+import rio
+
+
+#
+class Field(rio.Component):
+ """
+ This component represents a single field in the Tic Tac Toe game. It can
+ contain an X, an O, or be empty. If the field is empty, the player can press
+ on it to set their respective mark.
+ """
+
+ # The current value of the field.
+ value: Literal["X", "O", ""]
+
+ # If this is `True`, the field will dim its content. This allows the game to
+ # dim any fields that are not part of the winning combination when a player
+ # has won.
+ dim: bool
+
+ # The game needs to know when a player presses on a field, so it can update
+ # the board accordingly. This custom event handler serves that purpose.
+ on_press: rio.EventHandler[[]] = None
+
+ def build(self) -> rio.Component:
+ # We'll need to specify the size of several components. This variable
+ # makes it easier to adjust the size of the fields.
+ field_size = 3
+
+ # If the field is empty, allow the player to press on it. Since buttons
+ # would look out of place here, cards are a nice alternative.
+ if self.value == "":
+ return rio.Card(
+ content=rio.Spacer(
+ width=field_size,
+ height=field_size,
+ ),
+ on_press=self.on_press,
+ )
+
+ # For fields that already contain an X or O, show the respective
+ # icon.
+ #
+ # Vary the color based on the player
+ color = rio.Color.RED if self.value == "X" else rio.Color.BLUE
+
+ # If a player has won, and this field isn't part of the winning
+ # combination, dim it.
+ if self.dim:
+ color = color.replace(opacity=0.2)
+
+ # Create the icon to represent the field
+ return rio.Icon(
+ "material/close" if self.value == "X" else "material/circle",
+ fill=color,
+ width=field_size,
+ height=field_size,
+ )
+
+
+#
diff --git a/rio/snippets/snippet-files/project-template-Tic-Tac-Toe/pages/tic_tac_toe_page.py b/rio/snippets/snippet-files/project-template-Tic-Tac-Toe/pages/tic_tac_toe_page.py
index 356fd69e..fe6fe14f 100644
--- a/rio/snippets/snippet-files/project-template-Tic-Tac-Toe/pages/tic_tac_toe_page.py
+++ b/rio/snippets/snippet-files/project-template-Tic-Tac-Toe/pages/tic_tac_toe_page.py
@@ -2,11 +2,12 @@ from __future__ import annotations
#
import functools
-from dataclasses import field
from typing import * # type: ignore
import rio
+from .. import components as comps
+
#
@@ -21,8 +22,12 @@ class TicTacToePage(rio.Component):
Everything is contained right in this class.
"""
- # The contents of all fields
- fields: list[Literal["X", "O", ""]] = field(default_factory=lambda: [""] * 9)
+ # The contents of all fields. Each field can contain an X, an O, or be
+ # empty. The initial state is an empty board.
+ #
+ # The first value is the top-left field. The remaining fields follow from
+ # left to right, top to bottom.
+ fields: list[Literal["X", "O", ""]] = [""] * 9
# The player who is currently on turn
player: Literal["X", "O"] = "X"
@@ -32,7 +37,7 @@ class TicTacToePage(rio.Component):
# If there is a winner, these are the indices of the fields which made them
# win
- winning_indices: set[int] = field(default_factory=set)
+ winning_indices: set[int] = set()
def find_winner(self) -> None:
"""
@@ -104,41 +109,15 @@ class TicTacToePage(rio.Component):
def build(self) -> rio.Component:
# Spawn components for the fields
field_components: list[rio.Component] = []
- field_size = 3
for index, field in enumerate(self.fields):
- # Allow the player to press on empty fields. Since buttons would
- # look out of place here, cards are a nice alternative.
- if field == "":
- field_components.append(
- rio.Card(
- content=rio.Spacer(
- width=field_size,
- height=field_size,
- ),
- on_press=functools.partial(self.on_press, index),
- )
- )
-
- # For fields that already contain an X or O, show the respective
- # icon
- else:
- # Vary the color based on the player
- color = rio.Color.RED if field == "X" else rio.Color.BLUE
-
- # If a player has won highlight the winning fields
- if self.winner in ("X", "O") and index not in self.winning_indices:
- color = color.replace(opacity=0.3)
-
- # Create the icon to represent the field
- field_components.append(
- rio.Icon(
- "material/close" if field == "X" else "material/circle",
- fill=color,
- width=field_size,
- height=field_size,
- )
+ field_components.append(
+ comps.Field(
+ value=field,
+ dim=self.winner is not None and index not in self.winning_indices,
+ on_press=functools.partial(self.on_press, index),
)
+ )
# Come up with a status message
if self.winner in ("X", "O"):
diff --git a/rio/snippets/snippet-files/tutorial-tic-tac-toe-part-2/tic_tac_toe_page.py b/rio/snippets/snippet-files/tutorial-tic-tac-toe-part-2/tic_tac_toe_page.py
new file mode 100644
index 00000000..c2d60f3c
--- /dev/null
+++ b/rio/snippets/snippet-files/tutorial-tic-tac-toe-part-2/tic_tac_toe_page.py
@@ -0,0 +1,32 @@
+from __future__ import annotations
+
+from typing import * # type: ignore
+
+import rio
+
+
+#
+class TicTacToePage(rio.Component):
+ # The contents of all fields. Each field can contain an X, an O, or be
+ # empty. The initial state is an empty board.
+ fields: list[Literal["X", "O", ""]] = [""] * 9
+
+ def build(self) -> rio.Component:
+ # Spawn components for the fields
+ field_components: list[rio.Component] = []
+
+ for index, field in enumerate(self.fields):
+ field_components.append(rio.Text(f"Field {index}"))
+
+ # Arrange all components in a grid
+ return rio.Grid(
+ field_components[0:3],
+ field_components[3:6],
+ field_components[6:9],
+ row_spacing=1,
+ column_spacing=1,
+ align_x=0.5,
+ )
+
+
+#
diff --git a/rio/snippets/snippet-files/tutorial-tic-tac-toe-part-3/field.py b/rio/snippets/snippet-files/tutorial-tic-tac-toe-part-3/field.py
new file mode 100644
index 00000000..7e583912
--- /dev/null
+++ b/rio/snippets/snippet-files/tutorial-tic-tac-toe-part-3/field.py
@@ -0,0 +1,43 @@
+from __future__ import annotations
+
+from typing import * # type: ignore
+
+import rio
+
+
+#
+class Field(rio.Component):
+ value: Literal["X", "O", ""]
+
+ on_press: rio.EventHandler[[]] = None
+
+ def build(self) -> rio.Component:
+ # If the field is empty, allow the player to press on it. Since buttons
+ # would look out of place here, cards are a nice alternative.
+ if self.value == "":
+ return rio.Card(
+ content=rio.Spacer(
+ width=3,
+ height=3,
+ ),
+ on_press=self.on_press,
+ )
+
+ # For fields that already contain an X or O, show the respective icon.
+ # Also vary the color based on the player.
+ if self.value == "X":
+ color = rio.Color.RED
+ icon = "material/close"
+ else:
+ color = rio.Color.BLUE
+ icon = "material/circle"
+
+ return rio.Icon(
+ icon=icon,
+ fill=color,
+ width=3,
+ height=3,
+ )
+
+
+#
diff --git a/rio/snippets/snippet-files/tutorial-tic-tac-toe-part-3/tic_tac_toe_page.py b/rio/snippets/snippet-files/tutorial-tic-tac-toe-part-3/tic_tac_toe_page.py
new file mode 100644
index 00000000..afbf8a36
--- /dev/null
+++ b/rio/snippets/snippet-files/tutorial-tic-tac-toe-part-3/tic_tac_toe_page.py
@@ -0,0 +1,36 @@
+from __future__ import annotations
+
+from typing import * # type: ignore
+
+import rio
+
+from . import field as comps
+
+
+#
+class TicTacToePage(rio.Component):
+ # The contents of all fields. Each field can contain an X, an O, or be
+ # empty. The initial state is an empty board.
+ fields: list[Literal["X", "O", ""]] = [""] * 9
+
+ def build(self) -> rio.Component:
+ # Spawn components for the fields
+ field_components: list[rio.Component] = []
+
+ for index, field in enumerate(self.fields):
+ comps.Field(
+ value=field,
+ )
+
+ # Arrange all components in a grid
+ return rio.Grid(
+ field_components[0:3],
+ field_components[3:6],
+ field_components[6:9],
+ row_spacing=1,
+ column_spacing=1,
+ align_x=0.5,
+ )
+
+
+#
diff --git a/rio/snippets/snippet-files/tutorial-tic-tac-toe-part-4/field.py b/rio/snippets/snippet-files/tutorial-tic-tac-toe-part-4/field.py
new file mode 100644
index 00000000..f8089d8b
--- /dev/null
+++ b/rio/snippets/snippet-files/tutorial-tic-tac-toe-part-4/field.py
@@ -0,0 +1,40 @@
+from __future__ import annotations
+
+from typing import * # type: ignore
+
+import rio
+
+
+#
+class Field(rio.Component):
+ value: Literal["X", "O", ""]
+
+ on_press: rio.EventHandler[[]] = None
+
+ def build(self) -> rio.Component:
+ # If the field is empty, allow the player to press on it. Since buttons
+ # would look out of place here, cards are a nice alternative.
+ if self.value == "":
+ return rio.Card(
+ content=rio.Spacer(
+ width=3,
+ height=3,
+ ),
+ on_press=self.on_press,
+ )
+
+ # For fields that already contain an X or O, show the respective
+ # icon.
+ #
+ # Vary the color based on the player
+ color = rio.Color.RED if self.value == "X" else rio.Color.BLUE
+
+ return rio.Icon(
+ "material/close" if self.value == "X" else "material/circle",
+ fill=color,
+ width=3,
+ height=3,
+ )
+
+
+#
diff --git a/rio/snippets/snippet-files/tutorial-tic-tac-toe-part-4/tic_tac_toe_page.py b/rio/snippets/snippet-files/tutorial-tic-tac-toe-part-4/tic_tac_toe_page.py
new file mode 100644
index 00000000..d98e4fda
--- /dev/null
+++ b/rio/snippets/snippet-files/tutorial-tic-tac-toe-part-4/tic_tac_toe_page.py
@@ -0,0 +1,78 @@
+from __future__ import annotations
+
+import functools
+from typing import * # type: ignore
+
+import rio
+
+from . import field as comps
+
+
+#
+class TicTacToePage(rio.Component):
+ # The contents of all fields. Each field can contain an X, an O, or be
+ # empty. The initial state is an empty board.
+ fields: list[Literal["X", "O", ""]] = [""] * 9
+
+ # The player who is currently on turn
+ player: Literal["X", "O"] = "X"
+
+ async def on_press(self, index: int) -> None:
+ """
+ This function reacts to presses on the fields, and updates the game
+ state accordingly.
+ """
+
+ # Set the field on that index
+ self.fields[index] = self.player
+
+ # Next player
+ self.player = "X" if self.player == "O" else "O"
+
+ def on_reset(self) -> None:
+ """
+ Reset the game to its initial state.
+ """
+ self.fields = [""] * 9
+ self.player = "X"
+
+ def build(self) -> rio.Component:
+ # Spawn components for the fields
+ field_components: list[rio.Component] = []
+
+ for index, field in enumerate(self.fields):
+ field_components.append(
+ comps.Field(
+ value=field,
+ on_press=functools.partial(self.on_press, index),
+ )
+ )
+
+ # Come up with a status message
+ message = f"{self.player}'s turn"
+
+ # Arrange all components in a grid
+ return rio.Column(
+ rio.Text(message, style="heading1"),
+ rio.Grid(
+ field_components[0:3],
+ field_components[3:6],
+ field_components[6:9],
+ row_spacing=1,
+ column_spacing=1,
+ align_x=0.5,
+ ),
+ rio.Button(
+ "Reset",
+ icon="material/refresh",
+ style="plain",
+ on_press=self.on_reset,
+ ),
+ spacing=2,
+ margin=2,
+ align_x=0.5,
+ align_y=0.0,
+ )
+
+
+#
diff --git a/rio/snippets/snippet-files/tutorial-tic-tac-toe-part-5/field.py b/rio/snippets/snippet-files/tutorial-tic-tac-toe-part-5/field.py
new file mode 100644
index 00000000..a6a16a80
--- /dev/null
+++ b/rio/snippets/snippet-files/tutorial-tic-tac-toe-part-5/field.py
@@ -0,0 +1,46 @@
+from __future__ import annotations
+
+from typing import * # type: ignore
+
+import rio
+
+
+#
+class Field(rio.Component):
+ value: Literal["X", "O", ""]
+ dim: bool
+
+ on_press: rio.EventHandler[[]] = None
+
+ def build(self) -> rio.Component:
+ # If the field is empty, allow the player to press on it. Since buttons
+ # would look out of place here, cards are a nice alternative.
+ if self.value == "":
+ return rio.Card(
+ content=rio.Spacer(
+ width=3,
+ height=3,
+ ),
+ on_press=self.on_press,
+ )
+
+ # For fields that already contain an X or O, show the respective
+ # icon.
+ #
+ # Vary the color based on the player
+ color = rio.Color.RED if self.value == "X" else rio.Color.BLUE
+
+ # If a player has won, and this field isn't part of the winning
+ # combination, dim it.
+ if self.dim:
+ color = color.replace(opacity=0.2)
+
+ return rio.Icon(
+ "material/close" if self.value == "X" else "material/circle",
+ fill=color,
+ width=3,
+ height=3,
+ )
+
+
+#
diff --git a/rio/snippets/snippet-files/tutorial-tic-tac-toe-part-5/tic_tac_toe_page.py b/rio/snippets/snippet-files/tutorial-tic-tac-toe-part-5/tic_tac_toe_page.py
new file mode 100644
index 00000000..ff9ab0b4
--- /dev/null
+++ b/rio/snippets/snippet-files/tutorial-tic-tac-toe-part-5/tic_tac_toe_page.py
@@ -0,0 +1,139 @@
+from __future__ import annotations
+
+import functools
+from typing import * # type: ignore
+
+import rio
+
+from . import field as comps
+
+
+#
+class TicTacToePage(rio.Component):
+ # The contents of all fields. Each field can contain an X, an O, or be
+ # empty. The initial state is an empty board.
+ fields: list[Literal["X", "O", ""]] = [""] * 9
+
+ # The player who is currently on turn
+ player: Literal["X", "O"] = "X"
+
+ # The winner of the game, if any
+ winner: Literal["X", "O", "draw"] | None = None
+
+ # If there is a winner, these are the indices of the fields which made them
+ # win
+ winning_indices: set[int] = set()
+
+ def find_winner(self) -> None:
+ """
+ Look if there is a winner on the board, and stores it in the component's
+ state. Also updates the winning indices accordingly.
+ """
+ # Create a list of all possible winning combinations. If all of these
+ # indices have the same value, then that value is the winner.
+ winning_combinations = [
+ [0, 1, 2], # Top row
+ [3, 4, 5], # Middle row
+ [6, 7, 8], # Bottom row
+ [0, 3, 6], # Left column
+ [1, 4, 7], # Middle column
+ [2, 5, 8], # Right column
+ [0, 4, 8], # Top-left to bottom-right diagonal
+ [2, 4, 6], # Top-right to bottom-left diagonal
+ ]
+
+ # Look for winners
+ for combination in winning_combinations:
+ values = [self.fields[i] for i in combination]
+ if values.count("X") == 3:
+ self.winner = "X"
+ self.winning_indices = set(combination)
+ return
+
+ if values.count("O") == 3:
+ self.winner = "O"
+ self.winning_indices = set(combination)
+ return
+
+ # If no fields are empty, it's a draw
+ if "" not in self.fields:
+ self.winner = "draw"
+ return
+
+ # There is no winner yet
+ pass
+
+ async def on_press(self, index: int) -> None:
+ """
+ This function reacts to presses on the fields, and updates the game
+ state accordingly.
+ """
+
+ # If the game is already over, don't do anything
+ if self.winner:
+ return
+
+ # Set the field on that index
+ self.fields[index] = self.player
+
+ # Next player
+ self.player = "X" if self.player == "O" else "O"
+
+ # See if there is a winner
+ self.find_winner()
+
+ def on_reset(self) -> None:
+ """
+ Reset the game to its initial state.
+ """
+ self.fields = [""] * 9
+ self.player = "X"
+ self.winner = None
+ self.winning_indices = set()
+
+ def build(self) -> rio.Component:
+ # Spawn components for the fields
+ field_components: list[rio.Component] = []
+
+ for index, field in enumerate(self.fields):
+ field_components.append(
+ comps.Field(
+ value=field,
+ dim=self.winner is not None and index not in self.winning_indices,
+ on_press=functools.partial(self.on_press, index),
+ )
+ )
+
+ # Come up with a status message
+ if self.winner in ("X", "O"):
+ message = f"{self.winner} has won!"
+ elif self.winner == "draw":
+ message = "It's a draw!"
+ else:
+ message = f"{self.player}'s turn"
+
+ # Arrange all components in a grid
+ return rio.Column(
+ rio.Text(message, style="heading1"),
+ rio.Grid(
+ field_components[0:3],
+ field_components[3:6],
+ field_components[6:9],
+ row_spacing=1,
+ column_spacing=1,
+ align_x=0.5,
+ ),
+ rio.Button(
+ "Reset",
+ icon="material/refresh",
+ style="plain",
+ on_press=self.on_reset,
+ ),
+ spacing=2,
+ margin=2,
+ align_x=0.5,
+ align_y=0.0,
+ )
+
+
+#