rust_widgets 2.8.4

Pure Rust cross-platform native GUI library with hardware-adaptive rendering, 180 widgets, touch/gesture support, i18n, and SVG-pipeline-accurate output
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
// SPDX-FileCopyrightText: Copyright (c) 2026 Mike Li/Mikewolfli/Wei Li(mikewolfli@163.com)
// SPDX-License-Identifier: MIT

//! Runtime storage and application for declarative layouts.
//!
//! # Why this module exists separately from `json::layout`
//!
//! The declarative engine was one module: it read `serde_json` values into layout
//! kinds, built the layout objects, stored them per parent, and applied them by
//! writing child geometries. Only the *parsing* needs `serde_json`. Keeping the rest
//! there meant the C ABI — which builds a layout from a kind and four numbers, and
//! has no JSON to parse — could not reach any of it without depending on the JSON
//! module.
//!
//! So the split is by dependency, not by taste:
//!
//! * **here** — the registry and the geometry application, depending only on
//!   `crate::layout` and the widget runtime;
//! * **`json::layout`** — the `serde_json` → layout-kind translation, which calls
//!   into this module.
//!
//! Both paths then share one implementation of "store a layout and move the
//! children", which is what stops the JSON loader and the C ABI from drifting into
//! two subtly different layout engines.
//!
//! # Thread affinity
//!
//! The registry is thread-local, so a layout stored on one thread is invisible to
//! another. That is deliberate rather than incidental: a layout holds `ObjectId`s
//! that are only meaningful in the thread that mounted them, so sharing the map
//! across threads would let one thread apply a layout to another's widgets. The
//! consequence is that declarative layouts are single-threaded by construction, and
//! applying one from the wrong thread is a silent no-op rather than a corruption.

use crate::compat::{Box, HashMap, Vec};
use core::cell::RefCell;

use crate::core::{ObjectId, Rect};
use crate::layout::Layout;

/// Sentinel child id a layout uses to reserve stretchable space.
///
/// The generic [`Layout`] trait has no spacer method, so the declarative engine
/// smuggles a spacer through as a child whose id is this value — the widest possible
/// `ObjectId`, which no real widget can hold. Layouts that do not know the sentinel
/// treat it as an ordinary child and allocate it space, which is the intended effect.
///
/// It is exposed rather than private because a layout implementation outside this
/// module needs to recognise it, and because [`apply_layout`] must exclude it from
/// the geometries it writes: writing a geometry for this id would address nothing.
pub const SPACER_ID: ObjectId = ObjectId::MAX;

thread_local! {
    /// Layouts by parent id. See the module docs for why this is thread-local.
    #[allow(clippy::missing_const_for_thread_local)]
    static LAYOUT_MAP: RefCell<HashMap<ObjectId, Box<dyn Layout>>> =
        RefCell::new(HashMap::new());
}

/// Stores a layout manager for a parent widget, replacing any previous one.
///
/// Replacing rather than merging is the only defensible behaviour: two layouts on one
/// parent would each compute geometries for the same children and the second would
/// win arbitrarily.
pub fn store_layout(parent_id: ObjectId, layout: Box<dyn Layout>) {
    LAYOUT_MAP.with(|map| {
        map.borrow_mut().insert(parent_id, layout);
    });
}

/// Removes the layout stored for `parent_id`.
///
/// Returns `true` when there was one. Unmounting a container should call this, or the
/// registry keeps a layout whose children no longer exist and whose ids may later be
/// reused.
pub fn forget_layout(parent_id: ObjectId) -> bool {
    LAYOUT_MAP.with(|map| map.borrow_mut().remove(&parent_id).is_some())
}

/// Whether a layout is stored for `parent_id`.
pub fn has_layout(parent_id: ObjectId) -> bool {
    LAYOUT_MAP.with(|map| map.borrow().contains_key(&parent_id))
}

