# tempestroid — full documentation > Framework for building native Android apps in typed Python: one declarative, fully typed Pydantic widget tree is diffed by a renderer-agnostic reconciler into patches, applied by two leaf renderers — Qt (desktop simulator) and Jetpack Compose (device). Generated from the project docs (llmstxt.org convention). Source: https://mauriciobenjamin700.github.io/tempestroid/ — index at https://mauriciobenjamin700.github.io/tempestroid/llms.txt. --- # File: README.md # Tempestroid > 📖 **Documentação / Docs:** site MkDocs bilíngue em [`docs/`](docs/index.md) > com seletor **PT-BR / EN-US** no header — rode `uv run mkdocs serve` e abra > (PT) ou (EN). Guia do > usuário, arquitetura e referência da API. > > 🤖 **Ler com sua IA / Read with your AI:** o site publica > [`/llms.txt`](https://mauriciobenjamin700.github.io/tempestroid/llms.txt) (índice) > e [`/llms-full.txt`](https://mauriciobenjamin700.github.io/tempestroid/llms-full.txt) > (docs inteiras num arquivo) seguindo a convenção [llmstxt.org](https://llmstxt.org/) — > entregue a URL ao seu assistente para usar o projeto como referência (sem servidor/MCP). Build **native Android apps** in **typed Python**. You write one declarative, fully typed widget tree (a Pydantic IR). A **renderer-agnostic reconciler** diffs it into patches. Two leaf renderers apply those patches: **Qt** for the desktop simulator, **Jetpack Compose** for the device. The runtime is **async-first**, with an Expo-style dev loop: hot reload in the Qt simulator and LAN code-push to a device over QR — both shipping today. > This is a **framework, not a web service** — no FastAPI, SQLAlchemy, Redis, or > HTTP layering. See [`docs/plan.md`](docs/plan.md) for the full design and the > phase roadmap. --- ## Why - **Typed end to end.** Style model, widget primitives, events, and the Python↔Kotlin boundary contract are all Pydantic v2 / fully typed. `pyright` runs in strict mode. - **One tree, two targets.** The reconciler is pure data-in → patches-out. All platform divergence is confined to the two `Style` translators (Qt today, Compose next). - **Async-first.** Event handlers and lifecycle hooks may be sync or `async`; Python runs on a background asyncio loop, never the UI thread. - **Fast inner loop.** `tempest dev` watches your file and hot-restarts the Qt simulator on save — no device or emulator needed for UI work. --- ## How it works ```text view(app) ──build──▶ Node tree (IR) │ diff pure, renderer-agnostic ▼ [ Patch ] Insert / Remove / Update / Reorder / Replace ╱ ╲ Qt renderer Compose renderer (simulator) (device, B4) ``` 1. `view(app) -> Widget` builds a declarative widget tree from current state. 2. `build` lowers it to a `Node` IR; `diff` compares old vs. new and emits a minimal `Patch` list. 3. A renderer applies patches to live widgets. State changes coalesce into one rebuild per tick. --- ## Install **Building an app?** Install from PyPI — the **core** needs only `pydantic`: ```bash pip install tempestroid # core pip install "tempestroid[qt]" # + desktop simulator (PySide6 + qasync) pip install "tempestroid[icons]" # + tempest icon (Pillow) ``` Building the Android **APK** (`tempest build apk`) needs only a **JDK + Android SDK** (no NDK, no CPython toolchain, no repo clone — the `android-host` ships in the package). Run `tempest setup --install` to get the SDK and `tempest doctor` to check what's missing. **Contributing to the framework?** Clone this repo and use `uv` — one command installs the core + dev tooling + Qt simulator + docs: ```bash uv sync ``` See the [installation guide](https://mauriciobenjamin700.github.io/tempestroid/instalacao/) ([EN](https://mauriciobenjamin700.github.io/tempestroid/en/instalacao/)) for the full breakdown. --- ## Quick start ```python from dataclasses import dataclass from tempestroid import App, Button, Column, Style, Text, Widget @dataclass class CounterState: value: int = 0 def make_state() -> CounterState: return CounterState() def view(app: App[CounterState]) -> Widget: def increment() -> None: app.set_state(lambda s: setattr(s, "value", s.value + 1)) return Column( style=Style(gap=8.0), children=[ Text(content=f"Count: {app.state.value}", key="label"), Button(label="+", on_click=increment, key="inc"), ], ) if __name__ == "__main__": # Import the Qt renderer lazily — keep the module Qt-free so the SAME file # also loads on the Android device (which has no PySide6). A top-level # `from tempestroid.renderers.qt import run_qt` would crash the on-device # load; the framework now shows an error screen instead of a blank window, # but the fix is to import Qt only where you run the desktop simulator. from tempestroid.renderers.qt import run_qt raise SystemExit(run_qt(make_state(), view, title="counter")) ``` > 💡 The module above only ever imports `tempestroid` (renderer-agnostic) at the > top level — `run_qt` is imported lazily inside `__main__`. That is what lets > the same `make_state()` + `view(app)` run in the Qt simulator **and** on a > device via `tempest serve` with no changes. If an app file (or one of its > imports) fails to load on the device, the host now renders a red **error > screen** carrying the traceback instead of a silent white window. Full example with sync **and** `async` handlers: [`examples/counter/app.py`](examples/counter/app.py). --- ## Design system (M3 variants + Chakra-style API) The styled components carry a **Chakra-ergonomics variant API** — `variant` / `size` / `color_scheme` — and resolve a complete **Material 3** `Style` from a `Theme` (tonal color schemes, spacing/shape/typography/elevation scales). Seed one brand color with `Theme.from_seed(...)` and the whole palette (light + dark, WCAG-AA contrast, ≥ 48dp touch targets) comes for free. It is **additive**: raw `Style` still works, and an explicit `style=` is merged on top of the resolved variant. ```python from tempestroid import ( Button, Color, Column, FieldVariant, IconButton, Input, Size, Style, Theme, Variant, Widget, ) # One brand seed → a full Material 3 theme (light + dark). theme = Theme.from_seed(Color.from_hex("#2563eb")) def panel() -> Widget: return Column( style=Style(gap=12.0), children=[ # variant / size / color_scheme resolve a Material 3 Style: Button( label="Save", variant=Variant.SOLID, size=Size.MD, color_scheme="primary", theme=theme, key="save", ), IconButton(icon="add", label="Add", color_scheme="primary", theme=theme, key="add"), # The field family adds field_variant; pass the live theme so the # whole kit follows dark mode: Input( value="", placeholder="Name", field_variant=FieldVariant.OUTLINE, color_scheme="primary", theme=theme, key="name", ), ], ) ``` The full kit (Buttons + IconButtons, the field family, selection controls, slider, the BR inputs) is shown in [`examples/h2gallery/app.py`](examples/h2gallery/app.py) and the Button variant matrix in [`examples/h1buttons/app.py`](examples/h1buttons/app.py). > 📖 Tutorial-first guide: **[Theme and tokens](https://mauriciobenjamin700.github.io/tempestroid/guia/design-system/tokens/)** > · **[Chakra-style variants](https://mauriciobenjamin700.github.io/tempestroid/guia/design-system/variantes/)** > · **[Action and entry kit](https://mauriciobenjamin700.github.io/tempestroid/guia/design-system/kit/)** > · **[Surface & layout](https://mauriciobenjamin700.github.io/tempestroid/guia/design-system/superficie/)** > · **[Data display & feedback](https://mauriciobenjamin700.github.io/tempestroid/guia/design-system/feedback/)** > (in-repo: [`docs/guia/design-system/`](docs/guia/design-system/tokens.md)). --- ## Gallery A set of runnable example apps lives in [`examples/`](examples/README.md). Each exposes the same `make_state()` + `view(app)` contract, so it runs in the Qt simulator (`uv run python examples//app.py`) **and** on a device via code-push (`uv run tempest serve examples//app.py`) with no changes. | App | What it shows | |---|---| | [`counter`](examples/counter/app.py) | Sync + `async` handlers, the basics. | | [`todo`](examples/todo/app.py) | Type-to-add list — `Input` + `insert` / `remove` / `update` patches. | | [`calculator`](examples/calculator/app.py) | Dense nested `Row`/`Column` button grid. | | [`stopwatch`](examples/stopwatch/app.py) | Async loop ticking the UI via `asyncio.sleep`. | | [`colorpicker`](examples/colorpicker/app.py) | Dynamic `Style` updates (swatches + toggles). | | [`form`](examples/form/app.py) | The value-bearing inputs (`Input` / `Checkbox` / `DatePicker` / `FilePicker`) + their typed change events. | | [`forms`](examples/forms/app.py) | A validating `Form` of `FormField`s (typed validators block an invalid submit, per-field error inline) + `Dropdown` + `PinInput` OTP. | | [`gallery`](examples/gallery/app.py) | The expanded set — `Slider` / `Switch` / `ProgressBar` / `Spinner` / `Image` / `Icon` / `ScrollView`, secure + regex + multiline text fields, and a `Style.transition`. | | [`layout`](examples/layout/app.py) | Refined layout — `Wrap` chips that wrap, a paginated `PageView` (`PageChangeEvent` + dot indicator) and a `CollapsingAppBar` that shrinks on scroll. | | [`platform`](examples/platform/app.py) | Platform/system (E8) — haptics, real preferences, the lifecycle stream and a `KeyboardAvoidingView`. | | [`theming`](examples/theming/app.py) | Cross-cutting (E9) — a light/dark `ThemeMode` toggle (`App.set_theme`), a PT↔AR locale/RTL toggle (`App.set_locale` + `translate`), and a counter label carrying `Semantics(label=…)`. | | [`native_caps`](examples/native_caps/app.py) | Native capabilities — `clipboard` / `storage` / `database` (SQLite) / `secure_storage` / `system`, each a request/response round-trip returning a typed result (device-verified). | **Both renderers** — the Qt simulator and Compose on the device — support the full Track E widget set (~70 types): layout, text & action, the value-bearing inputs (`Input` / `TextArea` / `Checkbox` / `Switch` / `Slider` / `RangeSlider` / `Dropdown` / `DatePicker` / `TimePicker` / `FilePicker` / `PinInput` / `MaskedInput` / `Autocomplete` / `Form`) with their typed change events, virtualized lists, navigation, overlays, animation, gestures, and media. Parity is pinned by the conformance suite (golden snapshots of both `Style` translators) and device-verified across E0–E9. A few hardware widgets (`CameraPreview` / `QrScanner` / `MapView`) are device-only and show a signalled placeholder on Qt. See the per-widget [renderer-coverage matrix](https://mauriciobenjamin700.github.io/tempestroid/referencia/cobertura/) (Qt vs Compose), the [widget set](https://mauriciobenjamin700.github.io/tempestroid/guia/exemplos/#conjunto-de-widgets-atual) and [`examples/README.md`](examples/README.md). --- ## CLI ```bash uv run tempest new # scaffold in the CURRENT dir (id = folder name) uv run tempest dev # dev loop: edit + save → hot reload (reads pyproject) uv run tempest dev -d pixel-7 # …sized to a device preset (dp; matches Compose) uv run tempest install # download + adb-install the prebuilt host (no SDK/NDK) uv run tempest deploy # push the whole project to a device — offline, no SDK/NDK uv run tempest serve # LAN code-push + hot reload (whole project) in dev mode uv run tempest doctor # check the Android build/run prerequisites uv run tempest build apk # per-app APK (own id, installs side by side); reads [tool.tempest]; JDK+SDK uv run tempest build release-apk # release-signed standalone APK (distribute off-Play); --keystore uv run tempest build prd # store-ready release AAB (Play); reads [tool.tempest] + keystore uv run tempest run # build + install on a device + stream logs (needs SDK/NDK) uv run tempest icon logo.png # generate icon.png + splash.png from one image (needs [icons]) uv run tempest optimize model.onnx # quantize (INT8) + .ort for on-device ONNX (needs [vision]) uv run tempest spec # print the typed contract (widgets/events) as JSON uv run tempest uitest test_app.py # run a Playwright-style native UI test (headless) uv run tempest --version # print the framework version (also: tempest version) uv run tempest --help ``` Run `tempest new` **inside your already-created project folder** (and venv): it scaffolds in place and uses the **folder name as the app id** — no extra wrapping directory. Pass a name (`tempest new other`) only if you want a new subdirectory. The generated `pyproject.toml` carries `[tool.tempest] app = "app.py"`, so **`dev` / `serve` / `build` / `run` take no app argument inside a project** — pass an explicit path (`tempest build path/to/app.py`) only to override. Pick a starting structure with `--template`/`-t`: - `default` (the default) — a single `app.py`, great for a quick demo. - `multi` — a pythonic multi-file layout: a typed `state.py`, one `view` per screen under `screens/`, a reusable `Card` `Component` under `components/`, and an `app.py` that routes with `Navigator` / `Route` (push/pop + Android back). - `native` — the `multi` layout plus a screen that calls native capabilities: `notify` (fire-and-forget) and `await get_position()` (request/response, guarded by `on_device()` + `try/except NativeError`). ```bash uv run tempest new -t multi # multi-file project (in the current dir) ``` `tempest dev` cockpit commands: `r` (hot reload, state preserved), `R` (hot restart, clean state), `s` (raise window), `q` (quit). Saving the file hot-reloads; a reload incompatible with the live state falls back to a clean restart. Apps are **multi-file**: `main.py` may import sibling modules and packages from your project tree. The simulator (`tempest dev`/`run`) puts the project root on `sys.path`, and every device path (`deploy`/`serve`/`build`) bundles the **whole importable tree** (the project root — the nearest ancestor with a `pyproject.toml` — minus `.venv`, caches, VCS, build output) and puts it on `sys.path` on the device, so `from my_pkg.foo import bar` resolves identically on desktop and device. **Running on your own device — the easy path (no toolchain).** You do **not** need an Android SDK/NDK or the `android-host` source to test on hardware: ```bash uv run tempest deploy # install the bundled host (once) + push the whole project + launch ``` `tempest deploy ` ensures the prebuilt host APK (downloaded from the GitHub release on first use, then cached under `~/.cache/tempestroid`) is installed on the connected device, pushes the project bundle once over a short-lived dev server, launches it, and exits. No SDK/NDK, Gradle, or `android-host` checkout. Repeat runs skip the ~50 MB install (the host is already there) and just push the new bundle; pass `--force-install` to reinstall the host. The app keeps running on the device — but it lives in the host, so it is **not** a standalone artifact you can hand to someone else (use `tempest build` for that). For a **persistent hot-reload loop** instead, `tempest serve` keeps the dev server up: editing + saving any file in the tree hot-reloads on device. ```bash uv run tempest install # download (cached) + adb-install the prebuilt host APK uv run tempest serve # persistent LAN code-push: edit + save → hot reload on device ``` `tempest install` resolves the host APK in order: an explicit `.apk` path/URL → `TEMPESTROID_HOST_APK` → a bundled asset (only in a source checkout staged with `make stage-host`) → a download from the matching GitHub release (`TEMPESTROID_HOST_APK_URL` to override), cached under `~/.cache/tempestroid` so it's fetched only once. The published wheel does **not** embed the ~100 MB APK (it would exceed PyPI's per-file limit), so from a PyPI install the download is the normal path — offline thereafter. With a device connected, `tempest serve` wires `adb reverse` and launches the host in dev mode pointing at the dev server. Use `--no-launch` to serve only. **Shipping a standalone APK — `tempest build apk`.** To produce a self-contained `.apk` you can give to anyone (it runs the app with **no** dev server), use `tempest build apk`: it stamps the APK with the project's **own `applicationId`** so **any number of tempestroid apps install side by side** (never overwriting). Identity + branding come from **`[tool.tempest]`** in `pyproject.toml`: ```toml [tool.tempest] app = "app.py" id = "com.yourcompany.todolist" # applicationId; derived (com.example.) if unset name = "Todo List" # launcher label; icon / splash / splash_bg / version optional ``` The derived `com.example.*` id is a **placeholder, not publishable** (the Play Store rejects it) — set your own `id` before publishing and **keep it forever**. The build runs Gradle but **reuses the prebuilt host natives** (libpython / the JNI shim / stdlib that ship in the package) and bundles the `android-host` project **inside the wheel**, so it needs only a **JDK + the Android SDK** — **no NDK, no CPython toolchain, no `git clone`** (`tempest setup --install` bootstraps the SDK). Output: `dist/.apk` (debug-signed). `tempest build release-apk` is the **release-signed standalone** APK for distributing off the Play Store (signed with your own `--keystore`, else an auto-generated one; output `dist/-release.apk`, verify with `apksigner verify`); `tempest build prd` is the store-ready release **AAB**; `tempest run` = build + install + launch + logs. Without a JDK/SDK, `tempest build` falls back to **`--fast`** (repackage the prebuilt host, no SDK at all) with a warning — that APK keeps the shared `org.tempestroid.host` id (one app per device). `tempest deploy` covers the same toolchain-free path for your own connected device. > **Maintainers:** the host APK (~100 MB — it embeds CPython) is **not** shipped > inside the PyPI wheel (it would exceed PyPI's per-file limit). `make release` > builds it (`make apk`) and **attaches it to the GitHub release** as > `tempest-host-.apk`; `tempest install` / `deploy` download it from > there (cached). `make publish-host` (re)uploads the asset to an existing > release; `make stage-host` copies it into a local checkout > (`tempestroid/_assets/host.apk`, gitignored) so that checkout installs offline. **Transparent output.** `build`/`run`/`deploy`/`install` announce each step (`→ … ✓/✗` with elapsed time). `build`/`run` (the from-source APK paths) run a **preflight** first — checking the host tree, Android SDK, `adb`, and (for `run`) a connected device — so they fail fast with an actionable hint instead of an opaque Gradle stack trace; `tempest doctor` runs that same preflight on its own. Pass `-v`/`--verbose` (on `build`/`run`/`deploy`/`dev`) to echo the raw commands and stream the full adb/Gradle output; without it, a failed command's tail is surfaced and the happy path stays quiet. | Command | Status | Notes | |---|---|---| | `tempest new [name]` | ✅ | Scaffold a fully configured project **in the current dir** (id = folder name); pass a `name` only for a new subdirectory. Writes `pyproject.toml` + `app.py` + `.gitignore`. `--template`/`-t`: `default` (single file), `multi` (state + screens/ + components/ + Navigator), `native` (multi + native-capabilities screen) | | `tempest dev [app]` | ✅ | Simulator + hot reload / hot restart (needs `qt` extra); app from `[tool.tempest]` when omitted; `--device`/`-d` sizes the window to a device preset (e.g. `pixel-7`, `galaxy-s24` — dp, matches Compose); `-v` for tracebacks | | `tempest deploy [app]` | ✅ | Offline push of the whole project to a device (no SDK/NDK): install the bundled host (if needed) + push bundle + launch; `--force-install`, `-v` | | `tempest serve [app]` | ✅ | LAN code-push of the whole project + log relay + hot reload; auto `adb reverse` + launch in dev mode (`--no-launch` to skip) | | `tempest install [src]` | ✅ | Fetch + adb-install the prebuilt host APK (no SDK/NDK); resolves `src`/env/bundled/GitHub-release (cached); `src` = local `.apk`/URL | | `tempest icon ` | ✅ | Generate a square launcher `icon.png` + a centered `splash.png` from one source image (`--out`, `--icon-size`, `--splash-size`, `--splash-scale`). `--adaptive` also writes `ic_launcher_foreground.png` for an Android **adaptive icon** (the launcher applies its mask). Needs Pillow (`pip install tempestroid[icons]`); feed the output to `tempest build --icon/--splash` / `--adaptive-icon` | | `tempest spec` | ✅ | Typed widget/event contract as JSON | | `tempest doctor` | ✅ | Check the Android build/run prerequisites (JDK, android-host, SDK, adb, device); build readiness sets the exit code, a missing device is informational (only `run`/`install` need one) | | `tempest setup` | ✅ | Configure the build environment: diagnose JDK/SDK/NDK/build-tools/toolchain; `--install` auto-installs the Android SDK + NDK (`--sdk-dir`, `-v`) | | `tempest build [apk\|release-apk\|prd]` | ✅ | `apk` (default): a debug, **per-app** APK — its own `applicationId` + launcher label so **any number of tempestroid apps install side by side** (never overwriting). Reuses the prebuilt host natives → needs only **JDK + Android SDK** (no NDK, no CPython toolchain). `release-apk`: a **release-signed standalone** APK to distribute outside the Play Store (`--keystore`, else auto-generated; verify with `apksigner verify`). `prd`: a store-ready release **AAB**. Identity + branding come from **`[tool.tempest]`** (`id`/`name`/`icon`/`splash`/`splash_bg`/`version`/`adaptive_icon`/`icon_bg`) so the command stays short; flags (`--app-id`/`--app-name`/`--icon`/`--adaptive-icon`/`--icon-bg`/…) override. `--adaptive-icon --icon-bg <#rrggbb>` emits a real Android adaptive icon (the launcher masks it). **Heavy native capabilities are opt-in** — `--feature camera\|qr\|push\|video\|maps` (repeatable; also `[tool.tempest] features`) bundles only what the app uses; the lean default ships none, keeping the APK small. Each feature needs a from-source build (SDK/NDK). Advanced: `--fast` (repackage, no SDK, shared id, one app), `--from-source` (stage the CPython toolchain). `-o`, `-v` | | `tempest run [app]` | ✅ | `build` + install on a device + launch `/…MainActivity` + stream logs (needs the toolchain + adb); `--app-id`, `--app-name`, `--app-version`, `--version-code`, `-v` | | `tempest version` | ✅ | Print the framework version (alias of the global `--version`/`-V`) | | `tempest clean` | ✅ | Reset the build caches under `~/.tempestroid` (extracted host natives, bundled-host copy, cloned source) — fixes stale-cache build failures after an upgrade; `--keystore` also drops the cached release keystore | | `tempest lint [path]` | ✅ | `ruff check` on the target (lint only) | | `tempest fix [path]` | ✅ | `ruff check --fix` + `ruff format` in one pass; `--unsafe` also applies ruff's unsafe autofixes | | `tempest format [path]` | ✅ | `ruff format` (writes files) | | `tempest fmt-check [path]` | ✅ | `ruff format --check` (read-only) | | `tempest type [path]` | ✅ | `pyright` on the target (strict type check) | | `tempest test [path]` | ✅ | `pytest` (forwards the optional path filter) | | `tempest uitest ` | ✅ | Run a **Playwright-style native UI test** file (F9 driver): an app module + `async def test_*(page)` functions, driven against the renderer-agnostic IR with **auto-wait** (no `sleep`). `--target`/`-t`: `headless` (default, in-process, no renderer) or `emulator` (REAL Compose render on an Android emulator); `-j N` shards across N isolated emulators with a real screenshot per test; `--isolate-adb` (or `-P `) runs against a **private adb server** so parallel agents never contend on — nor wedge — the shared one. `qt`/`device` are reserved — the same script runs on every target unchanged | | `tempest check [path]` | ✅ | Full quality gate: lint + fmt-check + type + test (stops at the first failure). Each tool is resolved on `PATH` or via `uv run` | ### Running on a device from WSL Connecting a physical Android device to a **WSL 2** session needs USB passthrough plus an `adb` workaround for WSL's mirrored networking: 1. **Windows (admin PowerShell)** — install [usbipd-win](https://github.com/dorssel/usbipd-win) (`winget install usbipd`), then `usbipd bind --busid ` and `usbipd attach --wsl --busid ` (find `` via `usbipd list`). 2. **Device** — enable USB debugging; on MIUI/HyperOS also enable **"Install via USB"** (else `adb install` fails `INSTALL_FAILED_USER_RESTRICTED`). 3. **WSL** — under mirrored networking `adb start-server` hangs; start it in the foreground instead and leave it running: `adb nodaemon server &`, then `adb devices` responds normally. 4. Build + install: `ANDROID_SDK_ROOT=/usr/lib/android-sdk make apk-install` (Gradle wrapper 8.11.1). Full walkthrough + troubleshooting: **[Running on a device (WSL)](docs/guia/dispositivo-wsl.md)**. --- ## Public API Everything below is importable from the top-level `tempestroid` package. ### Style (`tempestroid.style`) Frozen Pydantic value objects, diffed by value. - **`Style`** — the style model (layout, box model, paint, typography, sizing, effects, animation). Notable fields: `opacity`, `shadow`, `align_self`, `letter_spacing`, `line_height`, `max_lines`, `text_overflow`, `aspect_ratio`, `flex_wrap` (flow wrapping for a `Wrap` container), and the phase-E9 typography knobs `text_scale` (a `font_size` multiplier — Qt scales the emitted `font-size`, Compose emits `textScale` for `LocalDensity`) and `font_asset` (a bundle-relative custom font path — Qt `QFontDatabase`, Compose `FontFamily`). - **`Color`** — `Color.from_hex("#101418")`. - **`Edge`** — insets; `Edge.all(24.0)`. - **`Border`** (uniform) / **`SideBorder`** (per-side, e.g. a bottom divider). - **`Corners`** — per-corner radii for `Style.radius` (e.g. top-rounded sheets). - **`Shadow`** — `box-shadow` / elevation (`color` / `blur` / `offset_x` / `offset_y`); Compose maps it to elevation, Qt to a `QGraphicsDropShadowEffect`. - **`Gradient`** + **`GradientStop`** — a linear gradient usable wherever a background `Color` is (QSS `qlineargradient` / Compose `Brush`). - **`Transition`** — implicit animation (`duration_ms` / `curve` / `delay_ms`): on rebuild the renderer tweens changed visual props instead of snapping (Compose maps it to `animate*AsState`; Qt animation is renderer-imperative). - Enums: **`FlexDirection`**, **`FlexWrap`** (`NOWRAP`/`WRAP`/`WRAP_REVERSE`), **`JustifyContent`**, **`AlignItems`**, **`TextAlign`**, **`FontWeight`**, **`FontStyle`**, **`TextDecoration`**, **`TextOverflow`**, **`GradientDirection`**, **`Curve`** (easing — `LINEAR`/`EASE_IN`/`EASE_OUT`/`EASE_IN_OUT` plus `EASE`/`BOUNCE`/`ELASTIC`), **`StackAlign`** (overlay child alignment in a `Stack`). ### Theme, media query + i18n (phase E9) Cross-cutting **context** the `view(app)` reads — not nodes in the tree. Changing any of them swaps an immutable snapshot on the `App` and schedules one coalesced rebuild (no new patch kind). - **`Theme`** (`tempestroid.theme`) — frozen: the active **`ThemeMode`** (`LIGHT`/`DARK`/`SYSTEM`) plus a small color palette (`primary`/`secondary`/ `background`/`surface`/`on_primary`/`on_background`/`error`). `Theme.is_dark(platform_dark_mode=...)` resolves `SYSTEM` against the OS. Swap it with `App.set_theme(theme)`. - **`MediaQueryData`** (`tempestroid.theme`) — frozen viewport/environment snapshot: `width`/`height`/`device_pixel_ratio`/`text_scale_factor`/ `platform_dark_mode`/`orientation`. The renderer keeps it current via `App._update_media(data)` on resize/config-change. - **`Locale`** (`tempestroid.i18n`) — frozen: `language` (BCP-47) + optional `region` + `rtl` (layout direction). Swap it with `App.set_locale(locale)`. When the renderer is told a node is RTL, both `Style` translators mirror the box model's start/end (padding/margin left↔right) and flip `text_align`. - **`translate(key, locale, translations, **kwargs)`** / alias **`t`** (`tempestroid.i18n`) — a dependency-free table lookup with `str.format` interpolation; a missing key/language degrades to the key itself. ### Design system (M3 variants + tokens) The Chakra-style variant API and Material 3 token surface, all re-exported from `tempestroid` (the underlying engine is `tempest_core`). - **`Variant`** — the visual emphasis of a styled component (`SOLID`/`OUTLINE`/ `GHOST`/`SOFT`/`LINK`), Chakra-style. - **`Size`** — the size scale (`XS`/`SM`/`MD`/`LG`/`XL`) driving padding, typography and touch-target sizing. - **`FieldVariant`** — the field-family flavor for inputs (`OUTLINE`/`FILLED`/ `UNDERLINE`). - **`CardVariant`** — the surface flavor for cards/surfaces (`ELEVATED`/`FILLED`/ `OUTLINED`), M3 — elevation resolves to a `Shadow`, no new `Style` field. - **`BadgeVariant`** — the badge/chip/tag flavor (`SOLID`/`SUBTLE`/`OUTLINE`); the `SUBTLE` treatment uses the tonal container pair (WCAG-AA safe for status). - **`AlertVariant`** — the alert/banner flavor (`SUBTLE`/`SOLID`/`LEFT_ACCENT`/ `TOP_ACCENT`); accents use a directional `SideBorder` (RTL-mirrored). - **Status `color_scheme`s** — beyond the brand roles, `success`/`warning`/`info` (+ `error`) are first-class color schemes (M3 tonal families generated from fixed seeds), so `Alert`/`Badge`/`ProgressBar` take `color_scheme="success"` etc. - **`ComponentState`** — the visual state a resolved `Style` targets (`DEFAULT`/`HOVER`/`PRESSED`/`FOCUS`/`DISABLED`) — the M3 state layers. - **`ColorRole`** — a semantic Material 3 color role (`PRIMARY`/`SECONDARY`/ `SURFACE`/`ERROR`/…) resolved against the active `Theme`. - **`TokenSet`** — the resolved bundle of M3 tonal/spacing/shape/typography/ elevation/motion tokens a `Theme` exposes. - **`TokenRef`** — a typed reference to a single token within a `TokenSet`, so a `Style` can point at a theme token instead of a raw literal. > 💡 The low-level resolver functions (`resolve_variant`, `resolve_field_variant`, > `resolve_selection_variant`, `resolve_slider_variant`) and `merge_styles` / > `VALID_COLOR_SCHEMES` are NOT re-exported here — import them from `tempest_core` > when you need the raw `variant → Style` machinery. ### Widgets (`tempestroid.widgets`) The declarative IR — bare-noun widgets. - **`Widget`** (base) — every node carries `key` / `style` plus the phase-E9 accessibility fields `semantics` (**`Semantics`**: `label`/`role`/`hint`, propagated to both renderers and `introspect()`), `focusable`, and `focus_order`. **`Text`**, **`Button`**, **`Column`**, **`Row`**, **`Container`**, **`ScrollView`** (scrollable container), **`SafeArea`** (insets its child past the status/navigation bars + notch; `edges` selects which sides, default all — `SafeAreaEdge` enum). - **`Stack`** — overlay/z-order container: children share one box, layered in declaration order. A child with `position=ABSOLUTE` is anchored by its `top`/`right`/`bottom`/`left` insets; the rest align by `Style.stack_align` (`StackAlign` enum). The framework's overlay primitive (scrim, modal, FAB). - Refined layout (phase E6) — **`Wrap`** (a flow container whose children wrap to the next line when the row fills, driven by `Style.flex_wrap`; Compose `FlowRow`/`FlowColumn`, Qt custom flow layout), **`PageView`** (a paginated horizontal carousel: `children` are pages, the active `page` lives in app state and `on_page_change` (**`PageChangeHandler`**) → **`PageChangeEvent`** updates it; Compose `HorizontalPager`, Qt `QStackedWidget` + prev/next) and **`AspectRatio`** (a single-child box fixing the `ratio` = width / height; Compose `Modifier.aspectRatio`, Qt derives the missing dimension). - Design-system layout (Trilho H3) — **`Surface`** (a theme-resolved M3 surface box, the primitive `Card` builds on; `variant`=`CardVariant`), **`Card`** styled with `CardVariant` (elevated/filled/outlined), **`StyledContainer`** (token-step padding over `Container`), **`HStack`**/**`VStack`** (`Row`/`Column` presets with a token-step `gap`), and **`Spacer`** (a flexible gap whose `grow` pushes siblings apart; Compose `Modifier.weight`, Qt layout stretch). - Platform layout (phase E8) — **`KeyboardAvoidingView`** (a vertical container that insets its `children` when the on-screen keyboard appears; Compose `Modifier.imePadding()` via `WindowInsets.ime`, Qt listens on `QApplication.inputMethod().keyboardRectangleChanged` and behaves like a `Column` on the desktop). Declares no event contract. - **`GestureDetector`** — wraps a `child` and reports pointer gestures via **`TapHandler`** / **`LongPressHandler`** / **`SwipeHandler`** props (`on_tap` / `on_double_tap` / `on_long_press` / `on_swipe`). - Advanced gestures (phase E4) — specialized single-purpose wrappers, each lowering to the same renderer-agnostic contract (Qt via mouse/`QGraphicsView`/ `QDrag`, Compose via `pointerInput`/`SwipeToDismissBox`/`graphicsLayer`): **`PanHandler`** (`on_pan` → **`PanEvent`**: delta + fling velocity), **`ScaleHandler`** (`on_scale` → **`ScaleEvent`**: pinch scale/focus/rotation, plus `on_double_tap`), **`DoubleTapHandler`** (`on_double_tap` → `TapEvent`), **`Draggable`** (`drag_data` + `on_drag` → **`DragEvent`**) paired with **`DragTarget`** (`on_drop` → `DragEvent`) — both via the **`DragHandler`** alias, **`Dismissible`** (swipe-to-delete: `direction` + `on_dismiss` → `DismissEvent`), **`ReorderableList`** (drag to reorder: `children` + `on_reorder` (**`ReorderHandler`**) → **`ReorderEvent`**; the handler mutates a keyed list so the A2 diff emits a `Reorder`) and **`InteractiveViewer`** (pan + zoom: `min_scale`/`max_scale` + `on_interaction` → `ScaleEvent`). - Animation widgets (phase E3) — the interpolation runs in the **core** (`AnimationController` advances a 0..1 value on the app's frame clock, `Tween` interpolates a `float`/`Color`/`Edge`, the `view` folds the result into a `Style`), so both renderers receive only the final per-frame props. **`Animated`** (wraps a `child` rebuilt with interpolated style each frame), **`AnimatedList`** (a `Column`/`Row` whose items fade + expand in on insert and collapse out on remove — `enter_duration_ms`/`exit_duration_ms`/curves), **`Hero`** (a `hero_tag` shared-element transition across `Navigator` screens), **`Shimmer`** (sweeps a gradient highlight over a `child` as a loading placeholder) and **`Skeleton`** (the childless rectangular shimmer). Qt interpolates in the core and drives `QPropertyAnimation`/`QTimer`; Compose can use its native animation engine (a documented conformance divergence). - Navigation hosts — render the `NavStack` into a tree (a route change diffs to an `Update`/`Replace`, no new patch kind): **`Navigator`** (stack host: shows the top `child`, `transition` slide/fade/none + `depth` drive the animation), **`TabView`** (tab strip + active tab `child`), **`TabBar`** (standalone tab strip), **`RouteDrawer`** (main `child` + a slide-over `drawer` panel toggled by `open`). Each emits **`RouteChangeEvent`** via an **`on_change`** (**`RouteChangeHandler`**) prop. In the Qt simulator `Esc` maps to back (`App.pop`); the device back button is the Compose/device half. - **`Component`** (base) — a composite widget that lowers to a primitive tree via `render()`; the reconciler expands it before diffing, so renderers never see it. - Value-bearing inputs: **`Input`** (text — with `secure` password masking + a modern eye / eye-off reveal toggle, regex `pattern`, `keyboard` type, `max_length`, and `leading_icon`/`trailing_icon` shown inside the field), **`TextArea`** (multi-line), **`Checkbox`** (boolean), **`Switch`** (boolean toggle), **`Slider`** (numeric range), **`DatePicker`** (ISO date), **`FilePicker`** (file selection). - Selection + segmented inputs (phase E5): **`Dropdown`** (single-choice select — `options` + `value`, emits **`SelectEvent`** with the option `value` + `index`), **`TimePicker`** (`"HH:MM"` value, emits **`TimeChangeEvent`**), **`RangeSlider`** (dual-handle `low`/`high` over `[min_value, max_value]`, emits **`RangeChangeEvent`**), **`Autocomplete`** (text + filtered suggestions; emits **`TextChangeEvent`** while typing and **`SelectEvent`** on pick), **`PinInput`** (segmented PIN/OTP of `length` cells; emits **`TextChangeEvent`** per edit and a **`SubmitEvent`** once full) and **`MaskedInput`** (input `mask` — `'9'` digit, `'A'` letter, else literal — emits **`TextChangeEvent`**). - Forms (phase E5, `tempestroid.widgets.forms`): **`Form`** (a container of **`FormField`**s, `on_submit` → **`SubmitEvent`**) and **`FormField`** (a labelled wrapper around a `child` input, carrying typed **`Validator`** rules, `name`, `error`, `on_validate` → **`ValidationEvent`**). A **`Validator`** is a `Callable[[Any], str | None]` (an error string or `None`). `Form.validate(values)` runs every field's validators **purely in Python** — the same boundary-validation philosophy as `parse_event` — and returns a **`FormState`** (a frozen `{"errors": dict[str, str], "valid": bool}` that serializes to plain JSON, with no nested models), so a renderer receives an already-validated tree with each field's `error` filled in; the app gates `SubmitEvent` on `FormState.valid`. Both `Form.fields` and `FormField.child` cross the bridge as child nodes (never as props); validators are pure Python and are never serialized. - Presentation widgets: **`Image`** (URL/asset, `fit`), **`Icon`** (named glyph — resolves a built-in [`Icons`](#icons-tempestroidicons) name to a vector glyph, else falls back to the platform set), **`ProgressBar`** (determinate/indeterminate), **`Spinner`** (activity). - Media + graphics widgets (phase E7): **`Canvas`** — a retained-mode drawing surface taking a `commands` list of serializable draw commands (**`MoveTo`** / **`LineTo`** / **`ArcTo`** / **`Close`** / **`FillCmd`** / **`StrokeCmd`** / **`DrawText`** / **`DrawRect`** / **`DrawOval`**, the discriminated **`DrawCommand`** union; colors are `[r, g, b, a]` float lists, so the list lowers to pure JSON and diffs by value); **`VideoPlayer`** (`src` + `autoplay`/`loop`/`controls`/`muted`), **`WebView`** (`url` + `javascript_enabled`), **`Svg`** (`src` + `fit`), **`CameraPreview`** (`facing`), **`QrScanner`** (`on_scan` → **`QrScanEvent`**), **`MapView`** (`latitude`/`longitude`/`zoom` + JSON `markers`), and the effect wrappers **`Blur`** / **`BackdropFilter`** (`radius` + `child`) and **`ClipPath`** (**`ClipShape`** `shape` + `radius` + `child`). `CameraPreview`/`QrScanner`/ `MapView` are device-only — the Qt simulator shows an explicit placeholder. - Virtualized lists (only the visible window is materialized; declare an `item_count` + an `item_builder(index) -> Widget`, never a static child list): **`LazyColumn`** / **`LazyRow`** (vertical/horizontal lazy lists), **`LazyGrid`** (`columns`-wide lazy grid), **`SectionList`** (a list of **`SectionHeader`** sections with sticky headers) and **`RefreshControl`** (standalone pull-to-refresh). The widgets materialize their **initial window** at `build` time — `child_nodes()` builds the items in `window` (when set) or the first `window_size` items (default **`DEFAULT_WINDOW_SIZE`** = 20), each keyed by its absolute index — so the very first mount has content. The app slides the window with `App.slide_window(key, start, end)` (and `App.slide_section_window(key, title, start, end)` for sections) from a scroll handler; the keyed diff turns a slide into a minimal remove/reorder/insert. They emit **`ScrollEvent`** (`on_scroll`), **`RefreshEvent`** (`on_refresh`) and **`EndReachedEvent`** (`on_end_reached`, fired past `end_reached_threshold` — wire it to paginate). The matching handler aliases are **`ScrollHandler`** / **`RefreshHandler`** / **`EndReachedHandler`**. - Overlay + feedback widgets (pushed onto the floating overlay layer via the `App` overlay API, not nested in the screen tree): **`Dialog`** (modal, optional `title` + body `children`, `on_dismiss`), **`BottomSheet`** (`children`, `on_dismiss`), **`Toast`** (transient `message` + `duration_s`, auto-dismisses), **`Tooltip`** (`message` + optional `child`), **`Menu`** (selectable **`MenuItem`** `items`, optional `anchor` key, `on_select`), **`Popover`** (anchored `child`, `on_dismiss`) and **`ActionSheet`** (titled `items`, `on_select`). `MenuItem` is a frozen value model (`label` / `value` / `icon`) that crosses the bridge as plain JSON. The matching handler aliases are **`DismissHandler`** and **`MenuSelectHandler`**. - Enums: **`KeyboardType`** (text/number/email/phone/url/password), **`ImageFit`** (contain/cover/fill/none), **`ClipShape`** (circle/rounded_rect/oval). - **`EventHandler`** — the typed handler-prop wrapper used by every handler field (`on_click`, `on_change`, `on_select`); sync or `async`, zero- or one-argument. ### Icons (`tempestroid.icons`) A curated, DIY (dependency-free) set of common line icons — Lucide-style vector glyphs both renderers draw identically by stroking one 24×24 SVG path. Pass a name to `Icon(name=…)` or to an input's `leading_icon`/`trailing_icon`. - **`Icons`** — a `StrEnum` of the curated names (`Icons.EYE`, `Icons.LOCK`, `Icons.SEARCH`, … `Icons.EYE == "eye"`), so you get autocomplete and may also pass the raw string. - **`ICON_PATHS`** — `dict[str, str]` mapping each name to its SVG path `d` data. - **`icon_path(name)`** — resolve an `Icons` member or raw string (curated or custom) to its `d` string, or `None` when unknown (renderers fall back to the platform set / the raw name). - **`icon_names()`** — the sorted list of available names (curated + custom). - **`svg_to_path(source)`** — convert an SVG image (a file path or raw markup) to one normalized `d` string, flattening `path`/`circle`/`line`/`rect`/`polyline`/ `polygon` shapes — so a project SVG becomes a tempestroid icon. - **`register_icon(name, source=…)` / `register_icon(name, path=…)`** — register a custom icon (from an SVG file/markup, or a ready `d`) under a name, so `Icon(name=…)`, an input's `leading_icon`/`trailing_icon` and `icon_path` all resolve it like a built-in. Input icon slots are typed **`Icons | str | None`**: pass an `Icons` member for autocomplete on the curated set, or any string for a registered custom / platform icon. ### Components (`tempestroid.components`) Higher-level, reusable building blocks — each a **`Component`** that lowers to primitive widgets, so they work in both renderers (Qt and Compose) with zero renderer changes and are fully device-ready. Every component takes an optional `style` that is merged over its default via **`merge_style`**. - **`AppBar`** — top bar: optional `leading` widget, `title`, trailing `actions`. - **`CollapsingAppBar`** — a sliver-style header that shrinks as the content scrolls: the app feeds the current `scroll_offset` (from a list's `on_scroll`) and the component eases its height from `expanded_height` down to `collapsed_height`, diffing the derived height as an ordinary prop (no new IR). - **`Header`** / **`Footer`** — page header band (title + optional subtitle) and a centered bottom bar holding arbitrary `children`. - **`Table`** — a static data table built from typed **`TableRow`** / **`TableCell`** values plus optional `headers`; **`DataTable`** — a string-matrix convenience (`columns` + `rows`, optional `sortable` header glyph). Both lower to a `Column` of `Row`s of cells, so they render in both renderers unchanged. - **`Sidebar`** — fixed-`width` lateral column of `children`. - **`Scaffold`** — page frame stacking `app_bar`, a growing `body` and an optional `bottom_bar` (set `scroll=True` to wrap the body in a `ScrollView`). - **`NavBar`** — selectable navigation/tab bar: `items` labels, an `active` index and an `on_select(index)` callback (generalises the `tabs` example). The active destination paints the `color_scheme` accent pill (Trilho H5). - **`Tabs`** (Trilho H5) — a styled M3 tab strip: `tabs` labels + `active` index + `on_select`; the active tab gets the `color_scheme` accent + an underline indicator. The H5 nav skins (AppBar/Footer/Sidebar/Drawer/SearchBar/Breadcrumb) resolve their surfaces/fields from the theme — no hand-set colors. - **`Burger`** / **`Drawer`** — a hamburger menu button (`on_click`) and a controlled lateral panel (`open` lives in app state; toggle it from the burger). - **`Calendar`** — month grid of selectable day cells: `month` (`"YYYY-MM"`), `selected` (`"YYYY-MM-DD"`) and `on_select(iso_date)`. - **`Clock`** — digital clock rendering a preformatted `time` string (the app drives the tick from state, as in `stopwatch`). - **`Card`** — elevated surface (shadow + radius) grouping `children`. - **`ListTile`** — list row: `leading` / `trailing` widgets around a `title` plus an optional `subtitle`. - **`Avatar`** — round badge of short `initials`; **`Divider`** — thin rule. - **`SegmentedControl`** / **`RadioGroup`** — single-choice pickers (`options`, `selected`, `on_select(index)`). - **`Chip`** — small rounded label, selectable when given an `on_click`. - **`Rating`** — a row of `max_stars` stars; `on_rate(value)` makes it tappable. - **`Stepper`** — numeric `-`/`+` around a value with optional `min_value` / `max_value` clamping; `on_change(value)`. - **`SearchBar`** — controlled text `Input` with an optional clear button. - **Brazilian form inputs** — labelled fields that lower to `Input` / `MaskedInput`, each calling `on_change(value)` with the new string: **`EmailInput`** (e-mail keyboard + `mail` icon), **`PasswordInput`** (secure, `lock` icon), **`PhoneInput`** (mask `(99) 99999-9999`), **`CPFInput`** (mask `999.999.999-99`), **`CNPJInput`** (mask `99.999.999/9999-99`) and the grouped **`AddressInput`** (CEP + street/number/complement/neighborhood/city/UF, `on_change(field, value)`). Pair them with the `validators` below in a `FormField`. - **Media pickers** — **`ImagePicker`** (`FilePicker` + inline `Image` preview, `on_pick(uri)`), **`DocumentPicker`** (`FilePicker` for documents) and **`ImagePicture`** (circular profile-photo picker — `ClipPath`-clipped `Image` with a `user`-icon placeholder, `on_pick(uri)`). - **`Accordion`** — controlled expand/collapse section (`open` in state, `on_toggle`). - **`Banner`** — inline status bar (`tone`: info/success/warning/error) with an optional `action`; **`Badge`** — small status pill; **`EmptyState`** — centered glyph + title + subtitle + action placeholder. - Data-display & feedback (Trilho H4, status-themed) — **`Alert`** (glyph + title + body + optional dismiss; `variant`=`AlertVariant`, `color_scheme` status), **`Stat`** (label + value + up/down-tinted `delta`), **`ProgressStepper`** (a wizard step indicator: `steps` + `current`, accent from `color_scheme`), and **`Tag`** (a closed, non-selectable `Chip` preset). `Badge`/`Chip`/`ProgressBar`/ `Spinner` take `variant`/`color_scheme` too. - Research / data-science (Trilho H6, ties to the `ort-vision-sdk`) — **`MetricCard`** / **`StatCard`** (label + value + tinted delta over a themed `Card`), **`ConfidenceBadge`** (a confidence pill colored by `confidence_scheme` — success/warning/error, WCAG-AA via the tonal container), **`LineChart`** / **`BarChart`** (lower to the `Canvas`: data → themed draw commands; data shape is **`ChartSeries`**), **`DetectionOverlay`** (an image + labeled bounding boxes from a list of **`DetectionBox`** — normalized `[0,1]` xyxy, conf-colored), and **`ResultView`** (an `ImagePicker` → result flow). `DataTable` gains sort/paginate; `Calendar`/`Clock` are theme-styled. `confidence_scheme(conf)` is the shared success/warning/error threshold picker. - **`Breadcrumb`** — path trail (`items` + `separator`, optional `on_select`). - **`Grid`** — equal-width `columns` grid of `children`. ### Validators (`tempestroid.validators`) Pure, dependency-free field validators matching the `Form` validator shape `Callable[[Any], str | None]` — they return a PT-BR error message when invalid or `None` when valid, after stripping mask characters. Plug them into a `FormField` (e.g. `FormField(validators=[validate_cpf], child=CPFInput(...))`): - **`validate_cpf`** — 11 digits + the two mod-11 check digits (rejects all-same-digit). - **`validate_cnpj`** — 14 digits + the two check digits with the standard CNPJ weights (rejects all-same-digit). - **`validate_email`** — a pragmatic email regex; **`EMAIL_PATTERN`** is the reusable pattern string (also used as `EmailInput`'s `Input.pattern`). - **`validate_phone`** — Brazilian phone: 10 (landline) or 11 (mobile) digits. ### Events (`tempestroid.widgets`) — typed boundary contract - **`Event`** (base), **`TapEvent`**, **`TextChangeEvent`** (carries `valid` against the input's `pattern`), **`ToggleEvent`**, **`SlideEvent`**, **`DateChangeEvent`**, **`FileSelectEvent`**. - Gesture events (from `GestureDetector`): **`LongPressEvent`** (optional `x`/`y`), **`SwipeEvent`** (`direction` + `dx`/`dy`) with the **`SwipeDirection`** enum (left/right/up/down). - Advanced-gesture events (phase E4): **`PanEvent`** (`dx`/`dy` delta + `vx`/`vy` fling velocity), **`ScaleEvent`** (`scale` + `focus_x`/`focus_y` focal point + `rotation`), **`DragEvent`** (`data` opaque label + optional `x`/`y` drop position) and **`ReorderEvent`** (`from_index` → `to_index`). `Dismissible` reuses **`DismissEvent`**. - **`RouteChangeEvent`** (`name` + typed `params`) — emitted when navigation settles on a new route. - Virtualized-list events: **`ScrollEvent`** (`offset` + `direction`), **`RefreshEvent`** (pull-to-refresh) and **`EndReachedEvent`** (threshold reached) — emitted by `LazyColumn` / `LazyRow` / `LazyGrid` / `SectionList` / `RefreshControl`. - Overlay events: **`DismissEvent`** (optional `overlay_id`) — an overlay dismissed by a host-owned gesture (`Dialog` / `BottomSheet` / `Popover`); and **`MenuSelectEvent`** (`value` + `label`) — a `Menu` / `ActionSheet` selection. - Input + form events (phase E5): **`SelectEvent`** (`value` + 0-based `index`), **`TimeChangeEvent`** (`"HH:MM"` `value`), **`RangeChangeEvent`** (`low` + `high` floats), **`SubmitEvent`** (flat `values: dict[str, str]`) and **`ValidationEvent`** (`field` + `value` + optional `error`). The matching handler aliases are **`SelectHandler`** / **`TimeChangeHandler`** / **`RangeChangeHandler`** / **`SubmitHandler`** / **`ValidationHandler`**. - Layout event (phase E6): **`PageChangeEvent`** (`page` + `previous`) — emitted by a `PageView` when the active page changes (handler alias **`PageChangeHandler`**). - Media event (phase E7): **`QrScanEvent`** (`data` + `format`) — emitted by a `QrScanner` for each decoded QR/barcode (handler prop `on_scan`). - Platform/system events (phase E8) — streamed from the host over reserved event tokens (no widget handler): **`LifecycleEvent`** (`state`, the **`AppState`** enum foreground/background/inactive), **`SensorEvent`** (`sensor` — the **`SensorType`** enum — + `values` + `timestamp_ms`), **`ConnectivityEvent`** (`state`, the **`ConnectivityState`** enum connected/disconnected/wifi/mobile) and **`DeepLinkEvent`** (`url` + parsed `params`). - Context events (phase E9) — streamed from the host over reserved tokens (no widget handler): **`ThemeChangeEvent`** (`mode`, the **`ThemeMode`** enum) over `THEME_TOKEN` → `App.set_theme`, and **`LocaleChangeEvent`** (`language` + optional `region` + `rtl`) over `LOCALE_TOKEN` → `App.set_locale`. - **`parse_event(event_type, raw)`** — boundary gate: validates a raw payload into a typed event or raises **`EventValidationError`** with structured field errors. This is the Python↔Kotlin contract for the device bridge. The bridge passes the validated event to handlers that accept a positional argument. ### Core — IR + reconciler (`tempestroid.core`) - **`Node`**, **`Path`** — the lowered IR. `Path` is `tuple[int | str, ...]`: a child-index path, except the reserved leading `"overlay"` token that addresses the overlay layer (`("overlay", i, …)`). - **`Scene`** — a full UI document: a `root` node plus an ascending z-order `overlays` layer (each overlay node keyed by its stable overlay id). - Patches: **`Insert`**, **`Remove`**, **`Update`**, **`Reorder`**, **`Replace`**, and the **`Patch`** union. Overlays reuse these — no new kind. - **`build(widget) -> Node`**, **`diff(old, new) -> list[Patch]`**, **`build_scene(widget, overlays) -> Scene`** (overlays as `(id, widget, barrier)` tuples), **`diff_scene(old, new) -> list[Patch]`** (root diffed as before; overlays diffed keyed under the `("overlay", …)` prefix). - **`App[S]`** — renderer-agnostic state container: owns state, builds via `view(app)` into a `Scene` (root tree + overlay layer), diffs, hands patches to an `apply_patches` callback. `App.start()` returns the `Scene` and `App.current_tree` is the live `Scene`. It also owns a `NavStack` (`app.nav`) and exposes navigation helpers: **`push(route)`** / **`pop() -> bool`** / **`replace(route)`** / **`reset(stack)`** — each mutates the stack and schedules the same coalesced rebuild (no new patch kind). `pop()` returns `False` at the root. - Overlay API (imperative, returns a stable overlay id for `dismiss`): **`show_dialog(widget, *, barrier=True)`**, **`show_sheet(widget, *, barrier=True)`**, **`show_menu(widget, *, anchor=None, barrier=False)`**, **`toast(widget, *, duration_s=2.5)`** (auto-dismisses via `loop.call_later`) and **`dismiss(overlay_id)`**. Each schedules the same coalesced rebuild; **`OverlayEntry`** is the internal overlay slot. ### Animation (`tempestroid.animation`) The interpolation runs in the **core**, so both renderers only ever see final per-frame props (the divergence — Qt interpolates in the core, Compose may drive its native engine — is pinned by the conformance suite). - **`AnimationController`** — drives a normalized `value` (0.0..1.0) on the app's frame clock: `forward()` ramps toward 1.0, `reverse()` toward 0.0, `stop()` halts and unregisters. Constructed with `duration_s` + `curve`, or a `Spring` for physics-based motion. Injectable `time_source` for deterministic tests. - **`Tween[T]`** — a frozen linear interpolator (`begin` → `end`); `at(t)` interpolates `float`, `Color` (per channel), `Edge` (per side) or a numeric `tuple`. The `view` reads `at(controller.value)` to feed an interpolated `Style`. - **`Spring`** — frozen spring parameters (`stiffness`/`damping`/`mass`) for an `AnimationController` instead of a fixed duration. - `App` owns the frame clock: **`register_animation(ctrl)`** starts a coalesced `loop.call_later(1/60)` tick that advances every active controller and requests a rebuild; the clock stops re-arming once no controller remains. The reserved `__frame__` device token routes to `App._tick_from_device()` (one advance per host frame). `App.__init__` accepts an optional `time_source` kwarg. ### Navigation (`tempestroid`) - **`Route`** — a frozen navigation destination: `name` + typed `params`. - **`NavStack`** — the mutable route stack (defaults to `[Route(name="/")]`); `top` is the visible screen and `can_pop` is `True` past the root. The stack is not a new IR node — `view(app)` reads `app.nav.top` to build the current screen, so changing routes diffs through the existing reconciler. - **`routes_from_path(path) -> list[Route]`** — resolve a deep-link path into an initial stack (`"/a/b"` → `["/", "/a", "/a/b"]`, so back pops through the intermediate screens). The entry point hands the result to `App.reset` so a deep link opens directly on the linked screen with its back stack built. ### Introspection (`tempestroid.core`) - **`introspect()`** — full JSON contract `{"widgets": {...}, "events": {...}}` (powers `tempest spec`). - **`widget_catalog()`**, **`event_catalog()`**. ### Renderer (`tempestroid.renderers.qt`, needs `qt` extra) - **`run_qt(state, view, *, title, size)`** — run an app in the Qt simulator. - **`run_dev(app_path)`** — the `tempest dev` cockpit. ### UI test driver (`tempestroid.testing`) A Playwright-style driver that automates an app against the **renderer-agnostic IR** (not pixels): locate nodes, inject typed events, assert with **auto-wait** (no `sleep`). Because every backend speaks the same IR + typed events, the **same script runs on every target**: the **headless** backend drives the IR/state/event core in-process, and the **emulator** backend drives a REAL app through the **Compose** renderer on an Android emulator. Drive it with `tempest uitest` — `--target emulator -j N` shards across N isolated emulators and saves a real on-device screenshot per test. Add `--isolate-adb` so the run uses a **private adb server** (`ANDROID_ADB_SERVER_PORT`): the emulator *instances* are already isolated by port/userdata, and this isolates the last shared resource so multiple agents can drive emulators in parallel without contending on — or wedging — one another's adb server. - **`Page`** — the top-level driver. Locators: `get_by_key` / `get_by_text` / `get_by_role` / `get_by_semantics` / `get_by_prop`. Actions: `tap` / `fill` / `back`. Auto-waiting assertions: `expect_text` / `expect_visible` / `expect_count`. `snapshot()` returns a JSON-able tree dump. - **`Locator`** — a lazy node query that resolves against the live scene at action/assert time (`first` / `all()` / `count()` / `resolve()`); raises `LocatorError` when zero or (for `resolve`) many nodes match. - **`TestBackend`** — the `Protocol` a renderer target implements (`mount` / `scene` / `dispatch` / `settle` / `patches`). - **`HeadlessBackend`** — the no-renderer reference backend (wraps an `App`). - **`EmulatorBackend(app_path, serial)`** — drives a real Compose render on an emulator over the dev-server **harness** bridge (mount/patch mirrored back into a host-side `Scene`; events fed through `DeviceApp.handle_event` — no C/JNI change). `screenshot(path)` captures REAL Compose pixels via `adb screencap`. - **`EmulatorPool`** — allocate / recycle N isolated emulators (reusing running ones, capped by CPU/RAM). `max_parallel_emulators()` / `running_emulators()`. - **`run_test_file(path, target="headless")`** — load + run a UI test file's `test_*` functions, returning a `TestReport` of `TestOutcome`s. - **`run_test_files_emulator(paths, serials)`** — shard files across emulator serials and run each shard in parallel. - **`deserialize_scene` / `apply_patches`** (in `tempestroid.testing.mirror`) — rebuild + patch the host-side scene mirror from the wire JSON. ```python from tempestroid.testing import HeadlessBackend, Page page = Page(HeadlessBackend(make_state, view)) await page.mount() await page.tap(page.get_by_key("inc")) await page.expect_text("Count: 1") # auto-waits until the tree settles ``` ### Device presets (`tempestroid.devices`) Logical (`dp`) viewport sizes for common Android phones, so the simulator window can match a real device instead of a generic guess. - **`Device`** — `Enum` of presets (Pixel, Galaxy S/A, Redmi / Redmi Note, Poco, Xiaomi, Moto, OnePlus). Each member carries `width` / `height` (in `dp`) and a human `label`; `.size` returns the `(width, height)` tuple. - **`DEFAULT_DEVICE`** — the simulator default (`Device.REDMI_NOTE_12`, 393×873 dp). - **`resolve_device(name)`** — resolve a forgiving name (`"pixel-7"`, `"PIXEL_7"`, `"Google Pixel 7"`) to a `Device`, or `None`. Backs `tempest dev --device`. ```python from tempestroid import Device, run_qt run_qt(state, view, size=Device.GALAXY_S23.size) ``` ### Compose + bridge — device side (phases B3/B4) The Python half is device-independent and tested without a phone; the JNI transport (B3) and the Kotlin Compose renderer (B4) are implemented in `android-host/` and verified on a real arm64 device. - **`to_compose(style)`** (`tempestroid.renderers.compose`) — serializable `Style → Compose` spec; the second `Style` translator (pairs with `Style → Qt`). - **`serialize_node` / `serialize_patch`** — lower the IR/patches to JSON-able dicts (handlers → path tokens, style → Compose spec). - **`MountMessage` / `PatchMessage` / `EventMessage`** — the wire protocol across the bridge: `mount` carries the full serialized tree (plus an `overlays` list of serialized overlay nodes), `patch` an incremental patch list (overlay patches ride under the `("overlay", …)` path), `event` a device→Python callback addressed by handler token. `mount`/`patch` also carry **`can_pop`** (the live `app.nav.can_pop`), so the host can gate its system-back handler without a round-trip, and **`has_animations`** (`app.has_animations`), so the host can start/stop its `withFrameNanos` frame loop without a round-trip. - **`BACK_TOKEN`** (`"__back__"`) — the reserved event token the host sends on a system back action (e.g. the Android back gesture). The bridge routes it straight to `App.pop` (no widget handler, no new JNI entry) — it pops a screen, or is a no-op at the root where the host's default close-the-app action runs. - **`FRAME_TOKEN`** (`"__frame__"`) — the reserved event token the host sends once per frame from its `withFrameNanos` loop while `has_animations` is `True`. The bridge routes it straight to `App._tick_from_device`, which advances every active `AnimationController` one frame and re-renders (no widget handler, no new JNI entry). The Qt simulator drives its own clock and never emits this token. - **`DISMISS_TOKEN_PREFIX`** (`"__dismiss__"`) — the reserved event-token prefix the host sends when an overlay is dismissed by a host-owned gesture (scrim tap, swipe-down): `"__dismiss__:"`. The bridge strips the prefix and routes the id to `App.dismiss` (no widget handler, no new JNI entry). - **`SENSOR_TOKEN_PREFIX`** (`"__sensor__"`) / **`LIFECYCLE_TOKEN`** (`"__lifecycle__"`) / **`CONNECTIVITY_TOKEN_PREFIX`** (`"__connectivity__"`) (phase E8) — reserved tokens carrying *continuous* host streams over the same event channel: `"__sensor__:"` → `dispatch_sensor_event`, `"__lifecycle__"` → `dispatch_lifecycle_event`, `"__connectivity__:"` → `dispatch_connectivity_event`. Each rides the existing transport (no new JNI/C entry) and is routed in **both** `bridge/jni.py` and `devserver/client.py` (so code-push gets them too). - **`THEME_TOKEN`** (`"__theme__"`) / **`LOCALE_TOKEN`** (`"__locale__"`) (phase E9) — reserved bare tokens carrying a host-driven context change over the same event channel: `"__theme__"` (payload `{"mode": "dark"}`, validated as a `ThemeChangeEvent`) → `App.set_theme`, and `"__locale__"` (payload `{"language": "ar", "rtl": true}`, validated as a `LocaleChangeEvent`) → `App.set_locale`. Both ride the existing transport (no new JNI/C entry). - **`DeviceApp`** + **`Bridge`** / **`LoopbackBridge`** — wire an `App` to a device transport; the device-side analogue of `run_qt`. Events come back by handler token, are validated by `parse_event`, and trigger coalesced patches. - **`JniBridge`** + **`run_device`** — the real on-device transport (phase B3): `JniBridge` ships messages to Kotlin via the native `_tempest_host` module; `run_device(state, view)` boots a `DeviceApp` on a fresh asyncio loop and marshals incoming events back onto it. Imports cleanly off-device (the native module is loaded lazily), so the framework still develops/tests on the desktop. ### Dev server — LAN code-push (phase B5) The Expo-style on-device inner loop: edit on the dev machine, hot-restart on the phone without rebuilding the APK (`tempest serve `). - **`DevServer`** — serves the app source (`/version`, `/app`) and relays device logs (`/log`) over HTTP. - **`run_dev_client`** — the device poll loop: fetch on change → re-exec source → hot-restart the `DeviceApp` (transport/fetch injected, so it's desktop-testable). - **`serve_device(url)`** — device entry point wiring the real `JniBridge` + the native sink + an `urllib` fetch into `run_dev_client`. - **`render_qr(url)`** — ASCII QR for pairing (falls back to the plain URL). ### Native capabilities (phase B6+) Device-native features driven from Python as `{"kind": "native"}` commands the Kotlin host routes to capability modules. Two shapes share the one JNI channel: **fire-and-forget** (one-way) and **request/response** (`await` a result; the host replies over the event channel under a reserved token — no extra native entry point). A failed request/response call raises `NativeError` carrying a machine-readable `code` (`permission_denied` / `cancelled` / `not_found` / `unavailable` / `io_error`). Permissions (location, camera, bluetooth) are requested on demand by the host. Fire-and-forget: - **`notify(title, body="")`** — post a system notification. - **`share(text="", url="", title="")`** — open the system share sheet. - **`share_to_whatsapp(text="", phone="")`** — share to WhatsApp (`wa.me`, optional E.164 number). - **`open_url(url)`** — open a URL with the default handler. - **`set_text(text)`** — write to the clipboard. Request/response (`async`, awaited from a handler): - **`await get_position(high_accuracy=True) -> Position`** — a single location fix (`latitude`/`longitude`/`accuracy`/`altitude`). - **`await take_photo(*, camera=CameraFacing.BACK, max_width=None, max_height=None) -> Photo`** — capture a photo (`path`/`width`/`height`); the host downscales to the size caps. - **`await record_video(*, camera=CameraFacing.BACK, max_duration_s=None, quality=VideoQuality.HIGH) -> Video`** — record a clip (`path`/`duration_ms`/`width`/`height`). - **`await record_audio(*, max_duration_s=None) -> AudioClip`** — record from the microphone (`path`/`duration_ms`). - **`await play_sound(src, *, volume=1.0)` / `stop_sound()`** — play/stop audio on the device speaker (`src` = local path or URL). - **`await read_file(name)` / `write_file(name, content)` / `delete_file(name)` / `list_files() -> list[str]`** — app-private device storage. - **`await get_text() -> str`** — read the clipboard. - **`await scan(timeout=8.0) -> list[BluetoothDevice]`** — discover nearby Bluetooth devices (`address`/`name`/`rssi`). ```python from tempestroid import App, Button, Text, Widget from tempestroid.native import get_position, share, NativeError async def _locate(app: App[State]) -> None: try: pos = await get_position() app.set_state(lambda s: setattr(s, "label", f"{pos.latitude}, {pos.longitude}")) except NativeError as exc: app.set_state(lambda s: setattr(s, "label", f"erro: {exc.code}")) ``` The `native_command` / `native_request` envelope + the host module router is the extension point for further capabilities (sensors, contacts, …). The Python side (envelopes, pending-future resolution, typed results) is fully unit-tested off-device; the Kotlin capability modules need an Android device to validate. **`on_device()`** reports whether the native host is present, so a module can emulate (prefs/SQLite) or stub (`device_only`) on the desktop. #### Platform + system (phase E8) A wider platform surface, same two shapes (plus the sensor/lifecycle/connectivity *streams* over the reserved tokens above). Capabilities with no desktop hardware stub on the Qt simulator with an explicit `device_only` `NativeError`; the ones that can be emulated run for real off-device. - **Haptics** (fire-and-forget): **`vibrate(duration_ms=50)`**, **`impact(style=ImpactStyle.MEDIUM)`** (the **`ImpactStyle`** enum light/medium/heavy). - **System** (set = fire-and-forget, get = `async`): **`set_status_bar(*, hidden=None, color=None, style=None)`** (**`StatusBarStyle`** enum), **`await get_brightness() -> float`**, **`set_brightness(value)`**, **`keep_awake(enabled)`**, **`set_orientation(orientation)`** (the **`Orientation`** enum portrait/landscape/auto). - **Sensors** (stream): **`start_sensor(sensor, callback, rate_ms=100) -> Callable[[], None]`** registers a `SensorEvent` callback (the **`SensorCallback`** alias; returns a `stop` handle) and **`stop_sensor(sensor)`**. - **Lifecycle** (stream): **`on_app_state_change(callback) -> Callable[[], None]`** registers a `LifecycleEvent` callback (the **`LifecycleCallback`** alias; returns an `unregister`); driven for real on the Qt simulator by `QApplication.applicationStateChanged`. - **Connectivity**: **`await get_connectivity() -> ConnectivityState`** and the stream **`on_connectivity_change(callback) -> Callable[[], None]`** (the **`ConnectivityCallback`** alias). - **Permissions** (`async`): **`await request_permission(permission)`** / **`await check_permission(permission)`** → **`PermissionResult`** (`permission` + **`PermissionStatus`** granted/denied/permanently_denied; the Qt simulator returns granted — the desktop has every capability). - **Biometrics** (`async`): **`await authenticate(reason="") -> BiometricResult`** (`authenticated` + optional `error`); Qt raises `device_only`. - **Secure storage**: **`await get_secret(key)`** / **`set_secret(key, value)`** / **`delete_secret(key)`** (Android Keystore-backed; Qt raises `device_only` — no silent plaintext fallback). - **Preferences** (real on the desktop — a JSON file under `~/.tempestroid/prefs.json`): **`await get_pref(key, default=None)`** / **`set_pref(key, value)`** / **`delete_pref(key)`** / **`await get_all_prefs() -> dict[str, Any]`**. - **Database** (real on the desktop — `sqlite3` under `~/.tempestroid/app.db`): **`await execute(sql, params=()) -> QueryResult`** (`columns` + `rows`) / **`await execute_many(sql, params_list)`**. - **Push** (FCM): **`await register_push() -> PushToken`** (Qt raises `device_only`; the device path needs `google-services.json` — drop it into `android-host/app/` and the build enables FCM) and **`schedule_notification(title, body, delay_s)`** (local notification). - **Background tasks** (WorkManager): **`schedule_task(name, *, interval_s=None)`** (one-shot when `interval_s` is `None`, else periodic ≥15 min) / **`cancel_task(name)`**, with **`on_background_task(name, callback)`** to run a handler when the task fires — the worker re-enters Python (the live interpreter if the app is up, else a fresh short-lived one). Example: [`examples/platform/app.py`](examples/platform/app.py) exercises haptics (with the Qt fallback), preferences (real JSON store on the desktop), the lifecycle stream and a `KeyboardAvoidingView`-wrapped input. The Python half is fully unit-tested off-device (envelopes, typed results, stream-callback registries, the real prefs/SQLite emulation via `tmp_path`); biometrics, FCM, WorkManager and real sensors are hardware-gated and validated on a device. --- ## Project layout ```text tempestroid/ ├── style.py # Style + value objects (Color/Edge/Border/Corners/Shadow/Gradient/Transition) + enums (frozen Pydantic) ├── widgets/ # Widget base + Component base + layout/inputs/media/indicators widgets + events.py ├── components/ # composite components (AppBar/Header/Footer/Sidebar/Scaffold/NavBar) ├── core/ # ir.py, reconciler.py, state.py, introspection.py ├── renderers/qt/ # renderer, Style→Qt, run_qt, simulator, dev_loop ├── renderers/compose/ # Style→Compose translator (device renderer, Python side) ├── bridge/ # IR/patch serialization, handler registry, DeviceApp └── cli/ # tempest entry point + app_loader + watcher # Trilho B (Android), outside the Python package: docs/research/ # web research + executable B0–B6 runbook toolchain/ # fetch CPython 3.14 + cibuildwheel native wheels android-host/ # Gradle/Kotlin host embedding official CPython via JNI ``` --- ## Status Track A (pure desktop CPython) is **complete: A0–A6**. | Phase | Scope | Status | |---|---|---| | A0 | Foundation: package, tooling, `tempest --help` | ✅ | | A1 | Style model + typed widget primitives | ✅ | | A2 | Reconciler: `build → diff → patch` | ✅ | | A3 | Qt renderer: patches → `QWidget`s, `Style → Qt` | ✅ | | A4 | Async event loop: asyncio ⨉ Qt (`qasync`) | ✅ | | A5 | `tempest dev`: watcher, hot restart, command loop | ✅ | | A6 | Typed event contract + introspection | ✅ | | B0–B6 | Android runtime: CPython 3.14 arm64, native wheels, Kotlin host, JNI bridge, Compose renderer, LAN code-push, native capabilities | ✅ | | C | Polish: `new`/`build`/`run` + stateful hot reload | ✅ | | D | Conformance golden snapshots (Qt vs Compose) | ✅ | | E0 | Navigation + routes (push/pop, tabs, drawer, back button, deep link) | ✅ | | E1 | Virtualized lists + scroll (lazy, sticky section, pull-to-refresh, infinite) | ✅ | | E2 | Overlays + feedback (dialog, bottom sheet, toast, tooltip, menu/popover, action sheet) | ✅ | | E3 | Animation framework (`AnimationController`/`Tween`/`Spring`, `Animated`/`AnimatedList`/`Hero`/`Shimmer`/`Skeleton`) | ✅ | | E4 | Advanced gestures (`PanHandler`/`ScaleHandler`/`Draggable`/`DragTarget`/`Dismissible`/`ReorderableList`/`InteractiveViewer`) | ✅ | | E5 | Inputs + forms (`Dropdown`/`TimePicker`/`RangeSlider`/`Autocomplete`/`PinInput`/`MaskedInput`, `Form`/`FormField`/`Validator`/`FormState`) | ✅ | | E6 | Refined layout (`flex_wrap`/`Wrap`/`PageView`/`AspectRatio`/`CollapsingAppBar`/`Table`/`DataTable`, `PageChangeEvent`) | ✅ | | E7 | Media + graphics (`Canvas`/`Svg`/`VideoPlayer`/`WebView`/`Blur`/`ClipPath`/`CameraPreview`/`QrScanner`/`MapView`) | ✅ | | E8 | Platform + system (haptics/sensors/system/lifecycle/permissions/biometrics/secure_storage/prefs/database/connectivity/push/background, `KeyboardAvoidingView`, `LifecycleEvent`/`SensorEvent`/`ConnectivityEvent`/`DeepLinkEvent`) | ✅ | | E9 | Cross-cutting: theme/dark mode (`Theme`/`ThemeMode`) + `MediaQueryData` + i18n/RTL (`Locale`/`translate`) + accessibility (`Semantics`/`focusable`) + custom fonts (`text_scale`/`font_asset`), `ThemeChangeEvent`/`LocaleChangeEvent` over `THEME_TOKEN`/`LOCALE_TOKEN` | ✅ | --- ## Develop ```bash uv run ruff check . uv run pyright # strict mode uv run pytest ``` Conventions: double quotes everywhere, every parameter/return/annotation typed, Google-style English docstrings, absolute imports re-exported from each `__init__.py`. See [`CLAUDE.md`](CLAUDE.md) for the full set. --- # File: docs/index.md — https://mauriciobenjamin700.github.io/tempestroid/ # Tempestroid Construa **apps Android nativos** em **Python tipado**. Você escreve uma única árvore de widgets declarativa e totalmente tipada (uma IR Pydantic). Um **reconciliador agnóstico de renderizador** faz o *diff* dessa árvore em *patches*. Dois renderizadores-folha aplicam esses *patches*: **Qt** para o simulador de desktop e **Jetpack Compose** para o dispositivo. O runtime é **async-first**, com um *dev loop* estilo Expo (hot reload no simulador e *code-push* por LAN no dispositivo). !!! note "É um framework, não um serviço web" Aqui não há FastAPI, SQLAlchemy, Redis nem camadas HTTP. O foco é a árvore de UI tipada e o reconciliador. Veja o [plano de design](plan.md) para o desenho completo e o [roadmap por fases](roadmap.md). !!! tip "🤖 Ler o projeto com sua IA (`llms.txt`)" Este site publica dois arquivos seguindo a convenção [llmstxt.org](https://llmstxt.org/) para você dar ao seu assistente de IA (Claude, ChatGPT, Cursor, etc.) o projeto inteiro como referência — **sem servidor, sem MCP**: - **[`/llms.txt`](https://mauriciobenjamin700.github.io/tempestroid/llms.txt)** — índice enxuto (resumo + links de todas as páginas). Use quando a IA puder navegar pelos links. - **[`/llms-full.txt`](https://mauriciobenjamin700.github.io/tempestroid/llms-full.txt)** — documentação **inteira** concatenada num só arquivo Markdown. Use para colar/anexar de uma vez quando a IA não navega. **Como usar:** cole a URL (ou o conteúdo) de `llms-full.txt` no contexto do seu assistente e peça para usar como referência do tempestroid. Os arquivos são regenerados a cada publicação das docs, então estão sempre em dia. ## Por quê - **Tipado de ponta a ponta.** Modelo de estilo, primitivas de widget, eventos e o contrato de fronteira Python↔Kotlin são todos Pydantic v2 / totalmente tipados. O `pyright` roda em modo estrito. - **Uma árvore, dois alvos.** O reconciliador é dado-puro-entra → *patches*-saem. Toda divergência de plataforma fica confinada aos dois tradutores de `Style` (Qt e Compose). - **Async-first.** *Handlers* de evento e *hooks* de ciclo de vida podem ser síncronos ou `async`; o Python roda em um loop asyncio de fundo, nunca na thread de UI. - **Loop interno rápido.** `tempest dev` observa o seu arquivo e faz hot reload do simulador Qt ao salvar — sem precisar de dispositivo ou emulador para trabalhar a UI. ## Como funciona ```text view(app) ──build──▶ Árvore de Node (IR) │ diff puro, agnóstico de renderizador ▼ [ Patch ] Insert / Remove / Update / Reorder / Replace ╱ ╲ Renderizador Qt Renderizador Compose (simulador) (dispositivo) ``` 1. `view(app) -> Widget` constrói a árvore de widgets a partir do estado atual. 2. `build` rebaixa a árvore para uma IR de `Node`; `diff` compara a versão antiga com a nova e emite uma lista mínima de `Patch`. 3. Um renderizador aplica os *patches* nos widgets vivos. Mudanças de estado são coalescidas em um único rebuild por *tick*. ## Próximos passos - [Instalação](instalacao.md) — instale o framework e o simulador. - [Começo rápido](inicio-rapido.md) — seu primeiro app em poucas linhas. - [Arquitetura](arquitetura.md) — IR, reconciliador, renderizadores e a ponte. - [Guia do usuário](guia/widgets.md) — widgets, estilos, eventos e a CLI. --- # File: docs/instalacao.md — https://mauriciobenjamin700.github.io/tempestroid/instalacao/ # Instalação Há **dois públicos** para o tempestroid, e cada um instala de um jeito: - **Você quer construir um app** com o framework → [Usuário final](#usuario-final). - **Você quer contribuir** com o próprio framework (mexer no código deste repositório) → [Contribuidor](#contribuidor-este-repositorio). Comece pela seção do seu caso — elas são independentes. ## Usuário final Você só vai **usar** o framework para construir apps. Instale com `pip` (ou `uv pip`). O **núcleo** depende apenas de `pydantic`: ```bash pip install tempestroid ``` ### Extras opcionais Instale só o que o seu fluxo precisa: ```bash pip install "tempestroid[qt]" # simulador de desktop (PySide6 + qasync) pip install "tempestroid[icons]" # gerador de ícone/splash (tempest icon — Pillow) ``` | Extra | O que adiciona | Quando usar | |---|---|---| | (núcleo) | só `pydantic` | sempre — a árvore de widgets tipada | | `qt` | `PySide6` + `qasync` | rodar/visualizar o app no **simulador de desktop** (`tempest dev`/`run`) | | `icons` | `Pillow` | gerar `icon.png` + `splash.png` de uma imagem (`tempest icon`) | !!! tip "Comece pelo simulador" Para experimentar sem nenhum aparelho Android, instale o extra `qt` e rode `tempest dev meuapp/app.py` — o app aparece numa janela de desktop com hot reload. Veja o [Começo rápido](inicio-rapido.md). ### Build para Android Gerar o **APK** (`tempest build apk`) precisa apenas de um **JDK** e do **Android SDK** — **não** precisa de NDK, nem do toolchain do CPython, nem de clonar este repositório. O projeto `android-host` que gera o APK **já vem dentro do pacote** instalado pelo `pip`. ```bash pip install tempestroid # já traz o android-host embarcado tempest setup --install # instala o Android SDK (se faltar) tempest doctor # diagnostica o que falta (JDK, SDK, adb, device) mkdir meu-app && cd meu-app # sua pasta de projeto (com seu venv) tempest new # scaffold AQUI; id = nome da pasta ("meu-app") tempest build apk # APK próprio (instala lado a lado com outros) ``` !!! info "JDK + SDK, e só" O APK reusa os binários nativos já compilados do host (CPython embutido), por isso dispensa NDK/toolchain. `tempest doctor` separa o que é necessário para **buildar** (JDK + SDK) do que é só para **rodar/instalar** (adb + aparelho). Detalhes em [Build, deploy e publicação](guia/build.md). | Alvo | Precisa de | |---|---| | Simulador (desktop) | Python ≥ 3.11 + extra `qt` | | Build do APK | JDK + Android SDK (`tempest setup --install`) — sem NDK/toolchain | | Instalar/rodar no aparelho | o acima + `adb` + um device conectado | ## Contribuidor (este repositório) Você vai mexer no **código do framework**. O fluxo usa [uv](https://docs.astral.sh/uv/); um único comando instala tudo: ```bash git clone https://github.com/mauriciobenjamin700/tempestroid cd tempestroid uv sync # núcleo + ferramental de dev + simulador Qt + docs ``` Além das dependências de runtime, o `uv sync` instala: - o grupo de dev (`ruff`, `pyright`, `pytest`, `pytest-asyncio`); - o simulador Qt (`PySide6`, `qasync`) — faz parte do loop de dev/teste; - o site de documentação (`mkdocs-material` + o plugin de idiomas). ### Portões de qualidade Rode antes de cada commit (ou use o `Makefile`): ```bash make gate # ruff + pyright(strict) + pytest + mkdocs --strict + convenções make quick # versão rápida (sem pytest) make docs-sync # confere README/CLI/tabela de fases em sincronia ``` ### Documentação O site (este conteúdo) é construído com MkDocs Material, instalado pelo `uv sync`: ```bash uv run mkdocs serve # servidor local com hot reload em http://127.0.0.1:8000 uv run mkdocs build --strict # build de produção; falha em qualquer aviso ``` O cabeçalho tem um **seletor de idioma PT-BR / EN-US** (plugin `mkdocs-static-i18n`). --- # File: docs/inicio-rapido.md — https://mauriciobenjamin700.github.io/tempestroid/inicio-rapido/ # Começo rápido Este guia leva você do zero a um app rodando no simulador em poucos minutos — mesmo que seja seu primeiro contato com o tempestroid. O caminho é sempre o mesmo: **criar** um projeto, **rodar** no simulador, **editar** e ver a mudança ao vivo. !!! tip "Pré-requisitos" - **Python ≥ 3.11** e o [uv](https://docs.astral.sh/uv/) instalados. - O framework com o simulador Qt: `pip install "tempestroid[qt]"` (ou, neste repositório de desenvolvimento, `uv sync`). Detalhes em [Instalação](instalacao.md). - No WSL/Linux sem ambiente gráfico, o simulador Qt precisa de um servidor de display. Veja [Rodar no dispositivo / WSL](guia/dispositivo-wsl.md). ## Passo 1 — Crie um projeto Você já está dentro da pasta do seu projeto (e do seu ambiente virtual). O comando `tempest new`, **sem argumentos**, gera os arquivos iniciais **aqui mesmo** — um `app.py` (contador de exemplo), `pyproject.toml`, `README.md` e `.gitignore` — e usa o **nome da pasta atual como id do app**. Sem pasta extra em volta. ```bash mkdir meu-app && cd meu-app # sua pasta de projeto (com seu venv) uv run tempest new # scaffold AQUI; id = "meu-app" ``` > Quer uma subpasta? Passe um nome: `uv run tempest new OutroApp` cria > `OutroApp/`. Mas o fluxo recomendado é o in-place acima. > > Instalou via `pip`? O binário fica disponível como `tempest new` (sem o > `uv run`). Ao longo deste guia usamos `uv run tempest …` por ser o fluxo do > repositório; remova o `uv run ` se instalou pelo `pip`. O `app.py` gerado é **Python puro**, sem nenhum import de Qt no nível do módulo — por isso o **mesmo arquivo** roda no simulador de desktop, vai pro dispositivo via `tempest serve` e empacota com `tempest build` sem mudar uma linha. ## Passo 2 — Rode no simulador ```bash uv run tempest dev # abre o simulador Qt + hot reload ``` `tempest dev` (sem argumento) lê o caminho do app de `[tool.tempest] app` no `pyproject.toml` — por isso você roda da raiz do projeto sem apontar o arquivo. Uma janela abre com o contador (`-`, o valor, `+`). O terminal vira um *cockpit* interativo: | Tecla | Ação | |---|---| | `r` | Hot reload — recarrega o código **preservando o estado** atual. | | `R` | Hot restart — recarrega do zero (estado limpo via `make_state`). | | `s` | Traz a janela do simulador à frente. | | `q` | Encerra. | ### Escolha o tamanho de tela (presets de aparelho) O simulador abre num tamanho de telefone genérico, mas você deve **testar nos tamanhos dos aparelhos-alvo**. Passe `--device` (ou `-d`) com um preset: ```bash uv run tempest dev --device pixel-7 # 412×915 dp uv run tempest dev -d galaxy-s24 # 384×824 dp uv run tempest dev -d redmi-note-12 # 393×873 dp (padrão) ``` Os tamanhos são em **dp** (pixels independentes de densidade) — exatamente o espaço de layout que o Compose usa no aparelho, então o que cabe no simulador cabe no device. Os nomes são tolerantes (`pixel-7`, `PIXEL_7`, `pixel 7`, `Google Pixel 7` resolvem o mesmo). O catálogo completo são 33 presets (Pixel, Galaxy S/A, Redmi/Poco/Xiaomi, Moto, OnePlus) no enum `Device` — use programaticamente com `run_qt(state, view, device=Device.PIXEL_7)`. !!! tip "Quais testar" Cubra um **telefone pequeno/estreito** (ex.: `galaxy-s8`, 360×740) e um **grande** (ex.: `pixel-8-pro`, 448×998) — se o layout se comporta nos dois extremos, os tamanhos intermediários seguem bem. !!! info "Até onde o simulador é fiel?" O simulador reflete fielmente estrutura, estado, eventos e a maior parte do `Style`, mas **não** a aparência nativa (Material 3), animações, overlays e fontes — esses só são 100% fiéis no aparelho. Veja [fidelidade do simulador](arquitetura.md#fidelidade-do-simulador-o-que-ele-reflete-e-o-que-nao). ## Passo 3 — Edite e veja ao vivo Com o simulador aberto, abra `app.py` no editor e mude algum texto — por exemplo o título dentro do `Text`. **Salve o arquivo.** O `tempest dev` detecta a gravação e faz hot reload automático: a janela atualiza sem perder o contador. Se uma edição quebrar o app, o erro é impresso no terminal e o loop **sobrevive** — corrija e salve de novo. Se a recarga for incompatível com o estado vivo, ele cai automaticamente para um restart limpo. Pronto: esse é o ciclo completo de desenvolvimento. O resto deste guia explica **o que** você acabou de rodar. ## O modelo mental Todo app tempestroid honra um contrato de **duas funções**: - **`make_state() -> S`** — devolve o **estado inicial**. É chamado a cada hot restart, então deve montar um estado limpo. `S` é qualquer objeto seu (um `@dataclass` é o caminho natural). - **`view(app: App[S]) -> Widget`** — recebe o app e devolve a **árvore de UI** para o estado atual. É uma função pura de estado → widgets: dado o mesmo estado, devolve a mesma árvore. O ciclo que conecta as duas: ```text estado ──view(app)──▶ árvore de widgets ──diff──▶ patches ──▶ tela ▲ │ └────────────── app.set_state(...) ◀── handler de evento ◀───┘ ``` 1. `view` constrói a árvore a partir de `app.state`. 2. Você liga *handlers* (clique, etc.) a `app.set_state(...)`. 3. Quando um handler chama `set_state`, o `App` reconstrói a `view`, faz o *diff* contra a árvore anterior e entrega só os *patches* (mudanças mínimas) ao renderizador. Várias chamadas de `set_state` no mesmo *tick* viram **um único** rebuild (coalescido). `set_state` recebe uma função que **muta o estado no lugar**: ```python app.set_state(lambda s: setattr(s, "value", s.value + 1)) ``` Você nunca toca na tela diretamente — só descreve a UI em função do estado e muda o estado. O framework cuida do resto. ## Um contador do zero O scaffold já dá um contador completo, mas vale construir o mínimo à mão para entender cada peça. Crie um `app.py`: ```python from dataclasses import dataclass from tempestroid import App, Button, Column, Style, Text, Widget from tempestroid.renderers.qt import run_qt @dataclass class CounterState: """O estado do app: um único contador.""" value: int = 0 def make_state() -> CounterState: """Devolve o estado inicial (chamado a cada hot restart).""" return CounterState() def view(app: App[CounterState]) -> Widget: """Constrói a árvore de UI para o estado atual.""" def increment() -> None: app.set_state(lambda s: setattr(s, "value", s.value + 1)) return Column( style=Style(gap=8.0), children=[ Text(content=f"Count: {app.state.value}", key="label"), Button(label="+", on_click=increment, key="inc"), ], ) if __name__ == "__main__": raise SystemExit(run_qt(make_state(), view, title="counter")) ``` Lendo de cima para baixo: - **`CounterState`** — seu estado, um `dataclass` simples com um campo `value`. - **`make_state`** — a fábrica do estado inicial. - **`view`** — descreve a tela: uma `Column` (empilha verticalmente) com um `Text` que mostra o valor e um `Button` que incrementa. `app.state.value` lê o estado; `increment` chama `set_state` para mudá-lo. - **`key="..."`** — identifica cada widget para o *diff* casar o widget velho com o novo entre rebuilds. Dê *keys* estáveis a filhos de listas. - **`Style(gap=8.0)`** — espaçamento entre filhos. Estilos são objetos tipados e imutáveis (veja o [guia de estilos](guia/estilos.md)). - **`if __name__ == "__main__"`** — `run_qt` abre a janela ao rodar o arquivo direto. Mantenha o import de Qt **dentro** deste bloco (ou só aqui no topo, sem ser usado por `view`/`make_state`) para o arquivo continuar rodando no dispositivo, que não tem Qt. Rode direto, sem o cockpit: ```bash uv run python app.py ``` Ou com hot reload (recomendado durante o desenvolvimento): ```bash uv run tempest dev app.py ``` ## Handlers assíncronos *Handlers* podem ser `async` — o runtime os agenda no loop asyncio sem travar a UI. Útil para esperar I/O (rede, disco) antes de atualizar o estado: ```python import asyncio def view(app: App[CounterState]) -> Widget: async def increment_later() -> None: await asyncio.sleep(0.5) app.set_state(lambda s: setattr(s, "value", s.value + 1)) return Button(label="+0.5s", on_click=increment_later, key="inc") ``` ## Problemas comuns | Sintoma | Causa / solução | |---|---| | `ModuleNotFoundError: tempestroid` | Framework não instalado no ambiente. Rode `uv sync` (repo) ou `pip install "tempestroid[qt]"`. | | Erro de import do `PySide6` / Qt ao rodar `dev` | O extra `qt` não está instalado. Use `pip install "tempestroid[qt]"`. | | `app.py must define a make_state()` / `view` | O arquivo precisa expor **as duas** funções no nível do módulo, com esses nomes exatos. | | A janela não abre no WSL/Linux headless | Falta um servidor de display. Veja [dispositivo / WSL](guia/dispositivo-wsl.md). | | Edição não recarrega | Confirme que está rodando via `tempest dev` (não `python app.py`) e que salvou o arquivo; ou aperte `r`. | ## Próximos passos - [Widgets](guia/widgets.md) — todas as primitivas (`Text`, `Column`, `Row`, `Button`, entradas, mídia…). - [Estilos](guia/estilos.md) — o modelo de `Style` tipado. - [Eventos](guia/eventos.md) — o contrato tipado de eventos. - [CLI](guia/cli.md) — todos os comandos `tempest`. - [Galeria de exemplos](guia/exemplos.md) — apps completos para estudar. Veja também o exemplo de referência em [`examples/counter/app.py`](https://github.com/mauriciobenjamin700/tempestroid/blob/main/examples/counter/app.py). --- # File: docs/arquitetura.md — https://mauriciobenjamin700.github.io/tempestroid/arquitetura/ # Arquitetura O tempestroid separa **o que renderizar** (uma IR tipada e serializável) de **como renderizar** (renderizadores-folha por plataforma), ligados por um **reconciliador puro**. ## Invariantes - **O reconciliador é agnóstico de renderizador** — dado puro entra, *patches* saem. Toda divergência de plataforma fica confinada aos dois tradutores de `Style`. - **A árvore de widgets é a IR** — modelos Pydantic serializáveis. Percorra qualquer árvore via `Widget.child_nodes()`; nunca alcance o armazenamento de filhos específico de um renderizador. - **O Python roda em uma thread de fundo** hospedando um loop asyncio, nunca na thread de UI. O *marshalling* atravessa uma única fronteira de ponte. ## O pipeline ```text view(app) ──build──▶ Árvore de Node (IR) │ diff ▼ [ Patch ] ╱ ╲ Renderizador Qt Renderizador Compose ``` ### 1. Widgets (a IR) `view(app)` devolve uma árvore de `Widget` — modelos Pydantic frozen onde representam valores imutáveis. Cada widget é um nó declarativo: `Text`, `Button`, `Column`, `Row`, `Container`, os inputs com valor (`Input`, `Checkbox`, `DatePicker`, `FilePicker`, …) e dezenas de outros (listas virtualizadas, navegação, overlays, animação, gestos, mídia) — todos suportados pelos **dois renderizadores**. A lista completa está no [guia de exemplos](guia/exemplos.md#conjunto-de-widgets-atual). ### 2. build → Node `build(widget) -> Node` rebaixa a árvore de widgets para a IR de `Node`: uma estrutura uniforme com `type`, `key`, `props` e `children`. É essa forma que o reconciliador e os serializadores entendem. ### 3. diff → Patch `diff(old, new) -> list[Patch]` compara duas árvores de `Node` e emite a lista mínima de *patches*: | Patch | Significado | |---|---| | `Insert` | Inserir um novo nó em uma posição. | | `Remove` | Remover um nó. | | `Update` | Atualizar `props` de um nó (campos a setar / remover). | | `Reorder` | Reordenar filhos (apenas para permutação pura de chaves). | | `Replace` | Trocar um nó por outro de tipo diferente. | !!! note "Diffing de filhos é posicional por padrão" Um único `Reorder` só é emitido para uma *permutação pura* (ambas as listas totalmente chaveadas, chaves únicas, mesmo conjunto, mesmo tamanho). Mistura de insert + reorder cai para posicional — correto, porém menos ótimo. ### 4. Renderizadores aplicam patches Cada renderizador-folha aplica os mesmos *patches* aos seus widgets vivos: - **Qt** (`renderers/qt`) — mapeia `Node`s para `QWidget`s e `Style` para `QBoxLayout` + QSS. É o simulador de desktop. - **Compose** (`renderers/compose` + host Kotlin) — mapeia a árvore serializada para `@Composable`s e `Style` para `Modifier`/`Arrangement`/`Alignment`. É o renderizador do dispositivo. ## Fidelidade do simulador (o que ele reflete — e o que não) O simulador Qt é um **proxy semântico fiel**, não um espelho pixel-a-pixel do aparelho. Vale saber a fronteira para confiar nele no lugar certo. **O que é idêntico** (a espinha dorsal): a mesma árvore IR, o mesmo reconciliador, o mesmo fluxo `view → diff → patch`, os mesmos eventos tipados e o mesmo estado coalescido. Layout, navegação, lógica, estado e eventos comportam-se igual. A maioria dos campos de `Style` é honrada nos dois (alinhamento, espaçamento `SPACE_*`, `STRETCH`, `text_align`, tamanho fixo, padding/margin, cor, fonte). Os tamanhos do simulador são em **dp**, o mesmo espaço de layout que o Compose usa — por isso o que cabe na janela cabe no aparelho (veja [escolher o tamanho de tela](inicio-rapido.md#escolha-o-tamanho-de-tela-presets-de-aparelho)). !!! check "Garantia de paridade" A suíte de **conformância** (`tests/conformance/`) fixa os **dois tradutores `Style` lado a lado** (golden snapshots de `to_qss` e `to_compose`) + uma tabela de cobertura por-campo. Eles **não podem divergir em silêncio** — uma mudança que regride a paridade quebra o *gate*. **O que só o aparelho mostra fielmente** (divergências esperadas): - **Aparência dos widgets** — o Qt usa QWidget/QSS; o device usa **Material 3**. Diálogos, menus, bottom sheets, pickers e campos têm o visual nativo de cada um. - **Animações** — Qt usa `QPropertyAnimation`; o device dirige o motor nativo do Compose (`animate*AsState`/`AnimatedContent`). - **Overlays e safe-area** — o Compose gerencia `WindowInsets.safeDrawing`/scrim próprios; o Qt aproxima com um scrim manual. - **Fontes e densidade do SO** mudam métricas finas de layout. - **Widgets de hardware** — `CameraPreview`/`QrScanner`/`MapView` são **device-only**; no simulador aparecem como *placeholder* sinalizado. !!! warning "Regra: verificação dual" Por isso, ao mexer em superfície de UI, valide **nos dois**: o simulador Qt **e** o aparelho físico (Compose) quando houver um conectado — `make dual-verify`. O simulador acelera o desenvolvimento; o aparelho confirma a aparência final, animações e overlays. ## Estado: `App[S]` `App[S]` é o container de estado agnóstico de renderizador. Ele: - guarda o estado (`app.state`); - constrói a UI via a função `view(app)`; - faz o *diff* e entrega os *patches* a um callback `apply_patches`. Rebuilds são **coalescidos**: `request_rebuild` agenda um único `_rebuild` via `loop.call_soon`, então vários `set_state` no mesmo *tick* geram um único *diff*. Rebuilds sem mudança não emitem *patches*. ## A fronteira tipada (Python↔Kotlin) Sem um WebView, não há fronteira JS↔Python; o contrato tipado vive na fronteira Python↔Kotlin. Eventos que voltam do lado nativo (um toque, uma mudança de texto) chegam como *payloads* crus e são **validados antes** de entrar em um *handler* — exatamente como o FastAPI valida um corpo de requisição. - `parse_event(event_type, raw)` é o portão de validação: transforma um *payload* cru em um evento tipado ou levanta `EventValidationError` com os erros estruturados por campo. - A serialização (`serialize_node` / `serialize_patch`) rebaixa a IR/patches para dicts JSON-able: *handlers* viram *tokens* de caminho, `Style` vira a *spec* Compose. Veja [Lado do dispositivo (ponte)](referencia/dispositivo.md) para o protocolo de fio e o transporte JNI. ## Recapitulando - O tempestroid separa **o que** renderizar (IR de widgets) de **como** (renderizadores-folha), ligados por um reconciliador puro. - O pipeline: `view → build → diff → patches → renderizador`. - `App[S]` guarda o estado e coalesce rebuilds (um *diff* por *tick*). - A fronteira Python↔Kotlin é tipada e validada (`parse_event`, `serialize_node`). ## Próximos passos ➡️ Conheça as primitivas em **[Widgets](guia/widgets.md)**, ou aprofunde a ponte em **[Lado do dispositivo](referencia/dispositivo.md)**. --- # File: docs/guia/widgets.md — https://mauriciobenjamin700.github.io/tempestroid/guia/widgets/ # Widgets Widgets são as primitivas declarativas da IR — uma árvore de modelos Pydantic que o reconciliador faz o *diff* e os renderizadores aplicam. Importe sempre do nível do pacote: `from tempestroid import Text, Button, ...`. O framework exporta **~100 widgets**, todos suportados pelos **dois renderizadores** (simulador Qt no desktop + Compose no dispositivo). Este guia é o índice; cada família tem sua própria página tutorial com exemplos completos e a tabela de props de cada widget. ## Catálogo por família | Família | O que cobre | |---|---| | [Texto, ação e indicadores](widgets/basics.md) | `Text` / `Button` / `ProgressBar` / `Spinner` | | [Layout](widgets/layout.md) | `Column` / `Row` / `Container` / `Stack` / `Wrap` / `ScrollView` / `SafeArea` / `AspectRatio` / `PageView` / `KeyboardAvoidingView` | | [Inputs com valor](widgets/inputs.md) | `Input` / `TextArea` / `Checkbox` / `Switch` / `Slider` / `RangeSlider` / `Dropdown` / `DatePicker` / `TimePicker` / `FilePicker` / `PinInput` / `MaskedInput` / `Autocomplete` / `Form` / `FormField` | | [Listas virtualizadas](widgets/lists.md) | `LazyColumn` / `LazyRow` / `LazyGrid` / `SectionList` / `RefreshControl` | | [Navegação](widgets/navigation.md) | `Navigator` / `TabView` / `TabBar` / `RouteDrawer` | | [Overlays e feedback](widgets/overlays.md) | `Dialog` / `BottomSheet` / `Menu` / `Popover` / `Toast` / `Tooltip` / `ActionSheet` | | [Animação](widgets/animation.md) | `Animated` / `AnimatedList` / `Hero` / `Shimmer` / `Skeleton` | | [Gestos](widgets/gestures.md) | `GestureDetector` / `PanHandler` / `ScaleHandler` / `DoubleTapHandler` / `Draggable` / `DragTarget` / `Dismissible` / `ReorderableList` / `InteractiveViewer` | | [Mídia e gráficos](widgets/media.md) | `Image` / `Icon` / `Canvas` / `Svg` / `VideoPlayer` / `WebView` / `Blur` / `BackdropFilter` / `ClipPath` / `CameraPreview` / `QrScanner` / `MapView` | | [Componentes compostos](widgets/components.md) | `Card` / `ListTile` / `Scaffold` / `AppBar` / `NavBar` / `SegmentedControl` / `Rating` / `Table` … (29) | !!! tip "Por onde começar" Se você está chegando agora, siga na ordem: **[Texto, ação e indicadores](widgets/basics.md)** → **[Layout](widgets/layout.md)** → **[Inputs com valor](widgets/inputs.md)**. O resto pode ser lido por demanda. ## Conceitos transversais Estes valem para qualquer widget — vale ler antes de mergulhar nas famílias. ### Chaves (`key`) Dê um `key` estável a cada filho de uma lista. O reconciliador usa chaves para emitir `Reorder` em vez de recriar widgets, e para casar nós entre rebuilds. ### Percorrendo a árvore Todo widget expõe `child_nodes()` — use-o para caminhar a árvore de forma genérica, sem alcançar o armazenamento interno de cada tipo. *Leaves* (`Text`, `Image`, inputs) devolvem `[]`. ### Estilo, semântica e foco Toda subclasse de `Widget` aceita `style` (um [`Style`](estilos.md)), `semantics`/`focusable`/`focus_order` (acessibilidade) e `key`. Por isso essas props não aparecem nas tabelas de cada família — são universais. ### Contrato de eventos por widget Cada widget declara o evento que cada *handler* emite via a classvar `event_schemas` (ex.: `Button.event_schemas == {"on_click": TapEvent}`). Esse contrato é publicado por [`introspect()`](../referencia/api.md) e consumido pela fronteira do dispositivo. Veja [Eventos](eventos.md). !!! info "Paridade dos dois renderizadores" O conjunto completo renderiza tanto no **simulador Qt** quanto no **dispositivo (Compose)** — a paridade é fixada pela suíte de conformância (*golden snapshots* dos dois tradutores `Style`). A única exceção são alguns widgets de hardware (`CameraPreview` / `QrScanner` / `MapView`), que são **device-only** e aparecem como *placeholder* sinalizado no Qt. ## Recapitulando - Widgets são modelos Pydantic; importe sempre do nível do pacote (`from tempestroid import ...`). - São ~100 widgets em 10 famílias — use o catálogo acima. - *Inputs* com valor emitem um evento de mudança tipado (`on_change` / `on_select`); dê um `key` estável a filhos de listas. - `style`/`semantics`/`focusable`/`key` são universais a todo widget. ## Próximos passos ➡️ Deixe os widgets bonitos com **[Estilos](estilos.md)**, entenda os **[Eventos](eventos.md)** tipados, ou veja apps completos na **[Galeria de exemplos](exemplos.md)**. --- # File: docs/guia/estilos.md — https://mauriciobenjamin700.github.io/tempestroid/guia/estilos/ # Estilos O estilo é descrito por objetos de valor Pydantic **frozen**, diferenciados por valor — é o que permite ao reconciliador fazer o *diff* do estilo de forma barata. Toda divergência entre Qt e Compose fica confinada aos dois tradutores de `Style`. ```python from tempestroid import ( AlignItems, Color, Column, Edge, FlexDirection, FontWeight, Style, Text, ) Column( style=Style( direction=FlexDirection.COLUMN, align=AlignItems.CENTER, gap=16.0, padding=Edge.all(24.0), background=Color.from_hex("#101418"), ), children=[ Text( content="Título", style=Style( color=Color.from_hex("#ffffff"), font_size=24.0, font_weight=FontWeight.BOLD, ), key="t", ), ], ) ``` ## Campos do `Style`, por grupo `Style` é um modelo único; abaixo os campos agrupados por intenção. | Grupo | Campos | |---|---| | **Layout** | `direction`, `justify`, `align`, `align_self`, `grow`, `gap`, `flex_wrap`, `stack_align` | | **Posição** | `position`, `top`, `right`, `bottom`, `left` | | **Caixa** | `padding`, `margin`, `border`, `radius` | | **Pintura** | `background`, `color`, `opacity`, `shadow` | | **Tipografia** | `font_family`, `font_asset`, `font_size`, `font_weight`, `font_style`, `text_align`, `text_decoration`, `text_scale`, `letter_spacing`, `line_height`, `max_lines`, `text_overflow` | | **Dimensão** | `width`, `height`, `min_width`, `max_width`, `min_height`, `max_height`, `aspect_ratio` | | **Animação** | `transition` | ## Objetos de valor | Tipo | Uso | |---|---| | `Color` | Cor; `Color.from_hex("#101418")`. | | `Edge` | Insets; `Edge.all(24.0)` ou `Edge.symmetric(vertical=8.0, horizontal=16.0)`. | | `Border` | Borda uniforme (largura, cor). | | `SideBorder` | Borda por lado (`top`/`right`/`bottom`/`left`) — ex.: um divisor inferior. | | `Corners` | Raios por canto para `Style.radius` (`top_left`/`top_right`/`bottom_right`/`bottom_left`) — ex.: folhas arredondadas só no topo. | | `Shadow` | `box-shadow`/elevação (`color`/`blur`/`offset_x`/`offset_y`). Compose mapeia para elevação; Qt para `QGraphicsDropShadowEffect`. | | `Gradient` + `GradientStop` | Gradiente linear usável onde um `background` `Color` é (QSS `qlineargradient` / Compose `Brush`). | | `Transition` | Animação implícita (`duration_ms`/`curve`/`delay_ms`). | ```python from tempestroid import ( Color, Corners, Gradient, GradientDirection, GradientStop, Shadow, Style, ) Style( background=Gradient( stops=[GradientStop(color=Color.from_hex("#3b82f6"), position=0.0), GradientStop(color=Color.from_hex("#9333ea"), position=1.0)], direction=GradientDirection.LEFT_RIGHT, ), radius=Corners(top_left=16.0, top_right=16.0), shadow=Shadow(color=Color.from_hex("#00000040"), blur=12.0, offset_y=4.0), opacity=0.95, ) ``` ## Enums Resumo dos mais usados em `Style`. A **[referência de enums](../referencia/enums.md)** documenta os 27 enums com cada membro, valor e significado. | Enum | Valores | |---|---| | `FlexDirection` | `ROW`, `COLUMN`. | | `JustifyContent` | `START`, `CENTER`, `END`, `SPACE_BETWEEN`, `SPACE_AROUND`, `SPACE_EVENLY`. | | `AlignItems` | `START`, `CENTER`, `END`, `STRETCH`. | | `FlexWrap` | `NOWRAP`, `WRAP`, `WRAP_REVERSE` (usado por `Wrap`/`flex_wrap`). | | `StackAlign` | `TOP_START`…`BOTTOM_END` (alinhamento de filhos num `Stack`). | | `Position` | `STATIC`, `ABSOLUTE` (com `top`/`right`/`bottom`/`left`). | | `TextAlign` | `LEFT`, `CENTER`, `RIGHT`, `JUSTIFY`. | | `FontWeight` | `NORMAL`, `BOLD` (e pesos numéricos). | | `FontStyle` | `NORMAL`, `ITALIC`. | | `TextDecoration` | `NONE`, `UNDERLINE`, `LINE_THROUGH`. | | `TextOverflow` | `CLIP`, `ELLIPSIS`. | | `GradientDirection` | `TOP_BOTTOM`, `BOTTOM_TOP`, `LEFT_RIGHT`, `RIGHT_LEFT`. | | `Curve` | `LINEAR`, `EASE_IN`, `EASE_OUT`, `EASE_IN_OUT` (easing de `Transition`). | | `ImageFit` | `CONTAIN`, `COVER`, `FILL`, `NONE` (usado por `Image`). | | `KeyboardType` | `TEXT`, `NUMBER`, `EMAIL`, `PHONE`, `URL`, `PASSWORD` (usado por `Input`). | ## Transições animadas `Style.transition` aceita um objeto `Transition` que descreve uma animação implícita — modelado em `transition` do CSS / nos *implicitly-animated widgets* do Flutter: quando uma prop estilizada muda entre rebuilds, o renderizador interpola até o novo valor (Compose mapeia para `animate*AsState`; no Qt a animação é imperativa no renderizador). ```python from tempestroid import Curve, Style, Transition Style( background=Color.from_hex("#3b82f6"), transition=Transition(duration_ms=200, curve=Curve.EASE_IN_OUT, delay_ms=0), ) ``` | Campo | Tipo | Significado | |---|---|---| | `duration_ms` | `int` | Duração da animação em milissegundos. | | `curve` | `Curve` | Curva de easing. | | `delay_ms` | `int` | Atraso antes de iniciar, em milissegundos. | ## Como cada renderizador traduz O mesmo `Style` alimenta os dois tradutores; a **suíte de conformidade** (fase D) fixa ambos com *golden snapshots* para impedir divergência silenciosa. - **Qt** (`Style → Qt`): *padding* vira QSS nos *leaves* e `contentsMargins` nos containers (sem dupla contagem); `justify`/`align` `START/CENTER/END` viram flags de alinhamento Qt; `grow` vira fator de *stretch* do layout. - **Compose** (`to_compose(style)`): emite uma *spec* serializável que o host Kotlin transforma em `Modifier` / `Arrangement` / `Alignment`. !!! note "Divergências conhecidas" Nem todo campo é honrado igualmente nos dois lados ainda. A suíte de conformidade documenta as divergências e falha se um tradutor passar a tratar (ou parar de tratar) um campo sem atualizar a tabela. ## Imutabilidade `Style` e seus objetos de valor são frozen. Para "mudar" um estilo, construa um novo objeto — é o que a `view` faz a cada rebuild, e o que permite o *diff* por valor. ## Recapitulando - `Style` é um modelo único, frozen, diferenciado por valor. - Campos agrupados por intenção: layout, caixa, pintura, tipografia, dimensão, animação. - Objetos de valor (`Color`, `Edge`, `Border`, `Shadow`, `Gradient`, `Transition`) montam os campos. - Um mesmo `Style` alimenta Qt e Compose; divergências ficam documentadas pela suíte de conformidade. ## Próximos passos ➡️ Ligue interação com **[Eventos](eventos.md)**, ou veja os estilos aplicados em apps completos na **[Galeria de exemplos](exemplos.md)**. --- # File: docs/guia/design-system/tokens.md — https://mauriciobenjamin700.github.io/tempestroid/guia/design-system/tokens/ # Tema e tokens Até aqui você estilizou cada widget na mão: `Color.from_hex("#2563eb")` aqui, um raio `12.0` ali, um padding `Edge.all(16.0)` acolá. Funciona, mas espalha decisões de marca por todo o app — e na hora do **dark mode** você reescreve cada cor. O **design system** do tempestroid resolve isso: um `Theme` carrega um conjunto de **tokens Material 3** (cores, espaçamento, raio, tipografia, elevação, movimento), e os componentes leem esses tokens em vez de valores crus. !!! info "De onde vêm os tokens" O motor de design (`Theme`, `TokenSet`, variantes) é implementado no pacote **`tempest-core`**, instalado junto com o tempestroid. Mas você não precisa importá-lo: todo o conjunto do design system — `Theme`, `ThemeMode`, `ColorRole`, `TokenSet`, `TokenRef`, `Variant` e companhia — é **re-exportado por `tempestroid`**, então você importa tudo de um único lugar, como qualquer outro widget ou enum. ## Um `Theme` em uma linha A porta de entrada é `Theme.from_seed(...)`: você dá **uma** cor de marca e recebe uma paleta Material 3 completa — claro e escuro, com todos os papéis de cor preenchidos e contraste garantido. ```python from tempestroid import Color, Theme theme = Theme.from_seed(Color.from_hex("#2563eb")) # A semente vira um esquema tonal M3 inteiro: print(theme.color("primary").to_hex()) # papel primário do esquema claro print(theme.color("on_primary").to_hex()) # conteúdo legível sobre o primário print(theme.space("md")) # 16.0 — gutter padrão (grade de 4dp) print(theme.radius("lg")) # 16.0 — raio de cantos grande print(theme.elevation(2)) # 3.0 — elevação nível 2, em dp ``` !!! tip "Não tem cor de marca ainda?" Construa `Theme()` sem argumentos: você ganha o tema baseline do Material 3 (o roxo de referência `#6750A4`). Tudo nesta página funciona igual. ## Os papéis de cor (color schemes) O Material 3 não pinta com cores cruas — pinta com **papéis semânticos**. Cada papel tem um par `on_*` que é o conteúdo legível desenhado sobre ele (gerado para atingir contraste WCAG-AA). O tempestroid expõe cinco famílias de papéis, que os componentes escolhem pelo `color_scheme`: | `color_scheme` | Papel base | Conteúdo (`on_*`) | Uso típico | |---|---|---|---| | `"primary"` | `primary` | `on_primary` | ação principal, estado ativo | | `"secondary"` | `secondary` | `on_secondary` | acento complementar | | `"tertiary"` | `tertiary` | `on_tertiary` | acento contrastante | | `"error"` | `error` | `on_error` | erro / ação destrutiva | | `"neutral"` | `on_surface` | `surface` | tratamento neutro, baixa ênfase | Além dessas famílias de ênfase, o design system promove **três papéis de status** a `color_scheme`s de primeira classe — `"success"`, `"warning"` e `"info"` (somando-se a `"error"`), cada um com seu par `on_*` e uma variante de *container* AA. Eles aparecem em qualquer componente que aceite `color_scheme`; o vocabulário de feedback está em [data display e feedback](feedback.md#os-color_schemes-de-status). O esquema completo também carrega os papéis de superfície que o app usa para o "chrome" da página — `surface` / `on_surface`, `background` / `on_background`, `outline`, `surface_variant` e seus `on_*`. Leia qualquer um deles pelo `ColorRole` ou pela string: ```python from tempestroid import Color, ColorRole, Theme theme = Theme.from_seed(Color.from_hex("#2563eb")) surface = theme.color(ColorRole.SURFACE) on_surface = theme.color(ColorRole.ON_SURFACE) outline = theme.color("outline") # string também resolve ``` !!! note "Sementes de acento à mão" `from_seed` deriva secundário/terciário girando o tom da semente. Quer escolher cada acento? Passe `secondary_seed` / `tertiary_seed` / `error_seed`, todos `Color`. ## Claro e escuro com `ThemeMode` O modo do tema decide qual esquema (claro ou escuro) os papéis resolvem. O mesmo app, só trocando o `ThemeMode`, vira: ![Kit com tema claro](../../assets/design-system/kit-light.png){ width=280 } ![O mesmo kit com tema escuro](../../assets/design-system/kit-dark.png){ width=280 } São três opções: === "Forçar claro/escuro" ```python from tempestroid import Color, Theme, ThemeMode light = Theme.from_seed(Color.from_hex("#2563eb"), mode=ThemeMode.LIGHT) dark = Theme.from_seed(Color.from_hex("#2563eb"), mode=ThemeMode.DARK) print(light.color("background").to_hex()) # superfície clara print(dark.color("background").to_hex()) # superfície escura ``` === "Seguir o sistema" ```python from tempestroid import Color, Theme, ThemeMode # SYSTEM resolve contra o dark mode da plataforma em tempo de build. theme = Theme.from_seed(Color.from_hex("#2563eb"), mode=ThemeMode.SYSTEM) print(theme.is_dark(platform_dark_mode=True)) # True quando o SO está escuro ``` `ThemeMode.SYSTEM` é o padrão: o app acompanha a configuração do aparelho. Para trocar o tema em tempo de execução (um botão "dark mode" no app), use `App.set_theme(...)` — veja o exemplo `examples/theming/app.py`. ## As escalas sistemáticas Além das cores, o `TokenSet` traz escalas Material 3 nomeadas — você pede `"md"` em vez de decorar `16.0`: | Escala | Acesso | Passos | |---|---|---| | **Espaçamento** (grade 4dp) | `theme.space(name)` | `none` `xs` `sm` `md` `lg` `xl` `xxl` | | **Forma** (raio) | `theme.radius(name)` | `none` `xs` `sm` `md` `lg` `xl` `full` | | **Tipografia** | `theme.typography(role)` | `display_*` `headline_*` `title_*` `body_*` `label_*` | | **Elevação** | `theme.elevation(level)` | níveis `0`–`5`, em dp | | **Movimento** | `theme.tokens.motion` | durações + curvas de easing | ```python from tempestroid import Color, Edge, Style, Theme theme = Theme.from_seed(Color.from_hex("#2563eb")) # Componha um Style a partir de tokens, não de números mágicos: title = theme.typography("title_large") card = Style( background=theme.color("surface_variant"), padding=Edge.all(theme.space("md")), radius=theme.radius("lg"), font_size=title.font_size, font_weight=title.font_weight, ) ``` !!! tip "`radius('full')` é a pílula" O passo `full` usa o sentinela `999.0`; o renderizador o interpreta como um formato totalmente arredondado (pílula/círculo) e faz o *clamp* ao tamanho real da caixa. ## Como um componente lê o tema A parte importante: **um componente resolve seu `Style` contra o `theme` que você entrega a ele.** Os componentes estilizados (`Button`, `Input`, `Checkbox`, …) aceitam um parâmetro `theme`. Passe sempre o tema vivo do app e o componente adapta a aparência — incluindo o dark mode — sem você reescrever uma cor: ```python from tempestroid import Button, Color, Theme, Variant theme = Theme.from_seed(Color.from_hex("#2563eb")) # O Button resolve background/color/raio a partir do tema dado: salvar = Button( label="Salvar", variant=Variant.SOLID, color_scheme="primary", theme=theme, ) print(salvar.style.background) # já é a cor do papel "primary" do tema ``` Trocou o `theme` por um escuro? O mesmo `Button` resolve cores escuras. É por isso que a galeria de exemplo (`examples/h2gallery/app.py`) passa `theme=app.theme` para cada componente: o app inteiro segue um único tema. !!! info "Tokens são aditivos — `Style` cru continua valendo" Nada disso quebra o que você já tem. Um `Style(background=Color.from_hex(...))` escrito à mão continua funcionando. O tema é uma fonte **alternativa e opcional** de valores. Você pode até carregar um `TokenRef` dentro de um `Style` (`Style(background=TokenRef.color("primary"))`) e deixar o tema resolvê-lo na hora do build via `theme.resolve_style(...)`. ## Recapitulando - Um **`Theme`** carrega um **`TokenSet`** Material 3: esquemas de cor + escalas de espaçamento/forma/tipografia/elevação/movimento. - **`Theme.from_seed("#rrggbb")`** transforma uma cor de marca em uma paleta M3 completa (claro + escuro), com contraste garantido. - As cores são **papéis semânticos** (`primary`/`secondary`/`tertiary`/`error`/ `neutral` + seus `on_*` e os papéis de superfície), não cores cruas. - **`ThemeMode`** (`LIGHT`/`DARK`/`SYSTEM`) decide qual esquema resolve; troque em runtime com `App.set_theme`. - Peça escalas por nome — `theme.space("md")`, `theme.radius("lg")`, `theme.typography("title_large")` — em vez de números mágicos. - **Um componente resolve contra o `theme` que você passa**: entregue `theme=app.theme` e tudo segue o tema do app, dark mode incluído. A seguir: a [API de variantes ao estilo Chakra](variantes.md), onde `variant`/`size`/`color_scheme` viram o jeito ergonômico de pedir um `Style` resolvido pelo tema. --- # File: docs/guia/design-system/variantes.md — https://mauriciobenjamin700.github.io/tempestroid/guia/design-system/variantes/ # Variantes ao estilo Chakra Com o [tema e os tokens](tokens.md) no lugar, você não precisa montar um `Style` a cada botão. Os componentes estilizados expõem uma API de **variantes** com a ergonomia do Chakra UI — três props (`variant` / `size` / `color_scheme`) — e o motor resolve, a partir do tema, um `Style` Material 3 completo, com estados de interação e alvo de toque acessível. Você descreve a *intenção*; o design system calcula os pixels. !!! info "Onde os nomes moram" `variant`/`size`/`color_scheme` são props comuns; os **enums** `Variant`, `Size`, `ComponentState` e o widget `IconButton` são re-exportados por **`tempestroid`**, junto com `Button`, `Theme` e `Color`. Importe tudo de um único lugar — `tempest_core` é só o motor por baixo. ## O `Button` em uma linha ```python from tempestroid import Button, Color, Size, Theme, Variant theme = Theme.from_seed(Color.from_hex("#2563eb")) salvar = Button( label="Salvar", variant=Variant.SOLID, size=Size.MD, color_scheme="primary", theme=theme, on_click=lambda: print("salvo!"), ) ``` Sem nenhuma prop além do `label`, você ganha um botão **solid / md / primary** — os padrões. Tudo o mais é opcional e aditivo. !!! tip "Override ainda funciona" Passou um `style=` explícito? Ele é **mesclado por cima** do estilo resolvido (os campos definidos vencem). Variantes não tiram o controle fino de você — só dispensam você de escrevê-lo quando não precisa. ## As quatro variantes A prop `variant` (enum `Variant`) escolhe o *tratamento visual*, espelhando o Material 3: | `Variant` | Aparência | |---|---| | `SOLID` | preenchido com a cor do papel + conteúdo `on_*` legível (a ação de maior ênfase) | | `OUTLINE` | fundo transparente, cor do papel como conteúdo **e** borda da mesma cor | | `GHOST` | fundo transparente, cor do papel como conteúdo, **sem** borda | | `LINK` | igual ao `ghost`, mais um sublinhado (parece um link de texto) | ```python from tempestroid import Button, Color, Row, Theme, Variant theme = Theme.from_seed(Color.from_hex("#2563eb")) Row( style=..., children=[ Button(label=v.value, variant=v, color_scheme="primary", theme=theme, key=v.value) for v in Variant # SOLID, OUTLINE, GHOST, LINK ], ) ``` ![As quatro variantes de Button no simulador Qt](../../assets/design-system/variants-buttons.png){ width=320 } *As quatro variantes (`examples/h1buttons`) nos tamanhos `md`/`lg`, renderizadas no simulador Qt.* ## Tamanhos e o alvo de toque de 48dp A prop `size` (enum `Size`) controla a **densidade visual** — padding e tamanho de fonte vêm das escalas do tema: | `Size` | Densidade | |---|---| | `XS` | mais compacto | | `SM` | compacto | | `MD` | padrão | | `LG` | maior | !!! check "Acessibilidade embutida" Por menor que seja o `size`, o **alvo de toque nunca fica abaixo de 48dp** (o mínimo do Material 3). Um `XS` só reduz a aparência; a área tocável continua acessível. O contraste WCAG-AA entre papel e `on_*` também é garantido pelos tokens. ## Esquema de cor A prop `color_scheme` escolhe a família de papéis Material 3 com que o componente pinta — uma das de ênfase (`"primary"`, `"secondary"`, `"tertiary"`, `"error"`, `"neutral"`) ou de **status** (`"success"`, `"warning"`, `"info"`, adicionadas pelo design system — veja [data display e feedback](feedback.md#os-color_schemes-de-status)) (a [tabela de papéis](tokens.md#os-papeis-de-cor-color-schemes) tem o resumo). ```python from tempestroid import Button, Color, Theme, Variant theme = Theme.from_seed(Color.from_hex("#2563eb")) excluir = Button(label="Excluir", variant=Variant.SOLID, color_scheme="error", theme=theme) cancelar = Button(label="Cancelar", variant=Variant.OUTLINE, color_scheme="neutral", theme=theme) ``` !!! warning "Esquema inválido falha cedo" Passar um `color_scheme` fora das famílias conhecidas falha na construção do componente — você descobre o erro na hora, não na renderização. ## Estados de interação (state layers M3) Cada componente resolve não só o estilo de repouso, mas a tabela completa de **estados de interação** — `default` / `hover` / `pressed` / `focus` / `disabled` — como *state layers* do Material 3 (a cor de conteúdo sobreposta ao fundo nas opacidades M3). O componente entrega essa tabela ao renderizador, que aplica o estilo certo nos eventos reais de ponteiro/foco: ```python from tempestroid import Button, Color, Theme, Variant theme = Theme.from_seed(Color.from_hex("#2563eb")) botao = Button(label="Salvar", variant=Variant.SOLID, color_scheme="primary", theme=theme) states = botao.state_styles() # {ComponentState.DEFAULT: Style(...), ComponentState.HOVER: Style(...), ...} print(sorted(s.value for s in states)) # ['default', 'disabled', 'focus', 'hover', 'pressed'] ``` !!! note "A resolução é pura; o evento→estado fica no renderizador" `state_styles()` é uma função pura no motor — mesmos inputs, mesma saída — e é fixada na suíte de conformância. Cada renderizador só mapeia o evento real para o estado: o **Qt** usa pseudo-estados de QSS; o **Compose** usa `InteractionSource` + os state layers nativos do Material 3. ## Tamanho responsivo `size` aceita também um mapa por breakpoint (estilo Chakra), resolvido *mobile-first* contra os breakpoints do tema e a largura atual da viewport: ```python from tempestroid import Button, Color, Size, Theme theme = Theme.from_seed(Color.from_hex("#2563eb")) responsivo = Button( label="Continuar", size={"base": Size.SM, "md": Size.LG}, # SM no celular, LG a partir de md color_scheme="primary", theme=theme, ) ``` A chave `"base"` é o tamanho de partida (largura 0); a partir de cada breakpoint nomeado (`sm`/`md`/`lg`/`xl`) o tamanho daquele breakpoint vence quando a viewport o alcança. O app passa a viewport via `media=` (um `MediaQueryData`); o runtime já mantém esse contexto atualizado. ## Exemplo completo: a vitrine de variantes O exemplo `examples/h1buttons/app.py` desenha a matriz de variantes ao vivo (e um contador de toques para provar o caminho tap → handler → patch). Eis o coração dele — um app `tempest` rodável de ponta a ponta: ```python from __future__ import annotations from dataclasses import dataclass from tempestroid import ( App, Button, Color, Column, Row, Size, Style, Text, Theme, Variant, Widget, ) @dataclass class State: taps: int = 0 def make_state() -> State: return State() def view(app: App[State]) -> Widget: theme = Theme.from_seed(Color.from_hex("#2563eb")) def bump() -> None: app.set_state(lambda s: setattr(s, "taps", s.taps + 1)) def variant_row(variant: Variant) -> Widget: return Row( style=Style(gap=12.0), key=f"row:{variant.value}", children=[ Button( label=f"{variant.value} {size.value}", on_click=bump, variant=variant, size=size, color_scheme="primary", theme=theme, key=size.value, ) for size in (Size.MD, Size.LG) ], ) return Column( style=Style(gap=16.0), children=[ Text(content=f"toques: {app.state.taps}", key="taps"), variant_row(Variant.SOLID), variant_row(Variant.OUTLINE), variant_row(Variant.GHOST), variant_row(Variant.LINK), ], ) def main() -> int: # Importe o renderizador Qt de forma preguiçosa — o aparelho carrega # view/make_state deste mesmo arquivo e não tem PySide6. from tempestroid.renderers.qt import run_qt return run_qt(make_state(), view, title="H1 buttons", size=(420, 520)) if __name__ == "__main__": raise SystemExit(main()) ``` Rode no simulador Qt: ```bash uv run python examples/h1buttons/app.py # ou: make run APP=examples/h1buttons/app.py ``` No aparelho, o mesmo `view`/`make_state` é carregado pelo host Compose; cada variante mapeia para sua *affordance* Material 3 (filled / outlined / text), e o Material 3 fornece os state layers nativos de press/hover/focus sobre as cores resolvidas. ## Recapitulando - A API de variantes é **três props**: `variant` (`SOLID`/`OUTLINE`/`GHOST`/ `LINK`), `size` (`XS`/`SM`/`MD`/`LG`) e `color_scheme` (as famílias de ênfase M3 + as de status `success`/`warning`/`info`). - O motor resolve, a partir do `theme`, um `Style` M3 completo — você descreve a intenção, não os pixels. - O **alvo de toque ≥ 48dp** e o **contraste WCAG-AA** são garantidos; um `size` menor muda só a densidade visual. - Cada componente entrega a tabela de **estados** (`state_styles()`) como state layers M3; o renderizador aplica o estado no evento real (Qt QSS / Compose `InteractionSource`). - `size` aceita um **mapa responsivo** (`{"base": Size.SM, "md": Size.LG}`), resolvido mobile-first contra os breakpoints do tema. - `style=` explícito ainda é mesclado por cima — nada é tirado de você. A seguir: o [kit de ação e entrada](kit.md) — `IconButton`, a família de campos (`Input`/`Dropdown`/`Autocomplete`), os controles de seleção e os inputs BR. --- # File: docs/guia/design-system/kit.md — https://mauriciobenjamin700.github.io/tempestroid/guia/design-system/kit/ # Kit de ação e entrada A [API de variantes](variantes.md) que você viu no `Button` é a **mesma** em todo o kit estilizado: botões de ícone, a família de campos de texto, os controles de seleção e os sliders. Cada um carrega `size` / `color_scheme` (a família de campos adiciona `field_variant`) e resolve seu `Style` Material 3 contra o `theme` que você passa. Esta página percorre o kit. ![O kit H2 no simulador Qt, tema claro](../../assets/design-system/kit-light.png){ width=300 } ![O mesmo kit em dark mode](../../assets/design-system/kit-dark.png){ width=300 } *O exemplo `examples/h2gallery` no simulador Qt: o mesmo código segue o tema do app — claro à esquerda, escuro à direita.* !!! info "Onde os nomes moram" Tudo do kit é importado de **`tempestroid`**: os widgets (`Input`, `Checkbox`, `Switch`, `Slider`, `RadioGroup`, `IconButton`, os inputs BR), os enums `Size`/`Variant`/`FieldVariant` e o `Theme`/`Color`. `tempest_core` é apenas o motor por baixo — você não precisa importá-lo. ## O padrão `theme=app.theme` A regra de ouro do kit: **passe sempre o tema vivo do app para cada componente.** Como cada componente resolve sua aparência contra o `theme` que recebe, entregar `theme=app.theme` faz o kit inteiro seguir o tema do app — inclusive o dark mode trocado em runtime via `App.set_theme`. ```python from tempestroid import App, Button, Variant, Widget def view(app: App) -> Widget: return Button( label="Salvar", variant=Variant.SOLID, color_scheme="primary", theme=app.theme, # ← segue o tema do app, dark mode incluído ) ``` A vitrine `examples/h2gallery/app.py` faz exatamente isso em todo componente — é por isso que a galeria escurece junto quando o app entra em dark mode. ## `IconButton` Um botão só de ícone, com a mesma API de variantes do `Button` — só que quadrado e circular, com o alvo de toque de 48dp. O padrão é `GHOST` (o tratamento de menor ênfase, focado no ícone). O `label` carrega o **nome acessível** (`contentDescription`), já que não há texto visível. ```python from tempestroid import Color, IconButton, Theme, Variant theme = Theme.from_seed(Color.from_hex("#2563eb")) adicionar = IconButton( icon="add", label="Adicionar item", # nome acessível (a11y) variant=Variant.SOLID, color_scheme="primary", theme=theme, on_click=lambda: print("clicou"), ) ``` !!! tip "Ícones curados + apelidos Material" O `icon` aceita um valor curado de `Icons` (ou sua string) — `"add"`, `"search"`, `"eye"`, `"trash"`, `"settings"`… — ou um nome de ícone arbitrário da plataforma. O simulador Qt mapeia nomes Material comuns (`photo_camera`, `history`, `person`…) para os glifos curados; o aparelho usa os ícones nativos. ## A família de campos Os campos de texto compartilham a prop `field_variant` (enum `FieldVariant`) — um tratamento de baixa ênfase no repouso, em que o `color_scheme` só tinge o foco/cursor/borda: | `FieldVariant` | Repouso | |---|---| | `OUTLINE` | borda completa na cor `outline` (o padrão) | | `FILLED` | preenchimento tonal (`surface_variant`), sem borda | | `FLUSHED` | apenas uma régua inferior | ```python from tempestroid import Color, Column, FieldVariant, Input, Size, Style, Theme, Widget from tempestroid.widgets import TextChangeEvent theme = Theme.from_seed(Color.from_hex("#2563eb")) def inputs(on_change) -> Widget: # on_change: callable recebendo TextChangeEvent return Column( style=Style(gap=8.0), children=[ Input( value="", placeholder=f"{fv.value} field", on_change=on_change, field_variant=fv, size=Size.MD, color_scheme="primary", theme=theme, key=fv.value, ) for fv in FieldVariant # OUTLINE, FILLED, FLUSHED ], ) ``` Toda a família de campos compartilha essas props: `Input`, `TextArea`, `Dropdown`, `Autocomplete`, `MaskedInput`, `PinInput`, `DatePicker`, `TimePicker`, `FilePicker`. !!! check "Estado inválido = papel `error`" Passe um `error="mensagem"` num `Input` e o campo resolve a borda/label no papel `error` em qualquer estado — o foco ainda engrossa a borda para 2px, de modo que o campo ativo leia como "focado e errado". O estado de foco tinge a borda no acento do `color_scheme`. ## Controles de seleção `Checkbox`, `Switch` e `RadioGroup` carregam o acento via `color_scheme` (sem `variant` — o Material 3 dá a cada controle de seleção uma única *affordance*): ```python from tempestroid import ( Checkbox, Color, Column, RadioGroup, Size, Style, Switch, Theme, Widget, ) from tempestroid.widgets import ToggleEvent theme = Theme.from_seed(Color.from_hex("#2563eb")) def selections(on_toggle, on_pick) -> Widget: return Column( style=Style(gap=8.0), children=[ Checkbox( label="Aceito os termos", checked=True, on_change=on_toggle, # recebe um ToggleEvent color_scheme="primary", theme=theme, key="chk", ), Switch( label="Notificações", checked=False, on_change=on_toggle, color_scheme="secondary", theme=theme, key="sw", ), RadioGroup( options=["Free", "Pro", "Team"], selected=0, on_select=on_pick, # recebe o índice (int) color_scheme="primary", theme=theme, key="radio", ), ], ) ``` ## `Slider` O slider pinta a faixa ativa + o thumb no acento do `color_scheme`; `size` controla a espessura da faixa: ```python from tempestroid import Color, Size, Slider, Theme from tempestroid.widgets import SlideEvent theme = Theme.from_seed(Color.from_hex("#2563eb")) volume = Slider( value=40.0, min_value=0.0, max_value=100.0, on_change=lambda e: print(e.value), # SlideEvent size=Size.MD, color_scheme="primary", theme=theme, ) ``` !!! note "Divergência Qt × Compose documentada" Para seleção e slider, o `color_scheme` controla **só a cor** — a geometria é fixa pelo Material 3 (a forma do checkbox, o trilho do switch, o thumb do slider). Os dois renderizadores casam na cor resolvida; a *affordance* nativa de cada plataforma fica idêntica em forma. Veja a [cobertura de renderizadores](../../referencia/cobertura.md) para a tabela completa de divergências. ## Inputs brasileiros Sobre a família de campos, o tempestroid oferece campos rotulados prontos para formulários BR — cada um lança ao seu `on_change` o **valor string** já mascarado/validado, sem você tocar no objeto de evento: ```python from tempestroid import ( CNPJInput, CPFInput, Column, EmailInput, PasswordInput, PhoneInput, Style, Theme, Widget, ) def br_form(theme: Theme, on_change) -> Widget: # on_change: callable(str) return Column( style=Style(gap=12.0), children=[ EmailInput(value="", on_change=on_change, theme=theme, key="email"), PasswordInput(value="", on_change=on_change, theme=theme, key="pwd"), PhoneInput(value="", on_change=on_change, theme=theme, key="phone"), CPFInput(value="", on_change=on_change, theme=theme, key="cpf"), CNPJInput(value="", on_change=on_change, theme=theme, key="cnpj"), ], ) ``` | Input BR | Faz | |---|---| | `EmailInput` | teclado de e-mail + ícone de correio + validação de padrão | | `PasswordInput` | campo seguro com o toggle de "olho" embutido | | `PhoneInput` | máscara brasileira `(99) 99999-9999` | | `CPFInput` | máscara `999.999.999-99` | | `CNPJInput` | máscara `99.999.999/9999-99` | !!! tip "Validadores prontos" Combine cada campo BR com o validador correspondente em `tempestroid.validators` (`validate_email`, `validate_phone`, `validate_cpf`, `validate_cnpj`) para preencher o `error` e bloquear o submit inválido. ## Exemplo completo: a galeria do kit `examples/h2gallery/app.py` desenha o kit inteiro — Buttons + IconButtons, os três `field_variant`, checkbox, switch, radio e slider — todos passando `theme=app.theme`, dentro de um `ScrollView` para caber no celular: ```bash uv run python examples/h2gallery/app.py # ou: make run APP=examples/h2gallery/app.py ``` No aparelho, o mesmo `view`/`make_state` é carregado pelo host Compose; cada componente mapeia para sua *affordance* Material 3 (`OutlinedTextField` / `TextField` preenchido / `Checkbox` / `Switch` / `Slider` / `FilledIconButton` …) sobre as cores resolvidas. ## Recapitulando - O kit estilizado compartilha a API de variantes: `size` / `color_scheme` em todos, `field_variant` na família de campos. - **Passe `theme=app.theme` em cada componente** — é o que faz o kit seguir o tema do app, dark mode incluído. - `IconButton` é botão só de ícone (padrão `GHOST`); o `label` é o nome acessível; o `icon` aceita os ícones curados ou um nome de plataforma. - A família de campos resolve com `field_variant` (`OUTLINE`/`FILLED`/ `FLUSHED`), é *focus-led*, e um `error` força o papel `error`. - Seleção (`Checkbox`/`Switch`/`RadioGroup`) e `Slider` acentuam pelo `color_scheme` — **cor apenas**; a geometria M3 é fixa (divergência Qt×Compose documentada). - Os inputs BR (`EmailInput`/`PasswordInput`/`PhoneInput`/`CPFInput`/`CNPJInput`) entregam o valor string mascarado/validado direto ao `on_change`. A seguir: [superfície e layout](superficie.md) — `Card`, `Surface`, os helpers de pilha (`HStack`/`VStack`/`Spacer`), `Divider`/`ListTile`. Para o catálogo completo de widgets e a referência de API, veja a [visão geral de widgets](../widgets.md) e a [API pública](../../referencia/api.md). --- # File: docs/guia/design-system/superficie.md — https://mauriciobenjamin700.github.io/tempestroid/guia/design-system/superficie/ # Superfície e layout O [kit de ação e entrada](kit.md) deu ergonomia aos controles. Agora falta a **moldura**: os cartões, as superfícies e os espaçadores que organizam a tela. O tempestroid traz essa camada com a **mesma** API de variantes que você já conhece — só que aqui o eixo é a *elevação* Material 3, não a ênfase do botão. ![A galeria H3 no simulador Qt](../../assets/examples/h3gallery.png){ width=300 } *O exemplo `examples/h3gallery` no simulador Qt: as três variantes de `Card`, um painel tingido com `ListTile` + `Divider`, e uma `Surface` crua.* !!! info "Onde os nomes moram" Tudo desta página é importado de **`tempestroid`**: os widgets (`Card`, `Surface`, `HStack`, `VStack`, `Spacer`, `Divider`, `ListTile`), o enum `CardVariant` e o `Theme`/`Color`. `tempest_core` é só o motor por baixo — você não importa ele. ## `Card` e as três variantes Material 3 Um `Card` agrupa conteúdo numa superfície com cantos arredondados e *padding* interno. A prop `variant` (enum `CardVariant`) escolhe o tratamento M3 — e, como no `Button`, o motor resolve o `Style` a partir do `theme`, sem você fixar cor nenhuma: | `CardVariant` | Tratamento | |---|---| | `ELEVATED` | superfície + **sombra** (a elevação vira um `Shadow`) — o padrão | | `FILLED` | preenchimento tonal (`surface_variant`), sem sombra | | `OUTLINED` | borda fina na cor `outline`, fundo da superfície | ```python from tempestroid import Card, CardVariant, Text, Widget def cards(theme) -> Widget: # theme: Theme return Card( variant=CardVariant.ELEVATED, theme=theme, children=[ Text(content="Título"), Text(content="Conteúdo do cartão"), ], ) ``` !!! tip "Os passos de espaçamento vêm do tema" `Card` carrega `padding_step` / `radius_step` / `gap_step` (padrão `"md"` / `"md"` / `"sm"`) — passos da escala 4dp do tema, não pixels soltos. Troque para `"sm"` ou `"lg"` e o cartão respira de acordo com o resto do app. ### Tingindo um cartão com `color_scheme` Um `Card` aceita `color_scheme` para tingir a superfície num [papel de cor](tokens.md#os-papeis-de-cor-color-schemes) (o padrão é `"neutral"`). Útil para destacar um painel sem sair do tema: ```python from tempestroid import Card, CardVariant, Text, Widget def painel(theme) -> Widget: # theme: Theme return Card( variant=CardVariant.ELEVATED, color_scheme="primary", # superfície tingida no acento theme=theme, children=[Text(content="Painel em destaque")], ) ``` ## `Surface` — a primitiva crua `Card` é uma conveniência sobre `Surface`: a `Surface` aplica a **mesma** resolução de variante (`ELEVATED`/`FILLED`/`OUTLINED` + `color_scheme` + `radius_step`), mas **sem o padding interno** e segurando **um único** filho (`child`, não `children`). Use quando você quer controlar o espaçamento por conta própria: ```python from tempestroid import CardVariant, Surface, Text, Widget def superficie(theme) -> Widget: # theme: Theme return Surface( variant=CardVariant.FILLED, theme=theme, child=Text(content="Superfície preenchida, sem padding interno"), ) ``` !!! note "Card constrói sobre Surface" Pense no `Card` como `Surface` + padding + um `Column` dos seus `children`. Quando o padding embutido do `Card` não serve, desça para a `Surface` e monte o miolo você mesmo. ## Helpers de pilha: `HStack`, `VStack`, `Spacer` Para o arranjo do dia a dia, os helpers de pilha são `Row`/`Column` com **gaps nomeados do tema** em vez de um número de pixels. `HStack` empilha na horizontal, `VStack` na vertical; o `gap` aceita um passo da escala de espaçamento (`"xs"`/`"sm"`/`"md"`/`"lg"`/`"xl"`): ```python from tempestroid import HStack, Spacer, Text, VStack, Widget def barra(theme) -> Widget: # theme: Theme return HStack( gap="md", theme=theme, children=[ Text(content="Início"), Spacer(), # empurra o que vem depois para a borda oposta Text(content="Configurações"), ], ) def coluna(theme) -> Widget: # theme: Theme return VStack( gap="sm", theme=theme, children=[Text(content="Linha 1"), Text(content="Linha 2")], ) ``` `Spacer` é o espaço elástico: ele cresce para preencher o eixo principal, então um `Spacer` entre dois filhos de um `HStack` joga o segundo para a borda contrária. Controle a proporção com `flex` (padrão `1.0`). ## `Divider` e `ListTile` temáticos `Divider` é uma régua fina que segue a cor `outline` do tema (ou um `color_scheme` que você passe); `ListTile` é a linha clássica de lista — `title` + `subtitle` + slots `leading`/`trailing` (que aceitam qualquer widget, como um `Avatar`): ```python from tempestroid import Avatar, Divider, ListTile, VStack, Widget def lista(theme) -> Widget: # theme: Theme return VStack( gap="xs", theme=theme, children=[ ListTile( title="Maria Silva", subtitle="maria@example.com", leading=Avatar(label="MS"), theme=theme, ), Divider(theme=theme), ListTile(title="João Souza", subtitle="joao@example.com", theme=theme), ], ) ``` ## Exemplo completo: a galeria de superfície `examples/h3gallery/app.py` junta tudo — as três variantes de `Card` lado a lado, um cartão tingido com `ListTile` + `Divider` + uma linha de ação que usa `Spacer` para empurrar o botão à borda, e uma `Surface` crua: ```bash uv run python examples/h3gallery/app.py # ou: make run APP=examples/h3gallery/app.py ``` No aparelho, o mesmo `view`/`make_state` carrega no host Compose: como `Card`, `Surface`, `HStack`, `VStack`, `Divider` e `ListTile` são **componentes compostos** (descem a primitivos via `Component.render`), eles renderizam pelos seus filhos primitivos nos **dois renderizadores** — sem um ramo Kotlin dedicado. !!! check "Divergência de superfície" A sombra do `ELEVATED` vira um `Shadow` resolvido pela elevação M3 e segue os dois tradutores `Style`. A geometria (raio, padding) sai dos passos do tema — idêntica nos dois renderizadores. Veja a [cobertura de renderizadores](../../referencia/cobertura.md) para a tabela completa. ## Recapitulando - `Card` agrupa conteúdo numa superfície M3; `variant` escolhe `ELEVATED` (sombra) / `FILLED` (tonal) / `OUTLINED` (borda) e o motor resolve o `Style` do tema. - `padding_step`/`radius_step`/`gap_step` vêm da **escala 4dp** do tema, não de pixels soltos; `color_scheme` tinge a superfície num papel de cor. - `Surface` é a primitiva crua que o `Card` usa — mesma resolução de variante, **sem padding** e com **um** `child`. - `HStack`/`VStack` são `Row`/`Column` com **gap nomeado do tema**; `Spacer` cresce para empurrar os vizinhos. - `Divider`/`ListTile` seguem o tema (linha + a clássica linha de lista com `leading`/`trailing`). - Tudo é **componente composto** → renderiza pelos primitivos nos dois renderizadores. A seguir: [data display e feedback](feedback.md) — `Alert`/`Banner`, a família `Badge`/`Chip`/`Tag`, `Stat`, `ProgressStepper` e os `color_scheme`s de status. --- # File: docs/guia/design-system/feedback.md — https://mauriciobenjamin700.github.io/tempestroid/guia/design-system/feedback/ # Data display e feedback Você já tem ação ([variantes](variantes.md), [kit](kit.md)) e moldura ([superfície](superficie.md)). Falta **conversar com o usuário**: avisar que algo deu certo, alertar de um erro, mostrar uma métrica, indicar o progresso. Esta página fecha o design system com a camada de *data display* + *feedback* — e introduz os `color_scheme`s de **status**. ![A galeria H4 no simulador Qt](../../assets/examples/h4gallery.png){ width=300 } *O exemplo `examples/h4gallery` no simulador Qt: alertas de status, a família de badges, `Stat`, `ProgressStepper`, `ProgressBar` e um `Banner`.* !!! info "Onde os nomes moram" Tudo desta página é importado de **`tempestroid`**: os widgets (`Alert`, `Banner`, `Badge`, `Chip`, `Tag`, `Stat`, `ProgressStepper`, `ProgressBar`), os enums `AlertVariant`/`BadgeVariant` e o `Theme`/`Color`. ## Os `color_scheme`s de status Até aqui você viu os papéis de ênfase: `primary`, `secondary`, `tertiary`, `error` e `neutral`. O design system adiciona **três papéis de status** de primeira classe — famílias tonais M3 completas, com seus pares `on_*` gerados para contraste WCAG-AA: | `color_scheme` | Significa | Uso típico | |---|---|---| | `"success"` | deu certo | confirmação, salvo, válido | | `"warning"` | atenção | aviso reversível, limite próximo | | `"info"` | informação | dica neutra, novidade | Eles entram em **qualquer** componente que aceite `color_scheme` — exatamente como os papéis de ênfase. Junto com `"error"`, são o vocabulário de feedback. !!! check "Cor + container, ambos AA" Cada papel de status carrega o par base/`on_*` **e** uma variante de *container* (fundo tonal claro + conteúdo escuro) que as variantes `SUBTLE` usam. Os dois pares são gerados para atingir contraste WCAG-AA no claro e no escuro — você escolhe o status, o motor garante a legibilidade. ## `Alert` e `Banner` Um `Alert` é a caixa de mensagem inline; um `Banner` é a faixa larga (topo de tela). Os dois carregam `color_scheme` (o status) e `variant` (enum `AlertVariant`) para o tratamento visual: | `AlertVariant` | Tratamento | |---|---| | `SUBTLE` | fundo tonal *container* + conteúdo escuro (o padrão) | | `SOLID` | fundo no papel cheio + conteúdo `on_*` | | `LEFT_ACCENT` | barra de acento à esquerda, fundo suave | | `TOP_ACCENT` | barra de acento no topo, fundo suave | ```python from tempestroid import Alert, AlertVariant, Widget def avisos(theme) -> Widget: # theme: Theme return Alert( title="Tudo certo", body="Suas alterações foram salvas.", color_scheme="success", variant=AlertVariant.SUBTLE, theme=theme, ) ``` ```python from tempestroid import AlertVariant, Banner, Widget def faixa(theme) -> Widget: # theme: Theme return Banner( message="Salvo na nuvem.", color_scheme="info", variant=AlertVariant.SOLID, theme=theme, ) ``` !!! tip "Dispensável" Passe um handler em `Alert(dismiss=...)` para mostrar o "x" de fechar; o `Banner` aceita um `action` (um widget, tipicamente um `Button`/`Chip`) na borda direita. ## A família `Badge` / `Chip` / `Tag` `Badge` é o rótulo compacto (contagem, status); `Chip` é o elemento interativo (filtro, seleção); `Tag` é um preset de `Chip` para etiquetas. O `Badge` carrega sua própria escala de variantes (enum `BadgeVariant`): | `BadgeVariant` | Tratamento | |---|---| | `SOLID` | fundo cheio no papel + texto `on_*` (o padrão) | | `SUBTLE` | fundo tonal *container* + texto escuro | | `OUTLINE` | só borda + texto na cor do papel | ```python from tempestroid import Badge, BadgeVariant, Chip, HStack, Tag, Widget def rotulos(theme) -> Widget: # theme: Theme return HStack( gap="sm", theme=theme, children=[ Badge(label="Novo", color_scheme="success", variant=BadgeVariant.SOLID, theme=theme), Badge(label="Beta", color_scheme="warning", variant=BadgeVariant.SUBTLE, theme=theme), Badge(label="3", color_scheme="info", variant=BadgeVariant.OUTLINE, theme=theme), Chip(label="Filtro", color_scheme="primary", theme=theme), Tag(label="Etiqueta", color_scheme="secondary", theme=theme), ], ) ``` !!! note "Chip e Tag são interativos" `Chip`/`Tag` carregam `selected` + `on_click` — toque alterna o estado de seleção. `Badge` é só display (sem handler). ## `Stat` — a métrica de KPI `Stat` mostra um número grande com rótulo e um *delta* opcional (a variação, verde para cima / vermelho para baixo via `delta_up`): ```python from tempestroid import HStack, Stat, Widget def metricas(theme) -> Widget: # theme: Theme return HStack( gap="md", theme=theme, children=[ Stat(label="Receita", value="R$ 12,4k", delta="+8,2%", delta_up=True, theme=theme), Stat(label="Churn", value="2,1%", delta="-0,4%", delta_up=False, theme=theme), ], ) ``` ## `ProgressStepper` e o acento da `ProgressBar` `ProgressStepper` desenha as etapas de um fluxo (carrinho → endereço → pagamento), com a etapa atual via `current`; `ProgressBar` ganhou um `color_scheme` para tingir a barra preenchida no acento do tema: ```python from tempestroid import ProgressBar, ProgressStepper, VStack, Widget def progresso(theme, etapa: int) -> Widget: # theme: Theme return VStack( gap="md", theme=theme, children=[ ProgressStepper( steps=["Carrinho", "Endereço", "Pagamento", "Pronto"], current=etapa, color_scheme="primary", theme=theme, ), ProgressBar(value=0.6, color_scheme="success"), ], ) ``` !!! tip "Mais display & feedback" Na mesma camada vivem `Avatar`, `EmptyState`, `SegmentedControl`, `Rating` e `Spinner` — todos seguem o tema e aceitam `color_scheme`. Veja o catálogo completo na [visão geral de widgets](../widgets.md) e na [API pública](../../referencia/api.md). ## Exemplo completo: a galeria de feedback `examples/h4gallery/app.py` desenha a camada inteira — os quatro status em `Alert`, a família de badges, dois `Stat`, o `ProgressStepper` (avança ao tocar o `Chip` "Next step"), uma `ProgressBar` e um `Banner`: ```bash uv run python examples/h4gallery/app.py # ou: make run APP=examples/h4gallery/app.py ``` No aparelho, o mesmo `view`/`make_state` carrega no host Compose: como toda essa camada é de **componentes compostos** (descem a primitivos via `Component.render`), eles renderizam pelos filhos primitivos nos **dois renderizadores**, sobre as cores de status resolvidas. ## Recapitulando - O design system promove **três papéis de status** a `color_scheme`s de primeira classe — `success` / `warning` / `info` (somando-se a `error`) — cada um com base/`on_*` + container, todos WCAG-AA. - `Alert` (inline) e `Banner` (faixa) carregam `color_scheme` + `AlertVariant` (`SUBTLE`/`SOLID`/`LEFT_ACCENT`/`TOP_ACCENT`). - `Badge` tem `BadgeVariant` (`SOLID`/`SUBTLE`/`OUTLINE`); `Chip`/`Tag` são interativos (`selected` + `on_click`). - `Stat` é a métrica com `delta`/`delta_up`; `ProgressStepper` desenha as etapas; `ProgressBar` aceita `color_scheme`. - Tudo é **componente composto** → renderiza pelos primitivos nos dois renderizadores. Você completou o design system: [tokens](tokens.md) → [variantes](variantes.md) → [kit](kit.md) → [superfície](superficie.md) → feedback. Para o catálogo completo de widgets e a referência de API, veja a [visão geral de widgets](../widgets.md) e a [API pública](../../referencia/api.md). --- # File: docs/guia/design-system/navegacao.md — https://mauriciobenjamin700.github.io/tempestroid/guia/design-system/navegacao/ # Navegação Você já tem ação ([variantes](variantes.md), [kit](kit.md)), moldura ([superfície](superficie.md)) e conversa ([feedback](feedback.md)). Falta a **casca de navegação**: a barra do topo, a barra de abas, a navegação inferior, a busca, o caminho de páginas. Esta página estiliza essa camada — todos os componentes resolvem as **superfícies M3** e o **acento do item ativo** a partir do `color_scheme`/`theme`, sem nenhuma cor escrita à mão. ![A galeria H5 no simulador Qt](../../assets/examples/h5gallery.png){ width=300 } *O exemplo `examples/h5gallery` no simulador Qt: `AppBar`, `Header`, `Breadcrumb`, `SearchBar`, `Tabs` e `NavBar` — todos tingidos pelo tema.* !!! info "Onde os nomes moram" Tudo desta página é importado de **`tempestroid`**: os componentes de navegação (`AppBar`, `CollapsingAppBar`, `NavBar`, `Drawer`, `Sidebar`, `Breadcrumb`, `Burger`, `Footer`, `Header`, `Scaffold`, `SearchBar`, `Tabs`) e o `Theme`. ## A `AppBar` e o `Header` A `AppBar` é a barra do topo — uma **superfície elevada** M3 com título, um widget `leading` (botão de voltar/menu) e uma lista de `actions` à direita. O `color_scheme` define o papel da superfície e do conteúdo: ```python from tempestroid import AppBar, Button, Variant, Widget def barra(theme) -> Widget: # theme: Theme return AppBar( title="Tempestroid", color_scheme="primary", actions=[ Button(label="Sair", variant=Variant.GHOST, theme=theme, key="out"), ], theme=theme, ) ``` O `Header` é o cabeçalho de conteúdo (não a barra do sistema): título grande + subtítulo, sobre a superfície da página. ```python from tempestroid import Header, Widget def cabecalho(theme) -> Widget: # theme: Theme return Header( title="Painel", subtitle="Visão geral do projeto", theme=theme, ) ``` !!! tip "Barra que colapsa ao rolar" `CollapsingAppBar` é a `AppBar` que encolhe conforme a tela rola: passe o `scroll_offset` (do `ScrollEvent`) e ela interpola entre `expanded_height` e `collapsed_height`. É o casamento da [app bar colapsável do E6](../widgets/layout.md) com os tokens do tema. ## `Tabs` — a faixa de abas M3 `Tabs` é a faixa de abas estilizada: uma lista de rótulos (`tabs`), o índice ativo (`active`) e um handler `on_select(index)`. A aba ativa ganha o **acento do `color_scheme`** mais um sublinhado: ```python from tempestroid import Tabs, Widget def abas(theme, ativa: int) -> Widget: # theme: Theme return Tabs( tabs=["Visão geral", "Atividade", "Ajustes"], active=ativa, on_select=lambda i: None, # troque o índice no seu estado color_scheme="primary", theme=theme, ) ``` !!! note "`Tabs` é a faixa; as telas são suas" `Tabs` só desenha e emite a seleção — ele não troca conteúdo sozinho. Guarde o índice no estado, renderize o corpo da aba ativa e atualize via `app.set_state`. Para uma pilha de telas com transição animada, use o [`TabView`/`Navigator` da navegação](../navegacao.md). ## `NavBar` — a navegação inferior `NavBar` é a barra de destinos (estilo "bottom navigation" do M3): rótulos via `items`, o destino ativo via `active`, e `on_select(index)`. O item ativo recebe a **pílula de acento** do `color_scheme`: ```python from tempestroid import NavBar, Widget def navegacao(theme, ativo: int) -> Widget: # theme: Theme return NavBar( items=["Início", "Buscar", "Perfil"], active=ativo, on_select=lambda i: None, color_scheme="primary", theme=theme, ) ``` ## `SearchBar` e `Breadcrumb` A `SearchBar` é o campo de busca M3 — um `field_variant` sobre a superfície, com `value`, `placeholder`, `on_change(texto)` e um `on_clear` opcional. O `Breadcrumb` é o caminho de páginas: uma lista de rótulos (`items`) com um `separator` e `on_select(index)`. ```python from tempestroid import Breadcrumb, SearchBar, VStack, Widget def busca_e_caminho(theme, consulta: str) -> Widget: # theme: Theme return VStack( gap="md", theme=theme, children=[ SearchBar( value=consulta, placeholder="Buscar…", on_change=lambda q: None, # guarde no estado color_scheme="primary", theme=theme, ), Breadcrumb( items=["Início", "Projetos", "Tempestroid"], on_select=lambda i: None, theme=theme, ), ], ) ``` !!! tip "O resto da casca" A mesma camada traz `Drawer`/`Sidebar` (gaveta lateral), `Burger` (o botão de menu), `Footer`, e o `Scaffold` (o esqueleto barra-superior + corpo + barra-inferior que junta tudo). Todos seguem o tema e aceitam `color_scheme`. Veja o catálogo completo na [visão geral de widgets](../widgets.md) e na [API pública](../../referencia/api.md). ## Exemplo completo: a galeria de navegação `examples/h5gallery/app.py` desenha a casca inteira — `AppBar`, `Header`, `Breadcrumb`, `SearchBar` (digite e o estado atualiza), `Tabs` (toque troca a aba ativa) e `NavBar` (toque troca o destino), todos tingidos pelo tema: ```bash uv run python examples/h5gallery/app.py # ou: make run APP=examples/h5gallery/app.py ``` O fonte completo está no [`examples/h5gallery/app.py`](https://github.com/mauriciobenjamin700/tempestroid/blob/main/examples/h5gallery/app.py). No aparelho, o mesmo `view`/`make_state` carrega no host Compose: como toda a camada é de **componentes compostos** (descem a primitivos via `Component.render`), eles renderizam pelos filhos primitivos nos **dois renderizadores**, sobre as superfícies e o acento resolvidos do tema. ## Recapitulando - A `AppBar` é a barra do topo (superfície elevada + `leading`/`actions`); o `Header` é o cabeçalho de conteúdo; `CollapsingAppBar` encolhe ao rolar via `scroll_offset`. - `Tabs` é a faixa de abas M3 (`tabs`/`active`/`on_select`), com acento + sublinhado na aba ativa — você guarda o índice e renderiza o corpo. - `NavBar` é a navegação inferior (`items`/`active`/`on_select`) com pílula de acento no destino ativo. - `SearchBar` é o campo de busca (`value`/`on_change`/`on_clear`); `Breadcrumb` é o caminho de páginas (`items`/`on_select`). - `Drawer`/`Sidebar`/`Burger`/`Footer`/`Scaffold` completam a casca — tudo segue o tema e o `color_scheme`. A seguir: os [componentes de pesquisa](pesquisa.md) — métricas, gráficos e a ponte com o `ort-vision-sdk`. --- # File: docs/guia/design-system/pesquisa.md — https://mauriciobenjamin700.github.io/tempestroid/guia/design-system/pesquisa/ # Componentes de pesquisa O design system foi pensado para que **pesquisadores acadêmicos** montem apps Android de validação com pouco esforço e visual profissional. Esta página fecha a vitrine com a camada **científica / data-science**: cartões de métrica, gráficos de dados, sobreposição de detecções e a tabela de resultados — a ponte direta com o [`ort-vision-sdk`](https://github.com/mauriciobenjamin700/ort-vision-sdk). ![A galeria H6 no simulador Qt](../../assets/examples/h6gallery.png){ width=300 } *O exemplo `examples/h6gallery` no simulador Qt: `DetectionOverlay`, `BarChart`, `LineChart`, `ConfidenceBadge`, `DataTable` e `MetricCard` — um dashboard de resultado de visão, todo tingido pelo tema.* !!! info "Onde os nomes moram" Tudo desta página é importado de **`tempestroid`**: os componentes (`MetricCard`, `StatCard`, `ConfidenceBadge`, `LineChart`, `BarChart`, `DetectionOverlay`, `ResultView`, `DataTable`, `Calendar`, `Clock`), os objetos de dados (`ChartSeries`, `DetectionBox`) e o helper `confidence_scheme`. ## `MetricCard` e `StatCard` — o KPI `MetricCard` é o cartão de métrica de uma tela de pesquisa: um número grande, um rótulo e um *delta* opcional (a variação, verde para cima / vermelho para baixo via `delta_up`). `StatCard` é o mesmo cartão em variante mais densa. ```python from tempestroid import HStack, MetricCard, Widget def metricas(theme) -> Widget: # theme: Theme return HStack( gap="md", theme=theme, children=[ MetricCard(label="Detecções", value="2", delta="+1", delta_up=True, color_scheme="primary", theme=theme, key="m1"), MetricCard(label="Classe top", value="banana", color_scheme="success", theme=theme, key="m2"), ], ) ``` ## `ConfidenceBadge` — a pílula de confiança `ConfidenceBadge` mostra um score de confiança como uma pílula colorida. Você passa `confidence` (um `float` em `[0,1]`) e um `label`; o componente escolhe o `color_scheme` pelo limiar via `confidence_scheme` — alto → `success`, médio → `warning`, baixo → `error` — sempre com contraste WCAG-AA. ```python from tempestroid import ConfidenceBadge, HStack, Widget def confiancas(theme) -> Widget: # theme: Theme return HStack( gap="sm", theme=theme, children=[ ConfidenceBadge(confidence=0.84, label="banana", theme=theme, key="c1"), ConfidenceBadge(confidence=0.41, label="apple", theme=theme, key="c2"), ], ) ``` !!! tip "O limiar é configurável" `confidence_scheme(conf, *, high=0.8, mid=0.5)` é o seletor compartilhado: é a mesma função que o `DetectionOverlay` usa para tingir as caixas. Chame-a direto se quiser o `color_scheme` (`"success"`/`"warning"`/`"error"`) de um score em outro componente. ## `LineChart` e `BarChart` — dados viram desenho Os gráficos transformam dados em comandos de `Canvas` (a mesma lista JSON da conformância, idêntica nos dois renderizadores). `BarChart` recebe `values` + `labels`; `LineChart` recebe uma lista de `ChartSeries` (cada série com `points`, `label` e `color_scheme`): ```python from tempestroid import BarChart, Widget def barras(theme) -> Widget: # theme: Theme return BarChart( values=[0.84, 0.41, 0.18, 0.09], labels=["banana", "apple", "pear", "lemon"], width=480.0, height=160.0, color_scheme="primary", theme=theme, ) ``` ```python from tempestroid import ChartSeries, LineChart, Widget def linha(theme) -> Widget: # theme: Theme return LineChart( series=[ ChartSeries( points=[920.0, 880.0, 860.0, 845.0, 830.0], label="latência ms", color_scheme="secondary", ), ], width=480.0, height=160.0, theme=theme, ) ``` ## `DetectionOverlay` — a ponte com o `ort-vision-sdk` `DetectionOverlay` desenha uma imagem com **caixas delimitadoras** por cima — exatamente a forma que um app de visão produz. Você passa `image_src` (caminho ou URL) e uma lista de `DetectionBox`, e o componente tinge cada caixa pela confiança (via `confidence_scheme`). ```python from tempestroid import DetectionBox, DetectionOverlay, Widget def deteccoes(theme) -> Widget: # theme: Theme return DetectionOverlay( image_src="/caminho/para/banana.jpg", boxes=[ DetectionBox(x1=0.18, y1=0.30, x2=0.82, y2=0.74, name="banana", conf=0.84), DetectionBox(x1=0.05, y1=0.05, x2=0.30, y2=0.22, name="apple", conf=0.41), ], width=320.0, height=240.0, theme=theme, ) ``` !!! tip "`DetectionBox` é normalizado — e o motor não conhece o SDK" Os campos `x1`/`y1`/`x2`/`y2` de um `DetectionBox` são **xyxy normalizado em `[0,1]`** (frações da largura/altura da imagem), não pixels — o `DetectionOverlay` escala para o tamanho que você der. O motor **não tem dependência do `ort-vision-sdk`**: o adaptador que converte o resultado do `Detector.predict(...)` em `DetectionBox`es mora no **seu app**. Você lê os boxes do SDK (em pixels), divide por largura/altura e monta a lista — o design system só desenha. ## `DataTable` — a tabela de resultados `DataTable` é a tabela estilizada com **ordenação e paginação**: `columns` + `rows` (uma lista de listas de células). Ela segue o tema e zebra as linhas. ```python from tempestroid import DataTable, Widget def tabela(theme) -> Widget: # theme: Theme return DataTable( columns=["Classe", "Conf"], rows=[["banana", "84%"], ["apple", "41%"], ["pear", "18%"]], theme=theme, ) ``` !!! tip "Mais componentes de pesquisa" A mesma camada traz `ResultView` (o invólucro imagem→resultado), e os utilitários de tempo `Calendar`/`Clock`. Todos seguem o tema. Veja o catálogo completo na [visão geral de widgets](../widgets.md) e na [API pública](../../referencia/api.md). ## Exemplo completo: o dashboard de visão `examples/h6gallery/app.py` desenha um dashboard de resultado de visão completo — `DetectionOverlay` com caixas sobre uma imagem real, dois `MetricCard`, a dupla de `ConfidenceBadge`, o `BarChart` de confiança por classe, o `LineChart` de latência e a `DataTable` de detecções: ```bash uv run python examples/h6gallery/app.py # ou: make run APP=examples/h6gallery/app.py ``` O fonte completo está no [`examples/h6gallery/app.py`](https://github.com/mauriciobenjamin700/tempestroid/blob/main/examples/h6gallery/app.py). No aparelho, o mesmo `view`/`make_state` carrega no host Compose; como toda a camada é de **componentes compostos**, os gráficos descem ao `Canvas` e as métricas aos primitivos nos **dois renderizadores**. ## Recapitulando - `MetricCard`/`StatCard` são o KPI (número + `delta`/`delta_up` + `color_scheme`). - `ConfidenceBadge` é a pílula de confiança; o limiar vem de `confidence_scheme(conf, *, high=0.8, mid=0.5)`, AA-safe. - `BarChart`/`LineChart` transformam dados (`values`/`labels` ou `ChartSeries`) em comandos de `Canvas` idênticos nos dois renderizadores. - `DetectionOverlay` desenha imagem + `DetectionBox`es **xyxy normalizado `[0,1]`**, tingidos por confiança — a ponte com o `ort-vision-sdk`, cujo adaptador é do app (o motor não depende do SDK). - `DataTable` é a tabela com ordenação/paginação; `ResultView`/`Calendar`/`Clock` completam a camada. A seguir: o [storybook (galeria)](storybook.md) — o sistema inteiro em um app só, com os toggles de claro/escuro e LTR/RTL. --- # File: docs/guia/design-system/storybook.md — https://mauriciobenjamin700.github.io/tempestroid/guia/design-system/storybook/ # Storybook (galeria) Você percorreu o design system inteiro: [tokens](tokens.md) → [variantes](variantes.md) → [kit](kit.md) → [superfície](superficie.md) → [feedback](feedback.md) → [navegação](navegacao.md) → [pesquisa](pesquisa.md). Esta é a **capstone**: um único app que reúne **todos** os componentes H1–H6 numa galeria navegável — o jeito de ver o sistema funcionando junto, e de provar que os toggles de tema re-pintam tudo de uma vez. ![O storybook no modo claro](../../assets/examples/storybook-light.png){ width=260 } ![O storybook no modo escuro](../../assets/examples/storybook-dark.png){ width=260 } ![O storybook em RTL](../../assets/examples/storybook-rtl.png){ width=260 } *O mesmo app `examples/storybook` no simulador Qt: **claro**, **escuro** e **RTL**. Os mesmos componentes, re-pintados pelos toggles — uma só fonte de verdade para o tema.* ## O que o storybook mostra `examples/storybook/app.py` é uma galeria estilo Storybook: - uma **`AppBar`** com dois toggles: **claro/escuro** e **LTR/RTL**; - uma faixa de **`Tabs`** que alterna entre categorias — **Action**, **Inputs**, **Surfaces**, **Feedback**, **Navigation** e **Research**; - um espécime representativo de **cada** componente H1–H6 dentro de sua categoria. Cada categoria mapeia para uma página deste guia: | Categoria | Componentes | Guia | |---|---|---| | Action | `Button`, `IconButton` (variantes/tamanhos) | [variantes](variantes.md) | | Inputs | `Input`, `Checkbox`, `Switch`, `Slider` | [kit](kit.md) | | Surfaces | `Card`, `Surface`, `Divider` | [superfície](superficie.md) | | Feedback | `Alert`, `Chip`, `ConfidenceBadge`, `ProgressBar` | [feedback](feedback.md) | | Navigation | `NavBar`, `Divider` (sob a `AppBar`/`Tabs`) | [navegação](navegacao.md) | | Research | `MetricCard`, `BarChart`, `DetectionOverlay` | [pesquisa](pesquisa.md) | ## Como rodar ```bash uv run python examples/storybook/app.py # ou: make run APP=examples/storybook/app.py ``` Toque nas abas para trocar de categoria; toque em **Dark**/**Light** e **RTL**/**LTR** na `AppBar` para re-pintar o sistema inteiro ao vivo. O fonte completo está no [`examples/storybook/app.py`](https://github.com/mauriciobenjamin700/tempestroid/blob/main/examples/storybook/app.py). ## A mágica: um tema, o sistema todo !!! note "Dark mode + RTL são propriedade do app, não de cada widget" Todo componente estilizado aceita um **`theme=`**. O app lê `app.theme` (claro/escuro) e `app.locale` (LTR/RTL) **como contexto** e os repassa a cada componente do `view`. Os toggles só chamam `app.set_theme(...)` / `app.set_locale(...)`; o rebuild coalescido reconstrói o `view` com o novo tema/locale, e **todos** os componentes resolvem suas cores e espelhamento de novo. É por isso que um toque re-pinta a galeria inteira — e é exatamente a **superfície de verificação de dark/RTL** do design system. O esqueleto do `view` do storybook é literalmente isto — lê o contexto, monta os toggles, despacha para a categoria ativa: ```python from tempestroid import App, Locale, Theme, ThemeMode, Widget def view(app: App[object]) -> Widget: # state: a categoria ativa theme = app.theme dark = theme.is_dark(platform_dark_mode=app.media.platform_dark_mode) rtl = app.locale.rtl def _toggle_dark() -> None: app.set_theme(Theme(mode=ThemeMode.LIGHT if dark else ThemeMode.DARK)) def _toggle_rtl() -> None: app.set_locale( Locale(language="pt", region="BR", rtl=False) if rtl else Locale(language="ar", region="EG", rtl=True) ) ... # AppBar(actions=[toggle dark, toggle RTL]) + Tabs + corpo da categoria ``` Cada espécime recebe esse mesmo `theme` — por exemplo, a categoria Action: ```python from tempestroid import Button, HStack, IconButton, Variant, Widget def action(theme) -> Widget: # theme: Theme return HStack( gap="sm", theme=theme, children=[ Button(label="Solid", variant=Variant.SOLID, theme=theme, key="b1"), Button(label="Outline", variant=Variant.OUTLINE, theme=theme, key="b2"), Button(label="Ghost", variant=Variant.GHOST, theme=theme, key="b3"), IconButton(icon="add", label="Add", theme=theme, key="ib"), ], ) ``` !!! tip "Use o storybook como banco de provas" Quando você adicionar um componente ou ajustar um token, abra o storybook e alterne claro/escuro + LTR/RTL: se a cor resolve e o layout espelha em todas as combinações, o componente está conforme. É a forma mais rápida de pegar uma regressão de contraste ou de espelhamento RTL antes de levar ao aparelho. ## Recapitulando - `examples/storybook/app.py` é o tour de um app só por **todo** o design system H1–H6, organizado em abas por categoria (Action/Inputs/Surfaces/Feedback/ Navigation/Research). - A `AppBar` traz os toggles **claro/escuro** e **LTR/RTL**; cada um chama `app.set_theme`/`app.set_locale`. - Todo componente recebe `theme=`; o app lê `app.theme`/`app.locale` como contexto, então um toque **re-pinta o sistema inteiro ao vivo** — a superfície de verificação de dark/RTL. - Rode com `uv run python examples/storybook/app.py`. Para o catálogo completo de widgets e a referência de API, veja a [visão geral de widgets](../widgets.md) e a [API pública](../../referencia/api.md). --- # File: docs/guia/eventos.md — https://mauriciobenjamin700.github.io/tempestroid/guia/eventos/ # Eventos Eventos são o contrato tipado da fronteira Python↔Kotlin. Quando o lado nativo reporta um toque ou uma mudança de valor, o *payload* chega cru e é **validado antes** de entrar em um *handler* — como o FastAPI valida um corpo de requisição. ## Tipos de evento Todos herdam de `Event` (Pydantic frozen). | Evento | Campos | Emitido por | |---|---|---| | `TapEvent` | `x: float \| None`, `y: float \| None` | `Button.on_click` | | `TextChangeEvent` | `value: str`, `valid: bool` (contra o `pattern` do input) | `Input.on_change`, `TextArea.on_change` | | `ToggleEvent` | `checked: bool` | `Checkbox.on_change`, `Switch.on_change` | | `SlideEvent` | `value: float` | `Slider.on_change` | | `DateChangeEvent` | `value: str` (ISO `yyyy-mm-dd`) | `DatePicker.on_change` | | `FileSelectEvent` | `uri: str`, `name: str \| None` | `FilePicker.on_select` | !!! info "Estes são os eventos de núcleo — há 31 no total" A tabela acima mostra os mais comuns. O Trilho E acrescentou muitos outros — navegação (`RouteChangeEvent`/`PageChangeEvent`), listas (`ScrollEvent`/ `EndReachedEvent`/`RefreshEvent`), gestos (`PanEvent`/`ScaleEvent`/ `SwipeEvent`/`ReorderEvent`/`LongPressEvent`/`DragEvent`), formulários (`SubmitEvent`/`ValidationEvent`/`RangeChangeEvent`/`TimeChangeEvent`/ `SelectEvent`), overlays (`DismissEvent`/`MenuSelectEvent`) e plataforma (`SensorEvent`/ `LifecycleEvent`/`ConnectivityEvent`/`DeepLinkEvent`/`QrScanEvent`/ `ThemeChangeEvent`/`LocaleChangeEvent`). Liste o contrato completo com `tempest spec` ou veja a [referência de API](../referencia/api.md). ## O portão de validação: `parse_event` `parse_event(event_type, raw)` transforma um *payload* cru (um *mapping*) em um evento tipado, ou levanta `EventValidationError` com os erros estruturados por campo (JSON-serializável): ```python from tempestroid import EventValidationError, TextChangeEvent, parse_event event = parse_event(TextChangeEvent, {"value": "olá"}) # -> TextChangeEvent(value="olá") try: parse_event(TextChangeEvent, {}) # falta o campo obrigatório except EventValidationError as exc: print(exc.errors) # [{"loc": ("value",), "type": "missing", ...}] ``` ## Handlers Um *handler* pode receber o evento tipado **ou** ser zero-argumento quando o valor não importa. O runtime detecta a aridade e passa (ou não) o evento: ```python # Recebe o evento tipado: def on_name(event: TextChangeEvent) -> None: app.set_state(lambda s: setattr(s, "name", event.value)) # Zero-argumento (ignora o payload): Button(label="+", on_click=lambda: app.set_state(...)) ``` *Handlers* podem ser síncronos ou `async` — o runtime agenda corrotinas no loop asyncio sem travar a UI. ## Aliases de handler tipados Para anotar props de *handler*, o pacote exporta **`EventHandler`** — o wrapper tipado genérico de prop de *handler* (ex.: `on_click`, `on_change`). Ele carrega uma anotação `WithJsonSchema` para que widgets com *handler* não quebrem a geração de esquema JSON. ## O contrato como dado Cada widget declara o evento que cada *handler* emite via a classvar `event_schemas`. A função [`introspect()`](../referencia/api.md#introspeccao) publica tudo isso como JSON — esquemas de prop dos widgets, o evento de cada *handler* e o esquema de *payload* de cada evento. É o que alimenta `tempest spec` e a fronteira do dispositivo. ## Recapitulando - Eventos são modelos Pydantic frozen (`TapEvent`, `TextChangeEvent`, …). - `parse_event` é o portão que valida o *payload* cru antes do *handler* — como o FastAPI valida um corpo de requisição. - *Handlers* podem receber o evento tipado ou ser zero-argumento; síncronos ou `async`. - O contrato (`event_schemas` + `introspect()`) é publicado como JSON por `tempest spec`. ## Próximos passos ➡️ Inspecione o contrato com a **[CLI (`tempest spec`)](cli.md)**, ou veja *handlers* reais na **[Galeria de exemplos](exemplos.md)**. --- # File: docs/guia/cli.md — https://mauriciobenjamin700.github.io/tempestroid/guia/cli/ # CLI (`tempest`) O ponto de entrada `tempest` cobre o ciclo de vida do app: criar, desenvolver no simulador, empurrar para o dispositivo, empacotar e inspecionar o contrato. ```bash uv run tempest new # scaffold na pasta atual (id = nome da pasta) uv run python examples/counter/app.py # rodar um app direto no simulador Qt uv run tempest dev examples/counter/app.py # dev loop: editar + salvar → hot reload uv run tempest deploy examples/multifile/main.py # push offline no aparelho (sem SDK/NDK) uv run tempest serve examples/device_counter/app.py # code-push por LAN, sem rebuild de APK uv run tempest build apk # APK com id próprio, lado a lado (JDK + SDK) uv run tempest build release-apk # APK de release assinado (distribuir fora da Play) uv run tempest run # build + instalar no dispositivo + logs uv run tempest spec # imprimir o contrato tipado (widgets/eventos) como JSON uv run tempest --help ``` ## Comandos | Comando | Status | Descrição | |---|---|---| | `tempest new` | ✅ | Cria um projeto de app executável **na pasta atual** (id = nome da pasta). Passe um nome só para criar uma subpasta. | | `tempest dev ` | ✅ | Simulador + hot reload / hot restart (precisa do extra `qt`). `--device`/`-d` dimensiona a janela a um preset de aparelho (ex.: `pixel-7`, `galaxy-s24`). | | `tempest deploy ` | ✅ | Push **offline** do projeto inteiro no aparelho (sem SDK/NDK): instala o host empacotado + empurra + abre. | | `tempest serve ` | ✅ | Code-push por LAN + hot reload do projeto inteiro (fase B5). | | `tempest install [src]` | ✅ | adb-instala o host pré-compilado (sem SDK/NDK). | | `tempest spec` | ✅ | Contrato tipado de widgets/eventos como JSON. | | `tempest doctor` | ✅ | Diagnostica os pré-requisitos de build/run Android (JDK, android-host, SDK, adb, dispositivo). Prontidão de build define o código de saída; device ausente é só informativo (só `run`/`install` precisam). | | `tempest setup` | ✅ | Configura o ambiente de build: diagnostica JDK/SDK/build-tools; `--install` instala o Android SDK. | | `tempest version` | ✅ | Imprime a versão do framework (igual a `--version`). | | `tempest clean` | ✅ | Limpa os caches de build em `~/.tempestroid` (nativos extraídos do host, cópia do host, clone do source) — resolve falhas de cache velho após upgrade; `--keystore` também apaga o keystore de release. | | `tempest build [apk\|release-apk\|prd]` | ✅ | `apk`: APK **per-app** (id próprio → N apps lado a lado), via Gradle reusando os nativos pré-compilados (**só JDK + SDK**, sem NDK/toolchain). `release-apk`: APK de release assinado com a sua keystore para distribuir **fora da Play** (`--keystore`; verifique com `apksigner verify`). `prd`: AAB de release. Lê `[tool.tempest]`. | | `tempest run` | ✅ | `build apk` + instala no dispositivo + transmite logs. | | `tempest icon ` | ✅ | Gera `icon.png` + `splash.png` de uma imagem (Pillow). | | `tempest lint [path]` | ✅ | `ruff check` no alvo (só lint). | | `tempest fix [path]` | ✅ | `ruff check --fix` + `ruff format` num passo (`--unsafe` para autofixes inseguros). | | `tempest format [path]` | ✅ | `ruff format` (escreve os arquivos). | | `tempest fmt-check [path]` | ✅ | `ruff format --check` (só leitura). | | `tempest type [path]` | ✅ | `pyright` no alvo (type check estrito). | | `tempest test [path]` | ✅ | `pytest` (encaminha o filtro de caminho opcional). | | `tempest check [path]` | ✅ | Portão de qualidade completo: lint + fmt-check + type + test. | Apps são **multi-arquivo**: a árvore do projeto vai junto (no `sys.path`) no simulador e no dispositivo. Veja [Build, deploy e publicação](build.md) para a diferença entre o push offline (`deploy`/`serve`) e o APK distribuível (`build`). ## Cockpit do `tempest dev` Comandos interativos enquanto o simulador roda: | Tecla | Ação | |---|---| | `r` | Hot reload (estado preservado). | | `R` | Hot restart (estado limpo). | | `s` | Traz a janela à frente. | | `q` | Encerra. | Salvar o arquivo dispara o hot reload automaticamente; se a recarga for incompatível com o estado vivo, o loop cai para um restart limpo. Uma gravação ruim é capturada e impressa — o loop sobrevive. !!! note "build / run precisam de JDK + Android SDK" `tempest build`/`run` rodam o Gradle reusando os nativos pré-compilados (o `android-host` vem no pacote), então precisam de **JDK + Android SDK** — **sem NDK, sem toolchain CPython, sem `git clone`**. Para rodar no aparelho **sem SDK**, use `tempest deploy`/`serve`. Veja [Build, deploy e publicação](build.md), a [instalação](../instalacao.md) e a [pesquisa de runtime](../research/android-runtime.md). ## Contrato do arquivo de app Para `tempest dev`/`serve`, o módulo precisa expor: - `make_state() -> S` — fábrica do estado inicial (chamada a cada hot restart). - `view(app) -> Widget` — construtor da UI. O carregador compila/executa o arquivo fresco a cada carga (sem reuso de `.pyc`), então recargas sempre veem a última edição. Mantenha o módulo livre de imports de Qt no nível de módulo (use `if __name__ == "__main__"`) para que o mesmo arquivo rode no desktop e no dispositivo. --- # File: docs/guia/exemplos.md — https://mauriciobenjamin700.github.io/tempestroid/guia/exemplos/ # Galeria de exemplos Um conjunto de apps de exemplo executáveis vive em [`examples/`](https://github.com/mauriciobenjamin700/tempestroid/tree/main/examples). Cada um expõe o mesmo contrato `make_state()` + `view(app)`, então roda no simulador Qt **e** no dispositivo via code-push, sem mudanças. **Clique no nome de qualquer app abaixo para ver o código-fonte** — todo `app.py` abre com um *docstring* explicando o que demonstra. ```bash # Simulador Qt no desktop (precisa do extra `qt`; instalado por `uv sync`) uv run python examples//app.py uv run tempest dev examples//app.py # + hot reload ao salvar # Em um dispositivo Android, via code-push por LAN (fase B5) adb reverse tcp:8765 tcp:8765 # via USB; pule se na mesma Wi-Fi uv run tempest serve examples//app.py ``` ## Fundamentos | App | O que mostra | Exercita | |---|---|---| | [`counter`](https://github.com/mauriciobenjamin700/tempestroid/blob/main/examples/counter/app.py) | O básico: handlers síncronos **e** `async` mutam estado e disparam um rebuild coalescido. | `Text`, `Button`, `Row`/`Column`; `update`. | | [`stopwatch`](https://github.com/mauriciobenjamin700/tempestroid/blob/main/examples/stopwatch/app.py) | Loop async-first: um handler corrotina conta via `asyncio.sleep` sem travar a UI (stop/reset seguem tocáveis). | Rebuilds coalescidos a partir do loop; `update`. | | [`todo`](https://github.com/mauriciobenjamin700/tempestroid/blob/main/examples/todo/app.py) | Digite uma tarefa no `Input` e toque "add"; tocar alterna concluída; "clear done" remove as feitas. | `Input` + chave estável; **todos** os child patches: `insert` / `remove` / `update`. | | [`calculator`](https://github.com/mauriciobenjamin700/tempestroid/blob/main/examples/calculator/app.py) | A grade de botões **é** a entrada (sem widget de texto) — vitrine de layout denso. | `Row`/`Column` aninhados, botões com chave; `update` no display. | | [`colorpicker`](https://github.com/mauriciobenjamin700/tempestroid/blob/main/examples/colorpicker/app.py) | `Style` dinâmico: *swatches* recolorem um preview vivo; toggles re-estilizam o texto. | `background` / `font_size` / `font_weight` pelo diff. | ## Componentes e shell | App | O que mostra | Exercita | |---|---|---| | [`shell`](https://github.com/mauriciobenjamin700/tempestroid/blob/main/examples/shell/app.py) | Tela inteira montada dos componentes compostos: `Scaffold` + `AppBar` (com `Burger`/`Drawer`) no topo, `NavBar` embaixo. | `tempestroid.components` reduzidos a primitivos via `Component.render`. | | [`gallery`](https://github.com/mauriciobenjamin700/tempestroid/blob/main/examples/gallery/app.py) | Widgets utilitários + estilização de input + transição implícita de `Style`. | `Slider`/`Switch`/`ProgressBar`/`Spinner`/`Image`/`Icon`/`ScrollView`/`TextArea`; `Input` seguro + regex; `Style.transition`. | ## Trilho E — paridade Flutter/RN | App | O que mostra | Exercita | |---|---|---| | [`navigation`](https://github.com/mauriciobenjamin700/tempestroid/blob/main/examples/navigation/app.py) | Os três hosts de navegação: pilha push/pop animada, abas e gaveta. | `Navigator` / `TabView` / `RouteDrawer` (E0). | | [`tabs`](https://github.com/mauriciobenjamin700/tempestroid/blob/main/examples/tabs/app.py) | Tab bar persistente troca o corpo entre 3 painéis; o estado compartilhado sobrevive à troca. | Padrão canônico de navegação por abas. | | [`lists`](https://github.com/mauriciobenjamin700/tempestroid/blob/main/examples/lists/app.py) | `LazyColumn` de 10k itens + paginação + pull-to-refresh, e `SectionList` com cabeçalho fixo. | Virtualização por janela (E1). | | [`overlays`](https://github.com/mauriciobenjamin700/tempestroid/blob/main/examples/overlays/app.py) | Dialog, bottom sheet, menu e toast pela API imperativa de overlay do `App`. | Camada de overlay z-ordenada (E2). | | [`animation`](https://github.com/mauriciobenjamin700/tempestroid/blob/main/examples/animation/app.py) | Caixa que faz *ease* de cor/opacidade, lista animada, `Hero` e `Shimmer`. | `AnimationController` + `Tween` no clock de frames (E3). | | [`gestures`](https://github.com/mauriciobenjamin700/tempestroid/blob/main/examples/gestures/app.py) | Swipe-to-delete (`Dismissible`), arrastar p/ reordenar e pinça-zoom. | Gestos avançados (E4). | | [`forms`](https://github.com/mauriciobenjamin700/tempestroid/blob/main/examples/forms/app.py) | `Form` de `FormField`s com validators tipados (bloqueia submit inválido) + inputs de seleção/segmento. | Validação em Python antes dos patches (E5). | | [`form`](https://github.com/mauriciobenjamin700/tempestroid/blob/main/examples/form/app.py) | Os inputs com valor básicos, cada um dobrando seu evento tipado de volta no estado. | `Input` / `Checkbox` / `DatePicker` / `FilePicker` + eventos tipados. | | [`layout`](https://github.com/mauriciobenjamin700/tempestroid/blob/main/examples/layout/app.py) | Chips com `Wrap`, `PageView` paginado e `CollapsingAppBar` que encolhe ao rolar. | Layout refinado (E6). | | [`media`](https://github.com/mauriciobenjamin700/tempestroid/blob/main/examples/media/app.py) | Desenho com `Canvas`, `Svg`, blur e clip. | Mídia e gráficos (E7). | | [`platform`](https://github.com/mauriciobenjamin700/tempestroid/blob/main/examples/platform/app.py) | Haptics, preferências reais, stream de lifecycle e `KeyboardAvoidingView`. | Plataforma/sistema (E8) — roda no Qt e no device. | | [`theming`](https://github.com/mauriciobenjamin700/tempestroid/blob/main/examples/theming/app.py) | Toggle dark/light (`App.set_theme`), locale PT↔árabe/RTL (`App.set_locale`) e `Semantics`. | Transversais: tema/i18n/acessibilidade (E9). | ## Dispositivo e multi-arquivo | App | O que mostra | Exercita | |---|---|---| | [`device_counter`](https://github.com/mauriciobenjamin700/tempestroid/blob/main/examples/device_counter/app.py) | Contador mínimo **sem import de Qt** — o alvo do code-push no aparelho. | Mesmo contrato, livre de Qt (B5). | | [`native_caps`](https://github.com/mauriciobenjamin700/tempestroid/blob/main/examples/native_caps/app.py) | Capacidades nativas sem config extra, cada uma um round-trip request/response tipado. | `clipboard` / `storage` / `database` (SQLite) / `secure_storage` / `system` (device-verificado). | | [`sysverify`](https://github.com/mauriciobenjamin700/tempestroid/blob/main/examples/sysverify/app.py) | Harness de verificação on-device das capacidades que exigem hardware real. | Sensores / biometria / push (device-only). | | [`multifile`](https://github.com/mauriciobenjamin700/tempestroid/tree/main/examples/multifile) | Projeto **multi-arquivo** (`main.py` + pacote `widgets/`) — o que `tempest new --template multi` gera. | Bundle do projeto inteiro no `sys.path` (Trilho C). | ## Trilho G — inferência ONNX no device Visão no aparelho com o [`ort-vision-sdk`](https://pypi.org/project/ort-vision-sdk/) (backend plugável), inferência pela AAR nativa `onnxruntime-android` — **sem OpenCV, sem wheel onnxruntime/Pillow**. Exigem o extra `[vision]` + o build com `--feature vision`; device-verificados no emulador x86_64. | App | O que mostra | Exercita | | --- | --- | --- | | [`onnxspike`](https://github.com/mauriciobenjamin700/tempestroid/blob/main/examples/onnxspike/app.py) | Prova mínima: `import numpy` + um cálculo rodam no interpretador embarcado (tela verde "numpy OK"). | numpy android no device (G0/G1). | | [`visionspike`](https://github.com/mauriciobenjamin700/tempestroid/blob/main/examples/visionspike/app.py) | Pipeline completo: imagem real (`banana.jpg`) → decode nativo (`BitmapFactory`) → `Classifier` do SDK via `AarBackend` → top-1 + latência. Modelo **embutido** ou **baixado** (`VISIONSPIKE_MODEL_URL`), `.onnx` fp32 ou `.int8.ort` quantizado (`VISIONSPIKE_MODEL`). | G1 (AAR) + G2 (imagem) + G3 (`tempest optimize`) + G4 (entrega). | ## Conjunto de widgets atual Os **dois renderizadores** — simulador Qt (desktop) e Compose (dispositivo) — suportam o conjunto completo do Trilho E. Não há mais o gap antigo de "o Compose só renderiza cinco widgets": os inputs com valor (`Input` / `TextArea` / `Checkbox` / `Switch` / `Slider` / `Dropdown` / `DatePicker` / `FilePicker` / …) renderizam **nativamente no aparelho** via Jetpack Compose e dobram seus eventos tipados de volta no estado. A paridade é fixada pela suíte de conformância (*golden snapshots* dos dois tradutores `Style → Qt` e `Style → Compose`) e foi verificada em device ao longo de E0–E9. Cobertura (ambos os renderizadores, salvo nota): | Categoria | Widgets | |---|---| | Layout | `Column` / `Row` / `Container` / `Stack` / `Wrap` / `ScrollView` / `SafeArea` / `AspectRatio` / `PageView` / `KeyboardAvoidingView` | | Texto e ação | `Text` / `Button` / `Icon` / `Image` (`on_click`) | | Inputs com valor | `Input` / `TextArea` / `Checkbox` / `Switch` / `Slider` / `RangeSlider` / `Dropdown` / `DatePicker` / `TimePicker` / `FilePicker` / `PinInput` / `MaskedInput` / `Autocomplete` / `Form` / `FormField` | | Listas virtualizadas | `LazyColumn` / `LazyRow` / `LazyGrid` / `SectionList` (+ pull-to-refresh, scroll infinito) | | Navegação | `Navigator` / `TabView` / `TabBar` / `RouteDrawer` | | Overlays | `Dialog` / `BottomSheet` / `Menu` / `Popover` / `Toast` / `Tooltip` / `ActionSheet` | | Animação | `Animated` / `AnimatedList` / `Hero` / `Shimmer` / `Skeleton` | | Gestos | `GestureDetector` / `PanHandler` / `ScaleHandler` / `DoubleTapHandler` / `Draggable` / `DragTarget` / `Dismissible` / `ReorderableList` / `InteractiveViewer` | | Mídia e gráficos | `Canvas` / `Svg` / `VideoPlayer` / `WebView` / `Blur` / `BackdropFilter` / `ClipPath` | | Indicadores | `ProgressBar` / `Spinner` | !!! note "Divergência de mídia/câmera (device-only)" Alguns widgets de hardware — `CameraPreview` / `QrScanner` / `MapView` — renderizam só no aparelho (Compose) e aparecem como **placeholder sinalizado no Qt**, não o contrário. As divergências por campo entre os dois tradutores estão documentadas na suíte de conformância (`tests/conformance/`). Os exemplos `form` e `gallery` exercitam os inputs com valor de verdade — no simulador **e** no aparelho. Exemplos como `calculator` continuam dirigidos por teclado numérico por *design* do app, não por limite do renderizador. !!! tip "Handlers estáveis" Rebuilds comparam props de *handler* por identidade, então um `lambda` novo a cada build lê como mudança de prop (limitação conhecida). Os exemplos ainda emitem *patches* corretos — apenas mais que o mínimo estrito. Prefira referências de *handler* estáveis em apps de produção. ## Capturas no dispositivo (emulador x86_64, sem hardware físico) !!! note "Geradas sem aparelho físico" A maioria foi renderizada pelo renderizador **Compose** num **emulador x86_64 headless** (`make emulator-verify` / `toolchain/validate_gallery.sh`) — zero hardware; as galerias do design system (`h1buttons`–`h4gallery`) são capturas do simulador Qt. `stopwatch` (animado) fica para uma recaptura como GIF (ver [Animados](#animados)). | | | | |---|---|---| | ![animation](../assets/examples/animation.png){ width=200 }
`animation` | ![brforms](../assets/examples/brforms.png){ width=200 }
`brforms` | ![calculator](../assets/examples/calculator.png){ width=200 }
`calculator` | | ![colorpicker](../assets/examples/colorpicker.png){ width=200 }
`colorpicker` | ![counter](../assets/examples/counter.png){ width=200 }
`counter` | ![device_counter](../assets/examples/device_counter.png){ width=200 }
`device_counter` | | ![form](../assets/examples/form.png){ width=200 }
`form` | ![forms](../assets/examples/forms.png){ width=200 }
`forms` | ![gallery](../assets/examples/gallery.png){ width=200 }
`gallery` | | ![gestures](../assets/examples/gestures.png){ width=200 }
`gestures` | ![icons](../assets/examples/icons.png){ width=200 }
`icons` | ![layout](../assets/examples/layout.png){ width=200 }
`layout` | | ![lists](../assets/examples/lists.png){ width=200 }
`lists` | ![media](../assets/examples/media.png){ width=200 }
`media` | ![multifile](../assets/examples/multifile.png){ width=200 }
`multifile` | | ![native_caps](../assets/examples/native_caps.png){ width=200 }
`native_caps` | ![navigation](../assets/examples/navigation.png){ width=200 }
`navigation` | ![overlays](../assets/examples/overlays.png){ width=200 }
`overlays` | | ![platform](../assets/examples/platform.png){ width=200 }
`platform` | ![shell](../assets/examples/shell.png){ width=200 }
`shell` | ![sysverify](../assets/examples/sysverify.png){ width=200 }
`sysverify` | | ![tabs](../assets/examples/tabs.png){ width=200 }
`tabs` | ![theming](../assets/examples/theming.png){ width=200 }
`theming` | ![todo](../assets/examples/todo.png){ width=200 }
`todo` | | ![h1buttons](../assets/examples/h1buttons.png){ width=200 }
`h1buttons` | ![h2gallery](../assets/examples/h2gallery.png){ width=200 }
`h2gallery` | ![h3gallery](../assets/examples/h3gallery.png){ width=200 }
`h3gallery` | | ![h4gallery](../assets/examples/h4gallery.png){ width=200 }
`h4gallery` | | | Os exemplos `h1buttons` (variantes de `Button`), `h2gallery` (o kit de ação e entrada), `h3gallery` (superfície e layout) e `h4gallery` (data display e feedback) acompanham o [design system](design-system/variantes.md). ### Animados Exemplos com movimento (`animation`, `gestures`, `stopwatch`) precisam de um **GIF** — um PNG estático não mostra a animação. O harness `toolchain/capture_gif.sh` captura uma rajada de frames no dispositivo e monta o GIF (via `toolchain/frames_to_gif.py`). Como esses exemplos são **estáticos em repouso**, dispare a animação com `TAP_X`/`TAP_Y` antes da rajada: ```bash # ex.: stopwatch — toca "start" e captura o relógio correndo TAP_X=540 TAP_Y=1400 ANDROID_SERIAL=emulator-5554 \ bash toolchain/capture_gif.sh examples/stopwatch/app.py ``` --- # File: docs/guia/build.md — https://mauriciobenjamin700.github.io/tempestroid/guia/build/ # Build, deploy e publicação Esta página mostra como sair do simulador e **rodar seu app num aparelho Android** — desde o teste rápido no seu próprio celular até gerar um **APK autocontido** que você manda para outra pessoa testar. Tudo a partir do seu projeto em Python. !!! tip "Comece pelo simulador" Para o ciclo de desenvolvimento (editar → ver), use `tempest dev` (o [simulador Qt](cli.md)). Esta página é sobre levar o mesmo app para o **dispositivo** e para um **APK distribuível**. ## Projetos multi-arquivo Seu app raramente é um arquivo só: o `main.py` importa módulos e pacotes vizinhos do seu projeto. O tempestroid trata isso de forma transparente. A **raiz do projeto** é o diretório ancestral mais próximo do app que contém um `pyproject.toml`. Toda a árvore importável a partir dela é empacotada e colocada no `sys.path` — no simulador **e** no dispositivo — então: ```python # main.py from meu_pacote.widgets import cartao # ✅ resolve igual no desktop e no device ``` resolve identicamente nos dois lados. O bundle **exclui** o que não é código de app: `.venv`, `__pycache__`, `.git`, `dist`, `build`, caches de editor/lint. !!! example "Layout típico de projeto" ```text meu-app/ ├── pyproject.toml # contém [tool.tempest] app = "main.py" ├── main.py # define view(app) + make_state() └── meu_pacote/ ├── __init__.py └── widgets.py # importado por main.py ``` O `pyproject.toml` ancora a raiz. Sem ele, a raiz vira o diretório do próprio `main.py` (modo arquivo-único). ```toml # pyproject.toml [tool.tempest] app = "main.py" ``` Com `[tool.tempest] app` definido, `dev` / `deploy` / `serve` / `build` / `run` dispensam o argumento de caminho dentro do projeto. ## Qual comando usar? | Quero… | Comando | Precisa de quê? | Entrega | |---|---|---|---| | Rodar rápido no **meu** aparelho | `tempest deploy` | nada (só adb) | App rodando no device (efêmero) | | Editar e ver ao vivo (hot reload) | `tempest serve` | nada (só adb) | Loop de code-push por LAN | | **Mandar um APK** para alguém testar | `tempest build apk` | JDK + Android SDK | `.apk` com `applicationId` próprio (**N apps lado a lado**) | | Distribuir **fora da Play** (site, link) | `tempest build release-apk` | JDK + SDK + keystore | `.apk` de release assinado com a **sua** chave | | Build + instalar + logs no device | `tempest run` | JDK + SDK + adb | Instala o APK e segue os logs | | Publicar na Play Store | `tempest build prd` | JDK + SDK + keystore | `.aab` de release assinado | | Iterar num app só, **sem instalar SDK** | `tempest build --fast` | só SDK build-tools | `.apk` (id compartilhado, 1 app) | !!! info "Como funciona (sem toolchain pesada)" `tempest build apk` roda o **Gradle** (que carimba o `applicationId` + todas as *provider authorities* por app → **instalam lado a lado sem colisão**), mas **reusa os nativos do host pré-compilado** (`libpython`, a stdlib, o JNI) que já vêm no pacote. Logo precisa só de **JDK + Android SDK** — **sem NDK, sem compilar CPython**. O projeto `android-host` vem **dentro do wheel**, então funciona de um `pip install` puro, **sem `git clone`**. - `deploy`/`serve`: empurram seu código pra um **host genérico** já instalado (rápido, offline) — o app vive dentro do host, não é um artefato distribuível. - `--fast`: repackage do host pré-compilado **sem SDK nenhum** (só build-tools), mas com id compartilhado `org.tempestroid.host` → **1 app por device**. - `--from-source`: build pesado, estagiando a toolchain CPython (raramente necessário). ## Rodar no meu aparelho (sem toolchain) Você **não** precisa de Android SDK/NDK nem do código-fonte `android-host` para testar no seu próprio celular. Conecte o aparelho (`adb devices` deve listá-lo) e: ```bash tempest deploy # instala o host empacotado (1x) + empurra o projeto + abre ``` O `tempest deploy`: 1. Instala o **host pré-compilado** (baixado do release do GitHub no primeiro uso e cacheado) se ainda não estiver no aparelho. Execuções seguintes pulam. 2. Empacota seu projeto e empurra **uma vez** por um servidor efêmero. 3. Abre o app e **encerra**. O app continua rodando no aparelho. !!! warning "`deploy` não gera artefato" O app empurrado por `deploy` vive na sessão do host. Em um boot frio, ou no celular de outra pessoa, o host roda o demo embutido — **não** o seu app. Para algo distribuível, use [`tempest build apk`](#gerar-um-apk-tempest-build-apk). Para um **loop de hot reload** (editar + salvar → recarrega no device): ```bash tempest install # só adb-instala o host (offline/embutido) tempest serve # code-push por LAN: salvar qualquer arquivo recarrega ``` O `tempest install` resolve o APK do host nesta ordem: caminho/URL `.apk` explícito → `TEMPESTROID_HOST_APK` → asset empacotado (só num checkout do código estagiado com `make stage-host`) → download do release do GitHub (`TEMPESTROID_HOST_APK_URL` para sobrescrever), cacheado em `~/.cache/tempestroid`. O wheel do PyPI **não** embute o APK (~100 MB), então num install via PyPI o download é o caminho normal (offline depois disso). ## Gerar um APK (`tempest build apk`) Para um `.apk` **autocontido** (roda sem dev server, com o **id próprio** do seu projeto → instala lado a lado com qualquer outro app tempestroid): ```bash tempest build apk # lê o [tool.tempest], gera dist/.apk tempest build apk -o /tmp/app.apk ``` A identidade e o visual vêm do **`[tool.tempest]`** no `pyproject.toml` — sem flag-soup: ```toml [tool.tempest] app = "app.py" id = "com.suaempresa.todolist" # applicationId; derivado do projeto se omitido name = "Todo List" # rótulo sob o ícone icon = "icone.png" # opcional splash = "splash.png" # opcional splash_bg = "#0b0f14" # opcional version = "1.0.0" # opcional (default 1.0.0) ``` O resultado fica em `dist/.apk`, assinado com a chave debug → `adb install` em qualquer aparelho e abre direto, sem servidor. Cada projeto sai com seu **próprio `applicationId`**, então **N apps instalam lado a lado** (nunca um sobre o outro). As flags `--app-id`/`--app-name`/`--icon`/… sobrescrevem o config por build. !!! info "Devo definir o `id` ou o framework gera sozinho?" **Os dois — mas, para algo real, defina o seu.** Omitido → o framework **deriva** `com.example.` só pra você buildar na hora. Esse `com.example.*` é **placeholder, não publicável** (a Play rejeita). Regra: **teste com o derivado; defina o seu `id`** (domínio reverso, ex. `com.suaempresa.app`) **e mantenha o mesmo pra sempre** — mudá-lo vira outro app aos olhos do Android/Play. O `id` é independente do pacote Java/JNI interno (`org.tempestroid.host`), então escolher o seu não quebra a ponte. !!! note "Precisa só de JDK + Android SDK (sem NDK, sem toolchain)" `tempest build apk` roda o Gradle **reusando os nativos do host pré-compilado** (libpython/JNI/stdlib que já vêm no pacote) → **não compila CPython, não usa NDK**. O `android-host` vem **dentro do wheel**, então funciona de um `pip install` puro, **sem `git clone`**. Rode `tempest setup --install` uma vez pro SDK (o JDK é pré-requisito). Sem JDK/SDK, o build **cai pro `--fast`** (id compartilhado) com aviso, em vez de falhar. ## Ícone e tela de carregamento (splash) Todo APK já sai com um **ícone tempestroid** e uma **splash** padrão que cobre o boot do interpretador Python (alguns segundos). Para personalizar por app: ```bash tempest build --icon icone.png \ --splash splash.png \ --splash-bg "#0b0f14" ``` !!! tip "Gere os dois a partir de UMA imagem com `tempest icon`" Não quer dimensionar à mão? Aponte para um logo e o CLI gera os dois PNGs: ```bash tempest icon logo.png --out assets # → assets/icon.png (quadrado) + assets/splash.png (centralizado, fundo transparente) tempest build --icon assets/icon.png --splash assets/splash.png --splash-bg "#0b0f14" ``` Precisa do Pillow: `pip install tempestroid[icons]` (ou `uv add tempestroid[icons]`). - `--icon icone.png` — o ícone de launcher (o que aparece na gaveta de apps). **Só no build Gradle** (o default): o ícone é um recurso *compilado*, e um repackage `--fast` não reescreve o `resources.arsc`, então com `--fast` o app mantém o ícone padrão (o CLI avisa). - `--splash splash.png` — a imagem mostrada centralizada enquanto o Python sobe. - `--splash-bg "#rrggbb"` — a cor de fundo da splash (default `#0b0f14`). ### Ícone adaptativo (a máscara do launcher) Um PNG quadrado simples não recebe a máscara do launcher (cantos arredondados / squircle). Para um **ícone adaptativo** de verdade — duas camadas, frente + fundo, que o launcher mascara como um app nativo — gere a camada de frente e passe-a no build: ```bash tempest icon logo.png --adaptive --out assets # → também escreve assets/ic_launcher_foreground.png (a marca centrada na zona segura) tempest build --adaptive-icon assets/ic_launcher_foreground.png --icon-bg "#0b0f14" ``` - `--adaptive-icon fg.png` — a camada de **frente** (a marca, com margem da zona segura). **Só no build Gradle** (recurso compilado; `--fast` mantém o ícone padrão e avisa). - `--icon-bg "#rrggbb"` — a cor de **fundo** do ícone adaptativo (default branco). !!! info "O que o build gera" Emite um adaptive icon Android real: `res/drawable/ic_launcher_foreground.png` + `res/values/ic_launcher_background.xml` (a cor) + os `res/mipmap-anydpi-v26/ic_launcher{,_round}.xml` que redirecionam o `@mipmap/ic_launcher` para eles no Android 8+ (API 26). Em versões antigas o PNG quadrado (`--icon`) continua valendo. !!! tip "A splash cobre o boot do CPython" O interpretador leva alguns segundos para iniciar. A splash é desenhada pela Activity a partir de **assets** e fica na tela **até o primeiro `mount`** do seu app — então o usuário vê sua marca, não uma tela em branco. Como vive em assets (caminho estável), `--splash`/`--splash-bg` funcionam em **todos** os caminhos de build, inclusive `--fast`. ## Capacidades nativas pesadas são opt-in (APK enxuto) O peso do APK não vem do Python (a stdlib do CPython já é enxugada no build) e sim das **dependências Android pesadas**: câmera, leitor de QR (ML Kit), push (Firebase), vídeo (media3) e mapas. Por isso elas são **opcionais** — o build **default não embute nenhuma**, cortando o APK debug de **~58 MB para ~47 MB (−11,4 MB)**. Você reativa só o que o app usa: ```toml # pyproject.toml [tool.tempest] features = ["camera", "qr"] # embute só câmera + leitor de QR ``` ou na linha de comando (repetível): ```bash tempest build --feature camera --feature qr ``` | Feature | Habilita | | --- | --- | | `camera` | widget `CameraPreview` + `take_photo`/`record_video` | | `qr` | widget `QrScanner` (puxa `camera` junto automaticamente) | | `push` | notificações push via FCM | | `video` | widget `VideoPlayer` | | `maps` | widget `MapView` | !!! info "Cada feature exige build from-source (SDK/NDK)" Um APK pré-compilado não tem como receber dependências Gradle novas, então qualquer `--feature` liga automaticamente o caminho `--from-source` (precisa do Android SDK + NDK). O **default enxuto** (sem features) continua usando o host pré-compilado — zero toolchain. !!! tip "Sem a feature, o widget vira um placeholder" Se o app usa um `CameraPreview`/`QrScanner`/`VideoPlayer`/`MapView` mas a feature não foi embutida, ele renderiza um marcador rotulado em vez de quebrar; uma chamada nativa não-embutida levanta `NativeError("feature_not_built")`. Os extras PyPI espelham as features (`pip install tempestroid[camera]`) apenas como **documentação de intenção** — o que de fato corta o APK é a flag de build acima, não o pip. ## Distribuir fora da Play (`tempest build release-apk` → APK assinado) Para mandar o app por um **site, loja alternativa ou link direto** — sem passar pela Play Store — você quer um **APK de release assinado com a sua própria chave** (não o debug-signed do `tempest build apk`, que serve só para teste). É o `tempest build release-apk`: roda o Gradle `assembleRelease` com a sua keystore. ```bash tempest build release-apk # usa [tool.tempest] id/name/version tempest build release-apk --keystore release.jks # sua keystore (senão gera em ~/.tempestroid/release.jks) tempest build release-apk --app-id com.acme.app --app-version 1.2.0 # → dist/-release.apk ``` Confira a assinatura com o `apksigner` do SDK: ```bash apksigner verify --print-certs dist/-release.apk ``` !!! warning "Build real obrigatório (sem fallback `--fast`)" Diferente do `tempest build apk`, o `release-apk` **não** cai no repackage `--fast` quando o toolchain falta — um APK assinado de release exige o Gradle de verdade. Sem JDK + SDK, ele falha com erro (resolva o toolchain com `tempest setup --install`). !!! note "Mesma keystore do `prd`" Reaproveita a keystore e o aviso do `prd` abaixo: **guarde a chave** e **defina o seu `id`** antes de distribuir. ## Publicar na Play Store (`tempest build prd` → AAB) A Play Store exige um **Android App Bundle** (`.aab`), assinado de release. `tempest build prd` gera isso via Gradle `bundleRelease`, lendo o `[tool.tempest]` e usando uma keystore (a sua via `--keystore`, ou uma gerada e cacheada): ```bash tempest build prd # usa [tool.tempest] id/name/version tempest build prd --keystore release.jks # sua keystore (senão gera em ~/.tempestroid/release.jks) # → dist/-release.aab (sobe no Play Console) ``` !!! warning "Guarde a keystore + defina o seu id" A keystore de release assina seu app. **Perdê-la impede atualizar o app na Play** depois — faça backup da `--keystore` (ou da gerada em `~/.tempestroid/release.jks`). E defina o seu `id` no `[tool.tempest]` — o placeholder `com.example.*` não publica. !!! note "Mesma base leve do `apk`" Como o `apk`, o `prd` reusa os nativos do host pré-compilado → **só JDK + Android SDK**, sem NDK nem toolchain CPython. (Se algum dia precisar buildar a toolchain do zero, há a flag avançada `--from-source`.) ## Configuração de ambiente !!! tip "Deixe o `tempest setup` configurar para você" ```bash tempest setup # diagnostica JDK/SDK/NDK/build-tools/toolchain + plano tempest setup --install # instala o Android SDK + NDK (precisa de um JDK) ``` `tempest setup` (sem flag) reporta o que falta e como resolver. Com `--install` ele baixa as command-line tools, aceita as licenças e instala `platform-tools` + `platforms;android-35` + `build-tools;35.0.0` + `ndk;27.3.13750724` num diretório gerenciado (`--sdk-dir` para escolher). O **JDK** e o `make toolchain` ficam guiados (não são instalados sozinhos). `tempest build apk`/`prd`/`run` precisam de: - **JDK** (`java -version`) — pré-requisito (guiado, não instalado pelo CLI). - **Android SDK.** `tempest setup --install` instala/configura; ou exporte `ANDROID_SDK_ROOT` para um SDK existente. **NDK não é necessário** (o build reusa os nativos pré-compilados). !!! note "Caminho avançado `--from-source`" Só se você passar `--from-source` o build estagia a toolchain pesada (CPython 3.14 + wheels nativos via `make toolchain`) e precisa do **NDK** + do Gradle wrapper 8.11.1. No fluxo normal (prebuilt) nada disso é preciso. No **aparelho**: ligue **Depuração USB**; em MIUI/HyperOS (Xiaomi/Redmi/POCO) ligue também **"Instalar via USB"**, senão `adb install` falha com `INSTALL_FAILED_USER_RESTRICTED`. !!! tip "Diagnóstico em um comando" `tempest doctor` roda o *preflight* (árvore do host, SDK, `adb`, aparelho) e aponta o que falta antes de um build. Rodando em WSL? Veja o guia dedicado de [USB no dispositivo (WSL)](dispositivo-wsl.md). ## Tamanho do APK O peso do APK vem quase todo do **CPython 3.14 embarcado** (os `.so` nativos + a biblioteca padrão) mais o `pydantic_core` nativo — tudo necessário em tempo de execução. O build já entrega um APK **enxuto**: dependências pesadas opcionais são *feature-gated* (`tempest.features` — câmera/QR/vídeo/push só entram quando pedidas), os ícones usam só o `material-icons-core` e a biblioteca padrão é podada antes de virar asset. Composição de um APK debug *lean* (arm64-v8a, sem features extras): | Componente | Tamanho (no APK) | Pode remover? | | --- | --- | --- | | `lib/arm64-v8a/*.so` (libpython, libcrypto, libsqlite, libssl) | ~11 MB | ❌ runtime (já *stripped*) | | `site-packages/pydantic_core` (wheel nativo) | ~4.6 MB | ❌ runtime | | `site-packages/pydantic` | ~2.0 MB | ❌ runtime | | `lib-dynload/*.so` (módulos de extensão da stdlib) | ~6.6 MB | parcial — só os de teste | | stdlib pura (`asyncio`, `email`, `json`, `re`, …) | ~6 MB | parcial | | Compose/AndroidX + DEX + recursos | ~3 MB | ❌ runtime | !!! info "O que é podado (F6)" O `CopyPythonStdlibTask` em `android-host/app/build.gradle.kts` exclui dos **assets** (não toca no prefixo de dev) tudo que um app não usa em tempo de execução: as suítes de teste (`test/`), o editor IDLE, Tk/turtle, as ferramentas de empacotamento (`ensurepip`/`venv`/`lib2to3`), `pydoc_data`, os caches de bytecode, **mais** o REPL interativo (`_pyrepl`), o servidor WSGI de referência (`wsgiref`), `doctest.py`/`pydoc.py` e os **módulos de extensão de teste** do CPython (`lib-dynload/_test*.so`, `_xxtestfuzz`, `xxsubtype`, `xxlimited*`). Nenhum é importado pelo framework nem pelo `pydantic` (validado off-device com um *trace* de imports). !!! note "Por que não cai abaixo de ~38 MB" Os nativos (`libpython3.14.so` 5.8 MB, `libcrypto` 3.7 MB) já vêm *stripped* (rodar `llvm-strip` economiza 0 bytes) e o `pydantic_core`/`pydantic` são obrigatórios. O que sobra para podar com segurança é dead-weight de teste, que comprime bem no zip — o ganho líquido é modesto (~1 MB). Cortes maiores (comprimir a stdlib num único arquivo, dropar os codecs CJK) exigem mexer no host Kotlin/C e validação no aparelho — fora do escopo *offline* desta fase. ## Mandar o APK para alguém testar 1. Gere: `tempest build apk`. 2. Pegue o `.apk` em `dist/.apk`. 3. Envie o arquivo (mensageiro, link, etc.). 4. A pessoa instala (`adb install .apk`, ou abrindo o `.apk` no aparelho com "fontes desconhecidas" liberado). O app roda standalone — sem o seu computador, sem dev server. ## Testar sem aparelho físico (emulador) O dispositivo físico no USB é o gargalo recorrente (USB-WSL cai, MIUI exige liberação, tela bloqueia). Para uma validação **sem hardware**, o tempestroid roda num **emulador x86_64 headless** — equivalente completo (boot do CPython + bridge JNI + Compose + capacidades nativas): ```bash make emulator-verify # sobe o AVD x86_64, builda o host x86_64, instala e roda make emulator-verify APP=examples/forms/app.py # qualquer app ``` O alvo (em `toolchain/emulator_verify.sh`) encadeia: subir o AVD headless → `make stage-x86` (prefixo CPython 3.14 x86_64 + `pydantic_core` x86_64) → `make apk-x86` (APK só com libs x86_64) → instalar no emulador → `tempest serve` → screenshot em `docs/assets/emulator/`. !!! tip "Por que x86_64" O emulador roda na arquitetura do host (x86_64), então precisa do CPython + wheel x86_64 — staged em `dist/python/x86_64` / `dist/site-packages-x86_64` (o `stage-x86` reusa o tarball que o cibuildwheel já cacheou). O caminho arm64 do aparelho físico fica **intacto** (`-Ptempest.depsDir` separa os dois). !!! note "Dois alvos adb" Se um aparelho físico também estiver conectado, os comandos miram o emulador explicitamente (`-s $(EMU_SERIAL)` / `ANDROID_SERIAL`) — sem ambiguidade. ## Recapitulando - Apps são **multi-arquivo**: a árvore do projeto vai junto, no `sys.path`, no simulador e no dispositivo. - **Sem hardware:** `make emulator-verify` valida tudo num emulador x86_64 headless. - `tempest deploy` / `serve` rodam no **seu** aparelho **sem toolchain** — ótimos para testar, mas não geram artefato. - `tempest build` gera um **APK autocontido distribuível** — precisa de SDK/NDK + checkout do `android-host`. - `tempest doctor` valida o ambiente; o [guia WSL](dispositivo-wsl.md) cobre a passagem de USB. --- # File: docs/guia/dispositivo-wsl.md — https://mauriciobenjamin700.github.io/tempestroid/guia/dispositivo-wsl/ # Rodar no dispositivo a partir do WSL Guia para conectar um aparelho Android físico a uma sessão **WSL 2** (Windows), compilar o host `android-host/` e instalar/rodar o app no dispositivo. Cobre o *setup* do **usbipd-win** (passagem de USB para o WSL) e o contorno do `adb` sob o modo de rede **mirrored** do WSL. ## Pré-requisitos No **host de build** (WSL): - Android SDK + NDK. Neste host vivem em `/usr/lib/android-sdk` (não no `ANDROID_HOME` obsoleto), então exporte `ANDROID_SDK_ROOT=/usr/lib/android-sdk`. - JDK 21 (`java -version`). - Gradle **wrapper 8.11.1** (`android-host/gradlew`) — o Gradle global 9.x é incompatível com o AGP 8.7; sempre use o wrapper. - A *toolchain* Python estagiada (`make toolchain`): CPython 3.14 + wheels + `toolchain/dist/`. Veja o [runbook Android](../research/android-runbook.md). No **aparelho**: - **Opções do desenvolvedor** → **Depuração USB** ligada. - Em MIUI/HyperOS (Xiaomi/Redmi/POCO): também ligue **"Instalar via USB"**, senão o `adb install` falha com `INSTALL_FAILED_USER_RESTRICTED`. No **Windows**: - **usbipd-win** instalado (passo abaixo). ## 1. Instalar o usbipd-win (Windows) Em um **PowerShell como administrador**: ```powershell winget install usbipd ``` Se o `winget` não achar, baixe o `.msi` do [release oficial](https://github.com/dorssel/usbipd-win/releases/latest), instale e **feche/reabra o PowerShell** (o `PATH` é atualizado). ## 2. Anexar o aparelho ao WSL (Windows) Com o cabo conectado e a depuração USB ligada, no PowerShell admin: ```powershell usbipd list ``` ```text Connected: BUSID VID:PID DEVICE STATE 1-7 2717:ff08 Redmi 12 Not shared ... ``` Pegue o `BUSID` do aparelho (ex.: `1-7`), então: ```powershell usbipd bind --busid 1-7 usbipd attach --wsl --busid 1-7 ``` - `bind` só na **primeira vez** (marca o device como compartilhável). - `attach` toda vez que reconectar o cabo (anexa o device ao WSL). Confira no WSL que o kernel viu o device: ```bash dmesg | grep -i "Product:\|SerialNumber:" | tail -3 # usb 1-1: Product: Redmi 12 # usb 1-1: SerialNumber: 0d474c147d75 ``` ## 3. Contorno do adb sob rede *mirrored* Sob o modo de rede **mirrored** do WSL 2, `adb start-server` **trava**: o *handshake* de prontidão do daemon pelo *loopback* `127.0.0.1:5037` não completa (o `adb devices`/`adb kill-server` ficam pendurados e expiram). Contorno: suba o servidor **em primeiro plano** (`nodaemon`) como um processo de fundo persistente e deixe os comandos do cliente conversarem com ele: ```bash # 1. inicie o servidor em background (deixe rodando): ANDROID_SDK_ROOT=/usr/lib/android-sdk \ /usr/lib/android-sdk/platform-tools/adb nodaemon server & # 2. agora o cliente responde normalmente: adb devices -l # List of devices attached # 0d474c147d75 device product:fire_global model:23053RN02A ... ``` Se o `adb` ficar preso de novo, mate todos os processos e repita: ```bash pkill -9 adb ``` ## 4. Compilar e instalar A partir da raiz do repositório: ```bash export ANDROID_SDK_ROOT=/usr/lib/android-sdk make apk # ./gradlew :app:assembleDebug — gera app-debug.apk (~49 MB) make install # adb install -r do APK no device # ou os dois de uma vez: make apk-install ``` Equivalente cru (com o serial explícito, útil com o servidor `nodaemon`): ```bash cd android-host && ANDROID_SDK_ROOT=/usr/lib/android-sdk ./gradlew :app:assembleDebug adb -s 0d474c147d75 install -r app/build/outputs/apk/debug/app-debug.apk ``` > O build estagia o Python a partir de `toolchain/dist/` (symlink ou cópia) e o > `tempestroid` puro a partir de `../tempestroid` (excluindo `renderers/qt`). O > APK extrai a stdlib na **primeira execução**, então o primeiro boot é lento > (dezenas de segundos). ## 5. Rodar e capturar ```bash adb -s 0d474c147d75 shell am start -n org.tempestroid.host/.MainActivity # espere o boot do interpretador (~20 s na primeira vez), depois: adb -s 0d474c147d75 exec-out screencap -p > shot.png adb -s 0d474c147d75 logcat -d | grep -iE "tempestroid|python|FATAL" ``` Sem `tempest_dev_url` e sem `tempest_app.py` empacotado, a Activity roda a **demo embutida** (`MainActivity.DEVICE_DEMO`). ### Modo dev (code-push por LAN) ```bash adb reverse tcp:8765 tcp:8765 tempest serve examples/device_counter/app.py adb shell am start -n org.tempestroid.host/.MainActivity \ --es tempest_dev_url http://localhost:8765 ``` ## Solução de problemas | Sintoma | Causa / correção | |---|---| | `adb start-server`/`devices` trava | Rede *mirrored* do WSL — use o servidor `nodaemon` em background (§3). | | `vhci_hcd: urb->status -104` no `dmesg` | Reset da conexão usbip — refaça `usbipd attach`, troque a porta USB/cabo. | | `INSTALL_FAILED_USER_RESTRICTED` | Ligue **"Instalar via USB"** nas Opções do desenvolvedor (MIUI/HyperOS). | | Gradle falha com erro de AGP | Use o **wrapper 8.11.1** (`./gradlew`), não o Gradle global. | | `dir _*` some do APK | O `ignoreAssetsPattern` padrão do AGP dropa dirs `_*`; já sobrescrito em `app/build.gradle.kts`. | | `usbipd` não reconhecido | usbipd-win não instalado — veja §1; reabra o PowerShell após instalar. | --- ## Sem hardware físico — emulador headless x86_64 (Trilho F7/F8) O aparelho físico no WSL é frágil (o usbipd cai, a MIUI exige *toggles*, a tela bloqueia). O caminho recomendado para validar o lado nativo **sem hardware** é um **emulador headless x86_64**, que cobre tudo que o device cobre — boot do CPython, ponte JNI, renderizador Compose e capacidades nativas — e roda em CI. !!! info "Preview-first" O **simulador Qt** (`make run` / `make dev`) é a sua visualização instantânea de iteração de UI. O **emulador** é a verificação de verdade do lado nativo. Itere no Qt; suba ao emulador só para confirmar Compose/JNI/nativas — não fique esperando o AVD a cada mudança de tela. ### Pré-requisito: KVM ```bash [ -r /dev/kvm ] && [ -w /dev/kvm ] && echo "KVM OK" || echo "sem KVM" ``` Sem `/dev/kvm` (CI sem virtualização aninhada) o emulador é lento demais — use uma **farm de device na nuvem** (Firebase Test Lab / Genymotion SaaS / BrowserStack) como contingência. Os scripts F8 detectam a ausência de KVM e avisam. ### 1. Provisionar o AVD (reprodutível) ```bash make provision-avd # cria o AVD pinado (idempotente; FORCE=1 recria) ``` Instala a *system image* exata (`android-34`, `google_apis`, `x86_64`) e cria o AVD `pixel8_api34`. Rodar de novo é *no-op* — o time inteiro tem o **mesmo** AVD. ### 2. Salvar o *snapshot* golden (boot rápido) ```bash make emulator-snapshot # boota uma vez (gravável) e salva o snapshot 'golden' ``` A partir daí `make emulator` **restaura do snapshot em segundos** (estado limpo conhecido) em vez de *cold-boot*. Re-rode quando a *system image* ou o host mudar. ### 3. Bootar + verificar ```bash make emulator # boot rápido pelo snapshot 'golden' (cai pra cold-boot se não houver) make emulator-verify APP=examples/counter/app.py # boot → stage x86 → APK x86 → install → serve → screenshot VISUAL=1 make emulator-verify APP=examples/counter/app.py # + regressão visual contra o golden versionado ``` O `emulator-verify` faz **gating de prontidão real** (`sys.boot_completed=1` + *bootanim* parado + `pm` respondendo) e **auto-recupera** um AVD travado uma vez antes de desistir — toda chamada `adb` é *time-bounded* (helpers `device_loop.sh` do F5), então um emulador preso nunca pendura o harness. ### 4. Regressão visual `VISUAL=1` compara o screenshot capturado contra um *golden* versionado em `docs/assets/emulator/golden/.png` (tolerância padrão 2%, via `toolchain/visual_regression.py` — Pillow). Um *golden* ausente é **criado** na primeira execução (baseline). Complementa os *goldens* JVM do Roborazzi (F7-camada B) e a conformância (fase D): aqueles pinam a tradução de `Style`; este pina o render ponta-a-ponta no emulador. ### 5. Pool de N emuladores em paralelo (experimental) ```bash make emulator-pool N=3 # sharda a galeria de exemplos entre 3 instâncias isoladas ``` Cada instância é **isolada** (porta/serial próprios, `-read-only` a partir do snapshot golden), então N emuladores compartilham a imagem base sem corromper o estado um do outro; um que trave é recuperado sem derrubar os demais. O tempo de validação cai ~linearmente com cores/RAM. **Experimental — ainda não validado num emulador bootando; valide ponta-a-ponta antes de confiar em CI.** ### 6. Espelhamento ao vivo (`scrcpy`) ```bash make mirror # espelha o emulador/device numa janela do host (precisa WSLg) ``` `scrcpy` mostra e clica o lado nativo ao vivo. No WSL precisa de **WSLg** (X). Não substitui o screenshot do `emulator-verify` — é para inspeção interativa. ### Robustez de GPU no WSL O padrão é `-gpu swiftshader_indirect` (render por software, estável headless). Se a tela vier preta/corrompida, tente `-gpu guest` ou `-gpu host` (este exige WSLg). Gotcha separado do simulador desktop: o Qt no WSL precisa de `QT_QPA_PLATFORM=xcb` (bug do backend wayland) — emulador e simulador têm gotchas de GPU distintos. ### Solução de problemas (emulador) | Sintoma | Causa / correção | |---|---| | `make emulator` faz cold-boot toda vez | Sem snapshot `golden` — rode `make emulator-snapshot` uma vez. | | AVD nunca fica pronto | `emulator-verify` auto-recupera uma vez; persistindo, `emulator -avd -wipe-data` (destrutivo) e re-snapshot. | | `adb` trava sob carga | Helpers `device_loop.sh` dão *timeout* em toda chamada; o `emulator-verify` chama `adb_unwedge` (mata o processo do server + reinicia *nodaemon*) — `adb kill-server` **também trava** quando o server está *wedged*, por isso não o use. | | snapshot save falha | Boot tem que ser **gravável** — `emulator-snapshot` já usa `EMU_READONLY=0`; não salve com `-read-only`. | | sem `/dev/kvm` | Sem aceleração — use farm na nuvem; não insista no emulador local. | --- # File: docs/referencia/api.md — https://mauriciobenjamin700.github.io/tempestroid/referencia/api/ # API pública Tudo abaixo é importável do nível do pacote `tempestroid`. Importe sempre do nível do pacote, nunca de submódulos. ## Estilo (`tempestroid.style`) Objetos de valor Pydantic frozen, diferenciados por valor. - **`Style`** — o modelo de estilo (layout, caixa, pintura, tipografia, dimensão, animação). Veja os campos agrupados no [guia de estilos](../guia/estilos.md). - **`Color`** — `Color.from_hex("#101418")`. - **`Edge`** — insets; `Edge.all(24.0)`. - **`Border`** (uniforme) / **`SideBorder`** (por lado). - **`Corners`** — raios por canto para `Style.radius`. - **`Shadow`** — `box-shadow`/elevação. - **`Gradient`** + **`GradientStop`** — gradiente linear. - **`Transition`** — animação implícita (`duration_ms`, `curve`, `delay_ms`). - Enums: **`FlexDirection`**, **`JustifyContent`**, **`AlignItems`**, **`FlexWrap`**, **`StackAlign`**, **`Position`**, **`TextAlign`**, **`FontWeight`**, **`FontStyle`**, **`TextDecoration`**, **`TextOverflow`**, **`GradientDirection`**, **`Curve`**, **`ImageFit`**, **`KeyboardType`**. Veja o [guia de estilos](../guia/estilos.md). ## Widgets (`tempestroid.widgets`) A IR declarativa — widgets como substantivos. - Layout/conteúdo: **`Widget`** (base), **`Text`**, **`Button`**, **`Column`**, **`Row`**, **`Container`**, **`ScrollView`**, **`SafeArea`** (afasta o filho das barras de status/navegação + notch; `edges`/**`SafeAreaEdge`** escolhe os lados, padrão todos). - **`Component`** (base) — widget composto que se reduz a uma árvore de primitivos via `render()`; o reconciliador o expande antes do *diff*. - Inputs com valor: **`Input`** (texto), **`TextArea`** (multilinha), **`Checkbox`**, **`Switch`** (booleanos), **`Slider`** (float), **`DatePicker`** (data ISO), **`FilePicker`** (seleção de arquivo). - Mídia: **`Image`**, **`Icon`**. - Indicadores: **`ProgressBar`**, **`Spinner`**. - **`EventHandler`** — wrapper tipado de prop de *handler*. Acima estão os **primitivos**. Em cima deles, o Trilho E (paridade Flutter/RN) acrescenta ~70 widgets a mais — navegação (**`Navigator`**/**`TabView`**/ **`TabBar`**/**`RouteDrawer`**), listas virtualizadas (**`LazyColumn`**/ **`LazyRow`**/**`LazyGrid`**/**`SectionList`**), overlays (**`Dialog`**/ **`BottomSheet`**/**`Menu`**/**`Popover`**/**`Toast`**/**`Tooltip`**/ **`ActionSheet`**), animação (**`Animated`**/**`AnimatedList`**/**`Hero`**/ **`Shimmer`**/**`Skeleton`**), gestos (**`GestureDetector`**/**`Draggable`**/ **`Dismissible`**/**`ReorderableList`**/**`InteractiveViewer`**/…), inputs avançados (**`Dropdown`**/**`TimePicker`**/**`RangeSlider`**/**`Autocomplete`**/ **`PinInput`**/**`MaskedInput`**/**`Form`**/**`FormField`**), layout refinado (**`Wrap`**/**`PageView`**/**`AspectRatio`**/**`KeyboardAvoidingView`**) e mídia (**`Canvas`**/**`Svg`**/**`VideoPlayer`**/**`WebView`**/**`Blur`**/ **`BackdropFilter`**/**`ClipPath`** + os device-only **`CameraPreview`**/ **`QrScanner`**/**`MapView`**). Todos suportados pelos **dois** renderizadores (salvo os device-only, que aparecem como *placeholder* no Qt). O catálogo completo, por família, está no [guia de widgets](../guia/widgets.md); a lista viva sai de `tempest spec`. Veja o [guia de widgets](../guia/widgets.md). ## Componentes (`tempestroid.components`) Blocos de construção reutilizáveis — cada um um **`Component`** que se reduz a widgets primitivos, então funcionam nos dois renderizadores (Qt e Compose) sem mudança alguma de renderizador e são prontos para o dispositivo. Todo componente aceita um `style` opcional mesclado sobre o padrão via **`merge_style`**. - **`AppBar`** — barra superior: `leading` opcional, `title` e `actions` à direita. - **`Header`** / **`Footer`** — faixa de cabeçalho (título + subtítulo opcional) e barra inferior centralizada com `children` arbitrários. - **`Sidebar`** — coluna lateral de largura fixa (`width`) com `children`. - **`Scaffold`** — moldura de página empilhando `app_bar`, um `body` que cresce e um `bottom_bar` opcional (`scroll=True` embrulha o corpo num `ScrollView`). - **`NavBar`** — barra de navegação/abas selecionável: rótulos `items`, índice `active` e *callback* `on_select(index)` (generaliza o exemplo `tabs`). - **`Burger`** / **`Drawer`** — botão de menu (☰, `on_click`) e painel lateral controlado (`open` vive no estado do app; alterne pelo burger). - **`Calendar`** — grade do mês com dias selecionáveis: `month` (`"AAAA-MM"`), `selected` (`"AAAA-MM-DD"`) e `on_select(data_iso)`. - **`Clock`** — relógio digital que renderiza um `time` já formatado (o app dirige o tick pelo estado, como o `stopwatch`). - **`Card`** — superfície elevada (sombra + raio) agrupando `children`. - **`ListTile`** — linha de lista: `leading` / `trailing` em volta de `title` + `subtitle` opcional. - **`Avatar`** — emblema redondo de `initials`; **`Divider`** — linha fina. - **`SegmentedControl`** / **`RadioGroup`** — escolha única (`options`, `selected`, `on_select(index)`). - **`Chip`** — rótulo arredondado pequeno, selecionável quando recebe `on_click`. - **`Rating`** — linha de `max_stars` estrelas; `on_rate(value)` torna tocável. - **`Stepper`** — `-`/`+` numérico em volta de um valor, com `min_value` / `max_value` opcionais; `on_change(value)`. - **`SearchBar`** — `Input` de texto controlado com botão de limpar opcional. - **`Accordion`** — seção expansível controlada (`open` no estado, `on_toggle`). - **`Banner`** — barra de status inline (`tone`: info/success/warning/error) com `action` opcional; **`Badge`** — pílula de status; **`EmptyState`** — glifo + título + subtítulo + ação centralizados. - **`Breadcrumb`** — trilha de caminho (`items` + `separator`, `on_select` opc.). - **`Grid`** — grade de `columns` de largura igual com `children`. ## Eventos (`tempestroid.widgets`) — contrato de fronteira tipado - **`Event`** (base), **`TapEvent`**, **`TextChangeEvent`**, **`ToggleEvent`**, **`SlideEvent`**, **`DateChangeEvent`**, **`FileSelectEvent`** e os eventos do Trilho E — navegação (**`RouteChangeEvent`**/**`PageChangeEvent`**), listas (**`ScrollEvent`**/**`EndReachedEvent`**/**`RefreshEvent`**), gestos (**`PanEvent`**/**`ScaleEvent`**/**`SwipeEvent`**/**`ReorderEvent`**/ **`LongPressEvent`**/**`DragEvent`**), formulários (**`SubmitEvent`**/ **`ValidationEvent`**/**`RangeChangeEvent`**/**`TimeChangeEvent`**/ **`SelectEvent`**), overlays (**`DismissEvent`**/**`MenuSelectEvent`**) e plataforma (**`SensorEvent`**/**`LifecycleEvent`**/**`ConnectivityEvent`**/ **`DeepLinkEvent`**/**`QrScanEvent`**/**`ThemeChangeEvent`**/ **`LocaleChangeEvent`**) — 31 no total. - **`parse_event(event_type, raw)`** — portão de fronteira: valida um *payload* cru em um evento tipado ou levanta **`EventValidationError`** com os erros estruturados por campo. É o contrato Python↔Kotlin para a ponte do dispositivo. Veja o [guia de eventos](../guia/eventos.md). ## Núcleo — IR + reconciliador (`tempestroid.core`) - **`Node`**, **`Path`** — a IR rebaixada. - Patches: **`Insert`**, **`Remove`**, **`Update`**, **`Reorder`**, **`Replace`**, e a união **`Patch`**. - **`build(widget) -> Node`**, **`diff(old, new) -> list[Patch]`**. - **`App[S]`** — container de estado agnóstico de renderizador: guarda o estado, constrói via `view(app)`, faz o *diff* e entrega *patches* a um callback `apply_patches`. ## Introspecção (`tempestroid.core`) {#introspeccao} - **`introspect()`** — contrato JSON completo `{"widgets": {...}, "events": {...}}` (alimenta `tempest spec`). - **`widget_catalog()`**, **`event_catalog()`**. ## Renderizador Qt (`tempestroid.renderers.qt`, precisa do extra `qt`) - **`run_qt(state, view, *, title, size)`** — roda um app no simulador Qt. - **`run_dev(app_path)`** — o cockpit do `tempest dev`. ## Lado do dispositivo Compose, ponte JNI, dev server e capacidades nativas — veja a página [Lado do dispositivo (ponte)](dispositivo.md). --- # File: docs/referencia/dispositivo.md — https://mauriciobenjamin700.github.io/tempestroid/referencia/dispositivo/ # Lado do dispositivo (ponte) A metade Python do lado do dispositivo é independente de hardware e testada sem um telefone; o transporte JNI (fase B3) e o renderizador Compose em Kotlin (fase B4) estão implementados em `android-host/` e verificados em um dispositivo arm64 real. ## Tradutor `Style → Compose` - **`to_compose(style)`** (`tempestroid.renderers.compose`) — *spec* serializável `Style → Compose`; o segundo tradutor de `Style` (par com `Style → Qt`). Os dois são fixados pela [suíte de conformidade](../roadmap.md) (fase D). ## Serialização - **`serialize_node` / `serialize_patch`** — rebaixam a IR/patches para dicts JSON-able: *handlers* viram *tokens* de caminho, `Style` vira a *spec* Compose. ## Protocolo de fio Mensagens atravessam uma única fronteira de *marshalling* (a ponte JNI no dispositivo, um canal em memória nos testes). - **`MountMessage`** — `mount` carrega a árvore serializada completa. - **`PatchMessage`** — `patch` carrega uma lista incremental de *patches*. - **`EventMessage`** — `event` carrega um callback dispositivo→Python endereçado por *token* de *handler*. Um *token* de *handler* identifica um *handler* pelo **caminho** do seu nó na árvore mais o nome da prop (ex.: `"0/1:on_click"`). É baseado em caminho (não em chave) para que o lado que emite (serializador) e o que despacha (registry) computem *tokens* idênticos a partir da mesma árvore. ## Transporte e app de dispositivo - **`DeviceApp`** + **`Bridge`** / **`LoopbackBridge`** — ligam um `App` a um transporte de dispositivo; o análogo de `run_qt` no dispositivo. Eventos voltam por *token* de *handler*, são validados por `parse_event` e disparam *patches* coalescidos. - **`JniBridge`** + **`run_device`** — o transporte real no dispositivo (fase B3): `JniBridge` envia mensagens ao Kotlin via o módulo nativo `_tempest_host`; `run_device(state, view)` boota um `DeviceApp` num loop asyncio fresco e marshala eventos de entrada de volta para ele. Importa limpo fora do dispositivo (o módulo nativo é carregado *lazy*), então o framework continua se desenvolvendo/testando no desktop. ## Dev server — code-push por LAN (fase B5) O loop interno estilo Expo: editar na máquina de dev, hot-restart no telefone sem reconstruir o APK (`tempest serve `). - **`DevServer`** — serve o código-fonte do app (`/version`, `/app`) e faz relay dos logs do dispositivo (`/log`) por HTTP. - **`run_dev_client`** — o loop de poll do dispositivo: busca ao mudar → re-exec do código → hot-restart do `DeviceApp`. - **`serve_device(url)`** — entrada do dispositivo ligando o `JniBridge` real + o *sink* nativo + um *fetch* `urllib` no `run_dev_client`. - **`render_qr(url)`** — QR ASCII para pareamento (cai para a URL pura). ## Capacidades nativas (fase B6) Recursos nativos do dispositivo dirigidos do Python como comandos `{"kind": "native"}` que o host Kotlin roteia para módulos de capacidade. - **`notify(title, body="")`** — posta uma notificação de sistema a partir de um *handler*. O padrão de extensão (envelope `native_command` + um roteador de módulos no host) está pronto para mais capacidades (câmera, sensores, …). --- # File: docs/roadmap.md — https://mauriciobenjamin700.github.io/tempestroid/roadmap/ # Roadmap e fases O desenvolvimento segue duas trilhas-base e uma trilha de expansão. **Trilho A** é o framework em Python puro (desktop/CPython). **Trilho B** é o runtime Android (CPython 3.14 + host Kotlin + ponte JNI + renderizador Compose). **Trilho E** é a paridade com Flutter/React Native (**concluído** — E0–E9). O plano completo está em [Plano de design (EN)](plan.md) e, para o Trilho E, em [Plano de paridade](plan-parity.md). ## Trilho A — framework (Python puro) | Fase | Escopo | Status | |---|---|---| | A0 | Fundação: pacote, ferramental, `tempest --help` | ✅ | | A1 | Modelo de estilo + primitivas de widget tipadas | ✅ | | A2 | Reconciliador: `build → diff → patch` | ✅ | | A3 | Renderizador Qt: patches → `QWidget`s, `Style → Qt` | ✅ | | A4 | Loop de eventos async: asyncio ⨉ Qt (`qasync`) | ✅ | | A5 | `tempest dev`: watcher, hot restart, loop de comandos | ✅ | | A6 | Contrato de eventos tipado + introspecção | ✅ | ## Trilho B — runtime Android Todo o Trilho B (B0–B6) está **implementado e verificado num device arm64 real** (Xiaomi `23053RN02A`, Android 15). | Fase | Escopo | Status | |---|---|---| | B0 | CPython 3.14 para arm64 | ✅ | | B1 | Wheels nativas (pydantic-core) + site-packages do dispositivo | ✅ | | B2 | Host Kotlin: embute CPython, boota o interpretador fora da thread de UI via JNI | ✅ | | B3 | Ponte JNI (nativa): transporte bidirecional Python↔Kotlin | ✅ | | B4 | Renderizador Compose (nativo): renderiza a árvore serializada, aplica patches, roteia toques | ✅ | | B5 | Dev server + QR (code-push por LAN + relay de logs) | ✅ | | B6 | Capacidades nativas (notificações) | ✅ | ## Polimento e conformidade | Fase | Escopo | Status | |---|---|---| | C | Polimento: `new`/`build`/`run` + hot reload com estado | ✅ | | D | *Golden snapshots* de conformidade (Qt vs Compose) | ✅ | !!! note "Suíte de conformidade (fase D)" `tests/conformance/` fixa os dois tradutores de `Style`: *golden snapshots* de `to_compose` + `to_qss`/`layout_alignment` para estilos canônicos (regenere com `UPDATE_GOLDEN=1`), além de uma tabela de paridade de cobertura por campo que falha se um tradutor passar a tratar (ou parar de tratar) um campo sem atualizar as divergências documentadas. ## Capacidades nativas — conjunto expandido (pós-B6) Além de `notify`, o pacote `native/` já expõe geolocalização (`get_position`), compartilhamento (`share`/`share_to_whatsapp`/`open_url`), câmera (`take_photo`), armazenamento (`read_file`/`write_file`/`delete_file`/`list_files`), área de transferência (`get_text`/`set_text`) e bluetooth (`scan`). Isso adicionou um formato **request/response** à ponte (antes só *fire-and-forget*): `send_native_request` envia um envelope com `request_id` e dá `await` num `asyncio.Future`; o host responde pelo **mesmo** canal de evento sob o token reservado `__native_result__:` — **sem mudança de C/JNI**. Falhas levantam `NativeError(code)`. !!! warning "Validação no device pendente" A metade Python (envelopes, resolução de *future*, resultados tipados) está **toda coberta por testes off-device** (`tests/unit/test_native.py`). Os módulos Kotlin de capacidade + permissões/`FileProvider` no manifest estão **escritos mas ainda não validados num device** — precisam do toolchain Android SDK/NDK. ## Trilho E — Paridade Flutter / React Native (concluído) Fechou o gap com o que Flutter + RN oferecem de fábrica. Toda fase entrega as **três camadas casadas** (IR/diff + renderizador Qt + renderizador Compose) e só fecha com os **dois renderizadores verdes** + (havendo device) verificação dual. Spec fase-a-fase em [Plano de paridade](plan-parity.md). **Sequência.** E0 (navegação) destrava multi-tela e é pré-requisito de quase tudo; E1–E2 são a base de UX; E3 (animação) é consumida por E0/E2 nas transições; E4–E9 acoplam menos e reordenam por demanda (exceto E6c←E1 e E3d←E0). | Fase | Escopo | Risco núcleo | Status | |---|---|---|---| | E0 | Navegação e rotas (pilha push/pop, abas, gaveta, botão voltar, deep link) | baixo (reusa diff) | ✅ | | E1 | Listas virtualizadas + scroll (lazy, seção sticky, pull-to-refresh, scroll infinito) | médio (diff por janela) | ✅ | | E2 | Overlays e feedback (dialog, bottom sheet, toast, tooltip, menu, action sheet) | **alto** (`Scene` + `Path` namespaced) | ✅ | | E3 | Framework de animação (controller, tween/curva, implícita, gesto, Hero, shimmer) | **alto** (clock de frames) | ✅ | | E4 | Gestos avançados (pan/drag-drop, pinça/zoom, double-tap, dismissible, reorder) | baixo (padrão pronto) | ✅ | | E5 | Inputs e formulários (dropdown, time, range, form/validação, autocomplete, OTP, máscara) | baixo | ✅ | | E6 | Layout refinado (flex-wrap, pager/carousel, app bar colapsável, tabela, aspect ratio) | baixo | ✅ | | E7 | Mídia e gráficos (vídeo, webview, canvas, svg, câmera live, QR, mapa, blur, clip) | médio (IR de canvas) | ✅ | | E8 | Plataforma/sistema (haptics, sensores, lifecycle, permissões, biometria, storage, SQLite, push) | baixo (padrão B6 + token p/ stream) | ✅ | | E9 | Transversais (tema/dark + MediaQuery, i18n/RTL, acessibilidade, fontes custom + escala) | médio (contexto + RTL) | ✅ | !!! info "Tudo dentro do projeto — sem projetos extras" Toda implementação do Trilho E mora **dentro do repositório `tempestroid`**: metade Python no pacote `tempestroid/`, metade Kotlin/Compose em `android-host/`. Nunca criar repositório, pacote PyPI, plugin ou app separado. O único movimento permitido é um **módulo dedicado novo** por área (ex.: `navigation.py`, `animation.py`), sempre re-exportado pelo `__init__.py`. ## Trilho H — design system: componentes estilizados (M3 + API Chakra) Elevar o catálogo de **46 componentes** já existentes (no engine `tempest-core`) a um **design system bonito e coeso**, ancorado visualmente em **Material 3** com a **ergonomia de API do Chakra UI** (`variant`/`size`/`color_scheme` + tokens de tema). Alvo de produto: **pesquisadores acadêmicos** montam apps Android de validação de resultados (junto com o Trilho G de inferência ONNX) com pouco esforço e visual profissional. Plano fase-a-fase em [`docs/plan-design-system.md`](plan-design-system.md). | Fase | Escopo | Risco | Status | |---|---|---|---| | H0 | Sistema de tokens: paleta tonal M3 + `color_scheme`s, escalas de espaçamento/raio/tipografia/elevação/motion; `Theme` resolve, `Style` referencia | **alto** | ✅ done (tempest-core 0.2.0, #109) | | H1 | API de variantes (Chakra): `variant`/`size`/`color_scheme` → `Style` via tema + estados (hover/press/disabled/focus); `Button` piloto | **alto** | ✅ done (tempest-core 0.3.0 + Qt #112 + Compose #113 + conformância #114) | | H2 | Kit base ação/entrada estilizado: Button/IconButton (+ sistema de ícones)/Input/Checkbox/RadioGroup/Switch/Select/Slider + inputs BR | médio | ✅ done (tempest-core 0.4.0 + Qt/Compose #116, device-verificado) | | H3 | Superfície & layout estilizado: Card (elevated/filled/outlined), Surface, Divider, Stack helpers, Container, Grid, ListTile, Accordion | baixo | ✅ done (tempest-core 0.5.0 + Qt + Compose + conformância) | | H4 | Data display & feedback estilizado: Badge/Tag/Chip/Avatar, Alert/Banner, Progress/Spinner, Skeleton, Tooltip, Stat, Rating, EmptyState, SegmentedControl, Stepper | baixo | ✅ done (tempest-core 0.6.0 + Qt + Compose + conformância) | | H5 | Navegação estilizada: AppBar/CollapsingAppBar, NavBar, Drawer/Sidebar, Breadcrumb, Burger, Footer, Header, Scaffold, SearchBar, Tabs (skins M3 sobre os hosts do E0) | médio | ✅ done (tempest-core 0.7.0 + skin pass + conformância) | | H6 | Componentes de pesquisa (liga ao G): MetricCard/StatCard, wrappers de gráfico (canvas E7), DataTable estilizada, ConfidenceBadge, DetectionOverlay (ort-vision-sdk), ImagePicker→ResultView | médio | ✅ done (tempest-core 0.8.1 + Qt + conformância) | | H7 | Galeria (storybook) + docs tutorial-first bilíngues + dark/RTL verificados + conformância (matriz representativa) de tokens/variants | baixo | ✅ done (storybook + docs + dark/RTL Qt + matriz H1–H6) | !!! warning "Trilho cross-repo — três camadas, dois repositórios" Diferente do Trilho E (tudo em `tempestroid`), o Trilho H atravessa **dois repos** porque o engine foi extraído (v0.13.0): camada IR/tokens/componentes → **`tempest-core`**; renderer Qt → **`tempestroid`**; renderer Compose → **`android-host`**. Cada fase só fecha com as **três camadas casadas** + conformância nos dois tradutores `Style`. Tokens/variantes são **aditivos** — `Style` cru continua aceito, apps existentes não quebram. Nenhum pacote PyPI novo: tudo dentro do ecossistema `tempest-core` + `tempestroid`. ## Trilho G — inferência ONNX + stack científica no device (investigação) Rodar inferência de modelos `.onnx` **dentro do app Android nativo** usando o [`ort-vision-sdk`](https://github.com/mauriciobenjamin700/ort-vision-sdk), com `numpy` / `pandas` / `scikit-learn` funcionando no aparelho. **Investigação primeiro** — a viabilidade (qual caminho, quais wheels fecham) é o entregável inicial. Pesquisa fundamentada em [`docs/research/onnx-ml-stack.md`](research/onnx-ml-stack.md). | Fase | Escopo | Risco | Status | |---|---|---|---| | G0 | Spike de viabilidade: deps reais do SDK, decidir caminho CPython-puro vs inferência-nativa, levantar EPs do device-alvo, provar `numpy`+`onnxruntime` no device | médio | ✅ done ([g0-feasibility.md](research/g0-feasibility.md)) — deps/A-B/EPs fechados; **`import numpy` roda no emulador** (wheel `android_24_x86_64` via cibuildwheel 4.1; o `$(BLDLIBRARY)` era bug da 3.4.1). `examples/onnxspike` + `toolchain/build_numpy_x86.sh` | | G1 | Wheel do `onnxruntime` (ou AAR Maven) + 1 modelo `.onnx` real ponta-a-ponta no device, fora da UI thread/loop + escolha de EP (NNAPI/XNNPACK/QNN, fallback CPU, latência medida) | **alto** | ✅ done (emulador) — caminho **(B) AAR**: `ort-vision-sdk` 0.4.0 (backend plugável) + `AarBackend` (Python) + `OnnxModule` Kotlin feature-gated (`onnxruntime-android:1.26.0`). Um `Classifier` real (squeezenet1.1) roda no emulador via AAR fora da UI thread (top-1 matchstick, 569ms). EP caiu pra **CPU** (NNAPI/XNNPACK fallback no emulador); **EP real + arm64 físico + decode de imagem (Pillow android, G2)** = follow-ups | | G2 | Caminho de imagem sem OpenCV (Pillow / `BitmapFactory`; cv2 → SDK nativo, não wheel) + pré/pós em `numpy` | médio | ✅ done (emulador) — `decode_image` (Python) bridge → módulo Kotlin `image` (`BitmapFactory`, `src/main`, sem dep pesada): arquivo/bytes → RGB HWC uint8 → ndarray → SDK. **Device-verificado:** `banana.jpg` real → decode nativo → squeezenet → top-1 **banana (83.9%)**, 963ms, **sem opencv-python nem wheel Pillow** na APK (resize fica no shim numpy). Pillow android wheel = futuro só se precisar de ops puro-Python | | G3 | Otimização de execução: pipeline `.onnx`→`.ort` + quantização (INT8/fp16); avaliar `onnxruntime-extensions` (pré/pós no grafo) | médio | ✅ done (emulador) — `tempest optimize model.onnx -q int8` (host): INT8 dynamic quant + conversão `.ort` (`cli/onnx_optimize.py`). squeezenet1.1 → `.int8.ort` **72% menor** (4840→1337 KiB). **Device-verificado:** o `.ort` quantizado roda via AAR no emulador — banana 81.5% (vs 83.9% fp32), 925ms (vs 819ms fp32: INT8 ganha **tamanho**, não latência no CPU EP do emulador). fp16 via onnxconverter-common (opcional). `onnxruntime-extensions` (pré/pós no grafo) = follow-up | | G4 | Entrega e storage do modelo: embutido vs download+cache, `mmap` no load, Play Asset Delivery p/ modelos grandes | médio | ✅ done (emulador) — **ambas as estratégias provadas no device:** **embutido** (asset no bundle, G1-G3) + **download+cache+verify** (`native/model_store.py` `ensure_model`: cache-first, sha256, off-loop, stdlib). Device-verificado: app baixa squeezenet de localhost (adb-reverse) → cache `…/cache/tempest_models/` → classifica banana (`source=download`, 921ms). `mmap` implícito no load-by-path da AAR. (Fix de host de brinde: TMPDIR não chegava no interpretador embarcado — quebrava todo `tempest serve` — + passthrough de env por intent com allowlist `VISIONSPIKE_`/`TEMPEST_`.) Play Asset Delivery = futuro p/ modelos grandes | | G5 | (opcional) `pandas` no device — feature-engineering tabular | médio | ⏳ planejado | | G6 | (opcional) `scipy` + `scikit-learn` + `scikit-image` no device — ML clássico + img (skimage gated atrás do scipy) | **alto** | 🔬 spike de viabilidade feito ([g6-sklearn-feasibility.md](research/g6-sklearn-feasibility.md)) — **scipy 1.18.0 + scikit-learn 1.9.0 cross-compilam para `android_26_x86_64` com clang puro, ZERO Fortran**: o "calcanhar" sumiu upstream (scipy fortran-free via `-D_without-fortran` + scipy#18566 fechado; OpenBLAS `NOFORTRAN=1 C_LAPACK=1` = LAPACK em C/f2c). Wheels buildadas (`build_openblas_x86.sh`/`build_scipy_x86.sh`/`build_sklearn_x86.sh`), OpenMP via NDK `libomp`. **✅ device (emulador x86_64): scipy 1.18.0 + sklearn 1.9.0 importam + `LogisticRegression.fit/predict` ([2,8]→[0,1]) + `scipy.linalg.solve`=[2,3]** (`examples/sklearnspike`, screenshot). Staging opt-in `make stage-science` (wheels + deps puras joblib/threadpoolctl/narwhals); 3 fixes de packaging (`build.gradle.kts`): reverter trims que dropavam `numpy/f2py`+`pydoc`/`_pyrepl`/`doctest` (scipy/sklearn importam em runtime), `.gz` rename p/ datasets do sklearn, excludes de tests. Pendente: skimage + rebuild arm64 | | G7 | Encolher APK: custom onnxruntime build + modelo quantizado + ABI splits + trim | médio | 🚧 em progresso — **lever 1 (foreign-ABI dead-weight) landado:** `assets/python` empacotava `.so` das DUAS ABIs mas o APK roda em uma só (`abiFilters`); o generated-assets dir acumulava entre trocas de ABI no mesmo checkout. Fix no `build.gradle.kts` (limpa outputDir + exclui `*--linux-android.so`) + **lever 2:** trim numpy runtime-dead (`tests` 7.7M/`f2py`/`_pyinstaller`/`*.pyi`, pure-python). **APK x86_64 73M→57M (−16MB, −22%), device-verificado** (counter tap 0→3 + numpy import/sum/dot). R8 OFF (interpretador chama Kotlin por-nome via JNI → quebra sem keep-rules; ganho dex-only). Falta: custom onnxruntime reduced-op + (talvez) R8 com keep-rules | !!! warning "Dois caminhos, decisão em G0" **(A) CPython puro** cross-compila `onnxruntime`+`numpy`(+`pandas`/`sklearn`) como wheels Android (padrão B1 = `pydantic-core`) e o SDK roda no interpretador embarcado. **(B) Inferência nativa** usa o AAR `onnxruntime-android` (Kotlin/C++) com um shim sobre a ponte JNI, evitando a wheel C++ mais pesada. `scipy`/`sklearn` são o calcanhar (Fortran/LAPACK + OpenMP) — por isso G5/G6 são opcionais e não bloqueiam o caminho de visão (G0→G4, que inclui aceleração por EP, formato `.ort`/quantização e entrega do modelo). Tudo **dentro do repositório**: metade Python em `tempestroid/`, metade Kotlin em `android-host/`; o `ort-vision-sdk` segue dependência externa (não re-implementado aqui). ## Manutenção — skills de qualidade (`.claude/skills/`) Guardas de saúde do framework, encadeadas pelos *gates*: | Skill | Comando | Papel | |---|---|---| | `framework-guard` | `make gate` (`check.sh [--quick]`) | ruff + pyright (strict) + pytest + `mkdocs build --strict` + heurísticas de convenção | | `docs-sync-check` | `make docs-sync` | README ↔ exports vivos ↔ comandos CLI ↔ tabelas de fase | | `phase-closer` | `close.sh ` | valida o "feito quando" de uma fase A–D antes de marcar ✅ | | `android-doctor` | `make doctor` (`check.sh [--quick]`) | valida o toolchain B: SDK/NDK, Gradle wrapper 8.11.1, JDK, device arm64 + gotcha MIUI, runtime staged | | `dual-verify` | `make dual-verify` (`verify.sh [APP]`) | verificação dual obrigatória: *gate* Qt + (havendo device) build/fluxo/screenshot no Compose | | `parity-phase` | `make parity PHASE=…` (`plan.sh `) | conta-parte do `phase-closer` para o Trilho E: spec da fase + invariante das três camadas + *gate* | ## Próximos passos abertos Trilhos A–D, B (B0–B6) e E (E0–E9) estão **concluídos** e verificados em device: os **dois renderizadores** (Qt + Compose) suportam o conjunto completo de widgets, incluindo os inputs com valor no aparelho. O que resta é estabilização para distribuição (Trilho F — ver [`docs/plan-stable.md`](plan-stable.md)): - **F2 — validar as capacidades nativas restantes no device** (1 PR por grupo): geolocation, câmera+áudio, share, bluetooth, connectivity+permissões, biometria plena (digital cadastrada) e push FCM real (precisa `google-services.json`). A metade Python já é testada off-device; falta o exercício em hardware (`make doctor` → `make apk-install` → `dual-verify`). - **F4 — distribuição profissional:** APK release-assinado standalone (keystore própria), ícone adaptativo (`tempest icon --adaptive`) e matriz de cobertura device dos widgets/nativas restantes. - **F7 — alvo de device sem hardware:** emulador headless x86_64 (provado E2E), falta empacotar em `make emulator-verify` + camada B (testes JVM do Compose). - **F8 — emulação estável + visualização nativa:** camada de confiabilidade sobre o F7 — AVD reprodutível, boot por snapshot, auto-recuperação, **pool de N emuladores isolados** (sharding da suíte), screenshot/regressão visual e `scrcpy` (espelhamento ao vivo no WSLg). Tira a dor recorrente do emulador. **Boot-proven (2026-06-14)** + **pool sharded PROVADO em paralelo (2026-06-20):** `make emulator-pool N=2` bootou 2 instâncias isoladas (`-read-only` do snapshot `golden`, portas próprias) → shardou counter+forms → ambos PASS → teardown limpo. Destravou no caminho a causa-raiz crônica do code-push: `resolve_project` subia pro `pyproject.toml` do framework ao servir um example → `tree_signature` no repo inteiro (~6.8s) → timeout; agora pula o pyproject do framework → signature 0ms, app monta. `provision_avd.sh`/`emulator_snapshot.sh`/`emulator_pool.sh`/ `visual_regression.py`/`emulator_verify.sh` + alvos `make` + runbook bilíngue. Pendente menor: N>2 em hardware maior, screen-record mp4, android-doctor checks. - **F9 — driver de testes nativo estilo Playwright:** ✅ **construído** — `tempestroid/testing/` (`Page`/`Locator`/auto-wait + `HeadlessBackend` + `EmulatorBackend` + `EmulatorPool`) + `tempest uitest --target headless|emulator [-j N]` + `examples/*/test_*.py`. API de automação de UI **cross-renderer** (mesmo script no backend in-process **e** no Compose real do emulador), com **auto-wait** (sem `sleep`), locators por Semantics/texto/key, rodando em paralelo/sharded sobre o pool do F8. O "Playwright do nativo". **Alvo `headless` PROVADO verde** (2026-06-20: counter 3/3 PASS — key→tap→ auto-wait→assert `0→1` + handler async); **alvo `emulator` provado no caminho do pool** (F8: `make emulator-pool N=2` shardou counter+forms no Compose real → PASS). O pool agora **fixa em `ANDROID_SERIAL`** p/ hosts compartilhados. `qt`/`device` reservados (`NotImplementedError`). --- # File: docs/plan.md — https://mauriciobenjamin700.github.io/tempestroid/plan/ # tempestroid — Plano do projeto > Framework pessoal para construir apps **Android nativos** escrevendo **Python tipado**, com um simulador desktop (Qt) e dev loop por QR code estilo Expo/Flutter. --- ## 1. Visão Poder construir apps Android usando conhecimento e bibliotecas pythônicas (HTTPX, Pydantic e cia.), com: - **UI nativa** (Jetpack Compose), sem WebView. - **Estilização "web-like"** declarada por objetos Pydantic tipados (vocabulário do CSS, sem a cascata). - **Tipagem como referência**, no espírito do FastAPI: você define o tipo uma vez e ele vira validação em runtime + autocomplete no editor. - **Dev loop instantâneo**: simulador Qt no desktop + celular físico via QR code, ambos com hot reload, controlados por um terminal interativo único. - **Async-first**: o runtime roda sobre um event loop (asyncio); handlers, hooks de ciclo de vida e chamadas nativas podem ser `async def`. Rodar código assíncrono — HTTPX, I/O, tarefas concorrentes — é o caminho padrão, não a exceção. É, em essência, um runtime Python embarcado no Android com uma camada de UI declarativa por cima — um "Flutter de Python", focado só em Android. --- ## 2. Escopo e não-objetivos **É:** - Android-only. - Foco em APK sideloaded (instalar direto no aparelho). - Controle total da toolchain (runtime, ponte, UI, empacotamento). - Suporte a wheels nativas (Rust/C), com o `pydantic-core` como teste de fogo. **Não é (decisões conscientes):** - Não é cross-platform (sem iOS, sem desktop como alvo de produção — o Qt é só simulador de dev). - Não mira lojas de aplicativo, não precisa de assinatura/distribuição formal. - Não é um motor de CSS: o estilo é inline tipado, não há seletores, specificity nem cascata. - Não mira jogos/120fps; é ótimo para apps de dados, formulários e ferramentas. --- ## 3. Arquitetura ### 3.1 A ideia central: uma árvore, múltiplos renderizadores Você não descreve a UI em widgets nativos diretamente. Descreve uma **árvore de widgets declarativa e tipada** (a IR — representação intermediária, modelos Pydantic). Um **reconciliador** faz o diff entre a árvore nova e a anterior e gera uma lista de **patches**. Cada renderizador sabe aplicar patches do seu jeito: ``` Código Python tipado │ ▼ Árvore de widgets (IR, Pydantic) ──diff──► patches │ ├──► Renderizador Qt (simulador desktop, sem device) ├──► Renderizador Compose (celular: dev via QR e APK final) └──► (eventos sobem pela mesma ponte: toque/input → handler → estado → rebuild) ``` O **reconciliador é o mesmo código Python** no desktop e no device. Só muda o renderizador-folha. Toda a divergência entre plataformas fica trancada em dois tradutores de estilo (`Style → Qt`, `Style → Compose`). ### 3.2 Quatro camadas ortogonais | Camada | Responsabilidade | Origem | |---|---|---| | Runtime | Interpretar Python no Android | CPython 3.13+ oficial (PEP 738, `Android/android.py`) | | Wheels nativas | Cross-compilar dependências com Rust/C | Android NDK + maturin + cibuildwheel/crossenv | | Ponte | Python ↔ Kotlin (APIs nativas) | pyjnius (ou JNI próprio) | | Empacotamento | Gerar o APK | Gradle + host Kotlin mínimo | ### 3.3 Onde a tipagem "vaza" (o contrato) Sem WebView, não há fronteira JS↔Python. O contrato tipado mora na fronteira **Python ↔ Kotlin**, e o Pydantic valida os três pontos de cruzamento, análogo ao request/response do FastAPI: 1. **IR → renderizador**: a árvore serializada que o Compose interpreta. 2. **Eventos → handlers**: payloads que voltam do Kotlin (toque, texto) validados antes de entrar na função Python. 3. **Chamadas nativas**: wrappers tipados sobre os módulos de capacidade (câmera, notificações), expostos como awaitables (ver §3.5). ### 3.4 Regra de ouro de execução O Python roda **numa thread de fundo** que hospeda um **event loop asyncio**, nunca na UI thread (senão dá ANR). A ponte marshala dados entre a thread do Python e a thread de UI do Compose. O Python roda **no device** (não no laptop): casa com produção e deixa o acesso nativo local, sem proxy de rede. ### 3.5 Async-first O loop asyncio da thread de fundo é cidadão de primeira classe, não um add-on: - **Handlers e hooks são `async`-friendly**: `on_click`, hooks de ciclo de vida (`on_mount`/`on_unmount`) e tarefas de fundo podem ser `async def`; o framework agenda no loop em vez de bloquear. Handlers síncronos continuam funcionando. - **HTTPX e I/O sem medo**: o cliente async do HTTPX, leitura de arquivos, timers e tarefas concorrentes rodam direto. `await` no meio de um handler é o caminho padrão. - **Estado e rebuild via loop**: quando uma tarefa async termina (ex.: a resposta HTTP chega), ela atualiza o estado e dispara um rebuild. Os rebuilds são **coalescidos no loop** — vários `set_state` no mesmo tick viram um único diff — evitando flicker e trabalho redundante. - **Chamadas nativas viram awaitables**: APIs Android baseadas em callback (câmera, permissões) são embrulhadas em `Future`/awaitable na ponte, então do lado Python você escreve `photo = await camera.capture()` em vez de gerenciar callback. - **Cancelamento no unmount**: tarefas async ligadas a um widget são canceladas quando ele sai da árvore (concorrência estruturada), evitando tarefas órfãs. --- ## 4. Sistema de estilo "web-like" tipado ### 4.1 Princípio Não é uma folha de estilo, é um **objeto de estilo inline tipado** (mais perto do `style` prop do React / CSS-in-JS). Herda o *vocabulário* do CSS como campos Pydantic; descarta a *máquina* (seletores, specificity, herança implícita). Todo estilo é explícito, validado e previsível. ### 4.2 Layout padronizado em flexbox Flexbox é o denominador comum que os dois backends replicam bem: - **Compose**: `Row`/`Column` + `Arrangement` + `Alignment` + `weight` ≈ `flex-direction` + `justify-content` + `align-items` + `flex-grow`. - **Qt**: `QBoxLayout` é flex-like, e o **QSS (Qt Style Sheets) já é uma linguagem tipo CSS** — padding/border/background/radius caem quase direto. ### 4.3 O que mapeia x o que não mapeia **Mapeia limpo (v1):** flex (direction, justify, align, grow), box model (padding, margin), border + radius, background, cor, tipografia (família, tamanho, peso, alinhamento), dimensões (width/height/min/max). **Não mapeia bem (limites conscientes / pós-v1):** `:hover` (não existe em toque), grid (possível, mas diverge mais — depois do flex), z-index complexo, `backdrop-filter`, gradientes elaborados, transições/animações (precisam de suporte explícito). ### 4.4 Defesa contra divergência Os dois tradutores **têm que concordar**. A garantia é um **suite de conformância com golden snapshots**: renderiza o mesmo `Style` no Qt e no Compose e compara. É o que mantém o simulador honesto em relação ao device. --- ## 5. Dev loop: terminal interativo (cockpit único) `tempest dev` controla os dois alvos ao mesmo tempo: sobe o simulador Qt, sobe o dev server na LAN com o QR, e fica num loop interativo estilo `flutter run`: ``` tempest dev ┌──────────────────────┐ │ ▄▄▄▄▄ ▄ ▄▄ ▄▄▄▄▄ │ Escaneie com o app host │ █ █ ▀█▄ █ █ │ na mesma rede Wi-Fi │ █▄▄▄█ ▄▀▄ █▄▄▄█ │ └──────────────────────┘ Dev server ws://192.168.0.42:8765 (WSL mirrored) Simulador Qt ● rodando Dispositivo Pixel 7 ● conectado Comandos: r hot reload (v1: reinicia preservando nada — ver abaixo) R hot restart (estado limpo) s abrir/recarregar simulador Qt d listar dispositivos q sair ``` ### 5.1 v1 = só hot restart Existe **hot reload** (re-roda o build, faz diff, aplica patch, **preserva estado**) e **hot restart** (re-roda do zero, **perde estado**). Preservar estado é a parte difícil. **Decisão v1:** começar **só com hot restart** — robusto e simples. O reload com preservação de estado fica como refinamento pós-v1. É a ordem que o próprio Flutter seguiu. ### 5.2 Modelo de conexão (estilo Expo Go) Um app **host** prebuildado no celular embarca o CPython + o renderizador Compose. O `tempest dev` sobe um dev server na LAN e imprime o QR. Você escaneia, o host puxa o código Python pela rede, roda localmente no celular, e edições disparam restart no simulador **e** no celular ao mesmo tempo. O dev server só faz duas coisas: mandar o código pro host e relayar logs de volta pro terminal. ### 5.3 WSL Pro celular alcançar o dev server na LAN, usar `networkingMode=mirrored` no `.wslconfig` (compartilha o IP com o Windows). Sem isso, configurar port proxy. ADB via Wireless Debugging (TCP) ou `usbipd-win`. --- ## 6. Estratégia: dois trilhos O caminho do simulador Qt **não precisa de nada da toolchain Android**. Isso permite paralelizar e ter retorno rápido. ### Trilho A — DX (CPython desktop puro, zero Android) Reconciliador, IR, modelo de estilo, API tipada de widgets, renderizador Qt, terminal interativo, hot restart no sim. Prova que o framework é gostoso de usar **antes** de encarar Android. Retorno em dias. ### Trilho B — Runtime Android (infra pesada) Cross-compile do `pydantic-core` arm64, host com CPython embarcado, renderizador Compose, ponte JNI, dev server + QR. Pesado, mas desacoplado: não bloqueia a validação da experiência. **Convergência:** o host no celular carrega o **mesmo reconciliador do Trilho A**, trocando só o renderizador Qt pelo Compose. O terminal do A5 ganha o alvo "device". --- ## 7. Fases e marcos Cada fase tem um "feito quando" testável. A ordem dentro de cada trilho é sequencial; os trilhos correm em paralelo após A1. ### Trilho A - **A0 — Fundação.** Layout do pacote, `pyproject.toml`, `CLAUDE.md`, ferramentas (ruff, pyright/mypy, pytest), `tempest --help`. *Feito quando:* `pip install -e .` e a CLI respondem; lint/type-check rodam. - **A1 — Modelo de estilo + widgets.** `Style` (Pydantic) e primitivos tipados: `Widget` base, `Text`, `Button`, `Column`, `Row`, `Container`. *Feito quando:* dá pra montar uma árvore, ela valida, e o type-checker passa limpo. - **A2 — Reconciliador.** `build → diff → patch`. Dados puros, agnóstico de renderizador (insert/remove/update/reorder). *Feito quando:* testes unitários do diff produzem a lista de patches correta. - **A3 — Renderizador Qt.** Aplica patches em `QWidget`s; tradutor `Style → Qt` (QBoxLayout + QSS). *Feito quando:* uma app de exemplo renderiza numa janela Qt a partir da árvore. - **A4 — Loop de eventos (async).** Integrar asyncio ao loop do Qt (ex.: `qasync`); evento → handler (sync ou `async`) → estado → rebuild coalescido → diff → patch. *Feito quando:* um handler `async` que faz `await` (ex.: sleep ou HTTPX) atualiza a tela ao concluir, sem travar a UI. - **A5 — `tempest dev` (sim).** File watcher, hot restart, loop de comandos (r/R/s/q). *Feito quando:* editar `app.py` + `R` reinicia o sim com a UI nova. - **A6 — Contrato tipado + introspecção.** Handlers tipados e validação Pydantic na fronteira; modo de introspecção (lista de widgets/handlers com schemas, análogo ao `/docs`). *Feito quando:* round-trip tipado com validação e erro estruturado. ### Trilho B - **B0 — CPython Android.** Build do CPython 3.13 para `aarch64-linux-android` (PEP 738). *Feito quando:* binário do interpretador para arm64. - **B1 — Wheels nativas (DERISK CRÍTICO).** Pipeline de cross-compile; `import pydantic` (`pydantic-core` Rust) num Python arm64. *Feito quando:* `import pydantic` funciona num celular físico arm64. - **B2 — Host Kotlin mínimo.** Activity, boot do CPython em thread de fundo, "hello from python". *Feito quando:* APK imprime saída do Python no logcat/tela. - **B3 — Ponte JNI.** pyjnius (ou própria) — chamadas Python ↔ Kotlin nos dois sentidos. *Feito quando:* Python dispara um toast/log nativo. - **B4 — Renderizador Compose.** Composable data-driven que interpreta a IR; tradutor `Style → Compose Modifier`. *Feito quando:* a mesma árvore de exemplo do Trilho A renderiza nativa. - **B5 — Dev server + QR.** Host puxa código pela LAN; hot restart no device. *Feito quando:* escanear o QR carrega o app por Wi-Fi e `R` reinicia no celular. - **B6 — Capacidades nativas.** Notificações (`NotificationManager`, canal obrigatório no Android 8+, permissão `POST_NOTIFICATIONS` no 13+); câmera (via `Intent` primeiro, módulo CameraX depois). *Feito quando:* notificação disparada do Python; foto capturada. ### Pós-convergência - **C — Polimento.** ✅ `tempest new` (scaffold de projeto rodável), `tempest build` (empacota o app como asset + dirige o Gradle do `android-host`), `tempest run` (build + `adb install` + logcat). Hot reload com preservação de estado via `App.swap_view`: no cockpit Qt `r` (e o save) reaplica por diff preservando o estado e cai para restart limpo se incompatível; `R` reinicia do zero; no device o code-push faz `DeviceApp.reload` preservando o estado on-device. (`build`/`run` no device exigem SDK/NDK Android.) - **D — Conformância.** Suite de golden snapshots Qt vs Compose, no CI. ### Trilho E — Paridade Flutter/RN (concluído — E0–E9) Trilhos A–D entregaram a fundação (IR, reconciliador, dois renderizadores, dev loop, capacidades nativas básicas). O **Trilho E** fechou o gap de superfície com Flutter + React Native: navegação/rotas, listas virtualizadas, overlays, motor de animação, gestos avançados, formulários, layout refinado, mídia/gráficos, plataforma/sistema e transversais (tema/i18n/a11y). O roadmap descritivo fase-a-fase (E0–E9, com passos IR · Qt · Compose · testes, metas e "feito quando") está em [`plan-parity.md`](plan-parity.md). Cada fase entrega as três camadas casadas e só fecha com os dois renderizadores verdes. O **Trilho G** (investigação) abre a frente de **inferência ONNX + stack científica no device**: rodar modelos `.onnx` via [`ort-vision-sdk`](https://github.com/mauriciobenjamin700/ort-vision-sdk) dentro do app nativo, com `numpy`/`pandas`/`scikit-learn` no aparelho. A viabilidade é o primeiro entregável — dois caminhos em aberto (CPython-puro com wheels android via cibuildwheel, padrão B1; vs inferência nativa pelo AAR `onnxruntime-android` sobre a ponte JNI). Pesquisa fundamentada em [`research/onnx-ml-stack.md`](research/onnx-ml-stack.md); fases G0–G5 em [`roadmap.md`](roadmap.md). `scipy`/`sklearn` são o calcanhar (Fortran/LAPACK + OpenMP) — por isso opcionais e gated, sem bloquear o caminho de visão (G0→G2). --- ## 8. Riscos e mitigação | Risco | Mitigação | |---|---| | Cross-compile de crates Rust mais chato que o esperado | É a fase B1, atacada cedo; pesquisar wheels Android prontas antes de buildar do zero | | pyjnius assume setup do p4a (pode não casar com CPython oficial) | Validar na B3; ter um JNI próprio como plano B; usar o Chaquopy como referência | | Os dois tradutores de `Style` divergirem | Suite de conformância com golden snapshots (fase D) | | tkinter/Qt nunca baterem pixel-a-pixel com o Compose | Sim é para velocidade de iteração; o celular via QR é o teste de verdade | | Ecossistema de wheels Android se moveu pós jan/2026 | Ligar busca web antes de cravar a B1 — pode poupar semanas | | Casar o loop asyncio (thread Python) com a threading do Android (UI thread, callbacks) | Uma única fronteira de marshalling na ponte: `call_soon_threadsafe` para entrar no loop, `runOnUiThread` para sair; callbacks nativos resolvem um `Future` no loop | > **Atenção:** o estado das wheels nativas para Android (suporte do `cibuildwheel`, repositórios de wheels prontas, facilidade do maturin para o target Android) evolui rápido e parte é recente. Verificar o estado atual antes de iniciar a B1. --- ## 9. Estrutura de repositório (proposta) ``` tempestroid/ ├── pyproject.toml ├── CLAUDE.md ├── README.md ├── tempestroid-plano.md # este documento ├── tempestroid/ │ ├── __init__.py │ ├── style.py # Style, Color, Spacing, Edge... (Pydantic) │ ├── widgets/ # Text, Button, Column, Row, Container... │ │ └── __init__.py │ ├── core/ │ │ ├── ir.py # nós da IR (Pydantic) │ │ ├── reconciler.py # build / diff / patch │ │ └── state.py # estado + loop de rebuild │ ├── renderers/ │ │ ├── qt/ # renderizador Qt + Style→Qt │ │ └── compose/ # bindings do lado Python (renderer em Kotlin) │ ├── bridge/ # wrappers JNI (pyjnius) │ ├── native/ # módulos de capacidade: notifications, camera │ ├── devserver/ # servidor LAN, envio de código, relay de logs │ └── cli/ # tempest dev/build/run/new + terminal + QR ├── android-host/ # app host Kotlin (Gradle), renderer Compose, JNI ├── toolchain/ # scripts CPython Android + cross-compile de wheels ├── examples/ │ └── counter/app.py └── tests/ ├── unit/ └── conformance/ # golden snapshots Qt vs Compose ``` --- ## 10. Convenções de código (para o `CLAUDE.md`) Manter o estilo já consolidado nos outros projetos: - **Strings:** aspas duplas em tudo. - **Tipagem:** obrigatória e completa (incluindo `Any` quando necessário); type-checker no CI. - **Docstrings:** estilo Google, em inglês. - **Imports:** de nível de módulo via `__init__.py`, mantendo `__all__`. - **Stack do projeto:** Pydantic (núcleo do modelo), PySide6/Qt (simulador), pyjnius (ponte, fase B). Sem FastAPI/SQLAlchemy/Redis aqui — é um framework, não um serviço web. - **Async-first:** o core assume um event loop asyncio; preferir APIs async, embrulhar callbacks em awaitables e usar concorrência estruturada para o ciclo de vida das tarefas. No Qt, integrar via `qasync`. - **Linguagem:** identificadores e docstrings em inglês; comentários explicativos podem ser PT-BR. Sugestão de fluxo com o Claude Code: - Trabalhar **uma fase por vez**, sempre fechando no "feito quando". - Usar o slash command `review-pr` antes de mergear cada fase. - Manter os testes da fase verdes antes de avançar — especialmente A2 (diff) e D (conformância), que são a espinha dorsal da corretude. --- ## 11. Glossário - **IR** — Intermediate Representation: a árvore de widgets serializável (Pydantic) que os renderizadores interpretam. - **Reconciliador** — compara a árvore nova com a anterior e emite patches. - **Patch** — operação mínima sobre a UI (insert/remove/update/reorder). - **Renderizador-folha** — quem aplica os patches numa tecnologia concreta (Qt no desktop, Compose no Android). - **Tradutor de estilo** — converte `Style` (Pydantic) para o vocabulário do backend (QSS/QBoxLayout ou Compose Modifier). - **Host** — app Android prebuildado que embarca o CPython e roda o código Python (em dev puxa pela LAN; em produção embute). - **Hot restart** — recarrega do zero, estado limpo (v1). - **Hot reload** — recarrega preservando estado (pós-v1). --- ## 12. Próximo passo imediato Começar pelo **Trilho A, fase A0 → A1**: fundação do repo + `Style` Pydantic + os primeiros widgets tipados (`Column`, `Row`, `Text`, `Button`, `Container`). É puro CPython desktop, dá pra ver a árvore de widgets ganhando forma sem tocar em NDK. --- # File: docs/plan-parity.md — https://mauriciobenjamin700.github.io/tempestroid/plan-parity/ # 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`](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) e `Sub-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 `Style` novo entra em `Style → Qt` (`renderers/qt/style_translator.py`) **e** `Style → Compose` (`renderers/compose/style_translator.py`), com entrada na suíte de conformância (`tests/conformance/`). - **Contrato tipado na fronteira.** Eventos novos viram modelos `Event` frozen em `widgets/events.py`, registrados no `event_schemas` (ClassVar) do widget e validados por `parse_event` antes do dispatch. Vão para `introspect()` 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__:` — **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 pacote `tempestroid/`, Kotlin/Compose em `android-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 sobre `androidx`/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 `QWidget`s | | 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; `ActivityResultLauncher`s; 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`):** ```python 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`, sub-tarefas `Ea/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 no `App.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 no `App`), `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 do `Navigator`/abas com `AnimatedContent`). - **Testes:** `tests/unit/test_navigation.py`, `tests/conformance/` (transições). ### Contrato ```python # 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 `view` montando outra subtree → o `diff` existente emite um `Replace` no nó do `Navigator`. **Nenhuma mudança no reconciliador** — só um hint de transição em `props` (`{"transition": "slide"}`). - **Botão voltar:** `MainActivity.onBackPressed()` → `PythonRuntime.dispatchEvent("__back__:")` → `bridge/jni.py:_on_event` reconhece o token reservado → `App.pop()`. Se `pop` retorna `False` (raiz), o host faz o back padrão (fecha app). - **Qt:** `Esc`/botão simulado → `App.pop()`. ### Sub-tarefas - **E0a (core):** `navigation.py` + helpers no `App` + `RouteChangeEvent`. Testes de pilha (push/pop/replace/reset, params tipados). *Não toca renderizador.* - **E0b (Qt):** `Navigator`/`TabView`/`RouteDrawer` no `renderer.py` com `QStackedWidget` + transição `QPropertyAnimation`; `Esc`→`pop`. - **E0c (Compose):** mesmos widgets em `TempestRenderer.kt` com `AnimatedContent`; 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ó com `item_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` (`LazyColumn` nativo), re-exports. - **Testes:** `tests/unit/test_lists.py`, `tests/unit/test_reconciler.py` (janela). ### Contrato ```python 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)` (via `ScrollEvent`); o core chama `item_builder(i)` só nesse range e diffa **apenas a janela** contra a anterior, por **chave de item** (reaproveita `_reconcile_keyed` do A2). O `Node` da lista guarda `props={"item_count": n, "window": [start,end]}` e `children` = só os itens da janela. Mudança de scroll → novo `ScrollEvent` → rebuild da janela. - **Decisão de divergência:** Compose (`LazyColumn`) já virtualiza nativo — pode receber só o `item_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`; `EndReachedEvent` via `derivedStateOf(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`/`diff` cientes da camada), `core/state.py` (`App` guarda `overlays` + API imperativa), `bridge/protocol.py` (`MountMessage`/`PatchMessage` ganham campo `overlays`), `bridge/serializer.py`, `renderers/qt/renderer.py` (z-order + barrier), `TempestTree.kt` (parsear `overlays`), `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**: ```python # 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. ``` - **`Path` ganha namespace:** hoje `tuple[int,...]`. Passa a `tuple[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, agenda `request_rebuild`; `dismiss(id)` remove. `toast(...)` agenda remoção por `loop.call_later`. O `_rebuild` passa a montar `Scene` e chamar `diff_scene`. - **Protocolo:** `MountMessage.overlays: list[dict]`, `PatchMessage` carrega patches com path namespaced. Kotlin: `TempestTree` guarda `root` + `overlays[]`, `MainActivity` renderiza 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` + `Path` namespaced + `App` overlay 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; `DeviceApp` envia `Scene`. - **E2c (Qt):** overlays como `QWidget` z-order + máscara (barrier); `QMenu`; toast com timer+fade. - **E2d (Compose):** `Dialog`/`ModalBottomSheet`/`DropdownMenu`/`Popup`/`SnackbarHost` em `TempestRenderer`/`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 enum `Curve`. - 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_rebuild` por tick — ver Contrato), `style.py` (ampliar `Curve`, talvez `Spring`), `renderers/qt/app_runner.py` (ticker `QTimer`/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** ```python # 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 registro `self._animations: set[AnimationController]`. Enquanto não-vazio, o `App` agenda um **tick por frame** (`loop.call_later(1/60)` no Qt; no device o host chama via `withFrameNanos` → evento `__frame__`). Cada tick: avança cada controller, e chama `request_rebuild` (coalescido). Controller que chega a `value==1` se desregistra → clock para (sem busy-loop). - **Determinismo de teste:** o clock aceita um `time_source` injetável; o teste avança manualmente e verifica frames-chave. **Sem `Date.now`** (proibido nos scripts/loop) — usar o relógio do loop. - **`Animated` widget:** guarda `target` + `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 `Style` alvo), Compose pode delegar ao motor nativo (`animateColorAsState` etc.) lendo `duration`/`curve` do spec — mais fluido. Qt interpola no core. `Hero` = Qt anima geometria no `Replace` de rota; Compose usa `SharedTransitionLayout`. Registrar essa divergência na tabela do Trilho D. ### Sub-tarefas - **E3a (core):** `animation.py` (`AnimationController`/`Tween`/`Curve`) + clock no `App` com `time_source` injetável. Testes determinísticos. *Sem renderizador.* - **E3b (Qt):** ticker no `app_runner`; `Animated`/`AnimatedList` interpolando 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, seguindo `TapHandler`/`SwipeHandler` existentes), `widgets/events.py` (5 eventos), `bridge/protocol.py` (`event_type_for` mapeia 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`/`SwipeEvent` em `gestures.py`/ `events.py`. Cada novo evento é frozen, registrado no `event_schemas` do handler, validado por `parse_event`, mapeado em `event_type_for`. - **Reorder usa o diff existente:** `ReorderEvent(from_index, to_index)` → handler reordena a lista no estado → o `diff` emite `Reorder` (A2). Zero mudança de core. - **Qt:** `QGestureRecognizer`/eventos de mouse; pinça `QPinchGesture`; DnD `QDrag`/`dropEvent`; `InteractiveViewer` = `QGraphicsView` com transform. - **Compose:** `pointerInput` + `detectDragGestures`/`detectTransformGestures`/`detectTapGestures(onDoubleTap)`; `SwipeToDismiss`; reorder via `detectDragGesturesAfterLongPress`; viewer = `graphicsLayer`. ### Sub-tarefas - **E4a:** eventos + handlers (Python) + `event_type_for`. Testes de parse/validação. - **E4b (Qt):** reconhecedores no `renderer.py`. - **E4c (Compose):** `pointerInput` no `TempestRenderer`. - **E4d:** `Dismissible` + `ReorderableList` (compõem E4a–c + diff `Reorder`). ### 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, seguindo `Input`/`Slider`/`DatePicker` existentes), `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 ou `None`); `FormState` agrega erros por campo + validade. `Form` guarda o estado dos campos no `App.state`; submit roda todos os validadores e bloqueia se houver erro — **erro estruturado JSON-serializável**, mesma filosofia do `EventValidationError`. - Novos inputs seguem o padrão de `Input` (valor + `on_change` tipado). - **Qt:** `QComboBox`/`QTimeEdit`/slider duplo custom/`QCompleter`/`setInputMask`. - **Compose:** `ExposedDropdownMenuBox`/`TimePicker` M3/`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`: campo `flex_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` (traduzir `flex_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 de `Style`. `PageView` guarda página ativa no estado; `CollapsingAppBar` coordena 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`/`Icon` já estão aí), `style.py` (talvez `blur`/`clip`), os dois `style_translator.py` (blur/clip — espelhar), `bridge/serializer.py` (spec de comandos do `Canvas`), `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])` onde `DrawCommand` é union frozen (`Path`/`Fill`/`Stroke`/`Text`). `serialize_node` baixa para JSON-able; o diff compara a lista (reusa `_diff_props`). Qt interpreta com `QPainter`; Compose com `drawIntoCanvas`. Entra na conformância dos comandos. - **Folhas com host nativo:** `VideoPlayer`/`WebView`/`CameraPreview`/`MapView` são `AndroidView` no 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` (folhas `AndroidView`). - **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` (em `widgets/layout.py`). - Eventos: `LifecycleEvent`, `SensorEvent`, `ConnectivityEvent`, `DeepLinkEvent`. ### Arquivos - **Novo:** os módulos `native/*.py` acima (seguir `native/camera.py`/`geolocation.py` existentes). - **Edita:** `native/__init__.py` + `native/dispatch.py` (re-export; padrão `send_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ões `ActivityResultContracts`), 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__:`. Resultados tipados (frozen) + `NativeError(code)` em falha. Espelha `native/camera.py`. - **Exceção — streams (sensores) e lifecycle:** eventos **contínuos** do host. Entram pelo canal de evento existente como `EventMessage` com **token reservado novo** (`__sensor__:`, `__lifecycle__`), roteado em `bridge/jni.py:_on_event` como 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 `sqlite3` stdlib, 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`) e `tempestroid/i18n.py` (`Locale`, `translate`/`t`, direção). - `Style`/widgets: campo `semantics` (label/role/hint), `focusable`; `Style` ganha `text_scale`/fonte. - Eventos: `ThemeChangeEvent`, `LocaleChangeEvent`. ### Arquivos - **Novo:** `tempestroid/theme.py`, `tempestroid/i18n.py`. - **Edita:** `core/state.py` (`App` expõe `theme`/`media`/`locale` para o `view` ler — **contexto, não Node**), `widgets/base.py` (`semantics`/`focusable` no `Widget`), `style.py` (`text_scale`/fonte; RTL inverte `start/end`), os dois `style_translator.py` (RTL espelhado + fonte — **conformância**), `core/introspection.py` (expor `semantics`), `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: o `view(app)` lê `app.theme`/`app.media`/`app.locale` e monta de acordo. Trocar tema/locale = mutar esse contexto + `request_rebuild`. **Mantém "árvore é IR".** - **RTL** inverte semântica `start/end` nos **dois** translators (espelhar + conformância). `semantics` é campo do `Widget`, propagado a ambos os renderizadores e ao `introspect()`. - **Qt:** paleta QSS trocável; `MediaQuery` lê tamanho da janela/preset `Device` (já existe); `setLayoutDirection`; `QAccessible`; `QFontDatabase`. - **Compose:** `MaterialTheme`/`isSystemInDarkTheme`; `LocalConfiguration`; `LocalLayoutDirection`; `Modifier.semantics`; `FontFamily` custom + `LocalDensity`. ### Sub-tarefas - **E9a:** `theme.py` + dark mode + `MediaQueryData` (contexto no `App`). 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.** --- # File: docs/plan-stable.md — https://mauriciobenjamin700.github.io/tempestroid/plan-stable/ # tempestroid — Plano de estabilização para uso pelo time (Trilho F) > **Trilho F — Adoção.** Roadmap fase-a-fase para o time usar o tempestroid em > projetos reais, de forma **simples e prática, no estilo pythônico**. > Continuação de [`plan.md`](plan.md) (A–D) e [`plan-parity.md`](plan-parity.md) > (E0–E9, paridade Flutter/RN, já fechado). Os Trilhos A–E entregaram o > framework; este Trilho fecha as **três lacunas operacionais** que separam > "funciona no meu device" de "o time empacota, valida e distribui sem atrito". > > **Nível de detalhe.** Cada fase traz `Estado atual` (o que já existe na árvore), > `Arquivos` (paths reais), `Sub-tarefas` (recorte do tamanho de um agente) e > `Feito quando` (testável e honesto). É spec de implementação, não só roadmap. --- ## 0. Premissas do trilho Herda as invariantes consolidadas (um reconciliador / dois renderizadores; tradutores de estilo espelhados; contrato tipado na fronteira; bridge sem mudança de C quando possível; **tudo dentro do repositório `tempestroid`** — sem projetos extras; verificação dual obrigatória com device; `feito quando` lastreado por testes verdes). Acrescenta uma regra própria: - **Pythônico acima de tudo.** A superfície de uso é Python puro tipado: o app é um módulo com `make_state() -> S` + `view(app) -> Widget`. Toda DX nova (templates, exemplos, build) deve reforçar esse contrato, nunca pedir que o time escreva Gradle/Kotlin/XML à mão. - **Regra de ouro do app:** o arquivo do app é **renderer-agnostic** — importa só `tempestroid` no topo; `run_qt` entra preguiçoso dentro de `__main__`. (Era a causa raiz da tela branca; ver [PR #39].) | Fase | Escopo | Status | Feito quando | |---|---|---|---| | F1 | `tempest build` Gradle por-app: `applicationId` único → apps do time instalam lado a lado | ✅ done (v0.7.0, PR #41) | dois apps tempestroid distintos instalam e rodam simultâneos no mesmo device; `--fast` repackage preservado | | F2 | Validação on-device das capacidades nativas Kotlin (uma PR de verificação por capacidade/grupo) | 🅿️ adiada (futuro) — grupo no-config ✅ (clipboard/storage/database/secure_storage/system); resto parkeado | cada capacidade exercida no device com evidência (screenshot/dumpsys/log) e resultado tipado; matriz de status verde | | F3 | `tempest new --template` multi-arquivo + exemplos de chamadas nativas | ✅ done (v0.7.0, PR #43) | `tempest new --template ` gera projeto multi-arquivo rodável (Qt + device) com exemplo nativo; coberto por teste de scaffold | | F-branding | Ícone + splash no `tempest build` + `tempest icon` (gera de uma imagem) | ✅ done (v0.8.0, PR #47) | ícone default + splash de assets cobrem o boot; `--icon/--splash/--splash-bg` + `tempest icon` device-verificados | | F4 | Distribuição profissional: APK release-signed standalone (keystore própria) + ícone adaptativo + cobertura device dos widgets/nativas restantes | 🚧 em progresso — **(1) APK release-signed ✅** (`tempest build release-apk`); **(2) ícone adaptativo ✅** (`tempest icon --adaptive` + `tempest build --adaptive-icon/--icon-bg`); **(3) matriz de cobertura de widgets publicada ✅** (`docs/referencia/cobertura.md`, PT+EN); (4) fechar F2 + (5) trim pendentes (device/investigação) | `tempest build release-apk --keystore` produz APK release-assinado instalável fora da Play; `tempest icon --adaptive` gera ícone adaptativo (fg/bg); matriz de widgets/nativas device-verificada | | F5 | **Harness de device confiável** — loop de validação on-device à prova de queda de USB (timeout por adb, detecção de drop, checkpoint/resume), base única do `dual-verify` | ✅ implementada (off-device) — **gate de toda device-verify futura** (bloqueia F2, device-verify do release-apk/ícone e os leftovers E8/E9); falta o teste de drop com device conectado | rodar os 24 examples no device sem hang (cada app ≤40s); desconectar o USB no meio aborta limpo com `ABORT … usb-drop` — detecção ≤20s no drop mid-run (~38s no pior caso de adb-server morto no start), sem adb wedged — e a re-execução **retoma** dos faltantes; `dual-verify` nunca reporta verde falso | | F6 | **Trim de tamanho do APK** — enxugar o CPython 3.14 embutido. **Fase-1 (off-device):** pruning seguro de stdlib morta. **Fase-2 (host-side, device-gated):** stdlib-archive/codecs/compressão | 🚧 fase-1 ✅ (PR #74) — baseline real **~39MB** (não ~50MB; já cortado por #70/#71), fase-1 rende ~1MB → **~38MB**; **~20MB exige fase-2 host-side** (Kotlin/C + device, não offline) | **fase-1:** excludes seguros (import trace verde) + APK rebuilda + tamanho documentado + gate verde; **fase-2 (futuro):** APK materialmente menor que boota e roda os examples (Qt + device via F5), custo de 1º boot medido | | F7 | **Alvo de device sem hardware físico** — emulador headless x86_64 (equivalente completo, dirigido pelo harness F5) + testes de tela do renderizador Compose na JVM (Roborazzi). Remove o device físico do caminho crítico | 🚧 **provado ponta-a-ponta** (2026-06-13): AVD x86_64 headless + APK x86_64 → counter renderiza e `0→3` por tap, ZERO hardware físico; falta empacotar em comandos (`make emulator-verify`) + camada B | `make emulator-verify` (sem USB) sobe o AVD x86_64, instala o host e roda a galeria F5 verde com screenshots (CPython+JNI+Compose+nativas); camada B pina o Compose em testes JVM no gate; `dual-verify` trata emulador como leg de device legítimo | | F8 | **Emulação estável + visualização nativa** — camada de confiabilidade sobre o F7: provisionamento reprodutível do AVD, boot determinístico por snapshot, gating de prontidão, auto-recuperação de AVD travado, **pool de N emuladores em paralelo** (isolados, sharding da suíte), pipeline de screenshot/screen-record + regressão visual, espelhamento ao vivo (`scrcpy`) e fallback de farm na nuvem quando não há KVM | ✅ **boot-proven no emulador (2026-06-14)** — `make emulator-snapshot` (cold-boot gravável → readiness gating → salva `golden` → stop) **OK**; `make emulator` restaura do snapshot em **3s**; `make emulator-verify VISUAL=1` **PASS real** (counter monta, screenshot bate o golden); tap "+" 3× → **Count 0→3** (round-trip evento→JNI→handler→patch→recompose) com a fidelidade Compose #80 (texto contrastante branco-no-escuro + cores dos botões corretas). **Bloqueador real encontrado+fixado:** o diálogo `POST_NOTIFICATIONS` (API 34) cobria o host → pre-grant no `emulator_verify.sh`. **Pool sharded PROVADO em paralelo (2026-06-20):** `make emulator-pool N=2` bootou 2 instâncias isoladas do snapshot → shard → ambos PASS → teardown limpo; destravou o code-push (fix do `tree_signature` que escaneava o repo inteiro) | `make emulator` sobe um AVD pinado em ≤ Ns por snapshot, com auto-recuperação se travar; **um pool de N instâncias isoladas roda a suíte em paralelo** (sharded), bound por hardware; o fluxo de um app é capturável (screenshot/vídeo) e comparável a golden; `scrcpy` espelha emulador/device no host (WSLg); CI sem KVM cai pra farm; zero passos manuais frágeis | | F9 | **Driver de testes nativo estilo Playwright** — API de automação de UI de alto nível, **cross-renderer** (mesmo script dirige o backend in-process **e** o Compose no emulador/device), com **auto-wait** (sem sleeps), locators por Semantics/texto/key, ações (tap/type/scroll/back), asserts, screenshot/trace. Roda sobre a ponte + a árvore de Semantics do E9 + a introspecção | ✅ **driver construído + dois alvos implementados** — `tempestroid/testing/` (`Page`/`Locator`/auto-wait + `HeadlessBackend` + `EmulatorBackend` + `EmulatorPool`) + comando `tempest uitest --target headless\|emulator [-j N]`; `examples/*/test_*.py`. **Alvo `headless` PROVADO verde** (2026-06-20: `tempest uitest examples/counter/test_counter.py` → 3/3 PASS — localiza `inc` por key, toca, auto-wait, afirma `Count: 0→1`, inclui handler async). **Alvo `emulator` implementado e provado no caminho do pool** (F8, 2026-06-20: `make emulator-pool N=2` shardou counter+forms no Compose real → ambos PASS); o pool agora **fixa em `ANDROID_SERIAL`** quando setado (`running_emulators`), p/ hosts compartilhados com vários emuladores. `qt`/`device` reservados (`PLANNED_TARGETS` → `NotImplementedError`) | um teste `tempest uitest` localiza um nó por Semantics/texto/key, age, espera a UI estabilizar e afirma o estado — o **mesmo script passa no headless e no emulador/device**; flakes eliminados pelo auto-wait; trace+screenshot por passo em falha | --- ## F1 — `tempest build` por-app (instalar vários apps lado a lado) ### Objetivo Cada app empacotado carrega seu próprio `applicationId`, então dois apps do time (`com.time.vendas`, `com.time.estoque`) instalam **simultâneos** no mesmo aparelho em vez de um sobrescrever o outro (hoje ambos viram `org.tempestroid.host`). ### Estado atual (já no working tree — falta fechar) Trabalho em voo (não commitado) em `cli/release_build.py` + `cli/main.py` + `tests/unit/test_cli.py`: - `derive_app_id(project_name)` → `applicationId` default a partir do nome. - `build_apk(app, *, app_id, ...)` → `gradlew assembleDebug -Ptempest.applicationId=`. - `build_aab(...)` → `gradlew bundleRelease` (loja). - `build_cmd`/`_run_build`/`_run_run` já despacham com `--app-id`. - O host Gradle **já** lê `-Ptempest.applicationId` (`android-host/app/build.gradle.kts:42`). - Testes: `test_build_dispatches_to_apk`, `test_build_uses_given_app_id`, `test_build_reports_gradle_failure`. ### ⚠️ Tensão de design a resolver (decisão da fase) O em-voo **troca o default** de `tempest build` do caminho **repackage sem-Gradle** (introduzido na v0.6.1: "roda de install PyPI, sem SDK/NDK") para **Gradle `assembleDebug`** (exige SDK/NDK + checkout `android-host`). É regressão de portabilidade. Motivo técnico legítimo: o repackage **não reescreve** o package do manifesto binário, então não dá `applicationId` distinto; só o Gradle dá. **Resolução recomendada (manter os dois caminhos):** - `tempest build` → **repackage sem-Gradle** (default, portátil; mantém `org.tempestroid.host` — bom para "rodar rápido", um app por vez). - `tempest build --app-id com.time.x` (ou `--gradle`) → caminho **Gradle** (lado-a-lado; exige toolchain). `--release` (AAB) já implica Gradle. - `derive_app_id` continua, mas só dispara o caminho Gradle quando `--app-id` é dado/derivado **explicitamente** para lado-a-lado. ### Arquivos - `tempestroid/cli/release_build.py` — manter `build_apk`/`build_aab`/`derive_app_id`; **não remover** `package_app_apk`/`apk_repack.repackage_host_apk` (caminho repackage). - `tempestroid/cli/main.py` — `build_cmd`/`_run_build`/`_run_run` despacham repackage **vs** Gradle conforme a flag. - `tempestroid/cli/apk_repack.py` — preservado. - `android-host/app/build.gradle.kts` — `-Ptempest.applicationId` (já existe). - `tests/unit/test_cli.py` — manter os 3 testes + 1 que prova o default repackage. ### Sub-tarefas 1. **Decidir + reintroduzir o default repackage** (resolver a tensão acima); manter Gradle atrás de `--app-id`/`--gradle`/`--release`. Atualizar docstrings/CHANGELOG. 2. **Commitar o em-voo** numa branch `feat/tempest-build-per-app` + gate verde. 3. **README/CLI table**: documentar os dois caminhos e quando cada um aplica. ### Feito quando - Dois projetos (`tempest new vendas`, `tempest new estoque`) → dois APKs com ids distintos → ambos instalam e abrem no **mesmo device** sem se sobrescrever (verificado por `pm list packages | grep time` + screenshot dos dois apps). - `tempest build` sem `--app-id` continua funcionando **de um install PyPI sem SDK/NDK** (caminho repackage preservado). - `framework-guard` + `docs-sync` verdes. --- ## F2 — Validar as capacidades nativas Kotlin no device !!! note "Adiada — fazer no futuro" O grupo **sem-config** (clipboard/storage/database/secure_storage/system) já foi device-verificado (`examples/native_caps/app.py`). Os grupos restantes (geolocation, camera+audio, share, bluetooth, connectivity+permissions, biometria plena, push FCM) ficam **parkeados para o futuro** — o foco atual é o **APK básico e funcional**. Quando retomar, siga "uma PR por grupo" abaixo. ### Objetivo Sair de "metade Python testada off-device" para **cada capacidade exercida no aparelho** com evidência, fechando a ressalva registrada no `CLAUDE.md`. ### Estado atual `NativeModules.kt` (router `handle()`) já tem **todas** as ~20 capacidades fiadas, com a metade Python unit-testada (`tests/unit/test_native.py`) e re-exportada por `tempestroid/native/__init__.py`. O bridge usa o envelope `{"kind":"native"}` + request/response no token `__native_result__:` (sem mudança de C). **Já device-validado** (CLAUDE.md, 2026-06-04): `haptics`, `lifecycle`, `prefs`, `sensors`, `background` (enqueue), `biometrics` (alcança o prompt), `push` (local). **Falta validar no device** (alvo desta fase): | Capacidade | Módulo Python | Precisa no device | |---|---|---| | geolocation | `native/geolocation.py` (`get_position`) | permissão de localização + GPS | | share | `native/share.py` (`share`/`whatsapp`/`open_url`) | intent chooser + WhatsApp instalado | | camera | `native/camera.py` (`take_photo`/`record_video`) | permissão câmera + FileProvider | | audio | `native/audio.py` (`record_audio`/`play_sound`) | permissão microfone | | storage | `native/storage.py` (`read/write/delete/list_files`) | escopo de arquivos do app | | clipboard | `native/clipboard.py` (`get_text`/`set_text`) | — | | bluetooth | `native/bluetooth.py` (`scan`) | permissões BT + hardware | | system | `native/system.py` (status bar/brilho) | — | | secure_storage | `native/secure_storage.py` | Keystore | | database | `native/database.py` (SQLite) | — | | connectivity | `native/connectivity.py` | toggles de rede | | permissions | `native/permissions.py` | fluxo de grant | | biometrics (sucesso pleno) | `native/biometrics.py` | digital cadastrada | | push (FCM real) | `native/push.py` | `google-services.json` + servidor | ### Arquivos - `examples/native//app.py` — um app mínimo por capacidade (ou um app "galeria nativa" com botões por capacidade). - `android-host/.../NativeModules.kt` — só corrige o que falhar no device (handlers já existem); manifesto/`FileProvider` conforme a permissão. - `tests/unit/test_native.py` — mantém a metade Python; device é evidência manual. ### Sub-tarefas (uma PR por linha, ou agrupando as sem-hardware) 1. **Grupo sem hardware/config** (clipboard, system, database, secure_storage, storage) — um app galeria + verificação device numa PR. 2. **geolocation** (permissão + GPS). 3. **camera + audio** (permissões + FileProvider). 4. **share** (chooser + WhatsApp). 5. **bluetooth** (permissões + scan). 6. **connectivity + permissions** (fluxos de grant/toggle). 7. **biometrics pleno** (device com digital) — depende de hardware. 8. **push FCM real** — depende de `google-services.json` + backend (registrar como bloqueado por config externa). ### Feito quando - Cada capacidade do grupo tem evidência on-device (screenshot / `dumpsys` / log) e devolve **resultado tipado** (ou `NativeError(code)` previsível). - Matriz de status no `CLAUDE.md`/README vira verde por capacidade; as bloqueadas por config externa ficam explicitamente marcadas, não silenciadas. --- ## F3 — `tempest new --template` multi-arquivo + exemplos nativos ### Objetivo O time começa um projeto real (multi-arquivo, com chamada nativa) em um comando, sem copiar exemplo na mão. Reforça o contrato pythônico. ### Estado atual - `tempest new ` (`cli/scaffold.py`) gera **um** `app.py` (single-file) + `pyproject` (`[tool.tempest] app`) + README + `.gitignore`. **Sem** `--template`. - A infra **multi-arquivo já existe**: `cli/bundle.py` (`resolve_project`/`build_bundle`/`extract_bundle`/`tree_signature`) e `spec_from_project` já bundlam a árvore inteira para o device (Trilho C). - Exemplos que tocam nativo hoje: `examples/sysverify`, `examples/platform`. ### Arquivos - `tempestroid/cli/scaffold.py` — adicionar `--template`; extrair os templates para um registro (`TEMPLATES: dict[str, Callable[..., ScaffoldResult]]`), mantendo `DEFAULT_APP_TEMPLATE` como `--template default`. - `tempestroid/cli/main.py` — `new_cmd` ganha `--template/-t` (Typer Option). - `tempestroid/cli/templates/` (novo pacote, re-exportado) — um módulo por template com os arquivos-fonte como strings (sem dependência externa). - `tests/unit/test_scaffold.py` (ou `test_cli.py`) — cada template scaffolda e o resultado **importa + casa o contrato** (`make_state`/`view` presentes). ### Templates propostos (mínimo viável) 1. **`default`** — o single-file atual (compatível). 2. **`multi`** — estrutura multi-arquivo pythônica: ``` meu_app/ ├── pyproject.toml # [tool.tempest] app = "app.py" ├── app.py # make_state() + view(app) — só compõe telas ├── state.py # @dataclass AppState (tipado) ├── screens/ # uma função view por tela (Home, Detalhe…) │ ├── __init__.py # re-exporta as telas │ └── home.py └── components/ # widgets compostos reutilizáveis (Component) ``` Demonstra `Navigator`/`Route` (E0) entre telas e o padrão de imports do projeto. 3. **`native`** — o `multi` + uma tela que usa uma capacidade (`notify()` num handler, e um `await get_position()` com `try/except NativeError`), mostrando o padrão async + fire-and-forget vs request/response. ### Sub-tarefas 1. **Registro de templates** em `scaffold.py` + `--template` no `new_cmd` (default inalterado → sem breaking change). 2. **Template `multi`** (state/screens/components + Navigator) + teste de scaffold. 3. **Template `native`** (notify + get_position com NativeError) + teste. 4. **Docs**: página de tutorial "começando um projeto do time" (padrão tiangolo, PT-BR + EN), ligando os três estágios de uso. ### Feito quando - `tempest new x --template multi` gera projeto multi-arquivo que **roda no Qt** (`tempest dev`) e **no device** (`tempest serve`) sem edição. - `tempest new x --template native` idem, com a chamada nativa funcionando (device). - Cada template tem teste verde provando o contrato; README/docs atualizados. --- ## F4 — Distribuição profissional (em progresso) ### Objetivo Sair de "APK pra amigos sideloarem" (debug-signed) para **distribuível de verdade**: APK release-assinado com keystore própria, ícone adaptativo, e a cobertura device fechada. Hoje (v0.8.0) já dá pra mandar um APK pros amigos (`tempest build` → debug-signed, id/ícone/splash próprios, instala por sideload); F4 cobre o salto para "profissional". ### Sub-tarefas 1. ✅ **APK release-signed standalone** — `tempest build release-apk --keystore minha.jks --app-id … --app-version …`: antes só existia APK **debug-signed** (`tempest build`) ou **AAB** de loja (`prd`); agora há um **APK** assinado com a keystore do publisher para distribuir **fora da Play** (site, loja alternativa, link direto) com identidade real. Gradle `assembleRelease` + signing config (reaproveita `ensure_release_keystore`/`ReleaseConfig`/ `_signing_props`; `build_release_apk` em `cli/release_build.py`, target `release-apk` em `cli/main.py` → `_run_release_apk`). Saída `dist/-release.apk`; verificável por `apksigner verify`. **Não cai no fallback `--fast`** — um APK release exige o build real. Device-verificação (instalar + abrir) pendente do toolchain Android. 2. ✅ **Ícone adaptativo** — `tempest icon --adaptive` grava o `ic_launcher_foreground.png`; `tempest build --adaptive-icon --icon-bg <#rrggbb>` emite o adaptive icon real (foreground/background + `mipmap-anydpi-v26/ic_launcher{,_round}.xml`), com o PNG quadrado como fallback pré-API-26. Lê `adaptive_icon`/`icon_bg` de `[tool.tempest]`; Gradle-only (`--fast` avisa). Device-verificação (máscara do launcher) pendente do toolchain Android. 3. ✅ **Cobertura device dos widgets** — matriz publicada em `docs/referencia/cobertura.md` (PT+EN, na nav MkDocs): todo widget primitivo exportado tem case nos DOIS renderizadores (Compose: 62 cases primitivos + 7 de overlay; sem case → `Box`/`Popup` forward-compat); componentes são lowered no Python e nunca chegam ao Kotlin. Gaps de wiring = zero; o que resta é device-verificação por widget (rodadas de device-verify das fases E, contínuo) e os placeholders device-only sinalizados (`CameraPreview`/`QrScanner`/`MapView`). Coluna Compose derivada do `when (node.type)` em `TempestRenderer.kt`. 4. **Fechar a F2** (capacidades nativas restantes no device) — pré-requisito para um app "profissional" que use câmera/geo/etc. 5. **Trim de tamanho** — promovido à fase própria **F6**; ver a seção F6. Saiu de "opcional" porque o peso do APK é o maior atrito de adoção. Achado ao medir (PR #74): baseline real ~39MB (não ~50MB), fase-1 off-device rende ~1MB; ~20MB exige fase-2 host-side device-gated. ### Feito quando - `tempest build --release-apk --keystore …` produz um `.apk` release-assinado que instala num device e abre, assinado com a chave do publisher (verificável por `apksigner verify`). - `tempest icon --adaptive` gera um adaptive icon que o launcher mascara. - A matriz de widgets/nativas device-verificada está publicada e verde nos itens prioritários. --- ## F5 — Harness de device confiável (gate de toda device-verify futura) ### Motivação (incidente 2026-06-13) A validação on-device é o **gargalo real** do trilho de estabilização: F2, o device-verify do `release-apk`/ícone adaptativo e os leftovers E8/E9 todos dependem de um loop de aparelho que funcione de ponta a ponta. Hoje ele **não é confiável**. Rodando `toolchain/validate_gallery.sh` (24 examples via code-push), o aparelho **desanexou do USB do WSL** no 2º app (`brforms`): nenhuma chamada `adb` tem `timeout`, então `cap_md5`/`screencap` **travaram indefinidamente**, o harness ficou 25 min preso, perdeu 23/24 apps (só `animation` chegou a `PASS`), deixou o `adb server` wedged (`adb devices` → rc=124) e `lsusb` parou de ver o device. Conclusão: antes de gastar device-verify em F2/E, o **loop precisa ser à prova de queda**. ### Objetivo Um loop de device que: **(1)** nunca trava — toda chamada `adb` com `timeout`; **(2)** detecta queda de USB/`adb` e **aborta limpo** com diagnóstico (sem deixar `adb` wedged); **(3)** faz **checkpoint por-app** e é **re-rodável** (retoma sem refazer os já-verdes); **(4)** é a **base única** que `dual-verify` e o agente `device-verifier` chamam — nunca um script ad-hoc por fase. ### Estado atual - `toolchain/validate_gallery.sh` existe mas está **untracked** (não commitado) e **frágil**: chamadas `adb` cruas, sem detecção de drop, sem resume, mata só o grupo do `serve` no caminho feliz. - `toolchain/_diag_hotreload.sh` (untracked) — diagnóstico de hot-reload, mesma fragilidade. - `.claude/skills/dual-verify/verify.sh` e `.claude/skills/android-doctor/check.sh` existem mas não detectam um device que **cai no meio** (só checam no início). ### Arquivos - `toolchain/device_loop.sh` (novo) — helper compartilhado: `adbq() { timeout "${ADB_TIMEOUT:-20}" adb "$@"; }`, `device_alive()` (cruza `adb get-state` + `lsusb`), `abort_clean()` (mata serve group + `adb kill-server`), sourced pelos harnesses. Sem dependência externa (estilo do resto da toolchain). - `toolchain/validate_gallery.sh` — **commitar** + endurecer: trocar todo `adb` por `adbq`; pré-checar `device_alive` antes de cada app (drop → grava `ABORT usb-drop` no RESULTS e sai com código distinto); **resume** (lê o RESULTS no início e pula apps já `PASS`); cleanup sempre mata o serve group. - `toolchain/_diag_hotreload.sh` — commitar + mesmo wrapper `adbq`. - `.claude/skills/dual-verify/verify.sh` — chamar o harness endurecido; ao detectar `ABORT … usb-drop`, reportar honestamente "device half abortou em ``" e **falhar** (nunca verde falso), com o passo de recuperação `usbipd attach`. - `.claude/skills/android-doctor/check.sh` — novo check "device estável" (2 leituras de `adb get-state` espaçadas + `lsusb` Android visível) e registrar o gotcha usbipd-WSL nas instruções de recuperação. ### Sub-tarefas 1. **`device_loop.sh`** (helper `adbq`/`device_alive`/`abort_clean`) + commitar os dois harnesses untracked migrados pra ele. 2. **Resume + drop-detect** no `validate_gallery.sh` (checkpoint por-app; abort em ≤20s; sem adb wedged) — testar desconectando o USB no meio. 3. **`dual-verify`/`android-doctor`** consomem o helper e reportam drop sem fingir verde; documentar a recuperação `usbipd attach --wsl --busid ` (Windows). 4. **README/CLAUDE.md**: a regra "device-verify passa pelo harness F5" + a nota do gotcha USB-WSL. ### Feito quando - Com o device conectado, o harness valida os 24 examples (screenshots + tabela PASS/FAIL) **sem nenhum hang** — cada app limitado a ~40s. - Desconectar o USB no meio → aborta em ≤20s com `ABORT … usb-drop`, mata o `serve`, **não** deixa `adb` wedged; a **re-execução retoma** dos apps faltantes (não refaz os `PASS`). - `dual-verify` reporta honestamente "device half abortou" quando cai — nunca verde falso — e `android-doctor` pega o device instável antes do build. - `framework-guard` verde (os scripts em `toolchain/` ficam fora dos gates Python, mas o `dual-verify`/`android-doctor` rodam limpos). --- ## F6 — Trim de tamanho do APK (baseline real ~39MB) ### Por que isso importa (alavanca direta de adoção) O peso do APK é o **maior atrito prático** para o time adotar o tempestroid: cada app que mandam pros colegas é um download/sideload, cada `tempest build`/`install` move os bytes pro device, e numa loja alternativa/link direto o tamanho afasta instalação. Um app "olá mundo" deveria ser o mais leve possível. ### ⚠️ Correção do alvo (medido em 2026-06-13, PR #74) A meta inicial "~50MB → ~20MB" foi calibrada contra um **baseline velho**. Medido de verdade, o APK lean (debug, arm64, sem features extras) já pesa **~39MB**, não ~50MB — o grosso já tinha sido cortado por **#71** (`material-icons-core`) e **#70** (feature-gating de camera/qr/push/video). O **piso é alto e em boa parte irredutível** sem mexer no host: | Componente | Tamanho | Redutível? | |---|---|---| | nativos (`libpython` 5.8MB + `libcrypto` 3.7MB + …) | ~11MB | **não** — já totalmente stripados (`llvm-strip` = 0 bytes) | | `pydantic_core` (wheel nativo) | ~4.6MB | não (dependência core) | | `pydantic` (puro-py) | ~2.0MB | não | | stdlib necessária + tempest_core + framework | resto | pouco | | stdlib morta (test/REPL/wsgiref/lib-dynload de teste) | ~1-2MB | **sim** (off-device, seguro) | **Conclusão honesta:** o único lever seguro off-device (excludes no `build.gradle.kts`) rende **~1MB**. Chegar a ~20MB **não é possível** só com pruning seguro off-device — exigiria mudança no host (stdlib como arquivo único montado em runtime, ou dropar codecs CJK) que é **Kotlin/C + device-gated**, fora do escopo de uma fase offline. O alvo realista off-device é **~37-38MB**; ~20MB vira um esforço host-side separado (F6-fase-2), device-gated. ### Estado / entregue - **F6-fase-1 (PR #74, ✅ off-device):** excludes seguros no `CopyPythonStdlibTask` (`build.gradle.kts`, source-mode, só assets — prefixo de dev intacto): `_pyrepl/`, `wsgiref/`, `doctest.py`, `pydoc.py` + lib-dynload de teste (`_test*.so`, `_xxtestfuzz*`, `xxsubtype*`, `xxlimited*`). Todos provados ausentes do grafo de import (framework + pydantic + tempest_core) por trace off-device. **39MB → ~38MB** (lib-dynload 67 → 54 `.so`). Doc bilíngue "Tamanho do APK" em `docs/guia/build.md` (+ `.en`). Caveat conhecido: os excludes vivem no `@TaskAction`, não num `@Input` → editar a lista exige `--rerun-tasks`/clean (build limpo aplica certo; só afeta re-medição em dev) — documentado nos deploy notes do PR. ### F6-fase-2 (host-side, device-gated — NÃO offline) Para ir materialmente abaixo de ~38MB, os levers restantes precisam do host + validação no device (depende do F5): - **stdlib como arquivo único** (zip/`.pyc` em um `.zip` no `sys.path`) montado em runtime em vez de ~1500 arquivos soltos — corta overhead de filesystem + permite compressão; precisa de mudança no `extractAssets`/boot (Kotlin/C) e medição de custo de 1º boot (o splash cobre). - **dropar codecs CJK** da stdlib (`encodings/`) se nenhum app precisar — economia média, risco médio (validar locale/i18n no device, cruza com E9). - **compressão dos assets** com descompressão no boot. ### Arquivos - `android-host/app/build.gradle.kts` — `CopyPythonStdlibTask` (excludes; fase-1 ✓). - `toolchain/02_stage_deps.sh` / `00_fetch_cpython.sh` — se a allow/deny-list migrar pra etapa de staging. - `MainActivity` (`extractAssets`) — fase-2: stdlib-archive/compressão no boot. - `docs/guia/build.md` (+ `.en`) — tabela antes/depois + piso documentado (fase-1 ✓). ### Feito quando - **Fase-1 (✅):** `build.gradle.kts` dropa a stdlib morta com segurança (import trace verde), APK rebuilda limpo, tamanho medido/documentado, gate verde. - **Fase-2 (futuro, device-gated):** se perseguida, `tempest build` produz um APK materialmente menor que ainda boota o interpretador e roda os examples nos dois renderizadores (Qt + device via F5), sem `ImportError`, com custo de 1º boot medido — e o ganho real vs a complexidade do host registrado antes de seguir. - A redução está medida e documentada (antes/depois por componente). - Nenhum módulo necessário em runtime foi removido (provado pela galeria F5 verde). --- ## F7 — Alvo de device sem hardware físico (emulador + testes de tela JVM) ### Por que isso importa (o device físico é o gargalo recorrente) Toda device-verify hoje depende de um aparelho físico ligado via USB. No WSL isso é **frágil e intermitente**: o usbipd cai (incidente 2026-06-13 que motivou o F5), a MIUI exige "Install via USB", a tela bloqueia. O device vira o gargalo de TODA validação (F2, device-verify, leftovers E8/E9, fase-2 do F6). Precisamos de um alvo **repetível, CI-able e sem hardware** que exercite exatamente o que só o device exercita: o boot do CPython, o transporte JNI, o renderizador Compose e as capacidades nativas. ### A solução em duas camadas **Camada A — emulador headless x86_64 (equivalente completo do device).** Um AVD x86_64 rodando headless cobre tudo que o aparelho cobre (CPython + JNI + Compose + nativas), e o **harness F5 o dirige sem mudança** — `adb -s emulator-5554` é só mais um alvo. Sem USB, sem usbipd, sem MIUI. Roda em CI. **Camada B — testes de tela do renderizador Compose na JVM (rápido, sem emulador).** Roborazzi/Robolectric (ou Compose-desktop test) renderizam os `@Composable` do `TempestRenderer.kt` num teste JVM e comparam contra golden images — pinam o mapeamento `Style → Modifier/Arrangement/Alignment` em segundos, sem device nem emulador. Complementa a conformância da fase D (que pina o lado Python `to_compose`); a camada B pina o **consumo Kotlin** desse spec. ### Estado atual (provado ponta-a-ponta — 2026-06-13) - ✅ **KVM presente** neste host (`/dev/kvm`, 24 flags de virt) → emulador acelerado. **AVD `pixel8_api34` (x86_64, android-34, google_apis) bootou headless** (`-no-window -gpu swiftshader_indirect`, `sys.boot_completed=1`). - ✅ **wheel x86_64 já buildado** (`toolchain/dist/wheels/pydantic_core-…-android_24_x86_64.whl`) e **tarball CPython 3.14 x86_64 já cacheado** pelo cibuildwheel (`~/.cache/cibuildwheel/python-3.14.3-x86_64-linux-android.tar.gz`) — sem download. - ✅ `build.gradle.kts` **já parametriza o ABI** (`-Ptempest.abi=x86_64 -Ptempest.pythonPrefix=…/x86_64`); `00_fetch_cpython.sh`/`env.sh` parametrizados por `TEMPEST_ABI`/`TEMPEST_RUST_TARGET`. - ✅ **PROVA E2E:** prefixo x86_64 staged do cache → `pydantic_core` x86_64 trocado no site-packages → `gradlew :app:assembleDebug -Ptempest.abi=x86_64` (APK 53MB, só libs x86_64) → `adb -s emulator-5554 install` → `tempest serve counter` → **CPython 3.14 x86_64 bootou** (`_socket.cpython-314-x86_64-linux-android.so`, asyncio), counter montou, e **3 taps no `+` → `Count: 3`** (round-trip JNI → handler → patch → recompose), cores do Style corretas. Screenshots em `docs/assets/emulator/`. - 🚧 **Falta:** empacotar os passos manuais em comandos repetíveis (`02_stage_deps.sh` ABI-aware + `make emulator`/`apk-x86`/`emulator-verify`) e a camada B (testes JVM). ### Arquivos - `toolchain/00_fetch_cpython.sh` — rodar com `TEMPEST_ABI=x86_64 TEMPEST_RUST_TARGET=x86_64-linux-android` (tarball oficial x86_64 → `dist/python/x86_64/`). - `toolchain/02_stage_deps.sh` — montar `dist/site-packages` x86_64 (a wheel x86_64 + pydantic puro-py + tempest_core). - `Makefile` — alvos novos: `make emulator` (boot headless do AVD), `make apk-x86` (build `-Ptempest.abi=x86_64`), `make emulator-verify` (boot + install + galeria F5). - `android-host/app/build.gradle.kts` — já suporta; só consome o prefixo x86_64. - `.claude/skills/android-doctor` + `dual-verify` — aceitar `emulator-*` como alvo válido (não só device físico) e preferir o emulador quando nenhum device físico. - **Camada B:** `android-host/app/src/test/java/org/tempestroid/host/` — asserts determinísticos (`StyleComposeMappingTest` + `TempestTreeParseTest`); deps de teste em `android-host/app/build.gradle.kts` (`testImplementation` JUnit4 + Compose BOM + `org.json`); `android-host/app/src/test/roborazzi/…` + `app/src/test/screenshots/*.png` — testes Roborazzi opt-in + goldens versionados. ### Sub-tarefas 1. **Stage x86_64** (CPython prefix + site-packages) — fecha o único gap. 2. **`make emulator` + `make apk-x86`** — boot headless + build x86_64. 3. **`make emulator-verify`** — install no emulador + galeria F5 → screenshots + scan de traceback, tudo sem hardware. Vira o caminho de device-verify default. 4. **android-doctor/dual-verify** aceitam emulador como alvo (e CI). 5. **Camada B** ✅ — testes de tela JVM do renderizador Compose. Duas frentes em `android-host/app/src/test/`: (a) **asserts determinísticos** (sempre no gate, `:app:testDebugUnitTest`, ~3s, sem Robolectric/rede) que pinam as funções puras `Style → Modifier/Arrangement/Alignment/Color` de `TempestRenderer.kt` (`StyleComposeMappingTest`, 20 testes) + o parse do envelope mount/patch em `TempestTree.kt` (`TempestTreeParseTest`, 14 testes) — espelham os mesmos estilos canônicos dos goldens da fase D; (b) **Roborazzi** (opt-in `-Ptempest.roborazzi=true`) que renderiza os `@Composable` via Robolectric e grava/compara PNGs golden em `app/src/test/screenshots/` (Column/Row/Stack/Text canônicos). Regenerar: `./gradlew :app:recordRoborazziDebug -Ptempest.roborazzi=true` (ou `make compose-shots`); verificar: `./gradlew :app:verifyRoborazziDebug -Ptempest.roborazzi=true`. Roborazzi fica off no gate default (baixa runtime do Robolectric na 1ª execução) — os asserts determinísticos é que rodam sempre. 6. **CI** — job que sobe o emulador headless e roda a galeria (gated por KVM no runner). ### Feito quando - `make emulator-verify` (sem nenhum aparelho USB) sobe o AVD x86_64, instala o host x86_64 e roda a galeria F5 verde com screenshots — provando CPython boot + JNI + Compose + nativas sem hardware físico. - ✅ A camada B pina o renderizador Compose em testes JVM que rodam em segundos sem emulador: 34 asserts determinísticos (`Style → Modifier/Arrangement/ Alignment/Color` + parse mount/patch) sempre no gate (`make compose-test`), mais 4 golden images Roborazzi opt-in (`make compose-shots`). Complementa a conformância da fase D (lado Python `to_compose`) pinando o consumo Kotlin. - `dual-verify` trata o emulador como um leg de device legítimo; o device físico vira opcional (confirmação final), não o gargalo. --- ## F8 — Emulação estável + visualização nativa ### Por que isso importa (a dor recorrente) O F7 provou que o **emulador headless x86_64 substitui o device físico**, mas o dia a dia ainda dói: o AVD demora pra bootar e às vezes **trava** (GPU `swiftshader` no WSL, `boot_completed` que nunca chega, `adb` que enrosca), o `make emulator-verify` faz **cold-boot toda vez** (`-no-snapshot -read-only`, lento e não-determinístico), e **ver** o que o renderizador Compose desenhou é difícil — hoje é tirar screenshot na mão. Resultado: o time perde tempo brigando com o emulador em vez de ver o app rodando. O F8 é a **camada de confiabilidade + visualização** sobre o alvo do F7. ### Estratégias (cada uma é uma sub-tarefa) 1. **Provisionamento reprodutível do AVD.** Um script idempotente (`avdmanager`/`sdkmanager`) cria o AVD pinando **system image + API + perfil** exatos (`pixel8_api34`, x86_64, google_apis) — o time inteiro tem o **mesmo** AVD, recriável do zero. Sem "funciona na minha máquina". 2. **Boot determinístico por snapshot.** Salvar um **snapshot "golden"** do AVD já bootado (pós-`boot_completed`) e **restaurar** dele (`-snapshot golden` em vez de `-no-snapshot`): boot em **segundos**, estado limpo conhecido. Cold-boot só quando o snapshot é invalidado (troca de imagem/host). 3. **Gating de prontidão robusto.** Antes de instalar/serve, esperar `sys.boot_completed=1` **e** `init.svc.bootanim=stopped` **e** `pm` respondendo — com timeout por etapa (padrão F5). Acaba com o install flaky "device offline". 4. **Auto-recuperação de AVD travado.** Detectar emulador preso (sem `boot_completed` em N s, `adb` enroscado, GPU morta) → `kill` + cold-boot, e em último caso **wipe-data**/recriar do passo 1. Gerência de **porta/serial** (evitar corrida no `emulator-5554`). 5. **Robustez de render no WSL.** Padrão `swiftshader_indirect`; documentar fallback `-gpu guest`/`host` + os sintomas de cada um. Cruza com o achado do Qt no WSL (`QT_QPA_PLATFORM=xcb` para o simulador) — visualização desktop e emulador têm gotchas de GPU separados, ambos documentados. 6. **Pipeline de screenshot + screen-record + regressão visual.** Capturar screenshot (e opcional `screenrecord` mp4) **por example** no `emulator-verify`, nomeados e versionados em `docs/assets/emulator/`; comparar contra **golden images** (diff de pixels com tolerância) — uma regressão visual no Compose falha o gate. Complementa a **camada B** (Roborazzi, F7) e a conformância (D). 7. **Espelhamento ao vivo (`scrcpy`).** `scrcpy` espelha o emulador (ou um device físico) numa janela no host com input — a forma de **ver e clicar** o lado nativo ao vivo. Documentar no WSL (precisa **WSLg**/X). Um `make mirror` abre. 8. **Preview-first: o Qt é a visualização rápida.** Reforçar o fluxo: o **simulador Qt** (`make run`/`dev`) é a visualização instantânea de iteração; o **emulador** é a verificação de verdade do lado nativo. O dev itera no Qt e só sobe ao emulador para confirmar Compose/JNI/nativas — não fica esperando AVD a cada mudança de UI. 9. **Fallback de farm na nuvem.** Quando **não há KVM** (CI sem virtualização aninhada, máquina sem `/dev/kvm`), cair para **Firebase Test Lab** / Genymotion SaaS / BrowserStack como contingência documentada — o `emulator-verify` detecta a ausência de KVM e aponta o caminho. 10. **Pool de N emuladores em paralelo (bound por hardware).** Subir **várias instâncias** do AVD ao mesmo tempo, cada uma **isolada** (serial/porta próprios via `-port`, `-read-only` + diretório de dados/snapshot próprio para não corromperem o mesmo AVD), e **shardar** a suíte de examples entre elas — o tempo de validação cai ~linearmente com o número de cores/RAM disponíveis. Um gerente de pool aloca/recicla instâncias, respeita um teto calculado do hardware (`nproc`/RAM/KVM) e derruba tudo no fim. É a base de execução paralela que o F9 consome. > **Isolamento é o que dá estabilidade no paralelo.** Cada instância roda > `-read-only` a partir do snapshot golden com **userdata próprio** — assim N > emuladores compartilham a imagem base sem corromper estado um do outro, e um > que trave é reciclado sem afetar os demais (auto-recuperação, item 4, por > instância). ### Estado / entregue (boot-proven no emulador — 2026-06-14) Toda a camada de scripts foi escrita, validada por `bash -n` + a lógica do `visual_regression`, e **boot-provada num emulador x86_64** (ver "Boot-proven" abaixo). Entregue: - `toolchain/device_loop.sh` — helpers de emulador: `kvm_available`, `emu_online`, `emu_wait_ready` (boot_completed + bootanim parado + `pm` respondendo, tudo `adbq`/time-bounded), `emu_boot` (snapshot-aware, `-read-only` por `EMU_READONLY`), `emu_stop`, `emu_recover` (cold-boot + reset do adb). - `toolchain/provision_avd.sh` (novo) — cria/recria o AVD pinado (idempotente, `FORCE=1`). - `toolchain/emulator_snapshot.sh` (novo) — boot gravável uma vez + salva o snapshot `golden`. - `toolchain/emulator_pool.sh` (novo, **experimental**) — N instâncias isoladas + sharding. - `toolchain/visual_regression.py` (novo) — diff Pillow por histograma + cria/atualiza golden. - `toolchain/emulator_verify.sh` — estendido: gating de prontidão + auto-recuperação + `VISUAL=1`. - `Makefile` — `provision-avd`, `emulator-snapshot`, `emulator-pool` (`N=`), `mirror` (`scrcpy`), `emulator` boot-por-snapshot (cai pra cold-boot sem snapshot). - `docs/guia/dispositivo-wsl.md` (+ `.en`) — runbook: KVM, AVD reprodutível, snapshot, regressão visual, pool, `scrcpy`/WSLg, GPU fallback, farm na nuvem, preview-first. ### Boot-proven (2026-06-14, emulador x86_64) - ✅ `make emulator-snapshot`: cold-boot gravável → readiness gating (`boot_completed` + bootanim parado + `pm` respondendo) → salva `golden` → stop. **OK**. - ✅ `make emulator`: restaura do snapshot `golden` em **~3s** (vs ~2min cold). - ✅ `make emulator-verify VISUAL=1`: **PASS real** — counter monta, screenshot bate o golden (`docs/assets/emulator/golden/counter.png`); tap "+" 3× → **Count 0→3** (round-trip evento→JNI→handler→patch→recompose); fidelidade Compose #80 confirmada (texto contrastante + cores dos botões). - ✅ **Bloqueador real fixado:** o diálogo `POST_NOTIFICATIONS` (API 34) cobria o host → pre-grant após o install no `emulator_verify.sh`. ### Pool sharded — PROVADO em paralelo (2026-06-20) ✅ `make emulator-pool N=2 APPS="examples/counter/app.py examples/forms/app.py"`: bootou **2 instâncias ISOLADAS** (`emulator-5554` + `emulator-5556`, cada uma `-read-only` do snapshot `golden` com console/porta próprios → compartilham a imagem base sem corromper estado), ambas atingiram readiness + host instalado, **shardou** os apps entre elas, ambos montaram o app real → screenshots → goldens criados → `EMULATOR-POOL: PASS`, **teardown limpo** (zero emulador stray). Bound por hardware (`N` ≤ `nproc/2`, cap 4). Dois bugs reais encontrados+corrigidos no caminho: 1. **Code-push não montava o app no pool (nem no `serve` em geral).** `resolve_project`, ao servir um `examples//app.py` de dentro do repo, subia até o `pyproject.toml` do **próprio framework** → `tree_signature` escaneava o repo INTEIRO (`docs/assets/*.png`, `android-host/`, todos os examples) em **~6.8s** → estourava o timeout de poll do code-push → o app nunca montava (o screenshot pegava a **home do Android**). Fix: pular o pyproject do framework (`name` ∈ tempestroid/tempest-core/tempestweb) → o example resolve pro **próprio diretório** → `tree_signature` em **0ms**, bundle pequeno e correto. **Isso destrava TODO o code-push/device-verify, não só o pool** (era a causa-raiz das falhas crônicas de `tempest serve` da sessão). 2. **Gate reportava FAIL falso com 0 falhas:** `fails=$(grep -c '^FAIL' || echo 0)` — `grep -c` imprime "0" E sai 1, então o `|| echo 0` duplicava → "0\n0" → `integer expression expected`. Corrigido pra capturar só a contagem. ### Pendente (menor) - `.claude/skills/android-doctor` — check de snapshot golden + KVM + `scrcpy`/WSLg. - Camada de screen-record (mp4) + golden por example (além de counter/forms). - Pool com N>2 em hardware maior (provado com N=2; a lógica escala por `nproc`). ### Feito quando - `make emulator` sobe o AVD **por snapshot em segundos** (não cold-boot), com gating de prontidão e auto-recuperação se travar — sem passos manuais frágeis. - `make emulator-verify` captura screenshot (e vídeo opcional) **por example** e **falha em regressão visual** contra os goldens versionados. - `make mirror` (`scrcpy`) espelha emulador/device no host (WSLg) para ver e interagir com o lado nativo ao vivo. - O runbook bilíngue documenta AVD reprodutível, snapshot, GPU fallback, farm na nuvem e o fluxo **preview-first** (Qt rápido → emulador confirma). - Ausência de KVM é detectada e aponta o fallback de farm, em vez de falhar opaco. - `make emulator-pool N=` sobe `k` instâncias isoladas e o `emulator-verify` **sharda a suíte** entre elas, com teto calculado do hardware; uma instância travada é reciclada sem derrubar as outras; tudo é destruído no fim. --- ## F9 — Driver de testes nativo estilo Playwright ### Por que isso importa (o "Playwright do nativo") Hoje a device-verify é "rode a galeria e olhe o screenshot". Falta o que o Playwright deu pra web: uma **API de automação de UI estável**, com **auto-wait** (sem `sleep` mágico), **locators** semânticos e **asserts** — escrita uma vez e rodando de forma determinística. O F8 dá emuladores estáveis e em paralelo; o F9 dá a **linguagem de teste** que dirige a UI por cima deles (e do simulador Qt). ### A grande sacada: cross-renderer O tempestroid já tem as três peças que um driver precisa, e que a web não tem de graça: a **árvore de Semantics** (E9: `Semantics`/`focusable`/`focus_order`), a **introspecção** tipada (A6) e a **ponte** bidirecional (`dispatchEvent` ↔ mount/patch). Logo o driver pode ser **agnóstico de renderizador**: o **mesmo script de teste** dirige o **simulador Qt** (rápido, local) **e** o **Compose no emulador/device** (verdade nativa), porque os dois falam o mesmo IR + eventos tipados. Isso é mais forte que o Playwright (preso ao DOM): aqui o "DOM" é a nossa IR, idêntica nos dois alvos. ### Forma da API (rascunho) ```python async def test_counter(page): # "page" = um app montado num alvo await page.get_by_text("Count: 0").expect_visible() await page.get_by_key("inc").tap() # locator por key estável da IR await page.get_by_role("button", name="+").tap() await page.expect_text("Count: 2") # auto-wait até a UI estabilizar await page.screenshot("counter-2.png") ``` - **Locators:** por `key` da IR, por texto, por Semantics/role/label (E9), por `focus_order`. Resolvem contra a árvore montada — não contra pixels. - **Auto-wait:** toda ação/asserção espera a árvore **estabilizar** (sem patches pendentes no ciclo de rebuild coalescido do A4) antes de prosseguir — a fonte de flake some sem `sleep`. - **Ações:** `tap`/`type`/`scroll`/`swipe`/`back` viram eventos tipados injetados pela ponte (o mesmo caminho do `dispatchEvent` do device e do `_invoke` do Qt). - **Asserts + trace:** `expect_*` com timeout; em falha, **trace** (sequência de árvores + eventos) e screenshot por passo — debug determinístico. - **Runner:** `tempest uitest ` roda os scripts e escolhe o alvo (`--target headless` — in-process, sem renderer — | `--target emulator` — Compose real); no emulador usa o **pool do F8** para rodar em **paralelo/sharded** (`-j N`). `qt`/`device` ficam reservados (selecioná-los levanta `NotImplementedError`). ### Relação com o que já existe - **Não** substitui a conformância (D) nem a camada B (F7): aqueles pinam tradução de `Style`; o F9 dirige **fluxo de UI ponta-a-ponta** (evento → estado → re-render) nos dois renderizadores. - Reusa o harness F5 (timeout/checkpoint/drop) para a execução no device/emulador. - O `dual-verify` passa a poder rodar **o mesmo teste F9** nos dois legs. ### Arquivos - `tempestroid/testing/` (novo) — o driver: `Page`, locators, auto-wait, ações, `expect_*`, trace; backends por alvo (Qt in-process; emulador/device via ponte). - `tempestroid/cli/` — comando `tempest uitest` (alvo + pool + relatório). - `android-host/` — hook de injeção de evento/serialização de árvore para o driver (reusa `dispatchEvent` + o canal de mount/patch; sem mudança de C/JNI esperada). - `docs/guia/testing.md` (+ `.en`) — tutorial-first do driver, exemplos rodáveis. - `examples/*/test_*.py` — testes de exemplo cross-renderer. ### Feito quando — ✅ atingido (headless verde; emulador via pool F8) - ✅ Um teste F9 localiza um nó por key, age e afirma o estado com **auto-wait** — e o **mesmo script passa no headless e no emulador/Compose** (`examples/counter/ test_counter.py`: headless 3/3 PASS em 2026-06-20; emulador via `make emulator-pool` na F8). `qt`/`device` ficam reservados (`NotImplementedError`). - ✅ O flake por timing some (sem `sleep`): a espera é pela árvore estabilizar (revisão do mirror quieta + nenhum evento em voo). - ✅ `tempest uitest --target emulator -j N` usa o **pool do F8** e shard a suíte em paralelo; o pool **fixa em `ANDROID_SERIAL`** quando setado (host compartilhado). - ✅ Falha gera screenshot real por teste (`docs/assets/emulator/uitest/`) + trace/dump da árvore; `dual-verify` pode rodar o mesmo teste nos dois legs. --- ## Ordem sugerida e dependências ``` F1 (build por-app) ──► desbloqueia distribuir vários apps do time ✅ F3 (templates) ──► acelera começar projetos; usa F1 para empacotar ✅ F4 (1)(2)(3) ──► distribuição profissional (release-apk/ícone/matriz) ✅ F5 (device loop) ──► GATE: harness de device à prova de drop ⬜ └─► F2 (native device) ─┐ └─► device-verify ├─► só confiáveis DEPOIS do F5 └─► leftovers E8/E9 ────┘ F6 (trim APK) ──► fase-1 ✅ ~39→~38MB (off-device); fase-2 ~20MB = host 🚧 F7 (emulador alvo) ──► device sem hardware físico (provado E2E) 🚧 └─► F8 (emulação estável + pool de N + visualização) ──► boot-proven no emulador ✅ (pool ainda experimental) └─► F9 (driver "Playwright nativo", cross-renderer, sobre o pool) ✅ (headless verde; emulador via pool F8) ``` F1/F3/F4(1-3) já entregaram criar + distribuir. **F5 é o novo gate**: a validação on-device era o gargalo frágil (queda de USB trava o loop), então F2, os device-verify pendentes e os leftovers E8/E9 só são confiáveis depois que o harness de device existir. **F6 (trim do APK):** a fase-1 off-device já cortou a stdlib morta com segurança (~39MB → ~38MB, PR #74); o ganho grande (alvo ~20MB) é **fase-2 host-side device-gated**, não off-device — então passou a depender do F5 como qualquer outro device-verify. Fechar F5 → device-verify deixa de ser aposta e destrava a fase-2 do F6. ### Próximos passos (pós-v0.13.0) v0.13.0 publicada: o engine compartilhado foi extraído para **`tempest-core`** (na PyPI) e o `tempestroid` agora o adota como dependência (cópia vendada dropada; `tempestroid/_adopt.py` aliasa `tempest_core.*` sob o path histórico). F4 (1)(2)(3) já entregues (v0.12.0). **F5 é o gate.** A queda de USB de 2026-06-13 mostrou que device-verify sem um harness à prova de drop é tempo perdido — então **F5 vem primeiro** e desbloqueia todo o resto. Ordem de alavanca: 1. **F5 — harness de device confiável** (pré-requisito formal; ver seção F5): `adbq`/timeout em toda chamada adb, detecção de queda de USB com abort limpo, checkpoint/resume por-app, `dual-verify`/`android-doctor` consumindo o helper. Sem isso, F2 e os device-verify abaixo não são confiáveis. 2. **Device-verify do já-mergeado** (barato — código pronto, só rodar **via F5**): adoção do `tempest-core` no device (rodar a galeria pelo harness novo); `release-apk` instala/abre + `apksigner verify`; `--adaptive-icon` mascarado pelo launcher (screenshot). 3. **F6 — trim de tamanho.** Fase-1 off-device **já entregue** (PR #74): pruning seguro da stdlib morta, **~39MB → ~38MB**. O alvo grande (~20MB) é **fase-2 host-side device-gated** (stdlib-archive/codecs/compressão — Kotlin/C + device), então depende do F5. Baseline real é ~39MB (não ~50MB; já cortado por #70/#71). Ver seção F6. 4. **F2 — native device** (1 PR por grupo, **via F5**): geolocation, camera+audio, share, bluetooth, connectivity+permissions, biometria plena (digital cadastrada), push FCM real (`google-services.json` + envio server). 5. **Leftovers E8/E9 no device** (via F5): TalkBack audível (E9), corpo real do WorkManager worker (E8), sucesso pleno da biometria (E8). 6. **F7 → F8 → F9 — caminho da emulação estável + testes.** F7 já provou o emulador headless como alvo (falta empacotar). **F8** mata a dor do dia a dia: AVD reprodutível, boot por snapshot, auto-recuperação, **pool de N emuladores isolados** e pipeline de screenshot/regressão visual + `scrcpy`. **F9** é o **"Playwright nativo"**: driver de UI cross-renderer (mesmo script no Qt e no emulador/device), auto-wait sem `sleep`, locators por Semantics — rodando em paralelo sobre o pool do F8. F8/F9 dependem do F5 (harness) para a execução. [PR #39]: https://github.com/mauriciobenjamin700/tempestroid/pull/39 --- # File: docs/research/android-runtime.md — https://mauriciobenjamin700.github.io/tempestroid/research/android-runtime/ # Pesquisa — rodar CPython recente no Android (Trilho B) > Levantamento web (2025–2026) para fundamentar as fases B0–B6 do `docs/plan.md`. > Fontes primárias citadas. Datas/versões verificadas em maio/2026. > **Atenção:** o ecossistema muda rápido — reconfirmar versões antes de cravar a B1. --- ## TL;DR — caminho recomendado | Fase | Decisão fundamentada | Por quê | |---|---|---| | **B0** runtime | **CPython 3.14 binary release oficial** (PEP 738, Tier 3). Fallback: `Android/android.py` para build custom. | 3.14 (out/2025) **passou a publicar binários Android oficiais** — elimina a B0 do "buildar do zero". | | **B1** wheels nativas | **cibuildwheel ≥ 3.1** para cross-compilar `pydantic-core` arm64. | Suporte Android oficial no cibuildwheel desde 3.1.0 (jul/2025); caminho recomendado para Py 3.13+. Sem wheel oficial de pydantic-core. | | **B2** embedding | Copiar o modelo do **CPython `Platforms/Android/testbed/`** (`PyConfig`/`Py_InitializeFromConfig`). Rodar o interpretador em **thread de fundo** (padrão python-for-android). | É o blueprint oficial de embedding; testbed roda na UI thread só por ser teste. | | **B3** ponte | **DECIDIDO: (Y) CPython oficial 3.14 + JNI próprio.** Confirmado por cross-check: a testbed oficial do CPython usa JNI hand-rolled (`external fun runPython`), não pyjnius/Chaquopy. `rubicon-java` morto; pyjnius/p4a usam forks patcheados. | Alinha ao "controle total da toolchain / CPython oficial" do plano. Ver §4 e runbook. | | **B4** renderer | **Compose data-driven DIY** (`@Composable` recursivo `when(node.type)` + `Modifier` em runtime). **Remote Compose** só como referência (alpha, sem binding Python). | Flexbox→Compose mapeia limpo. Nenhum OSS faz Python→Compose ainda. | | **B5** dev server | LAN HTTP/WS + QR (modelo Expo). WSL: `networkingMode=mirrored`. | Confirmado oficialmente. | --- ## 1. CPython oficial no Android — PEP 738 (Tier 3) - **PEP 738 Final.** Android virou plataforma suportada **Tier 3 no CPython 3.13** (não 3.14). Autor/implementador: **Malcolm Smith** (Chaquopy); contato de ABI: Petr Viktorin / Russell Keith-Magee. — , - **Triples (64-bit only):** `aarch64-linux-android` (arm64-v8a) e `x86_64-linux-android`. 32-bit excluído do Tier 3. - **CPython 3.14 (lançado 7/out/2025): "Binary releases for Android are now provided".** Grande melhoria sobre 3.13 (que exigia buildar você mesmo). — - **Min API level:** 3.13 = API 21 (Android 5.0); **3.14 = API 24 (Android 7.0)** (de `Android/android-env.sh`). - **NDK fixado:** `ndk_version=27.3.13750724` (NDK r27) nos branches 3.13 e 3.14. - **`Android/android.py`** (em `main` → `Platforms/Android/testbed/`): `configure-build` / `make-build` / `configure-host HOST` / `make-host HOST`; atalho `build HOST`; `package HOST` gera tarball em `cross-build/HOST/dist`; `test --connected|--managed` roda a testbed APK (`org.python.testbed`). Build host POSIX (Linux/macOS); precisa `ANDROID_HOME`, `curl`, `java`. — , doc: **Decisão B0:** usar **3.14 binary release oficial**; só cair pro `android.py` se precisar de build custom (debug, flags). Economiza a fase inteira de cross-compile do interpretador. --- ## 2. Wheels nativas — pydantic-core arm64 (DERISK CRÍTICO) - **cibuildwheel 3.1.0 (23/jul/2025) adicionou Android oficialmente** (`platform = "android"`, `CIBW_PLATFORM=android`, archs via `CIBW_ARCHS_ANDROID`). Atual **3.4.1 (abr/2026)**. **Caminho recomendado para Py 3.13+** (a própria doc do Chaquopy aponta pra ele). Host Linux x86_64/macOS, precisa Android SDK, frontend `build`/`uv` (pip **não** suportado pra Android). — , changelog , issue - **maturin** cross-compila `aarch64-linux-android` **na prática** (NDK + `cargo-ndk` ou linker explícito), mas **não é target oficialmente listado/testado**. Bug recente #2945 (jan/2026, "not planned") na detecção `platform.system()=="android"` no port nativo — cibuildwheel contorna; maturin "pelado" pode precisar de patch. — , - **pydantic-core: SEM wheels Android oficiais** (PyPI 2.47.0 só tem manylinux/musllinux/macOS/Win). **Não está** no repo Chaquopy nem no BeeWare mobile-wheels. → **buildar você mesmo com cibuildwheel 3.1+.** O repo `Eutalix/android-pydantic-core` (fresco, v2.46.3 mai/2026, Py 3.9–3.13) é **só Termux** (tag `linux_aarch64`, não PEP 738 `android_*`). — , - **Tag PEP 738:** `android__` (ex.: `android_24_arm64_v8a`). **PyPI já aceita upload de wheels Android** (warehouse PR #17559). — - **Repos prontos** (não têm pydantic-core, mas têm o pesado): Chaquopy `pypi-13.1` (numpy/pandas/scipy/cryptography/pillow/tensorflow...) e **BeeWare mobile-wheels** (~40 pacotes, `android_24_arm64`/`android_24_x86_64`; binários Android hospedados no índice do Chaquopy). — , - **crossenv**: engana pip/setuptools a cross-compilar (só path setuptools, **não** Rust; host Linux; versões build/host iguais). Mecanismo legado, parcialmente superado pelo cibuildwheel. — **Decisão B1:** pipeline `cibuildwheel ≥ 3.4` (host Linux x86_64) cross-compilando `pydantic-core` → wheel `android_24_arm64_v8a`. É a prova de fogo do plano §2. --- ## 3. Embedding do interpretador - **Modelo oficial (PEP 738):** Android só roda Python **embarcado** — carrega `libpython3.x.so` via JNI e dirige pela C-API. Sem Python de sistema, sem subprocess. — - **Blueprint = CPython `Platforms/Android/testbed/`:** - `MainActivity.kt`: `setenv TMPDIR`; `extractAssets()` copia `python/` (assets) → `filesDir/python` (vira PYTHONHOME); `System.loadLibrary("main_activity")` (o shim JNI do app, linkado contra `libpython`); `redirectStdioToLogcat()`; `runPython(home, argv)`. - `c/main_activity.c`: `PyConfig_InitPythonConfig` → `PyConfig_SetBytesArgv` → **`config.home = home`** (é assim que se seta PYTHONHOME, não env var) → `Py_InitializeFromConfig` → `Py_RunMain`. Desbloqueia `SIGUSR1` (Signal Catcher do Android bloqueia). stdout/stderr → pipes → `__android_log_write`. - `app/build.gradle.kts`: jniLibs = `libpython*.so` + `lib*_python.so`; assets = stdlib em `assets/python/lib/pythonX.Y/` + seu código em `site-packages`. **Trick `.gz`→`.gz-`**: AAPT auto-descomprime assets `.gz` e corromperia dados da stdlib; renomeia e `MainActivity` desfaz na extração. - — - **Off-UI-thread (anti-ANR):** o testbed roda na UI thread só por ser harness de teste. O padrão real de thread de fundo é o do **python-for-android**: `PythonService`/`PythonActivity` faz `new Thread(this).start()` cujo `run()` chama o `native nativeStart` que roda o interpretador (na thread do SDL). `run_on_ui_thread` volta pra UI. — (`bootstraps/.../PythonActivity.java`, `PythonService.java`) - **Trick `.so`-in-`lib/`** (Chaquopy): nativos têm que morar em `lib//` com nome `lib*.so` + `android:extractNativeLibs="false"` → armazenados descomprimidos, alinhados e `mmap`ados direto do APK. — **Decisão B2:** clonar a estrutura do testbed (PyConfig embedding) + **rodar `Py_RunMain`/loop asyncio numa background thread própria** (padrão p4a) para casar com a "regra de ouro" do plano §3.4. --- ## 4. Ponte Python↔Kotlin — a encruzilhada | | rubicon-java | pyjnius | Chaquopy | JNI próprio | |---|---|---|---|---| | Status 2025–26 | **arquivado 2022** ☠️ | ativo 1.7.0 (set/2025) | ativo **17.0.0 (dez/2025)** | — | | Mecanismo | JNI + C-API | JNI reflection (Cython) | JNI + CPython + Gradle plugin | C-API via shim JNI | | Embute CPython próprio? | não | não | **sim** | não | | Fit com CPython oficial | era o ideal (morto) | possível via `PYJNIUS_JNIENV_SYMBOL` (sem exemplo documentado) | **não** — adota o interpretador dele | **melhor fit** (é o modelo PEP 738) | | Esforço | — | médio (caminho não trilhado) | **baixo** | alto | | Licença | BSD (morto) | MIT | **MIT, grátis desde 12.0.1** | — | | Python | — | genérico | **3.10–3.14** | qualquer | - **`rubicon-java` MORTO** (arquivado 12/out/2022). Era o bridge "CPython oficial + JNI + C-API". **Não usar.** BeeWare migrou pra Chaquopy. `rubicon-objc` vive (só iOS). — - **Chaquopy** é o bridge mais completo e documentado: `from java import …`, `dynamic_proxy`/`static_proxy`, `Python.getInstance().getModule().callAttr()`, conversão automática de tipos. **MIT/grátis desde 12.0.1**, **17.0.0 suporta Py 3.10–3.14** (3.13+ exigido pro requisito de 16 KB page do Android 15, vigente 1/nov/2025). **Mas embute o próprio build de CPython** — você adota o interpretador do Chaquopy, não aponta pra um CPython oficial externo. Não pode rodar na main thread. — , - **pyjnius**: independente do Kivy; no Android pega o JVM existente. Hook **`PYJNIUS_JNIENV_SYMBOL`** permite plugar seu próprio getter de `JNIEnv*` → **em tese** roda com CPython oficial embarcado, mas **não há exemplo standalone documentado** (gap de evidência). `run_on_ui_thread` é do p4a, não do pyjnius. — , - **Threading/asyncio:** Java callback dispara em thread do JVM ≠ thread do loop asyncio → usar **`loop.call_soon_threadsafe(...)`** (ou `run_coroutine_threadsafe`) resolvendo um `Future` para transformar callback nativo (câmera/permissão) em awaitable. UI: `runOnUiThread`. (Composição é prática estabelecida, sem fonte única canônica.) **Encruzilhada B3 (decisão do usuário — muda semanas de trabalho):** - **Opção Y — CPython oficial 3.14 + JNI próprio.** Alinha 100% ao plano ("controle total da toolchain", "CPython oficial", §3.2/§6). Mais trabalho: reimplementar marshalling, GIL/`AttachCurrentThread`, refs JNI, tradução de exceções. É reconstruir o que o `rubicon-java` automatizava. - **Opção X — Chaquopy.** Caminho mais curto: resolve embedding + ponte + Gradle + repo de wheels de uma vez, grátis, suporta 3.14. **Custo:** cede o controle da toolchain (usa o CPython empacotado por ele) — contradiz a premissa do plano. `pydantic-core` ainda precisa de cibuildwheel de qualquer forma (não está no repo deles). - **Híbrido sugerido:** **spike rápido com Chaquopy** para provar o conceito ponta-a-ponta (APK rodando Python + host Compose) e validar `pydantic-core`; manter a porta aberta para migrar pra Y se o controle limitar. --- ## 5. Renderer Compose data-driven + dev loop - **Remote Compose (`androidx.compose.remote`)** — server-driven UI oficial do Google: árvore Compose serializada em binário, device renderiza nativo. **ALPHA (1.0.0-alpha11, mai/2026)** — API instável, cobertura de primitivos incompleta. Authoring `remote-creation-jvm` **sem dependência de Android SDK**, mas **sem binding Python** (emitir o formato binário seria trabalho novo). → **referência futura, não dependência.** — - **DIY (recomendado v1):** `@Composable` recursivo com `when(node.type) { is TextNode -> ...; is ColumnNode -> RenderChildren(...) }`; `Modifier` montado em runtime a partir do `Style`. OSS de referência: `skydoves/server-driven-compose`, `jesusdmedinac/json-to-compose`. - **Flexbox → Compose (API atual confirmada):** `flex-direction`→`Row`/`Column`; `justify-content`→`horizontalArrangement`/`verticalArrangement` (`Arrangement.Start/Center/SpaceBetween/spacedBy`); `align-items`→`verticalAlignment`/`horizontalAlignment` (`Alignment.*`); `flex-grow`→`Modifier.weight()`; wrap→`FlowRow`/`FlowColumn`. Box model→`Modifier.padding/background/border/clip/size`. **Nosso `Style` mapeia direto** — confirma plano §4.2. — , - **Prior art "Python define UI → nativo renderiza":** **Flet** (→ Flutter, protocolo JSON) e **BeeWare/Toga** (→ native Views). **Nenhum OSS faz Python→Compose** — tempestroid seria novidade nesse nicho. — - **Dev loop:** Expo = QR codifica URL do dev server; app container baixa o bundle via HTTP na LAN. Flutter hot reload = push de kernel `.dill` via VM Service Protocol. — , - **WSL2 → celular na mesma Wi-Fi:** `[wsl2] networkingMode=mirrored` no `.wslconfig` + `wsl --shutdown`; liberar Hy-V firewall inbound (`Set-NetFirewallHyperVVMSetting ... -DefaultInboundAction Allow`); bind do dev server em `0.0.0.0`. Alternativa NAT: `netsh interface portproxy`. ADB wireless: `adb pair`/`adb connect` (Android 11+). **usbipd-win instável com Android** (issues #232/#248) — preferir Wireless Debugging. — , **Decisão B4/B5:** renderer Compose DIY (`when(type)` + Modifier runtime); dev server LAN HTTP/WS + QR; WSL mirrored. --- ## Pontos de atenção (gaps de evidência) 1. Versões mudam rápido — **reconfirmar cibuildwheel/maturin/Chaquopy/CPython antes da B1.** 2. **Sem caminho documentado de pyjnius standalone** com CPython oficial (só o hook de código). 3. Provenance exata do CPython do Chaquopy (vanilla vs patched) não documentada. 4. Remote Compose é **alpha** — não shippar. 5. `pydantic-core` **tem que ser buildado** (cibuildwheel) em qualquer caminho — não há atalho de wheel pronto PEP 738. --- # File: docs/research/android-runbook.md — https://mauriciobenjamin700.github.io/tempestroid/research/android-runbook/ # Runbook executável — Trilho B (runtime Android) > Passo a passo para rodar numa máquina com toolchain Android (Linux x86_64 ou > macOS). **Não roda neste ambiente WSL sem SDK/NDK.** Fundamentado em > [`android-runtime.md`](./android-runtime.md) (fontes primárias, mai/2026). > > **Decisões fixadas:** > - Runtime: **CPython 3.14 oficial** (PEP 738 Tier 3; binários Android oficiais). > - Wheels nativas: **cibuildwheel ≥ 3.4**. > - Ponte: **JNI próprio** sobre a C-API (modelo da testbed oficial), **sem** > pyjnius/Chaquopy/p4a — controle total, CPython não-patcheado. > - Renderer device: **Jetpack Compose data-driven** (DIY, `when(type)` + `Modifier`). > - Reconciliador/IR/Style/eventos: **reutilizar o Trilho A** sem mudança. --- ## Pré-requisitos (host de build) | Ferramenta | Versão alvo | Notas | |---|---|---| | SO host | Linux x86_64 **ou** macOS | cross-build do CPython e cibuildwheel exigem POSIX. WSL2 conta como Linux. | | Android SDK | command-line tools em `cmdline-tools/latest`; `ANDROID_HOME` setado | `sdkmanager` instala o resto. | | Android NDK | **r27 (27.3.13750724)** | versão fixada por `Android/android-env.sh` do CPython 3.14. | | JDK | 17+ | Gradle/AGP. | | Rust | stable + `rustup target add aarch64-linux-android` | para `pydantic-core`. | | `cargo-ndk` | recente | linker NDK para crates Rust. | | uv | ≥ 0.7 | frontend de build (`pip` não suporta Android). | | Dispositivo | Android 7.0+ (API 24), arm64-v8a | + Wireless Debugging para B5. | Variáveis: `export ANDROID_HOME=...`, `export ANDROID_NDK_HOME=$ANDROID_HOME/ndk/27.3.13750724`. --- ## B0 — CPython 3.14 para arm64 **Caminho rápido (recomendado):** baixar o release Android oficial. ```bash # https://www.python.org/downloads/android/ (3.14.x, aarch64) # Resultado: prefix/ com lib/libpython3.14.so + lib/python3.14/ (stdlib) ``` **Caminho custom (se precisar de flags/debug):** cross-build com o helper oficial. ```bash git clone --branch v3.14.x --depth 1 https://github.com/python/cpython cd cpython ./android.py build aarch64-linux-android # configure/make build+host ./android.py package aarch64-linux-android # tarball em cross-build/.../dist # (em main/3.15: python3 Platforms/Android build aarch64-linux-android) ``` **Feito quando:** existe `libpython3.14.so` arm64 + stdlib empacotada (`prefix/`). **Validação:** `file libpython3.14.so` → `ELF 64-bit ... ARM aarch64`. --- ## B1 — Wheels nativas (DERISK CRÍTICO): `pydantic-core` arm64 Não há wheel Android pronta de `pydantic-core` → cross-compilar com cibuildwheel. ```bash uv tool install cibuildwheel # ≥ 3.4 git clone --depth 1 https://github.com/pydantic/pydantic-core && cd pydantic-core export CIBW_PLATFORM=android export CIBW_ARCHS_ANDROID=arm64_v8a # + x86_64 p/ emulador, se quiser export CIBW_BUILD="cp314-*" cibuildwheel --platform android --output-dir wheelhouse # host Linux x86_64/macOS; precisa ANDROID_HOME, NDK r27, Rust + target android, # cargo-ndk no PATH. Frontend de build = build/uv (NÃO pip). ``` **Feito quando:** `wheelhouse/pydantic_core-*-cp314-cp314-android_24_arm64_v8a.whl` existe e `import pydantic` roda num Python arm64 (testbed/emulador). **Riscos conhecidos:** detecção `platform.system()=="android"` no maturin (issue #2945) — cibuildwheel contorna; se buildar maturin "pelado", aplicar o patch. Reconfirmar versões de cibuildwheel/maturin antes de começar (muda rápido). **Atalho p/ deps pesadas** (numpy/cryptography/...): índice Chaquopy `pypi-13.1` ou BeeWare mobile-wheels (mas **não** têm pydantic-core). --- ## B2 — Host Kotlin mínimo + boot do CPython em thread de fundo Espelhar `Platforms/Android/testbed/` do CPython, com uma diferença: rodar o interpretador numa **thread de fundo** (não na UI thread — testbed só usa UI por ser teste). Estrutura em `android-host/` (Gradle): - `app/src/main/java/.../MainActivity.kt` - `setenv("TMPDIR", cacheDir)`; `extractAssets()` copia `python/` → `filesDir/python` (= PYTHONHOME); desfaz o rename `.gz-`. - `System.loadLibrary("tempest_host")` (shim JNI linkado contra `libpython3.14`). - **start em background:** `Thread { runPython(pythonHome, argv) }.start()` (a UI fica livre; sem ANR). - `redirectStdioToLogcat()`. - `app/src/main/c/tempest_host.c` (JNI): - `PyConfig_InitPythonConfig` → `PyConfig_SetBytesString(&config, &config.home, home)` → `Py_InitializeFromConfig` → roda o entrypoint (`Py_RunMain` ou `PyImport_ImportModule`). Desbloquear `SIGUSR1`. - `app/src/main/c/CMakeLists.txt`: `link_libraries(log python3.14)`, include de `python3.14`. - `app/build.gradle.kts`: jniLibs ← `libpython*.so`/`lib*_python.so`; assets ← stdlib em `assets/python/lib/python3.14/` + nosso pacote em `site-packages`; **rename `.gz`→`.gz-`** (AAPT); `android:extractNativeLibs="false"` + `.so` em `lib//`. **Feito quando:** APK builda, instala e imprime "hello from python" no logcat, com o interpretador rodando fora da UI thread. --- ## B3 — Ponte JNI própria (Python ↔ Kotlin), bidirecional Sobre a C-API, sem framework de ponte. Casar com o loop asyncio do Trilho A. - **Kotlin → Python:** o shim JNI mantém `JavaVM*`/`JNIEnv*` (cacheados em `JNI_OnLoad`); chama Python via `PyGILState_Ensure` + `PyObject_CallObject`. - **Python → Kotlin:** `ctypes`/CFFI sobre funções C exportadas, ou um C-ext que faz `AttachCurrentThread` + `CallObjectMethod`. Toast/log nativo como smoke. - **Marshalling de threads (a única fronteira, plano §8):** - entrar no loop Python a partir de callback Java → `loop.call_soon_threadsafe(...)`; - sair para a UI Android → `runOnUiThread(...)`; - callback nativo (câmera/permissão) resolve um `asyncio.Future` → vira `await`. - Reutilizar `parse_event` (A6) para validar payloads de evento na entrada. **Feito quando:** Python dispara um toast/log nativo **e** um toque no device chega a um handler Python (round-trip), com o payload validado por Pydantic. --- ## B4 — Renderer Compose data-driven (`Style → Compose`) Host Compose interpreta a **mesma IR** do Trilho A (serializada via a ponte) — trocar só o renderer-folha. Composable recursivo: ```kotlin @Composable fun RenderNode(node: Node) = when (node.type) { "Text" -> Text(node.props["content"], modifier = node.style.toModifier()) "Button" -> Button(onClick = { emitTap(node.key) }) { Text(node.props["label"]) } "Column" -> Column(node.style.arrangement(), node.style.alignment()) { node.children.forEach { RenderNode(it) } } "Row" -> Row(...) { ... } "Container" -> Box(node.style.toModifier()) { node.child?.let { RenderNode(it) } } } ``` Tradutor `Style → Compose` (espelha `Style → Qt`, plano §4.4): - `direction`→`Row`/`Column`; `justify`→`Arrangement` (`Start/Center/SpaceBetween/spacedBy`); `align`→`Alignment`; `grow`→`Modifier.weight`; wrap→`FlowRow`/`FlowColumn`. - box model→`Modifier.padding/background(color,shape)/border/clip/size`. - Aplicar **patches** do reconciliador (insert/remove/update/reorder/replace) ao estado Compose (lista observável de nós) — não rebuildar a árvore inteira. - **Remote Compose** (`androidx.compose.remote`) é referência futura (alpha, sem binding Python) — **não** dependência v1. **Feito quando:** a árvore de exemplo do Trilho A (counter) renderiza nativa no device e o botão incrementa via rebuild→diff→patch. --- ## B5 — Dev server + QR (estilo Expo) - `tempest dev` (estender o cockpit do A5) sobe um server LAN (HTTP/WS) que: 1. serve o código Python do app; 2. relaya logs de volta ao terminal. - Imprime QR codificando `ws://:`. App host escaneia, puxa o código, roda local, e salvar → restart no sim **e** no device. - **WSL:** `[wsl2] networkingMode=mirrored` no `%UserProfile%\.wslconfig` + `wsl --shutdown`; liberar Hyper-V firewall inbound; bind do server em `0.0.0.0`. ADB: `adb pair`/`adb connect` (Wireless Debugging). Evitar usbipd-win com Android (instável). **Feito quando:** escanear o QR carrega o app por Wi-Fi e salvar reinicia no celular. --- ## B6 — Capacidades nativas - **Notificações:** `NotificationManager` + canal obrigatório (Android 8+), permissão `POST_NOTIFICATIONS` (Android 13+). Wrapper Python em `native/`. - **Câmera:** via `Intent` primeiro (simples), módulo CameraX depois. Callback nativo → `Future` → `await camera.capture()` (padrão B3). **Feito quando:** notificação disparada do Python; foto capturada e devolvida ao Python. --- ## Ordem e convergência 1. **B0 → B1 em paralelo** (B1 é o derisk; atacar cedo, plano §8). 2. **B2 → B3** (host + ponte) depois de ter `libpython` arm64. 3. **B4** reusa reconciliador/IR/Style/eventos do Trilho A — só o renderer muda. 4. **B5/B6** por último. 5. **Convergência:** o host no celular carrega o **mesmo reconciliador** do Trilho A; `tempest dev` ganha o alvo "device" ao lado do "sim". 6. **D (conformância):** golden snapshots `Style→Qt` vs `Style→Compose` no CI — é o que mantém o simulador honesto contra o device. ## Reconfirmar antes de executar (muda rápido) - Versões: CPython 3.14.x, cibuildwheel, maturin, NDK, AGP/Compose BOM. - Estado das wheels Android de `pydantic-core` (pode surgir wheel pronta). - Status do Remote Compose (se sair de alpha, reavaliar para o renderer).