tempestroid — Plano de paridade (Flutter / React Native)¶
Trilho E — Paridade. Roadmap fase-a-fase para fechar o gap entre o tempestroid e o que Flutter + React Native oferecem de fábrica. Continuação natural de
plan.md: Trilhos A–D já entregaram a fundação (IR, reconciliador, dois renderizadores, dev loop, capacidades nativas básicas). Este documento cobre o que falta para o framework ser de uso geral.Nível de detalhe. Cada fase traz
Arquivos(paths reais a tocar),Contrato(assinaturas + pontos de plug no código existente) eSub-tarefas(recortes do tamanho de um agente). É spec de implementação, não só roadmap — um agente por sub-tarefa consegue codar sem redescobrir a base.
0. Premissas e regras do trilho¶
Todo o trilho respeita as invariantes já consolidadas:
- Um reconciliador, dois renderizadores. Toda fase entrega a superfície em três camadas casadas: (1) a IR/widget Pydantic + diff agnóstico, (2) o renderizador Qt (simulador desktop), (3) o renderizador Compose/Kotlin (device). Uma fase só fecha quando os dois renderizadores estão verdes.
- Tradutores de estilo espelhados. Qualquer campo de
Stylenovo entra emStyle → Qt(renderers/qt/style_translator.py) eStyle → Compose(renderers/compose/style_translator.py), com entrada na suíte de conformância (tests/conformance/). - Contrato tipado na fronteira. Eventos novos viram modelos
Eventfrozen emwidgets/events.py, registrados noevent_schemas(ClassVar) do widget e validados porparse_eventantes do dispatch. Vão paraintrospect()automaticamente. - Bridge sem mudança de C quando possível. Capacidades nativas seguem o
padrão B6: envelope
{"kind": "native"}+NativeModules/módulo Kotlin, e request/response pelo token reservado__native_result__:<id>— sem tocar no JNI/C. Só fases que exigem um canal novo (ex.: stream de sensores) abrem exceção, sinalizada no "feito quando" como token reservado novo (não C). - Tudo dentro do projeto atual — sem projetos extras. Toda implementação mora
dentro do repositório
tempestroid(Python no pacotetempestroid/, Kotlin/Compose emandroid-host/). Não criar repositório, pacote PyPI, plugin ou app separado. Limite permitido: (1) um módulo dedicado novo por área para organizar imports (sempre re-exportado pelo__init__.py, nunca ilha) e (2) uma seção de documentação extra (README/MkDocs). Preferir DIY sobreandroidx/Compose/Qt já presentes; dependência externa nova só com justificativa forte registrada no PR. - Verificação dual obrigatória. Com device conectado, toda fase é exercida no Qt e no Compose físico (screenshot). Sem device, valida no Qt e declara explícito que a metade device não foi exercida.
feito quandoé testável e honesto — sempre lastreado por testes verdes (unitários + conformância) e, quando há device, por evidência on-device.
0.1 Mapa de arquivos (âncoras reais)¶
Os blocos Arquivos de cada fase referenciam estes paths. Confirmados na
árvore atual — usar exatamente estes (não translate.py, que não existe):
| Camada | Arquivo | Papel |
|---|---|---|
| IR | tempestroid/core/ir.py |
Node(type, key, props, children); patches Replace/Update/Insert/Remove/Reorder; Path = tuple[int,...] |
| Reconciliador | tempestroid/core/reconciler.py |
build(widget)->Node, diff(old,new)->list[Patch], _reconcile*, _diff_props, _reconcile_keyed |
| Estado/loop | tempestroid/core/state.py |
App(state, view, apply_patches): .start()->Node, .current_tree, .swap_view, .set_state, .request_rebuild (coalesce via loop.call_soon(_rebuild)) |
| Introspecção | tempestroid/core/introspection.py |
introspect(), widget_catalog, event_catalog |
| Widget base | tempestroid/widgets/base.py |
Widget(BaseModel) + event_schemas: ClassVar, .widget_type, .child_nodes(); Component.render()->Widget; EventHandler |
| Eventos | tempestroid/widgets/events.py |
Event frozen base, parse_event(event_type, raw), EventValidationError |
| Widgets folha | tempestroid/widgets/{layout,inputs,media,indicators,button,text,gestures}.py |
primitivos; exemplo button.py |
| Componentes | tempestroid/components/*.py |
compostos que baixam para primitivos via Component.render |
| Qt renderer | tempestroid/renderers/qt/renderer.py |
aplica patches em QWidgets |
| Qt translator | tempestroid/renderers/qt/style_translator.py |
to_qss(style,*,with_padding)->str, layout_alignment, self_alignment |
| Qt runner | tempestroid/renderers/qt/app_runner.py |
run_qt (qasync) |
| Compose translator | tempestroid/renderers/compose/style_translator.py |
to_compose(style)->dict (spec JSON-able) |
| Bridge protocolo | tempestroid/bridge/protocol.py |
handler_token(path,prop), event_type_for, MountMessage/PatchMessage/EventMessage |
| Bridge serializer | tempestroid/bridge/serializer.py |
serialize_node(node,path=())->dict, serialize_patch(patch)->dict |
| Bridge handlers | tempestroid/bridge/handlers.py |
HandlerRegistry.refresh/dispatch/tokens |
| Bridge device | tempestroid/bridge/device.py |
Bridge ABC, LoopbackBridge, DeviceApp.start/reload/handle_event/_on_patches |
| Bridge JNI | tempestroid/bridge/jni.py |
JniBridge, run_device, _on_event (roteia __native_result__) |
| Nativo | tempestroid/native/*.py |
dispatch.py (send_native/send_native_request/resolve_native_result) + um módulo por capacidade |
| Kotlin árvore | android-host/app/src/main/java/org/tempestroid/host/TempestTree.kt |
TempestNode(type,props,children) snapshot; apply(msg) switch mount/patch; parseNode, applyPatch ops |
| Kotlin renderer | android-host/.../host/TempestRenderer.kt |
árvore → @Composable; spec de Style → Modifier/Arrangement |
| Kotlin nativo | android-host/.../host/NativeModules.kt |
por-activity; ActivityResultLaunchers; roteia comandos native |
| Kotlin runtime | android-host/.../host/PythonRuntime.kt |
dispatchEvent, onMessageFromPython, messageSink |
| Kotlin activity | android-host/.../host/MainActivity.kt |
ComponentActivity; alimenta a árvore; modo dev por intent |
| Testes | tests/unit/test_*.py, tests/conformance/test_conformance.py (+ golden/) |
unit por área; golden Qt vs Compose |
Padrão de widget novo (template, fiel a button.py):
class Dropdown(Widget):
"""..."""
event_schemas: ClassVar[dict[str, type[Event]]] = {"on_select": SelectEvent}
options: list[str]
value: str | None = None
on_select: EventHandler | None = None
Depois: re-exportar em widgets/__init__.py (+ __all__), em tempestroid/__init__.py (+ __all__), o evento em widgets/events.py (+ __all__), mapear no event_type_for se o token cruzar a ponte, e adicionar testes em tests/unit/.
Convenção de fases. E<n>, sub-tarefas E<n>a/b/c…. Sequenciais por
dependência: E0 (navegação) destrava multi-tela e é pré-requisito de quase tudo;
E1 (listas) e E2 (overlays) são a base de UX; E3 (animação) é consumida por
E0/E2 nas transições; daí em diante o acoplamento afrouxa. Cada fase tem:
Descrição → Superfície nova → Arquivos → Contrato → Sub-tarefas → Metas →
Feito quando.
E0 — Navegação e rotas¶
Descrição¶
Hoje o framework renderiza uma tela única. Falta o recurso mais estrutural de
qualquer app mobile: uma pilha de navegação (push/pop), abas, gaveta como
rota, integração com o botão voltar do Android e deep links. Equivalentes:
Navigator/go_router (Flutter), React Navigation (RN).
Superfície nova¶
- Módulo dedicado
tempestroid/navigation.py:Route,Router,NavStack. - API no
App:push/pop/replace/reset; a pilha vive noApp.state. - Widgets:
Navigator(host de pilha),TabView+TabBar,RouteDrawer. - Evento:
RouteChangeEvent.
Arquivos¶
- Novo:
tempestroid/navigation.py;widgets/navigation_widgets.py(Navigator/TabView/TabBar/RouteDrawer). - Edita:
core/state.py(helpers de navegação noApp),widgets/events.py(RouteChangeEvent),widgets/__init__.py+tempestroid/__init__.py(re-export),renderers/qt/renderer.py(transição de pilha),bridge/protocol.py(token reservado__back__),android-host/.../MainActivity.kt(onBackPressed→evento),TempestRenderer.kt(render doNavigator/abas comAnimatedContent). - Testes:
tests/unit/test_navigation.py,tests/conformance/(transições).
Contrato¶
# navigation.py
class Route(BaseModel): # frozen
name: str
params: dict[str, Any] = {}
class NavStack(BaseModel): # parte do App.state do usuário, ou embutida
stack: list[Route] = [Route(name="/")]
@property
def top(self) -> Route: ...
# A pilha NÃO é um Node novo: o view(app) lê app.nav.top e monta a tela.
# push/pop só mutam o NavStack + request_rebuild — reaproveita o loop coalescido.
class App(Generic[S]):
nav: NavStack
def push(self, route: Route) -> None: ... # nav.stack.append; request_rebuild
def pop(self) -> bool: ... # pop se len>1; request_rebuild; False se raiz
def replace(self, route: Route) -> None: ...
def reset(self, stack: list[Route]) -> None: ...
- Plug no diff: trocar de rota é o
viewmontando outra subtree → odiffexistente emite umReplaceno nó doNavigator. Nenhuma mudança no reconciliador — só um hint de transição emprops({"transition": "slide"}). - Botão voltar:
MainActivity.onBackPressed()→PythonRuntime.dispatchEvent("__back__:")→bridge/jni.py:_on_eventreconhece o token reservado →App.pop(). SepopretornaFalse(raiz), o host faz o back padrão (fecha app). - Qt:
Esc/botão simulado →App.pop().
Sub-tarefas¶
- E0a (core):
navigation.py+ helpers noApp+RouteChangeEvent. Testes de pilha (push/pop/replace/reset, params tipados). Não toca renderizador. - E0b (Qt):
Navigator/TabView/RouteDrawernorenderer.pycomQStackedWidget+ transiçãoQPropertyAnimation;Esc→pop. - E0c (Compose): mesmos widgets em
TempestRenderer.ktcomAnimatedContent; abas e gaveta. - E0d (back/deep link): token
__back__(protocol + jni + MainActivity); deep link = intent extra →reset(stack inicial).
Metas¶
Multi-tela com histórico, voltar do Android funcionando, abas e gaveta como rotas reais, deep link resolvendo para uma pilha inicial.
Feito quando¶
App de exemplo com 3 telas navega push/pop; botão voltar do Android faz pop
(verificado no device por screenshot); abas trocam de tela; tempest spec lista
RouteChangeEvent; conformância das transições verde.
E1 — Listas virtualizadas e scroll avançado¶
Descrição¶
ScrollView + Grid renderizam tudo — inviável para listas grandes. Falta
virtualização (só renderiza o visível), seções com cabeçalho fixo,
pull-to-refresh e scroll infinito. Equivalentes: ListView.builder,
GridView.builder, Slivers (Flutter); FlatList, SectionList (RN).
Superfície nova¶
- Widgets:
LazyColumn/LazyRow,LazyGrid,SectionList,RefreshControl. - API:
on_end_reached+end_reached_threshold. - Eventos:
ScrollEvent,RefreshEvent,EndReachedEvent.
Arquivos¶
- Novo:
widgets/lists.py(LazyColumn/LazyRow/LazyGrid/SectionList/RefreshControl). - Edita:
core/ir.py(suporte a nó comitem_count+ sem filhos materializados),core/reconciler.py(diff por janela visível — ver Contrato),widgets/events.py(3 eventos),bridge/serializer.py(serializar o builder-range),renderers/qt/renderer.py,TempestTree.kt/TempestRenderer.kt(LazyColumnnativo), re-exports. - Testes:
tests/unit/test_lists.py,tests/unit/test_reconciler.py(janela).
Contrato¶
class LazyColumn(Widget):
event_schemas: ClassVar[...] = {"on_end_reached": EndReachedEvent, "on_scroll": ScrollEvent}
item_count: int
item_builder: Callable[[int], Widget] # NÃO serializável direto
on_end_reached: EventHandler | None = None
- Janela visível: o nó da lista não materializa filhos. O renderizador
reporta o range visível
[start, end)(viaScrollEvent); o core chamaitem_builder(i)só nesse range e diffa apenas a janela contra a anterior, por chave de item (reaproveita_reconcile_keyeddo A2). ONodeda lista guardaprops={"item_count": n, "window": [start,end]}echildren= só os itens da janela. Mudança de scroll → novoScrollEvent→ rebuild da janela. - Decisão de divergência: Compose (
LazyColumn) já virtualiza nativo — pode receber só oitem_count+ um canal de "me dê o item i" (event request, padrão__native_result__). Qt monta a janela no Python. Documentar na conformância.
Sub-tarefas¶
- E1a (core): modelo de nó virtual + diff por janela + 3 eventos. Teste "monta só a janela".
- E1b (Qt):
QListView/viewport custom + sinal de scroll + overlay de refresh. - E1c (Compose):
LazyColumn/LazyVerticalGrid/stickyHeader/PullRefreshIndicator;EndReachedEventviaderivedStateOf(LazyListState). - E1d:
SectionList(seções + header fixo) sobre E1a–c.
Metas¶
Rolar 10k itens sem travar nos dois renderizadores; seção com cabeçalho fixo; puxar-para-atualizar; carregar mais ao chegar no fim.
Feito quando¶
Exemplo de lista de 10k itens rola fluido no Qt e no device; pull-to-refresh
dispara handler e atualiza; on_end_reached pagina; cabeçalho de seção gruda no
topo (screenshot device).
E2 — Overlays e feedback¶
Descrição¶
Faltam os overlays canônicos de mobile: diálogo modal, bottom sheet,
toast/snackbar transitório, tooltip, menu suspenso/popover e
action sheet. Equivalentes: showDialog/showModalBottomSheet/SnackBar/
PopupMenuButton (Flutter); Modal/ActionSheetIOS + libs (RN).
Superfície nova¶
- API imperativa no
App:show_dialog/show_sheet/toast/show_menu/dismiss. - Widgets:
Dialog,BottomSheet,Toast,Tooltip,Menu/MenuItem,Popover,ActionSheet. - Camada de overlay no estado do
App.
Arquivos¶
- Novo:
widgets/overlays.py. - Edita (núcleo — alto risco):
core/ir.py(raiz vira{root, overlays}— ver Contrato),core/reconciler.py(build/diffcientes da camada),core/state.py(Appguardaoverlays+ API imperativa),bridge/protocol.py(MountMessage/PatchMessageganham campooverlays),bridge/serializer.py,renderers/qt/renderer.py(z-order + barrier),TempestTree.kt(parsearoverlays),TempestRenderer.kt(Dialog/ModalBottomSheet/Popup/SnackbarHost). - Testes:
tests/unit/test_overlays.py,tests/unit/test_reconciler.py(camada),tests/conformance/.
Contrato — mudança de núcleo detalhada¶
A árvore deixa de ser um único Node. Introduzir um documento de UI:
# core/ir.py — NÃO mexe em Node; envolve a raiz
class Scene(_IRModel):
root: Node
overlays: list[Node] = [] # z-order crescente, acima da root
# core/reconciler.py
def build_scene(widget: Widget, overlays: list[Widget]) -> Scene: ...
def diff_scene(old: Scene, new: Scene) -> list[Patch]:
# diffa root como hoje (paths começam em ()).
# overlays diffam por CHAVE (cada overlay tem id estável) — reusa _reconcile_keyed.
# path do overlay i = ("overlay", i, ...) — Path passa a aceitar um tag inicial.
Pathganha namespace: hojetuple[int,...]. Passa atuple[int|str,...]onde o primeiro elemento pode ser"overlay". Renderizadores roteiam por esse prefixo. Compatível com paths atuais (sem prefixo = root).App:self._overlays: list[OverlayEntry].show_dialog(node, *, barrier=True)empurra um overlay com id, agendarequest_rebuild;dismiss(id)remove.toast(...)agenda remoção porloop.call_later. O_rebuildpassa a montarScenee chamardiff_scene.- Protocolo:
MountMessage.overlays: list[dict],PatchMessagecarrega patches com path namespaced. Kotlin:TempestTreeguardaroot+overlays[],MainActivityrenderiza root e, por cima, cada overlay no composable certo. - Barrier/dismiss:
DismissEvent(token reservado por overlay id) sobe pela ponte normal.
Sub-tarefas¶
- E2a (núcleo):
Scene+build_scene/diff_scene+Pathnamespaced +Appoverlay API. Só core, sem renderizador. Testes de camada (empilhar/dismiss/ordem). Esta sub-tarefa é a de maior risco — fechar e revisar antes das demais. - E2b (protocolo/bridge): estender
MountMessage/PatchMessage/serializer;DeviceAppenviaScene. - E2c (Qt): overlays como
QWidgetz-order + máscara (barrier);QMenu; toast com timer+fade. - E2d (Compose):
Dialog/ModalBottomSheet/DropdownMenu/Popup/SnackbarHostemTempestRenderer/MainActivity.
Metas¶
Diálogo modal com barrier e foco; bottom sheet arrastável; toast some sozinho; menu/popover ancorado; action sheet.
Feito quando¶
Cada overlay abre e fecha por handler no Qt e no device; barrier bloqueia toques
atrás; toast expira; menu abre no anchor (screenshot device); testes da camada
Scene verdes.
E3 — Framework de animação¶
Descrição¶
Só existe Transition (estilo CSS declarativo). Falta um motor de animação
real: controladores, curvas/tweens, animações implícitas, dirigidas por
gesto, transição de elemento compartilhado (Hero) e skeleton/shimmer.
Equivalentes: AnimationController/AnimatedContainer/Hero (Flutter);
Animated/Reanimated (RN).
Superfície nova¶
- Módulo dedicado
tempestroid/animation.py:AnimationController,Tween,Spring, ampliar o enumCurve. - Widgets:
Animated,AnimatedList,Hero,Shimmer/Skeleton. - Clock de frames no
App.
Arquivos¶
- Novo:
tempestroid/animation.py;widgets/animated.py. - Edita:
core/state.py(clock de frames +request_rebuildpor tick — ver Contrato),style.py(ampliarCurve, talvezSpring),renderers/qt/app_runner.py(tickerQTimer/qasync),renderers/compose/style_translator.py(spec carrega curva/duração),TempestRenderer.kt(animate*AsState/AnimatedVisibility/SharedTransitionLayout), re-exports. - Testes:
tests/unit/test_animation.py(clock injetável),tests/conformance/.
Contrato — clock de frames detalhado¶
# animation.py
class AnimationController:
def __init__(self, duration_s: float, curve: Curve = Curve.EASE) -> None: ...
value: float # 0..1, lido pelo view
def forward(self) -> None: ... # registra-se no clock do App
def reverse(self) -> None: ...
def stop(self) -> None: ...
class Tween(Generic[T]):
begin: T; end: T
def at(self, t: float) -> T: ... # interpola (cor/num/edge)
- Clock no
App: novo registroself._animations: set[AnimationController]. Enquanto não-vazio, oAppagenda um tick por frame (loop.call_later(1/60)no Qt; no device o host chama viawithFrameNanos→ evento__frame__). Cada tick: avança cada controller, e chamarequest_rebuild(coalescido). Controller que chega avalue==1se desregistra → clock para (sem busy-loop). - Determinismo de teste: o clock aceita um
time_sourceinjetável; o teste avança manualmente e verifica frames-chave. SemDate.now(proibido nos scripts/loop) — usar o relógio do loop. Animatedwidget: guardatarget+controller. A interpolação roda no core → os renderizadores recebem só props finais por frame (reconciliador permanece agnóstico).- Divergência Qt × Compose (documentar na conformância): para animação
declarativa (mudou o
Stylealvo), Compose pode delegar ao motor nativo (animateColorAsStateetc.) lendoduration/curvedo spec — mais fluido. Qt interpola no core.Hero= Qt anima geometria noReplacede rota; Compose usaSharedTransitionLayout. Registrar essa divergência na tabela do Trilho D.
Sub-tarefas¶
- E3a (core):
animation.py(AnimationController/Tween/Curve) + clock noAppcomtime_sourceinjetável. Testes determinísticos. Sem renderizador. - E3b (Qt): ticker no
app_runner;Animated/AnimatedListinterpolando no core;Shimmer. - E3c (Compose): spec de animação no translator +
animate*AsState/AnimatedVisibility; evento__frame__opcional. - E3d (Hero): transição de elemento compartilhado integrada à E0 (rotas).
Metas¶
Animar tamanho/cor/opacidade ao mudar estado; lista com itens entrando/saindo; Hero entre telas; shimmer de loading; animação dirigida por arrasto.
Feito quando¶
AnimatedContainer-equivalente anima ao mudar estado nos dois renderizadores;
AnimatedList anima insert/remove; Hero faz transição entre rotas no device
(screenshot/gravação); testes do controlador verdes com clock determinístico.
E4 — Gestos avançados¶
Descrição¶
Hoje só tap/long-press/swipe (em widgets/gestures.py). Faltam
pan/drag-and-drop, pinça/zoom/escala, toque duplo, dismissible,
lista reordenável e viewer interativo. Equivalentes: Draggable/
DragTarget/Dismissible/ReorderableListView/InteractiveViewer (Flutter).
Superfície nova¶
- Widgets/handlers (em
widgets/gestures.py):PanHandler,ScaleHandler,DoubleTapHandler,Draggable+DragTarget,Dismissible,ReorderableList,InteractiveViewer. - Eventos:
PanEvent,ScaleEvent,DragEvent,DismissEvent,ReorderEvent.
Arquivos¶
- Edita:
widgets/gestures.py(novos handlers, seguindoTapHandler/SwipeHandlerexistentes),widgets/events.py(5 eventos),bridge/protocol.py(event_type_formapeia os novos tokens),renderers/qt/renderer.py,TempestRenderer.kt(pointerInput), re-exports. - Testes:
tests/unit/test_overlay_gestures.py(já existe — estender),tests/conformance/.
Contrato¶
- Padrão já existe: seguir
SwipeHandler/SwipeEventemgestures.py/events.py. Cada novo evento é frozen, registrado noevent_schemasdo handler, validado porparse_event, mapeado emevent_type_for. - Reorder usa o diff existente:
ReorderEvent(from_index, to_index)→ handler reordena a lista no estado → odiffemiteReorder(A2). Zero mudança de core. - Qt:
QGestureRecognizer/eventos de mouse; pinçaQPinchGesture; DnDQDrag/dropEvent;InteractiveViewer=QGraphicsViewcom transform. - Compose:
pointerInput+detectDragGestures/detectTransformGestures/detectTapGestures(onDoubleTap);SwipeToDismiss; reorder viadetectDragGesturesAfterLongPress; viewer =graphicsLayer.
Sub-tarefas¶
- E4a: eventos + handlers (Python) +
event_type_for. Testes de parse/validação. - E4b (Qt): reconhecedores no
renderer.py. - E4c (Compose):
pointerInputnoTempestRenderer. - E4d:
Dismissible+ReorderableList(compõem E4a–c + diffReorder).
Metas¶
Arrastar e soltar entre alvos; pinça-zoom de imagem; swipe-to-delete em lista; reordenar por arrasto; duplo-toque.
Feito quando¶
Cada gesto dispara o evento tipado correto e muda o estado nos dois renderizadores;
swipe-to-delete remove item; reorder reordena (diff Reorder); pinça-zoom no
device (screenshot).
E5 — Inputs e formulários¶
Descrição¶
Faltam controles de formulário centrais: dropdown/select, time picker,
range slider, um framework de formulário/validação, autocomplete,
OTP/pin e input mascarado. Equivalentes: DropdownButton/showTimePicker/
RangeSlider/Form+TextFormField (Flutter).
Superfície nova¶
- Widgets (em
widgets/inputs.py):Dropdown/Select,TimePicker,RangeSlider,Autocomplete,PinInput,MaskedInput. - Módulo dedicado
widgets/forms.py:Form,FormField,Validator,FormState. - Eventos:
SelectEvent,TimeChangeEvent,RangeChangeEvent,SubmitEvent,ValidationEvent.
Arquivos¶
- Novo:
tempestroid/widgets/forms.py. - Edita:
widgets/inputs.py(novos controles, seguindoInput/Slider/DatePickerexistentes),widgets/events.py(5 eventos),bridge/protocol.py,renderers/qt/renderer.py,TempestRenderer.kt, re-exports. - Testes:
tests/unit/test_input_widgets.py(estender),tests/unit/test_forms.py.
Contrato¶
- Validação espelha
parse_event:Validatoré função tipada(value) -> str | None(erro ouNone);FormStateagrega erros por campo + validade.Formguarda o estado dos campos noApp.state; submit roda todos os validadores e bloqueia se houver erro — erro estruturado JSON-serializável, mesma filosofia doEventValidationError. - Novos inputs seguem o padrão de
Input(valor +on_changetipado). - Qt:
QComboBox/QTimeEdit/slider duplo custom/QCompleter/setInputMask. - Compose:
ExposedDropdownMenuBox/TimePickerM3/RangeSlider/VisualTransformation.
Sub-tarefas¶
- E5a: controles isolados (
Dropdown/TimePicker/RangeSlider/PinInput/MaskedInput) + eventos, nos dois renderizadores. - E5b:
forms.py(Form/FormField/Validator/FormState) +Autocomplete. Testes de validação.
Metas¶
Select com opções; escolher hora; faixa min–max; formulário que valida e mostra erro por campo; autocomplete filtrando; pin/OTP; máscara (CPF/telefone/etc.).
Feito quando¶
Formulário de exemplo valida e bloqueia submit inválido com erro por campo nos dois renderizadores; cada novo input dispara seu evento tipado; conformância dos controles verde.
E6 — Layout refinado¶
Descrição¶
Refinos de layout: flex-wrap, PageView/carousel, slivers (app bar colapsável/parallax), tabela/DataTable e AspectRatio.
Superfície nova¶
Style: campoflex_wrap.- Widgets:
Wrap,PageView,CollapsingAppBar,Table/DataTable,AspectRatio. - Evento:
PageChangeEvent.
Arquivos¶
- Edita:
style.py(flex_wrap),renderers/qt/style_translator.py+renderers/compose/style_translator.py(traduzirflex_wrap— espelhar),widgets/layout.py(Wrap/PageView/AspectRatio),components/(CollapsingAppBar/Table),widgets/events.py(PageChangeEvent), renderers, re-exports. - Testes:
tests/conformance/(flex_wrap),tests/unit/test_widgets.py.
Contrato¶
Wrapé só estilo (flex_wrap): entra nos dois translators + conformância, como qualquer campo deStyle.PageViewguarda página ativa no estado;CollapsingAppBarcoordena com o scroll da E1 (nested scroll).- Qt: flow layout custom (
Wrap);QStackedWidget+swipe (PageView); header colapsável por sinal de scroll;QTableView. - Compose:
FlowRow/FlowColumn;HorizontalPager;TopAppBar+nestedScroll;Modifier.aspectRatio.
Sub-tarefas¶
- E6a:
flex_wrap+Wrap+AspectRatio(puro estilo/layout). Conformância. - E6b:
PageView+PageChangeEvent. - E6c:
CollapsingAppBar(depende de E1) +Table/DataTable.
Metas¶
Chips/tags que quebram linha; carousel paginado com indicador; app bar que encolhe ao rolar; tabela de dados; razão de aspecto fixa.
Feito quando¶
Wrap quebra linha igual nos dois renderizadores (conformância); PageView
pagina e emite PageChangeEvent; app bar colapsa ao rolar (screenshot device).
E7 — Mídia e gráficos¶
Descrição¶
Lacuna de mídia/gráficos: player de vídeo, WebView, canvas/desenho
vetorial, SVG, preview de câmera ao vivo, leitor de QR, mapa,
blur/backdrop, clip de forma. Equivalentes: VideoPlayer/webview_flutter/
CustomPaint/CameraPreview/google_maps_flutter (Flutter).
Superfície nova¶
- Widgets (em
widgets/media.py):VideoPlayer,WebView,Canvas,Svg,CameraPreview,QrScanner,MapView,Blur/BackdropFilter,ClipPath.
Arquivos¶
- Edita:
widgets/media.py(novas folhas;Image/Iconjá estão aí),style.py(talvezblur/clip), os doisstyle_translator.py(blur/clip — espelhar),bridge/serializer.py(spec de comandos doCanvas),renderers/qt/renderer.py,TempestRenderer.kt,NativeModules.kt(QR scanner → resultado por__native_result__), manifest (permissões câmera). - Testes:
tests/unit/test_media.py,tests/unit/test_serializer.py(canvas),tests/conformance/(blur/clip).
Contrato¶
Canvas= lista de comandos serializável (o único item com IR nova):Canvas(commands: list[DrawCommand])ondeDrawCommandé union frozen (Path/Fill/Stroke/Text).serialize_nodebaixa para JSON-able; o diff compara a lista (reusa_diff_props). Qt interpreta comQPainter; Compose comdrawIntoCanvas. Entra na conformância dos comandos.- Folhas com host nativo:
VideoPlayer/WebView/CameraPreview/MapViewsãoAndroidViewno Compose (sem mudança de C); QR devolve resultado pelo canal de evento (padrão B6). Qt:QMediaPlayer/QWebEngineView/QCamera; mapa e QR no sim = placeholder com aviso explícito (sem equivalente desktop fiel).
Sub-tarefas¶
- E7a:
Canvas(IR de comandos + diff + ambos renderizadores). Conformância. - E7b:
VideoPlayer+WebView(folhasAndroidView). - E7c:
CameraPreview+QrScanner(CameraX +__native_result__). - E7d:
MapView+Blur/ClipPath(estilo) +Svg.
Metas¶
Tocar vídeo; embutir página web; desenhar formas/charts em canvas; renderizar SVG; ver câmera ao vivo; ler QR; mostrar mapa; aplicar blur/clip.
Feito quando¶
Vídeo toca e WebView carrega no device; Canvas desenha um chart simples idêntico
nos dois renderizadores (conformância dos comandos); SVG renderiza; preview de
câmera e leitura de QR funcionam no device (screenshot). Itens sem equivalente Qt
(mapa, scanner) declaram placeholder explícito no sim.
E8 — Plataforma e sistema nativo¶
Descrição¶
Capacidades de sistema: haptics/vibração, sensores, StatusBar, teclado (avoiding/dismiss), lifecycle (bg/fg), deep linking, permissões, biometria, secure storage/keychain, prefs, SQLite, connectivity, push (FCM) + notificação agendada, background tasks.
Superfície nova¶
native/:haptics.py,sensors.py,system.py,lifecycle.py,permissions.py,biometrics.py,secure_storage.py,prefs.py,database.py,connectivity.py,push.py,background.py.- Widget:
KeyboardAvoidingView(emwidgets/layout.py). - Eventos:
LifecycleEvent,SensorEvent,ConnectivityEvent,DeepLinkEvent.
Arquivos¶
- Novo: os módulos
native/*.pyacima (seguirnative/camera.py/geolocation.pyexistentes). - Edita:
native/__init__.py+native/dispatch.py(re-export; padrãosend_native/send_native_request/resolve_native_result),bridge/jni.py(token reservado novo__sensor__/__lifecycle__para streams),android-host/.../NativeModules.kt(um módulo Kotlin por capacidade — estender o router B6),MainActivity.kt(lifecycle/permissõesActivityResultContracts), manifest (permissões + FCM service),tempestroid/__init__.py. - Testes:
tests/unit/test_native.py(estender — já cobre o padrão request/response).
Contrato¶
- Maioria = padrão B6 sem mudança de C:
send_native_request(envelope)→await Future→ host responde por__native_result__:<id>. Resultados tipados (frozen) +NativeError(code)em falha. Espelhanative/camera.py. - Exceção — streams (sensores) e lifecycle: eventos contínuos do host.
Entram pelo canal de evento existente como
EventMessagecom token reservado novo (__sensor__:<type>,__lifecycle__), roteado embridge/jni.py:_on_eventcomo o__native_result__já é. Token novo, não mudança de C. - Simulador Qt: o que não tem hardware (sensores, biometria, FCM, WorkManager)
= stub/mock com aviso explícito ("device-only"); o que dá pra simular (prefs
em arquivo, SQLite via
sqlite3stdlib, clipboard, lifecycle por foco da janela) roda de verdade.
Sub-tarefas¶
(cada uma é um módulo + módulo Kotlin + teste; independentes entre si)
- E8a: haptics + system (statusbar/brilho/wakelock) + KeyboardAvoidingView.
- E8b: sensores (stream, token reservado) + lifecycle + connectivity + deep link.
- E8c: permissões (API explícita) + biometria.
- E8d: secure storage + prefs + SQLite (parte simulável no Qt).
- E8e: push (FCM) + notificação agendada + background/WorkManager.
Metas¶
Vibrar; sensores em stream; controlar status bar; teclado não cobre o input; reagir a bg/fg; deep link; pedir/checar permissão; biometria; segredo cifrado; prefs + SQLite; estado de rede; push + agendamento; tarefa em background.
Feito quando¶
Cada capacidade tem a metade Python unit-testada off-device; no device, haptics vibra, sensor faz stream, teclado recua a tela, permissão é pedida/concedida, biometria autentica, prefs/SQLite persistem, push chega e notificação agenda (evidência on-device). Stubs do simulador avisam explicitamente o que é device-only.
E9 — Transversais (tema, i18n, acessibilidade)¶
Descrição¶
Bases transversais: tema/dark mode + MediaQuery, i18n/l10n + RTL,
acessibilidade (semantics, leitor de tela, foco) e fontes custom + escala de
texto. Equivalentes: Theme/MediaQuery/Directionality/Semantics (Flutter).
Superfície nova¶
- Módulos dedicados
tempestroid/theme.py(Theme,ThemeMode,MediaQueryData) etempestroid/i18n.py(Locale,translate/t, direção). Style/widgets: camposemantics(label/role/hint),focusable;Styleganhatext_scale/fonte.- Eventos:
ThemeChangeEvent,LocaleChangeEvent.
Arquivos¶
- Novo:
tempestroid/theme.py,tempestroid/i18n.py. - Edita:
core/state.py(Appexpõetheme/media/localepara oviewler — contexto, não Node),widgets/base.py(semantics/focusablenoWidget),style.py(text_scale/fonte; RTL invertestart/end), os doisstyle_translator.py(RTL espelhado + fonte — conformância),core/introspection.py(exporsemantics),renderers/qt/renderer.py,TempestRenderer.kt/MainActivity.kt, re-exports. - Testes:
tests/conformance/(RTL start/end espelhados; light/dark),tests/unit/test_introspection.py(semantics),tests/unit/test_theme.py.
Contrato¶
- Tema/MediaQuery/Locale = contexto de entrada do
build, não nó da árvore: oview(app)lêapp.theme/app.media/app.localee monta de acordo. Trocar tema/locale = mutar esse contexto +request_rebuild. Mantém "árvore é IR". - RTL inverte semântica
start/endnos dois translators (espelhar + conformância).semanticsé campo doWidget, propagado a ambos os renderizadores e aointrospect(). - Qt: paleta QSS trocável;
MediaQuerylê tamanho da janela/presetDevice(já existe);setLayoutDirection;QAccessible;QFontDatabase. - Compose:
MaterialTheme/isSystemInDarkTheme;LocalConfiguration;LocalLayoutDirection;Modifier.semantics;FontFamilycustom +LocalDensity.
Sub-tarefas¶
- E9a:
theme.py+ dark mode +MediaQueryData(contexto noApp). Snapshot light/dark. - E9b:
i18n.py+ RTL (translators + conformância espelhada). - E9c: acessibilidade (
semantics/focusable+ introspect) + fontes custom/escala.
Metas¶
Trocar light/dark (e seguir o sistema); responsivo por breakpoint/orientação; traduzir + espelhar RTL; rótulos lidos pelo TalkBack; fontes custom + respeitar escala do sistema.
Feito quando¶
Dark mode aplica nos dois renderizadores (snapshot light/dark); RTL espelha
start/end (conformância); TalkBack lê os rótulos no device; troca de locale
re-renderiza; fonte custom carrega e a escala de texto do sistema é respeitada.
Resumo de fases¶
| Fase | Escopo | Sub-tarefas | Risco núcleo | Destrava |
|---|---|---|---|---|
| E0 | Navegação e rotas | a(core) b(Qt) c(Compose) d(back/deeplink) | baixo (reusa diff) | multi-tela — pré-req de quase tudo |
| E1 | Listas virtualizadas + scroll | a(core/janela) b(Qt) c(Compose) d(section) | médio (diff por janela) | performance de listas |
| E2 | Overlays e feedback | a(núcleo Scene) b(bridge) c(Qt) d(Compose) | ALTO (Scene + Path namespaced) |
UX básica de mobile |
| E3 | Framework de animação | a(core/clock) b(Qt) c(Compose) d(Hero) | ALTO (clock de frames + divergência) | movimento/transições |
| E4 | Gestos avançados | a(eventos) b(Qt) c(Compose) d(dismiss/reorder) | baixo (padrão pronto) | interação rica |
| E5 | Inputs e formulários | a(controles) b(forms/validação) | baixo | formulários sérios |
| E6 | Layout refinado | a(wrap/aspect) b(pager) c(collapsing/table) | baixo | layouts ricos |
| E7 | Mídia e gráficos | a(canvas) b(vídeo/web) c(câmera/QR) d(mapa/blur/svg) | médio (IR de canvas) | mídia/gráficos |
| E8 | Plataforma/sistema | a..e (um módulo cada) | baixo (padrão B6 + token p/ stream) | integração com o SO |
| E9 | Transversais | a(tema) b(i18n/RTL) c(a11y/fontes) | médio (contexto + RTL) | base transversal |
Ordem de delegação. E0 → E1 → E2 → E3 primeiro (E2a e E3a, as sub-tarefas de núcleo, fecham e passam por review antes das sub-tarefas de renderizador). E4–E9 acoplam menos e reordenam por demanda — exceto E6c (depende de E1) e E3d (depende de E0). Como nos outros trilhos: uma sub-tarefa por agente, fechando no "feito quando", com os dois renderizadores verdes e — havendo device — verificação dual obrigatória.