/// How many layouts are stored on this thread.
pub fn layout_count() -> usize {
    LAYOUT_MAP.with(|map| map.borrow().len())
}

/// Registers a widget as a layout child with its stretch factor.
///
/// Silent no-op when no layout is stored for `parent_id`. The stretch factor is
/// stored verbatim; `0` is not a valid stretch in most layout implementations and may
/// make the child invisible.
///
/// Returns `true` when a layout was found and told about the child.
pub fn add_widget_to_layout(child_id: ObjectId, stretch: u32, parent_id: ObjectId) -> bool {
    LAYOUT_MAP.with(|map| {
        let mut map = map.borrow_mut();
        match map.get_mut(&parent_id) {
            Some(layout) => {
                layout.add_widget(child_id, stretch);
                true
            }
            None => false,
        }
    })
}

/// Where a declaratively placed child sits, for the layouts that accept explicit cells.
///
/// # Why this is a separate call from [`add_widget_to_layout`]
///
/// Only some layouts have cells. The `Layout` trait's `add_widget` takes a widget and a
/// stretch factor and nothing else, and widening it for the one grid-shaped member would make
/// every one of the fifteen implementations carry a parameter all but one ignores. The cell
/// is therefore offered through the trait's downcasting door: a layout that understands cells
/// implements it, and one that does not is told nothing rather than being handed a placement
/// it would have to discard.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ChildPlacement {
    /// Column of the child's top-left cell.
    pub col: u32,
    /// Row of the child's top-left cell.
    pub row: u32,
    /// Columns covered, at least 1.
    pub col_span: u32,
    /// Rows covered, at least 1.
    pub row_span: u32,
}

/// Places `child_id` at an explicit cell of the layout stored for `parent_id`.
///
/// Returns `true` when the layout both exists and understands cells, so a caller can report
/// a placement that would otherwise be silently dropped — the failure this exists to prevent.
/// A layout that does not use cells (`BoxLayout`, `FlexLayout`, …) returns `false`, and the
/// caller decides whether that is a problem: the declarative loader turns it into a warning,
/// because a document that wrote `col`/`row` on a non-grid parent asked for something the
/// parent cannot do.
pub fn place_widget_in_layout(
    child_id: ObjectId,
    placement: ChildPlacement,
    parent_id: ObjectId,
) -> bool {
    LAYOUT_MAP.with(|map| {
        let mut map = map.borrow_mut();
        let Some(layout) = map.get_mut(&parent_id) else {
            return false;
        };
        match layout.as_any_mut().downcast_mut::<crate::layout::GridLayout>() {
            Some(grid) => {
                grid.set_widget_spanning(
                    placement.row,
                    placement.col,
                    placement.col_span,
                    placement.row_span,
                    child_id,
                );
                true
            }
            None => false,
        }
    })
}

/// Removes a widget from the layout stored for `parent_id`.
///
/// Returns `true` when a layout was found and told about the removal.
pub fn remove_widget_from_layout(child_id: ObjectId, parent_id: ObjectId) -> bool {
    LAYOUT_MAP.with(|map| {
        let mut map = map.borrow_mut();
        match map.get_mut(&parent_id) {
            Some(layout) => {
                layout.remove_widget(child_id);
                true
            }
            None => false,
        }
    })
}

/// Records a spacer with `stretch` for a parent layout.
///
/// See [`SPACER_ID`] for how a spacer is represented and why. Note the signature has
/// no layout argument: the earlier version took one and ignored it, looking the layout
/// up by parent id anyway, which made the parameter a misleading promise that the
/// caller's layout would be used.
///
/// Returns `true` when a layout was found.
pub fn add_spacer_to_layout(stretch: u32, parent_id: ObjectId) -> bool {
    add_widget_to_layout(SPACER_ID, stretch, parent_id)
}

