GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
feat(widget): the container concern + first containers: Switcher, ContentBg (Phase 5n)
The children/tree concern, designed transitional-first: tree links already
live in ctx.tree (Phase 1b — the Element defaults route through it, and
Adapted has a base id), so what containers actually need is (a) their own
child-pointer Vec (ctx-less set_rect arrangement) and (b) the subtree
plumbing every legacy container hand-copied. The model keeps (a) through
new Layout hooks (has_container_children / container_children /
child_added / children_cleared / parent_changed / adjust_rect /
arrange_children / layout_children_ctx / child_visible); the ADAPTER now
does (b) once, filtered by the child_visible policy: plain-quad
aggregation with the shared rounded-bg-skip rule (all_quads), rounded
recursion matching the Element default this override had been shadowing
(all_rounded_quads), per-kind text aggregation (each child contributes its
own fonts/bounds), get_text_items / prepare_text / tick / popover_rect /
render_popover recursion, tree lifecycle (add_child parents the child
back, container-style), Element::layout's ctx-carrying child pass, and
is_child_visible. Input gains two policies: hits_through_children (Layer/
Switcher hit via children only, no own-rect or self-coverage check) and
gates_presses=false (event-proxying containers must see every press —
Switcher unfocuses its child on an outside click, which a hit-gated
on_event would never learn about).
Switcher: the full delegating proxy ports with the model doing what's
per-widget — parent-clamped rects, active-child arrangement + layout,
event proxying through EventCtx::ui (bug-for-bug, including the double
mouse_input dispatch while a popover is open), paint-property proxies
(color / corner flags / solid_border from the active child), and the
17-method MenuController delegation on the 5k capability hooks.
ContentBg: actually a leaf (no children; its GraphController is a stub
except the grid setters) — the gradient-grid geometry moves to paint(),
the background stays host-drawn from color(), never hittable.
Verification: cce-system-settings (the only Switcher host; every page
lives under it) render-dump A/B against the stashed legacy build across
four pages — network and appearance byte-IDENTICAL; processes and storage
differ only in live system data (PIDs, CPU %, disk-usage bar width), zero
geometry or color diffs. 166 tests pass (router-level Switcher test:
add_child parenting + visibility sync, active-child arrangement,
aggregation/hit routing, controller delegation); all 19 client crates
build.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01LkfJazPs9bRchkcozmxXCX
src/widget/container/content_bg.rs | 134 ++++----
src/widget/container/switcher.rs | 633 ++++++++++++-------------------------
src/widget/model.rs | 429 ++++++++++++++++++++-----
3 files changed, 641 insertions(+), 555 deletions(-)
diff --git a/src/widget/container/content_bg.rs b/src/widget/container/content_bg.rs
index 6e26627..49949d4 100644
--- a/src/widget/container/content_bg.rs
+++ b/src/widget/container/content_bg.rs
@@ -1,9 +1,15 @@
+//! Narrow-trait `ContentBg` (Phase 5n) — a standalone gradient-grid page background. Despite
+//! the name it owns no children; its GraphController impl is a stub except the grid-geometry
+//! setters (hosts configure the grid through the controller interface). Never hittable; the
+//! background color itself is drawn by hosts reading `color()` (the geometry here is only the
+//! cell/gap gradient grid, exactly the legacy `extra_quads`).
+
use crate::colors;
-use crate::widget::*;
+use crate::scene::layout::Rect;
+use crate::scene::paint::PaintCtx;
+use crate::widget::{Adapted, GraphController, GraphNode, Input, Layout, Paint};
pub struct ContentBg {
- x: f32, y: f32, w: f32, h: f32,
- hovered: bool,
show_network_grid: bool,
grid_size_x: f32,
grid_size_y: f32,
@@ -14,20 +20,28 @@ pub struct ContentBg {
}
impl ContentBg {
- pub fn new() -> Self {
- Self { x: 0.0, y: 0.0, w: 0.0, h: 0.0, hovered: false, show_network_grid: false, grid_size_x: 150.0, grid_size_y: 75.0, grid_origin_x: 0.0, grid_origin_y: 0.0, skipped_row_h: 37.5, skipped_col_w: 37.5 }
+ pub fn new() -> Adapted<ContentBg> {
+ Adapted::new(ContentBg { show_network_grid: false, grid_size_x: 150.0, grid_size_y: 75.0, grid_origin_x: 0.0, grid_origin_y: 0.0, skipped_row_h: 37.5, skipped_col_w: 37.5 })
}
}
-impl Element for ContentBg {
- fn rect(&self) -> (f32, f32, f32, f32) { (self.x, self.y, self.w, self.h) }
- fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) { self.x = x; self.y = y; self.w = w; self.h = h; }
- fn as_ptr(&self) -> *mut (dyn Element + 'static) {
- self as *const Self as *mut Self as *mut (dyn Element + 'static)
+impl Layout for ContentBg {}
+
+impl Input for ContentBg {
+ /// Never hittable (legacy hit_test returned false unconditionally).
+ fn hit(&self, _rect: Rect, _x: f32, _y: f32) -> bool {
+ false
}
- fn as_ptr_mut(&mut self) -> *mut (dyn Element + 'static) {
- self as *mut Self as *mut (dyn Element + 'static)
+
+ fn graph_controller(&self) -> Option<&dyn GraphController> {
+ Some(self)
}
+ fn graph_controller_mut(&mut self) -> Option<&mut dyn GraphController> {
+ Some(self)
+ }
+}
+
+impl Paint for ContentBg {
fn color(&self) -> [f32; 4] {
if self.show_network_grid {
[0.0, 0.0, 0.0, 0.0]
@@ -35,14 +49,18 @@ impl Element for ContentBg {
colors::CONTENT_BG
}
}
- fn set_hovered(&mut self, v: bool) { self.hovered = v; }
- fn hovered(&self) -> bool { self.hovered }
- fn hit_test(&self, _px: f32, _py: f32, _ctx: &UiContext) -> bool { false }
- fn as_graph_controller(&self) -> Option<&dyn GraphController> { Some(self) }
- fn as_graph_controller_mut(&mut self) -> Option<&mut dyn GraphController> { Some(self) }
+ fn paint(&self, rect: Rect, ctx: &mut PaintCtx) {
+ for (qx, qy, qw, qh, qc) in self.grid_quads(rect) {
+ ctx.quad(Rect { x: qx, y: qy, width: qw, height: qh }, qc);
+ }
+ }
+}
- fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
+impl ContentBg {
+ /// The gradient grid geometry (legacy `extra_quads`), against `rect`.
+ fn grid_quads(&self, rect: Rect) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
+ let (x, y, w, h) = (rect.x, rect.y, rect.width, rect.height);
if !self.show_network_grid || self.grid_size_x <= 0.0 || self.grid_size_y <= 0.0 {
return vec![];
}
@@ -55,26 +73,26 @@ impl Element for ContentBg {
let step_x = self.grid_size_x + self.skipped_col_w;
if step_y >= 4.0 && step_x >= 4.0 {
- let ry_start = ((self.y - self.grid_origin_y) / step_y).floor() as i32 - 1;
- let ry_end = ((self.y + self.h - self.grid_origin_y) / step_y).ceil() as i32 + 1;
+ let ry_start = ((y - self.grid_origin_y) / step_y).floor() as i32 - 1;
+ let ry_end = ((y + h - self.grid_origin_y) / step_y).ceil() as i32 + 1;
let ry_start = ry_start.max(-100_000);
let ry_end = ry_end.min(100_000);
- let cx_start = ((self.x - self.grid_origin_x) / step_x).floor() as i32 - 1;
- let cx_end = ((self.x + self.w - self.grid_origin_x) / step_x).ceil() as i32 + 1;
+ let cx_start = ((x - self.grid_origin_x) / step_x).floor() as i32 - 1;
+ let cx_end = ((x + w - self.grid_origin_x) / step_x).ceil() as i32 + 1;
let cx_start = cx_start.max(-100_000);
let cx_end = cx_end.min(100_000);
// Draw individual cell backgrounds to avoid stacking with gradients
for ry in ry_start..=ry_end {
let y1 = self.grid_origin_y + (ry as f32) * step_y;
- let draw_start_y = y1.max(self.y);
- let draw_end_y = (y1 + self.grid_size_y).min(self.y + self.h);
+ let draw_start_y = y1.max(y);
+ let draw_end_y = (y1 + self.grid_size_y).min(y + h);
if draw_start_y < draw_end_y {
for cx in cx_start..=cx_end {
let x1 = self.grid_origin_x + (cx as f32) * step_x;
- let draw_start_x = x1.max(self.x);
- let draw_end_x = (x1 + self.grid_size_x).min(self.x + self.w);
+ let draw_start_x = x1.max(x);
+ let draw_end_x = (x1 + self.grid_size_x).min(x + w);
if draw_start_x < draw_end_x {
quads.push((draw_start_x, draw_start_y, draw_end_x - draw_start_x, draw_end_y - draw_start_y, colors::CONTENT_BG));
}
@@ -88,24 +106,24 @@ impl Element for ContentBg {
let step_y = self.grid_size_y + self.skipped_row_h;
let step_x = self.grid_size_x + self.skipped_col_w;
if step_y >= 4.0 && step_x >= 4.0 {
- let k_start = ((self.y - self.grid_origin_y) / step_y).floor() as i32 - 1;
- let k_end = ((self.y + self.h - self.grid_origin_y) / step_y).ceil() as i32 + 1;
+ let k_start = ((y - self.grid_origin_y) / step_y).floor() as i32 - 1;
+ let k_end = ((y + h - self.grid_origin_y) / step_y).ceil() as i32 + 1;
let k_start = k_start.max(-100_000);
let k_end = k_end.min(100_000);
- let cx_start = ((self.x - self.grid_origin_x) / step_x).floor() as i32 - 1;
- let cx_end = ((self.x + self.w - self.grid_origin_x) / step_x).ceil() as i32 + 1;
+ let cx_start = ((x - self.grid_origin_x) / step_x).floor() as i32 - 1;
+ let cx_end = ((x + w - self.grid_origin_x) / step_x).ceil() as i32 + 1;
let cx_start = cx_start.max(-100_000);
let cx_end = cx_end.min(100_000);
for k in k_start..=k_end {
let y1 = self.grid_origin_y + (k as f32) * step_y;
let y2 = y1 + self.grid_size_y;
- if y1 >= self.y + self.h {
+ if y1 >= y + h {
continue;
}
- let draw_start_y = y2.max(self.y);
- let draw_end_y = (y2 + self.skipped_row_h).min(self.y + self.h);
+ let draw_start_y = y2.max(y);
+ let draw_end_y = (y2 + self.skipped_row_h).min(y + h);
if draw_start_y >= draw_end_y {
continue;
}
@@ -119,8 +137,8 @@ impl Element for ContentBg {
for i in 0..steps {
let sx_start = x1 + i as f32 * sub_w;
let sx_end = sx_start + sub_w;
- let draw_start_x = sx_start.max(self.x);
- let draw_end_x = sx_end.min(self.x + self.w);
+ let draw_start_x = sx_start.max(x);
+ let draw_end_x = sx_end.min(x + w);
if draw_start_x < draw_end_x {
let sx_mid = (sx_start + sx_end) / 2.0;
let dist = (sx_mid - x_mid).abs();
@@ -143,24 +161,24 @@ impl Element for ContentBg {
let step_y = self.grid_size_y + self.skipped_row_h;
let step_x = self.grid_size_x + self.skipped_col_w;
if step_y >= 4.0 && step_x >= 4.0 {
- let k_start = ((self.x - self.grid_origin_x) / step_x).floor() as i32 - 1;
- let k_end = ((self.x + self.w - self.grid_origin_x) / step_x).ceil() as i32 + 1;
+ let k_start = ((x - self.grid_origin_x) / step_x).floor() as i32 - 1;
+ let k_end = ((x + w - self.grid_origin_x) / step_x).ceil() as i32 + 1;
let k_start = k_start.max(-100_000);
let k_end = k_end.min(100_000);
- let ry_start = ((self.y - self.grid_origin_y) / step_y).floor() as i32 - 1;
- let ry_end = ((self.y + self.h - self.grid_origin_y) / step_y).ceil() as i32 + 1;
+ let ry_start = ((y - self.grid_origin_y) / step_y).floor() as i32 - 1;
+ let ry_end = ((y + h - self.grid_origin_y) / step_y).ceil() as i32 + 1;
let ry_start = ry_start.max(-100_000);
let ry_end = ry_end.min(100_000);
for k in k_start..=k_end {
let x1 = self.grid_origin_x + (k as f32) * step_x;
let x2 = x1 + self.grid_size_x;
- if x1 >= self.x + self.w {
+ if x1 >= x + w {
continue;
}
- let draw_start_x = x2.max(self.x);
- let draw_end_x = (x2 + self.skipped_col_w).min(self.x + self.w);
+ let draw_start_x = x2.max(x);
+ let draw_end_x = (x2 + self.skipped_col_w).min(x + w);
if draw_start_x >= draw_end_x {
continue;
}
@@ -174,8 +192,8 @@ impl Element for ContentBg {
for i in 0..steps {
let sy_start = y1 + i as f32 * sub_h;
let sy_end = sy_start + sub_h;
- let draw_start_y = sy_start.max(self.y);
- let draw_end_y = sy_end.min(self.y + self.h);
+ let draw_start_y = sy_start.max(y);
+ let draw_end_y = sy_end.min(y + h);
if draw_start_y < draw_end_y {
let sy_mid = (sy_start + sy_end) / 2.0;
let dist = (sy_mid - y_mid).abs();
@@ -196,42 +214,42 @@ impl Element for ContentBg {
// Draw the grid borders
let step_y = self.grid_size_y + self.skipped_row_h;
if step_y >= 4.0 {
- let k_start = ((self.y - self.grid_origin_y) / step_y).floor() as i32 - 1;
- let k_end = ((self.y + self.h - self.grid_origin_y) / step_y).ceil() as i32 + 1;
+ let k_start = ((y - self.grid_origin_y) / step_y).floor() as i32 - 1;
+ let k_end = ((y + h - self.grid_origin_y) / step_y).ceil() as i32 + 1;
let k_start = k_start.max(-100_000);
let k_end = k_end.min(100_000);
for k in k_start..=k_end {
let y1 = self.grid_origin_y + (k as f32) * step_y;
let y2 = y1 + self.grid_size_y;
- if y1 >= self.y + self.h {
+ if y1 >= y + h {
continue;
}
- if y1 >= self.y {
- quads.push((self.x, y1, self.w, 1.0, grid_color));
+ if y1 >= y {
+ quads.push((x, y1, w, 1.0, grid_color));
}
- if y2 >= self.y && y2 < self.y + self.h {
- quads.push((self.x, y2, self.w, 1.0, grid_color));
+ if y2 >= y && y2 < y + h {
+ quads.push((x, y2, w, 1.0, grid_color));
}
}
}
let step_x = self.grid_size_x + self.skipped_col_w;
if step_x >= 4.0 {
- let k_start = ((self.x - self.grid_origin_x) / step_x).floor() as i32 - 1;
- let k_end = ((self.x + self.w - self.grid_origin_x) / step_x).ceil() as i32 + 1;
+ let k_start = ((x - self.grid_origin_x) / step_x).floor() as i32 - 1;
+ let k_end = ((x + w - self.grid_origin_x) / step_x).ceil() as i32 + 1;
let k_start = k_start.max(-100_000);
let k_end = k_end.min(100_000);
for k in k_start..=k_end {
let x1 = self.grid_origin_x + (k as f32) * step_x;
let x2 = x1 + self.grid_size_x;
- if x1 >= self.x + self.w {
+ if x1 >= x + w {
continue;
}
- if x1 >= self.x {
- quads.push((x1, self.y, 1.0, self.h, grid_color));
+ if x1 >= x {
+ quads.push((x1, y, 1.0, h, grid_color));
}
- if x2 >= self.x && x2 < self.x + self.w {
- quads.push((x2, self.y, 1.0, self.h, grid_color));
+ if x2 >= x && x2 < x + w {
+ quads.push((x2, y, 1.0, h, grid_color));
}
}
}
diff --git a/src/widget/container/switcher.rs b/src/widget/container/switcher.rs
index 6b6831c..9759fa6 100644
--- a/src/widget/container/switcher.rs
+++ b/src/widget/container/switcher.rs
@@ -1,24 +1,30 @@
-use crate::widget::*;
-use crate::widget::display::TextLabel;
+//! Narrow-trait `Switcher` (Phase 5n) — the first container across: it owns externally-managed
+//! child pointers (pages) and exposes exactly one of them at a time. The adapter's container
+//! concern does the subtree plumbing (geometry/text aggregation, tick/popover/text-item
+//! recursion, hit-through-children), filtered to the active child by
+//! [`Layout::child_visible`]; the model keeps the legacy specifics: parent-clamped rects,
+//! active-child arrangement, event proxying (via `EventCtx::ui`), paint-property proxies, and
+//! the [`MenuController`] delegation to the active child.
+
+use crate::scene::layout::Rect;
+use crate::scene::paint::PaintCtx;
+use crate::widget::{
+ Adapted, Element, ElementState, Event, EventCtx, Input, Layout, MenuController, Paint,
+ UiContext,
+};
#[derive(Debug, Clone)]
pub struct Switcher {
- pub base: Widget,
- pub parent: Option<*mut (dyn Element + 'static)>,
- pub children: Vec<*mut (dyn Element + 'static)>,
- pub active_index: Option<usize>,
- pub visible: bool,
+ children: Vec<*mut (dyn Element + 'static)>,
+ parent: Option<*mut (dyn Element + 'static)>,
+ active_index: Option<usize>,
}
impl Switcher {
- pub fn new(x: f32, y: f32, w: f32, h: f32) -> Self {
- Self {
- base: Widget::new_rect(x, y, w, h),
- parent: None,
- children: Vec::new(),
- active_index: None,
- visible: true,
- }
+ pub fn new(x: f32, y: f32, w: f32, h: f32) -> Adapted<Switcher> {
+ let mut s = Adapted::new(Switcher { children: Vec::new(), parent: None, active_index: None });
+ Element::set_rect(&mut s, x, y, w, h);
+ s
}
pub fn set_active_index(&mut self, index: Option<usize>) {
@@ -33,526 +39,305 @@ impl Switcher {
pub fn active_index(&self) -> Option<usize> {
self.active_index
}
-}
-
-impl Element for Switcher {
- crate::impl_widget_base!(Switcher);
- fn blocks_backplate_drag(&self) -> bool { false }
-
- fn visible(&self) -> bool {
- self.visible
- }
- fn set_visible(&mut self, visible: bool) {
- self.visible = visible;
- }
-
- fn color(&self) -> [f32; 4] {
- if let Some(idx) = self.active_index {
- if idx < self.children.len() {
- return unsafe { (*self.children[idx]).color() };
- }
- }
- [0.0, 0.0, 0.0, 0.0]
- }
-
- fn parent(&self, _ctx: &UiContext) -> Option<*mut (dyn Element + 'static)> {
- self.parent
+ fn active_child(&self) -> Option<*mut (dyn Element + 'static)> {
+ self.children.get(self.active_index?).copied()
}
+}
- fn set_parent(&mut self, parent: Option<*mut (dyn Element + 'static)>, _ctx: &mut UiContext) {
- self.parent = parent;
+impl Layout for Switcher {
+ fn has_container_children(&self) -> bool {
+ true
}
- fn children(&self, _ctx: &UiContext) -> Vec<*mut (dyn Element + 'static)> {
+ fn container_children(&self) -> Vec<*mut (dyn Element + 'static)> {
self.children.clone()
}
- fn is_child_visible(&self, child_id: WidgetId) -> bool {
- if let Some(idx) = self.active_index {
- if idx < self.children.len() {
- if let Some(b) = unsafe { (*self.children[idx]).base() } {
- return b.id() == child_id;
- }
- }
- }
- false
- }
-
- fn add_child(&mut self, child: *mut (dyn Element + 'static), ctx: &mut UiContext) {
+ fn child_added(&mut self, child: *mut (dyn Element + 'static)) {
self.children.push(child);
- let id = self.base.id();
- let self_ptr = self.as_ptr();
- if let Some(c_base) = unsafe { (*child).base() } {
- let c_id = c_base.id();
- ctx.register_widget(id, self_ptr);
- ctx.register_widget(c_id, child);
- ctx.link_ids(id, c_id);
- }
- unsafe {
- (*child).set_parent(Some(self_ptr), ctx);
- }
- // Sync visibility of newly added child
+ // Sync visibility of the newly added child with the active selection.
let idx = self.children.len() - 1;
unsafe {
(*child).set_visible(self.active_index == Some(idx));
}
}
- fn clear_children(&mut self, ctx: &mut UiContext) {
+ fn children_cleared(&mut self) {
self.children.clear();
- let id = self.base.id();
- ctx.clear_children_ids(id);
}
- fn rect(&self) -> (f32, f32, f32, f32) {
- (self.base.x, self.base.y, self.base.w, self.base.h)
+ fn parent_changed(&mut self, parent: Option<*mut (dyn Element + 'static)>) {
+ self.parent = parent;
}
- fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
- let (clamped_x, clamped_y, clamped_w, clamped_h) = if let Some(parent_ptr) = self.parent {
- let (px, py, pw, ph) = unsafe { (*parent_ptr).rect() };
- let cx = x.clamp(px, px + pw.max(0.0));
- let cy = y.clamp(py, py + ph.max(0.0));
- let cw = w.min((px + pw.max(0.0) - cx).max(0.0));
- let ch = h.min((py + ph.max(0.0) - cy).max(0.0));
- (cx, cy, cw, ch)
- } else {
- (x, y, w, h)
- };
-
- self.base.x = clamped_x;
- self.base.y = clamped_y;
- self.base.w = clamped_w;
- self.base.h = clamped_h;
-
- if !self.visible {
- return;
- }
-
- if let Some(idx) = self.active_index {
- if idx < self.children.len() {
- unsafe {
- (*self.children[idx]).set_rect(clamped_x, clamped_y, clamped_w, clamped_h);
- }
- }
- }
+ fn child_visible(&self, child: *mut (dyn Element + 'static)) -> bool {
+ self.active_child().map_or(false, |active| std::ptr::eq(active, child))
}
- fn layout(&mut self, origin: Point, constraints: LayoutConstraints, ctx: &mut UiContext) {
- let size = self.measure(constraints, ctx);
- self.set_rect(origin.x, origin.y, size.width, size.height);
-
- if self.visible {
- if let Some(idx) = self.active_index {
- if idx < self.children.len() {
- unsafe {
- (*self.children[idx]).layout(
- Point { x: self.base.x, y: self.base.y },
- LayoutConstraints::new(self.base.w, self.base.w, self.base.h, self.base.h),
- ctx,
- );
- }
- }
- }
- }
+ /// Legacy `set_rect` clamped the switcher into its parent's rect.
+ fn adjust_rect(&self, requested: Rect) -> Rect {
+ let Some(parent_ptr) = self.parent else {
+ return requested;
+ };
+ let (px, py, pw, ph) = unsafe { (*parent_ptr).rect() };
+ let cx = requested.x.clamp(px, px + pw.max(0.0));
+ let cy = requested.y.clamp(py, py + ph.max(0.0));
+ let cw = requested.width.min((px + pw.max(0.0) - cx).max(0.0));
+ let ch = requested.height.min((py + ph.max(0.0) - cy).max(0.0));
+ Rect { x: cx, y: cy, width: cw, height: ch }
}
- fn hit_test(&self, px: f32, py: f32, ctx: &UiContext) -> bool {
- if !self.visible {
- return false;
- }
- if let Some(idx) = self.active_index {
- if idx < self.children.len() {
- return unsafe { (*self.children[idx]).hit_test(px, py, ctx) };
+ /// The active child fills the switcher's rect.
+ fn arrange_children(&mut self, rect: Rect) {
+ if let Some(child) = self.active_child() {
+ unsafe {
+ (*child).set_rect(rect.x, rect.y, rect.width, rect.height);
}
}
- false
}
- fn all_quads(&self, ctx: &UiContext) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
- if !self.visible {
- return Vec::new();
- }
- let mut quads = Vec::new();
- if let Some(idx) = self.active_index {
- if idx < self.children.len() {
- let widget = unsafe { &*self.children[idx] };
- let (wx, wy, ww, wh) = widget.rect();
- let has_rounded = widget.rounded_corners() != (false, false, false, false);
- for (qx, qy, qw, qh, qc) in widget.all_quads(ctx) {
- if has_rounded && (qx - wx).abs() < 0.1 && (qy - wy).abs() < 0.1 && (qw - ww).abs() < 0.1 && (qh - wh).abs() < 0.1 {
- continue;
- }
- quads.push((qx, qy, qw, qh, qc));
- }
+ fn layout_children_ctx(&mut self, rect: Rect, ctx: &mut UiContext) {
+ if let Some(child) = self.active_child() {
+ unsafe {
+ (*child).layout(
+ crate::widget::Point { x: rect.x, y: rect.y },
+ crate::widget::LayoutConstraints::new(rect.width, rect.width, rect.height, rect.height),
+ ctx,
+ );
}
}
- quads
}
+}
- fn text_labels(&self) -> Vec<TextLabel> {
- if !self.visible {
- return Vec::new();
- }
- if let Some(idx) = self.active_index {
- if idx < self.children.len() {
- return unsafe { (*self.children[idx]).text_labels() };
- }
+impl Paint for Switcher {
+ /// The switcher shows as whatever its active child shows as (legacy proxied `color`,
+ /// `rounded_corners`, and `solid_border` — style-property painters read these).
+ fn color(&self) -> [f32; 4] {
+ match self.active_child() {
+ Some(child) => unsafe { (*child).color() },
+ None => [0.0, 0.0, 0.0, 0.0],
}
- Vec::new()
}
- fn text_labels_with_bounds(&self, ctx: &UiContext) -> Vec<(TextLabel, Option<[f32; 4]>)> {
- if !self.visible {
- return Vec::new();
- }
- if let Some(idx) = self.active_index {
- if idx < self.children.len() {
- return unsafe { (*self.children[idx]).text_labels_with_bounds(ctx) };
- }
- }
- Vec::new()
+ fn corner_style(&self) -> Option<(f32, (bool, bool, bool, bool))> {
+ // Legacy kept the Element-default 12.0 radius and proxied the corner flags.
+ let corners = match self.active_child() {
+ Some(child) => unsafe { (*child).rounded_corners() },
+ None => (false, false, false, false),
+ };
+ Some((12.0, corners))
}
- fn text_labels_with_font_and_bounds(&self, ctx: &UiContext) -> Vec<(TextLabel, Option<String>, Option<[f32; 4]>)> {
- if !self.visible {
- return Vec::new();
- }
- if let Some(idx) = self.active_index {
- if idx < self.children.len() {
- return unsafe { (*self.children[idx]).text_labels_with_font_and_bounds(ctx) };
- }
- }
- Vec::new()
+ fn solid_border(&self) -> Option<([f32; 4], f32)> {
+ self.active_child().and_then(|child| unsafe { (*child).solid_border() })
}
- fn get_text_items(&self) -> Vec<(&glyphon::Buffer, f32, f32, glyphon::Color)> {
- if !self.visible {
- return Vec::new();
- }
- if let Some(idx) = self.active_index {
- if idx < self.children.len() {
- return unsafe { (*self.children[idx]).get_text_items() };
- }
- }
- Vec::new()
+ /// No own geometry: the background is the active child's own business, and the adapter's
+ /// container aggregation carries the subtree on the legacy getters (the scene walk
+ /// recurses `children()` itself).
+ fn paint(&self, _rect: Rect, _ctx: &mut PaintCtx) {}
+}
+
+impl Input for Switcher {
+ fn blocks_backplate_drag(&self) -> bool {
+ false
}
- fn prepare_text(&mut self, fs: &mut glyphon::FontSystem) {
- if !self.visible {
- return;
- }
- if let Some(idx) = self.active_index {
- if idx < self.children.len() {
- unsafe {
- (*self.children[idx]).prepare_text(fs);
- }
- }
- }
+ fn hits_through_children(&self) -> bool {
+ true
}
- fn on_cursor_moved(&mut self, px: f32, py: f32, ctx: &mut UiContext) -> bool {
- if !self.visible {
- return false;
- }
- if let Some(idx) = self.active_index {
- if idx < self.children.len() {
- let widget = unsafe { &mut *self.children[idx] };
- if widget.is_dragging() {
- return widget.drag_update(px, py);
- } else {
- return widget.cursor_moved(px, py, ctx);
- }
- }
- }
+ /// Legacy `mouse_input` saw every press to unfocus the child on an outside click.
+ fn gates_presses(&self) -> bool {
false
}
- fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32, ctx: &mut UiContext) -> bool {
- if !self.visible {
+ fn on_event(&mut self, event: &Event, ectx: &mut EventCtx) -> bool {
+ let Some(child_ptr) = self.active_child() else {
return false;
- }
- if let Some(idx) = self.active_index {
- if idx < self.children.len() {
- let widget = unsafe { &mut *self.children[idx] };
+ };
+ let widget = unsafe { &mut *child_ptr };
+ match event {
+ Event::MouseButton { button, state, x: px, y: py, .. } => {
+ let Some(ui) = ectx.ui.as_deref_mut() else {
+ return false;
+ };
+ // Faithful port of the legacy body, including the double dispatch while a
+ // popover is open.
if widget.popover_rect().is_some() {
- if widget.mouse_input(button, state, px, py, ctx) {
+ if widget.mouse_input(*button, *state, *px, *py, ui) {
return true;
}
}
- if widget.mouse_input(button, state, px, py, ctx) {
+ if widget.mouse_input(*button, *state, *px, *py, ui) {
return true;
}
- if state == ElementState::Pressed && !widget.hit_test(px, py, ctx) {
+ if *state == ElementState::Pressed && !widget.hit_test(*px, *py, ui) {
widget.unfocus();
}
+ false
}
- }
- false
- }
-
- fn keyboard_input(&mut self, event: &KeyEvent, ctx: &mut UiContext) -> bool {
- if !self.visible {
- return false;
- }
- if let Some(idx) = self.active_index {
- if idx < self.children.len() {
- return unsafe { (*self.children[idx]).keyboard_input(event, ctx) };
- }
- }
- false
- }
-
- fn mouse_wheel(&mut self, delta: &MouseScrollDelta, px: f32, py: f32, ctx: &mut UiContext) -> bool {
- if !self.visible {
- return false;
- }
- if let Some(idx) = self.active_index {
- if idx < self.children.len() {
- return unsafe { (*self.children[idx]).mouse_wheel(delta, px, py, ctx) };
- }
- }
- false
- }
-
- fn popover_rect(&self) -> Option<(f32, f32, f32, f32)> {
- if !self.visible {
- return None;
- }
- if let Some(idx) = self.active_index {
- if idx < self.children.len() {
- return unsafe { (*self.children[idx]).popover_rect() };
- }
- }
- None
- }
-
- fn render_popover(&self, pc: &mut dyn crate::layout::RenderTarget) {
- if !self.visible {
- return;
- }
- if let Some(idx) = self.active_index {
- if idx < self.children.len() {
- unsafe {
- (*self.children[idx]).render_popover(pc);
+ Event::PointerMove { x: px, y: py, .. } => {
+ if widget.is_dragging() {
+ return widget.drag_update(*px, *py);
}
+ let Some(ui) = ectx.ui.as_deref_mut() else {
+ return false;
+ };
+ widget.cursor_moved(*px, *py, ui)
}
- }
- }
-
- fn tick(&mut self, dt: f32, ctx: &mut UiContext) -> bool {
- if !self.visible {
- return false;
- }
- if let Some(idx) = self.active_index {
- if idx < self.children.len() {
- return unsafe { (*self.children[idx]).tick(dt, ctx) };
- }
- }
- false
- }
-
- fn rounded_corners(&self) -> (bool, bool, bool, bool) {
- if let Some(idx) = self.active_index {
- if idx < self.children.len() {
- return unsafe { (*self.children[idx]).rounded_corners() };
+ Event::MouseWheel { delta, x: px, y: py, .. } => {
+ let Some(ui) = ectx.ui.as_deref_mut() else {
+ return false;
+ };
+ widget.mouse_wheel(delta, *px, *py, ui)
}
- }
- (false, false, false, false)
- }
-
- fn solid_border(&self) -> Option<([f32; 4], f32)> {
- if let Some(idx) = self.active_index {
- if idx < self.children.len() {
- return unsafe { (*self.children[idx]).solid_border() };
+ Event::KeyInput(key_event) => {
+ let Some(ui) = ectx.ui.as_deref_mut() else {
+ return false;
+ };
+ widget.keyboard_input(key_event, ui)
}
+ _ => false,
}
- None
}
- fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
- if !self.visible {
- return Vec::new();
- }
- if let Some(idx) = self.active_index {
- if idx < self.children.len() {
- return unsafe { (*self.children[idx]).extra_quads() };
- }
- }
- Vec::new()
+ fn menu_controller(&self) -> Option<&dyn MenuController> {
+ Some(self)
}
-
- fn extra_arcs(&self) -> Vec<(f32, f32, f32, f32, f32, f32, [f32; 4])> {
- if !self.visible {
- return Vec::new();
- }
- if let Some(idx) = self.active_index {
- if idx < self.children.len() {
- return unsafe { (*self.children[idx]).extra_arcs() };
- }
- }
- Vec::new()
+ fn menu_controller_mut(&mut self) -> Option<&mut dyn MenuController> {
+ Some(self)
}
-
- fn extra_circles(&self) -> Vec<(f32, f32, f32, [f32; 4])> {
- if !self.visible {
- return Vec::new();
- }
- if let Some(idx) = self.active_index {
- if idx < self.children.len() {
- return unsafe { (*self.children[idx]).extra_circles() };
- }
- }
- Vec::new()
- }
-
- fn as_menu_controller(&self) -> Option<&dyn MenuController> { Some(self) }
- fn as_menu_controller_mut(&mut self) -> Option<&mut dyn MenuController> { Some(self) }
}
impl MenuController for Switcher {
fn menu_click(&mut self) -> Option<(usize, usize)> {
- let idx = self.active_index?;
- let child_ptr = *self.children.get(idx)?;
- unsafe { &mut *child_ptr }.as_menu_controller_mut()?.menu_click()
+ unsafe { &mut *self.active_child()? }.as_menu_controller_mut()?.menu_click()
}
fn trigger_menu_click(&mut self, menu_idx: usize, item_idx: usize) {
- if let Some(idx) = self.active_index {
- if let Some(child) = self.children.get(idx) {
- if let Some(mc) = unsafe { &mut **child }.as_menu_controller_mut() {
- mc.trigger_menu_click(menu_idx, item_idx);
- }
+ if let Some(child) = self.active_child() {
+ if let Some(mc) = unsafe { &mut *child }.as_menu_controller_mut() {
+ mc.trigger_menu_click(menu_idx, item_idx);
}
}
}
fn set_item_checked(&mut self, menu_idx: usize, item_idx: usize, checked: bool) {
- if let Some(idx) = self.active_index {
- if let Some(child) = self.children.get(idx) {
- if let Some(mc) = unsafe { &mut **child }.as_menu_controller_mut() {
- mc.set_item_checked(menu_idx, item_idx, checked);
- }
+ if let Some(child) = self.active_child() {
+ if let Some(mc) = unsafe { &mut *child }.as_menu_controller_mut() {
+ mc.set_item_checked(menu_idx, item_idx, checked);
}
}
}
fn set_menu_items(&mut self, menu_idx: usize, items: &[String]) {
- if let Some(idx) = self.active_index {
- if let Some(child) = self.children.get(idx) {
- if let Some(mc) = unsafe { &mut **child }.as_menu_controller_mut() {
- mc.set_menu_items(menu_idx, items);
- }
+ if let Some(child) = self.active_child() {
+ if let Some(mc) = unsafe { &mut *child }.as_menu_controller_mut() {
+ mc.set_menu_items(menu_idx, items);
}
}
}
fn is_menu_bar(&self) -> bool {
- if let Some(idx) = self.active_index {
- if let Some(child) = self.children.get(idx) {
- if let Some(mc) = unsafe { &**child }.as_menu_controller() {
- return mc.is_menu_bar();
- }
- }
- }
- false
+ self.active_child()
+ .and_then(|c| unsafe { &*c }.as_menu_controller().map(|mc| mc.is_menu_bar()))
+ .unwrap_or(false)
}
fn is_menu_open(&self) -> bool {
- if let Some(idx) = self.active_index {
- if let Some(child) = self.children.get(idx) {
- if let Some(mc) = unsafe { &**child }.as_menu_controller() {
- return mc.is_menu_open();
- }
- }
- }
- false
+ self.active_child()
+ .and_then(|c| unsafe { &*c }.as_menu_controller().map(|mc| mc.is_menu_open()))
+ .unwrap_or(false)
}
fn menu_items(&self) -> Vec<String> {
- if let Some(idx) = self.active_index {
- if let Some(child) = self.children.get(idx) {
- if let Some(mc) = unsafe { &**child }.as_menu_controller() {
- return mc.menu_items();
- }
- }
- }
- Vec::new()
+ self.active_child()
+ .and_then(|c| unsafe { &*c }.as_menu_controller().map(|mc| mc.menu_items()))
+ .unwrap_or_default()
}
fn menu_item_checked(&self) -> Vec<Option<bool>> {
- if let Some(idx) = self.active_index {
- if let Some(child) = self.children.get(idx) {
- if let Some(mc) = unsafe { &**child }.as_menu_controller() {
- return mc.menu_item_checked();
- }
- }
- }
- Vec::new()
+ self.active_child()
+ .and_then(|c| unsafe { &*c }.as_menu_controller().map(|mc| mc.menu_item_checked()))
+ .unwrap_or_default()
}
fn is_vertical(&self) -> bool {
- if let Some(idx) = self.active_index {
- if let Some(child) = self.children.get(idx) {
- if let Some(mc) = unsafe { &**child }.as_menu_controller() {
- return mc.is_vertical();
- }
- }
- }
- false
+ self.active_child()
+ .and_then(|c| unsafe { &*c }.as_menu_controller().map(|mc| mc.is_vertical()))
+ .unwrap_or(false)
}
fn menu_names(&self) -> Vec<String> {
- if let Some(idx) = self.active_index {
- if let Some(child) = self.children.get(idx) {
- if let Some(mc) = unsafe { &**child }.as_menu_controller() {
- return mc.menu_names();
- }
- }
- }
- Vec::new()
+ self.active_child()
+ .and_then(|c| unsafe { &*c }.as_menu_controller().map(|mc| mc.menu_names()))
+ .unwrap_or_default()
}
fn menu_items_list(&self) -> Vec<Vec<String>> {
- if let Some(idx) = self.active_index {
- if let Some(child) = self.children.get(idx) {
- if let Some(mc) = unsafe { &**child }.as_menu_controller() {
- return mc.menu_items_list();
- }
- }
- }
- Vec::new()
+ self.active_child()
+ .and_then(|c| unsafe { &*c }.as_menu_controller().map(|mc| mc.menu_items_list()))
+ .unwrap_or_default()
}
fn menu_checked_list(&self) -> Vec<Vec<Option<bool>>> {
- if let Some(idx) = self.active_index {
- if let Some(child) = self.children.get(idx) {
- if let Some(mc) = unsafe { &**child }.as_menu_controller() {
- return mc.menu_checked_list();
- }
- }
- }
- Vec::new()
+ self.active_child()
+ .and_then(|c| unsafe { &*c }.as_menu_controller().map(|mc| mc.menu_checked_list()))
+ .unwrap_or_default()
}
fn take_context_change(&mut self) -> Option<usize> {
- let idx = self.active_index?;
- let child_ptr = *self.children.get(idx)?;
- unsafe { &mut *child_ptr }.as_menu_controller_mut()?.take_context_change()
+ unsafe { &mut *self.active_child()? }.as_menu_controller_mut()?.take_context_change()
}
fn set_context_selected(&mut self, selected: usize) {
- if let Some(idx) = self.active_index {
- if let Some(child) = self.children.get(idx) {
- if let Some(mc) = unsafe { &mut **child }.as_menu_controller_mut() {
- mc.set_context_selected(selected);
- }
+ if let Some(child) = self.active_child() {
+ if let Some(mc) = unsafe { &mut *child }.as_menu_controller_mut() {
+ mc.set_context_selected(selected);
}
}
}
fn set_center_items(&mut self, center: bool) {
- if let Some(idx) = self.active_index {
- if let Some(child) = self.children.get(idx) {
- if let Some(mc) = unsafe { &mut **child }.as_menu_controller_mut() {
- mc.set_center_items(center);
- }
+ if let Some(child) = self.active_child() {
+ if let Some(mc) = unsafe { &mut *child }.as_menu_controller_mut() {
+ mc.set_center_items(center);
}
}
}
fn get_menu_items_at(&self, px: f32, py: f32) -> Option<(usize, String, Vec<String>, f32, f32, f32, f32)> {
- let idx = self.active_index?;
- let child_ptr = *self.children.get(idx)?;
- unsafe { &*child_ptr }.as_menu_controller()?.get_menu_items_at(px, py)
+ unsafe { &*self.active_child()? }.as_menu_controller()?.get_menu_items_at(px, py)
}
}
unsafe impl Send for Switcher {}
unsafe impl Sync for Switcher {}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::context::UiContext;
+ use crate::widget::{Checkbox, Label};
+
+ #[test]
+ fn switcher_exposes_only_the_active_child() {
+ let mut ctx = UiContext::new();
+ let mut a = Label::new("page a");
+ let mut b = Checkbox::new();
+ let mut sw = Switcher::new(0.0, 0.0, 200.0, 100.0);
+ let (sw_id, sw_ptr) = (sw.id(), sw.as_ptr_mut());
+ ctx.register_widget(sw_id, sw_ptr);
+ Element::add_child(&mut sw, a.as_ptr_mut(), &mut ctx);
+ Element::add_child(&mut sw, b.as_ptr_mut(), &mut ctx);
+
+ // add_child parented both children back to the switcher and left them hidden (no
+ // active selection yet).
+ assert!(!Element::visible(&a) && !Element::visible(&b));
+ assert!(Element::children(&sw, &ctx).len() == 2);
+
+ // Activating a child shows it, arranges it into the switcher's rect, and routes the
+ // subtree getters through it alone.
+ sw.set_active_index(Some(1));
+ assert!(!Element::visible(&a) && Element::visible(&b));
+ Element::set_rect(&mut sw, 10.0, 20.0, 300.0, 150.0);
+ assert_eq!(Element::rect(&b), (10.0, 20.0, 300.0, 150.0), "active child fills the rect");
+ assert_ne!(Element::rect(&a), (10.0, 20.0, 300.0, 150.0), "inactive child untouched");
+
+ // Aggregation and hit-testing go through the active child only.
+ assert!(Element::is_child_visible(&sw, b.id()));
+ assert!(!Element::is_child_visible(&sw, a.id()));
+ assert!(Element::hit_test(&sw, 15.0, 25.0, &ctx), "hit lands on the active child");
+
+ // The menu-controller delegation returns None-ish defaults for non-menu children.
+ let elem: &dyn Element = &sw;
+ assert!(elem.as_menu_controller().unwrap().menu_items().is_empty());
+ }
+}
diff --git a/src/widget/model.rs b/src/widget/model.rs
index d1a4395..0bbb22a 100644
--- a/src/widget/model.rs
+++ b/src/widget/model.rs
@@ -83,6 +83,57 @@ pub trait Layout {
fn detached_label_inset(&self) -> f32 {
0.0
}
+
+ // --- Container concern (transitional). Legacy containers own `Vec<*mut dyn Element>`
+ // children (child-arranging `set_rect` has no ctx to reach the tree) and every one
+ // hand-copies the same subtree plumbing: geometry/text aggregation, tick/popover/text-item
+ // recursion, hit-through-children. A migrated container keeps the pointer Vec in its model
+ // (exposed through these hooks) and the ADAPTER does the shared plumbing once, filtered by
+ // `child_visible`. What stays per-widget: child arrangement (`arrange_children` /
+ // `layout_children_ctx`) and any event proxying (in `on_event`, via `EventCtx::ui`).
+ // Dies with `Element`: the arena owns the tree and the scene walk owns recursion.
+
+ /// Whether this widget is a container serving
+ /// [`container_children`](Layout::container_children). Cheap gate, checked per getter.
+ fn has_container_children(&self) -> bool {
+ false
+ }
+
+ /// The container's child pointers, in stacking order.
+ fn container_children(&self) -> Vec<*mut (dyn Element + 'static)> {
+ Vec::new()
+ }
+
+ /// A child was attached through `Element::add_child` (the adapter has already tree-linked
+ /// it and set its parent).
+ fn child_added(&mut self, _child: *mut (dyn Element + 'static)) {}
+
+ /// All children were detached through `Element::clear_children`.
+ fn children_cleared(&mut self) {}
+
+ /// The parent pointer changed through `Element::set_parent` (containers that clamp their
+ /// rect to the parent's keep a copy — the tree default needs a ctx that `set_rect` lacks).
+ fn parent_changed(&mut self, _parent: Option<*mut (dyn Element + 'static)>) {}
+
+ /// Adjust a rect assignment before it lands on the base (Switcher clamps to its parent).
+ /// Default: identity.
+ fn adjust_rect(&self, requested: Rect) -> Rect {
+ requested
+ }
+
+ /// Position children after a `set_rect` (no ctx available — use the owned pointers).
+ /// Called only while the widget is visible, matching the legacy overrides.
+ fn arrange_children(&mut self, _rect: Rect) {}
+
+ /// Recursive child layout for the `Element::layout` pass (this one has ctx). Called after
+ /// the adapter has measured and placed the container itself, only while visible.
+ fn layout_children_ctx(&mut self, _rect: Rect, _ctx: &mut UiContext) {}
+
+ /// Per-child visibility policy for the adapter's subtree plumbing (Switcher exposes only
+ /// the active child). Default: every child.
+ fn child_visible(&self, _child: *mut (dyn Element + 'static)) -> bool {
+ true
+ }
}
/// The paint concern — a widget's fill color, its own (non-recursive) geometry emission, and
@@ -240,6 +291,20 @@ pub trait Input {
false
}
+ /// Container hit policy: hit whenever any [`Layout::child_visible`] child hits (Layer,
+ /// Switcher). The container's own rect is not consulted. Default: own-rect hit.
+ fn hits_through_children(&self) -> bool {
+ false
+ }
+
+ /// Whether the adapter hit-gates `MouseButton` presses before `on_event` (the leaf
+ /// centralization). Event-proxying containers return `false`: legacy container
+ /// `mouse_input` overrides saw every press — Switcher unfocuses its active child when a
+ /// press lands outside it, which a gated `on_event` would never learn about.
+ fn gates_presses(&self) -> bool {
+ true
+ }
+
// --- The legacy polling/value-binding surface (`take_click`, `take_change`,
// `get_value_string`/`set_value_string`, `value`) apps read widget state through. Kept on
// `Input` to avoid a fourth trait bound; replaced by typed messages when RFC §3.5's EventCtx
@@ -469,6 +534,92 @@ impl<W: Layout + Paint + Input + 'static> Adapted<W> {
pc.finish().items.into_iter().map(|item| item.prim).collect()
}
+ /// The container's children that pass the [`Layout::child_visible`] policy — the set the
+ /// adapter's subtree plumbing (aggregation, recursion, hit-through) operates on. Empty for
+ /// non-containers.
+ fn visible_children(&self) -> Vec<*mut (dyn Element + 'static)> {
+ if !Layout::has_container_children(&self.inner) {
+ return Vec::new();
+ }
+ Layout::container_children(&self.inner)
+ .into_iter()
+ .filter(|c| Layout::child_visible(&self.inner, *c))
+ .collect()
+ }
+
+ /// This widget's OWN text (prim-derived + detached base label), before any child
+ /// aggregation — the shared source for the three text getters.
+ fn own_text_labels(&self) -> Vec<TextLabel> {
+ if !self.visible() {
+ return Vec::new();
+ }
+ let mut out: Vec<TextLabel> = self
+ .painted_prims()
+ .into_iter()
+ .filter_map(|prim| match prim {
+ Prim::Text { text, x, y, font_size, color } => {
+ Some(TextLabel { text, x, y, font_size, color })
+ }
+ _ => None,
+ })
+ .collect();
+ if !Layout::inline_label(&self.inner) {
+ out.extend(self.base_label_fallback());
+ }
+ out
+ }
+
+ /// Own text with font + bounds: [`Paint::text_bounds`] when the widget provides it, else a
+ /// replica of the `Element` default's scroll-ancestor viewport clipping.
+ fn own_labels_with_font_and_bounds(&self, ctx: &UiContext) -> Vec<(TextLabel, Option<String>, Option<[f32; 4]>)> {
+ let font = Paint::widget_font(&self.inner);
+ if let Some(bounds) = Paint::text_bounds(&self.inner, self.content_rect()) {
+ return self
+ .own_text_labels()
+ .into_iter()
+ .map(|l| (l, font.clone(), Some(bounds)))
+ .collect();
+ }
+
+ let mut labels = self
+ .own_text_labels()
+ .into_iter()
+ .map(|l| (l, font.clone(), None::<[f32; 4]>))
+ .collect::<Vec<_>>();
+ let mut curr = Element::parent(self, ctx);
+ let mut scroll_box_bounds = None;
+ while let Some(parent_ptr) = curr {
+ let parent = unsafe { &*parent_ptr };
+ if let Some(scroll_box) = parent.as_any().downcast_ref::<crate::widget::ScrollBox>() {
+ let (sb_x, _, sb_w, _) = parent.rect();
+ let view_min = scroll_box.viewport_y + 4.0;
+ let view_max = scroll_box.viewport_y + scroll_box.viewport_h - 4.0;
+ scroll_box_bounds = Some([sb_x, view_min, sb_x + sb_w, view_max]);
+ break;
+ } else if let Some(list) = parent.as_any().downcast_ref::<crate::widget::List>() {
+ let (sb_x, _, sb_w, _) = parent.rect();
+ let view_min = list.scroll_box.viewport_y + 4.0;
+ let view_max = list.scroll_box.viewport_y + list.scroll_box.viewport_h - 4.0;
+ scroll_box_bounds = Some([sb_x, view_min, sb_x + sb_w, view_max]);
+ break;
+ }
+ curr = parent.parent(ctx);
+ }
+ if let Some(sb_bounds) = scroll_box_bounds {
+ for item in &mut labels {
+ if let Some(ref mut b) = item.2 {
+ b[0] = b[0].max(sb_bounds[0]);
+ b[1] = b[1].max(sb_bounds[1]);
+ b[2] = b[2].min(sb_bounds[2]);
+ b[3] = b[3].min(sb_bounds[3]);
+ } else {
+ item.2 = Some(sb_bounds);
+ }
+ }
+ }
+ labels
+ }
+
/// The base-label text of a *detached*-label widget — a replica of the legacy default
/// `Element::text_labels` body (which an overriding impl can no longer call).
fn base_label_fallback(&self) -> Vec<TextLabel> {
@@ -536,6 +687,119 @@ impl<W: Layout + Paint + Input + 'static> Element for Adapted<W> {
self.visible
}
+ // --- Container concern: tree lifecycle, child layout, and subtree recursion. The tree
+ // itself stays in `ctx.tree` (the Element defaults' store); a container model additionally
+ // keeps its own pointer Vec via the `Layout` hooks, because `set_rect`-time arrangement
+ // has no ctx to reach the tree.
+
+ fn children(&self, ctx: &UiContext) -> Vec<*mut (dyn Element + 'static)> {
+ if Layout::has_container_children(&self.inner) {
+ return Layout::container_children(&self.inner);
+ }
+ ctx.tree.children_ptrs(self.base.id())
+ }
+
+ fn add_child(&mut self, child: *mut (dyn Element + 'static), ctx: &mut UiContext) {
+ // The Element default's tree link…
+ if let Some(c_base) = unsafe { (*child).base() } {
+ let c_id = c_base.id();
+ let p_id = self.base.id();
+ let self_ptr = self.as_ptr();
+ ctx.register_widget(p_id, self_ptr);
+ ctx.register_widget(c_id, child);
+ ctx.tree.link(p_id, c_id);
+ }
+ // …plus, for containers, the legacy container extras: parent the child back (Layer,
+ // Switcher) and record it in the model's own Vec.
+ if Layout::has_container_children(&self.inner) {
+ let self_ptr = self.as_ptr_mut();
+ unsafe { (*child).set_parent(Some(self_ptr), ctx) };
+ Layout::child_added(&mut self.inner, child);
+ }
+ }
+
+ fn clear_children(&mut self, ctx: &mut UiContext) {
+ ctx.clear_children_ids(self.base.id());
+ Layout::children_cleared(&mut self.inner);
+ }
+
+ fn set_parent(&mut self, parent: Option<*mut (dyn Element + 'static)>, ctx: &mut UiContext) {
+ Layout::parent_changed(&mut self.inner, parent);
+ // Replica of the Element default: symmetric tree link.
+ let id = self.base.id();
+ if let Some(p_ptr) = parent {
+ if let Some(p_base) = unsafe { (*p_ptr).base() } {
+ let p_id = p_base.id();
+ ctx.register_widget(p_id, p_ptr);
+ let self_ptr = self.as_ptr();
+ ctx.register_widget(id, self_ptr);
+ ctx.tree.set_parent(id, Some(p_id));
+ }
+ } else {
+ ctx.tree.set_parent(id, None);
+ }
+ }
+
+ fn is_child_visible(&self, child_id: WidgetId) -> bool {
+ if !Layout::has_container_children(&self.inner) {
+ return true;
+ }
+ for child in Layout::container_children(&self.inner) {
+ if let Some(b) = unsafe { (*child).base() } {
+ if b.id() == child_id {
+ return Layout::child_visible(&self.inner, child);
+ }
+ }
+ }
+ false
+ }
+
+ fn layout(&mut self, origin: crate::widget::Point, constraints: crate::widget::LayoutConstraints, ctx: &mut UiContext) {
+ // The Element default (measure + set_rect), plus recursive child layout for visible
+ // containers — the ctx-carrying half of the arrangement the model can't do in
+ // `arrange_children`.
+ let size = self.measure(constraints, ctx);
+ self.set_rect(origin.x, origin.y, size.width, size.height);
+ if Layout::has_container_children(&self.inner) && self.visible {
+ let rect = self.content_rect();
+ Layout::layout_children_ctx(&mut self.inner, rect, ctx);
+ }
+ }
+
+ fn get_text_items(&self) -> Vec<(&glyphon::Buffer, f32, f32, glyphon::Color)> {
+ let mut items = Vec::new();
+ if self.visible() {
+ for child in self.visible_children() {
+ items.extend(unsafe { &*child }.get_text_items());
+ }
+ }
+ items
+ }
+
+ fn prepare_text(&mut self, fs: &mut glyphon::FontSystem) {
+ if self.visible() {
+ for child in self.visible_children() {
+ unsafe { (*child).prepare_text(fs) };
+ }
+ }
+ }
+
+ fn popover_rect(&self) -> Option<(f32, f32, f32, f32)> {
+ if !self.visible() {
+ return None;
+ }
+ self.visible_children().into_iter().find_map(|c| unsafe { &*c }.popover_rect())
+ }
+
+ fn render_popover(&self, pc: &mut dyn crate::layout::RenderTarget) {
+ if !self.visible() {
+ return;
+ }
+ for child in self.visible_children() {
+ unsafe { &*child }.render_popover(pc);
+ }
+ }
+
// --- Layout concern -> `Layout` ---
fn layout_style(&self) -> Option<Style> {
Layout::layout_style(&self.inner)
@@ -557,15 +821,22 @@ impl<W: Layout + Paint + Input + 'static> Element for Adapted<W> {
/// `set_rect` overrides). Inline-label widgets ([`Layout::inline_label`]) draw the label
/// inside their rect and get no inflation. Zero-cost when no label is set.
fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
+ let r = Layout::adjust_rect(&self.inner, Rect { x, y, width: w, height: h });
let inflation = if Layout::inline_label(&self.inner) || !Layout::inflates_label_rect(&self.inner) {
0.0
} else {
self.base.label_offset()
};
- self.base.x = x;
- self.base.y = y;
- self.base.w = w;
- self.base.h = h + inflation;
+ self.base.x = r.x;
+ self.base.y = r.y;
+ self.base.w = r.width;
+ self.base.h = r.height + inflation;
+ // Containers position their children from the assigned rect (legacy `set_rect`
+ // overrides); hidden containers skip it, like the legacy impls.
+ if self.visible {
+ let content = self.content_rect();
+ Layout::arrange_children(&mut self.inner, content);
+ }
}
fn preferred_height(&self) -> Option<f32> {
@@ -627,77 +898,38 @@ impl<W: Layout + Paint + Input + 'static> Element for Adapted<W> {
/// widget draws — inline labels, readouts), plus the base-label text for detached-label
/// widgets (drawn by the adapter, since the label lives on the base).
/// The legacy bounded-text getters, honoring [`Paint::text_bounds`] (Graph clips node
- /// names to its own rect). Without it, `text_labels_with_bounds` matches the unbounded
- /// `Element` default, and `text_labels_with_font_and_bounds` replicates the default's
+ /// names to its own rect) and aggregating container children — each child contributes its
+ /// own getter of the same kind, so its fonts/bounds are preserved (the Layer pattern).
+ /// Without a text-bounds hook, `text_labels_with_bounds` matches the unbounded `Element`
+ /// default, and `text_labels_with_font_and_bounds` replicates the default's
/// scroll-ancestor walk (an overriding impl can no longer call it).
- fn text_labels_with_bounds(&self, _ctx: &UiContext) -> Vec<(TextLabel, Option<[f32; 4]>)> {
+ fn text_labels_with_bounds(&self, ctx: &UiContext) -> Vec<(TextLabel, Option<[f32; 4]>)> {
let bounds = Paint::text_bounds(&self.inner, self.content_rect());
- self.text_labels().into_iter().map(|l| (l, bounds)).collect()
+ let mut out: Vec<_> = self.own_text_labels().into_iter().map(|l| (l, bounds)).collect();
+ if self.visible() {
+ for child in self.visible_children() {
+ out.extend(unsafe { &*child }.text_labels_with_bounds(ctx));
+ }
+ }
+ out
}
fn text_labels_with_font_and_bounds(&self, ctx: &UiContext) -> Vec<(TextLabel, Option<String>, Option<[f32; 4]>)> {
- let font = Paint::widget_font(&self.inner);
- if let Some(bounds) = Paint::text_bounds(&self.inner, self.content_rect()) {
- return self
- .text_labels()
- .into_iter()
- .map(|l| (l, font.clone(), Some(bounds)))
- .collect();
- }
-
- // Replica of the `Element` default: clip to the viewport of a ScrollBox/List ancestor.
- let mut labels =
- self.text_labels().into_iter().map(|l| (l, font.clone(), None::<[f32; 4]>)).collect::<Vec<_>>();
- let mut curr = self.parent(ctx);
- let mut scroll_box_bounds = None;
- while let Some(parent_ptr) = curr {
- let parent = unsafe { &*parent_ptr };
- if let Some(scroll_box) = parent.as_any().downcast_ref::<crate::widget::ScrollBox>() {
- let (sb_x, _, sb_w, _) = parent.rect();
- let view_min = scroll_box.viewport_y + 4.0;
- let view_max = scroll_box.viewport_y + scroll_box.viewport_h - 4.0;
- scroll_box_bounds = Some([sb_x, view_min, sb_x + sb_w, view_max]);
- break;
- } else if let Some(list) = parent.as_any().downcast_ref::<crate::widget::List>() {
- let (sb_x, _, sb_w, _) = parent.rect();
- let view_min = list.scroll_box.viewport_y + 4.0;
- let view_max = list.scroll_box.viewport_y + list.scroll_box.viewport_h - 4.0;
- scroll_box_bounds = Some([sb_x, view_min, sb_x + sb_w, view_max]);
- break;
- }
- curr = parent.parent(ctx);
- }
- if let Some(sb_bounds) = scroll_box_bounds {
- for item in &mut labels {
- if let Some(ref mut b) = item.2 {
- b[0] = b[0].max(sb_bounds[0]);
- b[1] = b[1].max(sb_bounds[1]);
- b[2] = b[2].min(sb_bounds[2]);
- b[3] = b[3].min(sb_bounds[3]);
- } else {
- item.2 = Some(sb_bounds);
- }
+ let mut own = self.own_labels_with_font_and_bounds(ctx);
+ if self.visible() {
+ for child in self.visible_children() {
+ own.extend(unsafe { &*child }.text_labels_with_font_and_bounds(ctx));
}
}
- labels
+ own
}
fn text_labels(&self) -> Vec<TextLabel> {
- if !self.visible() {
- return Vec::new();
- }
- let mut out: Vec<TextLabel> = self
- .painted_prims()
- .into_iter()
- .filter_map(|prim| match prim {
- Prim::Text { text, x, y, font_size, color } => {
- Some(TextLabel { text, x, y, font_size, color })
- }
- _ => None,
- })
- .collect();
- if !Layout::inline_label(&self.inner) {
- out.extend(self.base_label_fallback());
+ let mut out = self.own_text_labels();
+ if self.visible() {
+ for child in self.visible_children() {
+ out.extend(unsafe { &*child }.text_labels());
+ }
}
out
}
@@ -710,11 +942,12 @@ impl<W: Layout + Paint + Input + 'static> Element for Adapted<W> {
// the widget's OWN geometry only: adapted widgets are leaves for now; recursion belongs to
// `scene::painter`.
- fn all_rounded_quads(&self, _ctx: &UiContext) -> Vec<(f32, f32, f32, f32, f32, [f32; 4], (bool, bool, bool, bool))> {
+ fn all_rounded_quads(&self, ctx: &UiContext) -> Vec<(f32, f32, f32, f32, f32, [f32; 4], (bool, bool, bool, bool))> {
if !self.visible() {
return Vec::new();
}
- self.painted_prims()
+ let mut out: Vec<_> = self
+ .painted_prims()
.into_iter()
.filter_map(|prim| match prim {
Prim::RoundedRect { rect, radius, corners, color } => {
@@ -722,7 +955,12 @@ impl<W: Layout + Paint + Input + 'static> Element for Adapted<W> {
}
_ => None,
})
- .collect()
+ .collect();
+ // Containers recurse, matching the `Element` default this override replaces.
+ for child in self.visible_children() {
+ out.extend(unsafe { &*child }.all_rounded_quads(ctx));
+ }
+ out
}
fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
@@ -746,11 +984,33 @@ impl<W: Layout + Paint + Input + 'static> Element for Adapted<W> {
/// only — `all_quads` must stay empty or they draw it twice. Mirrors legacy Graph's
/// highlight-only `all_quads` override. Otherwise: the `Element` default minus the shared
/// highlight (suppressed for all adapted widgets via `highlight_quad -> None`).
- fn all_quads(&self, _ctx: &UiContext) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
+ fn all_quads(&self, ctx: &UiContext) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
if Paint::serves_legacy_plain_quads(&self.inner) {
return Vec::new();
}
- self.extra_quads()
+ let mut quads = self.extra_quads();
+ if self.visible() {
+ // Container aggregation, replicating the shared legacy loop (Layer, Switcher):
+ // children contribute their plain quads, except a rounded-cornered child's
+ // background quad — that one arrives through `all_rounded_quads` instead.
+ for child in self.visible_children() {
+ let widget = unsafe { &*child };
+ let (wx, wy, ww, wh) = widget.rect();
+ let has_rounded = widget.rounded_corners() != (false, false, false, false);
+ for (qx, qy, qw, qh, qc) in widget.all_quads(ctx) {
+ if has_rounded
+ && (qx - wx).abs() < 0.1
+ && (qy - wy).abs() < 0.1
+ && (qw - ww).abs() < 0.1
+ && (qh - wh).abs() < 0.1
+ {
+ continue;
+ }
+ quads.push((qx, qy, qw, qh, qc));
+ }
+ }
+ }
+ quads
}
fn extra_circles(&self) -> Vec<(f32, f32, f32, [f32; 4])> {
@@ -809,9 +1069,15 @@ impl<W: Layout + Paint + Input + 'static> Element for Adapted<W> {
fn is_dragging(&self) -> bool {
Input::is_dragging(&self.inner)
}
- fn tick(&mut self, dt: f32, _ctx: &mut UiContext) -> bool {
+ fn tick(&mut self, dt: f32, ctx: &mut UiContext) -> bool {
let rect = self.content_rect();
- Input::tick(&mut self.inner, dt, rect)
+ let mut changed = Input::tick(&mut self.inner, dt, rect);
+ if self.visible {
+ for child in self.visible_children() {
+ changed |= unsafe { &mut *child }.tick(dt, ctx);
+ }
+ }
+ changed
}
fn wants_tick(&self) -> bool {
Input::wants_tick(&self.inner)
@@ -958,6 +1224,15 @@ impl<W: Layout + Paint + Input + 'static> Element for Adapted<W> {
if !self.visible() {
return false;
}
+ // Containers with a hit-through policy delegate entirely to their visible children
+ // (each child runs its own coverage check) — the legacy Layer/Switcher pattern, which
+ // never consulted the container's own rect or coverage.
+ if Input::hits_through_children(&self.inner) {
+ return self
+ .visible_children()
+ .into_iter()
+ .any(|c| unsafe { &*c }.hit_test(px, py, ctx));
+ }
// Preserve the legacy occlusion check (a covering layer swallows the hit), then delegate
// the geometric test to the narrow trait instead of the row/label-offset machinery.
if ctx.is_coordinate_covered(self as *const Self as *const () as usize, px, py) {
@@ -997,7 +1272,15 @@ impl<W: Layout + Paint + Input + 'static> Element for Adapted<W> {
// per-widget "check hit_test first" boilerplate legacy `mouse_input` overrides do.
// RELEASES are deliberately NOT gated: a press-tracking widget (Button) must see the
// release wherever the cursor ended up, to commit or cancel — exactly what legacy
- // `mouse_input` overrides did by receiving every release.
+ // `mouse_input` overrides did by receiving every release. Event-proxying containers
+ // opt out of the press gate (`Input::gates_presses`): legacy container overrides
+ // saw every press (Switcher unfocuses its child on an outside press).
+ Event::MouseButton { state: crate::widget::ElementState::Pressed, x: px, y: py, .. }
+ if !Input::gates_presses(&self.inner) =>
+ {
+ let _ = (px, py);
+ Input::on_event(&mut self.inner, event, &mut ectx!())
+ }
Event::MouseButton { state: crate::widget::ElementState::Pressed, x: px, y: py, .. }
| Event::MouseWheel { x: px, y: py, .. } => {
self.hit_test(*px, *py, ctx) && Input::on_event(&mut self.inner, event, &mut ectx!())