GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
feat(widget): migrate Checkbox + Toggle, the first interactive widgets (Phase 5e)
Both implement only Layout/Paint/Input and track hovered/focused from
the forwarded MouseEnter/MouseLeave/FocusIn/FocusOut events — the state
that becomes Animated<f32> in RFC 3.6.
Adapter machinery added: the legacy polling/value surface on Input
(take_click/take_change/value_string/set_value_string/value);
Input::opens_context_menu (adapter routes hit right-presses to
UiContext::handle_right_click); Layout::inline_label (no set_rect
inflation for widgets that draw their label inside their rect);
Paint::{solid_border, widget_font, sync_label}; prim-derived
text_labels for inline-label widgets with a base-label fallback for
detached ones; as_any exposing the inner widget so legacy concrete
downcasts keep working; Drop clearing global focus/context-menu refs;
Debug/Clone; and an inherent Adapted::set_label shadowing
Control::set_label, which writes only the base label and left
self-painted labels stale (caught by test).
Verified: cce-test-interface pixel-diffs 0 vs the pre-migration
baseline, and a wlrctl-injected click on the live compositor flipped
the Toggle's bordered half on-screen — the full input path through the
adapter (hit-gate -> on_event -> state -> redraw) exercised for real.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01LkfJazPs9bRchkcozmxXCX
docs/rfc-core-rebuild.md | 23 ++
src/widget/container/parameters_bg.rs | 2 +-
src/widget/input/checkbox.rs | 756 +++++++++++++++++++---------------
src/widget/input/multi_control.rs | 2 +-
src/widget/json_layout.rs | 6 +-
src/widget/model.rs | 213 +++++++++-
6 files changed, 643 insertions(+), 359 deletions(-)
diff --git a/docs/rfc-core-rebuild.md b/docs/rfc-core-rebuild.md
index 477887b..b2c66bf 100644
--- a/docs/rfc-core-rebuild.md
+++ b/docs/rfc-core-rebuild.md
@@ -401,6 +401,29 @@ Constraint respected: **each crate still builds standalone** — the new core is
in exact status colors). UsageBar verified byte-identical in the same dump (bg+fill rects at
the exact `with_colors` colors). 147 tests pass; status-interface, system-settings, and
test-interface all build.
+ - **5e — First interactive widgets: `Checkbox` + `Toggle`. DONE (verified end-to-end with an
+ injected live click).** New machinery: the legacy polling/value surface on `Input`
+ (`take_click`/`take_change`/`value_string`/`set_value_string`/`value` — kept there to avoid
+ a fourth bound; dies with RFC §3.5 typed messages); `Input::opens_context_menu` (the adapter
+ routes a hit right-press to `UiContext::handle_right_click`, which ctx-less `on_event`
+ can't); `Layout::inline_label` (Checkbox/Toggle draw the label inside their rect — no
+ `set_rect` inflation/content inset, matching the legacy `label_offset` type-name special
+ cases); `Paint::{solid_border, widget_font, sync_label}` (transitional forwards);
+ prim-derived `text_labels` for inline-label widgets (one paint source feeds every text
+ path) with a base-label fallback replica for detached ones; `as_any` now exposes the *inner*
+ widget so legacy `downcast_mut::<Checkbox>()` sites keep working; `Drop` on `Adapted`
+ clears the global focus/context-menu refs (bounds moved onto the struct for this);
+ `Debug`/`Clone` derives; and an inherent `Adapted::set_label` that shadows
+ `Control::set_label` (which writes only the base and left self-painted labels stale —
+ caught by a test; `Control` impls override to route here). Both widgets track
+ `hovered`/`focused` from the forwarded `MouseEnter`/`MouseLeave`/`FocusIn`/`FocusOut`
+ events — the state that becomes `Animated<f32>` in §3.6. In-crate consumers updated
+ (`json_layout` direct Element calls, `multi_control` enum variant, `parameters_bg` field);
+ app repos updated (system-settings network+notifications, data-editor, layout-interface
+ field types — construction sites unchanged). **Verification:** cce-test-interface pixel-
+ diffed 0 against the pre-migration baseline, and a `wlrctl`-injected click on the live
+ compositor flipped the Toggle's bordered half on-screen — the full input path through the
+ adapter exercised for real. 152 tests pass.
- **Still to do:** migrate remaining widgets
off `impl Element` onto the narrow traits (per-widget, Phase 6 flavour); replace the 8 live
`as_*_controller` downcast pairs (called by `cce-designer`, `cce-test-interface`, and ~10
diff --git a/src/widget/container/parameters_bg.rs b/src/widget/container/parameters_bg.rs
index 6b72469..ea8b810 100644
--- a/src/widget/container/parameters_bg.rs
+++ b/src/widget/container/parameters_bg.rs
@@ -16,7 +16,7 @@ pub struct ParametersBg {
pub buttons: Vec<Option<Button>>,
pub choices: Vec<Option<Dropdown>>,
pub texts: Vec<Option<TextBox>>,
- pub checkboxes: Vec<Option<Checkbox>>,
+ pub checkboxes: Vec<Option<crate::widget::Adapted<Checkbox>>>,
pub colors: Vec<Option<ColorSelector>>,
visible: bool,
pub children: Vec<*mut (dyn Element + 'static)>,
diff --git a/src/widget/input/checkbox.rs b/src/widget/input/checkbox.rs
index 285d5b7..3c8cc3c 100644
--- a/src/widget/input/checkbox.rs
+++ b/src/widget/input/checkbox.rs
@@ -1,32 +1,46 @@
+//! Narrow-trait `Checkbox` and `Toggle` (Phase 5e — first interactive widgets off `Element`).
+//!
+//! Both are inline-label widgets: they paint their own label (with hover/focus-dependent color)
+//! inside their rect, so they track `hovered`/`focused` themselves from the `MouseEnter`/
+//! `MouseLeave`/`FocusIn`/`FocusOut` events the adapter forwards — the migration shape for the
+//! state that becomes `Animated<f32>` in RFC §3.6.
+//!
+//! Geometry parity: `paint` emits the same conditional geometry as the legacy `extra_quads` /
+//! `extra_arcs` / `all_rounded_quads` overrides did, in the matching prim kinds, so the adapter's
+//! per-prim reverse bridges reproduce the legacy getters byte-for-byte.
+
use crate::colors;
-use crate::widget::*;
+use crate::scene::layout::Rect;
+use crate::scene::paint::PaintCtx;
+use crate::widget::{Adapted, Control, ElementState, Event, Input, Layout, MouseButton, Paint};
+
+fn parse_bool(val: &str) -> Option<bool> {
+ match val.trim().to_lowercase().as_str() {
+ "true" | "1" | "yes" | "on" => Some(true),
+ "false" | "0" | "no" | "off" => Some(false),
+ _ => None,
+ }
+}
pub struct Checkbox {
- base: Widget,
checked: bool,
just_clicked: bool,
pub just_changed: bool,
+ label: Option<String>,
+ hovered: bool,
+ focused: bool,
}
impl Checkbox {
- pub fn new() -> Self {
- Self {
- base: Widget::new(),
+ pub fn new() -> Adapted<Checkbox> {
+ Adapted::new(Checkbox {
checked: false,
just_clicked: false,
just_changed: false,
- }
- }
-
- pub fn with_label(mut self, label: &str) -> Self {
- self.base.label = Some(label.to_string());
- self
- }
-
- pub fn with_config(mut self, file: &str, key: &str) -> Self {
- self.base.config_file = Some(file.to_string());
- self.base.config_key = Some(key.to_string());
- self
+ label: None,
+ hovered: false,
+ focused: false,
+ })
}
pub fn set_checked(&mut self, checked: bool) {
@@ -36,180 +50,192 @@ impl Checkbox {
pub fn checked(&self) -> bool {
self.checked
}
-}
-impl Element for Checkbox {
- crate::impl_widget_base!(Checkbox);
+ /// Hover state, also settable directly for immediate-mode hosts that do their own
+ /// hit-testing instead of routing `MouseEnter`/`MouseLeave` (json_layout).
+ pub fn hovered(&self) -> bool {
+ self.hovered
+ }
- fn get_value_string(&self) -> Option<String> {
- Some(self.checked.to_string())
+ pub fn set_hovered(&mut self, hovered: bool) {
+ self.hovered = hovered;
}
- fn set_value_string(&mut self, val: &str) -> bool {
- let val_trimmed = val.trim().to_lowercase();
- let new_checked = if val_trimmed == "true" || val_trimmed == "1" || val_trimmed == "yes" || val_trimmed == "on" {
- true
- } else if val_trimmed == "false" || val_trimmed == "0" || val_trimmed == "no" || val_trimmed == "off" {
- false
+ fn box_color(&self) -> [f32; 4] {
+ if self.checked {
+ colors::checkbox_checked()
+ } else if self.hovered {
+ colors::checkbox_hover()
} else {
- return false;
- };
- if self.checked != new_checked {
- self.checked = new_checked;
- self.just_changed = true;
- return true;
+ colors::checkbox_bg()
}
- false
}
+}
- fn take_change(&mut self) -> bool {
- let ret = self.just_changed;
- self.just_changed = false;
- ret
+impl Layout for Checkbox {
+ fn inline_label(&self) -> bool {
+ true
}
+}
+impl Paint for Checkbox {
fn color(&self) -> [f32; 4] {
- let (_, _, w, _) = self.rect();
- if w > 30.0 {
- // Wide mode (with label) -> transparent widget background
+ // Legacy mode split: wide (labeled row) has a transparent widget background; standalone
+ // is the colored box itself. Style-property paths (demo `widget_vertices`) read this.
+ // Width isn't known here, so mirror the legacy intent via the label: labeled checkboxes
+ // are the wide rows.
+ if self.label.is_some() {
[0.0, 0.0, 0.0, 0.0]
} else {
- // Standalone mode -> colored widget background
- if self.checked {
- colors::checkbox_checked()
- } else if self.base.hovered {
- colors::checkbox_hover()
- } else {
- colors::checkbox_bg()
- }
+ self.box_color()
}
}
- fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32, ctx: &mut UiContext) -> bool {
- if button == MouseButton::Right && state == ElementState::Pressed {
- if self.hit_test(px, py, ctx) {
- ctx.handle_right_click(self.as_ptr_mut(), px, py);
- return true;
- }
- }
- if button != MouseButton::Left { return false; }
- match state {
- ElementState::Pressed => {
- if self.hit_test(px, py, ctx) {
- self.checked = !self.checked;
- self.just_clicked = true;
- self.just_changed = true;
- return true;
- }
- }
- _ => {}
- }
- false
+ fn sync_label(&mut self, label: &str) {
+ self.label = Some(label.to_string());
}
- fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
- let mut quads = Vec::new();
- let (x, y, w, h) = self.rect();
+ fn paint(&self, rect: Rect, ctx: &mut PaintCtx) {
+ let (x, y, w, h) = (rect.x, rect.y, rect.width, rect.height);
if w > 30.0 {
- // Draw box on the right
+ // Wide mode: label on the left, 18px box on the right — the legacy `extra_quads`
+ // geometry verbatim.
let box_size = 18.0f32;
let box_x = x + w - box_size - 8.0;
let box_y = y + (h - box_size) / 2.0;
- // Box background
- let bg_color = if self.checked {
- colors::checkbox_checked()
- } else if self.base.hovered {
- colors::checkbox_hover()
- } else {
- colors::checkbox_bg()
- };
- quads.push((box_x, box_y, box_size, box_size, bg_color));
+ ctx.quad(Rect { x: box_x, y: box_y, width: box_size, height: box_size }, self.box_color());
- // Box border
- let border_color = if self.base.hovered {
+ let border_color = if self.hovered {
[0.35, 0.35, 0.40, 1.0]
} else {
[0.25, 0.25, 0.30, 1.0]
};
- quads.push((box_x, box_y, box_size, 1.0, border_color));
- quads.push((box_x, box_y + box_size - 1.0, box_size, 1.0, border_color));
- quads.push((box_x, box_y, 1.0, box_size, border_color));
- quads.push((box_x + box_size - 1.0, box_y, 1.0, box_size, border_color));
+ ctx.quad(Rect { x: box_x, y: box_y, width: box_size, height: 1.0 }, border_color);
+ ctx.quad(Rect { x: box_x, y: box_y + box_size - 1.0, width: box_size, height: 1.0 }, border_color);
+ ctx.quad(Rect { x: box_x, y: box_y, width: 1.0, height: box_size }, border_color);
+ ctx.quad(Rect { x: box_x + box_size - 1.0, y: box_y, width: 1.0, height: box_size }, border_color);
- // Checked indicator
if self.checked {
let pad = 5.0f32;
- quads.push((box_x + pad, box_y + pad, box_size - 2.0 * pad, box_size - 2.0 * pad, [1.0, 1.0, 1.0, 0.9]));
+ ctx.quad(
+ Rect { x: box_x + pad, y: box_y + pad, width: box_size - 2.0 * pad, height: box_size - 2.0 * pad },
+ [1.0, 1.0, 1.0, 0.9],
+ );
}
} else {
- // Standalone mode -> centered checkmark inside the widget
+ // Standalone mode: the widget IS the box. The colored background was legacy
+ // `color()`; emitting it here puts it on every render path (the legacy
+ // `render_widget` path never drew it — same latent-invisibility class as StatusDot).
+ ctx.quad(rect, self.box_color());
if self.checked {
let pad_x = w * 0.25;
let pad_y = h * 0.25;
- quads.push((x + pad_x, y + pad_y, w - 2.0 * pad_x, h - 2.0 * pad_y, [1.0, 1.0, 1.0, 0.9]));
+ ctx.quad(
+ Rect { x: x + pad_x, y: y + pad_y, width: w - 2.0 * pad_x, height: h - 2.0 * pad_y },
+ [1.0, 1.0, 1.0, 0.9],
+ );
}
}
- quads
- }
- fn text_labels(&self) -> Vec<TextLabel> {
- let mut labels = Vec::new();
- if let Some(ref label) = self.base.label {
+ if let Some(ref label) = self.label {
let (_, font_size) = crate::layout::control_label_font_parsed();
- let y = crate::layout::align_text_y(self.base.y, self.base.h, font_size, 0.0);
- labels.push(TextLabel {
- text: label.clone(),
- x: self.base.x + 8.0,
- y,
+ let ty = crate::layout::align_text_y(y, h, font_size, 0.0);
+ ctx.text(
+ label.clone(),
+ x + 8.0,
+ ty,
font_size,
- color: colors::control_label_color_for_state(self.base.hovered, self.base.focused),
- });
+ colors::control_label_color_for_state(self.hovered, self.focused),
+ );
+ }
+ }
+}
+
+impl Input for Checkbox {
+ fn on_event(&mut self, event: &Event, _rect: Rect) -> bool {
+ match event {
+ Event::MouseButton { button: MouseButton::Left, state: ElementState::Pressed, .. } => {
+ // Already hit-gated by the adapter.
+ self.checked = !self.checked;
+ self.just_clicked = true;
+ self.just_changed = true;
+ true
+ }
+ Event::MouseEnter => {
+ self.hovered = true;
+ false
+ }
+ Event::MouseLeave => {
+ self.hovered = false;
+ false
+ }
+ Event::FocusIn => {
+ self.focused = true;
+ false
+ }
+ Event::FocusOut => {
+ self.focused = false;
+ false
+ }
+ _ => false,
}
- labels
+ }
+
+ fn opens_context_menu(&self) -> bool {
+ true
}
fn take_click(&mut self) -> bool {
- if self.just_clicked { self.just_clicked = false; true } else { false }
+ std::mem::take(&mut self.just_clicked)
}
- fn value(&self) -> i32 { if self.checked { 1 } else { 0 } }
-}
-impl Drop for Checkbox {
- fn drop(&mut self) {
- clear_widget_references(self);
+ fn take_change(&mut self) -> bool {
+ std::mem::take(&mut self.just_changed)
+ }
+
+ fn value_string(&self) -> Option<String> {
+ Some(self.checked.to_string())
+ }
+
+ fn set_value_string(&mut self, val: &str) -> bool {
+ let Some(new_checked) = parse_bool(val) else { return false };
+ if self.checked != new_checked {
+ self.checked = new_checked;
+ self.just_changed = true;
+ true
+ } else {
+ false
+ }
+ }
+
+ fn value(&self) -> i32 {
+ if self.checked { 1 } else { 0 }
}
}
#[derive(Debug, Clone)]
pub struct Toggle {
- base: Widget,
toggled: bool,
just_toggled: bool,
+ label: Option<String>,
+ hovered: bool,
+ focused: bool,
}
impl Toggle {
- pub fn new() -> Self {
- Self {
- base: Widget::new(),
+ pub fn new() -> Adapted<Toggle> {
+ Adapted::new(Toggle {
toggled: false,
just_toggled: false,
- }
- }
-
- pub fn with_label(mut self, label: &str) -> Self {
- self.base.label = Some(label.to_string());
- self
- }
-
- pub fn with_config(mut self, file: &str, key: &str) -> Self {
- self.base.config_file = Some(file.to_string());
- self.base.config_key = Some(key.to_string());
- self
+ label: None,
+ hovered: false,
+ focused: false,
+ })
}
pub fn set_label(&mut self, label: &str) {
- self.base.label = Some(label.to_string());
+ self.label = Some(label.to_string());
}
pub fn set_toggled(&mut self, v: bool) {
@@ -219,55 +245,45 @@ impl Toggle {
pub fn toggled(&self) -> bool {
self.toggled
}
-}
-
-impl Element for Toggle {
- crate::impl_widget_base!(Toggle);
-
- fn preferred_height(&self) -> Option<f32> {
- Some(crate::layout::toggle_height())
- }
-
- fn get_value_string(&self) -> Option<String> {
- Some(self.toggled.to_string())
- }
- fn set_value_string(&mut self, val: &str) -> bool {
- let val_trimmed = val.trim().to_lowercase();
- let new_toggled = if val_trimmed == "true" || val_trimmed == "1" || val_trimmed == "yes" || val_trimmed == "on" {
- true
- } else if val_trimmed == "false" || val_trimmed == "0" || val_trimmed == "no" || val_trimmed == "off" {
- false
+ fn border_color(&self) -> [f32; 4] {
+ if self.toggled {
+ colors::toggle_on_color()
} else {
- return false;
- };
- if self.toggled != new_toggled {
- self.toggled = new_toggled;
- self.just_toggled = true;
- return true;
+ colors::toggle_off_color()
}
- false
}
+}
- fn take_change(&mut self) -> bool {
- let ret = self.just_toggled;
- self.just_toggled = false;
- ret
+impl Layout for Toggle {
+ fn inline_label(&self) -> bool {
+ true
}
+ fn intrinsic_size(&self) -> Option<crate::scene::layout::Size> {
+ // Legacy `preferred_height`; width comes from the container.
+ Some(crate::scene::layout::Size::new(0.0, crate::layout::toggle_height()))
+ }
+}
+
+impl Paint for Toggle {
fn color(&self) -> [f32; 4] {
colors::toggle_bg_color()
}
- fn solid_border(&self) -> Option<([f32; 4], f32)> {
- let border_color = if self.toggled {
- colors::toggle_on_color()
+ fn corner_style(&self) -> Option<(f32, (bool, bool, bool, bool))> {
+ let r = crate::layout::toggle_corner_radius();
+ if r > 0.0 {
+ Some((r, (true, true, true, true)))
} else {
- colors::toggle_off_color()
- };
+ None
+ }
+ }
+
+ fn solid_border(&self) -> Option<([f32; 4], f32)> {
let border_w = crate::layout::toggle_border_width();
if border_w > 0.0 {
- Some((border_color, border_w))
+ Some((self.border_color(), border_w))
} else {
None
}
@@ -277,208 +293,278 @@ impl Element for Toggle {
Some(crate::layout::control_label_font())
}
- fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32, ctx: &mut UiContext) -> bool {
- if button != MouseButton::Left { return false; }
- match state {
- ElementState::Pressed => {
- if self.hit_test(px, py, ctx) {
- self.toggled = !self.toggled;
- self.just_toggled = true;
- return true;
+ fn sync_label(&mut self, label: &str) {
+ self.label = Some(label.to_string());
+ }
+
+ fn paint(&self, rect: Rect, ctx: &mut PaintCtx) {
+ let (x, y, w, h) = (rect.x, rect.y, rect.width, rect.height);
+ let radius = crate::layout::toggle_corner_radius();
+ let border_w = crate::layout::toggle_border_width();
+ let bg = colors::toggle_bg_color();
+ let border_color = self.border_color();
+
+ if radius > 0.0 {
+ // Rounded mode: the legacy `all_rounded_quads` half-split verbatim — the border
+ // hugs the "on" (top) or "off" (bottom) half.
+ if self.toggled {
+ ctx.rounded_rect(Rect { x, y: y + h / 2.0, width: w, height: h / 2.0 }, radius, (false, false, true, true), bg);
+ if border_w > 0.0 {
+ ctx.rounded_rect(Rect { x, y, width: w, height: h / 2.0 }, radius, (true, true, false, false), border_color);
+ let inner_radius = (radius - border_w).max(0.0);
+ ctx.rounded_rect(
+ Rect { x: x + border_w, y: y + border_w, width: w - 2.0 * border_w, height: h / 2.0 - border_w },
+ inner_radius,
+ (true, true, false, false),
+ bg,
+ );
+ }
+ else {
+ ctx.rounded_rect(Rect { x, y, width: w, height: h / 2.0 }, radius, (true, true, false, false), bg);
+ }
+ } else {
+ ctx.rounded_rect(Rect { x, y, width: w, height: h / 2.0 }, radius, (true, true, false, false), bg);
+ if border_w > 0.0 {
+ ctx.rounded_rect(Rect { x, y: y + h / 2.0, width: w, height: h / 2.0 }, radius, (false, false, true, true), border_color);
+ let inner_radius = (radius - border_w).max(0.0);
+ ctx.rounded_rect(
+ Rect { x: x + border_w, y: y + h / 2.0, width: w - 2.0 * border_w, height: h / 2.0 - border_w },
+ inner_radius,
+ (false, false, true, true),
+ bg,
+ );
+ } else {
+ ctx.rounded_rect(Rect { x, y: y + h / 2.0, width: w, height: h / 2.0 }, radius, (false, false, true, true), bg);
}
}
- _ => {}
- }
- false
- }
- fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
- let mut quads = Vec::new();
- if self.corner_radius() <= 0.0 {
- quads.push((self.base.x, self.base.y, self.base.w, self.base.h, self.color()));
-
- let border_w = crate::layout::toggle_border_width();
+ // Corner arcs of the bordered half (legacy `extra_arcs`).
+ if border_w > 0.0 && radius > 0.1 {
+ use std::f32::consts::PI;
+ if self.toggled {
+ ctx.arc(x + radius, y + radius, radius, border_w, PI, 1.5 * PI, border_color);
+ ctx.arc(x + w - radius, y + radius, radius, border_w, 1.5 * PI, 2.0 * PI, border_color);
+ } else {
+ ctx.arc(x + radius, y + h - radius, radius, border_w, 0.5 * PI, PI, border_color);
+ ctx.arc(x + w - radius, y + h - radius, radius, border_w, 0.0, 0.5 * PI, border_color);
+ }
+ }
+ } else {
+ // Square mode: the legacy `extra_quads` geometry verbatim — bg quad plus border
+ // edges on the "on" (top) or "off" (bottom) half.
+ ctx.quad(rect, bg);
if border_w > 0.0 {
- let x = self.base.x;
- let y = self.base.y;
- let w = self.base.w;
- let h = self.base.h;
- let r = crate::layout::toggle_corner_radius();
let t = border_w;
-
- let border_color = if self.toggled {
- colors::toggle_on_color()
- } else {
- colors::toggle_off_color()
- };
-
+ let r = radius; // 0.0 here, kept for formula parity with the legacy code
let edge_h = ((h / 2.0) - r).max(0.0);
-
if self.toggled {
- // Top edge
- quads.push((x + r, y, w - 2.0 * r, t, border_color));
- // Top half of left edge
+ ctx.quad(Rect { x: x + r, y, width: w - 2.0 * r, height: t }, border_color);
if edge_h > 0.0 {
- quads.push((x, y + r, t, edge_h, border_color));
- }
- // Top half of right edge
- if edge_h > 0.0 {
- quads.push((x + w - t, y + r, t, edge_h, border_color));
+ ctx.quad(Rect { x, y: y + r, width: t, height: edge_h }, border_color);
+ ctx.quad(Rect { x: x + w - t, y: y + r, width: t, height: edge_h }, border_color);
}
} else {
- // Bottom edge
- quads.push((x + r, y + h - t, w - 2.0 * r, t, border_color));
- // Bottom half of left edge
+ ctx.quad(Rect { x: x + r, y: y + h - t, width: w - 2.0 * r, height: t }, border_color);
if edge_h > 0.0 {
- quads.push((x, y + h / 2.0, t, edge_h, border_color));
- }
- // Bottom half of right edge
- if edge_h > 0.0 {
- quads.push((x + w - t, y + h / 2.0, t, edge_h, border_color));
+ ctx.quad(Rect { x, y: y + h / 2.0, width: t, height: edge_h }, border_color);
+ ctx.quad(Rect { x: x + w - t, y: y + h / 2.0, width: t, height: edge_h }, border_color);
}
}
}
}
- quads
- }
-
- fn extra_arcs(&self) -> Vec<(f32, f32, f32, f32, f32, f32, [f32; 4])> {
- let mut arcs = Vec::new();
- let border_w = crate::layout::toggle_border_width();
- let r = crate::layout::toggle_corner_radius();
-
- if border_w > 0.0 && r > 0.1 {
- let x = self.base.x;
- let y = self.base.y;
- let w = self.base.w;
- let h = self.base.h;
- let t = border_w;
-
- let border_color = if self.toggled {
- colors::toggle_on_color()
- } else {
- colors::toggle_off_color()
- };
-
- if self.toggled {
- // Top-Left corner arc
- arcs.push((
- x + r, y + r, r, t,
- std::f32::consts::PI, 1.5 * std::f32::consts::PI,
- border_color
- ));
- // Top-Right corner arc
- arcs.push((
- x + w - r, y + r, r, t,
- 1.5 * std::f32::consts::PI, 2.0 * std::f32::consts::PI,
- border_color
- ));
- } else {
- // Bottom-Left corner arc
- arcs.push((
- x + r, y + h - r, r, t,
- 0.5 * std::f32::consts::PI, std::f32::consts::PI,
- border_color
- ));
- // Bottom-Right corner arc
- arcs.push((
- x + w - r, y + h - r, r, t,
- 0.0, 0.5 * std::f32::consts::PI,
- border_color
- ));
- }
- }
- arcs
- }
- fn text_labels(&self) -> Vec<TextLabel> {
- let mut labels = Vec::new();
- if let Some(ref label) = self.base.label {
+ if let Some(ref label) = self.label {
let (font_fam, font_size) = crate::layout::control_label_font_parsed();
let est_w = crate::widget::display::measure_text_width(label, &font_fam, font_size);
- labels.push(TextLabel {
- text: label.clone(),
- x: self.base.x + (self.base.w - est_w) / 2.0,
- y: crate::layout::align_text_y(self.base.y, self.base.h, font_size, 0.0),
+ ctx.text(
+ label.clone(),
+ x + (w - est_w) / 2.0,
+ crate::layout::align_text_y(y, h, font_size, 0.0),
font_size,
- color: colors::control_label_color_for_state(self.base.hovered, self.base.focused),
- });
+ colors::control_label_color_for_state(self.hovered, self.focused),
+ );
+ }
+ }
+}
+
+impl Input for Toggle {
+ fn on_event(&mut self, event: &Event, _rect: Rect) -> bool {
+ match event {
+ Event::MouseButton { button: MouseButton::Left, state: ElementState::Pressed, .. } => {
+ self.toggled = !self.toggled;
+ self.just_toggled = true;
+ true
+ }
+ Event::MouseEnter => {
+ self.hovered = true;
+ false
+ }
+ Event::MouseLeave => {
+ self.hovered = false;
+ false
+ }
+ Event::FocusIn => {
+ self.focused = true;
+ false
+ }
+ Event::FocusOut => {
+ self.focused = false;
+ false
+ }
+ _ => false,
}
- labels
}
fn take_click(&mut self) -> bool {
- if self.just_toggled { self.just_toggled = false; true } else { false }
+ std::mem::take(&mut self.just_toggled)
}
- fn rounded_corners(&self) -> (bool, bool, bool, bool) {
- let r = crate::layout::toggle_corner_radius();
- if r > 0.0 {
- (true, true, true, true)
- } else {
- (false, false, false, false)
- }
+ fn take_change(&mut self) -> bool {
+ std::mem::take(&mut self.just_toggled)
}
- fn corner_radius(&self) -> f32 {
- crate::layout::toggle_corner_radius()
+ fn value_string(&self) -> Option<String> {
+ Some(self.toggled.to_string())
}
- 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();
- }
- let mut quads = Vec::new();
- let (r1, r2, r3, r4) = self.rounded_corners();
- let x = self.base.x;
- let y = self.base.y;
- let w = self.base.w;
- let h = self.base.h;
- let radius = self.corner_radius();
- let bg_color = self.color();
-
- if r1 || r2 || r3 || r4 {
- let border_w = crate::layout::toggle_border_width();
- let border_color = if self.toggled {
- colors::toggle_on_color()
- } else {
- colors::toggle_off_color()
- };
-
- if self.toggled {
- // Bottom half background
- quads.push((x, y + h / 2.0, w, h / 2.0, radius, bg_color, (false, false, true, true)));
-
- if border_w > 0.0 {
- // Top half border
- quads.push((x, y, w, h / 2.0, radius, border_color, (true, true, false, false)));
- // Top half inset background
- let inner_radius = (radius - border_w).max(0.0);
- quads.push((x + border_w, y + border_w, w - 2.0 * border_w, h / 2.0 - border_w, inner_radius, bg_color, (true, true, false, false)));
- } else {
- // Top half background (no border)
- quads.push((x, y, w, h / 2.0, radius, bg_color, (true, true, false, false)));
- }
- } else {
- // Top half background
- quads.push((x, y, w, h / 2.0, radius, bg_color, (true, true, false, false)));
-
- if border_w > 0.0 {
- // Bottom half border
- quads.push((x, y + h / 2.0, w, h / 2.0, radius, border_color, (false, false, true, true)));
- // Bottom half inset background
- let inner_radius = (radius - border_w).max(0.0);
- quads.push((x + border_w, y + h / 2.0, w - 2.0 * border_w, h / 2.0 - border_w, inner_radius, bg_color, (false, false, true, true)));
- } else {
- // Bottom half background (no border)
- quads.push((x, y + h / 2.0, w, h / 2.0, radius, bg_color, (false, false, true, true)));
- }
- }
+ fn set_value_string(&mut self, val: &str) -> bool {
+ let Some(new_toggled) = parse_bool(val) else { return false };
+ if self.toggled != new_toggled {
+ self.toggled = new_toggled;
+ self.just_toggled = true;
+ true
+ } else {
+ false
}
- quads
}
}
+// The `set_label` overrides route the trait entry point (e.g. `dyn Control` callers) to the
+// synced inherent version — `Control`'s default writes only the base label, which would leave
+// these self-painting labels stale.
+impl Control for Adapted<Checkbox> {
+ fn set_label(&mut self, label: &str) {
+ Adapted::set_label(self, label);
+ }
+}
+impl Control for Adapted<Toggle> {
+ fn set_label(&mut self, label: &str) {
+ Adapted::set_label(self, label);
+ }
+}
-impl Control for Checkbox {}
-impl Control for Toggle {}
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::widget::{Element, UiContext};
+
+ fn click_at(x: f32, y: f32) -> Event {
+ Event::MouseButton {
+ button: MouseButton::Left,
+ state: ElementState::Pressed,
+ x,
+ y,
+ local_x: x,
+ local_y: y,
+ }
+ }
+
+ #[test]
+ fn checkbox_click_toggles_and_polls_like_legacy() {
+ let mut ctx = UiContext::new();
+ let mut cb = Checkbox::new();
+ let (id, ptr) = (cb.id(), cb.as_ptr_mut());
+ ctx.register_widget(id, ptr);
+ Element::set_rect(&mut cb, 0.0, 0.0, 20.0, 20.0);
+
+ assert!(ctx.propagate_event(&click_at(10.0, 10.0), ptr), "in-rect click consumed");
+ assert!(cb.checked(), "click checked it");
+ assert!(Element::take_click(&mut cb), "take_click reads once");
+ assert!(!Element::take_click(&mut cb), "...then clears");
+ assert!(Element::take_change(&mut cb));
+
+ assert!(!ctx.propagate_event(&click_at(100.0, 100.0), ptr), "miss is not consumed");
+ assert!(cb.checked(), "miss does not toggle");
+ }
+
+ #[test]
+ fn checkbox_value_string_round_trip() {
+ let mut cb = Checkbox::new();
+ assert_eq!(Element::get_value_string(&cb), Some("false".to_string()));
+ assert!(Element::set_value_string(&mut cb, "on"));
+ assert!(cb.checked());
+ assert_eq!(Element::value(&cb), 1);
+ assert!(!Element::set_value_string(&mut cb, "on"), "unchanged value reports false");
+ assert!(!Element::set_value_string(&mut cb, "junk"), "unparsable reports false");
+ assert!(Element::take_change(&mut cb), "set_value_string marked the change");
+ }
+
+ /// Wide (labeled) mode reproduces the legacy `extra_quads` geometry through the bridge:
+ /// box bg + 4 border edges (+ indicator when checked), and the label text with a
+ /// hover-dependent color.
+ #[test]
+ fn checkbox_wide_mode_bridge_parity() {
+ let ctx = UiContext::new();
+ let mut cb = Checkbox::new().with_label("Enable");
+ Element::set_rect(&mut cb, 0.0, 0.0, 200.0, 24.0);
+
+ let quads = Element::extra_quads(&cb);
+ // Unchecked: box bg + 4 border edges = 5 quads, at the legacy box position.
+ assert_eq!(quads.len(), 5);
+ let (box_x, box_y, box_size) = (200.0 - 18.0 - 8.0, (24.0 - 18.0) / 2.0, 18.0);
+ assert_eq!(quads[0], (box_x, box_y, box_size, box_size, colors::checkbox_bg()));
+
+ // Hover flips the box + border colors (tracked from MouseEnter, not base state).
+ cb.inner_mut().hovered = true;
+ let quads = Element::extra_quads(&cb);
+ assert_eq!(quads[0].4, colors::checkbox_hover());
+
+ // Label text comes through the prim-derived text bridge at the legacy position.
+ let labels = Element::text_labels(&cb);
+ assert_eq!(labels.len(), 1);
+ assert_eq!(labels[0].text, "Enable");
+ assert_eq!(labels[0].x, 8.0);
+
+ // Inline label => no set_rect inflation.
+ assert_eq!(Element::rect(&cb), (0.0, 0.0, 200.0, 24.0));
+ let _ = &ctx;
+ }
+
+ #[test]
+ fn toggle_click_and_borders_switch_halves() {
+ let mut ctx = UiContext::new();
+ let mut t = Toggle::new();
+ let (id, ptr) = (t.id(), t.as_ptr_mut());
+ ctx.register_widget(id, ptr);
+ Element::set_rect(&mut t, 0.0, 0.0, 60.0, 30.0);
+
+ // Geometry parity is config-dependent (rounded vs square toggle); assert the invariant
+ // that holds in both: the bordered half flips with the state.
+ let before: Vec<_> = Element::all_rounded_quads(&t, &ctx);
+ let before_quads = Element::extra_quads(&t);
+
+ assert!(ctx.propagate_event(&click_at(30.0, 15.0), ptr), "toggle consumed the click");
+ assert!(t.toggled());
+ assert!(Element::take_click(&mut t));
+
+ let after: Vec<_> = Element::all_rounded_quads(&t, &ctx);
+ let after_quads = Element::extra_quads(&t);
+ assert!(
+ before != after || before_quads != after_quads,
+ "toggling changes the emitted geometry (border switches halves)",
+ );
+
+ // preferred_height forwards the legacy toggle height.
+ assert_eq!(Element::preferred_height(&t), Some(crate::layout::toggle_height()));
+ }
+
+ #[test]
+ fn toggle_set_label_via_deref_reaches_paint() {
+ let mut t = Toggle::new();
+ Element::set_rect(&mut t, 0.0, 0.0, 60.0, 30.0);
+ t.set_label("ON"); // the network.rs pattern: live label updates through Deref
+ let labels = Element::text_labels(&t);
+ assert_eq!(labels.len(), 1);
+ assert_eq!(labels[0].text, "ON");
+ }
+}
diff --git a/src/widget/input/multi_control.rs b/src/widget/input/multi_control.rs
index 8e29f2e..2d77491 100644
--- a/src/widget/input/multi_control.rs
+++ b/src/widget/input/multi_control.rs
@@ -14,7 +14,7 @@ pub struct InstancedControl {
pub enum InstancedWidget {
TextBox(TextBox),
Spinbox(Spinbox),
- Toggle(Toggle),
+ Toggle(Adapted<Toggle>),
Slider(Slider),
}
diff --git a/src/widget/json_layout.rs b/src/widget/json_layout.rs
index 0f29c55..059a662 100644
--- a/src/widget/json_layout.rs
+++ b/src/widget/json_layout.rs
@@ -256,9 +256,9 @@ impl JsonLayoutWidget {
w_state.w = usable_w;
if w_state.widget_type == "checkbox" {
- if let Some(cb) = w_state.widget.as_any_mut().downcast_mut::<Checkbox>() {
- cb.set_rect(w_state.x, w_state.y + 2.0, 18.0, 18.0);
- }
+ // set_rect is an Element method; call it on the box directly (the Phase 5
+ // Checkbox is an Adapted widget — as_any downcasts reach the model, not Element).
+ w_state.widget.set_rect(w_state.x, w_state.y + 2.0, 18.0, 18.0);
w_state.h = 22.0;
w_state.label_text = Some(TextLabel {
text: w_state.text.clone(),
diff --git a/src/widget/model.rs b/src/widget/model.rs
index 5857bfa..46fd7d3 100644
--- a/src/widget/model.rs
+++ b/src/widget/model.rs
@@ -28,7 +28,7 @@
use crate::scene::layout::{Rect, Size, Style};
use crate::scene::paint::{PaintCtx, Prim};
-use crate::widget::{Element, Event, UiContext, Widget, WidgetId};
+use crate::widget::{Element, Event, TextLabel, UiContext, Widget, WidgetId};
/// Layout inputs for the scene layout engine — the RFC's `Widget` concern, named `Layout` here to
/// avoid the existing [`Widget`] base struct. Mirrors the opt-in `Element::layout_style` /
@@ -51,6 +51,14 @@ pub trait Layout {
fn layout_children(&self) -> Option<Vec<Style>> {
None
}
+
+ /// Whether this widget draws its control label *inline* (inside its own rect, like
+ /// `Checkbox`/`Toggle`/`Button`) rather than detached above it (like `ProgressBar`/`Slider`).
+ /// Inline-label widgets get no `set_rect` height inflation and no content-rect inset —
+ /// mirroring the legacy `label_offset` free function's type-name special cases.
+ fn inline_label(&self) -> bool {
+ false
+ }
}
/// The paint concern — a widget's fill color, its own (non-recursive) geometry emission, and
@@ -86,6 +94,25 @@ pub trait Paint {
fn corner_style(&self) -> Option<(f32, (bool, bool, bool, bool))> {
None
}
+
+ /// Solid border `(color, thickness)` of the widget's background quad. **Transitional**, like
+ /// [`corner_style`](Paint::corner_style): `render_widget` gives a widget's background quad a
+ /// border+inset treatment when this is `Some` — `Toggle`'s square mode depends on it.
+ fn solid_border(&self) -> Option<([f32; 4], f32)> {
+ None
+ }
+
+ /// Font for this widget's text on legacy text paths (`render_widget` reads
+ /// `Element::widget_font`). **Transitional.**
+ fn widget_font(&self) -> Option<String> {
+ None
+ }
+
+ /// Receive the control label set on the wrapper via [`Adapted::with_label`] (and legacy
+ /// `Control::set_label` paths). Widgets that paint their label themselves (inline-label
+ /// widgets) store it here; the default discards it, leaving label drawing to the adapter's
+ /// base-label machinery.
+ fn sync_label(&mut self, _label: &str) {}
}
/// The input concern — hit-testing and event handling against the laid-out rect. Mirrors the
@@ -116,17 +143,65 @@ pub trait Input {
fn blocks_backplate_drag(&self) -> bool {
true
}
+
+ /// Whether a right-click on this widget opens the shared config context menu (the adapter
+ /// then routes it to `UiContext::handle_right_click`, which `on_event` can't reach — it has
+ /// no ctx by design). Default: no.
+ fn opens_context_menu(&self) -> bool {
+ false
+ }
+
+ // --- 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
+ // lands. All default to the inert legacy defaults.
+
+ /// Consume the "was clicked since last asked" flag.
+ fn take_click(&mut self) -> bool {
+ false
+ }
+
+ /// Consume the "value changed since last asked" flag.
+ fn take_change(&mut self) -> bool {
+ false
+ }
+
+ /// The widget's value serialized for the config system.
+ fn value_string(&self) -> Option<String> {
+ None
+ }
+
+ /// Set the widget's value from a config string. Returns whether it parsed and changed.
+ fn set_value_string(&mut self, _val: &str) -> bool {
+ false
+ }
+
+ /// The widget's value as an integer (legacy `Element::value`).
+ fn value(&self) -> i32 {
+ 0
+ }
}
/// Wraps a narrow-trait widget `W` so it lives in the legacy `*mut dyn Element` tree. Carries the
/// [`Widget`] base that `Element`'s rect / id / dirty machinery needs, and forwards the concern
/// methods to `W`. See the module docs for why this bridge exists rather than a supertrait split.
-pub struct Adapted<W> {
+///
+/// The bounds live on the struct (not just the `Element` impl) so `Drop` can clear the global
+/// focus / context-menu references through `&dyn Element` — the same guard legacy widgets with
+/// `Drop` impls (e.g. the old `Checkbox`) carried.
+#[derive(Debug, Clone)]
+pub struct Adapted<W: Layout + Paint + Input + 'static> {
base: Widget,
inner: W,
}
-impl<W> Adapted<W> {
+impl<W: Layout + Paint + Input + 'static> Drop for Adapted<W> {
+ fn drop(&mut self) {
+ crate::widget::clear_widget_references(self);
+ }
+}
+
+impl<W: Layout + Paint + Input + 'static> Adapted<W> {
/// Wrap `inner` with a fresh [`Widget`] base.
pub fn new(inner: W) -> Self {
Adapted { base: Widget::new(), inner }
@@ -147,18 +222,38 @@ impl<W> Adapted<W> {
self.base.id()
}
- /// Attach a detached control label (drawn above the widget by the shared `text_labels`
- /// machinery). Mirrors the `with_label` builders legacy control widgets carry, so
- /// construction sites keep their shape when a widget migrates.
+ /// Attach a control label. Mirrors the `with_label` builders legacy control widgets carry,
+ /// so construction sites keep their shape when a widget migrates. The label is stored on the
+ /// base (legacy machinery: label offsets, context-menu titles) *and* pushed into the widget
+ /// via [`Paint::sync_label`] for widgets that paint it themselves.
pub fn with_label(mut self, label: &str) -> Self {
self.base.label = Some(label.to_string());
+ self.inner.sync_label(label);
+ self
+ }
+
+ /// Bind this widget to a config file/key (right-click context-menu editing). Mirrors the
+ /// legacy `with_config` builders.
+ pub fn with_config(mut self, file: &str, key: &str) -> Self {
+ self.base.config_file = Some(file.to_string());
+ self.base.config_key = Some(key.to_string());
self
}
+ /// Update the control label, keeping the base copy (legacy machinery) and the widget's own
+ /// copy ([`Paint::sync_label`]) in step. Inherent so it shadows `Control::set_label` — which
+ /// writes only the base and would leave a self-painting label stale — at every call site,
+ /// regardless of which traits are in scope.
+ pub fn set_label(&mut self, label: &str) {
+ self.base.label = Some(label.to_string());
+ self.inner.sync_label(label);
+ }
+
/// The rect the wrapped widget paints into: the widget's rect minus the detached-label
- /// region at the top (zero inset when there is no label — `Widget::label_offset`).
+ /// region at the top (zero inset when there is no label, or when the widget draws its label
+ /// inline — `Widget::label_offset` / [`Layout::inline_label`]).
fn content_rect(&self) -> Rect {
- let top = self.base.label_offset();
+ let top = if Layout::inline_label(&self.inner) { 0.0 } else { self.base.label_offset() };
Rect {
x: self.base.x,
y: self.base.y + top,
@@ -168,29 +263,48 @@ impl<W> Adapted<W> {
}
}
-impl<W: Paint> Adapted<W> {
+impl<W: Layout + Paint + Input + 'static> Adapted<W> {
/// Run the wrapped widget's [`Paint::paint`] against its content rect and return the emitted
/// prims — the shared source for the reverse bridges (`extra_quads`, `all_rounded_quads`,
- /// `extra_circles`, `extra_arcs`) that legacy render loops read.
+ /// `extra_circles`, `extra_arcs`, prim-derived `text_labels`) that legacy render loops read.
fn painted_prims(&self) -> Vec<Prim> {
let mut pc = PaintCtx::new();
Paint::paint(&self.inner, self.content_rect(), &mut pc);
pc.finish().items.into_iter().map(|item| item.prim).collect()
}
+
+ /// 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> {
+ let b = &self.base;
+ if let Some(ref label) = b.label {
+ let (_, font_size) = crate::layout::control_label_font_detached_parsed();
+ let color = crate::colors::control_label_color_detached_for_state(b.hovered, b.focused);
+ if crate::layout::control_label_layout() == "side" {
+ let label_x = Element::label_x_offset(self);
+ if label_x > 0.0 {
+ let y_pos = crate::layout::align_text_y(b.y, b.h, font_size, 0.0);
+ return vec![TextLabel { text: label.clone(), x: b.x + 4.0, y: y_pos, font_size, color }];
+ }
+ }
+ return vec![TextLabel { text: label.clone(), x: b.x, y: b.y, font_size, color }];
+ }
+ Vec::new()
+ }
}
/// Auto-deref to the wrapped widget, so call sites keep using a migrated widget's own state and
/// methods directly (`dot.status`, `dot.set_status(..)`) without knowing about the wrapper.
/// (By-value builders can't flow through `Deref` — those get mirrored per-widget, like
/// `with_label` here or `UsageBar::with_colors`.)
-impl<W> std::ops::Deref for Adapted<W> {
+impl<W: Layout + Paint + Input + 'static> std::ops::Deref for Adapted<W> {
type Target = W;
fn deref(&self) -> &W {
&self.inner
}
}
-impl<W> std::ops::DerefMut for Adapted<W> {
+impl<W: Layout + Paint + Input + 'static> std::ops::DerefMut for Adapted<W> {
fn deref_mut(&mut self) -> &mut W {
&mut self.inner
}
@@ -203,11 +317,13 @@ impl<W: Layout + Paint + Input + 'static> Element for Adapted<W> {
fn base_mut(&mut self) -> Option<&mut Widget> {
Some(&mut self.base)
}
+ // `as_any` exposes the *inner* widget: legacy code downcasts by concrete widget type
+ // (`json_layout`'s `downcast_mut::<Checkbox>()`), and the adapter must be transparent to it.
fn as_any(&self) -> &dyn std::any::Any {
- self
+ &self.inner
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
- self
+ &mut self.inner
}
fn as_ptr(&self) -> *mut (dyn Element + 'static) {
self as *const Self as *mut Self as *mut (dyn Element + 'static)
@@ -231,12 +347,14 @@ impl<W: Layout + Paint + Input + 'static> Element for Adapted<W> {
/// The detached-label convention shared by legacy control widgets: the widget grows past the
/// rect its parent assigns to make room for the label above (`ProgressBar`/`Slider`-style
- /// `set_rect` overrides). Zero-cost when no label is set.
+ /// `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 inflation = if Layout::inline_label(&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 + self.base.label_offset();
+ self.base.h = h + inflation;
}
fn preferred_height(&self) -> Option<f32> {
@@ -270,11 +388,38 @@ impl<W: Layout + Paint + Input + 'static> Element for Adapted<W> {
fn rounded_corners(&self) -> (bool, bool, bool, bool) {
Paint::corner_style(&self.inner).map_or((false, false, false, false), |(_, c)| c)
}
+ fn solid_border(&self) -> Option<([f32; 4], f32)> {
+ Paint::solid_border(&self.inner)
+ }
+ fn widget_font(&self) -> Option<String> {
+ Paint::widget_font(&self.inner)
+ }
fn paint_self(&self, _ui: &UiContext, ctx: &mut PaintCtx) {
Paint::paint(&self.inner, self.content_rect(), ctx);
- // The detached label, exactly as the legacy default `paint_self` emits it.
- for tl in self.text_labels() {
- ctx.text(tl.text, tl.x, tl.y, tl.font_size, tl.color);
+ // Inline-label widgets emit their own text in `paint`; detached labels come from the
+ // base, exactly as the legacy default `paint_self` emits them.
+ if !Layout::inline_label(&self.inner) {
+ for tl in self.base_label_fallback() {
+ ctx.text(tl.text, tl.x, tl.y, tl.font_size, tl.color);
+ }
+ }
+ }
+
+ /// Inline-label widgets: text derived from the [`Paint::paint`] `Text` prims (one source of
+ /// truth for what the widget draws). Detached-label widgets: the legacy base-label text.
+ fn text_labels(&self) -> Vec<TextLabel> {
+ if Layout::inline_label(&self.inner) {
+ 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()
+ } else {
+ self.base_label_fallback()
}
}
@@ -346,6 +491,21 @@ impl<W: Layout + Paint + Input + 'static> Element for Adapted<W> {
fn blocks_backplate_drag(&self) -> bool {
Input::blocks_backplate_drag(&self.inner)
}
+ fn take_click(&mut self) -> bool {
+ Input::take_click(&mut self.inner)
+ }
+ fn take_change(&mut self) -> bool {
+ Input::take_change(&mut self.inner)
+ }
+ fn get_value_string(&self) -> Option<String> {
+ Input::value_string(&self.inner)
+ }
+ fn set_value_string(&mut self, val: &str) -> bool {
+ Input::set_value_string(&mut self.inner, val)
+ }
+ fn value(&self) -> i32 {
+ Input::value(&self.inner)
+ }
fn hit_test(&self, px: f32, py: f32, ctx: &UiContext) -> bool {
// Preserve the legacy occlusion check (a covering layer swallows the hit), then delegate
@@ -361,6 +521,21 @@ impl<W: Layout + Paint + Input + 'static> Element for Adapted<W> {
let (x, y, w, h) = self.rect();
let rect = Rect { x, y, width: w, height: h };
match event {
+ // A hit right-press on a context-menu widget routes to the shared config menu —
+ // `on_event` can't (no ctx), so the adapter owns this policy.
+ Event::MouseButton {
+ button: crate::widget::MouseButton::Right,
+ state: crate::widget::ElementState::Pressed,
+ x: px,
+ y: py,
+ ..
+ } if Input::opens_context_menu(&self.inner) => {
+ if self.hit_test(*px, *py, ctx) {
+ ctx.handle_right_click(self.as_ptr_mut(), *px, *py);
+ return true;
+ }
+ false
+ }
// Hit-gate pointer-positioned events once, here, so narrow widgets never carry the
// per-widget "check hit_test first" boilerplate legacy `mouse_input` overrides do.
Event::MouseButton { x: px, y: py, .. } | Event::MouseWheel { x: px, y: py, .. } => {