cloud storage client
git clone https://git.lucas.co/cce-cloud.git
Spacing: the ladder, not literals
The launcher named its own spacing numbers; every one is now the toolkit's
rung getter, read where it is used, so a config change to the ladder moves
the popup with the rest of the DE.
FuzzelWidget (src/main.rs):
- `const PAD = 15.0` (inset from the popup edge to the tab strip, the search
well and the list, on all four sides; the scroll region's rect; the paint,
label and hit-test paths, including a stray `let pad = 15.0` in icon_rect)
-> `cce_ui::layout::root_plate_inset()`. Const deleted.
- `TAB_STRIP_H = 30.0` with `- 8.0` for the run -> `TAB_RUN_H = 22.0` (the
run's height, a size) plus `root_plate_gap()` under it.
- `SEARCH_H + 10.0` (gap between the well and the list) -> `root_plate_gap()`.
- `pad + 10.0` (text inset in the well and in a row, and the icon column's
start) -> `text_x()`: the chip standoff plus `CONTROL_TEXT_INSET`, the
toolkit's one control-to-text inset.
- `const ICON_GAP = 8.0` (icon to label) -> `CONTROL_TEXT_INSET`. Deleted.
- `(widest + 20.0)` per tab title -> `2 * CONTROL_TEXT_INSET`, as a button label.
- `needed_height = 75.0 + …` and `needed_width = … + 50.0` (the popup's
chrome around its rows) -> `chrome_h()` / `chrome_w()`, derived from the
same rungs the layout uses.
- The chip's 2px standoff from the list edge is `CHIP_STANDOFF`, kept as a
deliberate hairline; `EDGE_GAP` (popup-to-output clearance) is kept as a
deliberate placement clearance, not a plate inset.
JSON layout (src/json_layout.rs and the size estimate in src/main.rs):
- `pad_x = 16.0` / `usable_w = bw - 32` / `page_current_y = 16` (inset from
the popup edge) -> `root_plate_inset()`.
- `spacing = 12.0` (gap between the widgets standing on the root plate)
-> `root_plate_gap()`.
- `+ 4.0` trailing padding -> the trailing gap traded for the inset below,
so the bottom inset is the top inset.
- `json_widget_desired_width`'s `+ 48/32/44/120/160` -> the two root-plate
insets plus the control's own width budget (the button's two text insets).
- The clip's `- 4.0` west of the content edge is `CLIP_SLACK`, deliberate.
- Tests assert against the getters instead of 16/12/268/32.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
src/json_layout.rs | 33 ++++++---
src/main.rs | 203 +++++++++++++++++++++++++++++++++--------------------
2 files changed, 150 insertions(+), 86 deletions(-)
diff --git a/src/json_layout.rs b/src/json_layout.rs
index 032e440..c3b5af8 100644
--- a/src/json_layout.rs
+++ b/src/json_layout.rs
@@ -11,6 +11,12 @@ use cce_ui::widget::{
};
use serde::Deserialize;
+/// style: deliberate — how far west of the content inset the panel's paint
+/// clip and text bounds begin, so a child drawn out to its own rect edge (a
+/// label's side bearing, a well's rim) is not cut at the inset. Slack, not a
+/// rung of the spacing ladder.
+const CLIP_SLACK: f32 = 4.0;
+
#[derive(Deserialize, Debug, Clone)]
pub struct JsonWidgetConfig {
#[serde(rename = "type")]
@@ -286,11 +292,13 @@ impl JsonLayoutWidget {
pub fn layout_children(&mut self) {
let (bx, by, bw, _) = self.rect();
- let pad_x = 16.0;
- let usable_w = bw - 2.0 * 16.0;
-
- let mut page_current_y = vec![16.0; 16]; // support up to 16 pages
- let spacing = 12.0;
+ // The widgets stand straight on the popup's root plate: the root-plate
+ // inset from its edge, the root-plate gap between them.
+ let pad_x = cce_ui::layout::root_plate_inset();
+ let usable_w = bw - 2.0 * pad_x;
+
+ let mut page_current_y = vec![pad_x; 16]; // support up to 16 pages
+ let spacing = cce_ui::layout::root_plate_gap();
for i in 0..self.widgets.len() {
let p_idx = self.widgets[i].page_idx;
@@ -344,10 +352,11 @@ impl JsonLayoutWidget {
*current_y += w_state.h + if run_continues { 0.0 } else { spacing };
}
- // Store total height of each page (adding a little padding at the end)
+ // Store total height of each page: the walk left a gap after the last
+ // widget, and what stands below it is the inset, not a gap.
for (i, &height) in page_current_y.iter().enumerate() {
if i < self.page_total_heights.len() {
- self.page_total_heights[i] = height + 4.0;
+ self.page_total_heights[i] = (height - spacing).max(pad_x) + pad_x;
}
}
}
@@ -637,8 +646,10 @@ impl cce_ui::widget::Paint for JsonLayoutWidget {
// labels here as well would double them. Page filtering and the panel clip
// mirror the dissolved `aggregate_quads` bounds.
let (bx, by, bw, bh) = self.rect();
- let pad_x = 16.0;
- let clip = Rect { x: bx + pad_x - 4.0, y: by, width: bw - (pad_x - 4.0), height: bh };
+ let pad_x = cce_ui::layout::root_plate_inset();
+ // style: deliberate — the clip starts CLIP_SLACK west of the content
+ // edge so a child painting out to its rect edge is not cut there.
+ let clip = Rect { x: bx + pad_x - CLIP_SLACK, y: by, width: bw - (pad_x - CLIP_SLACK), height: bh };
// One recess per run of adjacent menu rows, drawn BEFORE the rows so
// they sit inside it. A menu row draws no plate and no border of its
@@ -757,8 +768,8 @@ impl JsonLayoutWidget {
let (bx, by, bw, bh) = self.rect();
let active_page = self.active_page;
- let pad_x = 16.0;
- let content_bounds = Some([bx + pad_x - 4.0, by, bx + bw, by + bh]);
+ let pad_x = cce_ui::layout::root_plate_inset();
+ let content_bounds = Some([bx + pad_x - CLIP_SLACK, by, bx + bw, by + bh]);
for w in &self.widgets {
if w.page_idx != active_page {
diff --git a/src/main.rs b/src/main.rs
index 0a795b3..5814d51 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -704,20 +704,29 @@ pub struct TabPage {
query: String,
}
-/// Icon edge length inside a 25px row, and the gap between it and the label.
+/// Icon edge length inside a 25px row. The gap between it and the label is
+/// the toolkit's control text inset, the same standoff the label keeps from
+/// the selection chip's edge.
const ICON_PX: f32 = 17.0;
-const ICON_GAP: f32 = 8.0;
-/// The list chrome's metrics. These were repeated as bare `let pad = 15.0;`
-/// locals in every one of the paint, scroll and hit-test paths; the tab strip
-/// shifts the whole list down by its own height, so the offset has to be
-/// derived in one place or the rows, the clip and the click go out of step.
-const PAD: f32 = 15.0;
+/// The list chrome's metrics. The spacing around them — the inset from the
+/// popup edge, the gap under the tab strip and under the search well, the
+/// text inset inside a well or a row — is the toolkit's ladder
+/// (`cce_ui::layout::root_plate_inset` / `root_plate_gap` /
+/// `CONTROL_TEXT_INSET`), read where it is used; these were repeated as bare
+/// `let pad = 15.0;` locals in every one of the paint, scroll and hit-test
+/// paths. The tab strip shifts the whole list down by its own height, so the
+/// offset has to be derived in one place or the rows, the clip and the click
+/// go out of step.
const SEARCH_H: f32 = 35.0;
const ITEM_H: f32 = 25.0;
-/// Height the tab strip claims off the top: a 22px segmented run and the gap
-/// between it and the search well.
-const TAB_STRIP_H: f32 = 30.0;
+/// Height of the tab strip's segmented run; the strip claims this plus one
+/// root-plate gap off the top (see [`FuzzelWidget::tab_strip_h`]).
+const TAB_RUN_H: f32 = 22.0;
+/// style: deliberate — the hairline the selection chip (and the hover wash on
+/// its footprint) stands in from the list viewport on each side, so the chip's
+/// roll clears the clip. A standoff, not a rung of the spacing ladder.
+const CHIP_STANDOFF: f32 = 2.0;
/// Tab-title size — a step under the row labels, as a control label is.
const TAB_FONT_PX: f32 = 12.0;
@@ -825,19 +834,21 @@ impl FuzzelWidget {
self.switch_tab(idx)
}
- /// Height the tab strip takes off the top of the popup — 0 below two tabs.
+ /// Height the tab strip takes off the top of the popup — the run and the
+ /// gap between it and the search well; 0 below two tabs.
pub fn tab_strip_h(&self) -> f32 {
- if self.tabs.len() > 1 { TAB_STRIP_H } else { 0.0 }
+ if self.tabs.len() > 1 { TAB_RUN_H + cce_ui::layout::root_plate_gap() } else { 0.0 }
}
/// The segmented run itself, inset from the popup edge like the search
/// well under it. `None` when no strip is drawn.
fn tab_strip_rect(&self) -> Option<cce_ui::scene::layout::Rect> {
+ let inset = cce_ui::layout::root_plate_inset();
(self.tabs.len() > 1).then(|| cce_ui::scene::layout::Rect {
- x: self.x + PAD,
- y: self.y + PAD,
- width: self.w - PAD * 2.0,
- height: TAB_STRIP_H - 8.0,
+ x: self.x + inset,
+ y: self.y + inset,
+ width: self.w - inset * 2.0,
+ height: TAB_RUN_H,
})
}
@@ -866,19 +877,39 @@ impl FuzzelWidget {
/// Y of the search well's top edge: under the tab strip, where there is one.
fn search_y(&self) -> f32 {
- self.y + PAD + self.tab_strip_h()
+ self.y + cce_ui::layout::root_plate_inset() + self.tab_strip_h()
}
/// Y of the list viewport's top edge — the number the scroll math, the row
/// hit-test, the clip and the empty-state label all have to agree on.
fn list_y(&self) -> f32 {
- self.search_y() + SEARCH_H + 10.0
+ self.search_y() + SEARCH_H + cce_ui::layout::root_plate_gap()
}
/// Height of the list viewport: everything left between it and the bottom
/// inset.
fn list_h(&self) -> f32 {
- (self.y + self.h - PAD) - self.list_y()
+ (self.y + self.h - cce_ui::layout::root_plate_inset()) - self.list_y()
+ }
+
+ /// X of the text in a row (and of the query line in the search well): the
+ /// control text inset past the selection chip's edge, which itself stands
+ /// a hairline in from the list. Icons start here too.
+ fn text_x(&self) -> f32 {
+ self.x + cce_ui::layout::root_plate_inset() + CHIP_STANDOFF + cce_ui::layout::CONTROL_TEXT_INSET
+ }
+
+ /// Vertical chrome around the list: the inset above and below, the search
+ /// well and the gap under it, and the tab strip when there is one. What
+ /// the popup's height is over its rows.
+ pub fn chrome_h(&self) -> f32 {
+ 2.0 * cce_ui::layout::root_plate_inset() + self.tab_strip_h() + SEARCH_H + cce_ui::layout::root_plate_gap()
+ }
+
+ /// Horizontal chrome around a row's text: the text inset on both sides.
+ /// What the popup's width is over its widest label.
+ pub fn chrome_w(&self) -> f32 {
+ 2.0 * (self.text_x() - self.x)
}
/// Reserve the icon column only when some item on the ACTIVE tab resolved
@@ -887,7 +918,7 @@ impl FuzzelWidget {
/// (see [`Self::set_item_icons`]).
fn recompute_icon_gutter(&mut self) {
let any = self.all_items.iter().any(|t| self.icons.contains_key(t));
- self.icon_gutter = if any { ICON_PX + ICON_GAP } else { 0.0 };
+ self.icon_gutter = if any { ICON_PX + cce_ui::layout::CONTROL_TEXT_INSET } else { 0.0 };
}
/// Give rows an icon column. Only Apps mode calls this — Dmenu/Path items
@@ -900,14 +931,13 @@ impl FuzzelWidget {
/// The square an icon is fitted into for the row drawn at `draw_y`.
fn icon_rect(&self, draw_y: f32, item_h: f32, w: u32, h: u32) -> cce_ui::scene::layout::Rect {
- let pad = 15.0;
// Fit the longer side to ICON_PX so a non-square icon keeps its aspect
// ratio and stays centered in the column.
let (w, h) = (w.max(1) as f32, h.max(1) as f32);
let s = ICON_PX / w.max(h);
let (iw, ih) = (w * s, h * s);
cce_ui::scene::layout::Rect {
- x: self.x + pad + 10.0 + (ICON_PX - iw) / 2.0,
+ x: self.text_x() + (ICON_PX - iw) / 2.0,
y: draw_y + (item_h - ih) / 2.0,
width: iw,
height: ih,
@@ -1011,7 +1041,8 @@ impl cce_ui::widget::Layout for FuzzelWidget {
self.w = rect.width;
self.h = rect.height;
- self.scroll_box.set_rect(self.x + PAD, self.list_y(), self.w - PAD * 2.0, self.list_h());
+ let inset = cce_ui::layout::root_plate_inset();
+ self.scroll_box.set_rect(self.x + inset, self.list_y(), self.w - inset * 2.0, self.list_h());
self.update_scroll();
}
}
@@ -1023,7 +1054,7 @@ impl cce_ui::widget::Paint for FuzzelWidget {
fn paint(&self, _rect: cce_ui::scene::layout::Rect, ctx: &mut cce_ui::scene::paint::PaintCtx) {
use cce_ui::scene::layout::Rect;
- let pad = PAD;
+ let pad = cce_ui::layout::root_plate_inset();
// Tab strip — one well carved into the window plate with the segments
// butting together on its floor and the active one raised back out of
@@ -1106,10 +1137,10 @@ impl cce_ui::widget::Paint for FuzzelWidget {
// bar idles under the list bg and rides OVER the rows while
// raised, so the chip keeps its full width either way.
let sel = Rect {
- x: self.x + pad + 2.0,
+ x: self.x + pad + CHIP_STANDOFF,
y: draw_y,
- width: self.w - pad * 2.0 - 4.0,
- height: item_h - 2.0,
+ width: self.w - pad * 2.0 - 2.0 * CHIP_STANDOFF,
+ height: item_h - CHIP_STANDOFF,
};
let depth = cce_ui::color::plate_bevel_width().min(sel.height * 0.2);
ctx.bevel(sel, (4.0, 4.0, 4.0, 4.0), &cce_ui::scene::Material::from_fill([0.20, 0.35, 0.65, 0.9]), depth);
@@ -1124,10 +1155,10 @@ impl cce_ui::widget::Paint for FuzzelWidget {
if let Some(idx) = self.hovered.filter(|&i| i != self.selected) {
if let Some(draw_y) = self.scroll_box.get_draw_y(idx as f32 * item_h, item_h) {
let hov = Rect {
- x: self.x + pad + 2.0,
+ x: self.x + pad + CHIP_STANDOFF,
y: draw_y,
- width: self.w - pad * 2.0 - 4.0,
- height: item_h - 2.0,
+ width: self.w - pad * 2.0 - 2.0 * CHIP_STANDOFF,
+ height: item_h - CHIP_STANDOFF,
};
ctx.quad(hov, [0.20, 0.35, 0.65, 0.35]);
}
@@ -1233,6 +1264,8 @@ enum AppWindow {
}
/// Logical-px breathing room kept between a positioned popup and the screen edge.
+/// style: deliberate — a placement clearance against the OUTPUT edge, not an
+/// inset on any plate; the spacing ladder has no rung for it.
const EDGE_GAP: i32 = 8;
/// Logical geometry `(x, y, w, h)` of the output containing the point `(x, y)`, or —
@@ -1380,22 +1413,29 @@ struct State {
/// Logical-px width one JSON-layout widget wants for the auto-sizing popup.
///
-/// Buttons are the subtlety: `cce_ui::widget::Button` draws its label with an 8px inset on
-/// each side (see `Button::paint`) in the *button* font — not the menubar font `measure_text`
-/// assumes. So measure the label the way the button itself does (`measure_text_width` in the
-/// button font/size) and budget the button's 16px of inset on top of the container's 16px-per-
-/// side margins; otherwise a left-justified label starts 8px in and spills past the button's
-/// right edge (`JsonLayoutWidget::layout_children` sets `usable_w = width - 32`).
+/// Buttons are the subtlety: `cce_ui::widget::Button` draws its label with the control text
+/// inset on each side (see `Button::paint`) in the *button* font — not the menubar font
+/// `measure_text` assumes. So measure the label the way the button itself does
+/// (`measure_text_width` in the button font/size) and budget the button's two insets on top
+/// of the container's root-plate inset per side; otherwise a left-justified label starts
+/// an inset in and spills past the button's right edge (`JsonLayoutWidget::layout_children`
+/// sets `usable_w = width - 2 * root_plate_inset()`).
fn json_widget_desired_width(widget_type: &str, text: &str) -> f32 {
+ let margins = 2.0 * cce_ui::layout::root_plate_inset();
match widget_type {
"button" => {
let (family, size) = cce_ui::layout::parse_font_string(&cce_ui::layout::button_font());
- cce_ui::widget::display::measure_text_width(text, &family, size.unwrap_or(12.0)) + 48.0
+ cce_ui::widget::display::measure_text_width(text, &family, size.unwrap_or(12.0))
+ + margins
+ + 2.0 * cce_ui::layout::CONTROL_TEXT_INSET
}
- "label" => cce_ui::widget::display::measure_text(text, 13.0) + 32.0,
- "checkbox" => cce_ui::widget::display::measure_text(text, 13.0) + 44.0,
- "spinbox" | "color" => cce_ui::widget::display::measure_text(text, 13.0) + 120.0,
- "slider" => cce_ui::widget::display::measure_text(text, 13.0) + 160.0,
+ "label" => cce_ui::widget::display::measure_text(text, 13.0) + margins,
+ // The remainders are each control's own width beside its label (the
+ // checkbox's box column, the spinbox's field, the slider's track), not
+ // spacing.
+ "checkbox" => cce_ui::widget::display::measure_text(text, 13.0) + margins + 12.0,
+ "spinbox" | "color" => cce_ui::widget::display::measure_text(text, 13.0) + margins + 88.0,
+ "slider" => cce_ui::widget::display::measure_text(text, 13.0) + margins + 128.0,
_ => 150.0,
}
}
@@ -1449,7 +1489,12 @@ impl State {
max_widget_w.round() as u32
});
let h = config.height.unwrap_or_else(|| {
- let mut current_y = 16.0f32;
+ // The same walk as `JsonLayoutWidget::layout_children`:
+ // the root-plate inset above, a root-plate gap after each
+ // widget, and the trailing gap traded for the inset below.
+ let inset = cce_ui::layout::root_plate_inset();
+ let gap = cce_ui::layout::root_plate_gap();
+ let mut current_y = inset;
if let Some(ref widgets) = config.widgets {
for w_conf in widgets {
let h = match w_conf.widget_type.as_str() {
@@ -1460,12 +1505,12 @@ impl State {
"color" => 24.0,
_ => 20.0,
};
- current_y += h + 12.0;
+ current_y += h + gap;
}
} else if let Some(ref pages) = config.pages {
- let mut max_page_y = 16.0f32;
+ let mut max_page_y = inset;
for page in pages {
- let mut page_y = 16.0f32;
+ let mut page_y = inset;
for w_conf in &page.widgets {
let h = match w_conf.widget_type.as_str() {
"label" => 18.0,
@@ -1475,7 +1520,7 @@ impl State {
"color" => 24.0,
_ => 20.0,
};
- page_y += h + 12.0;
+ page_y += h + gap;
}
if page_y > max_page_y {
max_page_y = page_y;
@@ -1483,7 +1528,7 @@ impl State {
}
current_y = max_page_y;
}
- current_y += 4.0;
+ current_y = (current_y - gap).max(inset) + inset;
std::cmp::min(current_y.round() as u32, 600)
});
(w, h)
@@ -1824,7 +1869,7 @@ impl State {
}
let num_items = self.fuzzel.filtered_items.len();
let item_count = if num_items == 0 { 1 } else { num_items };
- let needed_height = 75.0 + self.fuzzel.tab_strip_h() + (item_count as f32) * 25.0;
+ let needed_height = self.fuzzel.chrome_h() + (item_count as f32) * ITEM_H;
let target_height = needed_height.min(self.max_height as f32);
// Calculate max text width
@@ -1869,14 +1914,16 @@ impl State {
let tw = buf.layout_runs().next().map(|r| r.line_w).unwrap_or(0.0);
widest = widest.max(tw);
}
- let strip_w = (widest + 20.0) * titles.len() as f32;
+ // Each title gets the control text inset on both sides, as a
+ // button label does.
+ let strip_w = (widest + 2.0 * cce_ui::layout::CONTROL_TEXT_INSET) * titles.len() as f32;
if strip_w > max_text_w {
max_text_w = strip_w;
}
}
let scrollbar_w = if needed_height > self.max_height as f32 { 10.0 } else { 0.0 };
- let needed_width = max_text_w + 50.0 + scrollbar_w;
+ let needed_width = max_text_w + self.fuzzel.chrome_w() + scrollbar_w;
let target_width = needed_width.clamp(300.0, self.max_width as f32);
self.resize_window(target_width.round() as u32, target_height.round() as u32);
@@ -4027,24 +4074,28 @@ mod tests {
#[test]
fn button_width_covers_label_inset() {
- // Regression: a left-justified Button draws its label 8px in from its own left edge
- // (Button::paint) in the button font. layout_children gives the button
- // `usable_w = popup_width - 32`. If the popup width doesn't budget the button's
- // 8px-per-side inset, the label spills past the button's right edge — the desktop
- // context-menu bug. Every desktop-menu label must fit within usable_w with the inset.
+ // Regression: a left-justified Button draws its label the control text inset in
+ // from its own left edge (Button::paint) in the button font. layout_children gives
+ // the button `usable_w = popup_width - 2 * root_plate_inset()`. If the popup width
+ // doesn't budget the button's inset per side, the label spills past the button's
+ // right edge — the desktop context-menu bug. Every desktop-menu label must fit
+ // within usable_w with the inset.
let (family, size) = cce_ui::layout::parse_font_string(&cce_ui::layout::button_font());
let size = size.unwrap_or(12.0);
+ let text_inset = cce_ui::layout::CONTROL_TEXT_INSET;
for label in [
"Terminal", "Files", "Data Editor", "Applications",
"System Settings", "Expose Windows", "Reload Config", "Logout",
] {
let popup_w = json_widget_desired_width("button", label);
- let usable_w = popup_w - 32.0; // container margins, per layout_children
+ // container margins, per layout_children
+ let usable_w = popup_w - 2.0 * cce_ui::layout::root_plate_inset();
let label_w = cce_ui::widget::display::measure_text_width(label, &family, size);
- // 8px left inset + label + 8px right breathing room must fit the button.
+ // left inset + label + right breathing room must fit the button.
assert!(
- usable_w >= label_w + 16.0,
- "button '{label}': usable_w {usable_w} < label {label_w} + 16 inset",
+ usable_w >= label_w + 2.0 * text_inset,
+ "button '{label}': usable_w {usable_w} < label {label_w} + {} inset",
+ 2.0 * text_inset,
);
}
}
@@ -4164,23 +4215,27 @@ mod tests {
assert!(layout.widgets[1].widget.as_dyn().as_any().downcast_ref::<cce_ui::widget::Checkbox>().is_some());
assert!(layout.widgets[2].widget.as_dyn().as_any().downcast_ref::<cce_ui::widget::Button>().is_some());
- // Check vertical sequence positions
- assert_eq!(w_label_y, 16.0);
+ // Check vertical sequence positions: the root-plate inset above, a
+ // root-plate gap after each widget.
+ let inset = cce_ui::layout::root_plate_inset();
+ let gap = cce_ui::layout::root_plate_gap();
+ assert_eq!(w_label_y, inset);
assert_eq!(w_label_h, 18.0);
- assert_eq!(w_check_y, 16.0 + 18.0 + 12.0); // y_prev + h_prev + spacing
+ assert_eq!(w_check_y, inset + 18.0 + gap); // y_prev + h_prev + spacing
assert_eq!(w_check_h, 22.0);
- assert_eq!(w_btn_y, w_check_y + 22.0 + 12.0);
+ assert_eq!(w_btn_y, w_check_y + 22.0 + gap);
assert_eq!(w_btn_h, 24.0);
- // Check horizontal positioning (should match usable width: 300 - 2 * 16 = 268)
- assert_eq!(w_label_x, 16.0);
- assert_eq!(w_label_w, 268.0);
- assert_eq!(w_check_x, 16.0);
- assert_eq!(w_check_w, 268.0);
- assert_eq!(w_btn_x, 16.0);
- assert_eq!(w_btn_w, 268.0);
+ // Check horizontal positioning (should match usable width: 300 - 2 * inset)
+ let usable_w = 300.0 - 2.0 * inset;
+ assert_eq!(w_label_x, inset);
+ assert_eq!(w_label_w, usable_w);
+ assert_eq!(w_check_x, inset);
+ assert_eq!(w_check_w, usable_w);
+ assert_eq!(w_btn_x, inset);
+ assert_eq!(w_btn_w, usable_w);
// Verify Checkbox initial state
assert_eq!(layout.widgets[1].widget.as_dyn().as_any().downcast_ref::<cce_ui::widget::Checkbox>().unwrap().checked(), false);
@@ -4526,7 +4581,6 @@ mod tests {
impl FuzzelWidget {
fn own_labels(&self) -> Vec<TextLabel> {
let mut labels = Vec::new();
- let pad = PAD;
// Tab titles, centred on their segments. Outside the list clip, like
// the rest of the chrome.
@@ -4562,7 +4616,7 @@ impl FuzzelWidget {
labels.push(TextLabel {
text: query_text,
- x: self.x + pad + 10.0,
+ x: self.text_x(),
y: self.search_y() + 9.0,
font_size: 14.0,
color: query_color,
@@ -4571,7 +4625,7 @@ impl FuzzelWidget {
if self.filtered_items.is_empty() {
labels.push(TextLabel {
text: "No matches found".to_string(),
- x: self.x + pad + 10.0,
+ x: self.text_x(),
y: self.list_y() + 4.0,
font_size: 13.0,
color: [0x88, 0x88, 0x99],
@@ -4585,7 +4639,6 @@ impl FuzzelWidget {
/// in) the viewport. Emitted under the paint walk's list clip, separately
/// from [`Self::own_labels`], which draws chrome outside it.
fn row_labels(&self) -> Vec<TextLabel> {
- let pad = PAD;
let item_h = ITEM_H;
let mut labels = Vec::new();
for (idx, item_text) in self.filtered_items.iter().enumerate() {
@@ -4604,7 +4657,7 @@ impl FuzzelWidget {
text: item_text.clone(),
// Indented past the icon column whether or not THIS row
// resolved an icon — see `icon_gutter`.
- x: self.x + pad + 10.0 + self.icon_gutter,
+ x: self.text_x() + self.icon_gutter,
y: draw_y + 4.0,
font_size: 13.0,
color,