/// Recomputes child geometries and writes them to the widgets.
///
/// Returns the geometries that were applied, so a caller can assert on the result
/// rather than on a count of side effects.
///
/// # Why the spacer is excluded
///
/// A layout reports a geometry for every child it holds, including [`SPACER_ID`].
/// Writing that one would call `set_widget_geometry` for an id that addresses no
/// widget, which is at best a wasted lookup and at worst a collision with an id the
/// runtime later issues. It is filtered here, at the one place geometries become
/// widget state.
pub fn apply_layout(parent_id: ObjectId, rect: Rect) -> Vec<(ObjectId, Rect)> {
    let geometries = LAYOUT_MAP.with(|map| {
        let map = map.borrow();
        let Some(layout) = map.get(&parent_id) else {
            return Vec::new();
        };
        let mut geometries = Vec::new();
        layout.update(rect, &mut |child_id, child_rect| {
            if child_id != SPACER_ID {
                geometries.push((child_id, child_rect));
            }
        });
        geometries
    });

    // `set_widget_geometry` is compiled out in the alloc-frugal profile (there is no
    // widget runtime to address), so there is nothing to move the geometries onto.
    // The computed list is still returned: a caller in that profile can use it to
    // place whatever it draws, which is the honest answer rather than a silent no-op.
    #[cfg(not(alloc_frugal))]
    for (child_id, child_rect) in &geometries {
        crate::set_widget_geometry(
            *child_id,
            child_rect.x,
            child_rect.y,
            child_rect.width,
            child_rect.height,
        );
    }

    geometries
}

/// The geometry a layout would compute, without writing anything.
///
/// Lets a caller preview or validate a layout — a design-time inspector, or a test
/// asserting the arrangement — without moving the widgets.
pub fn preview_layout(parent_id: ObjectId, rect: Rect) -> Vec<(ObjectId, Rect)> {
    LAYOUT_MAP.with(|map| {
        let map = map.borrow();
        let Some(layout) = map.get(&parent_id) else {
            return Vec::new();
        };
        let mut geometries = Vec::new();
        layout.update(rect, &mut |child_id, child_rect| {
            if child_id != SPACER_ID {
                geometries.push((child_id, child_rect));
            }
        });
        geometries
    })
}

/// Name of a `DeclarativeLayoutKind`, for diagnostics and the C ABI.
///
/// Defined here rather than on the enum so this module does not depend on the
/// `serde_json`-facing one; the enum's own `Display`-like spelling is a presentation
/// concern this module can answer without owning the type.
pub fn layout_kind_name(kind: LayoutKindName) -> &'static str {
    match kind {
        LayoutKindName::HorizontalBox => "hbox",
        LayoutKindName::VerticalBox => "vbox",
        LayoutKindName::Grid => "grid",
        LayoutKindName::Form => "form",
        LayoutKindName::Stack => "stack",
        LayoutKindName::HBox => "hbox",
    }
}

