Settings Panel — Controles de Seleção em Ação 🚀¶
Construa um painel de configurações completo com Switch, Checkbox, Slider, RadioGroup e SegmentedControl — e veja como vincular todos eles a um único dataclass de estado de forma limpa e tipada.
O que você vai construir¶
Um painel de configurações dividido em quatro seções, mais um cartão de resumo ao vivo:
| Seção | Widgets | O que controla |
|---|---|---|
| Notifications | Switch + Checkbox | Notificações push, alertas por e-mail, sons |
| Appearance | SegmentedControl + Slider | Tema (System/Light/Dark), tamanho de fonte, qualidade |
| Audio & Storage | Slider + Switch | Volume de reprodução, auto-save de rascunhos |
| Language | RadioGroup | Idioma da interface |
| Live summary | Card (apenas leitura) | Reflexo em tempo real de todos os valores acima |
Cada interação — mover um slider, marcar uma caixa, escolher um segmento — atualiza imediatamente o cartão de resumo, tornando a vinculação bidirecional visível de forma concreta.
Nota — por que um resumo ao vivo?
O cartão de resumo não é decoração. Ele prova que cada controle realmente modifica o estado compartilhado. Se você clicar num controle e o resumo não mudar, há um bug. É o teste de fumaça mais rápido que existe.
Pré-requisitos¶
Leitura recomendada antes de continuar:
- Tutorial básico —
App,vieweset_state - Gerenciando estado — como o ciclo de atualização funciona
- Modos de execução — WASM vs. servidor
Criando o projeto¶
Passo 1 — Definindo as constantes de opções¶
Antes do estado, defina as listas de opções para os controles de seleção. Mantê-las como constantes no topo do arquivo evita duplicação e facilita ajustes futuros.
from __future__ import annotations
_THEME_OPTIONS: list[str] = ["System", "Light", "Dark"]
_LANGUAGE_OPTIONS: list[str] = ["English", "Português", "Español", "Français"]
_QUALITY_OPTIONS: list[str] = ["Low", "Medium", "High", "Ultra"]
Dica — índice como estado, não a string
O estado guarda o índice (theme_index: int = 0), não a string "System". Isso torna o estado serializado compacto e independente de tradução. Para exibir o rótulo, use _THEME_OPTIONS[state.theme_index] na hora do render.
Passo 2 — Modelando o estado¶
Com as opções definidas, modele exatamente o que precisa persistir entre renders:
from dataclasses import dataclass
@dataclass
class SettingsState:
"""All mutable settings controlled by the panel.
Attributes:
notifications_enabled: Master switch for push notifications.
email_alerts: Whether to send e-mail alerts on events.
sound_enabled: Whether in-app sounds are active.
auto_save: Whether drafts are saved automatically.
theme_index: Index into ``_THEME_OPTIONS`` (0=System, 1=Light, 2=Dark).
language_index: Index into ``_LANGUAGE_OPTIONS``.
volume: Playback volume in ``[0, 100]``.
font_size: Preferred font size in ``[10, 30]`` logical points.
quality_index: Index into ``_QUALITY_OPTIONS`` (stream/render quality).
"""
notifications_enabled: bool = True
email_alerts: bool = False
sound_enabled: bool = True
auto_save: bool = True
theme_index: int = 0
language_index: int = 0
volume: float = 70.0
font_size: float = 16.0
quality_index: int = 2
def make_state() -> SettingsState:
"""Build the initial settings state.
Returns:
A fresh :class:`SettingsState` with sensible defaults.
"""
return SettingsState()
Observe que make_state é a função que o tempestweb chama para inicializar o app. Ela precisa existir com esse nome exato no módulo.
Passo 3 — Os tipos de evento¶
Dois tipos de evento chegam dos controles de entrada. Importe-os de tempestweb._core.widgets.events:
| Tipo | Usado por | Campo relevante |
|---|---|---|
ToggleEvent |
Switch, Checkbox |
.checked: bool |
SlideEvent |
Slider |
.value: float |
RadioGroup e SegmentedControl entregam diretamente o índice (int) ao callback — sem wrapper de evento.
Passo 4 — Seção Notifications¶
A primeira seção usa Switch para o controle mestre e dois Checkbox para sub-opções. Organizamos a UI em uma função _notifications_card que recebe o app e retorna um Card:
from tempestweb._core import App, Style, Widget
from tempestweb._core.components import AppBar, Card, Divider, Scaffold
from tempestweb._core.style import AlignItems, Edge, FontWeight
from tempestweb._core.widgets import Checkbox, Column, Row, Switch, Text
from tempestweb._core.widgets.events import ToggleEvent
def _notifications_card(app: App[SettingsState]) -> Widget:
"""Render the Notifications section with Switch and Checkbox controls.
Args:
app: The application handle exposing ``state`` and ``set_state``.
Returns:
A ``Card`` containing the notification preference controls.
"""
state: SettingsState = app.state
def on_notifications_toggle(event: ToggleEvent) -> None:
"""Toggle master notification switch."""
app.set_state(lambda s: setattr(s, "notifications_enabled", event.checked))
def on_email_toggle(event: ToggleEvent) -> None:
"""Toggle e-mail alert preference."""
app.set_state(lambda s: setattr(s, "email_alerts", event.checked))
def on_sound_toggle(event: ToggleEvent) -> None:
"""Toggle in-app sound preference."""
app.set_state(lambda s: setattr(s, "sound_enabled", event.checked))
return Card(
key="notifications-card",
children=[
Text(
content="Notifications",
key="notif-heading",
style=Style(font_size=16.0, font_weight=FontWeight.BOLD),
),
Divider(key="notif-divider"),
Row(
key="notif-master-row",
style=Style(gap=12.0, align=AlignItems.CENTER),
children=[
Text(
content="Enable notifications",
key="notif-master-label",
style=Style(font_size=14.0, grow=1.0),
),
Switch(
checked=state.notifications_enabled,
on_change=on_notifications_toggle,
key="notif-switch",
),
],
),
Checkbox(
label="Send e-mail alerts",
checked=state.email_alerts,
on_change=on_email_toggle,
key="email-checkbox",
),
Checkbox(
label="Play sounds",
checked=state.sound_enabled,
on_change=on_sound_toggle,
key="sound-checkbox",
),
],
)
Nota — Switch num Row com grow=1.0
O Text com grow=1.0 ocupa todo o espaço disponível na linha, empurrando o Switch para a direita — o padrão clássico de linha de configuração em iOS e Android. O gap=12.0 no Row adiciona o espaçamento horizontal entre os dois.
Passo 5 — Seção Appearance¶
Esta seção introduz SegmentedControl (para tema e qualidade) e Slider (para tamanho de fonte):
from tempestweb._core.components import SegmentedControl
from tempestweb._core.widgets import Slider
from tempestweb._core.widgets.events import SlideEvent
def _appearance_card(app: App[SettingsState]) -> Widget:
"""Render the Appearance section with SegmentedControl and Slider controls.
Args:
app: The application handle exposing ``state`` and ``set_state``.
Returns:
A ``Card`` containing the theme, font-size and quality controls.
"""
state: SettingsState = app.state
def on_theme_select(index: int) -> None:
"""Select a colour theme."""
app.set_state(lambda s: setattr(s, "theme_index", index))
def on_quality_select(index: int) -> None:
"""Select the render/stream quality level."""
app.set_state(lambda s: setattr(s, "quality_index", index))
def on_font_size_change(event: SlideEvent) -> None:
"""Adjust the preferred font size."""
app.set_state(lambda s: setattr(s, "font_size", round(event.value, 1)))
return Card(
key="appearance-card",
children=[
Text(
content="Appearance",
key="appearance-heading",
style=Style(font_size=16.0, font_weight=FontWeight.BOLD),
),
Divider(key="appearance-divider"),
Text(
content="Theme",
key="theme-label",
style=Style(font_size=13.0, font_weight=FontWeight.BOLD),
),
SegmentedControl(
options=_THEME_OPTIONS,
selected=state.theme_index,
on_select=on_theme_select,
key="theme-segments",
),
Text(
content=f"Font size: {state.font_size:.0f} pt",
key="font-size-label",
style=Style(font_size=13.0, font_weight=FontWeight.BOLD),
),
Slider(
value=state.font_size,
min_value=10.0,
max_value=30.0,
step=1.0,
on_change=on_font_size_change,
key="font-slider",
),
Text(
content="Render quality",
key="quality-label",
style=Style(font_size=13.0, font_weight=FontWeight.BOLD),
),
SegmentedControl(
options=_QUALITY_OPTIONS,
selected=state.quality_index,
on_select=on_quality_select,
key="quality-segments",
),
],
)
Dica — label dinâmico acima do Slider
O Text antes do Slider usa f"Font size: {state.font_size:.0f} pt". A cada movimento do slider o estado muda → view é chamada novamente → o label atualiza. Não há nenhuma variável local ou ref manual: o estado é a fonte da verdade.
Passo 6 — Seção Audio & Storage¶
Volume com Slider e auto-save com Switch, seguindo os mesmos padrões:
def _audio_card(app: App[SettingsState]) -> Widget:
"""Render the Audio section with a volume Slider and auto-save Switch.
Args:
app: The application handle exposing ``state`` and ``set_state``.
Returns:
A ``Card`` containing the audio and save controls.
"""
state: SettingsState = app.state
def on_volume_change(event: SlideEvent) -> None:
"""Adjust playback volume."""
app.set_state(lambda s: setattr(s, "volume", round(event.value)))
def on_auto_save_toggle(event: ToggleEvent) -> None:
"""Toggle auto-save preference."""
app.set_state(lambda s: setattr(s, "auto_save", event.checked))
return Card(
key="audio-card",
children=[
Text(
content="Audio & Storage",
key="audio-heading",
style=Style(font_size=16.0, font_weight=FontWeight.BOLD),
),
Divider(key="audio-divider"),
Text(
content=f"Volume: {state.volume:.0f}%",
key="volume-label",
style=Style(font_size=13.0, font_weight=FontWeight.BOLD),
),
Slider(
value=state.volume,
min_value=0.0,
max_value=100.0,
step=1.0,
on_change=on_volume_change,
key="volume-slider",
),
Row(
key="auto-save-row",
style=Style(gap=12.0, align=AlignItems.CENTER),
children=[
Text(
content="Auto-save drafts",
key="auto-save-label",
style=Style(font_size=14.0, grow=1.0),
),
Switch(
checked=state.auto_save,
on_change=on_auto_save_toggle,
key="auto-save-switch",
),
],
),
],
)
Passo 7 — Seção Language¶
RadioGroup é a escolha certa para seleção única com todos os itens visíveis simultaneamente:
from tempestweb._core.components import RadioGroup
def _language_card(app: App[SettingsState]) -> Widget:
"""Render the Language section with a RadioGroup control.
Args:
app: The application handle exposing ``state`` and ``set_state``.
Returns:
A ``Card`` containing the language radio group.
"""
state: SettingsState = app.state
def on_language_select(index: int) -> None:
"""Select the preferred interface language."""
app.set_state(lambda s: setattr(s, "language_index", index))
return Card(
key="language-card",
children=[
Text(
content="Language",
key="language-heading",
style=Style(font_size=16.0, font_weight=FontWeight.BOLD),
),
Divider(key="language-divider"),
RadioGroup(
options=_LANGUAGE_OPTIONS,
selected=state.language_index,
on_select=on_language_select,
key="language-radio",
),
],
)
Nota — RadioGroup vs. SegmentedControl
Use RadioGroup quando houver mais de 3-4 opções ou quando os rótulos forem longos — ele empilha as opções verticalmente. Use SegmentedControl para 2-4 opções curtas que cabem numa linha horizontal.
Passo 8 — O cartão de resumo ao vivo¶
Esta função recebe diretamente o state (sem o app inteiro), pois não precisa registrar handlers — é somente leitura:
def _summary_card(state: SettingsState) -> Widget:
"""Render a live summary of all current settings.
This card re-renders on every state change and shows all selected values
so the user can verify that every control is truly bound to the state.
Args:
state: The current snapshot of :class:`SettingsState`.
Returns:
A ``Card`` listing all current setting values.
"""
theme_name: str = _THEME_OPTIONS[state.theme_index]
language_name: str = _LANGUAGE_OPTIONS[state.language_index]
quality_name: str = _QUALITY_OPTIONS[state.quality_index]
notif_text: str = "on" if state.notifications_enabled else "off"
email_text: str = "yes" if state.email_alerts else "no"
sound_text: str = "on" if state.sound_enabled else "off"
save_text: str = "on" if state.auto_save else "off"
lines: list[Widget] = [
Text(
content="Live summary",
key="summary-heading",
style=Style(font_size=16.0, font_weight=FontWeight.BOLD),
),
Divider(key="summary-divider"),
Text(
content=f"Notifications: {notif_text} | E-mail alerts: {email_text}",
key="summary-notif",
style=Style(font_size=13.0),
),
Text(
content=f"Sound: {sound_text} | Auto-save: {save_text}",
key="summary-sound",
style=Style(font_size=13.0),
),
Text(
content=(
f"Theme: {theme_name} | Font: {state.font_size:.0f} pt"
f" | Quality: {quality_name}"
),
key="summary-appearance",
style=Style(font_size=13.0),
),
Text(
content=f"Volume: {state.volume:.0f}% | Language: {language_name}",
key="summary-audio",
style=Style(font_size=13.0),
),
]
return Card(key="summary-card", children=lines)
Passo 9 — Montando tudo em view¶
A função view é o ponto de entrada do tempestweb. Ela chama cada builder de seção e os organiza num Scaffold com AppBar:
def view(app: App[SettingsState]) -> Widget:
"""Render the full settings panel from the current state.
Args:
app: The application handle exposing ``state`` and ``set_state``.
Returns:
The full widget tree for the current state.
"""
return Scaffold(
key="settings-scaffold",
app_bar=AppBar(title="Settings", key="settings-appbar"),
body=Column(
key="settings-body",
style=Style(gap=16.0, padding=Edge.all(16.0)),
children=[
_notifications_card(app),
_appearance_card(app),
_audio_card(app),
_language_card(app),
_summary_card(app.state),
],
),
)
Dica — _summary_card(app.state) vs. _summary_card(app)
Passar app.state (em vez de app) ao cartão de resumo comunica claramente que ele é somente leitura. Quem lê o código sabe imediatamente que essa função não registra handlers. É uma convenção de design, não uma restrição técnica.
O app completo ✅¶
Aqui está o arquivo examples/settings-panel/app.py completo, pronto para copiar:
"""Settings panel — demonstrates selection controls bound to a settings dataclass.
Every control (Switch, Checkbox, Slider, RadioGroup, SegmentedControl) is wired
to a dedicated field in :class:`SettingsState`. Any change immediately re-renders
a live summary card at the bottom that reflects the current state — so the demo
makes the two-way binding visible.
Run unchanged in both modes::
tempestweb dev --mode wasm # Python in the browser (Pyodide)
tempestweb dev --mode server # Python on the server (FastAPI + WebSocket)
"""
from __future__ import annotations
from dataclasses import dataclass
from tempestweb._core import App, Style, Widget
from tempestweb._core.components import (
AppBar,
Card,
Divider,
RadioGroup,
Scaffold,
SegmentedControl,
)
from tempestweb._core.style import AlignItems, Edge, FontWeight
from tempestweb._core.widgets import (
Checkbox,
Column,
Row,
Slider,
Switch,
Text,
)
from tempestweb._core.widgets.events import SlideEvent, ToggleEvent
# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------
_THEME_OPTIONS: list[str] = ["System", "Light", "Dark"]
_LANGUAGE_OPTIONS: list[str] = ["English", "Português", "Español", "Français"]
_QUALITY_OPTIONS: list[str] = ["Low", "Medium", "High", "Ultra"]
# ---------------------------------------------------------------------------
# State
# ---------------------------------------------------------------------------
@dataclass
class SettingsState:
"""All mutable settings controlled by the panel.
Attributes:
notifications_enabled: Master switch for push notifications.
email_alerts: Whether to send e-mail alerts on events.
sound_enabled: Whether in-app sounds are active.
auto_save: Whether drafts are saved automatically.
theme_index: Index into ``_THEME_OPTIONS`` (0=System, 1=Light, 2=Dark).
language_index: Index into ``_LANGUAGE_OPTIONS``.
volume: Playback volume in ``[0, 100]``.
font_size: Preferred font size in ``[10, 30]`` logical points.
quality_index: Index into ``_QUALITY_OPTIONS`` (stream/render quality).
"""
notifications_enabled: bool = True
email_alerts: bool = False
sound_enabled: bool = True
auto_save: bool = True
theme_index: int = 0
language_index: int = 0
volume: float = 70.0
font_size: float = 16.0
quality_index: int = 2
def make_state() -> SettingsState:
"""Build the initial settings state.
Returns:
A fresh :class:`SettingsState` with sensible defaults.
"""
return SettingsState()
# ---------------------------------------------------------------------------
# Section builders
# ---------------------------------------------------------------------------
def _notifications_card(app: App[SettingsState]) -> Widget:
"""Render the Notifications section with Switch and Checkbox controls.
Args:
app: The application handle exposing ``state`` and ``set_state``.
Returns:
A ``Card`` containing the notification preference controls.
"""
state: SettingsState = app.state
def on_notifications_toggle(event: ToggleEvent) -> None:
"""Toggle master notification switch."""
app.set_state(lambda s: setattr(s, "notifications_enabled", event.checked))
def on_email_toggle(event: ToggleEvent) -> None:
"""Toggle e-mail alert preference."""
app.set_state(lambda s: setattr(s, "email_alerts", event.checked))
def on_sound_toggle(event: ToggleEvent) -> None:
"""Toggle in-app sound preference."""
app.set_state(lambda s: setattr(s, "sound_enabled", event.checked))
return Card(
key="notifications-card",
children=[
Text(
content="Notifications",
key="notif-heading",
style=Style(font_size=16.0, font_weight=FontWeight.BOLD),
),
Divider(key="notif-divider"),
Row(
key="notif-master-row",
style=Style(gap=12.0, align=AlignItems.CENTER),
children=[
Text(
content="Enable notifications",
key="notif-master-label",
style=Style(font_size=14.0, grow=1.0),
),
Switch(
checked=state.notifications_enabled,
on_change=on_notifications_toggle,
key="notif-switch",
),
],
),
Checkbox(
label="Send e-mail alerts",
checked=state.email_alerts,
on_change=on_email_toggle,
key="email-checkbox",
),
Checkbox(
label="Play sounds",
checked=state.sound_enabled,
on_change=on_sound_toggle,
key="sound-checkbox",
),
],
)
def _appearance_card(app: App[SettingsState]) -> Widget:
"""Render the Appearance section with SegmentedControl and Slider controls.
Args:
app: The application handle exposing ``state`` and ``set_state``.
Returns:
A ``Card`` containing the theme, font-size and quality controls.
"""
state: SettingsState = app.state
def on_theme_select(index: int) -> None:
"""Select a colour theme."""
app.set_state(lambda s: setattr(s, "theme_index", index))
def on_quality_select(index: int) -> None:
"""Select the render/stream quality level."""
app.set_state(lambda s: setattr(s, "quality_index", index))
def on_font_size_change(event: SlideEvent) -> None:
"""Adjust the preferred font size."""
app.set_state(lambda s: setattr(s, "font_size", round(event.value, 1)))
return Card(
key="appearance-card",
children=[
Text(
content="Appearance",
key="appearance-heading",
style=Style(font_size=16.0, font_weight=FontWeight.BOLD),
),
Divider(key="appearance-divider"),
Text(
content="Theme",
key="theme-label",
style=Style(font_size=13.0, font_weight=FontWeight.BOLD),
),
SegmentedControl(
options=_THEME_OPTIONS,
selected=state.theme_index,
on_select=on_theme_select,
key="theme-segments",
),
Text(
content=f"Font size: {state.font_size:.0f} pt",
key="font-size-label",
style=Style(font_size=13.0, font_weight=FontWeight.BOLD),
),
Slider(
value=state.font_size,
min_value=10.0,
max_value=30.0,
step=1.0,
on_change=on_font_size_change,
key="font-slider",
),
Text(
content="Render quality",
key="quality-label",
style=Style(font_size=13.0, font_weight=FontWeight.BOLD),
),
SegmentedControl(
options=_QUALITY_OPTIONS,
selected=state.quality_index,
on_select=on_quality_select,
key="quality-segments",
),
],
)
def _audio_card(app: App[SettingsState]) -> Widget:
"""Render the Audio section with a volume Slider and auto-save Switch.
Args:
app: The application handle exposing ``state`` and ``set_state``.
Returns:
A ``Card`` containing the audio and save controls.
"""
state: SettingsState = app.state
def on_volume_change(event: SlideEvent) -> None:
"""Adjust playback volume."""
app.set_state(lambda s: setattr(s, "volume", round(event.value)))
def on_auto_save_toggle(event: ToggleEvent) -> None:
"""Toggle auto-save preference."""
app.set_state(lambda s: setattr(s, "auto_save", event.checked))
return Card(
key="audio-card",
children=[
Text(
content="Audio & Storage",
key="audio-heading",
style=Style(font_size=16.0, font_weight=FontWeight.BOLD),
),
Divider(key="audio-divider"),
Text(
content=f"Volume: {state.volume:.0f}%",
key="volume-label",
style=Style(font_size=13.0, font_weight=FontWeight.BOLD),
),
Slider(
value=state.volume,
min_value=0.0,
max_value=100.0,
step=1.0,
on_change=on_volume_change,
key="volume-slider",
),
Row(
key="auto-save-row",
style=Style(gap=12.0, align=AlignItems.CENTER),
children=[
Text(
content="Auto-save drafts",
key="auto-save-label",
style=Style(font_size=14.0, grow=1.0),
),
Switch(
checked=state.auto_save,
on_change=on_auto_save_toggle,
key="auto-save-switch",
),
],
),
],
)
def _language_card(app: App[SettingsState]) -> Widget:
"""Render the Language section with a RadioGroup control.
Args:
app: The application handle exposing ``state`` and ``set_state``.
Returns:
A ``Card`` containing the language radio group.
"""
state: SettingsState = app.state
def on_language_select(index: int) -> None:
"""Select the preferred interface language."""
app.set_state(lambda s: setattr(s, "language_index", index))
return Card(
key="language-card",
children=[
Text(
content="Language",
key="language-heading",
style=Style(font_size=16.0, font_weight=FontWeight.BOLD),
),
Divider(key="language-divider"),
RadioGroup(
options=_LANGUAGE_OPTIONS,
selected=state.language_index,
on_select=on_language_select,
key="language-radio",
),
],
)
def _summary_card(state: SettingsState) -> Widget:
"""Render a live summary of all current settings.
Args:
state: The current snapshot of :class:`SettingsState`.
Returns:
A ``Card`` listing all current setting values.
"""
theme_name: str = _THEME_OPTIONS[state.theme_index]
language_name: str = _LANGUAGE_OPTIONS[state.language_index]
quality_name: str = _QUALITY_OPTIONS[state.quality_index]
notif_text: str = "on" if state.notifications_enabled else "off"
email_text: str = "yes" if state.email_alerts else "no"
sound_text: str = "on" if state.sound_enabled else "off"
save_text: str = "on" if state.auto_save else "off"
lines: list[Widget] = [
Text(
content="Live summary",
key="summary-heading",
style=Style(font_size=16.0, font_weight=FontWeight.BOLD),
),
Divider(key="summary-divider"),
Text(
content=f"Notifications: {notif_text} | E-mail alerts: {email_text}",
key="summary-notif",
style=Style(font_size=13.0),
),
Text(
content=f"Sound: {sound_text} | Auto-save: {save_text}",
key="summary-sound",
style=Style(font_size=13.0),
),
Text(
content=(
f"Theme: {theme_name} | Font: {state.font_size:.0f} pt"
f" | Quality: {quality_name}"
),
key="summary-appearance",
style=Style(font_size=13.0),
),
Text(
content=f"Volume: {state.volume:.0f}% | Language: {language_name}",
key="summary-audio",
style=Style(font_size=13.0),
),
]
return Card(key="summary-card", children=lines)
# ---------------------------------------------------------------------------
# view
# ---------------------------------------------------------------------------
def view(app: App[SettingsState]) -> Widget:
"""Render the full settings panel from the current state.
Args:
app: The application handle exposing ``state`` and ``set_state``.
Returns:
The full widget tree for the current state.
"""
return Scaffold(
key="settings-scaffold",
app_bar=AppBar(title="Settings", key="settings-appbar"),
body=Column(
key="settings-body",
style=Style(gap=16.0, padding=Edge.all(16.0)),
children=[
_notifications_card(app),
_appearance_card(app),
_audio_card(app),
_language_card(app),
_summary_card(app.state),
],
),
)
Rodando o exemplo ▶¶
Modo A — Python no browser (Pyodide / WASM)¶
O Python roda dentro do browser via Pyodide. Nenhum servidor necessário — abra o URL impresso no terminal.
Modo B — Python no servidor (FastAPI + WebSocket)¶
O Python roda no servidor; o browser recebe patches JSON pelo WebSocket e atualiza o DOM.
Verificação
Em qualquer modo, confirme que:
AppBarexibe o título Settings no topo- Quatro cartões aparecem: Notifications, Appearance, Audio & Storage, Language
- Desligar o
Switchmaster de Notifications atualiza o camponotificationsno cartão de resumo - Mover o slider de volume muda o label "Volume: XX%" acima dele e o campo correspondente no resumo
- Clicar num segmento do
SegmentedControlde tema muda o campoThemeno resumo - Selecionar um idioma no
RadioGroupmuda o campoLanguageno resumo
Verificação automatizada ✅¶
# Lint
ruff check .
# Formatação
ruff format --check .
# Tipos
mypy --strict tempestweb
# Testes
pytest -q
Todos os quatro devem passar em verde. O exemplo foi escrito para ser mypy --strict clean — toda variável, parâmetro e retorno é anotado explicitamente.
Como funciona por dentro¶
O ciclo de atualização¶
Usuário interage com um controle
│
▼
handler (ex: on_volume_change)
│
▼
app.set_state(mutador lambda)
│
▼
tempestweb aplica o mutador → estado novo
│
▼
view(app) chamada novamente → nova árvore de widgets
│
▼
reconciliador calcula diff (patches mínimos)
│
▼
DOM atualizado — só o que mudou
Por que dividir em builders de seção?¶
view poderia construir tudo inline, mas ficaria com mais de 200 linhas. Dividir em _notifications_card, _appearance_card etc. traz dois benefícios:
- Leitura: cada função cabe numa tela — propósito imediato, sem scroll.
- Testabilidade: cada builder recebe
App[SettingsState]e retornaWidget— é possível testá-los isoladamente injetando umappcom estado fixo.
Estado como índice, não como string¶
Guardar theme_index: int em vez de theme: str tem uma consequência importante: o mesmo estado serializado funciona com listas de opções em qualquer idioma. Se você quiser localizar os rótulos dos temas, basta trocar _THEME_OPTIONS — o estado não muda.
Recapitulando¶
Neste tutorial você aprendeu:
- ✅ Modelar múltiplos tipos de controle (bool, int, float) em um único dataclass tipado
- ✅ Usar
SwitcheCheckboxcomToggleEvent.checked - ✅ Usar
SlidercomSlideEvent.valuee arredondamento explícito - ✅ Usar
SegmentedControleRadioGroupcom índice inteiro como estado - ✅ Organizar a UI em builders de seção independentes e testáveis
- ✅ Construir um cartão de resumo ao vivo como prova de vinculação bidirecional
- ✅ Usar
Scaffold+AppBarcomo estrutura de página padrão
Próximos passos¶
- 💡 Explore Tabs Profile para ver
SwitcheCheckboxdentro de um painel com abas - 💡 Veja Stopwatch para aprender a gerenciar estado temporal com
asyncio - 💡 Leia Gerenciando estado para um tratamento completo do ciclo
set_state - 💡 Adicione persistência: serialize
SettingsStateparalocalStorageno Modo A viapyodide.ffiou para um endpoint REST no Modo B