Skip to main content

bevy_react/protocol/
props.rs

1//! [`Props`] — the content/attribute level of a host element — and its
2//! dirty/event bookkeeping ([`PropsDirty`], [`UpdateEvents`]).
3
4use serde::Deserialize;
5
6use crate::canvas::DrawCmd;
7
8use super::background_image::{AtlasSpec, ImageMode, SourceRect};
9use super::style::{Style, StyleDirty};
10
11/// Props for a host element. Event handlers never cross the boundary — the
12/// reconciler replaces them with booleans (e.g. `onClick: true`) and keeps the
13/// actual function in a JS-side map. Visual styling lives entirely in [`Style`];
14/// the fields here are content/attribute level.
15#[derive(Debug, Clone, Default, Deserialize)]
16#[serde(rename_all = "camelCase")]
17pub struct Props {
18    /// CSS-like layout + visual style, mapped onto `bevy_ui` components.
19    #[serde(default)]
20    pub style: Option<Style>,
21    /// Style overlaid on `style` while the element is hovered. Decoded exactly
22    /// like `style`; applied on the Bevy side from the node's `Interaction`.
23    #[serde(default)]
24    pub hover_style: Option<Style>,
25    /// Style overlaid on `style` (and `hover_style`) while the element is pressed.
26    #[serde(default)]
27    pub press_style: Option<Style>,
28    /// Style overlaid on `style` while the element is focused (currently
29    /// `editableText`). Applied on the Bevy side from the node's focus state, so
30    /// focus styling needs no React round-trip.
31    #[serde(default)]
32    pub focus_style: Option<Style>,
33    /// Whether this element has an `onClick` handler registered in JS.
34    #[serde(default)]
35    pub on_click: bool,
36    /// Whether this element has an `onPointerDown` handler registered in JS.
37    #[serde(default)]
38    pub on_pointer_down: bool,
39    /// Whether this element has an `onPointerMove` handler registered in JS.
40    /// Fires each frame while the pointer is held down (a drag).
41    #[serde(default)]
42    pub on_pointer_move: bool,
43    /// Whether this element has an `onPointerUp` handler registered in JS.
44    #[serde(default)]
45    pub on_pointer_up: bool,
46    /// Whether this element has an `onPointerEnter` handler registered in JS.
47    /// Fires once when the pointer enters the element (hover begins).
48    #[serde(default)]
49    pub on_pointer_enter: bool,
50    /// Whether this element has an `onPointerLeave` handler registered in JS.
51    /// Fires once when the pointer leaves the element (hover ends).
52    #[serde(default)]
53    pub on_pointer_leave: bool,
54
55    // --- controlled scroll (any node with `overflow: scroll`) ---
56    /// Controlled vertical scroll offset (logical px) → `ScrollPosition.y`. On
57    /// update it's pushed into the node only when it diverges from the live offset
58    /// (so a re-render echoing the user's own wheel scroll is a no-op — see
59    /// [`crate::reconcile`]). Each axis is independent; absent leaves it alone.
60    #[serde(default)]
61    pub scroll_top: Option<f32>,
62    /// Controlled horizontal scroll offset (logical px) → `ScrollPosition.x`.
63    #[serde(default)]
64    pub scroll_left: Option<f32>,
65    /// Logical pixels scrolled per mouse-wheel "line" for this container, overriding
66    /// the default. Maps to [`crate::bridge::ScrollStep`]; only scales `Line`-unit
67    /// wheels (trackpad `Pixel` deltas are used raw).
68    #[serde(default)]
69    pub scroll_step: Option<f32>,
70    /// Whether this element has an `onScroll` handler registered in JS. Present →
71    /// the reconciler stamps a [`crate::bridge::ScrollListener`] so the read-back
72    /// system reports offset changes (kept cheap by scoping its `Changed` query to
73    /// that marker, since `ScrollPosition` is a required component of every `Node`).
74    #[serde(default)]
75    pub on_scroll: bool,
76    /// Whether this element has an `onWheel` handler registered in JS. Present →
77    /// the reconciler stamps a [`crate::bridge::WheelListener`] so
78    /// [`crate::scroll::collect_wheel_events`] reports raw wheel deltas over the
79    /// node (any node, unlike `onScroll`, which needs `overflow: scroll`).
80    #[serde(default)]
81    pub on_wheel: bool,
82
83    /// World-anchor binding for an `<anchor>` element: the Bevy entity to follow and
84    /// an optional offset. Present → the reconciler stamps a [`crate::anchor::Anchored`]
85    /// so the per-frame positioning system tracks it. Pure-serde, Bevy-free.
86    #[serde(default)]
87    pub anchor: Option<crate::anchor::Anchor>,
88
89    // --- `image` element attributes ---
90    /// Asset path for an `image`, resolved by Bevy's `AssetServer` (relative to
91    /// the app's `assets/` folder). Absent → a solid-color image (see `tint`).
92    #[serde(default)]
93    pub src: Option<String>,
94    /// Tint multiplied with the image (hex); also the fill of a `src`-less image.
95    #[serde(default)]
96    pub tint: Option<String>,
97    /// Flip the image along its x-axis.
98    #[serde(default)]
99    pub flip_x: bool,
100    /// Flip the image along its y-axis.
101    #[serde(default)]
102    pub flip_y: bool,
103    /// How the image fits its box: the keyword `"auto"`/`"stretch"`, or a
104    /// `type`-tagged object for 9-slice (`"sliced"`) / `"tiled"` scaling.
105    #[serde(default)]
106    pub image_mode: Option<ImageMode>,
107    /// Source sub-rect of the texture to display, in source-texture pixels.
108    /// Maps to `ImageNode.rect`. With `atlas`, it offsets from the atlas cell's
109    /// top-left corner.
110    #[serde(default)]
111    pub source_rect: Option<SourceRect>,
112    /// Treat `src` as a uniform sprite-sheet grid and select one cell. Maps to
113    /// `ImageNode.texture_atlas` (builds/caches a `TextureAtlasLayout`).
114    #[serde(default)]
115    pub atlas: Option<AtlasSpec>,
116    /// Which box of the node the image fills: `"content"` | `"padding"`
117    /// (default) | `"border"`. Maps to `ImageNode.visual_box`.
118    #[serde(default)]
119    pub visual_box: Option<String>,
120
121    // --- `canvas` element attributes ---
122    /// The declarative display list for a `canvas` element: an ordered batch of
123    /// vector draw commands (the recorded form of an HTML-canvas-like
124    /// `ctx.moveTo/lineTo/…` session). Present → the retained surface is
125    /// **cleared and the list replayed** (raster state reset first).
126    /// `Some(vec![])` clears the canvas; absent leaves the retained pixels.
127    /// Imperative (accumulating) drawing rides [`super::op::Op::Draw`] instead.
128    #[serde(default)]
129    pub draw: Option<Vec<DrawCmd>>,
130    /// Whether this element has an `onResize` handler registered in JS. Cached
131    /// only so the delta stays truthful — `"resize"` events are **not** gated
132    /// on it (the JS runtime consumes them unconditionally, to replay a
133    /// declarative painter and keep the canvas handle's size fresh).
134    #[serde(default)]
135    pub on_resize: bool,
136
137    // --- `portal` element attribute ---
138    /// The render-target name a `portal` element displays. The reconciler stamps
139    /// a `crate::portal::RPortal` carrying it; the binding system points the
140    /// node's `ImageNode` at the texture the app registered under this name (or a
141    /// transparent placeholder until it appears). Pure-serde, Bevy-free.
142    #[serde(default)]
143    pub target: Option<String>,
144
145    // --- `svg` element + shape-child attributes ---
146    /// The folded SVG attributes of a shape child (`<circle>`/`<rect>`/…)
147    /// inside an `<svg>` element. The JS side folds the flat JSX attrs into
148    /// this one object; on update it **replaces atomically** (see
149    /// [`Props::merge_delta`]).
150    #[serde(default)]
151    pub shape: Option<crate::svg::ShapeAttrs>,
152    /// The `<svg>` element's `viewBox` (`"minX minY width height"`), parsed
153    /// at the serde boundary. (`rename_all = "camelCase"` yields exactly the
154    /// `viewBox` wire name — pinned by a test.)
155    #[serde(default, deserialize_with = "crate::svg::de_view_box")]
156    pub view_box: Option<crate::svg::ViewBox>,
157
158    // --- `editableText` element attributes ---
159    /// The controlled text value of an `editableText`. Seeds the field on create;
160    /// on update it's pushed into the widget only when it diverges from the live
161    /// buffer (so normal typing is never clobbered — see [`crate::reconcile`]).
162    #[serde(default)]
163    pub value: Option<String>,
164    /// Maximum number of characters an `editableText` accepts.
165    #[serde(default)]
166    pub max_length: Option<usize>,
167    /// Whether an `editableText` accepts newlines (multi-line input).
168    #[serde(default)]
169    pub multiline: bool,
170    /// Whether this element has an `onChange` handler registered in JS.
171    #[serde(default)]
172    pub on_change: bool,
173    /// Focus an `editableText` when it mounts (inserts `AutoFocus`).
174    #[serde(default)]
175    pub autofocus: bool,
176    /// Controlled selection anchor, a UTF-8 **byte** offset into the value.
177    /// When `selection_start`/`selection_end` diverge from the live selection
178    /// they're pushed into the widget (see [`crate::reconcile`]).
179    #[serde(default)]
180    pub selection_start: Option<usize>,
181    /// Controlled selection focus, a UTF-8 **byte** offset into the value.
182    #[serde(default)]
183    pub selection_end: Option<usize>,
184    /// Accessible name announced to assistive tech (sets the a11y node's label).
185    #[serde(default)]
186    pub aria_label: Option<String>,
187    /// Whether this element has an `onSelect` handler registered in JS.
188    #[serde(default)]
189    pub on_select: bool,
190    /// Whether this element has an `onFocus` handler registered in JS.
191    #[serde(default)]
192    pub on_focus: bool,
193    /// Whether this element has an `onBlur` handler registered in JS.
194    #[serde(default)]
195    pub on_blur: bool,
196}
197
198/// Which parts of a [`Props`] a delta update touched; drives which of the
199/// reconciler's `apply_*` helpers run. Style granularity lives in
200/// [`StyleDirty`]; the other flags are per prop group.
201#[derive(Debug, Clone, Copy, Default)]
202pub struct PropsDirty {
203    /// Style groups touched via `style` / `style_unset`.
204    pub style: StyleDirty,
205    /// `hoverStyle` set or unset.
206    pub hover_style: bool,
207    /// `pressStyle` set or unset.
208    pub press_style: bool,
209    /// `focusStyle` set or unset.
210    pub focus_style: bool,
211    /// Any of `onClick` / `onPointerDown|Move|Up|Enter|Leave` toggled.
212    pub pointer: bool,
213    /// `onScroll` toggled.
214    pub scroll_listener: bool,
215    /// `onWheel` toggled.
216    pub wheel: bool,
217    /// `scrollStep` changed.
218    pub scroll_step: bool,
219    /// `anchor` changed.
220    pub anchor: bool,
221    /// Any `image` attribute (`src`/`tint`/`flipX`/`flipY`/`imageMode`/
222    /// `sourceRect`/`atlas`/`visualBox`) changed.
223    pub image: bool,
224    /// `target` (portal/surface binding) changed.
225    pub target: bool,
226    /// `shape` (an SVG shape child's folded attrs) changed.
227    pub shape: bool,
228    /// `viewBox` (an `<svg>` element's user-unit rect) changed.
229    pub view_box: bool,
230    /// Any `editableText` handler flag (`onChange`/`onSelect`/`onFocus`/
231    /// `onBlur`) toggled.
232    pub editable_handlers: bool,
233    /// `ariaLabel` changed.
234    pub aria_label: bool,
235}
236
237impl PropsDirty {
238    /// Whether the [`crate::bridge::StyleVariants`] component needs rebuilding:
239    /// its `base` mirrors `style`, so any style-field change counts too.
240    pub fn any_style_variant(&self) -> bool {
241        self.style.any() || self.hover_style || self.press_style || self.focus_style
242    }
243}
244
245/// The "act now" props of an update, split from the retained state: pushed
246/// into the live widget once and never stored, so an unrelated later delta
247/// can't replay them (re-push a controlled value, re-clone a canvas display
248/// list). Absent fields mean "no event", exactly like the pre-delta protocol.
249#[derive(Debug, Default)]
250pub struct UpdateEvents {
251    /// Controlled `editableText` value to push (when diverging).
252    pub value: Option<String>,
253    /// Controlled selection anchor (UTF-8 byte offset).
254    pub selection_start: Option<usize>,
255    /// Controlled selection focus (UTF-8 byte offset).
256    pub selection_end: Option<usize>,
257    /// Controlled vertical scroll offset.
258    pub scroll_top: Option<f32>,
259    /// Controlled horizontal scroll offset.
260    pub scroll_left: Option<f32>,
261    /// A `<canvas>` display list to clear + replay.
262    pub draw: Option<Vec<DrawCmd>>,
263}
264
265/// Test helper shared by the protocol submodules' unit tests: decode a
266/// `Props` from a JSON value, panicking on malformed input.
267#[cfg(test)]
268pub(crate) fn props_from_json(json: serde_json::Value) -> Props {
269    serde_json::from_value(json).expect("valid props")
270}