From 5e2b9c069fa3dacfc1e4124876296771656c1cc6 Mon Sep 17 00:00:00 2001 From: Tienson Qin Date: Tue, 6 Oct 2026 02:32:26 -0700 Subject: [PATCH 1/5] Remove the Flutter backend Delete platform/flutter and the Flutter gallery app, drop FlutterHost from the protocol and extension registries, strip dart codegen from the component-schema and extension-API generators, and repoint the mobile build scripts at the upcoming examples/components/android app. All flutter/flutter-profile fingerprints and docs references are removed; rg -il flutter returns zero hits. --- .agents/skills/lui-ios-development/SKILL.md | 6 +- .gitignore | 6 +- README.md | 6 +- docs/adr/0002-dynamic-components.md | 14 +- docs/adr/0003-platform-tweaks.md | 6 +- ...-24-vercel-native-architecture-research.md | 11 +- .../2026-08-24-vercel-native-ui-api-parity.md | 27 +- .../2026-08-26-base-ui-web-interaction.md | 4 +- .../2026-08-29-web-simulator-platform.md | 7 +- .../2026-09-24-native-menu-trigger.md | 8 - .../2026-08-25-adaptive-component-gallery.md | 14 +- .../2026-08-24-production-components.md | 143 +- dune-project | 2 +- .../LUIComponentsApp/GalleryExtensions.swift | 2 +- .../components/native/components_bridge.ml | 7 +- .../app_icon_128.png | Bin examples/components/web/web_main.ml | 3 +- examples/gallery/extension_schemas.json | 80 +- examples/gallery/extension_schemas.ml | 12 +- examples/markdown/native/markdown_bridge.ml | 2 - examples/navigation/README.md | 2 +- .../navigation/native/navigation_bridge.ml | 1 - examples/split/native/split_bridge.ml | 2 - examples/todos/native/todos_bridge.ml | 1 - lui.opam | 2 +- lui.opam.locked | 2 +- .../Sources/LUIAppleBackend/LUISplit.swift | 8 +- .../LUIAppleBackend/LUIThemePolicy.swift | 2 +- platform/flutter/analysis_options.yaml | 10 - platform/flutter/lib/lui_flutter_backend.dart | 7455 ----------------- .../flutter/lib/lui_flutter_extension.dart | 333 - platform/flutter/lib/lui_flutter_split.dart | 805 -- platform/flutter/lib/lui_ocaml_bridge.dart | 455 - platform/flutter/lib/lui_wire_schema.g.dart | 190 - platform/flutter/native/lui_ocaml_bridge.c | 330 - platform/flutter/pubspec.lock | 213 - platform/flutter/pubspec.yaml | 20 - .../test/lui_flutter_backend_test.dart | 3771 --------- platform/web/src/lui-split.js | 8 +- schema/components.json | 88 - src/lui_element_combine.ml | 41 +- src/lui_extension.ml | 1 - src/lui_extension_check.ml | 4 +- src/lui_protocol.ml | 1 - src/lui_protocol.mli | 1 - src/lui_split.ml | 5 - test/test_lui.ml | 33 +- tooling/generate_component_schema.mjs | 26 - tooling/generate_extension_api.mjs | 4 +- tooling/mobile/build_components_android.sh | 4 +- .../mobile/build_components_ios_simulator.sh | 2 +- tooling/mobile/build_components_lg.sh | 1 - tooling/mobile/build_components_mobile.sh | 4 +- .../qualify_components_android_release.sh | 6 +- tooling/mobile/test_components_android_e2e.sh | 6 +- tooling/test/component_schema_test.mjs | 5 - 56 files changed, 162 insertions(+), 14040 deletions(-) rename examples/components/{flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset => web}/app_icon_128.png (100%) delete mode 100644 platform/flutter/analysis_options.yaml delete mode 100644 platform/flutter/lib/lui_flutter_backend.dart delete mode 100644 platform/flutter/lib/lui_flutter_extension.dart delete mode 100644 platform/flutter/lib/lui_flutter_split.dart delete mode 100644 platform/flutter/lib/lui_ocaml_bridge.dart delete mode 100644 platform/flutter/lib/lui_wire_schema.g.dart delete mode 100644 platform/flutter/native/lui_ocaml_bridge.c delete mode 100644 platform/flutter/pubspec.lock delete mode 100644 platform/flutter/pubspec.yaml delete mode 100644 platform/flutter/test/lui_flutter_backend_test.dart diff --git a/.agents/skills/lui-ios-development/SKILL.md b/.agents/skills/lui-ios-development/SKILL.md index 39a419d8..8e469d1d 100644 --- a/.agents/skills/lui-ios-development/SKILL.md +++ b/.agents/skills/lui-ios-development/SKILL.md @@ -18,7 +18,7 @@ error output**. - Diagnose by printing `caml_format_exception` in the host's emit bridge and checking whether the emitted op count per extension node matches what the - Swift/Flutter view expects. + Swift view expects. - Runtime validation errors carry details only when built with detailed messages (kind/property/value and parent/child kinds). If you see bare `invalid_arg "invalid property value"` / `"unsupported child kind"`, you're @@ -56,8 +56,8 @@ An extension node's declared schema must match what the app actually mounts: `standard-children:0` → `unsupported child kind` at emit. - The `children` whitelist (which extension kinds may nest under this one) and `standardChildren` flag are baked into the component **fingerprint** — and - the fingerprint is computed independently on each host (Swift, Flutter) and - in the OCaml registry. All three must stay in sync or one platform emits + the fingerprint is computed independently on each host (Swift) and + in the OCaml registry. Both must stay in sync or one platform emits while the other rejects. - Keep the schemas OCaml-declared once (see `examples/gallery/extension_schemas.ml`); host files carry only the `fingerprint:` literals. `Lui_extension_check.check_registry` diff --git a/.gitignore b/.gitignore index 7292412a..265a86fa 100644 --- a/.gitignore +++ b/.gitignore @@ -4,11 +4,7 @@ /.clj-kondo/ /.lsp/ /platform/apple/.build/ -/platform/flutter/.dart_tool/ -/platform/flutter/build/ -/examples/*/flutter/.dart_tool/ -/examples/*/flutter/build/ -/examples/components/flutter/android/app/src/main/jniLibs/ +/examples/components/android/app/src/main/jniLibs/ /examples/components/ios-swiftui/.build/ /examples/*/macos-appkit/.build/ /platform/web/node_modules/ diff --git a/README.md b/README.md index 73026233..b6698f4c 100644 --- a/README.md +++ b/README.md @@ -7,12 +7,11 @@ patches to real UI objects: - **Apple**: one SwiftUI backend shared by iOS and macOS (`platform/apple`, `LUIAppleBackend` Swift package). -- **Flutter**: Dart and Flutter widgets (`platform/flutter`). - **Web**: a Melange companion library (`platform/web/melange`) plus the shared stylesheet and icon set (`platform/web/src`). -Apple and Flutter use one JSON object per atomic `patch_batch` at their -native host boundaries; `Lui_wire` encodes the protocol directly. The Web +Apple uses one JSON object per atomic `patch_batch` at its +native host boundary; `Lui_wire` encodes the protocol directly. The Web backend runs in the same process as the OCaml application and applies typed patches to DOM nodes — no JSON bridge, no JavaScript UI framework. @@ -23,7 +22,6 @@ src/ the lui library (protocol, runtime, elements, app) examples/todos/ headless todo demo (pure OCaml + ocaml-signal) examples/gallery/ headless component-gallery demo platform/apple/ SwiftUI backend (SwiftPM package + tests) -platform/flutter/ Flutter backend platform/web/ Melange DOM library, stylesheet, icons schema/ canonical component schema (components.json) tooling/ schema code generator and tests diff --git a/docs/adr/0002-dynamic-components.md b/docs/adr/0002-dynamic-components.md index 522a6660..732ecc67 100644 --- a/docs/adr/0002-dynamic-components.md +++ b/docs/adr/0002-dynamic-components.md @@ -9,7 +9,7 @@ Date: 2026-08-25 LUI intentionally exposes a closed, typed standard component protocol. The closed protocol gives every standard element the same validation, retained identity, event semantics, accessibility contract, and backend coverage on -Web, Apple, and Flutter. +Web and Apple. Applications still need native capabilities that do not belong in the standard component catalog. Examples include an iOS MapKit view, a photo @@ -41,7 +41,7 @@ shadow or alter a standard component. ```text standard LUI node -> generated closed protocol - -> built-in Web / SwiftUI / Flutter renderer + -> built-in Web / SwiftUI renderer registered component -> extension schema and retained extension node @@ -288,16 +288,16 @@ structured collections. 4. Add an Apple component registry with a stable `AnyView` boundary. 5. Implement an application-owned MapKit example without adding its identifier to `schema/components.json`. -6. Add Web and Flutter registry interfaces with the same lifecycle and +6. Add a Web registry interface with the same lifecycle and validation rules. 7. Add macro-generated typed LG wrappers. Host registrations stay explicit so - native factories remain ordinary Swift, Dart, or Web code. + native factories remain ordinary Swift or Web code. ## Implementation status -Implemented on 2026-08-25 for the LG runtime and retained Web, SwiftUI, and -Flutter backends. The component Gallery exercises an application-owned MapKit -extension on Apple hosts and a native retained card on Web and Flutter without +Implemented on 2026-08-25 for the LG runtime and retained Web and SwiftUI +backends. The component Gallery exercises an application-owned MapKit +extension on Apple hosts and a native retained card on Web without adding either identifier to the standard generated component schema. ## Acceptance criteria diff --git a/docs/adr/0003-platform-tweaks.md b/docs/adr/0003-platform-tweaks.md index a8dbbdd9..0b3dc29c 100644 --- a/docs/adr/0003-platform-tweaks.md +++ b/docs/adr/0003-platform-tweaks.md @@ -299,15 +299,15 @@ presentation or interaction affordances could become invisible failures. 3. Lower selected tweak vectors to internal retained decorator nodes. 4. Extend atomic patch validation and wire encoding for tweak decorators. 5. Add Apple tweak rendering with a stable `AnyView` boundary. -6. Add Web and Flutter tweak factories with the same ordering and validation +6. Add a Web tweak factory with the same ordering and validation rules. 7. Add examples for ordered tweaks, reactive tweak properties, inactive platform keys, and application-shell alternatives. ## Implementation status -Implemented on 2026-08-25 in the LG authoring/runtime layer and retained Web, -SwiftUI, and Flutter hosts. The component Gallery applies one shared +Implemented on 2026-08-25 in the LG authoring/runtime layer and retained Web +and SwiftUI hosts. The component Gallery applies one shared `gallery-accent` declaration to a paragraph; each host registers its own presentation factory. The decorator remains a retained node and the Gallery still mounts one component page at a time. diff --git a/docs/agent-guide/implemented/architecture/2026-08-24-vercel-native-architecture-research.md b/docs/agent-guide/implemented/architecture/2026-08-24-vercel-native-architecture-research.md index 4b1ee7ce..ba329701 100644 --- a/docs/agent-guide/implemented/architecture/2026-08-24-vercel-native-architecture-research.md +++ b/docs/agent-guide/implemented/architecture/2026-08-24-vercel-native-architecture-research.md @@ -170,7 +170,7 @@ Signal graph can avoid deriving unrelated subtrees in the first place. The widget tree is laid out into a flat retained `WidgetLayoutTree`. The shared engine owns row, column, grid, overlays, scroll regions, controls and composite -layout. It does not delegate ordinary component layout to SwiftUI, Flutter or +layout. It does not delegate ordinary component layout to SwiftUI or HTML. Rendering emits stable-id display-list commands. The runtime compares the @@ -209,9 +209,9 @@ the component catalog to each platform's widget catalog. | Android | reference-rendered pixels copied into the Android surface | Android host, touch, soft keyboard/IME | Mobile is experimental. The iOS host is Objective-C/UIKit rather than SwiftUI, -and the Android host does not map the catalog to Flutter widgets. This backend +and the Android host does not map the catalog to platform widgets. This backend choice is not part of the public API contract and is not a reason for LUI to -abandon its SwiftUI and Flutter mapping strategy. +abandon its native mapping strategy. Vercel Native does selectively delegate experiences whose platform identity matters: scroll physics on macOS, application/context menus, dialogs, tray, @@ -361,13 +361,12 @@ The LUI showcase should borrow the source-of-truth relationship: 1. Keep Signal-granular dependency tracking instead of rebuilding the whole authored view after every state change. -2. Reconcile transient state inside retained DOM, SwiftUI and Flutter objects +2. Reconcile transient state inside retained DOM and SwiftUI objects rather than one custom canvas widget tree. 3. Let each backend use native layout and controls where doing so preserves the Vercel Native contract. 4. Use backend-specific dirty/presentation mechanisms: DOM property patches on - Web, observable model changes on SwiftUI, retained Element/RenderObject - updates in Flutter. + Web, observable model changes on SwiftUI. 5. Generate documentation and showcase metadata in a language-neutral artifact consumable by all LUI build stages. diff --git a/docs/agent-guide/implemented/architecture/2026-08-24-vercel-native-ui-api-parity.md b/docs/agent-guide/implemented/architecture/2026-08-24-vercel-native-ui-api-parity.md index d2de4c77..5c415961 100644 --- a/docs/agent-guide/implemented/architecture/2026-08-24-vercel-native-ui-api-parity.md +++ b/docs/agent-guide/implemented/architecture/2026-08-24-vercel-native-ui-api-parity.md @@ -187,22 +187,21 @@ The four direct text-entry elements are now schema-supported retained leaves. They share `text`, `placeholder`, `disabled`, `autofocus`, `label`, `on-input`, and `on-submit`; `textarea` alone adds `submit-on-enter` and grows without a public autoresize or line-count property. Web maps them to native -`input`/`textarea`, Apple to SwiftUI text controls, and Flutter to retained -Material `TextField` widgets. +`input`/`textarea` and Apple to SwiftUI text controls. ## Platform mapping -| Contract | Web | Apple | Flutter | -| --- | --- | --- | --- | -| row/column/list | CSS flex via Tailwind | `HStack`/`VStack` | `Row`/`Column` | -| stack/panel/card | CSS overlay/surface | SwiftUI overlay/ZStack | `Stack`/Material surface | -| scroll | CSS scrolling overlay box | `ScrollView` + `ZStack` | `SingleChildScrollView` + `Stack` | -| spinner | semantic CSS activity glyph | indeterminate `ProgressView` | `CircularProgressIndicator` | -| icon | bundled SVG CSS mask | SF Symbols `Image` | Material `Icon` | -| grid | CSS grid | `LazyVGrid` | `GridView` | -| controls | native HTML first | SwiftUI controls | Flutter widgets | -| list-item | native button row | SwiftUI Button row | Material `ListTile` | -| modal/menu | browser platform API when suitable | SwiftUI presentation | Flutter presentation APIs | +| Contract | Web | Apple | +| --- | --- | --- | +| row/column/list | CSS flex via Tailwind | `HStack`/`VStack` | +| stack/panel/card | CSS overlay/surface | SwiftUI overlay/ZStack | +| scroll | CSS scrolling overlay box | `ScrollView` + `ZStack` | +| spinner | semantic CSS activity glyph | indeterminate `ProgressView` | +| icon | bundled SVG CSS mask | SF Symbols `Image` | +| grid | CSS grid | `LazyVGrid` | +| controls | native HTML first | SwiftUI controls | +| list-item | native button row | SwiftUI Button row | +| modal/menu | browser platform API when suitable | SwiftUI presentation | Backends may differ internally, but they cannot expose backend-specific props on a standard component. @@ -214,7 +213,7 @@ Each delivered element must have: - a schema test for accepted and rejected attributes; - the same default and enum tests as the pinned reference; - retained wire and incremental update coverage; -- Web, SwiftUI and Flutter mapping coverage; +- Web and SwiftUI mapping coverage; - accessibility and interaction coverage proportional to the element; - a state-complete example in `examples/components/`. diff --git a/docs/agent-guide/implemented/architecture/2026-08-26-base-ui-web-interaction.md b/docs/agent-guide/implemented/architecture/2026-08-26-base-ui-web-interaction.md index 4dd583ec..ac7a4302 100644 --- a/docs/agent-guide/implemented/architecture/2026-08-26-base-ui-web-interaction.md +++ b/docs/agent-guide/implemented/architecture/2026-08-26-base-ui-web-interaction.md @@ -19,7 +19,7 @@ reference is the local Base UI checkout at commit The comparison applies to LUI's interactive Web elements. Pure layout and display elements keep native DOM semantics and do not need a Base UI state -machine. SwiftUI and Flutter continue to use their native controls and +machine. SwiftUI continues to use its native controls and presentation APIs; this report does not make Web behavior a cross-platform implementation detail. @@ -320,7 +320,7 @@ Copying contracts from Base UI keeps Web behavior predictable and reviewable. - Web interactive elements follow Base UI's behavior and motion contracts inside one LG-owned implementation; platform objects, the retained tree, and the schema stay LUI-owned. -- SwiftUI and Flutter keep native controls; this report does not make Web +- SwiftUI keeps native controls; this report does not make Web behavior a cross-platform detail. - Interaction gaps close in tested slices; each slice carries its own qualification evidence before moving on. diff --git a/docs/agent-guide/implemented/architecture/2026-08-29-web-simulator-platform.md b/docs/agent-guide/implemented/architecture/2026-08-29-web-simulator-platform.md index 7658cb6f..6e58ab9e 100644 --- a/docs/agent-guide/implemented/architecture/2026-08-29-web-simulator-platform.md +++ b/docs/agent-guide/implemented/architecture/2026-08-29-web-simulator-platform.md @@ -42,7 +42,7 @@ cover touch plus iPad/Android keyboard and pointer workflows. - Reimplementing SwiftUI as a general-purpose browser framework. - Making a full-screen Canvas renderer the default. - Pixel-copying private platform assets or undocumented system behavior. -- Replacing the native Apple and Flutter/Android production backends. +- Replacing the native Apple and Android production backends. ## Rendering boundary @@ -72,7 +72,7 @@ the platform keyboard height; blur restores zero without replacing the input. The runtime backend capability profile remains `WebOS + WebHost`. The simulated OS is a separate Web projection value, because a browser must not claim to be a -`SwiftUIHost` or `FlutterHost`. Extensions therefore continue to select their +`SwiftUIHost`. Extensions therefore continue to select their Web adapters honestly while their surfaces receive iOS or Android presentation. ## Platform fidelity @@ -99,8 +99,7 @@ the platform-sized control remains trailing. Dialog and Sheet action buttons emit ordinary model-owned events instead of mutating presentation locally. Phone sheets expose a decorative, accessibility-hidden drag handle and arbitrate downward dismissal against interactive descendants and scrolled content using -distance and velocity thresholds. SwiftUI uses `presentationDragIndicator`, -and Flutter uses the native Material bottom-sheet drag handle and route gesture. +distance and velocity thresholds. SwiftUI uses `presentationDragIndicator`. The initial vertical slice establishes profile ownership and visibly different tokens for Button, text entry, Switch, Card, Dialog, and Sheet. Later slices add diff --git a/docs/agent-guide/implemented/architecture/2026-09-24-native-menu-trigger.md b/docs/agent-guide/implemented/architecture/2026-09-24-native-menu-trigger.md index 1d8da3e0..3a74bbd2 100644 --- a/docs/agent-guide/implemented/architecture/2026-09-24-native-menu-trigger.md +++ b/docs/agent-guide/implemented/architecture/2026-09-24-native-menu-trigger.md @@ -82,12 +82,6 @@ accessibility label lives on the trigger. the anchored popover (sheets cannot nest menus) via `LUIMenuPresentationPolicy`. -### Flutter - -Parity is deferred: `menuTrigger` lands in the generated shared schema, -but `lui_flutter_backend` does not render it yet. Consumers that run on -Flutter keep working because nothing mounts `menu-trigger` there. - ## Migration - `menu_item [ dropdown_menu entries ]` → `submenu ... entries` (or @@ -131,7 +125,5 @@ label rules. the trigger, so nothing can clip the scalable icon. - `submenu` is a breaking API change: callers pass `label`/`disabled` instead of the old `role`/`variant`/`selected`/`checked` props. -- Consumers on Flutter cannot mount `menu-trigger` yet; the parity - pass is deferred. - `dune build @all`, `dune runtest`, `dune build @fmt`, `swift test`, and the schema generator tests pass with the change. diff --git a/docs/agent-guide/implemented/feature/2026-08-25-adaptive-component-gallery.md b/docs/agent-guide/implemented/feature/2026-08-25-adaptive-component-gallery.md index cf68c727..7192037c 100644 --- a/docs/agent-guide/implemented/feature/2026-08-25-adaptive-component-gallery.md +++ b/docs/agent-guide/implemented/feature/2026-08-25-adaptive-component-gallery.md @@ -15,7 +15,6 @@ Present the component showcase as one component page at a time on every host: - iPhone starts with a component list and pushes one component detail. - iPad and Mac use a persistent sidebar and detail column. -- Flutter uses Material navigation rail or drawer according to width. - Web uses a responsive sidebar or horizontal compact component picker. - The selected detail renders the existing LG retained subtree directly. - Component state and events remain owned by the LG model and Signals. @@ -47,16 +46,6 @@ The SwiftUI host uses `NavigationSplitView` and `List`: system background, and a navigation title; - the host owns navigation selection only; it never mirrors component state. -## Flutter mapping - -The Flutter host derives the same page descriptors from the wire-retained root: - -- compact width: Material `NavigationDrawer` plus one selected page; -- regular width: scrollable `NavigationRail` plus one selected page; -- page widgets keep stable retained node keys when navigation changes; -- patch callbacks rebuild only host projection metadata while backend node - handles continue to invalidate incrementally. - ## Web mapping The Web host derives the same page descriptors after the initial retained batch. @@ -71,8 +60,7 @@ The LG integration test proves all 64 public components are unique direct page roots. Hosts that register the Gallery's `NativeExtension` expose 65 derived navigation entries. Browser verification checks all 65 entries at a compact viewport, exactly one mounted component heading, and no page-level horizontal -overflow. Flutter backend tests cover direct-root section projection and -retained widget identity. +overflow. Maestro exercises list-to-detail navigation and real interactions for Button, Tabs, Dialog, text input, Checkbox/Switch Signal sharing, and Radio selection. diff --git a/docs/agent-guide/proposed/architecture/2026-08-24-production-components.md b/docs/agent-guide/proposed/architecture/2026-08-24-production-components.md index eebdd112..67ae6290 100644 --- a/docs/agent-guide/proposed/architecture/2026-08-24-production-components.md +++ b/docs/agent-guide/proposed/architecture/2026-08-24-production-components.md @@ -5,7 +5,7 @@ Status: active design and delivery plan ## Problem LUI needs a production-grade retained, incremental UI runtime that targets -Web, Apple platforms, and Flutter from one typed protocol, plus a component +Web and Apple platforms from one typed protocol, plus a component showcase that exercises the complete public catalog without becoming a separate complex application. The decision under consideration is the public component vocabulary, the runtime and platform strategy, and the delivery @@ -15,8 +15,8 @@ shape of that catalog. *Status: active design and delivery plan.* -LUI becomes a production-grade retained, incremental UI runtime for Web, -Apple platforms through one SwiftUI backend, and Flutter platforms. A small +LUI becomes a production-grade retained, incremental UI runtime for Web and +Apple platforms through one SwiftUI backend. A small showcase in `examples/` demonstrates the complete public component catalog and platform behavior without becoming a separate complex application. @@ -78,7 +78,7 @@ typed retained operations | shared retained tree | -Web DOM | SwiftUI model | Flutter model +Web DOM | SwiftUI model ``` Signal computations may depend on multiple Signals and other computed Signals. @@ -123,11 +123,6 @@ contains only a `UIHostingController` around `LUISwiftUIRoot`; the iPhone Simulator compile contract embeds it in a `UINavigationController` and proves that no UIKit component implementation or duplicate state bridge is required. -### Flutter - -LUI maps the shared contract to existing Flutter widgets and Material platform -APIs. It composes widgets before adding custom render objects. - ## Platform-specific tweaks The standard component API remains identical on every platform. Differences @@ -145,8 +140,8 @@ component map. ## Theming Each platform resolves semantic color names to its own best-practice theme: -the shared `--*` custom-property palette on Web, SwiftUI semantic colors on -Apple, and Material `colorScheme` on Flutter. Applications override those +the shared `--*` custom-property palette on Web and SwiftUI semantic colors on +Apple. Applications override those defaults through two scoped props rather than per-platform code: - `theme`: a flat JSON dictionary of design-token overrides, for example @@ -189,9 +184,6 @@ Per-backend resolution: - **Apple**: decoded entries merge over the `luiSemanticColors` environment dictionary that `LUIThemeColorResolver` already consults; `theme-mode` maps to `.preferredColorScheme` on the subtree. -- **Flutter**: an inherited `_LUIThemeScope` widget merges tokens down the - tree; `theme-mode` wraps the subtree in a `Theme` with a light/dark - `ColorScheme`. ## Component admission @@ -221,7 +213,7 @@ layout primitives. from LUI primitives. - Interactive composites such as Select, Dialog, Menu and Combobox remain semantic retained nodes. -- SwiftUI and Flutter map those nodes to their platform presentation and +- SwiftUI maps those nodes to their platform presentation and control APIs whenever the public contract permits it. - Web may combine native HTML APIs with reusable LUI behavior primitives for focus, selection, positioning, typeahead and dismissal. @@ -253,11 +245,10 @@ wire protocol vocabulary. Delivered parity slices: - one shared component-gallery model, reducer, and Signal view under - `examples/components/lg`; Web and Flutter hosts reuse it without duplicating - application state, and the Flutter integration test crosses the real OCaml - FFI boundary; + `examples/components/lg`; hosts reuse it without duplicating + application state; - one real Android emulator qualification path builds the shared OCaml/JNI - library and Flutter APK, installs it, and drives adaptive navigation, touch + library and Android APK, installs it, and drives adaptive navigation, touch disclosure, retained English and Chinese IME input, recursive menus, native Dialog, Sheet, Tooltip long press, Select, Combobox, Checkbox, Switch, Toggle, RadioGroup, and Slider interaction through Maestro; stable destination @@ -278,17 +269,17 @@ Delivered parity slices: dynamic regions preserve their local ordering and disposal boundaries; - `row`, `column`, and `grid`, including `main`, `cross`, `grow`, `columns`, and `gap` validation; -- direct retained `stack`, `panel`, and `card` overlay nodes on Web, SwiftUI, - and Flutter; +- direct retained `stack`, `panel`, and `card` overlay nodes on Web and + SwiftUI; - direct retained `list` flow nodes and multi-child `scroll` overlay semantics - on Web, SwiftUI, and Flutter; + on Web and SwiftUI; - direct retained `spinner` progress leaves with the reference 16/20/24 size - rungs, native SwiftUI/Flutter indicators, and a reduced-motion Web renderer; + rungs, native SwiftUI indicators, and a reduced-motion Web renderer; - direct retained `progress` leaves with a model-owned `float` fraction, render-time `0..1` clamping, and no range props or compound public parts; - direct retained `icon` leaves with the 51-name Vercel Native built-in vocabulary, 16/18/24 sizing, shared foreground tint, CSS-mask SVGs on Web, - SF Symbols on SwiftUI, and Material Icons on Flutter, plus immutable + SF Symbols on SwiftUI, plus immutable application-registered `app:` names on every backend; - direct text-bearing `checkbox` and `switch` controls with model-owned `checked`, `disabled`, `label`, and `on-toggle`; their labels and native @@ -309,17 +300,17 @@ Delivered parity slices: warm window that never enters application state; - direct retained `accordion` disclosure containers with the exact `text`/`selected`/`on-toggle`/`height` contract, model-owned expansion, - retained collapsed children, and native Web, SwiftUI, and Flutter controls; + retained collapsed children, and native Web and SwiftUI controls; - direct retained `table`, `table-row`, and `table-cell` nodes with strict structural nesting, aligned columns, model-owned row selection, pressable cells, and identity-preserving text and selection patches; - direct retained `tree` navigation over ordinary row nodes, with strict role/hierarchy metadata, one native roving focus set, model-owned selection - and disclosure, ARIA keyboard behavior, and local Signal patches on Web, - SwiftUI, and Flutter; + and disclosure, ARIA keyboard behavior, and local Signal patches on Web and + SwiftUI; - direct retained `resizable` stacking surfaces with source-width reconciliation, backend-owned drag state, bounded assistive resizing, and - identity-preserving children on Web, SwiftUI, and Flutter; + identity-preserving children on Web and SwiftUI; - direct retained `split` layouts with exactly two identity-preserving panes, a model-owned fraction, pane minimums, native animation and reduced-motion handling, plus pointer, keyboard, and assistive resizing on every applicable @@ -359,8 +350,7 @@ renderer even though the Button page's generated attribute table omits it. Web renders one native `button` and backend-owned inline icon content, styled through semantic Tailwind selectors and data attributes. SwiftUI renders one system `Button` with native label/icon composition, control sizing, focus, and -accessibility traits. Flutter uses Material button APIs and native gesture, -focus, semantics, and icon facilities. The internal event-capability bit does +accessibility traits. The internal event-capability bit does not become a public LG attribute. ### ToggleButton contract @@ -375,7 +365,7 @@ reconciliation channel. Native interaction updates pressed feedback immediately and the next changed model value corrects it. When `selected` is absent, selection is backend-owned transient state and survives unrelated property patches without entering the retained protocol tree. Web uses one -native `button` with `aria-pressed`; SwiftUI and Flutter keep the transient +native `button` with `aria-pressed`; SwiftUI keeps the transient selection in the identity-preserving native view state associated with that retained node. @@ -392,7 +382,6 @@ namespace and remain host resources: CSS mask channel as built-ins; - SwiftUI registers a bare app name to either an SF Symbol or an Asset Catalog image; -- Flutter registers a bare app name to `IconData`; - an unregistered but well-shaped `app:` name renders the platform's visible missing-icon fallback rather than becoming an invisible gap; - registration belongs to backend construction and is immutable while a @@ -422,8 +411,7 @@ two elements. Each element remains one retained LUI node. Backend-owned implementation children do not enter the retained tree: Web uses a native checkbox input in a semantic label wrapper, SwiftUI uses `Toggle` with the platform checkbox or -switch style, and Flutter composes its native `Checkbox` or `Switch` with the -label. A checked or text patch updates that retained node and must not replace +switch style. A checked or text patch updates that retained node and must not replace the platform control. ### Toggle, RadioGroup, Radio and Slider contract @@ -444,7 +432,7 @@ These daily value controls preserve the pinned Vercel Native API: Radio activation emits `on-change` only for a new selection. Activating an already selected radio may still reach the legacy toggle or press fallback. Web radios share one native input name per retained group so browser focus and -arrow navigation remain native. Flutter uses `RadioGroup`, and Apple keeps the +arrow navigation remain native. Apple keeps the same semantic group while rendering platform controls. Slider drag state stays host-owned during interaction; the applied fraction returns through the typed event pipeline and the Signal remains the reconciliation source. @@ -461,8 +449,7 @@ only when a backend renders the fill. Discrete indexes and counts remain integers. Web updates one retained progressbar DOM node and its CSS fill fraction. -SwiftUI uses `ProgressView(value:)`, and Flutter uses -`LinearProgressIndicator`; a Signal patch preserves the platform object and +SwiftUI uses `ProgressView(value:)`; a Signal patch preserves the platform object and does not rebuild the gallery section. ## State ownership @@ -503,8 +490,7 @@ the common Enter path to submit while Shift+Enter retains newline entry. Multiline `textarea` grows to its content until constrained by admitted layout bounds. Auto-sizing is an implementation behavior, not an extra `autoresize` prop. Web uses native `field-sizing: content`, SwiftUI uses vertical-axis native -text layout, and Flutter uses an unbounded native multiline `TextField`. Signal -text patches preserve the DOM element, SwiftUI model, Flutter controller, +text layout. Signal text patches preserve the DOM element, SwiftUI model, selection, focus, and IME-owned transient state. ### InputGroup contract @@ -532,8 +518,7 @@ Dynamic and keyed children remain ordinary LG retained structure. Web uses a semantic group and CSS `:focus-within` around the native textarea. SwiftUI composes the retained text editor and accessory row inside one native -rounded field, and Flutter composes the retained `TextField` and action row -inside one Material input surface. Text changes patch only the existing +rounded field. Text changes patch only the existing textarea node, action swaps patch only the actions segment, and focus chrome is backend-owned transient state: none of these updates rebuild the InputGroup or application component. @@ -563,10 +548,7 @@ focus move does not commit model selection until activation. Web uses retained native DOM controls, Tailwind component selectors, the shared Base UI-aligned portal, collision handling, keyboard traversal, and pointer -corridors for nested menus. Flutter keeps one stable `MenuAnchor` around the -trigger before and after the model mounts its menu, and maps nested rows to -Material `SubmenuButton`; Material owns overlay placement, focus traversal, -animation, collision handling, and outside dismissal. Apple keeps an equally +corridors for nested menus. Apple keeps an equally stable SwiftUI host. Select and action menus use a native popover, while the editable Combobox uses a small SwiftUI `Layout` overlay because presenting a popover resigns the iPhone text field and breaks IME composition. That platform @@ -597,9 +579,7 @@ waits to disambiguate it. Space dispatches `on-press`. Enter dispatches Web uses one retained native button row, native click/double-click ordering, and Tailwind component selectors. SwiftUI uses a native Button row with an -additive simultaneous double-tap gesture and return-key handling. Flutter -keeps a native Material `ListTile` and uses a small pointer-release adapter -only for the reference's immediate-plus-additive click contract. All three +additive simultaneous double-tap gesture and return-key handling. Both retain the row and its custom child subtree when a selection Signal changes. ### Avatar and registered-image contract @@ -619,12 +599,11 @@ that id, independently of LG patch generations. The registry never becomes application state and never owns loading policy. The host registers an already-decoded platform image under a caller-chosen id: -Web uses a URL plus decoded pixel dimensions, SwiftUI uses `CGImage`, and -Flutter uses `dart:ui.Image`. Replacing an existing id is atomic. Unregistering +Web uses a URL plus decoded pixel dimensions, and SwiftUI uses `CGImage`. +Replacing an existing id is atomic. Unregistering removes the registry reference and restores initials on every referencing Avatar. Callers retain responsibility for URL/object-URL lifetime on Web and for any -external references they keep to native image objects. Flutter explicitly -releases its cloned image handles when the backend is disposed. Apple and Web +external references they keep to native image objects. Apple and Web registry references follow the backend or renderer lifetime. On every backend, `unregister` immediately removes the registry slot. @@ -661,7 +640,7 @@ a submitted frame draws a subdued deterministic id-derived placeholder, so snapshots do not depend on producer timing. Surface ids occupy an independent registry from image ids. A host producer submits an already decoded platform frame under a stable id: Web supplies a URL plus decoded -pixel dimensions, SwiftUI a `CGImage`, and Flutter a `dart:ui.Image`. +pixel dimensions, and SwiftUI a `CGImage`. Submitting another frame atomically replaces the pending frame for that id; native presentation consumes the latest frame and invalidates only retained MediaSurface nodes that reference it. Unregistering the surface removes its @@ -696,8 +675,8 @@ metadata. The backend presents one horizontal native row with list semantics, numbered or completed indicators, labels, and connectors. Web uses semantic list items -and Tailwind component selectors; SwiftUI uses retained `HStack` composition; -Flutter uses retained `Row` widgets. Patching `active` updates the existing +and Tailwind component selectors; SwiftUI uses retained `HStack` composition. +Patching `active` updates the existing Stepper and direct Step presentations without rebuilding their child models or unrelated nodes. @@ -721,8 +700,7 @@ they do not want a trailing line. A pressable item binds one event to its root and adds platform affordance without duplicating handlers on its text. Web renders one semantic list-item row with Tailwind component selectors. -SwiftUI composes native labels, shapes, and Button behavior; Flutter composes -Material text, icon, and InkWell behavior. Each backend keeps the semantic +SwiftUI composes native labels, shapes, and Button behavior. Each backend keeps the semantic TimelineItem as one stable backend node: property Signals patch its internal native composition in place, and selection or copy changes never reconstruct the Timeline, sibling items, or application component. @@ -743,7 +721,7 @@ authored `gap` or padding value wins for that field. A selected trigger changes only that retained Button node; it does not replace the Tabs node, sibling triggers or selected content. Web maps the strip to a `tablist` and direct Button children to `tab` semantics. Its `label` names the tablist for assistive -technology. SwiftUI and Flutter use retained native Button controls with +technology. SwiftUI uses retained native Button controls with platform tab-strip presentation and selected semantics. Enter or Space activates the focused trigger through the Button's normal platform behavior. On Web, horizontal Left and Right navigation follows computed LTR or RTL @@ -782,8 +760,8 @@ move to the first and last eligible enabled control. Activation remains the child's native Enter or Space behavior and does not move selection by itself. Web exposes a labelled `group` with one roving Tab stop, preserves the latest eligible stop across unrelated retained patches, and implements the keymap by -delegation. SwiftUI and Flutter use retained native controls plus their native -focus systems. Reparenting a retained control into or out of a group updates +delegation. SwiftUI uses retained native controls plus its native +focus system. Reparenting a retained control into or out of a group updates only that control's contextual presentation and focus membership. ### Breadcrumb and Pagination contract @@ -817,8 +795,8 @@ IconButton element yet, so the current public contract exercises Button children. Breadcrumb's pressable Text participates in normal sequential focus and native activation, but not in the reference's Button-only arrow group. Focus movement never selects or activates a child. Web exposes labelled group -semantics with retained DOM nodes; SwiftUI and Flutter retain native controls -and their native focus objects. Reparenting updates contextual focus membership +semantics with retained DOM nodes; SwiftUI retains native controls +and its native focus objects. Reparenting updates contextual focus membership without recreating the child. ## Showcase @@ -836,11 +814,11 @@ element and covers: - reactive updates proving retained identity; - platform-specific tweaks that do not change the standard API. -The same LG showcase source runs on Web, SwiftUI desktop/iOS, and Flutter -desktop/mobile. Host shells stay deliberately small. +The same LG showcase source runs on Web and SwiftUI desktop/iOS. Host +shells stay deliberately small. -The picker and ListItem sections are exercised through the real Web host and -the Flutter host across the OCaml FFI boundary. Integration checks open and +The picker and ListItem sections are exercised through the real Web and +native hosts across the OCaml FFI boundary. Integration checks open and close overlays, commit selection and free-form submission, perform immediate single selection plus additive double-click activation, and assert that the retained Select, native text editor, and ListItem content survive their Signal @@ -906,7 +884,7 @@ label). `gap`, flow alignment, selection, and application resize callbacks are invalid. Children stack; authors put an explicit row or column inside for flow. Web uses the browser's horizontal resize affordance and Tailwind surface -chrome. SwiftUI and Flutter keep drag width in identity-preserving native view +chrome. SwiftUI keeps drag width in identity-preserving native view state keyed by the retained node. A changed source width reconciles that state; painting, child, or sibling patches do not. Keyboard or assistive increment and decrement resize by a platform step without creating an LG event. Every backend @@ -939,8 +917,7 @@ The remaining admitted Split attributes are the reference container surface vocabulary: `grow`, padding, colors, border, radius, width and height bounds, style class, and accessible label. Flow alignment and selection are invalid. Web uses a semantic separator with pointer capture, ArrowLeft/ArrowRight and -Home/End. SwiftUI uses a `DragGesture` and adjustable separator semantics; -Flutter uses a `GestureDetector`, resize cursor, and adjustable semantics. +Home/End. SwiftUI uses a `DragGesture` and adjustable separator semantics. All hosts retain both pane identities across value patches and user resizing, and reject batches with fewer or more than two children atomically. @@ -966,8 +943,7 @@ wins. Web presents an anchored menu surface at the pointer, clips it to the viewport, supports native menu keyboard traversal, and dismisses on Escape, outside press, item selection, blur, or a newer invocation. SwiftUI uses the system -`contextMenu` presentation. Flutter uses the platform Material popup-menu path -at the secondary-click or long-press position. Disabled items and separators +`contextMenu` presentation. Disabled items and separators preserve their declared slots. Selecting an enabled item dispatches that retained MenuItem's existing typed `Press` event; dismissal and presentation remain backend-owned and never enter the LG model. ContextMenu and item Signal @@ -1025,9 +1001,7 @@ Signals use those same names. Typography sizes `heading` and `display` are legal on TableCell but remain invalid on control-sized widgets. Web uses semantic `table`/`tr`/`td` DOM with grid, row, and grid-cell -accessibility semantics. SwiftUI uses `Grid` and `GridRow`. Flutter composes -retained per-row `Table` widgets so a row or cell patch does not rebuild an -enclosing application component. The final row has no divider, selectable rows +accessibility semantics. SwiftUI uses `Grid` and `GridRow`. The final row has no divider, selectable rows receive a full-width highlight, and an enabled pressable cell is one keyboard and accessibility activation target. @@ -1064,8 +1038,7 @@ must return through LG and patch the same retained row. Web uses `role="tree"` and `role="treeitem"` with roving `tabindex`. SwiftUI uses one retained focus coordinator around native row content and exposes the -same tree/list accessibility semantics on macOS and iOS. Flutter uses a native -`FocusTraversalGroup` plus `Focus`/`Actions` around retained row widgets. None +same tree/list accessibility semantics on macOS and iOS. None of the backends creates a parallel tree model or replaces the enclosing LG component when focus, selection, disclosure, or keyed row order changes. @@ -1080,8 +1053,8 @@ while the title is rendered by the platform surface chrome. Every backend presents a dialog relative to the root rather than its declaring layout container. Web uses the native `dialog` top layer, SwiftUI uses one -declarative modal presentation path for both Apple platforms, and Flutter uses -the platform modal route/widget API. Escape or the platform back action and a +declarative modal presentation path for both Apple platforms. +Escape or the platform back action and a backdrop press emit one `Dismiss` event for the topmost dialog. Presentation moves focus into the dialog, traps traversal while it is modal, and restores the previously focused control after removal. Nested declaration must not make @@ -1101,9 +1074,7 @@ patterns across Web, Apple, and Material platforms. Each backend uses its native sheet interaction. SwiftUI uses the system `.sheet` presentation, including platform swipe, keyboard, accessibility, and -adaptive iPhone/iPad/macOS behavior. Flutter uses the Material modal bottom -sheet route with its native drag, system-back, barrier, safe-area, and focus -behavior. Web follows Base UI's sheet/drawer popup behavior while exposing only +adaptive iPhone/iPad/macOS behavior. Web follows Base UI's sheet/drawer popup behavior while exposing only the portable `sheet` name. Platform adaptation must not leak additional public attributes. @@ -1140,9 +1111,8 @@ must neither replace the trigger nor leak a pending timer or overlay. Web uses a semantic `role="tooltip"` DOM leaf and a document-scoped hover-intent coordinator. SwiftUI uses one stateful presenter attached to the retained -trigger subtree and the platform popover/help accessibility path. Flutter -wraps the retained trigger subtree with its native `Tooltip` presentation and a -shared intent coordinator. Placement may auto-flip or adapt at host edges, but +trigger subtree and the platform popover/help accessibility path. +Placement may auto-flip or adapt at host edges, but the public edge preference and state boundary stay identical. #### Accordion contract @@ -1163,8 +1133,7 @@ real button trigger linked to a retained `region` panel. Activation emits one open state. The panel measures its content and uses the Base UI 150 ms height-transition contract for opening and closing; reduced motion settles synchronously. SwiftUI uses `DisclosureGroup` with a model-backed binding. -Flutter uses `ExpansionTile` with model-owned expansion and a stable retained -key. Property patches update the same native node and never remount its child +Property patches update the same native node and never remount its child models. Collapsing hides content visually but does not remove it from LUI's retained tree. @@ -1179,7 +1148,7 @@ retained tree. - cold reproducible builds and packaged host apps; - performance budgets and long-running mutation tests; - accessibility audits and keyboard-only passes; -- Web browser matrix, Apple desktop/iOS and Flutter desktop/Android coverage; +- Web browser matrix, Apple desktop/iOS and Android coverage; - component gallery parity audit against the pinned reference. Runtime performance is qualified separately from deterministic correctness diff --git a/dune-project b/dune-project index 1796c93a..07f6f8d6 100644 --- a/dune-project +++ b/dune-project @@ -13,7 +13,7 @@ (license "MIT") (synopsis "A reactive retained cross-platform UI runtime in OCaml") (description - "Declarative UI, incremental patch batching, and Apple, Flutter, and Web backends powered by ocaml-signal. Element constructors follow the convention unit -> ?props -> children:t list -> unit -> t so optional props erase under a trailing unit argument.") + "Declarative UI, incremental patch batching, and Apple and Web backends powered by ocaml-signal. Element constructors follow the convention unit -> ?props -> children:t list -> unit -> t so optional props erase under a trailing unit argument.") (depends (ocaml (>= 5.1)) (dune (>= 3.17)) diff --git a/examples/components/ios-swiftui/Sources/LUIComponentsApp/GalleryExtensions.swift b/examples/components/ios-swiftui/Sources/LUIComponentsApp/GalleryExtensions.swift index 2077d71c..4ee1e7ad 100644 --- a/examples/components/ios-swiftui/Sources/LUIComponentsApp/GalleryExtensions.swift +++ b/examples/components/ios-swiftui/Sources/LUIComponentsApp/GalleryExtensions.swift @@ -37,7 +37,7 @@ func galleryExtensionRegistry() throws -> LUIAppleExtensionRegistry { try registry.registerTweak( LUIAppleTweak( identifier: "gallery-accent", - fingerprint: "lui-tweak-v1|14:gallery-accent|profiles:android/flutter,ios/flutter,ios/swiftui,linux/flutter,linux/gpui,macos/flutter,macos/gpui,macos/swiftui,web/web,windows/flutter,windows/gpui|properties:" + fingerprint: "lui-tweak-v1|14:gallery-accent|profiles:ios/swiftui,linux/gpui,macos/gpui,macos/swiftui,web/web,windows/gpui|properties:" ) { content, _ in AnyView( content diff --git a/examples/components/native/components_bridge.ml b/examples/components/native/components_bridge.ml index 45fea096..a506759b 100644 --- a/examples/components/native/components_bridge.ml +++ b/examples/components/native/components_bridge.ml @@ -25,7 +25,6 @@ let operating_system = function let host_kind = function | 1 -> WebHost | 2 -> SwiftUIHost - | 3 -> FlutterHost | 6 -> GPUIHost | _ -> GenericHost @@ -139,9 +138,7 @@ let register prefix = register (prefix ^ "_dispose") dispose; register (prefix ^ "_root_node") root_node -(* platform/native/lui_ocaml_bridge.c looks up "lui_ocaml_*" and - platform/flutter/native/lui_ocaml_bridge.c looks up "lui_flutter_*". *) +(* platform/native/lui_ocaml_bridge.c looks up "lui_ocaml_*". *) let () = Printexc.record_backtrace true; - register "lui_ocaml"; - register "lui_flutter" + register "lui_ocaml" diff --git a/examples/components/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/examples/components/web/app_icon_128.png similarity index 100% rename from examples/components/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png rename to examples/components/web/app_icon_128.png diff --git a/examples/components/web/web_main.ml b/examples/components/web/web_main.ml index 499aa0b0..2b38166b 100644 --- a/examples/components/web/web_main.ml +++ b/examples/components/web/web_main.ml @@ -583,8 +583,7 @@ let mount_gallery_shell renderer root host = (* Entry *) -let app_icon_url = - "/examples/components/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png" +let app_icon_url = "/examples/components/web/app_icon_128.png" let main host = let registry = Extension_schemas.registry () in diff --git a/examples/gallery/extension_schemas.json b/examples/gallery/extension_schemas.json index e8a0d109..21e073f5 100644 --- a/examples/gallery/extension_schemas.json +++ b/examples/gallery/extension_schemas.json @@ -23,26 +23,10 @@ } ], "all": [ - { - "os": "android", - "host": "flutter" - }, - { - "os": "ios", - "host": "flutter" - }, { "os": "ios", "host": "swiftui" }, - { - "os": "linux", - "host": "flutter" - }, - { - "os": "macos", - "host": "flutter" - }, { "os": "macos", "host": "swiftui" @@ -51,10 +35,6 @@ "os": "web", "host": "web" }, - { - "os": "windows", - "host": "flutter" - }, { "os": "macos", "host": "gpui" @@ -77,26 +57,6 @@ "os": "ios", "host": "swiftui" }, - { - "os": "android", - "host": "flutter" - }, - { - "os": "ios", - "host": "flutter" - }, - { - "os": "linux", - "host": "flutter" - }, - { - "os": "macos", - "host": "flutter" - }, - { - "os": "windows", - "host": "flutter" - }, { "os": "web", "host": "web" @@ -132,44 +92,6 @@ "host": "gpui" } ], - "web-and-flutter": [ - { - "os": "web", - "host": "web" - }, - { - "os": "macos", - "host": "flutter" - }, - { - "os": "ios", - "host": "flutter" - }, - { - "os": "android", - "host": "flutter" - }, - { - "os": "linux", - "host": "flutter" - }, - { - "os": "windows", - "host": "flutter" - }, - { - "os": "macos", - "host": "gpui" - }, - { - "os": "linux", - "host": "gpui" - }, - { - "os": "windows", - "host": "gpui" - } - ], "gpui": [ { "os": "macos", @@ -359,7 +281,7 @@ }, { "identifier": "native-card", - "profiles": "web-and-flutter", + "profiles": "web", "standardChildren": false, "children": [], "properties": [ diff --git a/examples/gallery/extension_schemas.ml b/examples/gallery/extension_schemas.ml index 30ec04a6..32e899a3 100644 --- a/examples/gallery/extension_schemas.ml +++ b/examples/gallery/extension_schemas.ml @@ -274,35 +274,35 @@ let simulator_camera_schema = [ Lui_extension.event "state-change" [ Lui_extension.event_field "state" Lui_extension.StringScalar true ] ] let native_card_schema = - Lui_extension.component "native-card" [ { Lui_protocol.profile_os = WebOS; Lui_protocol.profile_host = WebHost }; { Lui_protocol.profile_os = MacOS; Lui_protocol.profile_host = FlutterHost }; { Lui_protocol.profile_os = IOS; Lui_protocol.profile_host = FlutterHost }; { Lui_protocol.profile_os = AndroidOS; Lui_protocol.profile_host = FlutterHost }; { Lui_protocol.profile_os = LinuxOS; Lui_protocol.profile_host = FlutterHost }; { Lui_protocol.profile_os = WindowsOS; Lui_protocol.profile_host = FlutterHost }; { Lui_protocol.profile_os = MacOS; Lui_protocol.profile_host = GPUIHost }; { Lui_protocol.profile_os = LinuxOS; Lui_protocol.profile_host = GPUIHost }; { Lui_protocol.profile_os = WindowsOS; Lui_protocol.profile_host = GPUIHost } ] + Lui_extension.component "native-card" [ { Lui_protocol.profile_os = WebOS; Lui_protocol.profile_host = WebHost }; { Lui_protocol.profile_os = MacOS; Lui_protocol.profile_host = GPUIHost }; { Lui_protocol.profile_os = LinuxOS; Lui_protocol.profile_host = GPUIHost }; { Lui_protocol.profile_os = WindowsOS; Lui_protocol.profile_host = GPUIHost } ] false [ ] [ Lui_extension.property "title" Lui_extension.StringScalar true None ] [ ] let split_view_schema = - Lui_extension.component "split-view" [ { Lui_protocol.profile_os = MacOS; Lui_protocol.profile_host = SwiftUIHost }; { Lui_protocol.profile_os = IOS; Lui_protocol.profile_host = SwiftUIHost }; { Lui_protocol.profile_os = AndroidOS; Lui_protocol.profile_host = FlutterHost }; { Lui_protocol.profile_os = IOS; Lui_protocol.profile_host = FlutterHost }; { Lui_protocol.profile_os = LinuxOS; Lui_protocol.profile_host = FlutterHost }; { Lui_protocol.profile_os = MacOS; Lui_protocol.profile_host = FlutterHost }; { Lui_protocol.profile_os = WindowsOS; Lui_protocol.profile_host = FlutterHost }; { Lui_protocol.profile_os = WebOS; Lui_protocol.profile_host = WebHost }; { Lui_protocol.profile_os = MacOS; Lui_protocol.profile_host = GPUIHost }; { Lui_protocol.profile_os = LinuxOS; Lui_protocol.profile_host = GPUIHost }; { Lui_protocol.profile_os = WindowsOS; Lui_protocol.profile_host = GPUIHost } ] + Lui_extension.component "split-view" [ { Lui_protocol.profile_os = MacOS; Lui_protocol.profile_host = SwiftUIHost }; { Lui_protocol.profile_os = IOS; Lui_protocol.profile_host = SwiftUIHost }; { Lui_protocol.profile_os = WebOS; Lui_protocol.profile_host = WebHost }; { Lui_protocol.profile_os = MacOS; Lui_protocol.profile_host = GPUIHost }; { Lui_protocol.profile_os = LinuxOS; Lui_protocol.profile_host = GPUIHost }; { Lui_protocol.profile_os = WindowsOS; Lui_protocol.profile_host = GPUIHost } ] false [ "split-branch"; "split-pane" ] [ Lui_extension.property "divider-thickness" Lui_extension.FloatScalar false None; Lui_extension.property "animation" Lui_extension.BoolScalar false None; Lui_extension.property "accessibility-identifier" Lui_extension.StringScalar false None ] [ ] let split_branch_schema = - Lui_extension.component "split-branch" [ { Lui_protocol.profile_os = MacOS; Lui_protocol.profile_host = SwiftUIHost }; { Lui_protocol.profile_os = IOS; Lui_protocol.profile_host = SwiftUIHost }; { Lui_protocol.profile_os = AndroidOS; Lui_protocol.profile_host = FlutterHost }; { Lui_protocol.profile_os = IOS; Lui_protocol.profile_host = FlutterHost }; { Lui_protocol.profile_os = LinuxOS; Lui_protocol.profile_host = FlutterHost }; { Lui_protocol.profile_os = MacOS; Lui_protocol.profile_host = FlutterHost }; { Lui_protocol.profile_os = WindowsOS; Lui_protocol.profile_host = FlutterHost }; { Lui_protocol.profile_os = WebOS; Lui_protocol.profile_host = WebHost }; { Lui_protocol.profile_os = MacOS; Lui_protocol.profile_host = GPUIHost }; { Lui_protocol.profile_os = LinuxOS; Lui_protocol.profile_host = GPUIHost }; { Lui_protocol.profile_os = WindowsOS; Lui_protocol.profile_host = GPUIHost } ] + Lui_extension.component "split-branch" [ { Lui_protocol.profile_os = MacOS; Lui_protocol.profile_host = SwiftUIHost }; { Lui_protocol.profile_os = IOS; Lui_protocol.profile_host = SwiftUIHost }; { Lui_protocol.profile_os = WebOS; Lui_protocol.profile_host = WebHost }; { Lui_protocol.profile_os = MacOS; Lui_protocol.profile_host = GPUIHost }; { Lui_protocol.profile_os = LinuxOS; Lui_protocol.profile_host = GPUIHost }; { Lui_protocol.profile_os = WindowsOS; Lui_protocol.profile_host = GPUIHost } ] false [ "split-branch"; "split-pane" ] [ Lui_extension.property "orientation" Lui_extension.StringScalar true None; Lui_extension.property "ratio" Lui_extension.FloatScalar true None ] [ Lui_extension.event "ratio-changed" [ Lui_extension.event_field "ratio" Lui_extension.FloatScalar true ] ] let split_pane_schema = - Lui_extension.component "split-pane" [ { Lui_protocol.profile_os = MacOS; Lui_protocol.profile_host = SwiftUIHost }; { Lui_protocol.profile_os = IOS; Lui_protocol.profile_host = SwiftUIHost }; { Lui_protocol.profile_os = AndroidOS; Lui_protocol.profile_host = FlutterHost }; { Lui_protocol.profile_os = IOS; Lui_protocol.profile_host = FlutterHost }; { Lui_protocol.profile_os = LinuxOS; Lui_protocol.profile_host = FlutterHost }; { Lui_protocol.profile_os = MacOS; Lui_protocol.profile_host = FlutterHost }; { Lui_protocol.profile_os = WindowsOS; Lui_protocol.profile_host = FlutterHost }; { Lui_protocol.profile_os = WebOS; Lui_protocol.profile_host = WebHost }; { Lui_protocol.profile_os = MacOS; Lui_protocol.profile_host = GPUIHost }; { Lui_protocol.profile_os = LinuxOS; Lui_protocol.profile_host = GPUIHost }; { Lui_protocol.profile_os = WindowsOS; Lui_protocol.profile_host = GPUIHost } ] + Lui_extension.component "split-pane" [ { Lui_protocol.profile_os = MacOS; Lui_protocol.profile_host = SwiftUIHost }; { Lui_protocol.profile_os = IOS; Lui_protocol.profile_host = SwiftUIHost }; { Lui_protocol.profile_os = WebOS; Lui_protocol.profile_host = WebHost }; { Lui_protocol.profile_os = MacOS; Lui_protocol.profile_host = GPUIHost }; { Lui_protocol.profile_os = LinuxOS; Lui_protocol.profile_host = GPUIHost }; { Lui_protocol.profile_os = WindowsOS; Lui_protocol.profile_host = GPUIHost } ] false [ "split-tab" ] [ Lui_extension.property "pane-id" Lui_extension.StringScalar true None; Lui_extension.property "selected" Lui_extension.StringScalar false None; Lui_extension.property "focused" Lui_extension.BoolScalar false None; Lui_extension.property "accessibility-identifier" Lui_extension.StringScalar false None ] [ Lui_extension.event "tab-selected" [ Lui_extension.event_field "tab" Lui_extension.StringScalar true ]; Lui_extension.event "tab-closed" [ Lui_extension.event_field "tab" Lui_extension.StringScalar true ]; Lui_extension.event "tab-moved" [ Lui_extension.event_field "tab" Lui_extension.StringScalar true; Lui_extension.event_field "index" Lui_extension.IntScalar true; Lui_extension.event_field "from-pane" Lui_extension.StringScalar true ]; Lui_extension.event "pane-focused" [ ]; Lui_extension.event "navigate" [ Lui_extension.event_field "direction" Lui_extension.StringScalar true ]; Lui_extension.event "split-requested" [ Lui_extension.event_field "orientation" Lui_extension.StringScalar true ]; Lui_extension.event "split-drop" [ Lui_extension.event_field "tab" Lui_extension.StringScalar true; Lui_extension.event_field "from-pane" Lui_extension.StringScalar true; Lui_extension.event_field "edge" Lui_extension.StringScalar true ]; Lui_extension.event "pane-closed" [ ] ] let split_tab_schema = - Lui_extension.component "split-tab" [ { Lui_protocol.profile_os = MacOS; Lui_protocol.profile_host = SwiftUIHost }; { Lui_protocol.profile_os = IOS; Lui_protocol.profile_host = SwiftUIHost }; { Lui_protocol.profile_os = AndroidOS; Lui_protocol.profile_host = FlutterHost }; { Lui_protocol.profile_os = IOS; Lui_protocol.profile_host = FlutterHost }; { Lui_protocol.profile_os = LinuxOS; Lui_protocol.profile_host = FlutterHost }; { Lui_protocol.profile_os = MacOS; Lui_protocol.profile_host = FlutterHost }; { Lui_protocol.profile_os = WindowsOS; Lui_protocol.profile_host = FlutterHost }; { Lui_protocol.profile_os = WebOS; Lui_protocol.profile_host = WebHost }; { Lui_protocol.profile_os = MacOS; Lui_protocol.profile_host = GPUIHost }; { Lui_protocol.profile_os = LinuxOS; Lui_protocol.profile_host = GPUIHost }; { Lui_protocol.profile_os = WindowsOS; Lui_protocol.profile_host = GPUIHost } ] + Lui_extension.component "split-tab" [ { Lui_protocol.profile_os = MacOS; Lui_protocol.profile_host = SwiftUIHost }; { Lui_protocol.profile_os = IOS; Lui_protocol.profile_host = SwiftUIHost }; { Lui_protocol.profile_os = WebOS; Lui_protocol.profile_host = WebHost }; { Lui_protocol.profile_os = MacOS; Lui_protocol.profile_host = GPUIHost }; { Lui_protocol.profile_os = LinuxOS; Lui_protocol.profile_host = GPUIHost }; { Lui_protocol.profile_os = WindowsOS; Lui_protocol.profile_host = GPUIHost } ] true [ ] [ Lui_extension.property "tab-id" Lui_extension.StringScalar true None; Lui_extension.property "title" Lui_extension.StringScalar true None; Lui_extension.property "icon" Lui_extension.StringScalar false None; Lui_extension.property "dirty" Lui_extension.BoolScalar false None; Lui_extension.property "closable" Lui_extension.BoolScalar false None; Lui_extension.property "accessibility-identifier" Lui_extension.StringScalar false None ] @@ -351,7 +351,7 @@ let gpui_table_schema = [ ] let gallery_accent_schema = - Lui_extension.tweak "gallery-accent" [ { Lui_protocol.profile_os = AndroidOS; Lui_protocol.profile_host = FlutterHost }; { Lui_protocol.profile_os = IOS; Lui_protocol.profile_host = FlutterHost }; { Lui_protocol.profile_os = IOS; Lui_protocol.profile_host = SwiftUIHost }; { Lui_protocol.profile_os = LinuxOS; Lui_protocol.profile_host = FlutterHost }; { Lui_protocol.profile_os = MacOS; Lui_protocol.profile_host = FlutterHost }; { Lui_protocol.profile_os = MacOS; Lui_protocol.profile_host = SwiftUIHost }; { Lui_protocol.profile_os = WebOS; Lui_protocol.profile_host = WebHost }; { Lui_protocol.profile_os = WindowsOS; Lui_protocol.profile_host = FlutterHost }; { Lui_protocol.profile_os = MacOS; Lui_protocol.profile_host = GPUIHost }; { Lui_protocol.profile_os = LinuxOS; Lui_protocol.profile_host = GPUIHost }; { Lui_protocol.profile_os = WindowsOS; Lui_protocol.profile_host = GPUIHost } ] + Lui_extension.tweak "gallery-accent" [ { Lui_protocol.profile_os = IOS; Lui_protocol.profile_host = SwiftUIHost }; { Lui_protocol.profile_os = MacOS; Lui_protocol.profile_host = SwiftUIHost }; { Lui_protocol.profile_os = WebOS; Lui_protocol.profile_host = WebHost }; { Lui_protocol.profile_os = MacOS; Lui_protocol.profile_host = GPUIHost }; { Lui_protocol.profile_os = LinuxOS; Lui_protocol.profile_host = GPUIHost }; { Lui_protocol.profile_os = WindowsOS; Lui_protocol.profile_host = GPUIHost } ] [ ] let registry () = diff --git a/examples/markdown/native/markdown_bridge.ml b/examples/markdown/native/markdown_bridge.ml index 06a49f14..9fd9c7bd 100644 --- a/examples/markdown/native/markdown_bridge.ml +++ b/examples/markdown/native/markdown_bridge.ml @@ -25,7 +25,6 @@ let operating_system = function let host_kind = function | 1 -> WebHost | 2 -> SwiftUIHost - | 3 -> FlutterHost | 6 -> GPUIHost | _ -> GenericHost @@ -139,6 +138,5 @@ let () = Callback.register "lui_ocaml_pointer_leave" pointer_leave; Callback.register "lui_ocaml_context_menu_press" context_menu_press; Callback.register "lui_ocaml_extension_event" extension_event; - Callback.register "lui_flutter_extension_event" extension_event; Callback.register "lui_ocaml_dispose" dispose; Callback.register "lui_ocaml_root_node" root_node diff --git a/examples/navigation/README.md b/examples/navigation/README.md index 8a63e1ad..a4acb5a3 100644 --- a/examples/navigation/README.md +++ b/examples/navigation/README.md @@ -56,7 +56,7 @@ animation or system/browser history integration. Covered page nodes are detached from the visible slot but remain in the LUI runtime with live scopes, subscriptions and OCaml local state. Popped pages dispose immediately. A host may recreate its visible widget when the page is reattached, so native widget -state and scroll position need platform-specific verification. Web, Flutter, +state and scroll position need platform-specific verification. Web and GPUI do not yet have native navigation extension renderers. ## Independent iOS Simulator demo diff --git a/examples/navigation/native/navigation_bridge.ml b/examples/navigation/native/navigation_bridge.ml index 645ac895..870e6417 100644 --- a/examples/navigation/native/navigation_bridge.ml +++ b/examples/navigation/native/navigation_bridge.ml @@ -173,7 +173,6 @@ let () = (ExtensionEvent (node, identifier, name, Lui_json.parse_values json)) in Callback.register "lui_ocaml_extension_event" extension_event; - Callback.register "lui_flutter_extension_event" extension_event; Callback.register "lui_ocaml_dispose" (fun () -> latest_patch := ""; ignore (Lui_app.dispose (app ())); diff --git a/examples/split/native/split_bridge.ml b/examples/split/native/split_bridge.ml index f04c35d7..4bfb73fa 100644 --- a/examples/split/native/split_bridge.ml +++ b/examples/split/native/split_bridge.ml @@ -25,7 +25,6 @@ let operating_system = function let host_kind = function | 1 -> WebHost | 2 -> SwiftUIHost - | 3 -> FlutterHost | 6 -> GPUIHost | _ -> GenericHost @@ -139,6 +138,5 @@ let () = Callback.register "lui_ocaml_pointer_leave" pointer_leave; Callback.register "lui_ocaml_context_menu_press" context_menu_press; Callback.register "lui_ocaml_extension_event" extension_event; - Callback.register "lui_flutter_extension_event" extension_event; Callback.register "lui_ocaml_dispose" dispose; Callback.register "lui_ocaml_root_node" root_node diff --git a/examples/todos/native/todos_bridge.ml b/examples/todos/native/todos_bridge.ml index 991a5d7f..14220195 100644 --- a/examples/todos/native/todos_bridge.ml +++ b/examples/todos/native/todos_bridge.ml @@ -25,7 +25,6 @@ let operating_system = function let host_kind = function | 1 -> WebHost | 2 -> SwiftUIHost - | 3 -> FlutterHost | 6 -> GPUIHost | _ -> GenericHost diff --git a/lui.opam b/lui.opam index a5197726..27b45b95 100644 --- a/lui.opam +++ b/lui.opam @@ -3,7 +3,7 @@ opam-version: "2.0" version: "0.1.0" synopsis: "A reactive retained cross-platform UI runtime in OCaml" description: - "Declarative UI, incremental patch batching, and Apple, Flutter, and Web backends powered by ocaml-signal. Element constructors follow the convention unit -> ?props -> children:t list -> unit -> t so optional props erase under a trailing unit argument." + "Declarative UI, incremental patch batching, and Apple and Web backends powered by ocaml-signal. Element constructors follow the convention unit -> ?props -> children:t list -> unit -> t so optional props erase under a trailing unit argument." maintainer: ["Tienson Qin "] authors: ["Tienson Qin"] license: "MIT" diff --git a/lui.opam.locked b/lui.opam.locked index 57702402..fd2cdf79 100644 --- a/lui.opam.locked +++ b/lui.opam.locked @@ -10,7 +10,7 @@ opam-version: "2.0" version: "0.1.0" synopsis: "A reactive retained cross-platform UI runtime in OCaml" description: - "Declarative UI, incremental patch batching, and Apple, Flutter, and Web backends powered by ocaml-signal. Element constructors follow the convention unit -> ?props -> children:t list -> unit -> t so optional props erase under a trailing unit argument." + "Declarative UI, incremental patch batching, and Apple and Web backends powered by ocaml-signal. Element constructors follow the convention unit -> ?props -> children:t list -> unit -> t so optional props erase under a trailing unit argument." maintainer: ["Tienson Qin "] authors: ["Tienson Qin"] license: "MIT" diff --git a/platform/apple/Sources/LUIAppleBackend/LUISplit.swift b/platform/apple/Sources/LUIAppleBackend/LUISplit.swift index ed18f0b3..a5ed2820 100644 --- a/platform/apple/Sources/LUIAppleBackend/LUISplit.swift +++ b/platform/apple/Sources/LUIAppleBackend/LUISplit.swift @@ -10,13 +10,13 @@ import UniformTypeIdentifiers @MainActor public enum LUISplit { static let viewFingerprint = - "lui-extension-v1|10:split-view|profiles:android/flutter,ios/flutter,ios/swiftui,linux/flutter,linux/gpui,macos/flutter,macos/gpui,macos/swiftui,web/web,windows/flutter,windows/gpui|standard-children:0|children:12:split-branch,10:split-pane|properties:17:divider-thickness:float:optional:none,24:accessibility-identifier:string:optional:none,9:animation:bool:optional:none|events:" + "lui-extension-v1|10:split-view|profiles:ios/swiftui,linux/gpui,macos/gpui,macos/swiftui,web/web,windows/gpui|standard-children:0|children:12:split-branch,10:split-pane|properties:17:divider-thickness:float:optional:none,24:accessibility-identifier:string:optional:none,9:animation:bool:optional:none|events:" static let branchFingerprint = - "lui-extension-v1|12:split-branch|profiles:android/flutter,ios/flutter,ios/swiftui,linux/flutter,linux/gpui,macos/flutter,macos/gpui,macos/swiftui,web/web,windows/flutter,windows/gpui|standard-children:0|children:12:split-branch,10:split-pane|properties:11:orientation:string:required:none,5:ratio:float:required:none|events:13:ratio-changed[5:ratio:float:required]" + "lui-extension-v1|12:split-branch|profiles:ios/swiftui,linux/gpui,macos/gpui,macos/swiftui,web/web,windows/gpui|standard-children:0|children:12:split-branch,10:split-pane|properties:11:orientation:string:required:none,5:ratio:float:required:none|events:13:ratio-changed[5:ratio:float:required]" static let paneFingerprint = - "lui-extension-v1|10:split-pane|profiles:android/flutter,ios/flutter,ios/swiftui,linux/flutter,linux/gpui,macos/flutter,macos/gpui,macos/swiftui,web/web,windows/flutter,windows/gpui|standard-children:0|children:9:split-tab|properties:24:accessibility-identifier:string:optional:none,7:focused:bool:optional:none,7:pane-id:string:required:none,8:selected:string:optional:none|events:10:split-drop[3:tab:string:required,4:edge:string:required,9:from-pane:string:required],10:tab-closed[3:tab:string:required],11:pane-closed[],12:pane-focused[],12:tab-selected[3:tab:string:required],15:split-requested[11:orientation:string:required],8:navigate[9:direction:string:required],9:tab-moved[3:tab:string:required,5:index:int:required,9:from-pane:string:required]" + "lui-extension-v1|10:split-pane|profiles:ios/swiftui,linux/gpui,macos/gpui,macos/swiftui,web/web,windows/gpui|standard-children:0|children:9:split-tab|properties:24:accessibility-identifier:string:optional:none,7:focused:bool:optional:none,7:pane-id:string:required:none,8:selected:string:optional:none|events:10:split-drop[3:tab:string:required,4:edge:string:required,9:from-pane:string:required],10:tab-closed[3:tab:string:required],11:pane-closed[],12:pane-focused[],12:tab-selected[3:tab:string:required],15:split-requested[11:orientation:string:required],8:navigate[9:direction:string:required],9:tab-moved[3:tab:string:required,5:index:int:required,9:from-pane:string:required]" static let tabFingerprint = - "lui-extension-v1|9:split-tab|profiles:android/flutter,ios/flutter,ios/swiftui,linux/flutter,linux/gpui,macos/flutter,macos/gpui,macos/swiftui,web/web,windows/flutter,windows/gpui|standard-children:1|children:|properties:24:accessibility-identifier:string:optional:none,4:icon:string:optional:none,5:dirty:bool:optional:none,5:title:string:required:none,6:tab-id:string:required:none,8:closable:bool:optional:none|events:" + "lui-extension-v1|9:split-tab|profiles:ios/swiftui,linux/gpui,macos/gpui,macos/swiftui,web/web,windows/gpui|standard-children:1|children:|properties:24:accessibility-identifier:string:optional:none,4:icon:string:optional:none,5:dirty:bool:optional:none,5:title:string:required:none,6:tab-id:string:required:none,8:closable:bool:optional:none|events:" private static let accessibilityIdentifier = LUIExtensionProperty( name: "accessibility-identifier", kind: .string) diff --git a/platform/apple/Sources/LUIAppleBackend/LUIThemePolicy.swift b/platform/apple/Sources/LUIAppleBackend/LUIThemePolicy.swift index d225b38b..7dd461c8 100644 --- a/platform/apple/Sources/LUIAppleBackend/LUIThemePolicy.swift +++ b/platform/apple/Sources/LUIAppleBackend/LUIThemePolicy.swift @@ -157,7 +157,7 @@ enum LUIModalBackgroundPolicy { systemBackground: Color ) -> Color { // Sheets and dialogs are elevated: they sit on the `surface` token, - // matching the Flutter backend's `colorScheme.surface` modal default. + // matching the Material `colorScheme.surface` modal default. semanticColors["surface"] ?? semanticColors["background"] ?? systemBackground } diff --git a/platform/flutter/analysis_options.yaml b/platform/flutter/analysis_options.yaml deleted file mode 100644 index 743e05ad..00000000 --- a/platform/flutter/analysis_options.yaml +++ /dev/null @@ -1,10 +0,0 @@ -analyzer: - exclude: - - build/** - - android/** - - ios/** - - web/** - - windows/** - - macos/** - - linux/** -include: package:flutter_lints/flutter.yaml diff --git a/platform/flutter/lib/lui_flutter_backend.dart b/platform/flutter/lib/lui_flutter_backend.dart deleted file mode 100644 index 459be28b..00000000 --- a/platform/flutter/lib/lui_flutter_backend.dart +++ /dev/null @@ -1,7455 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; -import 'dart:math' as math; -import 'dart:ui' as ui; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; -import 'package:flutter/scheduler.dart'; -import 'package:flutter/services.dart'; - -part 'lui_wire_schema.g.dart'; -part 'lui_flutter_extension.dart'; -part 'lui_flutter_split.dart'; - -sealed class LUIEvent { - const LUIEvent(); - - const factory LUIEvent.appear({required int node}) = LUIAppearEvent; - const factory LUIEvent.press({required int node}) = LUIPressEvent; - const factory LUIEvent.longPress({required int node}) = LUILongPressEvent; - const factory LUIEvent.textChanged({ - required int node, - required String text, - }) = LUITextChangedEvent; - const factory LUIEvent.submit({required int node}) = LUISubmitEvent; - const factory LUIEvent.toggleChanged({ - required int node, - required bool checked, - }) = LUIToggleChangedEvent; - const factory LUIEvent.change({required int node}) = LUIChangeEvent; - const factory LUIEvent.valueChanged({ - required int node, - required double value, - }) = LUIValueChangedEvent; - const factory LUIEvent.dismiss({required int node}) = LUIDismissEvent; - const factory LUIEvent.picked({ - required int node, - required String payload, - }) = LUIPickedEvent; - const factory LUIEvent.doublePress({required int node}) = LUIDoublePressEvent; - const factory LUIEvent.pressDetail({ - required int node, - required LUIPointerDetail detail, - }) = LUIPressDetailEvent; - const factory LUIEvent.pointerDown({ - required int node, - required LUIPointerDetail detail, - }) = LUIPointerDownEvent; - const factory LUIEvent.pointerUp({ - required int node, - required LUIPointerDetail detail, - }) = LUIPointerUpEvent; - const factory LUIEvent.pointerEnter({required int node}) = LUIPointerEnterEvent; - const factory LUIEvent.pointerLeave({required int node}) = LUIPointerLeaveEvent; - const factory LUIEvent.contextMenuPress({ - required int node, - required LUIPointerDetail detail, - }) = LUIContextMenuPressEvent; - const factory LUIEvent.extension({ - required int node, - required String identifier, - required String name, - required Map values, - }) = LUIExtensionComponentEvent; - const factory LUIEvent.scrollCompleted({ - required int node, - required int token, - required String outcome, - }) = LUIScrollCompletedEvent; - const factory LUIEvent.visibleRange({ - required int node, - required int first, - required int last, - }) = LUIVisibleRangeEvent; -} - -@immutable -final class LUIRootSection { - const LUIRootSection({required this.id, required this.title}); - - final int id; - final String title; - - @override - bool operator ==(Object other) => - other is LUIRootSection && other.id == id && other.title == title; - - @override - int get hashCode => Object.hash(id, title); -} - -final class LUIAppearEvent extends LUIEvent { - const LUIAppearEvent({required this.node}); - final int node; - - @override - bool operator ==(Object other) => - other is LUIAppearEvent && other.node == node; - - @override - int get hashCode => node.hashCode; -} - -/// Shared payload for the pointer-detail events. [x]/[y] are client-space -/// coordinates; [modifiers] is the PressModifiers bitmask (1=ctrl, 2=shift, -/// 4=meta, 8=secondary button); [button] is the browser-style index -/// (0 primary, 1 middle, 2 secondary); [targetClass] carries the deepest hit -/// element's class list on web and stays empty on this host. -@immutable -final class LUIPointerDetail { - const LUIPointerDetail({ - required this.x, - required this.y, - required this.modifiers, - required this.button, - required this.targetClass, - }); - - final double x; - final double y; - final int modifiers; - final int button; - final String targetClass; - - @override - bool operator ==(Object other) => - other is LUIPointerDetail && - other.x == x && - other.y == y && - other.modifiers == modifiers && - other.button == button && - other.targetClass == targetClass; - - @override - int get hashCode => Object.hash(x, y, modifiers, button, targetClass); -} - -final class LUIPressDetailEvent extends LUIEvent { - const LUIPressDetailEvent({required this.node, required this.detail}); - final int node; - final LUIPointerDetail detail; - - @override - bool operator ==(Object other) => - other is LUIPressDetailEvent && - other.node == node && - other.detail == detail; - - @override - int get hashCode => Object.hash(node, detail); -} - -final class LUIPointerDownEvent extends LUIEvent { - const LUIPointerDownEvent({required this.node, required this.detail}); - final int node; - final LUIPointerDetail detail; - - @override - bool operator ==(Object other) => - other is LUIPointerDownEvent && - other.node == node && - other.detail == detail; - - @override - int get hashCode => Object.hash(node, detail); -} - -final class LUIPointerUpEvent extends LUIEvent { - const LUIPointerUpEvent({required this.node, required this.detail}); - final int node; - final LUIPointerDetail detail; - - @override - bool operator ==(Object other) => - other is LUIPointerUpEvent && - other.node == node && - other.detail == detail; - - @override - int get hashCode => Object.hash(node, detail); -} - -final class LUIPointerEnterEvent extends LUIEvent { - const LUIPointerEnterEvent({required this.node}); - final int node; - - @override - bool operator ==(Object other) => - other is LUIPointerEnterEvent && other.node == node; - - @override - int get hashCode => node.hashCode; -} - -final class LUIPointerLeaveEvent extends LUIEvent { - const LUIPointerLeaveEvent({required this.node}); - final int node; - - @override - bool operator ==(Object other) => - other is LUIPointerLeaveEvent && other.node == node; - - @override - int get hashCode => node.hashCode; -} - -final class LUIContextMenuPressEvent extends LUIEvent { - const LUIContextMenuPressEvent({required this.node, required this.detail}); - final int node; - final LUIPointerDetail detail; - - @override - bool operator ==(Object other) => - other is LUIContextMenuPressEvent && - other.node == node && - other.detail == detail; - - @override - int get hashCode => Object.hash(node, detail); -} - -final class LUIDoublePressEvent extends LUIEvent { - const LUIDoublePressEvent({required this.node}); - final int node; - - @override - bool operator ==(Object other) => - other is LUIDoublePressEvent && other.node == node; - - @override - int get hashCode => node.hashCode; -} - -final class LUIDismissEvent extends LUIEvent { - const LUIDismissEvent({required this.node}); - final int node; - - @override - bool operator ==(Object other) => - other is LUIDismissEvent && other.node == node; - - @override - int get hashCode => node.hashCode; -} - -final class LUIPickedEvent extends LUIEvent { - const LUIPickedEvent({required this.node, required this.payload}); - final int node; - final String payload; - - @override - bool operator ==(Object other) => - other is LUIPickedEvent && other.node == node && other.payload == payload; - - @override - int get hashCode => Object.hash(node, payload); -} - -final class LUIChangeEvent extends LUIEvent { - const LUIChangeEvent({required this.node}); - final int node; - @override - bool operator ==(Object other) => - other is LUIChangeEvent && other.node == node; - @override - int get hashCode => node.hashCode; -} - -final class LUIValueChangedEvent extends LUIEvent { - const LUIValueChangedEvent({required this.node, required this.value}); - final int node; - final double value; - @override - bool operator ==(Object other) => - other is LUIValueChangedEvent && - other.node == node && - other.value == value; - @override - int get hashCode => Object.hash(node, value); -} - -final class LUILongPressEvent extends LUIEvent { - const LUILongPressEvent({required this.node}); - final int node; - - @override - bool operator ==(Object other) => - other is LUILongPressEvent && other.node == node; - - @override - int get hashCode => node.hashCode; -} - -final class LUIPressEvent extends LUIEvent { - const LUIPressEvent({required this.node}); - final int node; - - @override - bool operator ==(Object other) => - other is LUIPressEvent && other.node == node; - - @override - int get hashCode => node.hashCode; -} - -final class LUITextChangedEvent extends LUIEvent { - const LUITextChangedEvent({required this.node, required this.text}); - final int node; - final String text; - - @override - bool operator ==(Object other) => - other is LUITextChangedEvent && other.node == node && other.text == text; - - @override - int get hashCode => Object.hash(node, text); -} - -final class LUISubmitEvent extends LUIEvent { - const LUISubmitEvent({required this.node}); - final int node; - - @override - bool operator ==(Object other) => - other is LUISubmitEvent && other.node == node; - - @override - int get hashCode => node.hashCode; -} - -final class LUIToggleChangedEvent extends LUIEvent { - const LUIToggleChangedEvent({required this.node, required this.checked}); - final int node; - final bool checked; - - @override - bool operator ==(Object other) => - other is LUIToggleChangedEvent && - other.node == node && - other.checked == checked; - - @override - int get hashCode => Object.hash(node, checked); -} - -final class LUIScrollCompletedEvent extends LUIEvent { - const LUIScrollCompletedEvent({ - required this.node, - required this.token, - required this.outcome, - }); - final int node; - final int token; - final String outcome; - - @override - bool operator ==(Object other) => - other is LUIScrollCompletedEvent && - other.node == node && - other.token == token && - other.outcome == outcome; - - @override - int get hashCode => Object.hash(node, token, outcome); -} - -final class LUIVisibleRangeEvent extends LUIEvent { - const LUIVisibleRangeEvent({ - required this.node, - required this.first, - required this.last, - }); - final int node; - final int first; - final int last; - - @override - bool operator ==(Object other) => - other is LUIVisibleRangeEvent && - other.node == node && - other.first == first && - other.last == last; - - @override - int get hashCode => Object.hash(node, first, last); -} - -final class LUIBackendException implements Exception { - const LUIBackendException(this.message); - final String message; - - @override - String toString() => 'LUIBackendException: $message'; -} - -const _iconNames = { - 'alert', - 'archive', - 'arrow-down', - 'arrow-right', - 'arrow-up', - 'check', - 'check-circle', - 'chevron-down', - 'chevron-left', - 'chevron-right', - 'chevron-up', - 'circle-dot', - 'clock', - 'copy', - 'download', - 'edit', - 'ellipsis', - 'external-link', - 'eye', - 'file-text', - 'folder', - 'folder-open', - 'git-branch', - 'git-merge', - 'git-pull-request', - 'info', - 'menu', - 'mic', - 'moon', - 'music', - 'panel-left', - 'panel-right', - 'pause', - 'play', - 'plus', - 'refresh-cw', - 'repeat', - 'save', - 'search', - 'send', - 'settings', - 'shuffle', - 'skip-back', - 'skip-forward', - 'sun', - 'terminal', - 'trash', - 'volume', - 'wrench', - 'x', - 'x-circle', -}; - -final _appIconNamePattern = RegExp(r'^app:[a-z0-9]+(?:-[a-z0-9]+)*$'); - -IconData _materialIcon(String name) => switch (name) { - 'alert' => Icons.warning_amber, - 'archive' => Icons.archive_outlined, - 'arrow-down' => Icons.arrow_downward, - 'arrow-right' => Icons.arrow_forward, - 'arrow-up' => Icons.arrow_upward, - 'check' => Icons.check, - 'check-circle' => Icons.check_circle_outline, - 'chevron-down' => Icons.keyboard_arrow_down, - 'chevron-left' => Icons.chevron_left, - 'chevron-right' => Icons.chevron_right, - 'chevron-up' => Icons.keyboard_arrow_up, - 'circle-dot' => Icons.radio_button_checked, - 'clock' => Icons.schedule, - 'copy' => Icons.content_copy, - 'download' => Icons.download, - 'edit' => Icons.edit_outlined, - 'ellipsis' => Icons.more_horiz, - 'external-link' => Icons.open_in_new, - 'eye' => Icons.visibility_outlined, - 'file-text' => Icons.description_outlined, - 'folder' => Icons.folder_outlined, - 'folder-open' => Icons.folder_open, - 'git-branch' => Icons.account_tree_outlined, - 'git-merge' => Icons.merge, - 'git-pull-request' => Icons.call_merge, - 'info' => Icons.info_outline, - 'menu' => Icons.menu, - 'mic' => Icons.mic_none, - 'moon' => Icons.dark_mode_outlined, - 'music' => Icons.music_note, - 'panel-left' => Icons.view_sidebar_outlined, - 'panel-right' => Icons.view_sidebar, - 'pause' => Icons.pause, - 'play' => Icons.play_arrow, - 'plus' => Icons.add, - 'refresh-cw' => Icons.refresh, - 'repeat' => Icons.repeat, - 'save' => Icons.save_outlined, - 'search' => Icons.search, - 'send' => Icons.send_outlined, - 'settings' => Icons.settings_outlined, - 'shuffle' => Icons.shuffle, - 'skip-back' => Icons.skip_previous, - 'skip-forward' => Icons.skip_next, - 'sun' => Icons.light_mode_outlined, - 'terminal' => Icons.terminal, - 'trash' => Icons.delete_outline, - 'volume' => Icons.volume_up_outlined, - 'wrench' => Icons.build_outlined, - 'x' => Icons.close, - 'x-circle' => Icons.cancel_outlined, - _ => Icons.question_mark, -}; - -Color _mediaSurfacePlaceholderColor(int id) => Color.fromARGB( - 0xff, - 64 + (id * 37) % 64, - 64 + (id * 57) % 64, - 64 + (id * 83) % 64, -); - -final class _NodeState { - _NodeState(this.kind); - _NodeState.copy(_NodeState other) - : kind = other.kind, - parent = other.parent, - children = List.of(other.children), - properties = Map.of(other.properties); - - final _NodeKind kind; - int? parent; - List children = []; - Map properties = {}; - - bool rendersLike(_NodeState other) => - kind == other.kind && - parent == other.parent && - listEquals(children, other.children) && - mapEquals(properties, other.properties); -} - -final class _NodeHandle extends ChangeNotifier { - _NodeHandle(int id, this.state) - : focusNode = FocusNode(debugLabel: 'lui-node-$id'); - - _NodeState state; - final FocusNode focusNode; - int revision = 0; - - void markChanged() { - revision += 1; - notifyListeners(); - } - - void markDependencyChanged() { - notifyListeners(); - } - - @override - void dispose() { - focusNode.dispose(); - super.dispose(); - } -} - -// Scoped theme tokens: a `theme` prop carries a name→value table that merges -// over the nearest ancestor scope, and `_color` consults it before the -// platform-default semantic map. -final class _LUIThemeScope extends InheritedWidget { - const _LUIThemeScope({required this.tokens, required super.child}); - - final Map tokens; - - static Map? maybeTokens(BuildContext context) => - context.dependOnInheritedWidgetOfExactType<_LUIThemeScope>()?.tokens; - - @override - bool updateShouldNotify(_LUIThemeScope oldWidget) => - !mapEquals(tokens, oldWidget.tokens); -} - -final class _LUIAppearDispatcher extends StatefulWidget { - const _LUIAppearDispatcher({ - super.key, - required this.node, - required this.onAppear, - required this.child, - }); - - final int node; - final VoidCallback onAppear; - final Widget child; - - @override - State<_LUIAppearDispatcher> createState() => _LUIAppearDispatcherState(); -} - -final class _LUIAppearDispatcherState extends State<_LUIAppearDispatcher> { - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) widget.onAppear(); - }); - } - - @override - Widget build(BuildContext context) => widget.child; -} - -/// Registers the node's element in the backend's frame-probe table so -/// `_measureFrames` can locate its render object each frame. Transparent -/// to layout. -final class _FrameProbe extends StatefulWidget { - const _FrameProbe({ - super.key, - required this.backend, - required this.node, - required this.child, - }); - - final LUIFlutterBackend backend; - final int node; - final Widget child; - - @override - State<_FrameProbe> createState() => _FrameProbeState(); -} - -final class _FrameProbeState extends State<_FrameProbe> { - @override - void initState() { - super.initState(); - widget.backend._frameProbes[widget.node] = context; - } - - @override - void didUpdateWidget(_FrameProbe oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.node != widget.node) { - widget.backend._frameProbes.remove(oldWidget.node); - widget.backend._frameProbes[widget.node] = context; - } - } - - @override - void dispose() { - widget.backend._frameProbes.remove(widget.node); - super.dispose(); - } - - @override - Widget build(BuildContext context) => widget.child; -} - -final class LUIFlutterBackend { - LUIFlutterBackend({ - this.onEvent, - Map appIcons = const {}, - LUIFlutterExtensionRegistry? extensionRegistry, - this.filePathResolver, - }) : appIcons = Map.unmodifiable(appIcons), - _extensionRegistry = extensionRegistry ?? LUIFlutterExtensionRegistry() { - _extensionRegistry._freeze(); - } - - final void Function(LUIEvent event)? onEvent; - final Map appIcons; - - /// Resolves the `path` prop of `file-image`/`file-preview` nodes into an - /// on-disk path. Apps that persist assets under a private files directory - /// install a resolver so model-owned relative paths (e.g. "Assets/x.png") - /// reach the right location; nil keeps paths untouched. - final String Function(String path)? filePathResolver; - - /// Called once per frame while [frameReportingEnabled] is on, with the - /// full node-id → rect map in global coordinates — feeds drive's - /// live-attach `tap x y` hit-testing. - void Function(Map frames)? onFrames; - - final Map _frameProbes = {}; - Map? _lastFrames; - bool _frameReportingEnabled = false; - bool _frameMeasureScheduled = false; - - bool get frameReportingEnabled => _frameReportingEnabled; - set frameReportingEnabled(bool value) { - if (value == _frameReportingEnabled) return; - _frameReportingEnabled = value; - if (value) _scheduleFrameMeasure(); - } - - void _scheduleFrameMeasure() { - if (_frameMeasureScheduled) return; - _frameMeasureScheduled = true; - SchedulerBinding.instance.addPostFrameCallback((_) { - _frameMeasureScheduled = false; - _measureFrames(); - // A persistent loop: scrolling moves nodes without rebuilds, so - // frames must be re-measured every frame while reporting is on. - if (_frameReportingEnabled) _scheduleFrameMeasure(); - }); - } - - void _measureFrames() { - final frames = {}; - for (final entry in _frameProbes.entries) { - final renderObject = entry.value.findRenderObject(); - if (renderObject is RenderBox && renderObject.attached) { - frames[entry.key] = - renderObject.localToGlobal(Offset.zero) & renderObject.size; - } - } - if (!mapEquals(frames, _lastFrames)) { - _lastFrames = frames; - onFrames?.call(frames); - } - } - - final LUIFlutterExtensionRegistry _extensionRegistry; - final _LUITooltipSession _tooltipSession = _LUITooltipSession(); - Map _states = {}; - final Map _handles = {}; - // Live modal routes by owning node, so a patch that drops the node can - // remove the route even if the presenter's element never unmounts. - final Map, NavigatorState)> _modalRoutes = {}; - Map _extensionStates = {}; - final Map _extensionHandles = {}; - final Map _images = {}; - final Map _mediaSurfaces = {}; - int generation = 0; - - static Key nodeKey(int id) => ValueKey('lui-node-$id'); - - bool containsNode(int id) => - _states.containsKey(id) || _extensionStates.containsKey(id); - - List rootSections(int root) { - final rootState = _requireState(_states, root); - final children = - rootState.kind == _NodeKind.root && rootState.children.length == 1 - ? _requireState(_states, rootState.children.single).children - : rootState.children; - return List.unmodifiable( - children.map( - (pageID) => LUIRootSection( - id: pageID, - title: _firstSectionTitle(pageID) ?? 'Component $pageID', - ), - ), - ); - } - - String? _firstSectionTitle(int node) { - final state = _states[node]; - if (state != null) { - final text = state.properties['text']; - if ((state.kind == _NodeKind.heading || state.kind == _NodeKind.text) && - text is String && - text.isNotEmpty) { - return text; - } - for (final child in state.children) { - final title = _firstSectionTitle(child); - if (title != null) return title; - } - return null; - } - final extension = _extensionStates[node]; - if (extension == null) return null; - for (final child in extension.children) { - final title = _firstSectionTitle(child); - if (title != null) return title; - } - return null; - } - - int debugRevision(int id) => - _handles[id]?.revision ?? _requireExtensionHandle(id).revision; - - IconData _iconData(String name) { - if (name.startsWith('app:')) { - return appIcons[name.substring(4)] ?? Icons.question_mark; - } - return _materialIcon(name); - } - - void dispose() { - for (final handle in _handles.values) { - handle.dispose(); - } - _handles.clear(); - for (final handle in _extensionHandles.values) { - handle.dispose(); - } - _extensionHandles.clear(); - for (final image in _images.values) { - image.dispose(); - } - _images.clear(); - for (final image in _mediaSurfaces.values) { - image.dispose(); - } - _mediaSurfaces.clear(); - _states = {}; - _extensionStates = {}; - } - - void registerImage({required int id, required ui.Image image}) { - if (id <= 0) { - throw const LUIBackendException('registered image id must be positive'); - } - final replacement = image.clone(); - final previous = _images[id]; - _images[id] = replacement; - previous?.dispose(); - _invalidateAvatars(id); - } - - void unregisterImage(int id) { - final image = _images.remove(id); - if (image == null) return; - image.dispose(); - _invalidateAvatars(id); - } - - void _invalidateAvatars(int imageID) { - for (final entry in _states.entries) { - if ((entry.value.kind == _NodeKind.avatar || - entry.value.kind == _NodeKind.image) && - entry.value.properties['image'] == imageID) { - _handles[entry.key]?.markChanged(); - } - } - } - - void presentMediaSurfaceFrame({required int id, required ui.Image image}) { - if (id <= 0) { - throw const LUIBackendException('media surface id must be positive'); - } - final replacement = image.clone(); - final previous = _mediaSurfaces[id]; - _mediaSurfaces[id] = replacement; - previous?.dispose(); - _invalidateMediaSurfaces(id); - } - - void unregisterMediaSurface(int id) { - final image = _mediaSurfaces.remove(id); - if (image == null) return; - image.dispose(); - _invalidateMediaSurfaces(id); - } - - void _invalidateMediaSurfaces(int surfaceID) { - for (final entry in _states.entries) { - if (entry.value.kind == _NodeKind.mediaSurface && - entry.value.properties['surface'] == surfaceID) { - _handles[entry.key]?.markChanged(); - } - } - } - - void applyJson(String source) { - final Object? decoded; - try { - decoded = jsonDecode(source); - } on FormatException catch (error) { - throw LUIBackendException(error.message); - } - final batch = _objectMap(decoded, 'patch batch'); - final nextGeneration = _integer(batch['generation'], 'generation'); - final expectedGeneration = generation + 1; - if (nextGeneration != expectedGeneration) { - throw LUIBackendException( - 'expected patch generation $expectedGeneration, ' - 'received $nextGeneration', - ); - } - final operations = _objectList(batch['ops'], 'ops') - .map((operation) => _objectMap(operation, 'operation')) - .toList(growable: false); - final next = { - for (final entry in _states.entries) - entry.key: _NodeState.copy(entry.value), - }; - final nextExtensions = { - for (final entry in _extensionStates.entries) - entry.key: _ExtensionNodeState.copy(entry.value), - }; - - final filePickerRequests = {}; - for (final operation in operations) { - _applyState(next, nextExtensions, operation, filePickerRequests); - } - _validateStates(next); - _validateExtensionStates(next, nextExtensions); - final changedIDs = {}; - final removed = _handles.keys - .where((id) => !next.containsKey(id)) - .toList(growable: false); - final removedExtensions = _extensionHandles.keys - .where((id) => !nextExtensions.containsKey(id)) - .toList(growable: false); - for (final entry in next.entries) { - final handle = _handles[entry.key]; - if (handle == null) { - _handles[entry.key] = _NodeHandle(entry.key, entry.value); - } else { - if (!handle.state.rendersLike(entry.value)) { - changedIDs.add(entry.key); - } - handle.state = entry.value; - } - } - for (final id in removed) { - final identifier = - _handles[id]?.state.properties['accessibility-identifier']; - if (identifier is String) { - _staleSemanticsIdentifiers.add(identifier); - } - _handles.remove(id)?.dispose(); - final modal = _modalRoutes.remove(id); - if (modal != null) { - _scheduleRouteRemoval(modal.$1, modal.$2); - } - } - for (final entry in nextExtensions.entries) { - final handle = _extensionHandles[entry.key]; - if (handle == null) { - _extensionHandles[entry.key] = _ExtensionNodeHandle(entry.value); - } else { - if (!handle.state.rendersLike(entry.value)) { - changedIDs.add(entry.key); - } - handle.state = entry.value; - } - } - for (final id in removedExtensions) { - _extensionHandles.remove(id)?.dispose(); - } - _states = next; - _extensionStates = nextExtensions; - // A route whose node vanished without going through `removed` (e.g. its - // handle was already gone) still must not linger. - _modalRoutes.removeWhere((node, modal) { - if (next.containsKey(node)) return false; - _scheduleRouteRemoval(modal.$1, modal.$2); - return true; - }); - if (removed.isNotEmpty) { - _scheduleSemanticsHeal(); - } - generation = nextGeneration; - final changedSources = Set.of(changedIDs); - final dependentIDs = {}; - for (final source in changedSources) { - final sourceState = next[source]; - var parent = next[source]?.parent ?? nextExtensions[source]?.parent; - while (parent != null) { - final ancestor = next[parent]; - final extensionAncestor = nextExtensions[parent]; - if (ancestor == null && extensionAncestor == null) break; - if (ancestor?.kind == _NodeKind.radioGroup) changedIDs.add(parent); - if (ancestor?.kind == _NodeKind.bottomTabs) dependentIDs.add(parent); - if (ancestor?.kind == _NodeKind.stack && - (sourceState?.kind == _NodeKind.dropdownMenu || - sourceState?.kind == _NodeKind.tooltip)) { - changedIDs.add(parent); - } - if (extensionAncestor != null) changedIDs.add(parent); - parent = ancestor?.parent ?? extensionAncestor?.parent; - } - } - for (final id in changedIDs) { - _handles[id]?.markChanged(); - _extensionHandles[id]?.markChanged(); - } - for (final id in dependentIDs.difference(changedIDs)) { - _handles[id]?.markDependencyChanged(); - } - _scheduleLayoutSettle(); - // This backend cannot present a picker: every file-picker request the - // batch left unanswered (request set, not matching `completion`) is - // answered with `dismiss`, per the element contract. - for (final id in filePickerRequests) { - final state = _states[id]; - if (state == null || state.kind != _NodeKind.filePicker) continue; - final request = state.properties['request']; - if (request == null || request == state.properties['completion']) { - continue; - } - onEvent?.call(LUIEvent.dismiss(node: id)); - } - } - - // Render objects adopted or moved into a subtree that is already clean - // can keep the needsLayout flag set by attach() forever: layout() - // early-returns on their clean ancestors so the flag is never cleared, - // and debug semantics integrity asserts then abort every semantics - // flush — the platform a11y tree freezes on the last good emit while - // the app renders fine. Re-mark the orphaned node so propagation - // reaches its relayout boundary and the next layout pass lays it out. - bool _layoutSettleScheduled = false; - bool _layoutSettleDirtySeen = false; - - // markNeedsLayout() early-returns on an already-dirty node, so an orphan - // cannot be healed by marking it directly. Instead mark the nearest clean - // render ancestor: propagation from it reaches its relayout boundary, the - // boundary re-lays out, and the layout descent passes through the (dirty) - // chain to the orphan and finally lays it out. - void _settleOrphanedLayout(RenderObject node) { - if (node.attached && node.debugNeedsLayout) { - // An orphan's ancestor chain can itself contain attach-orphans whose - // dirty flag never propagated, so propagation may early-return before - // reaching a relayout boundary. Mark every clean ancestor on the path - // to the root: the whole path ends up dirty, some relayout boundary is - // queued, and the next layout descent reaches the orphan. - void walk(RenderObject ro) { - if (!ro.attached) return; - if (!ro.debugNeedsLayout) { - ro.markNeedsLayout(); - _layoutSettleDirtySeen = true; - } - final parent = ro.parent; - if (parent is RenderObject) walk(parent); - } - - final parent = node.parent; - if (parent is RenderObject) walk(parent); - } else if (node is RenderBox && node.attached && !node.hasSize) { - // Poisoned node: clean but never laid out. This happens when the first - // layout attempt threw inside performLayout — layout() marks the node - // clean anyway (debug only), so every later layout() early-returns and - // any parent that reads child.size hits the hasSize assert. Re-mark it - // so its next layout actually performs and assigns a size. - node.markNeedsLayout(); - _layoutSettleDirtySeen = true; - } - node.visitChildren(_settleOrphanedLayout); - } - - // Orphans can appear frames after the patch that built their subtree - // (deferred element reactivation), so keep checking while any are found. - void _scheduleLayoutSettle() { - if (_layoutSettleScheduled) return; - _layoutSettleScheduled = true; - var attempts = 0; - void step([Duration? _]) { - _layoutSettleDirtySeen = false; - for (final view in RendererBinding.instance.renderViews) { - _settleOrphanedLayout(view); - } - attempts += 1; - if (_layoutSettleDirtySeen && attempts < 120) { - SchedulerBinding.instance.addPostFrameCallback(step); - } else { - _layoutSettleScheduled = false; - } - } - - SchedulerBinding.instance.addPostFrameCallback(step); - } - - // A dropped subtree can leave its SemanticsNodes attached forever: - // flushSemantics clears _nodesNeedingSemanticsUpdate wholesale but only - // runs updateChildren on boundaries that are not needsLayout at flush - // time, so a boundary marked while mid-layout loses its update and the - // stale child list (e.g. a removed sheet subtree) is never re-derived. - // Re-mark every built boundary each frame until the identifiers of the - // dropped nodes leave the semantics tree — or a bounded frame count for - // subtrees that carry no identifiers — so the platform a11y tree cannot - // freeze on the removed subtree. - // - // Marks lost at a flush are not limited to child lists: a node that - // mounted or changed during the same churn can lose its own config emit - // (identifier/label never arrive) while remaining a live semantics node. - // The heal therefore keeps re-marking for a short settle tail after the - // stale identifiers leave, giving every dropped boundary update another - // chance to land. The same drop can also hit a *mount*: a subtree whose - // boundary mark is lost never materializes a SemanticsNode at all, so the - // heal additionally waits until every mounted subtree's identifiers have - // appeared in the semantics tree. - final Set _staleSemanticsIdentifiers = {}; - bool _semanticsHealScheduled = false; - static const int _semanticsHealMaxFrames = 120; - static const int _semanticsHealTailFrames = 30; - // Marking every boundary each frame recompiles the whole semantics tree - // per frame — enough main-thread churn to starve the platform a11y - // channel (UIAutomator/Maestro driver startup) on a slow device. Marks - // therefore go out every fourth frame: a dropped mark is retried a few - // frames later, which is all the wedge needs to converge. - static const int _semanticsHealMarkStride = 4; - // Mounted-but-hidden subtrees (a closed drawer, offstage content) carry - // identifiers that legitimately never emit, and shared identifiers keep - // a stale name present — so a nonzero unresolved count alone cannot - // hold the heal open. The heal converges only while each mark round - // actually shrinks the unresolved set; a plateau means the remainder - // are intentional exclusions and the heal exits. - static const int _semanticsHealPlateauRounds = 3; - - void _collectSubtreeIdentifiers( - Map states, - Map extensions, - int id, - Set out, - ) { - if (!_containsState(states, extensions, id)) { - return; - } - final properties = - states[id]?.properties ?? - _requireExtensionStateFrom(extensions, id).properties; - final identifier = properties['accessibility-identifier']; - if (identifier is String) { - out.add(identifier); - } - for (final child in _nodeChildren(states, extensions, id)) { - _collectSubtreeIdentifiers(states, extensions, child, out); - } - } - - // Identifiers of every mounted subtree — the nodes the semantics tree - // should contain once all pending boundary updates have landed. Mounted - // subtrees hang under the host's root state (a parentless node of any - // kind — the app root here is a stack, not kind 'root'); orphan tops - // detached by remove-child are parentless too, so identifiers already - // tracked as stale are excluded to keep stale and missing disjoint. - Set _mountedSemanticsIdentifiers() { - final out = {}; - void walk(int id) { - if (!_containsState(_states, _extensionStates, id)) { - return; - } - final properties = - _states[id]?.properties ?? - _requireExtensionStateFrom(_extensionStates, id).properties; - final identifier = properties['accessibility-identifier']; - if (identifier is String && - !_staleSemanticsIdentifiers.contains(identifier)) { - out.add(identifier); - } - for (final child in _nodeChildren(_states, _extensionStates, id)) { - walk(child); - } - } - - // Whole panes mount under extension states as well as standard states — - // both maps have to feed the expected set or a never-emitted extension - // subtree would go undetected. - for (final id in _states.keys) { - if (_nodeParent(_states, _extensionStates, id) == null) { - walk(id); - } - } - for (final id in _extensionStates.keys) { - if (_nodeParent(_states, _extensionStates, id) == null) { - walk(id); - } - } - return out; - } - - void _scheduleSemanticsHeal() { - if (_semanticsHealScheduled) { - return; - } - _semanticsHealScheduled = true; - var attempts = 0; - var tailFramesLeft = _semanticsHealTailFrames; - var lastUnresolved = -1; - var unchangedRounds = 0; - void step([Duration? _]) { - attempts += 1; - // Each RenderView flushes semantics through its own child - // PipelineOwner (each carries its own SemanticsOwner + - // rootSemanticsNode); the rootPipelineOwner's owner alone does not - // see them. - final present = {}; - final roots = [ - RendererBinding - .instance - .rootPipelineOwner - .semanticsOwner - ?.rootSemanticsNode, - for (final view in RendererBinding.instance.renderViews) - view.owner?.semanticsOwner?.rootSemanticsNode, - ]; - // Walk in traversal order — the order serialized into - // childrenInTraversalOrder, which is what the platform a11y bridge - // exposes. OverlayPortal children graft under their traversal parent; - // a child whose parent never emitted exists in paint order but never - // reaches the a11y tree, so paint-order presence is not sufficient. - void walk(SemanticsNode node) { - if (node.identifier.isNotEmpty) { - present.add(node.identifier); - } - List? children; - try { - children = node.debugListChildrenInOrder( - DebugSemanticsDumpOrder.traversalOrder, - ); - } on FlutterError { - // A malformed traversal graft (cycle) throws; fall back to the - // paint-order children so the walk cannot wedge the heal loop. - children = []; - node.visitChildren((SemanticsNode child) { - children!.add(child); - return true; - }); - } - for (final child in children) { - walk(child); - } - } - - for (final root in roots) { - if (root != null) { - walk(root); - } - } - final retainedSet = _staleSemanticsIdentifiers.intersection(present); - final mountedIds = _mountedSemanticsIdentifiers(); - final missingSet = mountedIds.difference(present); - final unresolved = retainedSet.length + missingSet.length; - if (attempts == 1 || attempts % _semanticsHealMarkStride == 0) { - void markBoundaries(RenderObject ro) { - if (!ro.attached) { - return; - } - // Marking a non-boundary walks up to the nearest semantics - // boundary, so marking every attached render object re-dirties - // all of them; the dirty set deduplicates. - ro.markNeedsSemanticsUpdate(); - ro.visitChildren(markBoundaries); - } - - for (final view in RendererBinding.instance.renderViews) { - markBoundaries(view); - } - if (unresolved == lastUnresolved) { - unchangedRounds += 1; - } else { - unchangedRounds = 0; - lastUnresolved = unresolved; - } - } - final stuck = - unresolved > 0 && unchangedRounds >= _semanticsHealPlateauRounds; - final keepRetaining = - unresolved > 0 && !stuck && attempts < _semanticsHealMaxFrames; - final tail = unresolved == 0 && tailFramesLeft-- > 0; - if (keepRetaining || tail) { - SchedulerBinding.instance.addPostFrameCallback(step); - } else { - _staleSemanticsIdentifiers.clear(); - _semanticsHealScheduled = false; - } - } - - SchedulerBinding.instance.addPostFrameCallback(step); - } - - void _registerModalRoute( - int node, - Route route, - NavigatorState navigator, - ) { - _modalRoutes[node] = (route, navigator); - } - - void _unregisterModalRoute(int node, Route route) { - final entry = _modalRoutes[node]; - if (entry != null && identical(entry.$1, route)) { - _modalRoutes.remove(node); - } - } - - // The Navigator can be mid-flush when the owning node disappears; removing - // the route must wait until it is idle or the route's overlay entries are - // stranded in the theater (and removeRoute asserts in debug builds). - void _scheduleRouteRemoval(Route route, NavigatorState navigator) { - scheduleMicrotask(() { - if (!route.isActive) { - _refreshSemanticsAfterModalRemoval(); - return; - } - if (route.isCurrent) { - // Pop runs the normal transition lifecycle — reverse animation, - // history flush, observer notifications — which removeRoute skips. - navigator.pop(); - } else { - navigator.removeRoute(route); - } - _refreshSemanticsAfterModalRemoval(route); - }); - } - - // The overlay's removed markers detach without dirtying their semantics - // ancestors; the a11y tree then freezes on the last emit (which still - // contains the sheet). Force the root to re-derive its semantics children - // so a post-removal update reaches the platform. - // - // The emit must happen after the route's overlay entries have left the - // theater: they stay mounted (and semantically attached) for the whole - // exit transition, and route.popped/whenComplete resolve at didPop — - // before the transition finishes. Wait until every entry is unmounted - // (or ~1.5s, whichever comes first), then mark every RenderView. The - // root pipeline owner has no rootNode on multi-view SDKs — render trees - // live in per-view child owners — so renderViews must be marked directly. - void _refreshSemanticsAfterModalRemoval([Route? route]) { - var attempts = 0; - void mark() { - for (final view in RendererBinding.instance.renderViews) { - view.markNeedsSemanticsUpdate(); - } - } - - // Chained frame callbacks rather than a Timer: marking a view requests - // a visual update, which schedules the frame the next callback runs - // on, so the loop keeps pace with the exit transition without pending - // timers outliving the widget tree. - void step([Duration? _]) { - final entries = route?.overlayEntries ?? const []; - mark(); - _scheduleLayoutSettle(); - _scheduleSemanticsHeal(); - if (entries.any((entry) => entry.mounted) && attempts < 30) { - attempts += 1; - SchedulerBinding.instance.addPostFrameCallback(step); - } - } - - // Mark once immediately so removals without a transition update right - // away, then re-mark once the entries are gone. - step(); - } - - Widget widget({required int node}) { - final extensionHandle = _extensionHandles[node]; - if (extensionHandle != null) { - return _FrameProbe( - key: nodeKey(node), - backend: this, - node: node, - child: ListenableBuilder( - listenable: extensionHandle, - builder: (context, _) => _buildExtensionNode(node), - ), - ); - } - final handle = _requireHandle(node); - return _FrameProbe( - key: nodeKey(node), - backend: this, - node: node, - child: ListenableBuilder( - listenable: handle, - // The Builder under _withTheme gives _buildNode a context that already - // carries this node's tokens, so a themed scope covers the node - // itself, not only its descendants. - builder: (context, _) => _withTheme( - context, - node, - Builder( - builder: (inner) => _withAppear(node, _buildNode(inner, node)), - ), - ), - ), - ); - } - - Widget _withTheme(BuildContext context, int node, Widget child) { - final state = _requireState(_states, node); - final tokens = _themeTokens(state.properties['theme']); - final mode = state.properties['theme-mode'] as String?; - if (tokens == null && (mode != 'light' && mode != 'dark')) { - return child; - } - Widget result = child; - if (mode == 'light' || mode == 'dark') { - result = Theme( - data: _themeForMode(context, dark: mode == 'dark'), - child: result, - ); - } - if (tokens != null) { - result = _LUIThemeScope( - tokens: {...?_LUIThemeScope.maybeTokens(context), ...tokens}, - child: result, - ); - } - return result; - } - - Widget _withAppear(int node, Widget child) { - final state = _requireState(_states, node); - if (state.properties['appear-enabled'] != true) return child; - return _LUIAppearDispatcher( - key: ValueKey('lui-appear-$node'), - node: node, - onAppear: () => onEvent?.call(LUIEvent.appear(node: node)), - child: child, - ); - } - - Widget _buildExtensionNode(int node) { - final state = _requireExtensionState(node); - final registration = _extensionRegistry._registration(state.identifier); - if (registration == null) { - throw LUIBackendException('unknown extension ${state.identifier}'); - } - return registration.builder(LUIFlutterExtensionContext._(node, this)); - } - - void performAction(int node) { - final state = _requireState(_states, node); - final treeItem = state.properties['role'] == 'treeitem'; - final pressable = - state.kind == _NodeKind.button || - state.kind == _NodeKind.select || - state.kind == _NodeKind.combobox || - state.kind == _NodeKind.menuItem || - state.kind == _NodeKind.listItem || - (state.kind == _NodeKind.bottomTab && - state.properties['press-enabled'] == true) || - (state.kind == _NodeKind.timelineItem && - state.properties['press-enabled'] == true) || - (treeItem && state.properties['press-enabled'] == true) || - (state.kind == _NodeKind.tableCell && - state.properties['press-enabled'] == true) || - (state.kind == _NodeKind.column && - state.properties['press-enabled'] == true) || - (state.kind == _NodeKind.text && - state.properties['press-enabled'] == true) || - (state.kind == _NodeKind.fileImage && - state.properties['press-enabled'] == true); - if (!pressable || state.properties['enabled'] == false) { - throw LUIBackendException( - 'node $node is not an enabled pressable control', - ); - } - onEvent?.call(LUIEvent.press(node: node)); - // GestureDetector's onTap exposes no hit position, so the press detail - // reports zero coordinates/modifiers — a real tap with fields this host - // cannot supply yet. - if (state.properties['pointer-enabled'] == true) { - onEvent?.call( - LUIEvent.pressDetail( - node: node, - detail: const LUIPointerDetail( - x: 0, - y: 0, - modifiers: 0, - button: 0, - targetClass: '', - ), - ), - ); - } - } - - void performExtensionEvent( - int node, { - required String name, - Map values = const {}, - }) { - final state = _requireExtensionState(node); - final registration = _extensionRegistry._registration(state.identifier); - if (registration == null) { - throw LUIBackendException('unknown extension ${state.identifier}'); - } - final event = registration.events - .where((candidate) => candidate.name == name) - .firstOrNull; - if (event == null) { - throw const LUIBackendException('unknown extension event'); - } - final fields = {for (final field in event.fields) field.name: field}; - if (!values.keys.every(fields.containsKey)) { - throw const LUIBackendException('unknown extension event field'); - } - for (final field in event.fields) { - final value = values[field.name]; - if (value == null) { - if (field.isRequired) { - throw const LUIBackendException( - 'missing required extension event field', - ); - } - } else if (!field.kind.accepts(value)) { - throw const LUIBackendException('invalid extension event field'); - } - } - final normalized = { - for (final entry in values.entries) - entry.key: fields[entry.key]!.kind.normalize(entry.value), - }; - onEvent?.call( - LUIEvent.extension( - node: node, - identifier: state.identifier, - name: name, - values: Map.unmodifiable(normalized), - ), - ); - } - - void _moveHorizontalFocus(int groupID, LogicalKeyboardKey key) { - final group = _requireState(_states, groupID); - final children = group.children - .where((childID) { - final child = _requireState(_states, childID); - return _isHorizontalGroupChild(group.kind, child.kind) && - child.properties['enabled'] != false; - }) - .toList(growable: false); - if (children.isEmpty) return; - - final current = children.indexWhere( - (childID) => _requireHandle(childID).focusNode.hasFocus, - ); - final targetIndex = switch (key) { - LogicalKeyboardKey.home => 0, - LogicalKeyboardKey.end => children.length - 1, - LogicalKeyboardKey.arrowLeft => - current < 0 ? 0 : (current + children.length - 1) % children.length, - _ => current < 0 ? 0 : (current + 1) % children.length, - }; - _requireHandle(children[targetIndex]).focusNode.requestFocus(); - } - - int? _treeAncestor(int node) { - var parent = _states[node]?.parent; - while (parent != null) { - final state = _states[parent]; - if (state == null) return null; - if (state.kind == _NodeKind.tree) return parent; - parent = state.parent; - } - return null; - } - - List _treeItems(int tree) { - final result = []; - void visit(int node) { - final state = _requireState(_states, node); - if (state.properties['role'] == 'treeitem' && - state.properties['enabled'] != false) { - result.add(node); - } - for (final child in state.children) { - visit(child); - } - } - - for (final child in _requireState(_states, tree).children) { - visit(child); - } - return result; - } - - int _treeLevel(int tree, int node) { - final explicit = _states[node]?.properties['tree-level'] as int?; - if (explicit != null) return explicit; - var level = 1; - var parent = _states[node]?.parent; - while (parent != null && parent != tree) { - if (_states[parent]?.properties['role'] == 'treeitem') level += 1; - parent = _states[parent]?.parent; - } - return level; - } - - int? _treeTabstop(int tree) { - final items = _treeItems(tree); - if (items.isEmpty) return null; - return items.firstWhere( - (node) => _states[node]?.properties['selected'] == true, - orElse: () => items.first, - ); - } - - void _selectTreeItem(int node) { - final state = _requireState(_states, node); - _requireHandle(node).focusNode.requestFocus(); - if (state.properties['change-enabled'] == true) { - performChange(node); - } else if (state.properties['press-enabled'] == true) { - performAction(node); - } - } - - void _performTreeTap(int node) { - final state = _requireState(_states, node); - if (state.properties['enabled'] == false) return; - if (state.properties['press-enabled'] == true) { - performAction(node); - } else if (state.properties['change-enabled'] == true) { - performChange(node); - } - if (state.properties['toggle-enabled'] == true) { - performToggle(node, state.properties['expanded'] != true); - } - } - - void _performTreeKey(int tree, int node, LogicalKeyboardKey key) { - final items = _treeItems(tree); - final index = items.indexOf(node); - if (index < 0) return; - if (key == LogicalKeyboardKey.arrowUp && index > 0) { - _selectTreeItem(items[index - 1]); - } else if (key == LogicalKeyboardKey.arrowDown && - index + 1 < items.length) { - _selectTreeItem(items[index + 1]); - } else if (key == LogicalKeyboardKey.home) { - _selectTreeItem(items.first); - } else if (key == LogicalKeyboardKey.end) { - _selectTreeItem(items.last); - } else if (key == LogicalKeyboardKey.arrowLeft) { - final state = _requireState(_states, node); - if (state.properties['expanded'] == true && - state.properties['toggle-enabled'] == true) { - performToggle(node, false); - return; - } - final level = _treeLevel(tree, node); - for (var candidate = index - 1; candidate >= 0; candidate -= 1) { - if (_treeLevel(tree, items[candidate]) == level - 1) { - _selectTreeItem(items[candidate]); - return; - } - } - } else if (key == LogicalKeyboardKey.arrowRight) { - final state = _requireState(_states, node); - if (state.properties['expanded'] == false && - state.properties['toggle-enabled'] == true) { - performToggle(node, true); - } else if (index + 1 < items.length && - _treeLevel(tree, items[index + 1]) == _treeLevel(tree, node) + 1) { - _selectTreeItem(items[index + 1]); - } - } - } - - void performDoublePress(int node) { - final state = _requireState(_states, node); - if (state.kind != _NodeKind.listItem || - state.properties['enabled'] == false || - state.properties['double-press-enabled'] != true) { - throw LUIBackendException( - 'node $node is not an enabled double-press control', - ); - } - onEvent?.call(LUIEvent.doublePress(node: node)); - } - - void performSubmit(int node) { - final state = _requireState(_states, node); - if (state.kind != _NodeKind.listItem || - state.properties['enabled'] == false || - state.properties['submit-enabled'] != true) { - throw LUIBackendException('node $node is not an enabled submit control'); - } - onEvent?.call(LUIEvent.submit(node: node)); - } - - void performLongPress(int node) { - final state = _requireState(_states, node); - if (!(_isButtonKind(state.kind) || state.kind == _NodeKind.listItem) || - state.properties['enabled'] == false || - state.properties['long-press-enabled'] != true) { - throw LUIBackendException('node $node is not enabled for long press'); - } - onEvent?.call(LUIEvent.longPress(node: node)); - } - - void performToggle(int node, bool checked) { - final state = _requireState(_states, node); - final treeItem = state.properties['role'] == 'treeitem'; - final isToggle = - state.kind == _NodeKind.toggleButton || - state.kind == _NodeKind.toggle || - state.kind == _NodeKind.accordion || - state.kind == _NodeKind.drawer || - treeItem; - final hasHandler = - (state.kind != _NodeKind.accordion && - state.kind != _NodeKind.drawer && - !treeItem) || - state.properties['toggle-enabled'] == true; - if (!isToggle || state.properties['enabled'] == false || !hasHandler) { - throw LUIBackendException('node $node is not an enabled toggle button'); - } - onEvent?.call(LUIEvent.toggleChanged(node: node, checked: checked)); - } - - void performChange(int node) { - final state = _requireState(_states, node); - if ((state.kind != _NodeKind.radio && - state.properties['role'] != 'treeitem') || - state.properties['enabled'] == false) { - throw LUIBackendException('node $node is not an enabled change control'); - } - if (state.properties['change-enabled'] == true) { - if (state.properties['checked'] != true) { - onEvent?.call(LUIEvent.change(node: node)); - } - } else if (state.properties['toggle-enabled'] == true) { - onEvent?.call(LUIEvent.toggleChanged(node: node, checked: true)); - } else if (state.properties['press-enabled'] == true) { - onEvent?.call(LUIEvent.press(node: node)); - } - } - - void performValueChange(int node, double value) { - final state = _requireState(_states, node); - if ((state.kind != _NodeKind.slider && - state.kind != _NodeKind.split && - state.kind != _NodeKind.numberStepper) || - state.properties['enabled'] == false || - !value.isFinite) { - throw LUIBackendException('node $node is not an enabled value control'); - } - var emitted = value.clamp(0.0, 1.0); - if (state.kind == _NodeKind.numberStepper) { - final minimum = - (state.properties['min'] as num?)?.toDouble() ?? 0.0; - final maximum = - (state.properties['max'] as num?)?.toDouble() ?? double.maxFinite; - emitted = value.clamp(minimum, maximum < minimum ? minimum : maximum); - } - onEvent?.call(LUIEvent.valueChanged(node: node, value: emitted.toDouble())); - } - - void performDismiss(int node) { - // Route completions can race the patch that already dropped the node; - // a stale dismiss is a no-op. - final state = _states[node]; - if (state == null) return; - if (state.kind != _NodeKind.select && - state.kind != _NodeKind.combobox && - state.kind != _NodeKind.dropdownMenu && - state.kind != _NodeKind.toast && - state.kind != _NodeKind.filePicker && - state.kind != _NodeKind.filePreview && - !state.kind.isModalSurface) { - throw LUIBackendException('node $node is not dismissible'); - } - onEvent?.call(LUIEvent.dismiss(node: node)); - } - - Widget _buildNode(BuildContext context, int id) { - final state = _requireState(_states, id); - final contextMenuID = state.children.cast().firstWhere( - (childID) => - childID != null && _states[childID]?.kind == _NodeKind.contextMenu, - orElse: () => null, - ); - final children = state.children - .where((child) => - child != contextMenuID && - _states[child]?.kind != _NodeKind.swipeActions) - .map((child) { - final childWidget = widget(node: child); - final childState = _states[child]; - if (state.kind == _NodeKind.row && - childState?.kind == _NodeKind.bubble && - childState?.properties['variant'] != 'ghost' && - childState?.properties.containsKey('width') == false) { - return Flexible(child: childWidget); - } - if (state.kind != _NodeKind.row && - state.kind != _NodeKind.column && - state.kind != _NodeKind.box && - state.kind != _NodeKind.list && - state.kind != _NodeKind.virtualList && - state.kind != _NodeKind.inputGroupActions) { - return childWidget; - } - final grow = childState?.properties['grow'] as num? ?? 0; - if (grow <= 0) return childWidget; - final scaled = (grow * 1000).round(); - return Expanded(flex: scaled < 1 ? 1 : scaled, child: childWidget); - }) - .toList(growable: false); - final enabled = state.properties['enabled'] as bool? ?? true; - final text = state.properties['text'] as String? ?? ''; - final placeholder = state.properties['placeholder'] as String?; - final accessibilityLabel = - state.properties['accessibility-label'] as String?; - final accessibilityIdentifier = - state.properties['accessibility-identifier'] as String?; - final checked = state.properties['checked'] as bool? ?? false; - final progressFraction = ((state.properties['value'] as double?) ?? 0) - .clamp(0.0, 1.0); - final orientation = - state.properties['orientation'] as String? ?? 'horizontal'; - final foreground = _color( - context, - state.properties['foreground'] as String?, - foreground: true, - ); - final background = _color( - context, - state.properties['background'] as String?, - ); - final borderColor = _color( - context, - state.properties['border-color'] as String?, - ); - final borderWidth = state.properties['border-width'] as int?; - final cornerRadius = state.properties['corner-radius'] as int?; - final gap = - (state.properties['gap'] as int? ?? - (_isHorizontalGroupKind(state.kind) - ? _horizontalGroupDefaultGap(state.kind) - : 0)) - .toDouble(); - final main = state.properties['main'] as String? ?? 'start'; - final cross = - state.properties['cross'] as String? ?? - (_isHorizontalGroupKind(state.kind) ? 'center' : 'stretch'); - final headingLevel = state.properties['heading-level'] as int? ?? 1; - final spinnerExtent = switch (state.properties['size'] as String? ?? - 'default') { - 'sm' => 16.0, - 'lg' => 24.0, - _ => 20.0, - }; - final iconExtent = switch (state.properties['size'] as String? ?? - 'default') { - 'sm' => 16.0, - 'lg' => 24.0, - 'icon' => 24.0, - _ => 18.0, - }; - final buttonVariant = state.properties['variant'] as String? ?? 'default'; - final buttonSize = state.properties['size'] as String? ?? 'default'; - final buttonIcon = state.properties['icon'] as String?; - final buttonIconPlacement = - state.properties['icon-placement'] as String? ?? 'leading'; - final buttonSelected = state.properties['selected'] as bool? ?? false; - final buttonAutofocus = state.properties['autofocus'] as bool? ?? false; - final longPressEnabled = - state.properties['long-press-enabled'] as bool? ?? false; - final isTabTrigger = - state.kind == _NodeKind.button && - state.parent != null && - _states[state.parent]?.kind == _NodeKind.tabs; - Widget numberStepperView(_NodeState stepperState, bool stepperEnabled, int id) { - final step = - (stepperState.properties['step'] as num?)?.toDouble() ?? 1.0; - final current = - (stepperState.properties['value'] as num?)?.toDouble() ?? 0.0; - final text = stepperState.properties['text'] as String? ?? ''; - final a11yLabel = - stepperState.properties['accessibility-label'] as String? ?? text; - return Semantics( - label: a11yLabel, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Flexible(child: Text(text)), - IconButton( - icon: const Icon(Icons.remove), - tooltip: 'Decrease', - onPressed: stepperEnabled - ? () => performValueChange(id, current - step) - : null, - ), - IconButton( - icon: const Icon(Icons.add), - tooltip: 'Increase', - onPressed: stepperEnabled - ? () => performValueChange(id, current + step) - : null, - ), - ], - ), - ); - } - - Widget textControl({required _NodeKind kind}) { - final multiline = kind == _NodeKind.textarea; - final combobox = kind == _NodeKind.combobox; - final grouped = - state.parent != null && - _states[state.parent]?.kind == _NodeKind.inputGroup; - return SizedBox( - width: grouped ? double.infinity : 240, - child: Semantics( - label: accessibilityLabel, - textField: true, - readOnly: false, - multiline: multiline, - child: _LUITextInput( - text: text, - enabled: enabled, - placeholder: placeholder, - foreground: foreground, - autofocus: state.properties['autofocus'] as bool? ?? false, - focusNode: _requireHandle(id).focusNode, - multiline: multiline, - secure: kind == _NodeKind.secureField, - search: kind == _NodeKind.searchField, - grouped: grouped, - onOpen: combobox && enabled ? () => performAction(id) : null, - submitOnEnter: - state.properties['submit-on-enter'] as bool? ?? false, - onChanged: (value) => - onEvent?.call(LUIEvent.textChanged(node: id, text: value)), - onSubmitted: (_) => state.properties['submit-enabled'] == true - ? onEvent?.call(LUIEvent.submit(node: id)) - : combobox - ? performAction(id) - : onEvent?.call(LUIEvent.submit(node: id)), - ), - ), - ); - } - - Widget toggleSemantics({required bool asSwitch, required Widget child}) { - final label = accessibilityLabel?.isNotEmpty ?? false - ? accessibilityLabel - : (text.isEmpty ? null : text); - return Semantics( - label: label, - enabled: enabled, - checked: asSwitch ? null : checked, - toggled: asSwitch ? checked : null, - onTap: enabled - ? () => onEvent?.call( - LUIEvent.toggleChanged(node: id, checked: !checked), - ) - : null, - excludeSemantics: true, - child: child, - ); - } - - Widget buttonLabel() { - final icon = buttonIcon == null - ? null - : Icon(_iconData(buttonIcon), size: iconExtent); - if (text.isEmpty) return icon ?? const SizedBox.shrink(); - if (icon == null) return Text(text); - return Row( - mainAxisSize: MainAxisSize.min, - spacing: 8, - children: buttonIconPlacement == 'trailing' - ? [Text(text), icon] - : [icon, Text(text)], - ); - } - - ButtonStyle buttonStyle({Color? backgroundColor}) { - final dimensions = switch (buttonSize) { - 'sm' => (const Size(0, 36), const EdgeInsets.symmetric(horizontal: 12)), - 'lg' => (const Size(0, 44), const EdgeInsets.symmetric(horizontal: 32)), - 'icon' => (const Size.square(48), EdgeInsets.zero), - _ => (const Size(0, 40), const EdgeInsets.symmetric(horizontal: 16)), - }; - return ButtonStyle( - minimumSize: WidgetStatePropertyAll(dimensions.$1), - padding: WidgetStatePropertyAll(dimensions.$2), - foregroundColor: foreground == null - ? null - : WidgetStatePropertyAll(foreground), - backgroundColor: backgroundColor == null - ? null - : WidgetStatePropertyAll(backgroundColor), - ); - } - - Widget button({ - required bool selected, - required VoidCallback? onPressed, - Key? semanticsKey, - }) { - final onLongPress = enabled && longPressEnabled - ? () => performLongPress(id) - : null; - final colors = Theme.of(context).colorScheme; - final selectedColor = selected ? colors.secondaryContainer : null; - final style = buttonStyle(backgroundColor: selectedColor); - final label = buttonLabel(); - final tabStyle = ButtonStyle( - minimumSize: const WidgetStatePropertyAll(Size(0, 32)), - padding: const WidgetStatePropertyAll( - EdgeInsets.symmetric(horizontal: 12), - ), - foregroundColor: WidgetStatePropertyAll(foreground ?? colors.onSurface), - backgroundColor: WidgetStatePropertyAll( - selected ? colors.surface : Colors.transparent, - ), - elevation: WidgetStatePropertyAll(selected ? 1 : 0), - shape: WidgetStatePropertyAll( - RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)), - ), - ); - final materialButton = isTabTrigger - ? TextButton( - onPressed: onPressed, - onLongPress: onLongPress, - autofocus: buttonAutofocus, - focusNode: _requireHandle(id).focusNode, - style: tabStyle, - child: label, - ) - : switch (buttonVariant) { - 'primary' => FilledButton( - onPressed: onPressed, - onLongPress: onLongPress, - autofocus: buttonAutofocus, - focusNode: _requireHandle(id).focusNode, - style: style, - child: label, - ), - 'secondary' => FilledButton.tonal( - onPressed: onPressed, - onLongPress: onLongPress, - autofocus: buttonAutofocus, - focusNode: _requireHandle(id).focusNode, - style: style, - child: label, - ), - 'outline' => OutlinedButton( - onPressed: onPressed, - onLongPress: onLongPress, - autofocus: buttonAutofocus, - focusNode: _requireHandle(id).focusNode, - style: style, - child: label, - ), - 'destructive' => FilledButton( - onPressed: onPressed, - onLongPress: onLongPress, - autofocus: buttonAutofocus, - focusNode: _requireHandle(id).focusNode, - style: buttonStyle(backgroundColor: colors.error), - child: label, - ), - _ => TextButton( - onPressed: onPressed, - onLongPress: onLongPress, - autofocus: buttonAutofocus, - focusNode: _requireHandle(id).focusNode, - style: style, - child: label, - ), - }; - return Semantics( - key: semanticsKey ?? (isTabTrigger ? ValueKey('lui-tab-$id') : null), - label: accessibilityLabel?.isNotEmpty ?? false - ? accessibilityLabel - : (text.isEmpty ? null : text), - button: true, - selected: selected, - enabled: enabled, - onTap: onPressed, - onLongPress: onLongPress, - excludeSemantics: true, - child: materialButton, - ); - } - - Widget toggleButton() => _LUIToggleSelection( - modelSelected: state.properties['selected'] as bool?, - builder: (context, selected, setSelected) => button( - selected: selected, - semanticsKey: ValueKey('lui-toggle-$id'), - onPressed: enabled - ? () { - final next = !selected; - setSelected(next); - performToggle(id, next); - } - : null, - ), - ); - - Widget grid() { - final requested = state.properties['columns'] as int? ?? 0; - final columns = requested > 0 - ? requested - : (children.isEmpty ? 1 : children.length); - return Wrap( - spacing: gap, - runSpacing: gap, - children: [ - for (final child in children) - _LUIGridCell(columns: columns, gap: gap, child: child), - ], - ); - } - - // RenderFlex requires a bounded cross axis for - // CrossAxisAlignment.stretch; _RenderLUIFlex.downgrades stretch to - // `start` when the cross axis is unbounded at layout time. - Widget row() => LUIFlex( - direction: Axis.horizontal, - mainAxisAlignment: _mainAxisAlignment(main), - crossAxisAlignment: _crossAxisAlignment(cross, canStretch: true), - spacing: gap, - children: children, - ); - - // `scroll-leading` pins the last child at the trailing edge while the - // rest scroll, matching the Apple backend's LUIToolbarLayoutPolicy. - Widget horizontalToolbar(List children, double gap) { - final pinTrailing = - (state.properties['style-class'] as String? ?? '') - .split(' ') - .contains('scroll-leading') && - children.length >= 2; - if (!pinTrailing) { - return SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - mainAxisSize: MainAxisSize.min, - spacing: gap, - children: children, - ), - ); - } - return Row( - spacing: gap, - children: [ - Expanded( - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - mainAxisSize: MainAxisSize.min, - spacing: gap, - children: children.sublist(0, children.length - 1), - ), - ), - ), - children.last, - ], - ); - } - - Widget horizontalGroupFlex() => LUIFlex( - direction: Axis.horizontal, - expandsForAlignment: state.properties.containsKey('main'), - mainAxisAlignment: _mainAxisAlignment(main), - crossAxisAlignment: _crossAxisAlignment(cross, canStretch: true), - spacing: gap, - children: children, - ); - - Widget horizontalGroup() => CallbackShortcuts( - bindings: { - const SingleActivator(LogicalKeyboardKey.arrowLeft): () => - _moveHorizontalFocus(id, LogicalKeyboardKey.arrowLeft), - const SingleActivator(LogicalKeyboardKey.arrowRight): () => - _moveHorizontalFocus(id, LogicalKeyboardKey.arrowRight), - const SingleActivator(LogicalKeyboardKey.home): () => - _moveHorizontalFocus(id, LogicalKeyboardKey.home), - const SingleActivator(LogicalKeyboardKey.end): () => - _moveHorizontalFocus(id, LogicalKeyboardKey.end), - }, - child: Semantics( - container: true, - label: accessibilityLabel, - child: horizontalGroupFlex(), - ), - ); - Widget column() { - final body = LUIFlex( - direction: Axis.vertical, - mainAxisAlignment: _mainAxisAlignment(main), - crossAxisAlignment: _crossAxisAlignment(cross, canStretch: true), - spacing: gap, - children: children, - ); - if (state.properties['press-enabled'] == true) { - return GestureDetector(onTap: () => performAction(id), child: body); - } - return body; - } - - Widget stack() { - final menuID = state.children.cast().firstWhere( - (childID) => - childID != null && _states[childID]?.kind == _NodeKind.dropdownMenu, - orElse: () => null, - ); - final tooltipID = state.children.cast().firstWhere( - (childID) => - childID != null && - _states[childID]?.kind == _NodeKind.tooltip && - _states[childID]?.properties.containsKey('anchor') == true, - orElse: () => null, - ); - final menuState = menuID == null ? null : _requireState(_states, menuID); - final tooltipState = tooltipID == null - ? null - : _requireState(_states, tooltipID); - Widget trigger = LUIStack( - clipBehavior: Clip.none, - children: state.children - .where((childID) => childID != menuID && childID != tooltipID) - .map((childID) { - final childWidget = widget(node: childID); - final grow = _states[childID]?.properties['grow'] as num? ?? 0; - // A growing stack child fills the stack's height, matching - // the Apple backend's maxHeight: .infinity + - // layoutPriority(1). _RenderLUIStack only fills under - // bounded height, like the flex gate. - return _LUIStackGrowFill(fill: grow > 0, child: childWidget); - }) - .toList(growable: false), - ); - if (tooltipState != null) { - trigger = _LUIRetainedTooltip( - session: _tooltipSession, - message: tooltipState.properties['text'] as String? ?? '', - waitDuration: Duration( - milliseconds: - tooltipState.properties['tooltip-delay'] as int? ?? 600, - ), - exitDuration: const Duration(milliseconds: 400), - preferBelow: tooltipState.properties['anchor'] != 'above', - verticalOffset: - (tooltipState.properties['anchor-offset'] as num?)?.toDouble() ?? - 4, - enableTapToDismiss: true, - child: trigger, - ); - } - return _LUIAnchoredStack( - anchor: menuState?.properties['anchor'] as String? ?? 'below', - alignment: - menuState?.properties['anchor-alignment'] as String? ?? 'start', - offset: - (menuState?.properties['anchor-offset'] as num?)?.toDouble() ?? 0, - menuID: menuID, - menuChildren: - menuState?.children - .map((childID) => widget(node: childID)) - .toList(growable: false) ?? - const [], - minimumWidth: (menuState?.properties['min-width'] as num?)?.toDouble(), - maximumWidth: (menuState?.properties['max-width'] as num?)?.toDouble(), - onDismiss: menuID == null ? null : () => performDismiss(menuID), - trigger: trigger, - ); - } - - /// Pins the children after the first to `edge` while the first child - /// fills the view beneath them. Pinned children group into a row across - /// side edges and a column under top/bottom; `gap` insets the group - /// from the pinned edge. - Widget edgeInset() { - final visible = state.properties['visible'] as bool? ?? true; - final gap = (state.properties['gap'] as num?)?.toDouble() ?? 0; - final edge = state.properties['edge'] as String? ?? 'top'; - final sideEdge = edge == 'leading' || edge == 'trailing'; - final pinned = children.skip(1).toList(growable: false); - return Stack( - children: [ - if (children.isNotEmpty) children.first, - if (visible && pinned.isNotEmpty) - Positioned.fill( - child: Padding( - padding: switch (edge) { - 'bottom' => EdgeInsetsDirectional.only(bottom: gap), - 'leading' => EdgeInsetsDirectional.only(start: gap), - 'trailing' => EdgeInsetsDirectional.only(end: gap), - _ => EdgeInsetsDirectional.only(top: gap), - }, - child: Align( - alignment: switch (edge) { - 'bottom' => AlignmentDirectional.bottomCenter, - 'leading' => AlignmentDirectional.centerStart, - 'trailing' => AlignmentDirectional.centerEnd, - _ => AlignmentDirectional.topCenter, - }, - child: sideEdge - ? Row(mainAxisSize: MainAxisSize.min, children: pinned) - : Column(mainAxisSize: MainAxisSize.min, children: pinned), - ), - ), - ), - ], - ); - } - - /// Renders the children after the first floating over the first child - /// without affecting its layout; each overlay child positions by its own - /// 'alignment' prop, falling back to the overlay's (default center). - Widget overlay() { - final fallback = - _overlayAlignment(state.properties['alignment'] as String?) ?? - AlignmentDirectional.center; - // `children` skips the context-menu child, so index the visible ids. - final overlayIDs = state.children - .where((child) => child != contextMenuID) - .toList(growable: false); - return Stack( - children: [ - if (children.isNotEmpty) children.first, - for (var index = 1; index < children.length; index += 1) - Positioned.fill( - child: Align( - alignment: - _overlayAlignment( - _states[overlayIDs[index]]?.properties['alignment'] - as String?, - ) ?? - fallback, - child: children[index], - ), - ), - ], - ); - } - - /// No native ViewThatFits equivalent; render the first candidate. - Widget viewThatFits() => - children.isEmpty ? const SizedBox.shrink() : children.first; - - Widget select() { - final colors = Theme.of(context).colorScheme; - final title = accessibilityLabel ?? placeholder ?? ''; - final value = text.isEmpty ? placeholder ?? '' : text; - return Semantics( - label: accessibilityLabel, - child: ListTile( - enabled: enabled, - onTap: enabled ? () => performAction(id) : null, - title: Text(title.isEmpty ? value : title), - subtitle: title.isNotEmpty && value != title && value.isNotEmpty - ? Text(value) - : null, - trailing: Icon( - Icons.expand_more_rounded, - color: colors.onSurfaceVariant, - ), - ), - ); - } - - Widget dropdownMenu() => CallbackShortcuts( - bindings: { - const SingleActivator(LogicalKeyboardKey.escape): () => - performDismiss(id), - }, - child: Focus( - autofocus: true, - child: TapRegion( - groupId: state.parent, - onTapOutside: (_) => performDismiss(id), - child: Material( - elevation: 8, - borderRadius: BorderRadius.circular(8), - clipBehavior: Clip.antiAlias, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - spacing: gap, - children: children, - ), - ), - ), - ), - ); - Widget menuItem() { - final submenuID = state.children.cast().firstWhere( - (childID) => - childID != null && _states[childID]?.kind == _NodeKind.dropdownMenu, - orElse: () => null, - ); - final menuItemIconExtent = switch (state.properties['size'] as String? ?? - 'default') { - 'sm' => 16.0, - 'lg' => 28.0, - _ => 16.0, - }; - final leadingIcon = buttonIcon == null - ? null - : Icon(_iconData(buttonIcon), size: menuItemIconExtent); - if (submenuID != null) { - final submenu = _requireState(_states, submenuID); - return SubmenuButton( - useRootOverlay: true, - animated: true, - leadingIcon: leadingIcon, - menuChildren: enabled - ? submenu.children - .map((childID) => widget(node: childID)) - .toList(growable: false) - : const [], - child: Text(text), - ); - } - final destructive = buttonVariant == 'destructive'; - final menuItemText = Text( - text, - style: destructive - ? TextStyle(color: Theme.of(context).colorScheme.error) - : null, - ); - return MergeSemantics( - child: Semantics( - selected: buttonSelected, - child: MenuItemButton( - onPressed: enabled ? () => performAction(id) : null, - leadingIcon: leadingIcon, - trailingIcon: buttonSelected ? const Icon(Icons.check) : null, - child: menuItemText, - ), - ), - ); - } - - Widget fileImage() { - final path = state.properties['path'] as String? ?? ''; - final resolved = filePathResolver?.call(path) ?? path; - final fit = state.properties['image-fit'] == 'fill' - ? BoxFit.cover - : BoxFit.contain; - const fallback = Center( - child: Icon(Icons.photo_outlined, size: 24), - ); - // Matches LUIFileImageLoader on Apple: decode at no more than - // max-pixel-size on the longest side (default 1024) so displaying a - // small image never rasterizes the file at native resolution. - final maxPixelSize = state.properties['max-pixel-size'] as int? ?? 1024; - Widget image = resolved.isEmpty - ? fallback - : Image.file( - File(resolved), - fit: fit, - width: double.infinity, - height: double.infinity, - semanticLabel: accessibilityLabel, - cacheWidth: maxPixelSize, - cacheHeight: maxPixelSize, - errorBuilder: (_, _, _) => fallback, - ); - final radius = (cornerRadius ?? 0).toDouble(); - if (radius > 0) { - image = ClipRRect( - borderRadius: BorderRadius.circular(radius), - child: image, - ); - } - return image; - } - - Widget menuTrigger() { - final menuID = state.children.cast().firstWhere( - (childID) => - childID != null && _states[childID]?.kind == _NodeKind.dropdownMenu, - orElse: () => null, - ); - final menuState = menuID == null ? null : _requireState(_states, menuID); - // Icon-only triggers act like icon buttons and keep the platform's - // standard 24dp glyph; labeled triggers use the compact 16dp glyph. - final iconExtent = text.isEmpty ? 24.0 : 16.0; - final icon = buttonIcon == null - ? null - : Icon(_iconData(buttonIcon), size: iconExtent, color: foreground); - final label = icon == null - ? Text(text, style: TextStyle(color: foreground)) - : text.isEmpty - ? icon - : Row( - mainAxisSize: MainAxisSize.min, - spacing: 4, - children: [ - icon, - Text(text, style: TextStyle(color: foreground)), - ], - ); - final accessibilityText = accessibilityLabel ?? text; - final triggerLabel = text.isEmpty && accessibilityText.isNotEmpty - ? Tooltip(message: accessibilityText, child: label) - : label; - return MenuAnchor( - builder: (context, controller, child) => Semantics( - label: accessibilityText, - button: true, - child: TextButton( - onPressed: enabled - ? () => - controller.isOpen ? controller.close() : controller.open() - : null, - child: triggerLabel, - ), - ), - menuChildren: - menuState?.children - .map((childID) => widget(node: childID)) - .toList(growable: false) ?? - const [], - ); - } - - Widget listItem() => _LUIListItem( - enabled: enabled, - focusable: state.properties['role'] != 'treeitem', - selected: buttonSelected, - leading: buttonIcon == null - ? null - : Icon(_iconData(buttonIcon), size: 16), - content: children.isEmpty - ? Text(text) - : text.isEmpty - ? (children.length == 1 ? children.single : Row(children: children)) - : Row( - children: [ - Flexible(child: Text(text, overflow: TextOverflow.ellipsis)), - ...children, - ], - ), - onPress: - state.properties['role'] == 'treeitem' && - (state.properties['press-enabled'] == true || - state.properties['change-enabled'] == true || - state.properties['toggle-enabled'] == true) - ? () => _performTreeTap(id) - : state.properties['press-enabled'] == true - ? () => performAction(id) - : null, - onDoublePress: state.properties['double-press-enabled'] == true - ? () => performDoublePress(id) - : null, - onLongPress: longPressEnabled ? () => performLongPress(id) : null, - onSubmit: state.properties['submit-enabled'] == true - ? () => performSubmit(id) - : null, - ); - Widget table() => Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: children, - ); - Widget tree() => FocusTraversalGroup( - child: Semantics( - container: true, - label: accessibilityLabel, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - spacing: gap, - children: children, - ), - ), - ); - Widget tableRow() { - final columnWidths = {}; - for (var index = 0; index < state.children.length; index += 1) { - final cell = _requireState(_states, state.children[index]); - final grow = (cell.properties['grow'] as num?)?.toDouble() ?? 0; - columnWidths[index] = grow > 0 - ? FlexColumnWidth(grow) - : const IntrinsicColumnWidth(); - } - final paddedCells = children - .map( - (child) => Padding( - padding: EdgeInsets.symmetric(horizontal: gap / 2), - child: child, - ), - ) - .toList(growable: false); - final parent = state.parent == null ? null : _states[state.parent]; - final isLast = parent == null || parent.children.last == id; - return Semantics( - container: true, - selected: buttonSelected, - child: _LUITableRowSurface( - selected: buttonSelected, - showDivider: !isLast, - child: Table( - defaultVerticalAlignment: TableCellVerticalAlignment.middle, - columnWidths: columnWidths, - children: [TableRow(children: paddedCells)], - ), - ), - ); - } - - Widget tableCell() { - final alignment = switch (state.properties['text-alignment']) { - 'center' => Alignment.center, - 'end' => Alignment.centerRight, - _ => Alignment.centerLeft, - }; - final textAlign = switch (state.properties['text-alignment']) { - 'center' => TextAlign.center, - 'end' => TextAlign.end, - _ => TextAlign.start, - }; - final label = Align( - alignment: alignment, - child: Text( - text, - textAlign: textAlign, - style: _textStyleForSize( - context, - state.properties['size'] as String?, - )?.copyWith(color: foreground), - ), - ); - if (state.properties['press-enabled'] != true) return label; - return Semantics( - button: true, - child: InkWell( - onTap: () => performAction(id), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 12), - child: label, - ), - ), - ); - } - - Widget accordion() => _LUIAccordion( - expanded: buttonSelected, - title: text, - onToggle: state.properties['toggle-enabled'] == true - ? (value) => performToggle(id, value) - : null, - children: children, - ); - Widget alert() => Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - spacing: 8, - children: [ - if (text.isNotEmpty) - Text(text, style: Theme.of(context).textTheme.titleSmall), - Stack(children: children), - ], - ); - Widget bubble() { - final reactionAlignment = switch (state.properties['text-alignment']) { - 'start' => Alignment.centerLeft, - 'center' => Alignment.center, - _ => Alignment.centerRight, - }; - return IntrinsicWidth( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Stack(children: children), - if (text.isNotEmpty) - Align( - alignment: reactionAlignment, - child: Chip( - label: Text(text), - labelStyle: Theme.of(context).textTheme.labelSmall, - visualDensity: VisualDensity.compact, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - ), - ), - ], - ), - ); - } - - Widget statusBar() => Align( - alignment: switch (state.properties['text-alignment']) { - 'center' => Alignment.center, - 'end' => Alignment.centerRight, - _ => Alignment.centerLeft, - }, - child: Text( - text, - textAlign: switch (state.properties['text-alignment']) { - 'center' => TextAlign.center, - 'end' => TextAlign.end, - _ => TextAlign.start, - }, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: foreground ?? Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - ); - Widget textNode() { - final label = Text( - text, - textAlign: switch (state.properties['text-alignment']) { - 'center' => TextAlign.center, - 'end' => TextAlign.end, - _ => TextAlign.start, - }, - style: _textStyleForSize( - context, - state.properties['size'] as String?, - )?.copyWith(color: foreground), - ); - if (state.properties['press-enabled'] != true) return label; - return TextButton( - onPressed: () => performAction(id), - focusNode: _requireHandle(id).focusNode, - style: TextButton.styleFrom( - foregroundColor: foreground, - minimumSize: Size.zero, - padding: EdgeInsets.zero, - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - ), - child: label, - ); - } - - Widget avatar() { - final imageID = state.properties['image'] as int? ?? 0; - final image = imageID == 0 ? null : _images[imageID]; - final sourceX = (state.properties['source-x'] as num?)?.toDouble(); - final sourceY = (state.properties['source-y'] as num?)?.toDouble(); - final sourceWidth = (state.properties['source-width'] as num?) - ?.toDouble(); - final sourceHeight = (state.properties['source-height'] as num?) - ?.toDouble(); - final source = sourceX == null - ? null - : Rect.fromLTWH(sourceX, sourceY!, sourceWidth!, sourceHeight!); - final imageBounds = image == null - ? null - : Rect.fromLTWH( - 0, - 0, - image.width.toDouble(), - image.height.toDouble(), - ); - final displayImage = - source == null || - imageBounds == null || - (source.left >= imageBounds.left && - source.top >= imageBounds.top && - source.right <= imageBounds.right && - source.bottom <= imageBounds.bottom) - ? image - : null; - return Semantics( - label: accessibilityLabel ?? text, - image: true, - excludeSemantics: true, - child: ClipOval( - child: SizedBox.square( - dimension: 40, - child: displayImage == null - ? ColoredBox( - color: Theme.of(context).colorScheme.secondaryContainer, - child: Center( - child: text.isEmpty - ? const Icon(Icons.person) - : Text( - text, - style: Theme.of(context).textTheme.labelLarge, - ), - ), - ) - : CustomPaint( - painter: _LUIAvatarPainter( - image: displayImage, - source: source, - ), - ), - ), - ), - ); - } - - Rect? imageSource(ui.Image? image) { - if (image == null || !state.properties.containsKey('source-x')) { - return null; - } - final requested = Rect.fromLTWH( - (state.properties['source-x'] as num).toDouble(), - (state.properties['source-y'] as num).toDouble(), - (state.properties['source-width'] as num).toDouble(), - (state.properties['source-height'] as num).toDouble(), - ); - final bounds = Rect.fromLTWH( - 0, - 0, - image.width.toDouble(), - image.height.toDouble(), - ); - final clipped = requested.intersect(bounds); - return clipped.isEmpty ? null : clipped; - } - - Widget image() { - final imageID = state.properties['image'] as int? ?? 0; - final pixels = imageID == 0 ? null : _images[imageID]; - final source = imageSource(pixels); - final hasDrawablePixels = - pixels != null && - (!state.properties.containsKey('source-x') || source != null); - final url = state.properties['url'] as String? ?? ''; - final altText = state.properties['alt'] as String? ?? ''; - final semanticsLabel = altText.isEmpty ? accessibilityLabel : altText; - return Semantics( - label: semanticsLabel, - image: semanticsLabel != null, - excludeSemantics: true, - child: !hasDrawablePixels - ? url.isEmpty - ? const SizedBox.expand() - : Image.network( - url, - fit: BoxFit.fill, - errorBuilder: (_, __, ___) => const SizedBox.expand(), - ) - : CustomPaint( - painter: _LUIImagePainter(image: pixels, source: source), - ), - ); - } - - Widget mediaSurface() { - final surfaceID = state.properties['surface'] as int; - final frame = surfaceID == 0 ? null : _mediaSurfaces[surfaceID]; - return Semantics( - label: accessibilityLabel, - image: accessibilityLabel != null, - excludeSemantics: true, - child: frame == null - ? surfaceID == 0 - ? const SizedBox.expand() - : ColoredBox(color: _mediaSurfacePlaceholderColor(surfaceID)) - : RawImage(image: frame, fit: BoxFit.fill), - ); - } - - Widget step() { - final parent = state.parent == null ? null : _states[state.parent]; - final index = parent?.children.indexOf(id) ?? 0; - final count = parent?.children.length ?? 1; - final active = parent?.properties['active'] as int? ?? 0; - final stepState = index < active - ? 'completed' - : index == active - ? 'active' - : 'pending'; - final activeColor = Theme.of(context).colorScheme.primary; - return Semantics( - label: '$text ($stepState)', - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - width: 24, - height: 24, - alignment: Alignment.center, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: stepState == 'pending' ? null : activeColor, - border: Border.all( - color: stepState == 'pending' - ? Theme.of(context).colorScheme.outline - : activeColor, - ), - ), - child: stepState == 'completed' - ? Icon( - Icons.check, - size: 14, - color: Theme.of(context).colorScheme.onPrimary, - ) - : Text( - '${index + 1}', - style: Theme.of(context).textTheme.labelSmall?.copyWith( - color: stepState == 'active' - ? Theme.of(context).colorScheme.onPrimary - : Theme.of(context).colorScheme.onSurfaceVariant, - fontWeight: FontWeight.w600, - ), - ), - ), - const SizedBox(width: 6), - Text( - text, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: stepState == 'pending' - ? Theme.of(context).colorScheme.onSurfaceVariant - : null, - fontWeight: stepState == 'active' - ? FontWeight.w600 - : FontWeight.normal, - ), - ), - if (index + 1 < count) ...[ - const SizedBox(width: 8), - Container( - width: 24, - height: 1, - color: Theme.of(context).colorScheme.outlineVariant, - ), - const SizedBox(width: 8), - ], - ], - ), - ); - } - - Widget timelineItem() { - final title = state.properties['title'] as String? ?? ''; - final description = state.properties['description'] as String? ?? ''; - final meta = state.properties['meta'] as String? ?? ''; - final indicator = state.properties['indicator'] as String? ?? ''; - final icon = state.properties['icon'] as String? ?? ''; - final connector = state.properties['connector'] as bool? ?? true; - final pressable = state.properties['press-enabled'] == true; - final variantColor = switch (buttonVariant) { - 'primary' => Theme.of(context).colorScheme.primary, - 'destructive' => Theme.of(context).colorScheme.error, - _ => Theme.of(context).colorScheme.outline, - }; - Widget marker; - if (icon.isNotEmpty) { - marker = Icon(_iconData(icon), size: 16, color: variantColor); - } else if (indicator.isNotEmpty) { - marker = Text( - indicator, - style: Theme.of(context).textTheme.labelSmall - ?.copyWith(color: variantColor, fontWeight: FontWeight.w600), - ); - } else { - marker = Container( - width: 10, - height: 10, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: variantColor, - ), - ); - } - final row = Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: buttonSelected - ? Theme.of(context).colorScheme.secondaryContainer - : null, - borderRadius: BorderRadius.circular(8), - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: 24, - child: Column( - children: [ - SizedBox(width: 24, height: 24, child: Center(child: marker)), - if (connector) - Container( - width: 1, - height: 28, - color: Theme.of(context).colorScheme.outlineVariant, - ), - ], - ), - ), - const SizedBox(width: 10), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - style: const TextStyle(fontWeight: FontWeight.w600), - ), - if (description.isNotEmpty) - Text( - description, - style: TextStyle( - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - if (meta.isNotEmpty) - Text( - meta, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - ], - ), - ), - if (pressable) - Icon( - Icons.chevron_right, - size: 18, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ], - ), - ); - return Semantics( - label: title, - selected: buttonSelected, - button: pressable, - child: pressable - ? GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () => performAction(id), - child: row, - ) - : row, - ); - } - - Widget inputGroup() { - final editor = children.first; - final editorBody = state.properties.containsKey('height') - ? Expanded(child: editor) - : editor; - return Semantics( - container: true, - label: accessibilityLabel, - child: _LUIInputGroupSurface( - child: Column( - mainAxisSize: state.properties.containsKey('height') - ? MainAxisSize.max - : MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [editorBody, if (children.length == 2) children[1]], - ), - ), - ); - } - - Widget inputGroupActions() => Padding( - padding: const EdgeInsets.fromLTRB(8, 4, 8, 8), - child: LUIFlex( - direction: Axis.horizontal, - crossAxisAlignment: CrossAxisAlignment.center, - spacing: gap, - children: children, - ), - ); - - Widget bottomTabs() { - final destinationIDs = state.children; - final selectedIndex = destinationIDs.indexWhere( - (nodeID) => - _requireState(_states, nodeID).properties['selected'] == true, - ); - final currentIndex = selectedIndex < 0 ? 0 : selectedIndex; - return Semantics( - container: true, - label: accessibilityLabel, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Expanded(child: widget(node: destinationIDs[currentIndex])), - NavigationBar( - selectedIndex: currentIndex, - onDestinationSelected: (index) { - final destinationID = destinationIDs[index]; - final destination = _requireState(_states, destinationID); - if (destination.properties['enabled'] != false && - destination.properties['press-enabled'] == true) { - performAction(destinationID); - } - }, - destinations: destinationIDs - .map( - (nodeID) => NavigationDestination( - icon: Icon( - _iconData( - _requireState(_states, nodeID).properties['icon'] - as String? ?? - '', - ), - ), - label: - _requireState(_states, nodeID).properties['title'] - as String? ?? - '', - enabled: - _requireState( - _states, - nodeID, - ).properties['enabled'] != - false, - ), - ) - .toList(growable: false), - ), - ], - ), - ); - } - - final content = switch (state.kind) { - _NodeKind.root => children.single, - _NodeKind.row => row(), - _NodeKind.tabs || - _NodeKind.buttonGroup || - _NodeKind.toggleGroup || - _NodeKind.breadcrumb || - _NodeKind.pagination => horizontalGroup(), - _NodeKind.bottomTabs => bottomTabs(), - _NodeKind.bottomTab => Stack(children: children), - _NodeKind.column || _NodeKind.list => column(), - _NodeKind.virtualList => ListView.separated( - itemCount: state.children.length, - itemBuilder: (context, index) => widget(node: state.children[index]), - separatorBuilder: (context, index) => SizedBox(height: gap), - ), - _NodeKind.grid => grid(), - _NodeKind.stack => stack(), - _NodeKind.edgeInset => edgeInset(), - _NodeKind.overlay => overlay(), - _NodeKind.viewThatFits => viewThatFits(), - _NodeKind.panel || - _NodeKind.card || - _NodeKind.resizable => Stack(children: children), - _NodeKind.alert => alert(), - _NodeKind.bubble => bubble(), - _NodeKind.drawer => _LUIDrawer( - backend: this, - sourcePresented: state.properties['selected'] as bool? ?? false, - enabled: enabled, - width: (state.properties['width'] as int? ?? 320).toDouble(), - label: accessibilityLabel ?? (text.isEmpty ? 'Navigation' : text), - onChanged: state.properties['toggle-enabled'] == true - ? (presented) => performToggle(id, presented) - : null, - main: children[0], - panel: children[1], - ), - _NodeKind.split => _LUISplit( - backend: this, - sourceFraction: (state.properties['value'] as double?) ?? 0, - gap: (state.properties['gap'] as int? ?? 9).toDouble(), - firstMinimum: - (_states[state.children[0]]?.properties['min-width'] as int? ?? 0) - .toDouble(), - secondMinimum: - (_states[state.children[1]]?.properties['min-width'] as int? ?? 0) - .toDouble(), - duration: Duration( - milliseconds: state.properties['resize-duration'] as int? ?? 0, - ), - easing: state.properties['resize-easing'] as String? ?? 'standard', - origin: state.properties['resize-origin'] as double?, - label: accessibilityLabel ?? 'Split', - onChanged: (value) => performValueChange(id, value), - first: children[0], - second: children[1], - ), - _NodeKind.box => LUIFlex( - direction: Axis.vertical, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: children, - ), - _NodeKind.text => textNode(), - _NodeKind.heading => Semantics( - header: true, - child: Text( - text, - style: _headingStyle( - context, - headingLevel, - )?.copyWith(color: foreground), - ), - ), - _NodeKind.paragraph => Text(text, style: TextStyle(color: foreground)), - _NodeKind.label => Text(text, style: TextStyle(color: foreground)), - _NodeKind.button => button( - selected: buttonSelected, - onPressed: enabled ? () => performAction(id) : null, - ), - _NodeKind.toggleButton => toggleButton(), - _NodeKind.select => select(), - _NodeKind.combobox => textControl(kind: state.kind), - _NodeKind.dropdownMenu => dropdownMenu(), - _NodeKind.contextMenu => const SizedBox.shrink(), - _NodeKind.tooltip => Text(text), - _NodeKind.toast => _LUIToast( - node: id, - duration: Duration( - milliseconds: state.properties['duration'] as int? ?? 0, - ), - label: accessibilityLabel ?? 'Notification', - onDismiss: () => performDismiss(id), - child: Card( - child: Padding( - padding: const EdgeInsets.all(16), - child: Row( - mainAxisSize: MainAxisSize.min, - spacing: 12, - children: children, - ), - ), - ), - ), - _NodeKind.toolbar => Semantics( - container: true, - label: accessibilityLabel, - child: orientation == 'vertical' - ? Column( - mainAxisSize: MainAxisSize.min, - spacing: gap, - children: children, - ) - : horizontalToolbar(children, gap), - ), - _NodeKind.accordion => accordion(), - _NodeKind.dialog || - _NodeKind.sheet => _LUIModalPresenter(backend: this, node: id), - _NodeKind.menuItem => menuItem(), - _NodeKind.menuTrigger => menuTrigger(), - _NodeKind.listItem => listItem(), - _NodeKind.listSection || - _NodeKind.listSectionHeader || - _NodeKind.listSectionFooter || - _NodeKind.swipeActions => column(), - _NodeKind.swipeAction => Text(text), - _NodeKind.table => table(), - _NodeKind.tree => tree(), - _NodeKind.tableRow => tableRow(), - _NodeKind.tableCell => tableCell(), - _NodeKind.avatar => avatar(), - _NodeKind.image => image(), - _NodeKind.mediaSurface => mediaSurface(), - _NodeKind.stepper => Semantics( - label: accessibilityLabel, - child: Row(mainAxisSize: MainAxisSize.min, children: children), - ), - _NodeKind.step => step(), - _NodeKind.timeline => Semantics( - label: accessibilityLabel, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - spacing: gap, - children: children, - ), - ), - _NodeKind.timelineItem => timelineItem(), - _NodeKind.inputGroup => inputGroup(), - _NodeKind.inputGroupActions => inputGroupActions(), - _NodeKind.toggle => FilterChip( - label: Text(text), - selected: checked, - onSelected: enabled ? (value) => performToggle(id, value) : null, - ), - _NodeKind.radioGroup => RadioGroup( - groupValue: _checkedRadio(state), - onChanged: (value) { - if (value != null) performChange(value); - }, - child: Row(mainAxisSize: MainAxisSize.min, children: children), - ), - _NodeKind.radio => MergeSemantics( - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Radio(value: id, enabled: enabled), - InkWell( - onTap: enabled ? () => performChange(id) : null, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 12), - child: Text(text), - ), - ), - ], - ), - ), - _NodeKind.slider => Slider( - value: ((state.properties['value'] as num?)?.toDouble() ?? 0).clamp( - 0, - 1, - ), - onChanged: enabled ? (value) => performValueChange(id, value) : null, - ), - _NodeKind.numberStepper => numberStepperView(state, enabled, id), - _NodeKind.textField || - _NodeKind.secureField || - _NodeKind.input || - _NodeKind.searchField || - _NodeKind.textarea => textControl(kind: state.kind), - _NodeKind.checkbox => toggleSemantics( - asSwitch: false, - child: CheckboxListTile( - value: checked, - onChanged: enabled - ? (value) => onEvent?.call( - LUIEvent.toggleChanged(node: id, checked: value ?? false), - ) - : null, - title: Text(text), - controlAffinity: ListTileControlAffinity.leading, - contentPadding: EdgeInsets.zero, - dense: true, - ), - ), - _NodeKind.switchControl => toggleSemantics( - asSwitch: true, - child: SwitchListTile( - value: checked, - onChanged: enabled - ? (value) => onEvent?.call( - LUIEvent.toggleChanged(node: id, checked: value), - ) - : null, - title: Text(text), - controlAffinity: ListTileControlAffinity.trailing, - contentPadding: EdgeInsets.zero, - dense: true, - ), - ), - _NodeKind.progress => LinearProgressIndicator( - value: progressFraction, - semanticsLabel: accessibilityLabel, - semanticsValue: '${(progressFraction * 100).round()}%', - ), - _NodeKind.divider => - orientation == 'vertical' - ? const VerticalDivider(width: 1) - : const Divider(height: 1), - _NodeKind.scroll => SingleChildScrollView( - scrollDirection: state.properties['orientation'] == 'horizontal' - ? Axis.horizontal - : Axis.vertical, - child: Stack(children: children), - ), - _NodeKind.spacer => const SizedBox.shrink(), - _NodeKind.spinner => SizedBox.square( - dimension: spinnerExtent, - child: CircularProgressIndicator(strokeWidth: 2, color: foreground), - ), - _NodeKind.icon => Icon( - _iconData(state.properties['name'] as String? ?? ''), - size: iconExtent, - color: foreground, - ), - _NodeKind.kbd => Container( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), - decoration: BoxDecoration( - color: Colors.black.withOpacity(0.08), - borderRadius: BorderRadius.circular(4), - border: Border.all(color: Colors.black.withOpacity(0.2), width: 0.5), - ), - child: Text( - state.properties['text'] as String? ?? '', - style: const TextStyle(fontFamily: 'monospace', fontSize: 10), - ), - ), - _NodeKind.statusBar => statusBar(), - // Non-visual node: presentation is driven by its properties on - // platforms that implement the file-picker backend; children render - // inline. - _NodeKind.filePicker => Stack(children: children), - _NodeKind.link => column(), - _NodeKind.fileImage => fileImage(), - // Non-visual node: file preview is presented by platform code - // (QuickLook on Apple); Flutter keeps platform-effect previews. - _NodeKind.filePreview => const SizedBox.shrink(), - // Line-break leaf: no native representation; renders nothing. - _NodeKind.br => const SizedBox.shrink(), - // Popover positioning is not implemented yet: render the children - // inline so the content stays reachable. - _NodeKind.popover => column(), - }; - - if (state.kind == _NodeKind.root || state.kind.isModalSurface) { - return accessibilityIdentifier == null - ? content - : Semantics(identifier: accessibilityIdentifier, child: content); - } - - final isSurface = state.kind.isOverlaySurface; - final padding = - state.properties['padding'] as int? ?? - (state.kind == _NodeKind.card - ? 24 - : state.kind == _NodeKind.alert - ? 16 - : state.kind == _NodeKind.bubble - ? 12 - : state.kind == _NodeKind.tabs - ? 4 - : 0); - final paddingHorizontal = - state.properties['padding-horizontal'] as int? ?? padding; - final paddingVertical = - state.properties['padding-vertical'] as int? ?? padding; - final effectiveBorderWidth = borderWidth ?? (isSurface ? 1 : 0); - final effectiveCornerRadius = - cornerRadius ?? - (isSurface - ? 12 - : state.kind == _NodeKind.tabs - ? 8 - : 0); - final contentPadding = EdgeInsets.symmetric( - horizontal: paddingHorizontal.toDouble(), - vertical: paddingVertical.toDouble(), - ); - Widget surface = isSurface - ? Material( - color: - background ?? - (state.kind == _NodeKind.bubble && buttonVariant == 'primary' - ? Theme.of(context).colorScheme.primary - : state.kind == _NodeKind.bubble && buttonVariant == 'ghost' - ? Colors.transparent - : state.kind == _NodeKind.alert && - buttonVariant == 'destructive' - ? Theme.of(context).colorScheme.errorContainer - : Theme.of(context).colorScheme.surface), - elevation: state.kind == _NodeKind.panel ? 1 : 0, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - effectiveCornerRadius.toDouble(), - ), - side: effectiveBorderWidth == 0 - ? BorderSide.none - : BorderSide( - color: - borderColor ?? - Theme.of(context).colorScheme.outlineVariant, - width: effectiveBorderWidth.toDouble(), - ), - ), - clipBehavior: Clip.antiAlias, - child: Padding(padding: contentPadding, child: content), - ) - : Container( - padding: contentPadding, - decoration: BoxDecoration( - color: - background ?? - (state.kind == _NodeKind.box && - state.properties['selected'] == true - ? Theme.of(context).colorScheme.secondaryContainer - : state.kind == _NodeKind.tabs - ? Theme.of(context).colorScheme.secondaryContainer - : null), - border: effectiveBorderWidth == 0 - ? null - : Border.all( - color: borderColor ?? Colors.transparent, - width: effectiveBorderWidth.toDouble(), - ), - borderRadius: effectiveCornerRadius == 0 - ? null - : BorderRadius.circular(effectiveCornerRadius.toDouble()), - ), - child: content, - ); - if (state.kind == _NodeKind.bubble && - buttonVariant != 'ghost' && - !state.properties.containsKey('width')) { - final bubbleSurface = surface; - surface = _LUIWidthCap(widthFactor: 0.8, child: bubbleSurface); - } - if (state.kind == _NodeKind.resizable) { - surface = _LUIResizable( - initialWidth: (state.properties['width'] as int?)?.toDouble(), - minimumWidth: (state.properties['min-width'] as int?)?.toDouble(), - maximumWidth: (state.properties['max-width'] as int?)?.toDouble(), - label: accessibilityLabel ?? 'Resizable', - child: surface, - ); - } - final width = - state.kind == _NodeKind.resizable || state.kind == _NodeKind.drawer - ? null - : state.properties['width'] as int?; - final height = state.properties['height'] as int?; - if (width != null || height != null) { - surface = SizedBox( - width: width?.toDouble(), - height: height?.toDouble(), - child: surface, - ); - } - final minWidth = state.kind == _NodeKind.resizable - ? null - : state.properties['min-width'] as int?; - final maxWidth = state.kind == _NodeKind.resizable - ? null - : state.properties['max-width'] as int?; - final minHeight = state.properties['min-height'] as int?; - final maxHeight = state.properties['max-height'] as int?; - if (minWidth != null || - maxWidth != null || - minHeight != null || - maxHeight != null) { - surface = ConstrainedBox( - constraints: BoxConstraints( - minWidth: minWidth?.toDouble() ?? 0, - maxWidth: maxWidth?.toDouble() ?? double.infinity, - minHeight: minHeight?.toDouble() ?? 0, - maxHeight: maxHeight?.toDouble() ?? double.infinity, - ), - child: surface, - ); - } - final opacity = (state.properties['opacity'] as num?)?.toDouble(); - if (opacity != null && opacity != 1.0) { - surface = Opacity(opacity: opacity, child: surface); - } - final tooltip = state.properties['tooltip'] as String?; - if (tooltip != null && tooltip.isNotEmpty) { - surface = Tooltip(message: tooltip, child: surface); - } - if (state.properties['role'] == 'treeitem') { - final tree = _treeAncestor(id)!; - final shortcuts = { - for (final key in const [ - LogicalKeyboardKey.arrowUp, - LogicalKeyboardKey.arrowDown, - LogicalKeyboardKey.arrowLeft, - LogicalKeyboardKey.arrowRight, - LogicalKeyboardKey.home, - LogicalKeyboardKey.end, - ]) - SingleActivator(key): () => _performTreeKey(tree, id, key), - }; - if (state.properties['press-enabled'] == true) { - shortcuts[const SingleActivator(LogicalKeyboardKey.enter)] = () => - performAction(id); - shortcuts[const SingleActivator(LogicalKeyboardKey.space)] = () => - performAction(id); - if (state.kind != _NodeKind.listItem) { - surface = GestureDetector( - onTap: () => _performTreeTap(id), - child: surface, - ); - } - } else if (state.kind != _NodeKind.listItem && - (state.properties['change-enabled'] == true || - state.properties['toggle-enabled'] == true)) { - surface = GestureDetector( - onTap: () => _performTreeTap(id), - child: surface, - ); - } - surface = Semantics( - container: true, - selected: buttonSelected, - expanded: state.properties['expanded'] as bool?, - enabled: enabled, - child: CallbackShortcuts( - bindings: shortcuts, - child: Focus( - focusNode: _requireHandle(id).focusNode, - canRequestFocus: enabled, - skipTraversal: id != _treeTabstop(tree), - child: surface, - ), - ), - ); - } - if (contextMenuID != null) { - final menu = _requireState(_states, contextMenuID); - Future showContextMenu(Offset position) async { - final selected = await showMenu( - context: context, - position: RelativeRect.fromLTRB( - position.dx, - position.dy, - position.dx, - position.dy, - ), - items: menu.children - .map>((childID) { - final child = _requireState(_states, childID); - if (child.kind == _NodeKind.divider) { - return const PopupMenuDivider(); - } - final childIcon = child.properties['icon'] as String?; - final childSelected = - child.properties['selected'] as bool? ?? false; - final childForeground = _color( - context, - child.properties['foreground'] as String?, - foreground: true, - ); - return PopupMenuItem( - value: childID, - enabled: child.properties['enabled'] as bool? ?? true, - child: Row( - spacing: 12, - children: [ - if (childIcon != null) - Icon( - _iconData(childIcon), - size: 20, - color: childForeground, - ), - Expanded( - child: Text( - child.properties['text'] as String? ?? '', - style: TextStyle(color: childForeground), - ), - ), - if (childSelected) const Icon(Icons.check, size: 20), - ], - ), - ); - }) - .toList(growable: false), - ); - if (selected != null) performAction(selected); - } - - surface = GestureDetector( - behavior: HitTestBehavior.opaque, - onSecondaryTapDown: (details) => - showContextMenu(details.globalPosition), - onLongPressStart: (details) => showContextMenu(details.globalPosition), - child: surface, - ); - } - return accessibilityIdentifier == null - ? surface - : Semantics(identifier: accessibilityIdentifier, child: surface); - } - - void _applyState( - Map states, - Map extensions, - Map operation, - Set filePickerRequests, - ) { - switch (_string(operation['op'], 'op')) { - case 'create-node': - final id = _integer(operation['id'], 'id'); - if (_containsState(states, extensions, id)) { - throw const LUIBackendException('node already exists'); - } - states[id] = _NodeState(_decodeNodeKind(operation['kind'])); - case 'create-extension': - final id = _integer(operation['id'], 'id'); - if (_containsState(states, extensions, id)) { - throw const LUIBackendException('node already exists'); - } - final identifier = _string(operation['identifier'], 'identifier'); - final fingerprint = _string(operation['fingerprint'], 'fingerprint'); - final registration = _extensionRegistry._registration(identifier); - if (registration == null) { - throw const LUIBackendException('unknown extension'); - } - if (registration.fingerprint != fingerprint) { - throw const LUIBackendException('extension fingerprint mismatch'); - } - extensions[id] = _ExtensionNodeState( - identifier: identifier, - fingerprint: fingerprint, - properties: { - for (final property in registration.properties) - if (property.defaultValue != null) - property.name: property.kind.normalize(property.defaultValue!), - }, - ); - case 'drop-node': - final id = _integer(operation['id'], 'id'); - final parent = _nodeParent(states, extensions, id); - final children = _nodeChildren(states, extensions, id); - if (parent != null || children.isNotEmpty) { - throw const LUIBackendException('cannot drop an attached node'); - } - if (states.remove(id) == null && extensions.remove(id) == null) { - throw LUIBackendException('unknown node $id'); - } - case 'set-prop': - final id = _integer(operation['id'], 'id'); - final node = _requireState(states, id); - final property = _string(operation['property'], 'property'); - final value = operation['value']; - if (!_supports(node.kind, property, value)) { - throw LUIBackendException( - 'unsupported property value: ${node.kind.name}.$property', - ); - } - node.properties[property] = value!; - if (node.kind == _NodeKind.filePicker && property == 'request') { - filePickerRequests.add(id); - } - case 'remove-prop': - final node = _requireState(states, _integer(operation['id'], 'id')); - final property = _string(operation['property'], 'property'); - if (!node.properties.containsKey(property)) { - throw LUIBackendException( - 'unsupported property: ${node.kind.name}.$property', - ); - } - node.properties.remove(property); - case 'set-extension-prop': - final node = _requireExtensionStateFrom( - extensions, - _integer(operation['id'], 'id'), - ); - final propertyName = _string(operation['property'], 'property'); - final registration = _extensionRegistry._registration(node.identifier)!; - final property = registration.properties - .where((candidate) => candidate.name == propertyName) - .firstOrNull; - final value = operation['value']; - if (property == null || - value == null || - !property.kind.accepts(value)) { - throw const LUIBackendException( - 'unsupported extension property value', - ); - } - node.properties[propertyName] = property.kind.normalize(value); - case 'remove-extension-prop': - final node = _requireExtensionStateFrom( - extensions, - _integer(operation['id'], 'id'), - ); - final propertyName = _string(operation['property'], 'property'); - final registration = _extensionRegistry._registration(node.identifier)!; - final property = registration.properties - .where((candidate) => candidate.name == propertyName) - .firstOrNull; - if (property == null) { - throw const LUIBackendException('unknown extension property'); - } - if (property.defaultValue == null) { - node.properties.remove(propertyName); - } else { - node.properties[propertyName] = property.kind.normalize( - property.defaultValue!, - ); - } - case 'insert-child': - final parentID = _integer(operation['parent'], 'parent'); - final childID = _integer(operation['child'], 'child'); - final index = _integer(operation['index'], 'index'); - if (!_containsState(states, extensions, parentID) || - !_containsState(states, extensions, childID)) { - throw const LUIBackendException('unknown parent or child node'); - } - if (_nodeParent(states, extensions, childID) != null) { - throw const LUIBackendException('child is already attached'); - } - _validateChildRelationship(states, extensions, parentID, childID); - final children = _nodeChildren(states, extensions, parentID); - if (index < 0 || index > children.length) { - throw const LUIBackendException('child index is out of bounds'); - } - if (_isDescendantAny( - states, - extensions, - target: parentID, - root: childID, - )) { - throw const LUIBackendException( - 'child insertion would create a cycle', - ); - } - children.insert(index, childID); - _setNodeParent(states, extensions, childID, parentID); - // A mounted subtree whose boundary's mark is dropped mid-flush - // never materializes SemanticsNodes — the heal's missing check - // re-marks until its identifiers appear. - _scheduleSemanticsHeal(); - case 'remove-child': - final parentID = _integer(operation['parent'], 'parent'); - final childID = _integer(operation['child'], 'child'); - final children = _nodeChildren(states, extensions, parentID); - if (!children.remove(childID)) { - throw const LUIBackendException('child is not attached to parent'); - } - _collectSubtreeIdentifiers( - states, - extensions, - childID, - _staleSemanticsIdentifiers, - ); - _scheduleSemanticsHeal(); - _setNodeParent(states, extensions, childID, null); - case 'move-child': - final parentID = _integer(operation['parent'], 'parent'); - final children = _nodeChildren(states, extensions, parentID); - final childID = _integer(operation['child'], 'child'); - final index = _integer(operation['index'], 'index'); - if (!children.remove(childID)) { - throw const LUIBackendException('child is not attached to parent'); - } - if (index < 0 || index > children.length) { - throw const LUIBackendException('child index is out of bounds'); - } - children.insert(index, childID); - default: - throw const LUIBackendException('unknown patch operation'); - } - } - - void _validateChildRelationship( - Map states, - Map extensions, - int parentID, - int childID, - ) { - if (states[childID]?.kind == _NodeKind.root) { - throw const LUIBackendException('runtime root cannot be nested'); - } - final transparentChild = extensions[childID]; - if (transparentChild != null && - _extensionRegistry - ._registration(transparentChild.identifier)! - .isTweak) { - if (transparentChild.children.length != 1) { - throw const LUIBackendException( - 'platform tweak requires exactly one child', - ); - } - _validateChildRelationship( - states, - extensions, - parentID, - transparentChild.children.single, - ); - return; - } - final parent = states[parentID]; - final child = states[childID]; - if (parent != null && child == null) { - if (!_acceptsExtensionChildren(parent.kind)) { - throw const LUIBackendException( - 'standard node cannot contain extension', - ); - } - return; - } - final extensionParent = extensions[parentID]; - final extensionChild = extensions[childID]; - if (extensionParent != null) { - final registration = _extensionRegistry._registration( - extensionParent.identifier, - )!; - if (registration.isTweak) { - if (extensionParent.children.isNotEmpty) { - throw const LUIBackendException( - 'platform tweak requires exactly one child', - ); - } - return; - } - if (child != null) { - if (!registration.acceptsStandardChildren) { - throw const LUIBackendException( - 'extension does not accept standard children', - ); - } - return; - } - if (extensionChild == null || - !registration.childIdentifiers.contains(extensionChild.identifier)) { - throw const LUIBackendException( - 'extension child relationship is not registered', - ); - } - return; - } - if (parent == null || child == null) { - throw const LUIBackendException('unknown parent or child node'); - } - if (!_canContainChildren(parent.kind)) { - throw const LUIBackendException('parent cannot contain child'); - } - if ((parent.kind == _NodeKind.dropdownMenu || - parent.kind == _NodeKind.contextMenu) && - child.kind != _NodeKind.menuItem && - child.kind != _NodeKind.menuTrigger && - child.kind != _NodeKind.divider) { - throw const LUIBackendException( - 'menu accepts only menu-item or separator children', - ); - } - if (parent.kind == _NodeKind.menuTrigger && - child.kind != _NodeKind.dropdownMenu) { - throw const LUIBackendException( - 'menu-trigger accepts only a dropdown-menu child', - ); - } - if (parent.kind == _NodeKind.menuItem && - child.kind != _NodeKind.contextMenu && - child.kind != _NodeKind.dropdownMenu) { - throw const LUIBackendException( - 'menu-item accepts only nested menu metadata', - ); - } - if (parent.kind != _NodeKind.menuItem && - _isContextMenuLeafHost(parent.kind) && - child.kind != _NodeKind.contextMenu) { - throw const LUIBackendException( - 'interactive leaf accepts only context-menu metadata', - ); - } - if (parent.kind == _NodeKind.table && child.kind != _NodeKind.tableRow) { - throw const LUIBackendException('table can contain only table-row'); - } - if (parent.kind == _NodeKind.tableRow && - child.kind != _NodeKind.tableCell) { - throw const LUIBackendException('table-row can contain only table-cell'); - } - if (parent.kind == _NodeKind.tree && !_isTreeRowKind(child.kind)) { - throw const LUIBackendException('tree accepts only row containers'); - } - if (parent.kind == _NodeKind.stepper && child.kind != _NodeKind.step) { - throw const LUIBackendException('stepper accepts only step children'); - } - if (parent.kind == _NodeKind.timeline && - child.kind != _NodeKind.timelineItem) { - throw const LUIBackendException( - 'timeline accepts only timeline-item children', - ); - } - if (parent.kind == _NodeKind.bottomTabs && - child.kind != _NodeKind.bottomTab) { - throw const LUIBackendException( - 'bottom-tabs accepts only bottom-tab children', - ); - } - if (parent.kind == _NodeKind.bottomTab && - child.kind == _NodeKind.bottomTab) { - throw const LUIBackendException( - 'bottom-tab cannot directly contain bottom-tab', - ); - } - if (parent.kind == _NodeKind.inputGroup && - child.kind != _NodeKind.textarea && - child.kind != _NodeKind.inputGroupActions) { - throw const LUIBackendException( - 'input-group accepts only textarea and input-group-actions children', - ); - } - if (parent.kind == _NodeKind.toolbar && !_isToolbarChild(child.kind)) { - throw const LUIBackendException( - 'toolbar accepts only interactive controls and dividers', - ); - } - if (parent.kind == _NodeKind.listSection && - child.kind != _NodeKind.listItem && - child.kind != _NodeKind.listSectionHeader && - child.kind != _NodeKind.listSectionFooter) { - throw const LUIBackendException( - 'list-section accepts only list rows and section header/footer', - ); - } - if ((child.kind == _NodeKind.listSectionHeader || - child.kind == _NodeKind.listSectionFooter) && - parent.kind != _NodeKind.listSection) { - throw const LUIBackendException( - 'list-section header/footer requires a list-section parent', - ); - } - if (child.kind == _NodeKind.listSection && parent.kind != _NodeKind.list) { - throw const LUIBackendException( - 'list-section requires a direct list parent', - ); - } - if (child.kind == _NodeKind.swipeActions && - parent.kind != _NodeKind.listItem) { - throw const LUIBackendException( - 'swipe-actions requires a list-item parent', - ); - } - if (parent.kind == _NodeKind.swipeActions && - child.kind != _NodeKind.swipeAction) { - throw const LUIBackendException( - 'swipe-actions accepts only swipe-action children', - ); - } - if (child.kind == _NodeKind.swipeAction && - parent.kind != _NodeKind.swipeActions) { - throw const LUIBackendException( - 'swipe-action requires a swipe-actions parent', - ); - } - } - - /// Direction-aware nine-point anchor; 'leading'/'trailing' follow the - /// ambient Directionality (matching SwiftUI's semantic edges). - static AlignmentGeometry? _overlayAlignment(String? name) => switch (name) { - 'top-leading' => AlignmentDirectional.topStart, - 'top' => AlignmentDirectional.topCenter, - 'top-trailing' => AlignmentDirectional.topEnd, - 'leading' => AlignmentDirectional.centerStart, - 'trailing' => AlignmentDirectional.centerEnd, - 'bottom-leading' => AlignmentDirectional.bottomStart, - 'bottom' => AlignmentDirectional.bottomCenter, - 'bottom-trailing' => AlignmentDirectional.bottomEnd, - _ => null, - }; - - static bool _supports(_NodeKind kind, String property, Object? value) { - if (property == 'accessibility-identifier') return value is String; - if (kind == _NodeKind.root) { - return (property == 'theme' && value is String) || - (property == 'theme-mode' && - value is String && - _themeModes.contains(value)); - } - if (kind == _NodeKind.contextMenu) return false; - // The pointer-detail family is opt-in via `pointer-enabled`, honored on - // every kind that renders (all but root, contextMenu, swipeActions). - if (property == 'pointer-enabled') { - return value is bool && - kind != _NodeKind.root && - kind != _NodeKind.swipeActions; - } - // Position hint honored on overlay children and the overlay itself; - // admitted before the restrictive kinds below. Inert elsewhere. - if (property == 'alignment') { - return kind != _NodeKind.root && - value is String && - _overlayAlignment(value) != null; - } - if (kind == _NodeKind.filePreview) { - return property == 'path' && value is String; - } - if (kind == _NodeKind.accordion) { - return switch (property) { - 'text' => value is String, - 'selected' || 'toggle-enabled' => value is bool, - 'height' => value is int && value >= 0, - _ => false, - }; - } - if (kind == _NodeKind.stepper) { - return switch (property) { - 'active' => value is int && value >= 0, - 'accessibility-label' => value is String, - _ => false, - }; - } - if (kind == _NodeKind.step) { - return property == 'text' && value is String; - } - if (kind == _NodeKind.timeline) { - return switch (property) { - 'gap' => value is int && value >= 0, - 'grow' => value is num && value.isFinite && value >= 0, - 'accessibility-label' => value is String, - _ => false, - }; - } - if (kind == _NodeKind.timelineItem) { - return switch (property) { - 'title' || 'description' || 'meta' || 'indicator' => value is String, - 'icon' => - value is String && - (_iconNames.contains(value) || - _appIconNamePattern.hasMatch(value)), - 'variant' => value is String && _buttonVariants.contains(value), - 'connector' || 'selected' || 'press-enabled' => value is bool, - _ => false, - }; - } - if (kind == _NodeKind.inputGroup) { - return switch (property) { - 'accessibility-label' => value is String, - 'width' || 'height' || 'min-width' => value is int && value >= 0, - 'grow' => value is num && value.isFinite && value >= 0, - _ => false, - }; - } - if (kind == _NodeKind.inputGroupActions) { - return property == 'gap' && value is int && value >= 0; - } - if (kind == _NodeKind.toast) { - return switch (property) { - 'duration' => value is int && value >= 0 && value <= 0x7fffffff, - 'accessibility-label' || 'style-class' => value is String, - _ => false, - }; - } - if (kind == _NodeKind.toolbar) { - return switch (property) { - 'orientation' => - value is String && (value == 'horizontal' || value == 'vertical'), - 'placement' => - value is String && - const { - 'automatic', - 'bottom', - 'navigation', - 'principal', - 'primary-action', - 'secondary-action', - 'status', - 'confirmation-action', - 'cancellation-action', - 'destructive-action', - 'top-bar-leading', - 'top-bar-trailing', - }.contains(value), - 'gap' => value is int && value >= 0, - 'accessibility-label' || 'style-class' => value is String, - _ => false, - }; - } - if (kind == _NodeKind.bottomTabs) { - return switch (property) { - 'accessibility-label' || 'style-class' => value is String, - 'grow' => value is num && value.isFinite && value >= 0, - 'width' || - 'height' || - 'min-width' || - 'max-width' || - 'min-height' || - 'max-height' => value is int && value >= 0, - _ => false, - }; - } - if (kind == _NodeKind.bottomTab) { - return switch (property) { - 'title' => value is String, - 'icon' => - value is String && - (_iconNames.contains(value) || - _appIconNamePattern.hasMatch(value)), - 'selected' || 'enabled' || 'press-enabled' => value is bool, - _ => false, - }; - } - if (kind == _NodeKind.listSection) { - return switch (property) { - 'key' => value is String, - 'separator' => value == 'visible' || value == 'hidden', - _ => false, - }; - } - if (kind == _NodeKind.swipeActions) return false; - if (kind == _NodeKind.swipeAction) { - return switch (property) { - 'text' || 'background' => value is String, - 'icon' => - value is String && - (_iconNames.contains(value) || - _appIconNamePattern.hasMatch(value)), - 'variant' => value is String && _buttonVariants.contains(value), - 'edge' => value == 'leading' || value == 'trailing', - 'enabled' || 'press-enabled' => value is bool, - _ => false, - }; - } - if (kind == _NodeKind.filePicker) { - return switch (property) { - 'request' || 'completion' => value is String || value is int, - 'types' => value is String, - 'multiple' || 'enabled' || 'appear-enabled' => value is bool, - 'accept' => value is String, - 'directory' => value is bool, - 'source' => - value is String && - const {'files', 'photos', 'camera'}.contains(value), - _ => false, - }; - } - // Mirrors OCaml common_property_supported: theme props are admitted on - // every non-restrictive container kind. - if (property == 'theme') { - return value is String && _canContainChildren(kind); - } - if (property == 'theme-mode') { - return value is String && - _themeModes.contains(value) && - _canContainChildren(kind); - } - return switch (property) { - 'main' => - value is String && - _mainAlignments.contains(value) && - (kind == _NodeKind.row || - kind == _NodeKind.column || - kind == _NodeKind.list || - kind == _NodeKind.virtualList || - _isHorizontalGroupKind(kind)), - 'cross' => - value is String && - _crossAlignments.contains(value) && - (kind == _NodeKind.row || - kind == _NodeKind.column || - kind == _NodeKind.list || - kind == _NodeKind.virtualList || - _isHorizontalGroupKind(kind)), - 'grow' => - value is num && - value.isFinite && - value >= 0 && - kind != _NodeKind.avatar && - kind != _NodeKind.tooltip && - !kind.isModalSurface, - 'columns' => value is int && value >= 0 && kind == _NodeKind.grid, - 'text' => - value is String && - (kind == _NodeKind.text || - kind == _NodeKind.heading || - kind == _NodeKind.paragraph || - kind == _NodeKind.label || - _isButtonKind(kind) || - _isTextControl(kind) || - kind == _NodeKind.checkbox || - kind == _NodeKind.switchControl || - kind == _NodeKind.toggle || - kind == _NodeKind.radio || - kind == _NodeKind.numberStepper || - kind == _NodeKind.select || - kind == _NodeKind.menuItem || - kind == _NodeKind.menuTrigger || - kind == _NodeKind.listItem || - kind == _NodeKind.tableCell || - kind == _NodeKind.avatar || - kind == _NodeKind.tooltip || - kind == _NodeKind.alert || - kind == _NodeKind.bubble || - kind == _NodeKind.statusBar || - kind == _NodeKind.drawer || - kind == _NodeKind.link || - kind.isModalSurface), - 'enabled' => - value is bool && - (_isButtonKind(kind) || - _isTextControl(kind) || - kind == _NodeKind.checkbox || - kind == _NodeKind.switchControl || - kind == _NodeKind.toggle || - kind == _NodeKind.radio || - kind == _NodeKind.slider || - kind == _NodeKind.numberStepper || - kind == _NodeKind.select || - kind == _NodeKind.menuItem || - kind == _NodeKind.listItem || - kind == _NodeKind.drawer || - kind == _NodeKind.link), - 'value' => - value is double && - value.isFinite && - (kind == _NodeKind.progress || - kind == _NodeKind.slider || - kind == _NodeKind.numberStepper || - kind == _NodeKind.split), - 'min' || 'max' => - value is num && value.isFinite && kind == _NodeKind.numberStepper, - 'step' => - value is num && - value.isFinite && - value > 0 && - kind == _NodeKind.numberStepper, - 'detents' => value is String && kind == _NodeKind.sheet, - 'sizing' => - value is String && - const {'form', 'fitted', 'page'}.contains(value) && - kind == _NodeKind.sheet, - 'resize-duration' => - value is int && value >= 0 && kind == _NodeKind.split, - 'resize-easing' => - value is String && - const { - 'linear', - 'standard', - 'emphasized', - 'spring', - }.contains(value) && - kind == _NodeKind.split, - 'resize-origin' => - value is double && value.isFinite && kind == _NodeKind.split, - 'orientation' => - value is String && - (value == 'horizontal' || value == 'vertical') && - (kind == _NodeKind.divider || - kind == _NodeKind.tabs || - kind == _NodeKind.scroll || - kind == _NodeKind.viewThatFits), - 'size' => - value is String && - (_controlSizes.contains(value) || - (kind == _NodeKind.tableCell && _textSizes.contains(value))) && - (_isButtonKind(kind) || - kind == _NodeKind.spinner || - kind == _NodeKind.icon || - kind == _NodeKind.tableCell || - kind == _NodeKind.menuItem), - 'name' => - value is String && - (_iconNames.contains(value) || - _appIconNamePattern.hasMatch(value)) && - kind == _NodeKind.icon, - 'variant' => - value is String && - _buttonVariants.contains(value) && - (_isButtonKind(kind) || - kind == _NodeKind.alert || - kind == _NodeKind.bubble || - kind == _NodeKind.menuItem), - 'icon' => - value is String && - (_iconNames.contains(value) || - _appIconNamePattern.hasMatch(value)) && - (_isButtonKind(kind) || - kind == _NodeKind.menuItem || - kind == _NodeKind.menuTrigger || - kind == _NodeKind.listItem || - kind == _NodeKind.link), - 'icon-placement' => - value is String && - (value == 'leading' || value == 'trailing' || value == 'top') && - (_isButtonKind(kind) || kind == _NodeKind.link), - 'selected' => - value is bool && - (_isButtonKind(kind) || - kind == _NodeKind.menuItem || - kind == _NodeKind.listItem || - kind == _NodeKind.tableRow || - kind == _NodeKind.drawer || - _isTreeRowKind(kind)), - 'long-press-enabled' => - value is bool && (_isButtonKind(kind) || kind == _NodeKind.listItem), - 'autofocus' => - value is bool && (_isButtonKind(kind) || _isTextControl(kind)), - 'submit-on-enter' => value is bool && kind == _NodeKind.textarea, - 'change-enabled' => - value is bool && (kind == _NodeKind.radio || _isTreeRowKind(kind)), - 'toggle-enabled' => - value is bool && - (kind == _NodeKind.radio || - kind == _NodeKind.drawer || - _isTreeRowKind(kind)), - 'press-enabled' => - value is bool && - (kind == _NodeKind.text || - kind == _NodeKind.column || - kind == _NodeKind.radio || - kind == _NodeKind.select || - kind == _NodeKind.combobox || - kind == _NodeKind.menuItem || - kind == _NodeKind.listItem || - kind == _NodeKind.tableCell || - kind == _NodeKind.fileImage || - _isTreeRowKind(kind)), - 'submit-enabled' => - value is bool && - (kind == _NodeKind.combobox || kind == _NodeKind.listItem), - 'double-press-enabled' => value is bool && kind == _NodeKind.listItem, - 'appear-enabled' => value is bool, - 'image' => - value is int && - value >= 0 && - (kind == _NodeKind.avatar || kind == _NodeKind.image), - 'surface' => value is int && value >= 0 && kind == _NodeKind.mediaSurface, - 'source-x' || 'source-y' || 'source-width' || 'source-height' => - value is num && - value.isFinite && - (kind == _NodeKind.avatar || kind == _NodeKind.image), - 'path' => value is String && kind == _NodeKind.fileImage, - 'url' => - value is String && (kind == _NodeKind.link || kind == _NodeKind.image), - 'target' => - value is String && - const {'_self', '_blank'}.contains(value) && - kind == _NodeKind.link, - 'opacity' => value is num && value >= 0 && value <= 1 && _canContainChildren(kind), - 'display' => - value == 'contents' && - (kind == _NodeKind.box || - kind == _NodeKind.column || - kind == _NodeKind.row), - 'tooltip' || 'tooltip-keys' => - value is String && - (kind == _NodeKind.button || - kind == _NodeKind.icon || - kind == _NodeKind.menuItem), - 'input-type' => - (value == 'text' || value == 'color') && kind == _NodeKind.input, - 'alt' => value is String && kind == _NodeKind.image, - 'loading' => - value is String && - const {'eager', 'lazy'}.contains(value) && - kind == _NodeKind.image, - 'referrer-policy' => - value is String && - const { - 'no-referrer', - 'origin', - 'strict-origin-when-cross-origin', - 'unsafe-url', - }.contains(value) && - kind == _NodeKind.image, - 'image-fit' => value is String && const {'fit', 'fill'}.contains(value) && kind == _NodeKind.fileImage, - 'max-pixel-size' => - value is int && value > 0 && kind == _NodeKind.fileImage, - 'anchor' => - value is String && - const {'above', 'below', 'left', 'right'}.contains(value) && - (kind == _NodeKind.dropdownMenu || kind == _NodeKind.tooltip || kind == _NodeKind.popover), - 'anchor-alignment' => - value is String && - const {'start', 'end', 'stretch'}.contains(value) && - (kind == _NodeKind.dropdownMenu || kind == _NodeKind.tooltip || kind == _NodeKind.popover), - 'anchor-offset' => - value is num && - value.isFinite && - (kind == _NodeKind.dropdownMenu || kind == _NodeKind.tooltip || kind == _NodeKind.popover), - 'x' => - value is num && value.isFinite && kind == _NodeKind.popover, - 'y' => - value is num && value.isFinite && kind == _NodeKind.popover, - 'available-height' => - value is num && value.isFinite && value >= 0 && kind == _NodeKind.popover, - 'edge' => - value is String && - const {'top', 'bottom', 'leading', 'trailing'}.contains(value) && - kind == _NodeKind.edgeInset, - 'visible' => value is bool && kind == _NodeKind.edgeInset, - // 'alignment' is admitted ahead of the restrictive kinds above. - 'alignment' => - value is String && _overlayAlignment(value) != null, - 'tooltip-delay' => - value is int && - value >= 0 && - value <= 0x7fffffff && - kind == _NodeKind.tooltip, - 'gap' => - value is int && - value >= 0 && - (kind == _NodeKind.row || - kind == _NodeKind.column || - kind == _NodeKind.grid || - kind == _NodeKind.list || - kind == _NodeKind.virtualList || - kind == _NodeKind.dropdownMenu || - kind == _NodeKind.tableRow || - kind == _NodeKind.tree || - kind == _NodeKind.split || - kind == _NodeKind.edgeInset || - _isHorizontalGroupKind(kind)), - 'padding' => - value is int && kind != _NodeKind.avatar && kind != _NodeKind.tooltip, - 'padding-horizontal' || 'padding-vertical' => - value is int && - value >= 0 && - (kind == _NodeKind.row || - kind == _NodeKind.column || - kind == _NodeKind.grid || - kind == _NodeKind.box), - 'background' => - value is String && - kind != _NodeKind.avatar && - kind != _NodeKind.tooltip && - !kind.isModalSurface, - 'foreground' => - value is String && - (kind == _NodeKind.edgeInset || - kind == _NodeKind.overlay || - kind == _NodeKind.viewThatFits || - kind == _NodeKind.text || - kind == _NodeKind.heading || - kind == _NodeKind.paragraph || - kind == _NodeKind.label || - _isButtonKind(kind) || - _isTextControl(kind) || - kind == _NodeKind.checkbox || - kind == _NodeKind.toggle || - kind == _NodeKind.radio || - kind == _NodeKind.slider || - kind == _NodeKind.numberStepper || - kind == _NodeKind.spinner || - kind == _NodeKind.icon || - kind == _NodeKind.select || - kind == _NodeKind.dropdownMenu || - kind == _NodeKind.menuItem || - kind == _NodeKind.menuTrigger || - kind == _NodeKind.listItem || - kind == _NodeKind.tableCell || - kind == _NodeKind.resizable || - kind == _NodeKind.split || - kind == _NodeKind.alert || - kind == _NodeKind.bubble || - kind == _NodeKind.statusBar || - kind == _NodeKind.link || - kind == _NodeKind.fileImage), - 'border-color' => - value is String && - kind != _NodeKind.avatar && - kind != _NodeKind.tooltip && - !kind.isModalSurface, - 'border-width' => - value is int && - value >= 0 && - kind != _NodeKind.avatar && - kind != _NodeKind.tooltip && - !kind.isModalSurface, - 'corner-radius' => - value is int && - value >= 0 && - kind != _NodeKind.avatar && - kind != _NodeKind.tooltip && - !kind.isModalSurface, - 'width' || 'height' => - value is int && - value >= 0 && - kind != _NodeKind.avatar && - kind != _NodeKind.tooltip, - 'min-width' || 'max-width' || 'min-height' || 'max-height' => - value is int && - value >= 0 && - kind != _NodeKind.avatar && - kind != _NodeKind.tooltip && - !kind.isModalSurface, - 'style-class' => - value is String && - kind != _NodeKind.avatar && - kind != _NodeKind.tooltip && - !kind.isModalSurface, - 'checked' => - value is bool && - (kind == _NodeKind.checkbox || - kind == _NodeKind.switchControl || - kind == _NodeKind.toggle || - kind == _NodeKind.radio), - 'heading-level' => - value is int && value >= 1 && value <= 6 && kind == _NodeKind.heading, - 'placeholder' => - value is String && (_isTextControl(kind) || kind == _NodeKind.select), - 'accessibility-label' => - value is String && - (_isButtonKind(kind) || - _isTextControl(kind) || - kind == _NodeKind.checkbox || - kind == _NodeKind.switchControl || - kind == _NodeKind.toggle || - kind == _NodeKind.radioGroup || - kind == _NodeKind.tabs || - kind == _NodeKind.buttonGroup || - kind == _NodeKind.toggleGroup || - kind == _NodeKind.breadcrumb || - kind == _NodeKind.pagination || - kind == _NodeKind.radio || - kind == _NodeKind.slider || - kind == _NodeKind.numberStepper || - kind == _NodeKind.avatar || - kind == _NodeKind.image || - kind == _NodeKind.mediaSurface || - kind == _NodeKind.tree || - kind == _NodeKind.resizable || - kind == _NodeKind.split || - kind == _NodeKind.drawer || - kind == _NodeKind.alert || - kind == _NodeKind.bubble || - kind == _NodeKind.select || - kind == _NodeKind.menuTrigger || - kind == _NodeKind.link || - kind == _NodeKind.fileImage || - _isTreeRowKind(kind)), - 'text-alignment' => - value is String && - _textAlignments.contains(value) && - (_isButtonKind(kind) || - kind == _NodeKind.text || - kind == _NodeKind.tableCell || - kind == _NodeKind.bubble || - kind == _NodeKind.statusBar), - 'role' => - (value == 'treeitem' && _isTreeRowKind(kind)) || - (value == 'menu' && kind == _NodeKind.popover), - 'tree-level' => value is int && value > 0 && _isTreeRowKind(kind), - 'expanded' => value is bool && _isTreeRowKind(kind), - 'key' => - value is String && - (kind == _NodeKind.listItem || kind == _NodeKind.listSection), - 'separator' => - (value == 'visible' || value == 'hidden') && - (kind == _NodeKind.listItem || kind == _NodeKind.listSection), - 'style' => - const {'plain', 'inset', 'inset-grouped'}.contains(value) && - kind == _NodeKind.list, - 'scroll-target' => value is String && kind == _NodeKind.list, - 'scroll-anchor' => - const {'top', 'center', 'bottom'}.contains(value) && - kind == _NodeKind.list, - 'scroll-token' => value is int && value >= 0 && kind == _NodeKind.list, - 'scroll-animated' || - 'track-visible-range' => value is bool && kind == _NodeKind.list, - 'edge' => - (value == 'leading' || value == 'trailing') && - kind == _NodeKind.swipeAction, - _ => false, - }; - } - - static void _validateStates(Map states) { - for (final state in states.values) { - if (state.kind == _NodeKind.root) { - if (state.parent != null) { - throw const LUIBackendException('runtime root cannot have a parent'); - } - if (state.children.length != 1) { - throw const LUIBackendException( - 'runtime root requires exactly one child', - ); - } - } - _validateSizeAxis(state, 'width', 'min-width', 'max-width'); - _validateSizeAxis(state, 'height', 'min-height', 'max-height'); - if (state.kind == _NodeKind.icon && - !state.properties.containsKey('name')) { - throw const LUIBackendException('icon requires name'); - } - if (state.kind == _NodeKind.edgeInset && - !state.properties.containsKey('edge')) { - throw const LUIBackendException('edge-inset requires edge'); - } - if (_isButtonKind(state.kind)) { - final text = state.properties['text'] as String? ?? ''; - final label = state.properties['accessibility-label'] as String? ?? ''; - final icon = state.properties['icon'] as String? ?? ''; - if (text.isEmpty && icon.isNotEmpty && label.isEmpty) { - throw const LUIBackendException('icon-only button requires label'); - } - if (text.isEmpty && label.isEmpty) { - throw const LUIBackendException('button requires an accessible name'); - } - } - if (state.kind == _NodeKind.toggle || state.kind == _NodeKind.radio) { - final text = state.properties['text'] as String? ?? ''; - final label = state.properties['accessibility-label'] as String? ?? ''; - if (text.isEmpty && label.isEmpty) { - throw const LUIBackendException( - 'value control requires an accessible name', - ); - } - } - if (state.kind == _NodeKind.radioGroup || - state.kind == _NodeKind.slider) { - final label = state.properties['accessibility-label'] as String? ?? ''; - if (label.isEmpty) { - throw const LUIBackendException( - 'value control requires an accessibility label', - ); - } - } - if (state.kind == _NodeKind.numberStepper) { - final value = state.properties['value']; - if (value is! double || !value.isFinite) { - throw const LUIBackendException( - 'number-stepper requires a finite value', - ); - } - final text = state.properties['text'] as String? ?? ''; - final label = - state.properties['accessibility-label'] as String? ?? ''; - if (text.isEmpty && label.isEmpty) { - throw const LUIBackendException('number-stepper requires a label'); - } - // min defaults to 0 and max is unbounded; compare the effective - // endpoints so a lone negative max still fails. - final minimum = (state.properties['min'] as num?)?.toDouble() ?? 0.0; - final maximum = - (state.properties['max'] as num?)?.toDouble() ?? double.maxFinite; - if (minimum > maximum) { - throw const LUIBackendException( - 'number-stepper min must not exceed max', - ); - } - } - if (state.kind == _NodeKind.split) { - if (state.children.length != 2) { - throw const LUIBackendException( - 'split requires exactly two children', - ); - } - final value = state.properties['value']; - if (value is! double || !value.isFinite) { - throw const LUIBackendException( - 'split requires a finite fractional value', - ); - } - final duration = state.properties['resize-duration'] as int? ?? 0; - if ((state.properties.containsKey('resize-easing') || - state.properties.containsKey('resize-origin')) && - duration <= 0) { - throw const LUIBackendException( - 'split animation options require a positive duration', - ); - } - } - if (state.kind == _NodeKind.drawer && state.children.length != 2) { - throw const LUIBackendException('drawer requires exactly two children'); - } - if (state.kind == _NodeKind.radio && - !_hasAncestor(states, state.parent, _NodeKind.radioGroup)) { - throw const LUIBackendException( - 'radio must be contained by a radio-group', - ); - } - if (state.kind == _NodeKind.select || state.kind == _NodeKind.combobox) { - final text = state.properties['text'] as String? ?? ''; - final placeholder = state.properties['placeholder'] as String? ?? ''; - if (text.isEmpty && placeholder.isEmpty) { - throw const LUIBackendException( - 'picker trigger requires text or placeholder', - ); - } - } - if (state.kind == _NodeKind.menuItem && - (state.properties['text'] as String? ?? '').isEmpty) { - throw const LUIBackendException('menu-item requires text'); - } - if (state.kind == _NodeKind.contextMenu) { - final parent = state.parent == null ? null : states[state.parent]; - if (parent == null || !_isContextMenuHost(parent)) { - throw const LUIBackendException( - 'context-menu requires an interactive direct host', - ); - } - final count = parent.children - .where((child) => states[child]?.kind == _NodeKind.contextMenu) - .length; - if (count != 1) { - throw const LUIBackendException( - 'host accepts at most one context-menu', - ); - } - for (final childID in state.children) { - final child = _requireState(states, childID); - if (child.kind == _NodeKind.menuItem) { - if (child.properties['press-enabled'] != true) { - throw const LUIBackendException( - 'context-menu menu-item requires press support', - ); - } - if (child.children.isNotEmpty) { - throw const LUIBackendException( - 'context-menu does not support nested menus', - ); - } - const allowed = { - 'text', - 'icon', - 'foreground', - 'enabled', - 'press-enabled', - 'variant', - 'accessibility-identifier', - }; - if (!child.properties.keys.every(allowed.contains)) { - throw const LUIBackendException( - 'context-menu menu-item has unsupported metadata', - ); - } - } else if (child.properties.isNotEmpty && - !(child.properties.length == 2 && - child.properties['orientation'] == 'horizontal' && - child.properties['style-class'] == 'lui-separator')) { - throw const LUIBackendException( - 'context-menu separator accepts no attributes', - ); - } - } - } - if (state.kind.isModalSurface && - (state.properties['text'] as String? ?? '').isEmpty) { - throw const LUIBackendException('modal surface requires text'); - } - if (state.kind == _NodeKind.tooltip) { - if ((state.properties['text'] as String? ?? '').isEmpty) { - throw const LUIBackendException('tooltip requires text'); - } - if (state.properties.containsKey('tooltip-delay') && - !state.properties.containsKey('anchor')) { - throw const LUIBackendException('tooltip-delay requires anchor'); - } - } - if (state.kind == _NodeKind.accordion && - (state.properties['text'] as String? ?? '').isEmpty) { - throw const LUIBackendException('accordion requires text'); - } - if (state.kind == _NodeKind.tree && - (state.properties['accessibility-label'] as String? ?? '').isEmpty) { - throw const LUIBackendException('tree requires an accessibility label'); - } - if (state.kind == _NodeKind.toolbar && - (state.properties['accessibility-label'] as String? ?? '').isEmpty) { - throw const LUIBackendException( - 'toolbar requires an accessibility label', - ); - } - if (state.kind == _NodeKind.bottomTabs) { - if ((state.properties['accessibility-label'] as String? ?? '') - .isEmpty) { - throw const LUIBackendException( - 'bottom-tabs requires an accessibility label', - ); - } - if (state.children.length < 2 || state.children.length > 5) { - throw const LUIBackendException( - 'bottom-tabs requires two to five destinations', - ); - } - } - if (state.kind == _NodeKind.bottomTab && - ((state.properties['title'] as String? ?? '').isEmpty || - state.properties['press-enabled'] != true || - state.children.isEmpty || - state.parent == null || - states[state.parent]?.kind != _NodeKind.bottomTabs)) { - throw const LUIBackendException( - 'bottom-tab requires title, press support, content, and a direct bottom-tabs parent', - ); - } - final hasTreeMetadata = - state.properties.containsKey('role') || - state.properties.containsKey('tree-level') || - state.properties.containsKey('expanded'); - if (hasTreeMetadata) { - // A list-item inside a list may carry expansion state as a - // disclosure row without the treeitem role. - final isDisclosureItem = - state.kind == _NodeKind.listItem && - !state.properties.containsKey('role') && - !state.properties.containsKey('tree-level') && - !state.properties.containsKey('change-enabled') && - _hasAncestor(states, state.parent, _NodeKind.list); - if (!isDisclosureItem && - (state.properties['role'] != 'treeitem' || - !_hasAncestor(states, state.parent, _NodeKind.tree))) { - throw const LUIBackendException( - 'tree row metadata requires a treeitem inside tree', - ); - } - if (state.properties.containsKey('expanded') && - state.properties['toggle-enabled'] != true) { - throw const LUIBackendException( - 'expanded treeitem requires toggle support', - ); - } - } - if (state.kind == _NodeKind.dropdownMenu || - state.kind == _NodeKind.tooltip) { - if (state.properties.containsKey('anchor-alignment') && - !state.properties.containsKey('anchor')) { - throw const LUIBackendException('anchor-alignment requires anchor'); - } - if (state.properties.containsKey('anchor-offset') && - !state.properties.containsKey('anchor')) { - throw const LUIBackendException('anchor-offset requires anchor'); - } - } - if (state.kind == _NodeKind.listItem) { - final disclosure = state.properties.containsKey('expanded'); - final hasText = (state.properties['text'] as String? ?? '').isNotEmpty; - final hasChildren = state.children.any((childID) { - final child = states[childID]; - if (child == null || - child.kind == _NodeKind.contextMenu || - child.kind == _NodeKind.swipeActions) { - return false; - } - // Nested list items on a disclosure row are rows, not content. - return !(disclosure && child.kind == _NodeKind.listItem); - }); - if (!hasText && !hasChildren) { - throw const LUIBackendException( - 'list-item requires text or children', - ); - } - if (state.children.any( - (child) => states[child]?.kind == _NodeKind.listItem, - ) && - !state.properties.containsKey('expanded')) { - throw const LUIBackendException( - 'nested list-item children require expanded', - ); - } - } - if (state.kind == _NodeKind.swipeAction) { - final text = state.properties['text'] as String? ?? ''; - final icon = state.properties['icon'] as String? ?? ''; - if (text.isEmpty && icon.isEmpty) { - throw const LUIBackendException('swipe-action requires text or icon'); - } - } - if (state.kind == _NodeKind.avatar || state.kind == _NodeKind.image) { - if (state.kind == _NodeKind.image && - !state.properties.containsKey('image') && - (state.properties['url'] as String? ?? '').isEmpty) { - throw const LUIBackendException('image requires image or url'); - } - const sourceNames = { - 'source-x', - 'source-y', - 'source-width', - 'source-height', - }; - final sourceCount = sourceNames - .where(state.properties.containsKey) - .length; - final mediaKind = state.kind == _NodeKind.avatar ? 'avatar' : 'image'; - if (sourceCount != 0 && sourceCount != sourceNames.length) { - throw LUIBackendException( - '$mediaKind source crop requires all four coordinates', - ); - } - if (sourceCount == sourceNames.length) { - if (!state.properties.containsKey('image') && - (state.properties['url'] as String? ?? '').isEmpty) { - throw LUIBackendException( - '$mediaKind source crop requires an image', - ); - } - final x = (state.properties['source-x'] as num).toDouble(); - final y = (state.properties['source-y'] as num).toDouble(); - final width = (state.properties['source-width'] as num).toDouble(); - final height = (state.properties['source-height'] as num).toDouble(); - if (x < 0 || y < 0) { - throw LUIBackendException( - '$mediaKind source crop coordinates must be non-negative', - ); - } - if (width <= 0 || height <= 0) { - throw LUIBackendException( - '$mediaKind source crop dimensions must be positive', - ); - } - } - } - if (state.kind == _NodeKind.mediaSurface && - !state.properties.containsKey('surface')) { - throw const LUIBackendException('media-surface requires surface'); - } - if ((state.kind == _NodeKind.fileImage || - state.kind == _NodeKind.filePreview) && - (state.properties['path'] as String? ?? '').isEmpty) { - throw LUIBackendException( - '${state.kind == _NodeKind.fileImage ? 'file-image' : 'file-preview'} requires a non-empty path', - ); - } - if (state.kind == _NodeKind.link && - (state.properties['url'] as String? ?? '').isEmpty) { - throw const LUIBackendException('link requires a non-empty url'); - } - if (state.kind == _NodeKind.stepper && - !state.properties.containsKey('active')) { - throw const LUIBackendException('stepper requires active'); - } - if (state.kind == _NodeKind.step) { - if ((state.properties['text'] as String? ?? '').isEmpty || - state.parent == null || - states[state.parent]?.kind != _NodeKind.stepper) { - throw const LUIBackendException( - 'step requires text and a direct stepper parent', - ); - } - } - if (state.kind == _NodeKind.timelineItem) { - if ((state.properties['title'] as String? ?? '').isEmpty) { - throw const LUIBackendException('timeline-item requires title'); - } - if (state.parent == null || - states[state.parent]?.kind != _NodeKind.timeline) { - throw const LUIBackendException( - 'timeline-item requires a direct timeline parent', - ); - } - } - if (state.kind == _NodeKind.inputGroup) { - if (state.children.isEmpty || state.children.length > 2) { - throw const LUIBackendException( - 'input-group requires one textarea and optional actions', - ); - } - if (states[state.children.first]?.kind != _NodeKind.textarea || - (state.children.length == 2 && - states[state.children[1]]?.kind != - _NodeKind.inputGroupActions)) { - throw const LUIBackendException( - 'input-group requires textarea first and actions second', - ); - } - } - if (state.kind == _NodeKind.inputGroupActions && - (state.parent == null || - states[state.parent]?.kind != _NodeKind.inputGroup)) { - throw const LUIBackendException( - 'input-group-actions requires a direct input-group parent', - ); - } - } - } - - void _validateExtensionStates( - Map states, - Map extensions, - ) { - for (final entry in extensions.entries) { - final id = entry.key; - final state = entry.value; - final registration = _extensionRegistry._registration(state.identifier); - if (registration == null || - registration.fingerprint != state.fingerprint) { - throw const LUIBackendException('invalid extension registration'); - } - if (registration.isTweak && state.children.length != 1) { - throw const LUIBackendException( - 'platform tweak requires exactly one child', - ); - } - final properties = { - for (final property in registration.properties) property.name: property, - }; - if (!state.properties.keys.every(properties.containsKey)) { - throw const LUIBackendException('unknown extension property'); - } - for (final property in registration.properties) { - final value = state.properties[property.name]; - if (value == null) { - if (property.isRequired) { - throw const LUIBackendException( - 'missing required extension property', - ); - } - } else if (!property.kind.accepts(value)) { - throw const LUIBackendException('invalid extension property'); - } - } - final parent = state.parent; - if (parent != null && - !_nodeChildren(states, extensions, parent).contains(id)) { - throw const LUIBackendException('extension parent is inconsistent'); - } - for (final child in state.children) { - if (_nodeParent(states, extensions, child) != id) { - throw const LUIBackendException('extension child is inconsistent'); - } - } - } - } - - static void _validateSizeAxis( - _NodeState state, - String fixedProperty, - String minProperty, - String maxProperty, - ) { - final minimum = state.properties[minProperty] as int? ?? 0; - final maximum = state.properties[maxProperty] as int? ?? 0x7fffffffffffffff; - final fixed = state.properties[fixedProperty] as int? ?? minimum; - if (minimum > maximum || fixed < minimum || fixed > maximum) { - throw const LUIBackendException('surface size constraints conflict'); - } - } - - static bool _canContainChildren(_NodeKind kind) => - kind == _NodeKind.root || - kind == _NodeKind.row || - kind == _NodeKind.column || - kind == _NodeKind.grid || - kind == _NodeKind.stack || - kind == _NodeKind.panel || - kind == _NodeKind.card || - kind == _NodeKind.box || - kind == _NodeKind.scroll || - kind == _NodeKind.list || - kind == _NodeKind.virtualList || - _isHorizontalGroupKind(kind) || - kind == _NodeKind.radioGroup || - kind == _NodeKind.dropdownMenu || - kind == _NodeKind.contextMenu || - kind == _NodeKind.listItem || - kind == _NodeKind.accordion || - kind == _NodeKind.table || - kind == _NodeKind.tableRow || - kind == _NodeKind.tree || - kind == _NodeKind.resizable || - kind == _NodeKind.split || - kind == _NodeKind.drawer || - kind == _NodeKind.alert || - kind == _NodeKind.bubble || - kind == _NodeKind.stepper || - kind == _NodeKind.timeline || - kind == _NodeKind.inputGroup || - kind == _NodeKind.menuTrigger || - kind == _NodeKind.inputGroupActions || - kind == _NodeKind.toast || - kind == _NodeKind.toolbar || - kind == _NodeKind.bottomTabs || - kind == _NodeKind.bottomTab || - kind == _NodeKind.filePicker || - kind == _NodeKind.link || - _isContextMenuLeafHost(kind) || - kind == _NodeKind.listSection || - kind == _NodeKind.listSectionHeader || - kind == _NodeKind.listSectionFooter || - kind == _NodeKind.swipeActions || - kind == _NodeKind.popover || - kind == _NodeKind.edgeInset || - kind == _NodeKind.overlay || - kind == _NodeKind.viewThatFits || - kind.isModalSurface; - - static bool _acceptsExtensionChildren(_NodeKind kind) => - kind == _NodeKind.root || - kind == _NodeKind.row || - kind == _NodeKind.column || - kind == _NodeKind.grid || - kind == _NodeKind.stack || - kind == _NodeKind.edgeInset || - kind == _NodeKind.overlay || - kind == _NodeKind.viewThatFits || - kind == _NodeKind.panel || - kind == _NodeKind.card || - kind == _NodeKind.box || - kind == _NodeKind.scroll || - kind == _NodeKind.list || - kind == _NodeKind.virtualList || - kind == _NodeKind.listItem || - kind == _NodeKind.dialog || - kind == _NodeKind.sheet || - kind == _NodeKind.accordion || - kind == _NodeKind.resizable || - kind == _NodeKind.split || - kind == _NodeKind.drawer || - kind == _NodeKind.alert || - kind == _NodeKind.bubble || - kind == _NodeKind.toast || - kind == _NodeKind.toolbar || - kind == _NodeKind.bottomTab || - kind == _NodeKind.listSection || - kind == _NodeKind.listSectionHeader || - kind == _NodeKind.listSectionFooter; - - static bool _isToolbarChild(_NodeKind kind) => - kind == _NodeKind.button || - kind == _NodeKind.toggleButton || - kind == _NodeKind.buttonGroup || - kind == _NodeKind.toggleGroup || - kind == _NodeKind.checkbox || - kind == _NodeKind.switchControl || - kind == _NodeKind.toggle || - kind == _NodeKind.radioGroup || - kind == _NodeKind.select || - kind == _NodeKind.combobox || - kind == _NodeKind.textField || - kind == _NodeKind.secureField || - kind == _NodeKind.input || - kind == _NodeKind.searchField || - kind == _NodeKind.menuItem || - kind == _NodeKind.menuTrigger || - kind == _NodeKind.spacer || - kind == _NodeKind.divider || - kind == _NodeKind.text; - - static bool _isContextMenuLeafHost(_NodeKind kind) { - const kinds = { - _NodeKind.button, - _NodeKind.toggleButton, - _NodeKind.toggle, - _NodeKind.radio, - _NodeKind.slider, - _NodeKind.numberStepper, - _NodeKind.textField, - _NodeKind.secureField, - _NodeKind.input, - _NodeKind.searchField, - _NodeKind.textarea, - _NodeKind.checkbox, - _NodeKind.switchControl, - _NodeKind.select, - _NodeKind.combobox, - _NodeKind.menuItem, - _NodeKind.text, - _NodeKind.tableCell, - }; - return kinds.contains(kind); - } - - static bool _isContextMenuHost(_NodeState state) { - const inherent = { - _NodeKind.button, - _NodeKind.toggleButton, - _NodeKind.toggle, - _NodeKind.radio, - _NodeKind.slider, - _NodeKind.numberStepper, - _NodeKind.textField, - _NodeKind.secureField, - _NodeKind.input, - _NodeKind.searchField, - _NodeKind.textarea, - _NodeKind.checkbox, - _NodeKind.switchControl, - _NodeKind.select, - _NodeKind.combobox, - _NodeKind.menuItem, - _NodeKind.listItem, - _NodeKind.accordion, - _NodeKind.text, - _NodeKind.tableCell, - }; - return inherent.contains(state.kind) || - state.properties['press-enabled'] == true || - state.properties['double-press-enabled'] == true || - state.properties['toggle-enabled'] == true || - state.properties['long-press-enabled'] == true; - } - - int? _checkedRadio(_NodeState root) { - for (final child in root.children) { - final state = _requireState(_states, child); - if (state.kind == _NodeKind.radio && - state.properties['checked'] == true) { - return child; - } - final nested = _checkedRadio(state); - if (nested != null) return nested; - } - return null; - } - - static bool _hasAncestor( - Map states, - int? parent, - _NodeKind kind, - ) { - if (parent == null) return false; - final state = _requireState(states, parent); - return state.kind == kind || _hasAncestor(states, state.parent, kind); - } - - static bool _isButtonKind(_NodeKind kind) => - kind == _NodeKind.button || kind == _NodeKind.toggleButton; - - static bool _isHorizontalGroupKind(_NodeKind kind) => - kind == _NodeKind.tabs || - kind == _NodeKind.buttonGroup || - kind == _NodeKind.toggleGroup || - kind == _NodeKind.breadcrumb || - kind == _NodeKind.pagination; - - static bool _isTreeRowKind(_NodeKind kind) => - kind == _NodeKind.row || - kind == _NodeKind.column || - kind == _NodeKind.panel || - kind == _NodeKind.card || - kind == _NodeKind.box || - kind == _NodeKind.listItem; - - static int _horizontalGroupDefaultGap(_NodeKind kind) => - kind == _NodeKind.pagination ? 2 : 4; - - static bool _isHorizontalGroupChild( - _NodeKind groupKind, - _NodeKind childKind, - ) => switch (groupKind) { - _NodeKind.tabs => childKind == _NodeKind.button, - _NodeKind.buttonGroup => _isButtonKind(childKind), - _NodeKind.toggleGroup => childKind == _NodeKind.toggleButton, - _NodeKind.breadcrumb || - _NodeKind.pagination => childKind == _NodeKind.button, - _ => false, - }; - - static bool _isTextControl(_NodeKind kind) => - kind == _NodeKind.textField || - kind == _NodeKind.secureField || - kind == _NodeKind.input || - kind == _NodeKind.searchField || - kind == _NodeKind.textarea || - kind == _NodeKind.combobox; - - static bool _containsState( - Map states, - Map extensions, - int id, - ) => states.containsKey(id) || extensions.containsKey(id); - - static int? _nodeParent( - Map states, - Map extensions, - int id, - ) { - final standard = states[id]; - if (standard != null) return standard.parent; - return _requireExtensionStateFrom(extensions, id).parent; - } - - static List _nodeChildren( - Map states, - Map extensions, - int id, - ) { - final standard = states[id]; - if (standard != null) return standard.children; - return _requireExtensionStateFrom(extensions, id).children; - } - - static void _setNodeParent( - Map states, - Map extensions, - int id, - int? parent, - ) { - final standard = states[id]; - if (standard != null) { - standard.parent = parent; - return; - } - _requireExtensionStateFrom(extensions, id).parent = parent; - } - - static bool _isDescendantAny( - Map states, - Map extensions, { - required int target, - required int root, - }) { - if (target == root) return true; - return _nodeChildren(states, extensions, root).any( - (child) => - _isDescendantAny(states, extensions, target: target, root: child), - ); - } - - static _NodeState _requireState(Map states, int id) { - final node = states[id]; - if (node == null) throw LUIBackendException('unknown node $id'); - return node; - } - - _NodeHandle _requireHandle(int id) { - final handle = _handles[id]; - if (handle == null) throw LUIBackendException('unknown node $id'); - return handle; - } - - static _ExtensionNodeState _requireExtensionStateFrom( - Map states, - int id, - ) { - final node = states[id]; - if (node == null) throw LUIBackendException('unknown extension node $id'); - return node; - } - - _ExtensionNodeState _requireExtensionState(int id) => - _requireExtensionStateFrom(_extensionStates, id); - - _ExtensionNodeHandle _requireExtensionHandle(int id) { - final handle = _extensionHandles[id]; - if (handle == null) { - throw LUIBackendException('unknown extension node $id'); - } - return handle; - } - - static Map _objectMap(Object? value, String name) { - if (value is! Map) { - throw LUIBackendException('$name must be an object'); - } - return value; - } - - static List _objectList(Object? value, String name) { - if (value is! List) { - throw LUIBackendException('$name must be an array'); - } - return value; - } - - static int _integer(Object? value, String name) { - if (value is! int) throw LUIBackendException('$name must be an integer'); - return value; - } - - static String _string(Object? value, String name) { - if (value is! String) throw LUIBackendException('$name must be a string'); - return value; - } - - static Map? _themeTokens(Object? value) { - if (value is! String || value.isEmpty) return null; - try { - final decoded = jsonDecode(value); - if (decoded is! Map) return null; - return { - for (final entry in decoded.entries) - entry.key.toString().toLowerCase(): entry.value, - }; - } on FormatException { - return null; - } - } - - // A token value is either a plain color string or a {"light", "dark"} - // object picked by the effective brightness. - static String? _tokenValue(BuildContext context, Object? value) { - if (value is String) return value; - if (value is Map) { - final dark = Theme.of(context).brightness == Brightness.dark; - final picked = dark ? value['dark'] : value['light']; - if (picked is String) return picked; - } - return null; - } - - static Color? _tokenColor(String? value) { - if (value == null) return null; - var raw = value.startsWith('#') ? value.substring(1) : value; - if (raw.length == 3 || raw.length == 4) { - raw = raw.split('').map((c) => c + c).join(); - } - if (raw.length == 6) { - raw = 'ff$raw'; - } else if (raw.length == 8) { - // CSS hex is #rrggbbaa; Color() takes ARGB. - raw = raw.substring(6) + raw.substring(0, 6); - } - if (raw.length != 8) return null; - final parsed = int.tryParse(raw, radix: 16); - return parsed == null ? null : Color(parsed); - } - - static ThemeData _themeForMode(BuildContext context, {required bool dark}) { - final base = Theme.of(context); - final brightness = dark ? Brightness.dark : Brightness.light; - return base.copyWith( - colorScheme: ColorScheme.fromSeed( - seedColor: base.colorScheme.primary, - brightness: brightness, - ), - ); - } - - static Color? _color( - BuildContext context, - String? name, { - bool foreground = false, - }) { - final colors = Theme.of(context).colorScheme; - final token = _tokenValue( - context, - _LUIThemeScope.maybeTokens(context)?[name?.toLowerCase()], - ); - if (token != null) { - final parsed = _tokenColor(token); - if (parsed != null) return parsed; - } - return switch (name?.toLowerCase()) { - null => null, - 'transparent' => Colors.transparent, - 'background' => colors.surface, - 'foreground' => colors.onSurface, - 'surface' => colors.surface, - 'surface-container-lowest' => colors.surfaceContainerLowest, - 'surface-container-low' => colors.surfaceContainerLow, - 'surface-container' => colors.surfaceContainer, - 'surface-container-high' => colors.surfaceContainerHigh, - 'surface-container-highest' => colors.surfaceContainerHighest, - 'surface-variant' => colors.surfaceContainerHighest, - 'autocomplete-row-background' => colors.surfaceContainerHigh, - 'glass' => colors.surface.withValues(alpha: 0.75), - 'glass-fallback' => colors.surface.withValues(alpha: 0.9), - // Apple Material.bar approximation: mostly-opaque theme surface. - 'bar' => colors.surface.withValues(alpha: 0.85), - 'primary' => colors.primary, - 'primary-foreground' => colors.onPrimary, - 'accent' => foreground ? colors.primary : colors.secondaryContainer, - 'accent-foreground' => - foreground ? colors.onPrimary : colors.onSecondaryContainer, - 'secondary' => - foreground ? colors.onSurfaceVariant : colors.secondaryContainer, - 'secondary-foreground' => colors.onSecondaryContainer, - 'muted' => colors.surfaceContainerHigh, - 'muted-foreground' => colors.onSurfaceVariant, - 'destructive' => colors.error, - 'destructive-foreground' => colors.onError, - 'success' => colors.tertiaryContainer, - 'success-foreground' => colors.onTertiaryContainer, - 'warning' => colors.secondaryContainer, - 'warning-foreground' => colors.onSecondaryContainer, - 'error' => colors.errorContainer, - 'error-foreground' => colors.onErrorContainer, - 'border' => colors.outlineVariant, - 'black' => Colors.black, - 'white' => Colors.white, - 'red' => Colors.red, - 'blue' => Colors.blue, - 'green' => Colors.green, - _ => Colors.transparent, - }; - } - - static TextStyle? _headingStyle(BuildContext context, int level) { - final textTheme = Theme.of(context).textTheme; - return switch (level) { - 1 => textTheme.headlineLarge, - 2 => textTheme.headlineMedium, - 3 => textTheme.titleLarge, - 4 => textTheme.titleMedium, - 5 => textTheme.titleSmall, - _ => textTheme.labelLarge, - }; - } - - static TextStyle? _textStyleForSize(BuildContext context, String? size) { - final textTheme = Theme.of(context).textTheme; - return switch (size) { - 'sm' => textTheme.bodySmall, - 'lg' => textTheme.titleMedium, - 'heading' => textTheme.headlineSmall, - 'display' => textTheme.displaySmall, - _ => textTheme.bodyMedium, - }; - } - - // Parses the `detents` prop into viewport-height fractions - // (`medium` = 0.5, `large` = 1.0, or a literal fraction in (0, 1]). - // Unknown tokens are ignored; returns null when nothing parses. - static List? _sheetDetentFractions(String? detents) { - if (detents == null) return null; - final fractions = detents - .split(',') - .map((token) { - final trimmed = token.trim(); - if (trimmed == 'medium') return 0.5; - if (trimmed == 'large') return 1.0; - final fraction = double.tryParse(trimmed); - return fraction != null && fraction > 0 && fraction <= 1 - ? fraction - : null; - }) - .nonNulls - .toList(growable: false); - return fractions.isEmpty ? null : fractions; - } - - Widget _modalSurface(BuildContext context, int node) { - final state = _requireState(_states, node); - final width = (state.properties['width'] as int?)?.toDouble(); - final height = (state.properties['height'] as int?)?.toDouble(); - return switch (state.kind) { - _NodeKind.dialog => Dialog( - child: SizedBox( - key: ValueKey('lui-dialog-surface-$node'), - width: width ?? 420, - height: height ?? 220, - child: _modalSurfaceBody(context, state), - ), - ), - _NodeKind.sheet => () { - // Approximate iOS presentation options: detents cap/choose the - // sheet height fraction (medium 0.5, large 1.0, or a literal - // fraction); sizing constrains width (form/fitted ~560px, page - // full width). The bottom sheet always opens at the first detent. - final size = MediaQuery.of(context).size; - final fractions = _sheetDetentFractions( - state.properties['detents'] as String?, - ); - final detentMaxHeight = fractions == null - ? null - : size.height * fractions.reduce(math.max); - final sizingMaxWidth = switch (state.properties['sizing']) { - 'form' || 'fitted' => 560.0, - _ => null, - }; - Widget surface = SizedBox( - key: ValueKey('lui-sheet-surface-$node'), - width: width ?? double.infinity, - height: height ?? - (fractions == null ? null : size.height * fractions.first), - child: SafeArea(child: _modalSurfaceBody(context, state)), - ); - if (detentMaxHeight != null || sizingMaxWidth != null) { - surface = Align( - alignment: Alignment.bottomCenter, - heightFactor: 1.0, - child: ConstrainedBox( - constraints: BoxConstraints( - maxHeight: detentMaxHeight ?? double.infinity, - maxWidth: sizingMaxWidth ?? double.infinity, - ), - child: surface, - ), - ); - } - return surface; - }(), - _ => throw const LUIBackendException('node is not a modal surface'), - }; - } - - Widget _modalSurfaceBody(BuildContext context, _NodeState state) { - final children = state.children - .map((child) => widget(node: child)) - .toList(growable: false); - return Padding( - padding: EdgeInsets.all( - (state.properties['padding'] as int? ?? 24).toDouble(), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - state.properties['text'] as String? ?? '', - style: Theme.of(context).textTheme.titleLarge, - ), - const SizedBox(height: 16), - Expanded(child: Stack(children: children)), - ], - ), - ); - } - - static MainAxisAlignment _mainAxisAlignment(String value) => switch (value) { - 'center' => MainAxisAlignment.center, - 'end' => MainAxisAlignment.end, - 'space_between' => MainAxisAlignment.spaceBetween, - _ => MainAxisAlignment.start, - }; - - static CrossAxisAlignment _crossAxisAlignment( - String value, { - required bool canStretch, - }) => switch (value) { - 'start' => CrossAxisAlignment.start, - 'center' => CrossAxisAlignment.center, - 'end' => CrossAxisAlignment.end, - _ => canStretch ? CrossAxisAlignment.stretch : CrossAxisAlignment.start, - }; - - static const _mainAlignments = {'start', 'center', 'end', 'space_between'}; - static const _themeModes = {'system', 'light', 'dark'}; - - static const _crossAlignments = {'stretch', 'start', 'center', 'end'}; - - static const _controlSizes = {'default', 'sm', 'lg', 'icon'}; - - static const _textSizes = {'heading', 'display'}; - - static const _textAlignments = {'start', 'center', 'end'}; - - static const _buttonVariants = { - 'default', - 'primary', - 'secondary', - 'outline', - 'ghost', - 'destructive', - }; -} - -final class _LUIToast extends StatefulWidget { - const _LUIToast({ - required this.node, - required this.duration, - required this.label, - required this.onDismiss, - required this.child, - }); - - final int node; - final Duration duration; - final String label; - final VoidCallback onDismiss; - final Widget child; - - @override - State<_LUIToast> createState() => _LUIToastState(); -} - -final class _LUIToastState extends State<_LUIToast> { - Timer? _timer; - var _dismissed = false; - - @override - void initState() { - super.initState(); - _scheduleDismissal(); - } - - @override - void didUpdateWidget(_LUIToast oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.duration != widget.duration) _scheduleDismissal(); - } - - @override - void dispose() { - _timer?.cancel(); - super.dispose(); - } - - @override - Widget build(BuildContext context) => Semantics( - container: true, - liveRegion: true, - label: widget.label, - child: MouseRegion( - onEnter: (_) => _timer?.cancel(), - onExit: (_) => _scheduleDismissal(), - child: Dismissible( - key: ValueKey('lui-toast-${widget.node}'), - direction: DismissDirection.horizontal, - onDismissed: (_) => _dismiss(), - child: widget.child, - ), - ), - ); - - void _scheduleDismissal() { - _timer?.cancel(); - if (_dismissed || widget.duration == Duration.zero) return; - _timer = Timer(widget.duration, _dismiss); - } - - void _dismiss() { - if (_dismissed) return; - _dismissed = true; - _timer?.cancel(); - widget.onDismiss(); - } -} - -final class _LUITableRowSurface extends StatefulWidget { - const _LUITableRowSurface({ - required this.selected, - required this.showDivider, - required this.child, - }); - - final bool selected; - final bool showDivider; - final Widget child; - - @override - State<_LUITableRowSurface> createState() => _LUITableRowSurfaceState(); -} - -final class _LUITableRowSurfaceState extends State<_LUITableRowSurface> { - var hovered = false; - - @override - Widget build(BuildContext context) { - final colors = Theme.of(context).colorScheme; - return MouseRegion( - onEnter: (_) => setState(() => hovered = true), - onExit: (_) => setState(() => hovered = false), - child: ColoredBox( - color: widget.selected || hovered - ? colors.secondaryContainer - : Colors.transparent, - child: DecoratedBox( - decoration: BoxDecoration( - border: widget.showDivider - ? Border(bottom: BorderSide(color: colors.outlineVariant)) - : null, - ), - child: widget.child, - ), - ), - ); - } -} - -final class _LUIInputGroupSurface extends StatefulWidget { - const _LUIInputGroupSurface({required this.child}); - - final Widget child; - - @override - State<_LUIInputGroupSurface> createState() => _LUIInputGroupSurfaceState(); -} - -final class _LUIInputGroupSurfaceState extends State<_LUIInputGroupSurface> { - var focused = false; - - @override - Widget build(BuildContext context) { - final colors = Theme.of(context).colorScheme; - return Focus( - canRequestFocus: false, - onFocusChange: (value) => setState(() => focused = value), - child: AnimatedContainer( - duration: const Duration(milliseconds: 100), - decoration: BoxDecoration( - color: colors.surface, - border: Border.all( - color: focused ? colors.primary : colors.outlineVariant, - width: focused ? 2 : 1, - ), - borderRadius: BorderRadius.circular(8), - ), - clipBehavior: Clip.antiAlias, - child: widget.child, - ), - ); - } -} - -class _LUIModalPresenter extends StatefulWidget { - const _LUIModalPresenter({required this.backend, required this.node}); - - final LUIFlutterBackend backend; - final int node; - - @override - State<_LUIModalPresenter> createState() => _LUIModalPresenterState(); -} - -class _LUIModalPresenterState extends State<_LUIModalPresenter> { - NavigatorState? _navigator; - Route? _route; - - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) => _present()); - } - - void _present() { - // The node can be dropped between mount and this post-frame callback; - // presenting for a stale node would throw in _requireState. - if (!mounted || - _route != null || - !widget.backend._states.containsKey(widget.node)) { - return; - } - final navigator = Navigator.of(context, rootNavigator: true); - final route = _modalRoute(navigator); - _navigator = navigator; - _route = route; - widget.backend._registerModalRoute(widget.node, route, navigator); - navigator.push(route).whenComplete(() { - widget.backend._unregisterModalRoute(widget.node, route); - widget.backend._refreshSemanticsAfterModalRemoval(route); - if (!mounted || _route != route) return; - _route = null; - widget.backend.performDismiss(widget.node); - }); - } - - Route _modalRoute(NavigatorState navigator) { - final state = LUIFlutterBackend._requireState( - widget.backend._states, - widget.node, - ); - final localizations = MaterialLocalizations.of(context); - // The route renders outside the LUI tree, so neither ancestor token - // scopes nor the modal node's own `_withTheme` reach it. Capture the - // merged inherited scope at present time and re-apply the node's own - // scope inside the route so themed modals keep their overrides. - final inheritedTokens = _LUIThemeScope.maybeTokens(context); - final capturedThemes = InheritedTheme.capture( - from: context, - to: navigator.context, - ); - Widget surface(BuildContext routeContext) { - if (!widget.backend._states.containsKey(widget.node)) { - return const SizedBox.shrink(); - } - Widget content = ListenableBuilder( - listenable: widget.backend._requireHandle(widget.node), - builder: (routeContext, _) => - widget.backend._states.containsKey(widget.node) - ? widget.backend._withTheme( - routeContext, - widget.node, - widget.backend._modalSurface(routeContext, widget.node), - ) - : const SizedBox.shrink(), - ); - return inheritedTokens == null - ? content - : _LUIThemeScope(tokens: {...inheritedTokens}, child: content); - } - - return switch (state.kind) { - _NodeKind.sheet => ModalBottomSheetRoute( - builder: surface, - capturedThemes: capturedThemes, - isScrollControlled: true, - barrierLabel: localizations.scrimLabel, - barrierOnTapHint: localizations.scrimOnTapHint( - localizations.bottomSheetLabel, - ), - isDismissible: true, - enableDrag: true, - showDragHandle: true, - useSafeArea: true, - ), - _NodeKind.dialog => DialogRoute( - context: context, - barrierDismissible: true, - barrierLabel: localizations.modalBarrierDismissLabel, - builder: surface, - themes: capturedThemes, - ), - _ => throw const LUIBackendException('node is not a modal surface'), - }; - } - - @override - void dispose() { - final route = _route; - final navigator = _navigator; - _route = null; - _navigator = null; - if (route != null) { - widget.backend._unregisterModalRoute(widget.node, route); - } - super.dispose(); - if (route != null && navigator != null) { - widget.backend._scheduleRouteRemoval(route, navigator); - } - } - - @override - Widget build(BuildContext context) => const SizedBox.shrink(); -} - -extension on _NodeKind { - bool get isModalSurface => - this == _NodeKind.dialog || this == _NodeKind.sheet; - - bool get isOverlaySurface => - this == _NodeKind.panel || - this == _NodeKind.card || - this == _NodeKind.alert || - this == _NodeKind.bubble || - this == _NodeKind.resizable; -} - -final class _LUIDrawer extends StatefulWidget { - const _LUIDrawer({ - required this.backend, - required this.sourcePresented, - required this.enabled, - required this.width, - required this.label, - required this.onChanged, - required this.main, - required this.panel, - }); - - final LUIFlutterBackend backend; - final bool sourcePresented; - final bool enabled; - final double width; - final String label; - final ValueChanged? onChanged; - final Widget main; - final Widget panel; - - @override - State<_LUIDrawer> createState() => _LUIDrawerState(); -} - -final class _LUIDrawerState extends State<_LUIDrawer> { - late bool _presented; - double _dragOffset = 0; - bool _dragging = false; - double? _reportedWidth; - - @override - void initState() { - super.initState(); - _presented = widget.sourcePresented; - } - - @override - void didUpdateWidget(covariant _LUIDrawer oldWidget) { - super.didUpdateWidget(oldWidget); - if (!widget.enabled) { - _dragging = false; - _dragOffset = 0; - } - if (widget.sourcePresented == oldWidget.sourcePresented) return; - _presented = widget.sourcePresented; - _dragOffset = 0; - } - - void _updatePresentation(bool presented) { - setState(() { - _presented = presented; - _dragOffset = 0; - }); - if (presented != widget.sourcePresented) widget.onChanged?.call(presented); - } - - @override - Widget build(BuildContext context) { - final availableWidth = _reportedWidth ?? widget.width; - final width = widget.width.clamp(0, availableWidth * 0.9).toDouble(); - final visibleWidth = ((_presented ? width : 0) + _dragOffset) - .clamp(0, width) - .toDouble(); - final progress = width == 0 ? 0.0 : visibleWidth / width; - final canToggle = widget.enabled && widget.onChanged != null && width > 0; - // Opening the drawer is an edge gesture: drags starting farther into - // the content only close it (or drag it further open once presented). - const edgeDragWidth = 24.0; - - return _LUIConstraintProbe( - onConstraints: (constraints) { - final next = constraints.hasBoundedWidth - ? constraints.maxWidth - : widget.width; - if (next != _reportedWidth) { - _reportedWidth = next; - SchedulerBinding.instance.addPostFrameCallback((_) { - if (mounted) { - setState(() {}); - widget.backend._scheduleLayoutSettle(); - } - }); - } - }, - child: PopScope( - canPop: !_presented, - onPopInvokedWithResult: (didPop, result) { - if (!didPop) _updatePresentation(false); - }, - child: GestureDetector( - behavior: HitTestBehavior.translucent, - onHorizontalDragStart: !canToggle - ? null - : (details) { - _dragging = - _presented || details.localPosition.dx <= edgeDragWidth; - }, - onHorizontalDragUpdate: !canToggle - ? null - : (details) { - if (!_dragging) return; - setState(() { - final next = _dragOffset + details.delta.dx; - _dragOffset = _presented - ? next.clamp(-width, 0).toDouble() - : next.clamp(0, width).toDouble(); - }); - }, - onHorizontalDragEnd: !canToggle - ? null - : (details) { - if (!_dragging) return; - _dragging = false; - final settledWidth = ((_presented ? width : 0) + _dragOffset) - .clamp(0, width) - .toDouble(); - final projectedWidth = - (settledWidth + (details.primaryVelocity ?? 0) * 0.2) - .clamp(0, width) - .toDouble(); - _updatePresentation(projectedWidth >= width * 0.5); - }, - child: Stack( - fit: StackFit.expand, - children: [ - widget.main, - if (visibleWidth > 0) - Positioned.fill( - key: const ValueKey('lui-drawer-scrim'), - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: !canToggle ? null : () => _updatePresentation(false), - child: ColoredBox( - color: Colors.black.withValues(alpha: 0.32 * progress), - ), - ), - ), - Positioned( - key: const ValueKey('lui-drawer-panel'), - left: -width + visibleWidth, - top: 0, - bottom: 0, - width: width, - child: Semantics( - container: true, - label: widget.label, - hidden: visibleWidth == 0, - child: widget.panel, - ), - ), - ], - ), - ), - ), - ); - } -} - -final class _LUISplit extends StatefulWidget { - const _LUISplit({ - required this.backend, - required this.sourceFraction, - required this.gap, - required this.firstMinimum, - required this.secondMinimum, - required this.duration, - required this.easing, - required this.origin, - required this.label, - required this.onChanged, - required this.first, - required this.second, - }); - - final LUIFlutterBackend backend; - final double sourceFraction; - final double gap; - final double firstMinimum; - final double secondMinimum; - final Duration duration; - final String easing; - final double? origin; - final String label; - final ValueChanged onChanged; - final Widget first; - final Widget second; - - @override - State<_LUISplit> createState() => _LUISplitState(); -} - -final class _LUISplitState extends State<_LUISplit> - with SingleTickerProviderStateMixin { - late final AnimationController _controller; - late double _fraction; - Animation? _animation; - double? _reportedMaxWidth; - - @override - void initState() { - super.initState(); - _fraction = _normalize( - widget.duration > Duration.zero - ? widget.origin ?? widget.sourceFraction - : widget.sourceFraction, - ); - _controller = AnimationController(vsync: this, duration: widget.duration) - ..addListener(() { - final animation = _animation; - if (animation == null) return; - setState(() => _fraction = animation.value); - widget.onChanged(_fraction); - }); - if (widget.origin != null && widget.duration > Duration.zero) { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) _animateTo(widget.sourceFraction); - }); - } - } - - @override - void didUpdateWidget(covariant _LUISplit oldWidget) { - super.didUpdateWidget(oldWidget); - _controller.duration = widget.duration; - if (widget.sourceFraction == oldWidget.sourceFraction) return; - final next = _normalize(widget.sourceFraction); - if ((next - _fraction).abs() <= 0.000001) return; - _animateTo(next); - } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - double _normalize(double value) => - value.isFinite && value > 0 ? value.clamp(0, 1).toDouble() : 0.5; - - double _effective( - double value, - double available, - double firstMinimum, - double secondMinimum, - ) { - if (!available.isFinite || available <= 0) return 0.5; - final base = _normalize(value); - final low = firstMinimum.clamp(0, double.infinity) / available; - final high = 1 - secondMinimum.clamp(0, double.infinity) / available; - if (low > high) { - return low / (low + (1 - high)).clamp(0.0001, double.infinity); - } - return base.clamp(low, high).toDouble(); - } - - Curve get _curve => switch (widget.easing) { - 'linear' => Curves.linear, - 'emphasized' => const Cubic(0.2, 0, 0, 1), - 'spring' => Curves.easeOutBack, - _ => Curves.easeInOut, - }; - - void _animateTo(double target) { - final next = _normalize(target); - final disableAnimations = - MediaQuery.maybeOf(context)?.disableAnimations ?? false; - if (widget.duration == Duration.zero || disableAnimations) { - _controller.stop(); - setState(() => _fraction = next); - return; - } - _controller.stop(); - _animation = Tween( - begin: _fraction, - end: next, - ).animate(CurvedAnimation(parent: _controller, curve: _curve)); - _controller.forward(from: 0); - } - - void _updateUser( - double value, - double available, - double firstMinimum, - double secondMinimum, - ) { - _controller.stop(); - final next = _effective(value, available, firstMinimum, secondMinimum); - setState(() => _fraction = next); - widget.onChanged(next); - } - - @override - Widget build(BuildContext context) { - final reported = - _reportedMaxWidth ?? - (widget.firstMinimum + widget.secondMinimum + widget.gap); - final gap = widget.gap.clamp(0, reported).toDouble(); - final available = (reported - gap).clamp(0, double.infinity).toDouble(); - final effective = _effective( - _fraction, - available, - widget.firstMinimum, - widget.secondMinimum, - ); - final firstWidth = available * effective; - final secondWidth = available - firstWidth; - return _LUIConstraintProbe( - onConstraints: (constraints) { - final next = constraints.hasBoundedWidth - ? constraints.maxWidth - : reported; - if (next != _reportedMaxWidth) { - _reportedMaxWidth = next; - SchedulerBinding.instance.addPostFrameCallback((_) { - if (mounted) { - setState(() {}); - widget.backend._scheduleLayoutSettle(); - } - }); - } - }, - child: Row( - children: [ - SizedBox(width: firstWidth, child: widget.first), - Semantics( - label: '${widget.label} divider', - value: '${(effective * 100).round()}%', - increasedValue: - '${((_effective(effective + 0.05, available, widget.firstMinimum, widget.secondMinimum)) * 100).round()}%', - decreasedValue: - '${((_effective(effective - 0.05, available, widget.firstMinimum, widget.secondMinimum)) * 100).round()}%', - onIncrease: () => _updateUser( - effective + 0.05, - available, - widget.firstMinimum, - widget.secondMinimum, - ), - onDecrease: () => _updateUser( - effective - 0.05, - available, - widget.firstMinimum, - widget.secondMinimum, - ), - child: MouseRegion( - cursor: SystemMouseCursors.resizeLeftRight, - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onHorizontalDragUpdate: (details) => _updateUser( - effective + - details.delta.dx / available.clamp(1, double.infinity), - available, - widget.firstMinimum, - widget.secondMinimum, - ), - child: SizedBox(width: gap, height: double.infinity), - ), - ), - ), - SizedBox(width: secondWidth, child: widget.second), - ], - ), - ); - } -} - -final class _LUIResizable extends StatefulWidget { - const _LUIResizable({ - required this.initialWidth, - required this.minimumWidth, - required this.maximumWidth, - required this.label, - required this.child, - }); - - final double? initialWidth; - final double? minimumWidth; - final double? maximumWidth; - final String label; - final Widget child; - - @override - State<_LUIResizable> createState() => _LUIResizableState(); -} - -final class _LUIResizableState extends State<_LUIResizable> { - double? _width; - - @override - void initState() { - super.initState(); - _width = _clamp(widget.initialWidth); - } - - @override - void didUpdateWidget(covariant _LUIResizable oldWidget) { - super.didUpdateWidget(oldWidget); - if (widget.initialWidth != oldWidget.initialWidth) { - _width = _clamp(widget.initialWidth); - } else if (widget.minimumWidth != oldWidget.minimumWidth || - widget.maximumWidth != oldWidget.maximumWidth) { - _width = _clamp(_width); - } - } - - double? _clamp(double? width) { - if (width == null) return null; - return width.clamp( - widget.minimumWidth ?? 0, - widget.maximumWidth ?? double.infinity, - ); - } - - void _resize(double delta, double fallbackWidth) { - setState(() { - _width = _clamp((_width ?? fallbackWidth) + delta); - }); - } - - @override - Widget build(BuildContext context) { - return Semantics( - container: true, - explicitChildNodes: true, - label: widget.label, - onIncrease: () => _resize(16, context.size?.width ?? 0), - onDecrease: () => _resize(-16, context.size?.width ?? 0), - child: SizedBox( - width: _width, - child: Stack( - fit: StackFit.passthrough, - children: [ - widget.child, - Positioned( - top: 0, - right: 0, - bottom: 0, - width: 12, - child: MouseRegion( - cursor: SystemMouseCursors.resizeLeftRight, - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onHorizontalDragUpdate: (details) => - _resize(details.delta.dx, context.size?.width ?? 0), - ), - ), - ), - ], - ), - ), - ); - } -} - -final class _LUIAvatarPainter extends CustomPainter { - const _LUIAvatarPainter({required this.image, required this.source}); - - final ui.Image image; - final Rect? source; - - @override - void paint(Canvas canvas, Size size) { - final available = - source ?? - Rect.fromLTWH(0, 0, image.width.toDouble(), image.height.toDouble()); - final fitted = applyBoxFit(BoxFit.cover, available.size, size); - final sourceRect = Alignment.center.inscribe(fitted.source, available); - final destinationRect = Alignment.center.inscribe( - fitted.destination, - Offset.zero & size, - ); - canvas.drawImageRect(image, sourceRect, destinationRect, Paint()); - } - - @override - bool shouldRepaint(_LUIAvatarPainter oldDelegate) => - oldDelegate.image != image || oldDelegate.source != source; -} - -final class _LUIImagePainter extends CustomPainter { - const _LUIImagePainter({required this.image, required this.source}); - - final ui.Image image; - final Rect? source; - - @override - void paint(Canvas canvas, Size size) { - final available = - source ?? - Rect.fromLTWH(0, 0, image.width.toDouble(), image.height.toDouble()); - final fitted = applyBoxFit(BoxFit.fill, available.size, size); - final sourceRect = Alignment.center.inscribe(fitted.source, available); - final destinationRect = Alignment.center.inscribe( - fitted.destination, - Offset.zero & size, - ); - canvas.drawImageRect(image, sourceRect, destinationRect, Paint()); - } - - @override - bool shouldRepaint(_LUIImagePainter oldDelegate) => - oldDelegate.image != image || oldDelegate.source != source; -} - -final class _LUIAccordion extends StatefulWidget { - const _LUIAccordion({ - required this.expanded, - required this.title, - required this.onToggle, - required this.children, - }); - - final bool expanded; - final String title; - final ValueChanged? onToggle; - final List children; - - @override - State<_LUIAccordion> createState() => _LUIAccordionState(); -} - -final class _LUIAccordionState extends State<_LUIAccordion> { - final ExpansibleController _controller = ExpansibleController(); - bool _syncing = false; - - @override - void didUpdateWidget(_LUIAccordion oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.expanded != widget.expanded) _scheduleSync(); - } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - void _scheduleSync() { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (!mounted || _controller.isExpanded == widget.expanded) return; - _syncing = true; - widget.expanded ? _controller.expand() : _controller.collapse(); - _syncing = false; - }); - } - - void _expansionChanged(bool expanded) { - if (_syncing) return; - widget.onToggle?.call(expanded); - _scheduleSync(); - } - - @override - Widget build(BuildContext context) => ExpansionTile( - controller: _controller, - initiallyExpanded: widget.expanded, - maintainState: true, - enabled: widget.onToggle != null, - title: Text(widget.title), - onExpansionChanged: _expansionChanged, - children: widget.children, - ); -} - -final class _LUIListItem extends StatefulWidget { - const _LUIListItem({ - required this.enabled, - required this.focusable, - required this.selected, - required this.leading, - required this.content, - required this.onPress, - required this.onDoublePress, - required this.onLongPress, - required this.onSubmit, - }); - - final bool enabled; - final bool focusable; - final bool selected; - final Widget? leading; - final Widget content; - final VoidCallback? onPress; - final VoidCallback? onDoublePress; - final VoidCallback? onLongPress; - final VoidCallback? onSubmit; - - @override - State<_LUIListItem> createState() => _LUIListItemState(); -} - -final class _LUIListItemState extends State<_LUIListItem> { - int? _primaryPointer; - Duration? _lastRelease; - Offset? _lastPosition; - Timer? _releaseTimer; - Timer? _longPressTimer; - var _didLongPress = false; - - void _handlePointerDown(PointerDownEvent event) { - if (widget.enabled && event.buttons & kPrimaryButton != 0) { - _primaryPointer = event.pointer; - _didLongPress = false; - _longPressTimer?.cancel(); - if (widget.onLongPress != null) { - _longPressTimer = Timer(const Duration(milliseconds: 450), () { - if (_primaryPointer != event.pointer || !mounted) return; - _didLongPress = true; - widget.onLongPress?.call(); - }); - } - } - } - - void _handlePointerCancel(PointerCancelEvent event) { - if (_primaryPointer == event.pointer) _primaryPointer = null; - _longPressTimer?.cancel(); - } - - // Events are emitted per physical tap so press and double-press stay - // independent: InkWell's gesture arena would suppress the taps that make - // up a double-tap. - void _handlePointerUp(PointerUpEvent event) { - if (_primaryPointer != event.pointer) return; - _primaryPointer = null; - _longPressTimer?.cancel(); - if (!widget.enabled) return; - if (_didLongPress) return; - widget.onPress?.call(); - - final previousRelease = _lastRelease; - final previousPosition = _lastPosition; - final isDoublePress = - previousRelease != null && - previousPosition != null && - event.timeStamp - previousRelease <= kDoubleTapTimeout && - (event.position - previousPosition).distance <= kDoubleTapSlop; - if (isDoublePress) { - _releaseTimer?.cancel(); - _lastRelease = null; - _lastPosition = null; - widget.onDoublePress?.call(); - } else { - _lastRelease = event.timeStamp; - _lastPosition = event.position; - _releaseTimer?.cancel(); - _releaseTimer = Timer(kDoubleTapTimeout, () { - _lastRelease = null; - _lastPosition = null; - }); - } - } - - @override - void dispose() { - _releaseTimer?.cancel(); - _longPressTimer?.cancel(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final press = widget.enabled ? widget.onPress : null; - final submit = widget.enabled ? widget.onSubmit : null; - final bindings = {}; - if (press != null) { - bindings[const SingleActivator(LogicalKeyboardKey.space)] = press; - } - if (submit != null || press != null) { - bindings[const SingleActivator(LogicalKeyboardKey.enter)] = - submit ?? press!; - } - return Semantics( - button: true, - enabled: widget.enabled, - selected: widget.selected, - onTap: press, - onLongPress: widget.enabled ? widget.onLongPress : null, - child: CallbackShortcuts( - bindings: bindings, - child: Focus( - canRequestFocus: widget.enabled && widget.focusable, - skipTraversal: !widget.focusable, - child: Listener( - behavior: HitTestBehavior.opaque, - onPointerDown: _handlePointerDown, - onPointerCancel: _handlePointerCancel, - onPointerUp: _handlePointerUp, - child: Material( - color: widget.selected - ? Theme.of(context).colorScheme.secondaryContainer - : Colors.transparent, - borderRadius: BorderRadius.circular(6), - clipBehavior: Clip.antiAlias, - child: InkWell( - onTap: widget.enabled ? () {} : null, - canRequestFocus: false, - child: ListTile( - enabled: widget.enabled, - leading: widget.leading, - title: widget.content, - dense: true, - contentPadding: const EdgeInsets.symmetric(horizontal: 12), - ), - ), - ), - ), - ), - ), - ); - } -} - -final class _LUIAnchoredStack extends StatefulWidget { - const _LUIAnchoredStack({ - required this.anchor, - required this.alignment, - required this.offset, - required this.menuID, - required this.menuChildren, - required this.minimumWidth, - required this.maximumWidth, - required this.onDismiss, - required this.trigger, - }); - - final String anchor; - final String alignment; - final double offset; - final int? menuID; - final List menuChildren; - final double? minimumWidth; - final double? maximumWidth; - final VoidCallback? onDismiss; - final Widget trigger; - - @override - State<_LUIAnchoredStack> createState() => _LUIAnchoredStackState(); -} - -final class _LUIAnchoredStackState extends State<_LUIAnchoredStack> { - final MenuController _menu = MenuController(); - bool _closingForModel = false; - - @override - void initState() { - super.initState(); - _syncMenu(); - } - - @override - void didUpdateWidget(_LUIAnchoredStack oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.menuID != widget.menuID) _syncMenu(); - } - - void _syncMenu() { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (!mounted) return; - if (widget.menuID == null) { - if (_menu.isOpen) { - _closingForModel = true; - _menu.close(); - } - } else if (!_menu.isOpen && widget.menuChildren.isNotEmpty) { - _menu.open(); - } - }); - } - - AlignmentGeometry get _menuAlignment => switch (widget.anchor) { - 'above' => switch (widget.alignment) { - 'center' => Alignment.topCenter, - 'end' => AlignmentDirectional.topEnd, - _ => AlignmentDirectional.topStart, - }, - 'left' => AlignmentDirectional.centerStart, - 'right' => AlignmentDirectional.centerEnd, - _ => switch (widget.alignment) { - 'center' => Alignment.bottomCenter, - 'end' => AlignmentDirectional.bottomEnd, - _ => AlignmentDirectional.bottomStart, - }, - }; - - Offset get _alignmentOffset => switch (widget.anchor) { - 'above' => Offset(0, -widget.offset), - 'left' => Offset(-widget.offset, 0), - 'right' => Offset(widget.offset, 0), - _ => Offset(0, widget.offset), - }; - - void _menuClosed() { - if (_closingForModel) { - _closingForModel = false; - if (widget.menuID != null) _syncMenu(); - return; - } - widget.onDismiss?.call(); - } - - @override - Widget build(BuildContext context) { - // OverlayPortal wraps its child in a non-container Semantics carrying - // traversalParentIdentifier, so the property never forms its own node; - // it merges upward into the nearest ancestor boundary, and the merge - // keeps only the first non-null identifier. Sibling anchors under one - // boundary (e.g. two anchored stacks in a row) then lose every graft - // except the first, leaving later portals' menus permanently absent - // from the platform accessibility tree. An explicit container boundary - // keeps each portal's identifier on its own node. - return Semantics( - container: true, - child: MenuAnchor( - controller: _menu, - useRootOverlay: true, - animated: true, - crossAxisUnconstrained: widget.alignment != 'stretch', - alignmentOffset: _alignmentOffset, - style: MenuStyle( - alignment: _menuAlignment, - minimumSize: widget.minimumWidth == null - ? null - : WidgetStatePropertyAll(Size(widget.minimumWidth!, 0)), - maximumSize: widget.maximumWidth == null - ? null - : WidgetStatePropertyAll( - Size(widget.maximumWidth!, double.infinity), - ), - ), - onClose: _menuClosed, - menuChildren: widget.menuChildren, - child: widget.trigger, - ), - ); - } -} - -final class _LUITooltipSession { - final Stopwatch _clock = Stopwatch()..start(); - Duration _warmUntil = Duration.zero; - - bool get isWarm => _clock.elapsed < _warmUntil; - - void warm() { - _warmUntil = _clock.elapsed + const Duration(milliseconds: 400); - } - - void clear() { - _warmUntil = Duration.zero; - } -} - -final class _LUIRetainedTooltip extends StatefulWidget { - const _LUIRetainedTooltip({ - required this.session, - required this.message, - required this.waitDuration, - required this.exitDuration, - required this.preferBelow, - required this.verticalOffset, - required this.enableTapToDismiss, - required this.child, - }); - - final _LUITooltipSession session; - final String message; - final Duration waitDuration; - final Duration exitDuration; - final bool preferBelow; - final double verticalOffset; - final bool enableTapToDismiss; - final Widget child; - - @override - State<_LUIRetainedTooltip> createState() => _LUIRetainedTooltipState(); -} - -final class _LUIRetainedTooltipState extends State<_LUIRetainedTooltip> - with WidgetsBindingObserver { - final GlobalKey _tooltipKey = GlobalKey(); - bool _pointerInside = false; - bool _pointerPresented = false; - - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addObserver(this); - } - - @override - void dispose() { - WidgetsBinding.instance.removeObserver(this); - widget.session.clear(); - Tooltip.dismissAllToolTips(); - super.dispose(); - } - - @override - void didChangeAppLifecycleState(AppLifecycleState state) { - if (state != AppLifecycleState.resumed) _dismissCold(); - } - - void _pointerEntered(PointerEnterEvent event) { - _pointerInside = true; - if (!widget.session.isWarm) return; - WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted && _pointerInside) { - _tooltipKey.currentState?.ensureTooltipVisible(); - } - }); - } - - void _pointerExited(PointerExitEvent event) { - _pointerInside = false; - if (_pointerPresented) widget.session.warm(); - _pointerPresented = false; - } - - void _focusChanged(bool focused) { - if (focused) { - _tooltipKey.currentState?.ensureTooltipVisible(); - } else if (!_pointerInside) { - Tooltip.dismissAllToolTips(); - } - } - - void _triggered() { - _pointerPresented = _pointerInside; - } - - void _dismissCold() { - widget.session.clear(); - _pointerPresented = false; - Tooltip.dismissAllToolTips(); - } - - @override - Widget build(BuildContext context) { - return CallbackShortcuts( - bindings: { - const SingleActivator(LogicalKeyboardKey.escape): _dismissCold, - }, - child: Focus( - onFocusChange: _focusChanged, - child: Listener( - behavior: HitTestBehavior.translucent, - onPointerDown: (event) { - if (event.kind == PointerDeviceKind.mouse) _dismissCold(); - }, - child: GestureDetector( - behavior: HitTestBehavior.translucent, - onLongPress: () { - _triggered(); - _tooltipKey.currentState?.ensureTooltipVisible(); - }, - child: Tooltip( - key: _tooltipKey, - message: widget.message, - waitDuration: widget.waitDuration, - exitDuration: widget.exitDuration, - preferBelow: widget.preferBelow, - verticalOffset: widget.verticalOffset, - enableTapToDismiss: widget.enableTapToDismiss, - triggerMode: TooltipTriggerMode.manual, - onTriggered: _triggered, - child: MouseRegion( - onEnter: _pointerEntered, - onExit: _pointerExited, - child: widget.child, - ), - ), - ), - ), - ), - ); - } -} - -typedef _LUIToggleSelectionBuilder = Widget Function( - BuildContext context, - bool selected, - ValueChanged setSelected, -); - -final class _LUIToggleSelection extends StatefulWidget { - const _LUIToggleSelection({ - required this.modelSelected, - required this.builder, - }); - - final bool? modelSelected; - final _LUIToggleSelectionBuilder builder; - - @override - State<_LUIToggleSelection> createState() => _LUIToggleSelectionState(); -} - -final class _LUIToggleSelectionState extends State<_LUIToggleSelection> { - late bool _selected; - - @override - void initState() { - super.initState(); - _selected = widget.modelSelected ?? false; - } - - @override - void didUpdateWidget(_LUIToggleSelection oldWidget) { - super.didUpdateWidget(oldWidget); - if (widget.modelSelected != null && - widget.modelSelected != oldWidget.modelSelected) { - _selected = widget.modelSelected!; - } - } - - @override - Widget build(BuildContext context) => widget.builder( - context, - _selected, - (selected) => setState(() => _selected = selected), - ); -} - -final class _LUITextInput extends StatefulWidget { - const _LUITextInput({ - required this.text, - required this.enabled, - required this.placeholder, - required this.foreground, - required this.autofocus, - required this.focusNode, - required this.multiline, - required this.secure, - required this.search, - required this.grouped, - required this.onOpen, - required this.submitOnEnter, - required this.onChanged, - required this.onSubmitted, - }); - - final String text; - final bool enabled; - final String? placeholder; - final Color? foreground; - final bool autofocus; - final FocusNode focusNode; - final bool multiline; - final bool secure; - final bool search; - final bool grouped; - final VoidCallback? onOpen; - final bool submitOnEnter; - final ValueChanged onChanged; - final ValueChanged onSubmitted; - - @override - State<_LUITextInput> createState() => _LUITextInputState(); -} - -final class _LUITextInputState extends State<_LUITextInput> { - late final TextEditingController _controller; - - @override - void initState() { - super.initState(); - _controller = TextEditingController(text: widget.text); - } - - @override - void didUpdateWidget(_LUITextInput oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.autofocus != widget.autofocus) { - widget.focusNode.requestFocus(); - } - if (_controller.text != widget.text) { - _controller.value = TextEditingValue( - text: widget.text, - selection: TextSelection.collapsed(offset: widget.text.length), - ); - } - } - - @override - Widget build(BuildContext context) { - return TextField( - controller: _controller, - enabled: widget.enabled, - autofocus: widget.autofocus, - focusNode: widget.focusNode, - keyboardType: widget.multiline - ? TextInputType.multiline - : TextInputType.text, - textInputAction: widget.multiline && !widget.submitOnEnter - ? TextInputAction.newline - : TextInputAction.done, - minLines: 1, - maxLines: widget.multiline ? null : 1, - obscureText: widget.secure, - style: TextStyle(color: widget.foreground), - decoration: InputDecoration( - hintText: widget.placeholder, - border: widget.grouped ? InputBorder.none : null, - prefixIcon: widget.search ? const Icon(Icons.search) : null, - suffixIcon: widget.search && _controller.text.isNotEmpty - ? IconButton( - tooltip: 'Clear', - icon: const Icon(Icons.clear), - onPressed: _clear, - ) - : widget.onOpen != null - ? IconButton( - tooltip: 'Open menu', - icon: const Icon(Icons.arrow_drop_down), - onPressed: widget.onOpen, - ) - : null, - ), - onChanged: _handleChanged, - onSubmitted: widget.onSubmitted, - ); - } - - void _handleChanged(String value) { - if (widget.search) setState(() {}); - widget.onChanged(value); - } - - void _clear() { - _controller.clear(); - setState(() {}); - widget.onChanged(''); - } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } -} - -// Constraint-aware containers that decide flex/stretch/fill at layout time. -// -// They deliberately avoid LayoutBuilder: a LayoutBuilder schedules a layout -// callback that rebuilds its widget subtree inside the layout phase, and -// while it runs the callback flushes every dirty element in its build -// scope — including mid-teardown overlay subtrees. Rebuilding or -// deactivating elements inside a layout callback trips debug asserts -// (updating inactive elements, relayout-boundary gaps), which can orphan -// deactivated elements: they stay mounted forever with render objects still -// attached, their semantics nodes never receive parent data, and every -// subsequent frame's flushSemantics assert then skips finalizeTree — so the -// inactive elements never unmount and the accessibility tree stays frozen -// on the last emitted (stale) tree. - -/// A [Flex] that drops child flex when the incoming main-axis constraints -/// are unbounded (children size to content, matching the previous -/// build-time flex unwrap) and falls back from -/// [CrossAxisAlignment.stretch] when the cross axis is unbounded. -class LUIFlex extends Flex { - const LUIFlex({ - super.key, - required super.direction, - this.expandsForAlignment = true, - super.mainAxisAlignment, - super.crossAxisAlignment, - super.spacing, - super.children, - }); - - /// Whether the flex expands to the maximum main-axis constraint when - /// bounded. When the main axis is unbounded the flex always sizes to its - /// children regardless of this flag. - final bool expandsForAlignment; - - @override - RenderFlex createRenderObject(BuildContext context) => _RenderLUIFlex( - direction: direction, - mainAxisAlignment: mainAxisAlignment, - mainAxisSize: expandsForAlignment ? MainAxisSize.max : MainAxisSize.min, - crossAxisAlignment: crossAxisAlignment, - spacing: spacing, - // Stretch downgraded to start under an unbounded cross axis needs a - // direction to resolve; default to LTR when no Directionality is - // reachable (e.g. a subtree hosted outside the app root). - textDirection: getEffectiveTextDirection(context) ?? TextDirection.ltr, - verticalDirection: verticalDirection, - textBaseline: textBaseline, - clipBehavior: clipBehavior, - ); - - @override - void updateRenderObject( - BuildContext context, - covariant RenderFlex renderObject, - ) { - renderObject - ..direction = direction - ..mainAxisAlignment = mainAxisAlignment - ..mainAxisSize = expandsForAlignment ? MainAxisSize.max : MainAxisSize.min - ..crossAxisAlignment = crossAxisAlignment - ..textDirection = getEffectiveTextDirection(context) ?? TextDirection.ltr - ..verticalDirection = verticalDirection - ..textBaseline = textBaseline - ..clipBehavior = clipBehavior - ..spacing = spacing; - } -} - -class _RenderLUIFlex extends RenderFlex { - _RenderLUIFlex({ - super.direction, - super.mainAxisAlignment, - super.mainAxisSize, - super.crossAxisAlignment, - super.spacing, - super.textDirection, - super.verticalDirection, - super.textBaseline, - super.clipBehavior, - }); - - final Map _strippedFlex = {}; - - // RenderFlex requires a bounded cross axis for - // CrossAxisAlignment.stretch; downgrade to `start` when the cross axis - // is unbounded at layout time. RenderFlex internals read the getter, so - // no field mutation (and no markNeedsLayout) is needed. - @override - CrossAxisAlignment get crossAxisAlignment { - final CrossAxisAlignment value = super.crossAxisAlignment; - if (value == CrossAxisAlignment.stretch) { - final bool bounded = direction == Axis.vertical - ? constraints.hasBoundedWidth - : constraints.hasBoundedHeight; - if (!bounded) { - return CrossAxisAlignment.start; - } - } - return value; - } - - @override - void performLayout() { - // Restore flex stripped by a previous unbounded layout; children keep - // their Flexible/Expanded widgets (and their intent) across layouts. - for (final MapEntry entry in _strippedFlex.entries) { - (entry.key.parentData! as FlexParentData).flex = entry.value; - } - _strippedFlex.clear(); - - final bool boundedMain = direction == Axis.vertical - ? constraints.hasBoundedHeight - : constraints.hasBoundedWidth; - if (!boundedMain) { - // A flex child is illegal when the flex parent's main-axis - // constraints are unbounded; strip flex so children fall back to - // their content size. FlexParentData.flex is a plain field, so - // mutating it never marks the render dirty mid-layout. - RenderBox? child = firstChild; - while (child != null) { - final FlexParentData parentData = child.parentData! as FlexParentData; - if (parentData.flex != null) { - _strippedFlex[child] = parentData.flex!; - parentData.flex = null; - } - child = parentData.nextSibling; - } - } - super.performLayout(); - } -} - -/// Stack parent data carrying the lui `grow` flag for [stack]'s children. -class _LUIStackParentData extends StackParentData { - /// Whether this child fills the stack's height when bounded. - bool growFill = false; -} - -/// Marks a [LUIStack] child as growing: under bounded height it is laid -/// out like `Positioned(top: 0, bottom: 0)` (the Flutter equivalent of the -/// Apple backend's `maxHeight: .infinity` + `layoutPriority(1)`); under -/// unbounded height it is laid out like a normal non-positioned child. -class _LUIStackGrowFill extends ParentDataWidget<_LUIStackParentData> { - const _LUIStackGrowFill({required this.fill, required super.child}); - - final bool fill; - - @override - void applyParentData(RenderObject renderObject) { - final parentData = renderObject.parentData! as _LUIStackParentData; - if (parentData.growFill != fill) { - parentData.growFill = fill; - renderObject.parent?.markNeedsLayout(); - } - } - - @override - Type get debugTypicalAncestorWidgetClass => LUIStack; -} - -/// A [Stack] that applies grow fill to [_LUIStackGrowFill] children at -/// layout time based on the incoming height constraints. -class LUIStack extends Stack { - const LUIStack({super.key, super.clipBehavior, super.children}); - - @override - RenderStack createRenderObject(BuildContext context) => _RenderLUIStack( - alignment: alignment, - textDirection: textDirection ?? Directionality.maybeOf(context), - fit: fit, - clipBehavior: clipBehavior, - ); -} - -class _RenderLUIStack extends RenderStack { - _RenderLUIStack({ - super.alignment, - super.textDirection, - super.fit, - super.clipBehavior, - }); - - @override - void setupParentData(RenderObject child) { - if (child.parentData is! _LUIStackParentData) { - child.parentData = _LUIStackParentData(); - } - } - - @override - void performLayout() { - // A grow child fills via a full Positioned rect, which the stack - // resolves against `constraints.biggest` — so filling requires a finite - // biggest on BOTH axes. Positioned(top/bottom) alone would leave - // RenderStack.layoutPositionedChild handing the child completely - // unbounded width constraints; a subtree that then sizes to the max - // reports an infinite size and the stack computes a NaN offset for it - // (invalid paint transform, zero-bounds semantics). Under unbounded - // constraints the child falls back to non-positioned content sizing - // and RenderStack's all-positioned path can never produce an infinite - // size. - final bool bounded = constraints.biggest.isFinite; - RenderBox? child = firstChild; - while (child != null) { - final parentData = child.parentData! as _LUIStackParentData; - if (parentData.growFill) { - if (bounded) { - parentData.left = 0.0; - parentData.right = 0.0; - parentData.top = 0.0; - parentData.bottom = 0.0; - } else { - parentData.left = null; - parentData.right = null; - parentData.top = null; - parentData.bottom = null; - } - } - child = parentData.nextSibling; - } - super.performLayout(); - } -} - -/// A grid cell that sizes itself to the column width computed from the -/// incoming constraints, replacing the previous LayoutBuilder + SizedBox -/// cell sizing inside `grid`. -class _LUIGridCell extends SingleChildRenderObjectWidget { - const _LUIGridCell({required this.columns, required this.gap, super.child}); - - final int columns; - final double gap; - - @override - RenderObject createRenderObject(BuildContext context) => - _RenderLUIGridCell(columns: columns, gap: gap); - - @override - void updateRenderObject( - BuildContext context, - covariant _RenderLUIGridCell renderObject, - ) { - renderObject - ..columns = columns - ..gap = gap; - } -} - -class _RenderLUIGridCell extends RenderProxyBox { - _RenderLUIGridCell({required int columns, required double gap}) - : _columns = columns, - _gap = gap; - - int get columns => _columns; - int _columns; - set columns(int value) { - if (_columns != value) { - _columns = value; - markNeedsLayout(); - } - } - - double get gap => _gap; - double _gap; - set gap(double value) { - if (_gap != value) { - _gap = value; - markNeedsLayout(); - } - } - - @override - void performLayout() { - final child = this.child; - if (child == null) { - size = constraints.smallest; - return; - } - final double available = constraints.maxWidth.isFinite - ? constraints.maxWidth - : 0.0; - final double cellWidth = available > 0 - ? (available - gap * (columns - 1)) / columns - : 0.0; - child.layout( - cellWidth > 0 - ? constraints.enforce(BoxConstraints.tightFor(width: cellWidth)) - : constraints, - parentUsesSize: true, - ); - size = child.size; - } -} - -/// Caps a child's width at a fraction of the incoming constraints, used by -/// `bubble` to limit surfaces to 80% of the available width. -class _LUIWidthCap extends SingleChildRenderObjectWidget { - const _LUIWidthCap({required this.widthFactor, super.child}); - - final double widthFactor; - - @override - RenderObject createRenderObject(BuildContext context) => - _RenderLUIWidthCap(widthFactor: widthFactor); - - @override - void updateRenderObject( - BuildContext context, - covariant _RenderLUIWidthCap renderObject, - ) { - renderObject.widthFactor = widthFactor; - } -} - -class _RenderLUIWidthCap extends RenderProxyBox { - _RenderLUIWidthCap({required double widthFactor}) - : _widthFactor = widthFactor; - - double get widthFactor => _widthFactor; - double _widthFactor; - set widthFactor(double value) { - if (_widthFactor != value) { - _widthFactor = value; - markNeedsLayout(); - } - } - - @override - void performLayout() { - final child = this.child; - if (child == null) { - size = constraints.smallest; - return; - } - final BoxConstraints childConstraints = constraints.hasBoundedWidth - ? constraints.copyWith( - minWidth: 0.0, - maxWidth: constraints.maxWidth * widthFactor, - ) - : constraints; - child.layout(childConstraints, parentUsesSize: true); - size = constraints.constrain(child.size); - } -} - -/// Reports its incoming constraints to the owning [State] so constraint- -/// dependent widget structure can rebuild on the next frame instead of in -/// a LayoutBuilder layout callback (which would run a nested buildScope -/// during layout — the mechanism that orphaned elements during teardown). -class _LUIConstraintProbe extends SingleChildRenderObjectWidget { - const _LUIConstraintProbe({required this.onConstraints, super.child}); - - /// Called from `performLayout` when the constraints change; must only - /// schedule work (e.g. a post-frame setState), never mutate the tree. - final ValueChanged onConstraints; - - @override - RenderObject createRenderObject(BuildContext context) => - _RenderLUIConstraintProbe(onConstraints: onConstraints); - - @override - void updateRenderObject( - BuildContext context, - covariant _RenderLUIConstraintProbe renderObject, - ) { - renderObject.onConstraints = onConstraints; - } -} - -class _RenderLUIConstraintProbe extends RenderProxyBox { - _RenderLUIConstraintProbe({required this.onConstraints}); - - ValueChanged onConstraints; - BoxConstraints? _lastConstraints; - - @override - void performLayout() { - if (_lastConstraints != constraints) { - _lastConstraints = constraints; - onConstraints(constraints); - } - super.performLayout(); - } -} diff --git a/platform/flutter/lib/lui_flutter_extension.dart b/platform/flutter/lib/lui_flutter_extension.dart deleted file mode 100644 index b4560c0d..00000000 --- a/platform/flutter/lib/lui_flutter_extension.dart +++ /dev/null @@ -1,333 +0,0 @@ -part of 'lui_flutter_backend.dart'; - -enum LUIExtensionValueKind { - string, - boolean, - integer, - doubleValue; - - bool accepts(Object? value) => switch (this) { - LUIExtensionValueKind.string => value is String, - LUIExtensionValueKind.boolean => value is bool, - LUIExtensionValueKind.integer => value is int, - LUIExtensionValueKind.doubleValue => value is num && value.isFinite, - }; - - Object normalize(Object value) => - this == LUIExtensionValueKind.doubleValue && value is int - ? value.toDouble() - : value; -} - -@immutable -final class LUIExtensionProperty { - const LUIExtensionProperty({ - required this.name, - required this.kind, - this.isRequired = false, - this.defaultValue, - }); - - final String name; - final LUIExtensionValueKind kind; - final bool isRequired; - final Object? defaultValue; -} - -@immutable -final class LUIExtensionEventField { - const LUIExtensionEventField({ - required this.name, - required this.kind, - this.isRequired = false, - }); - - final String name; - final LUIExtensionValueKind kind; - final bool isRequired; -} - -@immutable -final class LUIExtensionEventSchema { - const LUIExtensionEventSchema({this.name = '', this.fields = const []}); - - final String name; - final List fields; -} - -typedef LUIExtensionWidgetBuilder = - Widget Function(LUIFlutterExtensionContext context); - -@immutable -final class LUIFlutterExtension { - const LUIFlutterExtension({ - required this.identifier, - required this.fingerprint, - required this.builder, - this.acceptsStandardChildren = false, - this.childIdentifiers = const [], - this.properties = const [], - this.events = const [], - }) : isTweak = false; - - const LUIFlutterExtension._tweak({ - required this.identifier, - required this.fingerprint, - required this.builder, - required this.properties, - }) : acceptsStandardChildren = true, - childIdentifiers = const [], - events = const [], - isTweak = true; - - final String identifier; - final String fingerprint; - final bool acceptsStandardChildren; - final List childIdentifiers; - final List properties; - final List events; - final bool isTweak; - final LUIExtensionWidgetBuilder builder; -} - -typedef LUITweakWidgetBuilder = - Widget Function(Widget content, LUIFlutterExtensionContext context); - -@immutable -final class LUIFlutterTweak { - const LUIFlutterTweak({ - required this.identifier, - required this.fingerprint, - required this.builder, - this.properties = const [], - }); - - final String identifier; - final String fingerprint; - final List properties; - final LUITweakWidgetBuilder builder; -} - -final class LUIFlutterExtensionRegistry { - final Map _registrations = {}; - bool _isFrozen = false; - - void register(LUIFlutterExtension registration) { - if (_isFrozen) { - throw const LUIBackendException('extension registry is frozen'); - } - if (!_validExtensionName(registration.identifier)) { - throw const LUIBackendException('invalid extension identifier'); - } - if (_isStandardNodeName(registration.identifier)) { - throw const LUIBackendException( - 'extension identifier shadows a standard node', - ); - } - if (_registrations.containsKey(registration.identifier)) { - throw const LUIBackendException( - 'extension identifier is already registered', - ); - } - if (!registration.childIdentifiers.every(_validExtensionName)) { - throw const LUIBackendException('invalid extension child identifier'); - } - _validateUniqueNames(registration.childIdentifiers, 'child identifier'); - _validateUniqueNames( - registration.properties.map((property) => property.name), - 'property', - ); - _validateUniqueNames( - registration.events.map((event) => event.name), - 'event', - ); - for (final property in registration.properties) { - if (!_validExtensionName(property.name)) { - throw const LUIBackendException('invalid extension property name'); - } - final value = property.defaultValue; - if (value != null && !property.kind.accepts(value)) { - throw const LUIBackendException('invalid extension property default'); - } - } - for (final event in registration.events) { - if (!_validExtensionName(event.name)) { - throw const LUIBackendException('invalid extension event name'); - } - _validateUniqueNames( - event.fields.map((field) => field.name), - 'event field', - ); - if (!event.fields.every((field) => _validExtensionName(field.name))) { - throw const LUIBackendException('invalid extension event field name'); - } - } - _registrations[registration.identifier] = registration; - } - - void registerTweak(LUIFlutterTweak tweak) { - register( - LUIFlutterExtension._tweak( - identifier: tweak.identifier, - fingerprint: tweak.fingerprint, - properties: tweak.properties, - builder: (context) => tweak.builder(context.content, context), - ), - ); - } - - void _freeze() { - if (_isFrozen) return; - for (final registration in _registrations.values) { - for (final child in registration.childIdentifiers) { - if (!_registrations.containsKey(child)) { - throw const LUIBackendException('unknown extension child schema'); - } - } - } - _isFrozen = true; - } - - LUIFlutterExtension? _registration(String identifier) => - _registrations[identifier]; - - static void _validateUniqueNames(Iterable names, String label) { - final values = names.toList(growable: false); - if (values.toSet().length != values.length) { - throw LUIBackendException('duplicate extension $label'); - } - } -} - -@immutable -final class LUIExtensionComponentEvent extends LUIEvent { - const LUIExtensionComponentEvent({ - required this.node, - required this.identifier, - required this.name, - required this.values, - }); - - final int node; - final String identifier; - final String name; - final Map values; - - @override - bool operator ==(Object other) => - other is LUIExtensionComponentEvent && - other.node == node && - other.identifier == identifier && - other.name == name && - mapEquals(other.values, values); - - @override - int get hashCode => Object.hash( - node, - identifier, - name, - Object.hashAllUnordered(values.entries), - ); -} - -final class LUIFlutterExtensionContext { - const LUIFlutterExtensionContext._(this.nodeID, this._backend); - - final int nodeID; - final LUIFlutterBackend _backend; - - Object? property(String name) => - _backend._requireExtensionState(nodeID).properties[name]; - - List get childIDs => - List.unmodifiable(_backend._requireExtensionState(nodeID).children); - - Widget get content { - final children = childIDs; - return children.length == 1 - ? _backend.widget(node: children.single) - : const SizedBox.shrink(); - } - - /// Renders one declared child of this extension node (e.g. a `split-pane` - /// showing a specific `split-tab`). Unknown ids render nothing. - Widget contentFor(int childID) => childIDs.contains(childID) - ? _backend.widget(node: childID) - : const SizedBox.shrink(); - - /// Reads a property from the nearest ancestor extension node with the - /// given identifier (e.g. `split-view` settings from a nested branch). - Object? ancestorProperty(String identifier, String name) { - var parent = _backend._requireExtensionState(nodeID).parent; - while (parent != null) { - final state = _backend._extensionStates[parent]; - if (state == null) return null; - if (state.identifier == identifier) return state.properties[name]; - parent = state.parent; - } - return null; - } - - Object? childProperty(int childID, String name) => - _backend._requireExtensionState(childID).properties[name]; - - void emit({required String name, Map values = const {}}) { - _backend.performExtensionEvent(nodeID, name: name, values: values); - } -} - -final class _ExtensionNodeState { - _ExtensionNodeState({ - required this.identifier, - required this.fingerprint, - List? children, - Map? properties, - }) : parent = null, - children = children ?? [], - properties = properties ?? {}; - - _ExtensionNodeState.copy(_ExtensionNodeState other) - : identifier = other.identifier, - fingerprint = other.fingerprint, - parent = other.parent, - children = List.of(other.children), - properties = Map.of(other.properties); - - final String identifier; - final String fingerprint; - int? parent; - final List children; - final Map properties; - - bool rendersLike(_ExtensionNodeState other) => - identifier == other.identifier && - fingerprint == other.fingerprint && - parent == other.parent && - listEquals(children, other.children) && - mapEquals(properties, other.properties); -} - -final class _ExtensionNodeHandle extends ChangeNotifier { - _ExtensionNodeHandle(this.state); - - _ExtensionNodeState state; - int revision = 0; - - void markChanged() { - revision += 1; - notifyListeners(); - } -} - -final _extensionNamePattern = RegExp(r'^[a-z0-9]+(?:-[a-z0-9]+)*$'); - -bool _validExtensionName(String value) => _extensionNamePattern.hasMatch(value); - -bool _isStandardNodeName(String value) { - try { - _decodeNodeKind(value); - return true; - } on LUIBackendException { - return false; - } -} diff --git a/platform/flutter/lib/lui_flutter_split.dart b/platform/flutter/lib/lui_flutter_split.dart deleted file mode 100644 index 02d079d4..00000000 --- a/platform/flutter/lib/lui_flutter_split.dart +++ /dev/null @@ -1,805 +0,0 @@ -part of 'lui_flutter_backend.dart'; - -/// Bonsplit-style tabbed split panes for the Flutter backend. -/// -/// The OCaml app owns the tree (see src/lui_split.ml); these widgets own -/// gesture-time visuals — drag previews, drop-zone highlight, divider -/// feedback — so interactions run at display rate, and only committed -/// actions cross the bridge. -final class LUIFlutterSplit { - static const _viewFingerprint = - 'lui-extension-v1|10:split-view|profiles:android/flutter,ios/flutter,ios/swiftui,linux/flutter,linux/gpui,macos/flutter,macos/gpui,macos/swiftui,web/web,windows/flutter,windows/gpui|standard-children:0|children:12:split-branch,10:split-pane|properties:17:divider-thickness:float:optional:none,24:accessibility-identifier:string:optional:none,9:animation:bool:optional:none|events:'; - static const _branchFingerprint = - 'lui-extension-v1|12:split-branch|profiles:android/flutter,ios/flutter,ios/swiftui,linux/flutter,linux/gpui,macos/flutter,macos/gpui,macos/swiftui,web/web,windows/flutter,windows/gpui|standard-children:0|children:12:split-branch,10:split-pane|properties:11:orientation:string:required:none,5:ratio:float:required:none|events:13:ratio-changed[5:ratio:float:required]'; - static const _paneFingerprint = - 'lui-extension-v1|10:split-pane|profiles:android/flutter,ios/flutter,ios/swiftui,linux/flutter,linux/gpui,macos/flutter,macos/gpui,macos/swiftui,web/web,windows/flutter,windows/gpui|standard-children:0|children:9:split-tab|properties:24:accessibility-identifier:string:optional:none,7:focused:bool:optional:none,7:pane-id:string:required:none,8:selected:string:optional:none|events:10:split-drop[3:tab:string:required,4:edge:string:required,9:from-pane:string:required],10:tab-closed[3:tab:string:required],11:pane-closed[],12:pane-focused[],12:tab-selected[3:tab:string:required],15:split-requested[11:orientation:string:required],8:navigate[9:direction:string:required],9:tab-moved[3:tab:string:required,5:index:int:required,9:from-pane:string:required]'; - static const _tabFingerprint = - 'lui-extension-v1|9:split-tab|profiles:android/flutter,ios/flutter,ios/swiftui,linux/flutter,linux/gpui,macos/flutter,macos/gpui,macos/swiftui,web/web,windows/flutter,windows/gpui|standard-children:1|children:|properties:24:accessibility-identifier:string:optional:none,4:icon:string:optional:none,5:dirty:bool:optional:none,5:title:string:required:none,6:tab-id:string:required:none,8:closable:bool:optional:none|events:'; - - static const _accessibilityIdentifier = LUIExtensionProperty( - name: 'accessibility-identifier', - kind: LUIExtensionValueKind.string, - ); - - /// Registers all four split components. - static void register(LUIFlutterExtensionRegistry registry) { - registry - ..register( - LUIFlutterExtension( - identifier: 'split-view', - fingerprint: _viewFingerprint, - childIdentifiers: const ['split-branch', 'split-pane'], - properties: const [ - LUIExtensionProperty( - name: 'divider-thickness', - kind: LUIExtensionValueKind.doubleValue, - ), - LUIExtensionProperty( - name: 'animation', - kind: LUIExtensionValueKind.boolean, - ), - _accessibilityIdentifier, - ], - builder: _SplitView.new, - ), - ) - ..register( - LUIFlutterExtension( - identifier: 'split-branch', - fingerprint: _branchFingerprint, - childIdentifiers: const ['split-branch', 'split-pane'], - properties: const [ - LUIExtensionProperty( - name: 'orientation', - kind: LUIExtensionValueKind.string, - isRequired: true, - ), - LUIExtensionProperty( - name: 'ratio', - kind: LUIExtensionValueKind.doubleValue, - isRequired: true, - ), - ], - events: const [ - LUIExtensionEventSchema( - name: 'ratio-changed', - fields: [ - LUIExtensionEventField( - name: 'ratio', - kind: LUIExtensionValueKind.doubleValue, - isRequired: true, - ), - ], - ), - ], - builder: _SplitBranch.new, - ), - ) - ..register( - LUIFlutterExtension( - identifier: 'split-pane', - fingerprint: _paneFingerprint, - childIdentifiers: const ['split-tab'], - properties: const [ - LUIExtensionProperty( - name: 'pane-id', - kind: LUIExtensionValueKind.string, - isRequired: true, - ), - LUIExtensionProperty( - name: 'selected', - kind: LUIExtensionValueKind.string, - ), - LUIExtensionProperty( - name: 'focused', - kind: LUIExtensionValueKind.boolean, - ), - _accessibilityIdentifier, - ], - events: const [ - LUIExtensionEventSchema( - name: 'tab-selected', - fields: [ - LUIExtensionEventField( - name: 'tab', - kind: LUIExtensionValueKind.string, - isRequired: true, - ), - ], - ), - LUIExtensionEventSchema( - name: 'tab-closed', - fields: [ - LUIExtensionEventField( - name: 'tab', - kind: LUIExtensionValueKind.string, - isRequired: true, - ), - ], - ), - LUIExtensionEventSchema( - name: 'tab-moved', - fields: [ - LUIExtensionEventField( - name: 'tab', - kind: LUIExtensionValueKind.string, - isRequired: true, - ), - LUIExtensionEventField( - name: 'index', - kind: LUIExtensionValueKind.integer, - isRequired: true, - ), - LUIExtensionEventField( - name: 'from-pane', - kind: LUIExtensionValueKind.string, - isRequired: true, - ), - ], - ), - LUIExtensionEventSchema(name: 'pane-focused'), - LUIExtensionEventSchema( - name: 'navigate', - fields: [ - LUIExtensionEventField( - name: 'direction', - kind: LUIExtensionValueKind.string, - isRequired: true, - ), - ], - ), - LUIExtensionEventSchema( - name: 'split-requested', - fields: [ - LUIExtensionEventField( - name: 'orientation', - kind: LUIExtensionValueKind.string, - isRequired: true, - ), - ], - ), - LUIExtensionEventSchema( - name: 'split-drop', - fields: [ - LUIExtensionEventField( - name: 'tab', - kind: LUIExtensionValueKind.string, - isRequired: true, - ), - LUIExtensionEventField( - name: 'from-pane', - kind: LUIExtensionValueKind.string, - isRequired: true, - ), - LUIExtensionEventField( - name: 'edge', - kind: LUIExtensionValueKind.string, - isRequired: true, - ), - ], - ), - LUIExtensionEventSchema(name: 'pane-closed'), - ], - builder: _SplitPane.new, - ), - ) - ..register( - LUIFlutterExtension( - identifier: 'split-tab', - fingerprint: _tabFingerprint, - acceptsStandardChildren: true, - properties: const [ - LUIExtensionProperty( - name: 'tab-id', - kind: LUIExtensionValueKind.string, - isRequired: true, - ), - LUIExtensionProperty( - name: 'title', - kind: LUIExtensionValueKind.string, - isRequired: true, - ), - LUIExtensionProperty( - name: 'icon', - kind: LUIExtensionValueKind.string, - ), - LUIExtensionProperty( - name: 'dirty', - kind: LUIExtensionValueKind.boolean, - ), - LUIExtensionProperty( - name: 'closable', - kind: LUIExtensionValueKind.boolean, - ), - _accessibilityIdentifier, - ], - builder: _SplitTab.new, - ), - ); - } -} - -// MARK: - shared helpers - -typedef _TabDrag = ({String tab, String pane}); - -extension on LUIFlutterExtensionContext { - String? stringOf(String name) => property(name) as String?; - bool boolOf(String name, {bool fallback = false}) => - property(name) as bool? ?? fallback; - double doubleOf(String name, {double fallback = 0}) => - (property(name) as num?)?.toDouble() ?? fallback; - String? childString(int childID, String name) => - childProperty(childID, name) as String?; - bool childBool(int childID, String name, {bool fallback = false}) => - childProperty(childID, name) as bool? ?? fallback; -} - -/// Visual settings broadcast by `split-view` through the extension tree. -final class _SplitSettings extends InheritedWidget { - const _SplitSettings({ - required this.dividerThickness, - required this.animationEnabled, - required super.child, - }); - - final double dividerThickness; - final bool animationEnabled; - - static _SplitSettings of(BuildContext context) => - context.dependOnInheritedWidgetOfExactType<_SplitSettings>() ?? - const _SplitSettings( - dividerThickness: 9, - animationEnabled: true, - child: SizedBox.shrink(), - ); - - @override - bool updateShouldNotify(_SplitSettings old) => - dividerThickness != old.dividerThickness || - animationEnabled != old.animationEnabled; -} - -// MARK: - split-view - -final class _SplitView extends StatelessWidget { - const _SplitView(this.ext); - - final LUIFlutterExtensionContext ext; - - @override - Widget build(BuildContext context) => _SplitSettings( - dividerThickness: ext.doubleOf('divider-thickness', fallback: 9).clamp(1, 1e9), - animationEnabled: ext.boolOf('animation', fallback: true), - child: ext.content, - ); -} - -// MARK: - split-branch - -final class _SplitBranch extends StatefulWidget { - const _SplitBranch(this.ext); - - final LUIFlutterExtensionContext ext; - - @override - State<_SplitBranch> createState() => _SplitBranchState(); -} - -final class _SplitBranchState extends State<_SplitBranch> { - late double _ratio = _sourceRatio; - bool _dragging = false; - - double get _sourceRatio => - (widget.ext.doubleOf('ratio', fallback: 0.5)).clamp(0.0, 1.0); - - bool get _horizontal => - widget.ext.stringOf('orientation') != 'vertical'; - - @override - void didUpdateWidget(_SplitBranch old) { - super.didUpdateWidget(old); - if (!_dragging && (_sourceRatio - _ratio).abs() > 0.000001) { - _ratio = _sourceRatio; - } - } - - void _emit() => widget.ext.emit( - name: 'ratio-changed', - values: {'ratio': _ratio}, - ); - - void _nudge(double delta) { - setState(() => _ratio = (_ratio + delta).clamp(0.0, 1.0)); - _emit(); - } - - @override - Widget build(BuildContext context) { - final settings = _SplitSettings.of(context); - final childIDs = widget.ext.childIDs; - final first = childIDs.isNotEmpty ? childIDs[0] : null; - final second = childIDs.length > 1 ? childIDs[1] : null; - - return LayoutBuilder( - builder: (context, constraints) { - final axis = _horizontal ? constraints.maxWidth : constraints.maxHeight; - final gap = settings.dividerThickness; - final available = (axis - gap).clamp(0.0, double.infinity); - final firstLength = available * _ratio; - final secondLength = available - firstLength; - - final firstChild = SizedBox( - width: _horizontal ? firstLength : null, - height: _horizontal ? null : firstLength, - child: first != null ? widget.ext.contentFor(first) : null, - ); - final secondChild = SizedBox( - width: _horizontal ? secondLength : null, - height: _horizontal ? null : secondLength, - child: second != null ? widget.ext.contentFor(second) : null, - ); - - return Stack( - children: [ - Positioned.fill( - child: _horizontal - ? Row(children: [firstChild, SizedBox(width: gap), secondChild]) - : Column( - children: [firstChild, SizedBox(height: gap), secondChild], - ), - ), - Positioned( - left: _horizontal ? firstLength : 0, - top: _horizontal ? 0 : firstLength, - width: _horizontal ? gap : constraints.maxWidth, - height: _horizontal ? constraints.maxHeight : gap, - child: _Divider( - horizontal: _horizontal, - animate: settings.animationEnabled && !_dragging, - onDragStart: () => _dragging = true, - onDrag: (delta) { - setState( - () => _ratio = (_ratio + delta / available) - .clamp(0.0, 1.0), - ); - }, - onDragEnd: () { - _dragging = false; - _emit(); - }, - onNudge: _nudge, - ), - ), - ], - ); - }, - ); - } -} - -final class _Divider extends StatelessWidget { - const _Divider({ - required this.horizontal, - required this.animate, - required this.onDragStart, - required this.onDrag, - required this.onDragEnd, - required this.onNudge, - }); - - final bool horizontal; - final bool animate; - final VoidCallback onDragStart; - final ValueChanged onDrag; - final VoidCallback onDragEnd; - final ValueChanged onNudge; - - @override - Widget build(BuildContext context) => CallbackShortcuts( - bindings: { - SingleActivator( - horizontal ? LogicalKeyboardKey.arrowLeft : LogicalKeyboardKey.arrowUp, - ): () => onNudge(-0.05), - SingleActivator( - horizontal ? LogicalKeyboardKey.arrowRight : LogicalKeyboardKey.arrowDown, - ): () => onNudge(0.05), - }, - child: Focus( - child: MouseRegion( - cursor: horizontal - ? SystemMouseCursors.resizeColumn - : SystemMouseCursors.resizeRow, - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onPanStart: (_) => onDragStart(), - onPanUpdate: (details) => - onDrag(horizontal ? details.delta.dx : details.delta.dy), - onPanEnd: (_) => onDragEnd(), - onPanCancel: onDragEnd, - child: Center( - child: Container( - width: horizontal ? 1 : null, - height: horizontal ? null : 1, - color: Theme.of(context).dividerColor, - ), - ), - ), - ), - ), - ); -} - -// MARK: - split-pane - -final class _SplitPane extends StatefulWidget { - const _SplitPane(this.ext); - - final LUIFlutterExtensionContext ext; - - @override - State<_SplitPane> createState() => _SplitPaneState(); -} - -final class _SplitPaneState extends State<_SplitPane> { - String? _dropZone; // null | 'center' | 'left' | 'right' | 'top' | 'bottom' - int? _tabDropIndex; - final _focusNode = FocusNode(); - final _contentKey = GlobalKey(); - final _chipKeys = {}; - - GlobalKey _chipKey(int tabNode) => - _chipKeys.putIfAbsent(tabNode, GlobalKey.new); - - String get _paneId => widget.ext.stringOf('pane-id') ?? ''; - bool get _focused => widget.ext.boolOf('focused'); - - List get _tabs => widget.ext.childIDs; - - String _tabId(int tabNode) => - widget.ext.childString(tabNode, 'tab-id') ?? ''; - - String get _selectedId { - final selected = widget.ext.stringOf('selected'); - if (selected != null && selected.isNotEmpty) return selected; - return _tabs.isNotEmpty ? _tabId(_tabs.first) : ''; - } - - void _emit(String name, [Map values = const {}]) => - widget.ext.emit(name: name, values: values); - - void _acceptDrag(_TabDrag drag, String zone) { - setState(() => _dropZone = null); - if (zone == 'center') { - _emit('tab-moved', { - 'tab': drag.tab, - 'index': _tabs.length, - 'from-pane': drag.pane, - }); - } else { - _emit('split-drop', { - 'tab': drag.tab, - 'from-pane': drag.pane, - 'edge': zone, - }); - } - } - - @override - void dispose() { - _focusNode.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final shortcuts = { - for (final entry in { - 'left': LogicalKeyboardKey.arrowLeft, - 'right': LogicalKeyboardKey.arrowRight, - 'up': LogicalKeyboardKey.arrowUp, - 'down': LogicalKeyboardKey.arrowDown, - }.entries) - SingleActivator(entry.value, control: true, alt: true): () => - _emit('navigate', {'direction': entry.key}), - for (final entry in { - 'left': LogicalKeyboardKey.arrowLeft, - 'right': LogicalKeyboardKey.arrowRight, - 'up': LogicalKeyboardKey.arrowUp, - 'down': LogicalKeyboardKey.arrowDown, - }.entries) - SingleActivator(entry.value, meta: true, alt: true): () => - _emit('navigate', {'direction': entry.key}), - const SingleActivator(LogicalKeyboardKey.keyD, control: true, alt: true): - () => _emit('split-requested', {'orientation': 'horizontal'}), - const SingleActivator(LogicalKeyboardKey.keyD, meta: true, alt: true): - () => _emit('split-requested', {'orientation': 'horizontal'}), - const SingleActivator( - LogicalKeyboardKey.keyD, - control: true, - alt: true, - shift: true, - ): () => _emit('split-requested', {'orientation': 'vertical'}), - const SingleActivator( - LogicalKeyboardKey.keyD, - meta: true, - alt: true, - shift: true, - ): () => _emit('split-requested', {'orientation': 'vertical'}), - const SingleActivator(LogicalKeyboardKey.backslash, control: true): - () => _emit('split-requested', {'orientation': 'horizontal'}), - const SingleActivator(LogicalKeyboardKey.backslash, meta: true): - () => _emit('split-requested', {'orientation': 'horizontal'}), - const SingleActivator( - LogicalKeyboardKey.backslash, - control: true, - shift: true, - ): () => _emit('split-requested', {'orientation': 'vertical'}), - const SingleActivator(LogicalKeyboardKey.backslash, meta: true, shift: true): - () => _emit('split-requested', {'orientation': 'vertical'}), - const SingleActivator(LogicalKeyboardKey.keyW, control: true): - _closeSelected, - const SingleActivator(LogicalKeyboardKey.keyW, meta: true): - _closeSelected, - const SingleActivator(LogicalKeyboardKey.keyW, control: true, shift: true): - () => _emit('pane-closed'), - const SingleActivator(LogicalKeyboardKey.keyW, meta: true, shift: true): - () => _emit('pane-closed'), - }; - - return CallbackShortcuts( - bindings: shortcuts, - child: Focus( - focusNode: _focusNode, - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () { - _focusNode.requestFocus(); - _emit('pane-focused'); - }, - child: Container( - decoration: BoxDecoration( - border: Border.all( - color: _focused - ? Theme.of(context).colorScheme.primary.withAlpha(140) - : Colors.transparent, - width: 2, - ), - borderRadius: BorderRadius.circular(4), - ), - clipBehavior: Clip.antiAlias, - child: Column( - children: [ - _tabBar(context), - Expanded(child: _content(context)), - ], - ), - ), - ), - ), - ); - } - - void _closeSelected() { - final selected = _selectedId; - if (selected.isNotEmpty) _emit('tab-closed', {'tab': selected}); - } - - Widget _tabBar(BuildContext context) { - final theme = Theme.of(context); - return Container( - height: 30, - color: theme.colorScheme.surfaceContainerHighest, - child: ListView( - scrollDirection: Axis.horizontal, - children: [ - for (var i = 0; i < _tabs.length; i++) _tabChip(_tabs[i], i), - _trailingIndicator(), - ], - ), - ); - } - - Widget _trailingIndicator() => AnimatedSize( - duration: const Duration(milliseconds: 120), - child: _tabDropIndex == _tabs.length - ? Container(width: 2, height: 18, color: Colors.blueAccent) - : const SizedBox.shrink(), - ); - - Widget _tabChip(int tabNode, int index) { - final theme = Theme.of(context); - final tabId = _tabId(tabNode); - final selected = tabId == _selectedId; - final title = widget.ext.childString(tabNode, 'title') ?? tabId; - final dirty = widget.ext.childBool(tabNode, 'dirty'); - final closable = widget.ext.childBool(tabNode, 'closable', fallback: true); - - Widget chip = Container( - height: 30, - padding: const EdgeInsets.symmetric(horizontal: 10), - color: selected - ? theme.colorScheme.surface - : Colors.transparent, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (dirty) - const Padding( - padding: EdgeInsets.only(right: 4), - child: Icon(Icons.circle, size: 5), - ), - Text( - title, - style: theme.textTheme.labelMedium?.copyWith( - color: selected ? null : theme.hintColor, - ), - ), - if (closable) - GestureDetector( - onTap: () => _emit('tab-closed', {'tab': tabId}), - child: const Padding( - padding: EdgeInsets.only(left: 4), - child: Icon(Icons.close, size: 12), - ), - ), - ], - ), - ); - - chip = GestureDetector( - onTap: () { - _focusNode.requestFocus(); - _emit('tab-selected', {'tab': tabId}); - _emit('pane-focused'); - }, - child: chip, - ); - - chip = Draggable<_TabDrag>( - data: (tab: tabId, pane: _paneId), - feedback: Material( - color: Colors.transparent, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), - decoration: BoxDecoration( - color: theme.colorScheme.surfaceContainerHigh, - borderRadius: BorderRadius.circular(6), - boxShadow: kElevationToShadow[2], - ), - child: Text(title, style: theme.textTheme.labelMedium), - ), - ), - childWhenDragging: Opacity(opacity: 0.4, child: chip), - child: chip, - ); - - return DragTarget<_TabDrag>( - key: _chipKey(tabNode), - onMove: (details) { - final box = - _chipKey(tabNode).currentContext?.findRenderObject() as RenderBox?; - if (box == null) return; - final local = box.globalToLocal(details.offset); - // Left/right halves pick before/after this index. - setState( - () => _tabDropIndex = - local.dx < box.size.width / 2 ? index : index + 1, - ); - }, - onLeave: (_) { - if (_tabDropIndex == index || _tabDropIndex == index + 1) { - setState(() => _tabDropIndex = null); - } - }, - onAcceptWithDetails: (details) { - final target = _tabDropIndex ?? index; - setState(() => _tabDropIndex = null); - _emit('tab-moved', { - 'tab': details.data.tab, - 'index': target, - 'from-pane': details.data.pane, - }); - }, - builder: (context, candidates, _) => Stack( - children: [ - chip, - if (_tabDropIndex == index) - const Positioned( - left: 0, - top: 6, - bottom: 6, - child: SizedBox( - width: 2, - child: ColoredBox(color: Colors.blueAccent), - ), - ), - ], - ), - ); - } - - Widget _content(BuildContext context) => DragTarget<_TabDrag>( - key: _contentKey, - onMove: (details) => setState(() { - final box = _contentKey.currentContext?.findRenderObject() as RenderBox?; - if (box == null) return; - final local = box.globalToLocal(details.offset); - final ex = (box.size.width * 0.25).clamp(48.0, 160.0); - final ey = (box.size.height * 0.25).clamp(48.0, 160.0); - _dropZone = local.dx < ex - ? 'left' - : local.dx > box.size.width - ex - ? 'right' - : local.dy < ey - ? 'top' - : local.dy > box.size.height - ey - ? 'bottom' - : 'center'; - }), - onLeave: (_) => setState(() => _dropZone = null), - onAcceptWithDetails: (details) => _acceptDrag(details.data, _dropZone ?? 'center'), - builder: (context, candidates, _) => Stack( - fit: StackFit.expand, - children: [ - // Every tab stays mounted so scroll/focus/editor state survives - // switching; only the selected one is visible and interactive. - for (final tabNode in _tabs) - Offstage( - offstage: _tabId(tabNode) != _selectedId, - child: TickerMode( - enabled: _tabId(tabNode) == _selectedId, - child: widget.ext.contentFor(tabNode), - ), - ), - if (_dropZone != null) _dropZoneOverlay(context, _dropZone!), - ], - ), - ); - - Widget _dropZoneOverlay(BuildContext context, String zone) { - final accent = Theme.of(context).colorScheme.primary; - if (zone == 'center') { - return IgnorePointer(child: ColoredBox(color: accent.withAlpha(26))); - } - final horizontal = zone == 'left' || zone == 'right'; - return IgnorePointer( - child: LayoutBuilder( - builder: (context, constraints) { - final primary = - horizontal ? constraints.maxWidth : constraints.maxHeight; - final extent = (primary * 0.35).clamp(48.0, double.infinity); - return Align( - alignment: switch (zone) { - 'left' => Alignment.centerLeft, - 'right' => Alignment.centerRight, - 'top' => Alignment.topCenter, - _ => Alignment.bottomCenter, - }, - child: Container( - width: horizontal ? extent : constraints.maxWidth, - height: horizontal ? constraints.maxHeight : extent, - color: accent.withAlpha(46), - ), - ); - }, - ), - ); - } -} - -// MARK: - split-tab - -/// Tab nodes are data carriers; a bare `split-tab` stacks its children. -final class _SplitTab extends StatelessWidget { - const _SplitTab(this.ext); - - final LUIFlutterExtensionContext ext; - - @override - Widget build(BuildContext context) => Stack( - children: [ - for (final child in ext.childIDs) Positioned.fill(child: ext.contentFor(child)), - ], - ); -} diff --git a/platform/flutter/lib/lui_ocaml_bridge.dart b/platform/flutter/lib/lui_ocaml_bridge.dart deleted file mode 100644 index 48896f18..00000000 --- a/platform/flutter/lib/lui_ocaml_bridge.dart +++ /dev/null @@ -1,455 +0,0 @@ -import 'dart:ffi'; - -import 'package:ffi/ffi.dart'; -import 'package:flutter/foundation.dart'; - -typedef _NativePatchCallback = Void Function(Pointer json); -typedef _NativeStart = - Int32 Function( - Pointer> callback, - Int32 platform, - Int32 host, - ); -typedef _DartStart = - int Function( - Pointer> callback, - int platform, - int host, - ); -typedef _NativeAppear = Int32 Function(Int64 node); -typedef _DartAppear = int Function(int node); -typedef _NativePress = Int32 Function(Int64 node); -typedef _DartPress = int Function(int node); -typedef _NativeLongPress = Int32 Function(Int64 node); -typedef _DartLongPress = int Function(int node); -typedef _NativeTextChanged = Int32 Function( - Int64 node, - Pointer text, - Int32 byteLength, -); -typedef _DartTextChanged = int Function( - int node, - Pointer text, - int byteLength, -); -typedef _NativeSubmit = Int32 Function(Int64 node); -typedef _DartSubmit = int Function(int node); -typedef _NativeDismiss = Int32 Function(Int64 node); -typedef _DartDismiss = int Function(int node); -typedef _NativePicked = Int32 Function( - Int64 node, - Pointer payload, - Int32 byteLength, -); -typedef _DartPicked = int Function( - int node, - Pointer payload, - int byteLength, -); -typedef _NativeDoublePress = Int32 Function(Int64 node); -typedef _DartDoublePress = int Function(int node); -typedef _NativeToggleChanged = Int32 Function(Int64 node, Int32 checked); -typedef _DartToggleChanged = int Function(int node, int checked); -typedef _NativeRadioChanged = Int32 Function(Int64 node); -typedef _DartRadioChanged = int Function(int node); -typedef _NativeSliderChanged = Int32 Function(Int64 node, Double value); -typedef _DartSliderChanged = int Function(int node, double value); -typedef _NativeStop = Int32 Function(); -typedef _DartStop = int Function(); -typedef _NativeNode = Int64 Function(); -typedef _DartNode = int Function(); -typedef _NativeExtensionEvent = Int32 Function( - Int64 node, - Pointer identifier, - Pointer name, - Pointer jsonValues, -); -typedef _DartExtensionEvent = int Function( - int node, - Pointer identifier, - Pointer name, - Pointer jsonValues, -); - -typedef _NativePointerDetail = Int32 Function( - Int64 node, - Double x, - Double y, - Int32 modifiers, - Int32 button, - Pointer targetClass, -); -typedef _DartPointerDetail = int Function( - int node, - double x, - double y, - int modifiers, - int button, - Pointer targetClass, -); - -final class LUIOcamlBridge { - LUIOcamlBridge._(this.library, this.onPatch) - : _start = library.lookupFunction<_NativeStart, _DartStart>( - 'lui_ocaml_start', - ), - _appear = library.lookupFunction<_NativeAppear, _DartAppear>( - 'lui_ocaml_appear', - ), - _press = library.lookupFunction<_NativePress, _DartPress>( - 'lui_ocaml_press', - ), - _longPress = library.lookupFunction<_NativeLongPress, _DartLongPress>( - 'lui_ocaml_long_press', - ), - _textChanged = library - .lookupFunction<_NativeTextChanged, _DartTextChanged>( - 'lui_ocaml_text_changed_utf8', - ), - _submit = library.lookupFunction<_NativeSubmit, _DartSubmit>( - 'lui_ocaml_submit', - ), - _dismiss = library.lookupFunction<_NativeDismiss, _DartDismiss>( - 'lui_ocaml_dismiss', - ), - _picked = library.lookupFunction<_NativePicked, _DartPicked>( - 'lui_ocaml_picked_utf8', - ), - _doublePress = library - .lookupFunction<_NativeDoublePress, _DartDoublePress>( - 'lui_ocaml_double_press', - ), - _toggleChanged = library - .lookupFunction<_NativeToggleChanged, _DartToggleChanged>( - 'lui_ocaml_toggle_changed', - ), - _radioChanged = library - .lookupFunction<_NativeRadioChanged, _DartRadioChanged>( - 'lui_ocaml_radio_changed', - ), - _sliderChanged = library - .lookupFunction<_NativeSliderChanged, _DartSliderChanged>( - 'lui_ocaml_slider_changed', - ), - _stop = library.lookupFunction<_NativeStop, _DartStop>('lui_ocaml_stop'), - _rootNode = library.lookupFunction<_NativeNode, _DartNode>( - 'lui_ocaml_root_node', - ), - _extensionEvent = library - .lookupFunction<_NativeExtensionEvent, _DartExtensionEvent>( - 'lui_ocaml_extension_event', - ), - _pressDetail = library - .lookupFunction<_NativePointerDetail, _DartPointerDetail>( - 'lui_ocaml_press_detail', - ), - _pointerDown = library - .lookupFunction<_NativePointerDetail, _DartPointerDetail>( - 'lui_ocaml_pointer_down', - ), - _pointerUp = library - .lookupFunction<_NativePointerDetail, _DartPointerDetail>( - 'lui_ocaml_pointer_up', - ), - _contextMenuPress = library - .lookupFunction<_NativePointerDetail, _DartPointerDetail>( - 'lui_ocaml_context_menu_press', - ), - _pointerEnter = library.lookupFunction<_NativePress, _DartPress>( - 'lui_ocaml_pointer_enter', - ), - _pointerLeave = library.lookupFunction<_NativePress, _DartPress>( - 'lui_ocaml_pointer_leave', - ); - - factory LUIOcamlBridge.open( - String libraryPath, { - required void Function(String json) onPatch, - }) => LUIOcamlBridge._(DynamicLibrary.open(libraryPath), onPatch); - - factory LUIOcamlBridge.process({ - required void Function(String json) onPatch, - }) => LUIOcamlBridge._(DynamicLibrary.process(), onPatch); - - final DynamicLibrary library; - final void Function(String json) onPatch; - final _DartStart _start; - final _DartAppear _appear; - final _DartPress _press; - final _DartLongPress _longPress; - final _DartTextChanged _textChanged; - final _DartSubmit _submit; - final _DartDismiss _dismiss; - final _DartPicked _picked; - final _DartDoublePress _doublePress; - final _DartToggleChanged _toggleChanged; - final _DartRadioChanged _radioChanged; - final _DartSliderChanged _sliderChanged; - final _DartStop _stop; - final _DartNode _rootNode; - final _DartExtensionEvent _extensionEvent; - final _DartPointerDetail _pressDetail; - final _DartPointerDetail _pointerDown; - final _DartPointerDetail _pointerUp; - final _DartPointerDetail _contextMenuPress; - final _DartPress _pointerEnter; - final _DartPress _pointerLeave; - NativeCallable<_NativePatchCallback>? _patchCallback; - - void start() { - if (_patchCallback != null) { - throw StateError('OCaml bridge is already started'); - } - final callback = NativeCallable<_NativePatchCallback>.isolateLocal( - (Pointer json) => onPatch(json.toDartString()), - ); - _patchCallback = callback; - if (_start( - callback.nativeFunction, - _platformCode(defaultTargetPlatform), - 3, - ) != - 1) { - callback.close(); - _patchCallback = null; - throw StateError('OCaml runtime initialization failed'); - } - } - - static int _platformCode(TargetPlatform platform) => switch (platform) { - TargetPlatform.macOS => 1, - TargetPlatform.iOS => 2, - TargetPlatform.android => 3, - TargetPlatform.linux => 4, - TargetPlatform.windows => 5, - TargetPlatform.fuchsia => 0, - }; - - void appear(int node) { - if (_appear(node) != 1) throw StateError('OCaml appear dispatch failed'); - } - - void press(int node) { - if (_press(node) != 1) throw StateError('OCaml press dispatch failed'); - } - - int _pointerDetail( - _DartPointerDetail dispatch, - int node, - double x, - double y, - int modifiers, - int button, - String targetClass, - ) { - final nativeTarget = targetClass.toNativeUtf8(); - try { - return dispatch(node, x, y, modifiers, button, nativeTarget); - } finally { - malloc.free(nativeTarget); - } - } - - void pressDetail( - int node, - double x, - double y, - int modifiers, - int button, - String targetClass, - ) { - if (_pointerDetail( - _pressDetail, - node, - x, - y, - modifiers, - button, - targetClass, - ) != - 1) { - throw StateError('OCaml press-detail dispatch failed'); - } - } - - void pointerDown( - int node, - double x, - double y, - int modifiers, - int button, - String targetClass, - ) { - if (_pointerDetail( - _pointerDown, - node, - x, - y, - modifiers, - button, - targetClass, - ) != - 1) { - throw StateError('OCaml pointer-down dispatch failed'); - } - } - - void pointerUp( - int node, - double x, - double y, - int modifiers, - int button, - String targetClass, - ) { - if (_pointerDetail( - _pointerUp, - node, - x, - y, - modifiers, - button, - targetClass, - ) != - 1) { - throw StateError('OCaml pointer-up dispatch failed'); - } - } - - void contextMenuPress( - int node, - double x, - double y, - int modifiers, - int button, - String targetClass, - ) { - if (_pointerDetail( - _contextMenuPress, - node, - x, - y, - modifiers, - button, - targetClass, - ) != - 1) { - throw StateError('OCaml context-menu-press dispatch failed'); - } - } - - void pointerEnter(int node) { - if (_pointerEnter(node) != 1) { - throw StateError('OCaml pointer-enter dispatch failed'); - } - } - - void pointerLeave(int node) { - if (_pointerLeave(node) != 1) { - throw StateError('OCaml pointer-leave dispatch failed'); - } - } - - void longPress(int node) { - if (_longPress(node) != 1) { - throw StateError('OCaml long-press dispatch failed'); - } - } - - void doublePress(int node) { - if (_doublePress(node) != 1) { - throw StateError('OCaml double-press dispatch failed'); - } - } - - void textChanged(int node, String text) { - final nativeText = text.toNativeUtf8(); - try { - if (_textChanged(node, nativeText, nativeText.length) != 1) { - throw StateError('OCaml text dispatch failed'); - } - } finally { - malloc.free(nativeText); - } - } - - void submit(int node) { - if (_submit(node) != 1) throw StateError('OCaml submit dispatch failed'); - } - - void dismiss(int node) { - if (_dismiss(node) != 1) throw StateError('OCaml dismiss dispatch failed'); - } - - void picked(int node, String payload) { - final nativePayload = payload.toNativeUtf8(); - try { - if (_picked(node, nativePayload, nativePayload.length) != 1) { - throw StateError('OCaml picked dispatch failed'); - } - } finally { - malloc.free(nativePayload); - } - } - - void toggleChanged(int node, bool checked) { - if (_toggleChanged(node, checked ? 1 : 0) != 1) { - throw StateError('OCaml toggle dispatch failed'); - } - } - - void radioChanged(int node) { - if (_radioChanged(node) != 1) { - throw StateError('OCaml radio dispatch failed'); - } - } - - void sliderChanged(int node, double value) { - if (_sliderChanged(node, value) != 1) { - throw StateError('OCaml slider dispatch failed'); - } - } - - void extensionEvent( - int node, - String identifier, - String name, - String jsonValues, - ) { - final nativeIdentifier = identifier.toNativeUtf8(); - final nativeName = name.toNativeUtf8(); - final nativeValues = jsonValues.toNativeUtf8(); - try { - if (_extensionEvent( - node, - nativeIdentifier, - nativeName, - nativeValues, - ) != - 1) { - throw StateError('OCaml extension event dispatch failed'); - } - } finally { - malloc.free(nativeIdentifier); - malloc.free(nativeName); - malloc.free(nativeValues); - } - } - - int get rootNode => _requiredNode(_rootNode(), 'root'); - - int _requiredNode(int node, String name) { - if (node < 0) throw StateError('OCaml $name node lookup failed'); - return node; - } - - void close() { - final callback = _patchCallback; - if (callback == null) return; - if (_stop() != 1) { - throw StateError('OCaml runtime disposal failed'); - } - callback.close(); - _patchCallback = null; - } -} diff --git a/platform/flutter/lib/lui_wire_schema.g.dart b/platform/flutter/lib/lui_wire_schema.g.dart deleted file mode 100644 index 9009820b..00000000 --- a/platform/flutter/lib/lui_wire_schema.g.dart +++ /dev/null @@ -1,190 +0,0 @@ -// Generated from schema/components.json. Do not edit by hand. -part of 'lui_flutter_backend.dart'; - -enum _NodeKind { - root, - row, - column, - grid, - stack, - edgeInset, - overlay, - viewThatFits, - panel, - card, - alert, - bubble, - box, - text, - heading, - paragraph, - label, - button, - toggleButton, - toggle, - radioGroup, - radio, - slider, - numberStepper, - textField, - secureField, - input, - searchField, - textarea, - checkbox, - switchControl, - progress, - divider, - scroll, - list, - virtualList, - tabs, - bottomTabs, - bottomTab, - buttonGroup, - toggleGroup, - spacer, - spinner, - icon, - select, - combobox, - dropdownMenu, - contextMenu, - menuItem, - menuTrigger, - listItem, - avatar, - image, - mediaSurface, - stepper, - step, - timeline, - timelineItem, - inputGroup, - inputGroupActions, - breadcrumb, - pagination, - accordion, - table, - tableRow, - tableCell, - tree, - resizable, - split, - dialog, - drawer, - sheet, - tooltip, - toast, - toolbar, - statusBar, - listSection, - listSectionHeader, - listSectionFooter, - swipeActions, - swipeAction, - filePicker, - kbd, - link, - fileImage, - filePreview, - br, - popover, -} - -_NodeKind _decodeNodeKind(Object? value) { - if (value is! String) { - throw const LUIBackendException('kind must be a string'); - } - return switch (value) { - 'root' => _NodeKind.root, - 'row' => _NodeKind.row, - 'column' => _NodeKind.column, - 'grid' => _NodeKind.grid, - 'stack' => _NodeKind.stack, - 'edge-inset' => _NodeKind.edgeInset, - 'overlay' => _NodeKind.overlay, - 'view-that-fits' => _NodeKind.viewThatFits, - 'panel' => _NodeKind.panel, - 'card' => _NodeKind.card, - 'alert' => _NodeKind.alert, - 'bubble' => _NodeKind.bubble, - 'box' => _NodeKind.box, - 'text' => _NodeKind.text, - 'heading' => _NodeKind.heading, - 'paragraph' => _NodeKind.paragraph, - 'label' => _NodeKind.label, - 'button' => _NodeKind.button, - 'toggle-button' => _NodeKind.toggleButton, - 'toggle' => _NodeKind.toggle, - 'radio-group' => _NodeKind.radioGroup, - 'radio' => _NodeKind.radio, - 'slider' => _NodeKind.slider, - 'number-stepper' => _NodeKind.numberStepper, - 'text-field' => _NodeKind.textField, - 'secure-field' => _NodeKind.secureField, - 'input' => _NodeKind.input, - 'search-field' => _NodeKind.searchField, - 'textarea' => _NodeKind.textarea, - 'checkbox' => _NodeKind.checkbox, - 'switch' => _NodeKind.switchControl, - 'progress' => _NodeKind.progress, - 'divider' => _NodeKind.divider, - 'scroll' => _NodeKind.scroll, - 'list' => _NodeKind.list, - 'virtual-list' => _NodeKind.virtualList, - 'tabs' => _NodeKind.tabs, - 'bottom-tabs' => _NodeKind.bottomTabs, - 'bottom-tab' => _NodeKind.bottomTab, - 'button-group' => _NodeKind.buttonGroup, - 'toggle-group' => _NodeKind.toggleGroup, - 'spacer' => _NodeKind.spacer, - 'spinner' => _NodeKind.spinner, - 'icon' => _NodeKind.icon, - 'select' => _NodeKind.select, - 'combobox' => _NodeKind.combobox, - 'dropdown-menu' => _NodeKind.dropdownMenu, - 'context-menu' => _NodeKind.contextMenu, - 'menu-item' => _NodeKind.menuItem, - 'menu-trigger' => _NodeKind.menuTrigger, - 'list-item' => _NodeKind.listItem, - 'avatar' => _NodeKind.avatar, - 'image' => _NodeKind.image, - 'media-surface' => _NodeKind.mediaSurface, - 'stepper' => _NodeKind.stepper, - 'step' => _NodeKind.step, - 'timeline' => _NodeKind.timeline, - 'timeline-item' => _NodeKind.timelineItem, - 'input-group' => _NodeKind.inputGroup, - 'input-group-actions' => _NodeKind.inputGroupActions, - 'breadcrumb' => _NodeKind.breadcrumb, - 'pagination' => _NodeKind.pagination, - 'accordion' => _NodeKind.accordion, - 'table' => _NodeKind.table, - 'table-row' => _NodeKind.tableRow, - 'table-cell' => _NodeKind.tableCell, - 'tree' => _NodeKind.tree, - 'resizable' => _NodeKind.resizable, - 'split' => _NodeKind.split, - 'dialog' => _NodeKind.dialog, - 'drawer' => _NodeKind.drawer, - 'sheet' => _NodeKind.sheet, - 'tooltip' => _NodeKind.tooltip, - 'toast' => _NodeKind.toast, - 'toolbar' => _NodeKind.toolbar, - 'status-bar' => _NodeKind.statusBar, - 'list-section' => _NodeKind.listSection, - 'list-section-header' => _NodeKind.listSectionHeader, - 'list-section-footer' => _NodeKind.listSectionFooter, - 'swipe-actions' => _NodeKind.swipeActions, - 'swipe-action' => _NodeKind.swipeAction, - 'file-picker' => _NodeKind.filePicker, - 'kbd' => _NodeKind.kbd, - 'link' => _NodeKind.link, - 'file-image' => _NodeKind.fileImage, - 'file-preview' => _NodeKind.filePreview, - 'br' => _NodeKind.br, - 'popover' => _NodeKind.popover, - _ => throw const LUIBackendException('unknown node kind'), - }; -} diff --git a/platform/flutter/native/lui_ocaml_bridge.c b/platform/flutter/native/lui_ocaml_bridge.c deleted file mode 100644 index 4bf813d1..00000000 --- a/platform/flutter/native/lui_ocaml_bridge.c +++ /dev/null @@ -1,330 +0,0 @@ -#include -#include - -#include -#include -#include -#include -#include - -#if defined(_WIN32) -#define LUI_EXPORT __declspec(dllexport) -#else -#define LUI_EXPORT __attribute__((visibility("default"))) -#endif - -typedef void (*lui_patch_callback)(const char *json); - -static int runtime_started = 0; -static lui_patch_callback patch_callback = NULL; - -/* Lock discipline: after lui_ocaml_start the host thread does NOT hold the - OCaml runtime lock between calls, so OCaml worker domains keep running - (stop-the-world GC included) while the host sits in its event loop. Every - exported entry re-acquires the lock with caml_leave_blocking_section() on - the way in and releases it with caml_enter_blocking_section() on the way - out — including lui_ocaml_start, whose caml_startup leaves the lock held. - Callers must therefore be OCaml-registered threads (the thread that ran - lui_ocaml_start, or one registered via caml_c_thread_register) and must - not re-enter these functions from inside the patch callback, which runs - while the lock is held. */ - -static int emit_patch(value result) { - if (Is_exception_result(result)) { - return 0; - } - const char *json = String_val(result); - if (patch_callback != NULL && json[0] != '\0') { - patch_callback(json); - } - return 1; -} - -LUI_EXPORT int32_t lui_ocaml_start( - lui_patch_callback callback, - int32_t platform_code, - int32_t host_code) { - int32_t accepted = 0; - patch_callback = callback; - if (!runtime_started) { - char *arguments[] = {"lui_flutter", NULL}; - caml_startup(arguments); - runtime_started = 1; - } else { - caml_leave_blocking_section(); - } - - const value *initialize = caml_named_value("lui_flutter_init"); - if (initialize != NULL) { - accepted = emit_patch(caml_callback2_exn( - *initialize, - Val_long(platform_code), - Val_long(host_code))); - } - caml_enter_blocking_section(); - return accepted; -} - -LUI_EXPORT int32_t lui_ocaml_appear(int64_t node) { - const value *dispatch = caml_named_value("lui_flutter_appear"); - int32_t accepted = 0; - if (dispatch == NULL) { - return 0; - } - caml_leave_blocking_section(); - accepted = emit_patch(caml_callback_exn(*dispatch, Val_long(node))); - caml_enter_blocking_section(); - return accepted; -} - -LUI_EXPORT int32_t lui_ocaml_press(int64_t node) { - const value *dispatch = caml_named_value("lui_flutter_press"); - int32_t accepted = 0; - if (dispatch == NULL) { - return 0; - } - caml_leave_blocking_section(); - accepted = emit_patch(caml_callback_exn(*dispatch, Val_long(node))); - caml_enter_blocking_section(); - return accepted; -} - -LUI_EXPORT int32_t lui_ocaml_long_press(int64_t node) { - const value *dispatch = caml_named_value("lui_flutter_long_press"); - int32_t accepted = 0; - if (dispatch == NULL) { - return 0; - } - caml_leave_blocking_section(); - accepted = emit_patch(caml_callback_exn(*dispatch, Val_long(node))); - caml_enter_blocking_section(); - return accepted; -} - -static int32_t text_changed_locked( - int64_t node, - const char *text, - int32_t byte_length) { - CAMLparam0(); - CAMLlocal2(text_value, result); - int32_t accepted = 0; - const value *dispatch = caml_named_value("lui_flutter_text_changed"); - if (dispatch != NULL && text != NULL && byte_length >= 0) { - text_value = caml_alloc_initialized_string(byte_length, text); - result = caml_callback2_exn(*dispatch, Val_long(node), text_value); - accepted = emit_patch(result); - } - CAMLreturnT(int32_t, accepted); -} - -/* Length-explicit variant: text may contain embedded NUL bytes. */ -LUI_EXPORT int32_t lui_ocaml_text_changed_utf8( - int64_t node, - const char *text, - int32_t byte_length) { - int32_t accepted; - caml_leave_blocking_section(); - accepted = text_changed_locked(node, text, byte_length); - caml_enter_blocking_section(); - return accepted; -} - -LUI_EXPORT int32_t lui_ocaml_text_changed(int64_t node, const char *text) { - if (text == NULL) { - return 0; - } - return lui_ocaml_text_changed_utf8(node, text, (int32_t)strlen(text)); -} - -LUI_EXPORT int32_t lui_ocaml_submit(int64_t node) { - const value *dispatch = caml_named_value("lui_flutter_submit"); - int32_t accepted = 0; - if (dispatch == NULL) { - return 0; - } - caml_leave_blocking_section(); - accepted = emit_patch(caml_callback_exn(*dispatch, Val_long(node))); - caml_enter_blocking_section(); - return accepted; -} - -LUI_EXPORT int32_t lui_ocaml_dismiss(int64_t node) { - const value *dispatch = caml_named_value("lui_flutter_dismiss"); - int32_t accepted = 0; - if (dispatch == NULL) { - return 0; - } - caml_leave_blocking_section(); - accepted = emit_patch(caml_callback_exn(*dispatch, Val_long(node))); - caml_enter_blocking_section(); - return accepted; -} - -static int32_t picked_locked( - int64_t node, - const char *payload, - int32_t byte_length) { - CAMLparam0(); - CAMLlocal2(payload_value, result); - int32_t accepted = 0; - const value *dispatch = caml_named_value("lui_flutter_picked"); - if (dispatch != NULL && payload != NULL && byte_length >= 0) { - payload_value = caml_alloc_initialized_string(byte_length, payload); - result = caml_callback2_exn(*dispatch, Val_long(node), payload_value); - accepted = emit_patch(result); - } - CAMLreturnT(int32_t, accepted); -} - -/* Length-explicit variant: payload may contain embedded NUL bytes. */ -LUI_EXPORT int32_t lui_ocaml_picked_utf8( - int64_t node, - const char *payload, - int32_t byte_length) { - int32_t accepted; - caml_leave_blocking_section(); - accepted = picked_locked(node, payload, byte_length); - caml_enter_blocking_section(); - return accepted; -} - -LUI_EXPORT int32_t lui_ocaml_picked(int64_t node, const char *payload) { - if (payload == NULL) { - return 0; - } - return lui_ocaml_picked_utf8(node, payload, (int32_t)strlen(payload)); -} - -LUI_EXPORT int32_t lui_ocaml_double_press(int64_t node) { - const value *dispatch = caml_named_value("lui_flutter_double_press"); - int32_t accepted = 0; - if (dispatch == NULL) { - return 0; - } - caml_leave_blocking_section(); - accepted = emit_patch(caml_callback_exn(*dispatch, Val_long(node))); - caml_enter_blocking_section(); - return accepted; -} - -static int32_t toggle_changed_locked(int64_t node, int32_t checked) { - CAMLparam0(); - CAMLlocal1(result); - int32_t accepted = 0; - const value *dispatch = caml_named_value("lui_flutter_toggle_changed"); - if (dispatch != NULL) { - result = caml_callback2_exn( - *dispatch, - Val_long(node), - Val_bool(checked != 0)); - accepted = emit_patch(result); - } - CAMLreturnT(int32_t, accepted); -} - -LUI_EXPORT int32_t lui_ocaml_toggle_changed(int64_t node, int32_t checked) { - int32_t accepted; - caml_leave_blocking_section(); - accepted = toggle_changed_locked(node, checked); - caml_enter_blocking_section(); - return accepted; -} - -LUI_EXPORT int32_t lui_ocaml_radio_changed(int64_t node) { - const value *dispatch = caml_named_value("lui_flutter_radio_changed"); - int32_t accepted = 0; - if (dispatch == NULL) { - return 0; - } - caml_leave_blocking_section(); - accepted = emit_patch(caml_callback_exn(*dispatch, Val_long(node))); - caml_enter_blocking_section(); - return accepted; -} - -static int32_t slider_changed_locked(int64_t node, double fraction) { - CAMLparam0(); - CAMLlocal2(value_value, result); - int32_t accepted = 0; - const value *dispatch = caml_named_value("lui_flutter_slider_changed"); - if (dispatch != NULL) { - value_value = caml_copy_double(fraction); - result = caml_callback2_exn(*dispatch, Val_long(node), value_value); - accepted = emit_patch(result); - } - CAMLreturnT(int32_t, accepted); -} - -LUI_EXPORT int32_t lui_ocaml_slider_changed(int64_t node, double fraction) { - int32_t accepted; - caml_leave_blocking_section(); - accepted = slider_changed_locked(node, fraction); - caml_enter_blocking_section(); - return accepted; -} - -static int32_t extension_event_locked( - int64_t node, - const char *identifier, - const char *name, - const char *json_values) { - CAMLparam0(); - CAMLlocal4(identifier_value, name_value, values_value, result); - int32_t accepted = 0; - const value *dispatch = caml_named_value("lui_flutter_extension_event"); - if (dispatch != NULL && identifier != NULL && name != NULL) { - value arguments[4]; - identifier_value = caml_copy_string(identifier); - name_value = caml_copy_string(name); - values_value = caml_copy_string(json_values == NULL ? "" : json_values); - arguments[0] = Val_long(node); - arguments[1] = identifier_value; - arguments[2] = name_value; - arguments[3] = values_value; - result = caml_callbackN_exn(*dispatch, 4, arguments); - accepted = emit_patch(result); - } - CAMLreturnT(int32_t, accepted); -} - -LUI_EXPORT int32_t lui_ocaml_extension_event( - int64_t node, - const char *identifier, - const char *name, - const char *json_values) { - int32_t accepted; - caml_leave_blocking_section(); - accepted = extension_event_locked(node, identifier, name, json_values); - caml_enter_blocking_section(); - return accepted; -} - -LUI_EXPORT int32_t lui_ocaml_stop(void) { - const value *dispose = caml_named_value("lui_flutter_dispose"); - int32_t accepted = 0; - if (dispose == NULL) { - return 0; - } - caml_leave_blocking_section(); - accepted = emit_patch(caml_callback_exn(*dispose, Val_unit)); - caml_enter_blocking_section(); - return accepted; -} - -static int64_t read_node(const char *callback_name) { - const value *callback = caml_named_value(callback_name); - value result; - int64_t node; - if (callback == NULL) { - return -1; - } - caml_leave_blocking_section(); - result = caml_callback_exn(*callback, Val_unit); - node = Is_exception_result(result) ? -1 : Long_val(result); - caml_enter_blocking_section(); - return node; -} - -LUI_EXPORT int64_t lui_ocaml_root_node(void) { - return read_node("lui_flutter_root_node"); -} diff --git a/platform/flutter/pubspec.lock b/platform/flutter/pubspec.lock deleted file mode 100644 index bc4fb753..00000000 --- a/platform/flutter/pubspec.lock +++ /dev/null @@ -1,213 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - async: - dependency: transitive - description: - name: async - sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 - url: "https://pub.dev" - source: hosted - version: "2.13.1" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" - url: "https://pub.dev" - source: hosted - version: "2.1.2" - characters: - dependency: transitive - description: - name: characters - sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b - url: "https://pub.dev" - source: hosted - version: "1.4.1" - clock: - dependency: transitive - description: - name: clock - sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b - url: "https://pub.dev" - source: hosted - version: "1.1.2" - collection: - dependency: transitive - description: - name: collection - sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" - url: "https://pub.dev" - source: hosted - version: "1.19.1" - fake_async: - dependency: transitive - description: - name: fake_async - sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" - url: "https://pub.dev" - source: hosted - version: "1.3.3" - ffi: - dependency: "direct main" - description: - name: ffi - sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" - url: "https://pub.dev" - source: hosted - version: "2.2.0" - flutter: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_lints: - dependency: "direct dev" - description: - name: flutter_lints - sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" - url: "https://pub.dev" - source: hosted - version: "6.0.0" - flutter_test: - dependency: "direct dev" - description: flutter - source: sdk - version: "0.0.0" - leak_tracker: - dependency: transitive - description: - name: leak_tracker - sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" - url: "https://pub.dev" - source: hosted - version: "11.0.2" - leak_tracker_flutter_testing: - dependency: transitive - description: - name: leak_tracker_flutter_testing - sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" - url: "https://pub.dev" - source: hosted - version: "3.0.10" - leak_tracker_testing: - dependency: transitive - description: - name: leak_tracker_testing - sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" - url: "https://pub.dev" - source: hosted - version: "3.0.2" - lints: - dependency: transitive - description: - name: lints - sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" - url: "https://pub.dev" - source: hosted - version: "6.1.0" - matcher: - dependency: transitive - description: - name: matcher - sha256: "31bd099b47c10cd1aeb55146a2d46ce0277630ecef3f7dae54ad7873f36696cd" - url: "https://pub.dev" - source: hosted - version: "0.12.20" - material_color_utilities: - dependency: transitive - description: - name: material_color_utilities - sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" - url: "https://pub.dev" - source: hosted - version: "0.13.0" - meta: - dependency: transitive - description: - name: meta - sha256: "307249ce4ff29d58a18e97f6345f539382eb9c9c29ecda628900f31de0443dd9" - url: "https://pub.dev" - source: hosted - version: "1.19.0" - path: - dependency: transitive - description: - name: path - sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" - url: "https://pub.dev" - source: hosted - version: "1.9.1" - sky_engine: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - source_span: - dependency: transitive - description: - name: source_span - sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" - url: "https://pub.dev" - source: hosted - version: "1.10.2" - stack_trace: - dependency: transitive - description: - name: stack_trace - sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" - url: "https://pub.dev" - source: hosted - version: "1.12.1" - stream_channel: - dependency: transitive - description: - name: stream_channel - sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" - url: "https://pub.dev" - source: hosted - version: "2.1.4" - string_scanner: - dependency: transitive - description: - name: string_scanner - sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" - url: "https://pub.dev" - source: hosted - version: "1.4.1" - term_glyph: - dependency: transitive - description: - name: term_glyph - sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" - url: "https://pub.dev" - source: hosted - version: "1.2.2" - test_api: - dependency: transitive - description: - name: test_api - sha256: "2a122cbe059f8b610d3a5415f42e255b6c17b1f21eee1d960f31080237fb4f11" - url: "https://pub.dev" - source: hosted - version: "0.7.12" - vector_math: - dependency: transitive - description: - name: vector_math - sha256: f36f9f3be64c6198714492bb455c11056e33e2f85d9a0b676a48301e44fdcf47 - url: "https://pub.dev" - source: hosted - version: "2.4.2" - vm_service: - dependency: transitive - description: - name: vm_service - sha256: "5f37239c4851efcef929cea7824e76df7f2f0970aef85d66bbc430afa40e72f0" - url: "https://pub.dev" - source: hosted - version: "15.3.0" -sdks: - dart: ">=3.11.0-0 <4.0.0" - flutter: ">=3.18.0-18.0.pre.54" diff --git a/platform/flutter/pubspec.yaml b/platform/flutter/pubspec.yaml deleted file mode 100644 index 0c399b31..00000000 --- a/platform/flutter/pubspec.yaml +++ /dev/null @@ -1,20 +0,0 @@ -name: lui_flutter_backend -description: Flutter host backend for the LG LUI runtime. -version: 0.1.0 -publish_to: none - -environment: - sdk: ^3.9.0 - -dependencies: - ffi: ^2.1.4 - flutter: - sdk: flutter - -dev_dependencies: - flutter_test: - sdk: flutter - flutter_lints: ^6.0.0 - -flutter: - uses-material-design: true diff --git a/platform/flutter/test/lui_flutter_backend_test.dart b/platform/flutter/test/lui_flutter_backend_test.dart deleted file mode 100644 index e5fb537d..00000000 --- a/platform/flutter/test/lui_flutter_backend_test.dart +++ /dev/null @@ -1,3771 +0,0 @@ -import 'dart:ui' as ui; - -import 'package:flutter/material.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:lui_flutter_backend/lui_flutter_backend.dart'; - -void main() { - test('removes a retained property without replacing its node', () { - final backend = LUIFlutterBackend() - ..applyJson(''' - {"generation":1,"ops":[ - {"op":"create-node","id":1,"kind":"column"}, - {"op":"set-prop","id":1,"property":"padding","value":24} - ]} - '''); - - backend.applyJson(''' - {"generation":2,"ops":[ - {"op":"remove-prop","id":1,"property":"padding"} - ]} - '''); - - expect(backend.generation, 2); - expect(backend.containsNode(1), isTrue); - }); - - testWidgets('keeps one transparent runtime root while replacing its child', ( - tester, - ) async { - final backend = LUIFlutterBackend() - ..applyJson(''' - {"generation":1,"ops":[ - {"op":"create-node","id":1,"kind":"root"}, - {"op":"create-node","id":2,"kind":"text"}, - {"op":"set-prop","id":2,"property":"text","value":"Before"}, - {"op":"insert-child","parent":1,"child":2,"index":0} - ]} - '''); - - await tester.pumpWidget( - MaterialApp(home: Scaffold(body: backend.widget(node: 1))), - ); - final rootElement = tester.element( - find.byKey(LUIFlutterBackend.nodeKey(1)), - ); - expect(find.text('Before'), findsOneWidget); - - backend.applyJson(''' - {"generation":2,"ops":[ - {"op":"create-node","id":3,"kind":"text"}, - {"op":"set-prop","id":3,"property":"text","value":"After"}, - {"op":"remove-child","parent":1,"child":2}, - {"op":"insert-child","parent":1,"child":3,"index":0}, - {"op":"drop-node","id":2} - ]} - '''); - await tester.pump(); - - expect( - identical( - rootElement, - tester.element(find.byKey(LUIFlutterBackend.nodeKey(1))), - ), - isTrue, - ); - expect(find.text('Before'), findsNothing); - expect(find.text('After'), findsOneWidget); - expect(backend.containsNode(2), isFalse); - }); - - test('rejects a runtime root with more than one child', () { - final backend = LUIFlutterBackend() - ..applyJson(''' - {"generation":1,"ops":[ - {"op":"create-node","id":1,"kind":"root"}, - {"op":"create-node","id":2,"kind":"text"}, - {"op":"insert-child","parent":1,"child":2,"index":0} - ]} - '''); - - expect( - () => backend.applyJson(''' - {"generation":2,"ops":[ - {"op":"create-node","id":3,"kind":"text"}, - {"op":"insert-child","parent":1,"child":3,"index":1} - ]} - '''), - throwsA(isA()), - ); - expect(backend.generation, 1); - expect(backend.containsNode(3), isFalse); - }); - - testWidgets('registered extensions retain identity and validate events', ( - tester, - ) async { - final registry = LUIFlutterExtensionRegistry() - ..register( - LUIFlutterExtension( - identifier: 'native-card', - fingerprint: 'native-card-v1', - acceptsStandardChildren: true, - properties: const [ - LUIExtensionProperty( - name: 'title', - kind: LUIExtensionValueKind.string, - isRequired: true, - ), - LUIExtensionProperty( - name: 'elevation', - kind: LUIExtensionValueKind.doubleValue, - defaultValue: 1.0, - ), - ], - events: const [ - LUIExtensionEventSchema( - name: 'activate', - fields: [ - LUIExtensionEventField( - name: 'source', - kind: LUIExtensionValueKind.string, - isRequired: true, - ), - ], - ), - ], - builder: (context) => FilledButton( - onPressed: () => context.emit( - name: 'activate', - values: const {'source': 'flutter'}, - ), - child: Text(context.property('title')! as String), - ), - ), - ); - final events = []; - final backend = - LUIFlutterBackend(extensionRegistry: registry, onEvent: events.add) - ..applyJson(''' - {"generation":1,"ops":[ - {"op":"create-node","id":1,"kind":"column"}, - {"op":"create-extension","id":2,"identifier":"native-card","fingerprint":"native-card-v1"}, - {"op":"set-extension-prop","id":2,"property":"title","value":"Flutter native"}, - {"op":"insert-child","parent":1,"child":2,"index":0} - ]} - '''); - - await tester.pumpWidget( - MaterialApp(home: Scaffold(body: backend.widget(node: 1))), - ); - expect(find.text('Flutter native'), findsOneWidget); - expect(backend.debugRevision(2), 0); - - backend.applyJson(''' - {"generation":2,"ops":[ - {"op":"set-extension-prop","id":2,"property":"title","value":"Updated native"} - ]} - '''); - await tester.pump(); - expect(find.text('Updated native'), findsOneWidget); - expect(backend.debugRevision(2), 1); - - await tester.tap(find.text('Updated native')); - expect(events, const [ - LUIEvent.extension( - node: 2, - identifier: 'native-card', - name: 'activate', - values: {'source': 'flutter'}, - ), - ]); - - expect( - () => backend.applyJson(''' - {"generation":3,"ops":[ - {"op":"create-extension","id":3,"identifier":"native-card","fingerprint":"wrong"} - ]} - '''), - throwsA(isA()), - ); - expect(backend.generation, 2); - expect(backend.containsNode(3), isFalse); - }); - - test('extension registrations cannot shadow standard node names', () { - final registry = LUIFlutterExtensionRegistry(); - expect( - () => registry.register( - LUIFlutterExtension( - identifier: 'button', - fingerprint: 'shadow', - builder: (_) => const SizedBox.shrink(), - ), - ), - throwsA(isA()), - ); - expect( - () => registry.register( - LUIFlutterExtension( - identifier: 'native-card', - fingerprint: 'native-card-v1', - childIdentifiers: const ['native-child', 'native-child'], - builder: (_) => const SizedBox.shrink(), - ), - ), - throwsA(isA()), - ); - }); - - testWidgets('registered platform tweaks wrap exactly one retained child', ( - tester, - ) async { - final registry = LUIFlutterExtensionRegistry() - ..registerTweak( - LUIFlutterTweak( - identifier: 'material-card', - fingerprint: 'material-card-v1', - builder: (content, _) => - Padding(padding: const EdgeInsets.all(8), child: content), - ), - ); - final backend = LUIFlutterBackend(extensionRegistry: registry) - ..applyJson(''' - {"generation":1,"ops":[ - {"op":"create-extension","id":1,"identifier":"material-card","fingerprint":"material-card-v1"}, - {"op":"create-node","id":2,"kind":"button"}, - {"op":"set-prop","id":2,"property":"text","value":"Save"}, - {"op":"insert-child","parent":1,"child":2,"index":0}, - {"op":"create-node","id":3,"kind":"tabs"}, - {"op":"insert-child","parent":3,"child":1,"index":0} - ]} - '''); - - await tester.pumpWidget(MaterialApp(home: backend.widget(node: 3))); - expect(find.widgetWithText(TextButton, 'Save'), findsOneWidget); - expect( - () => backend.applyJson(''' - {"generation":2,"ops":[ - {"op":"create-extension","id":4,"identifier":"material-card","fingerprint":"material-card-v1"} - ]} - '''), - throwsA(isA()), - ); - expect(backend.generation, 1); - }); - - test('projects each direct retained root child as one Gallery section', () { - final backend = LUIFlutterBackend() - ..applyJson(''' - {"generation":1,"ops":[ - {"op":"create-node","id":1,"kind":"column"}, - {"op":"create-node","id":2,"kind":"column"}, - {"op":"create-node","id":3,"kind":"heading"}, - {"op":"set-prop","id":3,"property":"text","value":"Button"}, - {"op":"insert-child","parent":2,"child":3,"index":0}, - {"op":"insert-child","parent":1,"child":2,"index":0}, - {"op":"create-node","id":4,"kind":"column"}, - {"op":"create-node","id":5,"kind":"heading"}, - {"op":"set-prop","id":5,"property":"text","value":"Tabs"}, - {"op":"insert-child","parent":4,"child":5,"index":0}, - {"op":"insert-child","parent":1,"child":4,"index":1} - ]} - '''); - - expect(backend.rootSections(1), const [ - LUIRootSection(id: 2, title: 'Button'), - LUIRootSection(id: 4, title: 'Tabs'), - ]); - }); - - test('projects sections inside an application root container', () { - final backend = LUIFlutterBackend() - ..applyJson('''{"generation":1,"ops":[ - {"op":"create-node","id":1,"kind":"root"}, - {"op":"create-node","id":2,"kind":"column"}, - {"op":"create-node","id":3,"kind":"heading"}, - {"op":"set-prop","id":3,"property":"text","value":"Button"}, - {"op":"insert-child","parent":2,"child":3,"index":0}, - {"op":"insert-child","parent":1,"child":2,"index":0} - ]}'''); - expect(backend.rootSections(1), const [ - LUIRootSection(id: 3, title: 'Button'), - ]); - }); - - testWidgets('applies one LG patch batch to real Flutter widgets', ( - tester, - ) async { - final events = []; - final backend = LUIFlutterBackend(onEvent: events.add) - ..applyJson(_initialBatch); - - await tester.pumpWidget( - MaterialApp(home: Scaffold(body: backend.widget(node: 1))), - ); - - final row = tester.widget(find.byType(LUIFlex)); - expect(row.spacing, 12); - expect(find.text('Hello from LG'), findsOneWidget); - await tester.tap(find.widgetWithText(TextButton, 'Continue')); - expect(events, [const LUIEvent.press(node: 3)]); - }); - - testWidgets('emits appear events when nodes mount', (tester) async { - final events = []; - final backend = LUIFlutterBackend(onEvent: events.add) - ..applyJson(''' - {"generation":1,"ops":[ - {"op":"create-node","id":1,"kind":"column"}, - {"op":"create-node","id":2,"kind":"list-item"}, - {"op":"set-prop","id":2,"property":"text","value":"Load more"}, - {"op":"set-prop","id":2,"property":"appear-enabled","value":true}, - {"op":"insert-child","parent":1,"child":2,"index":0} - ]} - '''); - - await tester.pumpWidget( - MaterialApp(home: Scaffold(body: backend.widget(node: 1))), - ); - await tester.pump(); - - expect(events, const [LUIEvent.appear(node: 2)]); - }); - - testWidgets('maps Tabs to retained native Button triggers', (tester) async { - final semantics = tester.ensureSemantics(); - final events = []; - final backend = LUIFlutterBackend(onEvent: events.add) - ..applyJson(''' - {"generation":1,"ops":[ - {"op":"create-node","id":1,"kind":"tabs"}, - {"op":"create-node","id":2,"kind":"button"}, - {"op":"create-node","id":3,"kind":"button"}, - {"op":"create-node","id":4,"kind":"toggle-button"}, - {"op":"set-prop","id":1,"property":"gap","value":4}, - {"op":"set-prop","id":1,"property":"orientation","value":"horizontal"}, - {"op":"set-prop","id":1,"property":"accessibility-label","value":"Workspace sections"}, - {"op":"set-prop","id":2,"property":"text","value":"Overview"}, - {"op":"set-prop","id":2,"property":"selected","value":true}, - {"op":"set-prop","id":3,"property":"text","value":"Activity"}, - {"op":"set-prop","id":3,"property":"selected","value":false}, - {"op":"set-prop","id":4,"property":"text","value":"Pinned"}, - {"op":"set-prop","id":4,"property":"selected","value":false}, - {"op":"insert-child","parent":1,"child":2,"index":0}, - {"op":"insert-child","parent":1,"child":3,"index":1}, - {"op":"insert-child","parent":1,"child":4,"index":2} - ]} - '''); - - await tester.pumpWidget( - MaterialApp(home: Scaffold(body: backend.widget(node: 1))), - ); - - final tabs = tester.widget( - find - .descendant( - of: find.byKey(LUIFlutterBackend.nodeKey(1)), - matching: find.byWidgetPredicate( - (widget) => widget is LUIFlex && widget.direction == Axis.horizontal, - ), - ) - .first, - ); - expect(tabs.spacing, 4); - expect( - find.descendant( - of: find.byKey(LUIFlutterBackend.nodeKey(1)), - matching: find.byWidgetPredicate( - (widget) => - widget is Semantics && - widget.properties.label == 'Workspace sections', - ), - ), - findsOneWidget, - ); - expect(find.text('Overview'), findsOneWidget); - expect(find.text('Activity'), findsOneWidget); - expect( - tester - .widget(find.byKey(const ValueKey('lui-tab-2'))) - .properties - .selected, - isTrue, - ); - - await tester.tap(find.text('Activity')); - await tester.tap(find.text('Pinned')); - await tester.pump(); - expect(events, const [ - LUIEvent.press(node: 3), - LUIEvent.toggleChanged(node: 4, checked: true), - ]); - - final tabsRevision = backend.debugRevision(1); - final overviewRevision = backend.debugRevision(2); - final toggleRevision = backend.debugRevision(4); - backend.applyJson(''' - {"generation":2,"ops":[ - {"op":"set-prop","id":2,"property":"selected","value":false}, - {"op":"set-prop","id":3,"property":"selected","value":true} - ]} - '''); - await tester.pump(); - expect(backend.debugRevision(1), tabsRevision); - expect(backend.debugRevision(2), overviewRevision + 1); - expect(backend.debugRevision(4), toggleRevision); - expect( - tester - .widget(find.byKey(const ValueKey('lui-tab-3'))) - .properties - .selected, - isTrue, - ); - semantics.dispose(); - }); - - testWidgets('maps BottomTabs to Material NavigationBar with retained pages', ( - tester, - ) async { - final events = []; - final backend = LUIFlutterBackend(onEvent: events.add); - expect( - () => backend.applyJson(''' - {"generation":1,"ops":[ - {"op":"create-node","id":1,"kind":"bottom-tabs"}, - {"op":"create-node","id":2,"kind":"bottom-tab"}, - {"op":"create-node","id":3,"kind":"bottom-tab"}, - {"op":"create-node","id":4,"kind":"paragraph"}, - {"op":"create-node","id":5,"kind":"paragraph"}, - {"op":"set-prop","id":1,"property":"accessibility-label","value":"Primary destinations"}, - {"op":"set-prop","id":2,"property":"title","value":"Home"}, - {"op":"set-prop","id":2,"property":"icon","value":"folder"}, - {"op":"set-prop","id":2,"property":"selected","value":true}, - {"op":"set-prop","id":2,"property":"press-enabled","value":true}, - {"op":"set-prop","id":3,"property":"title","value":"Search"}, - {"op":"set-prop","id":3,"property":"icon","value":"search"}, - {"op":"set-prop","id":3,"property":"selected","value":false}, - {"op":"set-prop","id":3,"property":"press-enabled","value":true}, - {"op":"set-prop","id":4,"property":"text","value":"Home page"}, - {"op":"set-prop","id":5,"property":"text","value":"Search page"}, - {"op":"insert-child","parent":2,"child":4,"index":0}, - {"op":"insert-child","parent":3,"child":5,"index":0}, - {"op":"insert-child","parent":1,"child":2,"index":0}, - {"op":"insert-child","parent":1,"child":3,"index":1} - ]} - '''), - returnsNormally, - ); - - await tester.pumpWidget( - MaterialApp(home: Scaffold(body: backend.widget(node: 1))), - ); - - expect(find.byType(NavigationBar), findsOneWidget); - expect( - tester.widget(find.byType(NavigationBar)).selectedIndex, - 0, - ); - expect(find.text('Home page'), findsOneWidget); - expect(find.text('Search page'), findsNothing); - - await tester.tap(find.text('Search')); - await tester.pump(); - expect(events, const [LUIEvent.press(node: 3)]); - - final tabsRevision = backend.debugRevision(1); - final homePageRevision = backend.debugRevision(4); - backend.applyJson(''' - {"generation":2,"ops":[ - {"op":"set-prop","id":2,"property":"selected","value":false}, - {"op":"set-prop","id":3,"property":"selected","value":true} - ]} - '''); - await tester.pump(); - - expect( - tester.widget(find.byType(NavigationBar)).selectedIndex, - 1, - ); - expect(find.text('Home page'), findsNothing); - expect(find.text('Search page'), findsOneWidget); - expect(backend.debugRevision(1), tabsRevision); - expect(backend.debugRevision(4), homePageRevision); - }); - - testWidgets('maps ButtonGroup and ToggleGroup as retained native groups', ( - tester, - ) async { - final events = []; - final backend = LUIFlutterBackend(onEvent: events.add) - ..applyJson(''' - {"generation":1,"ops":[ - {"op":"create-node","id":1,"kind":"column"}, - {"op":"create-node","id":2,"kind":"button-group"}, - {"op":"create-node","id":3,"kind":"toggle-group"}, - {"op":"create-node","id":4,"kind":"button"}, - {"op":"create-node","id":5,"kind":"toggle-button"}, - {"op":"create-node","id":6,"kind":"toggle-button"}, - {"op":"set-prop","id":2,"property":"gap","value":4}, - {"op":"set-prop","id":3,"property":"gap","value":8}, - {"op":"set-prop","id":4,"property":"text","value":"Save"}, - {"op":"set-prop","id":5,"property":"text","value":"Pin"}, - {"op":"set-prop","id":5,"property":"selected","value":true}, - {"op":"set-prop","id":6,"property":"text","value":"Backend-owned"}, - {"op":"insert-child","parent":1,"child":2,"index":0}, - {"op":"insert-child","parent":1,"child":3,"index":1}, - {"op":"insert-child","parent":2,"child":4,"index":0}, - {"op":"insert-child","parent":2,"child":5,"index":1}, - {"op":"insert-child","parent":3,"child":6,"index":0} - ]} - '''); - - await tester.pumpWidget( - MaterialApp(home: Scaffold(body: backend.widget(node: 1))), - ); - - final buttonGroup = tester.widget( - find - .descendant( - of: find.byKey(LUIFlutterBackend.nodeKey(2)), - matching: find.byWidgetPredicate( - (widget) => widget is LUIFlex && widget.direction == Axis.horizontal, - ), - ) - .first, - ); - final toggleGroup = tester.widget( - find - .descendant( - of: find.byKey(LUIFlutterBackend.nodeKey(3)), - matching: find.byWidgetPredicate( - (widget) => widget is LUIFlex && widget.direction == Axis.horizontal, - ), - ) - .first, - ); - expect(buttonGroup.spacing, 4); - expect(toggleGroup.spacing, 8); - expect(find.text('Save'), findsOneWidget); - expect(find.text('Backend-owned'), findsOneWidget); - - await tester.tap(find.text('Save')); - await tester.tap(find.text('Pin')); - await tester.pump(); - expect(events, const [ - LUIEvent.press(node: 4), - LUIEvent.toggleChanged(node: 5, checked: false), - ]); - - final buttonGroupRevision = backend.debugRevision(2); - final toggleGroupRevision = backend.debugRevision(3); - final pinRevision = backend.debugRevision(5); - final backendOwnedRevision = backend.debugRevision(6); - backend.applyJson(''' - {"generation":2,"ops":[ - {"op":"set-prop","id":5,"property":"selected","value":false}, - {"op":"remove-child","parent":3,"child":6}, - {"op":"insert-child","parent":2,"child":6,"index":2} - ]} - '''); - await tester.pump(); - expect(backend.debugRevision(2), buttonGroupRevision + 1); - expect(backend.debugRevision(3), toggleGroupRevision + 1); - expect(backend.debugRevision(5), pinRevision + 1); - expect(backend.debugRevision(6), backendOwnedRevision + 1); - expect(find.text('Backend-owned'), findsOneWidget); - }); - - testWidgets('horizontal groups wrap focus and skip disabled controls', ( - tester, - ) async { - final events = []; - final backend = LUIFlutterBackend(onEvent: events.add) - ..applyJson(''' - {"generation":1,"ops":[ - {"op":"create-node","id":1,"kind":"button-group"}, - {"op":"create-node","id":2,"kind":"button"}, - {"op":"create-node","id":3,"kind":"button"}, - {"op":"create-node","id":4,"kind":"toggle-button"}, - {"op":"set-prop","id":2,"property":"text","value":"First"}, - {"op":"set-prop","id":2,"property":"autofocus","value":true}, - {"op":"set-prop","id":3,"property":"text","value":"Disabled"}, - {"op":"set-prop","id":3,"property":"enabled","value":false}, - {"op":"set-prop","id":4,"property":"text","value":"Last"}, - {"op":"insert-child","parent":1,"child":2,"index":0}, - {"op":"insert-child","parent":1,"child":3,"index":1}, - {"op":"insert-child","parent":1,"child":4,"index":2} - ]} - '''); - - await tester.pumpWidget( - MaterialApp(home: Scaffold(body: backend.widget(node: 1))), - ); - await tester.pumpAndSettle(); - - ButtonStyleButton control(String label) => tester.widget( - find.ancestor( - of: find.text(label), - matching: find.byWidgetPredicate( - (widget) => widget is ButtonStyleButton, - ), - ), - ); - - expect(control('First').focusNode?.hasFocus, isTrue); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); - await tester.pump(); - expect(control('Last').focusNode?.hasFocus, isTrue); - expect(events, isEmpty); - - await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); - await tester.pump(); - expect(control('First').focusNode?.hasFocus, isTrue); - - await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft); - await tester.pump(); - expect(control('Last').focusNode?.hasFocus, isTrue); - - await tester.sendKeyEvent(LogicalKeyboardKey.home); - await tester.pump(); - expect(control('First').focusNode?.hasFocus, isTrue); - - await tester.sendKeyEvent(LogicalKeyboardKey.end); - await tester.pump(); - expect(control('Last').focusNode?.hasFocus, isTrue); - expect(control('Disabled').focusNode?.hasFocus, isFalse); - expect(events, isEmpty); - }); - - testWidgets('maps Breadcrumb and Pagination as retained native groups', ( - tester, - ) async { - final semantics = tester.ensureSemantics(); - final events = []; - final backend = LUIFlutterBackend(onEvent: events.add) - ..applyJson(''' - {"generation":1,"ops":[ - {"op":"create-node","id":1,"kind":"column"}, - {"op":"create-node","id":2,"kind":"breadcrumb"}, - {"op":"create-node","id":3,"kind":"text"}, - {"op":"create-node","id":4,"kind":"icon"}, - {"op":"create-node","id":5,"kind":"text"}, - {"op":"create-node","id":6,"kind":"pagination"}, - {"op":"create-node","id":7,"kind":"button"}, - {"op":"create-node","id":8,"kind":"button"}, - {"op":"set-prop","id":2,"property":"accessibility-label","value":"Component path"}, - {"op":"set-prop","id":3,"property":"text","value":"Home"}, - {"op":"set-prop","id":3,"property":"press-enabled","value":true}, - {"op":"set-prop","id":4,"property":"name","value":"chevron-right"}, - {"op":"set-prop","id":5,"property":"text","value":"Components"}, - {"op":"set-prop","id":6,"property":"accessibility-label","value":"Gallery pages"}, - {"op":"set-prop","id":7,"property":"text","value":"1"}, - {"op":"set-prop","id":7,"property":"selected","value":true}, - {"op":"set-prop","id":8,"property":"text","value":"2"}, - {"op":"insert-child","parent":1,"child":2,"index":0}, - {"op":"insert-child","parent":1,"child":6,"index":1}, - {"op":"insert-child","parent":2,"child":3,"index":0}, - {"op":"insert-child","parent":2,"child":4,"index":1}, - {"op":"insert-child","parent":2,"child":5,"index":2}, - {"op":"insert-child","parent":6,"child":7,"index":0}, - {"op":"insert-child","parent":6,"child":8,"index":1} - ]} - '''); - - await tester.pumpWidget( - MaterialApp(home: Scaffold(body: backend.widget(node: 1))), - ); - - LUIFlex groupRow(int node) => tester.widget( - find - .descendant( - of: find.byKey(LUIFlutterBackend.nodeKey(node)), - matching: find.byWidgetPredicate( - (widget) => widget is LUIFlex && widget.direction == Axis.horizontal, - ), - ) - .first, - ); - expect(groupRow(2).spacing, 4); - expect(groupRow(6).spacing, 2); - expect(find.text('Home'), findsOneWidget); - expect(find.text('Components'), findsOneWidget); - expect( - tester.getSemantics(find.text('Home')), - matchesSemantics( - label: 'Home', - isButton: true, - hasEnabledState: true, - isEnabled: true, - isFocusable: true, - hasFocusAction: true, - hasTapAction: true, - ), - ); - - await tester.tap(find.text('Home')); - await tester.tap(find.text('2')); - await tester.pump(); - expect(events, const [LUIEvent.press(node: 3), LUIEvent.press(node: 8)]); - - final breadcrumbRevision = backend.debugRevision(2); - final homeRevision = backend.debugRevision(3); - final paginationRevision = backend.debugRevision(6); - backend.applyJson(''' - {"generation":2,"ops":[ - {"op":"set-prop","id":7,"property":"selected","value":false}, - {"op":"set-prop","id":8,"property":"selected","value":true} - ]} - '''); - await tester.pump(); - expect(backend.debugRevision(2), breadcrumbRevision); - expect(backend.debugRevision(3), homeRevision); - expect(backend.debugRevision(6), paginationRevision); - semantics.dispose(); - }); - - test('rejects text on Tabs instead of silently ignoring it', () { - final backend = LUIFlutterBackend(); - expect( - () => backend.applyJson(''' - {"generation":1,"ops":[ - {"op":"create-node","id":1,"kind":"tabs"}, - {"op":"set-prop","id":1,"property":"text","value":"Overview"} - ]} - '''), - throwsA( - isA().having( - (error) => error.message, - 'message', - contains('unsupported property'), - ), - ), - ); - expect(backend.generation, 0); - }); - - testWidgets('maps the complete Vercel Native Button contract', ( - tester, - ) async { - final semantics = tester.ensureSemantics(); - final events = []; - final backend = LUIFlutterBackend(onEvent: events.add) - ..applyJson(''' - {"generation":1,"ops":[ - {"op":"create-node","id":1,"kind":"button"}, - {"op":"set-prop","id":1,"property":"text","value":"Download"}, - {"op":"set-prop","id":1,"property":"variant","value":"primary"}, - {"op":"set-prop","id":1,"property":"size","value":"lg"}, - {"op":"set-prop","id":1,"property":"icon","value":"download"}, - {"op":"set-prop","id":1,"property":"icon-placement","value":"trailing"}, - {"op":"set-prop","id":1,"property":"selected","value":true}, - {"op":"set-prop","id":1,"property":"autofocus","value":true}, - {"op":"set-prop","id":1,"property":"accessibility-label","value":"Download report"}, - {"op":"set-prop","id":1,"property":"accessibility-identifier","value":"button.download"}, - {"op":"set-prop","id":1,"property":"long-press-enabled","value":true} - ]} - '''); - - await tester.pumpWidget( - MaterialApp(home: Scaffold(body: backend.widget(node: 1))), - ); - - final button = tester.widget(find.byType(FilledButton)); - expect(button.autofocus, isTrue); - expect(find.byIcon(Icons.download), findsOneWidget); - expect( - tester.getSemantics(find.byType(FilledButton)), - matchesSemantics( - label: 'Download report', - identifier: 'button.download', - hasSelectedState: true, - isButton: true, - isSelected: true, - isEnabled: true, - hasEnabledState: true, - hasTapAction: true, - hasLongPressAction: true, - ), - ); - - await tester.longPress(find.byType(FilledButton)); - await tester.pump(); - expect(events, [const LUIEvent.longPress(node: 1)]); - - events.clear(); - await tester.tap(find.byType(FilledButton)); - await tester.pump(); - expect(events, [const LUIEvent.press(node: 1)]); - semantics.dispose(); - }); - - test('rejects invalid and unnamed icon-only Buttons atomically', () { - final invalidVariant = LUIFlutterBackend(); - expect( - () => invalidVariant.applyJson(''' - {"generation":1,"ops":[ - {"op":"create-node","id":1,"kind":"button"}, - {"op":"set-prop","id":1,"property":"text","value":"Link"}, - {"op":"set-prop","id":1,"property":"variant","value":"link"} - ]} - '''), - throwsA(isA()), - ); - expect(invalidVariant.generation, 0); - - final unnamed = LUIFlutterBackend(); - expect( - () => unnamed.applyJson(''' - {"generation":1,"ops":[ - {"op":"create-node","id":1,"kind":"button"}, - {"op":"set-prop","id":1,"property":"size","value":"icon"}, - {"op":"set-prop","id":1,"property":"icon","value":"plus"} - ]} - '''), - throwsA(isA()), - ); - expect(unnamed.generation, 0); - }); - - testWidgets('retains controlled and uncontrolled ToggleButton selection', ( - tester, - ) async { - final events = []; - final backend = LUIFlutterBackend(onEvent: events.add) - ..applyJson(''' - {"generation":1,"ops":[ - {"op":"create-node","id":1,"kind":"row"}, - {"op":"create-node","id":2,"kind":"toggle-button"}, - {"op":"create-node","id":3,"kind":"toggle-button"}, - {"op":"set-prop","id":2,"property":"text","value":"Bold"}, - {"op":"set-prop","id":2,"property":"variant","value":"outline"}, - {"op":"set-prop","id":2,"property":"selected","value":false}, - {"op":"set-prop","id":3,"property":"text","value":"Italic"}, - {"op":"set-prop","id":3,"property":"icon","value":"edit"}, - {"op":"insert-child","parent":1,"child":2,"index":0}, - {"op":"insert-child","parent":1,"child":3,"index":1} - ]} - '''); - - await tester.pumpWidget( - MaterialApp(home: Scaffold(body: backend.widget(node: 1))), - ); - await tester.tap(find.widgetWithText(OutlinedButton, 'Bold')); - await tester.tap(find.widgetWithText(TextButton, 'Italic')); - await tester.pump(); - - expect(events, const [ - LUIEvent.toggleChanged(node: 2, checked: true), - LUIEvent.toggleChanged(node: 3, checked: true), - ]); - expect( - tester - .widget(find.byKey(const ValueKey('lui-toggle-2'))) - .properties - .selected, - isTrue, - ); - expect( - tester - .widget(find.byKey(const ValueKey('lui-toggle-3'))) - .properties - .selected, - isTrue, - ); - - backend.applyJson(''' - {"generation":2,"ops":[ - {"op":"set-prop","id":3,"property":"text","value":"Italic style"} - ]} - '''); - await tester.pump(); - expect( - tester - .widget(find.byKey(const ValueKey('lui-toggle-3'))) - .properties - .selected, - isTrue, - reason: 'an unrelated patch must not erase backend-owned selection', - ); - - backend.applyJson(''' - {"generation":3,"ops":[ - {"op":"set-prop","id":2,"property":"selected","value":true} - ]} - '''); - await tester.pump(); - expect( - tester - .widget(find.byKey(const ValueKey('lui-toggle-2'))) - .properties - .selected, - isTrue, - reason: 'a model selection patch must reconcile local interaction', - ); - }); - - testWidgets('keyed move preserves the Flutter RenderObject', (tester) async { - final backend = LUIFlutterBackend()..applyJson(_initialBatch); - - await tester.pumpWidget( - MaterialApp(home: Scaffold(body: backend.widget(node: 1))), - ); - final originalButton = tester.renderObject( - find.byKey(LUIFlutterBackend.nodeKey(3)), - ); - - backend.applyJson( - '{"generation":2,"ops":[' - '{"op":"move-child","parent":1,"child":3,"index":0}]}', - ); - await tester.pump(); - - expect( - tester.renderObject(find.byKey(LUIFlutterBackend.nodeKey(3))), - same(originalButton), - ); - }); - - test('rejects an invalid batch atomically', () { - final backend = LUIFlutterBackend()..applyJson(_initialBatch); - - expect( - () => backend.applyJson( - '{"generation":2,"ops":[' - '{"op":"create-node","id":4,"kind":"text"},' - '{"op":"insert-child","parent":99,"child":4,"index":0}]}', - ), - throwsA(isA()), - ); - expect(backend.containsNode(4), isFalse); - expect(backend.generation, 1); - }); - - test('rejects duplicate or skipped generations atomically', () { - final backend = LUIFlutterBackend()..applyJson(_initialBatch); - - expect( - () => backend.applyJson( - '{"generation":3,"ops":[' - '{"op":"create-node","id":4,"kind":"text"}]}', - ), - throwsA( - isA().having( - (error) => error.message, - 'message', - contains('expected patch generation 2'), - ), - ), - ); - expect(backend.containsNode(4), isFalse); - expect(backend.generation, 1); - }); - - test('property patches invalidate only their retained node', () { - final backend = LUIFlutterBackend()..applyJson(_initialBatch); - - backend.applyJson( - '{"generation":2,"ops":[' - '{"op":"set-prop","id":2,"property":"text","value":"Updated"}]}', - ); - - expect(backend.debugRevision(1), 0); - expect(backend.debugRevision(2), 1); - expect(backend.debugRevision(3), 0); - }); - - testWidgets('maps all direct text-entry kinds to native TextFields', ( - tester, - ) async { - final events = []; - final backend = LUIFlutterBackend(onEvent: events.add) - ..applyJson(''' - {"generation":1,"ops":[ - {"op":"create-node","id":1,"kind":"column"}, - {"op":"create-node","id":2,"kind":"text-field"}, - {"op":"create-node","id":3,"kind":"input"}, - {"op":"create-node","id":4,"kind":"search-field"}, - {"op":"create-node","id":5,"kind":"textarea"}, - {"op":"set-prop","id":2,"property":"text","value":"Draft"}, - {"op":"set-prop","id":2,"property":"placeholder","value":"Project"}, - {"op":"set-prop","id":2,"property":"autofocus","value":true}, - {"op":"set-prop","id":3,"property":"placeholder","value":"Email"}, - {"op":"set-prop","id":4,"property":"text","value":"Query"}, - {"op":"set-prop","id":4,"property":"placeholder","value":"Search"}, - {"op":"set-prop","id":5,"property":"text","value":"One line"}, - {"op":"set-prop","id":5,"property":"submit-on-enter","value":true}, - {"op":"insert-child","parent":1,"child":2,"index":0}, - {"op":"insert-child","parent":1,"child":3,"index":1}, - {"op":"insert-child","parent":1,"child":4,"index":2}, - {"op":"insert-child","parent":1,"child":5,"index":3} - ]} - '''); - - await tester.pumpWidget( - MaterialApp(home: Scaffold(body: backend.widget(node: 1))), - ); - - expect(find.byType(TextField), findsNWidgets(4)); - final textFieldFinder = find.descendant( - of: find.byKey(LUIFlutterBackend.nodeKey(2)), - matching: find.byType(TextField), - ); - final searchFinder = find.descendant( - of: find.byKey(LUIFlutterBackend.nodeKey(4)), - matching: find.byType(TextField), - ); - final textareaFinder = find.descendant( - of: find.byKey(LUIFlutterBackend.nodeKey(5)), - matching: find.byType(TextField), - ); - final textField = tester.widget(textFieldFinder); - final search = tester.widget(searchFinder); - final textarea = tester.widget(textareaFinder); - expect(textField.autofocus, isTrue); - expect(textField.decoration?.hintText, 'Project'); - expect(search.decoration?.prefixIcon, isA()); - expect(search.decoration?.suffixIcon, isA()); - expect(textarea.minLines, 1); - expect(textarea.maxLines, isNull); - expect(textarea.keyboardType, TextInputType.multiline); - - await tester.enterText(textFieldFinder, 'Updated'); - await tester.testTextInput.receiveAction(TextInputAction.done); - await tester.pump(); - expect( - events, - contains(const LUITextChangedEvent(node: 2, text: 'Updated')), - ); - expect(events, contains(const LUISubmitEvent(node: 2))); - - await tester.tap( - find.descendant( - of: find.byKey(LUIFlutterBackend.nodeKey(4)), - matching: find.byType(IconButton), - ), - ); - await tester.pump(); - expect(events, contains(const LUITextChangedEvent(node: 4, text: ''))); - }); - - testWidgets('maps picker primitives to native retained widgets and events', ( - tester, - ) async { - final semantics = tester.ensureSemantics(); - final events = []; - final backend = LUIFlutterBackend(onEvent: events.add) - ..applyJson(''' - {"generation":1,"ops":[ - {"op":"create-node","id":1,"kind":"column"}, - {"op":"create-node","id":2,"kind":"select"}, - {"op":"create-node","id":3,"kind":"combobox"}, - {"op":"create-node","id":4,"kind":"dropdown-menu"}, - {"op":"create-node","id":5,"kind":"menu-item"}, - {"op":"create-node","id":6,"kind":"stack"}, - {"op":"create-node","id":7,"kind":"menu-item"}, - {"op":"create-node","id":8,"kind":"dropdown-menu"}, - {"op":"create-node","id":9,"kind":"menu-item"}, - {"op":"set-prop","id":1,"property":"cross","value":"start"}, - {"op":"set-prop","id":2,"property":"text","value":"Production"}, - {"op":"set-prop","id":2,"property":"press-enabled","value":true}, - {"op":"set-prop","id":3,"property":"placeholder","value":"Search"}, - {"op":"set-prop","id":3,"property":"submit-enabled","value":true}, - {"op":"set-prop","id":4,"property":"anchor","value":"below"}, - {"op":"set-prop","id":4,"property":"anchor-alignment","value":"stretch"}, - {"op":"set-prop","id":4,"property":"anchor-offset","value":6.0}, - {"op":"set-prop","id":5,"property":"text","value":"Production"}, - {"op":"set-prop","id":5,"property":"selected","value":true}, - {"op":"set-prop","id":5,"property":"press-enabled","value":true}, - {"op":"set-prop","id":7,"property":"text","value":"More"}, - {"op":"set-prop","id":8,"property":"anchor","value":"right"}, - {"op":"set-prop","id":9,"property":"text","value":"Archive"}, - {"op":"set-prop","id":9,"property":"press-enabled","value":true}, - {"op":"insert-child","parent":1,"child":6,"index":0}, - {"op":"insert-child","parent":1,"child":3,"index":1}, - {"op":"insert-child","parent":6,"child":2,"index":0}, - {"op":"insert-child","parent":4,"child":5,"index":0}, - {"op":"insert-child","parent":4,"child":7,"index":1}, - {"op":"insert-child","parent":7,"child":8,"index":0}, - {"op":"insert-child","parent":8,"child":9,"index":0} - ]} - '''); - - await tester.pumpWidget( - MaterialApp(home: Scaffold(body: backend.widget(node: 1))), - ); - await tester.pump(); - - expect(find.widgetWithText(ListTile, 'Production'), findsOneWidget); - expect(find.byType(TextField), findsOneWidget); - final retainedSelect = tester.renderObject( - find.byKey(LUIFlutterBackend.nodeKey(2)), - ); - await tester.tap(find.widgetWithText(ListTile, 'Production')); - backend.applyJson(''' - {"generation":2,"ops":[ - {"op":"insert-child","parent":6,"child":4,"index":1} - ]} - '''); - await tester.pumpAndSettle(); - - final rootMenuFinder = find.byWidgetPredicate( - (widget) => widget is MenuAnchor && widget.menuChildren.length == 2, - ); - expect(rootMenuFinder, findsOneWidget); - expect(find.byType(SubmenuButton), findsOneWidget); - final nativeMenu = tester.widget(rootMenuFinder); - expect(nativeMenu.useRootOverlay, isTrue); - expect(nativeMenu.animated, isTrue); - expect(find.byType(MenuItemButton), findsOneWidget); - expect(find.byIcon(Icons.check), findsOneWidget); - final selectedMenuLabel = find.descendant( - of: find.byType(MenuItemButton), - matching: find.text('Production'), - ); - expect( - tester.getSemantics(selectedMenuLabel).flagsCollection.isSelected, - ui.Tristate.isTrue, - ); - expect( - tester.renderObject(find.byKey(LUIFlutterBackend.nodeKey(2))), - same(retainedSelect), - ); - - await tester.tap(find.byType(SubmenuButton)); - await tester.pumpAndSettle(); - expect(find.text('Archive'), findsOneWidget); - await tester.tap(find.text('Archive')); - await tester.pumpAndSettle(); - backend.applyJson(''' - {"generation":3,"ops":[ - {"op":"remove-child","parent":6,"child":4}, - {"op":"remove-child","parent":4,"child":5}, - {"op":"remove-child","parent":4,"child":7}, - {"op":"remove-child","parent":7,"child":8}, - {"op":"remove-child","parent":8,"child":9}, - {"op":"drop-node","id":5}, - {"op":"drop-node","id":9}, - {"op":"drop-node","id":8}, - {"op":"drop-node","id":7}, - {"op":"drop-node","id":4} - ]} - '''); - await tester.pump(); - await tester.enterText(find.byType(TextField), 'sol'); - await tester.testTextInput.receiveAction(TextInputAction.done); - await tester.pump(); - - expect(events, [ - const LUIPressEvent(node: 2), - const LUIPressEvent(node: 9), - const LUIDismissEvent(node: 4), - const LUITextChangedEvent(node: 3, text: 'sol'), - const LUISubmitEvent(node: 3), - ]); - semantics.dispose(); - }); - - testWidgets( - 'maps Tooltip to native hover intent while retaining its trigger', - (tester) async { - final events = []; - final backend = LUIFlutterBackend(onEvent: events.add) - ..applyJson(''' - {"generation":1,"ops":[ - {"op":"create-node","id":1,"kind":"column"}, - {"op":"create-node","id":2,"kind":"stack"}, - {"op":"create-node","id":3,"kind":"button"}, - {"op":"create-node","id":4,"kind":"tooltip"}, - {"op":"create-node","id":5,"kind":"tooltip"}, - {"op":"set-prop","id":3,"property":"text","value":"Bold"}, - {"op":"set-prop","id":4,"property":"text","value":"Bold the selection"}, - {"op":"set-prop","id":4,"property":"anchor","value":"above"}, - {"op":"set-prop","id":4,"property":"anchor-alignment","value":"end"}, - {"op":"set-prop","id":4,"property":"anchor-offset","value":8.0}, - {"op":"set-prop","id":4,"property":"tooltip-delay","value":250}, - {"op":"set-prop","id":5,"property":"text","value":"Copied!"}, - {"op":"insert-child","parent":1,"child":2,"index":0}, - {"op":"insert-child","parent":2,"child":3,"index":0}, - {"op":"insert-child","parent":2,"child":4,"index":1}, - {"op":"insert-child","parent":1,"child":5,"index":1} - ]} - '''); - - await tester.pumpWidget( - MaterialApp(home: Scaffold(body: backend.widget(node: 1))), - ); - - final nativeTooltip = tester.widget(find.byType(Tooltip)); - expect(nativeTooltip.message, 'Bold the selection'); - expect(nativeTooltip.waitDuration, const Duration(milliseconds: 250)); - expect(nativeTooltip.preferBelow, isFalse); - expect(find.text('Copied!'), findsOneWidget); - final buttonRenderObject = tester.renderObject( - find.byKey(LUIFlutterBackend.nodeKey(3)), - ); - - final mouse = await tester.createGesture( - kind: ui.PointerDeviceKind.mouse, - ); - await mouse.addPointer(location: const Offset(1, 1)); - await mouse.moveTo(tester.getCenter(find.text('Bold'))); - await tester.pump(const Duration(milliseconds: 249)); - expect(find.text('Bold the selection'), findsNothing); - await tester.pump(const Duration(milliseconds: 1)); - await tester.pump(); - expect(find.text('Bold the selection'), findsOneWidget); - - await tester.tap(find.text('Bold')); - await tester.pump(); - expect(events, const [LUIEvent.press(node: 3)]); - expect(find.text('Bold the selection'), findsNothing); - await mouse.removePointer(); - - final touch = await tester.startGesture( - tester.getCenter(find.text('Bold')), - kind: ui.PointerDeviceKind.touch, - ); - await tester.pump(kLongPressTimeout + const Duration(milliseconds: 100)); - expect(find.text('Bold the selection'), findsOneWidget); - await tester.pump(const Duration(seconds: 3)); - expect(find.text('Bold the selection'), findsOneWidget); - await touch.up(); - await tester.pump(); - expect(find.text('Bold the selection'), findsOneWidget); - await tester.pump(const Duration(seconds: 3)); - expect(find.text('Bold the selection'), findsOneWidget); - - backend.applyJson(''' - {"generation":2,"ops":[ - {"op":"set-prop","id":4,"property":"text","value":"Toggle bold"}, - {"op":"set-prop","id":4,"property":"tooltip-delay","value":0} - ]} - '''); - await tester.pump(); - expect( - tester.renderObject(find.byKey(LUIFlutterBackend.nodeKey(3))), - same(buttonRenderObject), - ); - expect( - tester.widget(find.byType(Tooltip)).message, - 'Toggle bold', - ); - expect( - tester.widget(find.byType(Tooltip)).waitDuration, - Duration.zero, - ); - expect( - () => backend.applyJson(''' - {"generation":3,"ops":[ - {"op":"set-prop","id":5,"property":"tooltip-delay","value":20} - ]} - '''), - throwsA(isA()), - ); - expect(backend.generation, 2); - }, - ); - - testWidgets('maps Toast and Toolbar to retained native widgets', ( - tester, - ) async { - final semantics = tester.ensureSemantics(); - final events = []; - final backend = LUIFlutterBackend(onEvent: events.add) - ..applyJson(''' - {"generation":1,"ops":[ - {"op":"create-node","id":1,"kind":"column"}, - {"op":"create-node","id":2,"kind":"toolbar"}, - {"op":"create-node","id":3,"kind":"button"}, - {"op":"create-node","id":4,"kind":"toast"}, - {"op":"create-node","id":5,"kind":"text"}, - {"op":"set-prop","id":2,"property":"orientation","value":"horizontal"}, - {"op":"set-prop","id":2,"property":"accessibility-label","value":"Formatting"}, - {"op":"set-prop","id":2,"property":"gap","value":4}, - {"op":"set-prop","id":3,"property":"text","value":"Bold"}, - {"op":"set-prop","id":4,"property":"duration","value":100}, - {"op":"set-prop","id":4,"property":"accessibility-label","value":"Saved"}, - {"op":"set-prop","id":5,"property":"text","value":"Draft saved"}, - {"op":"insert-child","parent":1,"child":2,"index":0}, - {"op":"insert-child","parent":2,"child":3,"index":0}, - {"op":"insert-child","parent":1,"child":4,"index":1}, - {"op":"insert-child","parent":4,"child":5,"index":0} - ]} - '''); - - await tester.pumpWidget( - MaterialApp(home: Scaffold(body: backend.widget(node: 1))), - ); - expect( - find.byWidgetPredicate( - (widget) => - widget is Semantics && widget.properties.label == 'Formatting', - ), - findsOneWidget, - ); - expect( - find.byWidgetPredicate( - (widget) => widget is Semantics && widget.properties.label == 'Saved', - ), - findsOneWidget, - ); - expect(find.byType(Dismissible), findsOneWidget); - expect(find.text('Draft saved'), findsOneWidget); - final toolbarRenderObject = tester.renderObject( - find.byKey(LUIFlutterBackend.nodeKey(2)), - ); - final toastRenderObject = tester.renderObject( - find.byKey(LUIFlutterBackend.nodeKey(4)), - ); - - backend.applyJson(''' - {"generation":2,"ops":[ - {"op":"set-prop","id":5,"property":"text","value":"Draft updated"} - ]} - '''); - await tester.pump(); - expect( - tester.renderObject(find.byKey(LUIFlutterBackend.nodeKey(2))), - same(toolbarRenderObject), - ); - expect( - tester.renderObject(find.byKey(LUIFlutterBackend.nodeKey(4))), - same(toastRenderObject), - ); - expect(find.text('Draft updated'), findsOneWidget); - - await tester.pump(const Duration(milliseconds: 100)); - expect(events, const [LUIEvent.dismiss(node: 4)]); - semantics.dispose(); - }); - - testWidgets('maps Stepper and Timeline to stable native compositions', ( - tester, - ) async { - final events = []; - final backend = LUIFlutterBackend(onEvent: events.add) - ..applyJson(''' - {"generation":1,"ops":[ - {"op":"create-node","id":1,"kind":"column"}, - {"op":"create-node","id":2,"kind":"stepper"}, - {"op":"set-prop","id":2,"property":"active","value":1}, - {"op":"set-prop","id":2,"property":"accessibility-label","value":"Release progress"}, - {"op":"create-node","id":3,"kind":"step"}, - {"op":"set-prop","id":3,"property":"text","value":"Draft"}, - {"op":"create-node","id":4,"kind":"step"}, - {"op":"set-prop","id":4,"property":"text","value":"Review"}, - {"op":"insert-child","parent":2,"child":3,"index":0}, - {"op":"insert-child","parent":2,"child":4,"index":1}, - {"op":"create-node","id":5,"kind":"timeline"}, - {"op":"set-prop","id":5,"property":"gap","value":6}, - {"op":"set-prop","id":5,"property":"accessibility-label","value":"Release activity"}, - {"op":"create-node","id":6,"kind":"timeline-item"}, - {"op":"set-prop","id":6,"property":"title","value":"Validated"}, - {"op":"set-prop","id":6,"property":"description","value":"Checks completed"}, - {"op":"set-prop","id":6,"property":"meta","value":"CI · 2m"}, - {"op":"set-prop","id":6,"property":"indicator","value":"2"}, - {"op":"set-prop","id":6,"property":"variant","value":"primary"}, - {"op":"set-prop","id":6,"property":"connector","value":true}, - {"op":"set-prop","id":6,"property":"selected","value":false}, - {"op":"set-prop","id":6,"property":"press-enabled","value":true}, - {"op":"insert-child","parent":5,"child":6,"index":0}, - {"op":"insert-child","parent":1,"child":2,"index":0}, - {"op":"insert-child","parent":1,"child":5,"index":1} - ]} - '''); - - await tester.pumpWidget( - MaterialApp(home: Scaffold(body: backend.widget(node: 1))), - ); - expect(find.text('Draft'), findsOneWidget); - expect(find.text('Review'), findsOneWidget); - expect(find.text('Validated'), findsOneWidget); - expect(find.text('Checks completed'), findsOneWidget); - expect(find.text('CI · 2m'), findsOneWidget); - expect( - find.descendant( - of: find.byKey(LUIFlutterBackend.nodeKey(6)), - matching: find.text('2'), - ), - findsOneWidget, - ); - expect(find.byIcon(Icons.check), findsOneWidget); - - final retainedStepper = tester.renderObject( - find.byKey(LUIFlutterBackend.nodeKey(2)), - ); - final retainedItem = tester.renderObject( - find.byKey(LUIFlutterBackend.nodeKey(6)), - ); - await tester.tap(find.text('Validated')); - expect(events, [const LUIPressEvent(node: 6)]); - - backend.applyJson(''' - {"generation":2,"ops":[ - {"op":"set-prop","id":2,"property":"active","value":2}, - {"op":"set-prop","id":4,"property":"text","value":"Approve"}, - {"op":"set-prop","id":6,"property":"title","value":"Deployed"}, - {"op":"set-prop","id":6,"property":"connector","value":false}, - {"op":"set-prop","id":6,"property":"selected","value":true} - ]} - '''); - await tester.pump(); - expect( - tester.renderObject(find.byKey(LUIFlutterBackend.nodeKey(2))), - same(retainedStepper), - ); - expect( - tester.renderObject(find.byKey(LUIFlutterBackend.nodeKey(6))), - same(retainedItem), - ); - expect(find.text('Approve'), findsOneWidget); - expect(find.text('Deployed'), findsOneWidget); - expect(find.byIcon(Icons.check), findsNWidgets(2)); - }); - - test('rejects incomplete Stepper and TimelineItem batches atomically', () { - final backend = LUIFlutterBackend(); - expect( - () => backend.applyJson(''' - {"generation":1,"ops":[ - {"op":"create-node","id":1,"kind":"stepper"} - ]} - '''), - throwsA(isA()), - ); - expect(backend.generation, 0); - expect(backend.containsNode(1), isFalse); - - expect( - () => backend.applyJson(''' - {"generation":1,"ops":[ - {"op":"create-node","id":1,"kind":"timeline-item"} - ]} - '''), - throwsA(isA()), - ); - expect(backend.generation, 0); - expect(backend.containsNode(1), isFalse); - }); - - testWidgets('maps InputGroup to one stable native composer field', ( - tester, - ) async { - final events = []; - final backend = LUIFlutterBackend(onEvent: events.add) - ..applyJson(''' - {"generation":1,"ops":[ - {"op":"create-node","id":1,"kind":"column"}, - {"op":"create-node","id":2,"kind":"input-group"}, - {"op":"set-prop","id":2,"property":"accessibility-label","value":"Message composer"}, - {"op":"set-prop","id":2,"property":"width","value":320}, - {"op":"set-prop","id":2,"property":"height","value":120}, - {"op":"set-prop","id":2,"property":"min-width","value":240}, - {"op":"set-prop","id":2,"property":"grow","value":1.0}, - {"op":"create-node","id":3,"kind":"textarea"}, - {"op":"set-prop","id":3,"property":"text","value":"Draft"}, - {"op":"set-prop","id":3,"property":"placeholder","value":"Message the team"}, - {"op":"create-node","id":4,"kind":"input-group-actions"}, - {"op":"set-prop","id":4,"property":"gap","value":8}, - {"op":"create-node","id":5,"kind":"button"}, - {"op":"set-prop","id":5,"property":"text","value":"Attach"}, - {"op":"create-node","id":6,"kind":"spacer"}, - {"op":"set-prop","id":6,"property":"grow","value":1.0}, - {"op":"create-node","id":7,"kind":"button"}, - {"op":"set-prop","id":7,"property":"text","value":"Send"}, - {"op":"insert-child","parent":4,"child":5,"index":0}, - {"op":"insert-child","parent":4,"child":6,"index":1}, - {"op":"insert-child","parent":4,"child":7,"index":2}, - {"op":"insert-child","parent":2,"child":3,"index":0}, - {"op":"insert-child","parent":2,"child":4,"index":1}, - {"op":"insert-child","parent":1,"child":2,"index":0} - ]} - '''); - - await tester.pumpWidget( - MaterialApp(home: Scaffold(body: backend.widget(node: 1))), - ); - final groupFinder = find.byKey(LUIFlutterBackend.nodeKey(2)); - final actionsFinder = find.byKey(LUIFlutterBackend.nodeKey(4)); - final textareaFinder = find.descendant( - of: groupFinder, - matching: find.byType(TextField), - ); - final semanticsFinder = find.descendant( - of: groupFinder, - matching: find.byWidgetPredicate( - (widget) => - widget is Semantics && - widget.properties.label == 'Message composer', - ), - ); - expect(textareaFinder, findsOneWidget); - expect(actionsFinder, findsOneWidget); - expect(find.text('Attach'), findsOneWidget); - expect(find.text('Send'), findsOneWidget); - expect(semanticsFinder, findsOneWidget); - expect( - tester.widget(textareaFinder).decoration?.border, - InputBorder.none, - ); - - final retainedGroup = tester.renderObject(groupFinder); - final retainedActions = tester.renderObject(actionsFinder); - final retainedEditor = tester.widget( - find.descendant(of: textareaFinder, matching: find.byType(EditableText)), - ); - await tester.tap(find.text('Attach')); - expect(events, [const LUIPressEvent(node: 5)]); - - backend.applyJson(''' - {"generation":2,"ops":[ - {"op":"set-prop","id":3,"property":"text","value":"Updated"} - ]} - '''); - await tester.pump(); - expect(tester.renderObject(groupFinder), same(retainedGroup)); - expect(tester.renderObject(actionsFinder), same(retainedActions)); - expect(find.text('Updated'), findsOneWidget); - expect( - tester - .widget( - find.descendant( - of: textareaFinder, - matching: find.byType(EditableText), - ), - ) - .focusNode, - same(retainedEditor.focusNode), - ); - }); - - test('rejects malformed InputGroup structure atomically', () { - final backend = LUIFlutterBackend(); - expect( - () => backend.applyJson(''' - {"generation":1,"ops":[ - {"op":"create-node","id":1,"kind":"input-group"}, - {"op":"create-node","id":2,"kind":"input-group-actions"}, - {"op":"insert-child","parent":1,"child":2,"index":0} - ]} - '''), - throwsA(isA()), - ); - expect(backend.generation, 0); - expect(backend.containsNode(1), isFalse); - - expect( - () => backend.applyJson(''' - {"generation":1,"ops":[ - {"op":"create-node","id":1,"kind":"input-group-actions"} - ]} - '''), - throwsA(isA()), - ); - expect(backend.generation, 0); - expect(backend.containsNode(1), isFalse); - }); - - testWidgets('maps Accordion to a controlled retained native disclosure', ( - tester, - ) async { - final events = []; - final backend = LUIFlutterBackend(onEvent: events.add) - ..applyJson(''' - {"generation":1,"ops":[ - {"op":"create-node","id":1,"kind":"accordion"}, - {"op":"create-node","id":2,"kind":"text"}, - {"op":"set-prop","id":1,"property":"text","value":"Details"}, - {"op":"set-prop","id":1,"property":"selected","value":false}, - {"op":"set-prop","id":1,"property":"toggle-enabled","value":true}, - {"op":"set-prop","id":1,"property":"height","value":180}, - {"op":"set-prop","id":2,"property":"text","value":"Retained content"}, - {"op":"insert-child","parent":1,"child":2,"index":0} - ]} - '''); - - await tester.pumpWidget( - MaterialApp(home: Scaffold(body: backend.widget(node: 1))), - ); - final tile = tester.widget(find.byType(ExpansionTile)); - expect(tile.title, isA()); - expect((tile.title as Text).data, 'Details'); - expect(tile.initiallyExpanded, isFalse); - expect(tile.maintainState, isTrue); - final retainedContent = tester.element( - find.text('Retained content', skipOffstage: false), - ); - - await tester.tap(find.text('Details')); - await tester.pumpAndSettle(); - expect(events, const [LUIEvent.toggleChanged(node: 1, checked: true)]); - expect(find.text('Retained content'), findsNothing); - - backend.applyJson(''' - {"generation":2,"ops":[ - {"op":"set-prop","id":1,"property":"text","value":"Advanced details"}, - {"op":"set-prop","id":1,"property":"selected","value":true} - ]} - '''); - await tester.pumpAndSettle(); - expect(find.text('Advanced details'), findsOneWidget); - expect(find.text('Retained content'), findsOneWidget); - expect( - tester.element(find.text('Retained content', skipOffstage: false)), - same(retainedContent), - ); - - expect( - () => backend.applyJson(''' - {"generation":3,"ops":[ - {"op":"set-prop","id":1,"property":"padding","value":8} - ]} - '''), - throwsA(isA()), - ); - expect(backend.generation, 2); - }); - - testWidgets( - 'maps ListItem text and custom children to retained native rows', - (tester) async { - final events = []; - final backend = LUIFlutterBackend(onEvent: events.add) - ..applyJson(''' - {"generation":1,"ops":[ - {"op":"create-node","id":1,"kind":"list"}, - {"op":"create-node","id":2,"kind":"list-item"}, - {"op":"create-node","id":3,"kind":"list-item"}, - {"op":"create-node","id":4,"kind":"row"}, - {"op":"create-node","id":5,"kind":"text"}, - {"op":"set-prop","id":2,"property":"text","value":"Quarterly report.md"}, - {"op":"set-prop","id":2,"property":"icon","value":"file-text"}, - {"op":"set-prop","id":2,"property":"selected","value":true}, - {"op":"set-prop","id":2,"property":"press-enabled","value":true}, - {"op":"set-prop","id":2,"property":"long-press-enabled","value":true}, - {"op":"set-prop","id":2,"property":"double-press-enabled","value":true}, - {"op":"set-prop","id":2,"property":"submit-enabled","value":true}, - {"op":"set-prop","id":5,"property":"text","value":"Custom child row"}, - {"op":"insert-child","parent":1,"child":2,"index":0}, - {"op":"insert-child","parent":1,"child":3,"index":1}, - {"op":"insert-child","parent":3,"child":4,"index":0}, - {"op":"insert-child","parent":4,"child":5,"index":0} - ]} - '''); - - await tester.pumpWidget( - MaterialApp(home: Scaffold(body: backend.widget(node: 1))), - ); - - expect(find.byType(ListTile), findsNWidgets(2)); - expect(find.text('Quarterly report.md'), findsOneWidget); - expect(find.text('Custom child row'), findsOneWidget); - expect(find.byIcon(Icons.description_outlined), findsOneWidget); - final retained = tester.renderObject(find.text('Quarterly report.md')); - - await tester.longPress(find.text('Quarterly report.md')); - await tester.pump(); - expect(events, [const LUIEvent.longPress(node: 2)]); - events.clear(); - - await tester.tap(find.text('Quarterly report.md')); - await tester.pump(const Duration(milliseconds: 400)); - await tester.tap(find.text('Quarterly report.md')); - await tester.pump(const Duration(milliseconds: 40)); - await tester.tap(find.text('Quarterly report.md')); - await tester.pump(); - expect( - events.map((event) => event.runtimeType.toString()), - containsAllInOrder([ - 'LUIPressEvent', - 'LUIPressEvent', - 'LUIPressEvent', - 'LUIDoublePressEvent', - ]), - ); - - backend.applyJson(''' - {"generation":2,"ops":[ - {"op":"set-prop","id":2,"property":"selected","value":false} - ]} - '''); - await tester.pump(); - expect( - tester.renderObject(find.text('Quarterly report.md')), - same(retained), - ); - }, - ); - - testWidgets( - 'maps Table rows to retained native Table widgets and patches locally', - (tester) async { - final semantics = tester.ensureSemantics(); - final events = []; - final backend = LUIFlutterBackend(onEvent: events.add) - ..applyJson(''' - {"generation":1,"ops":[ - {"op":"create-node","id":1,"kind":"table"}, - {"op":"create-node","id":2,"kind":"table-row"}, - {"op":"create-node","id":3,"kind":"table-row"}, - {"op":"create-node","id":4,"kind":"table-cell"}, - {"op":"create-node","id":5,"kind":"table-cell"}, - {"op":"create-node","id":6,"kind":"table-cell"}, - {"op":"create-node","id":7,"kind":"table-cell"}, - {"op":"set-prop","id":2,"property":"gap","value":4}, - {"op":"set-prop","id":3,"property":"selected","value":false}, - {"op":"set-prop","id":4,"property":"text","value":"Invoice"}, - {"op":"set-prop","id":4,"property":"size","value":"sm"}, - {"op":"set-prop","id":5,"property":"text","value":"Amount"}, - {"op":"set-prop","id":5,"property":"text-alignment","value":"end"}, - {"op":"set-prop","id":6,"property":"text","value":"INV-002"}, - {"op":"set-prop","id":6,"property":"press-enabled","value":true}, - {"op":"set-prop","id":7,"property":"text","value":"\$150.00"}, - {"op":"set-prop","id":7,"property":"text-alignment","value":"end"}, - {"op":"insert-child","parent":1,"child":2,"index":0}, - {"op":"insert-child","parent":1,"child":3,"index":1}, - {"op":"insert-child","parent":2,"child":4,"index":0}, - {"op":"insert-child","parent":2,"child":5,"index":1}, - {"op":"insert-child","parent":3,"child":6,"index":0}, - {"op":"insert-child","parent":3,"child":7,"index":1} - ]} - '''); - - await tester.pumpWidget( - MaterialApp(home: Scaffold(body: backend.widget(node: 1))), - ); - - expect(find.byType(Table), findsNWidgets(2)); - expect(find.text('Invoice'), findsOneWidget); - expect(find.text('INV-002'), findsOneWidget); - expect(find.text(r'$150.00'), findsOneWidget); - final retainedAmount = tester.element(find.text(r'$150.00')); - final rowSemantics = tester.widget( - find - .descendant( - of: find.byKey(LUIFlutterBackend.nodeKey(3)), - matching: find.byType(Semantics), - ) - .first, - ); - expect(rowSemantics.properties.selected, isFalse); - - await tester.tap(find.text('INV-002')); - await tester.pump(); - expect(events, const [LUIEvent.press(node: 6)]); - - final tableRevision = backend.debugRevision(1); - final rowRevision = backend.debugRevision(3); - final cellRevision = backend.debugRevision(7); - backend.applyJson(''' - {"generation":2,"ops":[ - {"op":"set-prop","id":3,"property":"selected","value":true}, - {"op":"set-prop","id":7,"property":"text","value":"\$175.00"} - ]} - '''); - await tester.pump(); - - expect(backend.debugRevision(1), tableRevision); - expect(backend.debugRevision(3), rowRevision + 1); - expect(backend.debugRevision(7), cellRevision + 1); - expect(tester.element(find.text(r'$175.00')), same(retainedAmount)); - expect( - tester - .widget( - find - .descendant( - of: find.byKey(LUIFlutterBackend.nodeKey(3)), - matching: find.byType(Semantics), - ) - .first, - ) - .properties - .selected, - isTrue, - ); - semantics.dispose(); - }, - ); - - test('rejects malformed Table nesting without a partial commit', () { - final backend = LUIFlutterBackend(); - expect( - () => backend.applyJson(''' - {"generation":1,"ops":[ - {"op":"create-node","id":1,"kind":"table"}, - {"op":"create-node","id":2,"kind":"text"}, - {"op":"set-prop","id":2,"property":"text","value":"Invalid"}, - {"op":"insert-child","parent":1,"child":2,"index":0} - ]} - '''), - throwsA( - isA().having( - (error) => error.message, - 'message', - contains('table can contain only table-row'), - ), - ), - ); - expect(backend.generation, 0); - expect(backend.containsNode(1), isFalse); - }); - - testWidgets('Resizable keeps drag width in retained native state', ( - tester, - ) async { - final semantics = tester.ensureSemantics(); - final backend = LUIFlutterBackend() - ..applyJson(''' - {"generation":1,"ops":[ - {"op":"create-node","id":1,"kind":"resizable"}, - {"op":"create-node","id":2,"kind":"text"}, - {"op":"set-prop","id":1,"property":"width","value":240}, - {"op":"set-prop","id":1,"property":"min-width","value":180}, - {"op":"set-prop","id":1,"property":"max-width","value":480}, - {"op":"set-prop","id":1,"property":"accessibility-label","value":"Resizable sidebar"}, - {"op":"set-prop","id":2,"property":"text","value":"Content"}, - {"op":"insert-child","parent":1,"child":2,"index":0} - ]} - '''); - - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: Align( - alignment: Alignment.topLeft, - child: backend.widget(node: 1), - ), - ), - ), - ); - - final node = find.byKey(LUIFlutterBackend.nodeKey(1)); - final retainedContent = tester.element(find.text('Content')); - expect(tester.getSize(node).width, 240); - final semanticsNode = tester.getSemantics( - find.bySemanticsLabel('Resizable sidebar'), - ); - final semanticsData = semanticsNode.getSemanticsData(); - expect(semanticsData.hasAction(ui.SemanticsAction.increase), isTrue); - expect(semanticsData.hasAction(ui.SemanticsAction.decrease), isTrue); - - final center = tester.getCenter(node); - final right = tester.getTopRight(node).dx; - await tester.dragFrom(Offset(right - 2, center.dy), const Offset(60, 0)); - await tester.pump(); - expect(tester.getSize(node).width, 300); - expect(backend.debugRevision(1), 0); - expect(tester.element(find.text('Content')), same(retainedContent)); - - backend.applyJson(''' - {"generation":2,"ops":[ - {"op":"set-prop","id":1,"property":"background","value":"secondary"} - ]} - '''); - await tester.pump(); - expect(tester.getSize(node).width, 300); - expect(tester.element(find.text('Content')), same(retainedContent)); - - backend.applyJson(''' - {"generation":3,"ops":[ - {"op":"set-prop","id":1,"property":"width","value":260} - ]} - '''); - await tester.pump(); - expect(tester.getSize(node).width, 260); - await tester.dragFrom( - Offset(tester.getTopRight(node).dx - 2, tester.getCenter(node).dy), - const Offset(-200, 0), - ); - await tester.pump(); - expect(tester.getSize(node).width, 180); - semantics.dispose(); - }); - - test('rejects flow properties on Resizable atomically', () { - final backend = LUIFlutterBackend(); - expect( - () => backend.applyJson(''' - {"generation":1,"ops":[ - {"op":"create-node","id":1,"kind":"resizable"}, - {"op":"set-prop","id":1,"property":"gap","value":8} - ]} - '''), - throwsA(isA()), - ); - expect(backend.generation, 0); - expect(backend.containsNode(1), isFalse); - }); - - testWidgets('Split drags one controlled divider without replacing panes', ( - tester, - ) async { - final semantics = tester.ensureSemantics(); - final events = []; - final backend = LUIFlutterBackend(onEvent: events.add) - ..applyJson(''' - {"generation":1,"ops":[ - {"op":"create-node","id":1,"kind":"split"}, - {"op":"create-node","id":2,"kind":"panel"}, - {"op":"create-node","id":3,"kind":"panel"}, - {"op":"set-prop","id":1,"property":"value","value":0.0}, - {"op":"set-prop","id":1,"property":"gap","value":8}, - {"op":"set-prop","id":1,"property":"width","value":800}, - {"op":"set-prop","id":1,"property":"height","value":200}, - {"op":"set-prop","id":1,"property":"accessibility-label","value":"Workspace panes"}, - {"op":"set-prop","id":2,"property":"min-width","value":180}, - {"op":"set-prop","id":3,"property":"min-width","value":320}, - {"op":"insert-child","parent":1,"child":2,"index":0}, - {"op":"insert-child","parent":1,"child":3,"index":1} - ]} - '''); - - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: Align( - alignment: Alignment.topLeft, - child: backend.widget(node: 1), - ), - ), - ), - ); - - // The constraint probe rebuilds the split after the first layout - // reports the real available width. - await tester.pump(); - final first = find.byKey(LUIFlutterBackend.nodeKey(2)); - final second = find.byKey(LUIFlutterBackend.nodeKey(3)); - final firstElement = tester.element(first); - final secondElement = tester.element(second); - expect(tester.getSize(first).width, 396); - expect(tester.getSize(second).width, 396); - final divider = find.bySemanticsLabel('Workspace panes divider'); - final semanticsData = tester.getSemantics(divider).getSemanticsData(); - expect(semanticsData.hasAction(ui.SemanticsAction.increase), isTrue); - expect(semanticsData.hasAction(ui.SemanticsAction.decrease), isTrue); - - await tester.drag(divider, const Offset(100, 0)); - await tester.pump(); - expect(tester.getSize(first).width, closeTo(472, 0.001)); - expect(tester.getSize(second).width, closeTo(320, 0.001)); - final resizeEvent = events.last as LUIValueChangedEvent; - expect(resizeEvent.node, 1); - expect(resizeEvent.value, closeTo(472 / 792, 0.0001)); - - backend.applyJson(''' - {"generation":2,"ops":[ - {"op":"set-prop","id":1,"property":"value","value":${472 / 792}} - ]} - '''); - await tester.pump(); - expect(tester.getSize(first).width, closeTo(472, 0.001)); - expect(tester.element(first), same(firstElement)); - expect(tester.element(second), same(secondElement)); - - backend.applyJson(''' - {"generation":3,"ops":[ - {"op":"set-prop","id":1,"property":"value","value":0.3} - ]} - '''); - await tester.pump(); - expect(tester.getSize(first).width, closeTo(237.6, 0.01)); - expect(tester.getSize(second).width, closeTo(554.4, 0.01)); - semantics.dispose(); - }); - - testWidgets('Split animates a changed model target and echoes presentation', ( - tester, - ) async { - final events = []; - final backend = LUIFlutterBackend(onEvent: events.add) - ..applyJson(''' - {"generation":1,"ops":[ - {"op":"create-node","id":1,"kind":"split"}, - {"op":"create-node","id":2,"kind":"panel"}, - {"op":"create-node","id":3,"kind":"panel"}, - {"op":"set-prop","id":1,"property":"value","value":0.3}, - {"op":"set-prop","id":1,"property":"gap","value":8}, - {"op":"set-prop","id":1,"property":"width","value":800}, - {"op":"set-prop","id":1,"property":"height","value":200}, - {"op":"set-prop","id":1,"property":"resize-duration","value":200}, - {"op":"set-prop","id":1,"property":"resize-easing","value":"linear"}, - {"op":"insert-child","parent":1,"child":2,"index":0}, - {"op":"insert-child","parent":1,"child":3,"index":1} - ]} - '''); - await tester.pumpWidget( - MaterialApp(home: Scaffold(body: backend.widget(node: 1))), - ); - // The constraint probe rebuilds the split after the first layout - // reports the real available width. - await tester.pump(); - final first = find.byKey(LUIFlutterBackend.nodeKey(2)); - expect(tester.getSize(first).width, closeTo(237.6, 0.01)); - - backend.applyJson(''' - {"generation":2,"ops":[ - {"op":"set-prop","id":1,"property":"value","value":0.7} - ]} - '''); - await tester.pump(); - await tester.pump(const Duration(milliseconds: 100)); - expect(tester.getSize(first).width, closeTo(396, 2)); - expect(events, isNotEmpty); - expect(events.last, isA()); - await tester.pump(const Duration(milliseconds: 100)); - expect(tester.getSize(first).width, closeTo(554.4, 0.01)); - }); - - test('rejects malformed Split structure and animation dependencies', () { - for (final operations in [ - ''' - {"op":"create-node","id":1,"kind":"split"}, - {"op":"create-node","id":2,"kind":"panel"}, - {"op":"insert-child","parent":1,"child":2,"index":0} - ''', - ''' - {"op":"create-node","id":1,"kind":"split"}, - {"op":"create-node","id":2,"kind":"panel"}, - {"op":"create-node","id":3,"kind":"panel"}, - {"op":"create-node","id":4,"kind":"panel"}, - {"op":"insert-child","parent":1,"child":2,"index":0}, - {"op":"insert-child","parent":1,"child":3,"index":1}, - {"op":"insert-child","parent":1,"child":4,"index":2} - ''', - ''' - {"op":"create-node","id":1,"kind":"split"}, - {"op":"create-node","id":2,"kind":"panel"}, - {"op":"create-node","id":3,"kind":"panel"}, - {"op":"set-prop","id":1,"property":"resize-origin","value":0.1}, - {"op":"insert-child","parent":1,"child":2,"index":0}, - {"op":"insert-child","parent":1,"child":3,"index":1} - ''', - ]) { - final backend = LUIFlutterBackend(); - expect( - () => backend.applyJson('{"generation":1,"ops":[$operations]}'), - throwsA(isA()), - ); - expect(backend.generation, 0); - expect(backend.containsNode(1), isFalse); - } - }); - - testWidgets('Drawer keeps two panes retained and emits controlled toggles', ( - tester, - ) async { - final events = []; - final backend = LUIFlutterBackend(onEvent: events.add) - ..applyJson(''' - {"generation":1,"ops":[ - {"op":"create-node","id":1,"kind":"drawer"}, - {"op":"create-node","id":2,"kind":"panel"}, - {"op":"create-node","id":3,"kind":"panel"}, - {"op":"set-prop","id":1,"property":"selected","value":false}, - {"op":"set-prop","id":1,"property":"enabled","value":false}, - {"op":"set-prop","id":1,"property":"toggle-enabled","value":true}, - {"op":"set-prop","id":1,"property":"width","value":320}, - {"op":"insert-child","parent":1,"child":2,"index":0}, - {"op":"insert-child","parent":1,"child":3,"index":1} - ]} - '''); - - await tester.pumpWidget( - MaterialApp(home: Scaffold(body: backend.widget(node: 1))), - ); - final main = find.byKey(LUIFlutterBackend.nodeKey(2)); - final panel = find.byKey(LUIFlutterBackend.nodeKey(3)); - final mainElement = tester.element(main); - final panelElement = tester.element(panel); - - await tester.dragFrom(const Offset(12, 100), const Offset(200, 0)); - await tester.pump(); - expect(events, isEmpty); - - backend.applyJson(''' - {"generation":2,"ops":[ - {"op":"set-prop","id":1,"property":"enabled","value":true} - ]} - '''); - await tester.pump(); - // Drags that start inside the content do not open the drawer; only a - // drag beginning in the edge strip does. - await tester.dragFrom(const Offset(300, 100), const Offset(200, 0)); - await tester.pump(); - expect(events, isEmpty); - await tester.dragFrom(const Offset(12, 100), const Offset(200, 0)); - await tester.pump(); - expect(events, const [LUIEvent.toggleChanged(node: 1, checked: true)]); - - backend.applyJson(''' - {"generation":3,"ops":[ - {"op":"set-prop","id":1,"property":"selected","value":true} - ]} - '''); - await tester.pump(); - expect(tester.element(main), same(mainElement)); - expect(tester.element(panel), same(panelElement)); - - await tester.tapAt(const Offset(500, 100)); - await tester.pump(); - expect(events, const [ - LUIEvent.toggleChanged(node: 1, checked: true), - LUIEvent.toggleChanged(node: 1, checked: false), - ]); - - backend.applyJson(''' - {"generation":4,"ops":[ - {"op":"set-prop","id":1,"property":"selected","value":false}, - {"op":"set-prop","id":1,"property":"width","value":0} - ]} - '''); - await tester.pump(); - await tester.dragFrom(const Offset(1, 100), const Offset(200, 0)); - expect(events, const [ - LUIEvent.toggleChanged(node: 1, checked: true), - LUIEvent.toggleChanged(node: 1, checked: false), - ]); - }); - - testWidgets('maps Tree rows to one retained native roving focus set', ( - tester, - ) async { - final semantics = tester.ensureSemantics(); - final events = []; - final backend = LUIFlutterBackend(onEvent: events.add) - ..applyJson(''' - {"generation":1,"ops":[ - {"op":"create-node","id":1,"kind":"tree"}, - {"op":"create-node","id":2,"kind":"list-item"}, - {"op":"create-node","id":3,"kind":"list-item"}, - {"op":"create-node","id":4,"kind":"panel"}, - {"op":"create-node","id":5,"kind":"text"}, - {"op":"set-prop","id":1,"property":"gap","value":2}, - {"op":"set-prop","id":1,"property":"accessibility-label","value":"Project files"}, - {"op":"set-prop","id":2,"property":"text","value":"src"}, - {"op":"set-prop","id":2,"property":"role","value":"treeitem"}, - {"op":"set-prop","id":2,"property":"tree-level","value":1}, - {"op":"set-prop","id":2,"property":"expanded","value":true}, - {"op":"set-prop","id":2,"property":"selected","value":true}, - {"op":"set-prop","id":2,"property":"press-enabled","value":true}, - {"op":"set-prop","id":2,"property":"change-enabled","value":true}, - {"op":"set-prop","id":2,"property":"toggle-enabled","value":true}, - {"op":"set-prop","id":3,"property":"text","value":"main.cljc"}, - {"op":"set-prop","id":3,"property":"role","value":"treeitem"}, - {"op":"set-prop","id":3,"property":"tree-level","value":2}, - {"op":"set-prop","id":3,"property":"press-enabled","value":true}, - {"op":"set-prop","id":3,"property":"change-enabled","value":true}, - {"op":"set-prop","id":4,"property":"role","value":"treeitem"}, - {"op":"set-prop","id":4,"property":"tree-level","value":1}, - {"op":"set-prop","id":4,"property":"change-enabled","value":true}, - {"op":"set-prop","id":5,"property":"text","value":"README"}, - {"op":"insert-child","parent":1,"child":2,"index":0}, - {"op":"insert-child","parent":1,"child":3,"index":1}, - {"op":"insert-child","parent":1,"child":4,"index":2}, - {"op":"insert-child","parent":4,"child":5,"index":0} - ]} - '''); - - await tester.pumpWidget( - MaterialApp(home: Scaffold(body: backend.widget(node: 1))), - ); - - expect( - find.descendant( - of: find.byKey(LUIFlutterBackend.nodeKey(1)), - matching: find.byType(FocusTraversalGroup), - ), - findsOneWidget, - ); - expect(find.text('src'), findsOneWidget); - expect(find.text('main.cljc'), findsOneWidget); - expect(find.text('README'), findsOneWidget); - final retainedFile = tester.element(find.text('main.cljc')); - final selectedSemantics = tester.widget( - find - .descendant( - of: find.byKey(LUIFlutterBackend.nodeKey(2)), - matching: find.byType(Semantics), - ) - .first, - ); - expect(selectedSemantics.properties.selected, isTrue); - expect(selectedSemantics.properties.expanded, isTrue); - - await tester.tap(find.text('src')); - await tester.pump(); - expect(events, const [ - LUIEvent.press(node: 2), - LUIEvent.toggleChanged(node: 2, checked: false), - ]); - events.clear(); - - final folderFocus = find - .descendant( - of: find.byKey(LUIFlutterBackend.nodeKey(2)), - matching: find.byType(Focus), - ) - .evaluate() - .map((element) => element.widget as Focus) - .firstWhere((focus) => focus.focusNode != null); - folderFocus.focusNode!.requestFocus(); - await tester.pump(); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.pump(); - expect(events, const [LUIEvent.change(node: 3)]); - expect( - FocusManager.instance.primaryFocus?.context - ?.findAncestorWidgetOfExactType(), - isNotNull, - ); - - events.clear(); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft); - await tester.pump(); - expect(events, const [LUIEvent.change(node: 2)]); - events.clear(); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft); - await tester.pump(); - expect(events, const [LUIEvent.toggleChanged(node: 2, checked: false)]); - - backend.applyJson(''' - {"generation":2,"ops":[ - {"op":"set-prop","id":2,"property":"expanded","value":false}, - {"op":"set-prop","id":2,"property":"selected","value":false}, - {"op":"set-prop","id":3,"property":"selected","value":true} - ]} - '''); - await tester.pump(); - expect(tester.element(find.text('main.cljc')), same(retainedFile)); - expect(backend.debugRevision(1), 0); - expect(backend.debugRevision(2), 1); - expect(backend.debugRevision(3), 1); - semantics.dispose(); - }); - - test('rejects orphaned or malformed Tree metadata atomically', () { - for (final operations in [ - ''' - {"op":"create-node","id":1,"kind":"column"}, - {"op":"create-node","id":2,"kind":"list-item"}, - {"op":"set-prop","id":2,"property":"role","value":"treeitem"}, - {"op":"insert-child","parent":1,"child":2,"index":0} - ''', - ''' - {"op":"create-node","id":1,"kind":"tree"}, - {"op":"create-node","id":2,"kind":"list-item"}, - {"op":"set-prop","id":2,"property":"expanded","value":true}, - {"op":"insert-child","parent":1,"child":2,"index":0} - ''', - ''' - {"op":"create-node","id":1,"kind":"tree"}, - {"op":"create-node","id":2,"kind":"list-item"}, - {"op":"set-prop","id":2,"property":"role","value":"treeitem"}, - {"op":"set-prop","id":2,"property":"tree-level","value":0}, - {"op":"insert-child","parent":1,"child":2,"index":0} - ''', - ]) { - final backend = LUIFlutterBackend(); - expect( - () => backend.applyJson('{"generation":1,"ops":[$operations]}'), - throwsA(isA()), - ); - expect(backend.generation, 0); - expect(backend.containsNode(1), isFalse); - } - }); - - testWidgets( - 'registering an image invalidates only Avatars that reference its ImageId', - (tester) async { - final recorder = ui.PictureRecorder(); - final canvas = ui.Canvas(recorder); - canvas.drawColor(const Color(0xff6750a4), BlendMode.src); - final image = await recorder.endRecording().toImage(64, 64); - addTearDown(image.dispose); - - final backend = LUIFlutterBackend() - ..applyJson(''' - {"generation":1,"ops":[ - {"op":"create-node","id":1,"kind":"row"}, - {"op":"create-node","id":2,"kind":"avatar"}, - {"op":"create-node","id":3,"kind":"avatar"}, - {"op":"set-prop","id":2,"property":"text","value":"ZN"}, - {"op":"set-prop","id":2,"property":"image","value":7}, - {"op":"set-prop","id":2,"property":"source-x","value":8.0}, - {"op":"set-prop","id":2,"property":"source-y","value":4.0}, - {"op":"set-prop","id":2,"property":"source-width","value":32.0}, - {"op":"set-prop","id":2,"property":"source-height","value":24.0}, - {"op":"set-prop","id":2,"property":"accessibility-label","value":"Profile picture"}, - {"op":"set-prop","id":3,"property":"text","value":"CT"}, - {"op":"set-prop","id":3,"property":"image","value":8}, - {"op":"insert-child","parent":1,"child":2,"index":0}, - {"op":"insert-child","parent":1,"child":3,"index":1} - ]} - '''); - - await tester.pumpWidget( - MaterialApp(home: Scaffold(body: backend.widget(node: 1))), - ); - final firstRevision = backend.debugRevision(2); - final secondRevision = backend.debugRevision(3); - final retained = tester.renderObject( - find.byKey(LUIFlutterBackend.nodeKey(2)), - ); - expect(find.text('ZN'), findsOneWidget); - expect(find.text('CT'), findsOneWidget); - - backend.registerImage(id: 7, image: image); - await tester.pump(); - expect(find.text('ZN'), findsNothing); - expect(find.text('CT'), findsOneWidget); - expect( - find.descendant( - of: find.byKey(LUIFlutterBackend.nodeKey(2)), - matching: find.byType(CustomPaint), - ), - findsOneWidget, - ); - expect(backend.debugRevision(2), firstRevision + 1); - expect(backend.debugRevision(3), secondRevision); - expect( - tester.renderObject(find.byKey(LUIFlutterBackend.nodeKey(2))), - same(retained), - ); - - backend.unregisterImage(7); - await tester.pump(); - expect(find.text('ZN'), findsOneWidget); - expect(backend.debugRevision(2), firstRevision + 2); - expect(backend.debugRevision(3), secondRevision); - - backend.unregisterImage(7); - expect(backend.debugRevision(2), firstRevision + 2); - expect( - () => backend.registerImage(id: 0, image: image), - throwsA(isA()), - ); - backend.dispose(); - }, - ); - - testWidgets( - 'Image and MediaSurface retain nodes and invalidate only resource dependents', - (tester) async { - final recorder = ui.PictureRecorder(); - final canvas = ui.Canvas(recorder); - canvas.drawColor(const Color(0xff6750a4), BlendMode.src); - final frame = await recorder.endRecording().toImage(64, 48); - addTearDown(frame.dispose); - - final backend = LUIFlutterBackend() - ..applyJson(''' - {"generation":1,"ops":[ - {"op":"create-node","id":1,"kind":"row"}, - {"op":"create-node","id":2,"kind":"image"}, - {"op":"create-node","id":3,"kind":"image"}, - {"op":"create-node","id":4,"kind":"media-surface"}, - {"op":"create-node","id":5,"kind":"media-surface"}, - {"op":"set-prop","id":2,"property":"image","value":7}, - {"op":"set-prop","id":2,"property":"source-x","value":8.0}, - {"op":"set-prop","id":2,"property":"source-y","value":4.0}, - {"op":"set-prop","id":2,"property":"source-width","value":40.0}, - {"op":"set-prop","id":2,"property":"source-height","value":24.0}, - {"op":"set-prop","id":2,"property":"width","value":160}, - {"op":"set-prop","id":2,"property":"height","value":96}, - {"op":"set-prop","id":3,"property":"image","value":8}, - {"op":"set-prop","id":3,"property":"width","value":160}, - {"op":"set-prop","id":3,"property":"height","value":96}, - {"op":"set-prop","id":4,"property":"surface","value":11}, - {"op":"set-prop","id":4,"property":"width","value":160}, - {"op":"set-prop","id":4,"property":"height","value":96}, - {"op":"set-prop","id":5,"property":"surface","value":12}, - {"op":"set-prop","id":5,"property":"width","value":160}, - {"op":"set-prop","id":5,"property":"height","value":96}, - {"op":"insert-child","parent":1,"child":2,"index":0}, - {"op":"insert-child","parent":1,"child":3,"index":1}, - {"op":"insert-child","parent":1,"child":4,"index":2}, - {"op":"insert-child","parent":1,"child":5,"index":3} - ]} - '''); - - await tester.pumpWidget( - MaterialApp(home: Scaffold(body: backend.widget(node: 1))), - ); - final imageRevision = backend.debugRevision(2); - final otherImageRevision = backend.debugRevision(3); - final surfaceRevision = backend.debugRevision(4); - final otherSurfaceRevision = backend.debugRevision(5); - final retainedImage = tester.element( - find.byKey(LUIFlutterBackend.nodeKey(2)), - ); - final retainedSurface = tester.element( - find.byKey(LUIFlutterBackend.nodeKey(4)), - ); - expect( - find.descendant( - of: find.byKey(LUIFlutterBackend.nodeKey(4)), - matching: find.byWidgetPredicate( - (widget) => - widget is ColoredBox && widget.color == const Color(0xff577351), - ), - ), - findsOneWidget, - ); - - backend.registerImage(id: 7, image: frame); - backend.presentMediaSurfaceFrame(id: 11, image: frame); - await tester.pump(); - expect(backend.debugRevision(2), imageRevision + 1); - expect(backend.debugRevision(3), otherImageRevision); - expect(backend.debugRevision(4), surfaceRevision + 1); - expect(backend.debugRevision(5), otherSurfaceRevision); - expect( - tester.element(find.byKey(LUIFlutterBackend.nodeKey(2))), - same(retainedImage), - ); - expect( - tester.element(find.byKey(LUIFlutterBackend.nodeKey(4))), - same(retainedSurface), - ); - expect( - find.descendant( - of: find.byKey(LUIFlutterBackend.nodeKey(2)), - matching: find.byType(CustomPaint), - ), - findsOneWidget, - ); - expect( - find.descendant( - of: find.byKey(LUIFlutterBackend.nodeKey(4)), - matching: find.byType(RawImage), - ), - findsOneWidget, - ); - - backend.unregisterImage(7); - backend.unregisterMediaSurface(11); - await tester.pump(); - expect(backend.debugRevision(2), imageRevision + 2); - expect(backend.debugRevision(4), surfaceRevision + 2); - expect( - () => backend.presentMediaSurfaceFrame(id: 0, image: frame), - throwsA(isA()), - ); - backend.dispose(); - }, - ); - - test('rejects missing media ids and partial Image crops atomically', () { - for (final kind in ['image', 'media-surface']) { - final backend = LUIFlutterBackend(); - expect( - () => backend.applyJson(''' - {"generation":1,"ops":[ - {"op":"create-node","id":1,"kind":"$kind"} - ]} - '''), - throwsA(isA()), - ); - expect(backend.generation, 0); - } - - final partial = LUIFlutterBackend(); - expect( - () => partial.applyJson(''' - {"generation":1,"ops":[ - {"op":"create-node","id":1,"kind":"image"}, - {"op":"set-prop","id":1,"property":"image","value":7}, - {"op":"set-prop","id":1,"property":"source-x","value":0.0} - ]} - '''), - throwsA( - isA().having( - (error) => error.message, - 'message', - contains('image source crop requires all four coordinates'), - ), - ), - ); - expect(partial.generation, 0); - }); - - test('rejects partial and invalid Avatar source crops atomically', () { - final partial = LUIFlutterBackend(); - expect( - () => partial.applyJson(''' - {"generation":1,"ops":[ - {"op":"create-node","id":1,"kind":"avatar"}, - {"op":"set-prop","id":1,"property":"text","value":"ZN"}, - {"op":"set-prop","id":1,"property":"image","value":7}, - {"op":"set-prop","id":1,"property":"source-x","value":0.0} - ]} - '''), - throwsA( - isA().having( - (error) => error.message, - 'message', - contains('avatar source crop requires all four coordinates'), - ), - ), - ); - expect(partial.generation, 0); - - final invalid = LUIFlutterBackend(); - expect( - () => invalid.applyJson(''' - {"generation":1,"ops":[ - {"op":"create-node","id":1,"kind":"avatar"}, - {"op":"set-prop","id":1,"property":"text","value":"ZN"}, - {"op":"set-prop","id":1,"property":"image","value":7}, - {"op":"set-prop","id":1,"property":"source-x","value":0.0}, - {"op":"set-prop","id":1,"property":"source-y","value":0.0}, - {"op":"set-prop","id":1,"property":"source-width","value":-1.0}, - {"op":"set-prop","id":1,"property":"source-height","value":24.0} - ]} - '''), - throwsA( - isA().having( - (error) => error.message, - 'message', - contains('avatar source crop dimensions must be positive'), - ), - ), - ); - expect(invalid.generation, 0); - - final expanded = LUIFlutterBackend(); - expect( - () => expanded.applyJson(''' - {"generation":1,"ops":[ - {"op":"create-node","id":1,"kind":"avatar"}, - {"op":"set-prop","id":1,"property":"text","value":"ZN"}, - {"op":"set-prop","id":1,"property":"width","value":40} - ]} - '''), - throwsA(isA()), - ); - expect(expanded.generation, 0); - }); - - testWidgets('maps semantic content primitives to native Flutter widgets', ( - tester, - ) async { - final backend = LUIFlutterBackend() - ..applyJson(''' - {"generation":1,"ops":[ - {"op":"create-node","id":1,"kind":"box"}, - {"op":"create-node","id":2,"kind":"heading"}, - {"op":"create-node","id":3,"kind":"paragraph"}, - {"op":"set-prop","id":2,"property":"heading-level","value":3}, - {"op":"set-prop","id":2,"property":"text","value":"Account"}, - {"op":"set-prop","id":3,"property":"text","value":"Manage your profile."}, - {"op":"insert-child","parent":1,"child":2,"index":0}, - {"op":"insert-child","parent":1,"child":3,"index":1} - ]} - '''); - - await tester.pumpWidget( - MaterialApp(home: Scaffold(body: backend.widget(node: 1))), - ); - - final box = tester.widget( - find.byWidgetPredicate( - (widget) => widget is LUIFlex && widget.direction == Axis.vertical, - ), - ); - final headingSemantics = tester.widget( - find.byWidgetPredicate( - (widget) => widget is Semantics && widget.properties.header == true, - ), - ); - expect(box.crossAxisAlignment, CrossAxisAlignment.stretch); - expect(headingSemantics.properties.header, isTrue); - expect(find.text('Account'), findsOneWidget); - expect(find.text('Manage your profile.'), findsOneWidget); - }); - - testWidgets('text nodes accept text-alignment', (tester) async { - final backend = LUIFlutterBackend() - ..applyJson(''' - {"generation":1,"ops":[ - {"op":"create-node","id":1,"kind":"box"}, - {"op":"create-node","id":2,"kind":"text"}, - {"op":"create-node","id":3,"kind":"text"}, - {"op":"set-prop","id":2,"property":"text","value":"Centered"}, - {"op":"set-prop","id":2,"property":"text-alignment","value":"center"}, - {"op":"set-prop","id":3,"property":"text","value":"Trailing"}, - {"op":"set-prop","id":3,"property":"text-alignment","value":"end"}, - {"op":"insert-child","parent":1,"child":2,"index":0}, - {"op":"insert-child","parent":1,"child":3,"index":1} - ]} - '''); - - await tester.pumpWidget( - MaterialApp(home: Scaffold(body: backend.widget(node: 1))), - ); - - expect(find.text('Centered'), findsOneWidget); - expect(find.text('Trailing'), findsOneWidget); - expect( - tester.widget(find.text('Centered')).textAlign, - TextAlign.center, - ); - expect(tester.widget(find.text('Trailing')).textAlign, TextAlign.end); - }); - - testWidgets('accepts drawer text, select accessibility-label, destructive menu-item', ( - tester, - ) async { - final backend = LUIFlutterBackend() - ..applyJson(''' - {"generation":1,"ops":[ - {"op":"create-node","id":1,"kind":"drawer"}, - {"op":"create-node","id":2,"kind":"box"}, - {"op":"create-node","id":3,"kind":"box"}, - {"op":"create-node","id":4,"kind":"select"}, - {"op":"create-node","id":5,"kind":"context-menu"}, - {"op":"create-node","id":6,"kind":"menu-item"}, - {"op":"create-node","id":7,"kind":"button"}, - {"op":"set-prop","id":1,"property":"text","value":"Navigation"}, - {"op":"set-prop","id":4,"property":"text","value":"Dark"}, - {"op":"set-prop","id":4,"property":"accessibility-label","value":"Theme"}, - {"op":"set-prop","id":4,"property":"press-enabled","value":true}, - {"op":"set-prop","id":6,"property":"text","value":"Delete"}, - {"op":"set-prop","id":6,"property":"variant","value":"destructive"}, - {"op":"set-prop","id":6,"property":"press-enabled","value":true}, - {"op":"set-prop","id":7,"property":"text","value":"More"}, - {"op":"insert-child","parent":1,"child":2,"index":0}, - {"op":"insert-child","parent":1,"child":3,"index":1}, - {"op":"insert-child","parent":2,"child":4,"index":0}, - {"op":"insert-child","parent":2,"child":7,"index":1}, - {"op":"insert-child","parent":7,"child":5,"index":0}, - {"op":"insert-child","parent":5,"child":6,"index":0} - ]} - '''); - - await tester.pumpWidget( - MaterialApp(home: Scaffold(body: backend.widget(node: 1))), - ); - - expect(find.text('Dark'), findsOneWidget); - expect(backend.generation, 1); - }); - - testWidgets('applies reusable Surface styles without replacing widgets', ( - tester, - ) async { - final backend = LUIFlutterBackend() - ..applyJson(''' - {"generation":1,"ops":[ - {"op":"create-node","id":1,"kind":"row"}, - {"op":"create-node","id":2,"kind":"text"}, - {"op":"set-prop","id":1,"property":"padding-horizontal","value":10}, - {"op":"set-prop","id":1,"property":"padding-vertical","value":2}, - {"op":"set-prop","id":1,"property":"background","value":"success"}, - {"op":"set-prop","id":1,"property":"border-color","value":"success-foreground"}, - {"op":"set-prop","id":1,"property":"border-width","value":1}, - {"op":"set-prop","id":1,"property":"corner-radius","value":6}, - {"op":"set-prop","id":2,"property":"text","value":"Styled"}, - {"op":"set-prop","id":2,"property":"foreground","value":"success-foreground"}, - {"op":"insert-child","parent":1,"child":2,"index":0} - ]} - '''); - - await tester.pumpWidget( - MaterialApp(home: Scaffold(body: backend.widget(node: 1))), - ); - - final rootFinder = find.byKey(LUIFlutterBackend.nodeKey(1)); - final surfaceFinder = find.descendant( - of: rootFinder, - matching: find.byType(Container), - ); - final surface = tester.widget(surfaceFinder.first); - final originalRow = tester.renderObject( - find.byKey(LUIFlutterBackend.nodeKey(1)), - ); - final originalText = tester.renderObject( - find.byKey(LUIFlutterBackend.nodeKey(2)), - ); - final decoration = surface.decoration! as BoxDecoration; - expect(surface.padding, const EdgeInsets.fromLTRB(10, 2, 10, 2)); - expect(decoration.border, isA()); - expect(decoration.borderRadius, BorderRadius.circular(6)); - expect(tester.widget(find.text('Styled')).style?.color, isNotNull); - - backend.applyJson(''' - {"generation":2,"ops":[ - {"op":"set-prop","id":1,"property":"corner-radius","value":999}, - {"op":"set-prop","id":1,"property":"background","value":"error"} - ]} - '''); - await tester.pump(); - - expect( - tester.renderObject(find.byKey(LUIFlutterBackend.nodeKey(1))), - same(originalRow), - ); - expect( - tester.renderObject(find.byKey(LUIFlutterBackend.nodeKey(2))), - same(originalText), - ); - }); - - testWidgets('maps typed Surface size constraints to native layout widgets', ( - tester, - ) async { - final backend = LUIFlutterBackend() - ..applyJson(''' - {"generation":1,"ops":[ - {"op":"create-node","id":1,"kind":"box"}, - {"op":"set-prop","id":1,"property":"width","value":120}, - {"op":"set-prop","id":1,"property":"height","value":24}, - {"op":"set-prop","id":1,"property":"min-width","value":80}, - {"op":"set-prop","id":1,"property":"max-width","value":160}, - {"op":"set-prop","id":1,"property":"min-height","value":16}, - {"op":"set-prop","id":1,"property":"max-height","value":32} - ]} - '''); - - await tester.pumpWidget( - MaterialApp(home: Scaffold(body: backend.widget(node: 1))), - ); - - final root = find.byKey(LUIFlutterBackend.nodeKey(1)); - final sized = tester.widget( - find.descendant(of: root, matching: find.byType(SizedBox)).first, - ); - final constrained = tester.widget( - find.descendant(of: root, matching: find.byType(ConstrainedBox)).first, - ); - expect(sized.width, 120); - expect(sized.height, 24); - expect( - constrained.constraints, - const BoxConstraints( - minWidth: 80, - maxWidth: 160, - minHeight: 16, - maxHeight: 32, - ), - ); - - final revision = backend.debugRevision(1); - expect( - () => backend.applyJson(''' - {"generation":2,"ops":[ - {"op":"set-prop","id":1,"property":"min-width","value":170}, - {"op":"set-prop","id":1,"property":"max-width","value":160} - ]} - '''), - throwsA(isA()), - ); - expect(backend.generation, 1); - expect(backend.debugRevision(1), revision); - }); - - testWidgets('maps the Vercel Native layout vocabulary to Flutter widgets', ( - tester, - ) async { - final backend = LUIFlutterBackend() - ..applyJson(''' - {"generation":1,"ops":[ - {"op":"create-node","id":1,"kind":"row"}, - {"op":"create-node","id":2,"kind":"box"}, - {"op":"create-node","id":3,"kind":"box"}, - {"op":"create-node","id":4,"kind":"grid"}, - {"op":"create-node","id":5,"kind":"box"}, - {"op":"create-node","id":6,"kind":"box"}, - {"op":"set-prop","id":1,"property":"width","value":300}, - {"op":"set-prop","id":1,"property":"height","value":100}, - {"op":"set-prop","id":1,"property":"main","value":"space_between"}, - {"op":"set-prop","id":1,"property":"cross","value":"end"}, - {"op":"set-prop","id":2,"property":"grow","value":1.0}, - {"op":"set-prop","id":2,"property":"height","value":20}, - {"op":"set-prop","id":3,"property":"width","value":50}, - {"op":"set-prop","id":3,"property":"height","value":20}, - {"op":"set-prop","id":4,"property":"width","value":300}, - {"op":"set-prop","id":4,"property":"columns","value":2}, - {"op":"set-prop","id":4,"property":"gap","value":6}, - {"op":"set-prop","id":5,"property":"height","value":20}, - {"op":"set-prop","id":6,"property":"height","value":20}, - {"op":"insert-child","parent":1,"child":2,"index":0}, - {"op":"insert-child","parent":1,"child":3,"index":1}, - {"op":"insert-child","parent":4,"child":5,"index":0}, - {"op":"insert-child","parent":4,"child":6,"index":1} - ]} - '''); - - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: Align( - alignment: Alignment.topLeft, - child: backend.widget(node: 1), - ), - ), - ), - ); - - final row = tester.widget( - find - .byWidgetPredicate( - (widget) => widget is LUIFlex && widget.direction == Axis.horizontal, - ) - .first, - ); - expect(row.mainAxisAlignment, MainAxisAlignment.spaceBetween); - expect(row.crossAxisAlignment, CrossAxisAlignment.end); - expect(tester.getSize(find.byKey(LUIFlutterBackend.nodeKey(2))).width, 250); - expect(tester.getTopLeft(find.byKey(LUIFlutterBackend.nodeKey(2))).dy, 80); - - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: Align( - alignment: Alignment.topLeft, - child: backend.widget(node: 4), - ), - ), - ), - ); - expect(tester.getSize(find.byKey(LUIFlutterBackend.nodeKey(5))).width, 147); - expect(tester.getTopLeft(find.byKey(LUIFlutterBackend.nodeKey(6))).dx, 153); - - expect( - () => backend.applyJson(''' - {"generation":2,"ops":[ - {"op":"set-prop","id":1,"property":"main","value":"between"} - ]} - '''), - throwsA(isA()), - ); - expect(backend.generation, 1); - }); - - testWidgets('maps Vercel Native overlay surfaces to native Flutter widgets', ( - tester, - ) async { - final backend = LUIFlutterBackend() - ..applyJson(''' - {"generation":1,"ops":[ - {"op":"create-node","id":1,"kind":"stack"}, - {"op":"create-node","id":2,"kind":"panel"}, - {"op":"create-node","id":3,"kind":"card"}, - {"op":"create-node","id":4,"kind":"text"}, - {"op":"create-node","id":5,"kind":"text"}, - {"op":"set-prop","id":1,"property":"width","value":320}, - {"op":"set-prop","id":1,"property":"height","value":180}, - {"op":"insert-child","parent":1,"child":2,"index":0}, - {"op":"insert-child","parent":1,"child":3,"index":1}, - {"op":"insert-child","parent":2,"child":4,"index":0}, - {"op":"insert-child","parent":3,"child":5,"index":0} - ]} - '''); - - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: Align( - alignment: Alignment.topLeft, - child: backend.widget(node: 1), - ), - ), - ), - ); - - expect( - find.descendant( - of: find.byKey(LUIFlutterBackend.nodeKey(1)), - matching: find.byWidgetPredicate((widget) => widget is Stack), - ), - findsNWidgets(3), - ); - final panelMaterial = tester.widget( - find - .descendant( - of: find.byKey(LUIFlutterBackend.nodeKey(2)), - matching: find.byType(Material), - ) - .first, - ); - final cardMaterial = tester.widget( - find - .descendant( - of: find.byKey(LUIFlutterBackend.nodeKey(3)), - matching: find.byType(Material), - ) - .first, - ); - expect(panelMaterial.elevation, 1); - expect(cardMaterial.elevation, 0); - expect( - find - .descendant( - of: find.byKey(LUIFlutterBackend.nodeKey(3)), - matching: find.byWidgetPredicate( - (widget) => - widget is Padding && - widget.padding == const EdgeInsets.all(24), - ), - ) - .evaluate(), - hasLength(1), - ); - - expect( - () => backend.applyJson(''' - {"generation":2,"ops":[ - {"op":"set-prop","id":3,"property":"gap","value":8} - ]} - '''), - throwsA(isA()), - ); - expect(backend.generation, 1); - }); - - testWidgets( - 'maps Dialog to a native modal route with model-owned dismissal', - (tester) async { - final events = []; - final backend = LUIFlutterBackend(onEvent: events.add) - ..applyJson(''' - {"generation":1,"ops":[ - {"op":"create-node","id":1,"kind":"column"}, - {"op":"create-node","id":2,"kind":"dialog"}, - {"op":"create-node","id":3,"kind":"input"}, - {"op":"set-prop","id":2,"property":"text","value":"Rename note"}, - {"op":"set-prop","id":2,"property":"width","value":380}, - {"op":"set-prop","id":2,"property":"height","value":240}, - {"op":"set-prop","id":2,"property":"padding","value":24}, - {"op":"insert-child","parent":1,"child":2,"index":0}, - {"op":"insert-child","parent":2,"child":3,"index":0} - ]} - '''); - - await tester.pumpWidget( - MaterialApp(home: Scaffold(body: backend.widget(node: 1))), - ); - await tester.pumpAndSettle(); - - expect(find.byType(Dialog), findsOneWidget); - expect(find.text('Rename note'), findsOneWidget); - expect(find.byKey(LUIFlutterBackend.nodeKey(3)), findsOneWidget); - expect( - tester - .getSize(find.byKey(const ValueKey('lui-dialog-surface-2'))) - .width, - 380, - ); - final inputRenderObject = tester.renderObject( - find.byKey(LUIFlutterBackend.nodeKey(3)), - ); - - backend.applyJson(''' - {"generation":2,"ops":[ - {"op":"set-prop","id":2,"property":"text","value":"Renamed note"}, - {"op":"set-prop","id":2,"property":"width","value":400} - ]} - '''); - await tester.pump(); - expect(find.text('Renamed note'), findsOneWidget); - expect( - tester - .getSize(find.byKey(const ValueKey('lui-dialog-surface-2'))) - .width, - 400, - ); - expect( - tester.renderObject(find.byKey(LUIFlutterBackend.nodeKey(3))), - same(inputRenderObject), - ); - - await tester.tapAt(const Offset(4, 4)); - await tester.pumpAndSettle(); - expect(events, const [LUIEvent.dismiss(node: 2)]); - expect(find.byType(Dialog), findsNothing); - - expect( - () => backend.performDismiss(1), - throwsA(isA()), - ); - }, - ); - - testWidgets('maps Sheet to a retained native Material bottom sheet', ( - tester, - ) async { - final events = []; - final backend = LUIFlutterBackend(onEvent: events.add) - ..applyJson(''' - {"generation":1,"ops":[ - {"op":"create-node","id":1,"kind":"column"}, - {"op":"create-node","id":2,"kind":"sheet"}, - {"op":"create-node","id":3,"kind":"input"}, - {"op":"set-prop","id":2,"property":"text","value":"Share"}, - {"op":"set-prop","id":2,"property":"height","value":320}, - {"op":"set-prop","id":2,"property":"padding","value":24}, - {"op":"insert-child","parent":1,"child":2,"index":0}, - {"op":"insert-child","parent":2,"child":3,"index":0} - ]} - '''); - - await tester.pumpWidget( - MaterialApp(home: Scaffold(body: backend.widget(node: 1))), - ); - await tester.pumpAndSettle(); - - expect(find.byType(BottomSheet), findsOneWidget); - expect( - tester.widget(find.byType(BottomSheet)).showDragHandle, - true, - ); - expect(find.text('Share'), findsOneWidget); - final surface = find.byKey(const ValueKey('lui-sheet-surface-2')); - expect(tester.getSize(surface).height, 320); - final inputRenderObject = tester.renderObject( - find.byKey(LUIFlutterBackend.nodeKey(3)), - ); - - backend.applyJson(''' - {"generation":2,"ops":[ - {"op":"set-prop","id":2,"property":"text","value":"Share link"}, - {"op":"set-prop","id":2,"property":"height","value":360} - ]} - '''); - await tester.pump(); - expect(find.text('Share link'), findsOneWidget); - expect(tester.getSize(surface).height, 360); - expect( - tester.renderObject(find.byKey(LUIFlutterBackend.nodeKey(3))), - same(inputRenderObject), - ); - - await tester.tapAt(const Offset(4, 4)); - await tester.pumpAndSettle(); - expect(events, const [LUIEvent.dismiss(node: 2)]); - expect(find.byType(BottomSheet), findsNothing); - }); - - testWidgets('maps List flow and multi-child Scroll to Flutter widgets', ( - tester, - ) async { - final backend = LUIFlutterBackend() - ..applyJson(''' - {"generation":1,"ops":[ - {"op":"create-node","id":1,"kind":"row"}, - {"op":"create-node","id":2,"kind":"list"}, - {"op":"create-node","id":3,"kind":"scroll"}, - {"op":"create-node","id":4,"kind":"text"}, - {"op":"create-node","id":5,"kind":"text"}, - {"op":"create-node","id":6,"kind":"text"}, - {"op":"set-prop","id":1,"property":"width","value":400}, - {"op":"set-prop","id":1,"property":"height","value":200}, - {"op":"set-prop","id":2,"property":"width","value":180}, - {"op":"set-prop","id":2,"property":"height","value":100}, - {"op":"set-prop","id":2,"property":"gap","value":8}, - {"op":"set-prop","id":2,"property":"main","value":"end"}, - {"op":"set-prop","id":2,"property":"cross","value":"stretch"}, - {"op":"set-prop","id":3,"property":"width","value":180}, - {"op":"set-prop","id":3,"property":"height","value":100}, - {"op":"insert-child","parent":1,"child":2,"index":0}, - {"op":"insert-child","parent":1,"child":3,"index":1}, - {"op":"insert-child","parent":2,"child":4,"index":0}, - {"op":"insert-child","parent":3,"child":5,"index":0}, - {"op":"insert-child","parent":3,"child":6,"index":1} - ]} - '''); - - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: Align( - alignment: Alignment.topLeft, - child: backend.widget(node: 1), - ), - ), - ), - ); - - final list = tester.widget( - find - .descendant( - of: find.byKey(LUIFlutterBackend.nodeKey(2)), - matching: find.byWidgetPredicate( - (widget) => widget is LUIFlex && widget.direction == Axis.vertical, - ), - ) - .first, - ); - expect(list.spacing, 8); - expect(list.mainAxisAlignment, MainAxisAlignment.end); - expect(list.crossAxisAlignment, CrossAxisAlignment.stretch); - - final scroll = tester.widget( - find - .descendant( - of: find.byKey(LUIFlutterBackend.nodeKey(3)), - matching: find.byType(SingleChildScrollView), - ) - .first, - ); - expect(scroll.child, isA()); - expect((scroll.child! as Stack).children, hasLength(2)); - }); - - testWidgets('maps a progress fraction to one retained native indicator', ( - tester, - ) async { - final semantics = tester.ensureSemantics(); - final backend = LUIFlutterBackend() - ..applyJson(''' - {"generation":1,"ops":[ - {"op":"create-node","id":2,"kind":"progress"}, - {"op":"set-prop","id":2,"property":"value","value":0.3} - ]} - '''); - - await tester.pumpWidget( - MaterialApp(home: Scaffold(body: backend.widget(node: 2))), - ); - - final originalProgress = tester.renderObject( - find.byKey(LUIFlutterBackend.nodeKey(2)), - ); - expect( - tester - .widget(find.byType(LinearProgressIndicator)) - .value, - 0.3, - ); - expect( - tester.getSemantics(find.byType(LinearProgressIndicator)), - matchesSemantics(value: '30%'), - ); - - backend.applyJson(''' - {"generation":2,"ops":[ - {"op":"set-prop","id":2,"property":"value","value":1.2} - ]} - '''); - await tester.pump(); - expect( - tester.renderObject(find.byKey(LUIFlutterBackend.nodeKey(2))), - same(originalProgress), - ); - expect( - tester - .widget(find.byType(LinearProgressIndicator)) - .value, - 1, - ); - semantics.dispose(); - }); - - testWidgets('maps Spinner size rungs to CircularProgressIndicator', ( - tester, - ) async { - final backend = LUIFlutterBackend() - ..applyJson(''' - {"generation":1,"ops":[ - {"op":"create-node","id":1,"kind":"spinner"}, - {"op":"set-prop","id":1,"property":"size","value":"sm"}, - {"op":"set-prop","id":1,"property":"foreground","value":"red"} - ]} - '''); - - await tester.pumpWidget( - MaterialApp(home: Scaffold(body: backend.widget(node: 1))), - ); - - final spinner = find.byType(CircularProgressIndicator); - final originalNode = tester.renderObject( - find.byKey(LUIFlutterBackend.nodeKey(1)), - ); - expect(spinner, findsOneWidget); - expect(tester.getSize(spinner), const Size.square(16)); - expect(tester.widget(spinner).color, Colors.red); - - backend.applyJson(''' - {"generation":2,"ops":[ - {"op":"set-prop","id":1,"property":"size","value":"lg"} - ]} - '''); - await tester.pump(); - expect( - tester.renderObject(find.byKey(LUIFlutterBackend.nodeKey(1))), - same(originalNode), - ); - expect(tester.getSize(spinner), const Size.square(24)); - - backend.applyJson(''' - {"generation":3,"ops":[ - {"op":"set-prop","id":1,"property":"width","value":32}, - {"op":"set-prop","id":1,"property":"height","value":28} - ]} - '''); - await tester.pump(); - expect(tester.getSize(spinner), const Size(32, 28)); - }); - - test('rejects unsupported Spinner size names atomically', () { - final backend = LUIFlutterBackend(); - - expect( - () => backend.applyJson(''' - {"generation":1,"ops":[ - {"op":"create-node","id":1,"kind":"spinner"}, - {"op":"set-prop","id":1,"property":"size","value":"heading"} - ]} - '''), - throwsA(isA()), - ); - expect(backend.containsNode(1), isFalse); - expect(backend.generation, 0); - }); - - testWidgets('maps Icon names to Material icons incrementally', ( - tester, - ) async { - final backend = LUIFlutterBackend() - ..applyJson(''' - {"generation":1,"ops":[ - {"op":"create-node","id":1,"kind":"icon"}, - {"op":"set-prop","id":1,"property":"name","value":"search"}, - {"op":"set-prop","id":1,"property":"size","value":"sm"}, - {"op":"set-prop","id":1,"property":"foreground","value":"red"} - ]} - '''); - - await tester.pumpWidget( - MaterialApp(home: Scaffold(body: backend.widget(node: 1))), - ); - final originalNode = tester.renderObject( - find.byKey(LUIFlutterBackend.nodeKey(1)), - ); - expect(tester.widget(find.byType(Icon)).icon, Icons.search); - expect(tester.widget(find.byType(Icon)).size, 16); - expect(tester.widget(find.byType(Icon)).color, Colors.red); - - backend.applyJson(''' - {"generation":2,"ops":[ - {"op":"set-prop","id":1,"property":"name","value":"trash"} - ]} - '''); - await tester.pump(); - expect( - tester.renderObject(find.byKey(LUIFlutterBackend.nodeKey(1))), - same(originalNode), - ); - expect(tester.widget(find.byType(Icon)).icon, Icons.delete_outline); - }); - - test('rejects unsupported Icon names atomically', () { - final backend = LUIFlutterBackend(); - expect( - () => backend.applyJson(''' - {"generation":1,"ops":[ - {"op":"create-node","id":1,"kind":"icon"}, - {"op":"set-prop","id":1,"property":"name","value":"unknown"} - ]} - '''), - throwsA(isA()), - ); - expect(backend.containsNode(1), isFalse); - expect(backend.generation, 0); - }); - - testWidgets('resolves application icons through the Flutter registry', ( - tester, - ) async { - final backend = - LUIFlutterBackend(appIcons: const {'wave-pulse': Icons.waves}) - ..applyJson(''' - {"generation":1,"ops":[ - {"op":"create-node","id":1,"kind":"icon"}, - {"op":"set-prop","id":1,"property":"name","value":"app:wave-pulse"} - ]} - '''); - - await tester.pumpWidget( - MaterialApp(home: Scaffold(body: backend.widget(node: 1))), - ); - expect(tester.widget(find.byType(Icon)).icon, Icons.waves); - - backend.applyJson(''' - {"generation":2,"ops":[ - {"op":"set-prop","id":1,"property":"name","value":"app:missing"} - ]} - '''); - await tester.pump(); - expect(tester.widget(find.byType(Icon)).icon, Icons.question_mark); - }); - - testWidgets('maps Separator orientation to retained native dividers', ( - tester, - ) async { - final backend = LUIFlutterBackend() - ..applyJson(''' - {"generation":1,"ops":[ - {"op":"create-node","id":1,"kind":"divider"}, - {"op":"set-prop","id":1,"property":"orientation","value":"horizontal"} - ]} - '''); - - await tester.pumpWidget( - MaterialApp(home: Scaffold(body: backend.widget(node: 1))), - ); - - final original = tester.renderObject( - find.byKey(LUIFlutterBackend.nodeKey(1)), - ); - expect(find.byType(Divider), findsOneWidget); - expect(find.byType(VerticalDivider), findsNothing); - - backend.applyJson(''' - {"generation":2,"ops":[ - {"op":"set-prop","id":1,"property":"orientation","value":"vertical"} - ]} - '''); - await tester.pump(); - - expect( - tester.renderObject(find.byKey(LUIFlutterBackend.nodeKey(1))), - same(original), - ); - expect(find.byType(Divider), findsNothing); - expect(find.byType(VerticalDivider), findsOneWidget); - - expect( - () => backend.applyJson(''' - {"generation":3,"ops":[ - {"op":"set-prop","id":1,"property":"orientation","value":"diagonal"} - ]} - '''), - throwsA(isA()), - ); - expect(backend.generation, 2); - expect(backend.debugRevision(1), 1); - }); - - testWidgets('textarea auto-resizes without losing focus or retained state', ( - tester, - ) async { - final backend = LUIFlutterBackend() - ..applyJson(''' - {"generation":1,"ops":[ - {"op":"create-node","id":1,"kind":"textarea"}, - {"op":"set-prop","id":1,"property":"text","value":"One line"}, - {"op":"set-prop","id":1,"property":"placeholder","value":"Notes"} - ]} - '''); - - await tester.pumpWidget( - MaterialApp(home: Scaffold(body: backend.widget(node: 1))), - ); - - final field = tester.widget(find.byType(TextField)); - final editable = tester.state(find.byType(EditableText)); - final oneLineHeight = tester.getSize(find.byType(TextField)).height; - await tester.tap(find.byType(TextField)); - await tester.pump(); - - expect(field.minLines, 1); - expect(field.maxLines, isNull); - expect(field.keyboardType, TextInputType.multiline); - expect(field.decoration?.hintText, 'Notes'); - expect(editable.widget.focusNode.hasFocus, isTrue); - - backend.applyJson(r''' - {"generation":2,"ops":[ - {"op":"set-prop","id":1,"property":"text","value":"One\nTwo\nThree"} - ]} - '''); - await tester.pump(); - - expect( - tester.getSize(find.byType(TextField)).height, - greaterThan(oneLineHeight), - ); - expect( - tester.state(find.byType(EditableText)), - same(editable), - ); - expect(editable.widget.focusNode.hasFocus, isTrue); - }); - - testWidgets('maps direct text-bearing checkbox and switch natively', ( - tester, - ) async { - final semantics = tester.ensureSemantics(); - final events = []; - final backend = LUIFlutterBackend(onEvent: events.add) - ..applyJson(''' - {"generation":1,"ops":[ - {"op":"create-node","id":1,"kind":"box"}, - {"op":"create-node","id":2,"kind":"checkbox"}, - {"op":"create-node","id":3,"kind":"switch"}, - {"op":"set-prop","id":2,"property":"accessibility-label","value":"Select all"}, - {"op":"set-prop","id":2,"property":"text","value":"Select everything"}, - {"op":"set-prop","id":2,"property":"checked","value":true}, - {"op":"set-prop","id":3,"property":"accessibility-label","value":"Notifications"}, - {"op":"set-prop","id":3,"property":"text","value":"Email notifications"}, - {"op":"set-prop","id":3,"property":"checked","value":false}, - {"op":"insert-child","parent":1,"child":2,"index":0}, - {"op":"insert-child","parent":1,"child":3,"index":1} - ]} - '''); - - await tester.pumpWidget( - MaterialApp(home: Scaffold(body: backend.widget(node: 1))), - ); - - final checkbox = tester.widget(find.byType(Checkbox)); - final toggle = tester.widget(find.byType(Switch)); - expect(checkbox.tristate, isFalse); - expect(checkbox.value, isTrue); - expect(toggle.value, isFalse); - expect(find.text('Select everything'), findsOneWidget); - expect(find.text('Email notifications'), findsOneWidget); - expect( - tester.getSemantics(find.bySemanticsLabel('Select all')), - matchesSemantics( - label: 'Select all', - hasCheckedState: true, - isChecked: true, - hasEnabledState: true, - isEnabled: true, - hasTapAction: true, - ), - ); - expect( - tester.getSemantics(find.bySemanticsLabel('Notifications')), - matchesSemantics( - label: 'Notifications', - hasToggledState: true, - hasEnabledState: true, - isEnabled: true, - hasTapAction: true, - ), - ); - await tester.tap(find.text('Select everything')); - expect(events, const [LUIEvent.toggleChanged(node: 2, checked: false)]); - events.clear(); - - final originalCheckbox = tester.renderObject( - find.byKey(LUIFlutterBackend.nodeKey(2)), - ); - final originalSwitch = tester.renderObject( - find.byKey(LUIFlutterBackend.nodeKey(3)), - ); - await tester.tap(find.byType(Checkbox)); - await tester.tap(find.byType(Switch)); - expect(events, const [ - LUIEvent.toggleChanged(node: 2, checked: false), - LUIEvent.toggleChanged(node: 3, checked: true), - ]); - - backend.applyJson(''' - {"generation":2,"ops":[ - {"op":"set-prop","id":2,"property":"checked","value":false}, - {"op":"set-prop","id":2,"property":"text","value":"All items"}, - {"op":"set-prop","id":3,"property":"checked","value":true} - ]} - '''); - await tester.pump(); - expect( - tester.renderObject(find.byKey(LUIFlutterBackend.nodeKey(2))), - same(originalCheckbox), - ); - expect( - tester.renderObject(find.byKey(LUIFlutterBackend.nodeKey(3))), - same(originalSwitch), - ); - expect(find.text('All items'), findsOneWidget); - expect( - () => backend.applyJson(''' - {"generation":3,"ops":[ - {"op":"set-prop","id":2,"property":"indeterminate","value":true} - ]} - '''), - throwsA(isA()), - ); - expect(backend.generation, 2); - semantics.dispose(); - }); - - testWidgets('maps the daily value-control batch to native Flutter widgets', ( - tester, - ) async { - final events = []; - final backend = LUIFlutterBackend(onEvent: events.add) - ..applyJson(''' - {"generation":1,"ops":[ - {"op":"create-node","id":1,"kind":"column"}, - {"op":"create-node","id":2,"kind":"toggle"}, - {"op":"create-node","id":3,"kind":"radio-group"}, - {"op":"create-node","id":4,"kind":"radio"}, - {"op":"create-node","id":5,"kind":"slider"}, - {"op":"set-prop","id":2,"property":"text","value":"Bold"}, - {"op":"set-prop","id":2,"property":"checked","value":false}, - {"op":"set-prop","id":3,"property":"accessibility-label","value":"Density"}, - {"op":"set-prop","id":4,"property":"text","value":"Comfortable"}, - {"op":"set-prop","id":4,"property":"checked","value":false}, - {"op":"set-prop","id":4,"property":"change-enabled","value":true}, - {"op":"set-prop","id":5,"property":"value","value":0.25}, - {"op":"set-prop","id":5,"property":"accessibility-label","value":"Volume"}, - {"op":"insert-child","parent":1,"child":2,"index":0}, - {"op":"insert-child","parent":1,"child":3,"index":1}, - {"op":"insert-child","parent":3,"child":4,"index":0}, - {"op":"insert-child","parent":1,"child":5,"index":2} - ]} - '''); - - await tester.pumpWidget( - MaterialApp(home: Scaffold(body: backend.widget(node: 1))), - ); - - expect(find.byType(FilterChip), findsOneWidget); - expect(find.byType(RadioGroup), findsOneWidget); - expect(find.byType(Radio), findsOneWidget); - expect(find.byType(Slider), findsOneWidget); - final originalToggle = tester.renderObject( - find.byKey(LUIFlutterBackend.nodeKey(2)), - ); - final originalRadio = tester.renderObject( - find.byKey(LUIFlutterBackend.nodeKey(4)), - ); - final originalSlider = tester.renderObject( - find.byKey(LUIFlutterBackend.nodeKey(5)), - ); - - await tester.tap(find.byType(FilterChip)); - await tester.tap(find.text('Comfortable')); - await tester.drag(find.byType(Slider), const Offset(100, 0)); - await tester.pump(); - - expect(events[0], const LUIEvent.toggleChanged(node: 2, checked: true)); - expect(events[1], const LUIEvent.change(node: 4)); - final sliderEvent = events.last as LUIValueChangedEvent; - expect(sliderEvent.node, 5); - expect(sliderEvent.value, greaterThan(0.25)); - - backend.applyJson(''' - {"generation":2,"ops":[ - {"op":"set-prop","id":2,"property":"checked","value":true}, - {"op":"set-prop","id":4,"property":"checked","value":true}, - {"op":"set-prop","id":5,"property":"value","value":0.75} - ]} - '''); - await tester.pump(); - expect( - tester.renderObject(find.byKey(LUIFlutterBackend.nodeKey(2))), - same(originalToggle), - ); - expect( - tester.renderObject(find.byKey(LUIFlutterBackend.nodeKey(4))), - same(originalRadio), - ); - expect( - tester.renderObject(find.byKey(LUIFlutterBackend.nodeKey(5))), - same(originalSlider), - ); - expect(tester.widget(find.byType(FilterChip)).selected, isTrue); - expect( - tester.widget>(find.byType(RadioGroup)).groupValue, - 4, - ); - expect(tester.widget(find.byType(Slider)).value, 0.75); - final eventCount = events.length; - await tester.tap(find.text('Comfortable')); - await tester.pump(); - expect(events, hasLength(eventCount)); - - final ungrouped = LUIFlutterBackend(); - expect( - () => ungrouped.applyJson(''' - {"generation":1,"ops":[ - {"op":"create-node","id":1,"kind":"radio"}, - {"op":"set-prop","id":1,"property":"text","value":"Orphan"} - ]} - '''), - throwsA(isA()), - ); - }); - testWidgets('maps ContextMenu to secondary-click and retained menu actions', ( - tester, - ) async { - final events = []; - final backend = LUIFlutterBackend(onEvent: events.add) - ..applyJson(''' - {"generation":1,"ops":[ - {"op":"create-node","id":1,"kind":"button"}, - {"op":"create-node","id":2,"kind":"context-menu"}, - {"op":"create-node","id":3,"kind":"menu-item"}, - {"op":"create-node","id":4,"kind":"divider"}, - {"op":"create-node","id":5,"kind":"menu-item"}, - {"op":"set-prop","id":1,"property":"text","value":"Document"}, - {"op":"set-prop","id":3,"property":"text","value":"Rename"}, - {"op":"set-prop","id":3,"property":"press-enabled","value":true}, - {"op":"set-prop","id":5,"property":"text","value":"Archive"}, - {"op":"set-prop","id":5,"property":"press-enabled","value":true}, - {"op":"set-prop","id":5,"property":"enabled","value":false}, - {"op":"insert-child","parent":1,"child":2,"index":0}, - {"op":"insert-child","parent":2,"child":3,"index":0}, - {"op":"insert-child","parent":2,"child":4,"index":1}, - {"op":"insert-child","parent":2,"child":5,"index":2} - ]} - '''); - - await tester.pumpWidget( - MaterialApp(home: Scaffold(body: backend.widget(node: 1))), - ); - expect(find.text('Document'), findsOneWidget); - expect(find.text('Rename'), findsNothing); - final host = find.byKey(LUIFlutterBackend.nodeKey(1)); - await tester.tapAt(tester.getCenter(host), buttons: kSecondaryButton); - await tester.pumpAndSettle(); - expect(find.text('Rename'), findsOneWidget); - expect(find.text('Archive'), findsOneWidget); - expect( - tester - .widget>(find.byType(PopupMenuItem).last) - .enabled, - isFalse, - ); - await tester.tap(find.text('Rename')); - await tester.pumpAndSettle(); - expect(events, const [LUIEvent.press(node: 3)]); - expect( - () => backend.applyJson(''' - {"generation":2,"ops":[ - {"op":"create-node","id":6,"kind":"context-menu"}, - {"op":"create-node","id":7,"kind":"menu-item"}, - {"op":"set-prop","id":7,"property":"text","value":"Nested"}, - {"op":"set-prop","id":7,"property":"press-enabled","value":true}, - {"op":"insert-child","parent":3,"child":6,"index":0}, - {"op":"insert-child","parent":6,"child":7,"index":0} - ]} - '''), - throwsA(isA()), - ); - expect(backend.generation, 1); - }); - - testWidgets('maps retained message and status surfaces natively', ( - tester, - ) async { - final backend = LUIFlutterBackend() - ..applyJson(''' - {"generation":1,"ops":[ - {"op":"create-node","id":1,"kind":"column"}, - {"op":"create-node","id":2,"kind":"alert"}, - {"op":"create-node","id":3,"kind":"text"}, - {"op":"create-node","id":4,"kind":"bubble"}, - {"op":"create-node","id":5,"kind":"text"}, - {"op":"create-node","id":6,"kind":"status-bar"}, - {"op":"set-prop","id":2,"property":"text","value":"Sync paused"}, - {"op":"set-prop","id":3,"property":"text","value":"Reconnect to continue"}, - {"op":"set-prop","id":4,"property":"variant","value":"primary"}, - {"op":"set-prop","id":4,"property":"text","value":"2 reactions"}, - {"op":"set-prop","id":4,"property":"text-alignment","value":"start"}, - {"op":"set-prop","id":5,"property":"text","value":"Shipped"}, - {"op":"set-prop","id":6,"property":"text","value":"3 items"}, - {"op":"insert-child","parent":1,"child":2,"index":0}, - {"op":"insert-child","parent":2,"child":3,"index":0}, - {"op":"insert-child","parent":1,"child":4,"index":1}, - {"op":"insert-child","parent":4,"child":5,"index":0}, - {"op":"insert-child","parent":1,"child":6,"index":2} - ]} - '''); - - await tester.pumpWidget( - MaterialApp(home: Scaffold(body: backend.widget(node: 1))), - ); - expect(find.text('Sync paused'), findsOneWidget); - expect(find.text('Reconnect to continue'), findsOneWidget); - expect(find.text('Shipped'), findsOneWidget); - expect(find.text('2 reactions'), findsOneWidget); - expect(find.text('3 items'), findsOneWidget); - - expect( - () => backend.applyJson(''' - {"generation":2,"ops":[ - {"op":"set-prop","id":4,"property":"gap","value":8} - ]} - '''), - throwsA(isA()), - ); - expect(backend.generation, 1); - }); - testWidgets('dropping a sheet node refreshes the semantics tree', ( - tester, - ) async { - final backend = LUIFlutterBackend() - ..applyJson(''' - {"generation":1,"ops":[ - {"op":"create-node","id":1,"kind":"column"}, - {"op":"create-node","id":2,"kind":"sheet"}, - {"op":"create-node","id":3,"kind":"text"}, - {"op":"set-prop","id":2,"property":"text","value":"Add sync graph"}, - {"op":"set-prop","id":2,"property":"accessibility-identifier","value":"sheet.graph-create"}, - {"op":"set-prop","id":3,"property":"text","value":"Sheet content"}, - {"op":"set-prop","id":3,"property":"accessibility-identifier","value":"text.in-sheet"}, - {"op":"insert-child","parent":1,"child":2,"index":0}, - {"op":"insert-child","parent":2,"child":3,"index":0} - ]} - '''); - - await tester.pumpWidget( - MaterialApp(home: Scaffold(body: backend.widget(node: 1))), - ); - await tester.pumpAndSettle(); - final semanticsHandle = tester.ensureSemantics(); - await tester.pump(); - - expect(find.byType(BottomSheet), findsOneWidget); - expect( - _collectSemanticsIdentifiers(tester), - contains('text.in-sheet'), - ); - - backend.applyJson(''' - {"generation":2,"ops":[ - {"op":"create-node","id":4,"kind":"button"}, - {"op":"set-prop","id":4,"property":"text","value":"Sidebar"}, - {"op":"set-prop","id":4,"property":"accessibility-identifier","value":"button.sidebar"}, - {"op":"remove-child","parent":2,"child":3}, - {"op":"remove-child","parent":1,"child":2}, - {"op":"drop-node","id":3}, - {"op":"drop-node","id":2}, - {"op":"insert-child","parent":1,"child":4,"index":0} - ]} - '''); - await tester.pumpAndSettle(); - await tester.pump(); - - expect(find.byType(BottomSheet), findsNothing); - final identifiers = _collectSemanticsIdentifiers(tester); - expect(identifiers, isNot(contains('sheet.graph-create'))); - expect(identifiers, isNot(contains('text.in-sheet'))); - expect(identifiers, contains('button.sidebar')); - semanticsHandle.dispose(); - }); - -} - -const _initialBatch = ''' -{"generation":1,"ops":[ - {"op":"create-node","id":1,"kind":"row"}, - {"op":"create-node","id":2,"kind":"text"}, - {"op":"create-node","id":3,"kind":"button"}, - {"op":"set-prop","id":1,"property":"gap","value":12}, - {"op":"set-prop","id":2,"property":"text","value":"Hello from LG"}, - {"op":"set-prop","id":3,"property":"text","value":"Continue"}, - {"op":"set-prop","id":3,"property":"enabled","value":true}, - {"op":"insert-child","parent":1,"child":2,"index":0}, - {"op":"insert-child","parent":1,"child":3,"index":1} -]} -'''; - -Set _collectSemanticsIdentifiers(WidgetTester tester) { - return tester.semantics - .simulatedAccessibilityTraversal(view: tester.view) - .map((node) => node.identifier) - .toSet(); -} - diff --git a/platform/web/src/lui-split.js b/platform/web/src/lui-split.js index d7d96517..4c2debca 100644 --- a/platform/web/src/lui-split.js +++ b/platform/web/src/lui-split.js @@ -25,13 +25,13 @@ const LUISplit = (() => { // checker extracts single-line literals, so keep each on one line). const fingerprints = { 'split-view': - 'lui-extension-v1|10:split-view|profiles:android/flutter,ios/flutter,ios/swiftui,linux/flutter,linux/gpui,macos/flutter,macos/gpui,macos/swiftui,web/web,windows/flutter,windows/gpui|standard-children:0|children:12:split-branch,10:split-pane|properties:17:divider-thickness:float:optional:none,24:accessibility-identifier:string:optional:none,9:animation:bool:optional:none|events:', + 'lui-extension-v1|10:split-view|profiles:ios/swiftui,linux/gpui,macos/gpui,macos/swiftui,web/web,windows/gpui|standard-children:0|children:12:split-branch,10:split-pane|properties:17:divider-thickness:float:optional:none,24:accessibility-identifier:string:optional:none,9:animation:bool:optional:none|events:', 'split-branch': - 'lui-extension-v1|12:split-branch|profiles:android/flutter,ios/flutter,ios/swiftui,linux/flutter,linux/gpui,macos/flutter,macos/gpui,macos/swiftui,web/web,windows/flutter,windows/gpui|standard-children:0|children:12:split-branch,10:split-pane|properties:11:orientation:string:required:none,5:ratio:float:required:none|events:13:ratio-changed[5:ratio:float:required]', + 'lui-extension-v1|12:split-branch|profiles:ios/swiftui,linux/gpui,macos/gpui,macos/swiftui,web/web,windows/gpui|standard-children:0|children:12:split-branch,10:split-pane|properties:11:orientation:string:required:none,5:ratio:float:required:none|events:13:ratio-changed[5:ratio:float:required]', 'split-pane': - 'lui-extension-v1|10:split-pane|profiles:android/flutter,ios/flutter,ios/swiftui,linux/flutter,linux/gpui,macos/flutter,macos/gpui,macos/swiftui,web/web,windows/flutter,windows/gpui|standard-children:0|children:9:split-tab|properties:24:accessibility-identifier:string:optional:none,7:focused:bool:optional:none,7:pane-id:string:required:none,8:selected:string:optional:none|events:10:split-drop[3:tab:string:required,4:edge:string:required,9:from-pane:string:required],10:tab-closed[3:tab:string:required],11:pane-closed[],12:pane-focused[],12:tab-selected[3:tab:string:required],15:split-requested[11:orientation:string:required],8:navigate[9:direction:string:required],9:tab-moved[3:tab:string:required,5:index:int:required,9:from-pane:string:required]', + 'lui-extension-v1|10:split-pane|profiles:ios/swiftui,linux/gpui,macos/gpui,macos/swiftui,web/web,windows/gpui|standard-children:0|children:9:split-tab|properties:24:accessibility-identifier:string:optional:none,7:focused:bool:optional:none,7:pane-id:string:required:none,8:selected:string:optional:none|events:10:split-drop[3:tab:string:required,4:edge:string:required,9:from-pane:string:required],10:tab-closed[3:tab:string:required],11:pane-closed[],12:pane-focused[],12:tab-selected[3:tab:string:required],15:split-requested[11:orientation:string:required],8:navigate[9:direction:string:required],9:tab-moved[3:tab:string:required,5:index:int:required,9:from-pane:string:required]', 'split-tab': - 'lui-extension-v1|9:split-tab|profiles:android/flutter,ios/flutter,ios/swiftui,linux/flutter,linux/gpui,macos/flutter,macos/gpui,macos/swiftui,web/web,windows/flutter,windows/gpui|standard-children:1|children:|properties:24:accessibility-identifier:string:optional:none,4:icon:string:optional:none,5:dirty:bool:optional:none,5:title:string:required:none,6:tab-id:string:required:none,8:closable:bool:optional:none|events:', + 'lui-extension-v1|9:split-tab|profiles:ios/swiftui,linux/gpui,macos/gpui,macos/swiftui,web/web,windows/gpui|standard-children:1|children:|properties:24:accessibility-identifier:string:optional:none,4:icon:string:optional:none,5:dirty:bool:optional:none,5:title:string:required:none,6:tab-id:string:required:none,8:closable:bool:optional:none|events:', }; const MIME = 'application/x-lui-split-tab'; diff --git a/schema/components.json b/schema/components.json index f26ffb7b..2cee0ec2 100644 --- a/schema/components.json +++ b/schema/components.json @@ -354,616 +354,528 @@ { "lg": "Root", "wire": "root", - "dart": "root", "swift": "root", "container": true }, { "lg": "Row", "wire": "row", - "dart": "row", "swift": "row", "container": true }, { "lg": "Column", "wire": "column", - "dart": "column", "swift": "column", "container": true }, { "lg": "Grid", "wire": "grid", - "dart": "grid", "swift": "grid", "container": true }, { "lg": "Stack", "wire": "stack", - "dart": "stack", "swift": "stack", "container": true }, { "lg": "EdgeInset", "wire": "edge-inset", - "dart": "edgeInset", "swift": "edgeInset", "container": true }, { "lg": "Overlay", "wire": "overlay", - "dart": "overlay", "swift": "overlay", "container": true }, { "lg": "ViewThatFits", "wire": "view-that-fits", - "dart": "viewThatFits", "swift": "viewThatFits", "container": true }, { "lg": "Panel", "wire": "panel", - "dart": "panel", "swift": "panel", "container": true }, { "lg": "Card", "wire": "card", - "dart": "card", "swift": "card", "container": true }, { "lg": "Alert", "wire": "alert", - "dart": "alert", "swift": "alert", "container": true }, { "lg": "Bubble", "wire": "bubble", - "dart": "bubble", "swift": "bubble", "container": true }, { "lg": "Box", "wire": "box", - "dart": "box", "swift": "box", "container": true }, { "lg": "Text", "wire": "text", - "dart": "text", "swift": "text", "container": true }, { "lg": "Heading", "wire": "heading", - "dart": "heading", "swift": "heading", "container": true }, { "lg": "Paragraph", "wire": "paragraph", - "dart": "paragraph", "swift": "paragraph", "container": true }, { "lg": "Label", "wire": "label", - "dart": "label", "swift": "label", "container": true }, { "lg": "Button", "wire": "button", - "dart": "button", "swift": "button", "container": true }, { "lg": "ToggleButton", "wire": "toggle-button", - "dart": "toggleButton", "swift": "toggleButton", "container": false }, { "lg": "Toggle", "wire": "toggle", - "dart": "toggle", "swift": "toggle", "container": false }, { "lg": "RadioGroup", "wire": "radio-group", - "dart": "radioGroup", "swift": "radioGroup", "container": true }, { "lg": "Radio", "wire": "radio", - "dart": "radio", "swift": "radio", "container": false }, { "lg": "Slider", "wire": "slider", - "dart": "slider", "swift": "slider", "container": false }, { "lg": "NumberStepper", "wire": "number-stepper", - "dart": "numberStepper", "swift": "numberStepper", "container": false }, { "lg": "TextField", "wire": "text-field", - "dart": "textField", "swift": "textField", "container": false }, { "lg": "SecureField", "wire": "secure-field", - "dart": "secureField", "swift": "secureField", "container": false }, { "lg": "Input", "wire": "input", - "dart": "input", "swift": "input", "container": false }, { "lg": "SearchField", "wire": "search-field", - "dart": "searchField", "swift": "searchField", "container": false }, { "lg": "Textarea", "wire": "textarea", - "dart": "textarea", "swift": "textarea", "container": false }, { "lg": "Checkbox", "wire": "checkbox", - "dart": "checkbox", "swift": "checkbox", "container": false }, { "lg": "SwitchControl", "wire": "switch", - "dart": "switchControl", "swift": "switchControl", "container": false }, { "lg": "Progress", "wire": "progress", - "dart": "progress", "swift": "progress", "container": false }, { "lg": "Divider", "wire": "divider", - "dart": "divider", "swift": "divider", "container": false }, { "lg": "Scroll", "wire": "scroll", - "dart": "scroll", "swift": "scroll", "container": true }, { "lg": "ListContainer", "wire": "list", - "dart": "list", "swift": "list", "container": true }, { "lg": "VirtualList", "wire": "virtual-list", - "dart": "virtualList", "swift": "virtualList", "container": true }, { "lg": "Tabs", "wire": "tabs", - "dart": "tabs", "swift": "tabs", "container": true }, { "lg": "BottomTabs", "wire": "bottom-tabs", - "dart": "bottomTabs", "swift": "bottomTabs", "container": true }, { "lg": "BottomTab", "wire": "bottom-tab", - "dart": "bottomTab", "swift": "bottomTab", "container": true }, { "lg": "ButtonGroup", "wire": "button-group", - "dart": "buttonGroup", "swift": "buttonGroup", "container": true }, { "lg": "ToggleGroup", "wire": "toggle-group", - "dart": "toggleGroup", "swift": "toggleGroup", "container": true }, { "lg": "Spacer", "wire": "spacer", - "dart": "spacer", "swift": "spacer", "container": false }, { "lg": "Spinner", "wire": "spinner", - "dart": "spinner", "swift": "spinner", "container": false }, { "lg": "Icon", "wire": "icon", - "dart": "icon", "swift": "icon", "container": false }, { "lg": "Select", "wire": "select", - "dart": "select", "swift": "select", "container": false }, { "lg": "Combobox", "wire": "combobox", - "dart": "combobox", "swift": "combobox", "container": false }, { "lg": "DropdownMenu", "wire": "dropdown-menu", - "dart": "dropdownMenu", "swift": "dropdownMenu", "container": true }, { "lg": "ContextMenu", "wire": "context-menu", - "dart": "contextMenu", "swift": "contextMenu", "container": true }, { "lg": "MenuItem", "wire": "menu-item", - "dart": "menuItem", "swift": "menuItem", "container": true }, { "lg": "MenuTrigger", "wire": "menu-trigger", - "dart": "menuTrigger", "swift": "menuTrigger", "container": true }, { "lg": "ListItem", "wire": "list-item", - "dart": "listItem", "swift": "listItem", "container": true }, { "lg": "Avatar", "wire": "avatar", - "dart": "avatar", "swift": "avatar", "container": false }, { "lg": "Image", "wire": "image", - "dart": "image", "swift": "image", "container": false }, { "lg": "MediaSurface", "wire": "media-surface", - "dart": "mediaSurface", "swift": "mediaSurface", "container": false }, { "lg": "Stepper", "wire": "stepper", - "dart": "stepper", "swift": "stepper", "container": true }, { "lg": "Step", "wire": "step", - "dart": "step", "swift": "step", "container": false }, { "lg": "Timeline", "wire": "timeline", - "dart": "timeline", "swift": "timeline", "container": true }, { "lg": "TimelineItem", "wire": "timeline-item", - "dart": "timelineItem", "swift": "timelineItem", "container": false }, { "lg": "InputGroup", "wire": "input-group", - "dart": "inputGroup", "swift": "inputGroup", "container": true }, { "lg": "InputGroupActions", "wire": "input-group-actions", - "dart": "inputGroupActions", "swift": "inputGroupActions", "container": true }, { "lg": "Breadcrumb", "wire": "breadcrumb", - "dart": "breadcrumb", "swift": "breadcrumb", "container": true }, { "lg": "Pagination", "wire": "pagination", - "dart": "pagination", "swift": "pagination", "container": true }, { "lg": "Accordion", "wire": "accordion", - "dart": "accordion", "swift": "accordion", "container": true }, { "lg": "Table", "wire": "table", - "dart": "table", "swift": "table", "container": true }, { "lg": "TableRow", "wire": "table-row", - "dart": "tableRow", "swift": "tableRow", "container": true }, { "lg": "TableCell", "wire": "table-cell", - "dart": "tableCell", "swift": "tableCell", "container": false }, { "lg": "Tree", "wire": "tree", - "dart": "tree", "swift": "tree", "container": true }, { "lg": "Resizable", "wire": "resizable", - "dart": "resizable", "swift": "resizable", "container": true }, { "lg": "Split", "wire": "split", - "dart": "split", "swift": "split", "container": true }, { "lg": "Dialog", "wire": "dialog", - "dart": "dialog", "swift": "dialog", "container": true }, { "lg": "Drawer", "wire": "drawer", - "dart": "drawer", "swift": "drawer", "container": true }, { "lg": "Sheet", "wire": "sheet", - "dart": "sheet", "swift": "sheet", "container": true }, { "lg": "Tooltip", "wire": "tooltip", - "dart": "tooltip", "swift": "tooltip", "container": false }, { "lg": "Toast", "wire": "toast", - "dart": "toast", "swift": "toast", "container": true }, { "lg": "Toolbar", "wire": "toolbar", - "dart": "toolbar", "swift": "toolbar", "container": true }, { "lg": "StatusBar", "wire": "status-bar", - "dart": "statusBar", "swift": "statusBar", "container": false }, { "lg": "ListSection", "wire": "list-section", - "dart": "listSection", "swift": "listSection", "container": true }, { "lg": "ListSectionHeader", "wire": "list-section-header", - "dart": "listSectionHeader", "swift": "listSectionHeader", "container": true }, { "lg": "ListSectionFooter", "wire": "list-section-footer", - "dart": "listSectionFooter", "swift": "listSectionFooter", "container": true }, { "lg": "SwipeActions", "wire": "swipe-actions", - "dart": "swipeActions", "swift": "swipeActions", "container": true }, { "lg": "SwipeAction", "wire": "swipe-action", - "dart": "swipeAction", "swift": "swipeAction", "container": false }, { "lg": "FilePicker", "wire": "file-picker", - "dart": "filePicker", "swift": "filePicker", "container": true }, { "lg": "Kbd", "wire": "kbd", - "dart": "kbd", "swift": "kbd", "container": false }, { "lg": "Link", "wire": "link", - "dart": "link", "swift": "link", "container": true }, { "lg": "FileImage", "wire": "file-image", - "dart": "fileImage", "swift": "fileImage", "container": false }, { "lg": "FilePreview", "wire": "file-preview", - "dart": "filePreview", "swift": "filePreview", "container": false }, { "lg": "Br", "wire": "br", - "dart": "br", "swift": "br", "container": false }, { "lg": "Popover", "wire": "popover", - "dart": "popover", "swift": "popover", "container": true } diff --git a/src/lui_element_combine.ml b/src/lui_element_combine.ml index 20ccba1f..5897526a 100644 --- a/src/lui_element_combine.ml +++ b/src/lui_element_combine.ml @@ -141,17 +141,10 @@ let composer_send_button context ?send_icon send_disabled on_send : t = let apple_icon = Option.value send_icon ~default:(`arrow_up : icon) in if Lui_ui.platform context = Lui_protocol.AndroidOS then - if Lui_ui.host context = Lui_protocol.FlutterHost - then - button - ~icon:android_icon ~variant:`primary ~size:`icon ~width:48 ~height:48 - ~label:"Send" ~accessibility_identifier:"button.send" - ?disabled_signal:send_disabled ~on_press:on_send [] - else - button - ~icon:android_icon ~variant:`primary ~label:"Send" - ~accessibility_identifier:"button.send" ?disabled_signal:send_disabled - ~on_press:on_send ~text:"Send" [] + button + ~icon:android_icon ~variant:`primary ~label:"Send" + ~accessibility_identifier:"button.send" ?disabled_signal:send_disabled + ~on_press:on_send ~text:"Send" [] else button ~icon:apple_icon ~variant:`ghost ~width:36 ~height:36 @@ -185,7 +178,6 @@ let composer : t = fun context parent -> - let flutter = Lui_ui.host context = Lui_protocol.FlutterHost in let attachment_strip = match attachments with | None -> [] @@ -223,9 +215,9 @@ let composer ?key ?accessibility_identifier ~style_class:"composer-surface" ~min_height:58 ~gap:0 - ~padding_horizontal:(if flutter then 12 else 16) - ~padding_vertical:(if flutter then 12 else 8) - ~background:(if flutter then "surface-container-high" else "glass") + ~padding_horizontal:16 + ~padding_vertical:8 + ~background:"glass" ~corner_radius:24 ([ box ~height:6 ~accessibility_identifier:"spacer.composer.top" [] ] @ attachment_strip @@ -296,19 +288,12 @@ let composer_collapsed : t = fun context parent -> - if Lui_ui.host context = Lui_protocol.FlutterHost - then - (button - ?key ?accessibility_identifier ~variant:`secondary ?icon ~grow:1.0 - ~height:58 ~padding_horizontal:20 ~label ~text:label ~on_press []) - context parent - else - (button - ?key ?accessibility_identifier ~variant:`ghost ~grow:1.0 ~height:58 - ~padding_horizontal:30 ~background:"glass" - ~foreground:"muted-foreground" ~corner_radius:999 ?icon ~text:label - ~on_press []) - context parent + (button + ?key ?accessibility_identifier ~variant:`ghost ~grow:1.0 ~height:58 + ~padding_horizontal:30 ~background:"glass" + ~foreground:"muted-foreground" ~corner_radius:999 ?icon ~text:label + ~on_press []) + context parent ;; (* ------------------------------------------------------------------ *) diff --git a/src/lui_extension.ml b/src/lui_extension.ml index 4afd8e07..a087f10b 100644 --- a/src/lui_extension.ml +++ b/src/lui_extension.ml @@ -158,7 +158,6 @@ let host_name host = | GenericHost -> "generic" | WebHost -> "web" | SwiftUIHost -> "swiftui" - | FlutterHost -> "flutter" | GPUIHost -> "gpui" let token value = string_of_int (String.length value) ^ ":" ^ value diff --git a/src/lui_extension_check.ml b/src/lui_extension_check.ml index 40c97478..f3e5216c 100644 --- a/src/lui_extension_check.ml +++ b/src/lui_extension_check.ml @@ -1,7 +1,7 @@ (* Extension fingerprint drift check. - Host extension registries (Swift [LUIAppleExtension]/[LUIAppleTweak], - Dart [LUIFlutterExtension]/[LUIFlutterTweak]) carry a hand-mirrored + Host extension registries (Swift [LUIAppleExtension]/[LUIAppleTweak]) + carry a hand-mirrored [fingerprint] string literal for every registered schema. The literal covers the full OCaml-declared schema — including the supported platform profiles, which the host does not know — so it cannot be recomputed on diff --git a/src/lui_protocol.ml b/src/lui_protocol.ml index 7c204d43..8379e6aa 100644 --- a/src/lui_protocol.ml +++ b/src/lui_protocol.ml @@ -106,7 +106,6 @@ type host_kind = | GenericHost | WebHost | SwiftUIHost - | FlutterHost | GPUIHost type platform_profile = { diff --git a/src/lui_protocol.mli b/src/lui_protocol.mli index 8e21822f..c40775fe 100644 --- a/src/lui_protocol.mli +++ b/src/lui_protocol.mli @@ -104,7 +104,6 @@ type host_kind = | GenericHost | WebHost | SwiftUIHost - | FlutterHost | GPUIHost type platform_profile = { diff --git a/src/lui_split.ml b/src/lui_split.ml index 2a6d6f5d..74a05104 100644 --- a/src/lui_split.ml +++ b/src/lui_split.ml @@ -21,11 +21,6 @@ let profiles = [ { profile_os = MacOS; profile_host = SwiftUIHost }; { profile_os = IOS; profile_host = SwiftUIHost }; - { profile_os = AndroidOS; profile_host = FlutterHost }; - { profile_os = IOS; profile_host = FlutterHost }; - { profile_os = LinuxOS; profile_host = FlutterHost }; - { profile_os = MacOS; profile_host = FlutterHost }; - { profile_os = WindowsOS; profile_host = FlutterHost }; { profile_os = MacOS; profile_host = GPUIHost }; { profile_os = LinuxOS; profile_host = GPUIHost }; { profile_os = WindowsOS; profile_host = GPUIHost }; diff --git a/test/test_lui.ml b/test/test_lui.ml index 7ae89214..1c0065fa 100644 --- a/test/test_lui.ml +++ b/test/test_lui.ml @@ -1417,7 +1417,7 @@ let test_fingerprint_format () = "lui-extension-v1|16:apple-map-marker|profiles:ios/swiftui,linux/gpui,macos/gpui,macos/swiftui,windows/gpui|standard-children:0|children:|properties:5:title:string:required:none,8:latitude:float:required:none,9:longitude:float:required:none|events:" (Lui_extension.fingerprint (gallery_schema "apple-map-marker")); Alcotest.(check string) "tweak fingerprint" - "lui-tweak-v1|14:gallery-accent|profiles:android/flutter,ios/flutter,ios/swiftui,linux/flutter,linux/gpui,macos/flutter,macos/gpui,macos/swiftui,web/web,windows/flutter,windows/gpui|properties:" + "lui-tweak-v1|14:gallery-accent|profiles:ios/swiftui,linux/gpui,macos/gpui,macos/swiftui,web/web,windows/gpui|properties:" (Lui_extension.tweak_fingerprint (gallery_schema "gallery-accent")) let test_host_literals_in_sync () = @@ -1458,21 +1458,17 @@ let test_drift_is_caught () = Alcotest.(check string) "undeclared identifier" "fake-map" declaration.host_identifier | _ -> Alcotest.fail "expected exactly one undeclared identifier"); - (* Dart host registrations use single-quoted literals: same extraction. *) - let dart_source = - "LUIFlutterExtension(\n\ - \ identifier: 'apple-map',\n\ - \ fingerprint: 'lui-extension-v1|9:apple-map|profiles:ios/swiftui,linux/gpui,macos/gpui,macos/swiftui,windows/gpui|standard-children:1|children:16:apple-map-marker|properties:14:latitude-delta:float:required:none,15:longitude-delta:float:required:none,8:latitude:float:required:none,9:longitude:float:required:none|events:',\n\ - \ builder: (_) => const SizedBox.shrink(),\n\ - )" - in - match Lui_extension_check.check_registry registry dart_source with + (* Single-quoted literals are extracted the same way. *) + let quoted_source = + "fingerprint: 'lui-extension-v1|9:apple-map|profiles:ios/swiftui,linux/gpui,macos/gpui,macos/swiftui,windows/gpui|standard-children:1|children:16:apple-map-marker|properties:14:latitude-delta:float:required:none,15:longitude-delta:float:required:none,8:latitude:float:required:none,9:longitude:float:required:none|events:'" + in + match Lui_extension_check.check_registry registry quoted_source with | [ Lui_extension_check.Drifted { declaration; expected } ] -> - Alcotest.(check string) "dart identifier" "apple-map" + Alcotest.(check string) "quoted identifier" "apple-map" declaration.host_identifier; Alcotest.(check bool) "expected has standard-children:0" true (contains expected "standard-children:0") - | _ -> Alcotest.fail "expected exactly one drifted Dart fingerprint" + | _ -> Alcotest.fail "expected exactly one drifted single-quoted fingerprint" let test_menu_trigger_rules () = @@ -2527,16 +2523,16 @@ let split_fingerprint identifier = let test_split_fingerprints () = Alcotest.(check string) "split-view fingerprint" - "lui-extension-v1|10:split-view|profiles:android/flutter,ios/flutter,ios/swiftui,linux/flutter,linux/gpui,macos/flutter,macos/gpui,macos/swiftui,web/web,windows/flutter,windows/gpui|standard-children:0|children:12:split-branch,10:split-pane|properties:17:divider-thickness:float:optional:none,24:accessibility-identifier:string:optional:none,9:animation:bool:optional:none|events:" + "lui-extension-v1|10:split-view|profiles:ios/swiftui,linux/gpui,macos/gpui,macos/swiftui,web/web,windows/gpui|standard-children:0|children:12:split-branch,10:split-pane|properties:17:divider-thickness:float:optional:none,24:accessibility-identifier:string:optional:none,9:animation:bool:optional:none|events:" (split_fingerprint "split-view"); Alcotest.(check string) "split-branch fingerprint" - "lui-extension-v1|12:split-branch|profiles:android/flutter,ios/flutter,ios/swiftui,linux/flutter,linux/gpui,macos/flutter,macos/gpui,macos/swiftui,web/web,windows/flutter,windows/gpui|standard-children:0|children:12:split-branch,10:split-pane|properties:11:orientation:string:required:none,5:ratio:float:required:none|events:13:ratio-changed[5:ratio:float:required]" + "lui-extension-v1|12:split-branch|profiles:ios/swiftui,linux/gpui,macos/gpui,macos/swiftui,web/web,windows/gpui|standard-children:0|children:12:split-branch,10:split-pane|properties:11:orientation:string:required:none,5:ratio:float:required:none|events:13:ratio-changed[5:ratio:float:required]" (split_fingerprint "split-branch"); Alcotest.(check string) "split-pane fingerprint" - "lui-extension-v1|10:split-pane|profiles:android/flutter,ios/flutter,ios/swiftui,linux/flutter,linux/gpui,macos/flutter,macos/gpui,macos/swiftui,web/web,windows/flutter,windows/gpui|standard-children:0|children:9:split-tab|properties:24:accessibility-identifier:string:optional:none,7:focused:bool:optional:none,7:pane-id:string:required:none,8:selected:string:optional:none|events:10:split-drop[3:tab:string:required,4:edge:string:required,9:from-pane:string:required],10:tab-closed[3:tab:string:required],11:pane-closed[],12:pane-focused[],12:tab-selected[3:tab:string:required],15:split-requested[11:orientation:string:required],8:navigate[9:direction:string:required],9:tab-moved[3:tab:string:required,5:index:int:required,9:from-pane:string:required]" + "lui-extension-v1|10:split-pane|profiles:ios/swiftui,linux/gpui,macos/gpui,macos/swiftui,web/web,windows/gpui|standard-children:0|children:9:split-tab|properties:24:accessibility-identifier:string:optional:none,7:focused:bool:optional:none,7:pane-id:string:required:none,8:selected:string:optional:none|events:10:split-drop[3:tab:string:required,4:edge:string:required,9:from-pane:string:required],10:tab-closed[3:tab:string:required],11:pane-closed[],12:pane-focused[],12:tab-selected[3:tab:string:required],15:split-requested[11:orientation:string:required],8:navigate[9:direction:string:required],9:tab-moved[3:tab:string:required,5:index:int:required,9:from-pane:string:required]" (split_fingerprint "split-pane"); Alcotest.(check string) "split-tab fingerprint" - "lui-extension-v1|9:split-tab|profiles:android/flutter,ios/flutter,ios/swiftui,linux/flutter,linux/gpui,macos/flutter,macos/gpui,macos/swiftui,web/web,windows/flutter,windows/gpui|standard-children:1|children:|properties:24:accessibility-identifier:string:optional:none,4:icon:string:optional:none,5:dirty:bool:optional:none,5:title:string:required:none,6:tab-id:string:required:none,8:closable:bool:optional:none|events:" + "lui-extension-v1|9:split-tab|profiles:ios/swiftui,linux/gpui,macos/gpui,macos/swiftui,web/web,windows/gpui|standard-children:1|children:|properties:24:accessibility-identifier:string:optional:none,4:icon:string:optional:none,5:dirty:bool:optional:none,5:title:string:required:none,6:tab-id:string:required:none,8:closable:bool:optional:none|events:" (split_fingerprint "split-tab") (* Every host source that carries lui-extension-v1 literals is checked so @@ -2546,7 +2542,6 @@ let test_split_fingerprints () = let split_host_sources () = let root = source_root () in [ "platform/apple/Sources/LUIAppleBackend/LUISplit.swift"; - "platform/flutter/lib/lui_flutter_split.dart"; "platform/web/src/lui-split.js" ] |> List.filter_map (fun rel -> let path = Filename.concat root rel in @@ -3085,8 +3080,8 @@ let test_navigation_fallback () = Alcotest.(check int) "no-animation fallback cleans immediately" 2 !disposes; Alcotest.(check int) "fallback programmatic path does not echo" 1 !callbacks; ignore (Lui_app.dispose app)) - [generic_profile (); profile WebOS WebHost; profile AndroidOS FlutterHost; - profile IOS FlutterHost; profile LinuxOS GPUIHost; profile WindowsOS GPUIHost] + [generic_profile (); profile WebOS WebHost; + profile LinuxOS GPUIHost; profile WindowsOS GPUIHost] let test_navigation_queued_stale () = let app, _, _, _, _, callbacks, _ = navigation_fixture () in diff --git a/tooling/generate_component_schema.mjs b/tooling/generate_component_schema.mjs index a0c3db0c..cbb819ec 100644 --- a/tooling/generate_component_schema.mjs +++ b/tooling/generate_component_schema.mjs @@ -38,7 +38,6 @@ function validate(schema) { unique(schema.publicElements, 'name', 'public element name'); unique(schema.nodeKinds, 'lg', 'node-kind LG name'); unique(schema.nodeKinds, 'wire', 'node-kind wire name'); - unique(schema.nodeKinds, 'dart', 'node-kind Dart name'); unique(schema.nodeKinds, 'swift', 'node-kind Swift name'); unique(schema.properties, 'lg', 'property LG name'); unique(schema.properties, 'wire', 'property wire name'); @@ -125,7 +124,6 @@ type host_kind = | GenericHost | WebHost | SwiftUIHost - | FlutterHost | GPUIHost type platform_profile = { @@ -348,29 +346,6 @@ ${extraEntries} `; } -function renderDart(schema) { - const enumCases = schema.nodeKinds.map(({ dart }) => ` ${dart},`).join('\n'); - const decoderCases = schema.nodeKinds - .map(({ dart, wire }) => ` '${wire}' => _NodeKind.${dart},`) - .join('\n'); - return `${generatedHeader('//')}part of 'lui_flutter_backend.dart'; - -enum _NodeKind { -${enumCases} -} - -_NodeKind _decodeNodeKind(Object? value) { - if (value is! String) { - throw const LUIBackendException('kind must be a string'); - } - return switch (value) { -${decoderCases} - _ => throw const LUIBackendException('unknown node kind'), - }; -} -`; -} - function renderRust(schema) { const kindLg = new Map(schema.nodeKinds.map(({ wire, lg }) => [wire, lg])); const propLg = new Map(schema.properties.map(({ wire, lg }) => [wire, lg])); @@ -492,7 +467,6 @@ function artifacts(schema) { ['src/lui_wire_schema.ml', renderOCamlWire(schema)], ['src/lui_wire_schema.mli', renderOCamlWireSignature()], ['platform/apple/Sources/LUIAppleBackend/LUIWireSchema.swift', renderSwift(schema)], - ['platform/flutter/lib/lui_wire_schema.g.dart', renderDart(schema)], ['platform/gpui/crates/lui-core/src/wire_schema.rs', renderRust(schema)], ]); } diff --git a/tooling/generate_extension_api.mjs b/tooling/generate_extension_api.mjs index 206ca399..4a33bfd2 100644 --- a/tooling/generate_extension_api.mjs +++ b/tooling/generate_extension_api.mjs @@ -33,7 +33,7 @@ // // kind: "string" | "bool" | "int" | "float" // os: "generic" | "web" | "macos" | "ios" | "android" | "linux" | "windows" -// host: "generic" | "web" | "swiftui" | "flutter" | "gpui" +// host: "generic" | "web" | "swiftui" | "gpui" // // Usage: node tooling/generate_extension_api.mjs --schema \ // --out [--mli ] @@ -65,7 +65,7 @@ const osNames = { const hostNames = { generic: 'GenericHost', web: 'WebHost', swiftui: 'SwiftUIHost', - flutter: 'FlutterHost', gpui: 'GPUIHost', + gpui: 'GPUIHost', }; const munge = (name) => name.replace(/-/g, '_'); diff --git a/tooling/mobile/build_components_android.sh b/tooling/mobile/build_components_android.sh index 2375f3c6..0181ba4d 100755 --- a/tooling/mobile/build_components_android.sh +++ b/tooling/mobile/build_components_android.sh @@ -19,7 +19,7 @@ opam_root=$(opam var root --safe) shared_root=${LG_OCAML_TOOLCHAIN_ROOT:-${LUI_MOBILE_TOOLCHAIN_ROOT:-$opam_root/lg-ocaml-toolchains}} target_prefix=${LG_ANDROID_OCAML_PREFIX:-$shared_root/ocaml-$ocaml_version/targets/$target} build_dir="$repo_root/_build/mobile-components/android/$android_abi" -jni_library="$repo_root/examples/components/flutter/android/app/src/main/jniLibs/$android_abi/liblui_components.so" +jni_library="$repo_root/examples/components/android/app/src/main/jniLibs/$android_abi/liblui_components.so" [[ -x $target_prefix/bin/ocamlopt.opt ]] || { echo "error: shared Android OCaml toolchain is missing: $target_prefix" >&2 @@ -68,7 +68,7 @@ fi --target="$target" \ -fPIC \ -I "$target_prefix/lib/ocaml" \ - -c "$repo_root/platform/flutter/native/lui_ocaml_bridge.c" \ + -c "$repo_root/platform/native/lui_ocaml_bridge.c" \ -o "$build_dir/lui_ocaml_bridge.o" "$ndk_bin/clang" \ diff --git a/tooling/mobile/build_components_ios_simulator.sh b/tooling/mobile/build_components_ios_simulator.sh index 63a3203e..aa5067d0 100755 --- a/tooling/mobile/build_components_ios_simulator.sh +++ b/tooling/mobile/build_components_ios_simulator.sh @@ -60,7 +60,7 @@ fi -isysroot "$sdk_path" \ -fPIC \ -I "$target_prefix/lib/ocaml" \ - -c "$repo_root/platform/flutter/native/lui_ocaml_bridge.c" \ + -c "$repo_root/platform/native/lui_ocaml_bridge.c" \ -o "$build_dir/lui_ocaml_bridge.o" native_fingerprint=$(shasum -a 256 \ diff --git a/tooling/mobile/build_components_lg.sh b/tooling/mobile/build_components_lg.sh index 370212e6..3f32ecef 100755 --- a/tooling/mobile/build_components_lg.sh +++ b/tooling/mobile/build_components_lg.sh @@ -9,6 +9,5 @@ opam exec -- dune build -j 1 lg/lui/lui_native.state lg mobile build "$@" \ --from "$repo_root/_build/default/lg/lui/lui_native.state" \ "$repo_root/lg/lui/backend/retained" \ - "$repo_root/lg/lui/backend/flutter" \ "$repo_root/examples/components/lg/components" \ "$repo_root/examples/components/native" diff --git a/tooling/mobile/build_components_mobile.sh b/tooling/mobile/build_components_mobile.sh index 3ec046aa..6708a293 100755 --- a/tooling/mobile/build_components_mobile.sh +++ b/tooling/mobile/build_components_mobile.sh @@ -21,6 +21,6 @@ LUI_GALLERY_OCAML_OBJECT="$android_root/android/arm64-v8a/mobile_app_complete.o" "$repo_root/tooling/mobile/build_components_android.sh" ( - cd "$repo_root/examples/components/flutter" - flutter build apk --debug --target-platform android-arm64 + cd "$repo_root/examples/components/android" + ./gradlew :app:assembleDebug ) diff --git a/tooling/mobile/qualify_components_android_release.sh b/tooling/mobile/qualify_components_android_release.sh index 649a59f5..b12823fa 100755 --- a/tooling/mobile/qualify_components_android_release.sh +++ b/tooling/mobile/qualify_components_android_release.sh @@ -5,7 +5,7 @@ set -euo pipefail repo_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd) qualification_dir="$repo_root/_build/mobile-components/android-release" keystore="$qualification_dir/qualification.jks" -bundle_source="$repo_root/examples/components/flutter/build/app/outputs/bundle/release/app-release.aab" +bundle_source="$repo_root/examples/components/android/app/build/outputs/bundle/release/app-release.aab" bundle="$qualification_dir/lui-components-release.aab" alias=lui-release-qualification password=lui-release-qualification @@ -28,12 +28,12 @@ fi "$repo_root/tooling/mobile/build_components_android.sh" >/dev/null ( - cd "$repo_root/examples/components/flutter" + cd "$repo_root/examples/components/android" LUI_ANDROID_KEYSTORE="$keystore" \ LUI_ANDROID_KEY_ALIAS="$alias" \ LUI_ANDROID_STORE_PASSWORD="$password" \ LUI_ANDROID_KEY_PASSWORD="$password" \ - flutter build appbundle --release --target-platform android-arm64 + ./gradlew :app:bundleRelease ) cp "$bundle_source" "$bundle" diff --git a/tooling/mobile/test_components_android_e2e.sh b/tooling/mobile/test_components_android_e2e.sh index 3d0e67ec..8631a6d2 100755 --- a/tooling/mobile/test_components_android_e2e.sh +++ b/tooling/mobile/test_components_android_e2e.sh @@ -4,7 +4,7 @@ set -euo pipefail repo_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd) app_id=dev.lui.components -apk_path="$repo_root/examples/components/flutter/build/app/outputs/flutter-apk/app-debug.apk" +apk_path="$repo_root/examples/components/android/app/build/outputs/apk/debug/app-debug.apk" flow="$repo_root/.maestro/android-components-interactions.yaml" screenshots_dir="$repo_root/_build/mobile-components/android-e2e" maestro_bin=${MAESTRO_BIN:-$(command -v maestro || true)} @@ -33,8 +33,8 @@ adb -s "$device" get-state >/dev/null \ if [[ ${LUI_ANDROID_E2E_SKIP_BUILD:-0} != 1 ]]; then "$repo_root/tooling/mobile/build_components_android.sh" >/dev/null ( - cd "$repo_root/examples/components/flutter" - flutter build apk --debug --target-platform android-arm64 + cd "$repo_root/examples/components/android" + ./gradlew :app:assembleDebug ) fi [[ -f $apk_path ]] || die "Android Gallery APK is missing: $apk_path" diff --git a/tooling/test/component_schema_test.mjs b/tooling/test/component_schema_test.mjs index 1da86e5b..94b84a02 100644 --- a/tooling/test/component_schema_test.mjs +++ b/tooling/test/component_schema_test.mjs @@ -129,14 +129,12 @@ test('Bottom Tabs reserve distinct retained navigation node kinds', () => { { lg: 'BottomTabs', wire: 'bottom-tabs', - dart: 'bottomTabs', swift: 'bottomTabs', container: true, }, { lg: 'BottomTab', wire: 'bottom-tab', - dart: 'bottomTab', swift: 'bottomTab', container: true, }, @@ -153,7 +151,6 @@ test('schema reserves one internal transparent root node', () => { assert.deepEqual(root, { lg: 'Root', wire: 'root', - dart: 'root', swift: 'root', container: true, }); @@ -176,14 +173,12 @@ test('schema validation rejects duplicate wire names before generation', () => { { lg: 'First', wire: 'duplicate', - dart: 'first', swift: 'first', container: false, }, { lg: 'Second', wire: 'duplicate', - dart: 'second', swift: 'second', container: false, }, From 294389c623236261e4def9df0ed9ec954aa6cd33 Mon Sep 17 00:00:00 2001 From: Tienson Qin Date: Tue, 6 Oct 2026 09:57:41 +0000 Subject: [PATCH 2/5] feat(android): Kotlin/Jetpack Compose backend skeleton Wire-schema codegen (renderKotlin) emits LuiWireSchema.kt; the dev.lui library carries the wire parser, retained store + validation, per-kind Compose renderers, Material-3 theme token mapping, JNI bridge (lui_jni_bridge.c + LuiBridge), and the gallery app shell at examples/components/android (appId dev.lui.components). Screenshot tests and the KotlinHost wiring still follow. --- .gitignore | 7 + .../components/android/app/build.gradle.kts | 70 + .../android/app/src/main/AndroidManifest.xml | 21 + .../android/app/src/main/cpp/CMakeLists.txt | 35 + .../app/src/main/cpp/lui_jni_bridge_stub.c | 91 + .../java/dev/lui/components/MainActivity.kt | 93 + examples/components/android/build.gradle.kts | 8 + examples/components/android/gradle.properties | 4 + .../components/android/settings.gradle.kts | 20 + platform/android/build.gradle.kts | 7 + platform/android/gradle.properties | 6 + .../android/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43583 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 + platform/android/gradlew | 252 +++ platform/android/gradlew.bat | 94 + platform/android/lui/build.gradle.kts | 64 + .../android/lui/src/main/AndroidManifest.xml | 1 + .../android/lui/src/main/cpp/lui_jni_bridge.c | 442 ++++ .../lui/src/main/kotlin/dev/lui/LuiBackend.kt | 100 + .../lui/src/main/kotlin/dev/lui/LuiBridge.kt | 293 +++ .../lui/src/main/kotlin/dev/lui/LuiEvents.kt | 47 + .../src/main/kotlin/dev/lui/LuiExtension.kt | 153 ++ .../lui/src/main/kotlin/dev/lui/LuiIcons.kt | 80 + .../src/main/kotlin/dev/lui/LuiKindRules.kt | 746 +++++++ .../src/main/kotlin/dev/lui/LuiNodeView.kt | 1795 +++++++++++++++++ .../lui/src/main/kotlin/dev/lui/LuiRender.kt | 289 +++ .../lui/src/main/kotlin/dev/lui/LuiStore.kt | 403 ++++ .../lui/src/main/kotlin/dev/lui/LuiTheme.kt | 201 ++ .../lui/src/main/kotlin/dev/lui/LuiWire.kt | 167 ++ .../src/main/kotlin/dev/lui/LuiWireSchema.kt | 258 +++ platform/android/settings.gradle.kts | 18 + tooling/generate_component_schema.mjs | 66 + 32 files changed, 5838 insertions(+) create mode 100644 examples/components/android/app/build.gradle.kts create mode 100644 examples/components/android/app/src/main/AndroidManifest.xml create mode 100644 examples/components/android/app/src/main/cpp/CMakeLists.txt create mode 100644 examples/components/android/app/src/main/cpp/lui_jni_bridge_stub.c create mode 100644 examples/components/android/app/src/main/java/dev/lui/components/MainActivity.kt create mode 100644 examples/components/android/build.gradle.kts create mode 100644 examples/components/android/gradle.properties create mode 100644 examples/components/android/settings.gradle.kts create mode 100644 platform/android/build.gradle.kts create mode 100644 platform/android/gradle.properties create mode 100644 platform/android/gradle/wrapper/gradle-wrapper.jar create mode 100644 platform/android/gradle/wrapper/gradle-wrapper.properties create mode 100755 platform/android/gradlew create mode 100644 platform/android/gradlew.bat create mode 100644 platform/android/lui/build.gradle.kts create mode 100644 platform/android/lui/src/main/AndroidManifest.xml create mode 100644 platform/android/lui/src/main/cpp/lui_jni_bridge.c create mode 100644 platform/android/lui/src/main/kotlin/dev/lui/LuiBackend.kt create mode 100644 platform/android/lui/src/main/kotlin/dev/lui/LuiBridge.kt create mode 100644 platform/android/lui/src/main/kotlin/dev/lui/LuiEvents.kt create mode 100644 platform/android/lui/src/main/kotlin/dev/lui/LuiExtension.kt create mode 100644 platform/android/lui/src/main/kotlin/dev/lui/LuiIcons.kt create mode 100644 platform/android/lui/src/main/kotlin/dev/lui/LuiKindRules.kt create mode 100644 platform/android/lui/src/main/kotlin/dev/lui/LuiNodeView.kt create mode 100644 platform/android/lui/src/main/kotlin/dev/lui/LuiRender.kt create mode 100644 platform/android/lui/src/main/kotlin/dev/lui/LuiStore.kt create mode 100644 platform/android/lui/src/main/kotlin/dev/lui/LuiTheme.kt create mode 100644 platform/android/lui/src/main/kotlin/dev/lui/LuiWire.kt create mode 100644 platform/android/lui/src/main/kotlin/dev/lui/LuiWireSchema.kt create mode 100644 platform/android/settings.gradle.kts diff --git a/.gitignore b/.gitignore index 7292412a..3c61eaa2 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,10 @@ /platform/web/dist/ /platform/gpui/target/ + +# Android / Gradle build outputs +.gradle/ +local.properties +/platform/android/**/build/ +/examples/components/android/**/build/ +/examples/components/android/app/src/main/jniLibs/ diff --git a/examples/components/android/app/build.gradle.kts b/examples/components/android/app/build.gradle.kts new file mode 100644 index 00000000..75ae84ec --- /dev/null +++ b/examples/components/android/app/build.gradle.kts @@ -0,0 +1,70 @@ +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") + id("org.jetbrains.kotlin.plugin.compose") +} + +val ocamlInclude = providers.gradleProperty("lui.ocaml.include") + .orElse(providers.environmentVariable("LUI_OCAML_INCLUDE")) + .orElse( + providers.environmentVariable("LUI_MOBILE_TOOLCHAIN_ROOT") + .map { "$it/lib/ocaml" }, + ) + .orElse( + providers.environmentVariable("LG_ANDROID_OCAML_PREFIX") + .map { "$it/lib/ocaml" }, + ) + .orNull + +android { + namespace = "dev.lui.components" + compileSdk = 36 + + defaultConfig { + applicationId = "dev.lui.components" + minSdk = 24 + targetSdk = 36 + versionCode = 1 + versionName = "1.0" + + externalNativeBuild { + cmake { + arguments += listOfNotNull( + ocamlInclude?.let { "-DLUI_OCAML_INCLUDE=$it" }, + ) + abiFilters += listOf("arm64-v8a", "x86_64") + } + } + } + + externalNativeBuild { + cmake { + path = file("src/main/cpp/CMakeLists.txt") + version = "3.22.1" + } + } + + buildFeatures { + compose = true + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlin { + compilerOptions { + jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17) + } + } +} + +dependencies { + implementation(project(":lui")) + implementation(platform("androidx.compose:compose-bom:2024.10.01")) + implementation("androidx.compose.ui:ui") + implementation("androidx.compose.material3:material3") + implementation("androidx.compose.material:material-icons-extended") + implementation("androidx.activity:activity-compose:1.9.3") +} diff --git a/examples/components/android/app/src/main/AndroidManifest.xml b/examples/components/android/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..e7f2c200 --- /dev/null +++ b/examples/components/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + diff --git a/examples/components/android/app/src/main/cpp/CMakeLists.txt b/examples/components/android/app/src/main/cpp/CMakeLists.txt new file mode 100644 index 00000000..f7e61c06 --- /dev/null +++ b/examples/components/android/app/src/main/cpp/CMakeLists.txt @@ -0,0 +1,35 @@ +cmake_minimum_required(VERSION 3.22.1) +project(lui_components_bridge C) + +# Compiles the JNI bridge into liblui_jni_bridge.so and links it against +# the OCaml runtime packaged by tooling/mobile/build_components_android.sh +# as jniLibs//liblui_components.so. +# +# LUI_OCAML_INCLUDE must point at the OCaml headers (/lib/ocaml) +# from the lg Android toolchain, and the liblui_components.so for this ABI +# must already exist under jniLibs. When either is missing we compile the +# stub exports instead so the app still builds and launches (the bridge +# reports "not accepted" and the gallery shows a placeholder). + +set(LUI_BACKEND_DIR "${CMAKE_SOURCE_DIR}/../../../../../../platform/android/lui") +set(LUI_COMPONENTS_SO "${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI}/liblui_components.so") + +find_library(log_lib log) + +if(LUI_OCAML_INCLUDE AND EXISTS "${LUI_COMPONENTS_SO}") + add_library(lui_jni_bridge SHARED + "${LUI_BACKEND_DIR}/src/main/cpp/lui_jni_bridge.c") + target_include_directories(lui_jni_bridge PRIVATE "${LUI_OCAML_INCLUDE}") + + add_library(lui_components SHARED IMPORTED) + set_target_properties(lui_components PROPERTIES + IMPORTED_LOCATION "${LUI_COMPONENTS_SO}") + target_link_libraries(lui_jni_bridge lui_components ${log_lib}) +else() + message(WARNING + "OCaml toolchain or liblui_components.so missing for ${ANDROID_ABI}; " + "building stub LuiBridge exports") + add_library(lui_jni_bridge SHARED + "${CMAKE_SOURCE_DIR}/lui_jni_bridge_stub.c") + target_link_libraries(lui_jni_bridge ${log_lib}) +endif() diff --git a/examples/components/android/app/src/main/cpp/lui_jni_bridge_stub.c b/examples/components/android/app/src/main/cpp/lui_jni_bridge_stub.c new file mode 100644 index 00000000..e44e6ce1 --- /dev/null +++ b/examples/components/android/app/src/main/cpp/lui_jni_bridge_stub.c @@ -0,0 +1,91 @@ +/* Stub exports for builds without the lg Android OCaml toolchain: every + * dev.lui.LuiBridge JNI entry resolves but returns "not accepted", so the + * gallery app still compiles and launches while showing its + * runtime-unavailable placeholder. The real implementation is + * platform/android/lui/src/main/cpp/lui_jni_bridge.c. */ + +#include + +#define LUI_JNI_NAME(name) Java_dev_lui_LuiBridge_##name + +JNIEXPORT jint JNICALL LUI_JNI_NAME(nativeStart)( + JNIEnv *env, jclass clazz, jint platform_code, jint host_code) { + (void)env; (void)clazz; (void)platform_code; (void)host_code; + return 0; +} + +JNIEXPORT jint JNICALL LUI_JNI_NAME(nativeThreadRegister)(JNIEnv *env, jclass clazz) { + (void)env; (void)clazz; + return 0; +} + +#define LUI_STUB_NODE(name) \ + JNIEXPORT jint JNICALL LUI_JNI_NAME(name)(JNIEnv *env, jclass clazz, \ + jlong node) { \ + (void)env; (void)clazz; (void)node; \ + return 0; \ + } + +LUI_STUB_NODE(nativeAppear) +LUI_STUB_NODE(nativePress) +LUI_STUB_NODE(nativeLongPress) +LUI_STUB_NODE(nativeSubmit) +LUI_STUB_NODE(nativeDismiss) +LUI_STUB_NODE(nativeDoublePress) +LUI_STUB_NODE(nativeRadioChanged) +LUI_STUB_NODE(nativePointerEnter) +LUI_STUB_NODE(nativePointerLeave) + +#define LUI_STUB_NODE_STRING(name) \ + JNIEXPORT jint JNICALL LUI_JNI_NAME(name)(JNIEnv *env, jclass clazz, \ + jlong node, jstring text) { \ + (void)env; (void)clazz; (void)node; (void)text; \ + return 0; \ + } + +LUI_STUB_NODE_STRING(nativeTextChanged) +LUI_STUB_NODE_STRING(nativePicked) + +JNIEXPORT jint JNICALL LUI_JNI_NAME(nativeToggleChanged)( + JNIEnv *env, jclass clazz, jlong node, jint checked) { + (void)env; (void)clazz; (void)node; (void)checked; + return 0; +} + +JNIEXPORT jint JNICALL LUI_JNI_NAME(nativeSliderChanged)( + JNIEnv *env, jclass clazz, jlong node, jdouble fraction) { + (void)env; (void)clazz; (void)node; (void)fraction; + return 0; +} + +#define LUI_STUB_POINTER(name) \ + JNIEXPORT jint JNICALL LUI_JNI_NAME(name)( \ + JNIEnv *env, jclass clazz, jlong node, jdouble x, jdouble y, \ + jint modifiers, jint button, jstring target_class) { \ + (void)env; (void)clazz; (void)node; (void)x; (void)y; \ + (void)modifiers; (void)button; (void)target_class; \ + return 0; \ + } + +LUI_STUB_POINTER(nativePressDetail) +LUI_STUB_POINTER(nativePointerDown) +LUI_STUB_POINTER(nativePointerUp) +LUI_STUB_POINTER(nativeContextMenuPress) + +JNIEXPORT jint JNICALL LUI_JNI_NAME(nativeExtensionEvent)( + JNIEnv *env, jclass clazz, jlong node, jstring identifier, + jstring name, jstring json_values) { + (void)env; (void)clazz; (void)node; + (void)identifier; (void)name; (void)json_values; + return 0; +} + +JNIEXPORT jint JNICALL LUI_JNI_NAME(nativeStop)(JNIEnv *env, jclass clazz) { + (void)env; (void)clazz; + return 0; +} + +JNIEXPORT jlong JNICALL LUI_JNI_NAME(nativeRootNode)(JNIEnv *env, jclass clazz) { + (void)env; (void)clazz; + return -1; +} diff --git a/examples/components/android/app/src/main/java/dev/lui/components/MainActivity.kt b/examples/components/android/app/src/main/java/dev/lui/components/MainActivity.kt new file mode 100644 index 00000000..ba5912fa --- /dev/null +++ b/examples/components/android/app/src/main/java/dev/lui/components/MainActivity.kt @@ -0,0 +1,93 @@ +package dev.lui.components + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import dev.lui.LuiBackend +import dev.lui.LuiBridge +import dev.lui.LuiTheme +import dev.lui.dispatchToBridge + +/** + * Components-gallery host: starts the OCaml runtime through [LuiBridge], + * applies each delivered patch batch to [LuiBackend], and renders the + * retained tree. When the OCaml toolchain .so was absent at build time the + * bridge is a stub (start returns false) and a placeholder is shown. + */ +class MainActivity : ComponentActivity() { + + private val backend = LuiBackend( + eventSink = dev.lui.LuiEventSink { event -> event.dispatchToBridge() }, + ) + + private var runtimeStarted = false + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + + runtimeStarted = runCatching { + LuiBridge.start(libraryName = "lui_jni_bridge") { json -> + backend.applyBatch(json) + } + }.getOrDefault(false) + + setContent { + LuiTheme(dark = isSystemInDarkTheme()) { + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background, + ) { + if (runtimeStarted) { + backend.Content() + } else { + RuntimeUnavailable() + } + } + } + } + } + + override fun onDestroy() { + if (runtimeStarted) { + LuiBridge.stop() + } + super.onDestroy() + } +} + +@Composable +private fun RuntimeUnavailable() { + Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.padding(32.dp), + ) { + Text( + "LUI runtime unavailable", + style = MaterialTheme.typography.titleMedium, + ) + Text( + "liblui_components.so was not built for this ABI — run " + + "tooling/mobile/build_components_android.sh with the lg " + + "Android OCaml toolchain, then rebuild.", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(top = 8.dp), + ) + } + } +} diff --git a/examples/components/android/build.gradle.kts b/examples/components/android/build.gradle.kts new file mode 100644 index 00000000..274bcb5d --- /dev/null +++ b/examples/components/android/build.gradle.kts @@ -0,0 +1,8 @@ +plugins { + id("com.android.application") version "8.6.1" apply false + id("com.android.library") version "8.6.1" apply false + id("org.jetbrains.kotlin.android") version "2.0.21" apply false + id("org.jetbrains.kotlin.plugin.compose") version "2.0.21" apply false + id("org.jetbrains.kotlin.plugin.serialization") version "2.0.21" apply false + id("app.cash.paparazzi") version "1.3.5" apply false +} diff --git a/examples/components/android/gradle.properties b/examples/components/android/gradle.properties new file mode 100644 index 00000000..a311f807 --- /dev/null +++ b/examples/components/android/gradle.properties @@ -0,0 +1,4 @@ +org.gradle.jvmargs=-Xmx4g -Dfile.encoding=UTF-8 +android.useAndroidX=true +android.nonTransitiveRClass=true +kotlin.code.style=official diff --git a/examples/components/android/settings.gradle.kts b/examples/components/android/settings.gradle.kts new file mode 100644 index 00000000..16549590 --- /dev/null +++ b/examples/components/android/settings.gradle.kts @@ -0,0 +1,20 @@ +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +dependencyResolutionManagement { + repositories { + google() + mavenCentral() + } +} + +rootProject.name = "lui-components-android" + +include(":app") +include(":lui") +project(":lui").projectDir = file("../../platform/android/lui") diff --git a/platform/android/build.gradle.kts b/platform/android/build.gradle.kts new file mode 100644 index 00000000..c031eddb --- /dev/null +++ b/platform/android/build.gradle.kts @@ -0,0 +1,7 @@ +plugins { + id("com.android.library") version "8.6.1" apply false + id("org.jetbrains.kotlin.android") version "2.0.21" apply false + id("org.jetbrains.kotlin.plugin.compose") version "2.0.21" apply false + id("org.jetbrains.kotlin.plugin.serialization") version "2.0.21" apply false + id("app.cash.paparazzi") version "1.3.5" apply false +} diff --git a/platform/android/gradle.properties b/platform/android/gradle.properties new file mode 100644 index 00000000..0c4dfe0d --- /dev/null +++ b/platform/android/gradle.properties @@ -0,0 +1,6 @@ +org.gradle.jvmargs=-Xmx4g +org.gradle.parallel=true +org.gradle.caching=true +android.useAndroidX=true +android.nonTransitiveRClass=true +kotlin.code.style=official diff --git a/platform/android/gradle/wrapper/gradle-wrapper.jar b/platform/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..a4b76b9530d66f5e68d973ea569d8e19de379189 GIT binary patch literal 43583 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 yu?e%!_ptzGB|8GrCfu%p?(_ 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 GZ~q4|mN%#X literal 0 HcmV?d00001 diff --git a/platform/android/gradle/wrapper/gradle-wrapper.properties b/platform/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..df97d72b --- /dev/null +++ b/platform/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/platform/android/gradlew b/platform/android/gradlew new file mode 100755 index 00000000..f5feea6d --- /dev/null +++ b/platform/android/gradlew @@ -0,0 +1,252 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/platform/android/gradlew.bat b/platform/android/gradlew.bat new file mode 100644 index 00000000..9b42019c --- /dev/null +++ b/platform/android/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/platform/android/lui/build.gradle.kts b/platform/android/lui/build.gradle.kts new file mode 100644 index 00000000..6d6932f8 --- /dev/null +++ b/platform/android/lui/build.gradle.kts @@ -0,0 +1,64 @@ +plugins { + id("com.android.library") + id("org.jetbrains.kotlin.android") + id("org.jetbrains.kotlin.plugin.compose") + id("org.jetbrains.kotlin.plugin.serialization") + id("app.cash.paparazzi") +} + +android { + namespace = "dev.lui" + compileSdk = 36 + + defaultConfig { + minSdk = 24 + + externalNativeBuild { + cmake { + // The OCaml headers/libs come from the host app's CMake + // project; the bridge itself is compiled by the app + // (see examples/components/android). This module ships + // the source only. + } + } + } + + buildFeatures { + compose = true + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlin { + compilerOptions { + jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17) + } + } + + testOptions { + unitTests.isReturnDefaultValues = true + } + + // The lui_jni_bridge.c source compiles inside the host app (which owns + // the OCaml static libs), so no externalNativeBuild block is set here + // deliberately. +} + +dependencies { + implementation(platform("androidx.compose:compose-bom:2024.10.01")) + implementation("androidx.compose.ui:ui") + implementation("androidx.compose.ui:ui-graphics") + implementation("androidx.compose.ui:ui-tooling-preview") + implementation("androidx.compose.foundation:foundation") + implementation("androidx.compose.material3:material3") + implementation("androidx.compose.material:material-icons-extended") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3") + + debugImplementation("androidx.compose.ui:ui-tooling") + + testImplementation("junit:junit:4.13.2") + testImplementation("app.cash.paparazzi:paparazzi:1.3.5") +} diff --git a/platform/android/lui/src/main/AndroidManifest.xml b/platform/android/lui/src/main/AndroidManifest.xml new file mode 100644 index 00000000..94cbbcfc --- /dev/null +++ b/platform/android/lui/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/platform/android/lui/src/main/cpp/lui_jni_bridge.c b/platform/android/lui/src/main/cpp/lui_jni_bridge.c new file mode 100644 index 00000000..894dd6e4 --- /dev/null +++ b/platform/android/lui/src/main/cpp/lui_jni_bridge.c @@ -0,0 +1,442 @@ +/* JNI bridge between the Kotlin backend and the OCaml LUI runtime. + * + * Ported from platform/flutter/native/lui_ocaml_bridge.c. Each exported + * Java method corresponds to one OCaml-side dispatch entry; the named + * values use the "lui_kotlin_" prefix, which the app registers alongside + * the other host prefixes (see examples/components/native/components_bridge.ml). + * + * Lock discipline (same contract as the Flutter bridge): after start() the + * host thread does NOT hold the OCaml runtime lock between calls, so OCaml + * worker domains keep running (stop-the-world GC included) while the host + * sits in its event loop. Every export re-acquires the lock with + * caml_leave_blocking_section() on the way in and releases it with + * caml_enter_blocking_section() on the way out — including start, whose + * caml_startup leaves the lock held. + * + * Callers must be OCaml-registered threads: the thread that ran + * nativeStart, or one registered via caml_c_thread_register (exposed here + * as nativeThreadRegister). LuiBridge routes every call through its + * dedicated LuiThread to satisfy this. + * + * The patch callback runs while the OCaml domain lock is held. It copies + * the JSON into a Java string and hands it to LuiBridge.dispatchPatch, + * which enqueues to the UI thread — it must not call back into OCaml + * exports from inside the callback. + */ + +#include +#include +#include + +#include +#include +#include +#include +#include + +static JavaVM *lui_jvm = NULL; +static jmethodID lui_dispatch_patch = NULL; +static int runtime_started = 0; + +#define LUI_JNI_NAME(name) Java_dev_lui_LuiBridge_##name + +JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { + (void)reserved; + JNIEnv *env = NULL; + lui_jvm = vm; + if ((*vm)->GetEnv(vm, (void **)&env, JNI_VERSION_1_6) != JNI_OK) { + return JNI_ERR; + } + jclass bridge = (*env)->FindClass(env, "dev/lui/LuiBridge"); + if (bridge == NULL) { + return JNI_ERR; + } + lui_dispatch_patch = (*env)->GetStaticMethodID( + env, bridge, "dispatchPatch", "(Ljava/lang/String;)V"); + if (lui_dispatch_patch == NULL) { + return JNI_ERR; + } + return JNI_VERSION_1_6; +} + +/* Invoked by OCaml while the runtime lock is held. Copy the JSON out and + * enqueue it; never call back into the exports below from here. */ +static void dispatch_patch_to_jvm(const char *json) { + JNIEnv *env = NULL; + int attached = 0; + jstring value; + if (lui_jvm == NULL || lui_dispatch_patch == NULL || json == NULL) { + return; + } + if ((*lui_jvm)->GetEnv(lui_jvm, (void **)&env, JNI_VERSION_1_6) != JNI_OK) { + if ((*lui_jvm)->AttachCurrentThread(lui_jvm, &env, NULL) != JNI_OK) { + return; + } + attached = 1; + } + value = (*env)->NewStringUTF(env, json); + if (value != NULL) { + jclass bridge = + (*env)->FindClass(env, "dev/lui/LuiBridge"); + if (bridge != NULL) { + (*env)->CallStaticVoidMethod(env, bridge, lui_dispatch_patch, value); + if ((*env)->ExceptionCheck(env)) { + (*env)->ExceptionClear(env); + } + } + (*env)->DeleteLocalRef(env, value); + } + if (attached) { + (*lui_jvm)->DetachCurrentThread(lui_jvm); + } +} + +static int emit_patch(value result) { + if (Is_exception_result(result)) { + return 0; + } + const char *json = String_val(result); + if (json[0] != '\0') { + dispatch_patch_to_jvm(json); + } + return 1; +} + +/* Dispatches a zero-argument named callback under the lock. */ +static int dispatch_unit(const char *callback_name, int64_t node) { + const value *callback = caml_named_value(callback_name); + int accepted = 0; + if (callback == NULL) { + return 0; + } + caml_leave_blocking_section(); + accepted = emit_patch(caml_callback_exn(*callback, Val_long(node))); + caml_enter_blocking_section(); + return accepted; +} + +/* Dispatches a (node, string) named callback under the lock. The string is + * passed as an explicit byte length so embedded NULs survive. */ +static int dispatch_string( + const char *callback_name, + JNIEnv *env, + int64_t node, + jstring text) { + CAMLparam0(); + CAMLlocal2(text_value, result); + int accepted = 0; + const value *callback = caml_named_value(callback_name); + const char *bytes; + jsize length; + if (callback == NULL || text == NULL) { + CAMLreturnT(int, 0); + } + bytes = (*env)->GetStringUTFChars(env, text, NULL); + if (bytes == NULL) { + CAMLreturnT(int, 0); + } + length = (*env)->GetStringUTFLength(env, text); + caml_leave_blocking_section(); + text_value = caml_alloc_initialized_string((intnat)length, bytes); + (*env)->ReleaseStringUTFChars(env, text, bytes); + result = caml_callback2_exn(*callback, Val_long(node), text_value); + accepted = emit_patch(result); + caml_enter_blocking_section(); + CAMLreturnT(int, accepted); +} + +/* Dispatches a pointer event under the lock. The OCaml callback receives + * (node, x, y, modifiers, button, target_class) — six arguments, matching + * the registered lui_kotlin_* entry points in components_bridge.ml. */ +static int dispatch_pointer( + const char *callback_name, + JNIEnv *env, + int64_t node, + jdouble x, + jdouble y, + jint modifiers, + jint button, + jstring target_class) { + CAMLparam0(); + CAMLlocal4(x_value, y_value, class_value, result); + int accepted = 0; + const value *callback = caml_named_value(callback_name); + const char *class_bytes; + value arguments[6]; + if (callback == NULL) { + CAMLreturnT(int, 0); + } + class_bytes = + target_class == NULL ? NULL : (*env)->GetStringUTFChars(env, target_class, NULL); + caml_leave_blocking_section(); + x_value = caml_copy_double(x); + y_value = caml_copy_double(y); + class_value = caml_copy_string(class_bytes == NULL ? "" : class_bytes); + if (class_bytes != NULL) { + (*env)->ReleaseStringUTFChars(env, target_class, class_bytes); + } + arguments[0] = Val_long(node); + arguments[1] = x_value; + arguments[2] = y_value; + arguments[3] = Val_long(modifiers); + arguments[4] = Val_long(button); + arguments[5] = class_value; + result = caml_callbackN_exn(*callback, 6, arguments); + accepted = emit_patch(result); + caml_enter_blocking_section(); + CAMLreturnT(int, accepted); +} + +JNIEXPORT jint JNICALL LUI_JNI_NAME(nativeStart)( + JNIEnv *env, + jclass clazz, + jint platform_code, + jint host_code) { + int32_t accepted = 0; + (void)env; + (void)clazz; + if (!runtime_started) { + char *arguments[] = {"lui_kotlin", NULL}; + caml_startup(arguments); + runtime_started = 1; + } else { + caml_leave_blocking_section(); + } + + const value *initialize = caml_named_value("lui_kotlin_init"); + if (initialize != NULL) { + accepted = emit_patch(caml_callback2_exn( + *initialize, + Val_long(platform_code), + Val_long(host_code))); + } + caml_enter_blocking_section(); + return accepted; +} + +/* Registers the calling thread with the OCaml runtime so it can invoke the + * exports below. The caller must have previously released the runtime lock + * (any thread other than the one that ran nativeStart is fine). */ +JNIEXPORT jint JNICALL LUI_JNI_NAME(nativeThreadRegister)( + JNIEnv *env, + jclass clazz) { + (void)env; + (void)clazz; + return caml_c_thread_register() == 1 ? 1 : 0; +} + +JNIEXPORT jint JNICALL LUI_JNI_NAME(nativeAppear)( + JNIEnv *env, jclass clazz, jlong node) { + (void)env; (void)clazz; + return dispatch_unit("lui_kotlin_appear", node); +} + +JNIEXPORT jint JNICALL LUI_JNI_NAME(nativePress)( + JNIEnv *env, jclass clazz, jlong node) { + (void)env; (void)clazz; + return dispatch_unit("lui_kotlin_press", node); +} + +JNIEXPORT jint JNICALL LUI_JNI_NAME(nativeLongPress)( + JNIEnv *env, jclass clazz, jlong node) { + (void)env; (void)clazz; + return dispatch_unit("lui_kotlin_long_press", node); +} + +JNIEXPORT jint JNICALL LUI_JNI_NAME(nativeTextChanged)( + JNIEnv *env, jclass clazz, jlong node, jstring text) { + (void)clazz; + return dispatch_string("lui_kotlin_text_changed", env, node, text); +} + +JNIEXPORT jint JNICALL LUI_JNI_NAME(nativeSubmit)( + JNIEnv *env, jclass clazz, jlong node) { + (void)env; (void)clazz; + return dispatch_unit("lui_kotlin_submit", node); +} + +JNIEXPORT jint JNICALL LUI_JNI_NAME(nativeDismiss)( + JNIEnv *env, jclass clazz, jlong node) { + (void)env; (void)clazz; + return dispatch_unit("lui_kotlin_dismiss", node); +} + +JNIEXPORT jint JNICALL LUI_JNI_NAME(nativePicked)( + JNIEnv *env, jclass clazz, jlong node, jstring payload) { + (void)clazz; + return dispatch_string("lui_kotlin_picked", env, node, payload); +} + +JNIEXPORT jint JNICALL LUI_JNI_NAME(nativeDoublePress)( + JNIEnv *env, jclass clazz, jlong node) { + (void)env; (void)clazz; + return dispatch_unit("lui_kotlin_double_press", node); +} + +JNIEXPORT jint JNICALL LUI_JNI_NAME(nativeToggleChanged)( + JNIEnv *env, jclass clazz, jlong node, jint checked) { + CAMLparam0(); + CAMLlocal1(result); + int32_t accepted = 0; + const value *dispatch = caml_named_value("lui_kotlin_toggle_changed"); + (void)env; (void)clazz; + if (dispatch == NULL) { + CAMLreturnT(jint, 0); + } + caml_leave_blocking_section(); + result = caml_callback2_exn( + *dispatch, Val_long(node), Val_bool(checked != 0)); + accepted = emit_patch(result); + caml_enter_blocking_section(); + CAMLreturnT(jint, accepted); +} + +JNIEXPORT jint JNICALL LUI_JNI_NAME(nativeRadioChanged)( + JNIEnv *env, jclass clazz, jlong node) { + (void)env; (void)clazz; + return dispatch_unit("lui_kotlin_radio_changed", node); +} + +JNIEXPORT jint JNICALL LUI_JNI_NAME(nativeSliderChanged)( + JNIEnv *env, jclass clazz, jlong node, jdouble fraction) { + CAMLparam0(); + CAMLlocal2(value_value, result); + int32_t accepted = 0; + const value *dispatch = caml_named_value("lui_kotlin_slider_changed"); + (void)env; (void)clazz; + if (dispatch == NULL) { + CAMLreturnT(jint, 0); + } + caml_leave_blocking_section(); + value_value = caml_copy_double(fraction); + result = caml_callback2_exn(*dispatch, Val_long(node), value_value); + accepted = emit_patch(result); + caml_enter_blocking_section(); + CAMLreturnT(jint, accepted); +} + +JNIEXPORT jint JNICALL LUI_JNI_NAME(nativePressDetail)( + JNIEnv *env, jclass clazz, jlong node, jdouble x, jdouble y, + jint modifiers, jint button, jstring target_class) { + (void)clazz; + return dispatch_pointer( + "lui_kotlin_press_detail", env, node, x, y, modifiers, button, target_class); +} + +JNIEXPORT jint JNICALL LUI_JNI_NAME(nativePointerDown)( + JNIEnv *env, jclass clazz, jlong node, jdouble x, jdouble y, + jint modifiers, jint button, jstring target_class) { + (void)clazz; + return dispatch_pointer( + "lui_kotlin_pointer_down", env, node, x, y, modifiers, button, target_class); +} + +JNIEXPORT jint JNICALL LUI_JNI_NAME(nativePointerUp)( + JNIEnv *env, jclass clazz, jlong node, jdouble x, jdouble y, + jint modifiers, jint button, jstring target_class) { + (void)clazz; + return dispatch_pointer( + "lui_kotlin_pointer_up", env, node, x, y, modifiers, button, target_class); +} + +JNIEXPORT jint JNICALL LUI_JNI_NAME(nativePointerEnter)( + JNIEnv *env, jclass clazz, jlong node) { + (void)env; (void)clazz; + return dispatch_unit("lui_kotlin_pointer_enter", node); +} + +JNIEXPORT jint JNICALL LUI_JNI_NAME(nativePointerLeave)( + JNIEnv *env, jclass clazz, jlong node) { + (void)env; (void)clazz; + return dispatch_unit("lui_kotlin_pointer_leave", node); +} + +JNIEXPORT jint JNICALL LUI_JNI_NAME(nativeContextMenuPress)( + JNIEnv *env, jclass clazz, jlong node, jdouble x, jdouble y, + jint modifiers, jint button, jstring target_class) { + (void)clazz; + return dispatch_pointer( + "lui_kotlin_context_menu_press", env, node, x, y, modifiers, button, target_class); +} + +JNIEXPORT jint JNICALL LUI_JNI_NAME(nativeExtensionEvent)( + JNIEnv *env, + jclass clazz, + jlong node, + jstring identifier, + jstring name, + jstring json_values) { + CAMLparam0(); + CAMLlocal4(identifier_value, name_value, values_value, result); + int32_t accepted = 0; + const value *dispatch = caml_named_value("lui_kotlin_extension_event"); + const char *identifier_bytes; + const char *name_bytes; + const char *values_bytes; + value arguments[4]; + (void)clazz; + if (dispatch == NULL || identifier == NULL || name == NULL) { + CAMLreturnT(jint, 0); + } + identifier_bytes = (*env)->GetStringUTFChars(env, identifier, NULL); + name_bytes = (*env)->GetStringUTFChars(env, name, NULL); + values_bytes = + json_values == NULL ? NULL : (*env)->GetStringUTFChars(env, json_values, NULL); + if (identifier_bytes == NULL || name_bytes == NULL) { + if (identifier_bytes != NULL) { + (*env)->ReleaseStringUTFChars(env, identifier, identifier_bytes); + } + if (name_bytes != NULL) { + (*env)->ReleaseStringUTFChars(env, name, name_bytes); + } + if (values_bytes != NULL) { + (*env)->ReleaseStringUTFChars(env, json_values, values_bytes); + } + CAMLreturnT(jint, 0); + } + caml_leave_blocking_section(); + identifier_value = caml_copy_string(identifier_bytes); + name_value = caml_copy_string(name_bytes); + values_value = caml_copy_string(values_bytes == NULL ? "" : values_bytes); + (*env)->ReleaseStringUTFChars(env, identifier, identifier_bytes); + (*env)->ReleaseStringUTFChars(env, name, name_bytes); + if (values_bytes != NULL) { + (*env)->ReleaseStringUTFChars(env, json_values, values_bytes); + } + arguments[0] = Val_long(node); + arguments[1] = identifier_value; + arguments[2] = name_value; + arguments[3] = values_value; + result = caml_callbackN_exn(*dispatch, 4, arguments); + accepted = emit_patch(result); + caml_enter_blocking_section(); + CAMLreturnT(jint, accepted); +} + +JNIEXPORT jint JNICALL LUI_JNI_NAME(nativeStop)(JNIEnv *env, jclass clazz) { + const value *dispose = caml_named_value("lui_kotlin_dispose"); + int32_t accepted = 0; + (void)env; (void)clazz; + if (dispose == NULL) { + return 0; + } + caml_leave_blocking_section(); + accepted = emit_patch(caml_callback_exn(*dispose, Val_unit)); + caml_enter_blocking_section(); + return accepted; +} + +JNIEXPORT jlong JNICALL LUI_JNI_NAME(nativeRootNode)(JNIEnv *env, jclass clazz) { + const value *callback = caml_named_value("lui_kotlin_root_node"); + value result; + int64_t node; + (void)env; (void)clazz; + if (callback == NULL) { + return -1; + } + caml_leave_blocking_section(); + result = caml_callback_exn(*callback, Val_unit); + node = Is_exception_result(result) ? -1 : Long_val(result); + caml_enter_blocking_section(); + return node; +} diff --git a/platform/android/lui/src/main/kotlin/dev/lui/LuiBackend.kt b/platform/android/lui/src/main/kotlin/dev/lui/LuiBackend.kt new file mode 100644 index 00000000..b9c3b74b --- /dev/null +++ b/platform/android/lui/src/main/kotlin/dev/lui/LuiBackend.kt @@ -0,0 +1,100 @@ +package dev.lui + +import android.graphics.BitmapFactory +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateMapOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.asImageBitmap + +/** + * Public entry point of the Kotlin/LUI backend: owns the retained tree, + * the extension registry, host-registered images, and the event sink + * that forwards user events to the bridge. + * + * Typical use: + * ``` + * val backend = LuiBackend { event -> LuiBridge.dispatch(event) } + * backend.applyBatch(patchJson) + * setContent { backend.Content() } + * ``` + */ +class LuiBackend( + val eventSink: LuiEventSink = LuiEventSink {}, + val extensions: LuiExtensionRegistry = LuiExtensionRegistry(), +) { + internal val tree = LuiRetainedTree(extensions) + + /** Bump on every applied batch so composables recompose. */ + internal var revision by mutableIntStateOf(0) + private set + + /** Host-registered raster images, keyed by the `image` prop id. */ + internal val images = mutableStateMapOf() + + /** Host-presented media frames, keyed by the `surface` prop id. */ + internal val mediaSurfaces = mutableStateMapOf() + + /** Resolver hook for `file-image` paths (e.g. remap asset paths). */ + var filePathResolver: ((String) -> String)? = null + + internal var lastError: String? = null + private set + + fun node(id: Int): LuiNode? = tree.nodes[id] + fun extensionNode(id: Int): LuiExtensionNode? = tree.extensionNodes[id] + + /** Apply one JSON patch batch. Returns false (state untouched) when the + * batch fails validation — mirrors the other backends' reject semantics. */ + fun applyBatch(source: String): Boolean { + return try { + tree.apply(LuiPatchBatch.parse(source)) + revision++ + lastError = null + true + } catch (error: LuiBackendException) { + lastError = error.message + false + } + } + + fun emit(event: LuiEvent) = eventSink.onEvent(event) + + internal fun emitExtensionEvent( + node: Int, + identifier: String, + name: String, + values: Map, + ) = emit(LuiEvent.Extension(node, identifier, name, values)) + + fun registerImage(id: Int, image: ImageBitmap) { + require(id > 0) { "registered image id must be positive" } + images[id] = image + } + + fun unregisterImage(id: Int) { + images.remove(id) + } + + fun presentMediaSurfaceFrame(id: Int, image: ImageBitmap) { + require(id > 0) { "media surface id must be positive" } + mediaSurfaces[id] = image + } + + fun unregisterMediaSurface(id: Int) { + mediaSurfaces.remove(id) + } + + /** Renders the retained tree's roots inside the ambient LuiTheme. */ + @Composable + fun Content() { + // Read the revision so this composable recomposes on each batch. + val applied = revision + if (applied == 0) return + for (rootId in tree.rootIds) { + LuiNodeView(backend = this, id = rootId) + } + } +} diff --git a/platform/android/lui/src/main/kotlin/dev/lui/LuiBridge.kt b/platform/android/lui/src/main/kotlin/dev/lui/LuiBridge.kt new file mode 100644 index 00000000..0b284a04 --- /dev/null +++ b/platform/android/lui/src/main/kotlin/dev/lui/LuiBridge.kt @@ -0,0 +1,293 @@ +package dev.lui + +import android.os.Handler +import android.os.HandlerThread +import android.os.Looper +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicReference + +/** + * JNI bridge to the OCaml LUI runtime, porting the semantics of + * `platform/flutter/native/lui_ocaml_bridge.c` for Android. + * + * Threading contract (mirrors the C header's lock discipline): after + * [start], the OCaml runtime lock is NOT held between calls, so all + * `native*` exports must be invoked on an OCaml-registered thread. This + * object owns a dedicated [HandlerThread] (`lui-ocaml`) that runs + * [nativeStart] once; every event method then enqueues on that thread so + * ordering is preserved and callers never touch the runtime lock. + * + * Patch delivery: the OCaml callbacks return the pending patch batch as a + * JSON string while the domain lock is held; [dispatchPatch] copies it + * into the JVM and posts to the main thread — it must not call back into + * native exports from inside that callback. + */ +object LuiBridge { + /** platform_code for `operating_system` in components_bridge.ml. */ + const val PLATFORM_ANDROID: Int = 3 + + /** host_code for KotlinHost in components_bridge.ml. */ + const val HOST_KOTLIN: Int = 4 + + fun interface PatchSink { + fun onPatch(json: String) + } + + private val ocamlThread = HandlerThread("lui-ocaml").apply { start() } + private val ocamlHandler = Handler(ocamlThread.looper) + private val mainHandler = Handler(Looper.getMainLooper()) + + @Volatile + private var patchSink: PatchSink? = null + + @Volatile + private var started = false + + /** + * Called from JNI while the OCaml domain lock is held. Only copies the + * string into the JVM and enqueues delivery on the main thread — never + * call back into native exports from here. + */ + @JvmStatic + @Suppress("unused") + private fun dispatchPatch(json: String) { + val sink = patchSink ?: return + mainHandler.post { sink.onPatch(json) } + } + + /** + * Loads the OCaml runtime library and starts the app, delivering patch + * batches to [sink] on the main thread. Blocks until `nativeStart` + * returns. Call once per process. + */ + fun start(libraryName: String, hostCode: Int = HOST_KOTLIN, sink: PatchSink): Boolean { + if (started) return true + patchSink = sink + System.loadLibrary(libraryName) + val result = AtomicReference() + val latch = CountDownLatch(1) + ocamlHandler.post { + result.set(nativeStart(PLATFORM_ANDROID, hostCode)) + latch.countDown() + } + latch.await(30, TimeUnit.SECONDS) + started = true + return result.get() != 0 + } + + private inline fun dispatch(crossinline call: () -> Int) { + check(started) { "LuiBridge.start() has not been called" } + ocamlHandler.post { call() } + } + + /** Synchronous dispatch for calls that must return a value. */ + private inline fun dispatchBlocking(crossinline call: () -> T): T { + check(started) { "LuiBridge.start() has not been called" } + val result = AtomicReference() + val latch = CountDownLatch(1) + ocamlHandler.post { + result.set(call()) + latch.countDown() + } + latch.await(10, TimeUnit.SECONDS) + return result.get() + } + + fun appear(node: Int) = dispatch { nativeAppear(node.toLong()) } + + fun press(node: Int) = dispatch { nativePress(node.toLong()) } + + fun longPress(node: Int) = dispatch { nativeLongPress(node.toLong()) } + + fun textChanged(node: Int, text: String) = dispatch { + nativeTextChanged(node.toLong(), text) + } + + fun submit(node: Int) = dispatch { nativeSubmit(node.toLong()) } + + fun dismiss(node: Int) = dispatch { nativeDismiss(node.toLong()) } + + fun picked(node: Int, payload: String) = dispatch { + nativePicked(node.toLong(), payload) + } + + fun doublePress(node: Int) = dispatch { nativeDoublePress(node.toLong()) } + + fun toggleChanged(node: Int, checked: Boolean) = dispatch { + nativeToggleChanged(node.toLong(), if (checked) 1 else 0) + } + + fun radioChanged(node: Int) = dispatch { nativeRadioChanged(node.toLong()) } + + fun sliderChanged(node: Int, fraction: Double) = dispatch { + nativeSliderChanged(node.toLong(), fraction) + } + + fun pressDetail(node: Int, detail: LuiPointerDetail) = dispatch { + nativePressDetail( + node.toLong(), detail.x, detail.y, + detail.modifiers, detail.button, detail.targetClass, + ) + } + + fun pointerDown(node: Int, detail: LuiPointerDetail) = dispatch { + nativePointerDown( + node.toLong(), detail.x, detail.y, + detail.modifiers, detail.button, detail.targetClass, + ) + } + + fun pointerUp(node: Int, detail: LuiPointerDetail) = dispatch { + nativePointerUp( + node.toLong(), detail.x, detail.y, + detail.modifiers, detail.button, detail.targetClass, + ) + } + + fun pointerEnter(node: Int) = dispatch { nativePointerEnter(node.toLong()) } + + fun pointerLeave(node: Int) = dispatch { nativePointerLeave(node.toLong()) } + + fun contextMenuPress(node: Int, detail: LuiPointerDetail) = dispatch { + nativeContextMenuPress( + node.toLong(), detail.x, detail.y, + detail.modifiers, detail.button, detail.targetClass, + ) + } + + fun extensionEvent( + node: Int, + identifier: String, + name: String, + jsonValues: String, + ) = dispatch { + nativeExtensionEvent(node.toLong(), identifier, name, jsonValues) + } + + fun stop() = dispatch { nativeStop() } + + /** Root node id of the running app, or -1 when unavailable. */ + fun rootNode(): Long = dispatchBlocking { nativeRootNode() } + + // ------------------------------------------------------------------ + // JNI exports — see src/main/cpp/lui_jni_bridge.c. Always invoked on + // the `lui-ocaml` thread via the dispatch helpers above. + // ------------------------------------------------------------------ + + private external fun nativeStart(platformCode: Int, hostCode: Int): Int + + private external fun nativeAppear(node: Long): Int + + private external fun nativePress(node: Long): Int + + private external fun nativeLongPress(node: Long): Int + + private external fun nativeTextChanged(node: Long, text: String): Int + + private external fun nativeSubmit(node: Long): Int + + private external fun nativeDismiss(node: Long): Int + + private external fun nativePicked(node: Long, payload: String): Int + + private external fun nativeDoublePress(node: Long): Int + + private external fun nativeToggleChanged(node: Long, checked: Int): Int + + private external fun nativeRadioChanged(node: Long): Int + + private external fun nativeSliderChanged(node: Long, fraction: Double): Int + + private external fun nativePressDetail( + node: Long, + x: Double, + y: Double, + modifiers: Int, + button: Int, + targetClass: String, + ): Int + + private external fun nativePointerDown( + node: Long, + x: Double, + y: Double, + modifiers: Int, + button: Int, + targetClass: String, + ): Int + + private external fun nativePointerUp( + node: Long, + x: Double, + y: Double, + modifiers: Int, + button: Int, + targetClass: String, + ): Int + + private external fun nativePointerEnter(node: Long): Int + + private external fun nativePointerLeave(node: Long): Int + + private external fun nativeContextMenuPress( + node: Long, + x: Double, + y: Double, + modifiers: Int, + button: Int, + targetClass: String, + ): Int + + private external fun nativeExtensionEvent( + node: Long, + identifier: String, + name: String, + jsonValues: String, + ): Int + + private external fun nativeStop(): Int + + private external fun nativeRootNode(): Long +} + +/** + * Maps a [LuiEvent] onto the [LuiBridge] JNI calls — the default event + * sink for a [LuiBackend] wired to the OCaml runtime. + */ +fun LuiEvent.dispatchToBridge() { + val bridge = LuiBridge + when (this) { + is LuiEvent.Press -> bridge.press(node) + is LuiEvent.LongPress -> bridge.longPress(node) + is LuiEvent.TextChanged -> bridge.textChanged(node, text) + is LuiEvent.Submit -> bridge.submit(node) + is LuiEvent.ToggleChanged -> bridge.toggleChanged(node, checked) + is LuiEvent.Change -> bridge.radioChanged(node) + is LuiEvent.ValueChanged -> bridge.sliderChanged(node, value) + is LuiEvent.Dismiss -> bridge.dismiss(node) + is LuiEvent.DoublePress -> bridge.doublePress(node) + is LuiEvent.Appear -> bridge.appear(node) + is LuiEvent.Picked -> bridge.picked(node, payload) + is LuiEvent.PressDetail -> bridge.pressDetail(node, detail) + is LuiEvent.PointerDown -> bridge.pointerDown(node, detail) + is LuiEvent.PointerUp -> bridge.pointerUp(node, detail) + is LuiEvent.PointerEnter -> bridge.pointerEnter(node) + is LuiEvent.PointerLeave -> bridge.pointerLeave(node) + is LuiEvent.ContextMenuPress -> bridge.contextMenuPress(node, detail) + is LuiEvent.Extension -> bridge.extensionEvent( + node, identifier, name, + values.entries.joinToString( + prefix = "{", postfix = "}", + separator = ",", + ) { (key, value) -> "\"$key\":${value.toJsonLiteral()}" }, + ) + // Events the mobile bridge has no dedicated export for; the OCaml + // side handles them through other paths or ignores them. + is LuiEvent.PressModifiers, + is LuiEvent.ScrollCompleted, + is LuiEvent.VisibleRange, + is LuiEvent.Load, + -> Unit + } +} diff --git a/platform/android/lui/src/main/kotlin/dev/lui/LuiEvents.kt b/platform/android/lui/src/main/kotlin/dev/lui/LuiEvents.kt new file mode 100644 index 00000000..d85aa94c --- /dev/null +++ b/platform/android/lui/src/main/kotlin/dev/lui/LuiEvents.kt @@ -0,0 +1,47 @@ +package dev.lui + +/** Pointer-level payload shared by press-detail/pointer events; matches the + * OCaml `pointer_detail` record. On gestures that expose no hit position the + * host emits zero coordinates — never invented values. */ +data class LuiPointerDetail( + val x: Double = 0.0, + val y: Double = 0.0, + val modifiers: Int = 0, + val button: Int = 0, + val targetClass: String = "", +) + +/** Events sent from the platform backend toward OCaml. */ +sealed class LuiEvent { + data class Press(val node: Int) : LuiEvent() + data class LongPress(val node: Int) : LuiEvent() + data class TextChanged(val node: Int, val text: String) : LuiEvent() + data class Submit(val node: Int) : LuiEvent() + data class ToggleChanged(val node: Int, val checked: Boolean) : LuiEvent() + data class Change(val node: Int) : LuiEvent() + data class ValueChanged(val node: Int, val value: Double) : LuiEvent() + data class Dismiss(val node: Int) : LuiEvent() + data class DoublePress(val node: Int) : LuiEvent() + data class Appear(val node: Int) : LuiEvent() + data class ScrollCompleted(val node: Int, val token: Int, val outcome: String) : LuiEvent() + data class VisibleRange(val node: Int, val first: Int, val last: Int) : LuiEvent() + data class Picked(val node: Int, val payload: String) : LuiEvent() + data class PressModifiers(val node: Int, val modifiers: Int) : LuiEvent() + data class PressDetail(val node: Int, val detail: LuiPointerDetail) : LuiEvent() + data class PointerDown(val node: Int, val detail: LuiPointerDetail) : LuiEvent() + data class PointerUp(val node: Int, val detail: LuiPointerDetail) : LuiEvent() + data class PointerEnter(val node: Int) : LuiEvent() + data class PointerLeave(val node: Int) : LuiEvent() + data class ContextMenuPress(val node: Int, val detail: LuiPointerDetail) : LuiEvent() + data class Load(val node: Int) : LuiEvent() + data class Extension( + val node: Int, + val identifier: String, + val name: String, + val values: Map, + ) : LuiEvent() +} + +fun interface LuiEventSink { + fun onEvent(event: LuiEvent) +} diff --git a/platform/android/lui/src/main/kotlin/dev/lui/LuiExtension.kt b/platform/android/lui/src/main/kotlin/dev/lui/LuiExtension.kt new file mode 100644 index 00000000..9c75b8c6 --- /dev/null +++ b/platform/android/lui/src/main/kotlin/dev/lui/LuiExtension.kt @@ -0,0 +1,153 @@ +package dev.lui + +import androidx.compose.runtime.Composable + +enum class LuiExtensionValueKind { + STRING, + BOOLEAN, + INTEGER, + DOUBLE, + ; + + fun accepts(value: LuiWireValue): Boolean = when (this) { + STRING -> value is LuiWireValue.Str + BOOLEAN -> value is LuiWireValue.Bool + INTEGER -> value is LuiWireValue.IntValue + DOUBLE -> value.doubleValue?.isFinite() == true + } + + fun normalize(value: LuiWireValue): LuiWireValue = + if (this == DOUBLE && value is LuiWireValue.IntValue) { + LuiWireValue.DoubleValue(value.value.toDouble()) + } else { + value + } +} + +class LuiExtensionProperty( + val name: String, + val kind: LuiExtensionValueKind, + val required: Boolean = false, + val defaultValue: LuiWireValue? = null, +) + +class LuiExtensionEventSchema( + val name: String = "", + val fields: List = emptyList(), +) + +/** View-model passed to extension composable builders. */ +class LuiExtensionContext internal constructor( + internal val backend: LuiBackend, + val nodeId: Int, + val identifier: String, + val properties: Map, + internal val renderChild: @Composable (Int) -> Unit, +) { + fun string(name: String): String? = properties[name]?.stringValue + fun bool(name: String): Boolean? = properties[name]?.boolValue + fun int(name: String): Int? = properties[name]?.intValue + fun double(name: String): Double? = properties[name]?.doubleValue + + fun emitEvent(name: String, values: Map = emptyMap()) { + backend.emitExtensionEvent(nodeId, identifier, name, values) + } + + @Composable + fun child(id: Int) = renderChild(id) +} + +class LuiExtensionRegistration internal constructor( + val identifier: String, + val fingerprint: String, + val builder: (@Composable LuiExtensionContext.() -> Unit)?, + val acceptsStandardChildren: Boolean = false, + val childIdentifiers: List = emptyList(), + val properties: List = emptyList(), + val events: List = emptyList(), + val isTweak: Boolean = false, +) { + internal fun defaultProperties(): MutableMap { + val map = mutableMapOf() + for (property in properties) { + property.defaultValue?.let { map[property.name] = property.kind.normalize(it) } + } + return map + } +} + +/** + * Host registry for extension components and tweaks, mirroring + * `LUIFlutterExtensionRegistry`. A registration binds an identifier to a + * Composable builder plus the declared prop/event schema used to validate + * `create-extension`/`set-extension-prop` ops on the wire. + */ +class LuiExtensionRegistry { + private val registrations = mutableMapOf() + private var frozen = false + + fun register(registration: LuiExtensionRegistration) { + check(!frozen) { "extension registry is frozen" } + require(isValidExtensionName(registration.identifier)) { + "invalid extension identifier" + } + require(!isStandardNodeName(registration.identifier)) { + "extension identifier shadows a standard node" + } + require(!registrations.containsKey(registration.identifier)) { + "extension identifier is already registered" + } + registrations[registration.identifier] = registration + } + + /** Convenience for component extensions. */ + fun register( + identifier: String, + fingerprint: String, + acceptsStandardChildren: Boolean = false, + childIdentifiers: List = emptyList(), + properties: List = emptyList(), + events: List = emptyList(), + builder: @Composable LuiExtensionContext.() -> Unit, + ) = register( + LuiExtensionRegistration( + identifier = identifier, + fingerprint = fingerprint, + builder = builder, + acceptsStandardChildren = acceptsStandardChildren, + childIdentifiers = childIdentifiers, + properties = properties, + events = events, + ), + ) + + /** A tweak wraps exactly one child with platform-specific styling. */ + fun registerTweak( + identifier: String, + fingerprint: String, + properties: List = emptyList(), + builder: (@Composable LuiExtensionContext.() -> Unit)?, + ) = register( + LuiExtensionRegistration( + identifier = identifier, + fingerprint = fingerprint, + builder = builder, + acceptsStandardChildren = true, + properties = properties, + isTweak = true, + ), + ) + + internal fun freeze() { + frozen = true + } + + fun registration(identifier: String): LuiExtensionRegistration? = registrations[identifier] + + fun isStandardNodeName(name: String): Boolean = LuiNodeKind.fromWire(name) != null + + private fun isValidExtensionName(name: String): Boolean = + name.isNotEmpty() && + name.all { it in 'a'..'z' || it in '0'..'9' || it == '-' || it == '_' } && + name.first() in 'a'..'z' +} diff --git a/platform/android/lui/src/main/kotlin/dev/lui/LuiIcons.kt b/platform/android/lui/src/main/kotlin/dev/lui/LuiIcons.kt new file mode 100644 index 00000000..c86d397f --- /dev/null +++ b/platform/android/lui/src/main/kotlin/dev/lui/LuiIcons.kt @@ -0,0 +1,80 @@ +package dev.lui + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.rounded.* +import androidx.compose.material.icons.rounded.* +import androidx.compose.ui.graphics.vector.ImageVector + +/** Maps LUI built-in icon names (schema/components.json's icon vocabulary) + * to Material symbols. `app:` icons resolve through the host's + * registered app-icon table first. */ +object LuiIcons { + private val registered = mutableMapOf() + + /** Hosts register their own `app:` icon assets here. */ + fun registerAppIcon(name: String, icon: ImageVector) { + registered[name] = icon + } + + fun appIcon(name: String): ImageVector? = registered[name] + + fun resolve(name: String): ImageVector { + if (name.startsWith("app:")) { + return registered[name.substring(4)] ?: Icons.Rounded.HelpOutline + } + return when (name) { + "alert" -> Icons.Rounded.Warning + "archive" -> Icons.Rounded.Archive + "arrow-down" -> Icons.Rounded.ArrowDownward + "arrow-right" -> Icons.AutoMirrored.Rounded.ArrowForward + "arrow-up" -> Icons.Rounded.ArrowUpward + "check" -> Icons.Rounded.Check + "check-circle" -> Icons.Rounded.CheckCircle + "chevron-down" -> Icons.Rounded.KeyboardArrowDown + "chevron-left" -> Icons.AutoMirrored.Rounded.KeyboardArrowLeft + "chevron-right" -> Icons.AutoMirrored.Rounded.KeyboardArrowRight + "chevron-up" -> Icons.Rounded.KeyboardArrowUp + "circle-dot" -> Icons.Rounded.Adjust + "clock" -> Icons.Rounded.Schedule + "copy" -> Icons.Rounded.ContentCopy + "download" -> Icons.Rounded.Download + "edit" -> Icons.Rounded.Edit + "ellipsis" -> Icons.Rounded.MoreHoriz + "external-link" -> Icons.Rounded.OpenInNew + "eye" -> Icons.Rounded.Visibility + "file-text" -> Icons.Rounded.Description + "folder" -> Icons.Rounded.Folder + "folder-open" -> Icons.Rounded.FolderOpen + "git-branch" -> Icons.Rounded.AccountTree + "git-merge" -> Icons.Rounded.Merge + "git-pull-request" -> Icons.Rounded.CallMerge + "info" -> Icons.Rounded.Info + "menu" -> Icons.Rounded.Menu + "mic" -> Icons.Rounded.Mic + "moon" -> Icons.Rounded.DarkMode + "music" -> Icons.Rounded.MusicNote + "panel-left" -> Icons.AutoMirrored.Rounded.ViewSidebar + "panel-right" -> Icons.AutoMirrored.Rounded.ViewSidebar + "pause" -> Icons.Rounded.Pause + "play" -> Icons.Rounded.PlayArrow + "plus" -> Icons.Rounded.Add + "refresh-cw" -> Icons.Rounded.Refresh + "repeat" -> Icons.Rounded.Repeat + "save" -> Icons.Rounded.Save + "search" -> Icons.Rounded.Search + "send" -> Icons.AutoMirrored.Rounded.Send + "settings" -> Icons.Rounded.Settings + "shuffle" -> Icons.Rounded.Shuffle + "skip-back" -> Icons.Rounded.SkipPrevious + "skip-forward" -> Icons.Rounded.SkipNext + "sun" -> Icons.Rounded.LightMode + "terminal" -> Icons.Rounded.Terminal + "trash" -> Icons.Rounded.Delete + "volume" -> Icons.Rounded.VolumeUp + "wrench" -> Icons.Rounded.Build + "x" -> Icons.Rounded.Close + "x-circle" -> Icons.Rounded.Cancel + else -> Icons.Rounded.HelpOutline + } + } +} diff --git a/platform/android/lui/src/main/kotlin/dev/lui/LuiKindRules.kt b/platform/android/lui/src/main/kotlin/dev/lui/LuiKindRules.kt new file mode 100644 index 00000000..6d22c3c7 --- /dev/null +++ b/platform/android/lui/src/main/kotlin/dev/lui/LuiKindRules.kt @@ -0,0 +1,746 @@ +package dev.lui + +/** Kind-predicate helpers and the set-prop admissibility matrix, ported from + * the Flutter backend's `_supports` so all backends accept/reject the same + * wire. */ +internal object LuiKindRules { + + val modalSurfaceKinds = setOf(LuiNodeKind.dialog, LuiNodeKind.sheet) + + val overlaySurfaceKinds = setOf( + LuiNodeKind.panel, + LuiNodeKind.card, + LuiNodeKind.alert, + LuiNodeKind.bubble, + LuiNodeKind.resizable, + ) + + fun isModalSurface(kind: LuiNodeKind): Boolean = kind in modalSurfaceKinds + + fun isOverlaySurface(kind: LuiNodeKind): Boolean = kind in overlaySurfaceKinds + + fun isButtonKind(kind: LuiNodeKind): Boolean = + kind == LuiNodeKind.button || kind == LuiNodeKind.toggleButton + + fun isHorizontalGroupKind(kind: LuiNodeKind): Boolean = + kind == LuiNodeKind.tabs || + kind == LuiNodeKind.buttonGroup || + kind == LuiNodeKind.toggleGroup || + kind == LuiNodeKind.breadcrumb || + kind == LuiNodeKind.pagination + + fun horizontalGroupDefaultGap(kind: LuiNodeKind): Int = + if (kind == LuiNodeKind.pagination) 2 else 4 + + fun isTreeRowKind(kind: LuiNodeKind): Boolean = + kind == LuiNodeKind.row || + kind == LuiNodeKind.column || + kind == LuiNodeKind.panel || + kind == LuiNodeKind.card || + kind == LuiNodeKind.box || + kind == LuiNodeKind.listItem + + fun isTextControl(kind: LuiNodeKind): Boolean = + kind == LuiNodeKind.textField || + kind == LuiNodeKind.secureField || + kind == LuiNodeKind.input || + kind == LuiNodeKind.searchField || + kind == LuiNodeKind.textarea || + kind == LuiNodeKind.combobox + + fun isContextMenuLeafHost(kind: LuiNodeKind): Boolean = kind in setOf( + LuiNodeKind.button, + LuiNodeKind.toggleButton, + LuiNodeKind.toggle, + LuiNodeKind.radio, + LuiNodeKind.slider, + LuiNodeKind.numberStepper, + LuiNodeKind.textField, + LuiNodeKind.secureField, + LuiNodeKind.input, + LuiNodeKind.searchField, + LuiNodeKind.textarea, + LuiNodeKind.checkbox, + LuiNodeKind.switchControl, + LuiNodeKind.select, + LuiNodeKind.combobox, + LuiNodeKind.menuItem, + LuiNodeKind.text, + LuiNodeKind.tableCell, + ) + + fun isToolbarChild(kind: LuiNodeKind): Boolean = + isButtonKind(kind) || + kind == LuiNodeKind.buttonGroup || + kind == LuiNodeKind.toggleGroup || + kind == LuiNodeKind.checkbox || + kind == LuiNodeKind.switchControl || + kind == LuiNodeKind.toggle || + kind == LuiNodeKind.radioGroup || + kind == LuiNodeKind.select || + kind == LuiNodeKind.combobox || + kind == LuiNodeKind.textField || + kind == LuiNodeKind.secureField || + kind == LuiNodeKind.input || + kind == LuiNodeKind.searchField || + kind == LuiNodeKind.menuItem || + kind == LuiNodeKind.menuTrigger || + kind == LuiNodeKind.spacer || + kind == LuiNodeKind.divider || + kind == LuiNodeKind.text + + fun canContainChildren(kind: LuiNodeKind): Boolean = + kind == LuiNodeKind.root || + kind == LuiNodeKind.row || + kind == LuiNodeKind.column || + kind == LuiNodeKind.grid || + kind == LuiNodeKind.stack || + kind == LuiNodeKind.panel || + kind == LuiNodeKind.card || + kind == LuiNodeKind.box || + kind == LuiNodeKind.scroll || + kind == LuiNodeKind.list || + kind == LuiNodeKind.virtualList || + isHorizontalGroupKind(kind) || + kind == LuiNodeKind.radioGroup || + kind == LuiNodeKind.dropdownMenu || + kind == LuiNodeKind.contextMenu || + kind == LuiNodeKind.listItem || + kind == LuiNodeKind.accordion || + kind == LuiNodeKind.table || + kind == LuiNodeKind.tableRow || + kind == LuiNodeKind.tree || + kind == LuiNodeKind.resizable || + kind == LuiNodeKind.split || + kind == LuiNodeKind.drawer || + kind == LuiNodeKind.alert || + kind == LuiNodeKind.bubble || + kind == LuiNodeKind.stepper || + kind == LuiNodeKind.timeline || + kind == LuiNodeKind.inputGroup || + kind == LuiNodeKind.menuTrigger || + kind == LuiNodeKind.inputGroupActions || + kind == LuiNodeKind.toast || + kind == LuiNodeKind.toolbar || + kind == LuiNodeKind.bottomTabs || + kind == LuiNodeKind.bottomTab || + kind == LuiNodeKind.filePicker || + kind == LuiNodeKind.link || + isContextMenuLeafHost(kind) || + kind == LuiNodeKind.listSection || + kind == LuiNodeKind.listSectionHeader || + kind == LuiNodeKind.listSectionFooter || + kind == LuiNodeKind.swipeActions || + kind == LuiNodeKind.popover || + kind == LuiNodeKind.edgeInset || + kind == LuiNodeKind.overlay || + kind == LuiNodeKind.viewThatFits || + isModalSurface(kind) + + fun acceptsExtensionChildren(kind: LuiNodeKind): Boolean = + kind == LuiNodeKind.root || + kind == LuiNodeKind.row || + kind == LuiNodeKind.column || + kind == LuiNodeKind.grid || + kind == LuiNodeKind.stack || + kind == LuiNodeKind.edgeInset || + kind == LuiNodeKind.overlay || + kind == LuiNodeKind.viewThatFits || + kind == LuiNodeKind.panel || + kind == LuiNodeKind.card || + kind == LuiNodeKind.box || + kind == LuiNodeKind.scroll || + kind == LuiNodeKind.list || + kind == LuiNodeKind.virtualList || + kind == LuiNodeKind.listItem || + kind == LuiNodeKind.dialog || + kind == LuiNodeKind.sheet || + kind == LuiNodeKind.accordion || + kind == LuiNodeKind.resizable || + kind == LuiNodeKind.split || + kind == LuiNodeKind.drawer || + kind == LuiNodeKind.alert || + kind == LuiNodeKind.bubble || + kind == LuiNodeKind.toast || + kind == LuiNodeKind.toolbar || + kind == LuiNodeKind.bottomTab || + kind == LuiNodeKind.listSection || + kind == LuiNodeKind.listSectionHeader || + kind == LuiNodeKind.listSectionFooter + + private val mainAlignments = setOf("start", "center", "end", "space_between") + private val crossAlignments = setOf("stretch", "start", "center", "end") + private val controlSizes = setOf("default", "sm", "lg", "icon") + private val textSizes = setOf("heading", "display") + private val textAlignments = setOf("start", "center", "end") + private val themeModes = setOf("system", "light", "dark") + private val buttonVariants = + setOf("default", "primary", "secondary", "outline", "ghost", "destructive") + private val overlayAlignments = setOf( + "top-leading", "top", "top-trailing", + "leading", "center", "trailing", + "bottom-leading", "bottom", "bottom-trailing", + ) + private val iconNames = setOf( + "alert", "archive", "arrow-down", "arrow-right", "arrow-up", + "check", "check-circle", "chevron-down", "chevron-left", "chevron-right", + "chevron-up", "circle-dot", "clock", "copy", "download", "edit", + "ellipsis", "external-link", "eye", "file-text", "folder", "folder-open", + "git-branch", "git-merge", "git-pull-request", "info", "menu", "mic", + "moon", "music", "panel-left", "panel-right", "pause", "play", "plus", + "refresh-cw", "repeat", "save", "search", "send", "settings", "shuffle", + "skip-back", "skip-forward", "sun", "terminal", "trash", "volume", + "wrench", "x", "x-circle", + ) + + fun isIconName(value: String): Boolean = + iconNames.contains(value) || isAppIconName(value) + + fun isAppIconName(value: String): Boolean { + if (!value.startsWith("app:")) return false + val name = value.substring(4) + val segments = name.split('-') + return segments.isNotEmpty() && segments.all { segment -> + segment.isNotEmpty() && segment.all { it in 'a'..'z' || it in '0'..'9' } + } + } + + /** Ported from the Flutter backend's `_supports`: whether a `set-prop` + * op is admissible for the node's kind. */ + fun supports(kind: LuiNodeKind, property: String, value: LuiWireValue): Boolean { + if (property == "accessibility-identifier") return value is LuiWireValue.Str + if (kind == LuiNodeKind.root) { + return (property == "theme" && value is LuiWireValue.Str) || + (property == "theme-mode" && value.stringValue in themeModes) + } + if (kind == LuiNodeKind.contextMenu) return false + // The pointer-detail family is opt-in via `pointer-enabled`, honored + // on every kind that renders. + if (property == "pointer-enabled") { + return value is LuiWireValue.Bool && + kind != LuiNodeKind.root && + kind != LuiNodeKind.swipeActions + } + // Position hint honored on overlay children and the overlay itself. + if (property == "alignment") { + return kind != LuiNodeKind.root && value.stringValue in overlayAlignments + } + if (kind == LuiNodeKind.filePreview) { + return property == "path" && value is LuiWireValue.Str + } + if (kind == LuiNodeKind.accordion) { + return when (property) { + "text" -> value is LuiWireValue.Str + "selected", "toggle-enabled" -> value is LuiWireValue.Bool + "height" -> value.intValue?.let { it >= 0 } == true + else -> false + } + } + if (kind == LuiNodeKind.stepper) { + return when (property) { + "active" -> value.intValue?.let { it >= 0 } == true + "accessibility-label" -> value is LuiWireValue.Str + else -> false + } + } + if (kind == LuiNodeKind.step) { + return property == "text" && value is LuiWireValue.Str + } + if (kind == LuiNodeKind.timeline) { + return when (property) { + "gap" -> value.intValue?.let { it >= 0 } == true + "grow" -> value.numberValue?.let { it.isFinite() && it >= 0 } == true + "accessibility-label" -> value is LuiWireValue.Str + else -> false + } + } + if (kind == LuiNodeKind.timelineItem) { + return when (property) { + "title", "description", "meta", "indicator" -> value is LuiWireValue.Str + "icon" -> value.stringValue?.let { isIconName(it) } == true + "variant" -> value.stringValue in buttonVariants + "connector", "selected", "press-enabled" -> value is LuiWireValue.Bool + else -> false + } + } + if (kind == LuiNodeKind.inputGroup) { + return when (property) { + "accessibility-label" -> value is LuiWireValue.Str + "width", "height", "min-width" -> value.intValue?.let { it >= 0 } == true + "grow" -> value.numberValue?.let { it.isFinite() && it >= 0 } == true + else -> false + } + } + if (kind == LuiNodeKind.inputGroupActions) { + return property == "gap" && value.intValue?.let { it >= 0 } == true + } + if (kind == LuiNodeKind.toast) { + return when (property) { + "duration" -> value.intValue?.let { it >= 0 } == true + "accessibility-label", "style-class" -> value is LuiWireValue.Str + else -> false + } + } + if (kind == LuiNodeKind.toolbar) { + return when (property) { + "orientation" -> value.stringValue == "horizontal" || + value.stringValue == "vertical" + "placement" -> value.stringValue in setOf( + "automatic", "bottom", "navigation", "principal", + "primary-action", "secondary-action", "status", + "confirmation-action", "cancellation-action", + "destructive-action", "top-bar-leading", "top-bar-trailing", + ) + "gap" -> value.intValue?.let { it >= 0 } == true + "accessibility-label", "style-class" -> value is LuiWireValue.Str + else -> false + } + } + if (kind == LuiNodeKind.bottomTabs) { + return when (property) { + "accessibility-label", "style-class" -> value is LuiWireValue.Str + "grow" -> value.numberValue?.let { it.isFinite() && it >= 0 } == true + "width", "height", "min-width", "max-width", + "min-height", "max-height" -> value.intValue?.let { it >= 0 } == true + else -> false + } + } + if (kind == LuiNodeKind.bottomTab) { + return when (property) { + "title" -> value is LuiWireValue.Str + "icon" -> value.stringValue?.let { isIconName(it) } == true + "selected", "enabled", "press-enabled" -> value is LuiWireValue.Bool + else -> false + } + } + if (kind == LuiNodeKind.listSection) { + return when (property) { + "key" -> value is LuiWireValue.Str + "separator" -> value.stringValue == "visible" || value.stringValue == "hidden" + else -> false + } + } + if (kind == LuiNodeKind.swipeActions) return false + if (kind == LuiNodeKind.swipeAction) { + return when (property) { + "text", "background" -> value is LuiWireValue.Str + "icon" -> value.stringValue?.let { isIconName(it) } == true + "variant" -> value.stringValue in buttonVariants + "edge" -> value.stringValue == "leading" || value.stringValue == "trailing" + "enabled", "press-enabled" -> value is LuiWireValue.Bool + else -> false + } + } + if (kind == LuiNodeKind.filePicker) { + return when (property) { + "request", "completion" -> + value is LuiWireValue.Str || value is LuiWireValue.IntValue + "types", "accept" -> value is LuiWireValue.Str + "multiple", "enabled", "appear-enabled" -> value is LuiWireValue.Bool + "directory" -> value is LuiWireValue.Bool + "source" -> value.stringValue in setOf("files", "photos", "camera") + else -> false + } + } + // Theme props are admitted on every non-restrictive container kind. + if (property == "theme") { + return value is LuiWireValue.Str && canContainChildren(kind) + } + if (property == "theme-mode") { + return value.stringValue in themeModes && canContainChildren(kind) + } + return when (property) { + "main" -> + value.stringValue in mainAlignments && + (kind == LuiNodeKind.row || + kind == LuiNodeKind.column || + kind == LuiNodeKind.list || + kind == LuiNodeKind.virtualList || + isHorizontalGroupKind(kind)) + "cross" -> + value.stringValue in crossAlignments && + (kind == LuiNodeKind.row || + kind == LuiNodeKind.column || + kind == LuiNodeKind.list || + kind == LuiNodeKind.virtualList || + isHorizontalGroupKind(kind)) + "grow" -> + value.numberValue?.let { it.isFinite() && it >= 0 } == true && + kind != LuiNodeKind.avatar && + kind != LuiNodeKind.tooltip && + !isModalSurface(kind) + "columns" -> value.intValue?.let { it >= 0 } == true && kind == LuiNodeKind.grid + "text" -> + value is LuiWireValue.Str && + (kind == LuiNodeKind.text || + kind == LuiNodeKind.heading || + kind == LuiNodeKind.paragraph || + kind == LuiNodeKind.label || + isButtonKind(kind) || + isTextControl(kind) || + kind == LuiNodeKind.checkbox || + kind == LuiNodeKind.switchControl || + kind == LuiNodeKind.toggle || + kind == LuiNodeKind.radio || + kind == LuiNodeKind.numberStepper || + kind == LuiNodeKind.select || + kind == LuiNodeKind.menuItem || + kind == LuiNodeKind.menuTrigger || + kind == LuiNodeKind.listItem || + kind == LuiNodeKind.tableCell || + kind == LuiNodeKind.avatar || + kind == LuiNodeKind.tooltip || + kind == LuiNodeKind.alert || + kind == LuiNodeKind.bubble || + kind == LuiNodeKind.statusBar || + kind == LuiNodeKind.drawer || + kind == LuiNodeKind.link || + isModalSurface(kind)) + "enabled" -> + value is LuiWireValue.Bool && + (isButtonKind(kind) || + isTextControl(kind) || + kind == LuiNodeKind.checkbox || + kind == LuiNodeKind.switchControl || + kind == LuiNodeKind.toggle || + kind == LuiNodeKind.radio || + kind == LuiNodeKind.slider || + kind == LuiNodeKind.numberStepper || + kind == LuiNodeKind.select || + kind == LuiNodeKind.menuItem || + kind == LuiNodeKind.listItem || + kind == LuiNodeKind.drawer || + kind == LuiNodeKind.link) + "value" -> + value is LuiWireValue.DoubleValue && + value.doubleValue?.isFinite() == true && + (kind == LuiNodeKind.progress || + kind == LuiNodeKind.slider || + kind == LuiNodeKind.numberStepper || + kind == LuiNodeKind.split) + "min", "max" -> + value.numberValue?.isFinite() == true && kind == LuiNodeKind.numberStepper + "step" -> + value.numberValue?.let { it.isFinite() && it > 0 } == true && + kind == LuiNodeKind.numberStepper + "detents" -> value is LuiWireValue.Str && kind == LuiNodeKind.sheet + "sizing" -> + value.stringValue in setOf("form", "fitted", "page") && + kind == LuiNodeKind.sheet + "resize-duration" -> + value.intValue?.let { it >= 0 } == true && kind == LuiNodeKind.split + "resize-easing" -> + value.stringValue in setOf("linear", "standard", "emphasized", "spring") && + kind == LuiNodeKind.split + "resize-origin" -> + value is LuiWireValue.DoubleValue && + value.doubleValue?.isFinite() == true && + kind == LuiNodeKind.split + "orientation" -> + (value.stringValue == "horizontal" || value.stringValue == "vertical") && + (kind == LuiNodeKind.divider || + kind == LuiNodeKind.tabs || + kind == LuiNodeKind.scroll || + kind == LuiNodeKind.viewThatFits) + "size" -> + (value.stringValue in controlSizes || + (kind == LuiNodeKind.tableCell && value.stringValue in textSizes)) && + (isButtonKind(kind) || + kind == LuiNodeKind.spinner || + kind == LuiNodeKind.icon || + kind == LuiNodeKind.tableCell || + kind == LuiNodeKind.menuItem) + "name" -> + value.stringValue?.let { isIconName(it) } == true && kind == LuiNodeKind.icon + "variant" -> + value.stringValue in buttonVariants && + (isButtonKind(kind) || + kind == LuiNodeKind.alert || + kind == LuiNodeKind.bubble || + kind == LuiNodeKind.menuItem) + "icon" -> + value.stringValue?.let { isIconName(it) } == true && + (isButtonKind(kind) || + kind == LuiNodeKind.menuItem || + kind == LuiNodeKind.menuTrigger || + kind == LuiNodeKind.listItem || + kind == LuiNodeKind.link) + "icon-placement" -> + (value.stringValue == "leading" || value.stringValue == "trailing" || + value.stringValue == "top") && + (isButtonKind(kind) || kind == LuiNodeKind.link) + "selected" -> + value is LuiWireValue.Bool && + (isButtonKind(kind) || + kind == LuiNodeKind.menuItem || + kind == LuiNodeKind.listItem || + kind == LuiNodeKind.tableRow || + kind == LuiNodeKind.drawer || + isTreeRowKind(kind)) + "long-press-enabled" -> + value is LuiWireValue.Bool && + (isButtonKind(kind) || kind == LuiNodeKind.listItem) + "autofocus" -> + value is LuiWireValue.Bool && (isButtonKind(kind) || isTextControl(kind)) + "submit-on-enter" -> value is LuiWireValue.Bool && kind == LuiNodeKind.textarea + "change-enabled" -> + value is LuiWireValue.Bool && + (kind == LuiNodeKind.radio || isTreeRowKind(kind)) + "toggle-enabled" -> + value is LuiWireValue.Bool && + (kind == LuiNodeKind.radio || + kind == LuiNodeKind.drawer || + isTreeRowKind(kind)) + "press-enabled" -> + value is LuiWireValue.Bool && + (kind == LuiNodeKind.text || + kind == LuiNodeKind.column || + kind == LuiNodeKind.radio || + kind == LuiNodeKind.select || + kind == LuiNodeKind.combobox || + kind == LuiNodeKind.menuItem || + kind == LuiNodeKind.listItem || + kind == LuiNodeKind.tableCell || + kind == LuiNodeKind.fileImage || + isTreeRowKind(kind)) + "submit-enabled" -> + value is LuiWireValue.Bool && + (kind == LuiNodeKind.combobox || kind == LuiNodeKind.listItem) + "double-press-enabled" -> + value is LuiWireValue.Bool && kind == LuiNodeKind.listItem + "appear-enabled" -> value is LuiWireValue.Bool + "image" -> + value.intValue?.let { it >= 0 } == true && + (kind == LuiNodeKind.avatar || kind == LuiNodeKind.image) + "surface" -> + value.intValue?.let { it >= 0 } == true && kind == LuiNodeKind.mediaSurface + "source-x", "source-y", "source-width", "source-height" -> + value.numberValue?.isFinite() == true && + (kind == LuiNodeKind.avatar || kind == LuiNodeKind.image) + "path" -> value is LuiWireValue.Str && kind == LuiNodeKind.fileImage + "url" -> + value is LuiWireValue.Str && + (kind == LuiNodeKind.link || kind == LuiNodeKind.image) + "target" -> + value.stringValue in setOf("_self", "_blank") && kind == LuiNodeKind.link + "opacity" -> + value.numberValue?.let { it >= 0 && it <= 1 } == true && + canContainChildren(kind) + "display" -> + value.stringValue == "contents" && + (kind == LuiNodeKind.box || + kind == LuiNodeKind.column || + kind == LuiNodeKind.row) + "tooltip", "tooltip-keys" -> + value is LuiWireValue.Str && + (kind == LuiNodeKind.button || + kind == LuiNodeKind.icon || + kind == LuiNodeKind.menuItem) + "input-type" -> + (value.stringValue == "text" || value.stringValue == "color") && + kind == LuiNodeKind.input + "alt" -> value is LuiWireValue.Str && kind == LuiNodeKind.image + "loading" -> + value.stringValue in setOf("eager", "lazy") && kind == LuiNodeKind.image + "referrer-policy" -> + value.stringValue in setOf( + "no-referrer", "origin", + "strict-origin-when-cross-origin", "unsafe-url", + ) && kind == LuiNodeKind.image + "image-fit" -> + (value.stringValue == "fit" || value.stringValue == "fill") && + kind == LuiNodeKind.fileImage + "max-pixel-size" -> + value.intValue?.let { it > 0 } == true && kind == LuiNodeKind.fileImage + "anchor" -> + value.stringValue in setOf("above", "below", "left", "right") && + (kind == LuiNodeKind.dropdownMenu || + kind == LuiNodeKind.tooltip || + kind == LuiNodeKind.popover) + "anchor-alignment" -> + value.stringValue in setOf("start", "end", "stretch") && + (kind == LuiNodeKind.dropdownMenu || + kind == LuiNodeKind.tooltip || + kind == LuiNodeKind.popover) + "anchor-offset" -> + value.numberValue?.isFinite() == true && + (kind == LuiNodeKind.dropdownMenu || + kind == LuiNodeKind.tooltip || + kind == LuiNodeKind.popover) + "x", "y" -> + value.numberValue?.isFinite() == true && kind == LuiNodeKind.popover + "available-height" -> + value.numberValue?.let { it.isFinite() && it >= 0 } == true && + kind == LuiNodeKind.popover + "edge" -> + (value.stringValue in setOf("top", "bottom", "leading", "trailing") && + kind == LuiNodeKind.edgeInset) || + ((value.stringValue == "leading" || value.stringValue == "trailing") && + kind == LuiNodeKind.swipeAction) + "visible" -> value is LuiWireValue.Bool && kind == LuiNodeKind.edgeInset + "alignment" -> value.stringValue in overlayAlignments + "tooltip-delay" -> + value.intValue?.let { it >= 0 } == true && kind == LuiNodeKind.tooltip + "gap" -> + value.intValue?.let { it >= 0 } == true && + (kind == LuiNodeKind.row || + kind == LuiNodeKind.column || + kind == LuiNodeKind.grid || + kind == LuiNodeKind.list || + kind == LuiNodeKind.virtualList || + kind == LuiNodeKind.dropdownMenu || + kind == LuiNodeKind.tableRow || + kind == LuiNodeKind.tree || + kind == LuiNodeKind.split || + kind == LuiNodeKind.edgeInset || + isHorizontalGroupKind(kind)) + "padding" -> + value is LuiWireValue.IntValue && + kind != LuiNodeKind.avatar && + kind != LuiNodeKind.tooltip + "padding-horizontal", "padding-vertical" -> + value.intValue?.let { it >= 0 } == true && + (kind == LuiNodeKind.row || + kind == LuiNodeKind.column || + kind == LuiNodeKind.grid || + kind == LuiNodeKind.box) + "background" -> + value is LuiWireValue.Str && + kind != LuiNodeKind.avatar && + kind != LuiNodeKind.tooltip && + !isModalSurface(kind) + "foreground" -> + value is LuiWireValue.Str && + (kind == LuiNodeKind.edgeInset || + kind == LuiNodeKind.overlay || + kind == LuiNodeKind.viewThatFits || + kind == LuiNodeKind.text || + kind == LuiNodeKind.heading || + kind == LuiNodeKind.paragraph || + kind == LuiNodeKind.label || + isButtonKind(kind) || + isTextControl(kind) || + kind == LuiNodeKind.checkbox || + kind == LuiNodeKind.toggle || + kind == LuiNodeKind.radio || + kind == LuiNodeKind.slider || + kind == LuiNodeKind.numberStepper || + kind == LuiNodeKind.spinner || + kind == LuiNodeKind.icon || + kind == LuiNodeKind.select || + kind == LuiNodeKind.dropdownMenu || + kind == LuiNodeKind.menuItem || + kind == LuiNodeKind.menuTrigger || + kind == LuiNodeKind.listItem || + kind == LuiNodeKind.tableCell || + kind == LuiNodeKind.resizable || + kind == LuiNodeKind.split || + kind == LuiNodeKind.alert || + kind == LuiNodeKind.bubble || + kind == LuiNodeKind.statusBar || + kind == LuiNodeKind.link || + kind == LuiNodeKind.fileImage) + "border-color" -> + value is LuiWireValue.Str && + kind != LuiNodeKind.avatar && + kind != LuiNodeKind.tooltip && + !isModalSurface(kind) + "border-width" -> + value.intValue?.let { it >= 0 } == true && + kind != LuiNodeKind.avatar && + kind != LuiNodeKind.tooltip && + !isModalSurface(kind) + "corner-radius" -> + value.intValue?.let { it >= 0 } == true && + kind != LuiNodeKind.avatar && + kind != LuiNodeKind.tooltip && + !isModalSurface(kind) + "width", "height" -> + value.intValue?.let { it >= 0 } == true && + kind != LuiNodeKind.avatar && + kind != LuiNodeKind.tooltip + "min-width", "max-width", "min-height", "max-height" -> + value.intValue?.let { it >= 0 } == true && + kind != LuiNodeKind.avatar && + kind != LuiNodeKind.tooltip && + !isModalSurface(kind) + "style-class" -> + value is LuiWireValue.Str && + kind != LuiNodeKind.avatar && + kind != LuiNodeKind.tooltip && + !isModalSurface(kind) + "checked" -> + value is LuiWireValue.Bool && + (kind == LuiNodeKind.checkbox || + kind == LuiNodeKind.switchControl || + kind == LuiNodeKind.toggle || + kind == LuiNodeKind.radio) + "heading-level" -> + value.intValue?.let { it in 1..6 } == true && kind == LuiNodeKind.heading + "placeholder" -> + value is LuiWireValue.Str && + (isTextControl(kind) || kind == LuiNodeKind.select) + "accessibility-label" -> + value is LuiWireValue.Str && + (isButtonKind(kind) || + isTextControl(kind) || + kind == LuiNodeKind.checkbox || + kind == LuiNodeKind.switchControl || + kind == LuiNodeKind.toggle || + kind == LuiNodeKind.radioGroup || + kind == LuiNodeKind.tabs || + kind == LuiNodeKind.buttonGroup || + kind == LuiNodeKind.toggleGroup || + kind == LuiNodeKind.breadcrumb || + kind == LuiNodeKind.pagination || + kind == LuiNodeKind.radio || + kind == LuiNodeKind.slider || + kind == LuiNodeKind.numberStepper || + kind == LuiNodeKind.avatar || + kind == LuiNodeKind.image || + kind == LuiNodeKind.mediaSurface || + kind == LuiNodeKind.tree || + kind == LuiNodeKind.resizable || + kind == LuiNodeKind.split || + kind == LuiNodeKind.drawer || + kind == LuiNodeKind.alert || + kind == LuiNodeKind.bubble || + kind == LuiNodeKind.select || + kind == LuiNodeKind.menuTrigger || + kind == LuiNodeKind.link || + kind == LuiNodeKind.fileImage || + isTreeRowKind(kind)) + "text-alignment" -> + value.stringValue in textAlignments && + (isButtonKind(kind) || + kind == LuiNodeKind.text || + kind == LuiNodeKind.tableCell || + kind == LuiNodeKind.bubble || + kind == LuiNodeKind.statusBar) + "role" -> + (value.stringValue == "treeitem" && isTreeRowKind(kind)) || + (value.stringValue == "menu" && kind == LuiNodeKind.popover) + "tree-level" -> + value.intValue?.let { it > 0 } == true && isTreeRowKind(kind) + "expanded" -> value is LuiWireValue.Bool && isTreeRowKind(kind) + "key" -> + value is LuiWireValue.Str && + (kind == LuiNodeKind.listItem || kind == LuiNodeKind.listSection) + "separator" -> + (value.stringValue == "visible" || value.stringValue == "hidden") && + (kind == LuiNodeKind.listItem || kind == LuiNodeKind.listSection) + "style" -> + value.stringValue in setOf("plain", "inset", "inset-grouped") && + kind == LuiNodeKind.list + "scroll-target" -> value is LuiWireValue.Str && kind == LuiNodeKind.list + "scroll-anchor" -> + value.stringValue in setOf("top", "center", "bottom") && + kind == LuiNodeKind.list + "scroll-token" -> + value.intValue?.let { it >= 0 } == true && kind == LuiNodeKind.list + "scroll-animated", "track-visible-range" -> + value is LuiWireValue.Bool && kind == LuiNodeKind.list + else -> false + } + } +} diff --git a/platform/android/lui/src/main/kotlin/dev/lui/LuiNodeView.kt b/platform/android/lui/src/main/kotlin/dev/lui/LuiNodeView.kt new file mode 100644 index 00000000..080c297e --- /dev/null +++ b/platform/android/lui/src/main/kotlin/dev/lui/LuiNodeView.kt @@ -0,0 +1,1795 @@ +package dev.lui + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.gestures.detectHorizontalDragGestures +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.KeyboardArrowDown +import androidx.compose.material.icons.filled.KeyboardArrowRight +import androidx.compose.material.icons.filled.Remove +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Card +import androidx.compose.material3.Checkbox +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FilterChip +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.ListItem +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Slider +import androidx.compose.material3.Surface +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TooltipBox +import androidx.compose.material3.TooltipDefaults +import androidx.compose.material3.VerticalDivider +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.material3.rememberTooltipState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.dp + +/** + * Renders one node of the retained tree plus its subtree. Ported from the + * Flutter backend's `_buildNode` so event dispatch and visual defaults + * match the mobile reference behavior. + */ +@Composable +fun LuiNodeView(backend: LuiBackend, id: Int) { + val node = backend.node(id) + if (node == null) { + LuiExtensionView(backend, id) + return + } + LuiThemeScope(node) { + LuiAppear(backend, node, id) + when { + node.kind == LuiNodeKind.root -> + node.children.firstOrNull()?.let { LuiNodeView(backend, it) } + LuiKindRules.isModalSurface(node.kind) -> LuiModalSurface(backend, node, id) + else -> LuiChromeSurface(backend, node, id) + } + } +} + +/** + * Applies the shared chrome (sizing/background/border/padding/gestures) + * around the kind-specific content, and renders an attached context-menu + * child as a popup anchored to the long-press point. + */ +@Composable +private fun LuiChromeSurface(backend: LuiBackend, node: LuiNode, id: Int) { + var contextMenuAt by remember(id) { mutableStateOf(null) } + val contextMenuId = + node.children.firstOrNull { backend.node(it)?.kind == LuiNodeKind.contextMenu } + val modifier = Modifier + .luiChrome(node) + .luiGestures(backend, node, id) { offset -> contextMenuAt = offset } + + Box { + LuiNodeContent(backend, node, id, modifier) + val menuId = contextMenuId + val anchor = contextMenuAt + if (anchor != null && menuId != null) { + val menu = backend.node(menuId) + if (menu != null) { + DropdownMenu( + expanded = true, + onDismissRequest = { + contextMenuAt = null + backend.emit(LuiEvent.Dismiss(menuId)) + }, + offset = with(LocalDensity.current) { DpOffset(anchor.x.toDp(), 0.dp) }, + ) { + menu.children.forEach { childId -> LuiMenuEntry(backend, childId) } + } + } + } + } +} + +/** + * Renders an extension node through its registered Composable builder; + * tweaks use the same path (their registration wraps the single child). + */ +@Composable +private fun LuiExtensionView(backend: LuiBackend, id: Int) { + val node = backend.extensionNode(id) ?: return + val builder = backend.extensions.registration(node.identifier)?.builder ?: return + val context = LuiExtensionContext( + backend = backend, + nodeId = id, + identifier = node.identifier, + properties = node.properties, + renderChild = { childId -> LuiNodeView(backend, childId) }, + ) + context.builder() +} + +private fun gapOf(node: LuiNode): Int = + node.prop("gap")?.intValue ?: if (LuiKindRules.isHorizontalGroupKind(node.kind)) { + LuiKindRules.horizontalGroupDefaultGap(node.kind) + } else { + 0 + } + +private fun emitValueChange(backend: LuiBackend, node: LuiNode, id: Int, value: Double) { + if (!node.isEnabled()) return + val emitted = when (node.kind) { + LuiNodeKind.numberStepper -> { + val min = node.propDouble("min") + val max = node.propDouble("max", Double.MAX_VALUE) + value.coerceIn(min, if (max < min) min else max) + } + else -> value.coerceIn(0.0, 1.0) + } + backend.emit(LuiEvent.ValueChanged(id, emitted)) +} + +/** Applies `grow` to a child inside a Row. */ +@Composable +private fun RowScope.GrowChildRow(backend: LuiBackend, childId: Int) { + val grow = backend.node(childId)?.prop("grow")?.numberValue ?: 0.0 + if (grow > 0) { + Box(Modifier.weight(grow.toFloat().coerceAtLeast(0.001f))) { + LuiNodeView(backend, childId) + } + } else { + LuiNodeView(backend, childId) + } +} + +/** Applies `grow` to a child inside a Column. */ +@Composable +private fun ColumnScope.GrowChildColumn(backend: LuiBackend, childId: Int) { + val grow = backend.node(childId)?.prop("grow")?.numberValue ?: 0.0 + if (grow > 0) { + Box(Modifier.weight(grow.toFloat().coerceAtLeast(0.001f))) { + LuiNodeView(backend, childId) + } + } else { + LuiNodeView(backend, childId) + } +} + +/** + * The kind-specific rendering of a node. `modifier` carries the shared + * chrome computed by [LuiChromeSurface]. + */ +@Composable +private fun LuiNodeContent( + backend: LuiBackend, + node: LuiNode, + id: Int, + modifier: Modifier, +) { + val children = node.children.filter { childId -> + val kind = backend.node(childId)?.kind + kind != LuiNodeKind.contextMenu && kind != LuiNodeKind.swipeActions + } + val enabled = node.isEnabled() + + when (node.kind) { + LuiNodeKind.root -> children.firstOrNull()?.let { LuiNodeView(backend, it) } + + LuiNodeKind.row -> Row( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy( + gapOf(node).dp, + alignment = mainHorizontalAlignment(node.propString("main")), + ), + verticalAlignment = when (node.propString("cross")) { + "start" -> Alignment.Top + "end" -> Alignment.Bottom + else -> Alignment.CenterVertically + }, + ) { + children.forEach { childId -> GrowChildRow(backend, childId) } + } + + LuiNodeKind.column, + LuiNodeKind.list, + LuiNodeKind.listSection, + LuiNodeKind.listSectionHeader, + LuiNodeKind.listSectionFooter, + LuiNodeKind.swipeActions, + LuiNodeKind.link, + LuiNodeKind.popover, + LuiNodeKind.tree, + LuiNodeKind.table, + LuiNodeKind.timeline, + -> Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(gapOf(node).dp), + ) { + children.forEach { childId -> GrowChildColumn(backend, childId) } + } + + LuiNodeKind.virtualList -> LazyColumn( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(gapOf(node).dp), + ) { + items(children.size) { index -> LuiNodeView(backend, children[index]) } + } + + LuiNodeKind.grid -> LuiGrid(backend, node, modifier, children) + + LuiNodeKind.stack, + LuiNodeKind.bottomTab, + LuiNodeKind.panel, + LuiNodeKind.card, + LuiNodeKind.resizable, + LuiNodeKind.filePicker, + -> LuiStackLike(backend, node, modifier, children) + + LuiNodeKind.edgeInset -> LuiEdgeInset(backend, node, modifier, children) + + LuiNodeKind.overlay -> LuiOverlay(backend, node, modifier, children) + + LuiNodeKind.viewThatFits -> Box(modifier) { + children.firstOrNull()?.let { LuiNodeView(backend, it) } + } + + LuiNodeKind.alert -> Column(modifier, verticalArrangement = Arrangement.spacedBy(8.dp)) { + if (node.text().isNotEmpty()) { + Text(node.text(), style = MaterialTheme.typography.titleSmall) + } + Box { children.forEach { LuiNodeView(backend, it) } } + } + + LuiNodeKind.bubble -> LuiBubble(backend, node, modifier, children) + + LuiNodeKind.box -> Column(modifier.fillMaxWidth()) { + children.forEach { childId -> GrowChildColumn(backend, childId) } + } + + LuiNodeKind.text, + LuiNodeKind.paragraph, + LuiNodeKind.label, + -> Text( + node.text(), + modifier = modifier, + color = luiThemeColor(node.propString("foreground"), foreground = true) + ?: Color.Unspecified, + textAlign = textAlignment(node.propString("text-alignment")), + style = LuiTypography.textStyleForSize(node.propString("size")), + ) + + LuiNodeKind.heading -> Text( + node.text(), + modifier = modifier, + color = luiThemeColor(node.propString("foreground"), foreground = true) + ?: Color.Unspecified, + style = LuiTypography.headingStyle(node.propInt("heading-level", 1)), + ) + + LuiNodeKind.button -> LuiButton(backend, node, id, modifier, children) + + LuiNodeKind.toggleButton -> LuiToggleButton(backend, node, id, modifier) + + LuiNodeKind.toggle -> FilterChip( + selected = node.propBool("checked"), + onClick = { + backend.emit(LuiEvent.ToggleChanged(id, !node.propBool("checked"))) + }, + label = { Text(node.text()) }, + modifier = modifier, + enabled = enabled, + ) + + LuiNodeKind.radioGroup -> Row(modifier) { + children.forEach { LuiNodeView(backend, it) } + } + + LuiNodeKind.radio -> Row( + modifier, + verticalAlignment = Alignment.CenterVertically, + ) { + RadioButton( + selected = node.propBool("checked"), + onClick = if (enabled) { + { + if (node.propBool("change-enabled")) { + backend.emit(LuiEvent.Change(id)) + } else if (node.propBool("toggle-enabled")) { + backend.emit(LuiEvent.ToggleChanged(id, true)) + } else { + backend.emit(LuiEvent.Press(id)) + } + } + } else { + null + }, + enabled = enabled, + ) + Text(node.text()) + } + + LuiNodeKind.slider -> Slider( + value = node.propDouble("value").toFloat().coerceIn(0f, 1f), + onValueChange = { value -> + if (enabled) backend.emit(LuiEvent.ValueChanged(id, value.toDouble())) + }, + modifier = modifier, + enabled = enabled, + ) + + LuiNodeKind.numberStepper -> Row( + modifier, + verticalAlignment = Alignment.CenterVertically, + ) { + Text(node.text()) + val step = node.propDouble("step", 1.0) + val current = node.propDouble("value") + IconButton( + onClick = { emitValueChange(backend, node, id, current - step) }, + enabled = enabled, + ) { Icon(Icons.Filled.Remove, contentDescription = "Decrease") } + IconButton( + onClick = { emitValueChange(backend, node, id, current + step) }, + enabled = enabled, + ) { Icon(Icons.Filled.Add, contentDescription = "Increase") } + } + + LuiNodeKind.textField, + LuiNodeKind.secureField, + LuiNodeKind.input, + LuiNodeKind.searchField, + LuiNodeKind.textarea, + LuiNodeKind.combobox, + -> LuiTextControl(backend, node, id, modifier) + + LuiNodeKind.checkbox -> Row( + modifier, + verticalAlignment = Alignment.CenterVertically, + ) { + Checkbox( + checked = node.propBool("checked"), + onCheckedChange = { value -> + if (enabled) backend.emit(LuiEvent.ToggleChanged(id, value)) + }, + enabled = enabled, + ) + Text(node.text()) + } + + LuiNodeKind.switchControl -> Row( + modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + Text(node.text(), Modifier.weight(1f)) + Switch( + checked = node.propBool("checked"), + onCheckedChange = { value -> + if (enabled) backend.emit(LuiEvent.ToggleChanged(id, value)) + }, + enabled = enabled, + ) + } + + LuiNodeKind.progress -> LinearProgressIndicator( + progress = { node.propDouble("value").toFloat().coerceIn(0f, 1f) }, + modifier = modifier, + ) + + LuiNodeKind.divider -> if (node.propString("orientation") == "vertical") { + VerticalDivider(modifier.width(1.dp).fillMaxHeight()) + } else { + HorizontalDivider(modifier) + } + + LuiNodeKind.scroll -> { + val horizontal = node.propString("orientation") == "horizontal" + Box( + modifier = modifier.then( + if (horizontal) { + Modifier.horizontalScroll(rememberScrollState()) + } else { + Modifier.verticalScroll(rememberScrollState()) + }, + ), + ) { + children.forEach { LuiNodeView(backend, it) } + } + } + + LuiNodeKind.tabs, + LuiNodeKind.buttonGroup, + LuiNodeKind.toggleGroup, + LuiNodeKind.breadcrumb, + LuiNodeKind.pagination, + -> Row( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy( + gapOf(node).dp, + alignment = mainHorizontalAlignment(node.propString("main")), + ), + verticalAlignment = Alignment.CenterVertically, + ) { + children.forEach { childId -> GrowChildRow(backend, childId) } + } + + LuiNodeKind.bottomTabs -> LuiBottomTabs(backend, node, modifier) + + LuiNodeKind.spacer -> Spacer(modifier.size(1.dp)) + + LuiNodeKind.spinner -> CircularProgressIndicator( + modifier = modifier.size( + when (node.propString("size")) { + "sm" -> 16.dp + "lg" -> 24.dp + else -> 20.dp + }, + ), + strokeWidth = 2.dp, + color = luiThemeColor(node.propString("foreground"), foreground = true) + ?: MaterialTheme.colorScheme.primary, + ) + + LuiNodeKind.icon -> Icon( + LuiIcons.resolve(node.propString("name") ?: ""), + contentDescription = node.propString("accessibility-label"), + modifier = modifier.size( + when (node.propString("size")) { + "sm" -> 16.dp + "lg", "icon" -> 24.dp + else -> 18.dp + }, + ), + tint = luiThemeColor(node.propString("foreground"), foreground = true) + ?: MaterialTheme.colorScheme.onSurface, + ) + + LuiNodeKind.select -> LuiSelect(backend, node, id, modifier) + + LuiNodeKind.dropdownMenu, + LuiNodeKind.contextMenu, + -> { + // Menus render through their anchor (stack child, menuTrigger, + // or context-menu popup); a detached menu renders inline as a + // surfaced column so its items stay reachable. + Surface( + modifier, + shape = RoundedCornerShape(8.dp), + tonalElevation = 2.dp, + shadowElevation = 8.dp, + ) { + Column { + children.forEach { childId -> LuiMenuEntry(backend, childId) } + } + } + } + + LuiNodeKind.menuItem -> LuiMenuItem(backend, node, id, modifier) + + LuiNodeKind.menuTrigger -> LuiMenuTrigger(backend, node, id, modifier) + + LuiNodeKind.listItem -> LuiListItem(backend, node, id, modifier, children) + + LuiNodeKind.avatar -> LuiAvatar(backend, node, modifier) + + LuiNodeKind.image -> LuiImage(backend, node, modifier) + + LuiNodeKind.fileImage -> LuiFileImage(backend, node, modifier) + + LuiNodeKind.mediaSurface -> LuiMediaSurface(backend, node, modifier) + + LuiNodeKind.stepper -> Row(modifier.wrapContentSize()) { + children.forEach { LuiNodeView(backend, it) } + } + + LuiNodeKind.step -> LuiStep(backend, node, id, modifier) + + LuiNodeKind.timelineItem -> LuiTimelineItem(backend, node, id, modifier) + + LuiNodeKind.inputGroup -> LuiInputGroup(backend, node, modifier, children) + + LuiNodeKind.inputGroupActions -> Row( + modifier.padding(start = 8.dp, top = 4.dp, end = 8.dp, bottom = 8.dp), + horizontalArrangement = Arrangement.spacedBy(gapOf(node).dp), + verticalAlignment = Alignment.CenterVertically, + ) { + children.forEach { childId -> GrowChildRow(backend, childId) } + } + + LuiNodeKind.accordion -> LuiAccordion(backend, node, id, modifier, children) + + LuiNodeKind.tableRow -> LuiTableRow(backend, node, id, modifier, children) + + LuiNodeKind.tableCell -> LuiTableCell(backend, node, id, modifier) + + LuiNodeKind.drawer -> LuiDrawer(backend, node, id, modifier, children) + + LuiNodeKind.split -> LuiSplit(backend, node, id, modifier, children) + + LuiNodeKind.tooltip -> Text(node.text(), modifier) + + LuiNodeKind.toast -> LuiToast(backend, node, id, modifier, children) + + LuiNodeKind.toolbar -> LuiToolbar(backend, node, modifier, children) + + LuiNodeKind.statusBar -> Text( + node.text(), + modifier = modifier, + textAlign = textAlignment(node.propString("text-alignment")), + style = MaterialTheme.typography.bodySmall, + color = luiThemeColor(node.propString("foreground"), foreground = true) + ?: MaterialTheme.colorScheme.onSurfaceVariant, + ) + + LuiNodeKind.kbd -> Box( + modifier + .clip(RoundedCornerShape(4.dp)) + .background(MaterialTheme.colorScheme.onSurface.copy(alpha = 0.08f)) + .border( + BorderStroke(0.5.dp, MaterialTheme.colorScheme.onSurface.copy(alpha = 0.2f)), + RoundedCornerShape(4.dp), + ) + .padding(horizontal = 6.dp, vertical = 2.dp), + ) { + Text( + node.text(), + style = MaterialTheme.typography.labelSmall.copy( + fontFamily = FontFamily.Monospace, + ), + ) + } + + LuiNodeKind.filePreview, + LuiNodeKind.br, + -> Unit + + LuiNodeKind.dialog, LuiNodeKind.sheet -> Unit // modal surfaces handled above + + else -> Column(modifier) { + children.forEach { LuiNodeView(backend, it) } + } + } +} + +private fun mainHorizontalAlignment(name: String?): Alignment.Horizontal = when (name) { + "center" -> Alignment.CenterHorizontally + "end" -> Alignment.End + else -> Alignment.Start +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +private fun LuiGrid( + backend: LuiBackend, + node: LuiNode, + modifier: Modifier, + children: List, +) { + val requested = node.propInt("columns") + val columns = if (requested > 0) requested else maxOf(children.size, 1) + val gap = gapOf(node) + FlowRow( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(gap.dp), + verticalArrangement = Arrangement.spacedBy(gap.dp), + maxItemsInEachRow = columns, + ) { + children.forEach { childId -> + Box(Modifier.weight(1f)) { LuiNodeView(backend, childId) } + } + } +} + +/** + * Stack-family renderer: children overlay each other; a dropdown-menu + * child renders as an anchored popup and a tooltip child (with `anchor`) + * wraps the stack in a tooltip. + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun LuiStackLike( + backend: LuiBackend, + node: LuiNode, + modifier: Modifier, + children: List, +) { + val menuId = children.firstOrNull { backend.node(it)?.kind == LuiNodeKind.dropdownMenu } + val tooltipId = children.firstOrNull { + val child = backend.node(it) + child?.kind == LuiNodeKind.tooltip && child.prop("anchor") != null + } + val visible = children.filter { it != menuId && it != tooltipId } + + val stackContent: @Composable () -> Unit = { + Box(modifier) { + visible.forEach { childId -> + val child = backend.node(childId) + val fill = (child?.prop("grow")?.numberValue ?: 0.0) > 0 + Box( + if (fill) Modifier.fillMaxSize() else Modifier, + contentAlignment = child?.propString("alignment")?.let(::overlayAlignment) + ?: Alignment.TopStart, + ) { + LuiNodeView(backend, childId) + } + } + } + } + + val withTooltip: @Composable () -> Unit = if (tooltipId != null) { + { + val tooltip = backend.node(tooltipId)!! + TooltipBox( + positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider( + spacingBetweenTooltipAndAnchor = ( + tooltip.prop("anchor-offset")?.numberValue ?: 4.0 + ).dp, + ), + tooltip = { Text(tooltip.propString("text") ?: "") }, + state = rememberTooltipState(), + ) { + stackContent() + } + } + } else { + stackContent + } + + if (menuId == null) { + withTooltip() + } else { + val menu = backend.node(menuId) + Box { + withTooltip() + if (menu != null) { + DropdownMenu( + expanded = true, + onDismissRequest = { backend.emit(LuiEvent.Dismiss(menuId)) }, + ) { + menu.children.forEach { childId -> LuiMenuEntry(backend, childId) } + } + } + } + } +} + +/** edge-inset: first child fills; the rest pin to `edge` in a row/column group. */ +@Composable +private fun LuiEdgeInset( + backend: LuiBackend, + node: LuiNode, + modifier: Modifier, + children: List, +) { + val visible = node.propBool("visible", true) + val edge = node.propString("edge") ?: "top" + val gap = gapOf(node) + Box(modifier) { + children.firstOrNull()?.let { LuiNodeView(backend, it) } + if (visible && children.size > 1) { + val pinned = children.drop(1) + val alignment = when (edge) { + "bottom" -> Alignment.BottomCenter + "leading" -> Alignment.CenterStart + "trailing" -> Alignment.CenterEnd + else -> Alignment.TopCenter + } + Box( + Modifier.fillMaxSize().padding( + top = if (edge == "top") gap.dp else 0.dp, + bottom = if (edge == "bottom") gap.dp else 0.dp, + start = if (edge == "leading") gap.dp else 0.dp, + end = if (edge == "trailing") gap.dp else 0.dp, + ), + ) { + if (edge == "leading" || edge == "trailing") { + Row(Modifier.align(alignment)) { + pinned.forEach { LuiNodeView(backend, it) } + } + } else { + Column(Modifier.align(alignment)) { + pinned.forEach { LuiNodeView(backend, it) } + } + } + } + } + } +} + +/** overlay: children after the first float over it, aligned per-child. */ +@Composable +private fun LuiOverlay( + backend: LuiBackend, + node: LuiNode, + modifier: Modifier, + children: List, +) { + val fallback = overlayAlignment(node.propString("alignment")) + Box(modifier) { + children.forEachIndexed { index, childId -> + if (index == 0) { + LuiNodeView(backend, childId) + } else { + val child = backend.node(childId) + Box( + Modifier.fillMaxSize(), + contentAlignment = child?.propString("alignment") + ?.let(::overlayAlignment) ?: fallback, + ) { + LuiNodeView(backend, childId) + } + } + } + } +} + +@Composable +private fun LuiBubble( + backend: LuiBackend, + node: LuiNode, + modifier: Modifier, + children: List, +) { + Column(modifier.width(IntrinsicSize.Max)) { + Box { children.forEach { LuiNodeView(backend, it) } } + if (node.text().isNotEmpty()) { + val alignment = when (node.propString("text-alignment")) { + "start" -> Alignment.Start + "center" -> Alignment.CenterHorizontally + else -> Alignment.End + } + Surface( + modifier = Modifier.align(alignment).padding(top = 4.dp), + shape = RoundedCornerShape(8.dp), + color = MaterialTheme.colorScheme.secondaryContainer, + ) { + Text( + node.text(), + modifier = Modifier.padding(horizontal = 8.dp, vertical = 2.dp), + style = MaterialTheme.typography.labelSmall, + ) + } + } + } +} + +/** + * button / tab-trigger: variant and size mapping ported from the Flutter + * backend; tab triggers render as compact surface chips inside `tabs`. + */ +@Composable +private fun LuiButton( + backend: LuiBackend, + node: LuiNode, + id: Int, + modifier: Modifier, + children: List, +) { + val enabled = node.isEnabled() + val variant = node.propString("variant") ?: "default" + val isTabTrigger = node.parent?.let { backend.node(it)?.kind } == LuiNodeKind.tabs + val selected = node.propBool("selected") + val foreground = luiThemeColor(node.propString("foreground"), foreground = true) + + val icon = node.propString("icon")?.let { LuiIcons.resolve(it) } + val iconSize = when (node.propString("size")) { + "sm" -> 16.dp + "lg", "icon" -> 24.dp + else -> 18.dp + } + + val label: @Composable () -> Unit = { + if (children.isNotEmpty()) { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + children.forEach { LuiNodeView(backend, it) } + } + } else { + when { + node.text().isEmpty() && icon != null -> Icon( + icon, + contentDescription = node.propString("accessibility-label"), + modifier = Modifier.size(iconSize), + ) + icon == null -> Text(node.text()) + else -> Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + if (node.propString("icon-placement") == "trailing") { + Text(node.text()) + Icon(icon, contentDescription = null, modifier = Modifier.size(iconSize)) + } else { + Icon(icon, contentDescription = null, modifier = Modifier.size(iconSize)) + Text(node.text()) + } + } + } + } + } + + val onClick = { backend.emit(LuiEvent.Press(id)) } + + if (isTabTrigger) { + TextButton( + onClick = onClick, + modifier = modifier.height(32.dp), + enabled = enabled, + shape = RoundedCornerShape(6.dp), + colors = androidx.compose.material3.ButtonDefaults.textButtonColors( + containerColor = if (selected) { + MaterialTheme.colorScheme.surface + } else { + Color.Transparent + }, + contentColor = foreground ?: MaterialTheme.colorScheme.onSurface, + ), + ) { label() } + return + } + + when (variant) { + "primary" -> androidx.compose.material3.Button( + onClick = onClick, + modifier = modifier, + enabled = enabled, + ) { label() } + "secondary" -> androidx.compose.material3.FilledTonalButton( + onClick = onClick, + modifier = modifier, + enabled = enabled, + ) { label() } + "outline" -> androidx.compose.material3.OutlinedButton( + onClick = onClick, + modifier = modifier, + enabled = enabled, + ) { label() } + "destructive" -> androidx.compose.material3.Button( + onClick = onClick, + modifier = modifier, + enabled = enabled, + colors = androidx.compose.material3.ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.error, + contentColor = MaterialTheme.colorScheme.onError, + ), + ) { label() } + else -> TextButton( + onClick = onClick, + modifier = modifier, + enabled = enabled, + ) { label() } + } +} + +/** Toggle buttons keep a local selection so taps feel instant; the model + * `selected` prop re-syncs on the next patch. */ +@Composable +private fun LuiToggleButton(backend: LuiBackend, node: LuiNode, id: Int, modifier: Modifier) { + val enabled = node.isEnabled() + var selected by remember(id) { mutableStateOf(node.propBool("selected")) } + val modelSelected = node.propBool("selected") + LaunchedEffect(modelSelected) { selected = modelSelected } + FilterChip( + selected = selected, + onClick = { + if (enabled) { + selected = !selected + backend.emit(LuiEvent.ToggleChanged(id, selected)) + } + }, + label = { Text(node.text()) }, + modifier = modifier, + enabled = enabled, + ) +} + +/** Text-entry kinds: text-field, secure-field, input, search-field, textarea, combobox. */ +@Composable +private fun LuiTextControl( + backend: LuiBackend, + node: LuiNode, + id: Int, + modifier: Modifier, +) { + val multiline = node.kind == LuiNodeKind.textarea + val secure = node.kind == LuiNodeKind.secureField + val search = node.kind == LuiNodeKind.searchField + val combobox = node.kind == LuiNodeKind.combobox + val enabled = node.isEnabled() + val grouped = node.parent?.let { backend.node(it)?.kind } == LuiNodeKind.inputGroup + + // Local text keeps typing responsive; the model's `text` prop re-syncs + // when a patch changes it. + var localText by remember(id) { mutableStateOf(node.text()) } + val modelText = node.text() + LaunchedEffect(modelText) { if (modelText != localText) localText = modelText } + + OutlinedTextField( + value = localText, + onValueChange = { value -> + localText = value + backend.emit(LuiEvent.TextChanged(id, value)) + }, + modifier = modifier.then( + if (grouped) Modifier.fillMaxWidth() else Modifier.width(240.dp), + ), + enabled = enabled, + placeholder = node.propString("placeholder")?.let { p -> { Text(p) } }, + singleLine = !multiline, + minLines = if (multiline) 3 else 1, + visualTransformation = if (secure) { + androidx.compose.ui.text.input.PasswordVisualTransformation() + } else { + androidx.compose.ui.text.input.VisualTransformation.None + }, + trailingIcon = if (combobox || search) { + { + Icon( + if (combobox) Icons.Filled.KeyboardArrowDown else LuiIcons.resolve("search"), + contentDescription = null, + modifier = if (combobox && enabled) { + Modifier.pointerInput(id) { + detectTapGestures { backend.emit(LuiEvent.Press(id)) } + } + } else { + Modifier + }, + ) + } + } else { + null + }, + textStyle = MaterialTheme.typography.bodyMedium.copy( + color = luiThemeColor(node.propString("foreground"), foreground = true) + ?: MaterialTheme.colorScheme.onSurface, + ), + ) +} + +@Composable +private fun LuiSelect(backend: LuiBackend, node: LuiNode, id: Int, modifier: Modifier) { + val enabled = node.isEnabled() + val title = node.propString("accessibility-label") ?: node.propString("placeholder") ?: "" + val value = node.text().ifEmpty { node.propString("placeholder") ?: "" } + ListItem( + headlineContent = { Text(if (title.isNotEmpty()) title else value) }, + supportingContent = if (title.isNotEmpty() && value != title && value.isNotEmpty()) { + { Text(value) } + } else { + null + }, + trailingContent = { + Icon( + Icons.Filled.KeyboardArrowDown, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + }, + modifier = modifier.clickable(enabled = enabled) { + backend.emit(LuiEvent.Press(id)) + }, + ) +} + +/** A menu entry (used both inside dropdown menus and context menus). */ +@Composable +internal fun LuiMenuEntry(backend: LuiBackend, childId: Int) { + val child = backend.node(childId) ?: return + when (child.kind) { + LuiNodeKind.divider -> HorizontalDivider() + LuiNodeKind.menuItem -> LuiMenuItem(backend, child, childId, Modifier) + LuiNodeKind.menuTrigger -> LuiMenuTrigger(backend, child, childId, Modifier) + else -> LuiNodeView(backend, childId) + } +} + +@Composable +private fun LuiMenuItem(backend: LuiBackend, node: LuiNode, id: Int, modifier: Modifier) { + val submenuId = + node.children.firstOrNull { backend.node(it)?.kind == LuiNodeKind.dropdownMenu } + val enabled = node.isEnabled() + val icon = node.propString("icon")?.let { LuiIcons.resolve(it) } + val selected = node.propBool("selected") + val destructive = node.propString("variant") == "destructive" + + if (submenuId != null) { + var submenuOpen by remember(id) { mutableStateOf(false) } + Box { + DropdownMenuItem( + text = { Text(node.text()) }, + onClick = { submenuOpen = true }, + enabled = enabled, + leadingIcon = icon?.let { { Icon(it, null, Modifier.size(16.dp)) } }, + trailingIcon = { + Icon(Icons.Filled.KeyboardArrowRight, null, Modifier.size(16.dp)) + }, + ) + val submenu = backend.node(submenuId) + if (submenu != null) { + DropdownMenu( + expanded = submenuOpen, + onDismissRequest = { submenuOpen = false }, + ) { + submenu.children.forEach { LuiMenuEntry(backend, it) } + } + } + } + return + } + + DropdownMenuItem( + text = { + Text( + node.text(), + color = if (destructive) MaterialTheme.colorScheme.error else Color.Unspecified, + ) + }, + onClick = { if (enabled) backend.emit(LuiEvent.Press(id)) }, + enabled = enabled, + modifier = modifier, + leadingIcon = icon?.let { { Icon(it, null, Modifier.size(16.dp)) } }, + trailingIcon = if (selected) { + { Icon(Icons.Filled.Check, null, Modifier.size(16.dp)) } + } else { + null + }, + ) +} + +@Composable +private fun LuiMenuTrigger(backend: LuiBackend, node: LuiNode, id: Int, modifier: Modifier) { + val menuId = node.children.firstOrNull { backend.node(it)?.kind == LuiNodeKind.dropdownMenu } + val enabled = node.isEnabled() + val icon = node.propString("icon")?.let { LuiIcons.resolve(it) } + val iconSize = if (node.text().isEmpty()) 24.dp else 16.dp + var open by remember(id) { mutableStateOf(false) } + + Box { + TextButton( + onClick = { open = !open }, + enabled = enabled, + modifier = modifier, + ) { + if (icon != null && node.text().isEmpty()) { + Icon( + icon, + contentDescription = node.propString("accessibility-label"), + modifier = Modifier.size(iconSize), + ) + } else if (icon != null) { + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon(icon, null, Modifier.size(iconSize)) + if (node.text().isNotEmpty()) Text(node.text()) + } + } else { + Text(node.text()) + } + } + val menu = menuId?.let { backend.node(it) } + if (menu != null && menuId != null) { + DropdownMenu( + expanded = open, + onDismissRequest = { + open = false + backend.emit(LuiEvent.Dismiss(menuId)) + }, + ) { + menu.children.forEach { LuiMenuEntry(backend, it) } + } + } + } +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +private fun LuiListItem( + backend: LuiBackend, + node: LuiNode, + id: Int, + modifier: Modifier, + children: List, +) { + val enabled = node.isEnabled() + val icon = node.propString("icon")?.let { LuiIcons.resolve(it) } + ListItem( + headlineContent = { + if (children.isEmpty()) { + Text(node.text()) + } else { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + if (node.text().isNotEmpty()) Text(node.text()) + children.forEach { LuiNodeView(backend, it) } + } + } + }, + leadingContent = icon?.let { { Icon(it, null, Modifier.size(16.dp)) } }, + modifier = modifier.combinedClickable( + enabled = enabled, + onClick = { + if (node.propString("role") == "treeitem") { + if (node.propBool("press-enabled")) { + backend.emit(LuiEvent.Press(id)) + } else if (node.propBool("change-enabled")) { + backend.emit(LuiEvent.Change(id)) + } + if (node.propBool("toggle-enabled")) { + backend.emit(LuiEvent.ToggleChanged(id, !node.propBool("expanded"))) + } + } else if (node.propBool("press-enabled")) { + backend.emit(LuiEvent.Press(id)) + } + }, + onLongClick = if (node.propBool("long-press-enabled")) { + { backend.emit(LuiEvent.LongPress(id)) } + } else { + null + }, + onDoubleClick = if (node.propBool("double-press-enabled")) { + { backend.emit(LuiEvent.DoublePress(id)) } + } else { + null + }, + ), + ) +} + +@Composable +private fun LuiAvatar(backend: LuiBackend, node: LuiNode, modifier: Modifier) { + val image = backend.images[node.propInt("image")] + Box( + modifier.size(40.dp).clip(CircleShape) + .background(MaterialTheme.colorScheme.secondaryContainer), + contentAlignment = Alignment.Center, + ) { + if (image != null) { + Image( + bitmap = image, + contentDescription = node.propString("accessibility-label") ?: node.text(), + modifier = Modifier.fillMaxSize(), + contentScale = ContentScale.Crop, + ) + } else if (node.text().isNotEmpty()) { + Text(node.text(), style = MaterialTheme.typography.labelLarge) + } else { + Icon( + LuiIcons.resolve("person"), + contentDescription = node.propString("accessibility-label"), + tint = MaterialTheme.colorScheme.onSecondaryContainer, + ) + } + } +} + +@Composable +private fun LuiImage(backend: LuiBackend, node: LuiNode, modifier: Modifier) { + val bitmap = backend.images[node.propInt("image")] + val alt = node.propString("alt") ?: node.propString("accessibility-label") + if (bitmap != null) { + Image( + bitmap = bitmap, + contentDescription = alt, + modifier = modifier, + contentScale = ContentScale.Crop, + ) + } else { + // URL loading is intentionally not built in: register pixels via + // LuiBackend.registerImage, or resolve paths through + // filePathResolver + file-image. Keeps the module network-free. + Box(modifier.fillMaxSize()) + } +} + +@Composable +private fun LuiFileImage(backend: LuiBackend, node: LuiNode, modifier: Modifier) { + val path = backend.filePathResolver?.invoke(node.propString("path") ?: "") + ?: node.propString("path") ?: "" + val maxPixel = node.propInt("max-pixel-size", 1024) + var bitmap by remember(path) { mutableStateOf(null) } + LaunchedEffect(path) { + if (path.isNotEmpty()) { + bitmap = runCatching { decodeSampled(path, maxPixel) }.getOrNull() + } + } + val radius = node.propInt("corner-radius") + val clip = if (radius > 0) Modifier.clip(RoundedCornerShape(radius.dp)) else Modifier + val image = bitmap + if (image != null) { + Image( + bitmap = image, + contentDescription = node.propString("accessibility-label"), + modifier = modifier.then(clip), + contentScale = if (node.propString("image-fit") == "fill") { + ContentScale.Crop + } else { + ContentScale.Fit + }, + ) + } else { + Box(modifier.then(clip), contentAlignment = Alignment.Center) { + Icon( + LuiIcons.resolve("image"), + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } +} + +private fun decodeSampled(path: String, maxPixel: Int): ImageBitmap? { + val bounds = android.graphics.BitmapFactory.Options().apply { inJustDecodeBounds = true } + android.graphics.BitmapFactory.decodeFile(path, bounds) + if (bounds.outWidth <= 0 || bounds.outHeight <= 0) return null + var sample = 1 + val longest = maxOf(bounds.outWidth, bounds.outHeight) + while (longest / (sample * 2) > maxPixel) sample *= 2 + val options = android.graphics.BitmapFactory.Options().apply { inSampleSize = sample } + return android.graphics.BitmapFactory.decodeFile(path, options)?.asImageBitmap() +} + +@Composable +private fun LuiMediaSurface(backend: LuiBackend, node: LuiNode, modifier: Modifier) { + val frame = backend.mediaSurfaces[node.propInt("surface")] + if (frame != null) { + Image( + bitmap = frame, + contentDescription = node.propString("accessibility-label"), + modifier = modifier, + contentScale = ContentScale.Crop, + ) + } else { + Box(modifier.fillMaxSize().background(MaterialTheme.colorScheme.surfaceContainerHigh)) + } +} + +@Composable +private fun LuiStep(backend: LuiBackend, node: LuiNode, id: Int, modifier: Modifier) { + val parent = node.parent?.let { backend.node(it) } + val index = parent?.children?.indexOf(id) ?: 0 + val count = parent?.children?.size ?: 1 + val active = parent?.propInt("active") ?: 0 + val state = when { + index < active -> "completed" + index == active -> "active" + else -> "pending" + } + val activeColor = MaterialTheme.colorScheme.primary + Row(modifier.wrapContentSize(), verticalAlignment = Alignment.CenterVertically) { + Box( + Modifier.size(24.dp).clip(CircleShape) + .background(if (state == "pending") Color.Transparent else activeColor) + .border( + BorderStroke( + 1.dp, + if (state == "pending") { + MaterialTheme.colorScheme.outline + } else { + activeColor + }, + ), + CircleShape, + ), + contentAlignment = Alignment.Center, + ) { + if (state == "completed") { + Icon( + Icons.Filled.Check, + contentDescription = null, + modifier = Modifier.size(14.dp), + tint = MaterialTheme.colorScheme.onPrimary, + ) + } else { + Text( + "${index + 1}", + style = MaterialTheme.typography.labelSmall.copy( + fontWeight = FontWeight.SemiBold, + color = if (state == "active") { + MaterialTheme.colorScheme.onPrimary + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, + ), + ) + } + } + Spacer(Modifier.width(6.dp)) + Text( + node.text(), + style = MaterialTheme.typography.bodyMedium.copy( + fontWeight = if (state == "active") FontWeight.SemiBold else FontWeight.Normal, + color = if (state == "pending") { + MaterialTheme.colorScheme.onSurfaceVariant + } else { + Color.Unspecified + }, + ), + ) + if (index + 1 < count) { + Spacer(Modifier.width(8.dp)) + Box(Modifier.width(24.dp).height(1.dp).background(MaterialTheme.colorScheme.outlineVariant)) + Spacer(Modifier.width(8.dp)) + } + } +} + +@Composable +private fun LuiTimelineItem(backend: LuiBackend, node: LuiNode, id: Int, modifier: Modifier) { + val variantColor = when (node.propString("variant")) { + "primary" -> MaterialTheme.colorScheme.primary + "destructive" -> MaterialTheme.colorScheme.error + else -> MaterialTheme.colorScheme.outline + } + val connector = node.propBool("connector", true) + val pressable = node.propBool("press-enabled") + val enabled = node.isEnabled() + + Surface( + modifier = modifier, + shape = RoundedCornerShape(8.dp), + color = if (node.propBool("selected")) { + MaterialTheme.colorScheme.secondaryContainer + } else { + Color.Transparent + }, + ) { + Row( + Modifier.padding(8.dp).then( + if (pressable && enabled) { + Modifier.pointerInput(id) { + detectTapGestures { backend.emit(LuiEvent.Press(id)) } + } + } else { + Modifier + }, + ), + verticalAlignment = Alignment.Top, + ) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Box(Modifier.size(24.dp), contentAlignment = Alignment.Center) { + val iconName = node.propString("icon") ?: "" + val indicator = node.propString("indicator") ?: "" + when { + iconName.isNotEmpty() -> Icon( + LuiIcons.resolve(iconName), + contentDescription = null, + modifier = Modifier.size(16.dp), + tint = variantColor, + ) + indicator.isNotEmpty() -> Text( + indicator, + style = MaterialTheme.typography.labelSmall.copy( + color = variantColor, + fontWeight = FontWeight.SemiBold, + ), + ) + else -> Box(Modifier.size(10.dp).clip(CircleShape).background(variantColor)) + } + } + if (connector) { + Box( + Modifier.width(1.dp).height(28.dp) + .background(MaterialTheme.colorScheme.outlineVariant), + ) + } + } + Spacer(Modifier.width(10.dp)) + Column(Modifier.weight(1f)) { + val title = node.propString("title") ?: "" + if (title.isNotEmpty()) { + Text( + title, + style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.SemiBold), + ) + } + val description = node.propString("description") ?: "" + if (description.isNotEmpty()) { + Text( + description, + style = MaterialTheme.typography.bodyMedium.copy( + color = MaterialTheme.colorScheme.onSurfaceVariant, + ), + ) + } + val meta = node.propString("meta") ?: "" + if (meta.isNotEmpty()) { + Text( + meta, + style = MaterialTheme.typography.bodySmall.copy( + color = MaterialTheme.colorScheme.onSurfaceVariant, + ), + ) + } + } + if (pressable) { + Icon( + Icons.Filled.KeyboardArrowRight, + contentDescription = null, + modifier = Modifier.size(18.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + } +} + +@Composable +private fun LuiInputGroup( + backend: LuiBackend, + node: LuiNode, + modifier: Modifier, + children: List, +) { + Surface( + modifier = modifier, + shape = RoundedCornerShape(8.dp), + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant), + color = MaterialTheme.colorScheme.surface, + ) { + Column(Modifier.fillMaxWidth()) { + children.forEach { LuiNodeView(backend, it) } + } + } +} + +@Composable +private fun LuiBottomTabs(backend: LuiBackend, node: LuiNode, modifier: Modifier) { + val destinationIds = node.children + val selectedIndex = destinationIds.indexOfFirst { + backend.node(it)?.propBool("selected") == true + }.let { if (it < 0) 0 else it } + Column(modifier.fillMaxSize()) { + Box(Modifier.weight(1f)) { + destinationIds.getOrNull(selectedIndex)?.let { LuiNodeView(backend, it) } + } + NavigationBar { + destinationIds.forEachIndexed { index, destId -> + val dest = backend.node(destId) ?: return@forEachIndexed + NavigationBarItem( + selected = index == selectedIndex, + onClick = { + if (dest.isEnabled() && dest.propBool("press-enabled")) { + backend.emit(LuiEvent.Press(destId)) + } + }, + icon = { + Icon(LuiIcons.resolve(dest.propString("icon") ?: ""), contentDescription = null) + }, + label = { Text(dest.propString("title") ?: "") }, + enabled = dest.isEnabled(), + ) + } + } + } +} + +@Composable +private fun LuiAccordion( + backend: LuiBackend, + node: LuiNode, + id: Int, + modifier: Modifier, + children: List, +) { + val expanded = node.propBool("selected") || node.propBool("expanded") + Column(modifier) { + Row( + Modifier.fillMaxWidth() + .clickable(enabled = node.propBool("toggle-enabled")) { + backend.emit(LuiEvent.ToggleChanged(id, !expanded)) + } + .padding(vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text(node.text(), Modifier.weight(1f), style = MaterialTheme.typography.titleSmall) + Icon( + if (expanded) Icons.Filled.KeyboardArrowDown else Icons.Filled.KeyboardArrowRight, + contentDescription = null, + ) + } + if (expanded) { + Column { + children.forEach { LuiNodeView(backend, it) } + } + } + } +} + +@Composable +private fun LuiTableRow( + backend: LuiBackend, + node: LuiNode, + id: Int, + modifier: Modifier, + children: List, +) { + val parent = node.parent?.let { backend.node(it) } + val isLast = parent == null || parent.children.lastOrNull() == id + val gap = gapOf(node) + Column(modifier) { + Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { + children.forEach { childId -> + val grow = backend.node(childId)?.prop("grow")?.numberValue ?: 0.0 + val cellModifier = Modifier.padding(horizontal = (gap / 2).dp).let { + if (grow > 0) it.weight(grow.toFloat().coerceAtLeast(0.001f)) else it + } + Box(cellModifier) { LuiNodeView(backend, childId) } + } + } + if (!isLast) { + HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant) + } + } +} + +@Composable +private fun LuiTableCell(backend: LuiBackend, node: LuiNode, id: Int, modifier: Modifier) { + val enabled = node.isEnabled() + Text( + node.text(), + modifier = modifier.fillMaxWidth().then( + if (node.propBool("press-enabled") && enabled) { + Modifier + .pointerInput(id) { + detectTapGestures { backend.emit(LuiEvent.Press(id)) } + } + .padding(vertical = 12.dp) + } else { + Modifier + }, + ), + textAlign = textAlignment(node.propString("text-alignment")), + style = LuiTypography.textStyleForSize(node.propString("size")).copy( + color = luiThemeColor(node.propString("foreground"), foreground = true) + ?: Color.Unspecified, + ), + ) +} + +/** + * drawer: modal side panel over the main content; `selected` controls + * presentation, scrim tap emits ToggleChanged(false). + */ +@Composable +private fun LuiDrawer( + backend: LuiBackend, + node: LuiNode, + id: Int, + modifier: Modifier, + children: List, +) { + val presented = node.propBool("selected") + val width = node.propInt("width", 320) + val enabled = node.isEnabled() + Box(modifier.fillMaxSize()) { + children.firstOrNull()?.let { LuiNodeView(backend, it) } + if (presented) { + Box( + Modifier.fillMaxSize() + .background(Color.Black.copy(alpha = 0.32f)) + .pointerInput(id) { + detectTapGestures { + if (enabled && node.propBool("toggle-enabled")) { + backend.emit(LuiEvent.ToggleChanged(id, false)) + } + } + }, + ) + Surface( + modifier = Modifier.fillMaxHeight() + .width(width.dp.coerceAtMost(360.dp)) + .align(Alignment.CenterStart), + color = MaterialTheme.colorScheme.surface, + shadowElevation = 8.dp, + ) { + Column { + children.drop(1).forEach { LuiNodeView(backend, it) } + } + } + } + } +} + +/** + * split: two panes separated by a draggable divider; `value` is the first + * pane's fraction. Dragging emits ValueChanged continuously (the model may + * echo the prop back). + */ +@Composable +private fun LuiSplit( + backend: LuiBackend, + node: LuiNode, + id: Int, + modifier: Modifier, + children: List, +) { + val fraction = node.propDouble("value").coerceIn(0.05, 0.95) + val enabled = node.isEnabled() + BoxWithConstraints(modifier.fillMaxSize()) { + val total = constraints.maxWidth.toFloat() + Row(Modifier.fillMaxSize()) { + Box(Modifier.weight(fraction.toFloat().coerceAtLeast(0.01f))) { + children.getOrNull(0)?.let { LuiNodeView(backend, it) } + } + Box( + Modifier.width(9.dp).fillMaxHeight().pointerInput(id, total) { + detectHorizontalDragGestures { _, dragAmount -> + if (enabled && total > 0) { + val next = fraction + dragAmount / total + backend.emit(LuiEvent.ValueChanged(id, next.toDouble())) + } + } + }, + contentAlignment = Alignment.Center, + ) { + Box( + Modifier.width(1.dp).fillMaxHeight() + .background(MaterialTheme.colorScheme.outlineVariant), + ) + } + Box(Modifier.weight((1f - fraction).toFloat().coerceAtLeast(0.01f))) { + children.getOrNull(1)?.let { LuiNodeView(backend, it) } + } + } + } +} + +/** toast: card with a timed auto-dismiss; duration 0 stays until dropped. */ +@Composable +private fun LuiToast( + backend: LuiBackend, + node: LuiNode, + id: Int, + modifier: Modifier, + children: List, +) { + val duration = node.propInt("duration") + if (duration > 0) { + LaunchedEffect(id, duration) { + kotlinx.coroutines.delay(duration.toLong()) + backend.emit(LuiEvent.Dismiss(id)) + } + } + Card(modifier) { + Row( + Modifier.padding(16.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + children.forEach { LuiNodeView(backend, it) } + } + } +} + +/** + * toolbar: horizontal scroll row (or vertical column); `scroll-leading` + * pins the last child at the trailing edge. + */ +@Composable +private fun LuiToolbar( + backend: LuiBackend, + node: LuiNode, + modifier: Modifier, + children: List, +) { + val gap = gapOf(node) + if (node.propString("orientation") == "vertical") { + Column(modifier, verticalArrangement = Arrangement.spacedBy(gap.dp)) { + children.forEach { LuiNodeView(backend, it) } + } + return + } + val pinTrailing = (node.propString("style-class") ?: "") + .split(" ").contains("scroll-leading") && children.size >= 2 + if (!pinTrailing) { + Row( + modifier.horizontalScroll(rememberScrollState()), + horizontalArrangement = Arrangement.spacedBy(gap.dp), + ) { + children.forEach { LuiNodeView(backend, it) } + } + } else { + Row(modifier, horizontalArrangement = Arrangement.spacedBy(gap.dp)) { + Row( + Modifier.weight(1f).horizontalScroll(rememberScrollState()), + horizontalArrangement = Arrangement.spacedBy(gap.dp), + ) { + children.dropLast(1).forEach { LuiNodeView(backend, it) } + } + LuiNodeView(backend, children.last()) + } + } +} + +/** + * Modal surfaces (dialog, sheet): shown while the node exists; dismissal + * emits Dismiss so the model drops the node. + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun LuiModalSurface(backend: LuiBackend, node: LuiNode, id: Int) { + val onDismiss = { backend.emit(LuiEvent.Dismiss(id)) } + when (node.kind) { + LuiNodeKind.dialog -> AlertDialog( + onDismissRequest = onDismiss, + confirmButton = {}, + text = { + Column { + node.children.forEach { LuiNodeView(backend, it) } + } + }, + ) + LuiNodeKind.sheet -> ModalBottomSheet( + onDismissRequest = onDismiss, + sheetState = rememberModalBottomSheetState(), + ) { + Column { + node.children.forEach { LuiNodeView(backend, it) } + } + } + else -> Unit + } +} diff --git a/platform/android/lui/src/main/kotlin/dev/lui/LuiRender.kt b/platform/android/lui/src/main/kotlin/dev/lui/LuiRender.kt new file mode 100644 index 00000000..c73f6372 --- /dev/null +++ b/platform/android/lui/src/main/kotlin/dev/lui/LuiRender.kt @@ -0,0 +1,289 @@ +package dev.lui + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.sizeIn +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.testTag +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import kotlin.math.roundToInt + +internal fun LuiNode.prop(name: String): LuiWireValue? = properties[name] + +internal fun LuiNode.propString(name: String): String? = properties[name]?.stringValue + +internal fun LuiNode.propBool(name: String, default: Boolean = false): Boolean = + properties[name]?.boolValue ?: default + +internal fun LuiNode.propInt(name: String, default: Int = 0): Int = + properties[name]?.intValue ?: default + +internal fun LuiNode.propDouble(name: String, default: Double = 0.0): Double = + properties[name]?.doubleValue ?: default + +internal fun LuiNode.text(): String = propString("text") ?: "" + +internal fun LuiNode.isEnabled(): Boolean = propBool("enabled", true) + +internal fun overlayAlignment(name: String?): Alignment = when (name) { + "top-leading" -> Alignment.TopStart + "top" -> Alignment.TopCenter + "top-trailing" -> Alignment.TopEnd + "leading" -> Alignment.CenterStart + "trailing" -> Alignment.CenterEnd + "bottom-leading" -> Alignment.BottomStart + "bottom" -> Alignment.BottomCenter + "bottom-trailing" -> Alignment.BottomEnd + else -> Alignment.Center +} + +internal fun textAlignment(name: String?): TextAlign = when (name) { + "center" -> TextAlign.Center + "end" -> TextAlign.End + else -> TextAlign.Start +} + +/** Default inner padding by kind, mirroring the Flutter backend. */ +private fun defaultPadding(kind: LuiNodeKind): Int = when (kind) { + LuiNodeKind.card -> 24 + LuiNodeKind.alert -> 16 + LuiNodeKind.bubble -> 12 + LuiNodeKind.tabs -> 4 + else -> 0 +} + +/** + * Shared visual chrome for a node: sizing, padding, background, border, + * corner radius, opacity. Ordering mirrors Flutter's Container: the + * decoration paints under the padding. + */ +@Composable +internal fun Modifier.luiChrome(node: LuiNode): Modifier { + val width = node.prop("width")?.intValue + val height = node.prop("height")?.intValue + val minWidth = node.prop("min-width")?.intValue ?: 0 + val maxWidth = node.prop("max-width")?.intValue ?: Int.MAX_VALUE + val minHeight = node.prop("min-height")?.intValue ?: 0 + val maxHeight = node.prop("max-height")?.intValue ?: Int.MAX_VALUE + var modifier: Modifier = this + if (width != null) modifier = modifier.width(width.dp) + if (height != null) modifier = modifier.height(height.dp) + if (minWidth > 0 || maxWidth != Int.MAX_VALUE || minHeight > 0 || maxHeight != Int.MAX_VALUE) { + modifier = modifier.sizeIn( + minWidth = minWidth.dp, + minHeight = minHeight.dp, + maxWidth = if (maxWidth == Int.MAX_VALUE) Dp.Infinity else maxWidth.dp, + maxHeight = if (maxHeight == Int.MAX_VALUE) Dp.Infinity else maxHeight.dp, + ) + } + val isSurface = LuiKindRules.isOverlaySurface(node.kind) + val padding = node.prop("padding")?.intValue ?: defaultPadding(node.kind) + val paddingH = node.prop("padding-horizontal")?.intValue ?: padding + val paddingV = node.prop("padding-vertical")?.intValue ?: padding + val radius = node.prop("corner-radius")?.intValue + ?: if (isSurface) 12 else if (node.kind == LuiNodeKind.tabs) 8 else 0 + val borderWidth = node.prop("border-width")?.intValue ?: if (isSurface) 1 else 0 + val background = luiThemeColor(node.propString("background")) + val borderColor = luiThemeColor(node.propString("border-color")) + val shape: Shape = if (radius > 0) { + androidx.compose.foundation.shape.RoundedCornerShape(radius.dp) + } else { + RectangleShape + } + val effectiveBackground = background ?: when { + node.kind == LuiNodeKind.box && node.propBool("selected") -> + MaterialTheme.colorScheme.secondaryContainer + node.kind == LuiNodeKind.tabs -> MaterialTheme.colorScheme.secondaryContainer + node.kind == LuiNodeKind.bubble && node.propString("variant") == "primary" -> + MaterialTheme.colorScheme.primary + isSurface -> MaterialTheme.colorScheme.surface + else -> null + } + if (effectiveBackground != null) { + modifier = modifier.background(effectiveBackground, shape) + } + if (borderWidth > 0) { + modifier = modifier.border( + BorderStroke(borderWidth.dp, borderColor ?: MaterialTheme.colorScheme.outlineVariant), + shape, + ) + } + if (radius > 0) modifier = modifier.clip(shape) + if (paddingH > 0 || paddingV > 0) { + modifier = modifier.padding(horizontal = paddingH.dp, vertical = paddingV.dp) + } + node.prop("opacity")?.numberValue?.let { opacity -> + if (opacity < 1) modifier = modifier.alpha(opacity.toFloat()) + } + node.propString("accessibility-identifier")?.let { identifier -> + modifier = modifier.semantics { testTag = identifier }.semantics { + contentDescription = identifier + } + } + node.propString("accessibility-label")?.let { label -> + modifier = modifier.semantics { contentDescription = label } + } + return modifier +} + +/** + * Interaction chrome: press/long-press/double-press on kinds that render + * gestures (column, text, tableCell, fileImage, tree rows, etc.), pointer + * detail when `pointer-enabled`, and an attached context-menu child shown + * on long-press. + */ +@OptIn(ExperimentalFoundationApi::class) +@Composable +internal fun Modifier.luiGestures( + backend: LuiBackend, + node: LuiNode, + id: Int, + onContextMenu: (Offset) -> Unit = {}, +): Modifier { + var modifier: Modifier = this + val enabled = node.isEnabled() + + val contextMenuId = node.children.firstOrNull { backend.node(it)?.kind == LuiNodeKind.contextMenu } + if (contextMenuId != null && enabled) { + modifier = modifier.pointerInput(id, contextMenuId) { + detectTapGestures( + onLongPress = { offset -> + onContextMenu(offset) + backend.emit( + LuiEvent.ContextMenuPress( + id, + LuiPointerDetail(x = offset.x.toDouble(), y = offset.y.toDouble()), + ), + ) + }, + ) + } + } + + val pressable = enabled && + (node.propBool("press-enabled") || + (node.propBool("change-enabled") && LuiKindRules.isTreeRowKind(node.kind)) || + (node.propBool("toggle-enabled") && LuiKindRules.isTreeRowKind(node.kind))) + val longPressable = enabled && node.propBool("long-press-enabled") + val doublePressable = enabled && node.propBool("double-press-enabled") + if (pressable || longPressable || doublePressable) { + modifier = modifier.combinedClickable( + onClick = { + if (node.propBool("press-enabled")) { + backend.emit(LuiEvent.Press(id)) + } else if (node.propBool("change-enabled")) { + backend.emit(LuiEvent.Change(id)) + } + if (node.propBool("toggle-enabled") && LuiKindRules.isTreeRowKind(node.kind)) { + backend.emit(LuiEvent.ToggleChanged(id, !node.propBool("expanded"))) + } + }, + onLongClick = if (longPressable) { + { backend.emit(LuiEvent.LongPress(id)) } + } else { + null + }, + onDoubleClick = if (doublePressable) { + { backend.emit(LuiEvent.DoublePress(id)) } + } else { + null + }, + ) + } + if (enabled && node.propBool("pointer-enabled")) { + modifier = modifier.pointerInput(id) { + detectTapGestures( + onPress = { offset -> + val detail = LuiPointerDetail( + x = offset.x.toDouble(), + y = offset.y.toDouble(), + ) + backend.emit(LuiEvent.PointerDown(id, detail)) + tryAwaitRelease() + backend.emit(LuiEvent.PointerUp(id, detail)) + }, + ) + } + } + return modifier +} + +/** Emits `Appear` once when an `appear-enabled` node first composes. */ +@Composable +internal fun LuiAppear(backend: LuiBackend, node: LuiNode, id: Int) { + if (node.propBool("appear-enabled")) { + LaunchedEffect(id) { backend.emit(LuiEvent.Appear(id)) } + } +} + +/** + * Applies a node's `theme`/`theme-mode` props: merges decoded tokens over + * the inherited semantic colors and optionally overrides the subtree + * appearance — a port of `LUIThemeScopeModifier`. + */ +@Composable +internal fun LuiThemeScope(node: LuiNode, content: @Composable () -> Unit) { + val inherited = LocalLuiSemanticColors.current + val ambientDark = LocalLuiDarkTheme.current + val schemeOverride = LuiThemeTokenDecoder.colorScheme(node.propString("theme-mode")) + val dark = schemeOverride ?: ambientDark + val own = LuiThemeTokenDecoder.colors(node.propString("theme"), dark) + if (own.isEmpty() && schemeOverride == null) { + content() + } else { + CompositionLocalProvider( + LocalLuiSemanticColors provides inherited + own, + LocalLuiDarkTheme provides dark, + ) { + content() + } + } +} diff --git a/platform/android/lui/src/main/kotlin/dev/lui/LuiStore.kt b/platform/android/lui/src/main/kotlin/dev/lui/LuiStore.kt new file mode 100644 index 00000000..ca384dcd --- /dev/null +++ b/platform/android/lui/src/main/kotlin/dev/lui/LuiStore.kt @@ -0,0 +1,403 @@ +package dev.lui + +/** A retained standard node. Property keys are wire property names. */ +class LuiNode( + val kind: LuiNodeKind, + var parent: Int? = null, + val children: MutableList = mutableListOf(), + val properties: MutableMap = mutableMapOf(), +) { + fun copy(): LuiNode = LuiNode(kind, parent, children.toMutableList(), properties.toMutableMap()) +} + +/** A retained extension node (component or platform tweak). */ +class LuiExtensionNode( + val identifier: String, + val fingerprint: String, + var parent: Int? = null, + val children: MutableList = mutableListOf(), + val properties: MutableMap = mutableMapOf(), +) { + fun copy(): LuiExtensionNode = + LuiExtensionNode(identifier, fingerprint, parent, children.toMutableList(), properties.toMutableMap()) +} + +/** + * Retained node store. Mirrors the Flutter backend's apply semantics: + * ops are validated against deep copies and the whole batch is rejected + * (no state change) when any op or post-condition fails. Generation must + * advance by exactly one per batch. + */ +class LuiRetainedTree(private val registry: LuiExtensionRegistry) { + var generation: Int = 0 + private set + + var nodes: Map = emptyMap() + private set + var extensionNodes: Map = emptyMap() + private set + + /** Nodes removed by the last applied batch (only `drop-node` deletes). */ + var lastDropped: Set = emptySet() + private set + + val rootIds: List + get() = nodes.filterValues { it.parent == null }.keys.toList() + + extensionNodes.filterValues { it.parent == null }.keys.toList() + + fun node(id: Int): LuiNode? = nodes[id] + fun extensionNode(id: Int): LuiExtensionNode? = extensionNodes[id] + fun contains(id: Int): Boolean = nodes.containsKey(id) || extensionNodes.containsKey(id) + + private fun parentOf( + nodes: Map, + ext: Map, + id: Int, + ): Int? = nodes[id]?.parent ?: ext[id]?.parent + + private fun childrenOf( + nodes: Map, + ext: Map, + id: Int, + ): MutableList? = nodes[id]?.children ?: ext[id]?.children + + private fun setParent( + nodes: Map, + ext: Map, + id: Int, + parent: Int?, + ) { + nodes[id]?.let { it.parent = parent } + ext[id]?.let { it.parent = parent } + } + + private fun requireNode(nodes: Map, id: Int): LuiNode = + nodes[id] ?: throw LuiBackendException("unknown node $id") + + private fun requireExtension( + ext: Map, + id: Int, + ): LuiExtensionNode = + ext[id] ?: throw LuiBackendException("unknown extension node $id") + + fun apply(batch: LuiPatchBatch) { + val expected = generation + 1 + if (batch.generation != expected) { + throw LuiBackendException( + "expected patch generation $expected, received ${batch.generation}", + ) + } + val nextNodes = nodes.mapValues { it.value.copy() }.toMutableMap() + val nextExt = extensionNodes.mapValues { it.value.copy() }.toMutableMap() + val dropped = mutableSetOf() + for (op in batch.ops) { + applyOp(nextNodes, nextExt, op, dropped) + } + validateStates(nextNodes) + nodes = nextNodes + extensionNodes = nextExt + lastDropped = dropped + generation = batch.generation + } + + private fun applyOp( + nodes: MutableMap, + ext: MutableMap, + op: LuiPatchOp, + dropped: MutableSet, + ) { + when (op) { + is LuiPatchOp.CreateNode -> { + if (nodes.containsKey(op.id) || ext.containsKey(op.id)) { + throw LuiBackendException("node already exists") + } + nodes[op.id] = LuiNode(op.kind) + } + is LuiPatchOp.CreateExtension -> { + if (nodes.containsKey(op.id) || ext.containsKey(op.id)) { + throw LuiBackendException("node already exists") + } + val registration = registry.registration(op.identifier) + ?: throw LuiBackendException("unknown extension") + if (registration.fingerprint != op.fingerprint) { + throw LuiBackendException("extension fingerprint mismatch") + } + ext[op.id] = LuiExtensionNode( + identifier = op.identifier, + fingerprint = op.fingerprint, + properties = registration.defaultProperties(), + ) + } + is LuiPatchOp.DropNode -> { + if (parentOf(nodes, ext, op.id) != null || + childrenOf(nodes, ext, op.id)?.isNotEmpty() == true + ) { + throw LuiBackendException("cannot drop an attached node") + } + if (nodes.remove(op.id) == null && ext.remove(op.id) == null) { + throw LuiBackendException("unknown node ${op.id}") + } + dropped.add(op.id) + } + is LuiPatchOp.SetProp -> { + val node = requireNode(nodes, op.id) + if (!LuiKindRules.supports(node.kind, op.property, op.value)) { + throw LuiBackendException( + "unsupported property value: ${node.kind}.${op.property}", + ) + } + node.properties[op.property] = op.value + } + is LuiPatchOp.RemoveProp -> { + val node = requireNode(nodes, op.id) + if (!node.properties.containsKey(op.property)) { + throw LuiBackendException( + "unsupported property: ${node.kind}.${op.property}", + ) + } + node.properties.remove(op.property) + } + is LuiPatchOp.SetExtensionProp -> { + val node = requireExtension(ext, op.id) + val registration = registry.registration(node.identifier)!! + val property = registration.properties.firstOrNull { it.name == op.property } + if (property == null || !property.kind.accepts(op.value)) { + throw LuiBackendException("unsupported extension property value") + } + node.properties[op.property] = property.kind.normalize(op.value) + } + is LuiPatchOp.RemoveExtensionProp -> { + val node = requireExtension(ext, op.id) + val registration = registry.registration(node.identifier)!! + val property = registration.properties.firstOrNull { it.name == op.property } + ?: throw LuiBackendException("unknown extension property") + val default = property.defaultValue + if (default == null) { + node.properties.remove(op.property) + } else { + node.properties[op.property] = property.kind.normalize(default) + } + } + is LuiPatchOp.InsertChild -> { + if ((!nodes.containsKey(op.parent) && !ext.containsKey(op.parent)) || + (!nodes.containsKey(op.child) && !ext.containsKey(op.child)) + ) { + throw LuiBackendException("unknown parent or child node") + } + if (parentOf(nodes, ext, op.child) != null) { + throw LuiBackendException("child is already attached") + } + validateChildRelationship(nodes, ext, op.parent, op.child) + val children = childrenOf(nodes, ext, op.parent)!! + if (op.index < 0 || op.index > children.size) { + throw LuiBackendException("child index is out of bounds") + } + if (isDescendant(nodes, ext, target = op.parent, root = op.child)) { + throw LuiBackendException("child insertion would create a cycle") + } + children.add(op.index, op.child) + setParent(nodes, ext, op.child, op.parent) + } + is LuiPatchOp.RemoveChild -> { + val children = childrenOf(nodes, ext, op.parent) + if (children == null || !children.remove(op.child)) { + throw LuiBackendException("child is not attached to parent") + } + setParent(nodes, ext, op.child, null) + } + is LuiPatchOp.MoveChild -> { + val children = childrenOf(nodes, ext, op.parent) + if (children == null || !children.remove(op.child)) { + throw LuiBackendException("child is not attached to parent") + } + if (op.index < 0 || op.index > children.size) { + throw LuiBackendException("child index is out of bounds") + } + children.add(op.index, op.child) + } + } + } + + private fun isDescendant( + nodes: Map, + ext: Map, + target: Int, + root: Int, + ): Boolean { + var cursor = parentOf(nodes, ext, target) + while (cursor != null) { + if (cursor == root) return true + cursor = parentOf(nodes, ext, cursor) + } + return false + } + + private fun validateChildRelationship( + nodes: Map, + ext: Map, + parentID: Int, + childID: Int, + ) { + if (nodes[childID]?.kind == LuiNodeKind.root) { + throw LuiBackendException("runtime root cannot be nested") + } + val transparentChild = ext[childID] + if (transparentChild != null && + registry.registration(transparentChild.identifier)?.isTweak == true + ) { + if (transparentChild.children.size != 1) { + throw LuiBackendException("platform tweak requires exactly one child") + } + validateChildRelationship(nodes, ext, parentID, transparentChild.children.single()) + return + } + val parent = nodes[parentID] + val child = nodes[childID] + if (parent != null && child == null) { + if (!LuiKindRules.acceptsExtensionChildren(parent.kind)) { + throw LuiBackendException("standard node cannot contain extension") + } + return + } + val extensionParent = ext[parentID] + val extensionChild = ext[childID] + if (extensionParent != null) { + val registration = registry.registration(extensionParent.identifier)!! + if (registration.isTweak) { + if (extensionParent.children.isNotEmpty()) { + throw LuiBackendException("platform tweak requires exactly one child") + } + return + } + if (child != null) { + if (!registration.acceptsStandardChildren) { + throw LuiBackendException("extension does not accept standard children") + } + return + } + if (extensionChild == null || + !registration.childIdentifiers.contains(extensionChild.identifier) + ) { + throw LuiBackendException("extension child relationship is not registered") + } + return + } + if (parent == null || child == null) { + throw LuiBackendException("unknown parent or child node") + } + if (!LuiKindRules.canContainChildren(parent.kind)) { + throw LuiBackendException("parent cannot contain child") + } + if ((parent.kind == LuiNodeKind.dropdownMenu || parent.kind == LuiNodeKind.contextMenu) && + child.kind != LuiNodeKind.menuItem && + child.kind != LuiNodeKind.menuTrigger && + child.kind != LuiNodeKind.divider + ) { + throw LuiBackendException("menu accepts only menu-item or separator children") + } + if (parent.kind == LuiNodeKind.menuTrigger && child.kind != LuiNodeKind.dropdownMenu) { + throw LuiBackendException("menu-trigger accepts only a dropdown-menu child") + } + if (parent.kind == LuiNodeKind.menuItem && + child.kind != LuiNodeKind.contextMenu && + child.kind != LuiNodeKind.dropdownMenu + ) { + throw LuiBackendException("menu-item accepts only nested menu metadata") + } + if (parent.kind != LuiNodeKind.menuItem && + LuiKindRules.isContextMenuLeafHost(parent.kind) && + child.kind != LuiNodeKind.contextMenu + ) { + throw LuiBackendException("interactive leaf accepts only context-menu metadata") + } + if (parent.kind == LuiNodeKind.table && child.kind != LuiNodeKind.tableRow) { + throw LuiBackendException("table can contain only table-row") + } + if (parent.kind == LuiNodeKind.tableRow && child.kind != LuiNodeKind.tableCell) { + throw LuiBackendException("table-row can contain only table-cell") + } + if (parent.kind == LuiNodeKind.tree && !LuiKindRules.isTreeRowKind(child.kind)) { + throw LuiBackendException("tree accepts only row containers") + } + if (parent.kind == LuiNodeKind.stepper && child.kind != LuiNodeKind.step) { + throw LuiBackendException("stepper accepts only step children") + } + if (parent.kind == LuiNodeKind.timeline && child.kind != LuiNodeKind.timelineItem) { + throw LuiBackendException("timeline accepts only timeline-item children") + } + if (parent.kind == LuiNodeKind.bottomTabs && child.kind != LuiNodeKind.bottomTab) { + throw LuiBackendException("bottom-tabs accepts only bottom-tab children") + } + if (parent.kind == LuiNodeKind.bottomTab && child.kind == LuiNodeKind.bottomTab) { + throw LuiBackendException("bottom-tab cannot directly contain bottom-tab") + } + if (parent.kind == LuiNodeKind.inputGroup && + child.kind != LuiNodeKind.textarea && + child.kind != LuiNodeKind.inputGroupActions + ) { + throw LuiBackendException( + "input-group accepts only textarea and input-group-actions children", + ) + } + if (parent.kind == LuiNodeKind.toolbar && !LuiKindRules.isToolbarChild(child.kind)) { + throw LuiBackendException("toolbar accepts only interactive controls and dividers") + } + if (parent.kind == LuiNodeKind.listSection && + child.kind != LuiNodeKind.listItem && + child.kind != LuiNodeKind.listSectionHeader && + child.kind != LuiNodeKind.listSectionFooter + ) { + throw LuiBackendException( + "list-section accepts only list rows and section header/footer", + ) + } + if ((child.kind == LuiNodeKind.listSectionHeader || + child.kind == LuiNodeKind.listSectionFooter) && + parent.kind != LuiNodeKind.listSection + ) { + throw LuiBackendException("list-section header/footer requires a list-section parent") + } + if (child.kind == LuiNodeKind.listSection && parent.kind != LuiNodeKind.list) { + throw LuiBackendException("list-section requires a direct list parent") + } + if (child.kind == LuiNodeKind.swipeActions && parent.kind != LuiNodeKind.listItem) { + throw LuiBackendException("swipe-actions requires a list-item parent") + } + if (parent.kind == LuiNodeKind.swipeActions && child.kind != LuiNodeKind.swipeAction) { + throw LuiBackendException("swipe-actions accepts only swipe-action children") + } + if (child.kind == LuiNodeKind.swipeAction && parent.kind != LuiNodeKind.swipeActions) { + throw LuiBackendException("swipe-action requires a swipe-actions parent") + } + } + + private fun validateStates(nodes: Map) { + for (state in nodes.values) { + if (state.kind == LuiNodeKind.root) { + if (state.parent != null) { + throw LuiBackendException("runtime root cannot have a parent") + } + if (state.children.size != 1) { + throw LuiBackendException("runtime root requires exactly one child") + } + } + validateSizeAxis(state, "width", "min-width", "max-width") + validateSizeAxis(state, "height", "min-height", "max-height") + } + } + + private fun validateSizeAxis(state: LuiNode, size: String, min: String, max: String) { + val sizeValue = state.properties[size]?.intValue + val minValue = state.properties[min]?.intValue + val maxValue = state.properties[max]?.intValue + if (sizeValue != null && minValue != null && sizeValue < minValue) { + throw LuiBackendException("$size below $min") + } + if (sizeValue != null && maxValue != null && sizeValue > maxValue) { + throw LuiBackendException("$size above $max") + } + if (minValue != null && maxValue != null && minValue > maxValue) { + throw LuiBackendException("$min above $max") + } + } +} diff --git a/platform/android/lui/src/main/kotlin/dev/lui/LuiTheme.kt b/platform/android/lui/src/main/kotlin/dev/lui/LuiTheme.kt new file mode 100644 index 00000000..4dcb11d5 --- /dev/null +++ b/platform/android/lui/src/main/kotlin/dev/lui/LuiTheme.kt @@ -0,0 +1,201 @@ +package dev.lui + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.ColorScheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.compositionLocalOf +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.jsonPrimitive + +/** + * LUI default theme — the "默认好看" surface. Views are authored with + * semantic tokens ("surface-container-high", "muted-foreground", "glass", + * "accent", ...); this file maps them onto a Material 3 colorScheme with + * carefully chosen defaults in both light and dark modes, mirroring + * `LUIThemePolicy.swift` (Apple) and the Flutter backend's `_color` table + * so all three hosts resolve the same token to an equivalent color. + */ + +/** Semantic-color overrides inherited down the tree (`theme` prop). */ +val LocalLuiSemanticColors = compositionLocalOf> { emptyMap() } + +/** Effective dark-mode flag inside the LUI tree (`theme-mode` prop). */ +val LocalLuiDarkTheme = compositionLocalOf { false } + +/** Hex resolver shared by color-name props and theme tokens. Accepts + * `#rgb`, `#rgba`, `#rrggbb` and `#rrggbbaa` (CSS alpha-suffix). + * Returns null for non-hex names. */ +fun luiHexColor(name: String): Color? { + var value = if (name.startsWith("#")) name.substring(1) else name + if (value.length == 3 || value.length == 4) { + value = value.map { "$it$it" }.joinToString("") + } + if (value.length == 6) value = "ff$value" + if (value.length != 8) return null + val rgba = value.toULongOrNull(16) ?: return null + // CSS hex is #rrggbbaa; Color() takes ARGB. + val argb = ((rgba and 0xffUL) shl 24) or (rgba shr 8) + return Color(argb.toLong()) +} + +/** Resolves a semantic color name against the inherited token table then + * the Material 3 scheme, mirroring the Flutter `_color` mapping so both + * mobile hosts agree. [foreground] picks the "on-*" variant for pair + * tokens like `accent`/`secondary`/`warning`/`success`/`error`. */ +@Composable +fun luiThemeColor(name: String?, foreground: Boolean = false): Color? { + if (name == null) return null + val lower = name.lowercase() + LocalLuiSemanticColors.current[lower]?.let { return it } + val scheme = MaterialTheme.colorScheme + return when (lower) { + "transparent" -> Color.Transparent + "background" -> scheme.surface + "foreground" -> scheme.onSurface + "surface" -> scheme.surface + "surface-container-lowest" -> scheme.surfaceContainerLowest + "surface-container-low" -> scheme.surfaceContainerLow + "surface-container" -> scheme.surfaceContainer + "surface-container-high" -> scheme.surfaceContainerHigh + "surface-container-highest" -> scheme.surfaceContainerHighest + "surface-variant" -> scheme.surfaceContainerHighest + "autocomplete-row-background" -> scheme.surfaceContainerHigh + "glass" -> scheme.surface.copy(alpha = 0.75f) + "glass-fallback" -> scheme.surface.copy(alpha = 0.9f) + // Apple Material.bar approximation: mostly-opaque theme surface. + "bar" -> scheme.surface.copy(alpha = 0.85f) + "primary" -> scheme.primary + "primary-foreground" -> scheme.onPrimary + "accent" -> if (foreground) scheme.primary else scheme.secondaryContainer + "accent-foreground" -> + if (foreground) scheme.onPrimary else scheme.onSecondaryContainer + "secondary" -> + if (foreground) scheme.onSurfaceVariant else scheme.secondaryContainer + "secondary-foreground" -> scheme.onSecondaryContainer + "muted" -> scheme.surfaceContainerHigh + "muted-foreground" -> scheme.onSurfaceVariant + "destructive" -> scheme.error + "destructive-foreground" -> scheme.onError + "success" -> scheme.tertiaryContainer + "success-foreground" -> scheme.onTertiaryContainer + "warning" -> scheme.secondaryContainer + "warning-foreground" -> scheme.onSecondaryContainer + "error" -> scheme.errorContainer + "error-foreground" -> scheme.onErrorContainer + "border" -> scheme.outlineVariant + "black" -> Color.Black + "white" -> Color.White + "red" -> Color(0xFFB3261E) + "blue" -> Color(0xFF0061A4) + "green" -> Color(0xFF2E7D32) + else -> luiHexColor(lower) ?: Color.Transparent + } +} + +/** LUI's Material 3 defaults. The stock schemes already carry the M3 + * baseline palette (primary #6750A4 — the same accent the web backend + * uses for `data-lui-platform="android"`), so the defaults are genuinely + * Material-3 quality with no per-app tuning. Hosts can pass their own + * schemes to [LuiTheme]. */ +object LuiThemeDefaults { + fun lightScheme(): ColorScheme = lightColorScheme() + fun darkScheme(): ColorScheme = darkColorScheme() +} + +/** + * Applies the LUI theme: a Material 3 `MaterialTheme` plus the semantic + * token CompositionLocals LUI renderers resolve through. [dark] selects + * the scheme (`null` follows the system); [semanticColors] are merged + * over inherited tokens; [light]/[darkScheme] let a host override the + * base palette. + */ +@Composable +fun LuiTheme( + dark: Boolean? = null, + semanticColors: Map = emptyMap(), + light: ColorScheme = LuiThemeDefaults.lightScheme(), + darkScheme: ColorScheme = LuiThemeDefaults.darkScheme(), + content: @Composable () -> Unit, +) { + val effectiveDark = dark ?: isSystemInDarkTheme() + MaterialTheme(colorScheme = if (effectiveDark) darkScheme else light) { + val inherited = LocalLuiSemanticColors.current + CompositionLocalProvider( + LocalLuiDarkTheme provides effectiveDark, + LocalLuiSemanticColors provides inherited + semanticColors, + ) { + content() + } + } +} + +object LuiThemeTokenDecoder { + private val json = Json { ignoreUnknownKeys = true } + + /** Parses the `theme` wire prop into semantic-color entries consulted + * before platform defaults. A token value is either a color string + * used in both modes or a `{"light": ..., "dark": ...}` object picked + * by [dark]. Entries that do not decode to a color are ignored so + * non-color tokens can join the table later without breaking hosts. */ + fun colors(jsonSource: String?, dark: Boolean): Map { + if (jsonSource.isNullOrEmpty()) return emptyMap() + val obj = try { + json.parseToJsonElement(jsonSource) as? JsonObject + } catch (_: Exception) { + null + } ?: return emptyMap() + val colors = mutableMapOf() + for ((key, element) in obj) { + val raw = when { + element is kotlinx.serialization.json.JsonPrimitive && element.isString -> + element.content + element is JsonObject -> + element[if (dark) "dark" else "light"]?.jsonPrimitive?.content + else -> null + } ?: continue + val color = luiHexColor(raw) ?: continue + colors[key.lowercase()] = color + } + return colors + } + + fun colorScheme(value: String?): Boolean? = when (value) { + "dark" -> true + "light" -> false + else -> null + } +} + +object LuiTypography { + @Composable + fun headingStyle(level: Int): TextStyle { + val theme = MaterialTheme.typography + return when (level) { + 1 -> theme.headlineLarge + 2 -> theme.headlineMedium + 3 -> theme.titleLarge + 4 -> theme.titleMedium + 5 -> theme.titleSmall + else -> theme.labelLarge + } + } + + @Composable + fun textStyleForSize(size: String?): TextStyle { + val theme = MaterialTheme.typography + return when (size) { + "sm" -> theme.bodySmall + "lg" -> theme.titleMedium + "heading" -> theme.headlineSmall + "display" -> theme.displaySmall + else -> theme.bodyMedium + } + } +} diff --git a/platform/android/lui/src/main/kotlin/dev/lui/LuiWire.kt b/platform/android/lui/src/main/kotlin/dev/lui/LuiWire.kt new file mode 100644 index 00000000..97b462ea --- /dev/null +++ b/platform/android/lui/src/main/kotlin/dev/lui/LuiWire.kt @@ -0,0 +1,167 @@ +package dev.lui + +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.boolean +import kotlinx.serialization.json.booleanOrNull +import kotlinx.serialization.json.doubleOrNull +import kotlinx.serialization.json.int +import kotlinx.serialization.json.intOrNull +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.longOrNull + +class LuiBackendException(message: String) : Exception(message) + +/** Wire-level property value, mirroring OCaml `wire_value`. */ +sealed class LuiWireValue { + data class Str(val value: String) : LuiWireValue() + data class Bool(val value: Boolean) : LuiWireValue() + data class IntValue(val value: Int) : LuiWireValue() + data class DoubleValue(val value: Double) : LuiWireValue() + + val stringValue: String? get() = (this as? Str)?.value + val boolValue: Boolean? get() = (this as? Bool)?.value + val intValue: Int? get() = (this as? IntValue)?.value + val doubleValue: Double? + get() = when (this) { + is DoubleValue -> value + is IntValue -> value.toDouble() + else -> null + } + val numberValue: Double? + get() = when (this) { + is DoubleValue -> value + is IntValue -> value.toDouble() + else -> null + } + + /** JSON literal form, for extension-event payloads. */ + fun toJsonLiteral(): String = when (this) { + is Str -> JsonPrimitive(value).toString() + is Bool -> value.toString() + is IntValue -> value.toString() + is DoubleValue -> value.toString() + } + + companion object { + fun of(element: JsonElement): LuiWireValue { + val primitive = element.jsonPrimitive + primitive.booleanOrNull?.let { return Bool(it) } + primitive.longOrNull?.let { return IntValue(it.toInt()) } + primitive.doubleOrNull?.let { return DoubleValue(it) } + if (primitive.isString) return Str(primitive.content) + throw LuiBackendException("unsupported wire value") + } + + fun toJson(value: LuiWireValue): Any = when (value) { + is Str -> value.value + is Bool -> value.value + is IntValue -> value.value + is DoubleValue -> value.value + } + } +} + +sealed class LuiPatchOp { + data class CreateNode(val id: Int, val kind: LuiNodeKind) : LuiPatchOp() + data class CreateExtension(val id: Int, val identifier: String, val fingerprint: String) : LuiPatchOp() + data class DropNode(val id: Int) : LuiPatchOp() + data class SetProp(val id: Int, val property: String, val value: LuiWireValue) : LuiPatchOp() + data class RemoveProp(val id: Int, val property: String) : LuiPatchOp() + data class SetExtensionProp(val id: Int, val property: String, val value: LuiWireValue) : LuiPatchOp() + data class RemoveExtensionProp(val id: Int, val property: String) : LuiPatchOp() + data class InsertChild(val parent: Int, val child: Int, val index: Int) : LuiPatchOp() + data class RemoveChild(val parent: Int, val child: Int) : LuiPatchOp() + data class MoveChild(val parent: Int, val child: Int, val index: Int) : LuiPatchOp() + + companion object { + private fun requiredInt(obj: JsonObject, key: String): Int = + obj[key]?.jsonPrimitive?.intOrNull + ?: throw LuiBackendException("missing or invalid '$key'") + + private fun requiredString(obj: JsonObject, key: String): String = + obj[key]?.jsonPrimitive?.takeIf { it.isString }?.content + ?: throw LuiBackendException("missing or invalid '$key'") + + fun of(element: JsonElement): LuiPatchOp { + val obj = element as? JsonObject + ?: throw LuiBackendException("operation must be an object") + return when (val op = obj["op"]?.jsonPrimitive?.content) { + "create-node" -> CreateNode( + id = requiredInt(obj, "id"), + kind = LuiNodeKind.fromWire(requiredString(obj, "kind")) + ?: throw LuiBackendException("unknown node kind"), + ) + "create-extension" -> CreateExtension( + id = requiredInt(obj, "id"), + identifier = requiredString(obj, "identifier"), + fingerprint = requiredString(obj, "fingerprint"), + ) + "drop-node" -> DropNode(id = requiredInt(obj, "id")) + "set-prop" -> SetProp( + id = requiredInt(obj, "id"), + property = requiredString(obj, "property"), + value = LuiWireValue.of( + obj["value"] ?: throw LuiBackendException("missing 'value'"), + ), + ) + "remove-prop" -> RemoveProp( + id = requiredInt(obj, "id"), + property = requiredString(obj, "property"), + ) + "set-extension-prop" -> SetExtensionProp( + id = requiredInt(obj, "id"), + property = requiredString(obj, "property"), + value = LuiWireValue.of( + obj["value"] ?: throw LuiBackendException("missing 'value'"), + ), + ) + "remove-extension-prop" -> RemoveExtensionProp( + id = requiredInt(obj, "id"), + property = requiredString(obj, "property"), + ) + "insert-child" -> InsertChild( + parent = requiredInt(obj, "parent"), + child = requiredInt(obj, "child"), + index = requiredInt(obj, "index"), + ) + "remove-child" -> RemoveChild( + parent = requiredInt(obj, "parent"), + child = requiredInt(obj, "child"), + ) + "move-child" -> MoveChild( + parent = requiredInt(obj, "parent"), + child = requiredInt(obj, "child"), + index = requiredInt(obj, "index"), + ) + else -> throw LuiBackendException("unknown patch operation: $op") + } + } + } +} + +class LuiPatchBatch(val generation: Int, val ops: List) { + companion object { + private val json = Json { ignoreUnknownKeys = true } + + fun parse(source: String): LuiPatchBatch { + val decoded = try { + json.parseToJsonElement(source) + } catch (error: Exception) { + throw LuiBackendException(error.message ?: "invalid patch batch") + } + val obj = decoded as? JsonObject + ?: throw LuiBackendException("patch batch must be an object") + val generation = obj["generation"]?.jsonPrimitive?.intOrNull + ?: throw LuiBackendException("missing 'generation'") + val ops = (obj["ops"] as? kotlinx.serialization.json.JsonArray) + ?.map(LuiPatchOp::of) + ?: throw LuiBackendException("missing 'ops'") + return LuiPatchBatch(generation, ops) + } + } +} diff --git a/platform/android/lui/src/main/kotlin/dev/lui/LuiWireSchema.kt b/platform/android/lui/src/main/kotlin/dev/lui/LuiWireSchema.kt new file mode 100644 index 00000000..c1200bc5 --- /dev/null +++ b/platform/android/lui/src/main/kotlin/dev/lui/LuiWireSchema.kt @@ -0,0 +1,258 @@ +// Generated from schema/components.json. Do not edit by hand. +package dev.lui + +enum class LuiNodeKind(val wireName: String, val isContainer: Boolean) { + root("root", true), + row("row", true), + column("column", true), + grid("grid", true), + stack("stack", true), + edgeInset("edge-inset", true), + overlay("overlay", true), + viewThatFits("view-that-fits", true), + panel("panel", true), + card("card", true), + alert("alert", true), + bubble("bubble", true), + box("box", true), + text("text", true), + heading("heading", true), + paragraph("paragraph", true), + label("label", true), + button("button", true), + toggleButton("toggle-button", false), + toggle("toggle", false), + radioGroup("radio-group", true), + radio("radio", false), + slider("slider", false), + numberStepper("number-stepper", false), + textField("text-field", false), + secureField("secure-field", false), + input("input", false), + searchField("search-field", false), + textarea("textarea", false), + checkbox("checkbox", false), + switchControl("switch", false), + progress("progress", false), + divider("divider", false), + scroll("scroll", true), + list("list", true), + virtualList("virtual-list", true), + tabs("tabs", true), + bottomTabs("bottom-tabs", true), + bottomTab("bottom-tab", true), + buttonGroup("button-group", true), + toggleGroup("toggle-group", true), + spacer("spacer", false), + spinner("spinner", false), + icon("icon", false), + select("select", false), + combobox("combobox", false), + dropdownMenu("dropdown-menu", true), + contextMenu("context-menu", true), + menuItem("menu-item", true), + menuTrigger("menu-trigger", true), + listItem("list-item", true), + avatar("avatar", false), + image("image", false), + mediaSurface("media-surface", false), + stepper("stepper", true), + step("step", false), + timeline("timeline", true), + timelineItem("timeline-item", false), + inputGroup("input-group", true), + inputGroupActions("input-group-actions", true), + breadcrumb("breadcrumb", true), + pagination("pagination", true), + accordion("accordion", true), + table("table", true), + tableRow("table-row", true), + tableCell("table-cell", false), + tree("tree", true), + resizable("resizable", true), + split("split", true), + dialog("dialog", true), + drawer("drawer", true), + sheet("sheet", true), + tooltip("tooltip", false), + toast("toast", true), + toolbar("toolbar", true), + statusBar("status-bar", false), + listSection("list-section", true), + listSectionHeader("list-section-header", true), + listSectionFooter("list-section-footer", true), + swipeActions("swipe-actions", true), + swipeAction("swipe-action", false), + filePicker("file-picker", true), + kbd("kbd", false), + link("link", true), + fileImage("file-image", false), + filePreview("file-preview", false), + br("br", false), + popover("popover", true), + ; + + companion object { + private val byWire: Map = entries.associateBy { it.wireName } + + fun fromWire(wire: String): LuiNodeKind? = byWire[wire] + } +} + +enum class LuiProperty(val wireName: String) { + text("text"), + enabled("enabled"), + gap("gap"), + main("main"), + cross("cross"), + grow("grow"), + columns("columns"), + padding("padding"), + paddingHorizontal("padding-horizontal"), + paddingVertical("padding-vertical"), + background("background"), + foreground("foreground"), + borderColor("border-color"), + borderWidth("border-width"), + cornerRadius("corner-radius"), + width("width"), + height("height"), + minWidth("min-width"), + maxWidth("max-width"), + minHeight("min-height"), + maxHeight("max-height"), + containerRelativeFrame("container-relative-frame"), + containerRelativeFrameInset("container-relative-frame-inset"), + placeholder("placeholder"), + accessibilityLabel("accessibility-label"), + accessibilityIdentifier("accessibility-identifier"), + styleClass("style-class"), + headingLevel("heading-level"), + checked("checked"), + progressValue("value"), + orientation("orientation"), + placement("placement"), + size("size"), + propName("name"), + variant("variant"), + icon("icon"), + iconPlacement("icon-placement"), + selected("selected"), + autofocus("autofocus"), + submitOnEnter("submit-on-enter"), + longPressEnabled("long-press-enabled"), + changeEnabled("change-enabled"), + toggleEnabled("toggle-enabled"), + pressEnabled("press-enabled"), + submitEnabled("submit-enabled"), + doublePressEnabled("double-press-enabled"), + appearEnabled("appear-enabled"), + image("image"), + surface("surface"), + active("active"), + title("title"), + description("description"), + meta("meta"), + indicator("indicator"), + connector("connector"), + sourceX("source-x"), + sourceY("source-y"), + sourceWidth("source-width"), + sourceHeight("source-height"), + anchor("anchor"), + anchorAlignment("anchor-alignment"), + anchorOffset("anchor-offset"), + tooltipDelay("tooltip-delay"), + duration("duration"), + textAlignment("text-alignment"), + role("role"), + treeLevel("tree-level"), + expanded("expanded"), + resizeDuration("resize-duration"), + resizeEasing("resize-easing"), + resizeOrigin("resize-origin"), + theme("theme"), + themeMode("theme-mode"), + key("key"), + separator("separator"), + style("style"), + scrollTarget("scroll-target"), + scrollAnchor("scroll-anchor"), + scrollToken("scroll-token"), + scrollAnimated("scroll-animated"), + trackVisibleRange("track-visible-range"), + edge("edge"), + request("request"), + types("types"), + multiple("multiple"), + source("source"), + completion("completion"), + minValue("min"), + maxValue("max"), + stepValue("step"), + detents("detents"), + sizing("sizing"), + path("path"), + url("url"), + maxPixelSize("max-pixel-size"), + imageFit("image-fit"), + visible("visible"), + alignment("alignment"), + pointerEnabled("pointer-enabled"), + dataAttrs("data-attrs"), + asTag("as"), + alt("alt"), + loading("loading"), + referrerPolicy("referrer-policy"), + target("target"), + opacity("opacity"), + display("display"), + tooltip("tooltip"), + tooltipKeys("tooltip-keys"), + inputType("input-type"), + pickerAccept("accept"), + pickerDirectory("directory"), + popupX("x"), + popupY("y"), + availableHeight("available-height"), + ; + + companion object { + private val byWire: Map = entries.associateBy { it.wireName } + + fun fromWire(wire: String): LuiProperty? = byWire[wire] + } +} + +/** + * Mirrors schema/components.json kindProperties: kinds listed here admit + * ONLY the given kind-specific props (plus the shared/common set), instead of + * the default "common set" behaviour for unlisted kinds. Absence means the + * kind follows the common-property rules in the backend. + */ +object LuiSchemaMatrix { + val restrictive: Map> = mapOf( + LuiNodeKind.accordion to setOf(LuiProperty.text, LuiProperty.selected, LuiProperty.toggleEnabled, LuiProperty.height, LuiProperty.pointerEnabled), + LuiNodeKind.stepper to setOf(LuiProperty.active, LuiProperty.accessibilityLabel, LuiProperty.pointerEnabled), + LuiNodeKind.step to setOf(LuiProperty.text, LuiProperty.pointerEnabled), + LuiNodeKind.timeline to setOf(LuiProperty.gap, LuiProperty.grow, LuiProperty.accessibilityLabel, LuiProperty.pointerEnabled), + LuiNodeKind.timelineItem to setOf(LuiProperty.title, LuiProperty.description, LuiProperty.meta, LuiProperty.indicator, LuiProperty.icon, LuiProperty.variant, LuiProperty.connector, LuiProperty.selected, LuiProperty.pressEnabled, LuiProperty.pointerEnabled), + LuiNodeKind.inputGroup to setOf(LuiProperty.accessibilityLabel, LuiProperty.width, LuiProperty.height, LuiProperty.minWidth, LuiProperty.grow, LuiProperty.pointerEnabled), + LuiNodeKind.inputGroupActions to setOf(LuiProperty.gap, LuiProperty.pointerEnabled), + LuiNodeKind.toast to setOf(LuiProperty.duration, LuiProperty.accessibilityLabel, LuiProperty.styleClass, LuiProperty.dataAttrs, LuiProperty.pointerEnabled), + LuiNodeKind.toolbar to setOf(LuiProperty.orientation, LuiProperty.accessibilityLabel, LuiProperty.gap, LuiProperty.styleClass, LuiProperty.dataAttrs, LuiProperty.placement, LuiProperty.pointerEnabled), + LuiNodeKind.bottomTabs to setOf(LuiProperty.accessibilityLabel, LuiProperty.styleClass, LuiProperty.dataAttrs, LuiProperty.grow, LuiProperty.width, LuiProperty.height, LuiProperty.minWidth, LuiProperty.maxWidth, LuiProperty.minHeight, LuiProperty.maxHeight, LuiProperty.pointerEnabled), + LuiNodeKind.bottomTab to setOf(LuiProperty.title, LuiProperty.icon, LuiProperty.selected, LuiProperty.enabled, LuiProperty.pressEnabled, LuiProperty.pointerEnabled), + LuiNodeKind.menuTrigger to setOf(LuiProperty.text, LuiProperty.icon, LuiProperty.accessibilityLabel, LuiProperty.enabled, LuiProperty.foreground, LuiProperty.styleClass, LuiProperty.dataAttrs, LuiProperty.pointerEnabled), + LuiNodeKind.listSection to setOf(LuiProperty.key, LuiProperty.separator, LuiProperty.pointerEnabled), + LuiNodeKind.swipeActions to setOf(), + LuiNodeKind.swipeAction to setOf(LuiProperty.text, LuiProperty.icon, LuiProperty.variant, LuiProperty.edge, LuiProperty.enabled, LuiProperty.background, LuiProperty.pressEnabled, LuiProperty.pointerEnabled), + LuiNodeKind.filePicker to setOf(LuiProperty.request, LuiProperty.types, LuiProperty.multiple, LuiProperty.source, LuiProperty.completion, LuiProperty.pickerAccept, LuiProperty.pickerDirectory, LuiProperty.enabled, LuiProperty.appearEnabled, LuiProperty.pointerEnabled), + LuiNodeKind.filePreview to setOf(LuiProperty.path, LuiProperty.pointerEnabled), + LuiNodeKind.kbd to setOf(LuiProperty.text, LuiProperty.styleClass, LuiProperty.dataAttrs, LuiProperty.pointerEnabled), + ) + + val extra: Map> = mapOf( + LuiNodeKind.dialog to setOf(LuiProperty.description), + ) +} diff --git a/platform/android/settings.gradle.kts b/platform/android/settings.gradle.kts new file mode 100644 index 00000000..68eac8c2 --- /dev/null +++ b/platform/android/settings.gradle.kts @@ -0,0 +1,18 @@ +rootProject.name = "lui-android" + +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +dependencyResolutionManagement { + repositories { + google() + mavenCentral() + } +} + +include(":lui") diff --git a/tooling/generate_component_schema.mjs b/tooling/generate_component_schema.mjs index a0c3db0c..7a3f48cd 100644 --- a/tooling/generate_component_schema.mjs +++ b/tooling/generate_component_schema.mjs @@ -371,6 +371,71 @@ ${decoderCases} `; } +function renderKotlin(schema) { + // Kotlin enum entry names reuse the `swift` schema field: the camelCase + // names are legal Kotlin identifiers, so no separate `kotlin` name set + // is needed. The only exception is `name`, which collides with + // Enum.name; it is emitted as `propName`. + const kotlinEnumName = (swift) => + swift === 'name' ? 'propName' : swift; + const kindSwift = new Map(schema.nodeKinds.map(({ wire, swift }) => [wire, kotlinEnumName(swift)])); + const propSwift = new Map(schema.properties.map(({ wire, swift }) => [wire, kotlinEnumName(swift)])); + const nodeKinds = schema.nodeKinds + .map(({ swift, wire, container }) => ` ${kotlinEnumName(swift)}("${wire}", ${container ? 'true' : 'false'}),`) + .join('\n'); + const properties = schema.properties + .map(({ swift, wire }) => ` ${kotlinEnumName(swift)}("${wire}"),`) + .join('\n'); + const matrixEntries = Object.entries(schema.kindProperties ?? {}) + .map(([kindWire, propWires]) => + ` LuiNodeKind.${kindSwift.get(kindWire)} to setOf(${propWires.map((w) => `LuiProperty.${propSwift.get(w)}`).join(', ')}),`) + .join('\n'); + const extraEntries = Object.entries(schema.kindExtraProperties ?? {}) + .map(([kindWire, propWires]) => + ` LuiNodeKind.${kindSwift.get(kindWire)} to setOf(${propWires.map((w) => `LuiProperty.${propSwift.get(w)}`).join(', ')}),`) + .join('\n'); + return `${generatedHeader('//')}package dev.lui + +enum class LuiNodeKind(val wireName: String, val isContainer: Boolean) { +${nodeKinds} + ; + + companion object { + private val byWire: Map = entries.associateBy { it.wireName } + + fun fromWire(wire: String): LuiNodeKind? = byWire[wire] + } +} + +enum class LuiProperty(val wireName: String) { +${properties} + ; + + companion object { + private val byWire: Map = entries.associateBy { it.wireName } + + fun fromWire(wire: String): LuiProperty? = byWire[wire] + } +} + +/** + * Mirrors schema/components.json kindProperties: kinds listed here admit + * ONLY the given kind-specific props (plus the shared/common set), instead of + * the default "common set" behaviour for unlisted kinds. Absence means the + * kind follows the common-property rules in the backend. + */ +object LuiSchemaMatrix { + val restrictive: Map> = mapOf( +${matrixEntries} + ) + + val extra: Map> = mapOf( +${extraEntries} + ) +} +`; +} + function renderRust(schema) { const kindLg = new Map(schema.nodeKinds.map(({ wire, lg }) => [wire, lg])); const propLg = new Map(schema.properties.map(({ wire, lg }) => [wire, lg])); @@ -494,6 +559,7 @@ function artifacts(schema) { ['platform/apple/Sources/LUIAppleBackend/LUIWireSchema.swift', renderSwift(schema)], ['platform/flutter/lib/lui_wire_schema.g.dart', renderDart(schema)], ['platform/gpui/crates/lui-core/src/wire_schema.rs', renderRust(schema)], + ['platform/android/lui/src/main/kotlin/dev/lui/LuiWireSchema.kt', renderKotlin(schema)], ]); } From cd90eb12fc0d1918b45fe14b876b316b2825b3b6 Mon Sep 17 00:00:00 2001 From: Tienson Qin Date: Tue, 6 Oct 2026 10:10:01 +0000 Subject: [PATCH 3/5] feat(android): unit tests + Paparazzi screenshots, compileSdk 35, app fixes - JUnit coverage for wire parse / store semantics / extension registry - Paparazzi snapshots (PIXEL_5, light+dark) feeding patch JSON through applyBatch - compileSdk/targetSdk 35: Paparazzi 1.3.5 breaks on android-36's Build layout - settings.gradle :lui projectDir path fix; vector launcher icon - switch/checkbox inline layout; button labels ellipsize instead of wrapping - AutoMirrored icon deprecations --- .../.cmake/api/v1/query/client-agp/cache-v2 | 0 .../api/v1/query/client-agp/cmakeFiles-v1 | 0 .../api/v1/query/client-agp/codemodel-v2 | 0 .../reply/cache-v2-915560f4f4d79638de20.json | 1219 +++++++++++++++++ .../cmakeFiles-v1-8bcca044c919e60a8a7f.json | 471 +++++++ .../codemodel-v2-200f15b97692ec9c4110.json | 60 + ...irectory-.-Debug-f5ebdc15457944623624.json | 14 + .../reply/index-2026-10-06T10-09-37-0320.json | 92 ++ ...jni_bridge-Debug-1fbc5b070db103f4a523.json | 117 ++ .../.cxx/Debug/406wr1e2/arm64-v8a/.ninja_deps | Bin 0 -> 1760 bytes .../.cxx/Debug/406wr1e2/arm64-v8a/.ninja_log | 3 + .../Debug/406wr1e2/arm64-v8a/CMakeCache.txt | 365 +++++ .../3.22.1-g37088a8/CMakeCCompiler.cmake | 72 + .../CMakeDetermineCompilerABI_C.bin | Bin 0 -> 7968 bytes .../3.22.1-g37088a8/CMakeSystem.cmake | 15 + .../CompilerIdC/CMakeCCompilerId.c | 803 +++++++++++ .../CompilerIdC/CMakeCCompilerId.o | Bin 0 -> 6048 bytes .../arm64-v8a/CMakeFiles/CMakeOutput.log | 129 ++ .../CMakeFiles/TargetDirectories.txt | 3 + .../arm64-v8a/CMakeFiles/cmake.check_cache | 1 + .../lui_jni_bridge_stub.c.o | Bin 0 -> 41056 bytes .../406wr1e2/arm64-v8a/CMakeFiles/rules.ninja | 64 + .../arm64-v8a/additional_project_files.txt | 0 .../arm64-v8a/android_gradle_build.json | 38 + .../arm64-v8a/android_gradle_build_mini.json | 27 + .../.cxx/Debug/406wr1e2/arm64-v8a/build.ninja | 154 +++ .../406wr1e2/arm64-v8a/build_file_index.txt | 1 + .../406wr1e2/arm64-v8a/cmake_install.cmake | 54 + .../406wr1e2/arm64-v8a/compile_commands.json | 7 + .../arm64-v8a/compile_commands.json.bin | Bin 0 -> 922 bytes .../arm64-v8a/configure_fingerprint.bin | 29 + .../arm64-v8a/metadata_generation_command.txt | 18 + .../406wr1e2/arm64-v8a/prefab_config.json | 4 + .../arm64-v8a/symbol_folder_index.txt | 1 + .../app/.cxx/Debug/406wr1e2/hash_key.txt | 25 + .../.cmake/api/v1/query/client-agp/cache-v2 | 0 .../api/v1/query/client-agp/cmakeFiles-v1 | 0 .../api/v1/query/client-agp/codemodel-v2 | 0 .../reply/cache-v2-2212472cb17d9cbd701d.json | 1219 +++++++++++++++++ .../cmakeFiles-v1-365eb2c77d0cb65a5b49.json | 471 +++++++ .../codemodel-v2-eec1c6e44002442862a0.json | 60 + ...irectory-.-Debug-f5ebdc15457944623624.json | 14 + .../reply/index-2026-10-06T10-09-37-0911.json | 92 ++ ...jni_bridge-Debug-a3d79980adbcdc46feee.json | 117 ++ .../.cxx/Debug/406wr1e2/x86_64/.ninja_deps | Bin 0 -> 1760 bytes .../app/.cxx/Debug/406wr1e2/x86_64/.ninja_log | 3 + .../.cxx/Debug/406wr1e2/x86_64/CMakeCache.txt | 365 +++++ .../3.22.1-g37088a8/CMakeCCompiler.cmake | 72 + .../CMakeDetermineCompilerABI_C.bin | Bin 0 -> 6992 bytes .../3.22.1-g37088a8/CMakeSystem.cmake | 15 + .../CompilerIdC/CMakeCCompilerId.c | 803 +++++++++++ .../CompilerIdC/CMakeCCompilerId.o | Bin 0 -> 5424 bytes .../x86_64/CMakeFiles/CMakeOutput.log | 126 ++ .../x86_64/CMakeFiles/TargetDirectories.txt | 3 + .../x86_64/CMakeFiles/cmake.check_cache | 1 + .../lui_jni_bridge_stub.c.o | Bin 0 -> 40168 bytes .../406wr1e2/x86_64/CMakeFiles/rules.ninja | 64 + .../x86_64/additional_project_files.txt | 0 .../406wr1e2/x86_64/android_gradle_build.json | 38 + .../x86_64/android_gradle_build_mini.json | 27 + .../.cxx/Debug/406wr1e2/x86_64/build.ninja | 154 +++ .../406wr1e2/x86_64/build_file_index.txt | 1 + .../Debug/406wr1e2/x86_64/cmake_install.cmake | 54 + .../406wr1e2/x86_64/compile_commands.json | 7 + .../406wr1e2/x86_64/compile_commands.json.bin | Bin 0 -> 918 bytes .../406wr1e2/x86_64/configure_fingerprint.bin | 28 + .../x86_64/metadata_generation_command.txt | 18 + .../Debug/406wr1e2/x86_64/prefab_config.json | 4 + .../406wr1e2/x86_64/symbol_folder_index.txt | 1 + .../debug/arm64-v8a/compile_commands.json | 7 + .../tools/debug/x86_64/compile_commands.json | 7 + .../components/android/app/build.gradle.kts | 4 +- .../android/app/src/main/AndroidManifest.xml | 2 +- .../app/src/main/res/drawable/ic_launcher.xml | 12 + .../components/android/settings.gradle.kts | 2 +- platform/android/lui/build.gradle.kts | 2 +- .../lui/src/main/kotlin/dev/lui/LuiIcons.kt | 6 +- .../src/main/kotlin/dev/lui/LuiNodeView.kt | 16 +- .../test/kotlin/dev/lui/LuiScreenshotTest.kt | 240 ++++ .../src/test/kotlin/dev/lui/LuiStoreTest.kt | 238 ++++ .../src/test/kotlin/dev/lui/LuiWireTest.kt | 122 ++ ...eenshotTest_controlsDark_controls-dark.png | Bin 0 -> 35463 bytes ...nshotTest_controlsLight_controls-light.png | Bin 0 -> 34963 bytes ...creenshotTest_galleryDark_gallery-dark.png | Bin 0 -> 34476 bytes ...eenshotTest_galleryLight_gallery-light.png | Bin 0 -> 34017 bytes 85 files changed, 8188 insertions(+), 13 deletions(-) create mode 100644 examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/.cmake/api/v1/query/client-agp/cache-v2 create mode 100644 examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/.cmake/api/v1/query/client-agp/cmakeFiles-v1 create mode 100644 examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/.cmake/api/v1/query/client-agp/codemodel-v2 create mode 100644 examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/.cmake/api/v1/reply/cache-v2-915560f4f4d79638de20.json create mode 100644 examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/.cmake/api/v1/reply/cmakeFiles-v1-8bcca044c919e60a8a7f.json create mode 100644 examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/.cmake/api/v1/reply/codemodel-v2-200f15b97692ec9c4110.json create mode 100644 examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/.cmake/api/v1/reply/directory-.-Debug-f5ebdc15457944623624.json create mode 100644 examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/.cmake/api/v1/reply/index-2026-10-06T10-09-37-0320.json create mode 100644 examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/.cmake/api/v1/reply/target-lui_jni_bridge-Debug-1fbc5b070db103f4a523.json create mode 100644 examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/.ninja_deps create mode 100644 examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/.ninja_log create mode 100644 examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/CMakeCache.txt create mode 100644 examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/CMakeFiles/3.22.1-g37088a8/CMakeCCompiler.cmake create mode 100755 examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/CMakeFiles/3.22.1-g37088a8/CMakeDetermineCompilerABI_C.bin create mode 100644 examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/CMakeFiles/3.22.1-g37088a8/CMakeSystem.cmake create mode 100644 examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/CMakeFiles/3.22.1-g37088a8/CompilerIdC/CMakeCCompilerId.c create mode 100644 examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/CMakeFiles/3.22.1-g37088a8/CompilerIdC/CMakeCCompilerId.o create mode 100644 examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/CMakeFiles/CMakeOutput.log create mode 100644 examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/CMakeFiles/TargetDirectories.txt create mode 100644 examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/CMakeFiles/cmake.check_cache create mode 100644 examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/CMakeFiles/lui_jni_bridge.dir/lui_jni_bridge_stub.c.o create mode 100644 examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/CMakeFiles/rules.ninja create mode 100644 examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/additional_project_files.txt create mode 100644 examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/android_gradle_build.json create mode 100644 examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/android_gradle_build_mini.json create mode 100644 examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/build.ninja create mode 100644 examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/build_file_index.txt create mode 100644 examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/cmake_install.cmake create mode 100644 examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/compile_commands.json create mode 100644 examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/compile_commands.json.bin create mode 100644 examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/configure_fingerprint.bin create mode 100644 examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/metadata_generation_command.txt create mode 100644 examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/prefab_config.json create mode 100644 examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/symbol_folder_index.txt create mode 100644 examples/components/android/app/.cxx/Debug/406wr1e2/hash_key.txt create mode 100644 examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/.cmake/api/v1/query/client-agp/cache-v2 create mode 100644 examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/.cmake/api/v1/query/client-agp/cmakeFiles-v1 create mode 100644 examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/.cmake/api/v1/query/client-agp/codemodel-v2 create mode 100644 examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/.cmake/api/v1/reply/cache-v2-2212472cb17d9cbd701d.json create mode 100644 examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/.cmake/api/v1/reply/cmakeFiles-v1-365eb2c77d0cb65a5b49.json create mode 100644 examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/.cmake/api/v1/reply/codemodel-v2-eec1c6e44002442862a0.json create mode 100644 examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/.cmake/api/v1/reply/directory-.-Debug-f5ebdc15457944623624.json create mode 100644 examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/.cmake/api/v1/reply/index-2026-10-06T10-09-37-0911.json create mode 100644 examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/.cmake/api/v1/reply/target-lui_jni_bridge-Debug-a3d79980adbcdc46feee.json create mode 100644 examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/.ninja_deps create mode 100644 examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/.ninja_log create mode 100644 examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/CMakeCache.txt create mode 100644 examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/CMakeFiles/3.22.1-g37088a8/CMakeCCompiler.cmake create mode 100755 examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/CMakeFiles/3.22.1-g37088a8/CMakeDetermineCompilerABI_C.bin create mode 100644 examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/CMakeFiles/3.22.1-g37088a8/CMakeSystem.cmake create mode 100644 examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/CMakeFiles/3.22.1-g37088a8/CompilerIdC/CMakeCCompilerId.c create mode 100644 examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/CMakeFiles/3.22.1-g37088a8/CompilerIdC/CMakeCCompilerId.o create mode 100644 examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/CMakeFiles/CMakeOutput.log create mode 100644 examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/CMakeFiles/TargetDirectories.txt create mode 100644 examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/CMakeFiles/cmake.check_cache create mode 100644 examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/CMakeFiles/lui_jni_bridge.dir/lui_jni_bridge_stub.c.o create mode 100644 examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/CMakeFiles/rules.ninja create mode 100644 examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/additional_project_files.txt create mode 100644 examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/android_gradle_build.json create mode 100644 examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/android_gradle_build_mini.json create mode 100644 examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/build.ninja create mode 100644 examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/build_file_index.txt create mode 100644 examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/cmake_install.cmake create mode 100644 examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/compile_commands.json create mode 100644 examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/compile_commands.json.bin create mode 100644 examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/configure_fingerprint.bin create mode 100644 examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/metadata_generation_command.txt create mode 100644 examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/prefab_config.json create mode 100644 examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/symbol_folder_index.txt create mode 100644 examples/components/android/app/.cxx/tools/debug/arm64-v8a/compile_commands.json create mode 100644 examples/components/android/app/.cxx/tools/debug/x86_64/compile_commands.json create mode 100644 examples/components/android/app/src/main/res/drawable/ic_launcher.xml create mode 100644 platform/android/lui/src/test/kotlin/dev/lui/LuiScreenshotTest.kt create mode 100644 platform/android/lui/src/test/kotlin/dev/lui/LuiStoreTest.kt create mode 100644 platform/android/lui/src/test/kotlin/dev/lui/LuiWireTest.kt create mode 100644 platform/android/lui/src/test/snapshots/images/dev.lui_LuiScreenshotTest_controlsDark_controls-dark.png create mode 100644 platform/android/lui/src/test/snapshots/images/dev.lui_LuiScreenshotTest_controlsLight_controls-light.png create mode 100644 platform/android/lui/src/test/snapshots/images/dev.lui_LuiScreenshotTest_galleryDark_gallery-dark.png create mode 100644 platform/android/lui/src/test/snapshots/images/dev.lui_LuiScreenshotTest_galleryLight_gallery-light.png diff --git a/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/.cmake/api/v1/query/client-agp/cache-v2 b/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/.cmake/api/v1/query/client-agp/cache-v2 new file mode 100644 index 00000000..e69de29b diff --git a/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/.cmake/api/v1/query/client-agp/cmakeFiles-v1 b/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/.cmake/api/v1/query/client-agp/cmakeFiles-v1 new file mode 100644 index 00000000..e69de29b diff --git a/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/.cmake/api/v1/query/client-agp/codemodel-v2 b/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/.cmake/api/v1/query/client-agp/codemodel-v2 new file mode 100644 index 00000000..e69de29b diff --git a/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/.cmake/api/v1/reply/cache-v2-915560f4f4d79638de20.json b/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/.cmake/api/v1/reply/cache-v2-915560f4f4d79638de20.json new file mode 100644 index 00000000..9fe54cab --- /dev/null +++ b/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/.cmake/api/v1/reply/cache-v2-915560f4f4d79638de20.json @@ -0,0 +1,1219 @@ +{ + "entries" : + [ + { + "name" : "ANDROID_ABI", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "No help, variable specified on the command line." + } + ], + "type" : "UNINITIALIZED", + "value" : "arm64-v8a" + }, + { + "name" : "ANDROID_NDK", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "No help, variable specified on the command line." + } + ], + "type" : "UNINITIALIZED", + "value" : "/home/ubuntu/android-sdk/ndk/26.1.10909125" + }, + { + "name" : "ANDROID_PLATFORM", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "No help, variable specified on the command line." + } + ], + "type" : "UNINITIALIZED", + "value" : "android-24" + }, + { + "name" : "CMAKE_ADDR2LINE", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Path to a program." + } + ], + "type" : "FILEPATH", + "value" : "/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-addr2line" + }, + { + "name" : "CMAKE_ANDROID_ARCH_ABI", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "No help, variable specified on the command line." + } + ], + "type" : "UNINITIALIZED", + "value" : "arm64-v8a" + }, + { + "name" : "CMAKE_ANDROID_NDK", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "No help, variable specified on the command line." + } + ], + "type" : "UNINITIALIZED", + "value" : "/home/ubuntu/android-sdk/ndk/26.1.10909125" + }, + { + "name" : "CMAKE_AR", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Archiver" + } + ], + "type" : "FILEPATH", + "value" : "/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar" + }, + { + "name" : "CMAKE_ASM_FLAGS", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "Flags used by the compiler during all build types." + } + ], + "type" : "STRING", + "value" : "" + }, + { + "name" : "CMAKE_ASM_FLAGS_DEBUG", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "Flags used by the compiler during debug builds." + } + ], + "type" : "STRING", + "value" : "" + }, + { + "name" : "CMAKE_ASM_FLAGS_RELEASE", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "Flags used by the compiler during release builds." + } + ], + "type" : "STRING", + "value" : "" + }, + { + "name" : "CMAKE_BUILD_TYPE", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "Choose the type of build, options are: None Debug Release RelWithDebInfo MinSizeRel ..." + } + ], + "type" : "STRING", + "value" : "Debug" + }, + { + "name" : "CMAKE_CACHEFILE_DIR", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "This is the directory where this CMakeCache.txt was created" + } + ], + "type" : "INTERNAL", + "value" : "/home/ubuntu/repos/lui/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a" + }, + { + "name" : "CMAKE_CACHE_MAJOR_VERSION", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "Major version of cmake used to create the current loaded cache" + } + ], + "type" : "INTERNAL", + "value" : "3" + }, + { + "name" : "CMAKE_CACHE_MINOR_VERSION", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "Minor version of cmake used to create the current loaded cache" + } + ], + "type" : "INTERNAL", + "value" : "22" + }, + { + "name" : "CMAKE_CACHE_PATCH_VERSION", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "Patch version of cmake used to create the current loaded cache" + } + ], + "type" : "INTERNAL", + "value" : "1" + }, + { + "name" : "CMAKE_COMMAND", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "Path to CMake executable." + } + ], + "type" : "INTERNAL", + "value" : "/home/ubuntu/android-sdk/cmake/3.22.1/bin/cmake" + }, + { + "name" : "CMAKE_CPACK_COMMAND", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "Path to cpack program executable." + } + ], + "type" : "INTERNAL", + "value" : "/home/ubuntu/android-sdk/cmake/3.22.1/bin/cpack" + }, + { + "name" : "CMAKE_CTEST_COMMAND", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "Path to ctest program executable." + } + ], + "type" : "INTERNAL", + "value" : "/home/ubuntu/android-sdk/cmake/3.22.1/bin/ctest" + }, + { + "name" : "CMAKE_CXX_FLAGS", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "Flags used by the compiler during all build types." + } + ], + "type" : "STRING", + "value" : "" + }, + { + "name" : "CMAKE_CXX_FLAGS_DEBUG", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "Flags used by the compiler during debug builds." + } + ], + "type" : "STRING", + "value" : "" + }, + { + "name" : "CMAKE_CXX_FLAGS_RELEASE", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "Flags used by the compiler during release builds." + } + ], + "type" : "STRING", + "value" : "" + }, + { + "name" : "CMAKE_C_COMPILER_AR", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "LLVM archiver" + } + ], + "type" : "FILEPATH", + "value" : "/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar" + }, + { + "name" : "CMAKE_C_COMPILER_RANLIB", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Generate index for LLVM archive" + } + ], + "type" : "FILEPATH", + "value" : "/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ranlib" + }, + { + "name" : "CMAKE_C_FLAGS", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Flags used by the compiler during all build types." + } + ], + "type" : "STRING", + "value" : "" + }, + { + "name" : "CMAKE_C_FLAGS_DEBUG", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Flags used by the compiler during debug builds." + } + ], + "type" : "STRING", + "value" : "" + }, + { + "name" : "CMAKE_C_FLAGS_MINSIZEREL", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Flags used by the C compiler during MINSIZEREL builds." + } + ], + "type" : "STRING", + "value" : "-Os -DNDEBUG" + }, + { + "name" : "CMAKE_C_FLAGS_RELEASE", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Flags used by the compiler during release builds." + } + ], + "type" : "STRING", + "value" : "" + }, + { + "name" : "CMAKE_C_FLAGS_RELWITHDEBINFO", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Flags used by the C compiler during RELWITHDEBINFO builds." + } + ], + "type" : "STRING", + "value" : "-O2 -g -DNDEBUG" + }, + { + "name" : "CMAKE_C_STANDARD_LIBRARIES", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Libraries linked by default with all C applications." + } + ], + "type" : "STRING", + "value" : "-latomic -lm" + }, + { + "name" : "CMAKE_DLLTOOL", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Path to a program." + } + ], + "type" : "FILEPATH", + "value" : "CMAKE_DLLTOOL-NOTFOUND" + }, + { + "name" : "CMAKE_EXECUTABLE_FORMAT", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "Executable file format" + } + ], + "type" : "INTERNAL", + "value" : "ELF" + }, + { + "name" : "CMAKE_EXE_LINKER_FLAGS", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Flags used by the linker." + } + ], + "type" : "STRING", + "value" : "" + }, + { + "name" : "CMAKE_EXE_LINKER_FLAGS_DEBUG", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Flags used by the linker during DEBUG builds." + } + ], + "type" : "STRING", + "value" : "" + }, + { + "name" : "CMAKE_EXE_LINKER_FLAGS_MINSIZEREL", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Flags used by the linker during MINSIZEREL builds." + } + ], + "type" : "STRING", + "value" : "" + }, + { + "name" : "CMAKE_EXE_LINKER_FLAGS_RELEASE", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Flags used by the linker during RELEASE builds." + } + ], + "type" : "STRING", + "value" : "" + }, + { + "name" : "CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Flags used by the linker during RELWITHDEBINFO builds." + } + ], + "type" : "STRING", + "value" : "" + }, + { + "name" : "CMAKE_EXPORT_COMPILE_COMMANDS", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "No help, variable specified on the command line." + } + ], + "type" : "UNINITIALIZED", + "value" : "ON" + }, + { + "name" : "CMAKE_EXTRA_GENERATOR", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "Name of external makefile project generator." + } + ], + "type" : "INTERNAL", + "value" : "" + }, + { + "name" : "CMAKE_GENERATOR", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "Name of generator." + } + ], + "type" : "INTERNAL", + "value" : "Ninja" + }, + { + "name" : "CMAKE_GENERATOR_INSTANCE", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "Generator instance identifier." + } + ], + "type" : "INTERNAL", + "value" : "" + }, + { + "name" : "CMAKE_GENERATOR_PLATFORM", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "Name of generator platform." + } + ], + "type" : "INTERNAL", + "value" : "" + }, + { + "name" : "CMAKE_GENERATOR_TOOLSET", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "Name of generator toolset." + } + ], + "type" : "INTERNAL", + "value" : "" + }, + { + "name" : "CMAKE_HOME_DIRECTORY", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "Source directory with the top level CMakeLists.txt file for this project" + } + ], + "type" : "INTERNAL", + "value" : "/home/ubuntu/repos/lui/examples/components/android/app/src/main/cpp" + }, + { + "name" : "CMAKE_INSTALL_PREFIX", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "Install path prefix, prepended onto install directories." + } + ], + "type" : "PATH", + "value" : "/usr/local" + }, + { + "name" : "CMAKE_INSTALL_SO_NO_EXE", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "Install .so files without execute permission." + } + ], + "type" : "INTERNAL", + "value" : "1" + }, + { + "name" : "CMAKE_LIBRARY_OUTPUT_DIRECTORY", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "No help, variable specified on the command line." + } + ], + "type" : "UNINITIALIZED", + "value" : "/home/ubuntu/repos/lui/examples/components/android/app/build/intermediates/cxx/Debug/406wr1e2/obj/arm64-v8a" + }, + { + "name" : "CMAKE_LINKER", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Path to a program." + } + ], + "type" : "FILEPATH", + "value" : "/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/bin/ld.lld" + }, + { + "name" : "CMAKE_MAKE_PROGRAM", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "No help, variable specified on the command line." + } + ], + "type" : "UNINITIALIZED", + "value" : "/home/ubuntu/android-sdk/cmake/3.22.1/bin/ninja" + }, + { + "name" : "CMAKE_MODULE_LINKER_FLAGS", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Flags used by the linker during the creation of modules." + } + ], + "type" : "STRING", + "value" : "" + }, + { + "name" : "CMAKE_MODULE_LINKER_FLAGS_DEBUG", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Flags used by the linker during the creation of modules during DEBUG builds." + } + ], + "type" : "STRING", + "value" : "" + }, + { + "name" : "CMAKE_MODULE_LINKER_FLAGS_MINSIZEREL", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Flags used by the linker during the creation of modules during MINSIZEREL builds." + } + ], + "type" : "STRING", + "value" : "" + }, + { + "name" : "CMAKE_MODULE_LINKER_FLAGS_RELEASE", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Flags used by the linker during the creation of modules during RELEASE builds." + } + ], + "type" : "STRING", + "value" : "" + }, + { + "name" : "CMAKE_MODULE_LINKER_FLAGS_RELWITHDEBINFO", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Flags used by the linker during the creation of modules during RELWITHDEBINFO builds." + } + ], + "type" : "STRING", + "value" : "" + }, + { + "name" : "CMAKE_NM", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Path to a program." + } + ], + "type" : "FILEPATH", + "value" : "/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-nm" + }, + { + "name" : "CMAKE_NUMBER_OF_MAKEFILES", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "number of local generators" + } + ], + "type" : "INTERNAL", + "value" : "1" + }, + { + "name" : "CMAKE_OBJCOPY", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Path to a program." + } + ], + "type" : "FILEPATH", + "value" : "/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-objcopy" + }, + { + "name" : "CMAKE_OBJDUMP", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Path to a program." + } + ], + "type" : "FILEPATH", + "value" : "/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-objdump" + }, + { + "name" : "CMAKE_PLATFORM_INFO_INITIALIZED", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "Platform information initialized" + } + ], + "type" : "INTERNAL", + "value" : "1" + }, + { + "name" : "CMAKE_PROJECT_DESCRIPTION", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "Value Computed by CMake" + } + ], + "type" : "STATIC", + "value" : "" + }, + { + "name" : "CMAKE_PROJECT_HOMEPAGE_URL", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "Value Computed by CMake" + } + ], + "type" : "STATIC", + "value" : "" + }, + { + "name" : "CMAKE_PROJECT_NAME", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "Value Computed by CMake" + } + ], + "type" : "STATIC", + "value" : "lui_components_bridge" + }, + { + "name" : "CMAKE_RANLIB", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Ranlib" + } + ], + "type" : "FILEPATH", + "value" : "/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ranlib" + }, + { + "name" : "CMAKE_READELF", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Path to a program." + } + ], + "type" : "FILEPATH", + "value" : "/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-readelf" + }, + { + "name" : "CMAKE_ROOT", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "Path to CMake installation." + } + ], + "type" : "INTERNAL", + "value" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22" + }, + { + "name" : "CMAKE_RUNTIME_OUTPUT_DIRECTORY", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "No help, variable specified on the command line." + } + ], + "type" : "UNINITIALIZED", + "value" : "/home/ubuntu/repos/lui/examples/components/android/app/build/intermediates/cxx/Debug/406wr1e2/obj/arm64-v8a" + }, + { + "name" : "CMAKE_SHARED_LINKER_FLAGS", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Flags used by the linker during the creation of dll's." + } + ], + "type" : "STRING", + "value" : "" + }, + { + "name" : "CMAKE_SHARED_LINKER_FLAGS_DEBUG", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Flags used by the linker during the creation of shared libraries during DEBUG builds." + } + ], + "type" : "STRING", + "value" : "" + }, + { + "name" : "CMAKE_SHARED_LINKER_FLAGS_MINSIZEREL", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Flags used by the linker during the creation of shared libraries during MINSIZEREL builds." + } + ], + "type" : "STRING", + "value" : "" + }, + { + "name" : "CMAKE_SHARED_LINKER_FLAGS_RELEASE", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Flags used by the linker during the creation of shared libraries during RELEASE builds." + } + ], + "type" : "STRING", + "value" : "" + }, + { + "name" : "CMAKE_SHARED_LINKER_FLAGS_RELWITHDEBINFO", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Flags used by the linker during the creation of shared libraries during RELWITHDEBINFO builds." + } + ], + "type" : "STRING", + "value" : "" + }, + { + "name" : "CMAKE_SKIP_INSTALL_RPATH", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "If set, runtime paths are not added when installing shared libraries, but are added when building." + } + ], + "type" : "BOOL", + "value" : "NO" + }, + { + "name" : "CMAKE_SKIP_RPATH", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "If set, runtime paths are not added when using shared libraries." + } + ], + "type" : "BOOL", + "value" : "NO" + }, + { + "name" : "CMAKE_STATIC_LINKER_FLAGS", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Flags used by the linker during the creation of static libraries during all build types." + } + ], + "type" : "STRING", + "value" : "" + }, + { + "name" : "CMAKE_STATIC_LINKER_FLAGS_DEBUG", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Flags used by the linker during the creation of static libraries during DEBUG builds." + } + ], + "type" : "STRING", + "value" : "" + }, + { + "name" : "CMAKE_STATIC_LINKER_FLAGS_MINSIZEREL", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Flags used by the linker during the creation of static libraries during MINSIZEREL builds." + } + ], + "type" : "STRING", + "value" : "" + }, + { + "name" : "CMAKE_STATIC_LINKER_FLAGS_RELEASE", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Flags used by the linker during the creation of static libraries during RELEASE builds." + } + ], + "type" : "STRING", + "value" : "" + }, + { + "name" : "CMAKE_STATIC_LINKER_FLAGS_RELWITHDEBINFO", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Flags used by the linker during the creation of static libraries during RELWITHDEBINFO builds." + } + ], + "type" : "STRING", + "value" : "" + }, + { + "name" : "CMAKE_STRIP", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "Strip" + } + ], + "type" : "FILEPATH", + "value" : "/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip" + }, + { + "name" : "CMAKE_SYSTEM_NAME", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "No help, variable specified on the command line." + } + ], + "type" : "UNINITIALIZED", + "value" : "Android" + }, + { + "name" : "CMAKE_SYSTEM_VERSION", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "No help, variable specified on the command line." + } + ], + "type" : "UNINITIALIZED", + "value" : "24" + }, + { + "name" : "CMAKE_TOOLCHAIN_FILE", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "The CMake toolchain file" + } + ], + "type" : "FILEPATH", + "value" : "/home/ubuntu/android-sdk/ndk/26.1.10909125/build/cmake/android.toolchain.cmake" + }, + { + "name" : "CMAKE_UNAME", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "uname command" + } + ], + "type" : "INTERNAL", + "value" : "/usr/bin/uname" + }, + { + "name" : "CMAKE_VERBOSE_MAKEFILE", + "properties" : + [ + { + "name" : "ADVANCED", + "value" : "1" + }, + { + "name" : "HELPSTRING", + "value" : "If this value is on, makefiles will be generated without the .SILENT directive, and all commands will be echoed to the console during the make. This is useful for debugging only. With Visual Studio IDE projects all commands are done without /nologo." + } + ], + "type" : "BOOL", + "value" : "FALSE" + }, + { + "name" : "log_lib", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "Path to a library." + } + ], + "type" : "FILEPATH", + "value" : "/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/aarch64-linux-android/24/liblog.so" + }, + { + "name" : "lui_components_bridge_BINARY_DIR", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "Value Computed by CMake" + } + ], + "type" : "STATIC", + "value" : "/home/ubuntu/repos/lui/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a" + }, + { + "name" : "lui_components_bridge_IS_TOP_LEVEL", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "Value Computed by CMake" + } + ], + "type" : "STATIC", + "value" : "ON" + }, + { + "name" : "lui_components_bridge_SOURCE_DIR", + "properties" : + [ + { + "name" : "HELPSTRING", + "value" : "Value Computed by CMake" + } + ], + "type" : "STATIC", + "value" : "/home/ubuntu/repos/lui/examples/components/android/app/src/main/cpp" + } + ], + "kind" : "cache", + "version" : + { + "major" : 2, + "minor" : 0 + } +} diff --git a/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/.cmake/api/v1/reply/cmakeFiles-v1-8bcca044c919e60a8a7f.json b/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/.cmake/api/v1/reply/cmakeFiles-v1-8bcca044c919e60a8a7f.json new file mode 100644 index 00000000..9e179de5 --- /dev/null +++ b/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/.cmake/api/v1/reply/cmakeFiles-v1-8bcca044c919e60a8a7f.json @@ -0,0 +1,471 @@ +{ + "inputs" : + [ + { + "path" : "CMakeLists.txt" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeDetermineSystem.cmake" + }, + { + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/ndk/26.1.10909125/build/cmake/android.toolchain.cmake" + }, + { + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/ndk/26.1.10909125/build/cmake/android-legacy.toolchain.cmake" + }, + { + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/ndk/26.1.10909125/build/cmake/platforms.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Platform/Android-Determine.cmake" + }, + { + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/ndk/26.1.10909125/build/cmake/hooks/pre/Android-Determine.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeSystem.cmake.in" + }, + { + "isGenerated" : true, + "path" : "/home/ubuntu/repos/lui/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/CMakeFiles/3.22.1-g37088a8/CMakeSystem.cmake" + }, + { + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/ndk/26.1.10909125/build/cmake/android.toolchain.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeSystemSpecificInitialize.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Platform/Android-Initialize.cmake" + }, + { + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/ndk/26.1.10909125/build/cmake/hooks/pre/Android-Initialize.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeDetermineCCompiler.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeDetermineCompiler.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Platform/Android-Determine-C.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Platform/Android/Determine-Compiler.cmake" + }, + { + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/ndk/26.1.10909125/build/cmake/hooks/pre/Determine-Compiler.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeDetermineCompilerId.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeCompilerIdDetection.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/ADSP-DetermineCompiler.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/ARMCC-DetermineCompiler.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/ARMClang-DetermineCompiler.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/AppleClang-DetermineCompiler.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/Clang-DetermineCompilerInternal.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/Borland-DetermineCompiler.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/Bruce-C-DetermineCompiler.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/Clang-DetermineCompiler.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/Clang-DetermineCompilerInternal.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/Compaq-C-DetermineCompiler.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/Cray-DetermineCompiler.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/Embarcadero-DetermineCompiler.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/Fujitsu-DetermineCompiler.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/FujitsuClang-DetermineCompiler.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/GHS-DetermineCompiler.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/GNU-C-DetermineCompiler.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/HP-C-DetermineCompiler.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/IAR-DetermineCompiler.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/Intel-DetermineCompiler.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/IntelLLVM-DetermineCompiler.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/MSVC-DetermineCompiler.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/NVHPC-DetermineCompiler.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/NVIDIA-DetermineCompiler.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/OpenWatcom-DetermineCompiler.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/PGI-DetermineCompiler.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/PathScale-DetermineCompiler.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/SCO-DetermineCompiler.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/SDCC-C-DetermineCompiler.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/SunPro-C-DetermineCompiler.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/TI-DetermineCompiler.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/TinyCC-C-DetermineCompiler.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/VisualAge-C-DetermineCompiler.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/IBMCPP-C-DetermineVersionInternal.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/Watcom-DetermineCompiler.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/XL-C-DetermineCompiler.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/IBMCPP-C-DetermineVersionInternal.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/XLClang-C-DetermineCompiler.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/zOS-C-DetermineCompiler.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/IBMCPP-C-DetermineVersionInternal.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeFindBinUtils.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/Clang-FindBinUtils.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeCCompiler.cmake.in" + }, + { + "isGenerated" : true, + "path" : "/home/ubuntu/repos/lui/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/CMakeFiles/3.22.1-g37088a8/CMakeCCompiler.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeSystemSpecificInformation.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeGenericSystem.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeInitializeConfigs.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Platform/Android.cmake" + }, + { + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/ndk/26.1.10909125/build/cmake/hooks/pre/Android.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Platform/Linux.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Platform/UnixPaths.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeCInformation.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeLanguageInformation.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/Clang-C.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/Clang.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/CMakeCommonCompilerMacros.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/GNU.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/CMakeCommonCompilerMacros.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Platform/Android-Clang-C.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Platform/Android-Clang.cmake" + }, + { + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/ndk/26.1.10909125/build/cmake/hooks/pre/Android-Clang.cmake" + }, + { + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/ndk/26.1.10909125/build/cmake/flags.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeCommonLanguageInclude.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeTestCCompiler.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeTestCompilerCommon.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeDetermineCompilerABI.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeParseImplicitIncludeInfo.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeParseImplicitLinkInfo.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeParseLibraryArchitecture.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeTestCompilerCommon.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeCCompilerABI.c" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeDetermineCompileFeatures.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Internal/FeatureTesting.cmake" + }, + { + "isCMake" : true, + "isExternal" : true, + "path" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeCCompiler.cmake.in" + }, + { + "isGenerated" : true, + "path" : "/home/ubuntu/repos/lui/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/CMakeFiles/3.22.1-g37088a8/CMakeCCompiler.cmake" + } + ], + "kind" : "cmakeFiles", + "paths" : + { + "build" : "/home/ubuntu/repos/lui/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a", + "source" : "/home/ubuntu/repos/lui/examples/components/android/app/src/main/cpp" + }, + "version" : + { + "major" : 1, + "minor" : 0 + } +} diff --git a/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/.cmake/api/v1/reply/codemodel-v2-200f15b97692ec9c4110.json b/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/.cmake/api/v1/reply/codemodel-v2-200f15b97692ec9c4110.json new file mode 100644 index 00000000..84db53d6 --- /dev/null +++ b/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/.cmake/api/v1/reply/codemodel-v2-200f15b97692ec9c4110.json @@ -0,0 +1,60 @@ +{ + "configurations" : + [ + { + "directories" : + [ + { + "build" : ".", + "jsonFile" : "directory-.-Debug-f5ebdc15457944623624.json", + "minimumCMakeVersion" : + { + "string" : "3.6.0" + }, + "projectIndex" : 0, + "source" : ".", + "targetIndexes" : + [ + 0 + ] + } + ], + "name" : "Debug", + "projects" : + [ + { + "directoryIndexes" : + [ + 0 + ], + "name" : "lui_components_bridge", + "targetIndexes" : + [ + 0 + ] + } + ], + "targets" : + [ + { + "directoryIndex" : 0, + "id" : "lui_jni_bridge::@6890427a1f51a3e7e1df", + "jsonFile" : "target-lui_jni_bridge-Debug-1fbc5b070db103f4a523.json", + "name" : "lui_jni_bridge", + "projectIndex" : 0 + } + ] + } + ], + "kind" : "codemodel", + "paths" : + { + "build" : "/home/ubuntu/repos/lui/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a", + "source" : "/home/ubuntu/repos/lui/examples/components/android/app/src/main/cpp" + }, + "version" : + { + "major" : 2, + "minor" : 3 + } +} diff --git a/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/.cmake/api/v1/reply/directory-.-Debug-f5ebdc15457944623624.json b/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/.cmake/api/v1/reply/directory-.-Debug-f5ebdc15457944623624.json new file mode 100644 index 00000000..3a67af9c --- /dev/null +++ b/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/.cmake/api/v1/reply/directory-.-Debug-f5ebdc15457944623624.json @@ -0,0 +1,14 @@ +{ + "backtraceGraph" : + { + "commands" : [], + "files" : [], + "nodes" : [] + }, + "installers" : [], + "paths" : + { + "build" : ".", + "source" : "." + } +} diff --git a/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/.cmake/api/v1/reply/index-2026-10-06T10-09-37-0320.json b/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/.cmake/api/v1/reply/index-2026-10-06T10-09-37-0320.json new file mode 100644 index 00000000..192ad591 --- /dev/null +++ b/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/.cmake/api/v1/reply/index-2026-10-06T10-09-37-0320.json @@ -0,0 +1,92 @@ +{ + "cmake" : + { + "generator" : + { + "multiConfig" : false, + "name" : "Ninja" + }, + "paths" : + { + "cmake" : "/home/ubuntu/android-sdk/cmake/3.22.1/bin/cmake", + "cpack" : "/home/ubuntu/android-sdk/cmake/3.22.1/bin/cpack", + "ctest" : "/home/ubuntu/android-sdk/cmake/3.22.1/bin/ctest", + "root" : "/home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22" + }, + "version" : + { + "isDirty" : false, + "major" : 3, + "minor" : 22, + "patch" : 1, + "string" : "3.22.1-g37088a8", + "suffix" : "g37088a8" + } + }, + "objects" : + [ + { + "jsonFile" : "codemodel-v2-200f15b97692ec9c4110.json", + "kind" : "codemodel", + "version" : + { + "major" : 2, + "minor" : 3 + } + }, + { + "jsonFile" : "cache-v2-915560f4f4d79638de20.json", + "kind" : "cache", + "version" : + { + "major" : 2, + "minor" : 0 + } + }, + { + "jsonFile" : "cmakeFiles-v1-8bcca044c919e60a8a7f.json", + "kind" : "cmakeFiles", + "version" : + { + "major" : 1, + "minor" : 0 + } + } + ], + "reply" : + { + "client-agp" : + { + "cache-v2" : + { + "jsonFile" : "cache-v2-915560f4f4d79638de20.json", + "kind" : "cache", + "version" : + { + "major" : 2, + "minor" : 0 + } + }, + "cmakeFiles-v1" : + { + "jsonFile" : "cmakeFiles-v1-8bcca044c919e60a8a7f.json", + "kind" : "cmakeFiles", + "version" : + { + "major" : 1, + "minor" : 0 + } + }, + "codemodel-v2" : + { + "jsonFile" : "codemodel-v2-200f15b97692ec9c4110.json", + "kind" : "codemodel", + "version" : + { + "major" : 2, + "minor" : 3 + } + } + } + } +} diff --git a/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/.cmake/api/v1/reply/target-lui_jni_bridge-Debug-1fbc5b070db103f4a523.json b/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/.cmake/api/v1/reply/target-lui_jni_bridge-Debug-1fbc5b070db103f4a523.json new file mode 100644 index 00000000..b4e46c0f --- /dev/null +++ b/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/.cmake/api/v1/reply/target-lui_jni_bridge-Debug-1fbc5b070db103f4a523.json @@ -0,0 +1,117 @@ +{ + "artifacts" : + [ + { + "path" : "/home/ubuntu/repos/lui/examples/components/android/app/build/intermediates/cxx/Debug/406wr1e2/obj/arm64-v8a/liblui_jni_bridge.so" + } + ], + "backtrace" : 1, + "backtraceGraph" : + { + "commands" : + [ + "add_library", + "target_link_libraries" + ], + "files" : + [ + "CMakeLists.txt" + ], + "nodes" : + [ + { + "file" : 0 + }, + { + "command" : 0, + "file" : 0, + "line" : 32, + "parent" : 0 + }, + { + "command" : 1, + "file" : 0, + "line" : 34, + "parent" : 0 + } + ] + }, + "compileGroups" : + [ + { + "compileCommandFragments" : + [ + { + "fragment" : "-g -DANDROID -fdata-sections -ffunction-sections -funwind-tables -fstack-protector-strong -no-canonical-prefixes -D_FORTIFY_SOURCE=2 -Wformat -Werror=format-security -fno-limit-debug-info -fPIC" + } + ], + "defines" : + [ + { + "define" : "lui_jni_bridge_EXPORTS" + } + ], + "language" : "C", + "sourceIndexes" : + [ + 0 + ], + "sysroot" : + { + "path" : "/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot" + } + } + ], + "id" : "lui_jni_bridge::@6890427a1f51a3e7e1df", + "link" : + { + "commandFragments" : + [ + { + "fragment" : "-static-libstdc++ -Wl,--build-id=sha1 -Wl,--no-rosegment -Wl,--no-undefined-version -Wl,--fatal-warnings -Wl,--no-undefined -Qunused-arguments", + "role" : "flags" + }, + { + "backtrace" : 2, + "fragment" : "/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/aarch64-linux-android/24/liblog.so", + "role" : "libraries" + }, + { + "fragment" : "-latomic -lm", + "role" : "libraries" + } + ], + "language" : "C", + "sysroot" : + { + "path" : "/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot" + } + }, + "name" : "lui_jni_bridge", + "nameOnDisk" : "liblui_jni_bridge.so", + "paths" : + { + "build" : ".", + "source" : "." + }, + "sourceGroups" : + [ + { + "name" : "Source Files", + "sourceIndexes" : + [ + 0 + ] + } + ], + "sources" : + [ + { + "backtrace" : 1, + "compileGroupIndex" : 0, + "path" : "lui_jni_bridge_stub.c", + "sourceGroupIndex" : 0 + } + ], + "type" : "SHARED_LIBRARY" +} diff --git a/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/.ninja_deps b/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/.ninja_deps new file mode 100644 index 0000000000000000000000000000000000000000..467de5c84aaafa43ef21415d64cca4912e0eedd9 GIT binary patch literal 1760 zcmc(g%Zk)M6o$*Yj=Fa_Ah_$C=|Niu1P26nK0{eamC2c@R26k;d%(r)_3_;K0xn#6 zzw4i-M{wwE$G}fgft>H$b939VJ5bt$v~^wxaUXj4SRI;2Y)v9tSROc*b!6R|N^?B* zN~T=us;NA_`;F@f?$r6-P^pW{g`x?5X@|^0^+kyLIY%aSzaKCSg zyDrBFcZC(-zg|$JcnMP>6Uu>5;91ALsT6-NkkY z_3JG(dgUx^Urs#^K-wjGR2vls`sJhSJkAQzF9%ugM(!Lx z?O{WOQXLdNg?c>H9`G^0q)+h#_nV(S&*cb}`r_r8vTGIMnOrfR&lQhaPMfYH=#dow%T8N+gH%RJ(wb{qRfhf~SXA;4UvlbLZX!*)!|IWm$ue0XHo7#>Zg zhDIenoyi@+Jrh~b+5$HukkSaej&VQ6E70gkqzR<=A{{|Gi}dqIzmHVLO}qOLX!l;J zwKcWF6^PSZKY=lc5t1FraAJSc=E$S^BiIk5Gl21j7-=q2AZ!F9VWdxiurZ9!aTZb~t^u4Nkuqg2km~ zIQ^%#c-j*vTQlFglf8l(bgDUK1`uF#}_WhRgqm0YG zbQSYB4l1x2Ap29BFJJad*{5pP7Kxtc_CLk-z0dqS^EiIe-(2$lh9tPd{B%T}p*y1^ zBgtrIe=HH}s+^zEZJmkU-mXN~z>erAN)tAIGj`rZl6R5mDC7ytq|25WwTn@wXQ02o zr{CNj&F0g^iRdxQDd!OPN{w!tblp;UXFQ(vPUqN!ZBOK_vOVQwtytDB#9iCYXD8Ei zu;-5zI!ccHB`fPj&B5`3!SNomv#+nO+w4trc3HjI#9+^0?|65nJ7Hz5ZnLYquWLJK zE!s8V^}#?mvY{>9qP61O(0d>dPOZOr)6UQhAssi#fTpF^;}Lfiq}KQ2WC>o233Ro# zqTEfP5F%?R84BHm$p%kbbQ@z>Lu>u6je#&h*KMML0oDdVXNZal^(NTM?-H41O+!;6Ws6;x}k(zKYrn zYZ_?%mN9DU8Uue5o%;|_I?WJ#kaQkH;Wc^#Es9eZwbsT!6R(g!MK}TtTQKt_64=og z*dYQH3&PM?Yp-Vi2#eL5TkyM9pWFEDS}t-@an#0;KG(Dl3)Ndm66))5tq1FxHY3wS zEdm^T2Xe)6+o&F*=yAAi-ZHGBnM)VFTs%TQAePBZh>T->c`D~vo=D9RE`H(v-Mo%Z z+65~Tj zP&L7YC4X*{0?r|o@QaS^ zw^l8zP2mwZA&w|#MHSux5Bu2qDr4#1^>+|_*ukT za30G1soWQ0xUXAI31UUtwPNA~=1e@c7@H_g#WGX5yxD}0$dfIoHkowt8tF{Nv5ujEiX-Pn(q*#OgYhBD zf*3Z}O=pl!VwRWkC}bTsgYPRvJRP@A$2!3%yJ^QY)Cs)RM3)^u-Sp|9PcMD?=+h6> z*#>vE;dM6R2@hA@q9m|6mWQFA1x53yAze;sH7G`Ls*tf9P|kwy$VQ_QALfyLOrH)d z>m;*3q&=8BKq7)l z=+J@Gp6W8Z=oBtT+?VJC^TKNH?{sw(eL0tf%K1#P6lCA?IJ)L6*Ozlw z=vEPA>eo*ojyJJN{c_J2I>vgmzfn77pOX`205Q5#i~Vx%7Ao>mugD1_6m}%h-{; agl+6t-{`B*$3o94`X)QJ|Mn^PSokkxry6ts literal 0 HcmV?d00001 diff --git a/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/CMakeFiles/3.22.1-g37088a8/CMakeSystem.cmake b/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/CMakeFiles/3.22.1-g37088a8/CMakeSystem.cmake new file mode 100644 index 00000000..29e1fdc6 --- /dev/null +++ b/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/CMakeFiles/3.22.1-g37088a8/CMakeSystem.cmake @@ -0,0 +1,15 @@ +set(CMAKE_HOST_SYSTEM "Linux-6.8.0-1061-aws") +set(CMAKE_HOST_SYSTEM_NAME "Linux") +set(CMAKE_HOST_SYSTEM_VERSION "6.8.0-1061-aws") +set(CMAKE_HOST_SYSTEM_PROCESSOR "x86_64") + +include("/home/ubuntu/android-sdk/ndk/26.1.10909125/build/cmake/android.toolchain.cmake") + +set(CMAKE_SYSTEM "Android-1") +set(CMAKE_SYSTEM_NAME "Android") +set(CMAKE_SYSTEM_VERSION "1") +set(CMAKE_SYSTEM_PROCESSOR "aarch64") + +set(CMAKE_CROSSCOMPILING "TRUE") + +set(CMAKE_SYSTEM_LOADED 1) diff --git a/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/CMakeFiles/3.22.1-g37088a8/CompilerIdC/CMakeCCompilerId.c b/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/CMakeFiles/3.22.1-g37088a8/CompilerIdC/CMakeCCompilerId.c new file mode 100644 index 00000000..41b99d77 --- /dev/null +++ b/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/CMakeFiles/3.22.1-g37088a8/CompilerIdC/CMakeCCompilerId.c @@ -0,0 +1,803 @@ +#ifdef __cplusplus +# error "A C++ compiler has been selected for C." +#endif + +#if defined(__18CXX) +# define ID_VOID_MAIN +#endif +#if defined(__CLASSIC_C__) +/* cv-qualifiers did not exist in K&R C */ +# define const +# define volatile +#endif + +#if !defined(__has_include) +/* If the compiler does not have __has_include, pretend the answer is + always no. */ +# define __has_include(x) 0 +#endif + + +/* Version number components: V=Version, R=Revision, P=Patch + Version date components: YYYY=Year, MM=Month, DD=Day */ + +#if defined(__INTEL_COMPILER) || defined(__ICC) +# define COMPILER_ID "Intel" +# if defined(_MSC_VER) +# define SIMULATE_ID "MSVC" +# endif +# if defined(__GNUC__) +# define SIMULATE_ID "GNU" +# endif + /* __INTEL_COMPILER = VRP prior to 2021, and then VVVV for 2021 and later, + except that a few beta releases use the old format with V=2021. */ +# if __INTEL_COMPILER < 2021 || __INTEL_COMPILER == 202110 || __INTEL_COMPILER == 202111 +# define COMPILER_VERSION_MAJOR DEC(__INTEL_COMPILER/100) +# define COMPILER_VERSION_MINOR DEC(__INTEL_COMPILER/10 % 10) +# if defined(__INTEL_COMPILER_UPDATE) +# define COMPILER_VERSION_PATCH DEC(__INTEL_COMPILER_UPDATE) +# else +# define COMPILER_VERSION_PATCH DEC(__INTEL_COMPILER % 10) +# endif +# else +# define COMPILER_VERSION_MAJOR DEC(__INTEL_COMPILER) +# define COMPILER_VERSION_MINOR DEC(__INTEL_COMPILER_UPDATE) + /* The third version component from --version is an update index, + but no macro is provided for it. */ +# define COMPILER_VERSION_PATCH DEC(0) +# endif +# if defined(__INTEL_COMPILER_BUILD_DATE) + /* __INTEL_COMPILER_BUILD_DATE = YYYYMMDD */ +# define COMPILER_VERSION_TWEAK DEC(__INTEL_COMPILER_BUILD_DATE) +# endif +# if defined(_MSC_VER) + /* _MSC_VER = VVRR */ +# define SIMULATE_VERSION_MAJOR DEC(_MSC_VER / 100) +# define SIMULATE_VERSION_MINOR DEC(_MSC_VER % 100) +# endif +# if defined(__GNUC__) +# define SIMULATE_VERSION_MAJOR DEC(__GNUC__) +# elif defined(__GNUG__) +# define SIMULATE_VERSION_MAJOR DEC(__GNUG__) +# endif +# if defined(__GNUC_MINOR__) +# define SIMULATE_VERSION_MINOR DEC(__GNUC_MINOR__) +# endif +# if defined(__GNUC_PATCHLEVEL__) +# define SIMULATE_VERSION_PATCH DEC(__GNUC_PATCHLEVEL__) +# endif + +#elif (defined(__clang__) && defined(__INTEL_CLANG_COMPILER)) || defined(__INTEL_LLVM_COMPILER) +# define COMPILER_ID "IntelLLVM" +#if defined(_MSC_VER) +# define SIMULATE_ID "MSVC" +#endif +#if defined(__GNUC__) +# define SIMULATE_ID "GNU" +#endif +/* __INTEL_LLVM_COMPILER = VVVVRP prior to 2021.2.0, VVVVRRPP for 2021.2.0 and + * later. Look for 6 digit vs. 8 digit version number to decide encoding. + * VVVV is no smaller than the current year when a version is released. + */ +#if __INTEL_LLVM_COMPILER < 1000000L +# define COMPILER_VERSION_MAJOR DEC(__INTEL_LLVM_COMPILER/100) +# define COMPILER_VERSION_MINOR DEC(__INTEL_LLVM_COMPILER/10 % 10) +# define COMPILER_VERSION_PATCH DEC(__INTEL_LLVM_COMPILER % 10) +#else +# define COMPILER_VERSION_MAJOR DEC(__INTEL_LLVM_COMPILER/10000) +# define COMPILER_VERSION_MINOR DEC(__INTEL_LLVM_COMPILER/100 % 100) +# define COMPILER_VERSION_PATCH DEC(__INTEL_LLVM_COMPILER % 100) +#endif +#if defined(_MSC_VER) + /* _MSC_VER = VVRR */ +# define SIMULATE_VERSION_MAJOR DEC(_MSC_VER / 100) +# define SIMULATE_VERSION_MINOR DEC(_MSC_VER % 100) +#endif +#if defined(__GNUC__) +# define SIMULATE_VERSION_MAJOR DEC(__GNUC__) +#elif defined(__GNUG__) +# define SIMULATE_VERSION_MAJOR DEC(__GNUG__) +#endif +#if defined(__GNUC_MINOR__) +# define SIMULATE_VERSION_MINOR DEC(__GNUC_MINOR__) +#endif +#if defined(__GNUC_PATCHLEVEL__) +# define SIMULATE_VERSION_PATCH DEC(__GNUC_PATCHLEVEL__) +#endif + +#elif defined(__PATHCC__) +# define COMPILER_ID "PathScale" +# define COMPILER_VERSION_MAJOR DEC(__PATHCC__) +# define COMPILER_VERSION_MINOR DEC(__PATHCC_MINOR__) +# if defined(__PATHCC_PATCHLEVEL__) +# define COMPILER_VERSION_PATCH DEC(__PATHCC_PATCHLEVEL__) +# endif + +#elif defined(__BORLANDC__) && defined(__CODEGEARC_VERSION__) +# define COMPILER_ID "Embarcadero" +# define COMPILER_VERSION_MAJOR HEX(__CODEGEARC_VERSION__>>24 & 0x00FF) +# define COMPILER_VERSION_MINOR HEX(__CODEGEARC_VERSION__>>16 & 0x00FF) +# define COMPILER_VERSION_PATCH DEC(__CODEGEARC_VERSION__ & 0xFFFF) + +#elif defined(__BORLANDC__) +# define COMPILER_ID "Borland" + /* __BORLANDC__ = 0xVRR */ +# define COMPILER_VERSION_MAJOR HEX(__BORLANDC__>>8) +# define COMPILER_VERSION_MINOR HEX(__BORLANDC__ & 0xFF) + +#elif defined(__WATCOMC__) && __WATCOMC__ < 1200 +# define COMPILER_ID "Watcom" + /* __WATCOMC__ = VVRR */ +# define COMPILER_VERSION_MAJOR DEC(__WATCOMC__ / 100) +# define COMPILER_VERSION_MINOR DEC((__WATCOMC__ / 10) % 10) +# if (__WATCOMC__ % 10) > 0 +# define COMPILER_VERSION_PATCH DEC(__WATCOMC__ % 10) +# endif + +#elif defined(__WATCOMC__) +# define COMPILER_ID "OpenWatcom" + /* __WATCOMC__ = VVRP + 1100 */ +# define COMPILER_VERSION_MAJOR DEC((__WATCOMC__ - 1100) / 100) +# define COMPILER_VERSION_MINOR DEC((__WATCOMC__ / 10) % 10) +# if (__WATCOMC__ % 10) > 0 +# define COMPILER_VERSION_PATCH DEC(__WATCOMC__ % 10) +# endif + +#elif defined(__SUNPRO_C) +# define COMPILER_ID "SunPro" +# if __SUNPRO_C >= 0x5100 + /* __SUNPRO_C = 0xVRRP */ +# define COMPILER_VERSION_MAJOR HEX(__SUNPRO_C>>12) +# define COMPILER_VERSION_MINOR HEX(__SUNPRO_C>>4 & 0xFF) +# define COMPILER_VERSION_PATCH HEX(__SUNPRO_C & 0xF) +# else + /* __SUNPRO_CC = 0xVRP */ +# define COMPILER_VERSION_MAJOR HEX(__SUNPRO_C>>8) +# define COMPILER_VERSION_MINOR HEX(__SUNPRO_C>>4 & 0xF) +# define COMPILER_VERSION_PATCH HEX(__SUNPRO_C & 0xF) +# endif + +#elif defined(__HP_cc) +# define COMPILER_ID "HP" + /* __HP_cc = VVRRPP */ +# define COMPILER_VERSION_MAJOR DEC(__HP_cc/10000) +# define COMPILER_VERSION_MINOR DEC(__HP_cc/100 % 100) +# define COMPILER_VERSION_PATCH DEC(__HP_cc % 100) + +#elif defined(__DECC) +# define COMPILER_ID "Compaq" + /* __DECC_VER = VVRRTPPPP */ +# define COMPILER_VERSION_MAJOR DEC(__DECC_VER/10000000) +# define COMPILER_VERSION_MINOR DEC(__DECC_VER/100000 % 100) +# define COMPILER_VERSION_PATCH DEC(__DECC_VER % 10000) + +#elif defined(__IBMC__) && defined(__COMPILER_VER__) +# define COMPILER_ID "zOS" + /* __IBMC__ = VRP */ +# define COMPILER_VERSION_MAJOR DEC(__IBMC__/100) +# define COMPILER_VERSION_MINOR DEC(__IBMC__/10 % 10) +# define COMPILER_VERSION_PATCH DEC(__IBMC__ % 10) + +#elif defined(__ibmxl__) && defined(__clang__) +# define COMPILER_ID "XLClang" +# define COMPILER_VERSION_MAJOR DEC(__ibmxl_version__) +# define COMPILER_VERSION_MINOR DEC(__ibmxl_release__) +# define COMPILER_VERSION_PATCH DEC(__ibmxl_modification__) +# define COMPILER_VERSION_TWEAK DEC(__ibmxl_ptf_fix_level__) + + +#elif defined(__IBMC__) && !defined(__COMPILER_VER__) && __IBMC__ >= 800 +# define COMPILER_ID "XL" + /* __IBMC__ = VRP */ +# define COMPILER_VERSION_MAJOR DEC(__IBMC__/100) +# define COMPILER_VERSION_MINOR DEC(__IBMC__/10 % 10) +# define COMPILER_VERSION_PATCH DEC(__IBMC__ % 10) + +#elif defined(__IBMC__) && !defined(__COMPILER_VER__) && __IBMC__ < 800 +# define COMPILER_ID "VisualAge" + /* __IBMC__ = VRP */ +# define COMPILER_VERSION_MAJOR DEC(__IBMC__/100) +# define COMPILER_VERSION_MINOR DEC(__IBMC__/10 % 10) +# define COMPILER_VERSION_PATCH DEC(__IBMC__ % 10) + +#elif defined(__NVCOMPILER) +# define COMPILER_ID "NVHPC" +# define COMPILER_VERSION_MAJOR DEC(__NVCOMPILER_MAJOR__) +# define COMPILER_VERSION_MINOR DEC(__NVCOMPILER_MINOR__) +# if defined(__NVCOMPILER_PATCHLEVEL__) +# define COMPILER_VERSION_PATCH DEC(__NVCOMPILER_PATCHLEVEL__) +# endif + +#elif defined(__PGI) +# define COMPILER_ID "PGI" +# define COMPILER_VERSION_MAJOR DEC(__PGIC__) +# define COMPILER_VERSION_MINOR DEC(__PGIC_MINOR__) +# if defined(__PGIC_PATCHLEVEL__) +# define COMPILER_VERSION_PATCH DEC(__PGIC_PATCHLEVEL__) +# endif + +#elif defined(_CRAYC) +# define COMPILER_ID "Cray" +# define COMPILER_VERSION_MAJOR DEC(_RELEASE_MAJOR) +# define COMPILER_VERSION_MINOR DEC(_RELEASE_MINOR) + +#elif defined(__TI_COMPILER_VERSION__) +# define COMPILER_ID "TI" + /* __TI_COMPILER_VERSION__ = VVVRRRPPP */ +# define COMPILER_VERSION_MAJOR DEC(__TI_COMPILER_VERSION__/1000000) +# define COMPILER_VERSION_MINOR DEC(__TI_COMPILER_VERSION__/1000 % 1000) +# define COMPILER_VERSION_PATCH DEC(__TI_COMPILER_VERSION__ % 1000) + +#elif defined(__CLANG_FUJITSU) +# define COMPILER_ID "FujitsuClang" +# define COMPILER_VERSION_MAJOR DEC(__FCC_major__) +# define COMPILER_VERSION_MINOR DEC(__FCC_minor__) +# define COMPILER_VERSION_PATCH DEC(__FCC_patchlevel__) +# define COMPILER_VERSION_INTERNAL_STR __clang_version__ + + +#elif defined(__FUJITSU) +# define COMPILER_ID "Fujitsu" +# if defined(__FCC_version__) +# define COMPILER_VERSION __FCC_version__ +# elif defined(__FCC_major__) +# define COMPILER_VERSION_MAJOR DEC(__FCC_major__) +# define COMPILER_VERSION_MINOR DEC(__FCC_minor__) +# define COMPILER_VERSION_PATCH DEC(__FCC_patchlevel__) +# endif +# if defined(__fcc_version) +# define COMPILER_VERSION_INTERNAL DEC(__fcc_version) +# elif defined(__FCC_VERSION) +# define COMPILER_VERSION_INTERNAL DEC(__FCC_VERSION) +# endif + + +#elif defined(__ghs__) +# define COMPILER_ID "GHS" +/* __GHS_VERSION_NUMBER = VVVVRP */ +# ifdef __GHS_VERSION_NUMBER +# define COMPILER_VERSION_MAJOR DEC(__GHS_VERSION_NUMBER / 100) +# define COMPILER_VERSION_MINOR DEC(__GHS_VERSION_NUMBER / 10 % 10) +# define COMPILER_VERSION_PATCH DEC(__GHS_VERSION_NUMBER % 10) +# endif + +#elif defined(__TINYC__) +# define COMPILER_ID "TinyCC" + +#elif defined(__BCC__) +# define COMPILER_ID "Bruce" + +#elif defined(__SCO_VERSION__) +# define COMPILER_ID "SCO" + +#elif defined(__ARMCC_VERSION) && !defined(__clang__) +# define COMPILER_ID "ARMCC" +#if __ARMCC_VERSION >= 1000000 + /* __ARMCC_VERSION = VRRPPPP */ + # define COMPILER_VERSION_MAJOR DEC(__ARMCC_VERSION/1000000) + # define COMPILER_VERSION_MINOR DEC(__ARMCC_VERSION/10000 % 100) + # define COMPILER_VERSION_PATCH DEC(__ARMCC_VERSION % 10000) +#else + /* __ARMCC_VERSION = VRPPPP */ + # define COMPILER_VERSION_MAJOR DEC(__ARMCC_VERSION/100000) + # define COMPILER_VERSION_MINOR DEC(__ARMCC_VERSION/10000 % 10) + # define COMPILER_VERSION_PATCH DEC(__ARMCC_VERSION % 10000) +#endif + + +#elif defined(__clang__) && defined(__apple_build_version__) +# define COMPILER_ID "AppleClang" +# if defined(_MSC_VER) +# define SIMULATE_ID "MSVC" +# endif +# define COMPILER_VERSION_MAJOR DEC(__clang_major__) +# define COMPILER_VERSION_MINOR DEC(__clang_minor__) +# define COMPILER_VERSION_PATCH DEC(__clang_patchlevel__) +# if defined(_MSC_VER) + /* _MSC_VER = VVRR */ +# define SIMULATE_VERSION_MAJOR DEC(_MSC_VER / 100) +# define SIMULATE_VERSION_MINOR DEC(_MSC_VER % 100) +# endif +# define COMPILER_VERSION_TWEAK DEC(__apple_build_version__) + +#elif defined(__clang__) && defined(__ARMCOMPILER_VERSION) +# define COMPILER_ID "ARMClang" + # define COMPILER_VERSION_MAJOR DEC(__ARMCOMPILER_VERSION/1000000) + # define COMPILER_VERSION_MINOR DEC(__ARMCOMPILER_VERSION/10000 % 100) + # define COMPILER_VERSION_PATCH DEC(__ARMCOMPILER_VERSION % 10000) +# define COMPILER_VERSION_INTERNAL DEC(__ARMCOMPILER_VERSION) + +#elif defined(__clang__) +# define COMPILER_ID "Clang" +# if defined(_MSC_VER) +# define SIMULATE_ID "MSVC" +# endif +# define COMPILER_VERSION_MAJOR DEC(__clang_major__) +# define COMPILER_VERSION_MINOR DEC(__clang_minor__) +# define COMPILER_VERSION_PATCH DEC(__clang_patchlevel__) +# if defined(_MSC_VER) + /* _MSC_VER = VVRR */ +# define SIMULATE_VERSION_MAJOR DEC(_MSC_VER / 100) +# define SIMULATE_VERSION_MINOR DEC(_MSC_VER % 100) +# endif + +#elif defined(__GNUC__) +# define COMPILER_ID "GNU" +# define COMPILER_VERSION_MAJOR DEC(__GNUC__) +# if defined(__GNUC_MINOR__) +# define COMPILER_VERSION_MINOR DEC(__GNUC_MINOR__) +# endif +# if defined(__GNUC_PATCHLEVEL__) +# define COMPILER_VERSION_PATCH DEC(__GNUC_PATCHLEVEL__) +# endif + +#elif defined(_MSC_VER) +# define COMPILER_ID "MSVC" + /* _MSC_VER = VVRR */ +# define COMPILER_VERSION_MAJOR DEC(_MSC_VER / 100) +# define COMPILER_VERSION_MINOR DEC(_MSC_VER % 100) +# if defined(_MSC_FULL_VER) +# if _MSC_VER >= 1400 + /* _MSC_FULL_VER = VVRRPPPPP */ +# define COMPILER_VERSION_PATCH DEC(_MSC_FULL_VER % 100000) +# else + /* _MSC_FULL_VER = VVRRPPPP */ +# define COMPILER_VERSION_PATCH DEC(_MSC_FULL_VER % 10000) +# endif +# endif +# if defined(_MSC_BUILD) +# define COMPILER_VERSION_TWEAK DEC(_MSC_BUILD) +# endif + +#elif defined(__VISUALDSPVERSION__) || defined(__ADSPBLACKFIN__) || defined(__ADSPTS__) || defined(__ADSP21000__) +# define COMPILER_ID "ADSP" +#if defined(__VISUALDSPVERSION__) + /* __VISUALDSPVERSION__ = 0xVVRRPP00 */ +# define COMPILER_VERSION_MAJOR HEX(__VISUALDSPVERSION__>>24) +# define COMPILER_VERSION_MINOR HEX(__VISUALDSPVERSION__>>16 & 0xFF) +# define COMPILER_VERSION_PATCH HEX(__VISUALDSPVERSION__>>8 & 0xFF) +#endif + +#elif defined(__IAR_SYSTEMS_ICC__) || defined(__IAR_SYSTEMS_ICC) +# define COMPILER_ID "IAR" +# if defined(__VER__) && defined(__ICCARM__) +# define COMPILER_VERSION_MAJOR DEC((__VER__) / 1000000) +# define COMPILER_VERSION_MINOR DEC(((__VER__) / 1000) % 1000) +# define COMPILER_VERSION_PATCH DEC((__VER__) % 1000) +# define COMPILER_VERSION_INTERNAL DEC(__IAR_SYSTEMS_ICC__) +# elif defined(__VER__) && (defined(__ICCAVR__) || defined(__ICCRX__) || defined(__ICCRH850__) || defined(__ICCRL78__) || defined(__ICC430__) || defined(__ICCRISCV__) || defined(__ICCV850__) || defined(__ICC8051__) || defined(__ICCSTM8__)) +# define COMPILER_VERSION_MAJOR DEC((__VER__) / 100) +# define COMPILER_VERSION_MINOR DEC((__VER__) - (((__VER__) / 100)*100)) +# define COMPILER_VERSION_PATCH DEC(__SUBVERSION__) +# define COMPILER_VERSION_INTERNAL DEC(__IAR_SYSTEMS_ICC__) +# endif + +#elif defined(__SDCC_VERSION_MAJOR) || defined(SDCC) +# define COMPILER_ID "SDCC" +# if defined(__SDCC_VERSION_MAJOR) +# define COMPILER_VERSION_MAJOR DEC(__SDCC_VERSION_MAJOR) +# define COMPILER_VERSION_MINOR DEC(__SDCC_VERSION_MINOR) +# define COMPILER_VERSION_PATCH DEC(__SDCC_VERSION_PATCH) +# else + /* SDCC = VRP */ +# define COMPILER_VERSION_MAJOR DEC(SDCC/100) +# define COMPILER_VERSION_MINOR DEC(SDCC/10 % 10) +# define COMPILER_VERSION_PATCH DEC(SDCC % 10) +# endif + + +/* These compilers are either not known or too old to define an + identification macro. Try to identify the platform and guess that + it is the native compiler. */ +#elif defined(__hpux) || defined(__hpua) +# define COMPILER_ID "HP" + +#else /* unknown compiler */ +# define COMPILER_ID "" +#endif + +/* Construct the string literal in pieces to prevent the source from + getting matched. Store it in a pointer rather than an array + because some compilers will just produce instructions to fill the + array rather than assigning a pointer to a static array. */ +char const* info_compiler = "INFO" ":" "compiler[" COMPILER_ID "]"; +#ifdef SIMULATE_ID +char const* info_simulate = "INFO" ":" "simulate[" SIMULATE_ID "]"; +#endif + +#ifdef __QNXNTO__ +char const* qnxnto = "INFO" ":" "qnxnto[]"; +#endif + +#if defined(__CRAYXT_COMPUTE_LINUX_TARGET) +char const *info_cray = "INFO" ":" "compiler_wrapper[CrayPrgEnv]"; +#endif + +#define STRINGIFY_HELPER(X) #X +#define STRINGIFY(X) STRINGIFY_HELPER(X) + +/* Identify known platforms by name. */ +#if defined(__linux) || defined(__linux__) || defined(linux) +# define PLATFORM_ID "Linux" + +#elif defined(__MSYS__) +# define PLATFORM_ID "MSYS" + +#elif defined(__CYGWIN__) +# define PLATFORM_ID "Cygwin" + +#elif defined(__MINGW32__) +# define PLATFORM_ID "MinGW" + +#elif defined(__APPLE__) +# define PLATFORM_ID "Darwin" + +#elif defined(_WIN32) || defined(__WIN32__) || defined(WIN32) +# define PLATFORM_ID "Windows" + +#elif defined(__FreeBSD__) || defined(__FreeBSD) +# define PLATFORM_ID "FreeBSD" + +#elif defined(__NetBSD__) || defined(__NetBSD) +# define PLATFORM_ID "NetBSD" + +#elif defined(__OpenBSD__) || defined(__OPENBSD) +# define PLATFORM_ID "OpenBSD" + +#elif defined(__sun) || defined(sun) +# define PLATFORM_ID "SunOS" + +#elif defined(_AIX) || defined(__AIX) || defined(__AIX__) || defined(__aix) || defined(__aix__) +# define PLATFORM_ID "AIX" + +#elif defined(__hpux) || defined(__hpux__) +# define PLATFORM_ID "HP-UX" + +#elif defined(__HAIKU__) +# define PLATFORM_ID "Haiku" + +#elif defined(__BeOS) || defined(__BEOS__) || defined(_BEOS) +# define PLATFORM_ID "BeOS" + +#elif defined(__QNX__) || defined(__QNXNTO__) +# define PLATFORM_ID "QNX" + +#elif defined(__tru64) || defined(_tru64) || defined(__TRU64__) +# define PLATFORM_ID "Tru64" + +#elif defined(__riscos) || defined(__riscos__) +# define PLATFORM_ID "RISCos" + +#elif defined(__sinix) || defined(__sinix__) || defined(__SINIX__) +# define PLATFORM_ID "SINIX" + +#elif defined(__UNIX_SV__) +# define PLATFORM_ID "UNIX_SV" + +#elif defined(__bsdos__) +# define PLATFORM_ID "BSDOS" + +#elif defined(_MPRAS) || defined(MPRAS) +# define PLATFORM_ID "MP-RAS" + +#elif defined(__osf) || defined(__osf__) +# define PLATFORM_ID "OSF1" + +#elif defined(_SCO_SV) || defined(SCO_SV) || defined(sco_sv) +# define PLATFORM_ID "SCO_SV" + +#elif defined(__ultrix) || defined(__ultrix__) || defined(_ULTRIX) +# define PLATFORM_ID "ULTRIX" + +#elif defined(__XENIX__) || defined(_XENIX) || defined(XENIX) +# define PLATFORM_ID "Xenix" + +#elif defined(__WATCOMC__) +# if defined(__LINUX__) +# define PLATFORM_ID "Linux" + +# elif defined(__DOS__) +# define PLATFORM_ID "DOS" + +# elif defined(__OS2__) +# define PLATFORM_ID "OS2" + +# elif defined(__WINDOWS__) +# define PLATFORM_ID "Windows3x" + +# elif defined(__VXWORKS__) +# define PLATFORM_ID "VxWorks" + +# else /* unknown platform */ +# define PLATFORM_ID +# endif + +#elif defined(__INTEGRITY) +# if defined(INT_178B) +# define PLATFORM_ID "Integrity178" + +# else /* regular Integrity */ +# define PLATFORM_ID "Integrity" +# endif + +#else /* unknown platform */ +# define PLATFORM_ID + +#endif + +/* For windows compilers MSVC and Intel we can determine + the architecture of the compiler being used. This is because + the compilers do not have flags that can change the architecture, + but rather depend on which compiler is being used +*/ +#if defined(_WIN32) && defined(_MSC_VER) +# if defined(_M_IA64) +# define ARCHITECTURE_ID "IA64" + +# elif defined(_M_ARM64EC) +# define ARCHITECTURE_ID "ARM64EC" + +# elif defined(_M_X64) || defined(_M_AMD64) +# define ARCHITECTURE_ID "x64" + +# elif defined(_M_IX86) +# define ARCHITECTURE_ID "X86" + +# elif defined(_M_ARM64) +# define ARCHITECTURE_ID "ARM64" + +# elif defined(_M_ARM) +# if _M_ARM == 4 +# define ARCHITECTURE_ID "ARMV4I" +# elif _M_ARM == 5 +# define ARCHITECTURE_ID "ARMV5I" +# else +# define ARCHITECTURE_ID "ARMV" STRINGIFY(_M_ARM) +# endif + +# elif defined(_M_MIPS) +# define ARCHITECTURE_ID "MIPS" + +# elif defined(_M_SH) +# define ARCHITECTURE_ID "SHx" + +# else /* unknown architecture */ +# define ARCHITECTURE_ID "" +# endif + +#elif defined(__WATCOMC__) +# if defined(_M_I86) +# define ARCHITECTURE_ID "I86" + +# elif defined(_M_IX86) +# define ARCHITECTURE_ID "X86" + +# else /* unknown architecture */ +# define ARCHITECTURE_ID "" +# endif + +#elif defined(__IAR_SYSTEMS_ICC__) || defined(__IAR_SYSTEMS_ICC) +# if defined(__ICCARM__) +# define ARCHITECTURE_ID "ARM" + +# elif defined(__ICCRX__) +# define ARCHITECTURE_ID "RX" + +# elif defined(__ICCRH850__) +# define ARCHITECTURE_ID "RH850" + +# elif defined(__ICCRL78__) +# define ARCHITECTURE_ID "RL78" + +# elif defined(__ICCRISCV__) +# define ARCHITECTURE_ID "RISCV" + +# elif defined(__ICCAVR__) +# define ARCHITECTURE_ID "AVR" + +# elif defined(__ICC430__) +# define ARCHITECTURE_ID "MSP430" + +# elif defined(__ICCV850__) +# define ARCHITECTURE_ID "V850" + +# elif defined(__ICC8051__) +# define ARCHITECTURE_ID "8051" + +# elif defined(__ICCSTM8__) +# define ARCHITECTURE_ID "STM8" + +# else /* unknown architecture */ +# define ARCHITECTURE_ID "" +# endif + +#elif defined(__ghs__) +# if defined(__PPC64__) +# define ARCHITECTURE_ID "PPC64" + +# elif defined(__ppc__) +# define ARCHITECTURE_ID "PPC" + +# elif defined(__ARM__) +# define ARCHITECTURE_ID "ARM" + +# elif defined(__x86_64__) +# define ARCHITECTURE_ID "x64" + +# elif defined(__i386__) +# define ARCHITECTURE_ID "X86" + +# else /* unknown architecture */ +# define ARCHITECTURE_ID "" +# endif + +#elif defined(__TI_COMPILER_VERSION__) +# if defined(__TI_ARM__) +# define ARCHITECTURE_ID "ARM" + +# elif defined(__MSP430__) +# define ARCHITECTURE_ID "MSP430" + +# elif defined(__TMS320C28XX__) +# define ARCHITECTURE_ID "TMS320C28x" + +# elif defined(__TMS320C6X__) || defined(_TMS320C6X) +# define ARCHITECTURE_ID "TMS320C6x" + +# else /* unknown architecture */ +# define ARCHITECTURE_ID "" +# endif + +#else +# define ARCHITECTURE_ID +#endif + +/* Convert integer to decimal digit literals. */ +#define DEC(n) \ + ('0' + (((n) / 10000000)%10)), \ + ('0' + (((n) / 1000000)%10)), \ + ('0' + (((n) / 100000)%10)), \ + ('0' + (((n) / 10000)%10)), \ + ('0' + (((n) / 1000)%10)), \ + ('0' + (((n) / 100)%10)), \ + ('0' + (((n) / 10)%10)), \ + ('0' + ((n) % 10)) + +/* Convert integer to hex digit literals. */ +#define HEX(n) \ + ('0' + ((n)>>28 & 0xF)), \ + ('0' + ((n)>>24 & 0xF)), \ + ('0' + ((n)>>20 & 0xF)), \ + ('0' + ((n)>>16 & 0xF)), \ + ('0' + ((n)>>12 & 0xF)), \ + ('0' + ((n)>>8 & 0xF)), \ + ('0' + ((n)>>4 & 0xF)), \ + ('0' + ((n) & 0xF)) + +/* Construct a string literal encoding the version number. */ +#ifdef COMPILER_VERSION +char const* info_version = "INFO" ":" "compiler_version[" COMPILER_VERSION "]"; + +/* Construct a string literal encoding the version number components. */ +#elif defined(COMPILER_VERSION_MAJOR) +char const info_version[] = { + 'I', 'N', 'F', 'O', ':', + 'c','o','m','p','i','l','e','r','_','v','e','r','s','i','o','n','[', + COMPILER_VERSION_MAJOR, +# ifdef COMPILER_VERSION_MINOR + '.', COMPILER_VERSION_MINOR, +# ifdef COMPILER_VERSION_PATCH + '.', COMPILER_VERSION_PATCH, +# ifdef COMPILER_VERSION_TWEAK + '.', COMPILER_VERSION_TWEAK, +# endif +# endif +# endif + ']','\0'}; +#endif + +/* Construct a string literal encoding the internal version number. */ +#ifdef COMPILER_VERSION_INTERNAL +char const info_version_internal[] = { + 'I', 'N', 'F', 'O', ':', + 'c','o','m','p','i','l','e','r','_','v','e','r','s','i','o','n','_', + 'i','n','t','e','r','n','a','l','[', + COMPILER_VERSION_INTERNAL,']','\0'}; +#elif defined(COMPILER_VERSION_INTERNAL_STR) +char const* info_version_internal = "INFO" ":" "compiler_version_internal[" COMPILER_VERSION_INTERNAL_STR "]"; +#endif + +/* Construct a string literal encoding the version number components. */ +#ifdef SIMULATE_VERSION_MAJOR +char const info_simulate_version[] = { + 'I', 'N', 'F', 'O', ':', + 's','i','m','u','l','a','t','e','_','v','e','r','s','i','o','n','[', + SIMULATE_VERSION_MAJOR, +# ifdef SIMULATE_VERSION_MINOR + '.', SIMULATE_VERSION_MINOR, +# ifdef SIMULATE_VERSION_PATCH + '.', SIMULATE_VERSION_PATCH, +# ifdef SIMULATE_VERSION_TWEAK + '.', SIMULATE_VERSION_TWEAK, +# endif +# endif +# endif + ']','\0'}; +#endif + +/* Construct the string literal in pieces to prevent the source from + getting matched. Store it in a pointer rather than an array + because some compilers will just produce instructions to fill the + array rather than assigning a pointer to a static array. */ +char const* info_platform = "INFO" ":" "platform[" PLATFORM_ID "]"; +char const* info_arch = "INFO" ":" "arch[" ARCHITECTURE_ID "]"; + + + +#if !defined(__STDC__) && !defined(__clang__) +# if defined(_MSC_VER) || defined(__ibmxl__) || defined(__IBMC__) +# define C_VERSION "90" +# else +# define C_VERSION +# endif +#elif __STDC_VERSION__ > 201710L +# define C_VERSION "23" +#elif __STDC_VERSION__ >= 201710L +# define C_VERSION "17" +#elif __STDC_VERSION__ >= 201000L +# define C_VERSION "11" +#elif __STDC_VERSION__ >= 199901L +# define C_VERSION "99" +#else +# define C_VERSION "90" +#endif +const char* info_language_standard_default = + "INFO" ":" "standard_default[" C_VERSION "]"; + +const char* info_language_extensions_default = "INFO" ":" "extensions_default[" +/* !defined(_MSC_VER) to exclude Clang's MSVC compatibility mode. */ +#if (defined(__clang__) || defined(__GNUC__) || \ + defined(__TI_COMPILER_VERSION__)) && \ + !defined(__STRICT_ANSI__) && !defined(_MSC_VER) + "ON" +#else + "OFF" +#endif +"]"; + +/*--------------------------------------------------------------------------*/ + +#ifdef ID_VOID_MAIN +void main() {} +#else +# if defined(__CLASSIC_C__) +int main(argc, argv) int argc; char *argv[]; +# else +int main(int argc, char* argv[]) +# endif +{ + int require = 0; + require += info_compiler[argc]; + require += info_platform[argc]; + require += info_arch[argc]; +#ifdef COMPILER_VERSION_MAJOR + require += info_version[argc]; +#endif +#ifdef COMPILER_VERSION_INTERNAL + require += info_version_internal[argc]; +#endif +#ifdef SIMULATE_ID + require += info_simulate[argc]; +#endif +#ifdef SIMULATE_VERSION_MAJOR + require += info_simulate_version[argc]; +#endif +#if defined(__CRAYXT_COMPUTE_LINUX_TARGET) + require += info_cray[argc]; +#endif + require += info_language_standard_default[argc]; + require += info_language_extensions_default[argc]; + (void)argv; + return require; +} +#endif diff --git a/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/CMakeFiles/3.22.1-g37088a8/CompilerIdC/CMakeCCompilerId.o b/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/CMakeFiles/3.22.1-g37088a8/CompilerIdC/CMakeCCompilerId.o new file mode 100644 index 0000000000000000000000000000000000000000..6a6f9546e744266eced6628e1b247b7d65ae29e6 GIT binary patch literal 6048 zcmdT|Z){sv6+h38T_?`mrJ=N2y3Rm>6_)pG$BCVSwp3ZWrIcnWn^1|0d9hy-Q`^sb ze$K)`tYZ7XCWNT=r4pJ{u_1=~W$*!(flwvB1dz~R5&~(6?h6zNA;E-T0#VMn?;gMF z`(985eBoBU_uSw8-E+=8_s=`&lP6A}Oc(}mG2pup_80}Ya(gH*2yp=pA@k-_@UxqD z!}j$l*nTq$Z{OSt+ZlkbWQ>Kke?N6`d$ifC-q^daoiRqUH~xz;(iP~1OaCjK*U>NZ z|I)Q&?H5aDPA(s-yX|hX<#;QLExWUR9t7BJ+5VdAwO39zJN+#Q+FpHQMan(j?lf$# zQENDBcE9DX6f1JX+47xEujzJrgR$i^=fgmvG-S87>3Bq6DTq?6n1N6zuuvMFJ)~-= zV@w$D%}u6~x!Vs+shyAZ2n0a8aGnY%BWMKMic zHhUk6d&(#-P2PV8Ps{8@KEUYvL^EchnSJSngUK8b-hT&C0kSDbNQlVe<)JQo8_(B> za5f`O+Fe7Ab7u#I#D}pmo-;<1j3ysKX=E9z{g5IsW8liL*|Ue57XF6#na2d~#u&OL3Wd*bNi;Ia7g13*aS69w?TQl^D>TQ zD130d)9~D8BY#h^Fgsf+l&bgUKiFM&v0imsJ~k~MJJanJVXJn}Y2@8b-YZutm2#zV zG+(DHm6r!RPe{ci7=W#J7X?dH1cuD6_?+xO~@iPzEcUAI-=u$vvL)!J-N zcRlyhPTkKp=GUt8Yvo39ZfoY{I|zC1s>HnTcYaO%!Xqck&DItq&)wJ$h} zi}GeKHOx9#8*baN`m6np-?u!c>-MZxziByJcDswlEqVsrj??jb?3ZPCyOvqs+Oi&S zR{QH#xiI&*S9D62?X~C1)05_V4F>(R4j%u>lLPOr~Y3e~DzwL;^IfNSxfRy%(7 z?D2E8k1c)TMD3At51**jpxIe-Yh;-opr9v%6?FI+3VQVZ1QLa{Z?8Kw{Tb4Rqi+$k zF|5e)!S>ecRBeLiJl1b|&j0Ur^JmI<;w%`SJ8Vpi-wkx`#o5P}?gc!M%O|eoNP-ve zHv%8P+Axg2=We40&pdqtjkvReL-?R1M1xiGnY5AGGxk<0_mR}0^cPaOQ>pB!R4)BJ zt#@!RHkJN9jw9_=Y?8TGQ`3VXnr01pV@OPYjcPuWLt zei3^Xe-jYKpCkS&froKsJc0TrAdEBP-x7T80hF*$F$xl*AhnC;jX*L62+vj^y*c6JI?iUsLqZe(}Cs)%1K;>ftz%9`)S*SkuG%_!~`6 z@XiAKLE)?C`JW0-{_wuj`J=*dcuw~7vL%0bkBC=NaMHu^<#@9mO46wW?`LqX)bo5^ z(WCC~1&Ons*Jb|+4Sz%SdELx^Q{tC3{%>Xf>l*$`**_Gx3n}cY{!}DRaZqtE8#HGUagC4+> zDA1Y~7?ACTOhwpF@}FYN^BMmpVq|~l?qdIWBjMAi}kVp|3ECx{ search starts here: + /home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/lib/clang/17/include + /home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/include/aarch64-linux-android + /home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/include +End of search list. +[2/2] Linking C executable cmTC_4bdad +Android (10552028, +pgo, +bolt, +lto, -mlgo, based on r487747d) clang version 17.0.2 (https://android.googlesource.com/toolchain/llvm-project d9f89f4d16663d5012e5c09495f3b30ece3d2362) +Target: aarch64-none-linux-android24 +Thread model: posix +InstalledDir: /home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/bin + "/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/bin/ld.lld" --sysroot=/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot -pie -EL --fix-cortex-a53-843419 -z now -z relro -z max-page-size=4096 --hash-style=gnu --eh-frame-hdr -m aarch64linux -dynamic-linker /system/bin/linker64 -o cmTC_4bdad /home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/aarch64-linux-android/24/crtbegin_dynamic.o -L/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/lib/clang/17/lib/linux/aarch64 -L/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/aarch64-linux-android/24 -L/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/aarch64-linux-android -L/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib --build-id=sha1 --no-rosegment --no-undefined-version --fatal-warnings --no-undefined CMakeFiles/cmTC_4bdad.dir/CMakeCCompilerABI.c.o /home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/lib/clang/17/lib/linux/libclang_rt.builtins-aarch64-android.a -l:libunwind.a -ldl -lc /home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/lib/clang/17/lib/linux/libclang_rt.builtins-aarch64-android.a -l:libunwind.a -ldl /home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/aarch64-linux-android/24/crtend_android.o + + + +Parsed C implicit include dir info from above output: rv=done + found start of include info + found start of implicit include info + add: [/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/lib/clang/17/include] + add: [/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/include/aarch64-linux-android] + add: [/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/include] + end of search list found + collapse include dir [/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/lib/clang/17/include] ==> [/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/lib/clang/17/include] + collapse include dir [/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/include/aarch64-linux-android] ==> [/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/include/aarch64-linux-android] + collapse include dir [/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/include] ==> [/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/include] + implicit include dirs: [/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/lib/clang/17/include;/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/include/aarch64-linux-android;/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/include] + + +Parsed C implicit link information from above output: + link line regex: [^( *|.*[/\])(ld\.lld|CMAKE_LINK_STARTFILE-NOTFOUND|([^/\]+-)?ld|collect2)[^/\]*( |$)] + ignore line: [Change Dir: /home/ubuntu/repos/lui/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/CMakeFiles/CMakeTmp] + ignore line: [] + ignore line: [Run Build Command(s):/home/ubuntu/android-sdk/cmake/3.22.1/bin/ninja cmTC_4bdad && [1/2] Building C object CMakeFiles/cmTC_4bdad.dir/CMakeCCompilerABI.c.o] + ignore line: [Android (10552028 +pgo +bolt +lto -mlgo based on r487747d) clang version 17.0.2 (https://android.googlesource.com/toolchain/llvm-project d9f89f4d16663d5012e5c09495f3b30ece3d2362)] + ignore line: [Target: aarch64-none-linux-android24] + ignore line: [Thread model: posix] + ignore line: [InstalledDir: /home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/bin] + ignore line: [ (in-process)] + ignore line: [ "/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/bin/clang" -cc1 -triple aarch64-none-linux-android24 -emit-obj -mrelax-all -disable-free -clear-ast-before-backend -disable-llvm-verifier -discard-value-names -main-file-name CMakeCCompilerABI.c -mrelocation-model pic -pic-level 2 -pic-is-pie -mframe-pointer=non-leaf -ffp-contract=on -fno-rounding-math -mconstructor-aliases -funwind-tables=2 -target-cpu generic -target-feature +neon -target-feature +v8a -target-feature +fix-cortex-a53-835769 -target-abi aapcs -mllvm -treat-scalable-fixed-error-as-warning -debug-info-kind=constructor -dwarf-version=4 -debugger-tuning=gdb -v -ffunction-sections -fdata-sections -fcoverage-compilation-dir=/home/ubuntu/repos/lui/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/CMakeFiles/CMakeTmp -resource-dir /home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/lib/clang/17 -dependency-file CMakeFiles/cmTC_4bdad.dir/CMakeCCompilerABI.c.o.d -MT CMakeFiles/cmTC_4bdad.dir/CMakeCCompilerABI.c.o -sys-header-deps -D ANDROID -D _FORTIFY_SOURCE=2 -isysroot /home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot -internal-isystem /home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/lib/clang/17/include -internal-isystem /home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/local/include -internal-externc-isystem /home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/include/aarch64-linux-android -internal-externc-isystem /home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/include -internal-externc-isystem /home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/include -Wformat -fdebug-compilation-dir=/home/ubuntu/repos/lui/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/CMakeFiles/CMakeTmp -ferror-limit 19 -stack-protector 2 -fno-signed-char -fgnuc-version=4.2.1 -target-feature +outline-atomics -D__GCC_HAVE_DWARF2_CFI_ASM=1 -o CMakeFiles/cmTC_4bdad.dir/CMakeCCompilerABI.c.o -x c /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeCCompilerABI.c] + ignore line: [clang -cc1 version 17.0.2 based upon LLVM 17.0.2 default target x86_64-unknown-linux-gnu] + ignore line: [ignoring nonexistent directory "/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/local/include"] + ignore line: [ignoring nonexistent directory "/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/include"] + ignore line: [#include "..." search starts here:] + ignore line: [#include <...> search starts here:] + ignore line: [ /home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/lib/clang/17/include] + ignore line: [ /home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/include/aarch64-linux-android] + ignore line: [ /home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/include] + ignore line: [End of search list.] + ignore line: [[2/2] Linking C executable cmTC_4bdad] + ignore line: [Android (10552028 +pgo +bolt +lto -mlgo based on r487747d) clang version 17.0.2 (https://android.googlesource.com/toolchain/llvm-project d9f89f4d16663d5012e5c09495f3b30ece3d2362)] + ignore line: [Target: aarch64-none-linux-android24] + ignore line: [Thread model: posix] + ignore line: [InstalledDir: /home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/bin] + link line: [ "/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/bin/ld.lld" --sysroot=/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot -pie -EL --fix-cortex-a53-843419 -z now -z relro -z max-page-size=4096 --hash-style=gnu --eh-frame-hdr -m aarch64linux -dynamic-linker /system/bin/linker64 -o cmTC_4bdad /home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/aarch64-linux-android/24/crtbegin_dynamic.o -L/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/lib/clang/17/lib/linux/aarch64 -L/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/aarch64-linux-android/24 -L/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/aarch64-linux-android -L/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib --build-id=sha1 --no-rosegment --no-undefined-version --fatal-warnings --no-undefined CMakeFiles/cmTC_4bdad.dir/CMakeCCompilerABI.c.o /home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/lib/clang/17/lib/linux/libclang_rt.builtins-aarch64-android.a -l:libunwind.a -ldl -lc /home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/lib/clang/17/lib/linux/libclang_rt.builtins-aarch64-android.a -l:libunwind.a -ldl /home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/aarch64-linux-android/24/crtend_android.o] + arg [/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/bin/ld.lld] ==> ignore + arg [--sysroot=/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot] ==> ignore + arg [-pie] ==> ignore + arg [-EL] ==> ignore + arg [--fix-cortex-a53-843419] ==> ignore + arg [-znow] ==> ignore + arg [-zrelro] ==> ignore + arg [-zmax-page-size=4096] ==> ignore + arg [--hash-style=gnu] ==> ignore + arg [--eh-frame-hdr] ==> ignore + arg [-m] ==> ignore + arg [aarch64linux] ==> ignore + arg [-dynamic-linker] ==> ignore + arg [/system/bin/linker64] ==> ignore + arg [-o] ==> ignore + arg [cmTC_4bdad] ==> ignore + arg [/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/aarch64-linux-android/24/crtbegin_dynamic.o] ==> obj [/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/aarch64-linux-android/24/crtbegin_dynamic.o] + arg [-L/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/lib/clang/17/lib/linux/aarch64] ==> dir [/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/lib/clang/17/lib/linux/aarch64] + arg [-L/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/aarch64-linux-android/24] ==> dir [/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/aarch64-linux-android/24] + arg [-L/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/aarch64-linux-android] ==> dir [/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/aarch64-linux-android] + arg [-L/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib] ==> dir [/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib] + arg [--build-id=sha1] ==> ignore + arg [--no-rosegment] ==> ignore + arg [--no-undefined-version] ==> ignore + arg [--fatal-warnings] ==> ignore + arg [--no-undefined] ==> ignore + arg [CMakeFiles/cmTC_4bdad.dir/CMakeCCompilerABI.c.o] ==> ignore + arg [/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/lib/clang/17/lib/linux/libclang_rt.builtins-aarch64-android.a] ==> lib [/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/lib/clang/17/lib/linux/libclang_rt.builtins-aarch64-android.a] + arg [-l:libunwind.a] ==> lib [-l:libunwind.a] + arg [-ldl] ==> lib [dl] + arg [-lc] ==> lib [c] + arg [/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/lib/clang/17/lib/linux/libclang_rt.builtins-aarch64-android.a] ==> lib [/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/lib/clang/17/lib/linux/libclang_rt.builtins-aarch64-android.a] + arg [-l:libunwind.a] ==> lib [-l:libunwind.a] + arg [-ldl] ==> lib [dl] + arg [/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/aarch64-linux-android/24/crtend_android.o] ==> obj [/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/aarch64-linux-android/24/crtend_android.o] + remove lib [/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/lib/clang/17/lib/linux/libclang_rt.builtins-aarch64-android.a] + remove lib [/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/lib/clang/17/lib/linux/libclang_rt.builtins-aarch64-android.a] + collapse library dir [/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/lib/clang/17/lib/linux/aarch64] ==> [/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/lib/clang/17/lib/linux/aarch64] + collapse library dir [/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/aarch64-linux-android/24] ==> [/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/aarch64-linux-android/24] + collapse library dir [/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/aarch64-linux-android] ==> [/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/aarch64-linux-android] + collapse library dir [/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib] ==> [/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib] + implicit libs: [-l:libunwind.a;dl;c;-l:libunwind.a;dl] + implicit objs: [/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/aarch64-linux-android/24/crtbegin_dynamic.o;/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/aarch64-linux-android/24/crtend_android.o] + implicit dirs: [/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/lib/clang/17/lib/linux/aarch64;/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/aarch64-linux-android/24;/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/aarch64-linux-android;/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib] + implicit fwks: [] + + diff --git a/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/CMakeFiles/TargetDirectories.txt b/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/CMakeFiles/TargetDirectories.txt new file mode 100644 index 00000000..a6a0c6f1 --- /dev/null +++ b/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/CMakeFiles/TargetDirectories.txt @@ -0,0 +1,3 @@ +/home/ubuntu/repos/lui/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/CMakeFiles/lui_jni_bridge.dir +/home/ubuntu/repos/lui/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/CMakeFiles/edit_cache.dir +/home/ubuntu/repos/lui/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/CMakeFiles/rebuild_cache.dir diff --git a/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/CMakeFiles/cmake.check_cache b/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/CMakeFiles/cmake.check_cache new file mode 100644 index 00000000..3dccd731 --- /dev/null +++ b/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/CMakeFiles/cmake.check_cache @@ -0,0 +1 @@ +# This file is generated by cmake for dependency checking of the CMakeCache.txt file diff --git a/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/CMakeFiles/lui_jni_bridge.dir/lui_jni_bridge_stub.c.o b/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/CMakeFiles/lui_jni_bridge.dir/lui_jni_bridge_stub.c.o new file mode 100644 index 0000000000000000000000000000000000000000..3e79aeca46a4c51becafed73be039e532058ba36 GIT binary patch literal 41056 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 O`1>*}cRc))`TqykVQioP literal 0 HcmV?d00001 diff --git a/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/CMakeFiles/rules.ninja b/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/CMakeFiles/rules.ninja new file mode 100644 index 00000000..1264916c --- /dev/null +++ b/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/CMakeFiles/rules.ninja @@ -0,0 +1,64 @@ +# CMAKE generated file: DO NOT EDIT! +# Generated by "Ninja" Generator, CMake Version 3.22 + +# This file contains all the rules used to get the outputs files +# built from the input files. +# It is included in the main 'build.ninja'. + +# ============================================================================= +# Project: lui_components_bridge +# Configurations: Debug +# ============================================================================= +# ============================================================================= + +############################################# +# Rule for compiling C files. + +rule C_COMPILER__lui_jni_bridge_Debug + depfile = $DEP_FILE + deps = gcc + command = /home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/bin/clang --target=aarch64-none-linux-android24 --sysroot=/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot $DEFINES $INCLUDES $FLAGS -MD -MT $out -MF $DEP_FILE -o $out -c $in + description = Building C object $out + + +############################################# +# Rule for linking C shared library. + +rule C_SHARED_LIBRARY_LINKER__lui_jni_bridge_Debug + command = $PRE_LINK && /home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/bin/clang --target=aarch64-none-linux-android24 --sysroot=/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot -fPIC $LANGUAGE_COMPILE_FLAGS $ARCH_FLAGS $LINK_FLAGS -shared $SONAME_FLAG$SONAME -o $TARGET_FILE $in $LINK_PATH $LINK_LIBRARIES && $POST_BUILD + description = Linking C shared library $TARGET_FILE + restat = $RESTAT + + +############################################# +# Rule for running custom commands. + +rule CUSTOM_COMMAND + command = $COMMAND + description = $DESC + + +############################################# +# Rule for re-running cmake. + +rule RERUN_CMAKE + command = /home/ubuntu/android-sdk/cmake/3.22.1/bin/cmake --regenerate-during-build -S/home/ubuntu/repos/lui/examples/components/android/app/src/main/cpp -B/home/ubuntu/repos/lui/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a + description = Re-running CMake... + generator = 1 + + +############################################# +# Rule for cleaning all built files. + +rule CLEAN + command = /home/ubuntu/android-sdk/cmake/3.22.1/bin/ninja $FILE_ARG -t clean $TARGETS + description = Cleaning all built files... + + +############################################# +# Rule for printing all primary targets available. + +rule HELP + command = /home/ubuntu/android-sdk/cmake/3.22.1/bin/ninja -t targets + description = All primary targets available: + diff --git a/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/additional_project_files.txt b/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/additional_project_files.txt new file mode 100644 index 00000000..e69de29b diff --git a/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/android_gradle_build.json b/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/android_gradle_build.json new file mode 100644 index 00000000..8526f9de --- /dev/null +++ b/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/android_gradle_build.json @@ -0,0 +1,38 @@ +{ + "buildFiles": [ + "/home/ubuntu/repos/lui/examples/components/android/app/src/main/cpp/CMakeLists.txt" + ], + "cleanCommandsComponents": [ + [ + "/home/ubuntu/android-sdk/cmake/3.22.1/bin/ninja", + "-C", + "/home/ubuntu/repos/lui/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a", + "clean" + ] + ], + "buildTargetsCommandComponents": [ + "/home/ubuntu/android-sdk/cmake/3.22.1/bin/ninja", + "-C", + "/home/ubuntu/repos/lui/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a", + "{LIST_OF_TARGETS_TO_BUILD}" + ], + "libraries": { + "lui_jni_bridge::@6890427a1f51a3e7e1df": { + "toolchain": "toolchain", + "abi": "arm64-v8a", + "artifactName": "lui_jni_bridge", + "output": "/home/ubuntu/repos/lui/examples/components/android/app/build/intermediates/cxx/Debug/406wr1e2/obj/arm64-v8a/liblui_jni_bridge.so", + "runtimeFiles": [] + } + }, + "toolchains": { + "toolchain": { + "cCompilerExecutable": "/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/bin/clang.lld", + "cppCompilerExecutable": "/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/bin/clang++.lld" + } + }, + "cFileExtensions": [ + "c" + ], + "cppFileExtensions": [] +} \ No newline at end of file diff --git a/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/android_gradle_build_mini.json b/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/android_gradle_build_mini.json new file mode 100644 index 00000000..1ce7ed08 --- /dev/null +++ b/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/android_gradle_build_mini.json @@ -0,0 +1,27 @@ +{ + "buildFiles": [ + "/home/ubuntu/repos/lui/examples/components/android/app/src/main/cpp/CMakeLists.txt" + ], + "cleanCommandsComponents": [ + [ + "/home/ubuntu/android-sdk/cmake/3.22.1/bin/ninja", + "-C", + "/home/ubuntu/repos/lui/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a", + "clean" + ] + ], + "buildTargetsCommandComponents": [ + "/home/ubuntu/android-sdk/cmake/3.22.1/bin/ninja", + "-C", + "/home/ubuntu/repos/lui/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a", + "{LIST_OF_TARGETS_TO_BUILD}" + ], + "libraries": { + "lui_jni_bridge::@6890427a1f51a3e7e1df": { + "artifactName": "lui_jni_bridge", + "abi": "arm64-v8a", + "output": "/home/ubuntu/repos/lui/examples/components/android/app/build/intermediates/cxx/Debug/406wr1e2/obj/arm64-v8a/liblui_jni_bridge.so", + "runtimeFiles": [] + } + } +} \ No newline at end of file diff --git a/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/build.ninja b/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/build.ninja new file mode 100644 index 00000000..c3913c71 --- /dev/null +++ b/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/build.ninja @@ -0,0 +1,154 @@ +# CMAKE generated file: DO NOT EDIT! +# Generated by "Ninja" Generator, CMake Version 3.22 + +# This file contains all the build statements describing the +# compilation DAG. + +# ============================================================================= +# Write statements declared in CMakeLists.txt: +# +# Which is the root file. +# ============================================================================= + +# ============================================================================= +# Project: lui_components_bridge +# Configurations: Debug +# ============================================================================= + +############################################# +# Minimal version of Ninja required by this file + +ninja_required_version = 1.5 + + +############################################# +# Set configuration variable for custom commands. + +CONFIGURATION = Debug +# ============================================================================= +# Include auxiliary files. + + +############################################# +# Include rules file. + +include CMakeFiles/rules.ninja + +# ============================================================================= + +############################################# +# Logical path to working directory; prefix for absolute paths. + +cmake_ninja_workdir = /home/ubuntu/repos/lui/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/ +# ============================================================================= +# Object build statements for SHARED_LIBRARY target lui_jni_bridge + + +############################################# +# Order-only phony target for lui_jni_bridge + +build cmake_object_order_depends_target_lui_jni_bridge: phony || CMakeFiles/lui_jni_bridge.dir + +build CMakeFiles/lui_jni_bridge.dir/lui_jni_bridge_stub.c.o: C_COMPILER__lui_jni_bridge_Debug /home/ubuntu/repos/lui/examples/components/android/app/src/main/cpp/lui_jni_bridge_stub.c || cmake_object_order_depends_target_lui_jni_bridge + DEFINES = -Dlui_jni_bridge_EXPORTS + DEP_FILE = CMakeFiles/lui_jni_bridge.dir/lui_jni_bridge_stub.c.o.d + FLAGS = -g -DANDROID -fdata-sections -ffunction-sections -funwind-tables -fstack-protector-strong -no-canonical-prefixes -D_FORTIFY_SOURCE=2 -Wformat -Werror=format-security -fno-limit-debug-info -fPIC + OBJECT_DIR = CMakeFiles/lui_jni_bridge.dir + OBJECT_FILE_DIR = CMakeFiles/lui_jni_bridge.dir + TARGET_COMPILE_PDB = CMakeFiles/lui_jni_bridge.dir/ + TARGET_PDB = /home/ubuntu/repos/lui/examples/components/android/app/build/intermediates/cxx/Debug/406wr1e2/obj/arm64-v8a/liblui_jni_bridge.pdb + + +# ============================================================================= +# Link build statements for SHARED_LIBRARY target lui_jni_bridge + + +############################################# +# Link the shared library /home/ubuntu/repos/lui/examples/components/android/app/build/intermediates/cxx/Debug/406wr1e2/obj/arm64-v8a/liblui_jni_bridge.so + +build /home/ubuntu/repos/lui/examples/components/android/app/build/intermediates/cxx/Debug/406wr1e2/obj/arm64-v8a/liblui_jni_bridge.so: C_SHARED_LIBRARY_LINKER__lui_jni_bridge_Debug CMakeFiles/lui_jni_bridge.dir/lui_jni_bridge_stub.c.o | /home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/aarch64-linux-android/24/liblog.so + LANGUAGE_COMPILE_FLAGS = -g -DANDROID -fdata-sections -ffunction-sections -funwind-tables -fstack-protector-strong -no-canonical-prefixes -D_FORTIFY_SOURCE=2 -Wformat -Werror=format-security -fno-limit-debug-info + LINK_FLAGS = -static-libstdc++ -Wl,--build-id=sha1 -Wl,--no-rosegment -Wl,--no-undefined-version -Wl,--fatal-warnings -Wl,--no-undefined -Qunused-arguments + LINK_LIBRARIES = /home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/aarch64-linux-android/24/liblog.so -latomic -lm + OBJECT_DIR = CMakeFiles/lui_jni_bridge.dir + POST_BUILD = : + PRE_LINK = : + SONAME = liblui_jni_bridge.so + SONAME_FLAG = -Wl,-soname, + TARGET_COMPILE_PDB = CMakeFiles/lui_jni_bridge.dir/ + TARGET_FILE = /home/ubuntu/repos/lui/examples/components/android/app/build/intermediates/cxx/Debug/406wr1e2/obj/arm64-v8a/liblui_jni_bridge.so + TARGET_PDB = /home/ubuntu/repos/lui/examples/components/android/app/build/intermediates/cxx/Debug/406wr1e2/obj/arm64-v8a/liblui_jni_bridge.pdb + + +############################################# +# Utility command for edit_cache + +build CMakeFiles/edit_cache.util: CUSTOM_COMMAND + COMMAND = cd /home/ubuntu/repos/lui/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a && /home/ubuntu/android-sdk/cmake/3.22.1/bin/cmake -E echo No\ interactive\ CMake\ dialog\ available. + DESC = No interactive CMake dialog available... + restat = 1 + +build edit_cache: phony CMakeFiles/edit_cache.util + + +############################################# +# Utility command for rebuild_cache + +build CMakeFiles/rebuild_cache.util: CUSTOM_COMMAND + COMMAND = cd /home/ubuntu/repos/lui/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a && /home/ubuntu/android-sdk/cmake/3.22.1/bin/cmake --regenerate-during-build -S/home/ubuntu/repos/lui/examples/components/android/app/src/main/cpp -B/home/ubuntu/repos/lui/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a + DESC = Running CMake to regenerate build system... + pool = console + restat = 1 + +build rebuild_cache: phony CMakeFiles/rebuild_cache.util + +# ============================================================================= +# Target aliases. + +build liblui_jni_bridge.so: phony /home/ubuntu/repos/lui/examples/components/android/app/build/intermediates/cxx/Debug/406wr1e2/obj/arm64-v8a/liblui_jni_bridge.so + +build lui_jni_bridge: phony /home/ubuntu/repos/lui/examples/components/android/app/build/intermediates/cxx/Debug/406wr1e2/obj/arm64-v8a/liblui_jni_bridge.so + +# ============================================================================= +# Folder targets. + +# ============================================================================= + +############################################# +# Folder: /home/ubuntu/repos/lui/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a + +build all: phony /home/ubuntu/repos/lui/examples/components/android/app/build/intermediates/cxx/Debug/406wr1e2/obj/arm64-v8a/liblui_jni_bridge.so + +# ============================================================================= +# Built-in targets + + +############################################# +# Re-run CMake if any of its inputs changed. + +build build.ninja: RERUN_CMAKE | /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeCCompiler.cmake.in /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeCCompilerABI.c /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeCInformation.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeCommonLanguageInclude.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeCompilerIdDetection.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeDetermineCCompiler.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeDetermineCompileFeatures.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeDetermineCompiler.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeDetermineCompilerABI.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeDetermineCompilerId.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeDetermineSystem.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeFindBinUtils.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeGenericSystem.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeInitializeConfigs.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeLanguageInformation.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeParseImplicitIncludeInfo.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeParseImplicitLinkInfo.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeParseLibraryArchitecture.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeSystem.cmake.in /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeSystemSpecificInformation.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeSystemSpecificInitialize.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeTestCCompiler.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeTestCompilerCommon.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/ADSP-DetermineCompiler.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/ARMCC-DetermineCompiler.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/ARMClang-DetermineCompiler.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/AppleClang-DetermineCompiler.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/Borland-DetermineCompiler.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/Bruce-C-DetermineCompiler.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/CMakeCommonCompilerMacros.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/Clang-C.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/Clang-DetermineCompiler.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/Clang-DetermineCompilerInternal.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/Clang-FindBinUtils.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/Clang.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/Compaq-C-DetermineCompiler.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/Cray-DetermineCompiler.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/Embarcadero-DetermineCompiler.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/Fujitsu-DetermineCompiler.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/FujitsuClang-DetermineCompiler.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/GHS-DetermineCompiler.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/GNU-C-DetermineCompiler.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/GNU.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/HP-C-DetermineCompiler.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/IAR-DetermineCompiler.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/IBMCPP-C-DetermineVersionInternal.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/Intel-DetermineCompiler.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/IntelLLVM-DetermineCompiler.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/MSVC-DetermineCompiler.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/NVHPC-DetermineCompiler.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/NVIDIA-DetermineCompiler.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/OpenWatcom-DetermineCompiler.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/PGI-DetermineCompiler.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/PathScale-DetermineCompiler.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/SCO-DetermineCompiler.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/SDCC-C-DetermineCompiler.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/SunPro-C-DetermineCompiler.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/TI-DetermineCompiler.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/TinyCC-C-DetermineCompiler.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/VisualAge-C-DetermineCompiler.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/Watcom-DetermineCompiler.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/XL-C-DetermineCompiler.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/XLClang-C-DetermineCompiler.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/zOS-C-DetermineCompiler.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Internal/FeatureTesting.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Platform/Android-Clang-C.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Platform/Android-Clang.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Platform/Android-Determine-C.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Platform/Android-Determine.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Platform/Android-Initialize.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Platform/Android.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Platform/Android/Determine-Compiler.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Platform/Linux.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Platform/UnixPaths.cmake /home/ubuntu/android-sdk/ndk/26.1.10909125/build/cmake/android-legacy.toolchain.cmake /home/ubuntu/android-sdk/ndk/26.1.10909125/build/cmake/android.toolchain.cmake /home/ubuntu/android-sdk/ndk/26.1.10909125/build/cmake/flags.cmake /home/ubuntu/android-sdk/ndk/26.1.10909125/build/cmake/hooks/pre/Android-Clang.cmake /home/ubuntu/android-sdk/ndk/26.1.10909125/build/cmake/hooks/pre/Android-Determine.cmake /home/ubuntu/android-sdk/ndk/26.1.10909125/build/cmake/hooks/pre/Android-Initialize.cmake /home/ubuntu/android-sdk/ndk/26.1.10909125/build/cmake/hooks/pre/Android.cmake /home/ubuntu/android-sdk/ndk/26.1.10909125/build/cmake/hooks/pre/Determine-Compiler.cmake /home/ubuntu/android-sdk/ndk/26.1.10909125/build/cmake/platforms.cmake /home/ubuntu/repos/lui/examples/components/android/app/src/main/cpp/CMakeLists.txt CMakeCache.txt CMakeFiles/3.22.1-g37088a8/CMakeCCompiler.cmake CMakeFiles/3.22.1-g37088a8/CMakeSystem.cmake + pool = console + + +############################################# +# A missing CMake input file is not an error. + +build /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeCCompiler.cmake.in /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeCCompilerABI.c /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeCInformation.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeCommonLanguageInclude.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeCompilerIdDetection.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeDetermineCCompiler.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeDetermineCompileFeatures.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeDetermineCompiler.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeDetermineCompilerABI.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeDetermineCompilerId.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeDetermineSystem.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeFindBinUtils.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeGenericSystem.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeInitializeConfigs.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeLanguageInformation.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeParseImplicitIncludeInfo.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeParseImplicitLinkInfo.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeParseLibraryArchitecture.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeSystem.cmake.in /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeSystemSpecificInformation.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeSystemSpecificInitialize.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeTestCCompiler.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeTestCompilerCommon.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/ADSP-DetermineCompiler.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/ARMCC-DetermineCompiler.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/ARMClang-DetermineCompiler.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/AppleClang-DetermineCompiler.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/Borland-DetermineCompiler.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/Bruce-C-DetermineCompiler.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/CMakeCommonCompilerMacros.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/Clang-C.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/Clang-DetermineCompiler.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/Clang-DetermineCompilerInternal.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/Clang-FindBinUtils.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/Clang.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/Compaq-C-DetermineCompiler.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/Cray-DetermineCompiler.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/Embarcadero-DetermineCompiler.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/Fujitsu-DetermineCompiler.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/FujitsuClang-DetermineCompiler.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/GHS-DetermineCompiler.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/GNU-C-DetermineCompiler.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/GNU.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/HP-C-DetermineCompiler.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/IAR-DetermineCompiler.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/IBMCPP-C-DetermineVersionInternal.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/Intel-DetermineCompiler.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/IntelLLVM-DetermineCompiler.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/MSVC-DetermineCompiler.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/NVHPC-DetermineCompiler.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/NVIDIA-DetermineCompiler.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/OpenWatcom-DetermineCompiler.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/PGI-DetermineCompiler.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/PathScale-DetermineCompiler.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/SCO-DetermineCompiler.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/SDCC-C-DetermineCompiler.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/SunPro-C-DetermineCompiler.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/TI-DetermineCompiler.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/TinyCC-C-DetermineCompiler.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/VisualAge-C-DetermineCompiler.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/Watcom-DetermineCompiler.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/XL-C-DetermineCompiler.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/XLClang-C-DetermineCompiler.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/zOS-C-DetermineCompiler.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Internal/FeatureTesting.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Platform/Android-Clang-C.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Platform/Android-Clang.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Platform/Android-Determine-C.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Platform/Android-Determine.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Platform/Android-Initialize.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Platform/Android.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Platform/Android/Determine-Compiler.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Platform/Linux.cmake /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/Platform/UnixPaths.cmake /home/ubuntu/android-sdk/ndk/26.1.10909125/build/cmake/android-legacy.toolchain.cmake /home/ubuntu/android-sdk/ndk/26.1.10909125/build/cmake/android.toolchain.cmake /home/ubuntu/android-sdk/ndk/26.1.10909125/build/cmake/flags.cmake /home/ubuntu/android-sdk/ndk/26.1.10909125/build/cmake/hooks/pre/Android-Clang.cmake /home/ubuntu/android-sdk/ndk/26.1.10909125/build/cmake/hooks/pre/Android-Determine.cmake /home/ubuntu/android-sdk/ndk/26.1.10909125/build/cmake/hooks/pre/Android-Initialize.cmake /home/ubuntu/android-sdk/ndk/26.1.10909125/build/cmake/hooks/pre/Android.cmake /home/ubuntu/android-sdk/ndk/26.1.10909125/build/cmake/hooks/pre/Determine-Compiler.cmake /home/ubuntu/android-sdk/ndk/26.1.10909125/build/cmake/platforms.cmake /home/ubuntu/repos/lui/examples/components/android/app/src/main/cpp/CMakeLists.txt CMakeCache.txt CMakeFiles/3.22.1-g37088a8/CMakeCCompiler.cmake CMakeFiles/3.22.1-g37088a8/CMakeSystem.cmake: phony + + +############################################# +# Clean all the built files. + +build clean: CLEAN + + +############################################# +# Print all primary targets available. + +build help: HELP + + +############################################# +# Make the all target the default. + +default all diff --git a/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/build_file_index.txt b/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/build_file_index.txt new file mode 100644 index 00000000..68b383b9 --- /dev/null +++ b/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/build_file_index.txt @@ -0,0 +1 @@ +/home/ubuntu/repos/lui/examples/components/android/app/src/main/cpp/CMakeLists.txt \ No newline at end of file diff --git a/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/cmake_install.cmake b/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/cmake_install.cmake new file mode 100644 index 00000000..717f8a46 --- /dev/null +++ b/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/cmake_install.cmake @@ -0,0 +1,54 @@ +# Install script for directory: /home/ubuntu/repos/lui/examples/components/android/app/src/main/cpp + +# Set the install prefix +if(NOT DEFINED CMAKE_INSTALL_PREFIX) + set(CMAKE_INSTALL_PREFIX "/usr/local") +endif() +string(REGEX REPLACE "/$" "" CMAKE_INSTALL_PREFIX "${CMAKE_INSTALL_PREFIX}") + +# Set the install configuration name. +if(NOT DEFINED CMAKE_INSTALL_CONFIG_NAME) + if(BUILD_TYPE) + string(REGEX REPLACE "^[^A-Za-z0-9_]+" "" + CMAKE_INSTALL_CONFIG_NAME "${BUILD_TYPE}") + else() + set(CMAKE_INSTALL_CONFIG_NAME "Debug") + endif() + message(STATUS "Install configuration: \"${CMAKE_INSTALL_CONFIG_NAME}\"") +endif() + +# Set the component getting installed. +if(NOT CMAKE_INSTALL_COMPONENT) + if(COMPONENT) + message(STATUS "Install component: \"${COMPONENT}\"") + set(CMAKE_INSTALL_COMPONENT "${COMPONENT}") + else() + set(CMAKE_INSTALL_COMPONENT) + endif() +endif() + +# Install shared libraries without execute permission? +if(NOT DEFINED CMAKE_INSTALL_SO_NO_EXE) + set(CMAKE_INSTALL_SO_NO_EXE "1") +endif() + +# Is this installation the result of a crosscompile? +if(NOT DEFINED CMAKE_CROSSCOMPILING) + set(CMAKE_CROSSCOMPILING "TRUE") +endif() + +# Set default install directory permissions. +if(NOT DEFINED CMAKE_OBJDUMP) + set(CMAKE_OBJDUMP "/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-objdump") +endif() + +if(CMAKE_INSTALL_COMPONENT) + set(CMAKE_INSTALL_MANIFEST "install_manifest_${CMAKE_INSTALL_COMPONENT}.txt") +else() + set(CMAKE_INSTALL_MANIFEST "install_manifest.txt") +endif() + +string(REPLACE ";" "\n" CMAKE_INSTALL_MANIFEST_CONTENT + "${CMAKE_INSTALL_MANIFEST_FILES}") +file(WRITE "/home/ubuntu/repos/lui/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/${CMAKE_INSTALL_MANIFEST}" + "${CMAKE_INSTALL_MANIFEST_CONTENT}") diff --git a/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/compile_commands.json b/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/compile_commands.json new file mode 100644 index 00000000..4fc5cabf --- /dev/null +++ b/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/compile_commands.json @@ -0,0 +1,7 @@ +[ +{ + "directory": "/home/ubuntu/repos/lui/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a", + "command": "/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/bin/clang --target=aarch64-none-linux-android24 --sysroot=/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot -Dlui_jni_bridge_EXPORTS -g -DANDROID -fdata-sections -ffunction-sections -funwind-tables -fstack-protector-strong -no-canonical-prefixes -D_FORTIFY_SOURCE=2 -Wformat -Werror=format-security -fno-limit-debug-info -fPIC -o CMakeFiles/lui_jni_bridge.dir/lui_jni_bridge_stub.c.o -c /home/ubuntu/repos/lui/examples/components/android/app/src/main/cpp/lui_jni_bridge_stub.c", + "file": "/home/ubuntu/repos/lui/examples/components/android/app/src/main/cpp/lui_jni_bridge_stub.c" +} +] \ No newline at end of file diff --git a/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/compile_commands.json.bin b/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a/compile_commands.json.bin new file mode 100644 index 0000000000000000000000000000000000000000..2f1e922badbe1ab8bf066181464f781b4e42262d GIT binary patch literal 922 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 oj;(io^ng9jU9}O&9D!f$kosH=x>24 & 0x00FF) +# define COMPILER_VERSION_MINOR HEX(__CODEGEARC_VERSION__>>16 & 0x00FF) +# define COMPILER_VERSION_PATCH DEC(__CODEGEARC_VERSION__ & 0xFFFF) + +#elif defined(__BORLANDC__) +# define COMPILER_ID "Borland" + /* __BORLANDC__ = 0xVRR */ +# define COMPILER_VERSION_MAJOR HEX(__BORLANDC__>>8) +# define COMPILER_VERSION_MINOR HEX(__BORLANDC__ & 0xFF) + +#elif defined(__WATCOMC__) && __WATCOMC__ < 1200 +# define COMPILER_ID "Watcom" + /* __WATCOMC__ = VVRR */ +# define COMPILER_VERSION_MAJOR DEC(__WATCOMC__ / 100) +# define COMPILER_VERSION_MINOR DEC((__WATCOMC__ / 10) % 10) +# if (__WATCOMC__ % 10) > 0 +# define COMPILER_VERSION_PATCH DEC(__WATCOMC__ % 10) +# endif + +#elif defined(__WATCOMC__) +# define COMPILER_ID "OpenWatcom" + /* __WATCOMC__ = VVRP + 1100 */ +# define COMPILER_VERSION_MAJOR DEC((__WATCOMC__ - 1100) / 100) +# define COMPILER_VERSION_MINOR DEC((__WATCOMC__ / 10) % 10) +# if (__WATCOMC__ % 10) > 0 +# define COMPILER_VERSION_PATCH DEC(__WATCOMC__ % 10) +# endif + +#elif defined(__SUNPRO_C) +# define COMPILER_ID "SunPro" +# if __SUNPRO_C >= 0x5100 + /* __SUNPRO_C = 0xVRRP */ +# define COMPILER_VERSION_MAJOR HEX(__SUNPRO_C>>12) +# define COMPILER_VERSION_MINOR HEX(__SUNPRO_C>>4 & 0xFF) +# define COMPILER_VERSION_PATCH HEX(__SUNPRO_C & 0xF) +# else + /* __SUNPRO_CC = 0xVRP */ +# define COMPILER_VERSION_MAJOR HEX(__SUNPRO_C>>8) +# define COMPILER_VERSION_MINOR HEX(__SUNPRO_C>>4 & 0xF) +# define COMPILER_VERSION_PATCH HEX(__SUNPRO_C & 0xF) +# endif + +#elif defined(__HP_cc) +# define COMPILER_ID "HP" + /* __HP_cc = VVRRPP */ +# define COMPILER_VERSION_MAJOR DEC(__HP_cc/10000) +# define COMPILER_VERSION_MINOR DEC(__HP_cc/100 % 100) +# define COMPILER_VERSION_PATCH DEC(__HP_cc % 100) + +#elif defined(__DECC) +# define COMPILER_ID "Compaq" + /* __DECC_VER = VVRRTPPPP */ +# define COMPILER_VERSION_MAJOR DEC(__DECC_VER/10000000) +# define COMPILER_VERSION_MINOR DEC(__DECC_VER/100000 % 100) +# define COMPILER_VERSION_PATCH DEC(__DECC_VER % 10000) + +#elif defined(__IBMC__) && defined(__COMPILER_VER__) +# define COMPILER_ID "zOS" + /* __IBMC__ = VRP */ +# define COMPILER_VERSION_MAJOR DEC(__IBMC__/100) +# define COMPILER_VERSION_MINOR DEC(__IBMC__/10 % 10) +# define COMPILER_VERSION_PATCH DEC(__IBMC__ % 10) + +#elif defined(__ibmxl__) && defined(__clang__) +# define COMPILER_ID "XLClang" +# define COMPILER_VERSION_MAJOR DEC(__ibmxl_version__) +# define COMPILER_VERSION_MINOR DEC(__ibmxl_release__) +# define COMPILER_VERSION_PATCH DEC(__ibmxl_modification__) +# define COMPILER_VERSION_TWEAK DEC(__ibmxl_ptf_fix_level__) + + +#elif defined(__IBMC__) && !defined(__COMPILER_VER__) && __IBMC__ >= 800 +# define COMPILER_ID "XL" + /* __IBMC__ = VRP */ +# define COMPILER_VERSION_MAJOR DEC(__IBMC__/100) +# define COMPILER_VERSION_MINOR DEC(__IBMC__/10 % 10) +# define COMPILER_VERSION_PATCH DEC(__IBMC__ % 10) + +#elif defined(__IBMC__) && !defined(__COMPILER_VER__) && __IBMC__ < 800 +# define COMPILER_ID "VisualAge" + /* __IBMC__ = VRP */ +# define COMPILER_VERSION_MAJOR DEC(__IBMC__/100) +# define COMPILER_VERSION_MINOR DEC(__IBMC__/10 % 10) +# define COMPILER_VERSION_PATCH DEC(__IBMC__ % 10) + +#elif defined(__NVCOMPILER) +# define COMPILER_ID "NVHPC" +# define COMPILER_VERSION_MAJOR DEC(__NVCOMPILER_MAJOR__) +# define COMPILER_VERSION_MINOR DEC(__NVCOMPILER_MINOR__) +# if defined(__NVCOMPILER_PATCHLEVEL__) +# define COMPILER_VERSION_PATCH DEC(__NVCOMPILER_PATCHLEVEL__) +# endif + +#elif defined(__PGI) +# define COMPILER_ID "PGI" +# define COMPILER_VERSION_MAJOR DEC(__PGIC__) +# define COMPILER_VERSION_MINOR DEC(__PGIC_MINOR__) +# if defined(__PGIC_PATCHLEVEL__) +# define COMPILER_VERSION_PATCH DEC(__PGIC_PATCHLEVEL__) +# endif + +#elif defined(_CRAYC) +# define COMPILER_ID "Cray" +# define COMPILER_VERSION_MAJOR DEC(_RELEASE_MAJOR) +# define COMPILER_VERSION_MINOR DEC(_RELEASE_MINOR) + +#elif defined(__TI_COMPILER_VERSION__) +# define COMPILER_ID "TI" + /* __TI_COMPILER_VERSION__ = VVVRRRPPP */ +# define COMPILER_VERSION_MAJOR DEC(__TI_COMPILER_VERSION__/1000000) +# define COMPILER_VERSION_MINOR DEC(__TI_COMPILER_VERSION__/1000 % 1000) +# define COMPILER_VERSION_PATCH DEC(__TI_COMPILER_VERSION__ % 1000) + +#elif defined(__CLANG_FUJITSU) +# define COMPILER_ID "FujitsuClang" +# define COMPILER_VERSION_MAJOR DEC(__FCC_major__) +# define COMPILER_VERSION_MINOR DEC(__FCC_minor__) +# define COMPILER_VERSION_PATCH DEC(__FCC_patchlevel__) +# define COMPILER_VERSION_INTERNAL_STR __clang_version__ + + +#elif defined(__FUJITSU) +# define COMPILER_ID "Fujitsu" +# if defined(__FCC_version__) +# define COMPILER_VERSION __FCC_version__ +# elif defined(__FCC_major__) +# define COMPILER_VERSION_MAJOR DEC(__FCC_major__) +# define COMPILER_VERSION_MINOR DEC(__FCC_minor__) +# define COMPILER_VERSION_PATCH DEC(__FCC_patchlevel__) +# endif +# if defined(__fcc_version) +# define COMPILER_VERSION_INTERNAL DEC(__fcc_version) +# elif defined(__FCC_VERSION) +# define COMPILER_VERSION_INTERNAL DEC(__FCC_VERSION) +# endif + + +#elif defined(__ghs__) +# define COMPILER_ID "GHS" +/* __GHS_VERSION_NUMBER = VVVVRP */ +# ifdef __GHS_VERSION_NUMBER +# define COMPILER_VERSION_MAJOR DEC(__GHS_VERSION_NUMBER / 100) +# define COMPILER_VERSION_MINOR DEC(__GHS_VERSION_NUMBER / 10 % 10) +# define COMPILER_VERSION_PATCH DEC(__GHS_VERSION_NUMBER % 10) +# endif + +#elif defined(__TINYC__) +# define COMPILER_ID "TinyCC" + +#elif defined(__BCC__) +# define COMPILER_ID "Bruce" + +#elif defined(__SCO_VERSION__) +# define COMPILER_ID "SCO" + +#elif defined(__ARMCC_VERSION) && !defined(__clang__) +# define COMPILER_ID "ARMCC" +#if __ARMCC_VERSION >= 1000000 + /* __ARMCC_VERSION = VRRPPPP */ + # define COMPILER_VERSION_MAJOR DEC(__ARMCC_VERSION/1000000) + # define COMPILER_VERSION_MINOR DEC(__ARMCC_VERSION/10000 % 100) + # define COMPILER_VERSION_PATCH DEC(__ARMCC_VERSION % 10000) +#else + /* __ARMCC_VERSION = VRPPPP */ + # define COMPILER_VERSION_MAJOR DEC(__ARMCC_VERSION/100000) + # define COMPILER_VERSION_MINOR DEC(__ARMCC_VERSION/10000 % 10) + # define COMPILER_VERSION_PATCH DEC(__ARMCC_VERSION % 10000) +#endif + + +#elif defined(__clang__) && defined(__apple_build_version__) +# define COMPILER_ID "AppleClang" +# if defined(_MSC_VER) +# define SIMULATE_ID "MSVC" +# endif +# define COMPILER_VERSION_MAJOR DEC(__clang_major__) +# define COMPILER_VERSION_MINOR DEC(__clang_minor__) +# define COMPILER_VERSION_PATCH DEC(__clang_patchlevel__) +# if defined(_MSC_VER) + /* _MSC_VER = VVRR */ +# define SIMULATE_VERSION_MAJOR DEC(_MSC_VER / 100) +# define SIMULATE_VERSION_MINOR DEC(_MSC_VER % 100) +# endif +# define COMPILER_VERSION_TWEAK DEC(__apple_build_version__) + +#elif defined(__clang__) && defined(__ARMCOMPILER_VERSION) +# define COMPILER_ID "ARMClang" + # define COMPILER_VERSION_MAJOR DEC(__ARMCOMPILER_VERSION/1000000) + # define COMPILER_VERSION_MINOR DEC(__ARMCOMPILER_VERSION/10000 % 100) + # define COMPILER_VERSION_PATCH DEC(__ARMCOMPILER_VERSION % 10000) +# define COMPILER_VERSION_INTERNAL DEC(__ARMCOMPILER_VERSION) + +#elif defined(__clang__) +# define COMPILER_ID "Clang" +# if defined(_MSC_VER) +# define SIMULATE_ID "MSVC" +# endif +# define COMPILER_VERSION_MAJOR DEC(__clang_major__) +# define COMPILER_VERSION_MINOR DEC(__clang_minor__) +# define COMPILER_VERSION_PATCH DEC(__clang_patchlevel__) +# if defined(_MSC_VER) + /* _MSC_VER = VVRR */ +# define SIMULATE_VERSION_MAJOR DEC(_MSC_VER / 100) +# define SIMULATE_VERSION_MINOR DEC(_MSC_VER % 100) +# endif + +#elif defined(__GNUC__) +# define COMPILER_ID "GNU" +# define COMPILER_VERSION_MAJOR DEC(__GNUC__) +# if defined(__GNUC_MINOR__) +# define COMPILER_VERSION_MINOR DEC(__GNUC_MINOR__) +# endif +# if defined(__GNUC_PATCHLEVEL__) +# define COMPILER_VERSION_PATCH DEC(__GNUC_PATCHLEVEL__) +# endif + +#elif defined(_MSC_VER) +# define COMPILER_ID "MSVC" + /* _MSC_VER = VVRR */ +# define COMPILER_VERSION_MAJOR DEC(_MSC_VER / 100) +# define COMPILER_VERSION_MINOR DEC(_MSC_VER % 100) +# if defined(_MSC_FULL_VER) +# if _MSC_VER >= 1400 + /* _MSC_FULL_VER = VVRRPPPPP */ +# define COMPILER_VERSION_PATCH DEC(_MSC_FULL_VER % 100000) +# else + /* _MSC_FULL_VER = VVRRPPPP */ +# define COMPILER_VERSION_PATCH DEC(_MSC_FULL_VER % 10000) +# endif +# endif +# if defined(_MSC_BUILD) +# define COMPILER_VERSION_TWEAK DEC(_MSC_BUILD) +# endif + +#elif defined(__VISUALDSPVERSION__) || defined(__ADSPBLACKFIN__) || defined(__ADSPTS__) || defined(__ADSP21000__) +# define COMPILER_ID "ADSP" +#if defined(__VISUALDSPVERSION__) + /* __VISUALDSPVERSION__ = 0xVVRRPP00 */ +# define COMPILER_VERSION_MAJOR HEX(__VISUALDSPVERSION__>>24) +# define COMPILER_VERSION_MINOR HEX(__VISUALDSPVERSION__>>16 & 0xFF) +# define COMPILER_VERSION_PATCH HEX(__VISUALDSPVERSION__>>8 & 0xFF) +#endif + +#elif defined(__IAR_SYSTEMS_ICC__) || defined(__IAR_SYSTEMS_ICC) +# define COMPILER_ID "IAR" +# if defined(__VER__) && defined(__ICCARM__) +# define COMPILER_VERSION_MAJOR DEC((__VER__) / 1000000) +# define COMPILER_VERSION_MINOR DEC(((__VER__) / 1000) % 1000) +# define COMPILER_VERSION_PATCH DEC((__VER__) % 1000) +# define COMPILER_VERSION_INTERNAL DEC(__IAR_SYSTEMS_ICC__) +# elif defined(__VER__) && (defined(__ICCAVR__) || defined(__ICCRX__) || defined(__ICCRH850__) || defined(__ICCRL78__) || defined(__ICC430__) || defined(__ICCRISCV__) || defined(__ICCV850__) || defined(__ICC8051__) || defined(__ICCSTM8__)) +# define COMPILER_VERSION_MAJOR DEC((__VER__) / 100) +# define COMPILER_VERSION_MINOR DEC((__VER__) - (((__VER__) / 100)*100)) +# define COMPILER_VERSION_PATCH DEC(__SUBVERSION__) +# define COMPILER_VERSION_INTERNAL DEC(__IAR_SYSTEMS_ICC__) +# endif + +#elif defined(__SDCC_VERSION_MAJOR) || defined(SDCC) +# define COMPILER_ID "SDCC" +# if defined(__SDCC_VERSION_MAJOR) +# define COMPILER_VERSION_MAJOR DEC(__SDCC_VERSION_MAJOR) +# define COMPILER_VERSION_MINOR DEC(__SDCC_VERSION_MINOR) +# define COMPILER_VERSION_PATCH DEC(__SDCC_VERSION_PATCH) +# else + /* SDCC = VRP */ +# define COMPILER_VERSION_MAJOR DEC(SDCC/100) +# define COMPILER_VERSION_MINOR DEC(SDCC/10 % 10) +# define COMPILER_VERSION_PATCH DEC(SDCC % 10) +# endif + + +/* These compilers are either not known or too old to define an + identification macro. Try to identify the platform and guess that + it is the native compiler. */ +#elif defined(__hpux) || defined(__hpua) +# define COMPILER_ID "HP" + +#else /* unknown compiler */ +# define COMPILER_ID "" +#endif + +/* Construct the string literal in pieces to prevent the source from + getting matched. Store it in a pointer rather than an array + because some compilers will just produce instructions to fill the + array rather than assigning a pointer to a static array. */ +char const* info_compiler = "INFO" ":" "compiler[" COMPILER_ID "]"; +#ifdef SIMULATE_ID +char const* info_simulate = "INFO" ":" "simulate[" SIMULATE_ID "]"; +#endif + +#ifdef __QNXNTO__ +char const* qnxnto = "INFO" ":" "qnxnto[]"; +#endif + +#if defined(__CRAYXT_COMPUTE_LINUX_TARGET) +char const *info_cray = "INFO" ":" "compiler_wrapper[CrayPrgEnv]"; +#endif + +#define STRINGIFY_HELPER(X) #X +#define STRINGIFY(X) STRINGIFY_HELPER(X) + +/* Identify known platforms by name. */ +#if defined(__linux) || defined(__linux__) || defined(linux) +# define PLATFORM_ID "Linux" + +#elif defined(__MSYS__) +# define PLATFORM_ID "MSYS" + +#elif defined(__CYGWIN__) +# define PLATFORM_ID "Cygwin" + +#elif defined(__MINGW32__) +# define PLATFORM_ID "MinGW" + +#elif defined(__APPLE__) +# define PLATFORM_ID "Darwin" + +#elif defined(_WIN32) || defined(__WIN32__) || defined(WIN32) +# define PLATFORM_ID "Windows" + +#elif defined(__FreeBSD__) || defined(__FreeBSD) +# define PLATFORM_ID "FreeBSD" + +#elif defined(__NetBSD__) || defined(__NetBSD) +# define PLATFORM_ID "NetBSD" + +#elif defined(__OpenBSD__) || defined(__OPENBSD) +# define PLATFORM_ID "OpenBSD" + +#elif defined(__sun) || defined(sun) +# define PLATFORM_ID "SunOS" + +#elif defined(_AIX) || defined(__AIX) || defined(__AIX__) || defined(__aix) || defined(__aix__) +# define PLATFORM_ID "AIX" + +#elif defined(__hpux) || defined(__hpux__) +# define PLATFORM_ID "HP-UX" + +#elif defined(__HAIKU__) +# define PLATFORM_ID "Haiku" + +#elif defined(__BeOS) || defined(__BEOS__) || defined(_BEOS) +# define PLATFORM_ID "BeOS" + +#elif defined(__QNX__) || defined(__QNXNTO__) +# define PLATFORM_ID "QNX" + +#elif defined(__tru64) || defined(_tru64) || defined(__TRU64__) +# define PLATFORM_ID "Tru64" + +#elif defined(__riscos) || defined(__riscos__) +# define PLATFORM_ID "RISCos" + +#elif defined(__sinix) || defined(__sinix__) || defined(__SINIX__) +# define PLATFORM_ID "SINIX" + +#elif defined(__UNIX_SV__) +# define PLATFORM_ID "UNIX_SV" + +#elif defined(__bsdos__) +# define PLATFORM_ID "BSDOS" + +#elif defined(_MPRAS) || defined(MPRAS) +# define PLATFORM_ID "MP-RAS" + +#elif defined(__osf) || defined(__osf__) +# define PLATFORM_ID "OSF1" + +#elif defined(_SCO_SV) || defined(SCO_SV) || defined(sco_sv) +# define PLATFORM_ID "SCO_SV" + +#elif defined(__ultrix) || defined(__ultrix__) || defined(_ULTRIX) +# define PLATFORM_ID "ULTRIX" + +#elif defined(__XENIX__) || defined(_XENIX) || defined(XENIX) +# define PLATFORM_ID "Xenix" + +#elif defined(__WATCOMC__) +# if defined(__LINUX__) +# define PLATFORM_ID "Linux" + +# elif defined(__DOS__) +# define PLATFORM_ID "DOS" + +# elif defined(__OS2__) +# define PLATFORM_ID "OS2" + +# elif defined(__WINDOWS__) +# define PLATFORM_ID "Windows3x" + +# elif defined(__VXWORKS__) +# define PLATFORM_ID "VxWorks" + +# else /* unknown platform */ +# define PLATFORM_ID +# endif + +#elif defined(__INTEGRITY) +# if defined(INT_178B) +# define PLATFORM_ID "Integrity178" + +# else /* regular Integrity */ +# define PLATFORM_ID "Integrity" +# endif + +#else /* unknown platform */ +# define PLATFORM_ID + +#endif + +/* For windows compilers MSVC and Intel we can determine + the architecture of the compiler being used. This is because + the compilers do not have flags that can change the architecture, + but rather depend on which compiler is being used +*/ +#if defined(_WIN32) && defined(_MSC_VER) +# if defined(_M_IA64) +# define ARCHITECTURE_ID "IA64" + +# elif defined(_M_ARM64EC) +# define ARCHITECTURE_ID "ARM64EC" + +# elif defined(_M_X64) || defined(_M_AMD64) +# define ARCHITECTURE_ID "x64" + +# elif defined(_M_IX86) +# define ARCHITECTURE_ID "X86" + +# elif defined(_M_ARM64) +# define ARCHITECTURE_ID "ARM64" + +# elif defined(_M_ARM) +# if _M_ARM == 4 +# define ARCHITECTURE_ID "ARMV4I" +# elif _M_ARM == 5 +# define ARCHITECTURE_ID "ARMV5I" +# else +# define ARCHITECTURE_ID "ARMV" STRINGIFY(_M_ARM) +# endif + +# elif defined(_M_MIPS) +# define ARCHITECTURE_ID "MIPS" + +# elif defined(_M_SH) +# define ARCHITECTURE_ID "SHx" + +# else /* unknown architecture */ +# define ARCHITECTURE_ID "" +# endif + +#elif defined(__WATCOMC__) +# if defined(_M_I86) +# define ARCHITECTURE_ID "I86" + +# elif defined(_M_IX86) +# define ARCHITECTURE_ID "X86" + +# else /* unknown architecture */ +# define ARCHITECTURE_ID "" +# endif + +#elif defined(__IAR_SYSTEMS_ICC__) || defined(__IAR_SYSTEMS_ICC) +# if defined(__ICCARM__) +# define ARCHITECTURE_ID "ARM" + +# elif defined(__ICCRX__) +# define ARCHITECTURE_ID "RX" + +# elif defined(__ICCRH850__) +# define ARCHITECTURE_ID "RH850" + +# elif defined(__ICCRL78__) +# define ARCHITECTURE_ID "RL78" + +# elif defined(__ICCRISCV__) +# define ARCHITECTURE_ID "RISCV" + +# elif defined(__ICCAVR__) +# define ARCHITECTURE_ID "AVR" + +# elif defined(__ICC430__) +# define ARCHITECTURE_ID "MSP430" + +# elif defined(__ICCV850__) +# define ARCHITECTURE_ID "V850" + +# elif defined(__ICC8051__) +# define ARCHITECTURE_ID "8051" + +# elif defined(__ICCSTM8__) +# define ARCHITECTURE_ID "STM8" + +# else /* unknown architecture */ +# define ARCHITECTURE_ID "" +# endif + +#elif defined(__ghs__) +# if defined(__PPC64__) +# define ARCHITECTURE_ID "PPC64" + +# elif defined(__ppc__) +# define ARCHITECTURE_ID "PPC" + +# elif defined(__ARM__) +# define ARCHITECTURE_ID "ARM" + +# elif defined(__x86_64__) +# define ARCHITECTURE_ID "x64" + +# elif defined(__i386__) +# define ARCHITECTURE_ID "X86" + +# else /* unknown architecture */ +# define ARCHITECTURE_ID "" +# endif + +#elif defined(__TI_COMPILER_VERSION__) +# if defined(__TI_ARM__) +# define ARCHITECTURE_ID "ARM" + +# elif defined(__MSP430__) +# define ARCHITECTURE_ID "MSP430" + +# elif defined(__TMS320C28XX__) +# define ARCHITECTURE_ID "TMS320C28x" + +# elif defined(__TMS320C6X__) || defined(_TMS320C6X) +# define ARCHITECTURE_ID "TMS320C6x" + +# else /* unknown architecture */ +# define ARCHITECTURE_ID "" +# endif + +#else +# define ARCHITECTURE_ID +#endif + +/* Convert integer to decimal digit literals. */ +#define DEC(n) \ + ('0' + (((n) / 10000000)%10)), \ + ('0' + (((n) / 1000000)%10)), \ + ('0' + (((n) / 100000)%10)), \ + ('0' + (((n) / 10000)%10)), \ + ('0' + (((n) / 1000)%10)), \ + ('0' + (((n) / 100)%10)), \ + ('0' + (((n) / 10)%10)), \ + ('0' + ((n) % 10)) + +/* Convert integer to hex digit literals. */ +#define HEX(n) \ + ('0' + ((n)>>28 & 0xF)), \ + ('0' + ((n)>>24 & 0xF)), \ + ('0' + ((n)>>20 & 0xF)), \ + ('0' + ((n)>>16 & 0xF)), \ + ('0' + ((n)>>12 & 0xF)), \ + ('0' + ((n)>>8 & 0xF)), \ + ('0' + ((n)>>4 & 0xF)), \ + ('0' + ((n) & 0xF)) + +/* Construct a string literal encoding the version number. */ +#ifdef COMPILER_VERSION +char const* info_version = "INFO" ":" "compiler_version[" COMPILER_VERSION "]"; + +/* Construct a string literal encoding the version number components. */ +#elif defined(COMPILER_VERSION_MAJOR) +char const info_version[] = { + 'I', 'N', 'F', 'O', ':', + 'c','o','m','p','i','l','e','r','_','v','e','r','s','i','o','n','[', + COMPILER_VERSION_MAJOR, +# ifdef COMPILER_VERSION_MINOR + '.', COMPILER_VERSION_MINOR, +# ifdef COMPILER_VERSION_PATCH + '.', COMPILER_VERSION_PATCH, +# ifdef COMPILER_VERSION_TWEAK + '.', COMPILER_VERSION_TWEAK, +# endif +# endif +# endif + ']','\0'}; +#endif + +/* Construct a string literal encoding the internal version number. */ +#ifdef COMPILER_VERSION_INTERNAL +char const info_version_internal[] = { + 'I', 'N', 'F', 'O', ':', + 'c','o','m','p','i','l','e','r','_','v','e','r','s','i','o','n','_', + 'i','n','t','e','r','n','a','l','[', + COMPILER_VERSION_INTERNAL,']','\0'}; +#elif defined(COMPILER_VERSION_INTERNAL_STR) +char const* info_version_internal = "INFO" ":" "compiler_version_internal[" COMPILER_VERSION_INTERNAL_STR "]"; +#endif + +/* Construct a string literal encoding the version number components. */ +#ifdef SIMULATE_VERSION_MAJOR +char const info_simulate_version[] = { + 'I', 'N', 'F', 'O', ':', + 's','i','m','u','l','a','t','e','_','v','e','r','s','i','o','n','[', + SIMULATE_VERSION_MAJOR, +# ifdef SIMULATE_VERSION_MINOR + '.', SIMULATE_VERSION_MINOR, +# ifdef SIMULATE_VERSION_PATCH + '.', SIMULATE_VERSION_PATCH, +# ifdef SIMULATE_VERSION_TWEAK + '.', SIMULATE_VERSION_TWEAK, +# endif +# endif +# endif + ']','\0'}; +#endif + +/* Construct the string literal in pieces to prevent the source from + getting matched. Store it in a pointer rather than an array + because some compilers will just produce instructions to fill the + array rather than assigning a pointer to a static array. */ +char const* info_platform = "INFO" ":" "platform[" PLATFORM_ID "]"; +char const* info_arch = "INFO" ":" "arch[" ARCHITECTURE_ID "]"; + + + +#if !defined(__STDC__) && !defined(__clang__) +# if defined(_MSC_VER) || defined(__ibmxl__) || defined(__IBMC__) +# define C_VERSION "90" +# else +# define C_VERSION +# endif +#elif __STDC_VERSION__ > 201710L +# define C_VERSION "23" +#elif __STDC_VERSION__ >= 201710L +# define C_VERSION "17" +#elif __STDC_VERSION__ >= 201000L +# define C_VERSION "11" +#elif __STDC_VERSION__ >= 199901L +# define C_VERSION "99" +#else +# define C_VERSION "90" +#endif +const char* info_language_standard_default = + "INFO" ":" "standard_default[" C_VERSION "]"; + +const char* info_language_extensions_default = "INFO" ":" "extensions_default[" +/* !defined(_MSC_VER) to exclude Clang's MSVC compatibility mode. */ +#if (defined(__clang__) || defined(__GNUC__) || \ + defined(__TI_COMPILER_VERSION__)) && \ + !defined(__STRICT_ANSI__) && !defined(_MSC_VER) + "ON" +#else + "OFF" +#endif +"]"; + +/*--------------------------------------------------------------------------*/ + +#ifdef ID_VOID_MAIN +void main() {} +#else +# if defined(__CLASSIC_C__) +int main(argc, argv) int argc; char *argv[]; +# else +int main(int argc, char* argv[]) +# endif +{ + int require = 0; + require += info_compiler[argc]; + require += info_platform[argc]; + require += info_arch[argc]; +#ifdef COMPILER_VERSION_MAJOR + require += info_version[argc]; +#endif +#ifdef COMPILER_VERSION_INTERNAL + require += info_version_internal[argc]; +#endif +#ifdef SIMULATE_ID + require += info_simulate[argc]; +#endif +#ifdef SIMULATE_VERSION_MAJOR + require += info_simulate_version[argc]; +#endif +#if defined(__CRAYXT_COMPUTE_LINUX_TARGET) + require += info_cray[argc]; +#endif + require += info_language_standard_default[argc]; + require += info_language_extensions_default[argc]; + (void)argv; + return require; +} +#endif diff --git a/examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/CMakeFiles/3.22.1-g37088a8/CompilerIdC/CMakeCCompilerId.o b/examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/CMakeFiles/3.22.1-g37088a8/CompilerIdC/CMakeCCompilerId.o new file mode 100644 index 0000000000000000000000000000000000000000..6c76f3cf766f9d551df1d82512c678a0b07855fe GIT binary patch literal 5424 zcmdT|TZ|KB6#i$r?UwCQSXPvqrrrVBnYP=#fChK7EbJNtf}r9C>ufvQt+bsookBG+ zK6qWxMB{@bYC?=A8Xmkn7<}P{2cjm1XiOwQeGsFG``{%;j6^->pL5#T>0p#M4|dKu z-+ylZWzIkAqvPARs){0*6tP*fG)9F;##{1;6i37wu-rQ__wDKNi-^oUa$#bw{^RNK zdDJG3geA+}d11-Pj%a9ner|mJKY-vj@th2CIqpZs=l7qQ+_81%rkqu-7E7i*HC8ez zh5bT;)so@NSax}8d$Ce)5NO!B*(s81j#0@Qc0QXoXN-EunMw`Oh}m$=O08&BYR$2o zJNDyLB`wIAJ!INwI+c`^8q!=LnPef|^5h7?TdY!aFP zsbeQRmJ7Mr5d~%@M6|OLSqXj%?4t((S%gk<{8=17u6p9(xQv&kZ09_qt(aa^{S>2o z>9K27)$Qhyb$Bh*2uxM^CD3P;$LD8{g}MoSe)S-&JZn5YUW+a$O%g7jayN=fH1s+I zim5L&7OGWJkHcuU`U2K0l$bPj*EZY{eFw&(xO`%?lDDm5KCw2H92iI^)59AQw^R!j zJg2Ra1FYnL(pN4aHf_|*e8Q?E?9A}cP-ZB2+f4PUC<%Hb!stWM0b&eby?QdVRWHaq7BlR;`*| zsuy*$VU(+oT*nq*Rm_T0y3r_8k87#_?HW^|ckE3}Q8 z-mj(8TB@(mKa?CEHiq?9<}m>iu|+mJx_kHNzU*C-_l;-w?AtY-&5B}W#>!$^>^!bU zE9ENOe_Rb4k1OGt){TOh_4gm&aC;LHWmr?Na)eB2zX)IM%cXvjgheNyuF#I9myCFv7OP^H77Dv>Vz(R<}0O}Cy;i)LfAMwS=zzy#< zCFl&n5HuE5GI)gMB1*WkD|&4>UJu72=fm+x!N+Gw55;E|jC4h2!=Xs*HdTe39IuUU zfT0fY(_d`^m>m8EQOJFb+lE4-rwv0O)LgxV!yy4p-A5mSzRZ(Xt|?&8z^_Nho-93J zX(gS7XOCD!8Z;G>RG3$ffUAax@pnP*X<7-9rt!TJUrbsn&N_)NX~TKXai=i$#lKui zktO0-72{hyI5tzpvmP9)Wn4B<&@oQ82Y&)&B*d{=wnq#JcRFa-7+)@}S`WtYgnEhb zK`#h0XaL0XV0@DBd)(n>{rd^e6OJmMDTEFa&T$Tq{o{nA+B%Iwyyn4izj$Ac z6V7oO6o=Od)z)bj;v-+2^`w98i}NJKnfKUZ4yHdzKr^g|_q~_=xE}0zND1P|c&`yp zdvJ`y>${C`j`KF@dp!27w@(OOZyfgSFRv@dIYa&r6KDJ^>2p5(E7Fhp@NYDyPGLx&9U1OG`|5Tnyd{|gQeA~gFg;jE38?Q_qo4@=68cuu^dy|wqtJ}RFgZ< zmREjKcypGD6_c#Y+3bvMl+B=PP=iT0#2o~o^C^0}BZIPH`Uqjiyh7)AK`OpW+uRjV{Q2is++_+AxAJ>@u z8Gj2fOts*h1AQ0e_s`3}exH%Qzy7EHW&NLke^CAR2Goy`zy6Dt$j|+i?ZHfvU;hoz zp#0T<{CMxMKd&GDsf$zf&!77u+k=TDzkUaxLHVB!$d7%U{W<^jfCc40_Al2T@Bg6u z#{%->yXw!6X($y)Eqar{ms^mO{I;(G>c`M`+w(=5odbU-w z|NT8i{=;lYoafK*)!-boe#fYP^Et!yy9$2n&v=iUpTjlz7nJ|0CgKWUzJR(fk^e+M ce*7!PpC7-6{By?y56J&F)aqlH7a_m@-y3j+R{#J2 literal 0 HcmV?d00001 diff --git a/examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/CMakeFiles/CMakeOutput.log b/examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/CMakeFiles/CMakeOutput.log new file mode 100644 index 00000000..bc395ad2 --- /dev/null +++ b/examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/CMakeFiles/CMakeOutput.log @@ -0,0 +1,126 @@ +The target system is: Android - 1 - x86_64 +The host system is: Linux - 6.8.0-1061-aws - x86_64 +Compiling the C compiler identification source file "CMakeCCompilerId.c" succeeded. +Compiler: /home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/bin/clang +Build flags: -g;-DANDROID;-fdata-sections;-ffunction-sections;-funwind-tables;-fstack-protector-strong;-no-canonical-prefixes;-D_FORTIFY_SOURCE=2;-Wformat;-Werror=format-security; +Id flags: -c;--target=x86_64-none-linux-android24 + +The output was: +0 + + +Compilation of the C compiler identification source "CMakeCCompilerId.c" produced "CMakeCCompilerId.o" + +The C compiler identification is Clang, found in "/home/ubuntu/repos/lui/examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/CMakeFiles/3.22.1-g37088a8/CompilerIdC/CMakeCCompilerId.o" + +Detecting C compiler ABI info compiled with the following output: +Change Dir: /home/ubuntu/repos/lui/examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/CMakeFiles/CMakeTmp + +Run Build Command(s):/home/ubuntu/android-sdk/cmake/3.22.1/bin/ninja cmTC_3cf1c && [1/2] Building C object CMakeFiles/cmTC_3cf1c.dir/CMakeCCompilerABI.c.o +Android (10552028, +pgo, +bolt, +lto, -mlgo, based on r487747d) clang version 17.0.2 (https://android.googlesource.com/toolchain/llvm-project d9f89f4d16663d5012e5c09495f3b30ece3d2362) +Target: x86_64-none-linux-android24 +Thread model: posix +InstalledDir: /home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/bin + (in-process) + "/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/bin/clang" -cc1 -triple x86_64-none-linux-android24 -emit-obj -mrelax-all -disable-free -clear-ast-before-backend -disable-llvm-verifier -discard-value-names -main-file-name CMakeCCompilerABI.c -mrelocation-model pic -pic-level 2 -pic-is-pie -mframe-pointer=all -ffp-contract=on -fno-rounding-math -mconstructor-aliases -funwind-tables=2 -target-cpu x86-64 -target-feature +sse4.2 -target-feature +popcnt -target-feature +cx16 -tune-cpu generic -mllvm -treat-scalable-fixed-error-as-warning -debug-info-kind=constructor -dwarf-version=4 -debugger-tuning=gdb -v -ffunction-sections -fdata-sections -fcoverage-compilation-dir=/home/ubuntu/repos/lui/examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/CMakeFiles/CMakeTmp -resource-dir /home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/lib/clang/17 -dependency-file CMakeFiles/cmTC_3cf1c.dir/CMakeCCompilerABI.c.o.d -MT CMakeFiles/cmTC_3cf1c.dir/CMakeCCompilerABI.c.o -sys-header-deps -D ANDROID -D _FORTIFY_SOURCE=2 -isysroot /home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot -internal-isystem /home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/lib/clang/17/include -internal-isystem /home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/local/include -internal-externc-isystem /home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/include/x86_64-linux-android -internal-externc-isystem /home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/include -internal-externc-isystem /home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/include -Wformat -fdebug-compilation-dir=/home/ubuntu/repos/lui/examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/CMakeFiles/CMakeTmp -ferror-limit 19 -stack-protector 2 -fgnuc-version=4.2.1 -D__GCC_HAVE_DWARF2_CFI_ASM=1 -o CMakeFiles/cmTC_3cf1c.dir/CMakeCCompilerABI.c.o -x c /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeCCompilerABI.c +clang -cc1 version 17.0.2 based upon LLVM 17.0.2 default target x86_64-unknown-linux-gnu +ignoring nonexistent directory "/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/local/include" +ignoring nonexistent directory "/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/include" +#include "..." search starts here: +#include <...> search starts here: + /home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/lib/clang/17/include + /home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/include/x86_64-linux-android + /home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/include +End of search list. +[2/2] Linking C executable cmTC_3cf1c +Android (10552028, +pgo, +bolt, +lto, -mlgo, based on r487747d) clang version 17.0.2 (https://android.googlesource.com/toolchain/llvm-project d9f89f4d16663d5012e5c09495f3b30ece3d2362) +Target: x86_64-none-linux-android24 +Thread model: posix +InstalledDir: /home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/bin + "/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/bin/ld.lld" --sysroot=/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot -pie -z now -z relro --hash-style=gnu --eh-frame-hdr -m elf_x86_64 -dynamic-linker /system/bin/linker64 -o cmTC_3cf1c /home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/x86_64-linux-android/24/crtbegin_dynamic.o -L/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/lib/clang/17/lib/linux/x86_64 -L/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/x86_64-linux-android/24 -L/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/x86_64-linux-android -L/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib --build-id=sha1 --no-rosegment --no-undefined-version --fatal-warnings --no-undefined CMakeFiles/cmTC_3cf1c.dir/CMakeCCompilerABI.c.o /home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/lib/clang/17/lib/linux/libclang_rt.builtins-x86_64-android.a -l:libunwind.a -ldl -lc /home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/lib/clang/17/lib/linux/libclang_rt.builtins-x86_64-android.a -l:libunwind.a -ldl /home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/x86_64-linux-android/24/crtend_android.o + + + +Parsed C implicit include dir info from above output: rv=done + found start of include info + found start of implicit include info + add: [/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/lib/clang/17/include] + add: [/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/include/x86_64-linux-android] + add: [/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/include] + end of search list found + collapse include dir [/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/lib/clang/17/include] ==> [/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/lib/clang/17/include] + collapse include dir [/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/include/x86_64-linux-android] ==> [/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/include/x86_64-linux-android] + collapse include dir [/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/include] ==> [/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/include] + implicit include dirs: [/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/lib/clang/17/include;/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/include/x86_64-linux-android;/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/include] + + +Parsed C implicit link information from above output: + link line regex: [^( *|.*[/\])(ld\.lld|CMAKE_LINK_STARTFILE-NOTFOUND|([^/\]+-)?ld|collect2)[^/\]*( |$)] + ignore line: [Change Dir: /home/ubuntu/repos/lui/examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/CMakeFiles/CMakeTmp] + ignore line: [] + ignore line: [Run Build Command(s):/home/ubuntu/android-sdk/cmake/3.22.1/bin/ninja cmTC_3cf1c && [1/2] Building C object CMakeFiles/cmTC_3cf1c.dir/CMakeCCompilerABI.c.o] + ignore line: [Android (10552028 +pgo +bolt +lto -mlgo based on r487747d) clang version 17.0.2 (https://android.googlesource.com/toolchain/llvm-project d9f89f4d16663d5012e5c09495f3b30ece3d2362)] + ignore line: [Target: x86_64-none-linux-android24] + ignore line: [Thread model: posix] + ignore line: [InstalledDir: /home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/bin] + ignore line: [ (in-process)] + ignore line: [ "/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/bin/clang" -cc1 -triple x86_64-none-linux-android24 -emit-obj -mrelax-all -disable-free -clear-ast-before-backend -disable-llvm-verifier -discard-value-names -main-file-name CMakeCCompilerABI.c -mrelocation-model pic -pic-level 2 -pic-is-pie -mframe-pointer=all -ffp-contract=on -fno-rounding-math -mconstructor-aliases -funwind-tables=2 -target-cpu x86-64 -target-feature +sse4.2 -target-feature +popcnt -target-feature +cx16 -tune-cpu generic -mllvm -treat-scalable-fixed-error-as-warning -debug-info-kind=constructor -dwarf-version=4 -debugger-tuning=gdb -v -ffunction-sections -fdata-sections -fcoverage-compilation-dir=/home/ubuntu/repos/lui/examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/CMakeFiles/CMakeTmp -resource-dir /home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/lib/clang/17 -dependency-file CMakeFiles/cmTC_3cf1c.dir/CMakeCCompilerABI.c.o.d -MT CMakeFiles/cmTC_3cf1c.dir/CMakeCCompilerABI.c.o -sys-header-deps -D ANDROID -D _FORTIFY_SOURCE=2 -isysroot /home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot -internal-isystem /home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/lib/clang/17/include -internal-isystem /home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/local/include -internal-externc-isystem /home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/include/x86_64-linux-android -internal-externc-isystem /home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/include -internal-externc-isystem /home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/include -Wformat -fdebug-compilation-dir=/home/ubuntu/repos/lui/examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/CMakeFiles/CMakeTmp -ferror-limit 19 -stack-protector 2 -fgnuc-version=4.2.1 -D__GCC_HAVE_DWARF2_CFI_ASM=1 -o CMakeFiles/cmTC_3cf1c.dir/CMakeCCompilerABI.c.o -x c /home/ubuntu/android-sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeCCompilerABI.c] + ignore line: [clang -cc1 version 17.0.2 based upon LLVM 17.0.2 default target x86_64-unknown-linux-gnu] + ignore line: [ignoring nonexistent directory "/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/local/include"] + ignore line: [ignoring nonexistent directory "/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/include"] + ignore line: [#include "..." search starts here:] + ignore line: [#include <...> search starts here:] + ignore line: [ /home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/lib/clang/17/include] + ignore line: [ /home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/include/x86_64-linux-android] + ignore line: [ /home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/include] + ignore line: [End of search list.] + ignore line: [[2/2] Linking C executable cmTC_3cf1c] + ignore line: [Android (10552028 +pgo +bolt +lto -mlgo based on r487747d) clang version 17.0.2 (https://android.googlesource.com/toolchain/llvm-project d9f89f4d16663d5012e5c09495f3b30ece3d2362)] + ignore line: [Target: x86_64-none-linux-android24] + ignore line: [Thread model: posix] + ignore line: [InstalledDir: /home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/bin] + link line: [ "/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/bin/ld.lld" --sysroot=/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot -pie -z now -z relro --hash-style=gnu --eh-frame-hdr -m elf_x86_64 -dynamic-linker /system/bin/linker64 -o cmTC_3cf1c /home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/x86_64-linux-android/24/crtbegin_dynamic.o -L/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/lib/clang/17/lib/linux/x86_64 -L/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/x86_64-linux-android/24 -L/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/x86_64-linux-android -L/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib --build-id=sha1 --no-rosegment --no-undefined-version --fatal-warnings --no-undefined CMakeFiles/cmTC_3cf1c.dir/CMakeCCompilerABI.c.o /home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/lib/clang/17/lib/linux/libclang_rt.builtins-x86_64-android.a -l:libunwind.a -ldl -lc /home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/lib/clang/17/lib/linux/libclang_rt.builtins-x86_64-android.a -l:libunwind.a -ldl /home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/x86_64-linux-android/24/crtend_android.o] + arg [/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/bin/ld.lld] ==> ignore + arg [--sysroot=/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot] ==> ignore + arg [-pie] ==> ignore + arg [-znow] ==> ignore + arg [-zrelro] ==> ignore + arg [--hash-style=gnu] ==> ignore + arg [--eh-frame-hdr] ==> ignore + arg [-m] ==> ignore + arg [elf_x86_64] ==> ignore + arg [-dynamic-linker] ==> ignore + arg [/system/bin/linker64] ==> ignore + arg [-o] ==> ignore + arg [cmTC_3cf1c] ==> ignore + arg [/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/x86_64-linux-android/24/crtbegin_dynamic.o] ==> obj [/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/x86_64-linux-android/24/crtbegin_dynamic.o] + arg [-L/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/lib/clang/17/lib/linux/x86_64] ==> dir [/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/lib/clang/17/lib/linux/x86_64] + arg [-L/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/x86_64-linux-android/24] ==> dir [/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/x86_64-linux-android/24] + arg [-L/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/x86_64-linux-android] ==> dir [/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/x86_64-linux-android] + arg [-L/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib] ==> dir [/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib] + arg [--build-id=sha1] ==> ignore + arg [--no-rosegment] ==> ignore + arg [--no-undefined-version] ==> ignore + arg [--fatal-warnings] ==> ignore + arg [--no-undefined] ==> ignore + arg [CMakeFiles/cmTC_3cf1c.dir/CMakeCCompilerABI.c.o] ==> ignore + arg [/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/lib/clang/17/lib/linux/libclang_rt.builtins-x86_64-android.a] ==> lib [/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/lib/clang/17/lib/linux/libclang_rt.builtins-x86_64-android.a] + arg [-l:libunwind.a] ==> lib [-l:libunwind.a] + arg [-ldl] ==> lib [dl] + arg [-lc] ==> lib [c] + arg [/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/lib/clang/17/lib/linux/libclang_rt.builtins-x86_64-android.a] ==> lib [/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/lib/clang/17/lib/linux/libclang_rt.builtins-x86_64-android.a] + arg [-l:libunwind.a] ==> lib [-l:libunwind.a] + arg [-ldl] ==> lib [dl] + arg [/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/x86_64-linux-android/24/crtend_android.o] ==> obj [/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/x86_64-linux-android/24/crtend_android.o] + remove lib [/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/lib/clang/17/lib/linux/libclang_rt.builtins-x86_64-android.a] + remove lib [/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/lib/clang/17/lib/linux/libclang_rt.builtins-x86_64-android.a] + collapse library dir [/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/lib/clang/17/lib/linux/x86_64] ==> [/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/lib/clang/17/lib/linux/x86_64] + collapse library dir [/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/x86_64-linux-android/24] ==> [/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/x86_64-linux-android/24] + collapse library dir [/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/x86_64-linux-android] ==> [/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/x86_64-linux-android] + collapse library dir [/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib] ==> [/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib] + implicit libs: [-l:libunwind.a;dl;c;-l:libunwind.a;dl] + implicit objs: [/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/x86_64-linux-android/24/crtbegin_dynamic.o;/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/x86_64-linux-android/24/crtend_android.o] + implicit dirs: [/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/lib/clang/17/lib/linux/x86_64;/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/x86_64-linux-android/24;/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/x86_64-linux-android;/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib] + implicit fwks: [] + + diff --git a/examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/CMakeFiles/TargetDirectories.txt b/examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/CMakeFiles/TargetDirectories.txt new file mode 100644 index 00000000..8976b414 --- /dev/null +++ b/examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/CMakeFiles/TargetDirectories.txt @@ -0,0 +1,3 @@ +/home/ubuntu/repos/lui/examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/CMakeFiles/lui_jni_bridge.dir +/home/ubuntu/repos/lui/examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/CMakeFiles/edit_cache.dir +/home/ubuntu/repos/lui/examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/CMakeFiles/rebuild_cache.dir diff --git a/examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/CMakeFiles/cmake.check_cache b/examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/CMakeFiles/cmake.check_cache new file mode 100644 index 00000000..3dccd731 --- /dev/null +++ b/examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/CMakeFiles/cmake.check_cache @@ -0,0 +1 @@ +# This file is generated by cmake for dependency checking of the CMakeCache.txt file diff --git a/examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/CMakeFiles/lui_jni_bridge.dir/lui_jni_bridge_stub.c.o b/examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/CMakeFiles/lui_jni_bridge.dir/lui_jni_bridge_stub.c.o new file mode 100644 index 0000000000000000000000000000000000000000..5572dbb31174bfecc9343a97833b45e37d9716ca GIT binary patch literal 40168 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 literal 0 HcmV?d00001 diff --git a/examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/configure_fingerprint.bin b/examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/configure_fingerprint.bin new file mode 100644 index 00000000..abf052bd --- /dev/null +++ b/examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/configure_fingerprint.bin @@ -0,0 +1,28 @@ +C/C++ Structured Logr +p +n/home/ubuntu/repos/lui/examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/additional_project_files.txtC +A +?com.android.build.gradle.internal.cxx.io.EncodedFileFingerPrint  ‡åÂ…‘4  þäÂ…‘4o +m +k/home/ubuntu/repos/lui/examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/android_gradle_build.json  ‡åÂ…‘4ù ÿäÂ…‘4t +r +p/home/ubuntu/repos/lui/examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/android_gradle_build_mini.json  ‡åÂ…‘4é …åÂ…‘4a +_ +]/home/ubuntu/repos/lui/examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/build.ninja  ‡åÂ…‘4³µ ôäÂ…‘4e +c +a/home/ubuntu/repos/lui/examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/build.ninja.txt  ‡åÂ…‘4j +h +f/home/ubuntu/repos/lui/examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/build_file_index.txt  ‡åÂ…‘4 R †åÂ…‘4k +i +g/home/ubuntu/repos/lui/examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/compile_commands.json  ‡åÂ…‘4· ôäÂ…‘4o +m +k/home/ubuntu/repos/lui/examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/compile_commands.json.bin  ‡åÂ…‘4 – ôäÂ…‘4u +s +q/home/ubuntu/repos/lui/examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/metadata_generation_command.txt  ˆåÂ…‘4 +‡ †åÂ…‘4h +f +d/home/ubuntu/repos/lui/examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/prefab_config.json  ˆåÂ…‘4  ( †åÂ…‘4m +k +i/home/ubuntu/repos/lui/examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/symbol_folder_index.txt  ˆåÂ…‘4  h †åÂ…‘4V +T +R/home/ubuntu/repos/lui/examples/components/android/app/src/main/cpp/CMakeLists.txt  ˆåÂ…‘4 ¨ Ð芅‘4 \ No newline at end of file diff --git a/examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/metadata_generation_command.txt b/examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/metadata_generation_command.txt new file mode 100644 index 00000000..a6da34c3 --- /dev/null +++ b/examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/metadata_generation_command.txt @@ -0,0 +1,18 @@ + -H/home/ubuntu/repos/lui/examples/components/android/app/src/main/cpp +-DCMAKE_SYSTEM_NAME=Android +-DCMAKE_EXPORT_COMPILE_COMMANDS=ON +-DCMAKE_SYSTEM_VERSION=24 +-DANDROID_PLATFORM=android-24 +-DANDROID_ABI=x86_64 +-DCMAKE_ANDROID_ARCH_ABI=x86_64 +-DANDROID_NDK=/home/ubuntu/android-sdk/ndk/26.1.10909125 +-DCMAKE_ANDROID_NDK=/home/ubuntu/android-sdk/ndk/26.1.10909125 +-DCMAKE_TOOLCHAIN_FILE=/home/ubuntu/android-sdk/ndk/26.1.10909125/build/cmake/android.toolchain.cmake +-DCMAKE_MAKE_PROGRAM=/home/ubuntu/android-sdk/cmake/3.22.1/bin/ninja +-DCMAKE_LIBRARY_OUTPUT_DIRECTORY=/home/ubuntu/repos/lui/examples/components/android/app/build/intermediates/cxx/Debug/406wr1e2/obj/x86_64 +-DCMAKE_RUNTIME_OUTPUT_DIRECTORY=/home/ubuntu/repos/lui/examples/components/android/app/build/intermediates/cxx/Debug/406wr1e2/obj/x86_64 +-DCMAKE_BUILD_TYPE=Debug +-B/home/ubuntu/repos/lui/examples/components/android/app/.cxx/Debug/406wr1e2/x86_64 +-GNinja + Build command args: [] + Version: 2 \ No newline at end of file diff --git a/examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/prefab_config.json b/examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/prefab_config.json new file mode 100644 index 00000000..e799de86 --- /dev/null +++ b/examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/prefab_config.json @@ -0,0 +1,4 @@ +{ + "enabled": false, + "packages": [] +} \ No newline at end of file diff --git a/examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/symbol_folder_index.txt b/examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/symbol_folder_index.txt new file mode 100644 index 00000000..46468415 --- /dev/null +++ b/examples/components/android/app/.cxx/Debug/406wr1e2/x86_64/symbol_folder_index.txt @@ -0,0 +1 @@ +/home/ubuntu/repos/lui/examples/components/android/app/build/intermediates/cxx/Debug/406wr1e2/obj/x86_64 \ No newline at end of file diff --git a/examples/components/android/app/.cxx/tools/debug/arm64-v8a/compile_commands.json b/examples/components/android/app/.cxx/tools/debug/arm64-v8a/compile_commands.json new file mode 100644 index 00000000..4fc5cabf --- /dev/null +++ b/examples/components/android/app/.cxx/tools/debug/arm64-v8a/compile_commands.json @@ -0,0 +1,7 @@ +[ +{ + "directory": "/home/ubuntu/repos/lui/examples/components/android/app/.cxx/Debug/406wr1e2/arm64-v8a", + "command": "/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/bin/clang --target=aarch64-none-linux-android24 --sysroot=/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot -Dlui_jni_bridge_EXPORTS -g -DANDROID -fdata-sections -ffunction-sections -funwind-tables -fstack-protector-strong -no-canonical-prefixes -D_FORTIFY_SOURCE=2 -Wformat -Werror=format-security -fno-limit-debug-info -fPIC -o CMakeFiles/lui_jni_bridge.dir/lui_jni_bridge_stub.c.o -c /home/ubuntu/repos/lui/examples/components/android/app/src/main/cpp/lui_jni_bridge_stub.c", + "file": "/home/ubuntu/repos/lui/examples/components/android/app/src/main/cpp/lui_jni_bridge_stub.c" +} +] \ No newline at end of file diff --git a/examples/components/android/app/.cxx/tools/debug/x86_64/compile_commands.json b/examples/components/android/app/.cxx/tools/debug/x86_64/compile_commands.json new file mode 100644 index 00000000..0cbcb2ea --- /dev/null +++ b/examples/components/android/app/.cxx/tools/debug/x86_64/compile_commands.json @@ -0,0 +1,7 @@ +[ +{ + "directory": "/home/ubuntu/repos/lui/examples/components/android/app/.cxx/Debug/406wr1e2/x86_64", + "command": "/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/bin/clang --target=x86_64-none-linux-android24 --sysroot=/home/ubuntu/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/sysroot -Dlui_jni_bridge_EXPORTS -g -DANDROID -fdata-sections -ffunction-sections -funwind-tables -fstack-protector-strong -no-canonical-prefixes -D_FORTIFY_SOURCE=2 -Wformat -Werror=format-security -fno-limit-debug-info -fPIC -o CMakeFiles/lui_jni_bridge.dir/lui_jni_bridge_stub.c.o -c /home/ubuntu/repos/lui/examples/components/android/app/src/main/cpp/lui_jni_bridge_stub.c", + "file": "/home/ubuntu/repos/lui/examples/components/android/app/src/main/cpp/lui_jni_bridge_stub.c" +} +] \ No newline at end of file diff --git a/examples/components/android/app/build.gradle.kts b/examples/components/android/app/build.gradle.kts index 75ae84ec..dcbc4d8d 100644 --- a/examples/components/android/app/build.gradle.kts +++ b/examples/components/android/app/build.gradle.kts @@ -18,12 +18,12 @@ val ocamlInclude = providers.gradleProperty("lui.ocaml.include") android { namespace = "dev.lui.components" - compileSdk = 36 + compileSdk = 35 defaultConfig { applicationId = "dev.lui.components" minSdk = 24 - targetSdk = 36 + targetSdk = 35 versionCode = 1 versionName = "1.0" diff --git a/examples/components/android/app/src/main/AndroidManifest.xml b/examples/components/android/app/src/main/AndroidManifest.xml index e7f2c200..2572485c 100644 --- a/examples/components/android/app/src/main/AndroidManifest.xml +++ b/examples/components/android/app/src/main/AndroidManifest.xml @@ -4,7 +4,7 @@ + + + diff --git a/examples/components/android/settings.gradle.kts b/examples/components/android/settings.gradle.kts index 16549590..0a85fc89 100644 --- a/examples/components/android/settings.gradle.kts +++ b/examples/components/android/settings.gradle.kts @@ -17,4 +17,4 @@ rootProject.name = "lui-components-android" include(":app") include(":lui") -project(":lui").projectDir = file("../../platform/android/lui") +project(":lui").projectDir = file("../../../platform/android/lui") diff --git a/platform/android/lui/build.gradle.kts b/platform/android/lui/build.gradle.kts index 6d6932f8..e4b3eaa9 100644 --- a/platform/android/lui/build.gradle.kts +++ b/platform/android/lui/build.gradle.kts @@ -8,7 +8,7 @@ plugins { android { namespace = "dev.lui" - compileSdk = 36 + compileSdk = 35 defaultConfig { minSdk = 24 diff --git a/platform/android/lui/src/main/kotlin/dev/lui/LuiIcons.kt b/platform/android/lui/src/main/kotlin/dev/lui/LuiIcons.kt index c86d397f..151fa0e3 100644 --- a/platform/android/lui/src/main/kotlin/dev/lui/LuiIcons.kt +++ b/platform/android/lui/src/main/kotlin/dev/lui/LuiIcons.kt @@ -20,7 +20,7 @@ object LuiIcons { fun resolve(name: String): ImageVector { if (name.startsWith("app:")) { - return registered[name.substring(4)] ?: Icons.Rounded.HelpOutline + return registered[name.substring(4)] ?: Icons.AutoMirrored.Rounded.HelpOutline } return when (name) { "alert" -> Icons.Rounded.Warning @@ -47,7 +47,7 @@ object LuiIcons { "folder-open" -> Icons.Rounded.FolderOpen "git-branch" -> Icons.Rounded.AccountTree "git-merge" -> Icons.Rounded.Merge - "git-pull-request" -> Icons.Rounded.CallMerge + "git-pull-request" -> Icons.AutoMirrored.Rounded.CallMerge "info" -> Icons.Rounded.Info "menu" -> Icons.Rounded.Menu "mic" -> Icons.Rounded.Mic @@ -74,7 +74,7 @@ object LuiIcons { "wrench" -> Icons.Rounded.Build "x" -> Icons.Rounded.Close "x-circle" -> Icons.Rounded.Cancel - else -> Icons.Rounded.HelpOutline + else -> Icons.AutoMirrored.Rounded.HelpOutline } } } diff --git a/platform/android/lui/src/main/kotlin/dev/lui/LuiNodeView.kt b/platform/android/lui/src/main/kotlin/dev/lui/LuiNodeView.kt index 080c297e..22d8b39a 100644 --- a/platform/android/lui/src/main/kotlin/dev/lui/LuiNodeView.kt +++ b/platform/android/lui/src/main/kotlin/dev/lui/LuiNodeView.kt @@ -87,6 +87,7 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp @@ -415,10 +416,11 @@ private fun LuiNodeContent( } LuiNodeKind.switchControl -> Row( - modifier.fillMaxWidth(), + modifier, + horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.CenterVertically, ) { - Text(node.text(), Modifier.weight(1f)) + if (node.text().isNotEmpty()) Text(node.text()) Switch( checked = node.propBool("checked"), onCheckedChange = { value -> @@ -850,17 +852,21 @@ private fun LuiButton( contentDescription = node.propString("accessibility-label"), modifier = Modifier.size(iconSize), ) - icon == null -> Text(node.text()) + icon == null -> Text( + node.text(), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) else -> Row( horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically, ) { if (node.propString("icon-placement") == "trailing") { - Text(node.text()) + Text(node.text(), maxLines = 1, overflow = TextOverflow.Ellipsis) Icon(icon, contentDescription = null, modifier = Modifier.size(iconSize)) } else { Icon(icon, contentDescription = null, modifier = Modifier.size(iconSize)) - Text(node.text()) + Text(node.text(), maxLines = 1, overflow = TextOverflow.Ellipsis) } } } diff --git a/platform/android/lui/src/test/kotlin/dev/lui/LuiScreenshotTest.kt b/platform/android/lui/src/test/kotlin/dev/lui/LuiScreenshotTest.kt new file mode 100644 index 00000000..453881ff --- /dev/null +++ b/platform/android/lui/src/test/kotlin/dev/lui/LuiScreenshotTest.kt @@ -0,0 +1,240 @@ +package dev.lui + +import app.cash.paparazzi.DeviceConfig +import app.cash.paparazzi.Paparazzi +import org.junit.Rule +import org.junit.Test + +/** + * Screenshot coverage for the Kotlin backend's default theme (light AND + * dark) at phone size. Trees are fed through the real wire path — + * applyBatch on hand-written patch JSON — so the shots exercise the same + * parse/store/render pipeline the gallery uses. + */ +class LuiScreenshotTest { + + @get:Rule + val paparazzi = Paparazzi( + deviceConfig = DeviceConfig.PIXEL_5.copy(softButtons = false), + ) + + private fun backendFor(vararg batches: String): LuiBackend { + val backend = LuiBackend() + batches.forEachIndexed { index, json -> + check(backend.applyBatch(json)) { "batch $index rejected: ${backend.lastError}" } + } + return backend + } + + private fun snapshot(name: String, backend: LuiBackend, dark: Boolean) { + paparazzi.snapshot(name = name) { + LuiTheme(dark = dark) { + androidx.compose.material3.Surface( + color = androidx.compose.material3.MaterialTheme.colorScheme.background, + ) { + backend.Content() + } + } + } + } + + private fun galleryScreen(dark: Boolean) { + // A representative settings-style screen: toolbar, card with + // controls, list items, and a bottom tab bar. + val backend = backendFor( + """ + {"generation":1,"ops":[ + {"op":"create-node","id":1,"kind":"root"}, + {"op":"create-node","id":2,"kind":"column"}, + {"op":"insert-child","parent":1,"child":2,"index":0}, + {"op":"set-prop","id":2,"property":"gap","value":16}, + {"op":"set-prop","id":2,"property":"padding","value":16}, + + {"op":"create-node","id":3,"kind":"heading"}, + {"op":"insert-child","parent":2,"child":3,"index":0}, + {"op":"set-prop","id":3,"property":"text","value":"Components"}, + {"op":"set-prop","id":3,"property":"heading-level","value":3}, + + {"op":"create-node","id":4,"kind":"card"}, + {"op":"insert-child","parent":2,"child":4,"index":1}, + {"op":"create-node","id":5,"kind":"column"}, + {"op":"insert-child","parent":4,"child":5,"index":0}, + {"op":"set-prop","id":5,"property":"gap","value":12}, + + {"op":"create-node","id":6,"kind":"text"}, + {"op":"insert-child","parent":5,"child":6,"index":0}, + {"op":"set-prop","id":6,"property":"text", + "value":"Polished Material 3 defaults — no per-app CSS."}, + + {"op":"create-node","id":7,"kind":"row"}, + {"op":"insert-child","parent":5,"child":7,"index":1}, + {"op":"set-prop","id":7,"property":"gap","value":8}, + + {"op":"create-node","id":8,"kind":"button"}, + {"op":"insert-child","parent":7,"child":8,"index":0}, + {"op":"set-prop","id":8,"property":"text","value":"Primary"}, + {"op":"set-prop","id":8,"property":"variant","value":"primary"}, + + {"op":"create-node","id":9,"kind":"button"}, + {"op":"insert-child","parent":7,"child":9,"index":1}, + {"op":"set-prop","id":9,"property":"text","value":"Secondary"}, + {"op":"set-prop","id":9,"property":"variant","value":"secondary"}, + + {"op":"create-node","id":10,"kind":"button"}, + {"op":"insert-child","parent":7,"child":10,"index":2}, + {"op":"set-prop","id":10,"property":"text","value":"Outline"}, + {"op":"set-prop","id":10,"property":"variant","value":"outline"}, + + {"op":"create-node","id":11,"kind":"text-field"}, + {"op":"insert-child","parent":5,"child":11,"index":2}, + {"op":"set-prop","id":11,"property":"placeholder", + "value":"Search components"}, + + {"op":"create-node","id":12,"kind":"row"}, + {"op":"insert-child","parent":5,"child":12,"index":3}, + {"op":"set-prop","id":12,"property":"gap","value":16}, + {"op":"set-prop","id":12,"property":"main","value":"start"}, + + {"op":"create-node","id":13,"kind":"switch"}, + {"op":"insert-child","parent":12,"child":13,"index":0}, + {"op":"set-prop","id":13,"property":"text","value":"Enabled"}, + {"op":"set-prop","id":13,"property":"checked","value":true}, + + {"op":"create-node","id":14,"kind":"checkbox"}, + {"op":"insert-child","parent":12,"child":14,"index":1}, + {"op":"set-prop","id":14,"property":"text","value":"Remember"}, + + {"op":"create-node","id":15,"kind":"slider"}, + {"op":"insert-child","parent":5,"child":15,"index":4}, + {"op":"set-prop","id":15,"property":"value","value":0.65}, + + {"op":"create-node","id":16,"kind":"progress"}, + {"op":"insert-child","parent":5,"child":16,"index":5}, + {"op":"set-prop","id":16,"property":"value","value":0.4}, + + {"op":"create-node","id":19,"kind":"list"}, + {"op":"insert-child","parent":2,"child":19,"index":2}, + {"op":"create-node","id":20,"kind":"list-section"}, + {"op":"insert-child","parent":19,"child":20,"index":0}, + {"op":"create-node","id":21,"kind":"list-item"}, + {"op":"insert-child","parent":20,"child":21,"index":0}, + {"op":"set-prop","id":21,"property":"text","value":"Inbox"}, + {"op":"set-prop","id":21,"property":"icon","value":"folder"}, + {"op":"set-prop","id":21,"property":"press-enabled","value":true}, + {"op":"create-node","id":22,"kind":"list-item"}, + {"op":"insert-child","parent":20,"child":22,"index":1}, + {"op":"set-prop","id":22,"property":"text","value":"Settings"}, + {"op":"set-prop","id":22,"property":"icon","value":"settings"}, + {"op":"set-prop","id":22,"property":"press-enabled","value":true}, + {"op":"create-node","id":24,"kind":"list-item"}, + {"op":"insert-child","parent":20,"child":24,"index":2}, + {"op":"set-prop","id":24,"property":"text","value":"About"}, + {"op":"set-prop","id":24,"property":"icon","value":"info"}, + {"op":"set-prop","id":24,"property":"press-enabled","value":true} + ]} + """.trimIndent(), + ) + snapshot(if (dark) "gallery-dark" else "gallery-light", backend, dark) + } + + @Test + fun galleryLight() = galleryScreen(dark = false) + + @Test + fun galleryDark() = galleryScreen(dark = true) + + private fun controlsScreen(dark: Boolean) { + val backend = backendFor( + """ + {"generation":1,"ops":[ + {"op":"create-node","id":1,"kind":"root"}, + {"op":"create-node","id":2,"kind":"column"}, + {"op":"insert-child","parent":1,"child":2,"index":0}, + {"op":"set-prop","id":2,"property":"gap","value":12}, + {"op":"set-prop","id":2,"property":"padding","value":16}, + + {"op":"create-node","id":3,"kind":"tabs"}, + {"op":"insert-child","parent":2,"child":3,"index":0}, + {"op":"create-node","id":4,"kind":"button"}, + {"op":"insert-child","parent":3,"child":4,"index":0}, + {"op":"set-prop","id":4,"property":"text","value":"Feed"}, + {"op":"set-prop","id":4,"property":"selected","value":true}, + {"op":"create-node","id":5,"kind":"button"}, + {"op":"insert-child","parent":3,"child":5,"index":1}, + {"op":"set-prop","id":5,"property":"text","value":"Saved"}, + {"op":"create-node","id":6,"kind":"button"}, + {"op":"insert-child","parent":3,"child":6,"index":2}, + {"op":"set-prop","id":6,"property":"text","value":"Settings"}, + + {"op":"create-node","id":7,"kind":"row"}, + {"op":"insert-child","parent":2,"child":7,"index":1}, + {"op":"set-prop","id":7,"property":"gap","value":12}, + + {"op":"create-node","id":8,"kind":"avatar"}, + {"op":"insert-child","parent":7,"child":8,"index":0}, + {"op":"set-prop","id":8,"property":"text","value":"JD"}, + + {"op":"create-node","id":9,"kind":"column"}, + {"op":"insert-child","parent":7,"child":9,"index":1}, + {"op":"set-prop","id":9,"property":"gap","value":2}, + {"op":"create-node","id":10,"kind":"text"}, + {"op":"insert-child","parent":9,"child":10,"index":0}, + {"op":"set-prop","id":10,"property":"text","value":"Jane Doe"}, + {"op":"create-node","id":11,"kind":"text"}, + {"op":"insert-child","parent":9,"child":11,"index":1}, + {"op":"set-prop","id":11,"property":"text", + "value":"jane@example.com"}, + {"op":"set-prop","id":11,"property":"foreground", + "value":"muted-foreground"}, + + {"op":"create-node","id":13,"kind":"radio-group"}, + {"op":"insert-child","parent":2,"child":13,"index":2}, + {"op":"create-node","id":14,"kind":"radio"}, + {"op":"insert-child","parent":13,"child":14,"index":0}, + {"op":"set-prop","id":14,"property":"text","value":"Daily"}, + {"op":"set-prop","id":14,"property":"checked","value":true}, + {"op":"create-node","id":15,"kind":"radio"}, + {"op":"insert-child","parent":13,"child":15,"index":1}, + {"op":"set-prop","id":15,"property":"text","value":"Weekly"}, + + {"op":"create-node","id":16,"kind":"alert"}, + {"op":"insert-child","parent":2,"child":16,"index":3}, + {"op":"set-prop","id":16,"property":"text","value":"Heads up"}, + {"op":"create-node","id":17,"kind":"text"}, + {"op":"insert-child","parent":16,"child":17,"index":0}, + {"op":"set-prop","id":17,"property":"text", + "value":"Sync finished with 3 warnings."}, + + {"op":"create-node","id":18,"kind":"accordion"}, + {"op":"insert-child","parent":2,"child":18,"index":4}, + {"op":"set-prop","id":18,"property":"text","value":"Advanced"}, + {"op":"set-prop","id":18,"property":"selected","value":true}, + {"op":"create-node","id":19,"kind":"text"}, + {"op":"insert-child","parent":18,"child":19,"index":0}, + {"op":"set-prop","id":19,"property":"text", + "value":"Hidden details stay reachable."}, + + {"op":"create-node","id":25,"kind":"stepper"}, + {"op":"insert-child","parent":2,"child":25,"index":5}, + {"op":"set-prop","id":25,"property":"active","value":1}, + {"op":"create-node","id":26,"kind":"step"}, + {"op":"insert-child","parent":25,"child":26,"index":0}, + {"op":"set-prop","id":26,"property":"text","value":"Account"}, + {"op":"create-node","id":27,"kind":"step"}, + {"op":"insert-child","parent":25,"child":27,"index":1}, + {"op":"set-prop","id":27,"property":"text","value":"Profile"}, + {"op":"create-node","id":28,"kind":"step"}, + {"op":"insert-child","parent":25,"child":28,"index":2}, + {"op":"set-prop","id":28,"property":"text","value":"Done"} + ]} + """.trimIndent(), + ) + snapshot(if (dark) "controls-dark" else "controls-light", backend, dark) + } + + @Test + fun controlsLight() = controlsScreen(dark = false) + + @Test + fun controlsDark() = controlsScreen(dark = true) +} diff --git a/platform/android/lui/src/test/kotlin/dev/lui/LuiStoreTest.kt b/platform/android/lui/src/test/kotlin/dev/lui/LuiStoreTest.kt new file mode 100644 index 00000000..9796f0e4 --- /dev/null +++ b/platform/android/lui/src/test/kotlin/dev/lui/LuiStoreTest.kt @@ -0,0 +1,238 @@ +package dev.lui + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Assert.assertThrows +import org.junit.Assert.assertTrue +import org.junit.Test + +class LuiStoreTest { + + private fun tree() = LuiRetainedTree(LuiExtensionRegistry()) + + private fun batch(generation: Int, vararg ops: LuiPatchOp) = + LuiPatchBatch(generation, ops.toList()) + + @Test + fun `applies create insert set-prop and drop in order`() { + val tree = tree() + tree.apply( + batch( + 1, + LuiPatchOp.CreateNode(1, LuiNodeKind.root), + LuiPatchOp.CreateNode(2, LuiNodeKind.column), + LuiPatchOp.InsertChild(1, 2, 0), + LuiPatchOp.SetProp(2, "gap", LuiWireValue.IntValue(8)), + ), + ) + assertEquals(1, tree.generation) + assertEquals(listOf(1), tree.rootIds) + assertEquals(listOf(2), tree.nodes[1]!!.children) + assertEquals(1, tree.nodes[2]!!.parent) + assertEquals(LuiWireValue.IntValue(8), tree.nodes[2]!!.properties["gap"]) + + tree.apply( + batch( + 2, + LuiPatchOp.CreateNode(3, LuiNodeKind.text), + LuiPatchOp.InsertChild(1, 3, 1), + LuiPatchOp.RemoveChild(1, 2), + LuiPatchOp.DropNode(2), + ), + ) + assertNull(tree.nodes[2]) + assertEquals(listOf(3), tree.nodes[1]!!.children) + assertEquals(setOf(2), tree.lastDropped) + } + + @Test + fun `rejects generation gaps`() { + val tree = tree() + tree.apply( + batch( + 1, + LuiPatchOp.CreateNode(1, LuiNodeKind.root), + LuiPatchOp.CreateNode(2, LuiNodeKind.column), + LuiPatchOp.InsertChild(1, 2, 0), + ), + ) + val error = assertThrows(LuiBackendException::class.java) { + tree.apply(batch(3, LuiPatchOp.CreateNode(5, LuiNodeKind.column))) + } + assertTrue(error.message!!.contains("generation 2")) + assertNull(tree.nodes[5]) + } + + @Test + fun `rolls the whole batch back on failure`() { + val tree = tree() + tree.apply( + batch( + 1, + LuiPatchOp.CreateNode(1, LuiNodeKind.root), + LuiPatchOp.CreateNode(2, LuiNodeKind.column), + LuiPatchOp.InsertChild(1, 2, 0), + ), + ) + assertThrows(LuiBackendException::class.java) { + tree.apply( + batch( + 2, + LuiPatchOp.CreateNode(3, LuiNodeKind.text), + // Attach an unknown parent -> whole batch must roll back. + LuiPatchOp.InsertChild(99, 3, 0), + ), + ) + } + assertEquals(1, tree.generation) + assertNull(tree.nodes[3]) + } + + @Test + fun `set-prop validates against the kind matrix`() { + val tree = tree() + tree.apply( + batch( + 1, + LuiPatchOp.CreateNode(1, LuiNodeKind.root), + LuiPatchOp.CreateNode(2, LuiNodeKind.text), + LuiPatchOp.InsertChild(1, 2, 0), + ), + ) + tree.apply( + batch(2, LuiPatchOp.SetProp(2, "text", LuiWireValue.Str("hi"))), + ) + assertEquals(LuiWireValue.Str("hi"), tree.nodes[2]!!.properties["text"]) + } + + @Test + fun `move-child reorders within a parent`() { + val tree = tree() + tree.apply( + batch( + 1, + LuiPatchOp.CreateNode(1, LuiNodeKind.root), + LuiPatchOp.CreateNode(2, LuiNodeKind.column), + LuiPatchOp.CreateNode(3, LuiNodeKind.text), + LuiPatchOp.CreateNode(4, LuiNodeKind.text), + LuiPatchOp.InsertChild(1, 2, 0), + LuiPatchOp.InsertChild(2, 3, 0), + LuiPatchOp.InsertChild(2, 4, 1), + ), + ) + tree.apply(batch(2, LuiPatchOp.MoveChild(2, 4, 0))) + assertEquals(listOf(4, 3), tree.nodes[2]!!.children) + } + + @Test + fun `drop-node requires a detached node`() { + val tree = tree() + tree.apply( + batch( + 1, + LuiPatchOp.CreateNode(1, LuiNodeKind.root), + LuiPatchOp.CreateNode(2, LuiNodeKind.column), + LuiPatchOp.InsertChild(1, 2, 0), + ), + ) + assertThrows(LuiBackendException::class.java) { + tree.apply(batch(2, LuiPatchOp.DropNode(2))) + } + assertEquals(1, tree.generation) + } + + @Test + fun `insert-child rejects cycles`() { + val tree = tree() + tree.apply( + batch( + 1, + LuiPatchOp.CreateNode(1, LuiNodeKind.root), + LuiPatchOp.CreateNode(2, LuiNodeKind.column), + LuiPatchOp.CreateNode(3, LuiNodeKind.column), + LuiPatchOp.InsertChild(1, 2, 0), + LuiPatchOp.InsertChild(2, 3, 0), + ), + ) + // Attaching the root under a descendant would create a cycle. + assertThrows(LuiBackendException::class.java) { + tree.apply(batch(2, LuiPatchOp.InsertChild(3, 1, 0))) + } + assertNull(tree.nodes[1]!!.parent) + } + + @Test + fun `extension nodes validate fingerprint and defaults`() { + val registry = LuiExtensionRegistry() + registry.register( + identifier = "chart", + fingerprint = "fp-1", + properties = listOf( + LuiExtensionProperty( + "title", + LuiExtensionValueKind.STRING, + defaultValue = LuiWireValue.Str("untitled"), + ), + ), + builder = {}, + ) + val tree = LuiRetainedTree(registry) + assertThrows(LuiBackendException::class.java) { + tree.apply( + batch(1, LuiPatchOp.CreateExtension(1, "chart", "wrong-fp")), + ) + } + tree.apply( + batch( + 1, + LuiPatchOp.CreateExtension(1, "chart", "fp-1"), + ), + ) + assertEquals("chart", tree.extensionNodes[1]!!.identifier) + assertEquals( + LuiWireValue.Str("untitled"), + tree.extensionNodes[1]!!.properties["title"], + ) + + tree.apply( + batch(2, LuiPatchOp.SetExtensionProp(1, "title", LuiWireValue.Str("q1"))), + ) + // apply() swaps in fresh node copies, so re-read after each batch. + assertEquals( + LuiWireValue.Str("q1"), + tree.extensionNodes[1]!!.properties["title"], + ) + // Removing restores the declared default. + tree.apply(batch(3, LuiPatchOp.RemoveExtensionProp(1, "title"))) + assertEquals( + LuiWireValue.Str("untitled"), + tree.extensionNodes[1]!!.properties["title"], + ) + } + + @Test + fun `backend reports failure without touching state`() { + val backend = LuiBackend() + assertTrue( + backend.applyBatch( + """{"generation":1,"ops":[ + {"op":"create-node","id":1,"kind":"root"}, + {"op":"create-node","id":9,"kind":"column"}, + {"op":"insert-child","parent":1,"child":9,"index":0} + ]}""", + ), + ) + assertFalse( + backend.applyBatch( + """{"generation":5,"ops":[]}""", + ), + ) + // A rejected batch leaves the committed generation untouched. + assertTrue( + backend.applyBatch( + """{"generation":2,"ops":[{"op":"create-node","id":2,"kind":"text"}]}""", + ), + ) + } +} diff --git a/platform/android/lui/src/test/kotlin/dev/lui/LuiWireTest.kt b/platform/android/lui/src/test/kotlin/dev/lui/LuiWireTest.kt new file mode 100644 index 00000000..3e39736f --- /dev/null +++ b/platform/android/lui/src/test/kotlin/dev/lui/LuiWireTest.kt @@ -0,0 +1,122 @@ +package dev.lui + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertThrows +import org.junit.Assert.assertTrue +import org.junit.Test + +class LuiWireTest { + + @Test + fun `parses a patch batch with every op kind`() { + val batch = LuiPatchBatch.parse( + """ + { + "generation": 1, + "ops": [ + {"op": "create-node", "id": 1, "kind": "root"}, + {"op": "create-node", "id": 2, "kind": "column"}, + {"op": "insert-child", "parent": 1, "child": 2, "index": 0}, + {"op": "set-prop", "id": 2, "property": "gap", "value": 8}, + {"op": "remove-prop", "id": 2, "property": "gap"}, + {"op": "move-child", "parent": 1, "child": 2, "index": 0}, + {"op": "remove-child", "parent": 1, "child": 2}, + {"op": "create-extension", "id": 9, "identifier": "split-view", + "fingerprint": "fp"}, + {"op": "set-extension-prop", "id": 9, "property": "title", + "value": "hi"}, + {"op": "remove-extension-prop", "id": 9, "property": "title"}, + {"op": "drop-node", "id": 2} + ] + } + """.trimIndent(), + ) + assertEquals(1, batch.generation) + assertEquals(11, batch.ops.size) + assertEquals( + LuiPatchOp.CreateNode(1, LuiNodeKind.root), + batch.ops[0], + ) + assertEquals( + LuiPatchOp.InsertChild(parent = 1, child = 2, index = 0), + batch.ops[2], + ) + assertEquals( + LuiPatchOp.SetProp(2, "gap", LuiWireValue.IntValue(8)), + batch.ops[3], + ) + assertEquals( + LuiPatchOp.CreateExtension(9, "split-view", "fp"), + batch.ops[7], + ) + assertEquals( + LuiPatchOp.SetExtensionProp(9, "title", LuiWireValue.Str("hi")), + batch.ops[8], + ) + assertEquals(LuiPatchOp.DropNode(2), batch.ops[10]) + } + + @Test + fun `decodes all wire value kinds`() { + val batch = LuiPatchBatch.parse( + """ + {"generation": 1, "ops": [ + {"op": "set-prop", "id": 1, "property": "text", "value": "hello"}, + {"op": "set-prop", "id": 1, "property": "enabled", "value": true}, + {"op": "set-prop", "id": 1, "property": "width", "value": 42}, + {"op": "set-prop", "id": 1, "property": "value", "value": 0.5} + ]} + """.trimIndent(), + ) + val values = batch.ops.map { (it as LuiPatchOp.SetProp).value } + assertEquals(LuiWireValue.Str("hello"), values[0]) + assertEquals(LuiWireValue.Bool(true), values[1]) + assertEquals(LuiWireValue.IntValue(42), values[2]) + assertEquals(LuiWireValue.DoubleValue(0.5), values[3]) + assertEquals(42.0, values[2].numberValue) + assertEquals(0.5, values[3].doubleValue) + } + + @Test + fun `rejects malformed batches`() { + assertThrows(LuiBackendException::class.java) { + LuiPatchBatch.parse("not json") + } + assertThrows(LuiBackendException::class.java) { + LuiPatchBatch.parse("""{"ops": []}""") + } + assertThrows(LuiBackendException::class.java) { + LuiPatchBatch.parse("""{"generation": 1}""") + } + assertThrows(LuiBackendException::class.java) { + LuiPatchBatch.parse( + """{"generation": 1, "ops": [{"op": "bogus"}]}""", + ) + } + assertThrows(LuiBackendException::class.java) { + LuiPatchBatch.parse( + """{"generation": 1, "ops": + [{"op": "create-node", "id": 1, "kind": "no-such-kind"}]}""", + ) + } + } + + @Test + fun `node kind wire mapping covers the full schema`() { + assertEquals(LuiNodeKind.bottomTabs, LuiNodeKind.fromWire("bottom-tabs")) + assertEquals(LuiNodeKind.numberStepper, LuiNodeKind.fromWire("number-stepper")) + assertEquals(null, LuiNodeKind.fromWire("not-a-kind")) + assertTrue(LuiNodeKind.entries.size >= 80) + assertTrue(LuiNodeKind.tabs.isContainer) + assertTrue(!LuiNodeKind.slider.isContainer) + } + + @Test + fun `wire value serializes to JSON literal`() { + assertEquals("\"hi\"", LuiWireValue.Str("hi").toJsonLiteral()) + assertEquals("true", LuiWireValue.Bool(true).toJsonLiteral()) + assertEquals("3", LuiWireValue.IntValue(3).toJsonLiteral()) + assertEquals("1.5", LuiWireValue.DoubleValue(1.5).toJsonLiteral()) + assertEquals("\"a\\\"b\"", LuiWireValue.Str("a\"b").toJsonLiteral()) + } +} diff --git a/platform/android/lui/src/test/snapshots/images/dev.lui_LuiScreenshotTest_controlsDark_controls-dark.png b/platform/android/lui/src/test/snapshots/images/dev.lui_LuiScreenshotTest_controlsDark_controls-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..b1a5ff1eca3686a0eecb57b0ffe453d7b05c0fc1 GIT binary patch literal 35463 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�bWs!M1YgbMKuX?{rVa^Ee^xmS;I@qmGwEf>-;9w{*6A z;+H;~Lw57tX||}Z2{MdrhsXuS)EdcJYFhbC^l|n{I$Zt8@q90ZPt>%^J;sxH&#Dp( zbYC0+-A=?g?SQ~l@!EvKq;V&Xp|O)DS3SCdASRS4{`0gbe_m<s5AA)tLi5f<@{@3OKso3?c5z~}L6V5thDlyX-9gmnvI3(cMU5kIfRwRm`> zZAV#2P8lznbV?5F2prdR`X&oA=i9(8%jJp9YV#vDaQjZ)&+JAGg+oJjkz(i%s6HMW z#p>V0ghU3}d(orhLA)rxt?xR3-_vMq#4V>&p2dFK1V;9M%gz0Z#r^l>0_TJOUs=2V z%|ZV^Q6^F?AgWCxrkAub^La^3B9gM0L^nnTBsU#`Bq__N=Wm;}@_W^i;QOzmN|RVP z-Ts*G3i|w_5 zLWQ~sCk0@X;^J1%Iz>iCs6II}7~pi4DS0kfD)xe~LlZ8&clZzl8#6}%! zitiX1Q~-&o&E)3PQN(J}k~C}oVCUqptQf(aae@(Tf=BT9lDLzcjfJ(rJ=$dJNwJFw z``6jMLoXjD#b@IOwiA&R)&z;6gMbm1o8$JX9?JK&ZWmYx@1|a#JjvIe zQno$LuD?=){@!aLVr!IWiS$~Gw&0nH;Z7TTQL*k-t5f+>6*xLfiI z#Wj;5u>`Oeurpzr$mXbLQlS5Qb>xke$L9Bb60B6ynRbBRVIiOsYyCTmf9Lg& zZ@&9a5TB~wV9gJUOZ|2crsSZV*MIWs*;3H$4y4B5O@7bCJ8J;Kl?(1xJT#Sbu+SS( zf(tt$&sJyh+k?a8o+TBOCKc8~$27a6pYwFwi{Y}neL4hEhP;*(-Gm&#^PLNl;_81EXM#THtBKk@I&pdmng6`53&m_c z8!)E-cJsHEaPvoTzrR|51t@no9q>TVef48kmh+M{xx_U4N%?C4X!QkB2s)}2;&V05 z0MPd~azP0I)f5r~3d5(=^v*!eFRbw-nDM#p?@`S-&1{4M)ojZE&P$bHaW&j~vVA?C z+px}oN~-K%2c6e~K9c)@gO&odCy1xnY!iQuA!xQ!(h;&B~@kp7=bw zx9KF=mn)m0Uso6RZlXWQZ%`0PN%{2&*S{Y|=c;gSyP()bW3#V)53Sm53VO~pBQJ;9a*TuC#P0_= z_qPi%HN{Hg*RaWTWy)WTZU45)^&{V331f~$+1r0~ALlgX876iidGQ3&>y$$|?WRAH zQKgAD)wl9(ljSt)OFU~DpH*zzeYQ7J+pi$wjVX?93m9mhYAJ)}ZM1X6E8;7w7NBy4W>z$HU?_wiktQut4#0fOk-U)|6l>7u&PzzCry zJNvU{NcH0rHk|+V1$g)0EV$2*!}4e6TRs9{VVEkmvGzImff`IiRwDnp|AXbcMI!Q7Vt-$Z+$+0?f=*}$qV zc`d&>tF0_^8x)_hLZb_m9NhD+&4;>NhNUa9N@UK{K)7v6u|_3qOO&tUkJY**Kvckm zmhY$ho1r@&zJ+ta>&<|f{d%Upo_~R$F#?4C$n*$?UOKne5-y~>UbX(u*&Jwa-3&-- z{wKlx-_Q84yZ-_lKs~{#_W!&RTVB!){5v|>% z_o#jmZmd@LP>MO0m?nu6=nb;!tFKeJO;-kjlmxv0 zVI1EyL;xbJo>18&JwWCqsc$w%)a3giS;c{*`DBfM`!&PrbGe#wZM3HpbN^GracVMi z*7tWf34NaR>#MWR?%z6qKl~z7!Qu7$q0hZ<)Rb;QfX`?=h7!vC>9g_w_ly*+3Sb>Ggt@ zAqnFFOe+buoA(!i#IKHag?iRp>(0GHijoy9(t;50QEcKwkz8@=$Z8G4*kP$qAByx@ zNf~IysAaJmiR+dyviEuOd^E5qa#XbBoCE@7exN7XEh@kgl|gp9lbBq2GzMKWpZ+cW(QNYb{i~JXK7j5wL#Uw_5McRA5?Qza3*+ z>6dCEKeZ?WK*ET`C)vyqF2(!-2aLPB2dT4wGdIr%1!&(!N@_nib*1UFT zqb-c!1sk=ai35%LMAaFcemgEdmtLFyLJtXSK`QR9)B!b$pZFK1AKY>!>{ zS6?s?fIwO_SHP?qVHKIaKi=nOyg5=VWr!5Q+90ggeR>pFUSvm*ch{*6FHI_d--!&^ zznh~wCI2i6Dd?0B{QJJEhZ#MQK+5#>@5a2~<-7y{UNKeBXA9m@hje(yKTgG#-=O6O zU2*IvBFb`)t#(JvzzewkUFyRrZyzFrS>~n8l586F+-#^{lJGgx0^ZqzZF0{vBb|eo% zp%!Nh1YkH|6$t=GWY>i&G8r*nS>C4L1jrp2pxU5IS)8m^U=sXIyn7*X366pst4Xg1 zdpApu7ccu?14^bzA1+XP5ZbyMP=>Xis8z8t+oDNXyKONi%rlojrlPx!%5SZW(%BSk<# zx=82{M0!^NX;K8~y?0PS=^)a3FVZ1E=uv431PGx+KnV~a5NZe^lzI5hIdi@_>pin( z)~q>e{uusp?G<6?$$s{;@85OZS11(=Ur#VBf3c9RbQEUsea-^3y(cnf`cy<{*ey2u zRHG)z3Ol68e*&_>&tDuijt@*yhRN%xnP)r&+)faoT%ckHwJQ{chtjWMo&&q)AL_g6 zf1yi_XEZ4`WePCRiRLCiO|@1P*-^ z51c^gDK1}V)r=Pi+V^-b^rveM+siqhK&2+< zN00QAq`(xu-@3g4qu>{)U@#I3H1mUv3C_}EWNPQF)?+0tZ~wSg-MLSp!1OuGE9tT% zgZ6ZpBhbt5_*I2`Q-;STE_c#N)9zW=6j#nhA{7!Z6Cu9M;v@fUQV=z91MW!Hh0im& zMft*Rt)c3kH+_9eNpd2CP|^cU-QVJz^Pzjjb#G&%_(o(K6ab$Y#f>|3GpZsPkHany z^S?BvaPrvf=k?UN&D+F=f2g2yHaZag9^rYv#DDs>*FC0th-*9nNe^c(El3C0{9MZ` zQK*XrTASF0qyY2gh^)puJ&jL%b&z9$~ z^Y)aKn@^8uz9>->>W0+;1*^o8QV0w{Z`S8(RQxRE&rhw1?5({-w0N_&L5edI1?Ee( zp|+V1c251H9<-M4!rL&Xmx;noMUiDJeWJ1zB_PD7Xe(bAK%Cs~Sh*x`H`*NM{03N~au>aFB<>JE17?`M_RPA|#qPXx_4pO`W4}F5fQ{{)Rd0)t?PrMnfH6 z=GX$y9I;6>^R%zhQu$84mHAR9dLKI%W{QmLM#RQI1!a-%# z3(lwgydFs*Nie_PuH9KW_2>Za+bfmd{c6Z`^U126G_`am;q{zJ zhpcgiqSQseCvG%MNAWm!-S+Z#IqSq;O=Laq(PQ)Mhfl9wpk$o6%sL;;zIp5YP-deF zBhlW}&f7;EwDRKh!bW%EUMNLEZ;w;hvR6h775S87c|MV{5Acs>nb=g|MOf^zNaXnyP@i zFXGevy2S9|Re8A=cl-*4sfr>&3m6ru1n>}9cGO5oRFk~ImTiDMbq9}6wrQdtoQ7L7 zNe7W6^EQ@Ez*dTHbNs27-|-tWlk3t(c82+mBP%i9 zgM$r{3wjx!!7mR07lbU2CM32gnX<94i=N#+P;?|t8@R(1i&_Fm!Lcz>agFi{?+?^G zr|NXG#+jC8dNP%Y+w@HtlD|jfTk>K1+2Jt&J5D{o%@3~_lu;u>8yo)Ir(x4nx!fV1 zX{h>)qjgRsO{e&gXt%j?#jJw)CShkkvq36UngdPvNLSDKEs5YDW0vMgUac z3C|MXjGtxy1sf*wjW|u^GO;lb%k5YH7nTU4fv=o$ThvI6A@wOym=J zo4Nu!Z3Qot6C>GfwyHCk;%T6qi!HN^R)|$e%p47SP9iWk+QCHn;OzJq5}_C(_~IPE zyd728zgTyl{V_6dJfu!1KM#Hbby@}Ob0yc6EW1_};0=5xwqk>_UN5dhM07EMLUw-| z{*v{p8>{mG-XT!QhiGtI^Dy|UyFM7t+2O1kW|dH4Xo!CESJ#t4(w8quY@}FQCK@T8 z;c%ZE`~D&7qG=C;4jKeBS1sTEla^qLz6x)=t4tlrCJF>bR2sAz^*Eq}kJQ;UW6(V! zqENLSi))|2muU_}1)N@9e$7>VLHJtwlfDu1xy-#!o|RitpSa&hDA-v`kKAj?L)8uV zByi3=Km-IJXPkO%n(vo8K8Mz6D(B6B->1wYtqn=O6BD*nTV0ron~hGfXaJxU0MRKg ziX)@wzpCoq@Lh+!(WSk^%o7IWYV;uCeb%I+LtUDcI_X)vjed5`cL#&O+gRNZ$Z08H zw%-Psg1m_Qqmann#;1>(owEi`jN^6Fn9U?`aSkhN? z!!#M2bmg9Tirox=7jW&A7EO0}$Vojese$0kKOV?sL4q_f@oEr#XfRKt@}~x__}qh@ zQH21O83s1hqPK3h4_Uhe!J-;ho+%_nlFVhS5n1%#rMajEBctd0BL;z6ypn(h8fg8U zLyss(0;Am`x0uDBvFg?eX0hV#fdxxIb#S0V7Y_+jVURc2W>BU;?gy>K#7y* z?nSIqQ2TM?lfK7f$9VjW!_^kHpy<_pqlmPh-zsCCzY!@z8jz(JEAxM5k<3Z!l722{ zAv-}*Qf9g3ktXR@0`wFsaoIm6yzjr$ry8MNm#xSxp+3`KYsN znv(@k3}8!n`(yQ2MtSe0s|RO7Yk6FDq>1c?!OB$2f)52c-~R}|F%c+Hp`_HxlvbZr zfvf)YoP5G5DI(aF>6R1s=4;oS2oP6nW;xXjb4MpbYl9zQ7ZnF?Qc#Ice&Ouudmeis z=u_G2+S@o*|B*#DGOSPxS*{4A4|2VqY9P8%h>`R-VC?F>XFVfy9z(AMeawQi5qq{X zwI3-rCm;#3GZtru! z=Qc*jQYZ#9ekNBY6W=#i(z*JHGQVnfr^AL((F1ob;M}$G`OZ{Jf5Y6Ho_)#Yg3ezS zb9^(zcxGvC_1G;rgd6mni-rrPy-f#MPDEENKZG? z2CPvA{{YqHxJPThyt2DQEi-6g69 zuArMKpL3Z6<7d7kzse0_0+_haMTc-z``?97x@U6){3lhwwI9C<^ zbuPPmDv%k$#_rA($ke!J`I9TJD?SyP>fp^b58rbD>l5P1-evKSx1+kb#W&fGH1b?4XpNzV9BM)J#Er)(& z>qo#z1~4=l9k&Ko75DWJ0nEMX7VV5)Tw+_}p6&(Th?4_ms4^{wisD<&Vz>#Oh83G& zwRTsBbk=6-`r`RDuVDbc&+hLD%eB$EwqxP}YjQYV)Hf;(?AO#Dc&s!D19<1brsUfM zPRWrCl-tvD-9qzTV{LnlW;A)L<7|n^5#z-+*OmHqz;=bhY(_6EZJ(m=GTkY`a>Zts z&kCB=8=beF!(c>pt6(6Y;rh+UdX>(Q-0!$`u}Sp=J;w;r4fz|a{J2YnLM~iynkb;cVR$ZynST;G^xL)QZI>8|>pRyYs3vzP{KVU%|XcS4--MZL??fj%NSQ-h(La|pU zSfyunO^aNsiW!(GYbaL%KS;xUN<2#J#wuWdU0lX-ojX3Qmo76raSeM|%FIMxO==F_ zc_jxjexxbgk=AE7m)COMsF#*A&?{oR@0H7P;`Ae!Dd1qA$SA?V{^_s^*-G+e8N;fn zYD@yDN@L-l906V3OJ+!HSp4cgX5KfAKrLQ-Y||U4UQYW%p;wz@@E*lO83}A3?poLz z&hz|CMH?`4gYArw;C3G6d$!FR7fdDfKR3lh#t1fY+zInOFC8O)dT`K0;{ohb=gIfu z6d-%H3VFO%eJd!QCyt?IG*}J0I98cy_bea~(8POv)leptmpF;{Qm}2Xx;J+pq{NZo zG|^mKt&9uYX@7l_$R)VGJG9xR4(N3F5G)8Jb&>Mf&(!y)Y?(K(+HW`RYc-f;a2vk8 zns|3K_;?E!mtaavbyWaAW$E=Nao6j(Wluj-?HhgMgO_dKV##Pf&v)-$ zw=A)}cmL`!TBM*4sLf^}P5lQzq{&PI*~gEj)du=vZTaB$OS!FY*M3EftzuLlzhCd- zMG6c(?rm@Z2;kti2(s5kJ4^os-4#l&ij_^z^BlSJ)@W{JH|ILF@gG9;`OhEr+GSV@ z59fA1xDfn(ac#vi{|$<>6*rOG#*^40OlNJ6hKi2n#?#0g=(Mbg;L{aot}sS`oFGFKkrtd{T%b|T3LiMB<;oAqYv*sT)g@qLc+#J8Z$W8 z;)9zL_8DsN7?Or|YC~qmHO2V8;64#3w&?cPjT*~y%7el9u=4R1bb1fX&*S#ER9Zf* zjsVkiTrTN(4mCBZ`GiD1m^-~UIzPS@d0lpACgv-o+$=gkbSpi`%m}}pX@`%3jt2S2 zezw_4aS4G|qxL?^&DGx>n3KWIvmeinH$RR@!Su7oSjs?Y-pd?*dP9UPz9G{JeD`Tn z|12eObw;`w@rFj`iJdVv0ymZI#IL-~?D-V!&pJSjf(iv2Nwqt~Z#r)h{<^05*PV>N z-i=)TEC2dm+ivVb|g}CW*piVkIcz3)d*bxL< zu?auia7w0__32bhKYfW&1P8<1j?HUs!7by#Y>m!-8rd+Zr995HuqLkJPzveFqegNr zA1(e2=9Y1`J#;`<+?l5Vg|{XN*bvEbtlr zZXh>tH|!lIo#MXjZ%_@Fs-m4eo^7#4kv;K{#1|ohcb-WXA`OWU5}~Sn(P6aNPjYMH<}Pd& zZ&;e0eR_PdFp9tA(l5IO+mc&@P{1^_Wezr+U4pXRW(?PgyqOOUWuwxOTN<)~_`bzs zY5&p*E790vldT;6l^cs+qWxd_ znB8vPo$*v7X$6?rnoi*t_fuIDYNyhAIo1M?_#%*zj3-j60u)zR(18xwKSs16z ztOe%xN!~P@=xE#80&ECX@#^7X&IzH9z`XOb?RFJTILmfIv*pZk^>U#xRMRAC!ptN; zeKNpe+}~JKw)_Y`RU)UT$hZ|ri$*B37+Y%8|H_9Wea~} zf4CJHdatNHBvCIywD|jZj1t3{M;ngtUF1$FT0946Gyzq`MLmh#tdXS9QjZ;Idf0lPEm6$iO-^I&%*rXQa<+^L<&rBPwZ+3GCT4&nBZ zJ3Ykbj#fnLmw352RZ?b9*5AxAOl$MEY$+Qw3p`zB%x6eGLP2nvX2HjrM@XL#&4r*H zOY)SLPg!5+TGY)e<8o6|1pN6!UR64vX9&pj?BQwqXa`(2OnV{tG{~7dcn2}Lw&u47 zy#_~0daD9br$V{SE@^fK~5;=1=cxqeKl=Hw6*~YrE`-o!mP$Ew57(78WJ6R*{ zU2Me;Li-Q~I|Wmnm@wiYi8eZX>Q#@2%uc0UlDCE_E(V6;gZQj9owD#T!K{W;V+2bxS=~c-%s5rt*p!s0kZU zfTy>N*{U?v=KvMzQ6}&9TUB^CH2Wb_)(8aYq)WvflqGe!Cs9#t`#A>S170IqU|Gof zFCuJq9S}dFgHFCq^#YG9*&0$eT5D)4Ito@j5_1yq>g6%hS^Se2HK_wgr}OqIPuy-6 zcsY(~p3>f>qfXQ*5OXr;A2orhQ)bN_I4q`1DdLFs>b;_UxldSII#-a%f@EQAF{&Q+ zQ>N#mB2&0&D*l&Tz;sWle#m??f(q$lcA7R|9sSCpZh^IfU^kz0tpyIGQJ2j#S0l>0k5yF&2QCT4eJ1XFMEq#Bz<;C5Nmv%M_g zwv`1lC*6MPq7IE8fu==tVwptpd20vvPcw*4D2px>N^pg`1f`^40TOmWr1}Oj$#)OLIrj zDNbvR;&h!yOS6kp@n6ZiAHjBU!vj(GV}qoyF?+kiZj^WFv=K7K9Xaetg;vb`>o0A4 zcd(x83?fFcvOy$_G^EMC>7Zw=^}X0Q{gX$@mY&~J!dJTMbnG|O8`ujQS>&%a$_uxK zp`|0=@yYsFW_BJPVylMklQG)bcAoUM5zApsw!a+tL72f#G5uP^k8&6G=biz*G$A$W zq%kSVGOPsMidBx^aSO=rc+CE0PLPlw6|) zoo*warAbn)M129{WLaM4>oYS7c{x2ok$aD$UM3wK3*X#xcY9O3 z_@Tp71LAF~G}TU8`LwPo;PuhkGru>$9_rjL580P^;ivp=a^fJ33d)!v8*JiZtHjGS zA!V_;>Mt+M^V`-=(Lt)!$sy$6qz5xjmw~v=XwqdCbmZimuE{7Sf)esN@cw9RHz@W@ zviE9XLHsNDY67~+zbjb7v2_baHd^Avx>EgJJF0#Sg)LvleQSLt94xlKlAvcftp2Km zp{FU1Swx7n3s^|*lm>`U*X8At3gbV0O%nNwBQZfS_`WoAlRn58kJQR4)o8$0@aK3^ zC;^<&_kopXLI=3bYYG$%7x-ToXUZ5$fzZ;?rqQtuvML1HWl`t-qQ#4tk5SKr1=bhw ztM%2)8O5g2M?)yv?DFQPwkx@%bo}Ah>Sy>1SNnNCS`Ti-^g|BdznAm8%?owo2*`TUVW7PAjwotrRGzaGa9rdB07<#4CN<-nU8Dl7yvl+Z3PcGiCrM7CvS~ zrI**!UW2y{;%CQQ3oqmA-Fnu4le9_80#l=DX6=I3$-N2{E?^QHv&YyD{*!e(SN~(q zpGonJGNkSgz@eZDlXi`&s#I1QY8#zm`j8(^pBfu!YYKoIC?$5g_+PV?Ald`13rdLdhEh{60Kn1L;Vl3@%lqw z$>C+bvQyh`{zQtJv1`?7hc6gsXA(wkA@065Zn!_-5=w8XTeNJMp-;G%WSpVDzhPXc zN6Q~G>U6i5)`Q@jdLsSHcALMYUcg0je?i)>d~_t{m8-_@NP)@qQi1GI3FtmxBr?Gi z&a><4cN|#9&N3F<6asSzTvN-XqPs|0vTXTYXpPIruz1oU#WBpm&-N38j?Hoqsq@Y< zL?+)*Rd-Ig3h9hDN(sP?lKIE0pVtO#(LV+)}M}8MCzRs;A?Y zoldhD+5`9`MTIy>pD&05TBdFpL)ngqey(pjFH6X87?#yF>E0mCDy>oMne|hBvM6}D zIT9nV@Cdg0N?;*AEszEhu4i-~R3R_ii%w~8mex0*a?Z}$dJ`j5R<=FhA`y9=&aIe5 zpEOC{%m#g25M6r=lxbIE2>g6TqKFS|JFRSX@|2tidsO;JjPswV&Q*aJ+U}(t#KnXJFB4RMA^>9-bNmvl2*py=5`d&eFIB`IpWiy|nfudZOjS0Xip z9G*zoiDVgqbbJKEV{KD*6d{hn!QBt^N$6H*rj7}#>SB7#ok>xNclnKSB` zDK~ab(}M0<(F%JOYFJv^^Ld$n3%9gF zd{{?TnB_3vqZu+*j2O%a*2jA|7aH^9g6_5WtymFn{k%3)C8H(aTo`eKZ{i*lq_Em; zlNt}{jU~BZzpc}_EMFIO6S_lV{?&_#^u+2%!`w2xWgfyk)~oMhPrafZ*0jM~wYAwc zQVA>2i?PTpxF-az@T9PkHn`6zt@$W3p8@i=&^=BF?xjdn_-NW{RoodAWmtl^7zH%i z`v>7Hz;%_4Mk2J6{Mz1;DZ$+Ii*jT_lw9*r>z-LV3%B+Hy~A)~bX1W2E3`21+XtZs zuEge^W#HFq+4JaI1; z*x#ySBRw8)8ukP$F~~e7Brp#1#gUW?`P78N_XbxcoRBwr96H2&h<%D$4HlNUmJ6&w z+dE84Zk@@_HIaq`2#c7>fq_T8LF!dM(_Cr#AfA-El0Jn22gQqr%7)4nG6Jm@XBBZS zn7Pr|`M;9sLE9s9GzFs(ceq`?juGCvxVggn0&7@4|Q$is`WYAI&@BJ9v#O z>*j)CQnm9Zl^sqmh#0~~_vV!soCv)c-+RkC0gi$xf+2V0vaL)Vjfs-H5~gmHlvSwR zY63G&F7hp}6{{SzB@ny5HMO!z84-j^4`8)4R--7HqQQ6;W1I`?t6j_mDKrn5+%DYV zFS4EPX?{~2zklgY#)enHtWYnFCEsrO{H3Jp_d)q!IE70xt0QH6=iW7OfQy_nlz*lt z|2hO)%WmGO3ippWoCpxqqCcZ{ru9Z=kE)eOw>H}TQ2vC zIl~(?sxp{Uk<%K`&HWx}2Ur1fZV96r-8y$@i>^2wAa=->e*0qz#Q+@xv;|BYx+z~T?j z=_`}PE8$SLcV&j$v^3_aaj7K|H*~m^avtw>#Yu zE56>vqJ_x`R-6f0kWZRr!HtBHYcp$0$lzh_&s(jgra*2R4IqbJnYB1OpW+?Mk|Ij^ z-Xc?(YK=|pjnCv$1am8;u50iV&DoM^D%ujf`kC_4Ha-bl{w;&t`@Hdv9o&o0rX7@SI>QkpX- z!HjBkviF~&{OX5%_0=hp3TemOTI4`Bmu9V%o9LV=&f&Ed=!`g;Gh^5#Fl3O9n_jfy z`c)MTPGK~9o?ej?-P5Ib-JQ)w{zzK8p`;$Ez|*ikoIh`)_%Z!*3t5E!eWO?%F1Q9X zE+a`Y+g}Nng0lXJ3wSPhHiuJmoloeE$@&~;8=SPew8aaubmVUR1zbK>DZM`*z0^;0 z#~`z}KX?dISzJ7D09KRe)PnS92y01+IE{6HoUZc z*bghBn#o%Ew)y9PgpykuQWeDQ?r}*A**HnMo^>U8T=JN!+eF5E^(ofTUT+^*|H_iO zKZMQ_ntjEvSMkw$?peL}&RK^sPy2Qt;z;~w6?Z_xX-DtIsA<%UbIY4Jcju=C_q_YV z7`_HT2fWwrz<~vw=1dQIaJ$VIgHzXgIZ5E>K96sWQr1yBoE;8XC2D>SYkNbrc;|qZo7PU?Cgd zx!m7v@MF5I=*$w7;?rv7Ez@RM(2g0_C@@#{XeZKpl4G4JzXhoUKHbI zZ-IK@{$Z96Pdmk)?C9X13S`fwP4Qk_c7}hpx3>REwIDATmLjvKBf0dgD&DH`oSD8w zVuNE@4gs+`LqNC%kn$cwd7zj%Z%V0lv z_S)G@8aTV$gd?T0vt0zICl(wpzim*-4Os7CvW}+Z)0B5+GDOIvyP4T&+_0Z0S{6;+ z4Cr~+@Mn_(Ig;tD+980=y!u+=k+zmm+ffI|>UfhS?>-2+Wye2^Iy)7(KKNCvBW1`| z0^IRrs|GJ1yfI^#Y}NyCzaT!#X?k+TBdNR+ zXPl~KFU<}F(1K*d2(x1yCxe~`h(zQ}!MNdC7lmXaEU#cB=5ZTPcOFMH-Kl{AWw`)> z4k%%v&(-?MJa0L$R_t0Dki>h2eTLE!Cka6PRFM#DLxmJ+CmKj1J z|5~*&tM8NSkRipjgIV0*p9%EhJPo{rS;EsXC)-#Rs97MWoze;MV9Gq~!qI&^ZW=c9 zG`^2Gu3wrolzwzNge}+a$KC*{Zc&#}&W@cQvYWaFO4vUK`8Ak+R}EO|D5++AVVty@ z?eePKDYUvp=mfx-yLzPcWEV`-*XrdVnZ4t4oU=x%307O6Wh4DKPPO;^ey%$#+PfPa zxuUrw0?&(vR(#6!7WS%6y@JEDRZ8?f%0Vk|@D>CV)X&_~R@N|BNCD2_o!u460Jf z{-O6JJ&Mzfm#T;RusKp&>e{lWufe84HD%>bwWV=O`%9L?MK!V~0=ZO(4ymOQ%FNu@ z)(~*#j(?@#r#luh`m|e{#$k%bA&8qbjv^p!iLW~67$GpY8&eJ7%dx#tUdBM*-kzUT zPz^OM{m1jZfs`4DdPfi4aehTEYOB+O^4oUDtK2j}uK>1DRB`kgBA;(~0|37v>@sEz zo&y6iKcrh-^kGe+9r&Z8{i#rG8vKZatag@ky%TP6bJE7VHDD!Z4L&(pET&(r4zU@8 z4-fNG3_ylgGjejEd53KapI-?C2y3#4p#~Q|F=aT#uh;5mpZ-E?$eU`HGKm7%xA|?w zdxlqF!eo@qPyWbAnt@l#RDzr@1NVp&#&fwe+O&~sHh(;#UqY+tFltYXs9tc30?I*W zd0PlhzNJUfzI%%4TV3_FDYrr{y&6n)x(AZN>_kYDDZu!AFGIA{2bPqns9SPIn;LwX zsPvU$HlS^i5!}oMHbB{uo4^+ZN+<{pC8JAO0~4GXL^-{9P0voU$ULqDqeR4(tt_Or z(nO^M#q1BOBwX*gEBfih8}sB2>Vz%#TFFAy!p(L9o$Wex-lz-5Fdu&-4__H!-*@*p zYhx|pjLj^4Bew98*}pC@M)jbh-HgS>lrYhI+_pL&d$UQ}ln974MQ+gLe(#C>HuulH zPgtFfLGXaXUt{i#&g7^78#g-E5ZAtE413yl3FNqpIO?DwZ{-?eK56?xmg? zTg{GF%U3@Ip0>^FQ4LNcCM}MulNHmRm zOEhy{6yC(2wosG`f{!G_^+;6OaQ!16jW$T{6UwX?3E4j0&`p2|WDN-Z#XiN+u3mGy z+1=dKIiPpKA~Vi@{D4kRHEkgfAHyse!jikAipx$58pxc*cQRP!R=8^H<&M^Vhf?$B z#I21bY7bPMt;$8X$ER1;xd0SzJIeG!**2Qp?njl*F4he}o7hy~Ap41IvC$i+o9Hz>f^nE5QB^WiGqu<(UrcN(_A?R93hB3HtQQM z4w;Jn+_7OvAN65x#+}2qFL0&x8YFJZi=d6JahK7(ZR)4?X>r^7ZqHYX>A^jt!|R`_O+fgqQN5k?rQ0Np4^8C zkS!S6jNPg|?1~u4dh+JTJAB0tGuHkx?mBn8%>I(rN9*Gk-r{TfX1WjcyTooc2en<= z`$+0kFJ%=HxV*S9Q*(@13#_%hMch&`JN*=6I?bZX_`e4^VWTLL%+JNs#EBjvq zW(&;+C&&Kr(Si$e@;e^3DHOZ_n!UH_qhfS2V@$-PSP-W@Z`Yx2lZHS|LvteN z2L?&Ck2i6xg1+8>r^y+w1vB|6WR4Z@N!V?asY=XQRMr(t}c|asnaGoTn z5NxnLSRmbN6wtEz#1y|SHBONnPf+-sBDmMhF}$LxSxnMJ3vace=cOeo-KtYpH3(& z+rB?+)`N~9%340UKIJ)Yh}vxrEuZ~pR0>3+7Ll!hjSx0oGMEC;VQ+#IO$MO98KPwF zRiPi=G=oD29EDoeGBT0IH8lftuam>9D`=f|pKMjm7-Wf6KwVEIP#+EDZ^5m@nX^sJ zw&Q0vK4pV_tq|o*34MYb(zC3R(y`3j?mhg2xm*{krdLEwTycsmirc?PW=oM04=D>M zp9_~RG!o~l<@X+BU1{XJrs$*U8L7nk6_D256?NNa7+}#>7Spzx-Tn>>oOcqbEGiw= zix`NQ)b(rd)1aDZto+_!TqAtg=@vK?JtkmbEl5M>%B^wuSA!BL$apvdXATeGiuRM}qd6 z43sx?8bHR>-%V9*S=>kdw6ytc-|q6LfQXuTO+1wser7q3a?zjQ2b%kVaFjQ8``(_v zQtnL!9E(D^t@(PBeH$J)_-KRO&)WCb$jnNVGHq(XGA_Y6VTgte)2cK)J;bS|)Bd^S zT6cLJLKR&)L0^+4j%y0k@juR2dPrlLp~E%&NGgFaP&EcX)f?UPvM7Tv&$$uPqskXu zeYoegrsJ4(_?uwUP+0~FXHVZ#rOYUnk3P#kq>Y=Jeu?qb9DaSeIg?X2EAQb>X}xm< zh?X7$+@?~n=Co)_=B}%V4p?D617@Ma2L)>}_Ep3asp%}w-6-hd+ouUp`lgvMbARK3 zo;w3s(*(?QkPw2C^MZ`b7IE&1(XY4P>2Ge%?Nl<~fH%8#wodi>cK8ejkJl`SzEPr# zb16;9jCcDwc}_?L*56@vcPbu$y7m<5hXYpA8HMp^k9cmQ3TGL+ZxmwmV?Gu=(3CkL zN4*SBRQ=}q7k-yI7SwuCm9u(`ywXp_;+K=EUWSALj~<#t$#Uz`>|~n`Fk$Q_@gQDZ zU_BwFy@4<2wQfe0F-xNM$ZBV7dTm$q4PMRwpihbOj0u{Q^h4abxpyhQu8?zSXE@V^ zJ_UTXiVcibjY;TrpWLrtB(GxB9CTu<43((1xX+>AjrF2g>!P!eb zm6fYZzG&i^Fu(tywz0w9kC0g_?c^Dne82q!CnO15-uisO7Y`3~P?=+)#-7VK>(Ld= z*WrgPCT?#CJ0-)rM+M`=G0S1vTnMLGk!H4UKxj>#WVXA~j?WV*ve~ATSk)(PZnGyJ z2440p29b%BZ+t(dtQ5N07&zZG0g&4UF*8|d+3k~z;K@=FE?7JqU*v|gm!^5dx!Ro= z%D$lnvDwOUbHQ2jqr6|**`^o`L0n*5@zcGeQq7!n*2U*-FP>hy0k<^vQQ6Y-_3*#E z5I59hvOS`#_!o0KoU1bMWZK8?TRW~VMMT?SvU2#%D^#}e<&kqIz;?_xlTKg797^Lo z`-4uVq&khkjMx+fBCDDwQX!)EBoP?!j#aKs9{ww4nYEpeaLfRRX}@Fu%%E-B1nN3C z_7EI|csXcGM@8)bO88{6d+aNXm~?!O+N5 z_Y;giHJ9#=KdczNL;$DsDPY$*WjZ%iTlCTw_Ii&N3)YhvseB5G>0uQzZ@62%25g3V zKb}3L!)vJ1cBa^n^##yMcRbS8rG9(+`28kQNw&k7w{5k*qX7$a6yB%w7rM%jNU?s8 zHzMGBoNSW`i$j@eQ9)GsgYJWOBaUvqnGODbLgt-lOr4{x9(XvMr#kxEFUc5&NSPBIPdPP2wb3w}gcvEp8*8af=D zq$NfC!ET!=`E<fz)$5;K!-}`qk|KBZ;e>3!-86{~h{#Q-(%7y>nzC4!{{$}Vu(IWoo;{KBs@qgor ze*EiiX7x8i|Hl*6k^4LI>i@Ez^?%9Y`fu0jZ!X{84E?>}^Gt{BziOhocm8gb|KC{n zf9DYX-?*Z>*Z!*}`v13*f6*uZ$>aV1x03(($oPM2EBVHW*-5@Y@N#}rcgS10RHz&E)t_uh{+i*@GooPGAQ_w)PzpRji-a@d%!F&{j5fUO`et^VM_!+{469&e&Q1J<-O z=T|*=5UQ^rEurZ-vy~cgjH@7X91OI_PVYa=!J&w;0xI&v*RYW(+ zhuf5&Jfg$>d%JQ;Hwygud9NE9xosa`!_PRS(W!%QkGyUvNk8LCqW=J40ACfq{ilo6 z2@Jpvl8~2@=>O#+6ByVb2tfzz`)_fTKWTmT{OpL~6xPBJWMQ+=3A5gi?~aLGZ+;SY zeo+5%=}up=#YCZULkYuM1-+;Aukh>T6GfV9VNYw$+Pn+yOY&faZq5{FRLpk)jEjIR(XS*& zsR?!|L#$yq-n3QJj>p?4I&=)@Q9)O*^^V6E`M-1-F)yc_UMlirhVuN8-^B_s`>rzr zpPWK33C?}A_%3TTnR><$w|a#x!Ab~kFWosItQ$*hbAP7$)e3fwKu-m(y*K=_4Bi)% zmkwHNhfN-X74aD07BpPcj#FFlCT4ahkG@xU@ze-ka+H`JD46qAL007tnS~Cl!oRU5 zfy5bsl1u}x@m82J;&v9zA9zwm7%napLcfDB@>8i1(s<8FFK9WHt=m+R-=zh;OJi2# z;R`}F?RXL1WMy&y4fg}25y!Akv&=N`O7YvjT@o1b6@aJD1Xlb8w8Z@J@$zq6tm%HE<*q-;m`81c zD0;#D-SWt$#Yw2mY!!4)M9l^mKytpAhlRP6*>Xmo(Z4+?NX`)RmAal^xo@C>$i#K=L|gee z#!kXT`j+GeLvN?+&EciSt=faxEwOfFN0$AzjeaNPr3Q@>&{XvCo2ro3LI2D*cOL~S zuVPD$s*Lv*8aPqb?ESR}!+PJ4z1iX06|$to=1XNoLQYQlh(z9z57!}^EQ&b$jZ|1x z|&x zL{ywTxVRd!Q)ddm>&fKFUhR$3nP4-q#xE@VS+Dt1U1PE0ZR?+SP7jG<-E!xG#P#2q z8^;1awaVF5CPm2+P^MNd@5#Hil)~h(ic#h26mw34l6ZoC6U|vjZ$smy2;FCML%46D zJ!y!C$*|>(56)!QS%qlwGq<$9?7Opi2;A(_`&y>(?vQuGjpVfWeQQoUzm5d6QI2H6 zw*va3r7sMj^>I0MWGlZ^mD9aMFzXbGAZ-)g{Z9v+EQivRjy9Ot3?gF>eCPF}z{0Bt zdtjvjOJW6wZItdv>%@V?XI~kMLaxCjcMc+-j3g2ADyjvzN{2jAlWl92@vFx5PyfDYG>3yU7!ip?-*e$8wq=0;E zDsF}TS08#ukCu&A1+AA0@teEmO`#ia?6I>nF0Z8X^rDj`a_eIKM(=_|1{ye&y?OJN zkkwHC>oFxOBaY!XI>KL69Y-1ogj#G%0TVyhPatZgNG%d-&z z=wz9>xLIB z&p-$G+Ap{L zYX>AxH(#AM3_Vi?z9=DBc*N_-1XjSie;mq6_OwbcBK@Weks!{ ze}T|CK9s_DTH}sW8CR^U?S2}}{UGi&^mFv(5roktE*|_eR$ViBy2KhU^=xx)QQ9N; zWcdNHF1h|$Y&vn)HmaeaJCg@)Y)JK}E}Dd}SAZg~81Aaf!wcasY3PCCbZS(UdQ3`O zzAARu$tFBB_)9jcgntz#f6~8{4s{74xXyn8@l^a4yQrKd-{@cr*K)U?H($B#iD(-9 zdmGllpcO?$tsapv?W`1Lme0!~qZ!BJLjjqoY}~!D(X!b@7N}-14;i%_-&sZ7Y5llm z(@j`i2o&73R9dJc*6Ci)bCEk&M@8e}gR;BPo zo25{FefjZ9NCWyWe_C%_&3%(uu4GOPnPhVjL0Dd+)fO~{`OIj;RnLRgRU}KqyzyKx=P9*&@TWKNW~v=4lAHs#)EKE_Wjkx$RIX#HIaW z0iRHE>5+`(@j+G3grNIDkciKA@$840q6<}8u=FSSx|v0R2brr5w5pMUNlg0MxdYQ0 z$!RM?90R2F<6QFnEA4lu2oY-`6M6HgUl;|sS+sh0z0@J@*^?W%QD#Qy<{$2=dKx}n zm+#E_>reg`Y42wP*N)Hsr05Ax!L3@+t*kKr3LZEAb7$Ef{E*{GJOxh<@;H`Z#us_{ zB|COKLmDz@wDFPbRW3A{A^NG>QDjdsGJY5m#@NbYs?P$GdCi&W{hgQr zObT_5Roi#?!kq{j;R|h7B4DRg-8&Z+sYk)hn<|XJv+pl)+BNdcz#)xE5+vjOgRHw5t6Qb)B03=2jiE19Dj2Z>+pd+r7fj zBgeX(+K1JuodyE*$%WnixXuqgz)$pUzEaeE+mIpVu02z?MMMI~2=IbcU5AsVAIYp7| z+Ih7UlWM}LrX_*A#v-o2JVQ21PvDuZf>7+kw*s#mk>i%%RkXaygWsN<_;K+*UMbQ? z*Q9bGd?X=~LJ@c){bQ)kWzd`L)t!{j(lsZSS|(hs#r5*dp0G`~Mbx(1W{v3@M`B5{ z#=cwV zHE5GrN@d&Fu7U;8x?OV%EfZ;@;hQ*dtVy&bZP?GL)=eM3>|#C4&}v8VB9t>?nn!CP zDxv{XA&&}foN#tVYe;o=Cs3HFIZV$3oH!<@!L-0^^8!2Xf@V7()3$5f2**5%dFiU( zWHM90y_zknUX%XA-F13Hz4z!B$TzF*)kBSwg}MqeW8qSKyC+SqOPGEXaYgmbF%GEP z0kl*oF8YcKmUUSY-WSI1%^w=8@bN2USG=uC92#cnuvZmrbXTkeD_K1Dse>oA6B0qy z?$hpiLXLhciA`L<)XSJe&>}vPf=`AK#aPzgeq+EGoprP1=kU|_Q0%mLssBNzx0A4N zBnb|wfbr8=9N=^CkpR$)+_T7p%M||EuMg*j$EoW>%TAHtbe*Z~b z?IXm${u;IO1{RE<1GXkuu^=KbeFtQrwA9z+!bbOLETn*K0JlQth~smVUAh?lLE3-M zrDPV`Ta(`pt(sIeyot8eCnjXBdqFKuxXyFU9S)pqogc+#OgDC=b@XAB&4>Kzw%)-) zde+pxVrnTqo5JuM7UnZF2xMU-ss7JVe)(||`%}L*-=`$e!!ge}YVLDLZ>oMVLp@pNA`Ox;T3(WE&`d zE@3EmK+Np$naSL_gRX_B%#Rq7%u|D|;m=9BcuY796RT^eSBYPB9bfp_zC^#^K-6}g zyqTw)ipa#F7Prl}Q|=}mtYcX9kslQ^is%_u`|Jq1l-^FljTijwL9nPraNV*t_ENGH z{(s_t|9j#OsiwXs3jYN{_yGz)cU)dT?mKpVZ5dxYi8u&gUur!kADp~22*7(-K`(S3 z)gA&%Odi=kf7}QF1bQL z({W|7r(-mEwKLx!b(t>YJh*q;($=!?tG)AweLO+8<46%V=Qv_D&I@{~pxAyUa3p`{ z6Kg})RGZ!j>CB28xZ~J`&}r0Iv7Ty^>rNK4%2AXO|JpvhT50R$TAq7UlRC@>%$x!b z1(iw6338zDnq1q4cVy`~QYJyF0br^_+xEGRUZOiQ|Jv>{p zHE@D%9bI7V+{O8rI>_P~L^7qw;YRIGZAJBs@v<^F%?S^~d2LQhuh}efXQs(O#HdC2 zs-<3Z-*>+XZ^|zWBP#J*h-zQj8gg~62kVIYGI7Nxg%A7Sc>+xMwCC%1`s(Sp@Da=U z31faLyON5a+iGpw5wTVC_+2smu7Ssx->v81LPShR#m)j$XQE=Js6v%F5$hsL&|T#W zl{f9K`4I%y1R5M=@T*}thN$3ar#f7&gpVHb>?%ke)ybY`xD}45(|A#AQWz)eh2}w7 zUB**%_YO#$szLT+h8l&~{X)~XgUnKwFGiizZDIcsvp@6$e6JPz*30MvvI4p7hUFFH zEhD*kA%O$P`b~e&=8LJIMz0%fBkdtOxLX8t9i}MX<(^63FFtoF#L4eC5684*Rhrc% z?_X3@uh)Kj$j`k&906b}|EEr-jyzFs>=Wx2Dr&vYe3V>{&WqKdnorX+Vi|Tv>gpB8 z!e3H@BSk5u1C*0t;r{FzjLJOfY;;-W3r*3BJORwcwF&*1f+o4*-2_kN1gqDQ%+aC) zsz7mRH!A^ps}qT{m*>CP8GF*3Ke46M$k)FJ6Z1OIR3zf0#qUyqlAHVtn3d1H1k+9N zYbH}4l^NG1MUrquo3Kfu=U?Qvg!^+!WO4Fjf*$U%9K-U%2!7HFUI&AS2lYw=F;B2+ z6l#ojzv2`R_gPmco|ZPQ4%RhO(zKPSH@xSW2>r~!!r7?cUp#tH;qM&`;-|io| zTDt6MbxC~eGt*4!MGdvK#ebJrRtMtN}+sYqxflT zn@`Nxmv{CVM*)>u0K_pBE;XeeH!6mYg1=SR<9Lf@qYgw?O`Q zgCXha;+}ldjI({mQ#hyseL?jZC5{0M2{XCMbAuc;O89hnZAM=TR5Cb3n~t5y`mke? z16U@9naL-(`(t(%tz+A~m5A7`%pe@R6~E7O{M1zR3o`QPz?yJv00_)L?HqlE6jlm8 zaH%6g-7azxg&e}o+g`DpZXH+ET))HvSdQaE$WXRK$^^^?e&R_yU{KN_Jjr2L&n_i_ z^Q6Xp6lb>jnH#CX=+^;yA&@NPmFGE0+{8DL#v`RzkB@2jTSvoS8DXDGky&rffkih~ z{_afMGXx!2`j5jPg??xdi=MLHMM^m=cWbeZb#k_SYi*mIuumrrPfB*01M?%^aFM@8 z8734l6w5>d_OAl)t<~$E=PumvCj@MMeTQG41}T}-#`t<+riGS$slBZ)BkDiinUIU{ z6D#0XRT$jy%2VvH5xNcefHz*C`!&H@KOX|d_&l{e?m+ce`55zaBCCcP#e&oAD*0zc z0(SO9)@xdTn+=B>%3n<+3gaLp{aKUC)wcK@5$fJ&n~WV13Gocr>C;azC>Ff(o=O$n z?oQf%r_lKENG1y8fr^j;SoG*4|7F%G)S49#bO+kZ@& zoDus)sJz=ac)J8U z6?A0L+ z-4Y-%|Cwx;_#h_8u?a@uKlzRF88Fx_YNeUBZt-X7mAhJAAY<&7eP?y@^$KNaGuPjI zpF~gPGyVLWQoy)3sidf{^LhFuc=mU9!aeC!vFF=?m&B+1^&L z(rRmDScQK@!*Koe8-Vy<_ycGcl?b8cJ~7=%FzEpl{iMQ-riMxyIU4Q?oK zut!3{ZBy7#vXaY9cqR*s2s|4B_i5pn1%S&!_jlRxrsW$DiwxwlLAq}a(ns$zF{fVp zjyXymuAO$cl;I3K@m1l)IjUt=f6ti#W?dT{$M5l_?+U^}rL%_BRz1ZYk7ue5E6dF} z2Ldm`6#+a}XGWv}{;2aEi*e6SgXP>3bU$MSPNIa*k8%DQLamq~2&n=&&P~XpOtz-o zzGn!0)Ecezq`+fBRFKaczZ0t8XQxF|jR%OQ#+HA?n>h8sBFbEb#gLRr0pNXmZvaeP zI3TUzx6nmI5E-79e)Tm4mC$XHc_(M^&Z1lQsSV>uI~$MAZegrmei8>}(R00anL7zD0SQC57j<;=~v)8+4f3>=Eh=rprBNszy_tnls79Jl_Sdnsa8iAT;WNwx7 ze4j@q?x~LRirTyJlFsKg#j9NDDrZm*_IN zCNtKxNPOaq8ZrMRrr&2Q4NCw9rfnB!tuH{(-Iu=ZEHn*~^{rW~M4aR80Gm?z6zo__ zL(cvn4_Ych;Jvp83+69(@GrS+mxv2;y2Qj9PTrjzZePSyTqS_;X-sUI_on^dX7pFh z-c_Czs_zOc03oO^pGQaiddc2y;}*l0T~zBhucgmx%lTqo1nM@}h87cN`dChB-{EndLE!?T*vO+;=Vz7O zY^{>adyJ*y5f?;@6RA<5TyiTSPXcS&VWNr-cbk%<>~6P6y;_xIf6+_c-C>%xJZ8>q z-oxsmm)_R0Yh4t20%$}Q?XgveYKZXGsNeVL#pp}^U##-k9)6qUT~LQLhFM#_lodOx zBrg#avbK&>>YdZFrp>D56ydsEj-f2?8RIWEWd=1>k)(opw8msgUxWtwR}?M{o3%6Z zt&{!N1%x)MXa*uDm!{NsMcw`=&s&|T_|-OfPyJGRNkWiV0-C23=h+{Tnc zFU)6>NS_jYe3DUTmo!DRtMATtH8bj-LDkN$2_RzTJ3a`jpZ9ZR<4fNHO*X88 zf^a-XXV_maQG1KlZWMt^&ML}y7MoW-MN)|UX3%Hr5?IgJ!m`*s!j`ROU+Z~H>^BIo zJQC9d;;RQ%%2_|FPx;hnjVV7^gxzEaoB!B7kDalfC`xo74s7>voK0bJ-x=C8#kY93 zxa1v~q)R1W*EYi#igz@7yXYW_L$?3fD8x-qhnC3D;(Y*B`Jn8tqzBXkGvlPa@fWPM zMHu@b=ygsvMAeXi34;7E``lf|=+qLk4piv%Fiz;DdTaW)LFyataM|Th@3`t0Cs;a% zQDodI^`Bk%occjq?-#I`9oZtDhbKYst(B1L#_YN~EK0e*dME+HJ>1H*w#T!BXMF*2x9_Jf zbLG$wGxDbo1g?7J*qs3XBqnyO?%Yn5chrriWxpKDp$ecqp$hPY)ldqRAu z-Ru-<%EF*4I|yjQ2Z(NcYU3}#%}P}%FzHfY2%u#rSmlUZn^?ClQw}8`akJbwSjEPJ zMtY2e1KOlBP?np&)VM46-#|=PgNrZ&tMUZrrI}A>g#wOp3Mk_!g2`|@sExutbei>^ zr#9J*%vrv)Um%Smp`mP{F!u<35?4Krl^ptn@LP<}vZrkV9tTa|c_^{-e&=IC#PGvh zGUy9a(?&u;NYkja4DK_F)uZ77(`*xIIO68(&e!L^s&FRH$Sj^%Mv3YpYAGkNk1;0* z*2IM49@OfI(^~mJ^wmoJnuz1l3o^gSyR2-pzut->=jvH%z`dcFPkT=T&)Yb-9SFG; zlSdY~XH}TAo1bH9ay%&RYr6ookVmA4XzSgW>s(E%e3~vAxy@`+rbXY^-JS-Uyhs9? z1;(8(ba=Z|yb#I((@4Chyxw^DWcJ_I2l_lTAJr6c$~#@1#=Q*p$4$&xspotUSQ~3B z4T=mTB|!ew?RuV_Vg+G{avh}^$TT?JcCJ-Zhm`BJO3Z%jSRU_*#$YzRqM?asp4>;C zW{ICDo`~eas#Fx8n&5JuUb!HoC0^ZQr;Dv^L`p?-oR2IndBQEfl`0F^b(Janw`b0c zcOP;2Ee&LuOZt-&G(kN)&4HIY$n($Rm-*FJ)6c+C`a{`b?kcoutbXBDZDR9iALL?a zD5Wim9^A}btL`AF@Y!?UJ&UWM&pP;o&YmGL0z5D)aAtBjqBeTS?6$N+2#q)Ym<<|Y z#>{%Dg2kk7^C4%Gb)#Hxfr$G}FCbTOZy+;er$TGLL5bD31fs#V_3HOlg007hAL9BM zai1Mq{Zy=8=ZD$63vGKniVC18()z|U{QDA^(*XmS7N6WHWxZBk4v%bm5au)!zTmR# zGwtJ$oXowhreri9OS^z+;LRO6 zxp{_TG^!&+lkIeOE({BYuru^e*^NXm5oBxWi|p(S=1OdCP4_h}*E$2M?s7 zqi&tvY2&$U0C|kx#_gf;yMc*)L`Tn4S_YCe(NZJy=(t+_WN4>ika~r=xhSZV4V5f7)AIm zbDvwofv{x}C`mQ5g_<>*ti3$l~^b>@$1LK*5%I&#KNpW^pBYi9Gl?=N!3g`&(S zCtx^Riv7*)pqD?L2?6~x<>cKO#ug1sz;T@>>KRuiO+G2(`$*hbV+Xpxuq$m%ALJ4F zbhUqT+Y|pCQWg&`)5X`LQUqu}Onvc3)hXt^>^R8WXWBns6dQmE>;k?$3B^im(zdL4 zT9EL*>FZ4bzI1Z4*bZ$$+IP&UR!qXU{k57&vZSR>JORcO?;8KL0t@r157wcGTbPr1 zaG4eSP>hL5Qb-u$W9sX+vEJ?n2%|7T;};>v7ud9Vnmzh@&sZ-zuBL~K>PqreDMkCD zLjgXeQ7?)ghV7TWw52~6dZX2U^<11n`Jn=2yZ!7@9P2s`%?T#Sog(Gn80KeS+Fl=5 z!2UYIuOe75^x3FJQqcrB)HB(1OE*N&TNQS^{)nmQxBZB}rU6a$7XiGB^8k4R{Vk)s z_W={x0|3YeLOSNC`Wx&g)mQqTmyPQIz*_rv)2AC@Hdy=An0Mny#&6A=e3%Ds#d_=B20nG5~R0)K6202&NiFznf%H%lo)16Mpwa z)C3}-*066IrD-DVZ=xooyc6SHoS0Ay#<;+vDrxu3O0Nl7P+;WQC8Y*BIElb%*q?C@VL{?~~2-bVKS0E_Q!WdDmx?7v#< zuAm*LKiuz_k89%RxU|qKFC0pcegQa*{adF>GWCs|d^%BPon>v*KnYo}%2SmaG#t4PmQ z_dN0}-{>a?%kL&$(!O&wjnxHg#xRTsCI8!N+STm~Qtt0(?}{~Y<5~+Ghkh^buD3zy z?nj37TOZCExr|zwXrGDsZ^UZZloh_DXDSrNXUV_qJ&?#}&;@3I10dWMW37$@D5%aH z#D?KiB2YJ}@-#blKu-7lVZG>>RCd~cUBi;v+{kZnX_Yf_<0hE9IK#%(p`&eG|-W>_`1c`q9vyxtbk@@hX! z2u<^4i=K=@$%H<7R&PX8iu@QNpY{N?&6JrnvZya))DMv(ldxx9O|paY~U?8!Yx z86*!U5(d#XyNr{UrPT#2tU1Igx~&i6bK4Jd>G7=v8j7uRaDJ1{c~O2@>`FaIYe1Rb z3>r~05%E*tcm+UJ=26C>gg;Ls0Qawv7bDf1xu+Jz|{h|LRlio*E_KrlWOw(w2z{{mYAeB1cF45>(xmE!X z%ls7~IWBslsOoTsyfA{?FD3%Yg40#oP^sTBexHX+ml)%tJRDL2z}b{zRys{!xvIRd zXo0O(DbEWOQM^^uEqFpMm$t*RIotN!GjfQ@*-dnd%^INefb+--5elinca>LP%4$7gRjXl@T4EpU%UfCyQr zPh9SA5n{z8pNoTR(2?=*4XKf0bUzShOfP%8grGMjW#4Y2|?d?ufNz0Ed?^DmS1zzDa;VXZO1?D%hy{WTK6RF{q8Td z`nxQIxpf0@9$K7#j}vm3t~GT@6}2yaauwvOo_6l@VaRlAAwQXH%8yA4k|pY7JSjoc zwsd7GCMDW@nv`!vDHt4rtEfS-h*^p8=HR015w`dl6Ung624gV9$@$E@IWWfq8kkd8 zQ{hVtk$kfGpim{{wyJ4TzTD_14q!TaNW$+N*juAIbbD`IB;oVSlR|6oyjAyu#We3T z(hjtWqRH=NT-=*bLN31X-WK13-<8;f8=@q3!+gZfyx-dq1`@U1?372ss-$J8hy-}TG1E7~lhZC?k&}>7h zJr~qPhn}JczE0T1Ja20jPG7q9fL5ZCM>lC}LEM!Nr zoXktm9(W6Q;@S)-O8&lK9QD7r^wJq+8)&_A@Q@#&m_L&_Vm*v`5o|VPpqMsu|m{QhwNEGJyKi_-q_VlMR}kVb>P{0exkx9>5QN zACCT{-e#-Gb$0!GYnxkoA90yVy*=;!xR@K&_kB`DP^8k+G2hgvhy;-e;SH5$JbXnj*et*;rV7rj-3 z%zcMc6FAtIr#08_jktI%4>Ql!=7w8ktI_vIM*zRHO3|f8BL{@$pAN!Tk6rKPxPlwx zg;mv-XL<|dO~i7{*Sbd>Z%@c|^}PwG@ElQ}84;fMKkq<1ak1_nofP*tR1bsN&PxEE zNwaS*-ouQvdDHt}K*Z&W_4dYaPmS5#)wofiMZ9M z(on#Sc_8?0N(I)4nIT~|ZTIWANR`iyhvcc~o z$d?#DVu;xv5uvw{F4LIt%5+~Q33^SWH43l534sarh3-bJ3TdI=hvR5&LeFIKH*>s{ z5GTM}nUs>1$>S$+nQuLNGHmal&~R3d;u4%{+l}!)wdzJqBIVni8M5m1Y z%wg`{LM@zEM>+2{QT3;nuw=VKMcq3Mi@g6rIr2mD<^^QB1ssl~VYlN1+v0E{Nljy2#y%oCqDLoNVsDHvKYv7_8Bh-&qPB#1pP9$7r`IJ>82Q*PCHl4dbxLPm91$ew zjjoqa?Jv4ki*9Uf=W7JZVw0k#&!%Da6$Ae$^`Q#DlFB z?0b6v=0w*=Ieq->dAG{y@Zllh`h1Z_&i&Rn$ zMIJ;>je)@vMnWd!y79QdCU`GEng}ewxbdQ~_veQ$oLBq~^?MXSC3D9!@Vv7u znS|s%R|KGL#>TaEZcdpYCR;+inWdgRA9Z3!@=iSq7df~qFY{^DPuq;;YNQ~{iuIIT zl-)m?wl#re+LLrRViPXz&`#K8Bb8}LF(8ephh)jM&Y}m>0qjv_Tc9< z6L#yIm_8J9&d$2TBb$G3rQB@ror8p)?TDv1t!x1-;lIf@M@uooogjyGq=uP3_b zoJf9$&y#5aDZj8rZ{&L0Bv|UbC5gRY2mXy#8$Jk3Diy0)8A)k_sod-QpR`|tz1)?-YF0YorLGcJE z9yTvzip}V!6}fr>gJk=Vw?qce*OKKtChOJD^J= z{qyO(BVjpH$n)Rdb}9j3m_89gx^FR2i4VQ%T`v5sekB8#D^M_j#plE6w14ZEC=~az zpY_>!HhLjn%>!N@o@2i^6tlhP_=q<4gExBO?8{Q02T$+Zktszd>1oCD$;ou^PZ=Wq zB`TJt{ZLHO$6lRWr0&L01tQ`)hjUnj07 z>%{v!RN);mJtg!%c~2sQUsV|aze-$5xm$_w$4y0QprU&{(EXxG=)4R91hB%HkXTBZfuL^UWh5RGhYH2fW_iRM{6cqc{v%N8W_BRz=h-EbCH9e6RmTLdW0|8V z0(Cvh_SeD~FCyc+&gKCZPY6rj28Y=472CT452;Y!cwhRi}Kb%L# zv~Qi_1IcM3VDgzx1a`xV!C4sr|d!TFia5*AH*5 zVXRRG-Wak9vxUu!^0yVZd>qtp=2uw~wIukx9{>ksHpjCAtVvF+4ST#JA5{e>uXE*= z`I~79;2;9|o(Az#v$mH<>0i<*1m^vFp;QU$h3mGaY&`wq@z_e;jw-&=fhvL#JOTFy zC_kLLKr5bZ_~@^IelaiM_qZU_w3To63sA20KyRUSJI{oOkBu76pW?mliVRz*Pui7W z8qj1}cQEq$)qIC5nGoWUwcP@SJ_Y(JHF$7<=AM$>*VF-?(|I?1q`YXJR)3bpr);biVE1CV6~oLB@Kw$b1w26nXvd}6k|+A675xt zf~LIkS3X>Si;ysN+6U&eW`VZOup5zPV|(s`?Sx_Gfv+E31s&YOwM8rFrB$q z)SF+`fL+}lnIY(}B6%&jhDhzJ_MUz%=>E4s+WO;VDsa=Wi|^8Yw^Aco-Q)lROR0Lp zTdtls_WIQ!1NqZkk@r3WjrhcQ(j$m?RypHh%^xMJAS3~^rXfd0lLh{I0>Ux zz1=5!>OZ}W&>&`|b>P0!_%)zXYuYYXH!i;E7MQG^oG|Yqe~Eo@yTK~t@F)8w>Z@2U z?E3eh-R+X0T%%swuP3|k`)Z%jyKG0@`mmsj5LBBY5DAzDhLmbF538#!7OE{)`Ic+) z!p_Kqoma+AyxxTO$%Qip(SxczD z<>_6t#X!82_fNv2H6=FEjK@Ke9rT<==>lV<%)f9rzzIZD`=|EYl#9ke)E4)HH$`nz*^48nc zrO_k4rA%O67zVfg-V*Lt#;lzucIeyTtVPEypvVxDT@rJ)fB7m?5_*a-gxh=(Gk(8H zW=pZ+37hkce3Pk6@cLu`A<}f0Voy%Ys3Y2mxyDlXx&weHz-J~8L$6V=nKAdn(n~%_ zld#Wb6EJlibKQf34VXh;Rr#VW%bLKEsvK$c(v!tnuxQN+eU0ppgf}m0d^WN!r(Rnt z7vrLjSOe5gYi7J;ESCE5*Pb6RIWaxDV|qlpKtUZ&o^g&Y1WZlK-v5H_U!Q?{DWi(N zMU)#rHNC*2(WJb;KO>19i>_ztw!g2$0KUBm$*lBbM?v`-gF=eS^Cj{PRqY|O`7b^= zN^s-Hz`LU^6g^WQBMhD_DEw!ZSVe<;)d2eFeaYVoi0rE{Q{^g~{Oy;Nsj+sw=(pZY zp2$<6_TqyN4q?S9B16DETuiKbzeq}^90uaj_FlW@opnF-1ty)YnSd{~prImEmU?1T zZfGj$M<72vgrZ7mh$ct&*bpdBDX_-)XF3-{wEN&}^2kqSzp<)57k@FVx$}8Gq&DSQ z{cn`tI^%Tp>br+tO`coFF>4Xp+qS*Q#d%vpqzp&W)bDm*pMbmpXYN?hm~k5I*GR_G{jF(-Odmx<>E4yJSl6Y% z4p&h-{1Mvl7{DR1Z%`tw$Nb?GW7MkBKPte+srNnx0)Y~n<~L!44x}~(fnwUz19(9Y znyf&cSPJc+55PageAty1n&WcPmA7j2)F(6b3wnI9Y#lo2e?~<*|2x@5$1oQh!0vfRE#SX z@Z+Vu-6tnRxgk3QfIZsp>!LyOeH=1*n+7LM0#U!eACvBH^M8~{qh?ci{IbT%#@{*^ zHuaoR{Vx&x|3{R+cfZ?9+*e)SOCbMhum45=f3EodSb5*W?n4eBz}&wy_uvv(h5%|T zaP0k1?L~U00+b=wYd_L~fO$1>%U{X@(ZKAyelF;Rcfzd!GFJXjdW__K3;6fw>BAF~9$jE6 zWoO;AdI|%!?0CRZn&)*B`b(bK@Ow&sdfgkTXxsHMLG?EQc%rSxLNTa;RtjOyA`%eIim-G5Jc2elg zLFbol4X}Koy^8KlI2^SYX`#1jcIa6c1(VxP3<{&aWwlNkq3Kq`rh{cr; z^`2Ui9EB|>%X|yNg6#btEVR-HS@jh3ciuOv3KC~vU3@X z^up=O68Sjd^5EgJDAaMG7Ursk{Q|hJbHQ_^PfIDBW$}_RlATh-`V0$&Ek4wA7vFo9Wp@s$E$G%Lh zG)4j@JHbVP33)#-cC_SDoyIhF6g%|D=A*1dLKh@rh2{3RqGY_LIs z1i&Dj(mE$k_HOZI;~#gxOyExq5nA+No32?$;IKfPu*GzV%6+ zC4q$RLtX;AkpvK~Szyg)-^xhqkuLYh>M)manr7wHUPy6VaXeo?@in5ayH%r^;~$pi zwTmp%K#t@mgB%eEs2?ZozT(8fS z$Bswnd$dz>Hu{Lc_z8D8X@!WdrPjF)MdgcGsEm4H-9 z2B4Fq*&!Qz`L~O5gDZfk(DYpthgIp1F?~fG9iPIL2@CHkhlt}NrLnJ6ogb+xm0fJHz z0))^5gupD`GT+R9-uY&}V|HfaWpfdZtlX@-T)*o)&xGKsJu}XE)arDF=V`xlYKToc z-O9W~EL2M$Xrai9hqVF5++4tzm)tGwte2U~^T<=^AAjXLCjMEfA*MI|57o{^6Nm0J z>^GJldTX-|<+6QfPEWW)n`wSQOqq@zN@EdMY-uRRrT<#V_p~SknjGM{YsIT`f1qml z)ufCSZ1f#%Dd!#K*~9S;;iO(+oR|rs>`Z!;{7qMVIm5H>uWbAU`$HF9mal&u*R8D~ zad$j1&NqDe;;ppQtcO!0GRg5Fwi<+>|7rlm62%RF2z=70zL>}CV^RF@ZmDU$Qwau; zERGV(iY_j*OFY9o7DbtncQf5V|zj^4J#HUc_$sBqR~G50Y%!_kOkkWmlR!A$a#o zLM5mfE>GAUcojTjNMFfz!`Z@)(Vw3a#8k{xJfn5IVWoS)Y99wkTp-V2O@lS_k$#zD z8LisF&-4&Y8>93H`Eqi^?gyQOm1a*&C%EgZA!QZfxy%m}dnukrKv_R6_tt^1He@WuU zS@lS;s`d+CB2EQ+azHm=sp~oG0w4l2&@H&3_iL=phMzmCPX~Q7Sy<6bIh_16z^W%P z3MiHUQ*mtpDqtMGo-3?F`n2HTP-2o>Vf)~LgQ|hSjuhO91 zO<1FoC3_P6UcES4H%4=bo4%5C%A}kpgk?t+SuSwOt_ywGO9Xq=pWI<3o|^3Z@Te#} zZAyB!!?_P9rwdDybv%NmNRve!1;AG&Ea2GTyy)RldINvE?KEvX;?%3GB&G(t!pi3M zn!IR;*HBr#lVFlXHSR{c^}QGV`Qpdv4SdoDtakqX7iodKgD$17emE4U1=gq}R!ih% z2)@ge7JQ)P5U&M@0CN;|>wqR-u91a*=IQ9=@j`d(Bg^~=R206=<}!~EIZATuOWyfA zJ1aOVjd#X3bLIgt^$jlU6HA8o+k-62AKoZ*Bbpf>zd?MEk@0x5xOH3=6*iqvH#ii2 zd-UlMzj}DnV*BAA6)mne4i{_FqU8nR=FS=D?A=fHm}{^TMCE;W5L7X3^3_@)XuJ07 zdFzWGuCqK>=InveXHmwnUP(I{Dq@m?WtPP-&GhT6D}x1}S0%&;G%s?H*Ay8pqRsA_ zSr}~s_7e;mW#X<(dnf%l;fEzA?qWjVWw{&`W$K;dhF+JwRT3Yj6ZY{QyWEaLV|{sa z4H;YO(gCcYFh=yOWpqNd-i$Mp1K)g%01gr`ahDSPZl_eYvq8^dT?O)8#5&9^N0Gj1 zOoS5KmyT1vT5|Mbr4s=az;bXa?ZpLG%SwooqCUenpv}s8UP3n0#xkG7{mdz!I!Vtr z4taU}zcq}{y5h9{x_%@;SichcQ+oVk1~@rv75yw`_57oL-nM@IT$%M*U4nuTpx(U| zf7=HtnH5t_8XlO=Y1Wlaj=!rm@oWKb#91?flN!3fchZ2`Wo~qTOaedXC+m*mUY8?( zDB@wi0QBK+Pd)OdE4W(TfyRAs_;q?NOCEW>2k>m{Y2+z}(+bY6R#j0z2?>_V;+TSDn=9qZYO*Jj^8#R%$o z6n2B^Wn?nyaz+(-jSmD>-0cKPNWUdNgYM|n@d@g2g9IzqnCD8a_jQb0kU0Z*E_}ML z1C!T$d@(t{m+*n}4KtU#;0St5jJ0{r`qGkIXW+?*%_yLi<`V%JW_}5vE<7|5vFozu zN~D13*+5hh`wmu0J6TTEJ4M=uiJ7dh^6Kv6aG`*izKqg11ntW4(@wdqm-myuCZ91y zDb6QT$>*Vxl5X%dKx;fwErIx_OT@ht;c|0DWLzJ80Jjc#v4IS|B6vss4}#`B&CVtG>)xUQ$2Uo_}r7$J7z7j z&O}!xnMaR8C)v~ZKlOSi@R9szB*i=0tjYOB_v*b{E2E`T#jLYEqnp1r+)*Y7o{-Cw z_h7#xeAcn^m-(L@`%0FS`mlj5u%)$QxZyu3cg+jn<%Rkc4K92{Hn^BN4)h4N8}cQ%RE<;VZjg)%cl2aCaIB<(czpFikO}{ z(>Uny722uh=dGct>fWx&!uas5H6L8GV8DJQnAxKc8NG2=@}X>+MD#$T54`}ErT!ae zI(3dq;@JH3$wCNU;SG^IdQ3*kXP1M=Re0<1ba>dn7gcQ?V&Ny){~M9S?_b zML)`YS5W@wlkxK$+&19w@5K})<&B8*p?_E;f5t}MksLQLuESCIo+0;D1KCyXu$|0) z%QlC+?bQ;npkMRFKZY6#na{d=GjWm2)8!>Mw-@X=u*W1$$`CeBe*G&IfGJxJJBs$a z@EF4ddQCJ?`7G9GOFQVuj0@{mX!o&X7iofv+p=cNH?6nHRZi)D5{3%8qkOzAka}Ut zVf9fLABB64{d2vt=jrJ-t)$!Hh?T^>_6jv74$k8tZVHhV^wB??q`h;s07=`$C$c?P8J-0FTI|KWe<3_8IJ+&b+4S*D4MUSBzlfs` z<3kBE@8kMdN&#>h4O#X6#h2XCGZdIN`^TutRNNvT`^i$;Ge5agmCx(#zvYS)nBUtf zr7NBhu=`ws@&MuWfO=etH4!Mc|BqPqKMM0py2s@9UyAW0iZH5a?MyRdg!H-5R_OhK zs?qZdFVIJR%F|Z>>joM@D!3_Kf`RbPwm6w}?=05JKFwXD( zBY@=J%>w-L?tg(aI6g2dHH8ddRPU3?b-y6u!MdinBj(P^kl(Ax|9`7|J;pKrAxHh6DPMsd|39U%$F$~uo|^c- zE`ptx`$7P6K7LnX+JRsK(mFyn7|qOmtjH~%qxBnj*))r-vz&Le1htDkwWLp)0)E)T zKIt{g*}mC=Ms+1n8QNdu#u(5DWKY3!nPH53kJ;Th2t&)P!Jy6zchuUK2RwD zlvnGgwT_jmzo(kQbPxB1N-yLBC7-K~!7++@1ay3UEsx8=ZJ2F=T6wpk!;V831sIC8 zfuMpsXr|@{@rW)4xU%zb@YYALA?4XPuJvKC}L`>?`hv1}zeXKO_mwS{r{-J_AI)un7s++ZG1uLuUco zvu!s*XXhZcD3xH&H;I1-@T8nkV0*O6mHWPnX*bT^r@UPFG;-{iKjt>au?$;eg?3@Rs2TvHws3!{ zu?>Q10Rc~U*7wSq^Plwk)*PF<*`7R_XXF66?cH+8O#3U39&a8ds~o@3fuoBj-mO zy8wZ8PdZHv^52iq0h!nrSwsF8+&rA3R(jG=qD)p_mia0sU5qQEHA_>ZZ*fcrL*TqN zAtc#IX5Qk&L@o}?Woy;Yk*K!3yOJz!RR&Qyd_&G2yFGPYXfKF#%ueuzwKZRhcEt)f z-?uAVTBN=Lrr5B2JgI-Nzs{CE!ZmQMAE)` z^nlaU@G?`CZqKSn4G^IztrGLnlv5`E5kEt@+T_5eG3lf}H8UZ5=Jx_s#mMT~^WL$o}g@3?C_*c!dnhzJQj_iMi zw}k|bU}Qobu>CeoISKja4kTn}?B&@Fwo%Ck>J9;*xb3d@UC^44z?lJh%L+2U>cx*0 z4bF*7CGbP|yR`@=ZgC4Ba`V!XEXH~lQ0*CgA#~Rt<=@s@VHC7bPDl*?I4fx*57CDA zbe*X1_~eUUK?7%`Pa1W{UC=PAl56xjHXlp=o3yorO*ClX447DwToRt-bdx8y?)I~) zG;zB|-***bITr4axW#oNOdMn4*dyLNh2>GNZpZms8Q2H^turw%U`T1q` z{LaavN!Qrnry8+2-vHY1`z7EDTVHYY9T+DH}p!>J0t9D*CjjwU`zbShwEl z_$Pk2IqmkTpZi5?^~l~+UK~;Wk`1;0kw@Fc?iJC_tO}$IOdYJ*DLiz$HuupqpPYz` z_`cgt#4j5yKUjf>Wc}y9{qI{iaN7INsG_|i@^myZ1&ar*f##{oLy>aKPwu?mg$%_ zs*3FxZrKkqW;qtXsCVKr>;%8`$pf9(! z%~zTX&<9Ca84@AF2$?&XkZv%k6pcKbmYYRb*e#4fgH}2(9a@6K=n9Z~e>>_t{sx)w zc*Vw6K1tjTe)i7R;2#w^c^m)3F4u`1$|wm;VE2^7bu|rnkAdNQNcx=_;)s3a#7@hbL(W;%R&Sy@g;Cnu@YK(oS7jH#7J`XsSM8NFiA_KQ5bC-{E3 z1x<|&|3w9F4B-`Tj9*pTOghbMK)k9oP*3;j95nMi$71r=+g?r(l_u-=nGq$Y+4lAD za17=5s-o9jBU$xD`(WK#p9a$1rMqE(DmB z-r|G8+UzzQ%d_l^coh%Y7EtgV;T&+8QBlmZID> zwiW2~#7)}Xe(c%O_hufon9D&n+0DD9H(i%Pu^Vetxh|yWM9{kBIt&Ziy}_JsX41u& zLb%{ZwW0o|$=ZTctx)9k!&W0q|Lh6D2=3B|+p)w9yE&;&XSy$8%5ui+K)k^fakL9T zzyUa7qT~`qWrvf~E8S+yd}zbkSW~>Sc4qQnU5;;-sRz7EX4U8ds!64Q)0g&DwRM#A zCe1R6x;sR}#**vG1_t8-lPCR$c%ip({x^ZKFfAqzR8U{DjH^GAQp^q~BSM!Z=tCtm zgv=88fZOW8ak-8>s+5OK&-j`kzqRO=&5=2A6D6*z1sD_eL#lJ6Z0KehE zW(N7|w9@?&>rg3^`I&*c&J~|@PVyXqHmV#k@nh4U2IR>r2bSyB=lEWk?|)l7ST4P| z+Efq0y)3lBf)3kWGJP5y!`JD!Xf9@Ac8BXA>%4Q@N43;L*FV1nM&=S+GrSXf)QMB` zdDxOiZo_@KSe8icLN0-z_Sr2 z5MFV#8v3+Ce_4rPaj#v-6+L}qqwGYhn;umv$Ylb_e%~a#KcJ}^sH7e?W4%a~;S+ab z$Kbm=p?g7+k=!&vDBq;Kb+FfF$v2N6t4Ks3D@#md_J}~Oo|aWAIK2as z_fCY7*OJ~bNvIN;_BOj4M8dNeP%hy%6oiuZ=W+CU7>_r_m|V|`Xw=%LW*uoOQFbd4 zpxn$hrNEye*%LKgGNjjZrV=--b4G~i2qN4wo5Qma=tgNwPD^yPg1Fz0ImS4W=d4f%{+MC~eE+n>+fkzl{}x5iG9@M~^^1#IlhJUu2wUkE=mWrGb_ zO#60}Hw$sCt$gE#vc?i)|J-FpwJHVl@fiC@usojAOhog90)~;&w{oZMDAwHK1B)Cf zgAZuUpW}WvRG_K-pCKDnOYgd9F-!LQ=lIx9`aP*1jH5!ve`w_i-^xlkEvVSafLk=r z-3J6d)c78Rq^}24%&xVe#fr6mQh7y-s+q1&=%f`cUrcUBST7{h7rJVon(>iO&Ubvd zKEg>^m`S+86Ku17m~u6FFbDDUwkoloRO?26Qtj!GCH?3*>Fq+Zn}`3_I@qpzQ=_M? zDmZK07A1j@78B2s+tEPmETme0^-k>49-2UI5?c5Cx{35{{^w8jkw^4UpL^03OWVBS zOrK_7|EWzh2=ve(V(2C@fy^zyY9DNh_1j#0=mD4i6WuO?s!afIqYj!} z3=A{HMR>;!-s)m_yWZ%zo)I9tpL?*dBxLrWrO!sy@#Dr5-s!9aKJ~$x`$H!)VPu@SJ^z&eL z_JPn282F!4KtDI~e{^j6A?e*pB)AeKMG-%wU3iwu(Z$R3{qG8m4`$d=jew_iE=;RO zk8lpP<$hIPK+eW0`Gau+oeT|uje$jh@LRmACTuXtspg)FP-5=(22K6>VJI5n=WKET z1v|-+IfvKSemX`}B1CEKNiKA}1Y`%fmM)qv8@5v>3nZNSi)H6If7tGqIhv|E`A%dw zo#`-2RI%}t@tL|tjlEFvX)rFkZx;f7$sr1B^+$+8jE#}?kOE@XDxA;gLcE=sjDhIS zTq~3DPU3WE;!&QccxwsnH=6yQwlqBNrhYxqJ@Bwgf3Qr!a%k>Nq@PLC^95@!S9t{y`Ym2)jmTw3S~4>;(&D{ISi`Q(0rxrU zp=*7SVaTjnex7iTx#7kjWXWLMgE)b)4HNtjl*Vq)0ZI&?n;xzl_gWzH?}Q>u&$xhH zx_><0bBeSEAY-X7r{byX}81eQqNWYHp+;JqbAl$Ezz zdgSkv(KId*I5tk8MSfP|(!@x^- zS-`9qbmN23CF$g&!;c|>;d%HE4>6pS&;7UKFs zH7ZTs3nP-v(Wo&QFA&ail>rr^=C@MhL!xLz8bSlUECxF=R9uBsmw`-GHD%18FiqoE zz}ohKi$?Z)&}OiAZ?3Y?_=(N(;9ZmNhL=*A!I}#eyfC$MZC_+*%h$jzbqCdkJgEeR znP5*3=o8mB2Ej|!c<`J46$4ef{I^hq^UG`wwwGrDi|52mepU|_ZgIF(|rIh=7r;D|zIJX1PHKVSRkL$3&OdvZRL74l`#f;!qGtPpm zll2+jJDC8FzVE69`#e@Gr#4n-Yq3AK_AY2)z8O#7@Em&6^XQ8bWii9-MoMK?d~-q3 zW5?|m7a0NRWD&z>AuQP0rb$1Wqws2{PG6B}#8$Wf^SkafXRwQAeGstsZHQpZ=``G$ zA-E`@aMtgV7@kte7^pgM@(P=7zrALA=c7z>88GI&-UclZ&6aHR-mKr<T3sY^Bhpb(#Vx8Zgq|2PyG4P8U_!@$S1% zsvIQcVw#<^;A(PAFLrQ_@GZ<{M}WiNpqf;~9|!rWhMBII4)eVGkOh%}#kDyi6n(MC z^iuan@R_M<4d-i_M*SA`&DEYGh7f=5A;Zk4%Cdwn6}J`3%(M8=GLJvbC9OA)iof-C)8+cFyXm8d}NXWSt18%k<@_)?m~ zVMiDf;R)_^oTaB!0{de^$?}?D)9l`y^6y?z2^;g$g*(hI?aUF0YOz&uOp5v)w<}FI z2ki~ZyAYPf`}qnBXB0wB~>YsTC%^-DQ??*Qx@5cO}Bv= z=oas_7WL156V}&`A+r|3JKJu}5#KPu0p-#R|;Hw?}!d{f-sdbk&rUu&~_Yo)i^X zHZQwMjeQL*>zOk-_Zo;N2KAv?F9}6{PhsYFv?4Z4w&{+Xz3*T`iS9b7-XFcx`Rz?4 zVZR1Z^lT}e^=^Q?;mF9?c`aw2OuIcckI}K03$t(W+hP@WE=lw(j|Cuuk7Q=7psOLg zMicc8;l0WDr3M33Pbb}7ytAYkHTQiaY#`VgLK79~D!Srs>+;;Q%q!g#Bd(*?yA2uD znd#fOAhFWc;#nu`IaYj&UwVQ;<8_zYLJ@&!obt!y8u%oG-t9|LOyMf%lUm6N4l2f9 zPdxe@Z)z>gZ>rIgzvsdHzrZ!loB&YAfnj#D16W5Fc_<*Z8)gBMbJT`WCmLOcG8Ra! z8lZ_g#`A^wqL@$MnZmu=l!9gru6;K`qwD$<1(P^aQ3a8mkX`q)Y+G!td$lS@N(aYc zgJ#0uP}>5P!PjbjJ6f?AQF>uPs25mfdeGxJphT7;{nY%9(deP-m}TK_)i?XUTAJ|j z=Tr*QwA}5wuN375wcXK$-CymK{!<-I8Zhm6R_aG9)YV_f6qn!W_4k>>lVG*9{2jCJ zX<5)eBM^#B?}St=Oow6$2b+^cwNOT1%L0tB^lg zS->eJ+x7SRVp3}tXxvFUZtx&v2P1f~Cr{TPL^0@9vdcuBH|JL84IaLU;UGeRl4V;O zbCrlI2$is$?A7A(loRAzKyGB&hps1*@*j4@zVb-*LgqVijyu1CZSBxp30k$7F}u5u zH=WQ1@GyCy?@EEsmgwWPq?FOd!kxwdeV}AAgKQ8lq2Q^9pCY!ueiFBMS5*ca1A85b z;}+>f-o&FMUJp69@=1e5-;O9{w&$T<+yzfEQgHRnQq$9PFSdlG;w>7H(~* zm|mXwtZ{%jPXd(gZnZ(YfJ(E9v9GxfwMk`?unq(U9LNwOG(T1H#IguoCC4oJ+Dsa_ zK>oH%hvEb$>)&w6I6SEk*FvnhWz||^dn913@+$P;8S6}_`TI)G-*92+(3zv15Ymbc z48CTZRak?S{rI=h8>-W2C+YvaA^UoL)CaW&@6(t1Q)eE_HMGbucT|`k_9=>84_W9M z+ejkExkga-Sl<&C*;Vz~d5(rd$ZNh`;0-B_`l`W%$#JuX&{o0i#d{&&8{vbbmcOR1 zau1Ga$#kF*Z419k9m8HYw8H=a!_NSR`lg%JWE$wMG!fEpquyns{~yxYPqG=4ULX6R z3(>RFmg8Q)yL$NG(Z}-*ey6W=RIzvo*SoYOInrOMC zpPR5kDSag+o1lQ@t?pmdBdeRkx!;YJ-Mybs5bLYLSYcakY2|?Bjdn|veT9Ao8r-+- ziKS7VSmZ+0?F6OAVhv`F6lIj!#{MC%{nSIj4Q9eOH#wOoVHpiLhsmisp2k{#W03}% zpd&^4F1gz$?_jo;uWab#aIzk4zWKKd&vbCva(XHA+MX`v4p2hx(9kDMc4nyU{>KbL zRt}${nz=u*Z*}uv8{H?>J?$)bl60zA`$f@)HyAfmO*qLO8fdr|TA6zPjj;osf;SCsHCk?ZrpEzfQ z*tV=q={}oGSIVodj4xeWXF$3Aqq2?pHJtY*zMOU6!h0bX5~e1&1J_u% zMhs`y1cWfWt>o{vN^79i7;)shLXQrvr|Z^^zJx0EAq+&F-17AKv$p3P`wTshD{5N~ zp`SgyX;N%DW>5J}49D0V61_?7bY|@y9<02K;{N-(W%1DYhB_K@=LrQ#`w_CYLWlFD z9a6%NY?X<&8a>J)5Lr*B5F+aQF_n`g4RV7B(_taKOA!Wh-ioKlHL;cj1%(+#9)-FX ztie|KqPNPxvIlm5Cv2^`h1Rxp5ar(~5dVzQL(BWvp=pAO<@Hma<3>o%i<-Z8>N0b0 zc2vpWy*$$K4gc9!E7(0&AOP?EXYR8I)VnVxcOWxp*Q}*%kIi*itWlQ|m-&>!XLs-> zqUG?v^2*R3^Du~y*_X&J1G#dwE5HiOfxyVyayEmcR zTmU17xqp_I)4$-KB4Urm#&o20CtClTSMd8u$XOk6R0m8ov| zl7!g-K8qrEBl4y6)?9*uK+u!=X>ZJKFKJ2&ZE!RNZnW03#;nvx=a#*VRVX}1A%A32 z{WDnC(V$eQ#-w~#1vvPOB#1jk5uzhQ+1SV*_f*fVhQDfQT&4rGd?q3mH!~_(-!%W* zO$*K1qbHJJ%9YaaDp9g`2dED-!WEpmQ}>7*M$QF5`Q5x53Tet2i}Ujh;|uxpzI1M zUtDuY_;;L%Xq@&BUHu|>bw;dYB#T-3Q-i4DkDd@^!)@iv$skmtihnqIR+SYC@TJVa z3D>clxz>WE#5+HCi#ig^ufe>pRdV=Q%xNNR*m8yxR%^$g#bdb!3ugry8(mXNvmQd= zXev0gU#WPGCv$_I1+Fn*nu?e*OyJ%!?DAZQ!DgYxM8)aK*$hK3HS)oziDbSe722SZ zI}Z%4yzYTt{Wp#s_@f3vpCd9LuRFz`t~8WL?9FuGe$Yjk{dW721a5HVs0FNgB35>& z*e2C~Fr3+@8A(Nv9e-M}Vi`|I5GE5qJDrMZo0|s2`Q}j<_V`4lX8FZdeoqrbB)M_= z&UZPe@4MBESxSZ4q&R8Qpv^qDC(Q5+kyQo$`|pmIVSuSWY)aJo8l)o^xUFoo5#0|s zPyMJo_sh>2nhcXwxK)oRGieL6vleP3qsT^cg35(YcJIv)`_P2^4kkrm^vCXnL*ef# z&5Kf5s7e-i_SV$rnXjMpl~?Qhy;INmF1BsV{+j)>*gss%U}oZc`VxwRFYNoM*->=Q zr7*W=mg}1_(qZjiLPYyF*pk@9^kIrZLHvds2e>5S0#hk<4sF~zQ!f^)^E?rT zBV_Av%UX=-?jwdr|xKPO!7 zMK7PdgSVQJxTaX%yM#o8GHI3J&7kh`74;wKt zY{)$J{U^4$vb&Foav}gw`B4~4U(;ZD3KY6er^0veAaAOvS>wQD?Nyl13^i6?>{?Fe zA!MW)b+Wk zwrx6tE$<0^YFdO78MPZ%Ak!3*p>e!k3C}mJZ{4%m?v;paOP4^(Xf4kF$>yen?wIvZ z%aTudW%JD=Yt<5&A_}9zSyBYGgCdv@v_x4DtZRMhmk3AK@*_MTa+^X{< z0M*^WAH2P`cR{msM>~=57q@GDjEUKCB@6KyAfJ~b_W}@3J(H~js>PMqq}l3ZQ>=8| z--(=e%k)1@TOCwT;hXQmCTE?GdPMm?mCGuJ1|nnhZ4(BL*QtRwBi|?0CKQ}AQbm!> zrc56@A7!fq9R;;ykLU%cn)yE)Vu2{st}?3h95}rvAUM^KLyK}V9;L%=o;?A~sEz~+ z;^5Tv2o$c2p^bs+}$bc9$s156v}UU0X5#Z zfMzKy>?BLh+d*j-h49bb;jU|I^-QSU)nDe9Y^{nS(^9>Ek?l!uUluz~in|7$?Wkoj zuExkVXxlm(axZ2DZdi03x|t2Re^+&QvSeXguzUqr)`lA0f@SeN3eM8HnUgcMzv=rs zOK_r`<6(WpAY!#-=8=@D?o^?n;X^nWw^w@oC?YmM`qyBb#K#1Rl=bvIkFt=s52}*k zd&@~H60V)?%%TU1J4v|MC8eNEdSi{Da9 zrtyo5bDL!Ew!=}&NT-X`l&o8X5yuPJ)@pnWqGD%4ybsQ?eXzaQyA$s8biYmBoR_xW z@9JT2g)}>KPfsJ8 z<7C+GkDhBZgx#Ts%vY>OJJB_;4XXZ<5o&6hiaglpZhehwfXTA|pK#`0ElfU5{U$;-d+*@4NrvPRZjfuP z<}8S?%xG3~G{Ye!HL^1^s7f(YMj%@NiG-F5K0Jt8qR>MzgPz~W^Pwz&x6Okao)7#y ze!BKX)4c$Qrn22%a@Jk)=M_&`kuMQ=;n}`?+Lk3f2RYtQts^s#8V7D;XfS_m)GHfB z9x2zQ#)veu=qqqwj=Ge&t$Cr;0CHAG^}zx9B_mRo-EJ^vm#eJ;m*E7-=CG7TilE;? z?RGS9)%yHQan_kdCnE_}ZAK!hTC2@kGkOwNd8TmI<0^z2(;$wzUB_sXGEZ+;vmUo* zNrk-GFA11Cyw%SaF|n5O$<^NRoT&qDAznTmbsGb(x#mZ=rXzF*|Lx{5w$PE$KQ2Z zqaV~=EKm(oB&a*iXAl1^_jDP^P!%QDQlVh=n>bDG+dxMmZz{z>BquxQ={n@Ar?;Ek zkjCj*RqV{sjqiInE7H63Mx_yMW})9#EH*dgvW%}(vA`tH(B_2;u?WS zvLAh*wi!#BF}9ZJeoDqwG{jfN?b~b9d2ElI|CzsB#-X^dc~kNUms&nj?4Pu>1l1fv zoKq0rXv-?Y%P0m|aI_2KG^-lgoT?+ItJNg4c6f#>w!|0?&piRWf1@>6?^d zCnvSa=jUkHtof32bsmEnx~-s|vHq5I1QNV_KSRfNIdhr>}W#z4c39kmW%0d5ROz zspUWN=MU?0nY)~$o`$#&?_S#B1okC86-szr&gp?&UNpn^PX&4ddY)cQLXi%hY%~3d zPDDS^L_6?N>_8$m8rphKHzVarV~<&KGH8{s?A@2_ySHebam{b_E2_knRznbj+*<5@ z^<$@p!rS^#sny(*HrmIv`pHK$#_d*uJo`wkgT$$Orle%%4J)_(*-wzbQ${UWb?`Bq0i!EO0Qb>vfrUNlS}IiR!U_wH?>Rpz$cpn z&!He{Gu1>5p{!WSlG6!jOSc}+dDFWF#be*+WFJ4rhflg|eJ0Z+n z1)|W7CN<+vKTBnrj#)dv%MubVp%78o)cI{ixsjaSTG#!&%9T33&O}|PK6UC~DdbYW zl?2zPwy(5I^5lcAdb7C;4}C&#8z)L5wqe z4v7H7!`Y{&rOm7i*>CN0j3S}x0BH0Dz!&Y$G&7dRbG6@OwvjQVe0|RGFG`K7`U&vD zr#OWld;r+jEuc3V7V6bn#{n4iHAFaoYqeem*4H!0aq3BKK*wq6y=VQ${qsmP@pA2oyL3O5ws zQlo2qzg=$=w<>-Wf`PB<7K4Af5(}PHtd)GPd?kwCLQsp!YJi&^53valjmBu z`%7QgTzn0JxR~0wQTi5K4}N{sAj%1w!F_sb8GaSrmH=%}`1jp>hn1S{DRiX_^qECg zzbmo`7}$H^6Ow;a=RX!-nOBMqsc_o`wQUzHa6mSHbuxCH#H|!FVhwZy61ZB6u7lEL zMePs=)sb2Kw+^&qCUyu+q_zrXLgg5zf`{?`ELMVRD+pB+9pQvxs*(-7u#Aa-4n@?( z=6o{Ns~P(6Kg4*R4F6}H9zd!evHn+=00H0sQ1ku!tN!atfXwed*!chcs()Ah9m@d! zpDzJ#@~^M@xA(;{{C|4?|JS#~Fa6&`@-P3T{~nV6%Y*yhL-OB4@-MUGzgXV>`ziVV f{Zld?brAlJ13Jv=a4+}-@T0AvuU_%!`P=^us%bEB literal 0 HcmV?d00001 diff --git a/platform/android/lui/src/test/snapshots/images/dev.lui_LuiScreenshotTest_galleryDark_gallery-dark.png b/platform/android/lui/src/test/snapshots/images/dev.lui_LuiScreenshotTest_galleryDark_gallery-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..ac96667e8a5101ea7afa3d2156fa0c15117cf512 GIT binary patch literal 34476 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 A(EtDd literal 0 HcmV?d00001 diff --git a/platform/android/lui/src/test/snapshots/images/dev.lui_LuiScreenshotTest_galleryLight_gallery-light.png b/platform/android/lui/src/test/snapshots/images/dev.lui_LuiScreenshotTest_galleryLight_gallery-light.png new file mode 100644 index 0000000000000000000000000000000000000000..fc61e01e3fc6549222afdcf31c572c4aee913670 GIT binary patch literal 34017 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Ⓢ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 fzt6bB_5+{V=q&O1^@TVxJBAYMs`u+a_^0s(I literal 0 HcmV?d00001 From bc9c86b9375548c9527567bd081f17fd7afec171 Mon Sep 17 00:00:00 2001 From: Tienson Qin Date: Tue, 6 Oct 2026 10:20:35 +0000 Subject: [PATCH 4/5] feat(android): KotlinHost wiring, split extensions, kotlin OCaml callbacks - host_kind gains KotlinHost (protocol, bridge code 4, 'kotlin' host name) - extension_schemas.json: {android,kotlin} in all/splits profiles; native-card moves to new web-and-kotlin profile - fingerprint literals recomputed across split host sources and tests - lui_element_combine: restore mobile-host branches keyed on KotlinHost - LuiSplit.kt: split-view/branch/pane/tab Compose extensions with fingerprint literals, added to split_host_sources drift check - components_bridge.ml: register "lui_kotlin" callback prefix and the missing extension_event entry point (shared gap; split/markdown bridges already had it) - gallery app registers split components + native-card + gallery-accent --- .../dev/lui/components/GalleryExtensions.kt | 59 +++ .../java/dev/lui/components/MainActivity.kt | 1 + .../LUIComponentsApp/GalleryExtensions.swift | 2 +- .../components/native/components_bridge.ml | 19 +- examples/gallery/extension_schemas.json | 20 +- examples/gallery/extension_schemas.ml | 12 +- .../src/main/kotlin/dev/lui/LuiExtension.kt | 4 + .../src/main/kotlin/dev/lui/LuiNodeView.kt | 1 + .../lui/src/main/kotlin/dev/lui/LuiSplit.kt | 355 ++++++++++++++++++ .../Sources/LUIAppleBackend/LUISplit.swift | 8 +- platform/web/src/lui-split.js | 8 +- src/lui_element_combine.ml | 41 +- src/lui_extension.ml | 1 + src/lui_protocol.ml | 1 + src/lui_protocol.mli | 1 + src/lui_split.ml | 1 + test/test_lui.ml | 11 +- tooling/generate_component_schema.mjs | 1 + tooling/generate_extension_api.mjs | 2 +- 19 files changed, 511 insertions(+), 37 deletions(-) create mode 100644 examples/components/android/app/src/main/java/dev/lui/components/GalleryExtensions.kt create mode 100644 platform/android/lui/src/main/kotlin/dev/lui/LuiSplit.kt diff --git a/examples/components/android/app/src/main/java/dev/lui/components/GalleryExtensions.kt b/examples/components/android/app/src/main/java/dev/lui/components/GalleryExtensions.kt new file mode 100644 index 00000000..4d120759 --- /dev/null +++ b/examples/components/android/app/src/main/java/dev/lui/components/GalleryExtensions.kt @@ -0,0 +1,59 @@ +package dev.lui.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Card +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import dev.lui.LuiExtensionProperty +import dev.lui.LuiExtensionRegistry +import dev.lui.LuiExtensionValueKind +import dev.lui.LuiSplit + +/** + * Gallery extension set for the Kotlin host: the LUI split components plus + * the gallery's own native-card and gallery-accent declarations. The + * fingerprint literals must equal the ones Lui_extension computes from + * examples/gallery/extension_schemas.ml — the drift check in test_lui.ml + * enforces the same invariant for the other hosts. + */ +object GalleryExtensions { + + private const val NATIVE_CARD_FINGERPRINT = + "lui-extension-v1|10:native-card|profiles:android/kotlin,web/web|standard-children:0|children:|properties:5:title:string:required:none|events:" + private const val GALLERY_ACCENT_FINGERPRINT = + "lui-tweak-v1|14:gallery-accent|profiles:android/kotlin,ios/swiftui,linux/gpui,macos/gpui,macos/swiftui,web/web,windows/gpui|properties:" + + fun register(registry: LuiExtensionRegistry) { + LuiSplit.register(registry) + registry.register( + identifier = "native-card", + fingerprint = NATIVE_CARD_FINGERPRINT, + properties = listOf( + LuiExtensionProperty("title", LuiExtensionValueKind.STRING, required = true), + ), + builder = { + Card(Modifier.fillMaxSize().padding(8.dp)) { + Column(Modifier.padding(16.dp)) { + Text( + string("title") ?: "", + style = MaterialTheme.typography.titleMedium, + ) + } + } + }, + ) + registry.registerTweak( + identifier = "gallery-accent", + fingerprint = GALLERY_ACCENT_FINGERPRINT, + ) { + Column(Modifier.fillMaxSize()) { + children.forEach { child(it) } + } + } + } +} diff --git a/examples/components/android/app/src/main/java/dev/lui/components/MainActivity.kt b/examples/components/android/app/src/main/java/dev/lui/components/MainActivity.kt index ba5912fa..1e89331a 100644 --- a/examples/components/android/app/src/main/java/dev/lui/components/MainActivity.kt +++ b/examples/components/android/app/src/main/java/dev/lui/components/MainActivity.kt @@ -38,6 +38,7 @@ class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() + GalleryExtensions.register(backend.extensions) runtimeStarted = runCatching { LuiBridge.start(libraryName = "lui_jni_bridge") { json -> diff --git a/examples/components/ios-swiftui/Sources/LUIComponentsApp/GalleryExtensions.swift b/examples/components/ios-swiftui/Sources/LUIComponentsApp/GalleryExtensions.swift index 4ee1e7ad..6d8833fc 100644 --- a/examples/components/ios-swiftui/Sources/LUIComponentsApp/GalleryExtensions.swift +++ b/examples/components/ios-swiftui/Sources/LUIComponentsApp/GalleryExtensions.swift @@ -37,7 +37,7 @@ func galleryExtensionRegistry() throws -> LUIAppleExtensionRegistry { try registry.registerTweak( LUIAppleTweak( identifier: "gallery-accent", - fingerprint: "lui-tweak-v1|14:gallery-accent|profiles:ios/swiftui,linux/gpui,macos/gpui,macos/swiftui,web/web,windows/gpui|properties:" + fingerprint: "lui-tweak-v1|14:gallery-accent|profiles:android/kotlin,ios/swiftui,linux/gpui,macos/gpui,macos/swiftui,web/web,windows/gpui|properties:" ) { content, _ in AnyView( content diff --git a/examples/components/native/components_bridge.ml b/examples/components/native/components_bridge.ml index a506759b..f5f4d50b 100644 --- a/examples/components/native/components_bridge.ml +++ b/examples/components/native/components_bridge.ml @@ -25,6 +25,7 @@ let operating_system = function let host_kind = function | 1 -> WebHost | 2 -> SwiftUIHost + | 4 -> KotlinHost | 6 -> GPUIHost | _ -> GenericHost @@ -108,6 +109,17 @@ let context_menu_press node x y modifiers button target_class = dispatch (ContextMenuPress (node, pointer_detail x y modifiers button target_class)) +(* Extension events travel as a flat JSON object of scalar values; decode it + and let the runtime validate the payload against the declared schema. *) +let extension_event node identifier name json_values = + let values = + match Lui_json.parse_values json_values with + | values -> values + | exception Lui_json.Parse_error message -> + invalid_arg ("invalid extension event payload: " ^ message) + in + dispatch (ExtensionEvent (node, identifier, name, values)) + let dispose () = latest_patch := ""; ignore (Lui_app.dispose (app ())); @@ -135,10 +147,13 @@ let register prefix = register (prefix ^ "_pointer_enter") pointer_enter; register (prefix ^ "_pointer_leave") pointer_leave; register (prefix ^ "_context_menu_press") context_menu_press; + register (prefix ^ "_extension_event") extension_event; register (prefix ^ "_dispose") dispose; register (prefix ^ "_root_node") root_node -(* platform/native/lui_ocaml_bridge.c looks up "lui_ocaml_*". *) +(* platform/native/lui_ocaml_bridge.c looks up "lui_ocaml_*"; + platform/android/lui's JNI bridge looks up "lui_kotlin_*". *) let () = Printexc.record_backtrace true; - register "lui_ocaml" + register "lui_ocaml"; + register "lui_kotlin" diff --git a/examples/gallery/extension_schemas.json b/examples/gallery/extension_schemas.json index 21e073f5..f690ee00 100644 --- a/examples/gallery/extension_schemas.json +++ b/examples/gallery/extension_schemas.json @@ -46,6 +46,10 @@ { "os": "windows", "host": "gpui" + }, + { + "os": "android", + "host": "kotlin" } ], "splits": [ @@ -72,6 +76,10 @@ { "os": "windows", "host": "gpui" + }, + { + "os": "android", + "host": "kotlin" } ], "web": [ @@ -105,6 +113,16 @@ "os": "windows", "host": "gpui" } + ], + "web-and-kotlin": [ + { + "os": "web", + "host": "web" + }, + { + "os": "android", + "host": "kotlin" + } ] }, "components": [ @@ -281,7 +299,7 @@ }, { "identifier": "native-card", - "profiles": "web", + "profiles": "web-and-kotlin", "standardChildren": false, "children": [], "properties": [ diff --git a/examples/gallery/extension_schemas.ml b/examples/gallery/extension_schemas.ml index 32e899a3..9f83e043 100644 --- a/examples/gallery/extension_schemas.ml +++ b/examples/gallery/extension_schemas.ml @@ -274,35 +274,35 @@ let simulator_camera_schema = [ Lui_extension.event "state-change" [ Lui_extension.event_field "state" Lui_extension.StringScalar true ] ] let native_card_schema = - Lui_extension.component "native-card" [ { Lui_protocol.profile_os = WebOS; Lui_protocol.profile_host = WebHost }; { Lui_protocol.profile_os = MacOS; Lui_protocol.profile_host = GPUIHost }; { Lui_protocol.profile_os = LinuxOS; Lui_protocol.profile_host = GPUIHost }; { Lui_protocol.profile_os = WindowsOS; Lui_protocol.profile_host = GPUIHost } ] + Lui_extension.component "native-card" [ { Lui_protocol.profile_os = WebOS; Lui_protocol.profile_host = WebHost }; { Lui_protocol.profile_os = AndroidOS; Lui_protocol.profile_host = KotlinHost } ] false [ ] [ Lui_extension.property "title" Lui_extension.StringScalar true None ] [ ] let split_view_schema = - Lui_extension.component "split-view" [ { Lui_protocol.profile_os = MacOS; Lui_protocol.profile_host = SwiftUIHost }; { Lui_protocol.profile_os = IOS; Lui_protocol.profile_host = SwiftUIHost }; { Lui_protocol.profile_os = WebOS; Lui_protocol.profile_host = WebHost }; { Lui_protocol.profile_os = MacOS; Lui_protocol.profile_host = GPUIHost }; { Lui_protocol.profile_os = LinuxOS; Lui_protocol.profile_host = GPUIHost }; { Lui_protocol.profile_os = WindowsOS; Lui_protocol.profile_host = GPUIHost } ] + Lui_extension.component "split-view" [ { Lui_protocol.profile_os = MacOS; Lui_protocol.profile_host = SwiftUIHost }; { Lui_protocol.profile_os = IOS; Lui_protocol.profile_host = SwiftUIHost }; { Lui_protocol.profile_os = WebOS; Lui_protocol.profile_host = WebHost }; { Lui_protocol.profile_os = MacOS; Lui_protocol.profile_host = GPUIHost }; { Lui_protocol.profile_os = LinuxOS; Lui_protocol.profile_host = GPUIHost }; { Lui_protocol.profile_os = WindowsOS; Lui_protocol.profile_host = GPUIHost }; { Lui_protocol.profile_os = AndroidOS; Lui_protocol.profile_host = KotlinHost } ] false [ "split-branch"; "split-pane" ] [ Lui_extension.property "divider-thickness" Lui_extension.FloatScalar false None; Lui_extension.property "animation" Lui_extension.BoolScalar false None; Lui_extension.property "accessibility-identifier" Lui_extension.StringScalar false None ] [ ] let split_branch_schema = - Lui_extension.component "split-branch" [ { Lui_protocol.profile_os = MacOS; Lui_protocol.profile_host = SwiftUIHost }; { Lui_protocol.profile_os = IOS; Lui_protocol.profile_host = SwiftUIHost }; { Lui_protocol.profile_os = WebOS; Lui_protocol.profile_host = WebHost }; { Lui_protocol.profile_os = MacOS; Lui_protocol.profile_host = GPUIHost }; { Lui_protocol.profile_os = LinuxOS; Lui_protocol.profile_host = GPUIHost }; { Lui_protocol.profile_os = WindowsOS; Lui_protocol.profile_host = GPUIHost } ] + Lui_extension.component "split-branch" [ { Lui_protocol.profile_os = MacOS; Lui_protocol.profile_host = SwiftUIHost }; { Lui_protocol.profile_os = IOS; Lui_protocol.profile_host = SwiftUIHost }; { Lui_protocol.profile_os = WebOS; Lui_protocol.profile_host = WebHost }; { Lui_protocol.profile_os = MacOS; Lui_protocol.profile_host = GPUIHost }; { Lui_protocol.profile_os = LinuxOS; Lui_protocol.profile_host = GPUIHost }; { Lui_protocol.profile_os = WindowsOS; Lui_protocol.profile_host = GPUIHost }; { Lui_protocol.profile_os = AndroidOS; Lui_protocol.profile_host = KotlinHost } ] false [ "split-branch"; "split-pane" ] [ Lui_extension.property "orientation" Lui_extension.StringScalar true None; Lui_extension.property "ratio" Lui_extension.FloatScalar true None ] [ Lui_extension.event "ratio-changed" [ Lui_extension.event_field "ratio" Lui_extension.FloatScalar true ] ] let split_pane_schema = - Lui_extension.component "split-pane" [ { Lui_protocol.profile_os = MacOS; Lui_protocol.profile_host = SwiftUIHost }; { Lui_protocol.profile_os = IOS; Lui_protocol.profile_host = SwiftUIHost }; { Lui_protocol.profile_os = WebOS; Lui_protocol.profile_host = WebHost }; { Lui_protocol.profile_os = MacOS; Lui_protocol.profile_host = GPUIHost }; { Lui_protocol.profile_os = LinuxOS; Lui_protocol.profile_host = GPUIHost }; { Lui_protocol.profile_os = WindowsOS; Lui_protocol.profile_host = GPUIHost } ] + Lui_extension.component "split-pane" [ { Lui_protocol.profile_os = MacOS; Lui_protocol.profile_host = SwiftUIHost }; { Lui_protocol.profile_os = IOS; Lui_protocol.profile_host = SwiftUIHost }; { Lui_protocol.profile_os = WebOS; Lui_protocol.profile_host = WebHost }; { Lui_protocol.profile_os = MacOS; Lui_protocol.profile_host = GPUIHost }; { Lui_protocol.profile_os = LinuxOS; Lui_protocol.profile_host = GPUIHost }; { Lui_protocol.profile_os = WindowsOS; Lui_protocol.profile_host = GPUIHost }; { Lui_protocol.profile_os = AndroidOS; Lui_protocol.profile_host = KotlinHost } ] false [ "split-tab" ] [ Lui_extension.property "pane-id" Lui_extension.StringScalar true None; Lui_extension.property "selected" Lui_extension.StringScalar false None; Lui_extension.property "focused" Lui_extension.BoolScalar false None; Lui_extension.property "accessibility-identifier" Lui_extension.StringScalar false None ] [ Lui_extension.event "tab-selected" [ Lui_extension.event_field "tab" Lui_extension.StringScalar true ]; Lui_extension.event "tab-closed" [ Lui_extension.event_field "tab" Lui_extension.StringScalar true ]; Lui_extension.event "tab-moved" [ Lui_extension.event_field "tab" Lui_extension.StringScalar true; Lui_extension.event_field "index" Lui_extension.IntScalar true; Lui_extension.event_field "from-pane" Lui_extension.StringScalar true ]; Lui_extension.event "pane-focused" [ ]; Lui_extension.event "navigate" [ Lui_extension.event_field "direction" Lui_extension.StringScalar true ]; Lui_extension.event "split-requested" [ Lui_extension.event_field "orientation" Lui_extension.StringScalar true ]; Lui_extension.event "split-drop" [ Lui_extension.event_field "tab" Lui_extension.StringScalar true; Lui_extension.event_field "from-pane" Lui_extension.StringScalar true; Lui_extension.event_field "edge" Lui_extension.StringScalar true ]; Lui_extension.event "pane-closed" [ ] ] let split_tab_schema = - Lui_extension.component "split-tab" [ { Lui_protocol.profile_os = MacOS; Lui_protocol.profile_host = SwiftUIHost }; { Lui_protocol.profile_os = IOS; Lui_protocol.profile_host = SwiftUIHost }; { Lui_protocol.profile_os = WebOS; Lui_protocol.profile_host = WebHost }; { Lui_protocol.profile_os = MacOS; Lui_protocol.profile_host = GPUIHost }; { Lui_protocol.profile_os = LinuxOS; Lui_protocol.profile_host = GPUIHost }; { Lui_protocol.profile_os = WindowsOS; Lui_protocol.profile_host = GPUIHost } ] + Lui_extension.component "split-tab" [ { Lui_protocol.profile_os = MacOS; Lui_protocol.profile_host = SwiftUIHost }; { Lui_protocol.profile_os = IOS; Lui_protocol.profile_host = SwiftUIHost }; { Lui_protocol.profile_os = WebOS; Lui_protocol.profile_host = WebHost }; { Lui_protocol.profile_os = MacOS; Lui_protocol.profile_host = GPUIHost }; { Lui_protocol.profile_os = LinuxOS; Lui_protocol.profile_host = GPUIHost }; { Lui_protocol.profile_os = WindowsOS; Lui_protocol.profile_host = GPUIHost }; { Lui_protocol.profile_os = AndroidOS; Lui_protocol.profile_host = KotlinHost } ] true [ ] [ Lui_extension.property "tab-id" Lui_extension.StringScalar true None; Lui_extension.property "title" Lui_extension.StringScalar true None; Lui_extension.property "icon" Lui_extension.StringScalar false None; Lui_extension.property "dirty" Lui_extension.BoolScalar false None; Lui_extension.property "closable" Lui_extension.BoolScalar false None; Lui_extension.property "accessibility-identifier" Lui_extension.StringScalar false None ] @@ -351,7 +351,7 @@ let gpui_table_schema = [ ] let gallery_accent_schema = - Lui_extension.tweak "gallery-accent" [ { Lui_protocol.profile_os = IOS; Lui_protocol.profile_host = SwiftUIHost }; { Lui_protocol.profile_os = MacOS; Lui_protocol.profile_host = SwiftUIHost }; { Lui_protocol.profile_os = WebOS; Lui_protocol.profile_host = WebHost }; { Lui_protocol.profile_os = MacOS; Lui_protocol.profile_host = GPUIHost }; { Lui_protocol.profile_os = LinuxOS; Lui_protocol.profile_host = GPUIHost }; { Lui_protocol.profile_os = WindowsOS; Lui_protocol.profile_host = GPUIHost } ] + Lui_extension.tweak "gallery-accent" [ { Lui_protocol.profile_os = IOS; Lui_protocol.profile_host = SwiftUIHost }; { Lui_protocol.profile_os = MacOS; Lui_protocol.profile_host = SwiftUIHost }; { Lui_protocol.profile_os = WebOS; Lui_protocol.profile_host = WebHost }; { Lui_protocol.profile_os = MacOS; Lui_protocol.profile_host = GPUIHost }; { Lui_protocol.profile_os = LinuxOS; Lui_protocol.profile_host = GPUIHost }; { Lui_protocol.profile_os = WindowsOS; Lui_protocol.profile_host = GPUIHost }; { Lui_protocol.profile_os = AndroidOS; Lui_protocol.profile_host = KotlinHost } ] [ ] let registry () = diff --git a/platform/android/lui/src/main/kotlin/dev/lui/LuiExtension.kt b/platform/android/lui/src/main/kotlin/dev/lui/LuiExtension.kt index 9c75b8c6..ebb08fe3 100644 --- a/platform/android/lui/src/main/kotlin/dev/lui/LuiExtension.kt +++ b/platform/android/lui/src/main/kotlin/dev/lui/LuiExtension.kt @@ -42,6 +42,7 @@ class LuiExtensionContext internal constructor( val nodeId: Int, val identifier: String, val properties: Map, + val children: List = emptyList(), internal val renderChild: @Composable (Int) -> Unit, ) { fun string(name: String): String? = properties[name]?.stringValue @@ -53,6 +54,9 @@ class LuiExtensionContext internal constructor( backend.emitExtensionEvent(nodeId, identifier, name, values) } + /** The extension node behind a declared-child id, if it is one. */ + fun extensionChild(id: Int): LuiExtensionNode? = backend.extensionNode(id) + @Composable fun child(id: Int) = renderChild(id) } diff --git a/platform/android/lui/src/main/kotlin/dev/lui/LuiNodeView.kt b/platform/android/lui/src/main/kotlin/dev/lui/LuiNodeView.kt index 22d8b39a..3675ef15 100644 --- a/platform/android/lui/src/main/kotlin/dev/lui/LuiNodeView.kt +++ b/platform/android/lui/src/main/kotlin/dev/lui/LuiNodeView.kt @@ -163,6 +163,7 @@ private fun LuiExtensionView(backend: LuiBackend, id: Int) { nodeId = id, identifier = node.identifier, properties = node.properties, + children = node.children, renderChild = { childId -> LuiNodeView(backend, childId) }, ) context.builder() diff --git a/platform/android/lui/src/main/kotlin/dev/lui/LuiSplit.kt b/platform/android/lui/src/main/kotlin/dev/lui/LuiSplit.kt new file mode 100644 index 00000000..e2fc18fb --- /dev/null +++ b/platform/android/lui/src/main/kotlin/dev/lui/LuiSplit.kt @@ -0,0 +1,355 @@ +package dev.lui + +import androidx.compose.foundation.gestures.detectDragGestures +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.VerticalDivider +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.compositionLocalOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableDoubleStateOf +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp + +/** + * Bonsplit-style tabbed split panes for the Kotlin backend (port of + * `lui_flutter_split.dart`). The OCaml app owns the tree; this file owns + * gesture-time visuals (divider drag, tab selection) and reports semantic + * events back through the extension-event channel. + * + * The fingerprint literals are the canonical `lui-extension-v1` strings — + * test_lui.ml's `split_host_sources` drift check verifies them against + * src/lui_split.ml, so keep them byte-identical. + */ +object LuiSplit { + private const val VIEW_FINGERPRINT = + "lui-extension-v1|10:split-view|profiles:android/kotlin,ios/swiftui,linux/gpui,macos/gpui,macos/swiftui,web/web,windows/gpui|standard-children:0|children:12:split-branch,10:split-pane|properties:17:divider-thickness:float:optional:none,24:accessibility-identifier:string:optional:none,9:animation:bool:optional:none|events:" + private const val BRANCH_FINGERPRINT = + "lui-extension-v1|12:split-branch|profiles:android/kotlin,ios/swiftui,linux/gpui,macos/gpui,macos/swiftui,web/web,windows/gpui|standard-children:0|children:12:split-branch,10:split-pane|properties:11:orientation:string:required:none,5:ratio:float:required:none|events:13:ratio-changed[5:ratio:float:required]" + private const val PANE_FINGERPRINT = + "lui-extension-v1|10:split-pane|profiles:android/kotlin,ios/swiftui,linux/gpui,macos/gpui,macos/swiftui,web/web,windows/gpui|standard-children:0|children:9:split-tab|properties:24:accessibility-identifier:string:optional:none,7:focused:bool:optional:none,7:pane-id:string:required:none,8:selected:string:optional:none|events:10:split-drop[3:tab:string:required,4:edge:string:required,9:from-pane:string:required],10:tab-closed[3:tab:string:required],11:pane-closed[],12:pane-focused[],12:tab-selected[3:tab:string:required],15:split-requested[11:orientation:string:required],8:navigate[9:direction:string:required],9:tab-moved[3:tab:string:required,5:index:int:required,9:from-pane:string:required]" + private const val TAB_FINGERPRINT = + "lui-extension-v1|9:split-tab|profiles:android/kotlin,ios/swiftui,linux/gpui,macos/gpui,macos/swiftui,web/web,windows/gpui|standard-children:1|children:|properties:24:accessibility-identifier:string:optional:none,4:icon:string:optional:none,5:dirty:bool:optional:none,5:title:string:required:none,6:tab-id:string:required:none,8:closable:bool:optional:none|events:" + + private val accessibilityIdentifier = + LuiExtensionProperty("accessibility-identifier", LuiExtensionValueKind.STRING) + + /** Registers all four split components. */ + fun register(registry: LuiExtensionRegistry) { + registry.register( + identifier = "split-view", + fingerprint = VIEW_FINGERPRINT, + childIdentifiers = listOf("split-branch", "split-pane"), + properties = listOf( + LuiExtensionProperty("divider-thickness", LuiExtensionValueKind.DOUBLE), + LuiExtensionProperty("animation", LuiExtensionValueKind.BOOLEAN), + accessibilityIdentifier, + ), + builder = { LuiSplitView(this) }, + ) + registry.register( + identifier = "split-branch", + fingerprint = BRANCH_FINGERPRINT, + childIdentifiers = listOf("split-branch", "split-pane"), + properties = listOf( + LuiExtensionProperty("orientation", LuiExtensionValueKind.STRING, required = true), + LuiExtensionProperty("ratio", LuiExtensionValueKind.DOUBLE, required = true), + ), + events = listOf( + LuiExtensionEventSchema( + "ratio-changed", + listOf( + LuiExtensionProperty("ratio", LuiExtensionValueKind.DOUBLE, required = true), + ), + ), + ), + builder = { LuiSplitBranch(this) }, + ) + registry.register( + identifier = "split-pane", + fingerprint = PANE_FINGERPRINT, + childIdentifiers = listOf("split-tab"), + properties = listOf( + LuiExtensionProperty("pane-id", LuiExtensionValueKind.STRING, required = true), + LuiExtensionProperty("selected", LuiExtensionValueKind.STRING), + LuiExtensionProperty("focused", LuiExtensionValueKind.BOOLEAN), + accessibilityIdentifier, + ), + events = listOf( + stringEvent("tab-selected", "tab"), + stringEvent("tab-closed", "tab"), + LuiExtensionEventSchema( + "tab-moved", + listOf( + LuiExtensionProperty("tab", LuiExtensionValueKind.STRING, required = true), + LuiExtensionProperty("index", LuiExtensionValueKind.INTEGER, required = true), + LuiExtensionProperty("from-pane", LuiExtensionValueKind.STRING, required = true), + ), + ), + LuiExtensionEventSchema("pane-focused"), + stringEvent("navigate", "direction"), + stringEvent("split-requested", "orientation"), + LuiExtensionEventSchema( + "split-drop", + listOf( + LuiExtensionProperty("tab", LuiExtensionValueKind.STRING, required = true), + LuiExtensionProperty("from-pane", LuiExtensionValueKind.STRING, required = true), + LuiExtensionProperty("edge", LuiExtensionValueKind.STRING, required = true), + ), + ), + LuiExtensionEventSchema("pane-closed"), + ), + builder = { LuiSplitPane(this) }, + ) + registry.register( + identifier = "split-tab", + fingerprint = TAB_FINGERPRINT, + acceptsStandardChildren = true, + properties = listOf( + LuiExtensionProperty("tab-id", LuiExtensionValueKind.STRING, required = true), + LuiExtensionProperty("title", LuiExtensionValueKind.STRING, required = true), + LuiExtensionProperty("icon", LuiExtensionValueKind.STRING), + LuiExtensionProperty("dirty", LuiExtensionValueKind.BOOLEAN), + LuiExtensionProperty("closable", LuiExtensionValueKind.BOOLEAN), + accessibilityIdentifier, + ), + builder = { + Column(Modifier.fillMaxSize()) { + children.forEach { child(it) } + } + }, + ) + } + + private fun stringEvent(name: String, field: String) = + LuiExtensionEventSchema( + name, + listOf(LuiExtensionProperty(field, LuiExtensionValueKind.STRING, required = true)), + ) +} + +/** Visual settings broadcast by split-view through the Compose tree. */ +private data class LuiSplitSettings( + val dividerThickness: Double = 9.0, + val animationEnabled: Boolean = true, +) + +private val LocalSplitSettings = compositionLocalOf { LuiSplitSettings() } + +@Composable +private fun LuiSplitView(ext: LuiExtensionContext) { + val settings = LuiSplitSettings( + dividerThickness = (ext.double("divider-thickness") ?: 9.0).coerceAtLeast(1.0), + animationEnabled = ext.bool("animation") ?: true, + ) + CompositionLocalProvider(LocalSplitSettings provides settings) { + Column(Modifier.fillMaxSize()) { + ext.children.forEach { ext.child(it) } + } + } +} + +@Composable +private fun LuiSplitBranch(ext: LuiExtensionContext) { + val settings = LocalSplitSettings.current + val horizontal = ext.string("orientation") != "vertical" + val sourceRatio = (ext.double("ratio") ?: 0.5).coerceIn(0.0, 1.0) + var ratio by remember { mutableDoubleStateOf(sourceRatio) } + var dragging by remember { mutableStateOf(false) } + if (!dragging && kotlin.math.abs(ratio - sourceRatio) > 1e-6) { + ratio = sourceRatio + } + var axisPx by remember { mutableIntStateOf(0) } + + val thickness = settings.dividerThickness.dp + val divider: @Composable () -> Unit = { + val handle = Modifier + .then(if (horizontal) Modifier.width(thickness).fillMaxHeight() else Modifier.height(thickness).fillMaxWidth()) + .pointerInput(horizontal) { + detectDragGestures( + onDragStart = { dragging = true }, + onDragEnd = { dragging = false }, + onDragCancel = { dragging = false }, + onDrag = { change, amount -> + change.consume() + if (axisPx > 0) { + val delta = (if (horizontal) amount.x else amount.y) / axisPx + ratio = (ratio + delta).toDouble().coerceIn(0.05, 0.95) + ext.emitEvent( + "ratio-changed", + mapOf("ratio" to LuiWireValue.DoubleValue(ratio)), + ) + } + }, + ) + } + if (horizontal) { + VerticalDivider(handle) + } else { + HorizontalDivider(handle) + } + } + + if (horizontal) { + Row(Modifier.fillMaxSize().onSizeChanged { axisPx = it.width }) { + ext.children.getOrNull(0)?.let { first -> + Box(Modifier.weight(ratio.toFloat().coerceAtLeast(0.001f)).fillMaxHeight()) { + ext.child(first) + } + } + divider() + ext.children.getOrNull(1)?.let { second -> + Box(Modifier.weight((1f - ratio.toFloat()).coerceAtLeast(0.001f)).fillMaxHeight()) { + ext.child(second) + } + } + } + } else { + Column(Modifier.fillMaxSize().onSizeChanged { axisPx = it.height }) { + ext.children.getOrNull(0)?.let { first -> + Box(Modifier.weight(ratio.toFloat().coerceAtLeast(0.001f)).fillMaxWidth()) { + ext.child(first) + } + } + divider() + ext.children.getOrNull(1)?.let { second -> + Box(Modifier.weight((1f - ratio.toFloat()).coerceAtLeast(0.001f)).fillMaxWidth()) { + ext.child(second) + } + } + } + } +} + +@Composable +private fun LuiSplitPane(ext: LuiExtensionContext) { + val tabs = ext.children.mapNotNull { id -> + ext.extensionChild(id)?.let { id to it } + } + val selected = ext.string("selected") ?: tabs.firstOrNull()?.let { + it.second.properties["tab-id"]?.stringValue + } + val selectedPair = tabs.firstOrNull { + it.second.properties["tab-id"]?.stringValue == selected + } ?: tabs.firstOrNull() + + Column(Modifier.fillMaxSize()) { + Surface(tonalElevation = 1.dp) { + Row( + Modifier + .fillMaxWidth() + .horizontalScroll(rememberScrollState()), + ) { + tabs.forEach { (id, tab) -> + val tabId = tab.properties["tab-id"]?.stringValue ?: return@forEach + val isSelected = id == selectedPair?.first + SplitTabChip( + node = tab, + selected = isSelected, + onSelect = { + ext.emitEvent("pane-focused") + ext.emitEvent("tab-selected", mapOf("tab" to LuiWireValue.Str(tabId))) + }, + onClose = { + ext.emitEvent("tab-closed", mapOf("tab" to LuiWireValue.Str(tabId))) + }, + ) + } + } + } + HorizontalDivider() + Box(Modifier.weight(1f).fillMaxWidth()) { + selectedPair?.let { (id, _) -> + // Mount only the selected tab; the OCaml side re-issues + // patches when it is evicted, matching the web backend. + ext.child(id) + } + } + } +} + +@Composable +private fun SplitTabChip( + node: LuiExtensionNode, + selected: Boolean, + onSelect: () -> Unit, + onClose: () -> Unit, +) { + val title = node.properties["title"]?.stringValue ?: "" + val icon = node.properties["icon"]?.stringValue?.let { LuiIcons.resolve(it) } + val dirty = node.properties["dirty"]?.boolValue == true + val closable = node.properties["closable"]?.boolValue == true + + Surface( + color = if (selected) { + MaterialTheme.colorScheme.surface + } else { + MaterialTheme.colorScheme.surfaceContainerLow + }, + onClick = onSelect, + ) { + Row( + Modifier.padding(horizontal = 12.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + if (icon != null) { + Icon( + icon, + contentDescription = null, + modifier = Modifier.size(16.dp).padding(end = 2.dp), + tint = if (selected) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, + ) + } + Text( + if (dirty) "$title •" else title, + style = MaterialTheme.typography.labelLarge, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = if (selected) { + MaterialTheme.colorScheme.onSurface + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, + ) + if (closable) { + IconButton(onClick = onClose, modifier = Modifier.size(20.dp)) { + Icon( + LuiIcons.resolve("x"), + contentDescription = "Close", + modifier = Modifier.size(12.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + } + } +} diff --git a/platform/apple/Sources/LUIAppleBackend/LUISplit.swift b/platform/apple/Sources/LUIAppleBackend/LUISplit.swift index a5ed2820..91df5e48 100644 --- a/platform/apple/Sources/LUIAppleBackend/LUISplit.swift +++ b/platform/apple/Sources/LUIAppleBackend/LUISplit.swift @@ -10,13 +10,13 @@ import UniformTypeIdentifiers @MainActor public enum LUISplit { static let viewFingerprint = - "lui-extension-v1|10:split-view|profiles:ios/swiftui,linux/gpui,macos/gpui,macos/swiftui,web/web,windows/gpui|standard-children:0|children:12:split-branch,10:split-pane|properties:17:divider-thickness:float:optional:none,24:accessibility-identifier:string:optional:none,9:animation:bool:optional:none|events:" + "lui-extension-v1|10:split-view|profiles:android/kotlin,ios/swiftui,linux/gpui,macos/gpui,macos/swiftui,web/web,windows/gpui|standard-children:0|children:12:split-branch,10:split-pane|properties:17:divider-thickness:float:optional:none,24:accessibility-identifier:string:optional:none,9:animation:bool:optional:none|events:" static let branchFingerprint = - "lui-extension-v1|12:split-branch|profiles:ios/swiftui,linux/gpui,macos/gpui,macos/swiftui,web/web,windows/gpui|standard-children:0|children:12:split-branch,10:split-pane|properties:11:orientation:string:required:none,5:ratio:float:required:none|events:13:ratio-changed[5:ratio:float:required]" + "lui-extension-v1|12:split-branch|profiles:android/kotlin,ios/swiftui,linux/gpui,macos/gpui,macos/swiftui,web/web,windows/gpui|standard-children:0|children:12:split-branch,10:split-pane|properties:11:orientation:string:required:none,5:ratio:float:required:none|events:13:ratio-changed[5:ratio:float:required]" static let paneFingerprint = - "lui-extension-v1|10:split-pane|profiles:ios/swiftui,linux/gpui,macos/gpui,macos/swiftui,web/web,windows/gpui|standard-children:0|children:9:split-tab|properties:24:accessibility-identifier:string:optional:none,7:focused:bool:optional:none,7:pane-id:string:required:none,8:selected:string:optional:none|events:10:split-drop[3:tab:string:required,4:edge:string:required,9:from-pane:string:required],10:tab-closed[3:tab:string:required],11:pane-closed[],12:pane-focused[],12:tab-selected[3:tab:string:required],15:split-requested[11:orientation:string:required],8:navigate[9:direction:string:required],9:tab-moved[3:tab:string:required,5:index:int:required,9:from-pane:string:required]" + "lui-extension-v1|10:split-pane|profiles:android/kotlin,ios/swiftui,linux/gpui,macos/gpui,macos/swiftui,web/web,windows/gpui|standard-children:0|children:9:split-tab|properties:24:accessibility-identifier:string:optional:none,7:focused:bool:optional:none,7:pane-id:string:required:none,8:selected:string:optional:none|events:10:split-drop[3:tab:string:required,4:edge:string:required,9:from-pane:string:required],10:tab-closed[3:tab:string:required],11:pane-closed[],12:pane-focused[],12:tab-selected[3:tab:string:required],15:split-requested[11:orientation:string:required],8:navigate[9:direction:string:required],9:tab-moved[3:tab:string:required,5:index:int:required,9:from-pane:string:required]" static let tabFingerprint = - "lui-extension-v1|9:split-tab|profiles:ios/swiftui,linux/gpui,macos/gpui,macos/swiftui,web/web,windows/gpui|standard-children:1|children:|properties:24:accessibility-identifier:string:optional:none,4:icon:string:optional:none,5:dirty:bool:optional:none,5:title:string:required:none,6:tab-id:string:required:none,8:closable:bool:optional:none|events:" + "lui-extension-v1|9:split-tab|profiles:android/kotlin,ios/swiftui,linux/gpui,macos/gpui,macos/swiftui,web/web,windows/gpui|standard-children:1|children:|properties:24:accessibility-identifier:string:optional:none,4:icon:string:optional:none,5:dirty:bool:optional:none,5:title:string:required:none,6:tab-id:string:required:none,8:closable:bool:optional:none|events:" private static let accessibilityIdentifier = LUIExtensionProperty( name: "accessibility-identifier", kind: .string) diff --git a/platform/web/src/lui-split.js b/platform/web/src/lui-split.js index 4c2debca..706c44e4 100644 --- a/platform/web/src/lui-split.js +++ b/platform/web/src/lui-split.js @@ -25,13 +25,13 @@ const LUISplit = (() => { // checker extracts single-line literals, so keep each on one line). const fingerprints = { 'split-view': - 'lui-extension-v1|10:split-view|profiles:ios/swiftui,linux/gpui,macos/gpui,macos/swiftui,web/web,windows/gpui|standard-children:0|children:12:split-branch,10:split-pane|properties:17:divider-thickness:float:optional:none,24:accessibility-identifier:string:optional:none,9:animation:bool:optional:none|events:', + 'lui-extension-v1|10:split-view|profiles:android/kotlin,ios/swiftui,linux/gpui,macos/gpui,macos/swiftui,web/web,windows/gpui|standard-children:0|children:12:split-branch,10:split-pane|properties:17:divider-thickness:float:optional:none,24:accessibility-identifier:string:optional:none,9:animation:bool:optional:none|events:', 'split-branch': - 'lui-extension-v1|12:split-branch|profiles:ios/swiftui,linux/gpui,macos/gpui,macos/swiftui,web/web,windows/gpui|standard-children:0|children:12:split-branch,10:split-pane|properties:11:orientation:string:required:none,5:ratio:float:required:none|events:13:ratio-changed[5:ratio:float:required]', + 'lui-extension-v1|12:split-branch|profiles:android/kotlin,ios/swiftui,linux/gpui,macos/gpui,macos/swiftui,web/web,windows/gpui|standard-children:0|children:12:split-branch,10:split-pane|properties:11:orientation:string:required:none,5:ratio:float:required:none|events:13:ratio-changed[5:ratio:float:required]', 'split-pane': - 'lui-extension-v1|10:split-pane|profiles:ios/swiftui,linux/gpui,macos/gpui,macos/swiftui,web/web,windows/gpui|standard-children:0|children:9:split-tab|properties:24:accessibility-identifier:string:optional:none,7:focused:bool:optional:none,7:pane-id:string:required:none,8:selected:string:optional:none|events:10:split-drop[3:tab:string:required,4:edge:string:required,9:from-pane:string:required],10:tab-closed[3:tab:string:required],11:pane-closed[],12:pane-focused[],12:tab-selected[3:tab:string:required],15:split-requested[11:orientation:string:required],8:navigate[9:direction:string:required],9:tab-moved[3:tab:string:required,5:index:int:required,9:from-pane:string:required]', + 'lui-extension-v1|10:split-pane|profiles:android/kotlin,ios/swiftui,linux/gpui,macos/gpui,macos/swiftui,web/web,windows/gpui|standard-children:0|children:9:split-tab|properties:24:accessibility-identifier:string:optional:none,7:focused:bool:optional:none,7:pane-id:string:required:none,8:selected:string:optional:none|events:10:split-drop[3:tab:string:required,4:edge:string:required,9:from-pane:string:required],10:tab-closed[3:tab:string:required],11:pane-closed[],12:pane-focused[],12:tab-selected[3:tab:string:required],15:split-requested[11:orientation:string:required],8:navigate[9:direction:string:required],9:tab-moved[3:tab:string:required,5:index:int:required,9:from-pane:string:required]', 'split-tab': - 'lui-extension-v1|9:split-tab|profiles:ios/swiftui,linux/gpui,macos/gpui,macos/swiftui,web/web,windows/gpui|standard-children:1|children:|properties:24:accessibility-identifier:string:optional:none,4:icon:string:optional:none,5:dirty:bool:optional:none,5:title:string:required:none,6:tab-id:string:required:none,8:closable:bool:optional:none|events:', + 'lui-extension-v1|9:split-tab|profiles:android/kotlin,ios/swiftui,linux/gpui,macos/gpui,macos/swiftui,web/web,windows/gpui|standard-children:1|children:|properties:24:accessibility-identifier:string:optional:none,4:icon:string:optional:none,5:dirty:bool:optional:none,5:title:string:required:none,6:tab-id:string:required:none,8:closable:bool:optional:none|events:', }; const MIME = 'application/x-lui-split-tab'; diff --git a/src/lui_element_combine.ml b/src/lui_element_combine.ml index 5897526a..525f9e89 100644 --- a/src/lui_element_combine.ml +++ b/src/lui_element_combine.ml @@ -141,10 +141,17 @@ let composer_send_button context ?send_icon send_disabled on_send : t = let apple_icon = Option.value send_icon ~default:(`arrow_up : icon) in if Lui_ui.platform context = Lui_protocol.AndroidOS then - button - ~icon:android_icon ~variant:`primary ~label:"Send" - ~accessibility_identifier:"button.send" ?disabled_signal:send_disabled - ~on_press:on_send ~text:"Send" [] + if Lui_ui.host context = Lui_protocol.KotlinHost + then + button + ~icon:android_icon ~variant:`primary ~size:`icon ~width:48 ~height:48 + ~label:"Send" ~accessibility_identifier:"button.send" + ?disabled_signal:send_disabled ~on_press:on_send [] + else + button + ~icon:android_icon ~variant:`primary ~label:"Send" + ~accessibility_identifier:"button.send" ?disabled_signal:send_disabled + ~on_press:on_send ~text:"Send" [] else button ~icon:apple_icon ~variant:`ghost ~width:36 ~height:36 @@ -178,6 +185,7 @@ let composer : t = fun context parent -> + let kotlin = Lui_ui.host context = Lui_protocol.KotlinHost in let attachment_strip = match attachments with | None -> [] @@ -215,9 +223,9 @@ let composer ?key ?accessibility_identifier ~style_class:"composer-surface" ~min_height:58 ~gap:0 - ~padding_horizontal:16 - ~padding_vertical:8 - ~background:"glass" + ~padding_horizontal:(if kotlin then 12 else 16) + ~padding_vertical:(if kotlin then 12 else 8) + ~background:(if kotlin then "surface-container-high" else "glass") ~corner_radius:24 ([ box ~height:6 ~accessibility_identifier:"spacer.composer.top" [] ] @ attachment_strip @@ -288,12 +296,19 @@ let composer_collapsed : t = fun context parent -> - (button - ?key ?accessibility_identifier ~variant:`ghost ~grow:1.0 ~height:58 - ~padding_horizontal:30 ~background:"glass" - ~foreground:"muted-foreground" ~corner_radius:999 ?icon ~text:label - ~on_press []) - context parent + if Lui_ui.host context = Lui_protocol.KotlinHost + then + (button + ?key ?accessibility_identifier ~variant:`secondary ?icon ~grow:1.0 + ~height:58 ~padding_horizontal:20 ~label ~text:label ~on_press []) + context parent + else + (button + ?key ?accessibility_identifier ~variant:`ghost ~grow:1.0 ~height:58 + ~padding_horizontal:30 ~background:"glass" + ~foreground:"muted-foreground" ~corner_radius:999 ?icon ~text:label + ~on_press []) + context parent ;; (* ------------------------------------------------------------------ *) diff --git a/src/lui_extension.ml b/src/lui_extension.ml index a087f10b..7ed9f565 100644 --- a/src/lui_extension.ml +++ b/src/lui_extension.ml @@ -158,6 +158,7 @@ let host_name host = | GenericHost -> "generic" | WebHost -> "web" | SwiftUIHost -> "swiftui" + | KotlinHost -> "kotlin" | GPUIHost -> "gpui" let token value = string_of_int (String.length value) ^ ":" ^ value diff --git a/src/lui_protocol.ml b/src/lui_protocol.ml index 8379e6aa..2109939e 100644 --- a/src/lui_protocol.ml +++ b/src/lui_protocol.ml @@ -106,6 +106,7 @@ type host_kind = | GenericHost | WebHost | SwiftUIHost + | KotlinHost | GPUIHost type platform_profile = { diff --git a/src/lui_protocol.mli b/src/lui_protocol.mli index c40775fe..8ee8ee09 100644 --- a/src/lui_protocol.mli +++ b/src/lui_protocol.mli @@ -104,6 +104,7 @@ type host_kind = | GenericHost | WebHost | SwiftUIHost + | KotlinHost | GPUIHost type platform_profile = { diff --git a/src/lui_split.ml b/src/lui_split.ml index 74a05104..d30e853d 100644 --- a/src/lui_split.ml +++ b/src/lui_split.ml @@ -21,6 +21,7 @@ let profiles = [ { profile_os = MacOS; profile_host = SwiftUIHost }; { profile_os = IOS; profile_host = SwiftUIHost }; + { profile_os = AndroidOS; profile_host = KotlinHost }; { profile_os = MacOS; profile_host = GPUIHost }; { profile_os = LinuxOS; profile_host = GPUIHost }; { profile_os = WindowsOS; profile_host = GPUIHost }; diff --git a/test/test_lui.ml b/test/test_lui.ml index 1c0065fa..1cfb859f 100644 --- a/test/test_lui.ml +++ b/test/test_lui.ml @@ -1417,7 +1417,7 @@ let test_fingerprint_format () = "lui-extension-v1|16:apple-map-marker|profiles:ios/swiftui,linux/gpui,macos/gpui,macos/swiftui,windows/gpui|standard-children:0|children:|properties:5:title:string:required:none,8:latitude:float:required:none,9:longitude:float:required:none|events:" (Lui_extension.fingerprint (gallery_schema "apple-map-marker")); Alcotest.(check string) "tweak fingerprint" - "lui-tweak-v1|14:gallery-accent|profiles:ios/swiftui,linux/gpui,macos/gpui,macos/swiftui,web/web,windows/gpui|properties:" + "lui-tweak-v1|14:gallery-accent|profiles:android/kotlin,ios/swiftui,linux/gpui,macos/gpui,macos/swiftui,web/web,windows/gpui|properties:" (Lui_extension.tweak_fingerprint (gallery_schema "gallery-accent")) let test_host_literals_in_sync () = @@ -2523,16 +2523,16 @@ let split_fingerprint identifier = let test_split_fingerprints () = Alcotest.(check string) "split-view fingerprint" - "lui-extension-v1|10:split-view|profiles:ios/swiftui,linux/gpui,macos/gpui,macos/swiftui,web/web,windows/gpui|standard-children:0|children:12:split-branch,10:split-pane|properties:17:divider-thickness:float:optional:none,24:accessibility-identifier:string:optional:none,9:animation:bool:optional:none|events:" + "lui-extension-v1|10:split-view|profiles:android/kotlin,ios/swiftui,linux/gpui,macos/gpui,macos/swiftui,web/web,windows/gpui|standard-children:0|children:12:split-branch,10:split-pane|properties:17:divider-thickness:float:optional:none,24:accessibility-identifier:string:optional:none,9:animation:bool:optional:none|events:" (split_fingerprint "split-view"); Alcotest.(check string) "split-branch fingerprint" - "lui-extension-v1|12:split-branch|profiles:ios/swiftui,linux/gpui,macos/gpui,macos/swiftui,web/web,windows/gpui|standard-children:0|children:12:split-branch,10:split-pane|properties:11:orientation:string:required:none,5:ratio:float:required:none|events:13:ratio-changed[5:ratio:float:required]" + "lui-extension-v1|12:split-branch|profiles:android/kotlin,ios/swiftui,linux/gpui,macos/gpui,macos/swiftui,web/web,windows/gpui|standard-children:0|children:12:split-branch,10:split-pane|properties:11:orientation:string:required:none,5:ratio:float:required:none|events:13:ratio-changed[5:ratio:float:required]" (split_fingerprint "split-branch"); Alcotest.(check string) "split-pane fingerprint" - "lui-extension-v1|10:split-pane|profiles:ios/swiftui,linux/gpui,macos/gpui,macos/swiftui,web/web,windows/gpui|standard-children:0|children:9:split-tab|properties:24:accessibility-identifier:string:optional:none,7:focused:bool:optional:none,7:pane-id:string:required:none,8:selected:string:optional:none|events:10:split-drop[3:tab:string:required,4:edge:string:required,9:from-pane:string:required],10:tab-closed[3:tab:string:required],11:pane-closed[],12:pane-focused[],12:tab-selected[3:tab:string:required],15:split-requested[11:orientation:string:required],8:navigate[9:direction:string:required],9:tab-moved[3:tab:string:required,5:index:int:required,9:from-pane:string:required]" + "lui-extension-v1|10:split-pane|profiles:android/kotlin,ios/swiftui,linux/gpui,macos/gpui,macos/swiftui,web/web,windows/gpui|standard-children:0|children:9:split-tab|properties:24:accessibility-identifier:string:optional:none,7:focused:bool:optional:none,7:pane-id:string:required:none,8:selected:string:optional:none|events:10:split-drop[3:tab:string:required,4:edge:string:required,9:from-pane:string:required],10:tab-closed[3:tab:string:required],11:pane-closed[],12:pane-focused[],12:tab-selected[3:tab:string:required],15:split-requested[11:orientation:string:required],8:navigate[9:direction:string:required],9:tab-moved[3:tab:string:required,5:index:int:required,9:from-pane:string:required]" (split_fingerprint "split-pane"); Alcotest.(check string) "split-tab fingerprint" - "lui-extension-v1|9:split-tab|profiles:ios/swiftui,linux/gpui,macos/gpui,macos/swiftui,web/web,windows/gpui|standard-children:1|children:|properties:24:accessibility-identifier:string:optional:none,4:icon:string:optional:none,5:dirty:bool:optional:none,5:title:string:required:none,6:tab-id:string:required:none,8:closable:bool:optional:none|events:" + "lui-extension-v1|9:split-tab|profiles:android/kotlin,ios/swiftui,linux/gpui,macos/gpui,macos/swiftui,web/web,windows/gpui|standard-children:1|children:|properties:24:accessibility-identifier:string:optional:none,4:icon:string:optional:none,5:dirty:bool:optional:none,5:title:string:required:none,6:tab-id:string:required:none,8:closable:bool:optional:none|events:" (split_fingerprint "split-tab") (* Every host source that carries lui-extension-v1 literals is checked so @@ -2542,6 +2542,7 @@ let test_split_fingerprints () = let split_host_sources () = let root = source_root () in [ "platform/apple/Sources/LUIAppleBackend/LUISplit.swift"; + "platform/android/lui/src/main/kotlin/dev/lui/LuiSplit.kt"; "platform/web/src/lui-split.js" ] |> List.filter_map (fun rel -> let path = Filename.concat root rel in diff --git a/tooling/generate_component_schema.mjs b/tooling/generate_component_schema.mjs index 68b5a513..24dcd09e 100644 --- a/tooling/generate_component_schema.mjs +++ b/tooling/generate_component_schema.mjs @@ -124,6 +124,7 @@ type host_kind = | GenericHost | WebHost | SwiftUIHost + | KotlinHost | GPUIHost type platform_profile = { diff --git a/tooling/generate_extension_api.mjs b/tooling/generate_extension_api.mjs index 4a33bfd2..16ff5c91 100644 --- a/tooling/generate_extension_api.mjs +++ b/tooling/generate_extension_api.mjs @@ -65,7 +65,7 @@ const osNames = { const hostNames = { generic: 'GenericHost', web: 'WebHost', swiftui: 'SwiftUIHost', - gpui: 'GPUIHost', + kotlin: 'KotlinHost', gpui: 'GPUIHost', }; const munge = (name) => name.replace(/-/g, '_'); From 7852a8663ffeb3a89d306658944e5f5223d1086c Mon Sep 17 00:00:00 2001 From: Tienson Qin Date: Tue, 6 Oct 2026 10:39:52 +0000 Subject: [PATCH 5/5] refactor(android): align dev.lui API surface with chat's vendored backend Node ids and wire-op id fields are Long throughout. LuiBackend now takes (onEvent, extensions, icons) constructor args with applyJson(String?), Content(rootId: Long), and node(id). LuiIcons becomes the LuiIconResolver fun interface with .or() chaining and a DEFAULT instance, so app-owned (app:-prefixed) icon names fall through to the app-level resolver. LuiExtension gains the minimal register(id, fingerprint) { context -> ... } form plus flag/property/emit(name, Map)/Children() helpers, and extension events carry Map. LocalLuiThemeDark (Boolean?) lets hosts override dark mode; null falls back to the system setting. This keeps the public surface identical to the dev.lui module logseq/chat vendored in its Android app so switching to includeBuild is a pure delete. --- .../java/dev/lui/components/MainActivity.kt | 2 +- .../lui/src/main/kotlin/dev/lui/LuiBackend.kt | 43 +++-- .../lui/src/main/kotlin/dev/lui/LuiBridge.kt | 66 ++++--- .../lui/src/main/kotlin/dev/lui/LuiEvents.kt | 59 +++--- .../src/main/kotlin/dev/lui/LuiExtension.kt | 48 ++++- .../lui/src/main/kotlin/dev/lui/LuiIcons.kt | 176 +++++++++++------- .../src/main/kotlin/dev/lui/LuiNodeView.kt | 125 +++++++------ .../lui/src/main/kotlin/dev/lui/LuiRender.kt | 11 +- .../lui/src/main/kotlin/dev/lui/LuiSplit.kt | 8 +- .../lui/src/main/kotlin/dev/lui/LuiStore.kt | 78 ++++---- .../lui/src/main/kotlin/dev/lui/LuiTheme.kt | 11 +- .../lui/src/main/kotlin/dev/lui/LuiWire.kt | 59 +++--- .../src/test/kotlin/dev/lui/LuiStoreTest.kt | 98 +++++----- .../src/test/kotlin/dev/lui/LuiWireTest.kt | 4 +- 14 files changed, 462 insertions(+), 326 deletions(-) diff --git a/examples/components/android/app/src/main/java/dev/lui/components/MainActivity.kt b/examples/components/android/app/src/main/java/dev/lui/components/MainActivity.kt index 1e89331a..93023d9d 100644 --- a/examples/components/android/app/src/main/java/dev/lui/components/MainActivity.kt +++ b/examples/components/android/app/src/main/java/dev/lui/components/MainActivity.kt @@ -30,7 +30,7 @@ import dev.lui.dispatchToBridge class MainActivity : ComponentActivity() { private val backend = LuiBackend( - eventSink = dev.lui.LuiEventSink { event -> event.dispatchToBridge() }, + onEvent = { event -> event.dispatchToBridge() } ) private var runtimeStarted = false diff --git a/platform/android/lui/src/main/kotlin/dev/lui/LuiBackend.kt b/platform/android/lui/src/main/kotlin/dev/lui/LuiBackend.kt index b9c3b74b..c02046a8 100644 --- a/platform/android/lui/src/main/kotlin/dev/lui/LuiBackend.kt +++ b/platform/android/lui/src/main/kotlin/dev/lui/LuiBackend.kt @@ -11,19 +11,20 @@ import androidx.compose.ui.graphics.asImageBitmap /** * Public entry point of the Kotlin/LUI backend: owns the retained tree, - * the extension registry, host-registered images, and the event sink - * that forwards user events to the bridge. + * the extension registry, host-registered images, the icon resolver, and + * the event handler that forwards user events to the bridge. * * Typical use: * ``` - * val backend = LuiBackend { event -> LuiBridge.dispatch(event) } - * backend.applyBatch(patchJson) - * setContent { backend.Content() } + * val backend = LuiBackend(onEvent = { it.dispatchToBridge() }) + * backend.applyJson(patchJson) + * setContent { backend.Content(rootId) } * ``` */ class LuiBackend( - val eventSink: LuiEventSink = LuiEventSink {}, + val onEvent: (LuiEvent) -> Unit = {}, val extensions: LuiExtensionRegistry = LuiExtensionRegistry(), + val icons: LuiIconResolver = LuiIconResolver.DEFAULT, ) { internal val tree = LuiRetainedTree(extensions) @@ -43,12 +44,13 @@ class LuiBackend( internal var lastError: String? = null private set - fun node(id: Int): LuiNode? = tree.nodes[id] - fun extensionNode(id: Int): LuiExtensionNode? = tree.extensionNodes[id] + fun node(id: Long): LuiNode? = tree.nodes[id] + fun extensionNode(id: Long): LuiExtensionNode? = tree.extensionNodes[id] /** Apply one JSON patch batch. Returns false (state untouched) when the * batch fails validation — mirrors the other backends' reject semantics. */ - fun applyBatch(source: String): Boolean { + fun applyJson(source: String?): Boolean { + if (source.isNullOrEmpty()) return false return try { tree.apply(LuiPatchBatch.parse(source)) revision++ @@ -60,13 +62,16 @@ class LuiBackend( } } - fun emit(event: LuiEvent) = eventSink.onEvent(event) + /** Alias of [applyJson] kept for internal callers. */ + fun applyBatch(source: String): Boolean = applyJson(source) + + fun emit(event: LuiEvent) = onEvent(event) internal fun emitExtensionEvent( - node: Int, + node: Long, identifier: String, name: String, - values: Map, + values: Map, ) = emit(LuiEvent.Extension(node, identifier, name, values)) fun registerImage(id: Int, image: ImageBitmap) { @@ -87,14 +92,22 @@ class LuiBackend( mediaSurfaces.remove(id) } - /** Renders the retained tree's roots inside the ambient LuiTheme. */ + /** + * Renders a node of the retained tree inside the ambient LuiTheme. + * Pass the id the bridge reports via `rootNode()`; a negative id + * renders every current root. + */ @Composable - fun Content() { + fun Content(rootId: Long = -1L) { // Read the revision so this composable recomposes on each batch. val applied = revision if (applied == 0) return - for (rootId in tree.rootIds) { + if (rootId >= 0) { LuiNodeView(backend = this, id = rootId) + return + } + for (id in tree.rootIds) { + LuiNodeView(backend = this, id = id) } } } diff --git a/platform/android/lui/src/main/kotlin/dev/lui/LuiBridge.kt b/platform/android/lui/src/main/kotlin/dev/lui/LuiBridge.kt index 0b284a04..c8a560ca 100644 --- a/platform/android/lui/src/main/kotlin/dev/lui/LuiBridge.kt +++ b/platform/android/lui/src/main/kotlin/dev/lui/LuiBridge.kt @@ -94,75 +94,75 @@ object LuiBridge { return result.get() } - fun appear(node: Int) = dispatch { nativeAppear(node.toLong()) } + fun appear(node: Long) = dispatch { nativeAppear(node) } - fun press(node: Int) = dispatch { nativePress(node.toLong()) } + fun press(node: Long) = dispatch { nativePress(node) } - fun longPress(node: Int) = dispatch { nativeLongPress(node.toLong()) } + fun longPress(node: Long) = dispatch { nativeLongPress(node) } - fun textChanged(node: Int, text: String) = dispatch { - nativeTextChanged(node.toLong(), text) + fun textChanged(node: Long, text: String) = dispatch { + nativeTextChanged(node, text) } - fun submit(node: Int) = dispatch { nativeSubmit(node.toLong()) } + fun submit(node: Long) = dispatch { nativeSubmit(node) } - fun dismiss(node: Int) = dispatch { nativeDismiss(node.toLong()) } + fun dismiss(node: Long) = dispatch { nativeDismiss(node) } - fun picked(node: Int, payload: String) = dispatch { - nativePicked(node.toLong(), payload) + fun picked(node: Long, payload: String) = dispatch { + nativePicked(node, payload) } - fun doublePress(node: Int) = dispatch { nativeDoublePress(node.toLong()) } + fun doublePress(node: Long) = dispatch { nativeDoublePress(node) } - fun toggleChanged(node: Int, checked: Boolean) = dispatch { - nativeToggleChanged(node.toLong(), if (checked) 1 else 0) + fun toggleChanged(node: Long, checked: Boolean) = dispatch { + nativeToggleChanged(node, if (checked) 1 else 0) } - fun radioChanged(node: Int) = dispatch { nativeRadioChanged(node.toLong()) } + fun radioChanged(node: Long) = dispatch { nativeRadioChanged(node) } - fun sliderChanged(node: Int, fraction: Double) = dispatch { - nativeSliderChanged(node.toLong(), fraction) + fun sliderChanged(node: Long, fraction: Double) = dispatch { + nativeSliderChanged(node, fraction) } - fun pressDetail(node: Int, detail: LuiPointerDetail) = dispatch { + fun pressDetail(node: Long, detail: LuiPointerDetail) = dispatch { nativePressDetail( - node.toLong(), detail.x, detail.y, + node, detail.x, detail.y, detail.modifiers, detail.button, detail.targetClass, ) } - fun pointerDown(node: Int, detail: LuiPointerDetail) = dispatch { + fun pointerDown(node: Long, detail: LuiPointerDetail) = dispatch { nativePointerDown( - node.toLong(), detail.x, detail.y, + node, detail.x, detail.y, detail.modifiers, detail.button, detail.targetClass, ) } - fun pointerUp(node: Int, detail: LuiPointerDetail) = dispatch { + fun pointerUp(node: Long, detail: LuiPointerDetail) = dispatch { nativePointerUp( - node.toLong(), detail.x, detail.y, + node, detail.x, detail.y, detail.modifiers, detail.button, detail.targetClass, ) } - fun pointerEnter(node: Int) = dispatch { nativePointerEnter(node.toLong()) } + fun pointerEnter(node: Long) = dispatch { nativePointerEnter(node) } - fun pointerLeave(node: Int) = dispatch { nativePointerLeave(node.toLong()) } + fun pointerLeave(node: Long) = dispatch { nativePointerLeave(node) } - fun contextMenuPress(node: Int, detail: LuiPointerDetail) = dispatch { + fun contextMenuPress(node: Long, detail: LuiPointerDetail) = dispatch { nativeContextMenuPress( - node.toLong(), detail.x, detail.y, + node, detail.x, detail.y, detail.modifiers, detail.button, detail.targetClass, ) } fun extensionEvent( - node: Int, + node: Long, identifier: String, name: String, jsonValues: String, ) = dispatch { - nativeExtensionEvent(node.toLong(), identifier, name, jsonValues) + nativeExtensionEvent(node, identifier, name, jsonValues) } fun stop() = dispatch { nativeStop() } @@ -280,7 +280,7 @@ fun LuiEvent.dispatchToBridge() { values.entries.joinToString( prefix = "{", postfix = "}", separator = ",", - ) { (key, value) -> "\"$key\":${value.toJsonLiteral()}" }, + ) { (key, value) -> "\"$key\":${luiScalarLiteral(value)}" }, ) // Events the mobile bridge has no dedicated export for; the OCaml // side handles them through other paths or ignores them. @@ -291,3 +291,13 @@ fun LuiEvent.dispatchToBridge() { -> Unit } } + +/** Serializes one extension-event scalar (String/Number/Boolean/null) to + * its JSON literal. */ +private fun luiScalarLiteral(value: Any?): String = when (value) { + null -> "null" + is String -> kotlinx.serialization.json.JsonPrimitive(value).toString() + is Number -> value.toString() + is Boolean -> value.toString() + else -> "null" +} diff --git a/platform/android/lui/src/main/kotlin/dev/lui/LuiEvents.kt b/platform/android/lui/src/main/kotlin/dev/lui/LuiEvents.kt index d85aa94c..5cfa23db 100644 --- a/platform/android/lui/src/main/kotlin/dev/lui/LuiEvents.kt +++ b/platform/android/lui/src/main/kotlin/dev/lui/LuiEvents.kt @@ -11,34 +11,45 @@ data class LuiPointerDetail( val targetClass: String = "", ) -/** Events sent from the platform backend toward OCaml. */ +/** Events sent from the platform backend toward OCaml. Node ids are the + * wire int64 values. */ sealed class LuiEvent { - data class Press(val node: Int) : LuiEvent() - data class LongPress(val node: Int) : LuiEvent() - data class TextChanged(val node: Int, val text: String) : LuiEvent() - data class Submit(val node: Int) : LuiEvent() - data class ToggleChanged(val node: Int, val checked: Boolean) : LuiEvent() - data class Change(val node: Int) : LuiEvent() - data class ValueChanged(val node: Int, val value: Double) : LuiEvent() - data class Dismiss(val node: Int) : LuiEvent() - data class DoublePress(val node: Int) : LuiEvent() - data class Appear(val node: Int) : LuiEvent() - data class ScrollCompleted(val node: Int, val token: Int, val outcome: String) : LuiEvent() - data class VisibleRange(val node: Int, val first: Int, val last: Int) : LuiEvent() - data class Picked(val node: Int, val payload: String) : LuiEvent() - data class PressModifiers(val node: Int, val modifiers: Int) : LuiEvent() - data class PressDetail(val node: Int, val detail: LuiPointerDetail) : LuiEvent() - data class PointerDown(val node: Int, val detail: LuiPointerDetail) : LuiEvent() - data class PointerUp(val node: Int, val detail: LuiPointerDetail) : LuiEvent() - data class PointerEnter(val node: Int) : LuiEvent() - data class PointerLeave(val node: Int) : LuiEvent() - data class ContextMenuPress(val node: Int, val detail: LuiPointerDetail) : LuiEvent() - data class Load(val node: Int) : LuiEvent() + abstract val node: Long + + data class Appear(override val node: Long) : LuiEvent() + data class Press(override val node: Long) : LuiEvent() + data class LongPress(override val node: Long) : LuiEvent() + data class DoublePress(override val node: Long) : LuiEvent() + data class Change(override val node: Long) : LuiEvent() + data class Submit(override val node: Long) : LuiEvent() + data class Dismiss(override val node: Long) : LuiEvent() + data class TextChanged(override val node: Long, val text: String) : LuiEvent() + data class ToggleChanged(override val node: Long, val checked: Boolean) : LuiEvent() + data class ValueChanged(override val node: Long, val value: Double) : LuiEvent() + data class Picked(override val node: Long, val payload: String) : LuiEvent() + data class VisibleRange(override val node: Long, val first: Long, val last: Long) : LuiEvent() + data class ScrollCompleted( + override val node: Long, + val token: Long, + val outcome: String, + ) : LuiEvent() + + data class PressModifiers(override val node: Long, val modifiers: Int) : LuiEvent() + data class PressDetail(override val node: Long, val detail: LuiPointerDetail) : LuiEvent() + data class PointerDown(override val node: Long, val detail: LuiPointerDetail) : LuiEvent() + data class PointerUp(override val node: Long, val detail: LuiPointerDetail) : LuiEvent() + data class PointerEnter(override val node: Long) : LuiEvent() + data class PointerLeave(override val node: Long) : LuiEvent() + data class ContextMenuPress(override val node: Long, val detail: LuiPointerDetail) : LuiEvent() + data class Load(override val node: Long) : LuiEvent() + + /** `values` is a flat object of scalar values (String/Number/Boolean/null) + * validated OCaml-side against the extension's declared event schema. */ data class Extension( - val node: Int, + override val node: Long, val identifier: String, val name: String, - val values: Map, + val values: Map, ) : LuiEvent() } diff --git a/platform/android/lui/src/main/kotlin/dev/lui/LuiExtension.kt b/platform/android/lui/src/main/kotlin/dev/lui/LuiExtension.kt index ebb08fe3..ac2804be 100644 --- a/platform/android/lui/src/main/kotlin/dev/lui/LuiExtension.kt +++ b/platform/android/lui/src/main/kotlin/dev/lui/LuiExtension.kt @@ -39,26 +39,51 @@ class LuiExtensionEventSchema( /** View-model passed to extension composable builders. */ class LuiExtensionContext internal constructor( internal val backend: LuiBackend, - val nodeId: Int, + val nodeId: Long, val identifier: String, val properties: Map, - val children: List = emptyList(), - internal val renderChild: @Composable (Int) -> Unit, + val children: List = emptyList(), + internal val renderChild: @Composable (Long) -> Unit, ) { fun string(name: String): String? = properties[name]?.stringValue fun bool(name: String): Boolean? = properties[name]?.boolValue fun int(name: String): Int? = properties[name]?.intValue fun double(name: String): Double? = properties[name]?.doubleValue + /** Boolean prop that defaults to false when unset. */ + fun flag(name: String): Boolean = bool(name) ?: false + + /** Property as an untyped scalar (String/Int/Double/Boolean). */ + @Suppress("UNCHECKED_CAST") + fun property(name: String): T? = properties[name]?.scalarValue as? T + fun emitEvent(name: String, values: Map = emptyMap()) { + backend.emitExtensionEvent( + nodeId, + identifier, + name, + values.mapValues { it.value.scalarValue }, + ) + } + + fun emit(name: String, values: Map) { backend.emitExtensionEvent(nodeId, identifier, name, values) } + fun emit(name: String, vararg values: Pair) = + emit(name, values.toMap()) + /** The extension node behind a declared-child id, if it is one. */ - fun extensionChild(id: Int): LuiExtensionNode? = backend.extensionNode(id) + fun extensionChild(id: Long): LuiExtensionNode? = backend.extensionNode(id) @Composable - fun child(id: Int) = renderChild(id) + fun child(id: Long) = renderChild(id) + + /** Renders every standard child in order. */ + @Composable + fun Children() { + children.forEach { child(it) } + } } class LuiExtensionRegistration internal constructor( @@ -125,6 +150,19 @@ class LuiExtensionRegistry { ), ) + /** Minimal registration: `register("id", fingerprint) { context -> ... }`. */ + fun register( + identifier: String, + fingerprint: String, + builder: @Composable (LuiExtensionContext) -> Unit, + ) = register( + LuiExtensionRegistration( + identifier = identifier, + fingerprint = fingerprint, + builder = { builder(this) }, + ), + ) + /** A tweak wraps exactly one child with platform-specific styling. */ fun registerTweak( identifier: String, diff --git a/platform/android/lui/src/main/kotlin/dev/lui/LuiIcons.kt b/platform/android/lui/src/main/kotlin/dev/lui/LuiIcons.kt index 151fa0e3..41a2da00 100644 --- a/platform/android/lui/src/main/kotlin/dev/lui/LuiIcons.kt +++ b/platform/android/lui/src/main/kotlin/dev/lui/LuiIcons.kt @@ -1,80 +1,118 @@ package dev.lui import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.rounded.* -import androidx.compose.material.icons.rounded.* +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.automirrored.filled.ArrowForward +import androidx.compose.material.icons.automirrored.filled.CallMerge +import androidx.compose.material.icons.automirrored.filled.HelpOutline +import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft +import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight +import androidx.compose.material.icons.automirrored.filled.Send +import androidx.compose.material.icons.automirrored.filled.Undo +import androidx.compose.material.icons.filled.* import androidx.compose.ui.graphics.vector.ImageVector -/** Maps LUI built-in icon names (schema/components.json's icon vocabulary) - * to Material symbols. `app:` icons resolve through the host's - * registered app-icon table first. */ -object LuiIcons { - private val registered = mutableMapOf() +/** + * Resolves the wire `icon` prop (a symbolic name such as "send" or + * "chevron-right") to an ImageVector. Apps chain their own resolver in + * front of [DEFAULT] via [or] — the backend resolver returns null for + * names it doesn't own (e.g. `app:`-prefixed app icons) so they fall + * through to the next resolver. + */ +fun interface LuiIconResolver { + fun icon(name: String): ImageVector? - /** Hosts register their own `app:` icon assets here. */ - fun registerAppIcon(name: String, icon: ImageVector) { - registered[name] = icon + fun or(other: LuiIconResolver): LuiIconResolver = LuiIconResolver { name -> + icon(name) ?: other.icon(name) } - fun appIcon(name: String): ImageVector? = registered[name] + companion object { + val DEFAULT = LuiIconResolver { name -> defaultIcons[name] } - fun resolve(name: String): ImageVector { - if (name.startsWith("app:")) { - return registered[name.substring(4)] ?: Icons.AutoMirrored.Rounded.HelpOutline - } - return when (name) { - "alert" -> Icons.Rounded.Warning - "archive" -> Icons.Rounded.Archive - "arrow-down" -> Icons.Rounded.ArrowDownward - "arrow-right" -> Icons.AutoMirrored.Rounded.ArrowForward - "arrow-up" -> Icons.Rounded.ArrowUpward - "check" -> Icons.Rounded.Check - "check-circle" -> Icons.Rounded.CheckCircle - "chevron-down" -> Icons.Rounded.KeyboardArrowDown - "chevron-left" -> Icons.AutoMirrored.Rounded.KeyboardArrowLeft - "chevron-right" -> Icons.AutoMirrored.Rounded.KeyboardArrowRight - "chevron-up" -> Icons.Rounded.KeyboardArrowUp - "circle-dot" -> Icons.Rounded.Adjust - "clock" -> Icons.Rounded.Schedule - "copy" -> Icons.Rounded.ContentCopy - "download" -> Icons.Rounded.Download - "edit" -> Icons.Rounded.Edit - "ellipsis" -> Icons.Rounded.MoreHoriz - "external-link" -> Icons.Rounded.OpenInNew - "eye" -> Icons.Rounded.Visibility - "file-text" -> Icons.Rounded.Description - "folder" -> Icons.Rounded.Folder - "folder-open" -> Icons.Rounded.FolderOpen - "git-branch" -> Icons.Rounded.AccountTree - "git-merge" -> Icons.Rounded.Merge - "git-pull-request" -> Icons.AutoMirrored.Rounded.CallMerge - "info" -> Icons.Rounded.Info - "menu" -> Icons.Rounded.Menu - "mic" -> Icons.Rounded.Mic - "moon" -> Icons.Rounded.DarkMode - "music" -> Icons.Rounded.MusicNote - "panel-left" -> Icons.AutoMirrored.Rounded.ViewSidebar - "panel-right" -> Icons.AutoMirrored.Rounded.ViewSidebar - "pause" -> Icons.Rounded.Pause - "play" -> Icons.Rounded.PlayArrow - "plus" -> Icons.Rounded.Add - "refresh-cw" -> Icons.Rounded.Refresh - "repeat" -> Icons.Rounded.Repeat - "save" -> Icons.Rounded.Save - "search" -> Icons.Rounded.Search - "send" -> Icons.AutoMirrored.Rounded.Send - "settings" -> Icons.Rounded.Settings - "shuffle" -> Icons.Rounded.Shuffle - "skip-back" -> Icons.Rounded.SkipPrevious - "skip-forward" -> Icons.Rounded.SkipNext - "sun" -> Icons.Rounded.LightMode - "terminal" -> Icons.Rounded.Terminal - "trash" -> Icons.Rounded.Delete - "volume" -> Icons.Rounded.VolumeUp - "wrench" -> Icons.Rounded.Build - "x" -> Icons.Rounded.Close - "x-circle" -> Icons.Rounded.Cancel - else -> Icons.AutoMirrored.Rounded.HelpOutline - } + private val defaultIcons: Map = mapOf( + "add" to Icons.Filled.Add, + "add-circle" to Icons.Filled.AddCircle, + "alert" to Icons.Filled.Warning, + "arrow-back" to Icons.AutoMirrored.Filled.ArrowBack, + "arrow-down" to Icons.Filled.ArrowDropDown, + "arrow-forward" to Icons.AutoMirrored.Filled.ArrowForward, + "arrow-left" to Icons.AutoMirrored.Filled.ArrowBack, + "arrow-right" to Icons.AutoMirrored.Filled.ArrowForward, + "arrow-up" to Icons.Filled.KeyboardArrowUp, + "attach-file" to Icons.Filled.Add, + "audio-lines" to Icons.Filled.Star, + "audio-waveform" to Icons.Filled.Star, + "bell" to Icons.Filled.Notifications, + "bold" to Icons.Filled.Edit, + "bookmark" to Icons.Filled.Bookmark, + "brush" to Icons.Filled.Edit, + "calendar" to Icons.Filled.DateRange, + "camera" to Icons.Filled.Add, + "check" to Icons.Filled.Check, + "check-circle" to Icons.Filled.CheckCircle, + "chevron-down" to Icons.Filled.KeyboardArrowDown, + "chevron-left" to Icons.AutoMirrored.Filled.KeyboardArrowLeft, + "chevron-right" to Icons.AutoMirrored.Filled.KeyboardArrowRight, + "chevron-up" to Icons.Filled.KeyboardArrowUp, + "clock" to Icons.Filled.Star, + "close" to Icons.Filled.Close, + "cloud" to Icons.Filled.Star, + "code" to Icons.Filled.Edit, + "copy" to Icons.Filled.Add, + "dice" to Icons.Filled.Star, + "dots" to Icons.Filled.MoreVert, + "download" to Icons.Filled.Share, + "edit" to Icons.Filled.Edit, + "external-link" to Icons.Filled.Share, + "eye" to Icons.Filled.Star, + "file" to Icons.Filled.Add, + "flag" to Icons.Filled.Star, + "folder" to Icons.Filled.Star, + "hash" to Icons.Filled.Tag, + "heading" to Icons.Filled.Edit, + "help" to Icons.AutoMirrored.Filled.HelpOutline, + "history" to Icons.Filled.Star, + "home" to Icons.Filled.Home, + "image" to Icons.Filled.Star, + "info" to Icons.Filled.Info, + "italic" to Icons.Filled.Edit, + "link" to Icons.Filled.Share, + "list" to Icons.Filled.List, + "loader" to Icons.Filled.Refresh, + "lock" to Icons.Filled.Lock, + "logout" to Icons.Filled.ExitToApp, + "menu" to Icons.Filled.Menu, + "merge" to Icons.AutoMirrored.Filled.CallMerge, + "message" to Icons.Filled.Email, + "mic" to Icons.Filled.Edit, + "minus" to Icons.Filled.Clear, + "more-horizontal" to Icons.Filled.MoreVert, + "more-vertical" to Icons.Filled.MoreVert, + "paperclip" to Icons.Filled.Add, + "pause" to Icons.Filled.Star, + "pencil" to Icons.Filled.Edit, + "play" to Icons.Filled.PlayArrow, + "plus" to Icons.Filled.Add, + "refresh" to Icons.Filled.Refresh, + "search" to Icons.Filled.Search, + "send" to Icons.AutoMirrored.Filled.Send, + "settings" to Icons.Filled.Settings, + "share" to Icons.Filled.Share, + "sparkles" to Icons.Filled.Star, + "star" to Icons.Filled.Star, + "stop" to Icons.Filled.Close, + "sync" to Icons.Filled.Refresh, + "table" to Icons.Filled.List, + "tag" to Icons.Filled.Tag, + "task" to Icons.Filled.CheckCircle, + "trash" to Icons.Filled.Delete, + "undo" to Icons.AutoMirrored.Filled.Undo, + "upload" to Icons.Filled.Share, + "user" to Icons.Filled.Person, + "video" to Icons.Filled.PlayArrow, + "warning" to Icons.Filled.Warning, + "wifi" to Icons.Filled.Star, + "x" to Icons.Filled.Close, + ) } } diff --git a/platform/android/lui/src/main/kotlin/dev/lui/LuiNodeView.kt b/platform/android/lui/src/main/kotlin/dev/lui/LuiNodeView.kt index 3675ef15..6ce4f08c 100644 --- a/platform/android/lui/src/main/kotlin/dev/lui/LuiNodeView.kt +++ b/platform/android/lui/src/main/kotlin/dev/lui/LuiNodeView.kt @@ -39,6 +39,9 @@ import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.KeyboardArrowDown import androidx.compose.material.icons.filled.KeyboardArrowRight +import androidx.compose.material.icons.filled.Person +import androidx.compose.material.icons.filled.Search +import androidx.compose.material.icons.filled.Star import androidx.compose.material.icons.filled.Remove import androidx.compose.material3.AlertDialog import androidx.compose.material3.Card @@ -97,7 +100,7 @@ import androidx.compose.ui.unit.dp * match the mobile reference behavior. */ @Composable -fun LuiNodeView(backend: LuiBackend, id: Int) { +fun LuiNodeView(backend: LuiBackend, id: Long) { val node = backend.node(id) if (node == null) { LuiExtensionView(backend, id) @@ -120,7 +123,7 @@ fun LuiNodeView(backend: LuiBackend, id: Int) { * child as a popup anchored to the long-press point. */ @Composable -private fun LuiChromeSurface(backend: LuiBackend, node: LuiNode, id: Int) { +private fun LuiChromeSurface(backend: LuiBackend, node: LuiNode, id: Long) { var contextMenuAt by remember(id) { mutableStateOf(null) } val contextMenuId = node.children.firstOrNull { backend.node(it)?.kind == LuiNodeKind.contextMenu } @@ -155,7 +158,7 @@ private fun LuiChromeSurface(backend: LuiBackend, node: LuiNode, id: Int) { * tweaks use the same path (their registration wraps the single child). */ @Composable -private fun LuiExtensionView(backend: LuiBackend, id: Int) { +private fun LuiExtensionView(backend: LuiBackend, id: Long) { val node = backend.extensionNode(id) ?: return val builder = backend.extensions.registration(node.identifier)?.builder ?: return val context = LuiExtensionContext( @@ -176,7 +179,7 @@ private fun gapOf(node: LuiNode): Int = 0 } -private fun emitValueChange(backend: LuiBackend, node: LuiNode, id: Int, value: Double) { +private fun emitValueChange(backend: LuiBackend, node: LuiNode, id: Long, value: Double) { if (!node.isEnabled()) return val emitted = when (node.kind) { LuiNodeKind.numberStepper -> { @@ -191,7 +194,7 @@ private fun emitValueChange(backend: LuiBackend, node: LuiNode, id: Int, value: /** Applies `grow` to a child inside a Row. */ @Composable -private fun RowScope.GrowChildRow(backend: LuiBackend, childId: Int) { +private fun RowScope.GrowChildRow(backend: LuiBackend, childId: Long) { val grow = backend.node(childId)?.prop("grow")?.numberValue ?: 0.0 if (grow > 0) { Box(Modifier.weight(grow.toFloat().coerceAtLeast(0.001f))) { @@ -204,7 +207,7 @@ private fun RowScope.GrowChildRow(backend: LuiBackend, childId: Int) { /** Applies `grow` to a child inside a Column. */ @Composable -private fun ColumnScope.GrowChildColumn(backend: LuiBackend, childId: Int) { +private fun ColumnScope.GrowChildColumn(backend: LuiBackend, childId: Long) { val grow = backend.node(childId)?.prop("grow")?.numberValue ?: 0.0 if (grow > 0) { Box(Modifier.weight(grow.toFloat().coerceAtLeast(0.001f))) { @@ -223,7 +226,7 @@ private fun ColumnScope.GrowChildColumn(backend: LuiBackend, childId: Int) { private fun LuiNodeContent( backend: LuiBackend, node: LuiNode, - id: Int, + id: Long, modifier: Modifier, ) { val children = node.children.filter { childId -> @@ -490,19 +493,21 @@ private fun LuiNodeContent( ?: MaterialTheme.colorScheme.primary, ) - LuiNodeKind.icon -> Icon( - LuiIcons.resolve(node.propString("name") ?: ""), - contentDescription = node.propString("accessibility-label"), - modifier = modifier.size( - when (node.propString("size")) { - "sm" -> 16.dp - "lg", "icon" -> 24.dp - else -> 18.dp - }, - ), - tint = luiThemeColor(node.propString("foreground"), foreground = true) - ?: MaterialTheme.colorScheme.onSurface, - ) + LuiNodeKind.icon -> backend.icons.icon(node.propString("name") ?: "")?.let { vector -> + Icon( + vector, + contentDescription = node.propString("accessibility-label"), + modifier = modifier.size( + when (node.propString("size")) { + "sm" -> 16.dp + "lg", "icon" -> 24.dp + else -> 18.dp + }, + ), + tint = luiThemeColor(node.propString("foreground"), foreground = true) + ?: MaterialTheme.colorScheme.onSurface, + ) + } LuiNodeKind.select -> LuiSelect(backend, node, id, modifier) @@ -623,7 +628,7 @@ private fun LuiGrid( backend: LuiBackend, node: LuiNode, modifier: Modifier, - children: List, + children: List, ) { val requested = node.propInt("columns") val columns = if (requested > 0) requested else maxOf(children.size, 1) @@ -651,7 +656,7 @@ private fun LuiStackLike( backend: LuiBackend, node: LuiNode, modifier: Modifier, - children: List, + children: List, ) { val menuId = children.firstOrNull { backend.node(it)?.kind == LuiNodeKind.dropdownMenu } val tooltipId = children.firstOrNull { @@ -719,7 +724,7 @@ private fun LuiEdgeInset( backend: LuiBackend, node: LuiNode, modifier: Modifier, - children: List, + children: List, ) { val visible = node.propBool("visible", true) val edge = node.propString("edge") ?: "top" @@ -762,7 +767,7 @@ private fun LuiOverlay( backend: LuiBackend, node: LuiNode, modifier: Modifier, - children: List, + children: List, ) { val fallback = overlayAlignment(node.propString("alignment")) Box(modifier) { @@ -788,7 +793,7 @@ private fun LuiBubble( backend: LuiBackend, node: LuiNode, modifier: Modifier, - children: List, + children: List, ) { Column(modifier.width(IntrinsicSize.Max)) { Box { children.forEach { LuiNodeView(backend, it) } } @@ -821,9 +826,9 @@ private fun LuiBubble( private fun LuiButton( backend: LuiBackend, node: LuiNode, - id: Int, + id: Long, modifier: Modifier, - children: List, + children: List, ) { val enabled = node.isEnabled() val variant = node.propString("variant") ?: "default" @@ -831,7 +836,7 @@ private fun LuiButton( val selected = node.propBool("selected") val foreground = luiThemeColor(node.propString("foreground"), foreground = true) - val icon = node.propString("icon")?.let { LuiIcons.resolve(it) } + val icon = node.propString("icon")?.let { backend.icons.icon(it) } val iconSize = when (node.propString("size")) { "sm" -> 16.dp "lg", "icon" -> 24.dp @@ -930,7 +935,7 @@ private fun LuiButton( /** Toggle buttons keep a local selection so taps feel instant; the model * `selected` prop re-syncs on the next patch. */ @Composable -private fun LuiToggleButton(backend: LuiBackend, node: LuiNode, id: Int, modifier: Modifier) { +private fun LuiToggleButton(backend: LuiBackend, node: LuiNode, id: Long, modifier: Modifier) { val enabled = node.isEnabled() var selected by remember(id) { mutableStateOf(node.propBool("selected")) } val modelSelected = node.propBool("selected") @@ -954,7 +959,7 @@ private fun LuiToggleButton(backend: LuiBackend, node: LuiNode, id: Int, modifie private fun LuiTextControl( backend: LuiBackend, node: LuiNode, - id: Int, + id: Long, modifier: Modifier, ) { val multiline = node.kind == LuiNodeKind.textarea @@ -991,7 +996,7 @@ private fun LuiTextControl( trailingIcon = if (combobox || search) { { Icon( - if (combobox) Icons.Filled.KeyboardArrowDown else LuiIcons.resolve("search"), + if (combobox) Icons.Filled.KeyboardArrowDown else backend.icons.icon("search") ?: Icons.Filled.Search, contentDescription = null, modifier = if (combobox && enabled) { Modifier.pointerInput(id) { @@ -1013,7 +1018,7 @@ private fun LuiTextControl( } @Composable -private fun LuiSelect(backend: LuiBackend, node: LuiNode, id: Int, modifier: Modifier) { +private fun LuiSelect(backend: LuiBackend, node: LuiNode, id: Long, modifier: Modifier) { val enabled = node.isEnabled() val title = node.propString("accessibility-label") ?: node.propString("placeholder") ?: "" val value = node.text().ifEmpty { node.propString("placeholder") ?: "" } @@ -1039,7 +1044,7 @@ private fun LuiSelect(backend: LuiBackend, node: LuiNode, id: Int, modifier: Mod /** A menu entry (used both inside dropdown menus and context menus). */ @Composable -internal fun LuiMenuEntry(backend: LuiBackend, childId: Int) { +internal fun LuiMenuEntry(backend: LuiBackend, childId: Long) { val child = backend.node(childId) ?: return when (child.kind) { LuiNodeKind.divider -> HorizontalDivider() @@ -1050,11 +1055,11 @@ internal fun LuiMenuEntry(backend: LuiBackend, childId: Int) { } @Composable -private fun LuiMenuItem(backend: LuiBackend, node: LuiNode, id: Int, modifier: Modifier) { +private fun LuiMenuItem(backend: LuiBackend, node: LuiNode, id: Long, modifier: Modifier) { val submenuId = node.children.firstOrNull { backend.node(it)?.kind == LuiNodeKind.dropdownMenu } val enabled = node.isEnabled() - val icon = node.propString("icon")?.let { LuiIcons.resolve(it) } + val icon = node.propString("icon")?.let { backend.icons.icon(it) } val selected = node.propBool("selected") val destructive = node.propString("variant") == "destructive" @@ -1103,10 +1108,10 @@ private fun LuiMenuItem(backend: LuiBackend, node: LuiNode, id: Int, modifier: M } @Composable -private fun LuiMenuTrigger(backend: LuiBackend, node: LuiNode, id: Int, modifier: Modifier) { +private fun LuiMenuTrigger(backend: LuiBackend, node: LuiNode, id: Long, modifier: Modifier) { val menuId = node.children.firstOrNull { backend.node(it)?.kind == LuiNodeKind.dropdownMenu } val enabled = node.isEnabled() - val icon = node.propString("icon")?.let { LuiIcons.resolve(it) } + val icon = node.propString("icon")?.let { backend.icons.icon(it) } val iconSize = if (node.text().isEmpty()) 24.dp else 16.dp var open by remember(id) { mutableStateOf(false) } @@ -1154,12 +1159,12 @@ private fun LuiMenuTrigger(backend: LuiBackend, node: LuiNode, id: Int, modifier private fun LuiListItem( backend: LuiBackend, node: LuiNode, - id: Int, + id: Long, modifier: Modifier, - children: List, + children: List, ) { val enabled = node.isEnabled() - val icon = node.propString("icon")?.let { LuiIcons.resolve(it) } + val icon = node.propString("icon")?.let { backend.icons.icon(it) } ListItem( headlineContent = { if (children.isEmpty()) { @@ -1224,7 +1229,7 @@ private fun LuiAvatar(backend: LuiBackend, node: LuiNode, modifier: Modifier) { Text(node.text(), style = MaterialTheme.typography.labelLarge) } else { Icon( - LuiIcons.resolve("person"), + backend.icons.icon("person") ?: Icons.Filled.Person, contentDescription = node.propString("accessibility-label"), tint = MaterialTheme.colorScheme.onSecondaryContainer, ) @@ -1279,7 +1284,7 @@ private fun LuiFileImage(backend: LuiBackend, node: LuiNode, modifier: Modifier) } else { Box(modifier.then(clip), contentAlignment = Alignment.Center) { Icon( - LuiIcons.resolve("image"), + backend.icons.icon("image") ?: Icons.Filled.Star, contentDescription = null, tint = MaterialTheme.colorScheme.onSurfaceVariant, ) @@ -1314,7 +1319,7 @@ private fun LuiMediaSurface(backend: LuiBackend, node: LuiNode, modifier: Modifi } @Composable -private fun LuiStep(backend: LuiBackend, node: LuiNode, id: Int, modifier: Modifier) { +private fun LuiStep(backend: LuiBackend, node: LuiNode, id: Long, modifier: Modifier) { val parent = node.parent?.let { backend.node(it) } val index = parent?.children?.indexOf(id) ?: 0 val count = parent?.children?.size ?: 1 @@ -1384,7 +1389,7 @@ private fun LuiStep(backend: LuiBackend, node: LuiNode, id: Int, modifier: Modif } @Composable -private fun LuiTimelineItem(backend: LuiBackend, node: LuiNode, id: Int, modifier: Modifier) { +private fun LuiTimelineItem(backend: LuiBackend, node: LuiNode, id: Long, modifier: Modifier) { val variantColor = when (node.propString("variant")) { "primary" -> MaterialTheme.colorScheme.primary "destructive" -> MaterialTheme.colorScheme.error @@ -1421,7 +1426,7 @@ private fun LuiTimelineItem(backend: LuiBackend, node: LuiNode, id: Int, modifie val indicator = node.propString("indicator") ?: "" when { iconName.isNotEmpty() -> Icon( - LuiIcons.resolve(iconName), + backend.icons.icon(iconName) ?: Icons.Filled.Star, contentDescription = null, modifier = Modifier.size(16.dp), tint = variantColor, @@ -1488,7 +1493,7 @@ private fun LuiInputGroup( backend: LuiBackend, node: LuiNode, modifier: Modifier, - children: List, + children: List, ) { Surface( modifier = modifier, @@ -1523,7 +1528,7 @@ private fun LuiBottomTabs(backend: LuiBackend, node: LuiNode, modifier: Modifier } }, icon = { - Icon(LuiIcons.resolve(dest.propString("icon") ?: ""), contentDescription = null) + backend.icons.icon(dest.propString("icon") ?: "")?.let { Icon(it, contentDescription = null) } }, label = { Text(dest.propString("title") ?: "") }, enabled = dest.isEnabled(), @@ -1537,9 +1542,9 @@ private fun LuiBottomTabs(backend: LuiBackend, node: LuiNode, modifier: Modifier private fun LuiAccordion( backend: LuiBackend, node: LuiNode, - id: Int, + id: Long, modifier: Modifier, - children: List, + children: List, ) { val expanded = node.propBool("selected") || node.propBool("expanded") Column(modifier) { @@ -1569,9 +1574,9 @@ private fun LuiAccordion( private fun LuiTableRow( backend: LuiBackend, node: LuiNode, - id: Int, + id: Long, modifier: Modifier, - children: List, + children: List, ) { val parent = node.parent?.let { backend.node(it) } val isLast = parent == null || parent.children.lastOrNull() == id @@ -1593,7 +1598,7 @@ private fun LuiTableRow( } @Composable -private fun LuiTableCell(backend: LuiBackend, node: LuiNode, id: Int, modifier: Modifier) { +private fun LuiTableCell(backend: LuiBackend, node: LuiNode, id: Long, modifier: Modifier) { val enabled = node.isEnabled() Text( node.text(), @@ -1624,9 +1629,9 @@ private fun LuiTableCell(backend: LuiBackend, node: LuiNode, id: Int, modifier: private fun LuiDrawer( backend: LuiBackend, node: LuiNode, - id: Int, + id: Long, modifier: Modifier, - children: List, + children: List, ) { val presented = node.propBool("selected") val width = node.propInt("width", 320) @@ -1669,9 +1674,9 @@ private fun LuiDrawer( private fun LuiSplit( backend: LuiBackend, node: LuiNode, - id: Int, + id: Long, modifier: Modifier, - children: List, + children: List, ) { val fraction = node.propDouble("value").coerceIn(0.05, 0.95) val enabled = node.isEnabled() @@ -1709,9 +1714,9 @@ private fun LuiSplit( private fun LuiToast( backend: LuiBackend, node: LuiNode, - id: Int, + id: Long, modifier: Modifier, - children: List, + children: List, ) { val duration = node.propInt("duration") if (duration > 0) { @@ -1740,7 +1745,7 @@ private fun LuiToolbar( backend: LuiBackend, node: LuiNode, modifier: Modifier, - children: List, + children: List, ) { val gap = gapOf(node) if (node.propString("orientation") == "vertical") { @@ -1777,7 +1782,7 @@ private fun LuiToolbar( */ @OptIn(ExperimentalMaterial3Api::class) @Composable -private fun LuiModalSurface(backend: LuiBackend, node: LuiNode, id: Int) { +private fun LuiModalSurface(backend: LuiBackend, node: LuiNode, id: Long) { val onDismiss = { backend.emit(LuiEvent.Dismiss(id)) } when (node.kind) { LuiNodeKind.dialog -> AlertDialog( diff --git a/platform/android/lui/src/main/kotlin/dev/lui/LuiRender.kt b/platform/android/lui/src/main/kotlin/dev/lui/LuiRender.kt index c73f6372..2516a145 100644 --- a/platform/android/lui/src/main/kotlin/dev/lui/LuiRender.kt +++ b/platform/android/lui/src/main/kotlin/dev/lui/LuiRender.kt @@ -28,6 +28,7 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect @@ -185,7 +186,7 @@ internal fun Modifier.luiChrome(node: LuiNode): Modifier { internal fun Modifier.luiGestures( backend: LuiBackend, node: LuiNode, - id: Int, + id: Long, onContextMenu: (Offset) -> Unit = {}, ): Modifier { var modifier: Modifier = this @@ -258,7 +259,7 @@ internal fun Modifier.luiGestures( /** Emits `Appear` once when an `appear-enabled` node first composes. */ @Composable -internal fun LuiAppear(backend: LuiBackend, node: LuiNode, id: Int) { +internal fun LuiAppear(backend: LuiBackend, node: LuiNode, id: Long) { if (node.propBool("appear-enabled")) { LaunchedEffect(id) { backend.emit(LuiEvent.Appear(id)) } } @@ -272,16 +273,16 @@ internal fun LuiAppear(backend: LuiBackend, node: LuiNode, id: Int) { @Composable internal fun LuiThemeScope(node: LuiNode, content: @Composable () -> Unit) { val inherited = LocalLuiSemanticColors.current - val ambientDark = LocalLuiDarkTheme.current + val ambientDark = LocalLuiThemeDark.current val schemeOverride = LuiThemeTokenDecoder.colorScheme(node.propString("theme-mode")) - val dark = schemeOverride ?: ambientDark + val dark = schemeOverride ?: (ambientDark ?: isSystemInDarkTheme()) val own = LuiThemeTokenDecoder.colors(node.propString("theme"), dark) if (own.isEmpty() && schemeOverride == null) { content() } else { CompositionLocalProvider( LocalLuiSemanticColors provides inherited + own, - LocalLuiDarkTheme provides dark, + LocalLuiThemeDark provides dark, ) { content() } diff --git a/platform/android/lui/src/main/kotlin/dev/lui/LuiSplit.kt b/platform/android/lui/src/main/kotlin/dev/lui/LuiSplit.kt index e2fc18fb..7054bd4f 100644 --- a/platform/android/lui/src/main/kotlin/dev/lui/LuiSplit.kt +++ b/platform/android/lui/src/main/kotlin/dev/lui/LuiSplit.kt @@ -14,6 +14,8 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.material3.HorizontalDivider +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -270,6 +272,7 @@ private fun LuiSplitPane(ext: LuiExtensionContext) { val isSelected = id == selectedPair?.first SplitTabChip( node = tab, + icons = ext.backend.icons, selected = isSelected, onSelect = { ext.emitEvent("pane-focused") @@ -296,12 +299,13 @@ private fun LuiSplitPane(ext: LuiExtensionContext) { @Composable private fun SplitTabChip( node: LuiExtensionNode, + icons: LuiIconResolver, selected: Boolean, onSelect: () -> Unit, onClose: () -> Unit, ) { val title = node.properties["title"]?.stringValue ?: "" - val icon = node.properties["icon"]?.stringValue?.let { LuiIcons.resolve(it) } + val icon = node.properties["icon"]?.stringValue?.let { icons.icon(it) } val dirty = node.properties["dirty"]?.boolValue == true val closable = node.properties["closable"]?.boolValue == true @@ -343,7 +347,7 @@ private fun SplitTabChip( if (closable) { IconButton(onClick = onClose, modifier = Modifier.size(20.dp)) { Icon( - LuiIcons.resolve("x"), + icons.icon("x") ?: Icons.Filled.Close, contentDescription = "Close", modifier = Modifier.size(12.dp), tint = MaterialTheme.colorScheme.onSurfaceVariant, diff --git a/platform/android/lui/src/main/kotlin/dev/lui/LuiStore.kt b/platform/android/lui/src/main/kotlin/dev/lui/LuiStore.kt index ca384dcd..4c4fa238 100644 --- a/platform/android/lui/src/main/kotlin/dev/lui/LuiStore.kt +++ b/platform/android/lui/src/main/kotlin/dev/lui/LuiStore.kt @@ -3,8 +3,8 @@ package dev.lui /** A retained standard node. Property keys are wire property names. */ class LuiNode( val kind: LuiNodeKind, - var parent: Int? = null, - val children: MutableList = mutableListOf(), + var parent: Long? = null, + val children: MutableList = mutableListOf(), val properties: MutableMap = mutableMapOf(), ) { fun copy(): LuiNode = LuiNode(kind, parent, children.toMutableList(), properties.toMutableMap()) @@ -14,8 +14,8 @@ class LuiNode( class LuiExtensionNode( val identifier: String, val fingerprint: String, - var parent: Int? = null, - val children: MutableList = mutableListOf(), + var parent: Long? = null, + val children: MutableList = mutableListOf(), val properties: MutableMap = mutableMapOf(), ) { fun copy(): LuiExtensionNode = @@ -32,51 +32,51 @@ class LuiRetainedTree(private val registry: LuiExtensionRegistry) { var generation: Int = 0 private set - var nodes: Map = emptyMap() + var nodes: Map = emptyMap() private set - var extensionNodes: Map = emptyMap() + var extensionNodes: Map = emptyMap() private set /** Nodes removed by the last applied batch (only `drop-node` deletes). */ - var lastDropped: Set = emptySet() + var lastDropped: Set = emptySet() private set - val rootIds: List + val rootIds: List get() = nodes.filterValues { it.parent == null }.keys.toList() + extensionNodes.filterValues { it.parent == null }.keys.toList() - fun node(id: Int): LuiNode? = nodes[id] - fun extensionNode(id: Int): LuiExtensionNode? = extensionNodes[id] - fun contains(id: Int): Boolean = nodes.containsKey(id) || extensionNodes.containsKey(id) + fun node(id: Long): LuiNode? = nodes[id] + fun extensionNode(id: Long): LuiExtensionNode? = extensionNodes[id] + fun contains(id: Long): Boolean = nodes.containsKey(id) || extensionNodes.containsKey(id) private fun parentOf( - nodes: Map, - ext: Map, - id: Int, - ): Int? = nodes[id]?.parent ?: ext[id]?.parent + nodes: Map, + ext: Map, + id: Long, + ): Long? = nodes[id]?.parent ?: ext[id]?.parent private fun childrenOf( - nodes: Map, - ext: Map, - id: Int, - ): MutableList? = nodes[id]?.children ?: ext[id]?.children + nodes: Map, + ext: Map, + id: Long, + ): MutableList? = nodes[id]?.children ?: ext[id]?.children private fun setParent( - nodes: Map, - ext: Map, - id: Int, - parent: Int?, + nodes: Map, + ext: Map, + id: Long, + parent: Long?, ) { nodes[id]?.let { it.parent = parent } ext[id]?.let { it.parent = parent } } - private fun requireNode(nodes: Map, id: Int): LuiNode = + private fun requireNode(nodes: Map, id: Long): LuiNode = nodes[id] ?: throw LuiBackendException("unknown node $id") private fun requireExtension( - ext: Map, - id: Int, + ext: Map, + id: Long, ): LuiExtensionNode = ext[id] ?: throw LuiBackendException("unknown extension node $id") @@ -89,7 +89,7 @@ class LuiRetainedTree(private val registry: LuiExtensionRegistry) { } val nextNodes = nodes.mapValues { it.value.copy() }.toMutableMap() val nextExt = extensionNodes.mapValues { it.value.copy() }.toMutableMap() - val dropped = mutableSetOf() + val dropped = mutableSetOf() for (op in batch.ops) { applyOp(nextNodes, nextExt, op, dropped) } @@ -101,10 +101,10 @@ class LuiRetainedTree(private val registry: LuiExtensionRegistry) { } private fun applyOp( - nodes: MutableMap, - ext: MutableMap, + nodes: MutableMap, + ext: MutableMap, op: LuiPatchOp, - dropped: MutableSet, + dropped: MutableSet, ) { when (op) { is LuiPatchOp.CreateNode -> { @@ -219,10 +219,10 @@ class LuiRetainedTree(private val registry: LuiExtensionRegistry) { } private fun isDescendant( - nodes: Map, - ext: Map, - target: Int, - root: Int, + nodes: Map, + ext: Map, + target: Long, + root: Long, ): Boolean { var cursor = parentOf(nodes, ext, target) while (cursor != null) { @@ -233,10 +233,10 @@ class LuiRetainedTree(private val registry: LuiExtensionRegistry) { } private fun validateChildRelationship( - nodes: Map, - ext: Map, - parentID: Int, - childID: Int, + nodes: Map, + ext: Map, + parentID: Long, + childID: Long, ) { if (nodes[childID]?.kind == LuiNodeKind.root) { throw LuiBackendException("runtime root cannot be nested") @@ -371,7 +371,7 @@ class LuiRetainedTree(private val registry: LuiExtensionRegistry) { } } - private fun validateStates(nodes: Map) { + private fun validateStates(nodes: Map) { for (state in nodes.values) { if (state.kind == LuiNodeKind.root) { if (state.parent != null) { diff --git a/platform/android/lui/src/main/kotlin/dev/lui/LuiTheme.kt b/platform/android/lui/src/main/kotlin/dev/lui/LuiTheme.kt index 4dcb11d5..00e734fb 100644 --- a/platform/android/lui/src/main/kotlin/dev/lui/LuiTheme.kt +++ b/platform/android/lui/src/main/kotlin/dev/lui/LuiTheme.kt @@ -26,8 +26,10 @@ import kotlinx.serialization.json.jsonPrimitive /** Semantic-color overrides inherited down the tree (`theme` prop). */ val LocalLuiSemanticColors = compositionLocalOf> { emptyMap() } -/** Effective dark-mode flag inside the LUI tree (`theme-mode` prop). */ -val LocalLuiDarkTheme = compositionLocalOf { false } +/** Effective dark-mode flag inside the LUI tree (`theme-mode` prop, or a + * host-provided override). Null means "not provided" — fall back to the + * system setting. */ +val LocalLuiThemeDark = compositionLocalOf { null } /** Hex resolver shared by color-name props and theme tokens. Accepts * `#rgb`, `#rgba`, `#rrggbb` and `#rrggbbaa` (CSS alpha-suffix). @@ -124,11 +126,12 @@ fun LuiTheme( darkScheme: ColorScheme = LuiThemeDefaults.darkScheme(), content: @Composable () -> Unit, ) { - val effectiveDark = dark ?: isSystemInDarkTheme() + // An explicit [dark] wins, then an app-provided local, then the system. + val effectiveDark = dark ?: LocalLuiThemeDark.current ?: isSystemInDarkTheme() MaterialTheme(colorScheme = if (effectiveDark) darkScheme else light) { val inherited = LocalLuiSemanticColors.current CompositionLocalProvider( - LocalLuiDarkTheme provides effectiveDark, + LocalLuiThemeDark provides effectiveDark, LocalLuiSemanticColors provides inherited + semanticColors, ) { content() diff --git a/platform/android/lui/src/main/kotlin/dev/lui/LuiWire.kt b/platform/android/lui/src/main/kotlin/dev/lui/LuiWire.kt index 97b462ea..4a914988 100644 --- a/platform/android/lui/src/main/kotlin/dev/lui/LuiWire.kt +++ b/platform/android/lui/src/main/kotlin/dev/lui/LuiWire.kt @@ -47,6 +47,15 @@ sealed class LuiWireValue { is DoubleValue -> value.toString() } + /** Platform-neutral scalar form (String/Int/Double/Boolean). */ + val scalarValue: Any + get() = when (this) { + is Str -> value + is Bool -> value + is IntValue -> value + is DoubleValue -> value + } + companion object { fun of(element: JsonElement): LuiWireValue { val primitive = element.jsonPrimitive @@ -67,22 +76,26 @@ sealed class LuiWireValue { } sealed class LuiPatchOp { - data class CreateNode(val id: Int, val kind: LuiNodeKind) : LuiPatchOp() - data class CreateExtension(val id: Int, val identifier: String, val fingerprint: String) : LuiPatchOp() - data class DropNode(val id: Int) : LuiPatchOp() - data class SetProp(val id: Int, val property: String, val value: LuiWireValue) : LuiPatchOp() - data class RemoveProp(val id: Int, val property: String) : LuiPatchOp() - data class SetExtensionProp(val id: Int, val property: String, val value: LuiWireValue) : LuiPatchOp() - data class RemoveExtensionProp(val id: Int, val property: String) : LuiPatchOp() - data class InsertChild(val parent: Int, val child: Int, val index: Int) : LuiPatchOp() - data class RemoveChild(val parent: Int, val child: Int) : LuiPatchOp() - data class MoveChild(val parent: Int, val child: Int, val index: Int) : LuiPatchOp() + data class CreateNode(val id: Long, val kind: LuiNodeKind) : LuiPatchOp() + data class CreateExtension(val id: Long, val identifier: String, val fingerprint: String) : LuiPatchOp() + data class DropNode(val id: Long) : LuiPatchOp() + data class SetProp(val id: Long, val property: String, val value: LuiWireValue) : LuiPatchOp() + data class RemoveProp(val id: Long, val property: String) : LuiPatchOp() + data class SetExtensionProp(val id: Long, val property: String, val value: LuiWireValue) : LuiPatchOp() + data class RemoveExtensionProp(val id: Long, val property: String) : LuiPatchOp() + data class InsertChild(val parent: Long, val child: Long, val index: Int) : LuiPatchOp() + data class RemoveChild(val parent: Long, val child: Long) : LuiPatchOp() + data class MoveChild(val parent: Long, val child: Long, val index: Int) : LuiPatchOp() companion object { private fun requiredInt(obj: JsonObject, key: String): Int = obj[key]?.jsonPrimitive?.intOrNull ?: throw LuiBackendException("missing or invalid '$key'") + private fun requiredLong(obj: JsonObject, key: String): Long = + obj[key]?.jsonPrimitive?.longOrNull + ?: throw LuiBackendException("missing or invalid '$key'") + private fun requiredString(obj: JsonObject, key: String): String = obj[key]?.jsonPrimitive?.takeIf { it.isString }?.content ?: throw LuiBackendException("missing or invalid '$key'") @@ -92,50 +105,50 @@ sealed class LuiPatchOp { ?: throw LuiBackendException("operation must be an object") return when (val op = obj["op"]?.jsonPrimitive?.content) { "create-node" -> CreateNode( - id = requiredInt(obj, "id"), + id = requiredLong(obj, "id"), kind = LuiNodeKind.fromWire(requiredString(obj, "kind")) ?: throw LuiBackendException("unknown node kind"), ) "create-extension" -> CreateExtension( - id = requiredInt(obj, "id"), + id = requiredLong(obj, "id"), identifier = requiredString(obj, "identifier"), fingerprint = requiredString(obj, "fingerprint"), ) - "drop-node" -> DropNode(id = requiredInt(obj, "id")) + "drop-node" -> DropNode(id = requiredLong(obj, "id")) "set-prop" -> SetProp( - id = requiredInt(obj, "id"), + id = requiredLong(obj, "id"), property = requiredString(obj, "property"), value = LuiWireValue.of( obj["value"] ?: throw LuiBackendException("missing 'value'"), ), ) "remove-prop" -> RemoveProp( - id = requiredInt(obj, "id"), + id = requiredLong(obj, "id"), property = requiredString(obj, "property"), ) "set-extension-prop" -> SetExtensionProp( - id = requiredInt(obj, "id"), + id = requiredLong(obj, "id"), property = requiredString(obj, "property"), value = LuiWireValue.of( obj["value"] ?: throw LuiBackendException("missing 'value'"), ), ) "remove-extension-prop" -> RemoveExtensionProp( - id = requiredInt(obj, "id"), + id = requiredLong(obj, "id"), property = requiredString(obj, "property"), ) "insert-child" -> InsertChild( - parent = requiredInt(obj, "parent"), - child = requiredInt(obj, "child"), + parent = requiredLong(obj, "parent"), + child = requiredLong(obj, "child"), index = requiredInt(obj, "index"), ) "remove-child" -> RemoveChild( - parent = requiredInt(obj, "parent"), - child = requiredInt(obj, "child"), + parent = requiredLong(obj, "parent"), + child = requiredLong(obj, "child"), ) "move-child" -> MoveChild( - parent = requiredInt(obj, "parent"), - child = requiredInt(obj, "child"), + parent = requiredLong(obj, "parent"), + child = requiredLong(obj, "child"), index = requiredInt(obj, "index"), ) else -> throw LuiBackendException("unknown patch operation: $op") diff --git a/platform/android/lui/src/test/kotlin/dev/lui/LuiStoreTest.kt b/platform/android/lui/src/test/kotlin/dev/lui/LuiStoreTest.kt index 9796f0e4..25110237 100644 --- a/platform/android/lui/src/test/kotlin/dev/lui/LuiStoreTest.kt +++ b/platform/android/lui/src/test/kotlin/dev/lui/LuiStoreTest.kt @@ -20,30 +20,30 @@ class LuiStoreTest { tree.apply( batch( 1, - LuiPatchOp.CreateNode(1, LuiNodeKind.root), - LuiPatchOp.CreateNode(2, LuiNodeKind.column), - LuiPatchOp.InsertChild(1, 2, 0), + LuiPatchOp.CreateNode(1L, LuiNodeKind.root), + LuiPatchOp.CreateNode(2L, LuiNodeKind.column), + LuiPatchOp.InsertChild(1L, 2L, 0), LuiPatchOp.SetProp(2, "gap", LuiWireValue.IntValue(8)), ), ) assertEquals(1, tree.generation) - assertEquals(listOf(1), tree.rootIds) - assertEquals(listOf(2), tree.nodes[1]!!.children) - assertEquals(1, tree.nodes[2]!!.parent) - assertEquals(LuiWireValue.IntValue(8), tree.nodes[2]!!.properties["gap"]) + assertEquals(listOf(1L), tree.rootIds) + assertEquals(listOf(2L), tree.nodes[1L]!!.children) + assertEquals(1L, tree.nodes[2L]!!.parent) + assertEquals(LuiWireValue.IntValue(8), tree.nodes[2L]!!.properties["gap"]) tree.apply( batch( 2, - LuiPatchOp.CreateNode(3, LuiNodeKind.text), - LuiPatchOp.InsertChild(1, 3, 1), - LuiPatchOp.RemoveChild(1, 2), - LuiPatchOp.DropNode(2), + LuiPatchOp.CreateNode(3L, LuiNodeKind.text), + LuiPatchOp.InsertChild(1L, 3L, 1), + LuiPatchOp.RemoveChild(1L, 2L), + LuiPatchOp.DropNode(2L), ), ) - assertNull(tree.nodes[2]) - assertEquals(listOf(3), tree.nodes[1]!!.children) - assertEquals(setOf(2), tree.lastDropped) + assertNull(tree.nodes[2L]) + assertEquals(listOf(3L), tree.nodes[1L]!!.children) + assertEquals(setOf(2L), tree.lastDropped) } @Test @@ -52,16 +52,16 @@ class LuiStoreTest { tree.apply( batch( 1, - LuiPatchOp.CreateNode(1, LuiNodeKind.root), - LuiPatchOp.CreateNode(2, LuiNodeKind.column), - LuiPatchOp.InsertChild(1, 2, 0), + LuiPatchOp.CreateNode(1L, LuiNodeKind.root), + LuiPatchOp.CreateNode(2L, LuiNodeKind.column), + LuiPatchOp.InsertChild(1L, 2L, 0), ), ) val error = assertThrows(LuiBackendException::class.java) { - tree.apply(batch(3, LuiPatchOp.CreateNode(5, LuiNodeKind.column))) + tree.apply(batch(3, LuiPatchOp.CreateNode(5L, LuiNodeKind.column))) } assertTrue(error.message!!.contains("generation 2")) - assertNull(tree.nodes[5]) + assertNull(tree.nodes[5L]) } @Test @@ -70,23 +70,23 @@ class LuiStoreTest { tree.apply( batch( 1, - LuiPatchOp.CreateNode(1, LuiNodeKind.root), - LuiPatchOp.CreateNode(2, LuiNodeKind.column), - LuiPatchOp.InsertChild(1, 2, 0), + LuiPatchOp.CreateNode(1L, LuiNodeKind.root), + LuiPatchOp.CreateNode(2L, LuiNodeKind.column), + LuiPatchOp.InsertChild(1L, 2L, 0), ), ) assertThrows(LuiBackendException::class.java) { tree.apply( batch( 2, - LuiPatchOp.CreateNode(3, LuiNodeKind.text), + LuiPatchOp.CreateNode(3L, LuiNodeKind.text), // Attach an unknown parent -> whole batch must roll back. - LuiPatchOp.InsertChild(99, 3, 0), + LuiPatchOp.InsertChild(99L, 3L, 0), ), ) } assertEquals(1, tree.generation) - assertNull(tree.nodes[3]) + assertNull(tree.nodes[3L]) } @Test @@ -95,15 +95,15 @@ class LuiStoreTest { tree.apply( batch( 1, - LuiPatchOp.CreateNode(1, LuiNodeKind.root), - LuiPatchOp.CreateNode(2, LuiNodeKind.text), - LuiPatchOp.InsertChild(1, 2, 0), + LuiPatchOp.CreateNode(1L, LuiNodeKind.root), + LuiPatchOp.CreateNode(2L, LuiNodeKind.text), + LuiPatchOp.InsertChild(1L, 2L, 0), ), ) tree.apply( batch(2, LuiPatchOp.SetProp(2, "text", LuiWireValue.Str("hi"))), ) - assertEquals(LuiWireValue.Str("hi"), tree.nodes[2]!!.properties["text"]) + assertEquals(LuiWireValue.Str("hi"), tree.nodes[2L]!!.properties["text"]) } @Test @@ -112,17 +112,17 @@ class LuiStoreTest { tree.apply( batch( 1, - LuiPatchOp.CreateNode(1, LuiNodeKind.root), - LuiPatchOp.CreateNode(2, LuiNodeKind.column), - LuiPatchOp.CreateNode(3, LuiNodeKind.text), - LuiPatchOp.CreateNode(4, LuiNodeKind.text), - LuiPatchOp.InsertChild(1, 2, 0), - LuiPatchOp.InsertChild(2, 3, 0), - LuiPatchOp.InsertChild(2, 4, 1), + LuiPatchOp.CreateNode(1L, LuiNodeKind.root), + LuiPatchOp.CreateNode(2L, LuiNodeKind.column), + LuiPatchOp.CreateNode(3L, LuiNodeKind.text), + LuiPatchOp.CreateNode(4L, LuiNodeKind.text), + LuiPatchOp.InsertChild(1L, 2L, 0), + LuiPatchOp.InsertChild(2L, 3L, 0), + LuiPatchOp.InsertChild(2L, 4L, 1), ), ) - tree.apply(batch(2, LuiPatchOp.MoveChild(2, 4, 0))) - assertEquals(listOf(4, 3), tree.nodes[2]!!.children) + tree.apply(batch(2, LuiPatchOp.MoveChild(2L, 4L, 0))) + assertEquals(listOf(4L, 3L), tree.nodes[2L]!!.children) } @Test @@ -131,13 +131,13 @@ class LuiStoreTest { tree.apply( batch( 1, - LuiPatchOp.CreateNode(1, LuiNodeKind.root), - LuiPatchOp.CreateNode(2, LuiNodeKind.column), - LuiPatchOp.InsertChild(1, 2, 0), + LuiPatchOp.CreateNode(1L, LuiNodeKind.root), + LuiPatchOp.CreateNode(2L, LuiNodeKind.column), + LuiPatchOp.InsertChild(1L, 2L, 0), ), ) assertThrows(LuiBackendException::class.java) { - tree.apply(batch(2, LuiPatchOp.DropNode(2))) + tree.apply(batch(2, LuiPatchOp.DropNode(2L))) } assertEquals(1, tree.generation) } @@ -148,18 +148,18 @@ class LuiStoreTest { tree.apply( batch( 1, - LuiPatchOp.CreateNode(1, LuiNodeKind.root), - LuiPatchOp.CreateNode(2, LuiNodeKind.column), - LuiPatchOp.CreateNode(3, LuiNodeKind.column), - LuiPatchOp.InsertChild(1, 2, 0), - LuiPatchOp.InsertChild(2, 3, 0), + LuiPatchOp.CreateNode(1L, LuiNodeKind.root), + LuiPatchOp.CreateNode(2L, LuiNodeKind.column), + LuiPatchOp.CreateNode(3L, LuiNodeKind.column), + LuiPatchOp.InsertChild(1L, 2L, 0), + LuiPatchOp.InsertChild(2L, 3L, 0), ), ) // Attaching the root under a descendant would create a cycle. assertThrows(LuiBackendException::class.java) { - tree.apply(batch(2, LuiPatchOp.InsertChild(3, 1, 0))) + tree.apply(batch(2, LuiPatchOp.InsertChild(3L, 1L, 0))) } - assertNull(tree.nodes[1]!!.parent) + assertNull(tree.nodes[1L]!!.parent) } @Test diff --git a/platform/android/lui/src/test/kotlin/dev/lui/LuiWireTest.kt b/platform/android/lui/src/test/kotlin/dev/lui/LuiWireTest.kt index 3e39736f..6ad9748f 100644 --- a/platform/android/lui/src/test/kotlin/dev/lui/LuiWireTest.kt +++ b/platform/android/lui/src/test/kotlin/dev/lui/LuiWireTest.kt @@ -34,7 +34,7 @@ class LuiWireTest { assertEquals(1, batch.generation) assertEquals(11, batch.ops.size) assertEquals( - LuiPatchOp.CreateNode(1, LuiNodeKind.root), + LuiPatchOp.CreateNode(1L, LuiNodeKind.root), batch.ops[0], ) assertEquals( @@ -53,7 +53,7 @@ class LuiWireTest { LuiPatchOp.SetExtensionProp(9, "title", LuiWireValue.Str("hi")), batch.ops[8], ) - assertEquals(LuiPatchOp.DropNode(2), batch.ops[10]) + assertEquals(LuiPatchOp.DropNode(2L), batch.ops[10]) } @Test