widget gallery and compositor test bench
refactor(panel)!: the ControlPanel endgame — the panel dissolves to scroll chrome
The gallery's last stored-child-pointer container is gone: the panel model
keeps only its ScrollBox and bg/border paint; the app lays the child slots
out at SCREEN coordinates (arrange_control_panel, scroll applied at layout
time, re-run per frame), emits their geometry/text clamped to the panel
viewport with the legacy partial-clip and border-inset rules, and
dispatches them as ordinary routed roots behind cp_gate (panel rect or an
open child popover — without it, fold-hidden slots stole chrome clicks).
The panel takes the wheel before its children; keys stay focused-path-only.
Legacy's double-drawn scrollbar (rounded AND plain aggregates) is single-
drawn now. Roster grew per-slot draggable/is_dragging matches (gallery +
child mode) for the trait shrink.
Verified: Windows page A/B vs pre-dissolution baseline across static/
scrolled/popover states (masks empty except the scrollbar single-draw
strip); dropdown select -> Surface Info update; spinbox increment; wheel
scroll; page switching both ways; child mode alive. Thumb drag: user
spot-check.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01N4ajhvVZtyEEEus9bodsj3
src/main.rs | 481 ++++++++++++++++++++++++++++++++++++++++++-----
src/ti_widgets.rs | 552 +++---------------------------------------------------
2 files changed, 462 insertions(+), 571 deletions(-)
diff --git a/src/main.rs b/src/main.rs
index c1d1267..6005209 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -79,6 +79,127 @@ pub struct GallerySlots {
pub const GALLERY_COUNT: usize = 53;
impl GallerySlots {
+
+ // Per-slot drag queries (the ControlPanel endgame took `draggable`/`is_dragging`
+ // off `WidgetHost`).
+ pub fn draggable(&self, idx: usize) -> bool {
+ match idx {
+ 0 => self.menu_bar.draggable(),
+ 1 => self.paginator.draggable(),
+ 2 => self.status_bar.draggable(),
+ 3 => self.button_demo.draggable(),
+ 4 => self.label4.draggable(),
+ 5 => self.label5.draggable(),
+ 6 => self.label6.draggable(),
+ 7 => self.label7.draggable(),
+ 8 => self.label8.draggable(),
+ 9 => self.checkbox_demo.draggable(),
+ 10 => self.toggle_demo.draggable(),
+ 11 => self.progress_demo.draggable(),
+ 12 => self.slider_demo.draggable(),
+ 13 => self.spinbox_demo.draggable(),
+ 14 => self.panel_demo.draggable(),
+ 15 => self.create_window_btn.draggable(),
+ 16 => self.tile_windows_btn.draggable(),
+ 17 => self.opacity_toggle.draggable(),
+ 18 => self.label18.draggable(),
+ 19 => self.transparency_slider.draggable(),
+ 20 => self.transparency_label.draggable(),
+ 21 => self.window_type_dd.draggable(),
+ 22 => self.window_shape_dd.draggable(),
+ 23 => self.enable_toggle.draggable(),
+ 24 => self.label24.draggable(),
+ 25 => self.width_spin.draggable(),
+ 26 => self.height_spin.draggable(),
+ 27 => self.surface_plate.draggable(),
+ 28 => self.surface_info_label.draggable(),
+ 29 => self.surface_desc_label.draggable(),
+ 30 => self.range_slider_demo.draggable(),
+ 31 => self.trackpad_demo.draggable(),
+ 32 => self.portal_panel.draggable(),
+ 33 => self.portal_label.draggable(),
+ 34 => self.open_dialog_btn.draggable(),
+ 35 => self.save_dialog_btn.draggable(),
+ 36 => self.textbox_demo.draggable(),
+ 37 => self.plate_demo.draggable(),
+ 38 => self.backplate_toggle.draggable(),
+ 39 => self.menubar_toggle.draggable(),
+ 40 => self.statusbar_toggle.draggable(),
+ 41 => self.border_section.draggable(),
+ 42 => self.bevel_toggle.draggable(),
+ 43 => self.border_width_spin.draggable(),
+ 44 => self.bevel_depth_spin.draggable(),
+ 45 => self.elements_section.draggable(),
+ 46 => self.page_selector.draggable(),
+ 47 => self.color_ramp_btn.draggable(),
+ 48 => self.bevel_shape_btn.draggable(),
+ 49 => self.bevel_ramp.draggable(),
+ 50 => self.ramp_btn.draggable(),
+ 51 => self.control_panel.draggable(),
+ 52 => self.layout_dd.draggable(),
+ _ => panic!("gallery slot index out of range: {idx}"),
+ }
+ }
+
+ pub fn is_dragging(&self, idx: usize) -> bool {
+ match idx {
+ 0 => self.menu_bar.is_dragging(),
+ 1 => self.paginator.is_dragging(),
+ 2 => self.status_bar.is_dragging(),
+ 3 => self.button_demo.is_dragging(),
+ 4 => self.label4.is_dragging(),
+ 5 => self.label5.is_dragging(),
+ 6 => self.label6.is_dragging(),
+ 7 => self.label7.is_dragging(),
+ 8 => self.label8.is_dragging(),
+ 9 => self.checkbox_demo.is_dragging(),
+ 10 => self.toggle_demo.is_dragging(),
+ 11 => self.progress_demo.is_dragging(),
+ 12 => self.slider_demo.is_dragging(),
+ 13 => self.spinbox_demo.is_dragging(),
+ 14 => self.panel_demo.is_dragging(),
+ 15 => self.create_window_btn.is_dragging(),
+ 16 => self.tile_windows_btn.is_dragging(),
+ 17 => self.opacity_toggle.is_dragging(),
+ 18 => self.label18.is_dragging(),
+ 19 => self.transparency_slider.is_dragging(),
+ 20 => self.transparency_label.is_dragging(),
+ 21 => self.window_type_dd.is_dragging(),
+ 22 => self.window_shape_dd.is_dragging(),
+ 23 => self.enable_toggle.is_dragging(),
+ 24 => self.label24.is_dragging(),
+ 25 => self.width_spin.is_dragging(),
+ 26 => self.height_spin.is_dragging(),
+ 27 => self.surface_plate.is_dragging(),
+ 28 => self.surface_info_label.is_dragging(),
+ 29 => self.surface_desc_label.is_dragging(),
+ 30 => self.range_slider_demo.is_dragging(),
+ 31 => self.trackpad_demo.is_dragging(),
+ 32 => self.portal_panel.is_dragging(),
+ 33 => self.portal_label.is_dragging(),
+ 34 => self.open_dialog_btn.is_dragging(),
+ 35 => self.save_dialog_btn.is_dragging(),
+ 36 => self.textbox_demo.is_dragging(),
+ 37 => self.plate_demo.is_dragging(),
+ 38 => self.backplate_toggle.is_dragging(),
+ 39 => self.menubar_toggle.is_dragging(),
+ 40 => self.statusbar_toggle.is_dragging(),
+ 41 => self.border_section.is_dragging(),
+ 42 => self.bevel_toggle.is_dragging(),
+ 43 => self.border_width_spin.is_dragging(),
+ 44 => self.bevel_depth_spin.is_dragging(),
+ 45 => self.elements_section.is_dragging(),
+ 46 => self.page_selector.is_dragging(),
+ 47 => self.color_ramp_btn.is_dragging(),
+ 48 => self.bevel_shape_btn.is_dragging(),
+ 49 => self.bevel_ramp.is_dragging(),
+ 50 => self.ramp_btn.is_dragging(),
+ 51 => self.control_panel.is_dragging(),
+ 52 => self.layout_dd.is_dragging(),
+ _ => panic!("gallery slot index out of range: {idx}"),
+ }
+ }
+
pub fn get_dyn(&self, idx: usize) -> &(dyn WidgetHost + 'static) {
match idx {
0 => &self.menu_bar,
@@ -232,6 +353,55 @@ pub struct ChildSlots {
pub const CHILD_COUNT: usize = 5;
impl ChildSlots {
+
+ pub fn draggable(&self, idx: usize) -> bool {
+ match idx {
+ 0 => match &self.bg {
+ ChildBg::Backplate(w) => w.draggable(),
+ ChildBg::ContentBg(w) => w.draggable(),
+ },
+ 1 => match &self.main {
+ ChildMain::ColorRamp(w) => w.draggable(),
+ ChildMain::Ramp(w) => w.draggable(),
+ ChildMain::Desc(w) => w.draggable(),
+ },
+ 2 => self.close.draggable(),
+ 3 => match &self.aux3 {
+ ChildAux3::Label(w) => w.draggable(),
+ ChildAux3::MenuBar(w) => w.draggable(),
+ },
+ 4 => match &self.aux4 {
+ ChildAux4::Label(w) => w.draggable(),
+ ChildAux4::StatusBar(w) => w.draggable(),
+ },
+ _ => panic!("child slot index out of range: {idx}"),
+ }
+ }
+
+ pub fn is_dragging(&self, idx: usize) -> bool {
+ match idx {
+ 0 => match &self.bg {
+ ChildBg::Backplate(w) => w.is_dragging(),
+ ChildBg::ContentBg(w) => w.is_dragging(),
+ },
+ 1 => match &self.main {
+ ChildMain::ColorRamp(w) => w.is_dragging(),
+ ChildMain::Ramp(w) => w.is_dragging(),
+ ChildMain::Desc(w) => w.is_dragging(),
+ },
+ 2 => self.close.is_dragging(),
+ 3 => match &self.aux3 {
+ ChildAux3::Label(w) => w.is_dragging(),
+ ChildAux3::MenuBar(w) => w.is_dragging(),
+ },
+ 4 => match &self.aux4 {
+ ChildAux4::Label(w) => w.is_dragging(),
+ ChildAux4::StatusBar(w) => w.is_dragging(),
+ },
+ _ => panic!("child slot index out of range: {idx}"),
+ }
+ }
+
pub fn get_dyn(&self, idx: usize) -> &(dyn WidgetHost + 'static) {
match idx {
0 => match &self.bg {
@@ -303,6 +473,20 @@ impl Roster {
}
}
+ pub fn draggable(&self, idx: usize) -> bool {
+ match self {
+ Roster::Gallery(s) => s.draggable(idx),
+ Roster::Child(s) => s.draggable(idx),
+ }
+ }
+
+ pub fn is_dragging(&self, idx: usize) -> bool {
+ match self {
+ Roster::Gallery(s) => s.is_dragging(idx),
+ Roster::Child(s) => s.is_dragging(idx),
+ }
+ }
+
pub fn get_dyn_mut(&mut self, idx: usize) -> &mut (dyn WidgetHost + 'static) {
match self {
Roster::Gallery(s) => s.get_dyn_mut(idx),
@@ -581,6 +765,33 @@ fn is_control_panel_child(i: usize) -> bool {
matches!(i, 15..=26 | 38..=45 | 48)
}
+/// The panel's child slots in the legacy arrangement/aggregation order (the ControlPanel
+/// endgame: the panel no longer stores pointers to them — the app lays them out, emits
+/// them clamped to the panel viewport, and dispatches them as ordinary routed roots).
+const CP_CHILDREN: [usize; 19] = [15, 16, 21, 22, 17, 19, 20, 23, 25, 26, 38, 39, 40, 41, 42, 43, 44, 45, 48];
+
+impl State {
+ /// The dissolved panel's hit gate: pointer events reach the panel's children only
+ /// inside the panel rect or an open child popover (the legacy `ControlPanel::hit` +
+ /// viewport-Y forwarding rule). Children sit at real screen rects now — without this
+ /// gate, slots clipped BELOW the visible fold would take clicks meant for the chrome
+ /// beneath them.
+ fn cp_gate(&self, x: f32, y: f32) -> bool {
+ let (px, py, pw, ph) = self.roster.gallery().control_panel.rect();
+ if x >= px && x <= px + pw && y >= py && y <= py + ph {
+ return true;
+ }
+ for &ci in CP_CHILDREN.iter() {
+ if let Some((rx, ry, rw, rh)) = self.roster.get_dyn(ci).popover_rect() {
+ if x >= rx && x <= rx + rw && y >= ry && y <= ry + rh {
+ return true;
+ }
+ }
+ }
+ false
+ }
+}
+
fn open_file_dialog_portal(sender: calloop::channel::Sender<String>) {
if let Some(path) = cce_ui::file_dialog::pick_file("Open File Dialog", &[]) {
let _ = sender.send(format!("Selected: {}", path.display()));
@@ -642,20 +853,18 @@ impl State {
let visible = self.is_widget_visible(i);
let pos = self.positions[i];
if i < self.roster.len() {
- let widget = self.roster.get_dyn_mut(i);
- if widget.is_dragging() {
+ if self.roster.is_dragging(i) {
continue;
}
+ let widget = self.roster.get_dyn_mut(i);
if !self.is_child && i == 46 {
continue;
}
- let is_cp_child = is_control_panel_child(i);
- if is_cp_child {
- if !visible {
- widget.set_rect(-1000.0, -1000.0, 0.0, 0.0);
- }
+ if is_control_panel_child(i) {
+ // Laid out by arrange_control_panel after this loop (real screen
+ // coordinates; every consumer gates on page visibility).
continue;
}
@@ -686,6 +895,59 @@ impl State {
let dx = sb_rect.0 + sb_rect.2 - dw - pad_x;
let dy = sb_rect.1 + pad_y;
self.roster.get_dyn_mut(46).set_rect(dx, dy, dw, ddh);
+
+ self.arrange_control_panel();
+ }
+ }
+
+ /// Lay the panel's child slots out at SCREEN coordinates (the ControlPanel endgame).
+ /// The legacy panel arranged them in content space and shifted at aggregate time;
+ /// applying the scroll offset at layout time means hit-testing, dispatch, text, and
+ /// popovers all see real positions. Runs from apply_layout — every rebuild — which is
+ /// also what re-arranges after a scroll (the wheel handler sets needs_rebuild).
+ fn arrange_control_panel(&mut self) {
+ let s = self.roster.gallery_mut();
+ let (x, y, w, h) = s.control_panel.rect();
+ let padding = cce_ui::layout::control_panel_padding();
+ let gap = cce_ui::layout::control_panel_gap();
+ // 12px reserved for the scrollbar track, like the legacy arrangement.
+ let mut col = cce_ui::widget::ColumnLayout::new(x, y, w - 12.0, gap, padding);
+
+ let create_p: *mut (dyn WidgetHost + 'static) = &mut s.create_window_btn;
+ let tile_p: *mut (dyn WidgetHost + 'static) = &mut s.tile_windows_btn;
+ col.add_row(&[create_p, tile_p], 28.0, 12.0);
+ let width_p: *mut (dyn WidgetHost + 'static) = &mut s.width_spin;
+ let height_p: *mut (dyn WidgetHost + 'static) = &mut s.height_spin;
+ col.add_row(&[width_p, height_p], 42.0, 12.0);
+ col.add_widget(&mut s.window_type_dd, 44.0);
+ col.add_widget(&mut s.window_shape_dd, 44.0);
+ col.add_widget(&mut s.opacity_toggle, 28.0);
+ col.add_widget(&mut s.enable_toggle, 28.0);
+ col.add_widget(&mut s.transparency_label, 12.0);
+ col.add_widget(&mut s.transparency_slider, 20.0);
+ col.add_widget(&mut s.elements_section, 20.0);
+ let bp_p: *mut (dyn WidgetHost + 'static) = &mut s.backplate_toggle;
+ let mb_p: *mut (dyn WidgetHost + 'static) = &mut s.menubar_toggle;
+ let sb_p: *mut (dyn WidgetHost + 'static) = &mut s.statusbar_toggle;
+ col.add_row(&[bp_p, mb_p, sb_p], 28.0, 10.0);
+ col.add_widget(&mut s.border_section, 20.0);
+ col.add_widget(&mut s.bevel_toggle, 28.0);
+ let bw_p: *mut (dyn WidgetHost + 'static) = &mut s.border_width_spin;
+ let bd_p: *mut (dyn WidgetHost + 'static) = &mut s.bevel_depth_spin;
+ col.add_row(&[bw_p, bd_p], 42.0, 12.0);
+ col.add_widget(&mut s.bevel_shape_btn, 28.0);
+
+ let total_h = col.current_y();
+ s.control_panel.scroll_box.update_bounds(total_h, y, h);
+
+ // Content coordinates -> screen coordinates.
+ let scroll_y = s.control_panel.scroll_box.scroll_y;
+ if scroll_y != 0.0 {
+ for &ci in CP_CHILDREN.iter() {
+ let w = self.roster.get_dyn_mut(ci);
+ let (cx, cy, cw, ch) = w.rect();
+ w.set_rect(cx, cy - scroll_y, cw, ch);
+ }
}
}
@@ -979,15 +1241,6 @@ cascades in cce."
};
if !is_child {
- let child_ptrs: Vec<*mut (dyn WidgetHost + 'static)> = [15, 16, 21, 22, 17, 19, 20, 23, 25, 26, 38, 39, 40, 41, 42, 43, 44, 45, 48]
- .iter()
- .map(|&idx| state.roster.get_dyn_mut(idx) as *mut (dyn WidgetHost + 'static))
- .collect();
- let cp = state.roster.get_dyn_mut(51).as_any_mut().downcast_mut::<ControlPanel>().expect("widget 51 must be a ControlPanel");
- for ptr in child_ptrs {
- cp.add_child(ptr);
- }
-
// Link page selector (46) under StatusBar (2) — the old set_parent + add_child
// pair as the one tree link it always was (6bd batch 4).
let statusbar_ptr = state.roster.get_dyn_mut(2) as *mut (dyn WidgetHost + 'static);
@@ -1111,9 +1364,6 @@ cascades in cce."
for i in 0..self.roster.len() {
let w = self.roster.get_dyn_mut(i);
if is_visible(i) {
- if is_control_panel_child(i) {
- continue;
- }
if w.tick(dt, &mut self.ui_context) {
changed = true;
if self.is_child && self.child_type.as_deref() == Some("Ramp") && i == 1 {
@@ -1140,6 +1390,12 @@ cascades in cce."
// rebuild_text_items cache and its 14 invalidation call sites are gone).
use cce_ui::scene::layout::Rect;
self.register_roster();
+ // Panel children follow the scroll offset at LAYOUT time (the dissolved panel
+ // shifted at aggregate time): re-arrange every frame so a wheel scroll moves the
+ // content on the frame it repaints. Idempotent and cheap (~20 set_rects).
+ if !self.is_child {
+ self.arrange_control_panel();
+ }
if (self.width - size.width as f32).abs() > 0.001 || (self.height - size.height as f32).abs() > 0.001 || (self.scale - scale).abs() > 0.001 {
self.width = size.width as f32;
self.height = size.height as f32;
@@ -1251,6 +1507,78 @@ cascades in cce."
}
}
+ // ── ControlPanel children (the dissolved panel's aggregate, app-side): the slots
+ // are at real screen coordinates now; the legacy clamp-to-viewport, partial-clip
+ // radius zeroing, and solid-border synthesis/inset rules apply verbatim, and the
+ // scrollbar draws last, over the children, like the aggregate did. ──
+ if !self.is_child && self.current_page == Page::Windows {
+ let (panel_x, panel_y, panel_w, panel_h) = self.roster.gallery().control_panel.rect();
+ let _ = (panel_x, panel_w);
+ let y_start = panel_y;
+ let y_end = panel_y + panel_h;
+ for &ci in CP_CHILDREN.iter() {
+ let w = self.roster.get_dyn(ci);
+ let solid_border_opt = if w.type_name() == "Toggle" { None } else { w.solid_border() };
+ let (cx, cy, cw, ch) = w.rect();
+
+ if let Some((b_color, _thickness)) = solid_border_opt {
+ let cy_top = cy;
+ let cy_bottom = cy + ch;
+ if cy_bottom > y_start && cy_top < y_end {
+ let visible_top = cy_top.max(y_start);
+ let visible_bottom = cy_bottom.min(y_end);
+ let visible_h = visible_bottom - visible_top;
+ if visible_h > 0.0 {
+ let (child_r, child_corners) = w.corner_style();
+ let radii_adjusted = if visible_top > cy_top || visible_bottom < cy_bottom {
+ 0.0
+ } else {
+ child_r
+ };
+ push_rounded(&mut pc, cx, visible_top, cw, visible_h, radii_adjusted, b_color, child_corners);
+ }
+ }
+ }
+
+ for (qx, qy, qw, qh, qr, qc, qcorners) in w.all_rounded_quads(&self.ui_context) {
+ let mut rx = qx;
+ let mut ry = qy;
+ let mut rw = qw;
+ let mut rh = qh;
+ let mut rqr = qr;
+
+ if let Some((_, thickness)) = solid_border_opt {
+ if (qx - cx).abs() < 0.1 && (qy - cy).abs() < 0.1 && (qw - cw).abs() < 0.1 && (qh - ch).abs() < 0.1 {
+ rx += thickness;
+ ry += thickness;
+ rw -= 2.0 * thickness;
+ rh -= 2.0 * thickness;
+ rqr = (qr - thickness).max(0.0);
+ }
+ }
+
+ let qy_top = ry;
+ let qy_bottom = ry + rh;
+ if qy_bottom > y_start && qy_top < y_end {
+ let visible_top = qy_top.max(y_start);
+ let visible_bottom = qy_bottom.min(y_end);
+ let visible_h = visible_bottom - visible_top;
+ if visible_h > 0.0 {
+ let radii_adjusted = if visible_top > qy_top || visible_bottom < qy_bottom {
+ 0.0
+ } else {
+ rqr
+ };
+ push_rounded(&mut pc, rx, visible_top, rw, visible_h, radii_adjusted, qc, qcorners);
+ }
+ }
+ }
+ }
+ for (sx, sy, sw, sh, sc) in self.roster.gallery().control_panel.scroll_box.extra_quads() {
+ push_rounded(&mut pc, sx, sy, sw, sh, 0.0, sc, (false, false, false, false));
+ }
+ }
+
// ── Plain geometry (the legacy view() body) ──
for i in 0..self.roster.len() {
let w = self.roster.get_dyn(i);
@@ -1282,6 +1610,36 @@ cascades in cce."
}
}
+ // ── ControlPanel children, plain decorations (the dissolved aggregate_plain):
+ // the child's own rounded background is skipped like the legacy rule, the rest
+ // clamps to the panel viewport. ──
+ if !self.is_child && self.current_page == Page::Windows {
+ let (_panel_x, panel_y, _panel_w, panel_h) = self.roster.gallery().control_panel.rect();
+ let y_start = panel_y;
+ let y_end = panel_y + panel_h;
+ for &ci in CP_CHILDREN.iter() {
+ let w = self.roster.get_dyn(ci);
+ let (cx, cy, cw, ch) = w.rect();
+ let has_rounded = w.corner_style().1 != (false, false, false, false);
+ let has_bg = w.color()[3].abs() > 0.001;
+ for (qx, qy, qw, qh, qc) in w.all_quads(&self.ui_context) {
+ if has_rounded && has_bg && (qx - cx).abs() < 0.1 && (qy - cy).abs() < 0.1 && (qw - cw).abs() < 0.1 && (qh - ch).abs() < 0.1 {
+ continue;
+ }
+ let qy_top = qy;
+ let qy_bottom = qy + qh;
+ if qy_bottom > y_start && qy_top < y_end {
+ let visible_top = qy_top.max(y_start);
+ let visible_bottom = qy_bottom.min(y_end);
+ let visible_h = visible_bottom - visible_top;
+ if visible_h > 0.0 {
+ pc.quad(Rect { x: qx, y: visible_top, width: qw, height: visible_h }, qc);
+ }
+ }
+ }
+ }
+ }
+
// ── Popovers: geometry then labels, in-frame, on top of everything ──
let mut popover_pc = cce_ui::layout::PopoverCollector::new();
for i in 0..self.roster.len() {
@@ -1289,9 +1647,6 @@ cascades in cce."
if !self.is_widget_visible(i) {
continue;
}
- if is_control_panel_child(i) {
- continue;
- }
if w.popover_rect().is_some() {
w.render_popover(&mut popover_pc);
}
@@ -1354,18 +1709,22 @@ cascades in cce."
if !self.is_widget_visible(i) {
continue;
}
- if is_control_panel_child(i) {
- continue;
- }
// Text via the paint walk (not the legacy getters): same labels with the
// widget's content font and clip bounds; the popover cull stays on the prim
- // coordinates. ControlPanel children are covered by the panel's walk descent,
- // and any widget with a ui-tree parent (the page selector under the status
- // bar) is covered by that parent's descent — walking it here too would emit
- // its text twice.
+ // coordinates. Any widget with a ui-tree parent (the page selector under the
+ // status bar) is covered by that parent's descent — walking it here too would
+ // emit its text twice. ControlPanel children clamp their bounds to the panel
+ // viewport (the dissolved scrolled_child_labels rule, minus the shift — the
+ // slots sit at real screen coordinates now).
if self.ui_context.tree.parent_ptr(w.base().id()).is_some() {
continue;
}
+ let cp_clip = if is_control_panel_child(i) {
+ let (px_, py_, pw_, ph_) = self.roster.gallery().control_panel.rect();
+ Some([px_, py_, px_ + pw_, py_ + ph_])
+ } else {
+ None
+ };
let mut scratch = cce_ui::scene::paint::PaintCtx::new();
cce_ui::scene::painter::append_widget_text(&self.ui_context, w, &mut scratch);
for item in scratch.finish().items {
@@ -1373,6 +1732,13 @@ cascades in cce."
if in_any_popover(x, y) {
continue;
}
+ let bounds = match (bounds, cp_clip) {
+ (Some([l, t, r, b]), Some([pl, pt, pr, pb])) => {
+ Some([l.max(pl), t.max(pt), r.min(pr), b.min(pb)])
+ }
+ (None, Some(clip)) => Some(clip),
+ (b, None) => b,
+ };
pc.text_with(text, x, y, font_size, color, font, bounds);
}
}
@@ -1582,9 +1948,6 @@ cascades in cce."
if !is_visible(i) {
continue;
}
- if is_control_panel_child(i) {
- continue;
- }
let root = self.roster.get_dyn(i).base().id();
if self.ui_context.propagate_event(&mv, root) {
changed = true;
@@ -1630,16 +1993,29 @@ cascades in cce."
if state == ElementState::Pressed {
let mut clicked_idx = None;
- for i in (0..self.roster.len()).rev() {
- if !is_visible(i) {
- continue;
- }
- if is_control_panel_child(i) {
- continue;
+ // Panel children first: they sit inside the panel's rect, so the plain
+ // reverse scan below (where the panel's higher index wins) must only see
+ // the panel when no child claims the point.
+ if !self.is_child && self.current_page == Page::Windows && self.cp_gate(lx, ly) {
+ for &ci in CP_CHILDREN.iter() {
+ if self.roster.get_dyn_mut(ci).hit_test(lx, ly, &self.ui_context) {
+ clicked_idx = Some(ci);
+ break;
+ }
}
- if self.roster.get_dyn_mut(i).hit_test(lx, ly, &self.ui_context) {
- clicked_idx = Some(i);
- break;
+ }
+ if clicked_idx.is_none() {
+ for i in (0..self.roster.len()).rev() {
+ if !is_visible(i) {
+ continue;
+ }
+ if is_control_panel_child(i) {
+ continue;
+ }
+ if self.roster.get_dyn_mut(i).hit_test(lx, ly, &self.ui_context) {
+ clicked_idx = Some(i);
+ break;
+ }
}
}
if button == MouseButton::Left {
@@ -1661,7 +2037,7 @@ cascades in cce."
if press_handled {
changed = true;
}
- if button == MouseButton::Left && !press_handled && self.roster.get_dyn(i).draggable() {
+ if button == MouseButton::Left && !press_handled && self.roster.draggable(i) {
let id = self.roster.get_dyn(i).base().id();
self.ui_context.drag_target = Some(id);
}
@@ -1677,11 +2053,12 @@ cascades in cce."
changed = true;
}
let ev = cce_ui::widget::Event::MouseButton { button, state, x: lx, y: ly, local_x: lx, local_y: ly };
+ let cp_release_ok = !self.is_child && self.cp_gate(lx, ly);
for i in 0..self.roster.len() {
if !is_visible(i) {
continue;
}
- if is_control_panel_child(i) {
+ if is_control_panel_child(i) && !cp_release_ok {
continue;
}
let root = self.roster.get_dyn(i).base().id();
@@ -1966,11 +2343,25 @@ full screen background.",
}
};
let ev = cce_ui::widget::Event::MouseWheel { delta: *delta, x: lx, y: ly, local_x: lx, local_y: ly };
+ // Panel scroll first (legacy: the panel's scroll frame consumed the wheel before
+ // its children saw it); a consumed wheel never reaches the panel's children.
+ let mut cp_took_wheel = false;
+ if !self.is_child && is_visible(51) {
+ let root = self.roster.get_dyn(51).base().id();
+ if self.ui_context.propagate_event(&ev, root) {
+ changed = true;
+ cp_took_wheel = true;
+ }
+ }
+ let cp_wheel_ok = !self.is_child && self.cp_gate(lx, ly);
for i in 0..self.roster.len() {
+ if i == 51 {
+ continue;
+ }
if !is_visible(i) {
continue;
}
- if is_control_panel_child(i) {
+ if is_control_panel_child(i) && (cp_took_wheel || !cp_wheel_ok) {
continue;
}
let root = self.roster.get_dyn(i).base().id();
@@ -2025,6 +2416,8 @@ full screen background.",
if !is_visible(i) {
continue;
}
+ // Panel children take keys only through the focused path above (the
+ // legacy panel never forwarded keys).
if is_control_panel_child(i) {
continue;
}
diff --git a/src/ti_widgets.rs b/src/ti_widgets.rs
index cb04088..9ab6f9c 100644
--- a/src/ti_widgets.rs
+++ b/src/ti_widgets.rs
@@ -17,11 +17,16 @@ pub struct ControlPanel {
y: f32,
w: f32,
h: f32,
- pub children: Vec<*mut (dyn WidgetHost + 'static)>,
pub scroll_box: ScrollBox,
- pub active_drag_widget: Option<*mut (dyn WidgetHost + 'static)>,
}
+/// The panel is scroll chrome only (the ControlPanel endgame): background, borders, and
+/// the ScrollBox machinery. Its former stored child pointers, label-matched arrangement,
+/// aggregate views, and dyn event/tick/drag forwarding are DISSOLVED into the app —
+/// main.rs lays the child slots out at SCREEN (scrolled) coordinates
+/// (`arrange_control_panel`), emits their geometry/text clamped to the panel viewport in
+/// `display_list`, and dispatches them as ordinary routed roots. No `*mut dyn` storage,
+/// no dummy contexts, no scroll-translated coordinates anywhere.
impl ControlPanel {
pub fn new() -> Adapted<ControlPanel> {
let mut sb = ScrollBox::new();
@@ -32,234 +37,14 @@ impl ControlPanel {
y: 0.0,
w: 0.0,
h: 0.0,
- children: Vec::new(),
scroll_box: sb,
- active_drag_widget: None,
})
}
- pub fn add_child(&mut self, child: *mut (dyn WidgetHost + 'static)) {
- self.children.push(child);
- }
-
/// The laid-out rect, mirrored from the adapter by `Layout::rect_assigned`.
- fn rect(&self) -> (f32, f32, f32, f32) {
+ pub fn rect(&self) -> (f32, f32, f32, f32) {
(self.x, self.y, self.w, self.h)
}
-
- /// First open child popover, in scrolled (screen) coordinates — the old
- /// `WidgetHost::popover_rect` override, verbatim (the temporary base-y shift reaches the
- /// children through their raw pointers).
- fn popover_scan(&self) -> Option<(f32, f32, f32, f32)> {
- let scroll_y = self.scroll_box.scroll_y;
- unsafe {
- for child_ptr in &self.children {
- let child = &mut **child_ptr;
- let old_y = child.base().y;
- child.base_mut().y = old_y - scroll_y;
- let res = child.popover_rect();
- child.base_mut().y = old_y;
- if res.is_some() {
- return res;
- }
- }
- }
- None
- }
-
- /// The subtree's rounded view (the old `WidgetHost::all_rounded_quads` override):
- /// background, borders, children with the scroll shift + viewport clamp + border
- /// inset, scrollbar. External readers get it through the adapter's reverse bridge.
- fn aggregate_rounded(&self, ctx: &UiContext) -> Vec<(f32, f32, f32, f32, f32, [f32; 4], (bool, bool, bool, bool))> {
- let mut quads = Vec::new();
- let (x, y, w, h) = self.rect();
-
- // 1. Background
- quads.push((x, y, w, h, 0.0, colors::control_panel_color(), (false, false, false, false)));
-
- // 2. Borders
- let border_color = colors::control_panel_border_color();
- quads.push((x, y, w, 1.0, 0.0, border_color, (false, false, false, false)));
- quads.push((x, y + h - 1.0, w, 1.0, 0.0, border_color, (false, false, false, false)));
- quads.push((x, y, 1.0, h, 0.0, border_color, (false, false, false, false)));
- quads.push((x + w - 1.0, y, 1.0, h, 0.0, border_color, (false, false, false, false)));
-
- // 3. Child elements clipped to viewport bounds
- let scroll_y = self.scroll_box.scroll_y;
- let y_start = y;
- let y_end = y + h;
-
- unsafe {
- for child_ptr in &self.children {
- let child = &**child_ptr;
- let solid_border_opt = if child.type_name() == "Toggle" { None } else { child.solid_border() };
- let (cx, cy, cw, ch) = child.rect();
-
- if let Some((b_color, _thickness)) = solid_border_opt {
- let cy_shifted = cy - scroll_y;
- let cy_top = cy_shifted;
- let cy_bottom = cy_shifted + ch;
- if cy_bottom > y_start && cy_top < y_end {
- let visible_top = cy_top.max(y_start);
- let visible_bottom = cy_bottom.min(y_end);
- let visible_h = visible_bottom - visible_top;
- if visible_h > 0.0 {
- let (child_r, child_corners) = child.corner_style();
- let radii_adjusted = if visible_top > cy_top || visible_bottom < cy_bottom {
- 0.0
- } else {
- child_r
- };
- quads.push((
- cx,
- visible_top,
- cw,
- visible_h,
- radii_adjusted,
- b_color,
- child_corners,
- ));
- }
- }
- }
-
- for (qx, qy, qw, qh, qr, qc, qcorners) in child.all_rounded_quads(ctx) {
- let mut rx = qx;
- let mut ry = qy;
- let mut rw = qw;
- let mut rh = qh;
- let mut rqr = qr;
-
- if let Some((_, thickness)) = solid_border_opt {
- if (qx - cx).abs() < 0.1 && (qy - cy).abs() < 0.1 && (qw - cw).abs() < 0.1 && (qh - ch).abs() < 0.1 {
- rx += thickness;
- ry += thickness;
- rw -= 2.0 * thickness;
- rh -= 2.0 * thickness;
- rqr = (qr - thickness).max(0.0);
- }
- }
-
- let qy_shifted = ry - scroll_y;
- let qy_top = qy_shifted;
- let qy_bottom = qy_shifted + rh;
- if qy_bottom > y_start && qy_top < y_end {
- let visible_top = qy_top.max(y_start);
- let visible_bottom = qy_bottom.min(y_end);
- let visible_h = visible_bottom - visible_top;
- if visible_h > 0.0 {
- let radii_adjusted = if visible_top > qy_top || visible_bottom < qy_bottom {
- 0.0
- } else {
- rqr
- };
- quads.push((rx, visible_top, rw, visible_h, radii_adjusted, qc, qcorners));
- }
- }
- }
- }
- }
-
- // 4. Scrollbar
- for (sx, sy, sw, sh, sc) in self.scroll_box.extra_quads() {
- quads.push((sx, sy, sw, sh, 0.0, sc, (false, false, false, false)));
- }
-
- quads
- }
-
- /// The subtree's plain view (the old `WidgetHost::extra_quads` override): children's
- /// decoration quads with the scroll shift + viewport clamp, plus the scrollbar.
- fn aggregate_plain(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
- let mut quads = Vec::new();
- let (_x, y, _w, h) = self.rect();
- let scroll_y = self.scroll_box.scroll_y;
- let y_start = y;
- let y_end = y + h;
- let ctx_dummy = cce_ui::context::UiContext::new();
-
- unsafe {
- for child_ptr in &self.children {
- let child = &**child_ptr;
- let (cx, cy, cw, ch) = child.rect();
- let has_rounded = child.corner_style().1 != (false, false, false, false);
- let has_bg = child.color()[3].abs() > 0.001;
-
- for (qx, qy, qw, qh, qc) in child.all_quads(&ctx_dummy) {
- if has_rounded && has_bg && (qx - cx).abs() < 0.1 && (qy - cy).abs() < 0.1 && (qw - cw).abs() < 0.1 && (qh - ch).abs() < 0.1 {
- continue;
- }
- let qy_shifted = qy - scroll_y;
- let qy_top = qy_shifted;
- let qy_bottom = qy_shifted + qh;
- if qy_bottom > y_start && qy_top < y_end {
- let visible_top = qy_top.max(y_start);
- let visible_bottom = qy_bottom.min(y_end);
- let visible_h = visible_bottom - visible_top;
- if visible_h > 0.0 {
- quads.push((qx, visible_top, qw, visible_h, qc));
- }
- }
- }
- }
- }
-
- quads.extend(self.scroll_box.extra_quads());
- quads
- }
-
- /// The children's arcs with the scroll shift (the old `WidgetHost::extra_arcs` override).
- fn aggregate_arcs(&self) -> Vec<(f32, f32, f32, f32, f32, f32, [f32; 4])> {
- let mut arcs = Vec::new();
- let scroll_y = self.scroll_box.scroll_y;
- let y_start = self.y;
- let y_end = self.y + self.h;
-
- unsafe {
- for child_ptr in &self.children {
- let child = &**child_ptr;
- for (cx, cy, r, t, start, end, color) in child.extra_arcs() {
- let cy_shifted = cy - scroll_y;
- if cy_shifted + r > y_start && cy_shifted - r < y_end {
- arcs.push((cx, cy_shifted, r, t, start, end, color));
- }
- }
- }
- }
- arcs
- }
-
- /// Children's walk text with the panel's scroll shift and viewport clamp — what the
- /// deleted fonted getter served: children are laid out UNSCROLLED and the offset is
- /// an aggregate-time transform.
- pub(crate) fn scrolled_child_labels(&self, ctx: &UiContext) -> Vec<(TextLabel, Option<String>, Option<[f32; 4]>)> {
- let scroll_y = self.scroll_box.scroll_y;
- let (x, y, w, h) = self.rect();
- let mut labels = Vec::new();
- for &child_ptr in &self.children {
- let mut scratch = cce_ui::scene::paint::PaintCtx::new();
- cce_ui::scene::painter::append_widget_text(ctx, unsafe { &*child_ptr }, &mut scratch);
- for item in scratch.finish().items {
- if let cce_ui::scene::paint::Prim::Text { text, x: lx, y: ly, font_size, color, font, bounds, .. } = item.prim {
- let new_bounds = if let Some([l, t, r, b]) = bounds {
- let nl = l.max(x);
- let nt = (t - scroll_y).max(y);
- let nr = r.min(x + w);
- let nb = (b - scroll_y).min(y + h);
- Some([nl, nt, nr, nb])
- } else {
- Some([x, y, x + w, y + h])
- };
- labels.push((
- TextLabel { text, x: lx, y: ly - scroll_y, font_size, color },
- font,
- new_bounds,
- ));
- }
- }
- }
- labels
- }
}
impl cce_ui::widget::Layout for ControlPanel {
@@ -274,152 +59,7 @@ impl cce_ui::widget::Layout for ControlPanel {
self.y = rect.y;
self.w = rect.width;
self.h = rect.height;
- }
-
- // The old `set_rect` override's arrangement: children keep their label-matched slots
- // in a ColumnLayout, laid out UNSCROLLED; the scroll offset is an aggregate-time
- // transform. (The dummy-ctx child re-parenting onto the host adapter is gone,
- // 6bd: every effect of it was discarded with the dummy ctx.)
- fn arrange_children(&mut self, rect: Rect, _host: *mut (dyn WidgetHost + 'static)) {
- let (x, y, w, h) = (rect.x, rect.y, rect.width, rect.height);
-
- self.scroll_box.set_rect(x, y, w, h);
-
-
- let padding = cce_ui::layout::control_panel_padding();
- let gap = cce_ui::layout::control_panel_gap();
- let mut col = ColumnLayout::new(x, y, w - 12.0, gap, padding); // 12px reserved for scrollbar track
-
- unsafe {
- let mut create_btn: Option<*mut dyn WidgetHost> = None;
- let mut tile_btn: Option<*mut dyn WidgetHost> = None;
- let mut opacity_toggle: Option<*mut dyn WidgetHost> = None;
- let mut enable_toggle: Option<*mut dyn WidgetHost> = None;
- let mut slider: Option<*mut dyn WidgetHost> = None;
- let mut slider_label: Option<*mut dyn WidgetHost> = None;
- let mut type_dd: Option<*mut dyn WidgetHost> = None;
- let mut shape_dd: Option<*mut dyn WidgetHost> = None;
- let mut border_style_dd: Option<*mut dyn WidgetHost> = None;
- let mut width_spin: Option<*mut dyn WidgetHost> = None;
- let mut height_spin: Option<*mut dyn WidgetHost> = None;
- let mut backplate_toggle: Option<*mut dyn WidgetHost> = None;
- let mut menubar_toggle: Option<*mut dyn WidgetHost> = None;
- let mut statusbar_toggle: Option<*mut dyn WidgetHost> = None;
- let mut border_sec: Option<*mut dyn WidgetHost> = None;
- let mut bevel_toggle: Option<*mut dyn WidgetHost> = None;
- let mut border_width_spin: Option<*mut dyn WidgetHost> = None;
- let mut bevel_depth_spin: Option<*mut dyn WidgetHost> = None;
- let mut win_sec: Option<*mut dyn WidgetHost> = None;
- let mut bevel_shape_btn: Option<*mut dyn WidgetHost> = None;
-
- for &child_ptr in &self.children {
- let child = &mut *child_ptr;
- let label = child.base().label.as_ref().map(|s| s.as_str()).unwrap_or("");
- match label {
- "Create Window" => create_btn = Some(child_ptr),
- "Tile Windows" => tile_btn = Some(child_ptr),
- "Opacity" => opacity_toggle = Some(child_ptr),
- "Enable" => enable_toggle = Some(child_ptr),
- "Transparency Level" => slider_label = Some(child_ptr),
- "Border" => border_sec = Some(child_ptr),
- "Window Elements" => win_sec = Some(child_ptr),
- "Window Type" => type_dd = Some(child_ptr),
- "Window Shape" => shape_dd = Some(child_ptr),
- "Border Style" => border_style_dd = Some(child_ptr),
- "Width" => width_spin = Some(child_ptr),
- "Height" => height_spin = Some(child_ptr),
- "Backplate" => backplate_toggle = Some(child_ptr),
- "MenuBar" => menubar_toggle = Some(child_ptr),
- "StatusBar" => statusbar_toggle = Some(child_ptr),
- "Bevel" => bevel_toggle = Some(child_ptr),
- "Border Width" => border_width_spin = Some(child_ptr),
- "Bevel Depth" => bevel_depth_spin = Some(child_ptr),
- "Bevel Shape..." => bevel_shape_btn = Some(child_ptr),
- _ => {
- if child.base().label.is_none() {
- slider = Some(child_ptr);
- }
- }
- }
- }
-
- if let (Some(c), Some(t)) = (create_btn, tile_btn) {
- col.add_row(&[c, t], 28.0, 12.0);
- } else {
- if let Some(c) = create_btn {
- col.add_widget(&mut *c, 28.0);
- }
- if let Some(t) = tile_btn {
- col.add_widget(&mut *t, 28.0);
- }
- }
-
- if let (Some(w_sp), Some(h_sp)) = (width_spin, height_spin) {
- col.add_row(&[w_sp, h_sp], 42.0, 12.0);
- } else {
- if let Some(w_sp) = width_spin {
- col.add_widget(&mut *w_sp, 42.0);
- }
- if let Some(h_sp) = height_spin {
- col.add_widget(&mut *h_sp, 42.0);
- }
- }
-
- if let Some(t_dd) = type_dd {
- col.add_widget(&mut *t_dd, 44.0);
- }
- if let Some(s_dd) = shape_dd {
- col.add_widget(&mut *s_dd, 44.0);
- }
- if let Some(op_t) = opacity_toggle {
- col.add_widget(&mut *op_t, 28.0);
- }
- if let Some(en_t) = enable_toggle {
- col.add_widget(&mut *en_t, 28.0);
- }
- if let Some(sl_lbl) = slider_label {
- col.add_widget(&mut *sl_lbl, 12.0);
- }
- if let Some(sl) = slider {
- col.add_widget(&mut *sl, 20.0);
- }
-
- if let Some(w_s) = win_sec {
- col.add_widget(&mut *w_s, 20.0);
- }
- let toggles = [backplate_toggle, menubar_toggle, statusbar_toggle];
- let active_toggles: Vec<*mut dyn WidgetHost> = toggles.iter().filter_map(|&t| t).collect();
- if !active_toggles.is_empty() {
- col.add_row(&active_toggles, 28.0, 10.0);
- }
-
- if let Some(b_s) = border_sec {
- col.add_widget(&mut *b_s, 20.0);
- }
- if let Some(bs_dd) = border_style_dd {
- col.add_widget(&mut *bs_dd, 44.0);
- }
- if let Some(bev_t) = bevel_toggle {
- col.add_widget(&mut *bev_t, 28.0);
- }
-
- if let (Some(bw_sp), Some(bd_sp)) = (border_width_spin, bevel_depth_spin) {
- col.add_row(&[bw_sp, bd_sp], 42.0, 12.0);
- } else {
- if let Some(bw_sp) = border_width_spin {
- col.add_widget(&mut *bw_sp, 42.0);
- }
- if let Some(bd_sp) = bevel_depth_spin {
- col.add_widget(&mut *bd_sp, 42.0);
- }
- }
- if let Some(bs_btn) = bevel_shape_btn {
- col.add_widget(&mut *bs_btn, 28.0);
- }
-
- let total_h = col.current_y();
- self.scroll_box.update_bounds(total_h, y, h);
- }
+ self.scroll_box.set_rect(rect.x, rect.y, rect.width, rect.height);
}
}
@@ -433,162 +73,45 @@ impl cce_ui::widget::Paint for ControlPanel {
Some((12.0, (true, true, true, true)))
}
- // ControlPanel is a legacy scroll frame: children are laid out UNSCROLLED and the
- // scroll offset is applied at aggregate time. The walk must emit these aggregates
- // and not descend — descending would paint the children unshifted, desyncing text
- // from geometry as soon as the panel scrolls.
- fn paints_own_subtree(&self) -> bool {
- true
- }
-
+ /// Background + borders, as radius-0 rounded prims so they ride the adapter's
+ /// rounded-tuple bridge exactly where the legacy aggregate emitted them (the app
+ /// emits the children and the scrollbar after these, in the legacy order).
fn paint(&self, _rect: Rect, pc: &mut PaintCtx) {
- // The children are Adapted widgets whose aggregate getters read nothing from the
- // routing context; a fresh one stands in for the ctx `Paint::paint` doesn't carry.
- let dummy = cce_ui::context::UiContext::new();
- for (x, y, qw, qh, r, c, corners) in self.aggregate_rounded(&dummy) {
- pc.rounded_rect(Rect { x, y, width: qw, height: qh }, r, corners, c);
- }
- for (x, y, qw, qh, c) in self.aggregate_plain() {
- pc.quad(Rect { x, y, width: qw, height: qh }, c);
- }
- for (cx, cy, r, t, s, e, c) in self.aggregate_arcs() {
- pc.arc(cx, cy, r, t, s, e, c);
- }
- for (tl, font, bounds) in self.scrolled_child_labels(&dummy) {
- pc.text_with(tl.text, tl.x, tl.y, tl.font_size, tl.color, font, bounds);
- }
- }
-
- fn popover(&self, _rect: Rect) -> Option<(f32, f32, f32, f32)> {
- self.popover_scan()
- }
-
- fn draw_popover(&self, _rect: Rect, pc: &mut dyn cce_ui::layout::RenderTarget) {
- let scroll_y = self.scroll_box.scroll_y;
- unsafe {
- for child_ptr in &self.children {
- if (**child_ptr).popover_rect().is_some() {
- let child = &mut **child_ptr;
- let old_y = child.base().y;
- child.base_mut().y = old_y - scroll_y;
- child.render_popover(pc);
- child.base_mut().y = old_y;
- }
- }
- }
+ let (x, y, w, h) = self.rect();
+ let none = (false, false, false, false);
+ pc.rounded_rect(Rect { x, y, width: w, height: h }, 0.0, none, colors::control_panel_color());
+ let border_color = colors::control_panel_border_color();
+ pc.rounded_rect(Rect { x, y, width: w, height: 1.0 }, 0.0, none, border_color);
+ pc.rounded_rect(Rect { x, y: y + h - 1.0, width: w, height: 1.0 }, 0.0, none, border_color);
+ pc.rounded_rect(Rect { x, y, width: 1.0, height: h }, 0.0, none, border_color);
+ pc.rounded_rect(Rect { x: x + w - 1.0, y, width: 1.0, height: h }, 0.0, none, border_color);
}
}
impl cce_ui::widget::Input for ControlPanel {
- /// The legacy hit shape: an open child popover (scrolled coordinates) or the panel rect.
- fn hit(&self, rect: Rect, x: f32, y: f32) -> bool {
- if let Some(pop_rect) = self.popover_scan() {
- if x >= pop_rect.0 && x <= pop_rect.0 + pop_rect.2 && y >= pop_rect.1 && y <= pop_rect.1 + pop_rect.3 {
- return true;
- }
- }
- if rect.width <= 0.0 || rect.height <= 0.0 {
- return false;
- }
- x >= rect.x && x <= rect.x + rect.width && y >= rect.y && y <= rect.y + rect.height
- }
-
- // The legacy direct-dispatch broadcast reached mouse_input ungated: the panel resets
- // its drag target on every press and lets children with open popovers see off-panel
- // presses.
- fn gates_presses(&self) -> bool {
- false
- }
-
fn on_event(&mut self, event: &Event, ectx: &mut EventCtx) -> bool {
let Some(ctx) = ectx.ui.as_deref_mut() else { return false };
match event {
Event::MouseButton { button, state, x: px, y: py, .. } => {
- if *state == ElementState::Pressed {
- self.active_drag_widget = None;
- }
-
- if self.scroll_box.mouse_input(*button, *state, *px, *py, ctx) {
- return true;
- }
-
- let scroll_y = self.scroll_box.scroll_y;
- let py_translated = *py + scroll_y;
- let (_x, y, _w, h) = self.rect();
-
- unsafe {
- for child_ptr in &self.children {
- let has_popover = (**child_ptr).popover_rect().is_some();
- // Only dispatch if the click Y is inside the viewport or the child has an active popover
- if has_popover || (*py >= y && *py <= y + h) {
- let ev = cce_ui::widget::Event::MouseButton { button: *button, state: *state, x: *px, y: py_translated, local_x: *px, local_y: py_translated };
- if (**child_ptr).handle_event(&ev, ctx) {
- if *state == ElementState::Pressed && (**child_ptr).draggable() {
- self.active_drag_widget = Some(*child_ptr);
- }
- return true;
- }
- }
- }
- }
- false
+ self.scroll_box.mouse_input(*button, *state, *px, *py, ctx)
}
Event::PointerMove { x: px, y: py, .. } => {
- let mut changed = self.scroll_box.cursor_moved(*px, *py, ctx);
-
- let scroll_y = self.scroll_box.scroll_y;
- let py_translated = *py + scroll_y;
- unsafe {
- for child_ptr in &self.children {
- let ev = cce_ui::widget::Event::PointerMove { x: *px, y: py_translated, local_x: *px, local_y: py_translated };
- if (**child_ptr).handle_event(&ev, ctx) {
- changed = true;
- }
- }
- }
- changed
+ self.scroll_box.cursor_moved(*px, *py, ctx)
}
Event::MouseWheel { delta, x: px, y: py, .. } => {
- if self.scroll_box.mouse_wheel(delta, *px, *py, ctx) {
- return true;
- }
-
- let scroll_y = self.scroll_box.scroll_y;
- let py_translated = *py + scroll_y;
- unsafe {
- for child_ptr in &self.children {
- let ev = cce_ui::widget::Event::MouseWheel { delta: *delta, x: *px, y: py_translated, local_x: *px, local_y: py_translated };
- if (**child_ptr).handle_event(&ev, ctx) {
- return true;
- }
- }
- }
- false
+ self.scroll_box.mouse_wheel(delta, *px, *py, ctx)
}
_ => false,
}
}
fn draggable(&self, _rect: Rect) -> bool {
- self.scroll_box.draggable() || self.active_drag_widget.is_some()
+ self.scroll_box.draggable()
}
fn drag_begin(&mut self, px: f32, py: f32, _rect: Rect) {
if self.scroll_box.hit_test_scrollbar(px, py) {
self.scroll_box.drag_begin(px, py);
- return;
- }
-
- let scroll_y = self.scroll_box.scroll_y;
- let py_translated = py + scroll_y;
- if let Some(child_ptr) = self.active_drag_widget {
- // Dyn child: the Drag* events map onto the Input drag hooks in handle_event
- // (6bd collapse); the ctx is unused on that path.
- let mut dummy = cce_ui::context::UiContext::new();
- let ev = cce_ui::widget::Event::DragStart { start_x: px, start_y: py_translated };
- unsafe {
- (*child_ptr).handle_event(&ev, &mut dummy);
- }
}
}
@@ -596,41 +119,16 @@ impl cce_ui::widget::Input for ControlPanel {
if self.scroll_box.draggable() {
return self.scroll_box.drag_update(px, py);
}
-
- let scroll_y = self.scroll_box.scroll_y;
- let py_translated = py + scroll_y;
- if let Some(child_ptr) = self.active_drag_widget {
- let mut dummy = cce_ui::context::UiContext::new();
- let ev = cce_ui::widget::Event::DragUpdate { dx: 0.0, dy: 0.0, x: px, y: py_translated, local_x: px, local_y: py_translated };
- unsafe {
- return (*child_ptr).handle_event(&ev, &mut dummy);
- }
- }
false
}
fn drag_end(&mut self) {
self.scroll_box.drag_end();
- if let Some(child_ptr) = self.active_drag_widget {
- let mut dummy = cce_ui::context::UiContext::new();
- unsafe {
- (*child_ptr).handle_event(&cce_ui::widget::Event::DragEnd, &mut dummy);
- }
- self.active_drag_widget = None;
- }
}
fn tick_ctx(&mut self, dt: f32, ectx: &mut EventCtx) -> bool {
let Some(ctx) = ectx.ui.as_deref_mut() else { return false };
- let mut changed = self.scroll_box.tick(dt, ctx);
- unsafe {
- for child_ptr in &self.children {
- if (**child_ptr).tick(dt, ctx) {
- changed = true;
- }
- }
- }
- changed
+ self.scroll_box.tick(dt, ctx)
}
}