GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
refactor(widget)!: ButtonStrip onto the narrow traits — Element has ONE implementor (Phase 6az complete)
The last production raw impl converts to Layout/Paint/Input; the embedders'
fields become Adapted<ButtonStrip> (MenuBar's menus, Paginator's
sidebar_menu). Field and inherent access ride Deref; Element dispatch rides
the wrapper — except two name collisions where the wrapper's Element methods
shadow the model's richer inherent ones (set_selected(bool) vs
Option<usize>, take_click() bool vs Option<usize>): those sites go through
inner_mut(). Paginator's raw-pointer registration becomes as_ptr_mut/id;
gates_presses stays off (a press on another tab must land while a dropdown
popover covers the strip, as the ungated legacy dispatch allowed); the
config-watch label regeneration moves to Input::tick, the rect-change
regeneration to rect_assigned, and the rotated-SVG tab quads + state tints +
labels into one paint.
With this, every widget in the workspace — cce-ui and all apps — reaches the
tree through Adapted<W>: `Element` has exactly one production implementor,
the precondition for retyping the *mut dyn Element machinery.
Verified: full workspace builds; 168 cce-ui tests; cce-layout-interface
(Paginator vertical strip, rotated SVG tabs) static A/B AE=0 and cross-build
AE=0 after an identical tab click that switches panels; cce-email (MenuBar
strip) cross-build AE=0 after an identical menu-open click, initial diff =
the tab hover tint under the parked cursor.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_018u7qTwzX95dd5ysAkaSCLk
src/widget/container/menu.rs | 10 +-
src/widget/container/paginator.rs | 26 ++--
src/widget/input/button_strip.rs | 278 ++++++++++++++++++++------------------
3 files changed, 169 insertions(+), 145 deletions(-)
diff --git a/src/widget/container/menu.rs b/src/widget/container/menu.rs
index 110a71e..4455649 100644
--- a/src/widget/container/menu.rs
+++ b/src/widget/container/menu.rs
@@ -34,7 +34,7 @@ pub struct MenuBar {
pub blur: bool,
pub color: Option<[f32; 4]>,
pub title: String,
- pub menus: ButtonStrip,
+ pub menus: Adapted<ButtonStrip>,
pub menu_items: Vec<String>,
pub vertical_items: Vec<String>,
pub menu_dropdowns: Vec<Vec<String>>,
@@ -70,7 +70,7 @@ impl MenuBar {
blur: false,
color: None,
title: String::new(),
- menus: ButtonStrip::new(x, y, w, h).with_inherit_menubar_font(true),
+ menus: Adapted::new(ButtonStrip::new(x, y, w, h).with_inherit_menubar_font(true)),
menu_items: Vec::new(),
vertical_items: Vec::new(),
menu_dropdowns: Vec::new(),
@@ -329,7 +329,7 @@ impl MenuBar {
self.context_dropdown_open = false;
self.context_hovered_item = None;
self.hovered_dropdown_item = None;
- self.menus.set_selected(None);
+ self.menus.inner_mut().set_selected(None);
}
/// The conditional focus claim of the legacy `focus()`: hold the global focus only while
@@ -735,7 +735,7 @@ impl Input for MenuBar {
fn set_selected(&mut self, selected: bool) {
self.focused = selected;
if !selected {
- self.menus.set_selected(None);
+ self.menus.inner_mut().set_selected(None);
}
}
@@ -1061,7 +1061,7 @@ impl PageSelector for MenuBar {
}
fn set_selected_page(&mut self, page: usize) {
- self.menus.set_selected(Some(page));
+ self.menus.inner_mut().set_selected(Some(page));
}
fn sidebar_w(&self) -> f32 {
diff --git a/src/widget/container/paginator.rs b/src/widget/container/paginator.rs
index 2221f27..d578c36 100644
--- a/src/widget/container/paginator.rs
+++ b/src/widget/container/paginator.rs
@@ -24,7 +24,7 @@ use crate::widget::{
};
pub struct Paginator {
- pub sidebar_menu: ButtonStrip,
+ pub sidebar_menu: Adapted<ButtonStrip>,
pub selected_page: usize,
pub sidebar_w: f32,
pub page_labels: Vec<String>,
@@ -37,7 +37,7 @@ impl Paginator {
let num_pages = pages.len();
let temp_paginator = Paginator {
- sidebar_menu: ButtonStrip::new(0.0, 0.0, 0.0, 0.0),
+ sidebar_menu: Adapted::new(ButtonStrip::new(0.0, 0.0, 0.0, 0.0)),
selected_page: 0,
sidebar_w: 0.0,
page_labels: pages.clone(),
@@ -46,11 +46,13 @@ impl Paginator {
};
let sidebar_w = temp_paginator.sidebar_w();
- let mut sidebar_menu = ButtonStrip::new(0.0, 0.0, sidebar_w, 0.0)
- .with_vertical(true)
- .with_buttons(pages.clone());
+ let mut sidebar_menu = Adapted::new(
+ ButtonStrip::new(0.0, 0.0, sidebar_w, 0.0)
+ .with_vertical(true)
+ .with_buttons(pages.clone()),
+ );
if num_pages > 0 {
- sidebar_menu.set_selected(Some(0));
+ sidebar_menu.inner_mut().set_selected(Some(0));
}
Adapted::new(Paginator {
@@ -103,7 +105,7 @@ impl PageSelector for Paginator {
fn set_selected_page(&mut self, page: usize) {
if page < self.page_labels.len() {
self.selected_page = page;
- self.sidebar_menu.set_selected(Some(page));
+ self.sidebar_menu.inner_mut().set_selected(Some(page));
if let Some(ref cb) = self.on_page_changed_cb {
cb(page);
}
@@ -160,9 +162,9 @@ impl Layout for Paginator {
}
fn register_embedded_children(&mut self, host_id: WidgetId, ctx: &mut UiContext) {
- let menu_ptr = &mut self.sidebar_menu as *mut ButtonStrip;
- ctx.register_widget(self.sidebar_menu.base.id(), menu_ptr);
- ctx.link_ids(host_id, self.sidebar_menu.base.id());
+ let menu_ptr = self.sidebar_menu.as_ptr_mut();
+ ctx.register_widget(self.sidebar_menu.id(), menu_ptr);
+ ctx.link_ids(host_id, self.sidebar_menu.id());
}
}
@@ -235,7 +237,7 @@ impl Input for Paginator {
let mut changed = false;
if self.sidebar_menu.mouse_input(*button, *state, *px, *py, ui) {
changed = true;
- if let Some(idx) = self.sidebar_menu.take_click() {
+ if let Some(idx) = self.sidebar_menu.inner_mut().take_click() {
self.set_selected_page(idx);
self.just_clicked = Some(idx);
}
@@ -330,7 +332,7 @@ mod tests {
// The embedded strip + pages land in the registry on tick (the spatial grid feeds off
// it — the registered strip is what blocks backplate drags over the sidebar).
Element::tick(&mut p, 0.016, &mut ctx);
- let strip_id = p.sidebar_menu.base.id();
+ let strip_id = p.sidebar_menu.id();
assert!(
ctx.tree.iter_registered().any(|(w_id, _)| w_id == strip_id),
"strip registered by the tick-path healing"
diff --git a/src/widget/input/button_strip.rs b/src/widget/input/button_strip.rs
index 9e89a06..567876f 100644
--- a/src/widget/input/button_strip.rs
+++ b/src/widget/input/button_strip.rs
@@ -4,7 +4,10 @@ use crate::widget::input::get_font_db;
#[derive(Debug, Clone)]
pub struct ButtonStrip {
- pub base: Widget,
+ x: f32,
+ y: f32,
+ w: f32,
+ h: f32,
pub buttons: Vec<String>,
pub selected: Option<usize>,
pub vertical: bool,
@@ -22,7 +25,10 @@ pub struct ButtonStrip {
impl ButtonStrip {
pub fn new(x: f32, y: f32, w: f32, h: f32) -> Self {
Self {
- base: Widget::new_rect(x, y, w, h),
+ x,
+ y,
+ w,
+ h,
buttons: Vec::new(),
selected: None,
vertical: false,
@@ -44,6 +50,12 @@ impl ButtonStrip {
self
}
+ /// The laid-out rect, mirrored from the adapter by `Layout::rect_assigned` (or the
+ /// constructor arguments until the first layout).
+ fn rect(&self) -> (f32, f32, f32, f32) {
+ (self.x, self.y, self.w, self.h)
+ }
+
fn current_font(&self) -> String {
if self.inherit_menubar_font {
crate::layout::menubar_font()
@@ -272,99 +284,33 @@ impl ButtonStrip {
}
}
-impl Element for ButtonStrip {
- crate::impl_widget_base!(ButtonStrip);
-
- fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
- if self.base.x != x || self.base.y != y || self.base.w != w || self.base.h != h {
- self.base.x = x;
- self.base.y = y;
- self.base.w = w;
- self.base.h = h;
- self.generate_rotated_labels();
- }
- }
-
- fn tick(&mut self, _dt: f32, _ctx: &mut UiContext) -> bool {
- let mut changed = false;
- let current_padding = crate::layout::button_padding();
- let current_font = self.current_font();
- let current_scale = crate::scale::scale_factor().max(1.0);
- if self.last_padding != Some(current_padding)
- || self.last_font.as_ref() != Some(¤t_font)
- || self.last_scale != Some(current_scale)
- {
+impl crate::widget::Layout for ButtonStrip {
+ // The legacy set_rect override: regenerate the rotated tab labels only when the rect
+ // actually changed.
+ fn rect_assigned(&mut self, rect: crate::scene::layout::Rect) {
+ if self.x != rect.x || self.y != rect.y || self.w != rect.width || self.h != rect.height {
+ self.x = rect.x;
+ self.y = rect.y;
+ self.w = rect.width;
+ self.h = rect.height;
self.generate_rotated_labels();
- changed = true;
}
- changed
- }
-
- fn wants_tick(&self) -> bool {
- true
- }
-
- fn highlight_quad(&self, _ctx: &UiContext) -> Option<(f32, f32, f32, f32, [f32; 4])> {
- None
}
+}
+impl crate::widget::Paint for ButtonStrip {
fn color(&self) -> [f32; 4] {
[0.0, 0.0, 0.0, 0.0]
}
- fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32, _ctx: &mut UiContext) -> bool {
- if button != MouseButton::Left {
- return false;
- }
- let mut changed = false;
- match state {
- ElementState::Pressed => {
- for i in 0..self.buttons.len() {
- let r = self.item_rect(i);
- if px >= r.0 && px < r.0 + r.2 && py >= r.1 && py < r.1 + r.3 {
- self.pressed_idx = Some(i);
- changed = true;
- break;
- }
- }
- }
- ElementState::Released => {
- if let Some(pressed) = self.pressed_idx {
- let r = self.item_rect(pressed);
- if px >= r.0 && px < r.0 + r.2 && py >= r.1 && py < r.1 + r.3 {
- if self.selected != Some(pressed) {
- self.selected = Some(pressed);
- self.just_clicked = Some(pressed);
- self.generate_rotated_labels();
- } else {
- self.selected = None;
- self.just_clicked = Some(pressed);
- self.generate_rotated_labels();
- }
- }
- changed = true;
- }
- self.pressed_idx = None;
- }
- }
- changed
- }
-
- fn cursor_moved(&mut self, px: f32, py: f32, _ctx: &mut UiContext) -> bool {
- let old_hovered = self.hovered_idx;
- self.hovered_idx = None;
- for i in 0..self.buttons.len() {
- let r = self.item_rect(i);
- if px >= r.0 && px < r.0 + r.2 && py >= r.1 && py < r.1 + r.3 {
- self.hovered_idx = Some(i);
- break;
- }
- }
- old_hovered != self.hovered_idx
+ fn widget_font(&self) -> Option<String> {
+ Some(self.current_font())
}
- fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
- let mut quads = Vec::new();
+ fn paint(&self, _rect: crate::scene::layout::Rect, pc: &mut crate::scene::paint::PaintCtx) {
+ use crate::scene::layout::Rect;
+ // The legacy extra_quads body: per-item state backgrounds, plus the rotated
+ // (SVG-rasterized) vertical tab text clamped to the strip.
for i in 0..self.buttons.len() {
let r = self.item_rect(i);
let mut bg_color = [0.0, 0.0, 0.0, 0.0];
@@ -376,13 +322,13 @@ impl Element for ButtonStrip {
bg_color = colors::PANEL_MENU_HOVER;
}
if bg_color != [0.0, 0.0, 0.0, 0.0] {
- quads.push((r.0, r.1, r.2, r.3, bg_color));
+ pc.quad(Rect { x: r.0, y: r.1, width: r.2, height: r.3 }, bg_color);
}
if self.vertical {
if i < self.tab_text_quads.len() {
- let min_y = self.base.y;
- let max_y = self.base.y + self.base.h;
+ let min_y = self.y;
+ let max_y = self.y + self.h;
let page_name = &self.buttons[i];
let trimmed = page_name.trim();
let space_idx = trimmed.find(' ');
@@ -393,70 +339,146 @@ impl Element for ButtonStrip {
for &(qx, qy, qw, qh, qc) in &self.tab_text_quads[i] {
let absolute_x = r.0 + qx;
let absolute_y = r.1 + y_offset + qy - 15.0;
-
+
let ry1 = absolute_y.max(min_y);
let ry2 = (absolute_y + qh).min(max_y);
let rh = ry2 - ry1;
if rh > 0.0 {
- quads.push((absolute_x, ry1, qw, rh, qc));
+ pc.quad(Rect { x: absolute_x, y: ry1, width: qw, height: rh }, qc);
}
}
}
}
}
- quads
+
+ // Own labels (horizontal button text / vertical icon glyphs).
+ for tl in self.own_labels() {
+ pc.text(tl.text, tl.x, tl.y, tl.font_size, tl.color);
+ }
}
+}
- // Leaf legacy widget: own fonted labels via paint_self (the trait text getters
- // are deleted).
- fn paint_self(&self, ui: &UiContext, ctx: &mut crate::scene::paint::PaintCtx) {
- crate::scene::painter::paint_legacy_leaf(
- self, ui, ctx,
- crate::scene::painter::fonted_leaf_labels(self, ui, self.own_labels()),
- );
+impl crate::widget::Input for ButtonStrip {
+ // The legacy dispatch reached mouse_input ungated (the hosts call it directly, and a
+ // press on another tab must land while a dropdown popover covers the strip); the item
+ // scan below is the real gate.
+ fn gates_presses(&self) -> bool {
+ false
}
- fn keyboard_input(&mut self, event: &KeyEvent, _ctx: &mut UiContext) -> bool {
- if event.state != ElementState::Pressed { return false; }
- if self.buttons.is_empty() { return false; }
+ fn wants_tick(&self) -> bool {
+ true
+ }
- let current = self.selected.unwrap_or(0);
- let next;
+ // Config watch: regenerate the rotated labels when padding/font/scale change.
+ fn tick(&mut self, _dt: f32, _rect: crate::scene::layout::Rect) -> bool {
+ let mut changed = false;
+ let current_padding = crate::layout::button_padding();
+ let current_font = self.current_font();
+ let current_scale = crate::scale::scale_factor().max(1.0);
+ if self.last_padding != Some(current_padding)
+ || self.last_font.as_ref() != Some(¤t_font)
+ || self.last_scale != Some(current_scale)
+ {
+ self.generate_rotated_labels();
+ changed = true;
+ }
+ changed
+ }
- match event.logical_key {
- Key::Named(NamedKey::ArrowLeft) | Key::Named(NamedKey::ArrowUp) => {
- if current > 0 {
- next = current - 1;
- } else {
- next = self.buttons.len() - 1;
+ fn on_event(&mut self, event: &Event, _ectx: &mut crate::widget::EventCtx) -> bool {
+ match event {
+ Event::MouseButton { button, state, x: px, y: py, .. } => {
+ if *button != MouseButton::Left {
+ return false;
}
+ let mut changed = false;
+ match state {
+ ElementState::Pressed => {
+ for i in 0..self.buttons.len() {
+ let r = self.item_rect(i);
+ if *px >= r.0 && *px < r.0 + r.2 && *py >= r.1 && *py < r.1 + r.3 {
+ self.pressed_idx = Some(i);
+ changed = true;
+ break;
+ }
+ }
+ }
+ ElementState::Released => {
+ if let Some(pressed) = self.pressed_idx {
+ let r = self.item_rect(pressed);
+ if *px >= r.0 && *px < r.0 + r.2 && *py >= r.1 && *py < r.1 + r.3 {
+ if self.selected != Some(pressed) {
+ self.selected = Some(pressed);
+ self.just_clicked = Some(pressed);
+ self.generate_rotated_labels();
+ } else {
+ self.selected = None;
+ self.just_clicked = Some(pressed);
+ self.generate_rotated_labels();
+ }
+ }
+ changed = true;
+ }
+ self.pressed_idx = None;
+ }
+ }
+ changed
}
- Key::Named(NamedKey::ArrowRight) | Key::Named(NamedKey::ArrowDown) => {
- if current + 1 < self.buttons.len() {
- next = current + 1;
- } else {
- next = 0;
+ Event::PointerMove { x: px, y: py, .. } => {
+ let old_hovered = self.hovered_idx;
+ self.hovered_idx = None;
+ for i in 0..self.buttons.len() {
+ let r = self.item_rect(i);
+ if *px >= r.0 && *px < r.0 + r.2 && *py >= r.1 && *py < r.1 + r.3 {
+ self.hovered_idx = Some(i);
+ break;
+ }
}
+ old_hovered != self.hovered_idx
}
- _ => return false,
- }
+ Event::KeyInput(event) => {
+ if event.state != ElementState::Pressed {
+ return false;
+ }
+ if self.buttons.is_empty() {
+ return false;
+ }
- if Some(next) != self.selected {
- self.selected = Some(next);
- self.just_clicked = Some(next);
- self.generate_rotated_labels();
- return true;
- }
- false
- }
+ let current = self.selected.unwrap_or(0);
+ let next;
- fn widget_font(&self) -> Option<String> {
- Some(self.current_font())
+ match event.logical_key {
+ Key::Named(NamedKey::ArrowLeft) | Key::Named(NamedKey::ArrowUp) => {
+ if current > 0 {
+ next = current - 1;
+ } else {
+ next = self.buttons.len() - 1;
+ }
+ }
+ Key::Named(NamedKey::ArrowRight) | Key::Named(NamedKey::ArrowDown) => {
+ if current + 1 < self.buttons.len() {
+ next = current + 1;
+ } else {
+ next = 0;
+ }
+ }
+ _ => return false,
+ }
+
+ if Some(next) != self.selected {
+ self.selected = Some(next);
+ self.just_clicked = Some(next);
+ self.generate_rotated_labels();
+ return true;
+ }
+ false
+ }
+ _ => false,
+ }
}
}
-impl Control for ButtonStrip {}
-
impl ButtonStrip {
pub(crate) fn own_labels(&self) -> Vec<TextLabel> {
let mut labels = Vec::new();