Skip to main content

stipple_core/
element.rs

1//! The element IR: the lowered, paint-ready description of a UI subtree.
2//!
3//! Declarative [`View`](crate::View)s build a tree of [`Element`]s; the layout
4//! and paint passes (see [`crate::render`]) consume it. Keeping a concrete IR
5//! between widgets and rendering is what lets the (future) reactive runtime
6//! diff one tree against the next.
7
8use crate::runtime::{
9    ActionId, ContextId, Cx, DragId, FocusId, KeyInput, ScrollId, TextPosId, ViewportId,
10};
11use stipple_geometry::Insets;
12use stipple_layout::Axis;
13use stipple_render::Color;
14
15/// Alignment of children along an axis.
16#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
17pub enum Align {
18    #[default]
19    Start,
20    Center,
21    End,
22    /// Cross-axis only: stretch children to fill the cross extent.
23    Stretch,
24}
25
26/// Fixed-size overrides for an element. `None` means "size to content / fill".
27#[derive(Clone, Copy, Debug, Default, PartialEq)]
28pub struct SizeOverride {
29    pub width: Option<f64>,
30    pub height: Option<f64>,
31}
32
33/// Layout properties common to every element.
34#[derive(Clone, Copy, Debug, Default, PartialEq)]
35pub struct LayoutStyle {
36    pub padding: Insets,
37    pub size: SizeOverride,
38    /// Main-axis grow weight when this element is a flex child. `0.0` = fixed.
39    pub grow: f64,
40}
41
42/// The painted decoration of an element's box: fill, corner radius, border.
43/// Applies to any element — a leaf bar, a button, or a container panel.
44#[derive(Clone, Copy, Debug, Default, PartialEq)]
45pub struct BoxStyle {
46    pub fill: Option<Color>,
47    pub radius: f64,
48    /// Border as `(color, width)`.
49    pub border: Option<(Color, f64)>,
50}
51
52/// What an element arranges; all kinds share [`Element::decoration`].
53#[derive(Clone, Debug, PartialEq)]
54pub enum ElementKind {
55    /// A leaf with no children.
56    Leaf,
57    /// A single line of text. Sizes to the shaped run (via the active font);
58    /// painted with [`Scene::fill_text`](stipple_render::Scene::fill_text).
59    Text {
60        text: String,
61        size: f64,
62        color: Color,
63    },
64    /// A linear container that lays children along `axis`.
65    Stack {
66        axis: Axis,
67        gap: f64,
68        main_align: Align,
69        cross_align: Align,
70        children: Vec<Element>,
71    },
72    /// An embedded-content viewport: a leaf reserving a rectangle the compositor
73    /// fills with externally-rendered pixels for `id`. Sizes to its
74    /// [`width`](Element::width)/[`height`](Element::height) overrides (or grow),
75    /// not to content.
76    Viewport { id: ViewportId },
77}
78
79/// A node in the element tree: layout properties, decoration, an optional
80/// interaction handle, and a kind.
81///
82/// `PartialEq` enables the runtime's no-op repaint skip: if a rebuilt tree
83/// equals the previous one, the cached frame is reused. Handler ids compare by
84/// value and are assigned deterministically per build, so an unchanged view
85/// produces an equal tree.
86#[derive(Clone, Debug, PartialEq)]
87pub struct Element {
88    pub layout: LayoutStyle,
89    pub decoration: BoxStyle,
90    /// Handler this element routes pointer taps to, if any. Set via
91    /// [`Element::on_tap`]; resolved against the [`Cx`] handler table.
92    pub action: Option<ActionId>,
93    /// Focus + keyboard handle, if this element is focusable. Set via
94    /// [`Element::on_key`].
95    pub focus: Option<FocusId>,
96    /// Drag handle, if this element responds to pointer drags. Set via
97    /// [`Element::on_drag`].
98    pub drag: Option<DragId>,
99    /// Secondary-click (context) handle: this element opens a context menu on
100    /// right-click. Set via [`Element::on_context`].
101    pub context: Option<ContextId>,
102    /// Caret position as a byte index into this element's text, for an editable
103    /// text leaf. `None` for non-editable text and non-text elements. Set via
104    /// [`Element::caret`]; the focus overlay draws the caret bar there.
105    pub caret: Option<usize>,
106    /// Selected byte range `[start, end)` into this element's text, for an
107    /// editable text leaf. `None` when there is no selection. Set via
108    /// [`Element::selection`]; the focus overlay highlights it.
109    pub selection: Option<(usize, usize)>,
110    /// Text-pointer handle: pointer presses/drags on this element resolve to a
111    /// byte index in its text. Set via [`Element::on_text_pos`].
112    pub text_pos: Option<TextPosId>,
113    /// When `true` on a text element, the text greedily word-wraps to the
114    /// element's content width instead of laying out on one line. Set via
115    /// [`Element::wrap`].
116    pub wrap: bool,
117    /// Scroll-container handle: when set, this element lays its children out at
118    /// natural size (overflowing its bounds), clips them, and routes wheel events
119    /// to the app's offset for this id. Set via [`Element::scrollable`].
120    pub scroll: Option<ScrollId>,
121    /// When `true`, children are clipped to this element's painted bounds. Set
122    /// implicitly by [`Element::scrollable`] and via [`Element::clip`].
123    pub clip: bool,
124    pub kind: ElementKind,
125}
126
127impl Element {
128    /// A decorated leaf (background/button/divider).
129    pub fn boxed(style: BoxStyle) -> Self {
130        Self {
131            layout: LayoutStyle::default(),
132            decoration: style,
133            action: None,
134            focus: None,
135            drag: None,
136            context: None,
137            caret: None,
138            selection: None,
139            text_pos: None,
140            wrap: false,
141            scroll: None,
142            clip: false,
143            kind: ElementKind::Leaf,
144        }
145    }
146
147    /// A single line of text in `color` at `size` logical pixels.
148    pub fn text(text: impl Into<String>, size: f64, color: Color) -> Self {
149        Self {
150            layout: LayoutStyle::default(),
151            decoration: BoxStyle::default(),
152            action: None,
153            focus: None,
154            drag: None,
155            context: None,
156            caret: None,
157            selection: None,
158            text_pos: None,
159            wrap: false,
160            scroll: None,
161            clip: false,
162            kind: ElementKind::Text {
163                text: text.into(),
164                size,
165                color,
166            },
167        }
168    }
169
170    /// An embedded-content viewport leaf for `id`: a reserved rectangle the app
171    /// composites externally-rendered content into. Give it a size via
172    /// [`width`](Element::width)/[`height`](Element::height) (or
173    /// [`grow`](Element::grow) inside a flex parent); add a
174    /// [`border`](Element::border) to frame it. The app locates it by `id` with
175    /// [`collect_viewports`](crate::collect_viewports) to blit content over the
176    /// placeholder, and routes pointer/keys landing inside it to that content.
177    pub fn viewport(id: ViewportId) -> Self {
178        Self {
179            layout: LayoutStyle::default(),
180            decoration: BoxStyle::default(),
181            action: None,
182            focus: None,
183            drag: None,
184            context: None,
185            caret: None,
186            selection: None,
187            text_pos: None,
188            wrap: false,
189            scroll: None,
190            clip: false,
191            kind: ElementKind::Viewport { id },
192        }
193    }
194
195    /// An undecorated container laying `children` along `axis`.
196    pub fn stack(axis: Axis, children: Vec<Element>) -> Self {
197        Self {
198            layout: LayoutStyle::default(),
199            decoration: BoxStyle::default(),
200            action: None,
201            focus: None,
202            drag: None,
203            context: None,
204            caret: None,
205            selection: None,
206            text_pos: None,
207            wrap: false,
208            scroll: None,
209            clip: false,
210            kind: ElementKind::Stack {
211                axis,
212                gap: 0.0,
213                main_align: Align::Start,
214                cross_align: Align::Start,
215                children,
216            },
217        }
218    }
219
220    /// Route pointer taps on this element to `handler`, which runs against the
221    /// app state. Registers the handler in `cx` and stamps its [`ActionId`].
222    ///
223    /// ```
224    /// # use stipple_core::{Element, BoxStyle, runtime::Cx};
225    /// # use stipple_style::Theme;
226    /// let theme = Theme::light();
227    /// let mut cx = Cx::new(&theme);
228    /// let button = Element::boxed(BoxStyle::default())
229    ///     .width(80.0)
230    ///     .height(32.0)
231    ///     .on_tap(&mut cx, |count: &mut i32| *count += 1);
232    /// assert!(button.action.is_some());
233    /// ```
234    pub fn on_tap<S>(mut self, cx: &mut Cx<'_, S>, handler: impl FnMut(&mut S) + 'static) -> Self {
235        self.action = Some(cx.register(handler));
236        self
237    }
238
239    /// Route secondary (right) clicks on this element to `handler`, which runs
240    /// against the app state and receives the click position in logical pixels —
241    /// typically used to open a context menu there via [`Cx::overlay`]. Registers
242    /// the handler in `cx` and stamps its [`ContextId`].
243    pub fn on_context<S>(
244        mut self,
245        cx: &mut Cx<'_, S>,
246        handler: impl FnMut(&mut S, stipple_geometry::Point) + 'static,
247    ) -> Self {
248        self.context = Some(cx.register_context(handler));
249        self
250    }
251
252    /// Make this element focusable and route keyboard input to `handler`.
253    /// The handler receives a [`KeyInput`] (committed text or an editing key)
254    /// while this element holds focus. Registers in `cx` and stamps the
255    /// resulting [`FocusId`].
256    pub fn on_key<S>(
257        mut self,
258        cx: &mut Cx<'_, S>,
259        handler: impl FnMut(&mut S, &KeyInput) + 'static,
260    ) -> Self {
261        self.focus = Some(cx.register_key(handler));
262        self
263    }
264
265    /// Make this element respond to pointer drags. The `handler` receives the
266    /// pointer's fractional x position (0..=1) across the element on press and
267    /// while dragging. Registers in `cx` and stamps the resulting [`DragId`].
268    pub fn on_drag<S>(
269        mut self,
270        cx: &mut Cx<'_, S>,
271        handler: impl FnMut(&mut S, f64) + 'static,
272    ) -> Self {
273        self.drag = Some(cx.register_drag(handler));
274        self
275    }
276
277    /// Resolve pointer presses/drags on this element to a byte index in its
278    /// text. The `handler` receives the resolved index and an `extend` flag
279    /// (`false` = initial press / place caret, `true` = drag / extend
280    /// selection). Registers in `cx` and stamps the resulting [`TextPosId`].
281    pub fn on_text_pos<S>(
282        mut self,
283        cx: &mut Cx<'_, S>,
284        handler: impl FnMut(&mut S, usize, bool) + 'static,
285    ) -> Self {
286        self.text_pos = Some(cx.register_text_pos(handler));
287        self
288    }
289
290    // --- decoration modifiers ---
291
292    pub fn fill(mut self, color: Color) -> Self {
293        self.decoration.fill = Some(color);
294        self
295    }
296
297    pub fn radius(mut self, radius: f64) -> Self {
298        self.decoration.radius = radius;
299        self
300    }
301
302    pub fn border(mut self, color: Color, width: f64) -> Self {
303        self.decoration.border = Some((color, width));
304        self
305    }
306
307    // --- layout modifiers ---
308
309    pub fn padding(mut self, insets: Insets) -> Self {
310        self.layout.padding = insets;
311        self
312    }
313
314    pub fn width(mut self, w: f64) -> Self {
315        self.layout.size.width = Some(w);
316        self
317    }
318
319    pub fn height(mut self, h: f64) -> Self {
320        self.layout.size.height = Some(h);
321        self
322    }
323
324    pub fn grow(mut self, grow: f64) -> Self {
325        self.layout.grow = grow;
326        self
327    }
328
329    /// Mark this text element's caret position (a byte index into its text), so
330    /// the focus overlay draws the caret bar there instead of at the end. No
331    /// effect on non-text elements.
332    pub fn caret(mut self, byte_index: usize) -> Self {
333        self.caret = Some(byte_index);
334        self
335    }
336
337    /// Mark a selected byte range `[start, end)` into this text element, so the
338    /// focus overlay highlights it. An empty or reversed range is ignored.
339    pub fn selection(mut self, range: Option<(usize, usize)>) -> Self {
340        self.selection = range.filter(|(s, e)| e > s);
341        self
342    }
343
344    /// Enable word-wrapping for a text element: the text wraps to the element's
345    /// content width across as many lines as needed. No effect on non-text
346    /// elements.
347    pub fn wrap(mut self) -> Self {
348        self.wrap = true;
349        self
350    }
351
352    /// Clip this element's children to its painted bounds (e.g. for an overlay
353    /// panel). [`scrollable`](Element::scrollable) sets this implicitly.
354    pub fn clip(mut self) -> Self {
355        self.clip = true;
356        self
357    }
358
359    /// Make this element a scroll container for `id`: its children lay out at
360    /// natural size (overflowing), are clipped to its bounds, and wheel events
361    /// over it scroll the app's offset for `id`. Give it a fixed height (e.g.
362    /// `.height(..)`) to define the viewport.
363    pub fn scrollable(mut self, id: ScrollId) -> Self {
364        self.scroll = Some(id);
365        self.clip = true;
366        self
367    }
368
369    pub fn gap(mut self, gap: f64) -> Self {
370        if let ElementKind::Stack { gap: g, .. } = &mut self.kind {
371            *g = gap;
372        }
373        self
374    }
375
376    pub fn align(mut self, main: Align, cross: Align) -> Self {
377        if let ElementKind::Stack {
378            main_align,
379            cross_align,
380            ..
381        } = &mut self.kind
382        {
383            *main_align = main;
384            *cross_align = cross;
385        }
386        self
387    }
388}