plushie 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.yardopts +1 -1
- data/AGENTS.md +595 -0
- data/CHANGELOG.md +147 -3
- data/CLAUDE.md +1 -0
- data/README.md +93 -227
- data/Steepfile +46 -2
- data/docs/README.md +56 -0
- data/docs/guides/01-introduction.md +266 -0
- data/docs/guides/02-getting-started.md +472 -0
- data/docs/guides/03-your-first-app.md +516 -0
- data/docs/guides/04-the-development-loop.md +499 -0
- data/docs/guides/05-events.md +548 -0
- data/docs/guides/06-lists-and-inputs.md +605 -0
- data/docs/guides/07-layout.md +530 -0
- data/docs/guides/08-styling.md +599 -0
- data/docs/guides/09-animation.md +516 -0
- data/docs/guides/10-subscriptions.md +470 -0
- data/docs/guides/11-async-and-effects.md +531 -0
- data/docs/guides/12-canvas.md +583 -0
- data/docs/guides/13-custom-widgets.md +705 -0
- data/docs/guides/14-state-management.md +600 -0
- data/docs/guides/15-testing.md +649 -0
- data/docs/guides/16-shared-state.md +567 -0
- data/docs/guides/17-packaging.md +549 -0
- data/docs/reference/accessibility.md +420 -0
- data/docs/reference/animation.md +482 -0
- data/docs/reference/app-lifecycle.md +504 -0
- data/docs/reference/built-in-widgets.md +702 -0
- data/docs/reference/canvas.md +589 -0
- data/docs/reference/commands.md +376 -0
- data/docs/reference/composition-patterns.md +816 -0
- data/docs/reference/configuration.md +302 -0
- data/docs/reference/custom-types.md +203 -0
- data/docs/reference/custom-widgets.md +681 -0
- data/docs/reference/dsl.md +500 -0
- data/docs/reference/events.md +596 -0
- data/docs/reference/native-extension.md +636 -0
- data/docs/reference/rake-tasks.md +440 -0
- data/docs/reference/scoped-ids.md +336 -0
- data/docs/reference/subscriptions.md +248 -0
- data/docs/reference/testing.md +448 -0
- data/docs/reference/themes-and-styling.md +511 -0
- data/docs/reference/versioning.md +174 -0
- data/docs/reference/windows-and-layout.md +692 -0
- data/docs/reference/wire-protocol.md +328 -0
- data/docs/stewardship/README.md +109 -0
- data/docs/stewardship/concurrency-shape.md +208 -0
- data/docs/stewardship/dsl-discipline.md +204 -0
- data/docs/stewardship/elm-invariants.md +196 -0
- data/docs/stewardship/goals-and-non-goals.md +95 -0
- data/docs/stewardship/performance-bar.md +157 -0
- data/docs/stewardship/posture.md +118 -0
- data/docs/stewardship/resilience.md +159 -0
- data/docs/stewardship/roadmap/README.md +20 -0
- data/docs/stewardship/simplicity.md +182 -0
- data/docs/stewardship/test-discipline.md +154 -0
- data/docs/stewardship/triage.md +153 -0
- data/docs/stewardship/trust-model.md +112 -0
- data/examples/README.md +2 -2
- data/examples/async_fetch.rb +2 -2
- data/examples/color_picker.rb +19 -69
- data/examples/counter.rb +4 -4
- data/examples/notes.rb +2 -2
- data/examples/rate_plushie.rb +135 -103
- data/examples/shortcuts.rb +1 -1
- data/examples/widgets/color_picker_widget.rb +254 -27
- data/examples/widgets/star_rating.rb +90 -44
- data/examples/widgets/theme_toggle.rb +79 -33
- data/lib/plushie/animation/sequence.rb +58 -0
- data/lib/plushie/animation/spring.rb +88 -0
- data/lib/plushie/animation/transition.rb +86 -0
- data/lib/plushie/animation/tween.rb +264 -0
- data/lib/plushie/animation.rb +19 -239
- data/lib/plushie/app.rb +8 -1
- data/lib/plushie/binary.rb +45 -10
- data/lib/plushie/bounded_queue.rb +32 -0
- data/lib/plushie/bridge.rb +148 -34
- data/lib/plushie/canvas/shape/group.rb +17 -1
- data/lib/plushie/canvas/shape/rect.rb +7 -2
- data/lib/plushie/canvas/shape/transform.rb +4 -2
- data/lib/plushie/canvas/shape.rb +19 -2
- data/lib/plushie/canvas_widget.rb +508 -0
- data/lib/plushie/cargo_plushie.rb +121 -0
- data/lib/plushie/command/image.rb +78 -0
- data/lib/plushie/command/scroll.rb +51 -0
- data/lib/plushie/command/text.rb +55 -0
- data/lib/plushie/command/window.rb +146 -0
- data/lib/plushie/command/window_query.rb +54 -0
- data/lib/plushie/command.rb +194 -297
- data/lib/plushie/connection.rb +68 -15
- data/lib/plushie/dev_server.rb +35 -8
- data/lib/plushie/effect.rb +170 -0
- data/lib/plushie/encode.rb +32 -12
- data/lib/plushie/event/diagnostic.rb +221 -0
- data/lib/plushie/event/specs.rb +211 -0
- data/lib/plushie/event.rb +253 -162
- data/lib/plushie/key_modifiers.rb +5 -5
- data/lib/plushie/node.rb +9 -5
- data/lib/plushie/protocol/decode.rb +624 -209
- data/lib/plushie/protocol/encode.rb +141 -43
- data/lib/plushie/protocol.rb +3 -3
- data/lib/plushie/rake.rb +69 -62
- data/lib/plushie/renderer_env.rb +14 -5
- data/lib/plushie/renderer_exit.rb +41 -0
- data/lib/plushie/runtime/commands.rb +150 -39
- data/lib/plushie/runtime/subscriptions.rb +34 -17
- data/lib/plushie/runtime/windows.rb +178 -0
- data/lib/plushie/runtime.rb +1076 -73
- data/lib/plushie/selection.rb +4 -4
- data/lib/plushie/subscription.rb +101 -90
- data/lib/plushie/test/case.rb +2 -1
- data/lib/plushie/test/helpers.rb +170 -10
- data/lib/plushie/test/script.rb +1 -1
- data/lib/plushie/test/session.rb +272 -58
- data/lib/plushie/test/session_pool.rb +27 -11
- data/lib/plushie/test/snapshot.rb +18 -0
- data/lib/plushie/test.rb +0 -1
- data/lib/plushie/thread_pool.rb +1 -1
- data/lib/plushie/timer_scheduler.rb +103 -0
- data/lib/plushie/transport/framing.rb +40 -1
- data/lib/plushie/transport/tcp_adapter.rb +7 -0
- data/lib/plushie/tree/diff.rb +208 -0
- data/lib/plushie/tree/search.rb +98 -0
- data/lib/plushie/tree.rb +684 -170
- data/lib/plushie/type/a11y.rb +17 -2
- data/lib/plushie/type/alignment.rb +4 -4
- data/lib/plushie/type/border.rb +16 -1
- data/lib/plushie/type/color.rb +1 -1
- data/lib/plushie/type/font.rb +5 -9
- data/lib/plushie/type/gradient.rb +47 -10
- data/lib/plushie/type/line_height.rb +42 -0
- data/lib/plushie/type/padding.rb +21 -14
- data/lib/plushie/type/style_map.rb +9 -9
- data/lib/plushie/type/theme.rb +71 -0
- data/lib/plushie/ui.rb +222 -156
- data/lib/plushie/undo.rb +46 -12
- data/lib/plushie/version.rb +6 -3
- data/lib/plushie/widget/build.rb +67 -6
- data/lib/plushie/widget/button.rb +9 -52
- data/lib/plushie/widget/canvas.rb +28 -58
- data/lib/plushie/widget/checkbox.rb +26 -45
- data/lib/plushie/widget/column.rb +5 -41
- data/lib/plushie/widget/combo_box.rb +27 -55
- data/lib/plushie/widget/container.rb +11 -49
- data/lib/plushie/widget/floating.rb +10 -47
- data/lib/plushie/widget/grid.rb +14 -52
- data/lib/plushie/widget/image.rb +23 -50
- data/lib/plushie/widget/keyed_column.rb +10 -47
- data/lib/plushie/widget/markdown.rb +18 -47
- data/lib/plushie/widget/native_build.rb +333 -0
- data/lib/plushie/widget/overlay.rb +12 -49
- data/lib/plushie/widget/pane_grid.rb +15 -52
- data/lib/plushie/widget/pick_list.rb +26 -54
- data/lib/plushie/widget/pin.rb +9 -46
- data/lib/plushie/widget/pointer_area.rb +38 -0
- data/lib/plushie/widget/progress_bar.rb +15 -46
- data/lib/plushie/widget/qr_code.rb +15 -44
- data/lib/plushie/widget/radio.rb +22 -53
- data/lib/plushie/widget/responsive.rb +7 -44
- data/lib/plushie/widget/rich_text.rb +68 -35
- data/lib/plushie/widget/row.rb +5 -41
- data/lib/plushie/widget/rule.rb +10 -36
- data/lib/plushie/widget/scrollable.rb +20 -59
- data/lib/plushie/widget/sensor.rb +8 -46
- data/lib/plushie/widget/slider.rb +22 -53
- data/lib/plushie/widget/space.rb +7 -35
- data/lib/plushie/widget/stack.rb +8 -45
- data/lib/plushie/widget/svg.rb +18 -47
- data/lib/plushie/widget/table.rb +62 -54
- data/lib/plushie/widget/text.rb +7 -44
- data/lib/plushie/widget/text_editor.rb +28 -54
- data/lib/plushie/widget/text_input.rb +9 -41
- data/lib/plushie/widget/themer.rb +7 -46
- data/lib/plushie/widget/toggler.rb +22 -50
- data/lib/plushie/widget/tooltip.rb +15 -53
- data/lib/plushie/widget/vertical_slider.rb +20 -52
- data/lib/plushie/widget/window.rb +13 -43
- data/lib/plushie/widget.rb +760 -0
- data/lib/plushie/widget_set.rb +82 -0
- data/lib/plushie.rb +45 -17
- data/sig/base64.rbs +6 -0
- data/sig/msgpack.rbs +13 -0
- data/sig/plushie/animation.rbs +74 -33
- data/sig/plushie/app.rbs +1 -1
- data/sig/plushie/bounded_queue.rbs +13 -0
- data/sig/plushie/bridge.rbs +14 -4
- data/sig/plushie/canvas/shape.rbs +3 -2
- data/sig/plushie/canvas_widget.rbs +55 -0
- data/sig/plushie/cargo_plushie.rbs +11 -0
- data/sig/plushie/command/image.rbs +11 -0
- data/sig/plushie/command/scroll.rbs +10 -0
- data/sig/plushie/command/text.rbs +11 -0
- data/sig/plushie/command/window.rbs +29 -0
- data/sig/plushie/command/window_query.rbs +14 -0
- data/sig/plushie/command.rbs +32 -42
- data/sig/plushie/effect.rbs +31 -0
- data/sig/plushie/encode.rbs +5 -0
- data/sig/plushie/event.rbs +170 -86
- data/sig/plushie/key_modifiers.rbs +1 -0
- data/sig/plushie/node.rbs +9 -2
- data/sig/plushie/protocol.rbs +58 -6
- data/sig/plushie/route.rbs +2 -2
- data/sig/plushie/runtime/windows.rbs +32 -0
- data/sig/plushie/runtime.rbs +100 -6
- data/sig/plushie/subscription.rbs +24 -20
- data/sig/plushie/timer_scheduler.rbs +19 -0
- data/sig/plushie/tree/diff.rbs +13 -0
- data/sig/plushie/tree/search.rbs +12 -0
- data/sig/plushie/tree.rbs +44 -3
- data/sig/plushie/type/a11y.rbs +44 -0
- data/sig/plushie/type/font.rbs +23 -0
- data/sig/plushie/type/gradient.rbs +9 -0
- data/sig/plushie/type/line_height.rbs +10 -0
- data/sig/plushie/ui.rbs +9 -1
- data/sig/plushie/undo.rbs +14 -6
- data/sig/plushie/widget/build.rbs +18 -0
- data/sig/plushie/widget_dsl.rbs +47 -0
- data/sig/plushie/widget_set.rbs +5 -0
- data/sig/plushie.rbs +17 -1
- metadata +104 -26
- data/docs/accessibility.md +0 -489
- data/docs/app-behaviour.md +0 -614
- data/docs/commands.md +0 -882
- data/docs/composition-patterns.md +0 -1037
- data/docs/dsl-internals.md +0 -343
- data/docs/effects.md +0 -108
- data/docs/events.md +0 -653
- data/docs/extensions.md +0 -1791
- data/docs/getting-started.md +0 -237
- data/docs/layout.md +0 -266
- data/docs/running.md +0 -356
- data/docs/scoped-ids.md +0 -200
- data/docs/testing.md +0 -844
- data/docs/theming.md +0 -292
- data/docs/tutorial.md +0 -369
- data/examples/catalog.rb +0 -448
- data/lib/plushie/effects.rb +0 -169
- data/lib/plushie/extension/build.rb +0 -293
- data/lib/plushie/extension.rb +0 -339
- data/lib/plushie/test/event_decoder.rb +0 -118
- data/lib/plushie/widget/mouse_area.rb +0 -72
- data/sig/plushie/effects.rbs +0 -31
- data/sig/plushie/extension.rbs +0 -40
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: eefba997742401e8ca49f13be903aa36ea00b6da2008a18a6b35935913aef9d4
|
|
4
|
+
data.tar.gz: 9bf004d365977ed0de5ae5423d679f138e7b25662552adafe54e1f6b56fa8d75
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ed4381541afdc914022cf542fa7d70124efd6156525a590bc89abd5e7a833c020cbd87624e3205df75d6d5567938921eba145f49dac4e740c4d249393d7c1e62
|
|
7
|
+
data.tar.gz: 8d2e5c47b78276e568b4bafd66f2a6926229c9dc5d16b5de5712d00332207200bbb0c5734432535cb2c3a429b71774ff52e153ab93a6a303c8ece7022ac36fe0
|
data/.yardopts
CHANGED
data/AGENTS.md
ADDED
|
@@ -0,0 +1,595 @@
|
|
|
1
|
+
# plushie-ruby
|
|
2
|
+
|
|
3
|
+
This file is not version controlled. Do not reference it in commit
|
|
4
|
+
messages, pull requests, or documentation.
|
|
5
|
+
|
|
6
|
+
Native desktop GUI framework for Ruby, powered by iced. Implements
|
|
7
|
+
the Elm architecture (init/update/view) with commands and subscriptions.
|
|
8
|
+
Communicates with the Rust binary over stdin/stdout using MessagePack
|
|
9
|
+
(default) or JSONL.
|
|
10
|
+
|
|
11
|
+
## Stewardship
|
|
12
|
+
|
|
13
|
+
Direction, trust posture, goals, and explicit non-goals are captured
|
|
14
|
+
in `docs/stewardship/`. That directory is the authority on what work
|
|
15
|
+
the project takes on and what it declines. The summary below is enough
|
|
16
|
+
for routine work; pull the relevant doc when an axis is in play. Use
|
|
17
|
+
`docs/stewardship/triage.md` as the routing tool when the answer is
|
|
18
|
+
not self-evident.
|
|
19
|
+
|
|
20
|
+
Pre-1.0: no backcompat, right design wins, rename across SDKs is fine.
|
|
21
|
+
Post-1.0: stability obligations begin (Hyrum's Law). plushie-rust =
|
|
22
|
+
protocol authority. plushie-elixir = canonical API-shape reference;
|
|
23
|
+
plushie-ruby follows. Cross-SDK parity audited in sibling
|
|
24
|
+
`plushie-sdk-parity/`. Gem is a library, not an auto-bootstrap
|
|
25
|
+
framework.
|
|
26
|
+
|
|
27
|
+
### Disciplines (non-negotiable)
|
|
28
|
+
|
|
29
|
+
Tests through real renderer; cross-SDK claims verified by reading
|
|
30
|
+
source on each side; design before code at boundaries (public API,
|
|
31
|
+
DSL surface, wire codec, transport contract); clarity is the bar; no
|
|
32
|
+
half-built features; local cleanup not scope creep; no legacy shims
|
|
33
|
+
pre-1.0.
|
|
34
|
+
|
|
35
|
+
### Goals
|
|
36
|
+
|
|
37
|
+
Wire codec fidelity on host side; cross-SDK concept parity (semantics
|
|
38
|
+
converge, syntax diverges per language); Elm-architecture purity
|
|
39
|
+
(init/update/view, return-shape validation, commands as pure data,
|
|
40
|
+
pure view, declarative subs); lightweight runtime (no idle work, no
|
|
41
|
+
polling, minimal tree diff via LIS); fault tolerance (renderer crash
|
|
42
|
+
auto-recovers + state re-syncs with bounded backoff, app exception
|
|
43
|
+
reverts to last good model, neither side takes the other down); DSL
|
|
44
|
+
clarity.
|
|
45
|
+
|
|
46
|
+
### Non-goals (declined, not deprioritized)
|
|
47
|
+
|
|
48
|
+
Backcompat before 1.0; per-Ruby API ergonomics that diverge from
|
|
49
|
+
cross-SDK shape; API stability hardening pre-1.0 (single 1.0 sweep,
|
|
50
|
+
not piecemeal); coverage targets as a metric; mocking renderer for
|
|
51
|
+
speed; micro-optimization at cost of readability; refactoring without
|
|
52
|
+
a forcing function; DSL extensions for hypothetical future widgets;
|
|
53
|
+
heavy metaprogramming where ordinary code would do; fiber/ractor
|
|
54
|
+
runtime architectures; defending against speculative deployment shapes.
|
|
55
|
+
|
|
56
|
+
### Trust model
|
|
57
|
+
|
|
58
|
+
Asymmetric. Renderer-to-host = closed and typed; host structurally
|
|
59
|
+
protected today (typed event decoding, no opaque-blob path, effect/
|
|
60
|
+
query response correlation by wire ID, no host-side eval, no `to_sym`
|
|
61
|
+
on renderer-supplied strings on hot paths, strict enums via Parsers).
|
|
62
|
+
Host-to-renderer = broad by design (file paths, fonts, images,
|
|
63
|
+
screenshots, effects, `--exec`); bounding it is the capability-manifest
|
|
64
|
+
roadmap in plushie-rust. Wire = byte-stream agnostic; confidentiality
|
|
65
|
+
+ integrity delegated to outer transport. Same-access (user attacking
|
|
66
|
+
themselves) out of scope.
|
|
67
|
+
|
|
68
|
+
### Resilience
|
|
69
|
+
|
|
70
|
+
Things-go-wrong axis, not adversary axis. App exception revert in
|
|
71
|
+
init/update/view (rescue StandardError, not Exception); renderer crash
|
|
72
|
+
auto-recovery via Bridge with exponential backoff + fresh snapshot
|
|
73
|
+
re-sync; bridge heartbeat watchdog catches hung renderers; defensive
|
|
74
|
+
parsing on the wire (reject + structured error); return-shape
|
|
75
|
+
validation in `unwrap_result` raises immediately; bounded queue +
|
|
76
|
+
coalescable events for high-frequency sources; subscription failure
|
|
77
|
+
isolated; thread isolation for async commands. Fail-fast on programming-
|
|
78
|
+
error invariant violations and unrecoverable bridge startup. Degrade
|
|
79
|
+
gracefully on user-facing input. Log suppression after 100 consecutive
|
|
80
|
+
errors. Don't `rescue Exception` (swallows interrupts).
|
|
81
|
+
|
|
82
|
+
### Performance
|
|
83
|
+
|
|
84
|
+
Lightweight = baseline, not optimization-after-fact. Don't do
|
|
85
|
+
unnecessary work in the first place; cost compounds. Worth doing
|
|
86
|
+
without benchmark (readability preserved/improved): consolidate
|
|
87
|
+
redundant traversals, right data structure, avoid unnecessary `each`/
|
|
88
|
+
`map` passes, move per-frame work that doesn't depend on per-frame
|
|
89
|
+
inputs to the edge. Need benchmark first (readability cost real):
|
|
90
|
+
clever encoding, big-O without realistic N, optimization on idle paths.
|
|
91
|
+
Ruby axes: GVL serializes Ruby across threads (push CPU-bound user
|
|
92
|
+
work to `Command.task`); allocation discipline (`frozen_string_literal`
|
|
93
|
+
everywhere, `Data.define` over Struct, avoid runtime string building
|
|
94
|
+
on hot paths); GC pauses; no `to_sym` on untrusted strings. Numeric
|
|
95
|
+
direction: 16.67ms frame budget at a few hundred to ~1000 nodes; idle
|
|
96
|
+
CPU = no measurable work; tree diff is the load-bearing piece (LIS-
|
|
97
|
+
based child reorder, memo cache, widget view cache).
|
|
98
|
+
|
|
99
|
+
### Test discipline
|
|
100
|
+
|
|
101
|
+
Integration spine: tests exercise real renderer (default `mock`
|
|
102
|
+
backend = real binary, real wire, real Core, no GPU). Three modes
|
|
103
|
+
(cross-SDK contract): mock (default, fastest), headless (tiny-skia,
|
|
104
|
+
pixels), windowed (headless weston preferred, Xvfb works for X11).
|
|
105
|
+
Pooled mock backend
|
|
106
|
+
multiplexes via `--max-sessions N`. Both Minitest (`Plushie::Test::Case`)
|
|
107
|
+
and RSpec (`Plushie::Test::RSpec`) are first-class. Stubs acceptable
|
|
108
|
+
only for forced crash sim, malformed wire bytes, direct `update` shape
|
|
109
|
+
tests, test infra. Sync via the Session API, never via
|
|
110
|
+
`instance_variable_get` into the runtime. Tests as documentation;
|
|
111
|
+
slow tests = slow code; failing test before fix. Flaky tests are real
|
|
112
|
+
bugs, not tests to retry.
|
|
113
|
+
|
|
114
|
+
### Simplicity
|
|
115
|
+
|
|
116
|
+
Clarity = constraint, not aspiration. Reader-cost compounds.
|
|
117
|
+
Readability wins ties. Abstraction earns its place: 3 similar lines
|
|
118
|
+
> premature abstraction; 3rd use earns consideration not commitment;
|
|
119
|
+
single-user mixin = costume; "we might need this someday" = reason
|
|
120
|
+
not to extract. Local complexity > global. Cohesion across file >
|
|
121
|
+
brevity of any one file. Mixed-paradigm flavor: pure where possible,
|
|
122
|
+
immutable (`Data.define` records, frozen `with` semantics), pattern
|
|
123
|
+
matching for events, composition over inheritance (single-level
|
|
124
|
+
mixins, no multi-level framework hierarchies), errors as values where
|
|
125
|
+
clean else raise, blocks over higher-order callbacks. Comments answer
|
|
126
|
+
why-not-what. RBS in `sig/` is real type info; drift from
|
|
127
|
+
implementation = bug.
|
|
128
|
+
|
|
129
|
+
### Elm invariants
|
|
130
|
+
|
|
131
|
+
`init` and `update` return: bare model | `[model, Command::Cmd]` |
|
|
132
|
+
`[model, [Command::Cmd, ...]]`. Anything else raises `ArgumentError`
|
|
133
|
+
from `unwrap_result`. Commands are pure data; runtime executes.
|
|
134
|
+
`view(model)` is pure function of model; top level must be `window(...)`
|
|
135
|
+
node or array of windows (`Tree.normalize` raises otherwise). Subs
|
|
136
|
+
declarative; runtime diffs each cycle (short-circuits when only
|
|
137
|
+
`max_rate` changed). Widget event flow walks scope chain innermost-
|
|
138
|
+
first; handlers return `:ignored`/`:consumed`/`[:update_state, _]`/
|
|
139
|
+
`[:emit, family, data]`. Canvas-internal events auto-consumed if not
|
|
140
|
+
captured. Wire IDs: `window#scope/path/id`; events split into
|
|
141
|
+
`id`/`scope`/`window_id`; `Event.target(event)` reconstructs forward
|
|
142
|
+
path; commands use forward-order path strings.
|
|
143
|
+
|
|
144
|
+
### DSL discipline
|
|
145
|
+
|
|
146
|
+
Block-based DSL via `include Plushie::App`. Blocks yield in caller's
|
|
147
|
+
binding (no `instance_eval` against user blocks; `self` stays the
|
|
148
|
+
app instance, private helpers work). Thread-local context stack tracks
|
|
149
|
+
parent-child. New DSL form earns its place when: 2+ real users,
|
|
150
|
+
replaces harder-to-read runtime construct, real bug class detectable
|
|
151
|
+
at finalization time, generated code reads as cleanly as hand-written,
|
|
152
|
+
errors point at user's call site. Used: `define_method` for setters,
|
|
153
|
+
`class_eval(&block)` for `Widget.define`, module hooks, frozen
|
|
154
|
+
`Data.define` records. Avoided: `instance_eval` against user blocks
|
|
155
|
+
for the UI DSL, `method_missing` as routing, broad refinements, deep
|
|
156
|
+
inheritance. Generated code is what users read in stack traces; stable
|
|
157
|
+
predictable structure, named methods match expectations, errors name
|
|
158
|
+
DSL context.
|
|
159
|
+
|
|
160
|
+
### Concurrency shape
|
|
161
|
+
|
|
162
|
+
Three layers: Connection (pipe + framing + writer mutex + reader
|
|
163
|
+
thread), Bridge (restart logic + heartbeat watchdog + exponential
|
|
164
|
+
backoff), Runtime (Elm loop + model + tree + commands). Runtime
|
|
165
|
+
thread is the only thread that touches app state; other threads push
|
|
166
|
+
tagged tuples through a `BoundedQueue`. Dedicated threads for
|
|
167
|
+
`Command.task`/`Command.stream` (cancellable); single `TimerScheduler`
|
|
168
|
+
thread (deadline-based `IO.select`). GVL-aware: CPU-bound user work
|
|
169
|
+
belongs in `Command.task`; runtime doesn't block on I/O. Transport
|
|
170
|
+
adapters (spawn/stdio/iostream) earn their place. SessionPool
|
|
171
|
+
multiplexes mock/headless via `--max-sessions N`. No `Mutex` for app
|
|
172
|
+
state, no concurrent-ruby, no Async reactor, no fibers/ractors, no
|
|
173
|
+
auto-bootstrap.
|
|
174
|
+
|
|
175
|
+
### Common shapes -> outcomes
|
|
176
|
+
|
|
177
|
+
- "mock the renderer for speed" -> decline
|
|
178
|
+
- "reach into runtime via `instance_variable_get`" -> rewrite to
|
|
179
|
+
Session API
|
|
180
|
+
- "add deprecation warnings / API hardening" -> decline; 1.0 sweep
|
|
181
|
+
- "this is O(n) on a hot path" -> need realistic N
|
|
182
|
+
- "split this large module" -> need forcing function
|
|
183
|
+
- "harden against malicious renderer" -> structurally protected;
|
|
184
|
+
check if proposal loosens that, otherwise misframed
|
|
185
|
+
- "harden against malicious host" -> defer to capability-manifest
|
|
186
|
+
(plushie-rust roadmap)
|
|
187
|
+
- "wire should encrypt / sign" -> outer transport's job
|
|
188
|
+
- "consolidate N redundant traversals" -> do
|
|
189
|
+
- "extract this single-use mixin" -> decline; costume
|
|
190
|
+
- "rescue Exception in this loop" -> no, `rescue StandardError`
|
|
191
|
+
- "let users return `nil` from update" -> no, bare model is no-change
|
|
192
|
+
- "rename field across SDKs" -> route through parity workflow
|
|
193
|
+
- "use `instance_eval` for the block DSL" -> no, breaks user `self`
|
|
194
|
+
- "switch runtime to fibers / ractors" -> stewardship-level question
|
|
195
|
+
- "add `method_missing` routing" -> default no; explicit definition
|
|
196
|
+
- "add a new DSL declaration form" -> run dsl-discipline criteria
|
|
197
|
+
|
|
198
|
+
## Before committing
|
|
199
|
+
|
|
200
|
+
Run `bundle exec rake`. It mirrors CI: tests, linter, type check.
|
|
201
|
+
|
|
202
|
+
For full preflight (including headless renderer tests against a fresh
|
|
203
|
+
build), run `bundle exec rake plushie:preflight`. When
|
|
204
|
+
`PLUSHIE_RUST_SOURCE_PATH` is set to a plushie-rust checkout, preflight
|
|
205
|
+
runs `cargo build --release -p plushie-renderer` against that workspace
|
|
206
|
+
first and exports `PLUSHIE_BINARY_PATH` so the headless tests use the
|
|
207
|
+
freshly built binary. Without it, the existing binary resolution chain
|
|
208
|
+
runs unchanged.
|
|
209
|
+
|
|
210
|
+
## Commit hygiene
|
|
211
|
+
|
|
212
|
+
Every commit should be self-contained and functional. Preflight
|
|
213
|
+
should pass at each commit, not just at the tip.
|
|
214
|
+
|
|
215
|
+
Commits after `origin/main` are unpublished and can be freely
|
|
216
|
+
amended, squashed, or reordered to keep the history clean. Run
|
|
217
|
+
`git fetch origin` first to ensure the boundary is current. Use
|
|
218
|
+
`--amend` to fold small fixes into the commit they belong to
|
|
219
|
+
rather than creating "fix the fix" commits. If a later commit
|
|
220
|
+
fixes a bug introduced by an earlier unpublished commit, squash
|
|
221
|
+
them together.
|
|
222
|
+
|
|
223
|
+
Never amend or rebase commits that are already on `origin/main`.
|
|
224
|
+
|
|
225
|
+
## Commit messages
|
|
226
|
+
|
|
227
|
+
Commit messages should describe what changed and why. Do not include:
|
|
228
|
+
- Counts of any kind (findings, files, tests, items). If the
|
|
229
|
+
content is listed, the reader can count. Counts add noise.
|
|
230
|
+
- Ticket, review, or tracking IDs (R-001, PROJ-123, etc.)
|
|
231
|
+
- References to this file
|
|
232
|
+
|
|
233
|
+
More broadly, think carefully before including counts anywhere
|
|
234
|
+
(code comments, docs, log messages). If the count is derivable
|
|
235
|
+
from the surrounding content, it doesn't add value.
|
|
236
|
+
|
|
237
|
+
## Writing style
|
|
238
|
+
|
|
239
|
+
Do not use `--` (double dash) as a separator or em-dash substitute
|
|
240
|
+
in prose, docs, comments, or bullet lists. Use a single `-` for
|
|
241
|
+
list item separators and reword sentences to avoid inline dashes
|
|
242
|
+
(use commas, periods, colons, or parentheses instead). `--` should
|
|
243
|
+
only appear as part of CLI flag names (e.g. `--watch`, `--release`).
|
|
244
|
+
|
|
245
|
+
## Quick reference
|
|
246
|
+
|
|
247
|
+
```
|
|
248
|
+
bundle exec rake # tests + linter + type check
|
|
249
|
+
bundle exec rake test # tests only
|
|
250
|
+
bundle exec rake standard # linter only
|
|
251
|
+
bundle exec rake steep # type check only
|
|
252
|
+
bundle exec rake yard # generate API docs to doc/
|
|
253
|
+
bundle exec ruby examples/counter.rb # run an example (needs binary)
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Test backend selection:
|
|
257
|
+
```
|
|
258
|
+
bundle exec rake test # mock (default, fastest)
|
|
259
|
+
PLUSHIE_TEST_BACKEND=headless bundle exec rake test # real rendering, no display
|
|
260
|
+
PLUSHIE_TEST_BACKEND=windowed bundle exec rake test # real windows (needs weston or Xvfb)
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
## Configuration
|
|
264
|
+
|
|
265
|
+
Environment variables:
|
|
266
|
+
- `PLUSHIE_BINARY_PATH`: path to the renderer binary (overrides all resolution)
|
|
267
|
+
- `PLUSHIE_RUST_SOURCE_PATH`: path to a local plushie-rust checkout.
|
|
268
|
+
When set, `rake plushie:build` runs cargo-plushie out of that
|
|
269
|
+
checkout via `cargo run -p cargo-plushie` (no install required).
|
|
270
|
+
- `PLUSHIE_TEST_BACKEND`: test backend: `mock`, `headless`, `windowed`
|
|
271
|
+
- `PLUSHIE_BIN_FILE`: override binary destination for download/build tasks
|
|
272
|
+
- `PLUSHIE_WASM_DIR`: override WASM output directory for download tasks
|
|
273
|
+
|
|
274
|
+
The native widget build delegates workspace generation and
|
|
275
|
+
`cargo build` to `cargo-plushie`. See `docs/versioning.md` for how
|
|
276
|
+
the installed cargo-plushie version is pinned to
|
|
277
|
+
`Plushie::PLUSHIE_RUST_VERSION`.
|
|
278
|
+
|
|
279
|
+
## Known gotchas
|
|
280
|
+
|
|
281
|
+
- **`width: "fill"` on intermediate containers.** Iced containers
|
|
282
|
+
default to shrink-wrapping. If a row or column doesn't have
|
|
283
|
+
`width: "fill"`, its children's `width: "fill"` has nothing to
|
|
284
|
+
fill. This is the most common layout bug. Always check parent
|
|
285
|
+
containers when a widget collapses to zero width.
|
|
286
|
+
|
|
287
|
+
- **Event field access: `event.value` not `event.data`.**
|
|
288
|
+
All event payloads use the `value` field. Scalar events
|
|
289
|
+
(input, slider) carry the value directly; structured events
|
|
290
|
+
(pointer, key) carry a symbol-keyed Hash. There is no `data`
|
|
291
|
+
field. Tests with manually constructed events hide type
|
|
292
|
+
mismatches; always test through the renderer with
|
|
293
|
+
`Plushie::Test::Case`.
|
|
294
|
+
|
|
295
|
+
- **Native widget tests need `new_instance()` in Rust.** The
|
|
296
|
+
test framework's session pool multiplexes sessions over one
|
|
297
|
+
renderer process. Native widgets that don't implement
|
|
298
|
+
`new_instance()` cause the pool to hang. Add it to every
|
|
299
|
+
PlushieWidget impl.
|
|
300
|
+
|
|
301
|
+
- **Steep and Widget.define blocks.** Steep cannot analyze the
|
|
302
|
+
`class_eval` block inside `Widget.define` because it resolves
|
|
303
|
+
`self` to the Widget module, not the new class. Widget files
|
|
304
|
+
are excluded from the Steepfile check list. Their RBS
|
|
305
|
+
declarations are retained for downstream type consumers.
|
|
306
|
+
|
|
307
|
+
## Ruby version
|
|
308
|
+
|
|
309
|
+
Requires Ruby >= 3.2 for `Data.define` and stable pattern matching.
|
|
310
|
+
Developed on Ruby 4.0+.
|
|
311
|
+
|
|
312
|
+
## Dependencies
|
|
313
|
+
|
|
314
|
+
Required (gemspec):
|
|
315
|
+
- `msgpack`: MessagePack encoding/decoding
|
|
316
|
+
- `logger`: standard Ruby logging (extracted from stdlib in Ruby 4.0)
|
|
317
|
+
|
|
318
|
+
Dev/test (Gemfile only):
|
|
319
|
+
- `minitest`: testing framework
|
|
320
|
+
- `standard`: linting (zero-config RuboCop)
|
|
321
|
+
- `rake`: task runner
|
|
322
|
+
|
|
323
|
+
## Project layout
|
|
324
|
+
|
|
325
|
+
```
|
|
326
|
+
lib/
|
|
327
|
+
plushie.rb # top-level: Plushie.run / Plushie.start API
|
|
328
|
+
plushie/
|
|
329
|
+
version.rb # VERSION + PLUSHIE_RUST_VERSION constants
|
|
330
|
+
model.rb # Model.define wrapper (Data.define + #with)
|
|
331
|
+
node.rb # Node: immutable UI tree node (Data.define)
|
|
332
|
+
app.rb # App module (include in user classes)
|
|
333
|
+
ui.rb # block-based DSL (thread-local context stack)
|
|
334
|
+
event.rb # all event Data types (Widget, Key, Ime, etc.)
|
|
335
|
+
event/
|
|
336
|
+
specs.rb # canonical event type catalog (BuiltinSpecs)
|
|
337
|
+
command.rb # Command.Cmd Data type + constructor facade
|
|
338
|
+
command/
|
|
339
|
+
text.rb scroll.rb # command submodules (text, scroll,
|
|
340
|
+
window.rb window_query.rb # window, window_query, image)
|
|
341
|
+
image.rb
|
|
342
|
+
subscription.rb # Subscription.Sub Data type + constructors
|
|
343
|
+
effect.rb # platform effects (file dialogs, clipboard, etc.)
|
|
344
|
+
tree.rb # normalization, delegation to search/diff
|
|
345
|
+
tree/
|
|
346
|
+
search.rb # find, exists?, ids, find_first, find_all
|
|
347
|
+
diff.rb # LIS-based tree diff (patch generation)
|
|
348
|
+
connection.rb # low-level protocol client (pipe management)
|
|
349
|
+
bridge.rb # renderer lifecycle (restart, backoff)
|
|
350
|
+
runtime.rb # Elm update loop, command/subscription engine
|
|
351
|
+
runtime/
|
|
352
|
+
commands.rb # command execution engine
|
|
353
|
+
subscriptions.rb # subscription lifecycle diffing
|
|
354
|
+
protocol.rb # wire protocol facade
|
|
355
|
+
protocol/
|
|
356
|
+
encode.rb # outbound encoding (all message types)
|
|
357
|
+
decode.rb # inbound decoding (all event families)
|
|
358
|
+
keys.rb # named key and physical key wire maps
|
|
359
|
+
parsers.rb # string-to-symbol parsers (mouse, modifiers)
|
|
360
|
+
binary.rb # renderer binary resolution + download
|
|
361
|
+
cargo_plushie.rb # cargo-plushie resolver (source vs PATH)
|
|
362
|
+
thread_pool.rb # simple bounded thread pool for async
|
|
363
|
+
encode.rb # canonical value encoding (fail-fast)
|
|
364
|
+
widget.rb # unified widget system (Widget.define + include)
|
|
365
|
+
widget_set.rb # widget DSL overrides (WidgetSet.create)
|
|
366
|
+
canvas_widget.rb # canvas widget extension system
|
|
367
|
+
renderer_env.rb # filtered subprocess environment (whitelist)
|
|
368
|
+
dsl/
|
|
369
|
+
buildable.rb # Buildable pattern for DSL block types
|
|
370
|
+
type/ # property type modules
|
|
371
|
+
a11y.rb alignment.rb anchor.rb border.rb color.rb
|
|
372
|
+
content_fit.rb direction.rb filter_method.rb font.rb
|
|
373
|
+
gradient.rb length.rb line_height.rb padding.rb position.rb
|
|
374
|
+
shadow.rb shaping.rb style_map.rb theme.rb wrapping.rb
|
|
375
|
+
widget/ # typed builder modules
|
|
376
|
+
build.rb # shared build helpers
|
|
377
|
+
native_build.rb # native widget build: virtual manifest + cargo-plushie shell-out
|
|
378
|
+
button.rb canvas.rb checkbox.rb column.rb combo_box.rb
|
|
379
|
+
container.rb floating.rb grid.rb image.rb keyed_column.rb
|
|
380
|
+
markdown.rb overlay.rb pane_grid.rb pick_list.rb pin.rb
|
|
381
|
+
pointer_area.rb progress_bar.rb qr_code.rb radio.rb
|
|
382
|
+
responsive.rb rich_text.rb row.rb rule.rb scrollable.rb
|
|
383
|
+
sensor.rb slider.rb space.rb stack.rb svg.rb table.rb
|
|
384
|
+
text.rb text_editor.rb text_input.rb themer.rb toggler.rb
|
|
385
|
+
tooltip.rb vertical_slider.rb window.rb
|
|
386
|
+
canvas/
|
|
387
|
+
shape.rb # pure shape builder functions
|
|
388
|
+
shape/ # typed shape structs
|
|
389
|
+
canvas_image.rb canvas_svg.rb canvas_text.rb circle.rb
|
|
390
|
+
clip.rb dash.rb drag_bounds.rb group.rb hit_rect.rb
|
|
391
|
+
line.rb linear_gradient.rb path.rb rect.rb
|
|
392
|
+
shape_style.rb stroke.rb transform.rb
|
|
393
|
+
transport/
|
|
394
|
+
framing.rb # frame encode/decode for raw byte streams
|
|
395
|
+
tcp_adapter.rb # iostream adapter for TCP sockets
|
|
396
|
+
animation.rb # animation system (descriptors + SDK-side tween)
|
|
397
|
+
animation/
|
|
398
|
+
transition.rb # renderer-side timed transition descriptor
|
|
399
|
+
spring.rb # renderer-side spring physics descriptor
|
|
400
|
+
sequence.rb # renderer-side sequential animation chain
|
|
401
|
+
tween.rb # SDK-side interpolation (host-driven)
|
|
402
|
+
route.rb # navigation stack
|
|
403
|
+
selection.rb # single/multi/range selection
|
|
404
|
+
undo.rb # undo/redo with coalescing
|
|
405
|
+
data.rb # DataQuery: filter/search/sort/paginate
|
|
406
|
+
state.rb # path-based state with revisions
|
|
407
|
+
key_modifiers.rb # modifier query predicates
|
|
408
|
+
dev_server.rb # file watcher + hot reload
|
|
409
|
+
rake.rb # Rake task definitions
|
|
410
|
+
test.rb # test framework loader
|
|
411
|
+
test/
|
|
412
|
+
case.rb # Minitest case with setup/teardown
|
|
413
|
+
rspec.rb # RSpec integration helpers
|
|
414
|
+
helpers.rb # test DSL (click/find/assert_text/etc.)
|
|
415
|
+
session.rb # test session (Elm loop + renderer I/O)
|
|
416
|
+
session_pool.rb # shared renderer process, session mux
|
|
417
|
+
snapshot.rb # tree hash + screenshot assertions
|
|
418
|
+
script.rb # .plushie script parser
|
|
419
|
+
script/
|
|
420
|
+
runner.rb # script executor
|
|
421
|
+
examples/
|
|
422
|
+
counter.rb clock.rb todo.rb async_fetch.rb
|
|
423
|
+
notes.rb shortcuts.rb color_picker.rb rate_plushie.rb
|
|
424
|
+
widgets/ # example custom widget extensions
|
|
425
|
+
color_picker_widget.rb star_rating.rb theme_toggle.rb
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
## Architecture
|
|
429
|
+
|
|
430
|
+
### Layered design
|
|
431
|
+
|
|
432
|
+
- **Layer 0: Wire** (`Protocol::Encode`, `Protocol::Decode`).
|
|
433
|
+
Pure encoding/decoding for every message type in protocol.md.
|
|
434
|
+
No I/O; works with any data source.
|
|
435
|
+
|
|
436
|
+
- **Layer 1: Connection** (`Plushie::Connection`).
|
|
437
|
+
Manages the pipe to the renderer binary. Spawns the process,
|
|
438
|
+
handles framing, thread-safe writes via Mutex, reader thread
|
|
439
|
+
pushes decoded messages to a Queue. Usable standalone for
|
|
440
|
+
scripting and custom architectures.
|
|
441
|
+
|
|
442
|
+
- **Layer 2: Runtime** (`Plushie::Runtime`).
|
|
443
|
+
The Elm update loop. init/update/view cycle, tree diffing,
|
|
444
|
+
command execution (async via ThreadPool, timers, widget/window
|
|
445
|
+
ops, effects), subscription lifecycle, error recovery, renderer
|
|
446
|
+
restart (exponential backoff).
|
|
447
|
+
|
|
448
|
+
- **Layer 3: App** (`Plushie::App`).
|
|
449
|
+
`include Plushie::App` gives the UI DSL, default callbacks,
|
|
450
|
+
convenience aliases (Event, Command, Subscription).
|
|
451
|
+
|
|
452
|
+
- **Layer 4: Test** (`Plushie::Test`).
|
|
453
|
+
Session pool manages a shared `plushie --mock --max-sessions N`
|
|
454
|
+
process. Each test gets an isolated session. All three backends
|
|
455
|
+
(mock/headless/windowed) are transparent.
|
|
456
|
+
|
|
457
|
+
### Concurrency model
|
|
458
|
+
|
|
459
|
+
All state lives in the runtime thread. Events are processed
|
|
460
|
+
sequentially from a `Thread::Queue`. No shared mutable state
|
|
461
|
+
between threads; the Queue is the sole synchronization point.
|
|
462
|
+
|
|
463
|
+
- **Runtime thread**: sequential event processing
|
|
464
|
+
- **Bridge thread**: reads renderer stdout, pushes to queue
|
|
465
|
+
- **Thread pool**: bounded (CPU count) for Command.async work
|
|
466
|
+
- **Timer threads**: for send_after and subscription timers
|
|
467
|
+
- **Write mutex**: Connection#send_message is thread-safe
|
|
468
|
+
|
|
469
|
+
## Testing
|
|
470
|
+
|
|
471
|
+
All app testing goes through the renderer binary. No Ruby-side
|
|
472
|
+
mocks or stubs. The mock backend is fast enough for TDD.
|
|
473
|
+
|
|
474
|
+
Test flow:
|
|
475
|
+
1. Session pool starts `plushie --mock --max-sessions N`.
|
|
476
|
+
2. Test gets a session ID, sends Settings + initial Snapshot.
|
|
477
|
+
3. `click("#btn")` sends Interact to the renderer.
|
|
478
|
+
4. Renderer returns synthetic events via interact_response.
|
|
479
|
+
5. Test feeds events through app.update, re-renders, patches.
|
|
480
|
+
6. `assert_text("#count", "1")` sends Query and checks result.
|
|
481
|
+
|
|
482
|
+
Headless mode uses interact_step round-trips (renderer injects
|
|
483
|
+
real iced events, waits for snapshot back after each step).
|
|
484
|
+
|
|
485
|
+
## Non-obvious patterns
|
|
486
|
+
|
|
487
|
+
**Thread-local DSL context.** The block-based UI DSL uses a
|
|
488
|
+
thread-local stack (`UI::Context`) to track parent-child
|
|
489
|
+
relationships. Blocks execute in the caller's binding (no
|
|
490
|
+
`instance_eval`), so `self` stays the app instance and private
|
|
491
|
+
helpers work. Widget calls (e.g. `button(...)`) append to the
|
|
492
|
+
current context as side effects, not return values.
|
|
493
|
+
|
|
494
|
+
**Model.define immutability.** `Model.define` wraps `Data.define`
|
|
495
|
+
and adds `.with()` for partial updates returning new frozen
|
|
496
|
+
instances. Forgetting to reassign the result is a silent no-op.
|
|
497
|
+
|
|
498
|
+
**Encode fail-fast.** `Encode.encode_value` raises `ArgumentError`
|
|
499
|
+
on unknown types immediately, never silently passing through.
|
|
500
|
+
Custom types implement `.to_wire()` for wire serialization.
|
|
501
|
+
|
|
502
|
+
**Return validation.** `update` must return a bare model or
|
|
503
|
+
`[model, command]`. Invalid shapes raise with a helpful message.
|
|
504
|
+
|
|
505
|
+
**Error recovery.** Exceptions in update/view are rescued
|
|
506
|
+
(`StandardError` only), logged, and the previous model is
|
|
507
|
+
preserved. After 100 consecutive errors, log output is suppressed
|
|
508
|
+
entirely until every 1000th error.
|
|
509
|
+
`NoMatchingPatternError` gets a special message suggesting an
|
|
510
|
+
`else` clause.
|
|
511
|
+
|
|
512
|
+
**Bridge restart.** When the renderer crashes, exponential backoff
|
|
513
|
+
restart (100ms to 1600ms, max 5 retries). The Bridge handles
|
|
514
|
+
connection lifecycle; the Runtime owns the resync (re-sends
|
|
515
|
+
settings, fresh snapshot, subscription sync).
|
|
516
|
+
|
|
517
|
+
**Effect tracking.** Effects use a two-way wire ID mapping
|
|
518
|
+
(`@effect_tags`, `@effect_ids`). Responses arrive with a wire ID
|
|
519
|
+
that the runtime maps back to the user's tag for delivery as
|
|
520
|
+
`Event::Effect`.
|
|
521
|
+
|
|
522
|
+
**Subscription diffing.** After each update, `sync_subscriptions`
|
|
523
|
+
diffs the new set against active ones. If only `max_rate` changed
|
|
524
|
+
(keys unchanged), it short-circuits and updates rates without
|
|
525
|
+
re-creating subscriptions. Renderer subscriptions (key press, etc.)
|
|
526
|
+
do not take a tag; the management key is `[type, window_id]`.
|
|
527
|
+
|
|
528
|
+
**Widget system.** One unified system for all widgets. Two entry
|
|
529
|
+
points: `Widget.define(:type) { ... }` for declarative widgets
|
|
530
|
+
(leaf, container, native) and `include Plushie::Widget` for
|
|
531
|
+
behavioral widgets (with view/state/events). Both share the same
|
|
532
|
+
DSL (prop, children, positional, default_a11y, state, event,
|
|
533
|
+
cache_key) and finalization pipeline. Widget.define returns a
|
|
534
|
+
fully-formed class (mirrors Data.define). Classes using `include`
|
|
535
|
+
are lazily finalized on first instantiation. Widget view output
|
|
536
|
+
is rendered during tree normalization. Events flow through the
|
|
537
|
+
scope chain before reaching `app.update()`.
|
|
538
|
+
|
|
539
|
+
**A11y defaults.** Widgets declare `default_a11y role: :button,
|
|
540
|
+
label_from: :label`. During build, `Build.resolve_a11y` produces
|
|
541
|
+
a string-keyed hash (wire-ready format). The `:label_from`
|
|
542
|
+
directive copies the named prop value into the `label` field.
|
|
543
|
+
User-provided a11y overrides win per field.
|
|
544
|
+
|
|
545
|
+
**Event::Specs.** Canonical catalog of all built-in widget event
|
|
546
|
+
types with carrier (:none/:value), field declarations, and type
|
|
547
|
+
hints. Widget events have category predicates: `event.pointer?`,
|
|
548
|
+
`event.keyboard?`, `event.pane?`, `event.focus?`, `event.drag?`.
|
|
549
|
+
|
|
550
|
+
**Canvas widget registry.** Registry keys are scoped IDs directly
|
|
551
|
+
(e.g. `"main#form/rating"`), matching the normalized tree ID
|
|
552
|
+
format. No separate composite key; `widget_key(window, local)`
|
|
553
|
+
just joins with `#`.
|
|
554
|
+
|
|
555
|
+
**Tree submodules.** Search and diff are extracted into
|
|
556
|
+
`Tree::Search` and `Tree::Diff`. Tree delegates to them. Both
|
|
557
|
+
are type-checked by Steep. The LIS-based diff algorithm produces
|
|
558
|
+
minimal patches for reordered children.
|
|
559
|
+
|
|
560
|
+
**Memo caching.** `UI::MemoCache` is a thread-local prev/current
|
|
561
|
+
cache. The runtime seeds it before each render and captures it
|
|
562
|
+
after. Memo nodes (`__memo__`) check the cache by deps; widget
|
|
563
|
+
`cache_key` checks by props+state. Both skip re-rendering on
|
|
564
|
+
cache hit.
|
|
565
|
+
|
|
566
|
+
**Status-based focus tracking.** The renderer emits `status`
|
|
567
|
+
events for interactive widgets. The runtime intercepts these to
|
|
568
|
+
track `@focused_widget_id` and derives `:focused`/`:blurred`
|
|
569
|
+
Widget events from status transitions. No per-widget opt-in
|
|
570
|
+
needed.
|
|
571
|
+
|
|
572
|
+
**Bridge heartbeat.** A watchdog timer detects hung renderers.
|
|
573
|
+
When no message arrives within `heartbeat_interval` (default
|
|
574
|
+
30s), a synthetic close is pushed to trigger the restart path.
|
|
575
|
+
|
|
576
|
+
## Reference SDK
|
|
577
|
+
|
|
578
|
+
The plushie-elixir SDK (`../plushie-elixir/`) is the primary
|
|
579
|
+
reference for Ruby due to similar dynamic language conventions.
|
|
580
|
+
Consult it for architecture patterns when adding features.
|
|
581
|
+
|
|
582
|
+
## Related repositories
|
|
583
|
+
|
|
584
|
+
These are expected as sibling directories (e.g. `../plushie-rust/`):
|
|
585
|
+
|
|
586
|
+
- plushie-rust - Rust workspace (SDK, widget SDK, renderer)
|
|
587
|
+
- plushie-elixir - Elixir SDK (reference implementation)
|
|
588
|
+
- plushie-gleam - Gleam SDK
|
|
589
|
+
- plushie-iced - vendored iced fork
|
|
590
|
+
|
|
591
|
+
## Protocol reference
|
|
592
|
+
|
|
593
|
+
The wire protocol is defined in `../plushie-rust/docs/protocol.md`.
|
|
594
|
+
That document is the source of truth for all message types, event
|
|
595
|
+
families, and interaction semantics.
|