//! Widget tree — the data structure laid out by [`super::layout`]. //! //! Stage 8 splits widgets cleanly into **what** (the [`WidgetKind`]) and //! **how** (the [`LayoutStyle`] held on every node). The kind decides whether //! a node has children and how they're arranged; the style is the same fields //! on every widget so the layout algorithm has one place to look. //! //! Piece 1 ships only what the layout algorithm needs: a [`Leaf`](WidgetKind::Leaf) //! placeholder with an intrinsic size, and three container kinds — [`Stack`] //! (row/column), [`Grid`], and [`AnchorGroup`]. Interactive widgets (button, //! checkbox, slider, text input, …) are layered on top in later pieces by //! decorating leaves with kind-specific style/state; they all participate in //! the same layout pass without the algorithm having to know about them. //! //! # Building a tree //! //! ``` //! use glam::Vec2; //! use oxide_engine::ui::{Insets, LayoutStyle, Sizing, Widget}; //! //! let panel = Widget::row() //! .with_id("toolbar") //! .with_style(LayoutStyle { //! width: Sizing::Grow(1.0), //! height: Sizing::Fixed(32.0), //! padding: Insets::all(4.0), //! ..Default::default() //! }) //! .with_child(Widget::leaf(Vec2::new(80.0, 24.0)).with_id("file")) //! .with_child(Widget::leaf(Vec2::new(80.0, 24.0)).with_id("edit")); //! assert_eq!(panel.children().len(), 2); //! ``` use glam::Vec2; use serde::{Deserialize, Serialize}; use super::style::LayoutStyle; use super::value::WidgetValue; use super::visual::VisualStyle; /// Stable identifier for a widget — used to look up its laid-out rect in a /// [`LayoutTree`](super::layout::LayoutTree) and (in later pieces) to wire up /// input routing and data binding. /// /// Stored as `String` so UI documents can ship author-facing names (`"play"`, /// `"volume-slider"`) straight through RON. The empty id (`""`) is the default /// and means "anonymous"; multiple anonymous widgets are allowed and lookups /// by empty id are rejected by [`LayoutTree::find`](super::layout::LayoutTree::find). #[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct WidgetId(pub String); impl WidgetId { /// `true` if the id string is empty. pub fn is_empty(&self) -> bool { self.0.is_empty() } /// Borrow the underlying string. pub fn as_str(&self) -> &str { &self.0 } } impl From<&str> for WidgetId { fn from(s: &str) -> Self { Self(s.to_owned()) } } /// A path from a root [`Widget`] to one of its descendants: the sequence of /// child indices to follow from the root. The **empty** path denotes the root /// itself. /// /// Unlike [`WidgetId`] (optional, author-facing, possibly absent or duplicated) /// a path addresses *exactly one* node positionally, so it is what the editor's /// UI canvas uses to target structural edits — insert, remove, move — and to /// record them on the undo stack. Paths are only valid against the tree they /// were derived from; an edit that changes sibling order invalidates the paths /// after it (the move helper accounts for this itself). #[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct WidgetPath(pub Vec); impl WidgetPath { /// The root path (addresses the tree's root widget). pub fn root() -> Self { Self(Vec::new()) } /// Whether this path addresses the root (is empty). pub fn is_root(&self) -> bool { self.0.is_empty() } /// Depth from the root (number of indices). pub fn len(&self) -> usize { self.0.len() } /// Whether the path is empty — alias of [`is_root`](Self::is_root), provided /// for the clippy `len`/`is_empty` pairing. pub fn is_empty(&self) -> bool { self.0.is_empty() } /// A child path one level deeper, selecting child `index`. pub fn child(&self, index: usize) -> Self { let mut v = self.0.clone(); v.push(index); Self(v) } /// Splits into `(parent_path, last_index)`, or `None` for the root. pub fn split_last(&self) -> Option<(WidgetPath, usize)> { let (last, rest) = self.0.split_last()?; Some((WidgetPath(rest.to_vec()), *last)) } /// Whether `self` is `other` or lies underneath it (prefix test). Used to /// reject moving a subtree into its own descendant. pub fn starts_with(&self, other: &WidgetPath) -> bool { self.0.starts_with(&other.0) } } impl From for WidgetId { fn from(s: String) -> Self { Self(s) } } /// A widget tree node — id, layout style, optional visual style + theme /// reference, and a kind that decides what children it holds. /// /// `style` (Stage-8 piece 1) controls layout — where the widget is. /// `visual` (piece 2) carries per-instance visual overrides — what the /// widget looks like — and `theme_style` opts into a named entry in the /// project's [`Theme`](super::theme::Theme). Both default to empty so a /// piece-1 UI document still parses unchanged. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] pub struct Widget { #[serde(default, skip_serializing_if = "WidgetId::is_empty")] pub id: WidgetId, #[serde(default)] pub style: LayoutStyle, #[serde(default, skip_serializing_if = "VisualStyle::is_empty")] pub visual: VisualStyle, #[serde(default, skip_serializing_if = "Option::is_none")] pub theme_style: Option, /// Text content shaped inside this widget's `content_rect`. Orthogonal /// to `kind`: a button is a `Leaf` with `text` + `visual.background`; a /// label is a `Leaf` with `text` only. Renderers shape this string /// against the resolved [`VisualStyle::font`] and [`VisualStyle::font_size`]. #[serde(default, skip_serializing_if = "Option::is_none")] pub text: Option, /// Per-widget typed state — `Bool` for a checkbox, `Float` for a /// slider, `Text` for a text input. Orthogonal to `kind`; absent /// means "no state". See [`super::value::WidgetValue`] and the /// piece-6 [`Widget::value`](Self::value) / [`set_value`](Self::set_value) /// helpers. #[serde(default, skip_serializing_if = "Option::is_none")] pub value: Option, pub kind: WidgetKind, } /// What a widget *is* — leaf or one of three container layout modes. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum WidgetKind { /// A childless node with an intrinsic logical size. Real interactive /// widgets (label, button, image) layer on top of this in later pieces. Leaf { intrinsic: Vec2 }, /// Row or column container. Stack(Stack), /// Equal-cell grid container. Grid(Grid), /// Container that positions each child via the child's own /// [`Anchor`](super::style::Anchor). Anchor(AnchorGroup), } impl Default for WidgetKind { fn default() -> Self { Self::Leaf { intrinsic: Vec2::ZERO, } } } /// Stack container — arranges children along a main axis. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] pub struct Stack { pub direction: StackDirection, /// Logical-pixel gap between adjacent children. #[serde(default)] pub gap: f32, /// How leftover space on the main axis is distributed *after* children /// have been sized. Ignored when any child uses [`Sizing::Grow`](super::style::Sizing::Grow), /// since `Grow` consumes the leftover space directly. #[serde(default)] pub main_align: super::style::Align, #[serde(default)] pub children: Vec, } /// Direction of a [`Stack`]. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)] pub enum StackDirection { /// Children flow left-to-right. #[default] Row, /// Children flow top-to-bottom. Column, } /// Equal-cell grid container — `cols × rows` cells filled in row-major order. /// /// Piece-1 grids are intentionally simple: every cell is the same size, /// computed from the parent's content rect. More flexible grids (auto-sized /// rows/columns, spans) are a follow-up; the use cases the editor's Stage-7 /// preferences page and the Stage-8 settings examples actually need are all /// served by the equal-cell case. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] pub struct Grid { pub cols: u32, pub rows: u32, /// `gap.x` between columns, `gap.y` between rows (logical pixels). #[serde(default)] pub gap: Vec2, #[serde(default)] pub children: Vec, } /// Anchor container — each child is placed according to its own /// [`LayoutStyle::anchor`](super::style::LayoutStyle::anchor). #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] pub struct AnchorGroup { #[serde(default)] pub children: Vec, } impl Widget { /// Build a leaf widget with the given intrinsic logical size. pub fn leaf(intrinsic: Vec2) -> Self { Self { kind: WidgetKind::Leaf { intrinsic }, ..Default::default() } } /// Build an empty stack with the given direction (gap 0, default align). pub fn stack(direction: StackDirection) -> Self { Self { kind: WidgetKind::Stack(Stack { direction, ..Default::default() }), ..Default::default() } } /// Shortcut for `Widget::stack(StackDirection::Row)`. pub fn row() -> Self { Self::stack(StackDirection::Row) } /// Shortcut for `Widget::stack(StackDirection::Column)`. pub fn column() -> Self { Self::stack(StackDirection::Column) } /// Build an empty grid container. pub fn grid(cols: u32, rows: u32) -> Self { Self { kind: WidgetKind::Grid(Grid { cols, rows, ..Default::default() }), ..Default::default() } } /// Build an empty anchor container. pub fn anchor() -> Self { Self { kind: WidgetKind::Anchor(AnchorGroup::default()), ..Default::default() } } /// Set the widget id (builder). pub fn with_id(mut self, id: impl Into) -> Self { self.id = id.into(); self } /// Replace the whole [`LayoutStyle`] (builder). pub fn with_style(mut self, style: LayoutStyle) -> Self { self.style = style; self } /// Replace the per-instance [`VisualStyle`] (builder). pub fn with_visual(mut self, visual: VisualStyle) -> Self { self.visual = visual; self } /// Opt this widget into a named entry of the active /// [`Theme`](super::theme::Theme) (builder). Pass `""` or call /// [`Widget::clear_theme_style`] to remove the reference. pub fn with_theme_style(mut self, name: impl Into) -> Self { let name = name.into(); self.theme_style = if name.is_empty() { None } else { Some(name) }; self } /// Drop any `theme_style` reference (builder). pub fn clear_theme_style(mut self) -> Self { self.theme_style = None; self } /// Set this widget's text content (builder). Pass `""` to clear it. The /// text is shaped at paint time against the widget's resolved font and /// font size from the active theme. pub fn with_text(mut self, text: impl Into) -> Self { let s = text.into(); self.text = if s.is_empty() { None } else { Some(s) }; self } /// Set this widget's typed value (builder). pub fn with_value(mut self, value: impl Into) -> Self { self.value = Some(value.into()); self } /// Set the stack gap (builder). Panics if not a stack — surfaces author /// mistakes during construction rather than producing a silently /// misshapen UI at layout time. pub fn with_gap(mut self, gap: f32) -> Self { match &mut self.kind { WidgetKind::Stack(s) => s.gap = gap, _ => panic!("with_gap is only valid on Stack widgets"), } self } /// Set the stack main-axis alignment (builder). Panics if not a stack. pub fn with_main_align(mut self, align: super::style::Align) -> Self { match &mut self.kind { WidgetKind::Stack(s) => s.main_align = align, _ => panic!("with_main_align is only valid on Stack widgets"), } self } /// Set the grid gap vector (builder). Panics if not a grid. pub fn with_grid_gap(mut self, gap: Vec2) -> Self { match &mut self.kind { WidgetKind::Grid(g) => g.gap = gap, _ => panic!("with_grid_gap is only valid on Grid widgets"), } self } /// Append a single child to a container widget (builder). Panics on a /// leaf so the misuse is caught at construction. pub fn with_child(mut self, child: Widget) -> Self { children_mut(&mut self.kind, |c| c.push(child)); self } /// Append many children (builder). pub fn with_children(mut self, children: impl IntoIterator) -> Self { children_mut(&mut self.kind, |c| c.extend(children)); self } /// Borrow the direct children of this widget. Empty for leaves. pub fn children(&self) -> &[Widget] { match &self.kind { WidgetKind::Leaf { .. } => &[], WidgetKind::Stack(s) => &s.children, WidgetKind::Grid(g) => &g.children, WidgetKind::Anchor(a) => &a.children, } } /// Borrow the direct children mutably. Empty slice for leaves. /// /// Underpins [`find_by_id_mut`](Self::find_by_id_mut) and the piece-6 /// data-binding helpers; safer than reaching into `kind` because all /// container kinds funnel through one accessor. pub fn children_mut(&mut self) -> &mut [Widget] { match &mut self.kind { WidgetKind::Leaf { .. } => &mut [], WidgetKind::Stack(s) => &mut s.children, WidgetKind::Grid(g) => &mut g.children, WidgetKind::Anchor(a) => &mut a.children, } } /// Borrow this widget's children as the owning `Vec`, or `None` for a /// [`Leaf`](WidgetKind::Leaf) (which cannot hold children). Unlike /// [`children_mut`](Self::children_mut) this exposes the `Vec` itself, so /// callers can insert/remove — the basis of the structural edits below. pub fn children_vec_mut(&mut self) -> Option<&mut Vec> { match &mut self.kind { WidgetKind::Leaf { .. } => None, WidgetKind::Stack(s) => Some(&mut s.children), WidgetKind::Grid(g) => Some(&mut g.children), WidgetKind::Anchor(a) => Some(&mut a.children), } } /// Whether this widget is a container (can hold children) rather than a leaf. pub fn is_container(&self) -> bool { !matches!(self.kind, WidgetKind::Leaf { .. }) } /// Borrow the widget addressed by `path` (the root for the empty path), or /// `None` if any index along the way is out of range. pub fn get_path(&self, path: &WidgetPath) -> Option<&Widget> { let mut node = self; for &i in &path.0 { node = node.children().get(i)?; } Some(node) } /// Mutable counterpart of [`get_path`](Self::get_path). pub fn get_path_mut(&mut self, path: &WidgetPath) -> Option<&mut Widget> { let mut node = self; for &i in &path.0 { node = node.children_mut().get_mut(i)?; } Some(node) } /// Inserts `child` at `index` among the children of the widget addressed by /// `parent`, returning whether it succeeded. `index` is clamped to the /// child count (so it can append). Fails if `parent` does not resolve or is /// a leaf. pub fn insert_child(&mut self, parent: &WidgetPath, index: usize, child: Widget) -> bool { let Some(parent) = self.get_path_mut(parent) else { return false; }; let Some(children) = parent.children_vec_mut() else { return false; }; children.insert(index.min(children.len()), child); true } /// Appends `child` to the children of the widget addressed by `parent`. /// Convenience over [`insert_child`](Self::insert_child) with a trailing /// index. pub fn push_child_at(&mut self, parent: &WidgetPath, child: Widget) -> bool { self.insert_child(parent, usize::MAX, child) } /// Removes and returns the widget addressed by `path`. The root cannot be /// removed (returns `None` for the empty path), nor can an out-of-range or /// unreachable path. pub fn remove_path(&mut self, path: &WidgetPath) -> Option { let (parent, index) = path.split_last()?; let children = self.get_path_mut(&parent)?.children_vec_mut()?; (index < children.len()).then(|| children.remove(index)) } /// Moves the subtree at `from` to be child `index` of `to_parent`, /// returning whether it succeeded. Rejects moving the root, or moving a node /// into itself or one of its own descendants. Sibling indices shift when the /// node is detached, so both `to_parent` and `index` are adjusted internally /// to mean what the caller intended *before* the move. pub fn move_subtree( &mut self, from: &WidgetPath, to_parent: &WidgetPath, index: usize, ) -> bool { if from.is_root() || to_parent.starts_with(from) { return false; } // The destination must exist and be a container; check before detaching // (removing `from`, which is not an ancestor of `to_parent`, leaves the // destination node itself unchanged — only its path may shift). if !self.get_path(to_parent).is_some_and(Widget::is_container) { return false; } let Some(node) = self.remove_path(from) else { return false; }; let to_parent = adjust_path_for_removal(to_parent, from); let (from_parent, from_index) = from.split_last().expect("non-root checked above"); // Inserting back into the same parent after the detach point shifts the // target slot down by one. let index = if from_parent.0 == to_parent.0 && from_index < index { index - 1 } else { index }; self.insert_child(&to_parent, index, node) } /// Find a descendant (or self) with this id. Returns the first match /// in pre-order. `None` if no widget matches (or `id` is empty). pub fn find_by_id(&self, id: &WidgetId) -> Option<&Widget> { if id.is_empty() { return None; } if self.id == *id { return Some(self); } for child in self.children() { if let Some(found) = child.find_by_id(id) { return Some(found); } } None } /// Mutable counterpart of [`find_by_id`](Self::find_by_id). pub fn find_by_id_mut(&mut self, id: &WidgetId) -> Option<&mut Widget> { if id.is_empty() { return None; } if self.id == *id { return Some(self); } for child in self.children_mut() { if let Some(found) = child.find_by_id_mut(id) { return Some(found); } } None } /// Borrow the [`WidgetValue`] of the descendant with this id, if any. /// One half of the piece-6 data-binding loop: read what the UI says. pub fn value(&self, id: &WidgetId) -> Option<&WidgetValue> { self.find_by_id(id).and_then(|w| w.value.as_ref()) } /// Set the [`WidgetValue`] of the descendant with this id, returning /// `true` if such a widget exists. The other half of the piece-6 /// data-binding loop: write game state into the UI. pub fn set_value(&mut self, id: &WidgetId, value: impl Into) -> bool { match self.find_by_id_mut(id) { Some(w) => { w.value = Some(value.into()); true } None => false, } } /// Recursive count of nodes including `self`. Handy for sanity checks /// in tests when comparing against a [`LayoutTree::nodes`](super::layout::LayoutTree::nodes) /// length. pub fn node_count(&self) -> usize { 1 + self .children() .iter() .map(Widget::node_count) .sum::() } /// Resolve this widget's effective [`VisualStyle`] under a given theme, /// cascading `theme.default` → `theme.styles[self.theme_style]` → /// `self.visual`. See [`Theme::resolve`](super::theme::Theme::resolve) /// for the merge rules. Children are *not* recursively resolved here — /// piece 4 walks the tree pairing each [`super::layout::LayoutNode`] with /// its resolved style. pub fn resolve_visual(&self, theme: &super::theme::Theme) -> VisualStyle { theme.resolve(self.theme_style.as_deref(), &self.visual) } /// Serialize this widget tree to a pretty-printed RON string — the /// canonical UI-document format an editor saves and the runtime loads. pub fn to_ron(&self) -> Result { ron::ser::to_string_pretty(self, ron::ser::PrettyConfig::default()) } /// Parse a widget tree from a RON string produced by [`to_ron`](Self::to_ron). pub fn from_ron(text: &str) -> Result { ron::de::from_str(text) } } /// Rewrites `path` to stay valid after the widget at `removed` is detached. /// /// Detaching shifts the later siblings of `removed` down by one. A path is /// affected only if it descends through `removed`'s parent and its index at /// that depth is *after* the removed index; then that one index decrements. /// `path` must not be `removed` or beneath it (the caller guarantees this). fn adjust_path_for_removal(path: &WidgetPath, removed: &WidgetPath) -> WidgetPath { let Some((removed_parent, removed_index)) = removed.split_last() else { return path.clone(); }; let depth = removed_parent.0.len(); let mut out = path.0.clone(); if out.len() > depth && out[..depth] == removed_parent.0[..] && out[depth] > removed_index { out[depth] -= 1; } WidgetPath(out) } fn children_mut(kind: &mut WidgetKind, f: impl FnOnce(&mut Vec)) { match kind { WidgetKind::Stack(s) => f(&mut s.children), WidgetKind::Grid(g) => f(&mut g.children), WidgetKind::Anchor(a) => f(&mut a.children), WidgetKind::Leaf { .. } => panic!("cannot add children to a Leaf widget"), } } #[cfg(test)] mod tests { use super::*; /// A row root with three leaf children id'd "a","b","c". fn abc_tree() -> Widget { Widget::row() .with_id("root") .with_child(Widget::leaf(Vec2::ZERO).with_id("a")) .with_child(Widget::leaf(Vec2::ZERO).with_id("b")) .with_child(Widget::leaf(Vec2::ZERO).with_id("c")) } fn ids_of(children: &[Widget]) -> Vec<&str> { children.iter().map(|w| w.id.as_str()).collect() } #[test] fn get_path_addresses_nodes() { let root = abc_tree(); assert_eq!( root.get_path(&WidgetPath::root()).unwrap().id.as_str(), "root" ); assert_eq!( root.get_path(&WidgetPath(vec![1])).unwrap().id.as_str(), "b" ); assert!(root.get_path(&WidgetPath(vec![9])).is_none()); } #[test] fn insert_and_remove_children_by_path() { let mut root = abc_tree(); // Insert "x" between a and b. assert!(root.insert_child( &WidgetPath::root(), 1, Widget::leaf(Vec2::ZERO).with_id("x") )); assert_eq!(ids_of(root.children()), ["a", "x", "b", "c"]); // Append "z" via the clamping path. assert!(root.push_child_at(&WidgetPath::root(), Widget::leaf(Vec2::ZERO).with_id("z"))); assert_eq!(ids_of(root.children()), ["a", "x", "b", "c", "z"]); // A leaf rejects children; the root cannot be removed. assert!(!root.insert_child(&WidgetPath(vec![0]), 0, Widget::default())); assert!(root.remove_path(&WidgetPath::root()).is_none()); // Remove "x". let removed = root.remove_path(&WidgetPath(vec![1])).unwrap(); assert_eq!(removed.id.as_str(), "x"); assert_eq!(ids_of(root.children()), ["a", "b", "c", "z"]); } #[test] fn move_subtree_reorders_within_parent() { let mut root = abc_tree(); // Move "a" (index 0) to the end (index 3 in pre-removal terms). assert!(root.move_subtree(&WidgetPath(vec![0]), &WidgetPath::root(), 3)); assert_eq!(ids_of(root.children()), ["b", "c", "a"]); } #[test] fn move_subtree_across_branches_adjusts_paths() { // root[ col(0) [a], b(1), c(2) ]: move c into the column before a. let mut root = Widget::row() .with_id("root") .with_child( Widget::column() .with_id("col") .with_child(Widget::leaf(Vec2::ZERO).with_id("a")), ) .with_child(Widget::leaf(Vec2::ZERO).with_id("b")) .with_child(Widget::leaf(Vec2::ZERO).with_id("c")); assert!(root.move_subtree(&WidgetPath(vec![2]), &WidgetPath(vec![0]), 0)); // c now leads the column; root has col + b left. assert_eq!( ids_of(root.get_path(&WidgetPath(vec![0])).unwrap().children()), ["c", "a"] ); assert_eq!(ids_of(root.children()), ["col", "b"]); } #[test] fn move_subtree_rejects_into_own_descendant_and_root() { let mut root = Widget::row().with_id("root").with_child( Widget::column() .with_id("col") .with_child(Widget::leaf(Vec2::ZERO).with_id("a")), ); // Can't move "col" (path [0]) under its own child "a" (path [0,0]). assert!(!root.move_subtree(&WidgetPath(vec![0]), &WidgetPath(vec![0, 0]), 0)); // Can't move the root. assert!(!root.move_subtree(&WidgetPath::root(), &WidgetPath(vec![0]), 0)); // Tree is unchanged. assert_eq!(ids_of(root.children()), ["col"]); } #[test] fn widget_id_from_str_and_string() { let a: WidgetId = "abc".into(); let b: WidgetId = String::from("abc").into(); assert_eq!(a, b); assert_eq!(a.as_str(), "abc"); assert!(!a.is_empty()); assert!(WidgetId::default().is_empty()); } #[test] fn default_widget_is_zero_leaf() { let w = Widget::default(); assert_eq!(w.id, WidgetId::default()); assert_eq!(w.style, LayoutStyle::default()); assert!(matches!(w.kind, WidgetKind::Leaf { intrinsic } if intrinsic == Vec2::ZERO)); } #[test] fn builder_methods_compose() { let w = Widget::row() .with_id("toolbar") .with_gap(4.0) .with_main_align(super::super::style::Align::Center) .with_child(Widget::leaf(Vec2::new(10.0, 10.0)).with_id("a")) .with_children([Widget::leaf(Vec2::new(20.0, 10.0)).with_id("b")]); assert_eq!(w.id.as_str(), "toolbar"); let WidgetKind::Stack(s) = &w.kind else { panic!("expected stack"); }; assert_eq!(s.direction, StackDirection::Row); assert_eq!(s.gap, 4.0); assert_eq!(s.main_align, super::super::style::Align::Center); assert_eq!(s.children.len(), 2); assert_eq!(s.children[0].id.as_str(), "a"); assert_eq!(s.children[1].id.as_str(), "b"); } #[test] #[should_panic(expected = "cannot add children to a Leaf widget")] fn adding_child_to_leaf_panics() { let _ = Widget::leaf(Vec2::new(1.0, 1.0)).with_child(Widget::leaf(Vec2::ONE)); } #[test] #[should_panic(expected = "with_gap is only valid on Stack widgets")] fn gap_on_non_stack_panics() { let _ = Widget::grid(2, 2).with_gap(4.0); } #[test] fn node_count_recurses() { let tree = Widget::row() .with_child(Widget::leaf(Vec2::ONE)) .with_child( Widget::column() .with_child(Widget::leaf(Vec2::ONE)) .with_child(Widget::leaf(Vec2::ONE)), ); // root + leaf + (column + 2 leaves) = 5 assert_eq!(tree.node_count(), 5); } #[test] fn widget_round_trips_through_ron() { let w = Widget::row() .with_id("root") .with_gap(8.0) .with_child(Widget::leaf(Vec2::new(40.0, 20.0)).with_id("a")) .with_child(Widget::anchor().with_child(Widget::leaf(Vec2::new(10.0, 10.0)))); let text = ron::ser::to_string_pretty(&w, ron::ser::PrettyConfig::default()).unwrap(); let decoded: Widget = ron::de::from_str(&text).unwrap(); assert_eq!(w, decoded); } #[test] fn visual_and_theme_style_builders_set_fields() { use super::super::visual::VisualStyle; use crate::math::Color; let w = Widget::leaf(Vec2::ONE) .with_id("a") .with_visual(VisualStyle { background: Some(Color::RED), ..VisualStyle::EMPTY }) .with_theme_style("button"); assert_eq!(w.visual.background, Some(Color::RED)); assert_eq!(w.theme_style.as_deref(), Some("button")); // Passing an empty string drops the reference. let cleared = w.clone().with_theme_style(""); assert_eq!(cleared.theme_style, None); let explicitly_cleared = w.clear_theme_style(); assert_eq!(explicitly_cleared.theme_style, None); } #[test] fn resolve_visual_cascades_theme_named_overrides() { use super::super::theme::Theme; use super::super::visual::VisualStyle; use crate::math::Color; let theme = Theme::new() .with_default(VisualStyle { foreground: Some(Color::BLACK), background: Some(Color::WHITE), ..VisualStyle::EMPTY }) .with_style( "button", VisualStyle { background: Some(Color::rgb(0.85, 0.85, 0.9)), ..VisualStyle::EMPTY }, ); let w = Widget::leaf(Vec2::ONE) .with_theme_style("button") .with_visual(VisualStyle { foreground: Some(Color::RED), ..VisualStyle::EMPTY }); let resolved = w.resolve_visual(&theme); assert_eq!(resolved.foreground, Some(Color::RED)); // per-instance assert_eq!(resolved.background, Some(Color::rgb(0.85, 0.85, 0.9))); // named } #[test] fn widget_with_visual_and_theme_style_round_trips_through_ron() { use super::super::visual::{FontRef, VisualStyle}; use crate::math::Color; let w = Widget::row() .with_id("toolbar") .with_theme_style("toolbar") .with_visual(VisualStyle { background: Some(Color::rgb(0.1, 0.1, 0.1)), font: Some(FontRef::bold("Inter")), ..VisualStyle::EMPTY }) .with_child(Widget::leaf(Vec2::new(40.0, 20.0)).with_theme_style("button")); let text = w.to_ron().unwrap(); let decoded = Widget::from_ron(&text).unwrap(); assert_eq!(w, decoded); } #[test] fn default_widget_serializes_without_new_fields() { // The new `visual` and `theme_style` fields skip when empty/None, so // a piece-1 default widget should still serialize to the piece-1 // form (no `visual:` or `theme_style:` keys in the output). let w = Widget::default(); let text = w.to_ron().unwrap(); assert!(!text.contains("visual:")); assert!(!text.contains("theme_style:")); // And re-parsing yields the same value. assert_eq!(Widget::from_ron(&text).unwrap(), w); } #[test] fn find_by_id_walks_the_subtree() { let tree = Widget::row() .with_id("root") .with_child(Widget::leaf(Vec2::ONE).with_id("a")) .with_child( Widget::column() .with_id("group") .with_child(Widget::leaf(Vec2::ONE).with_id("buried")), ); assert_eq!(tree.find_by_id(&"root".into()).unwrap().id.as_str(), "root"); assert_eq!(tree.find_by_id(&"a".into()).unwrap().id.as_str(), "a"); assert_eq!( tree.find_by_id(&"buried".into()).unwrap().id.as_str(), "buried" ); assert!(tree.find_by_id(&"missing".into()).is_none()); // Empty id is never a match. assert!(tree.find_by_id(&WidgetId::default()).is_none()); } #[test] fn set_value_updates_a_descendant() { let mut tree = Widget::row() .with_id("root") .with_child(Widget::leaf(Vec2::ONE).with_id("volume")) .with_child(Widget::leaf(Vec2::ONE).with_id("invert_y")); assert!(tree.set_value(&"volume".into(), 0.75_f32)); assert!(tree.set_value(&"invert_y".into(), true)); assert_eq!( tree.value(&"volume".into()).and_then(|v| v.as_float()), Some(0.75_f32 as f64) ); assert_eq!( tree.value(&"invert_y".into()).and_then(|v| v.as_bool()), Some(true) ); // Unknown id: returns false, tree unchanged. assert!(!tree.set_value(&"missing".into(), 0.0_f32)); } #[test] fn with_value_builder_sets_value() { let w = Widget::leaf(Vec2::ONE).with_id("checkbox").with_value(true); assert_eq!(w.value.as_ref().unwrap().as_bool(), Some(true)); } #[test] fn value_round_trips_through_widget_ron() { use super::super::value::WidgetValue; let w = Widget::leaf(Vec2::ONE) .with_id("slider") .with_value(WidgetValue::Float(0.42)); let text = w.to_ron().unwrap(); let decoded = Widget::from_ron(&text).unwrap(); assert_eq!(w, decoded); } }