/// The layout spellings this module can name.
///
/// A separate enum rather than a reference to `json::DeclarativeLayoutKind`, because
/// naming a layout must work in profiles where the JSON module is not compiled.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LayoutKindName {
    /// A row of children.
    HorizontalBox,
    /// A column of children.
    VerticalBox,
    /// A row/column grid.
    Grid,
    /// Two-column label/field rows.
    Form,
    /// One visible child at a time.
    Stack,
    /// Alias of [`Self::HorizontalBox`], kept because the declarative spelling uses it.
    HBox,
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::layout::{BoxLayout, Orientation};

    fn box_layout() -> Box<dyn Layout> {
        Box::new(BoxLayout::new(Orientation::Horizontal, 0, 0))
    }

    /// A layout stored on one thread is invisible to another, which is the documented
    /// thread affinity rather than an accident.
    ///
    /// Spawned thread rather than a second `thread_local` cell so the assertion is
    /// about the real map.
    #[test]
    fn layouts_are_thread_local() {
        const PARENT: ObjectId = 0x5EED;
        store_layout(PARENT, box_layout());
        assert!(has_layout(PARENT));

        let seen = std::thread::spawn(move || has_layout(PARENT))
            .join()
            .expect("the probe thread must not panic");
        assert!(!seen, "a layout stored on one thread must not be visible on another");

        assert!(forget_layout(PARENT));
    }

    #[test]
    fn storing_replaces_rather_than_accumulates() {
        const PARENT: ObjectId = 0x5EEF;
        let before = layout_count();

        store_layout(PARENT, box_layout());
        store_layout(PARENT, box_layout());
        assert_eq!(layout_count(), before + 1, "one parent holds one layout");

        assert!(forget_layout(PARENT));
        assert_eq!(layout_count(), before);
    }

    #[test]
    fn forgetting_an_unknown_parent_is_false() {
        assert!(!forget_layout(0xDEAD_BEEF));
    }

    /// Adding a child to a parent with no layout must report the miss rather than
    /// silently doing nothing.
    #[test]
    fn adding_a_child_without_a_layout_reports_failure() {
        assert!(!add_widget_to_layout(1, 0, 0xDEAD_BEEF));
        assert!(!add_spacer_to_layout(1, 0xDEAD_BEEF));
    }

    /// Applying a layout must produce one geometry per child, and must not produce one
    /// for the spacer sentinel.
    #[test]
    fn applying_a_layout_returns_child_geometries_and_skips_the_spacer() {
        const PARENT: ObjectId = 0x5EF0;
        store_layout(PARENT, box_layout());
        assert!(add_widget_to_layout(101, 0, PARENT));
        assert!(add_widget_to_layout(102, 0, PARENT));
        assert!(add_spacer_to_layout(1, PARENT));

        let geometries = preview_layout(PARENT, Rect::new(0, 0, 300, 100));
        assert_eq!(geometries.len(), 2, "the spacer must not appear as a widget");
        let ids: Vec<ObjectId> = geometries.iter().map(|(id, _)| *id).collect();
        assert_eq!(ids, vec![101, 102]);

        assert!(forget_layout(PARENT));
    }

    /// A horizontal box places its two children side by side, which is the assertion
    /// that separates "the layout ran" from "the layout did something".
    #[test]
    fn a_horizontal_box_places_children_side_by_side() {
        const PARENT: ObjectId = 0x5EF1;
        store_layout(PARENT, box_layout());
        add_widget_to_layout(201, 0, PARENT);
        add_widget_to_layout(202, 0, PARENT);

        let geometries = preview_layout(PARENT, Rect::new(0, 0, 300, 100));
        let first = geometries.iter().find(|(id, _)| *id == 201).expect("first child").1;
        let second = geometries.iter().find(|(id, _)| *id == 202).expect("second child").1;

        assert!(
            second.x > first.x,
            "the second child must start after the first: {first:?} then {second:?}"
        );
        assert_eq!(first.y, second.y, "a horizontal box keeps one row");

        assert!(forget_layout(PARENT));
    }

    #[test]
    fn applying_an_unknown_parent_yields_nothing() {
        assert!(apply_layout(0xDEAD_BEEF, Rect::new(0, 0, 10, 10)).is_empty());
        assert!(preview_layout(0xDEAD_BEEF, Rect::new(0, 0, 10, 10)).is_empty());
    }

    /// Unregistering must be possible, or a reused id would inherit a stale layout.
    #[test]
    fn removing_a_child_reaches_the_layout() {
        const PARENT: ObjectId = 0x5EF2;
        store_layout(PARENT, box_layout());
        add_widget_to_layout(301, 0, PARENT);
        assert!(remove_widget_from_layout(301, PARENT));

        let geometries = preview_layout(PARENT, Rect::new(0, 0, 100, 50));
        assert!(
            geometries.iter().all(|(id, _)| *id != 301),
            "a removed child must not be laid out"
        );

        assert!(!remove_widget_from_layout(301, 0xDEAD_BEEF));
        assert!(forget_layout(PARENT));
    }
}