GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
feat(widget): migrate the deferred leaves — Float3, LayoutPreview, PreviewState, StatusBar (Phase 5t)
The last four Element widgets move onto the narrow traits, completing the
Phase 5 per-widget migration (everything still on impl Element is
embedded-base machinery that dissolves via Phase 6 scene adoption).
- Float3: rect cached via rect_assigned (get_row_rects is pub API),
readout-edit + track-drag in on_event/the drag hooks, the legacy
focus::set_focused rides EventCtx::request_focus. Only consumer is
ParametersBg; its pub-field reach flows through Deref unchanged.
- LayoutPreview: mechanical; zero consumers workspace-wide. The
duplicated SimNode match collapsed into one helper.
- PreviewState: canvas quads from paint, canvas labels (per-label
monospace) through the 5s serves_legacy_labels hatch; plain
text_labels stays empty like legacy (prims too would double-render
under container aggregation). New blanket impl Default for
Adapted<W: Default> keeps cce-files' Default construction site.
- StatusBar: MenuBar-style backplate parent coupling (tracked parent,
themed color/corners at the parent radius) plus one new hook,
Paint::text_items — pre-shaped glyphon buffers for the legacy
get_text_items path, which prim-derived text cannot serve
(cce-status-interface drives the bar by hand: prepare_text then
get_text_items into its own paint). inline_label keeps set_text's
base-label write from leaking a detached label; the legacy
no-widget-font asymmetry on the container text path is preserved.
Verified: 177 tests (new: Float3 readout/drag flow, StatusBar
manual-host pipeline); live A/B — cce-files preview pane byte-identical
across select/text-preview/wheel-scroll, cce-designer params pane
byte-identical across node-select/readout-edit/row-wheel (residual =
the calibrated status-strip live noise), cce-system-settings
whole-window byte-identical.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_016MjP3pGQEDLkJbV5WEmYBe
src/widget/container/parameters_bg.rs | 4 +-
src/widget/display/float3.rs | 503 ++++++++++++++++++----------------
src/widget/display/layout_preview.rs | 350 ++++++++++-------------
src/widget/display/preview.rs | 131 ++++-----
src/widget/display/status_bar.rs | 267 +++++++++++-------
src/widget/model.rs | 22 ++
6 files changed, 651 insertions(+), 626 deletions(-)
diff --git a/src/widget/container/parameters_bg.rs b/src/widget/container/parameters_bg.rs
index 0c67246..b76576b 100644
--- a/src/widget/container/parameters_bg.rs
+++ b/src/widget/container/parameters_bg.rs
@@ -27,7 +27,7 @@ use crate::scene::paint::PaintCtx;
use crate::widget::display::{Float3, TextLabel};
use crate::widget::input::{Button, Checkbox, ColorSelector, Dropdown, Slider, Spinbox, TextBox};
use crate::widget::{
- Adapted, Element, ElementState, Event, EventCtx, Input, Key, KeyEvent, Layout, MouseButton,
+ Adapted, Element, ElementState, Event, EventCtx, Input, Key, Layout, MouseButton,
MouseScrollDelta, NamedKey, Paint, ParamController, TextEditorState, UiContext,
};
@@ -39,7 +39,7 @@ pub struct ParametersBg {
pub code_editor: Option<TextEditorState>,
mouse_pos: Option<(f32, f32)>,
pub sliders: Vec<Option<Adapted<Slider>>>,
- pub float3s: Vec<Option<Float3>>,
+ pub float3s: Vec<Option<Adapted<Float3>>>,
pub spinboxes: Vec<Option<Adapted<Spinbox>>>,
pub buttons: Vec<Option<Adapted<Button>>>,
pub choices: Vec<Option<Adapted<Dropdown>>>,
diff --git a/src/widget/display/float3.rs b/src/widget/display/float3.rs
index 87b099a..9c1746a 100644
--- a/src/widget/display/float3.rs
+++ b/src/widget/display/float3.rs
@@ -1,13 +1,26 @@
+//! Narrow-trait `Float3` (Phase 5t) — three labeled slider rows (X/Y/Z) with click-to-edit
+//! numeric readouts, embedded by value inside `ParametersBg` (its only consumer), which drives
+//! it through direct `Element` calls and reads the pub value/edit fields through `Deref`. The
+//! model caches its laid-out rect ([`Layout::rect_assigned`] — `get_row_rects` is pub API with
+//! no rect parameter), draws everything in [`Paint::paint`], and keeps the legacy drag surface
+//! on the `Input` drag hooks. The readout click's legacy `focus::set_focused(self)` rides
+//! `EventCtx::request_focus`.
+
use crate::colors;
-use crate::widget::*;
-use crate::widget::display::TextLabel;
+use crate::scene::layout::Rect;
+use crate::scene::paint::PaintCtx;
+use crate::widget::{
+ Adapted, ElementState, Event, EventCtx, Input, Key, Layout, MouseButton, NamedKey, Paint,
+ TextEditorState,
+};
pub struct Float3 {
- base: Widget,
+ rect: Rect,
pub values: [f32; 3],
pub(crate) mins: [f32; 3],
pub(crate) maxs: [f32; 3],
labels: [String; 3],
+ label: Option<String>,
dragging_idx: Option<usize>,
drag_offset: f32,
pub editing_idx: Option<usize>,
@@ -15,186 +28,36 @@ pub struct Float3 {
}
impl Float3 {
- pub fn new() -> Self {
- Self {
- base: Widget::new(),
+ pub fn new() -> Adapted<Float3> {
+ Adapted::new(Float3 {
+ rect: Rect { x: 0.0, y: 0.0, width: 0.0, height: 0.0 },
values: [0.5, 0.5, 0.5],
mins: [0.0, 0.0, 0.0],
maxs: [1.0, 1.0, 1.0],
labels: ["X".to_string(), "Y".to_string(), "Z".to_string()],
+ label: None,
dragging_idx: None,
drag_offset: 0.0,
editing_idx: None,
edit_buffer: String::new(),
- }
- }
-
- pub fn with_values(mut self, values: [f32; 3]) -> Self {
- self.values = values;
- self
- }
-
- pub fn with_range(mut self, min: f32, max: f32) -> Self {
- self.mins = [min, min, min];
- self.maxs = [max, max, max];
- self
+ })
}
pub fn set_values(&mut self, values: [f32; 3]) {
self.values = values;
}
- pub fn with_label(mut self, label: &str) -> Self {
- self.base.label = Some(label.to_string());
- self
- }
-
pub fn get_row_rects(&self) -> Vec<(f32, f32, f32, f32)> {
let mut rects = Vec::new();
- let by = self.base.y + 20.0;
+ let by = self.rect.y + 20.0;
for i in 0..3 {
- rects.push((self.base.x + 8.0, by + 6.0 + i as f32 * 26.0, self.base.w - 16.0, 20.0));
+ rects.push((self.rect.x + 8.0, by + 6.0 + i as f32 * 26.0, self.rect.width - 16.0, 20.0));
}
rects
}
-}
-
-impl Element for Float3 {
- crate::impl_widget_base!(Float3);
- fn color(&self) -> [f32; 4] { [0.0, 0.0, 0.0, 0.0] }
- fn rect(&self) -> (f32, f32, f32, f32) { (self.base.x, self.base.y, self.base.w, self.base.h) }
- fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
- self.base.x = x; self.base.y = y; self.base.w = w; self.base.h = h;
- }
-
- fn draggable(&self) -> bool { self.dragging_idx.is_some() }
- fn is_dragging(&self) -> bool { self.dragging_idx.is_some() }
-
- fn drag_begin(&mut self, _px: f32, _py: f32) {}
-
- fn drag_update(&mut self, px: f32, _py: f32) -> bool {
- if let Some(i) = self.dragging_idx {
- let track_x = self.base.x + 100.0;
- let track_w = self.base.w - 188.0;
- let thumb_size = 12.0 * 0.9;
- let range = track_w - thumb_size;
- if range > 0.0 {
- let raw = (px - self.drag_offset - track_x) / range;
- let new_val = raw.clamp(0.0, 1.0);
- if (new_val - self.values[i]).abs() > 0.001 {
- self.values[i] = new_val;
- if self.editing_idx == Some(i) {
- let scaled_val = self.mins[i] + self.values[i] * (self.maxs[i] - self.mins[i]);
- self.edit_buffer = format!("{:.2}", scaled_val);
- }
- return true;
- }
- }
- }
- false
- }
-
- fn drag_end(&mut self) {
- self.dragging_idx = None;
- }
- fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32, _ctx: &mut UiContext) -> bool {
- if button != MouseButton::Left { return false; }
- let rects = self.get_row_rects();
-
- for i in 0..3 {
- let r = rects[i];
- let rx = self.base.x + self.base.w - 68.0;
- let ry = r.1 + 4.0;
- let rh = 12.0;
- let readout_w = 60.0;
-
- if px >= rx && px <= rx + readout_w && py >= ry && py <= ry + rh {
- if state == ElementState::Pressed {
- if self.editing_idx != Some(i) {
- self.unfocus();
- self.editing_idx = Some(i);
- let scaled_val = self.mins[i] + self.values[i] * (self.maxs[i] - self.mins[i]);
- self.edit_buffer = format!("{:.2}", scaled_val);
- focus::set_focused(self);
- }
- }
- return true;
- }
- }
-
- if state == ElementState::Pressed {
- for i in 0..3 {
- let r = rects[i];
- let track_x = self.base.x + 100.0;
- let track_w = self.base.w - 188.0;
- let track_y = r.1 + 4.0;
- let track_h = 12.0;
- let thumb_size = track_h * 0.9;
- let range = track_w - thumb_size;
- let thumb_x = track_x + self.values[i] * range;
-
- if px >= track_x && px <= track_x + track_w && py >= track_y && py <= track_y + track_h {
- self.dragging_idx = Some(i);
- self.drag_offset = px - thumb_x;
- return true;
- }
- }
- } else if state == ElementState::Released {
- if self.dragging_idx.is_some() {
- self.dragging_idx = None;
- return true;
- }
- }
- false
- }
-
- fn keyboard_input(&mut self, event: &KeyEvent, _ctx: &mut UiContext) -> bool {
- let _idx = match self.editing_idx {
- Some(i) => i,
- None => return false,
- };
- if event.state != ElementState::Pressed { return false; }
-
- let mut state = TextEditorState {
- buffer: self.edit_buffer.clone(),
- cursor_idx: self.edit_buffer.chars().count(),
- select_anchor: None,
- all_selected: false,
- };
-
- let mut handled = false;
- match &event.logical_key {
- Key::Named(NamedKey::Backspace) => {
- state.delete_backwards();
- handled = true;
- }
- Key::Named(NamedKey::Enter) => {
- self.unfocus();
- handled = true;
- }
- Key::Named(NamedKey::Escape) => {
- self.editing_idx = None;
- handled = true;
- }
- Key::Character(s) => {
- for ch in s.chars() {
- if ch.is_ascii_digit() || ch == '.' || (ch == '-' && state.buffer.is_empty()) {
- state.insert_text(&ch.to_string());
- }
- }
- handled = true;
- }
- _ => {}
- }
-
- if self.editing_idx.is_some() {
- self.edit_buffer = state.buffer;
- }
- handled
- }
-
- fn unfocus(&mut self) {
+ /// Commit the in-flight readout edit back into the value — the legacy `unfocus` body.
+ fn commit_edit(&mut self) {
if let Some(i) = self.editing_idx.take() {
if let Ok(new_val) = self.edit_buffer.parse::<f32>() {
let range = self.maxs[i] - self.mins[i];
@@ -206,33 +69,69 @@ impl Element for Float3 {
}
}
}
+}
+
+impl Adapted<Float3> {
+ pub fn with_values(mut self, values: [f32; 3]) -> Self {
+ self.values = values;
+ self
+ }
+
+ pub fn with_range(mut self, min: f32, max: f32) -> Self {
+ self.mins = [min, min, min];
+ self.maxs = [max, max, max];
+ self
+ }
+}
+
+impl Layout for Float3 {
+ /// The control label draws inside the rect (row one's header), like legacy.
+ fn inline_label(&self) -> bool {
+ true
+ }
+
+ fn rect_assigned(&mut self, rect: Rect) {
+ self.rect = rect;
+ }
+}
+
+impl Paint for Float3 {
+ fn color(&self) -> [f32; 4] {
+ [0.0, 0.0, 0.0, 0.0]
+ }
- fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
- let mut quads = Vec::new();
-
+ fn sync_label(&mut self, label: &str) {
+ self.label = Some(label.to_string());
+ }
+
+ fn paint(&self, _rect: Rect, ctx: &mut PaintCtx) {
// Outline border box
- let bx = self.base.x + 4.0;
- let bw = self.base.w - 8.0;
- let by = self.base.y + 20.0;
+ let bx = self.rect.x + 4.0;
+ let bw = self.rect.width - 8.0;
+ let by = self.rect.y + 20.0;
let bh = 84.0;
let border_color = [0.18, 0.18, 0.27, 1.0];
let border_t = 1.0;
-
- quads.push((bx, by, bw, border_t, border_color));
- quads.push((bx, by + bh - border_t, bw, border_t, border_color));
- quads.push((bx, by, border_t, bh, border_color));
- quads.push((bx + bw - border_t, by, border_t, bh, border_color));
-
+
+ ctx.quad(Rect { x: bx, y: by, width: bw, height: border_t }, border_color);
+ ctx.quad(Rect { x: bx, y: by + bh - border_t, width: bw, height: border_t }, border_color);
+ ctx.quad(Rect { x: bx, y: by, width: border_t, height: bh }, border_color);
+ ctx.quad(Rect { x: bx + bw - border_t, y: by, width: border_t, height: bh }, border_color);
+
+ if let Some(ref l) = self.label {
+ ctx.text(l.clone(), self.rect.x + 8.0, self.rect.y + 2.0, 13.0, [0xee, 0xee, 0xf0]);
+ }
+
let rects = self.get_row_rects();
for i in 0..3 {
let r = rects[i];
- let track_x = self.base.x + 100.0;
- let track_w = self.base.w - 188.0;
+ let track_x = self.rect.x + 100.0;
+ let track_w = self.rect.width - 188.0;
let track_y = r.1 + 4.0;
let track_h = 12.0;
-
- quads.push((track_x, track_y, track_w, track_h, colors::slider_track()));
-
+
+ ctx.quad(Rect { x: track_x, y: track_y, width: track_w, height: track_h }, colors::slider_track());
+
let thumb_size = track_h * 0.9;
let range = track_w - thumb_size;
let thumb_x = track_x + self.values[i] * range;
@@ -241,76 +140,222 @@ impl Element for Float3 {
} else {
colors::SLIDER_THUMB
};
- quads.push((thumb_x, track_y + (track_h - thumb_size)/2.0, thumb_size, thumb_size, thumb_color));
-
- let rx = self.base.x + self.base.w - 68.0;
+ ctx.quad(
+ Rect { x: thumb_x, y: track_y + (track_h - thumb_size) / 2.0, width: thumb_size, height: thumb_size },
+ thumb_color,
+ );
+
+ let rx = self.rect.x + self.rect.width - 68.0;
let bg_color = if self.editing_idx == Some(i) {
[0.06, 0.10, 0.18, 1.0]
} else {
[0.10, 0.10, 0.13, 1.0]
};
- quads.push((rx, track_y, 60.0, track_h, bg_color));
-
+ ctx.quad(Rect { x: rx, y: track_y, width: 60.0, height: track_h }, bg_color);
+
if self.editing_idx == Some(i) {
let border_color = [0.20, 0.50, 0.85, 1.0];
- quads.push((rx, track_y, 60.0, border_t, border_color));
- quads.push((rx, track_y + track_h - border_t, 60.0, border_t, border_color));
- quads.push((rx, track_y, border_t, track_h, border_color));
- quads.push((rx + 60.0 - border_t, track_y, border_t, track_h, border_color));
+ ctx.quad(Rect { x: rx, y: track_y, width: 60.0, height: border_t }, border_color);
+ ctx.quad(Rect { x: rx, y: track_y + track_h - border_t, width: 60.0, height: border_t }, border_color);
+ ctx.quad(Rect { x: rx, y: track_y, width: border_t, height: track_h }, border_color);
+ ctx.quad(Rect { x: rx + 60.0 - border_t, y: track_y, width: border_t, height: track_h }, border_color);
}
- }
- quads
- }
- fn text_labels(&self) -> Vec<TextLabel> {
- let mut labels = Vec::new();
-
- if let Some(ref l) = self.base.label {
- labels.push(TextLabel {
- text: l.clone(),
- x: self.base.x + 8.0,
- y: self.base.y + 2.0,
- font_size: 13.0,
- color: [0xee, 0xee, 0xf0],
- });
- }
-
- let rects = self.get_row_rects();
- for i in 0..3 {
- let r = rects[i];
- let track_y = r.1 + 4.0;
+ // Row label + readout text (the legacy `text_labels` body).
let ry = track_y;
-
- labels.push(TextLabel {
- text: self.labels[i].clone(),
- x: self.base.x + 16.0,
- y: ry - 2.0,
- font_size: 12.0,
- color: [0xaa, 0xaa, 0xbb],
- });
-
- let rx = self.base.x + self.base.w - 68.0;
+ ctx.text(self.labels[i].clone(), self.rect.x + 16.0, ry - 2.0, 12.0, [0xaa, 0xaa, 0xbb]);
let text = if self.editing_idx == Some(i) {
self.edit_buffer.clone()
} else {
let scaled_val = self.mins[i] + self.values[i] * (self.maxs[i] - self.mins[i]);
format!("{:.2}", scaled_val)
};
- labels.push(TextLabel {
- text,
- x: rx + 8.0,
- y: ry - 2.0,
- font_size: 12.0,
- color: [0xee, 0xee, 0xf0],
- });
+ ctx.text(text, rx + 8.0, ry - 2.0, 12.0, [0xee, 0xee, 0xf0]);
}
- labels
}
}
-impl Drop for Float3 {
- fn drop(&mut self) {
- clear_widget_references(self);
+impl Input for Float3 {
+ fn draggable(&self, _rect: Rect) -> bool {
+ self.dragging_idx.is_some()
+ }
+
+ fn is_dragging(&self) -> bool {
+ self.dragging_idx.is_some()
+ }
+
+ fn drag_begin(&mut self, _px: f32, _py: f32, _rect: Rect) {}
+
+ fn drag_update(&mut self, px: f32, _py: f32, _rect: Rect) -> bool {
+ if let Some(i) = self.dragging_idx {
+ let track_x = self.rect.x + 100.0;
+ let track_w = self.rect.width - 188.0;
+ let thumb_size = 12.0 * 0.9;
+ let range = track_w - thumb_size;
+ if range > 0.0 {
+ let raw = (px - self.drag_offset - track_x) / range;
+ let new_val = raw.clamp(0.0, 1.0);
+ if (new_val - self.values[i]).abs() > 0.001 {
+ self.values[i] = new_val;
+ if self.editing_idx == Some(i) {
+ let scaled_val = self.mins[i] + self.values[i] * (self.maxs[i] - self.mins[i]);
+ self.edit_buffer = format!("{:.2}", scaled_val);
+ }
+ return true;
+ }
+ }
+ }
+ false
+ }
+
+ fn drag_end(&mut self) {
+ self.dragging_idx = None;
+ }
+
+ fn on_event(&mut self, event: &Event, ectx: &mut EventCtx) -> bool {
+ match event {
+ Event::MouseButton { button, state, x: px, y: py, .. } => {
+ if *button != MouseButton::Left {
+ return false;
+ }
+ let (state, px, py) = (*state, *px, *py);
+ let rects = self.get_row_rects();
+
+ for i in 0..3 {
+ let r = rects[i];
+ let rx = self.rect.x + self.rect.width - 68.0;
+ let ry = r.1 + 4.0;
+ let rh = 12.0;
+ let readout_w = 60.0;
+
+ if px >= rx && px <= rx + readout_w && py >= ry && py <= ry + rh {
+ if state == ElementState::Pressed {
+ if self.editing_idx != Some(i) {
+ self.commit_edit();
+ self.editing_idx = Some(i);
+ let scaled_val = self.mins[i] + self.values[i] * (self.maxs[i] - self.mins[i]);
+ self.edit_buffer = format!("{:.2}", scaled_val);
+ ectx.request_focus();
+ }
+ }
+ return true;
+ }
+ }
+
+ if state == ElementState::Pressed {
+ for i in 0..3 {
+ let r = rects[i];
+ let track_x = self.rect.x + 100.0;
+ let track_w = self.rect.width - 188.0;
+ let track_y = r.1 + 4.0;
+ let track_h = 12.0;
+ let thumb_size = track_h * 0.9;
+ let range = track_w - thumb_size;
+ let thumb_x = track_x + self.values[i] * range;
+
+ if px >= track_x && px <= track_x + track_w && py >= track_y && py <= track_y + track_h {
+ self.dragging_idx = Some(i);
+ self.drag_offset = px - thumb_x;
+ return true;
+ }
+ }
+ } else if state == ElementState::Released {
+ if self.dragging_idx.is_some() {
+ self.dragging_idx = None;
+ return true;
+ }
+ }
+ false
+ }
+ Event::KeyInput(event) => {
+ if self.editing_idx.is_none() {
+ return false;
+ }
+ if event.state != ElementState::Pressed {
+ return false;
+ }
+
+ let mut state = TextEditorState {
+ buffer: self.edit_buffer.clone(),
+ cursor_idx: self.edit_buffer.chars().count(),
+ select_anchor: None,
+ all_selected: false,
+ };
+
+ let mut handled = false;
+ match &event.logical_key {
+ Key::Named(NamedKey::Backspace) => {
+ state.delete_backwards();
+ handled = true;
+ }
+ Key::Named(NamedKey::Enter) => {
+ self.commit_edit();
+ handled = true;
+ }
+ Key::Named(NamedKey::Escape) => {
+ self.editing_idx = None;
+ handled = true;
+ }
+ Key::Character(s) => {
+ for ch in s.chars() {
+ if ch.is_ascii_digit() || ch == '.' || (ch == '-' && state.buffer.is_empty()) {
+ state.insert_text(&ch.to_string());
+ }
+ }
+ handled = true;
+ }
+ _ => {}
+ }
+
+ if self.editing_idx.is_some() {
+ self.edit_buffer = state.buffer;
+ }
+ handled
+ }
+ Event::FocusOut => {
+ self.commit_edit();
+ true
+ }
+ _ => false,
+ }
}
}
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::context::UiContext;
+ use crate::widget::Element;
+
+ /// The ParametersBg drive pattern: readout click opens the edit, Enter/unfocus commits
+ /// back into the normalized value, track press starts a drag.
+ #[test]
+ fn readout_edit_commits_on_unfocus() {
+ let mut ctx = UiContext::new();
+ let mut f = Float3::new().with_values([0.5, 0.5, 0.5]).with_range(0.0, 10.0);
+ Element::set_rect(&mut f, 0.0, 0.0, 300.0, 108.0);
+
+ let rows = f.get_row_rects();
+ assert_eq!(rows.len(), 3);
+ // Click row 1's readout (x within [w-68, w-8], y within row top+4..+16).
+ let rx = 300.0 - 68.0 + 5.0;
+ let ry = rows[1].1 + 8.0;
+ assert!(f.mouse_input(MouseButton::Left, ElementState::Pressed, rx, ry, &mut ctx));
+ assert_eq!(f.editing_idx, Some(1));
+ assert_eq!(f.edit_buffer, "5.00");
+
+ f.edit_buffer = "7.5".to_string();
+ Element::unfocus(&mut f);
+ assert_eq!(f.editing_idx, None);
+ assert!((f.values[1] - 0.75).abs() < 1e-4, "7.5 of 0..10 normalizes to 0.75");
+
+ // Track press starts a drag; drag_update moves the value; release ends it.
+ let track_y = rows[0].1 + 8.0;
+ assert!(f.mouse_input(MouseButton::Left, ElementState::Pressed, 150.0, track_y, &mut ctx));
+ assert!(Element::is_dragging(&f));
+ Element::drag_update(&mut f, 260.0, track_y);
+ assert!(f.values[0] > 0.5, "drag right raises the value");
+ Element::drag_end(&mut f);
+ assert!(!Element::is_dragging(&f));
+ }
+}
diff --git a/src/widget/display/layout_preview.rs b/src/widget/display/layout_preview.rs
index e140dd4..69e77ef 100644
--- a/src/widget/display/layout_preview.rs
+++ b/src/widget/display/layout_preview.rs
@@ -1,5 +1,11 @@
-use crate::widget::*;
-use crate::widget::display::TextLabel;
+//! Narrow-trait `LayoutPreview` (Phase 5t) — a static thumbnail of a window-layout mode
+//! (fullscreen / cascade / grid / …) drawn as bordered boxes with per-node labels. Pure
+//! display: all geometry and text come out of [`Paint::paint`]. No consumers exist
+//! workspace-wide (only the re-exports); migrated for completeness of the Phase 5 sweep.
+
+use crate::scene::layout::Rect;
+use crate::scene::paint::PaintCtx;
+use crate::widget::{Adapted, Input, Layout, Paint};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PreviewLayoutMode {
@@ -24,48 +30,139 @@ struct SimNode {
#[derive(Debug, Clone)]
pub struct LayoutPreview {
- base: Widget,
pub mode: PreviewLayoutMode,
pub is_active: bool,
+ label: Option<String>,
}
impl LayoutPreview {
- pub fn new(mode: PreviewLayoutMode) -> Self {
- Self {
- base: Widget::new(),
- mode,
- is_active: false,
+ pub fn new(mode: PreviewLayoutMode) -> Adapted<LayoutPreview> {
+ Adapted::new(LayoutPreview { mode, is_active: false, label: None })
+ }
+
+ /// The simulated node boxes for `mode` inside a preview area of the given size — the one
+ /// source both the boxes and their labels are drawn from (legacy duplicated this match in
+ /// `extra_quads` and `text_labels`).
+ fn sim_nodes(&self, preview_w: f32, preview_h: f32) -> Vec<SimNode> {
+ let mut nodes = Vec::new();
+ match self.mode {
+ PreviewLayoutMode::Fullscreen => {
+ nodes.push(SimNode { x: 2.0, y: 2.0, w: preview_w - 4.0, h: preview_h - 4.0, label: "F".to_string() });
+ }
+ PreviewLayoutMode::Cascade => {
+ nodes.push(SimNode { x: 2.0, y: 2.0, w: preview_w - 12.0, h: preview_h - 12.0, label: "1".to_string() });
+ nodes.push(SimNode { x: 6.0, y: 6.0, w: preview_w - 12.0, h: preview_h - 12.0, label: "2".to_string() });
+ nodes.push(SimNode { x: 10.0, y: 10.0, w: preview_w - 12.0, h: preview_h - 12.0, label: "3".to_string() });
+ }
+ PreviewLayoutMode::Stack => {
+ nodes.push(SimNode { x: 2.0, y: 2.0, w: preview_w - 4.0, h: preview_h - 4.0, label: "Stack".to_string() });
+ }
+ PreviewLayoutMode::Grid => {
+ let hw = (preview_w - 6.0) / 2.0;
+ let hh = (preview_h - 6.0) / 2.0;
+ nodes.push(SimNode { x: 2.0, y: 2.0, w: hw, h: hh, label: "1".to_string() });
+ nodes.push(SimNode { x: 4.0 + hw, y: 2.0, w: hw, h: hh, label: "2".to_string() });
+ nodes.push(SimNode { x: 2.0, y: 4.0 + hh, w: hw, h: hh, label: "3".to_string() });
+ nodes.push(SimNode { x: 4.0 + hw, y: 4.0 + hh, w: hw, h: hh, label: "4".to_string() });
+ }
+ PreviewLayoutMode::LeftTiled => {
+ let mw = (preview_w - 6.0) * 0.55;
+ let sw = (preview_w - 6.0) - mw;
+ let sh = (preview_h - 6.0) / 2.0;
+ nodes.push(SimNode { x: 2.0, y: 2.0, w: mw, h: preview_h - 4.0, label: "M".to_string() });
+ nodes.push(SimNode { x: 4.0 + mw, y: 2.0, w: sw, h: sh, label: "1".to_string() });
+ nodes.push(SimNode { x: 4.0 + mw, y: 4.0 + sh, w: sw, h: sh, label: "2".to_string() });
+ }
+ PreviewLayoutMode::RightTiled => {
+ let mw = (preview_w - 6.0) * 0.55;
+ let sw = (preview_w - 6.0) - mw;
+ let sh = (preview_h - 6.0) / 2.0;
+ nodes.push(SimNode { x: 2.0, y: 2.0, w: sw, h: sh, label: "1".to_string() });
+ nodes.push(SimNode { x: 2.0, y: 4.0 + sh, w: sw, h: sh, label: "2".to_string() });
+ nodes.push(SimNode { x: 4.0 + sw, y: 2.0, w: mw, h: preview_h - 4.0, label: "M".to_string() });
+ }
+ PreviewLayoutMode::Equal => {
+ let ew = (preview_w - 8.0) / 3.0;
+ nodes.push(SimNode { x: 2.0, y: 2.0, w: ew, h: preview_h - 4.0, label: "1".to_string() });
+ nodes.push(SimNode { x: 4.0 + ew, y: 2.0, w: ew, h: preview_h - 4.0, label: "2".to_string() });
+ nodes.push(SimNode { x: 6.0 + 2.0 * ew, y: 2.0, w: ew, h: preview_h - 4.0, label: "3".to_string() });
+ }
+ PreviewLayoutMode::Spiral => {
+ let w1 = (preview_w - 6.0) * 0.5;
+ let w2 = (preview_w - 6.0) - w1;
+ let h2 = (preview_h - 6.0) * 0.5;
+ nodes.push(SimNode { x: 2.0, y: 2.0, w: w1, h: preview_h - 4.0, label: "1".to_string() });
+ nodes.push(SimNode { x: 4.0 + w1, y: 2.0, w: w2, h: h2, label: "2".to_string() });
+ nodes.push(SimNode { x: 4.0 + w1, y: 4.0 + h2, w: w2 * 0.5, h: h2, label: "3".to_string() });
+ nodes.push(SimNode { x: 4.0 + w1 + w2 * 0.5, y: 4.0 + h2, w: w2 * 0.5, h: h2, label: "4".to_string() });
+ }
+ PreviewLayoutMode::Floating => {
+ nodes.push(SimNode { x: 4.0, y: 6.0, w: preview_w * 0.45, h: preview_h * 0.5, label: "1".to_string() });
+ nodes.push(SimNode { x: preview_w * 0.4, y: 12.0, w: preview_w * 0.5, h: preview_h * 0.45, label: "2".to_string() });
+ nodes.push(SimNode { x: 8.0, y: preview_h * 0.4, w: preview_w * 0.55, h: preview_h * 0.5, label: "3".to_string() });
+ }
+ }
+ nodes
+ }
+
+ fn mode_name(&self) -> &'static str {
+ match self.mode {
+ PreviewLayoutMode::Fullscreen => "Fullscreen",
+ PreviewLayoutMode::Cascade => "Cascade",
+ PreviewLayoutMode::Stack => "Stack",
+ PreviewLayoutMode::Grid => "Grid",
+ PreviewLayoutMode::LeftTiled => "L-Tiled",
+ PreviewLayoutMode::RightTiled => "R-Tiled",
+ PreviewLayoutMode::Equal => "Equal",
+ PreviewLayoutMode::Spiral => "Spiral",
+ PreviewLayoutMode::Floating => "Floating",
}
}
+}
+impl Adapted<LayoutPreview> {
pub fn with_active(mut self, active: bool) -> Self {
self.is_active = active;
self
}
-
- pub fn with_label(mut self, label: &str) -> Self {
- self.base.label = Some(label.to_string());
- self
+}
+
+impl Layout for LayoutPreview {
+ /// The tag/name header draws inside the widget rect.
+ fn inline_label(&self) -> bool {
+ true
}
}
-impl Element for LayoutPreview {
- crate::impl_widget_base!(LayoutPreview);
- fn color(&self) -> [f32; 4] { [0.0, 0.0, 0.0, 0.0] }
- fn highlight_quad(&self, _ctx: &UiContext) -> Option<(f32, f32, f32, f32, [f32; 4])>{ None }
+impl Paint for LayoutPreview {
+ fn color(&self) -> [f32; 4] {
+ [0.0, 0.0, 0.0, 0.0]
+ }
+
+ 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 (x, y, w, h) = self.rect();
- let mut quads = Vec::new();
+ fn paint(&self, rect: Rect, ctx: &mut PaintCtx) {
+ let (x, y, w, h) = (rect.x, rect.y, rect.width, rect.height);
let bg_col = if self.is_active { [0.12, 0.24, 0.14, 0.55] } else { [0.08, 0.08, 0.12, 0.35] };
let border_col = if self.is_active { [0.36, 0.56, 0.38, 0.95] } else { [0.24, 0.24, 0.28, 0.45] };
- quads.push((x, y, w, h, bg_col));
- quads.push((x, y, w, 1.0, border_col));
- quads.push((x, y + h - 1.0, w, 1.0, border_col));
- quads.push((x, y, 1.0, h, border_col));
- quads.push((x + w - 1.0, y, 1.0, h, border_col));
+ ctx.quad(Rect { x, y, width: w, height: h }, bg_col);
+ ctx.quad(Rect { x, y, width: w, height: 1.0 }, border_col);
+ ctx.quad(Rect { x, y: y + h - 1.0, width: w, height: 1.0 }, border_col);
+ ctx.quad(Rect { x, y, width: 1.0, height: h }, border_col);
+ ctx.quad(Rect { x: x + w - 1.0, y, width: 1.0, height: h }, border_col);
+
+ ctx.text(
+ self.label.clone().unwrap_or_else(|| "TAG".to_string()),
+ x + 8.0,
+ y + 8.0,
+ 10.0,
+ [140, 140, 153],
+ );
+ ctx.text(self.mode_name().to_string(), x + 8.0, y + 20.0, 13.0, [230, 230, 242]);
let preview_x = x + 8.0;
let preview_y = y + 38.0;
@@ -73,204 +170,39 @@ impl Element for LayoutPreview {
let preview_h = h - 46.0;
if preview_w > 0.0 && preview_h > 0.0 {
- quads.push((preview_x, preview_y, preview_w, preview_h, [0.16, 0.16, 0.20, 0.6]));
+ ctx.quad(Rect { x: preview_x, y: preview_y, width: preview_w, height: preview_h }, [0.16, 0.16, 0.20, 0.6]);
let preview_border = [0.22, 0.22, 0.26, 0.8];
- quads.push((preview_x, preview_y, preview_w, 1.0, preview_border));
- quads.push((preview_x, preview_y + preview_h - 1.0, preview_w, 1.0, preview_border));
- quads.push((preview_x, preview_y, 1.0, preview_h, preview_border));
- quads.push((preview_x + preview_w - 1.0, preview_y, 1.0, preview_h, preview_border));
+ ctx.quad(Rect { x: preview_x, y: preview_y, width: preview_w, height: 1.0 }, preview_border);
+ ctx.quad(Rect { x: preview_x, y: preview_y + preview_h - 1.0, width: preview_w, height: 1.0 }, preview_border);
+ ctx.quad(Rect { x: preview_x, y: preview_y, width: 1.0, height: preview_h }, preview_border);
+ ctx.quad(Rect { x: preview_x + preview_w - 1.0, y: preview_y, width: 1.0, height: preview_h }, preview_border);
- let mut nodes = Vec::new();
- match self.mode {
- PreviewLayoutMode::Fullscreen => {
- nodes.push(SimNode { x: 2.0, y: 2.0, w: preview_w - 4.0, h: preview_h - 4.0, label: "F".to_string() });
- }
- PreviewLayoutMode::Cascade => {
- nodes.push(SimNode { x: 2.0, y: 2.0, w: preview_w - 12.0, h: preview_h - 12.0, label: "1".to_string() });
- nodes.push(SimNode { x: 6.0, y: 6.0, w: preview_w - 12.0, h: preview_h - 12.0, label: "2".to_string() });
- nodes.push(SimNode { x: 10.0, y: 10.0, w: preview_w - 12.0, h: preview_h - 12.0, label: "3".to_string() });
- }
- PreviewLayoutMode::Stack => {
- nodes.push(SimNode { x: 2.0, y: 2.0, w: preview_w - 4.0, h: preview_h - 4.0, label: "Stack".to_string() });
- }
- PreviewLayoutMode::Grid => {
- let hw = (preview_w - 6.0) / 2.0;
- let hh = (preview_h - 6.0) / 2.0;
- nodes.push(SimNode { x: 2.0, y: 2.0, w: hw, h: hh, label: "1".to_string() });
- nodes.push(SimNode { x: 4.0 + hw, y: 2.0, w: hw, h: hh, label: "2".to_string() });
- nodes.push(SimNode { x: 2.0, y: 4.0 + hh, w: hw, h: hh, label: "3".to_string() });
- nodes.push(SimNode { x: 4.0 + hw, y: 4.0 + hh, w: hw, h: hh, label: "4".to_string() });
- }
- PreviewLayoutMode::LeftTiled => {
- let mw = (preview_w - 6.0) * 0.55;
- let sw = (preview_w - 6.0) - mw;
- let sh = (preview_h - 6.0) / 2.0;
- nodes.push(SimNode { x: 2.0, y: 2.0, w: mw, h: preview_h - 4.0, label: "M".to_string() });
- nodes.push(SimNode { x: 4.0 + mw, y: 2.0, w: sw, h: sh, label: "1".to_string() });
- nodes.push(SimNode { x: 4.0 + mw, y: 4.0 + sh, w: sw, h: sh, label: "2".to_string() });
- }
- PreviewLayoutMode::RightTiled => {
- let mw = (preview_w - 6.0) * 0.55;
- let sw = (preview_w - 6.0) - mw;
- let sh = (preview_h - 6.0) / 2.0;
- nodes.push(SimNode { x: 2.0, y: 2.0, w: sw, h: sh, label: "1".to_string() });
- nodes.push(SimNode { x: 2.0, y: 4.0 + sh, w: sw, h: sh, label: "2".to_string() });
- nodes.push(SimNode { x: 4.0 + sw, y: 2.0, w: mw, h: preview_h - 4.0, label: "M".to_string() });
- }
- PreviewLayoutMode::Equal => {
- let ew = (preview_w - 8.0) / 3.0;
- nodes.push(SimNode { x: 2.0, y: 2.0, w: ew, h: preview_h - 4.0, label: "1".to_string() });
- nodes.push(SimNode { x: 4.0 + ew, y: 2.0, w: ew, h: preview_h - 4.0, label: "2".to_string() });
- nodes.push(SimNode { x: 6.0 + 2.0 * ew, y: 2.0, w: ew, h: preview_h - 4.0, label: "3".to_string() });
- }
- PreviewLayoutMode::Spiral => {
- let w1 = (preview_w - 6.0) * 0.5;
- let w2 = (preview_w - 6.0) - w1;
- let h2 = (preview_h - 6.0) * 0.5;
- nodes.push(SimNode { x: 2.0, y: 2.0, w: w1, h: preview_h - 4.0, label: "1".to_string() });
- nodes.push(SimNode { x: 4.0 + w1, y: 2.0, w: w2, h: h2, label: "2".to_string() });
- nodes.push(SimNode { x: 4.0 + w1, y: 4.0 + h2, w: w2 * 0.5, h: h2, label: "3".to_string() });
- nodes.push(SimNode { x: 4.0 + w1 + w2 * 0.5, y: 4.0 + h2, w: w2 * 0.5, h: h2, label: "4".to_string() });
- }
- PreviewLayoutMode::Floating => {
- nodes.push(SimNode { x: 4.0, y: 6.0, w: preview_w * 0.45, h: preview_h * 0.5, label: "1".to_string() });
- nodes.push(SimNode { x: preview_w * 0.4, y: 12.0, w: preview_w * 0.5, h: preview_h * 0.45, label: "2".to_string() });
- nodes.push(SimNode { x: 8.0, y: preview_h * 0.4, w: preview_w * 0.55, h: preview_h * 0.5, label: "3".to_string() });
- }
- }
-
- for node in nodes {
+ for node in self.sim_nodes(preview_w, preview_h) {
let rect_x = preview_x + node.x;
let rect_y = preview_y + node.y;
let node_bg = if self.is_active { [0.30, 0.45, 0.65, 0.45] } else { [0.20, 0.24, 0.30, 0.25] };
let node_border = if self.is_active { [0.45, 0.65, 0.90, 0.85] } else { [0.35, 0.40, 0.45, 0.55] };
- quads.push((rect_x, rect_y, node.w, node.h, node_bg));
- quads.push((rect_x, rect_y, node.w, 1.0, node_border));
- quads.push((rect_x, rect_y + node.h - 1.0, node.w, 1.0, node_border));
- quads.push((rect_x, rect_y, 1.0, node.h, node_border));
- quads.push((rect_x + node.w - 1.0, rect_y, 1.0, node.h, node_border));
- }
- }
-
- quads
- }
-
- fn text_labels(&self) -> Vec<TextLabel> {
- let (x, y, w, h) = self.rect();
- let mut labels = Vec::new();
+ ctx.quad(Rect { x: rect_x, y: rect_y, width: node.w, height: node.h }, node_bg);
+ ctx.quad(Rect { x: rect_x, y: rect_y, width: node.w, height: 1.0 }, node_border);
+ ctx.quad(Rect { x: rect_x, y: rect_y + node.h - 1.0, width: node.w, height: 1.0 }, node_border);
+ ctx.quad(Rect { x: rect_x, y: rect_y, width: 1.0, height: node.h }, node_border);
+ ctx.quad(Rect { x: rect_x + node.w - 1.0, y: rect_y, width: 1.0, height: node.h }, node_border);
- labels.push(TextLabel {
- text: self.base.label.clone().unwrap_or_else(|| "TAG".to_string()),
- x: x + 8.0,
- y: y + 8.0,
- font_size: 10.0,
- color: [140, 140, 153],
- });
-
- let layout_name = match self.mode {
- PreviewLayoutMode::Fullscreen => "Fullscreen",
- PreviewLayoutMode::Cascade => "Cascade",
- PreviewLayoutMode::Stack => "Stack",
- PreviewLayoutMode::Grid => "Grid",
- PreviewLayoutMode::LeftTiled => "L-Tiled",
- PreviewLayoutMode::RightTiled => "R-Tiled",
- PreviewLayoutMode::Equal => "Equal",
- PreviewLayoutMode::Spiral => "Spiral",
- PreviewLayoutMode::Floating => "Floating",
- };
-
- labels.push(TextLabel {
- text: layout_name.to_string(),
- x: x + 8.0,
- y: y + 20.0,
- font_size: 13.0,
- color: [230, 230, 242],
- });
-
- let preview_x = x + 8.0;
- let preview_y = y + 38.0;
- let preview_w = w - 16.0;
- let preview_h = h - 46.0;
-
- if preview_w > 0.0 && preview_h > 0.0 {
- let mut nodes = Vec::new();
- match self.mode {
- PreviewLayoutMode::Fullscreen => {
- nodes.push(SimNode { x: 2.0, y: 2.0, w: preview_w - 4.0, h: preview_h - 4.0, label: "F".to_string() });
- }
- PreviewLayoutMode::Cascade => {
- nodes.push(SimNode { x: 2.0, y: 2.0, w: preview_w - 12.0, h: preview_h - 12.0, label: "1".to_string() });
- nodes.push(SimNode { x: 6.0, y: 6.0, w: preview_w - 12.0, h: preview_h - 12.0, label: "2".to_string() });
- nodes.push(SimNode { x: 10.0, y: 10.0, w: preview_w - 12.0, h: preview_h - 12.0, label: "3".to_string() });
- }
- PreviewLayoutMode::Stack => {
- nodes.push(SimNode { x: 2.0, y: 2.0, w: preview_w - 4.0, h: preview_h - 4.0, label: "Stack".to_string() });
- }
- PreviewLayoutMode::Grid => {
- let hw = (preview_w - 6.0) / 2.0;
- let hh = (preview_h - 6.0) / 2.0;
- nodes.push(SimNode { x: 2.0, y: 2.0, w: hw, h: hh, label: "1".to_string() });
- nodes.push(SimNode { x: 4.0 + hw, y: 2.0, w: hw, h: hh, label: "2".to_string() });
- nodes.push(SimNode { x: 2.0, y: 4.0 + hh, w: hw, h: hh, label: "3".to_string() });
- nodes.push(SimNode { x: 4.0 + hw, y: 4.0 + hh, w: hw, h: hh, label: "4".to_string() });
- }
- PreviewLayoutMode::LeftTiled => {
- let mw = (preview_w - 6.0) * 0.55;
- let sw = (preview_w - 6.0) - mw;
- let sh = (preview_h - 6.0) / 2.0;
- nodes.push(SimNode { x: 2.0, y: 2.0, w: mw, h: preview_h - 4.0, label: "M".to_string() });
- nodes.push(SimNode { x: 4.0 + mw, y: 2.0, w: sw, h: sh, label: "1".to_string() });
- nodes.push(SimNode { x: 4.0 + mw, y: 4.0 + sh, w: sw, h: sh, label: "2".to_string() });
- }
- PreviewLayoutMode::RightTiled => {
- let mw = (preview_w - 6.0) * 0.55;
- let sw = (preview_w - 6.0) - mw;
- let sh = (preview_h - 6.0) / 2.0;
- nodes.push(SimNode { x: 2.0, y: 2.0, w: sw, h: sh, label: "1".to_string() });
- nodes.push(SimNode { x: 2.0, y: 4.0 + sh, w: sw, h: sh, label: "2".to_string() });
- nodes.push(SimNode { x: 4.0 + sw, y: 2.0, w: mw, h: preview_h - 4.0, label: "M".to_string() });
- }
- PreviewLayoutMode::Equal => {
- let ew = (preview_w - 8.0) / 3.0;
- nodes.push(SimNode { x: 2.0, y: 2.0, w: ew, h: preview_h - 4.0, label: "1".to_string() });
- nodes.push(SimNode { x: 4.0 + ew, y: 2.0, w: ew, h: preview_h - 4.0, label: "2".to_string() });
- nodes.push(SimNode { x: 6.0 + 2.0 * ew, y: 2.0, w: ew, h: preview_h - 4.0, label: "3".to_string() });
- }
- PreviewLayoutMode::Spiral => {
- let w1 = (preview_w - 6.0) * 0.5;
- let w2 = (preview_w - 6.0) - w1;
- let h2 = (preview_h - 6.0) * 0.5;
- nodes.push(SimNode { x: 2.0, y: 2.0, w: w1, h: preview_h - 4.0, label: "1".to_string() });
- nodes.push(SimNode { x: 4.0 + w1, y: 2.0, w: w2, h: h2, label: "2".to_string() });
- nodes.push(SimNode { x: 4.0 + w1, y: 4.0 + h2, w: w2 * 0.5, h: h2, label: "3".to_string() });
- nodes.push(SimNode { x: 4.0 + w1 + w2 * 0.5, y: 4.0 + h2, w: w2 * 0.5, h: h2, label: "4".to_string() });
- }
- PreviewLayoutMode::Floating => {
- nodes.push(SimNode { x: 4.0, y: 6.0, w: preview_w * 0.45, h: preview_h * 0.5, label: "1".to_string() });
- nodes.push(SimNode { x: preview_w * 0.4, y: 12.0, w: preview_w * 0.5, h: preview_h * 0.45, label: "2".to_string() });
- nodes.push(SimNode { x: 8.0, y: preview_h * 0.4, w: preview_w * 0.55, h: preview_h * 0.5, label: "3".to_string() });
- }
- }
-
- for node in nodes {
- let rect_x = preview_x + node.x;
- let rect_y = preview_y + node.y;
let text_sz = 9.0;
let text_w = node.label.len() as f32 * 6.0;
let text_color = if self.is_active { [242, 242, 255] } else { [178, 178, 191] };
let tx_offset = ((node.w - text_w) / 2.0).max(1.0);
-
- labels.push(TextLabel {
- text: node.label,
- x: rect_x + tx_offset,
- y: crate::layout::align_text_y(rect_y, node.h, text_sz, 0.0),
- font_size: text_sz,
- color: text_color,
- });
+ ctx.text(
+ node.label,
+ rect_x + tx_offset,
+ crate::layout::align_text_y(rect_y, node.h, text_sz, 0.0),
+ text_sz,
+ text_color,
+ );
}
}
-
- labels
}
}
+
+impl Input for LayoutPreview {}
diff --git a/src/widget/display/preview.rs b/src/widget/display/preview.rs
index f8f5637..6bc0485 100644
--- a/src/widget/display/preview.rs
+++ b/src/widget/display/preview.rs
@@ -1,6 +1,17 @@
+//! Narrow-trait `PreviewState` (Phase 5t) — cce-files' file-preview pane: a procedural render
+//! of a preview section (text lines in monospace, RLE-drawn image pixels) over a details
+//! section, produced by an internal `WidgetCanvas: RenderTarget`. The canvas labels carry
+//! per-label fonts (content lines are monospace, metadata is default-font), which is exactly
+//! the [`Paint::serves_legacy_labels`] hatch from Phase 5s; the quads flow from
+//! [`Paint::paint`]. Plain `text_labels` stays EMPTY like legacy (the pane's text is served
+//! only through the font-and-bounds getter — emitting it as prims too would double-render
+//! under container aggregation). The app owns all the data fields and mutates them through
+//! `Deref`; scrolling is the inherent [`PreviewState::handle_mouse_wheel`], driven by hand.
+
use std::path::PathBuf;
-use crate::widget::{Widget, Element};
-use crate::context::UiContext;
+use crate::scene::layout::Rect;
+use crate::scene::paint::PaintCtx;
+use crate::widget::{Input, Layout, Paint, UiContext};
use crate::layout::{RenderTarget, SectionContext};
use crate::color;
@@ -11,9 +22,9 @@ pub struct ImagePreviewData {
pub pixels: Vec<[u8; 4]>,
}
+#[derive(Debug, Clone)]
pub struct PreviewState {
- pub base: Widget,
- pub visible: bool,
+ rect: Rect,
pub path: Option<PathBuf>,
pub path_display: String,
pub name: String,
@@ -31,8 +42,7 @@ pub struct PreviewState {
impl Default for PreviewState {
fn default() -> Self {
Self {
- base: Widget::new(),
- visible: true,
+ rect: Rect { x: 0.0, y: 0.0, width: 0.0, height: 0.0 },
path: None,
path_display: String::new(),
name: String::new(),
@@ -49,48 +59,6 @@ impl Default for PreviewState {
}
}
-impl std::fmt::Debug for PreviewState {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- f.debug_struct("PreviewState")
- .field("base", &self.base)
- .field("visible", &self.visible)
- .field("path", &self.path)
- .field("path_display", &self.path_display)
- .field("name", &self.name)
- .field("is_dir", &self.is_dir)
- .field("size", &self.size)
- .field("permissions", &self.permissions)
- .field("modified", &self.modified)
- .field("file_type", &self.file_type)
- .field("target", &self.target)
- .field("content_preview", &self.content_preview)
- .field("image_preview", &self.image_preview)
- .field("scroll_line", &self.scroll_line)
- .finish()
- }
-}
-
-impl Clone for PreviewState {
- fn clone(&self) -> Self {
- Self {
- base: self.base.clone(),
- visible: self.visible,
- path: self.path.clone(),
- path_display: self.path_display.clone(),
- name: self.name.clone(),
- is_dir: self.is_dir,
- size: self.size.clone(),
- permissions: self.permissions.clone(),
- modified: self.modified.clone(),
- file_type: self.file_type.clone(),
- target: self.target.clone(),
- content_preview: self.content_preview.clone(),
- image_preview: self.image_preview.clone(),
- scroll_line: self.scroll_line,
- }
- }
-}
-
impl PreviewState {
pub fn handle_mouse_wheel(&mut self, delta: &crate::widget::MouseScrollDelta, ch: f32) -> bool {
let content = match &self.content_preview {
@@ -231,11 +199,11 @@ impl RenderTarget for WidgetCanvas {
impl PreviewState {
fn render_to_canvas(&self) -> WidgetCanvas {
let mut canvas = WidgetCanvas::new();
-
- let cx = self.base.x;
- let cy = self.base.y;
- let cw = self.base.w;
- let ch = self.base.h;
+
+ let cx = self.rect.x;
+ let cy = self.rect.y;
+ let cw = self.rect.width;
+ let ch = self.rect.height;
let text_fg = color::TEXT_FG;
let text_dim = color::TEXT_DIM;
@@ -264,17 +232,17 @@ impl PreviewState {
let box_h = rect_h;
let img_w = image_data.width as f32;
let img_h = image_data.height as f32;
-
+
let scale_x = box_w / img_w;
let scale_y = box_h / img_h;
let scale = scale_x.min(scale_y).min(4.0).max(1.0);
-
+
let draw_w = img_w * scale;
let draw_h = img_h * scale;
-
+
let start_x = cx + 12.0 + (box_w - draw_w) * 0.5;
let start_y = rect_y + (box_h - draw_h) * 0.5;
-
+
for row in 0..image_data.height {
let mut col = 0;
while col < image_data.width {
@@ -287,7 +255,7 @@ impl PreviewState {
let g = pixel[1];
let b = pixel[2];
let a = pixel[3];
-
+
let mut run_len = 1;
while col + run_len < image_data.width {
let next_idx = (row * image_data.width + col + run_len) as usize;
@@ -300,13 +268,13 @@ impl PreviewState {
break;
}
}
-
+
let alpha = a as f32 / 255.0;
if alpha > 0.0 {
let rf = r as f32 / 255.0;
let gf = g as f32 / 255.0;
let bf = b as f32 / 255.0;
-
+
canvas.rect(
[rf, gf, bf, alpha],
start_x + col as f32 * scale,
@@ -315,7 +283,7 @@ impl PreviewState {
scale,
);
}
-
+
col += run_len;
}
}
@@ -364,7 +332,7 @@ impl PreviewState {
let header_y = details_content_start_y + 6.0;
canvas.text(icon, cx + 12.0, header_y, 20.0, text_fg);
-
+
let name_truncated = if self.name.len() > 30 {
format!("{}...", &self.name[..27])
} else {
@@ -375,7 +343,7 @@ impl PreviewState {
let mut y = details_content_start_y + 36.0;
for (label, val) in &details {
canvas.text(label, cx + 12.0, y, 12.0, label_fg);
-
+
let val_str = if val.len() > 40 {
format!("...{}", &val[val.len() - 37..])
} else {
@@ -388,7 +356,7 @@ impl PreviewState {
if !self.target.is_empty() {
y += 8.0;
canvas.text("Target", cx + 12.0, y, 12.0, label_fg);
-
+
let target_str = if self.target.len() > 40 {
format!("...{}", &self.target[self.target.len() - 37..])
} else {
@@ -401,32 +369,33 @@ impl PreviewState {
}
}
-impl Element for PreviewState {
- crate::impl_widget_base!(PreviewState);
+impl Layout for PreviewState {
+ fn rect_assigned(&mut self, rect: Rect) {
+ self.rect = rect;
+ }
+}
+impl Paint for PreviewState {
fn color(&self) -> [f32; 4] {
[0.0, 0.0, 0.0, 0.0]
}
- fn visible(&self) -> bool {
- self.visible
+ /// Quads only: the canvas text carries per-label fonts and is served exclusively through
+ /// the labels hatch — legacy's plain `text_labels` was empty, and the scene path (which
+ /// drained it) accordingly showed no text either.
+ fn paint(&self, _rect: Rect, ctx: &mut PaintCtx) {
+ for (qx, qy, qw, qh, qc) in self.render_to_canvas().quads {
+ ctx.quad(Rect { x: qx, y: qy, width: qw, height: qh }, qc);
+ }
}
- fn set_visible(&mut self, visible: bool) {
- self.visible = visible;
+ fn serves_legacy_labels(&self) -> bool {
+ true
}
- fn all_quads(&self, _ctx: &UiContext) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
- if !self.visible {
- return Vec::new();
- }
- self.render_to_canvas().quads
- }
-
- fn text_labels_with_font_and_bounds(&self, _ctx: &UiContext) -> Vec<(crate::widget::display::TextLabel, Option<String>, Option<[f32; 4]>)> {
- if !self.visible {
- return Vec::new();
- }
+ fn legacy_labels_with_font_and_bounds(&self, _rect: Rect, _ctx: &UiContext) -> Vec<(crate::widget::display::TextLabel, Option<String>, Option<[f32; 4]>)> {
self.render_to_canvas().labels
}
}
+
+impl Input for PreviewState {}
diff --git a/src/widget/display/status_bar.rs b/src/widget/display/status_bar.rs
index 177dc44..44432c3 100644
--- a/src/widget/display/status_bar.rs
+++ b/src/widget/display/status_bar.rs
@@ -1,12 +1,20 @@
+//! Narrow-trait `StatusBar` (Phase 5t) — a one-line text bar whose theming is parent-coupled
+//! exactly like MenuBar's: when its tracked parent is a Backplate it pulls the backplate
+//! statusbar color/text-color/blur and derives its rounded corners from where it sits against
+//! the parent's edges ([`Paint::corner_style`] + the corners walk). Two text paths, both
+//! legacy: `TextLabel`s out of [`Paint::paint`] (container aggregation — deliberately with NO
+//! `widget_font`, matching the legacy default-font behavior on that path), and pre-shaped
+//! glyphon buffers through [`Paint::text_items`] (new with this migration) for manual hosts —
+//! cce-status-interface calls `prepare_text` then `get_text_items` into its own paint.
+
use crate::colors;
-use crate::widget::*;
-use crate::widget::display::{TextLabel, make_widget_text_buffer};
-use crate::context::UiContext;
+use crate::scene::layout::Rect;
+use crate::scene::paint::PaintCtx;
+use crate::widget::display::make_widget_text_buffer;
+use crate::widget::{Adapted, Element, Input, Layout, Paint};
pub struct StatusBar {
- pub base: Widget,
- x: f32, y: f32, w: f32, h: f32,
- hovered: bool,
+ rect: Rect,
pub text: String,
pub text_buf: Option<glyphon::Buffer>,
pub text_offset_x: Option<f32>,
@@ -16,38 +24,18 @@ pub struct StatusBar {
}
impl StatusBar {
- pub fn new() -> Self {
- Self {
- base: Widget::new_rect(0.0, 0.0, 0.0, 0.0),
- x: 0.0,
- y: 0.0,
- w: 0.0,
- h: 0.0,
- hovered: false,
+ pub fn new() -> Adapted<StatusBar> {
+ Adapted::new(StatusBar {
+ rect: Rect { x: 0.0, y: 0.0, width: 0.0, height: 0.0 },
text: String::new(),
text_buf: None,
text_offset_x: None,
text_color: None,
bg_color: None,
parent: None,
- }
- }
- pub fn with_text(mut self, text: &str) -> Self {
- self.text = text.to_string();
- self
- }
- pub fn with_text_offset_x(mut self, offset: f32) -> Self {
- self.text_offset_x = Some(offset);
- self
- }
- pub fn with_text_color(mut self, color: [f32; 4]) -> Self {
- self.text_color = Some(color);
- self
- }
- pub fn with_bg_color(mut self, color: [f32; 4]) -> Self {
- self.bg_color = Some(color);
- self
+ })
}
+
pub fn set_text_offset_x(&mut self, offset: f32) {
self.text_offset_x = Some(offset);
}
@@ -75,23 +63,8 @@ impl StatusBar {
}
false
}
-}
-impl Element for StatusBar {
- fn base(&self) -> Option<&Widget> { Some(&self.base) }
- fn base_mut(&mut self) -> Option<&mut Widget> { Some(&mut self.base) }
- fn rect(&self) -> (f32, f32, f32, f32) { (self.x, self.y, self.w, self.h) }
- fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
- self.x = x; self.y = y; self.w = w; self.h = h;
- self.base.x = x; self.base.y = y; self.base.w = w; self.base.h = h;
- }
- fn as_ptr(&self) -> *mut (dyn Element + 'static) {
- self as *const Self as *mut Self as *mut (dyn Element + 'static)
- }
- fn as_ptr_mut(&mut self) -> *mut (dyn Element + 'static) {
- self as *mut Self as *mut (dyn Element + 'static)
- }
- fn color(&self) -> [f32; 4] {
+ fn bg(&self) -> [f32; 4] {
if let Some(p_ptr) = self.parent {
if unsafe { (*p_ptr).is_backplate() } {
let theme_color = crate::colors::backplate_statusbar_color();
@@ -102,27 +75,16 @@ impl Element for StatusBar {
}
self.bg_color.unwrap_or(colors::STATUS_BG)
}
- fn set_hovered(&mut self, v: bool) { self.hovered = v; }
- fn hovered(&self) -> bool { self.hovered }
- fn blocks_backplate_drag(&self) -> bool { false }
-
- fn parent(&self, _ctx: &UiContext) -> Option<*mut (dyn Element + 'static)> {
- self.parent
- }
-
- fn set_parent(&mut self, parent: Option<*mut (dyn Element + 'static)>, _ctx: &mut UiContext) {
- self.parent = parent;
- }
- fn rounded_corners(&self) -> (bool, bool, bool, bool) {
+ fn corners_against_parent(&self, rect: Rect) -> (bool, bool, bool, bool) {
if let Some(p_ptr) = self.parent {
let is_bp = unsafe { (*p_ptr).is_backplate() };
if is_bp {
let (px, py, pw, ph) = unsafe { (*p_ptr).rect() };
- let (x, y, w, h) = self.rect();
+ let (x, y, w, h) = (rect.x, rect.y, rect.width, rect.height);
let is_at_top = (y - py).abs() < 0.1;
let is_at_bottom = (y + h - (py + ph)).abs() < 0.1;
-
+
if is_at_top && is_at_bottom {
let is_at_left = (x - px).abs() < 0.1;
let is_at_right = (x + w - (px + pw)).abs() < 0.1;
@@ -137,29 +99,106 @@ impl Element for StatusBar {
(false, false, false, false)
}
- fn corner_radius(&self) -> f32 {
- if let Some(p_ptr) = self.parent {
- unsafe { (*p_ptr).corner_radius() }
- } else {
- 0.0
- }
+ fn statusbar_font_size(&self) -> f32 {
+ let (_, font_size) = crate::layout::statusbar_font_parsed();
+ if font_size > 0.0 { font_size } else { 12.0 }
}
+}
- fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
- let (r1, r2, r3, r4) = self.rounded_corners();
- if !r1 && !r2 && !r3 && !r4 {
- vec![(self.x, self.y, self.w, self.h, self.color())]
- } else {
- Vec::new()
- }
+impl Adapted<StatusBar> {
+ pub fn with_text(mut self, text: &str) -> Self {
+ self.text = text.to_string();
+ self
}
- fn set_text(&mut self, text: &str) {
- if self.text != text {
- self.text = text.to_string();
+ pub fn with_text_offset_x(mut self, offset: f32) -> Self {
+ self.text_offset_x = Some(offset);
+ self
+ }
+ pub fn with_text_color(mut self, color: [f32; 4]) -> Self {
+ self.text_color = Some(color);
+ self
+ }
+ pub fn with_bg_color(mut self, color: [f32; 4]) -> Self {
+ self.bg_color = Some(color);
+ self
+ }
+}
+
+impl Layout for StatusBar {
+ /// The status text draws inside the bar; the base label must never inflate the rect or
+ /// emit a detached label (`Element::set_text` writes both the base copy and
+ /// [`Paint::sync_label`]).
+ fn inline_label(&self) -> bool {
+ true
+ }
+
+ fn rect_assigned(&mut self, rect: Rect) {
+ self.rect = rect;
+ }
+
+ fn parent_changed(&mut self, parent: Option<*mut (dyn Element + 'static)>) {
+ self.parent = parent;
+ }
+
+ fn tracked_parent(&self) -> Option<Option<*mut (dyn Element + 'static)>> {
+ Some(self.parent)
+ }
+}
+
+impl Paint for StatusBar {
+ fn color(&self) -> [f32; 4] {
+ self.bg()
+ }
+
+ fn corner_style(&self, rect: Rect) -> Option<(f32, (bool, bool, bool, bool))> {
+ let radius = match self.parent {
+ Some(p_ptr) => unsafe { (*p_ptr).corner_radius() },
+ None => 0.0,
+ };
+ Some((radius, self.corners_against_parent(rect)))
+ }
+
+ /// `Element::set_text` lands here: swap the text and drop the shaped buffer so
+ /// `prepare_text` rebuilds it.
+ fn sync_label(&mut self, label: &str) {
+ if self.text != label {
+ self.text = label.to_string();
self.text_buf = None;
}
}
- fn prepare_text(&mut self, fs: &mut glyphon::FontSystem) {
+
+ /// Background exactly on the legacy split: a plain quad when cornerless (the legacy
+ /// `extra_quads` body), a rounded rect against the parent's corners otherwise (the legacy
+ /// default `all_rounded_quads` path) — plus the text label (the legacy `text_labels`
+ /// body; deliberately no `widget_font`, see module docs).
+ fn paint(&self, rect: Rect, ctx: &mut PaintCtx) {
+ let corners = self.corners_against_parent(rect);
+ let bg = self.bg();
+ if corners == (false, false, false, false) {
+ ctx.quad(rect, bg);
+ } else if bg[3].abs() > 0.001 {
+ let radius = match self.parent {
+ Some(p_ptr) => unsafe { (*p_ptr).corner_radius() },
+ None => 0.0,
+ };
+ ctx.rounded_rect(rect, radius, corners, bg);
+ }
+
+ if !self.text.is_empty() {
+ let offset_x = self.text_offset_x.unwrap_or(12.0);
+ let c = self.get_actual_text_color();
+ let color = [
+ (c[0] * 255.0) as u8,
+ (c[1] * 255.0) as u8,
+ (c[2] * 255.0) as u8,
+ ];
+ let size = self.statusbar_font_size();
+ let text_y = crate::layout::align_text_y(rect.y, rect.height, size, 0.0);
+ ctx.text(self.text.clone(), rect.x + offset_x, text_y, size, color);
+ }
+ }
+
+ fn prepare_text(&mut self, fs: &mut glyphon::FontSystem, _rect: Rect) {
if !self.text.is_empty() && self.text_buf.is_none() {
let (font_fam, font_size) = crate::layout::statusbar_font_parsed();
let size = if font_size > 0.0 { font_size } else { 12.0 };
@@ -167,7 +206,8 @@ impl Element for StatusBar {
self.text_buf = Some(make_widget_text_buffer(fs, &self.text, size, &fam));
}
}
- fn get_text_items(&self) -> Vec<(&glyphon::Buffer, f32, f32, glyphon::Color)> {
+
+ fn text_items(&self) -> Vec<(&glyphon::Buffer, f32, f32, glyphon::Color)> {
if let Some(ref text_buf) = self.text_buf {
let offset_x = self.text_offset_x.unwrap_or(12.0);
let c = self.get_actual_text_color();
@@ -176,35 +216,52 @@ impl Element for StatusBar {
(c[1] * 255.0) as u8,
(c[2] * 255.0) as u8,
);
- let (_, font_size) = crate::layout::statusbar_font_parsed();
- let size = if font_size > 0.0 { font_size } else { 12.0 };
- let text_y = crate::layout::align_text_y(self.y, self.h, size, 0.0);
- vec![(text_buf, self.x + offset_x, text_y, color)]
+ let size = self.statusbar_font_size();
+ let text_y = crate::layout::align_text_y(self.rect.y, self.rect.height, size, 0.0);
+ vec![(text_buf, self.rect.x + offset_x, text_y, color)]
} else {
Vec::new()
}
}
- fn text_labels(&self) -> Vec<TextLabel> {
- if !self.text.is_empty() {
- let offset_x = self.text_offset_x.unwrap_or(12.0);
- let c = self.get_actual_text_color();
- let color = [
- (c[0] * 255.0) as u8,
- (c[1] * 255.0) as u8,
- (c[2] * 255.0) as u8,
- ];
- let (_, font_size) = crate::layout::statusbar_font_parsed();
- let size = if font_size > 0.0 { font_size } else { 12.0 };
- let text_y = crate::layout::align_text_y(self.y, self.h, size, 0.0);
- vec![TextLabel {
- text: self.text.clone(),
- x: self.x + offset_x,
- y: text_y,
- font_size: size,
- color,
- }]
- } else {
- Vec::new()
- }
+}
+
+impl Input for StatusBar {
+ fn blocks_backplate_drag(&self) -> bool {
+ false
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::widget::Element;
+
+ /// The manual-host path cce-status-interface drives by hand: `set_text` drops the shaped
+ /// buffer, `prepare_text` rebuilds it, `get_text_items` serves it (the new
+ /// `Paint::text_items` hook) at the bar's rect.
+ #[test]
+ fn manual_host_text_pipeline() {
+ let mut fs = glyphon::FontSystem::new();
+ let mut bar = StatusBar::new().with_text("hello").with_text_offset_x(15.0);
+ Element::set_rect(&mut bar, 0.0, 570.0, 800.0, 30.0);
+
+ assert!(Element::get_text_items(&bar).is_empty(), "no buffer before prepare_text");
+ Element::prepare_text(&mut bar, &mut fs);
+ let items = Element::get_text_items(&bar);
+ assert_eq!(items.len(), 1, "one shaped buffer");
+ assert_eq!(items[0].1, 15.0, "x = rect.x + text_offset_x");
+
+ // set_text drops the stale buffer; prepare_text reshapes.
+ Element::set_text(&mut bar, "world");
+ assert!(Element::get_text_items(&bar).is_empty(), "buffer dropped on text change");
+ Element::prepare_text(&mut bar, &mut fs);
+ assert_eq!(Element::get_text_items(&bar).len(), 1);
+ assert_eq!(bar.text, "world");
+
+ // Parentless: cornerless plain bg through the plain-quad bridge, at STATUS_BG.
+ let extra = Element::extra_quads(&bar);
+ assert_eq!(extra.len(), 1, "cornerless bg quad");
+ assert_eq!(Element::rounded_corners(&bar), (false, false, false, false));
+ assert!(!Element::blocks_backplate_drag(&bar));
}
}
diff --git a/src/widget/model.rs b/src/widget/model.rs
index 8bcae4d..d5d143d 100644
--- a/src/widget/model.rs
+++ b/src/widget/model.rs
@@ -340,6 +340,17 @@ pub trait Paint {
fn legacy_labels_with_font_and_bounds(&self, _rect: Rect, _ctx: &UiContext) -> Vec<(TextLabel, Option<String>, Option<[f32; 4]>)> {
Vec::new()
}
+
+ /// Shaped glyphon buffers for the legacy `Element::get_text_items` path — hosts that
+ /// build text off pre-shaped buffers instead of `TextLabel`s (cce-status-interface drives
+ /// its StatusBar by hand: `prepare_text` then `get_text_items` into its own paint).
+ /// Prim-derived text can't serve this (the getter returns borrows of buffers the widget
+ /// owns), so the widget serves them itself; shape the buffers in
+ /// [`prepare_text`](Paint::prepare_text). Default: none — the `Element` default most
+ /// widgets kept.
+ fn text_items(&self) -> Vec<(&glyphon::Buffer, f32, f32, glyphon::Color)> {
+ Vec::new()
+ }
}
/// What an event handler may reach beyond its own state — the RFC §3.5 `EventCtx`, grown as
@@ -658,6 +669,14 @@ impl<W: Layout + Paint + Input + 'static> Drop for Adapted<W> {
}
}
+/// Plain-data widgets constructed via `Default` (PreviewState in cce-files) keep their
+/// construction sites when the wrapper lands.
+impl<W: Layout + Paint + Input + Default + 'static> Default for Adapted<W> {
+ fn default() -> Self {
+ Adapted::new(W::default())
+ }
+}
+
impl<W: Layout + Paint + Input + 'static> Adapted<W> {
/// Wrap `inner` with a fresh [`Widget`] base.
pub fn new(inner: W) -> Self {
@@ -1014,6 +1033,9 @@ impl<W: Layout + Paint + Input + 'static> Element for Adapted<W> {
fn get_text_items(&self) -> Vec<(&glyphon::Buffer, f32, f32, glyphon::Color)> {
let mut items = Vec::new();
if self.visible() {
+ // Own shaped buffers ([`Paint::text_items`] — StatusBar's manual-host path), then
+ // the container recursion.
+ items.extend(Paint::text_items(&self.inner));
for child in self.visible_children() {
items.extend(unsafe { &*child }.get_text_items());
}