GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
feat(label)!: one control-label convention, no label-in-recess tabs
Every labeled control now carries its label the same way: the adapter
draws it detached in the strip above the content rect, `control_label_strip`
tall (font size + margin, the one formula), at `DETACHED_LABEL_INSET`,
outside the control's own relief. The two places that pulled the label
INTO the control's recess — the Dropdown trigger's carve-out tab and the
Slider2D pad's shared `carve_labeled_well` — are gone: the plate or well is
the control alone (Slider2D is a plain 64px pad again).
The conventions that let labels differ per widget go with it:
- `Layout::inflates_label_rect` and the adapter's `label_inflation`: a
widget's rect is always its whole block (label strip + content), for
`set_rect`, `rect`, hit-testing and paint alike. `layout` lands the
content at the origin with the strip above; `render_widget` and the
Column/Row/Section builders speak the same block; `LayoutHelper` too.
- The "side" label layout (`style.control.label.layout`) and the 90px
`label_x_offset` it drove, in the trait, the painter, ParametersBg and
every model's `side_offset` replica.
- `widget::label_offset(w)` and its type-name exemption list: callers use
`WidgetHost::label_strip`, whose default is the base label's strip.
- Trackpad's model-drawn label (it rides the adapter now, like the rest).
- The per-model strip replicas (Dropdown, TextBox, Float3, Ramp, Slider2D)
collapse onto `slider::detached_strip` / `layout::control_label_strip`.
`Layout::inline_label` stays for widgets whose base label IS their content
(Button face, Checkbox row, Label, ListItem, InfoBox, StatusBar).
Co-Authored-By: Claude Fable 5.1 <[email protected]>
src/backend/window_runner.rs | 8 +-
src/layout.rs | 42 ++++-----
src/scene/painter.rs | 7 --
src/widget/container/parameters_bg.rs | 39 ++++-----
src/widget/core.rs | 13 +--
src/widget/display/float3.rs | 45 ++--------
src/widget/display/progress_bar.rs | 15 ++--
src/widget/input/button_strip.rs | 2 +-
src/widget/input/color_selector.rs | 6 +-
src/widget/input/dropdown.rs | 158 ++++------------------------------
src/widget/input/ramp.rs | 6 +-
src/widget/input/slider.rs | 156 +++------------------------------
src/widget/input/slider2d.rs | 48 ++---------
src/widget/input/spinbox.rs | 21 +----
src/widget/input/text_box.rs | 83 ++++++------------
src/widget/input/trackpad.rs | 51 ++---------
src/widget/layout_helper.rs | 12 +--
src/widget/mod.rs | 44 +++-------
src/widget/model.rs | 84 ++++++------------
19 files changed, 174 insertions(+), 666 deletions(-)
diff --git a/src/backend/window_runner.rs b/src/backend/window_runner.rs
index 3d4055e..e4df153 100644
--- a/src/backend/window_runner.rs
+++ b/src/backend/window_runner.rs
@@ -2787,7 +2787,7 @@ pub fn push_extra_quad_vertices(
}
let (wx, mut wy, ww, mut wh) = target_w.rect();
- let top_room = crate::widget::label_offset(target_w);
+ let top_room = target_w.label_strip();
wy += top_room;
wh -= top_room;
let extra_radii = crate::widget::CornerRadii::new(
@@ -2801,7 +2801,7 @@ pub fn push_extra_quad_vertices(
if let Some((color, thickness)) = target_w.solid_border() {
let (rx, mut ry, rw, mut rh) = target_w.rect();
- let top = crate::widget::label_offset(target_w);
+ let top = target_w.label_strip();
ry += top;
rh -= top;
if (qx - rx).abs() < 0.1 && (qy - ry).abs() < 0.1 && (qw - rw).abs() < 0.1 && (qh - rh).abs() < 0.1 {
@@ -2863,7 +2863,7 @@ pub fn push_extra_quad_vertices_clipped(
}
let (wx, mut wy, ww, mut wh) = target_w.rect();
- let top_room = crate::widget::label_offset(target_w);
+ let top_room = target_w.label_strip();
wy += top_room;
wh -= top_room;
let extra_radii = crate::widget::CornerRadii::new(
@@ -2877,7 +2877,7 @@ pub fn push_extra_quad_vertices_clipped(
if let Some((color, thickness)) = target_w.solid_border() {
let (rx, mut ry, rw, mut rh) = target_w.rect();
- let top = crate::widget::label_offset(target_w);
+ let top = target_w.label_strip();
ry += top;
rh -= top;
if (qx - rx).abs() < 0.1 && (qy - ry).abs() < 0.1 && (qw - rw).abs() < 0.1 && (qh - rh).abs() < 0.1 {
diff --git a/src/layout.rs b/src/layout.rs
index 855bf50..10230e2 100644
--- a/src/layout.rs
+++ b/src/layout.rs
@@ -86,7 +86,6 @@ fn flatten_json_to_flat_props(val: &serde_json::Value, prefix: &str, flat_props:
"style.control.label.font" => "control_label_font",
"style.control.label.font_detached" => "control_label_font_detached",
"style.control.label.margin" => "control_label_margin",
- "style.control.label.layout" => "control_label_layout",
"style.control.slider.height" => "slider_height",
"style.control.slider.corner_radius" => "slider_corner_radius",
"style.control.slider.band_thickness" => "slider_band_thickness",
@@ -389,7 +388,6 @@ static CONTROL_LABEL_FONT_CACHED: RwLock<Option<(String, f32)>> = RwLock::new(No
static CONTROL_LABEL_FONT_DETACHED: RwLock<String> = RwLock::new(String::new());
static CONTROL_LABEL_FONT_DETACHED_CACHED: RwLock<Option<(String, f32)>> = RwLock::new(None);
static CONTROL_LABEL_MARGIN: RwLock<f32> = RwLock::new(6.0);
-static CONTROL_LABEL_LAYOUT: RwLock<String> = RwLock::new(String::new());
static PLATE_CORNER_RADIUS: RwLock<f32> = RwLock::new(12.0);
static LIST_FONT: RwLock<String> = RwLock::new(String::new());
static LIST_FONT_CACHED: RwLock<Option<(String, f32)>> = RwLock::new(None);
@@ -464,13 +462,6 @@ pub fn reload_config() {
}
}
}
- if let Some(rest) = trimmed.strip_prefix("control_label_layout") {
- let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
- let val_str = rest.trim_end_matches('"').trim().to_string();
- if let Ok(mut lock) = CONTROL_LABEL_LAYOUT.write() {
- *lock = val_str;
- }
- }
if let Some(rest) = trimmed.strip_prefix("nested_section_label_alignment") {
let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
let val_str = rest.trim_end_matches('"').trim();
@@ -1228,13 +1219,9 @@ pub fn control_label_margin() -> f32 {
*CONTROL_LABEL_MARGIN.read().unwrap()
}
-pub fn control_label_layout() -> String {
- let lock = CONTROL_LABEL_LAYOUT.read().unwrap();
- if lock.is_empty() {
- "top".to_string()
- } else {
- lock.clone()
- }
+pub(crate) fn control_label_strip() -> f32 {
+ let (_, font_size) = control_label_font_detached_parsed();
+ font_size + control_label_margin()
}
pub fn label_margin() -> f32 {
@@ -3682,11 +3669,12 @@ pub fn render_widget<T: WidgetHost + 'static>(pc: &mut dyn RenderTarget, w: &mut
if let Some(w_id) = id {
ctx.register_widget(w_id, w as *mut T as *mut (dyn WidgetHost + 'static));
}
- // The legacy contract: `y` is the top of the detached label, `wh` the content
- // height below it. `layout` takes the CONTENT origin (the label hangs above it),
- // so step down by the strip.
+ // The flat-host contract, the same block `set_rect` takes: `(x, y)` is the top of
+ // the detached label and `wh` the block height, label strip included. `layout`
+ // takes the CONTENT origin and height, so step down by the strip.
let strip = w.label_strip();
- w.layout(crate::widget::Point { x, y: y + strip }, crate::widget::LayoutConstraints::new(ww, ww, wh, wh), ctx);
+ let content_h = (wh - strip).max(0.0);
+ w.layout(crate::widget::Point { x, y: y + strip }, crate::widget::LayoutConstraints::new(ww, ww, content_h, content_h), ctx);
// Shape, which on this path nobody else does. A flat host consumes
// `all_quads`, so `prepare_text` — where a TextBox records the per-glyph x
@@ -3703,7 +3691,7 @@ pub fn render_widget<T: WidgetHost + 'static>(pc: &mut dyn RenderTarget, w: &mut
let (style_r, corners) = w.corner_style();
let r = if corners != (false, false, false, false) { style_r } else { 0.0 };
let (wx, mut wy, www, mut whh) = w.rect();
- let top_room = crate::widget::label_offset(w);
+ let top_room = w.label_strip();
wy += top_room;
whh -= top_room;
@@ -4053,7 +4041,7 @@ impl Column {
if let Some(pref) = w.preferred_height() {
wh = pref;
}
- let top_room = crate::widget::label_offset(w);
+ let top_room = w.label_strip();
let total_h = wh + top_room;
let x = self.ax(x_off);
let y = self.ay();
@@ -4234,7 +4222,7 @@ impl Section {
wh = pref;
}
let pad = self.padding();
- let top_room = crate::widget::label_offset(w);
+ let top_room = w.label_strip();
let total_h = wh + top_room;
let name = w.type_name();
@@ -5426,7 +5414,7 @@ impl<'a, P: RenderTarget> SectionContext<'a, P> {
wh = pref;
}
let pad = self.padding();
- let top_room = crate::widget::label_offset(w);
+ let top_room = w.label_strip();
let total_h = wh + top_room;
let name = w.type_name();
@@ -5682,7 +5670,7 @@ impl<'b, 'a, P: RenderTarget> VStack<'b, 'a, P> {
let y = max_h;
let pref_h = w.preferred_height().unwrap_or(wh);
- let top_room = crate::widget::label_offset(w);
+ let top_room = w.label_strip();
let total_h = pref_h + top_room;
w.set_row_rect(self.context.left + pad, self.context.cw - 2.0 * pad);
@@ -5856,11 +5844,11 @@ mod tests {
impl WidgetHost for MockWidgetWithLabel {
crate::impl_widget_base!(MockWidgetWithLabel);
fn rect(&self) -> (f32, f32, f32, f32) {
- let offset = crate::widget::label_offset(self);
+ let offset = self.label_strip();
(self.base.x, self.base.y - offset, self.base.w, self.base.h + offset)
}
fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
- let offset = crate::widget::label_offset(self);
+ let offset = self.label_strip();
self.base.x = x;
self.base.y = y + offset;
self.base.w = w;
diff --git a/src/scene/painter.rs b/src/scene/painter.rs
index 500a8ee..21314f8 100644
--- a/src/scene/painter.rs
+++ b/src/scene/painter.rs
@@ -143,13 +143,6 @@ pub fn base_control_label(w: &dyn WidgetHost) -> Vec<TextLabel> {
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 = w.label_x_offset();
- 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 }];
}
}
diff --git a/src/widget/container/parameters_bg.rs b/src/widget/container/parameters_bg.rs
index 13254e0..d84c30c 100644
--- a/src/widget/container/parameters_bg.rs
+++ b/src/widget/container/parameters_bg.rs
@@ -613,12 +613,7 @@ impl ParametersBg {
// only the interior left edge. A ring encapsulated
// within the bevel doubled the valley on the adjoining
// sides.
- let label_top = if crate::layout::control_label_layout() == "side" {
- 0.0
- } else {
- crate::layout::control_label_font_detached_parsed().1
- + crate::layout::control_label_margin()
- };
+ let label_top = crate::layout::control_label_strip();
let band_h = r.3 - label_top;
let inset = crate::layout::bevel_width().min(band_h * 0.2);
let by = r.1 + label_top + inset;
@@ -1194,7 +1189,7 @@ impl ParametersBg {
if let Some(t) = &self.toggles[i] {
let (x, y, w, h) = t.rect();
if w > 0.0 && h > 0.0 {
- let ty = crate::widget::label_offset(t);
+ let ty = t.label_strip();
let rect = Rect { x, y: y + ty, width: w, height: h - ty };
for c in t.inner().flat_carves(rect) {
let raised = match c.kind {
@@ -1222,7 +1217,7 @@ impl ParametersBg {
// a fill with its own lit edge and stays on the widget's paint.
if let Some(c) = &self.colors[i] {
let (x, y, w, h) = c.rect();
- let ty = crate::widget::label_offset(c);
+ let ty = c.label_strip();
if let Some((rx, ry, rw, rh, rr, rd)) =
c.inner().field_relief(Rect { x, y: y + ty, width: w, height: h - ty })
{
@@ -1241,13 +1236,10 @@ impl ParametersBg {
if ww <= 0.0 || h <= 0.0 {
continue;
}
- // The side-label inset the widget's own paint applies (0 for the
- // exempt kinds — button, toggle), and the top-label band, which
- // stays outside the relief like every other host.
- let lx = w.label_x_offset();
- let ty = crate::widget::label_offset(w);
+ // The top-label band stays outside the relief like every other host.
+ let ty = w.label_strip();
let depth = crate::layout::bevel_width().min((h - ty) * 0.2);
- out.push((x + lx, y + ty, ww - lx, h - ty, r4(radius), depth, raised, all));
+ out.push((x, y + ty, ww, h - ty, r4(radius), depth, raised, all));
}
}
out
@@ -1283,7 +1275,7 @@ impl ParametersBg {
if let (Some(d), Some(tb)) = (&self.choices[i], &self.texts[i]) {
let (bx, by, bw, bh) = d.rect();
let (_, _, _, th) = tb.rect();
- let ty = crate::widget::label_offset(tb);
+ let ty = tb.label_strip();
if bw > 0.0 && bh > 0.0 {
let depth = crate::layout::bevel_width().min((th - ty) * 0.2);
let r = (crate::layout::textbox_corner_radius() - depth).max(2.0);
@@ -1294,7 +1286,7 @@ impl ParametersBg {
} else if p.2.starts_with("spinbox") {
if let Some(sb) = &self.spinboxes[i] {
let (x, y, w, h) = sb.rect();
- let ty = crate::widget::label_offset(sb);
+ let ty = sb.label_strip();
let band = Rect { x, y: y + ty, width: w, height: h - ty };
if let Some((_, Some(((run, radii, rd, edges), _)))) =
sb.inner().relief_parts(band)
@@ -1305,18 +1297,17 @@ impl ParametersBg {
} else if p.2.starts_with("choice") {
// The dropdown trigger: the widget's own raised paint is one
// `inset_plate` on its content band — the same ring here, on
- // the same band (side-label inset, top-label band excluded),
+ // the same band (top-label band excluded),
// same radius, same depth cap. The face stays the plate: the
// pane carries no dropdown fill (a `Border` face never reaches
// the rounded-quad view), so the trigger is flush and bare.
if let Some(d) = &self.choices[i] {
let (x, y, w, h) = d.rect();
if w > 0.0 && h > 0.0 {
- let lx = d.label_x_offset();
- let ty = crate::widget::label_offset(d);
+ let ty = d.label_strip();
let depth = crate::layout::bevel_width().min((h - ty) * 0.2);
let r = crate::layout::dropdown_corner_radius();
- out.push((x + lx, y + ty, w - lx, h - ty, (r, r, r, r), depth, (true, true, true, true)));
+ out.push((x, y + ty, w, h - ty, (r, r, r, r), depth, (true, true, true, true)));
}
}
} else if p.2 == "toggle" || p.2 == "checkbox" {
@@ -1326,7 +1317,7 @@ impl ParametersBg {
if let Some(t) = &self.toggles[i] {
let (x, y, w, h) = t.rect();
if w > 0.0 && h > 0.0 {
- let ty = crate::widget::label_offset(t);
+ let ty = t.label_strip();
let rect = Rect { x, y: y + ty, width: w, height: h - ty };
for c in t.inner().flat_carves(rect) {
if matches!(c.kind, crate::layout::CarveKind::Trough) {
@@ -1358,7 +1349,7 @@ impl ParametersBg {
}
if let Some(sb) = &self.spinboxes[i] {
let (x, y, w, h) = sb.rect();
- let ty = crate::widget::label_offset(sb);
+ let ty = sb.label_strip();
let band = Rect { x, y: y + ty, width: w, height: h - ty };
if let Some((_, Some(((_, _, rd, _), (sa, sb2, sw, host))))) =
sb.inner().relief_parts(band)
@@ -1675,7 +1666,7 @@ impl Input for ParametersBg {
if p.2.starts_with("slider") {
let r = rects[i];
if let Some(s) = &mut self.sliders[i] {
- let top = crate::widget::label_offset(s);
+ let top = s.label_strip();
if py >= r.1 + top && py <= r.1 + r.3 {
s.drag_begin(px, py);
self.dragging_param = Some(i);
@@ -2584,7 +2575,7 @@ impl Input for ParametersBg {
let latched = !ui.scroll_gesture_new
&& ui.scroll_initiate_widget_id == Some(s.base().id());
let (sx, sy, sw, sh) = s.rect();
- let ty = crate::widget::label_offset(s);
+ let ty = s.label_strip();
latched
|| s.inner().scroll_hit(
Rect { x: sx, y: sy + ty, width: sw, height: sh - ty },
diff --git a/src/widget/core.rs b/src/widget/core.rs
index 93834a1..a2ba8b1 100644
--- a/src/widget/core.rs
+++ b/src/widget/core.rs
@@ -740,16 +740,11 @@ impl Widget {
}
}
+ /// The detached-label strip this widget carries above its content: the one
+ /// control-label formula (`layout::control_label_strip`) when a label is set,
+ /// zero otherwise.
pub fn label_offset(&self) -> f32 {
- if crate::layout::control_label_layout() == "side" {
- return 0.0;
- }
- if self.label.is_some() {
- let (_, font_size) = crate::layout::control_label_font_detached_parsed();
- font_size + crate::layout::control_label_margin()
- } else {
- 0.0
- }
+ if self.label.is_some() { crate::layout::control_label_strip() } else { 0.0 }
}
}
diff --git a/src/widget/display/float3.rs b/src/widget/display/float3.rs
index 8bd82b8..bb0a283 100644
--- a/src/widget/display/float3.rs
+++ b/src/widget/display/float3.rs
@@ -1,8 +1,7 @@
//! Narrow-trait `Float3` — a labeled group of three STANDARD [`Slider`]s (X/Y/Z), each with
//! the toolkit's readout, embedded by value inside `ParametersBg` (its only consumer), which
//! drives it through direct `WidgetHost` calls. The group label is the ordinary detached
-//! control label (the adapter's, exactly like a slider row's — `inflates_label_rect = false`,
-//! the label eats into the assigned rect); below it sit three `Adapted<Slider>` children in
+//! control label (the adapter's, exactly like a slider row's); below it sit three `Adapted<Slider>` children in
//! whatever style the DE config gives every other slider (the band that swallowed the rodent,
//! the recessed well, the square track), each fronted by its axis letter. The model caches its
//! laid-out rect ([`Layout::rect_assigned`]) and lays the children out from it; paint and input
@@ -52,15 +51,9 @@ impl Float3 {
})
}
- /// The height a labeled (`labeled`) group lays out to: the detached label band (none in
- /// the side layout) plus three slider rows and their gaps — the row-height table entry.
+ /// The height a labeled (`labeled`) group lays out to: the detached label band plus three slider rows and their gaps — the row-height table entry.
pub fn preferred_height(labeled: bool) -> f32 {
- let top = if labeled && crate::layout::control_label_layout() != "side" {
- let (_, font_size) = crate::layout::control_label_font_detached_parsed();
- font_size + crate::layout::control_label_margin()
- } else {
- 0.0
- };
+ let top = if labeled { crate::layout::control_label_strip() } else { 0.0 };
top + 3.0 * crate::layout::slider_height() + 2.0 * ROW_GAP
}
@@ -95,36 +88,18 @@ impl Float3 {
&self.sliders
}
- /// Detached-label band above the rows — a replica of `Widget::label_offset` over the
- /// synced label (zero in the side layout or unlabeled), the Slider's own formula.
+ /// Detached-label band above the rows (zero unlabeled) — the adapter's
+ /// `Widget::label_offset` over the synced label.
fn label_top(&self) -> f32 {
- if crate::layout::control_label_layout() == "side" {
- return 0.0;
- }
- if self.label.is_some() {
- let (_, font_size) = crate::layout::control_label_font_detached_parsed();
- font_size + crate::layout::control_label_margin()
- } else {
- 0.0
- }
- }
-
- /// The side-layout label inset (`WidgetHost::label_x_offset` — this type is not exempt).
- fn side_offset(&self) -> f32 {
- if crate::layout::control_label_layout() == "side" && self.label.is_some() {
- 90.0
- } else {
- 0.0
- }
+ crate::widget::input::slider::detached_strip(&self.label)
}
/// The three slider rows' rects (`(x, y, w, h)`, X/Y/Z), laid out below the label band and
/// right of the axis-letter column. Each is exactly the rect its sub-slider is assigned.
pub fn get_row_rects(&self) -> Vec<(f32, f32, f32, f32)> {
let top = self.rect.y + self.label_top();
- let side = self.side_offset();
- let x = self.rect.x + side + AXIS_W;
- let w = (self.rect.width - side - AXIS_W).max(10.0);
+ let x = self.rect.x + AXIS_W;
+ let w = (self.rect.width - AXIS_W).max(10.0);
let h = crate::layout::slider_height();
(0..3).map(|i| (x, top + i as f32 * (h + ROW_GAP), w, h)).collect()
}
@@ -179,10 +154,6 @@ impl Adapted<Float3> {
impl Layout for Float3 {
/// The Slider convention: the label eats into the assigned rect, the host sizes the row
/// for it ([`Float3::preferred_height`]).
- fn inflates_label_rect(&self) -> bool {
- false
- }
-
/// The three rows alone: the adapter adds the detached-label strip itself
/// (`Adapted::preferred_height`), as it does for every non-inflating widget.
diff --git a/src/widget/display/progress_bar.rs b/src/widget/display/progress_bar.rs
index 8280af6..9bee403 100644
--- a/src/widget/display/progress_bar.rs
+++ b/src/widget/display/progress_bar.rs
@@ -140,18 +140,19 @@ mod tests {
}
}
- /// The detached-label convention survives the adapter: `set_rect` grows the widget by the
- /// label offset, and painting is inset below the label region (config-independent: the
- /// expected offset is derived from the observed rect).
+ /// The one detached-label convention: the assigned rect is the whole block, the
+ /// label strip at its top and the track painted below it (config-independent: the
+ /// strip is read back from the widget).
#[test]
- fn label_inflates_rect_and_insets_paint() {
+ fn label_strip_heads_the_block_and_insets_paint() {
let ctx = UiContext::new();
let mut bar = ProgressBar::new(0.5).with_recessed(false).with_label("Progress");
- WidgetHost::set_rect(&mut bar, 0.0, 10.0, 100.0, 8.0);
+ let offset = WidgetHost::label_strip(&bar);
+ assert!(offset > 0.0, "a labeled bar carries a strip");
+ WidgetHost::set_rect(&mut bar, 0.0, 10.0, 100.0, 8.0 + offset);
let (_, y, _, h) = WidgetHost::rect(&bar);
- let offset = h - 8.0;
- assert!(offset >= 0.0, "rect grew by the label offset");
+ assert_eq!(h, 8.0 + offset, "the rect is the block it was given");
assert_eq!(y, 10.0, "origin is unchanged");
let quads = WidgetHost::all_rounded_quads(&bar, &ctx);
diff --git a/src/widget/input/button_strip.rs b/src/widget/input/button_strip.rs
index 54d8451..81abe3d 100644
--- a/src/widget/input/button_strip.rs
+++ b/src/widget/input/button_strip.rs
@@ -69,7 +69,7 @@ impl ButtonStrip {
/// The laid-out content rect: the rect mirrored from the adapter by
/// `Layout::rect_assigned` (or the constructor arguments until the first layout)
- /// less the detached label strip the adapter inflated it by, so the segments,
+ /// less the detached label strip at the top of that block, so the segments,
/// the well and the hit-testing all sit below the label.
fn rect(&self) -> (f32, f32, f32, f32) {
let strip = crate::widget::input::slider::detached_strip(&self.label);
diff --git a/src/widget/input/color_selector.rs b/src/widget/input/color_selector.rs
index 6af8d99..8311104 100644
--- a/src/widget/input/color_selector.rs
+++ b/src/widget/input/color_selector.rs
@@ -226,9 +226,9 @@ impl Paint for ColorSelector {
/// The legacy `extra_quads` body against the laid-out rect (field, caret while
/// editing, and the soft-glow rounded color preview), plus the hex readout label.
fn paint(&self, rect: Rect, ctx: &mut PaintCtx) {
- // The well is color_selector_height tall, seated at the rect's TOP —
- // the label rides above the rect (inflating-label convention) and the
- // row's bottom band belongs to the NEXT row's label. Hosts that hand
+ // The well is color_selector_height tall, seated at the content rect's TOP —
+ // the label rides in the strip above it and the row's bottom band belongs
+ // to the NEXT row's label. Hosts that hand
// over a whole param row (ParametersBg's 40px color rows) get a
// standard control-height well instead of a row-tall one.
let well_h = crate::layout::color_selector_height().min(rect.height);
diff --git a/src/widget/input/dropdown.rs b/src/widget/input/dropdown.rs
index a9a2582..d22a935 100644
--- a/src/widget/input/dropdown.rs
+++ b/src/widget/input/dropdown.rs
@@ -1,7 +1,7 @@
//! Narrow-trait `Dropdown` (Phase 5p — first popover widget through `Paint::popover` /
-//! `draw_popover`, the 5o surface). Detached-label control on the Slider convention (no rect
-//! inflation; the label eats into the assigned rect), `Control::control_label`'s +4px inset via
-//! `Layout::detached_label_inset`, side-label inset computed from the synced label.
+//! `draw_popover`, the 5o surface). Detached-label control: the adapter draws the label in
+//! the strip above the trigger, at `Layout::detached_label_inset`; the trigger's plate is the
+//! control alone.
//!
//! Parity notes (all legacy-faithful, verified against the pre-migration impl):
//! - `parent_snapshot` is the data form of the legacy public, direct-write-only `parent`
@@ -72,16 +72,6 @@ fn text_advance(text: &str, font_family: &str, font_size: f32) -> f32 {
}
}
-/// Side-layout label inset — the legacy `WidgetHost::label_x_offset` default for non-exempt
-/// widgets (Dropdown was never in the exempt list).
-fn side_offset(label: &Option<String>) -> f32 {
- if crate::layout::control_label_layout() == "side" && label.is_some() {
- 90.0
- } else {
- 0.0
- }
-}
-
#[derive(Debug, Clone)]
pub struct Dropdown {
pub options: Vec<String>,
@@ -224,18 +214,10 @@ impl Dropdown {
text_advance(&self.display_text(), &font_family, font_size) + Self::LABEL_INSET
}
- /// The detached-label strip height above the content rect — a replica of
- /// `Widget::label_offset` over the synced label (zero in side layout or unlabeled).
+ /// The detached-label strip height above the content rect (zero unlabeled) —
+ /// the adapter's `Widget::label_offset` over the synced label.
fn label_top(&self) -> f32 {
- if crate::layout::control_label_layout() == "side" {
- return 0.0;
- }
- if self.label.is_some() {
- let (_, font_size) = crate::layout::control_label_font_detached_parsed();
- font_size + crate::layout::control_label_margin()
- } else {
- 0.0
- }
+ crate::widget::input::slider::detached_strip(&self.label)
}
/// Expansion/contraction duration — the status-interface module-menu pace.
@@ -319,9 +301,8 @@ impl Dropdown {
// No band: the revealed menu IS the whole open surface.
return (ax, ay, aw, ah);
}
- let label_x = side_offset(&self.label);
- let (tx, ty) = (content.x + label_x, content.y);
- let (tw, th) = ((content.width - label_x).max(0.0), content.height);
+ let (tx, ty) = (content.x, content.y);
+ let (tw, th) = (content.width, content.height);
let x0 = tx.min(ax);
let y0 = ty.min(ay);
let x1 = (tx + tw).max(ax + aw);
@@ -360,9 +341,7 @@ impl Dropdown {
let base_y = content.y - self.label_top();
let open_upward = self.open_upward.unwrap_or(base_y > 400.0);
- let label_x = side_offset(&self.label);
-
- let mut rx = content.x + label_x;
+ let mut rx = content.x;
let mut ry = if self.menu_replaces_trigger {
// The menu takes the trigger's slot: flush with its bottom edge
// (upward) or its top edge (downward) rather than stacked past it.
@@ -427,9 +406,8 @@ impl Dropdown {
/// with the root plate-concentric corner adjustment) or `extra_quads` (plain) depending on
/// the configured radius, byte-for-byte on the same content rect.
fn paint_background(&self, content: Rect, ctx: &mut PaintCtx) {
- let label_x = side_offset(&self.label);
- let x = content.x + label_x;
- let w = content.width - label_x;
+ let x = content.x;
+ let w = content.width;
let y = content.y;
let visual_h = content.height;
@@ -487,105 +465,6 @@ impl Dropdown {
let (x, y, w, visual_h) = (inner.x, inner.y, inner.width, inner.height);
let r4 = [r4t.0, r4t.1, r4t.2, r4t.3];
let face = if raw_bg[3] > 0.001 { bg_color } else { [0.0; 4] };
- let strip = self.label_top();
- if strip > 0.0 {
- // Labeled: the label sits in a CARVE-OUT tab, the section-
- // title idiom — a flat recessed well hugging the label run,
- // its bottom open into the trigger's groove ring below (the
- // tab's walls: top, right, left). Right of the tab the ring
- // keeps its normal top wall, starting at the tab's throat.
- let g = depth * 0.5;
- let orad = (r4[0] + g, r4[1] + g, r4[2] + g, r4[3] + g);
- let (outer_x, outer_r) = (x - g, x + w + g);
- let (tab_top, ring_top) = (y - strip - g, y - g);
- // The tab hugs the label run (drawn at x + inset): text width
- // plus the inset each side, kept inside the trigger's span.
- let (fam, fsize) = crate::layout::control_label_font_detached_parsed();
- let text_w = self
- .label
- .as_deref()
- .map(|l| crate::widget::display::measure_text_width(l, &fam, fsize))
- .unwrap_or(0.0);
- let inset = crate::layout::DETACHED_LABEL_INSET; // the label's x offset
- let tab_w = (text_w + 2.0 * inset + 2.0 * g)
- .max(2.0 * orad.0 + 8.0)
- .min(outer_r - outer_x);
- let tab_r = outer_x + tab_w;
-
- // The tab: bottom open into the ring, pieces extended `depth`
- // past their interior seams so the tessellator's host fades
- // crossfade there instead of notching the walls. With the
- // fillet, the tab's right wall must END at the fillet's
- // vertical tangent (crossfading out under the arc) or its
- // straight run ghosts through the curve — the tab piece stops
- // there and a left-only bridge carries the left wall across
- // the fillet span down to the ring's own fade-in.
- let fr = 6.0_f32.min(strip * 0.5);
- let filleted = outer_r - tab_r > fr + 4.0;
- let tab_bottom = if filleted { ring_top - fr } else { ring_top };
- ctx.recess_edges(
- Rect { x: outer_x, y: tab_top, width: tab_w, height: tab_bottom - tab_top + depth },
- (orad.0, orad.1.min(strip * 0.5), 0.0, 0.0),
- depth,
- (true, true, false, true),
- );
- if filleted {
- ctx.recess_edges(
- Rect { x: outer_x, y: ring_top - fr, width: tab_w, height: fr + depth },
- (0.0, 0.0, 0.0, 0.0),
- depth,
- (false, false, false, true),
- );
- }
- // The ring proper: right + bottom + left walls, one prim so
- // its corners blend internally.
- ctx.recess_edges(
- Rect { x: outer_x, y: ring_top, width: w + 2.0 * g, height: visual_h + 2.0 * g },
- (0.0, 0.0, orad.2, orad.3),
- depth,
- (false, true, true, true),
- );
- // Ring top wall, right of the tab. The concave fillet rounds
- // the throat; the straight run starts a fillet radius past it
- // (extended `depth` left so its fade-in lands under the
- // fillet's hard tangent cut instead of leaving a gap).
- if filleted {
- ctx.concave_fillet(
- tab_r + fr,
- ring_top - fr,
- fr,
- depth,
- std::f32::consts::FRAC_PI_2,
- false,
- );
- ctx.recess_edges(
- Rect {
- x: tab_r + fr - depth,
- y: ring_top,
- width: outer_r - tab_r - fr + depth,
- height: visual_h + 2.0 * g,
- },
- (0.0, orad.1, 0.0, 0.0),
- depth,
- (true, false, false, false),
- );
- } else if outer_r - tab_r > 0.5 {
- ctx.recess_edges(
- Rect { x: tab_r - depth, y: ring_top, width: outer_r - tab_r + depth, height: visual_h + 2.0 * g },
- (0.0, orad.1, 0.0, 0.0),
- depth,
- (true, false, false, false),
- );
- }
- let rect = Rect { x, y, width: w, height: visual_h };
- let rrad = (r4[0], r4[1], r4[2], r4[3]);
- if face[3] > 0.001 {
- ctx.bevel(rect, rrad, face, depth);
- } else {
- ctx.boss(rect, rrad, depth);
- }
- return;
- }
ctx.inset_plate(
Rect { x, y, width: w, height: visual_h },
(r4[0], r4[1], r4[2], r4[3]),
@@ -673,9 +552,8 @@ impl Dropdown {
};
let (font_family, font_size) = crate::layout::control_label_font_detached_parsed();
- let label_x = side_offset(&self.label);
- let x = content.x + label_x;
- let w = content.width - label_x;
+ let x = content.x;
+ let w = content.width;
let start_x = x + 8.0;
let right_limit = x + w - 28.0; // 10px margin before the arrow
let fade_start_x = (right_limit - 24.0).max(start_x); // Fade out over the last 24px
@@ -911,11 +789,6 @@ impl Layout for Dropdown {
}
}
- fn inflates_label_rect(&self) -> bool {
- false
- }
-
-
/// Content size for the scene layout engine (Phase 2b). A normal dropdown is wide enough for
/// the widest option (via `content_width`, which already includes the arrow/padding inset), so
/// the control doesn't resize as the selection changes. A menu-button dropdown (fixed
@@ -1053,9 +926,8 @@ impl Paint for Dropdown {
// trigger paint) — display text left, ▼ right, the paint_text palette.
// A menu that replaces the trigger has no band to redraw on.
if !self.menu_replaces_trigger {
- let label_x = side_offset(&self.label);
- let (tx, ty) = (rect.x + label_x, rect.y);
- let (tw, th) = ((rect.width - label_x).max(0.0), rect.height);
+ let (tx, ty) = (rect.x, rect.y);
+ let (tw, th) = (rect.width, rect.height);
let band_bounds = Some([ux, uy, ux + uw, uy + uh]);
let font = crate::layout::control_label_font_detached();
let text_y = crate::layout::align_text_y(ty, th, 12.0, 0.0);
diff --git a/src/widget/input/ramp.rs b/src/widget/input/ramp.rs
index 1914c13..a37227c 100644
--- a/src/widget/input/ramp.rs
+++ b/src/widget/input/ramp.rs
@@ -943,11 +943,7 @@ impl Ramp {
/// The detached-label strip height the labeled dropdowns carry
/// (`Widget::label_offset`'s formula).
pub fn label_strip() -> f32 {
- if crate::layout::control_label_layout() == "side" {
- return 0.0;
- }
- let (_, font_size) = crate::layout::control_label_font_detached_parsed();
- font_size + crate::layout::control_label_margin()
+ crate::layout::control_label_strip()
}
/// Lay out the control strip under the curve area. One rhythm: the label
diff --git a/src/widget/input/slider.rs b/src/widget/input/slider.rs
index fd3dccf..3df9f91 100644
--- a/src/widget/input/slider.rs
+++ b/src/widget/input/slider.rs
@@ -1,8 +1,6 @@
-//! Narrow-trait `Slider` and `RangeSlider` (Phase 5h). Detached-label widgets that do NOT
-//! inflate their rect (`inflates_label_rect = false`): the label eats into the assigned rect,
-//! so the adapter's content rect is exactly the legacy `y + label_offset` / `h - label_offset`
-//! band the old geometry used. The side-label inset (`label_x_offset`) is computed by the model
-//! from its synced label + config. Drags are host-driven through the `Input` drag hooks; the
+//! Narrow-trait `Slider` and `RangeSlider` (Phase 5h). Detached-label widgets: the adapter
+//! draws the control label in the strip above the content rect the geometry here works in.
+//! Drags are host-driven through the `Input` drag hooks; the
//! readout edit mode uses `EventCtx::request_focus` and the wheel gating uses the legacy scroll
//! gesture state through `EventCtx::ui`.
@@ -25,14 +23,6 @@ struct SliderGeom {
track_w: f32,
}
-fn side_offset(label: &Option<String>) -> f32 {
- if crate::layout::control_label_layout() == "side" && label.is_some() {
- 90.0
- } else {
- 0.0
- }
-}
-
#[derive(Debug, Clone)]
pub struct Slider {
dragging: bool,
@@ -124,9 +114,8 @@ impl Slider {
fn geom(&self, rect: Rect) -> SliderGeom {
- let side = side_offset(&self.label);
- let x = rect.x + side;
- let w = rect.width - side;
+ let x = rect.x;
+ let w = rect.width;
let (track_x, track_w) = if self.show_readout {
let readout_w = 60.0;
let gap = 8.0;
@@ -232,12 +221,6 @@ impl Adapted<Slider> {
}
impl Layout for Slider {
- fn inflates_label_rect(&self) -> bool {
- false // legacy Slider::set_rect stored the assigned rect verbatim
- }
-
-
-
fn intrinsic_size(&self) -> Option<Size> {
Some(Size::new(0.0, crate::layout::slider_height()))
}
@@ -662,123 +645,14 @@ pub(crate) fn paint_band_shape(ctx: &mut PaintCtx, track_x: f32, track_w: f32, c
}
}
-/// The recessed track's carve, in the labeled composition of the Slider2D pad (and
-/// any well that reaches up into its label strip): with a detached label (`strip` > 0, the label strip's height above
-/// `track`) the label sits in a CARVE-OUT tab, the section-title idiom (the labeled
-/// Dropdown's composition) — a flat recessed well hugging the label run, bottom open
-/// into the track's well; the well's top wall picks up right of the tab's throat.
-/// Pieces extend `depth` past interior seams (host-fade crossfade), the tab's right
-/// wall ends at the fillet's vertical tangent (or it ghosts through the arc), and a
-/// left-only bridge carries the left wall across the fillet span. Unlabeled, one
-/// plain recess.
-pub(crate) fn carve_labeled_well(ctx: &mut PaintCtx, track: Rect, strip: f32, label_w: f32, radius: f32, depth: f32) {
- // Carve INSIDE the track (`layout::carve_inside`): every piece derives from
- // this rect, so the whole composition — tab, fillet, bridge — moves in with it
- // and the outer walls land on the track's edges.
- let (track, radii) = crate::layout::carve_inside(track, (radius, radius, radius, radius), depth);
- let radius = radii.0;
- let track_end = track.x + track.width;
- if strip > 0.0 {
- // Labeled: the label sits in a CARVE-OUT tab, the section-
- // title idiom (the labeled Dropdown's composition) — a flat
- // recessed well hugging the label run, bottom open into the
- // track's well; the well's top wall picks up right of the
- // tab's throat.
- let inset = crate::layout::DETACHED_LABEL_INSET; // the label's x offset
- let fr = 6.0_f32.min(strip * 0.5);
- let mut tab_w = (label_w + 2.0 * inset).max(2.0 * radius + 8.0).min(track.width);
- // A throat too short for the fillet and a run of top wall past it reads
- // as a notch beside the tab: then the tab spans the whole track (the
- // Slider2D's pad, a narrow slider) and the well's top wall is the tab's.
- if track.width - tab_w < 2.0 * fr + 4.0 {
- tab_w = track.width;
- }
- let tab_r = track.x + tab_w;
- // The labeled-Dropdown composition: pieces extend `depth`
- // past interior seams (host-fade crossfade), the tab's right
- // wall ends at the fillet's vertical tangent (or it ghosts
- // through the arc), and a left-only bridge carries the left
- // wall across the fillet span.
- let filleted = track_end - tab_r > 2.0 * fr + 4.0;
- let tab_bottom = if filleted { track.y - fr } else { track.y };
- // The tab's crossfade extension past its bottom seam is capped at the
- // fillet radius: a deep wall (a tall well's) otherwise ran on below the
- // well's top edge as a stub beside the fillet.
- let ext = if filleted { depth.min(fr) } else { depth };
- ctx.recess_edges(
- Rect { x: track.x, y: track.y - strip, width: tab_w, height: tab_bottom - (track.y - strip) + ext },
- (radius, radius.min(strip * 0.5), 0.0, 0.0),
- depth,
- (true, true, false, true),
- );
- if filleted {
- ctx.recess_edges(
- Rect { x: track.x, y: track.y - fr, width: tab_w, height: fr + depth },
- (0.0, 0.0, 0.0, 0.0),
- depth,
- (false, false, false, true),
- );
- }
- ctx.recess_edges(track, (0.0, 0.0, radius, radius), depth, (false, true, true, true));
- if filleted {
- ctx.concave_fillet(
- tab_r + fr,
- track.y - fr,
- fr,
- depth,
- std::f32::consts::FRAC_PI_2,
- false,
- );
- ctx.recess_edges(
- Rect {
- x: tab_r + fr - depth,
- y: track.y,
- width: track_end - tab_r - fr + depth,
- height: track.height,
- },
- (0.0, radius, 0.0, 0.0),
- depth,
- (true, false, false, false),
- );
- } else if track_end - tab_r > 0.5 {
- ctx.recess_edges(
- Rect { x: tab_r - depth, y: track.y, width: track_end - tab_r + depth, height: track.height },
- (0.0, radius, 0.0, 0.0),
- depth,
- (true, false, false, false),
- );
- }
- } else {
- ctx.recess(track, (radius, radius, radius, radius), depth);
- }
-}
-
-/// The detached-label strip height above a content rect (zero in side layout or
-/// unlabeled) — the adapter's `label_offset`, replicated for widgets that reach up
-/// into the strip to carve the label tab.
+/// The detached-label strip height above a content rect (zero unlabeled) — the
+/// adapter's `Widget::label_offset` over a model's synced label, for models whose
+/// cached rect is the whole block.
pub(crate) fn detached_strip(label: &Option<String>) -> f32 {
- if crate::layout::control_label_layout() == "side" {
- return 0.0;
- }
- if label.is_some() {
- let (_, font_size) = crate::layout::control_label_font_detached_parsed();
- font_size + crate::layout::control_label_margin()
- } else {
- 0.0
- }
-}
-
-/// The detached label's measured width (the tab hugs it), zero when unlabeled.
-pub(crate) fn detached_label_width(label: &Option<String>) -> f32 {
- let (fam, fsize) = crate::layout::control_label_font_detached_parsed();
- label.as_deref().map(|l| crate::widget::display::measure_text_width(l, &fam, fsize)).unwrap_or(0.0)
+ if label.is_some() { crate::layout::control_label_strip() } else { 0.0 }
}
impl Layout for RangeSlider {
- fn inflates_label_rect(&self) -> bool {
- false
- }
-
fn intrinsic_size(&self) -> Option<Size> {
Some(Size::new(0.0, crate::layout::rangeslider_height()))
}
@@ -798,8 +672,7 @@ impl Paint for RangeSlider {
/// swallowed length. The swells sit where the thumbs' centres were, so the
/// drag geometry below is unchanged.
fn paint(&self, rect: Rect, ctx: &mut PaintCtx) {
- let side = side_offset(&self.label);
- let (x, y, w, h) = (rect.x + side, rect.y, rect.width - side, rect.height);
+ let (x, y, w, h) = (rect.x, rect.y, rect.width, rect.height);
let thumb_size = h * 0.9;
let range = w - thumb_size;
let lo = x + self.value_low * range + thumb_size / 2.0;
@@ -817,9 +690,8 @@ impl Input for RangeSlider {
if !ui.scroll_gesture_new && ui.scroll_initiate_widget_id != Some(ectx.id) {
return false;
}
- let side = side_offset(&self.label);
let r = ectx.rect;
- let (x, y, w, h) = (r.x + side, r.y, r.width - side, r.height);
+ let (x, y, w, h) = (r.x, r.y, r.width, r.height);
if *px >= r.x && *px <= r.x + r.width && *py >= y && *py <= y + h {
if ui.scroll_gesture_new {
ui.scroll_initiate_widget_id = Some(ectx.id);
@@ -865,8 +737,7 @@ impl Input for RangeSlider {
self.active_thumb.is_some()
}
fn drag_begin(&mut self, px: f32, _py: f32, rect: Rect) {
- let side = side_offset(&self.label);
- let (x, w) = (rect.x + side, rect.width - side);
+ let (x, w) = (rect.x, rect.width);
let thumb_size = rect.height * 0.9;
let range = w - thumb_size;
let thumb_low_x = x + self.value_low * range;
@@ -890,8 +761,7 @@ impl Input for RangeSlider {
}
fn drag_update(&mut self, px: f32, _py: f32, rect: Rect) -> bool {
let Some(active) = self.active_thumb else { return false };
- let side = side_offset(&self.label);
- let (x, w) = (rect.x + side, rect.width - side);
+ let (x, w) = (rect.x, rect.width);
let thumb_size = rect.height * 0.9;
let range = w - thumb_size;
if range <= 0.0 {
diff --git a/src/widget/input/slider2d.rs b/src/widget/input/slider2d.rs
index a413114..64cc3f8 100644
--- a/src/widget/input/slider2d.rs
+++ b/src/widget/input/slider2d.rs
@@ -1,7 +1,6 @@
//! `Slider2D` — a two-axis pad control: one thumb dragged across a recessed
//! square well maps to an `(x, y)` pair in 0..1 × 0..1, y-up. Follows the
-//! `Slider` narrow-trait shape: a detached label that does NOT inflate the
-//! rect (the label eats into the assigned rect), host-driven drags through
+//! `Slider` narrow-trait shape: the adapter's detached label above the pad, host-driven drags through
//! the `Input` drag hooks, and `take_change`-based polling by composites.
use crate::scene::layout::{Rect, Size};
@@ -55,20 +54,6 @@ impl Slider2D {
)
}
- /// The detached-label strip height above the content rect — the Slider /
- /// Dropdown replica of `Widget::label_offset` over the synced label.
- fn label_top(&self) -> f32 {
- if crate::layout::control_label_layout() == "side" {
- return 0.0;
- }
- if self.label.is_some() {
- let (_, font_size) = crate::layout::control_label_font_detached_parsed();
- font_size + crate::layout::control_label_margin()
- } else {
- 0.0
- }
- }
-
fn set_from_point(&mut self, px: f32, py: f32, rect: Rect) -> bool {
let inset = Self::THUMB_R + 2.0;
let w = (rect.width - 2.0 * inset).max(1.0);
@@ -87,22 +72,9 @@ impl Slider2D {
}
impl Layout for Slider2D {
- fn inflates_label_rect(&self) -> bool {
- false // the Slider rule: the detached label eats into the assigned rect
- }
-
-
- /// A 64px pad, or wide enough for its label's carve-out tab plus a filleted
- /// throat beside it (the tab is clipped to the pad; the label spilled past a
- /// 64px one, and a throat too short for the fillet reads as a notch).
+ /// A 64px pad.
fn intrinsic_size(&self) -> Option<Size> {
- let label_w = crate::widget::input::slider::detached_label_width(&self.label);
- let tab_w = if label_w > 0.0 {
- label_w + 2.0 * crate::layout::DETACHED_LABEL_INSET + 24.0 + crate::layout::bevel_width()
- } else {
- 0.0
- };
- Some(Size::new(64.0_f32.max(tab_w), 64.0))
+ Some(Size::new(64.0, 64.0))
}
fn intrinsic_measure_width(&self) -> bool {
@@ -150,16 +122,10 @@ impl Paint for Slider2D {
);
let depth = crate::layout::bevel_width().min(rect.height * 0.2);
- // The well's ring, with a labeled pad's label in a carve-out tab — the one
- // labeled-well composition the sliders share (`carve_labeled_well`).
- crate::widget::input::slider::carve_labeled_well(
- ctx,
- rect,
- self.label_top(),
- crate::widget::input::slider::detached_label_width(&self.label),
- radius,
- depth,
- );
+ // The well's ring, carved inside the pad's rect (`layout::carve_inside`); the
+ // control label above is the adapter's, outside the well like every control's.
+ let (inner, radii) = crate::layout::carve_inside(rect, (radius, radius, radius, radius), depth);
+ ctx.recess(inner, radii, depth);
}
}
diff --git a/src/widget/input/spinbox.rs b/src/widget/input/spinbox.rs
index b2a60a5..e4531ff 100644
--- a/src/widget/input/spinbox.rs
+++ b/src/widget/input/spinbox.rs
@@ -1,5 +1,5 @@
-//! Narrow-trait `Spinbox` (Phase 5i). Slider-style label convention (no rect inflation; label
-//! eats into the assigned rect, side-label inset computed from the synced label). Sub-zone
+//! Narrow-trait `Spinbox` (Phase 5i). The adapter's detached label sits above the content rect
+//! the geometry here works in. Sub-zone
//! hover (the -/+ buttons) is tracked from `PointerMove` against the content rect; a click on
//! the display area enters edit mode and takes focus via `EventCtx::request_focus`.
@@ -11,14 +11,6 @@ use crate::widget::{
Paint, TextEditorState,
};
-fn side_offset(label: &Option<String>) -> f32 {
- if crate::layout::control_label_layout() == "side" && label.is_some() {
- 90.0
- } else {
- 0.0
- }
-}
-
#[derive(Debug, Clone)]
pub struct Spinbox {
pub value: i32,
@@ -115,9 +107,8 @@ impl Spinbox {
}
fn geom(&self, rect: Rect) -> SpinGeom {
- let side = side_offset(&self.label);
- let x = rect.x + side;
- let w = rect.width - side;
+ let x = rect.x;
+ let w = rect.width;
let pad = crate::layout::spinbox_button_padding();
SpinGeom {
x,
@@ -272,10 +263,6 @@ impl Adapted<Spinbox> {
}
impl Layout for Spinbox {
- fn inflates_label_rect(&self) -> bool {
- false
- }
-
fn intrinsic_size(&self) -> Option<Size> {
diff --git a/src/widget/input/text_box.rs b/src/widget/input/text_box.rs
index cc29a14..1bfd635 100644
--- a/src/widget/input/text_box.rs
+++ b/src/widget/input/text_box.rs
@@ -11,7 +11,7 @@
//! Parity notes:
//! - The legacy render split is asymmetric and preserved faithfully: the non-rounded path
//! (`extra_quads`) draws at the full base x/width with a disabled special-case; the rounded
-//! path (`all_rounded_quads`) insets by the side label and has NO disabled branch.
+//! path (`all_rounded_quads`) has NO disabled branch.
//! - Releases: legacy `mouse_input` hit-gated releases too (out-of-rect releases were dropped).
//! The adapter delivers releases ungated, so the model re-checks containment itself against
//! the plain rect (the row-substituted release geometry is approximated — flagged).
@@ -36,16 +36,6 @@ pub fn get_font_db() -> &'static resvg::usvg::fontdb::Database {
})
}
-/// Side-layout label inset — the legacy `WidgetHost::label_x_offset` default for non-exempt
-/// widgets (TextBox was never in the exempt list).
-fn side_offset(label: &Option<String>) -> f32 {
- if crate::layout::control_label_layout() == "side" && label.is_some() {
- 90.0
- } else {
- 0.0
- }
-}
-
#[derive(Debug, Clone)]
pub struct TextBox {
pub text: String,
@@ -106,7 +96,7 @@ pub struct TextBox {
/// first shape (readers fall back to the grid).
line_glyph_positions: Vec<Vec<f32>>,
pub update_on_type: bool,
- /// Synced control label ([`Paint::sync_label`]) — drives the side/detached offsets.
+ /// Synced control label ([`Paint::sync_label`]) — drives the detached strip offset.
label: Option<String>,
/// Own hover flag, maintained from `MouseEnter`/`MouseLeave` (adapter bookkeeping).
hovered: bool,
@@ -165,23 +155,14 @@ impl TextBox {
})
}
- /// The detached-label strip height — a replica of `Widget::label_offset` over the synced
- /// label (zero in side layout or unlabeled).
+ /// The detached-label strip height above the content (zero unlabeled) — the
+ /// adapter's `Widget::label_offset` over the synced label.
fn label_top(&self) -> f32 {
- if crate::layout::control_label_layout() == "side" {
- return 0.0;
- }
- if self.label.is_some() {
- let (_, font_size) = crate::layout::control_label_font_detached_parsed();
- font_size + crate::layout::control_label_margin()
- } else {
- 0.0
- }
+ crate::widget::input::slider::detached_strip(&self.label)
}
fn map_x_to_idx(&self, click_x: f32) -> usize {
- let label_x = side_offset(&self.label);
- let relative_x = click_x - (self.rect.x + label_x + 8.0) + self.scroll_x;
+ let relative_x = click_x - (self.rect.x + 8.0) + self.scroll_x;
if self.glyph_positions.is_empty() {
let char_width = self.char_width();
return ((relative_x / char_width).round() as isize)
@@ -690,20 +671,19 @@ impl TextBox {
/// Map a press/drag position to a buffer index — the shared body of the legacy
/// `mouse_input` press arm and `drag_update`.
- fn position_to_idx(&self, px: f32, py: f32, with_label_x: bool) -> usize {
+ fn position_to_idx(&self, px: f32, py: f32) -> usize {
let char_width = self.char_width();
let top = self.label_top();
- let label_x = if with_label_x { side_offset(&self.label) } else { 0.0 };
if self.multiline {
let line_height = self.line_height();
let max_chars = if self.line_wrap_enabled() {
- ((((self.rect.width - label_x) - 16.0) / char_width).floor() as usize).max(1)
+ (((self.rect.width - 16.0) / char_width).floor() as usize).max(1)
} else {
999999
};
let (lines, index_map) = self.wrap_text(max_chars);
let click_line = (((py - (self.rect.y + top + 8.0) + self.scroll_y) / line_height).floor() as isize).max(0) as usize;
- let rel_x = px - (self.rect.x + label_x + 8.0) + self.scroll_x;
+ let rel_x = px - (self.rect.x + 8.0) + self.scroll_x;
let click_col = self.line_x_to_col(click_line.min(lines.len() - 1), rel_x);
self.map_2d_to_1d(&index_map, click_line, click_col, lines.len() - 1)
} else {
@@ -714,7 +694,7 @@ impl TextBox {
/// Extend the selection to a drag position — the shared body of the legacy
/// `on_cursor_moved` drag arm and `drag_update` (which used no label inset).
fn extend_selection_to(&mut self, px: f32, py: f32) -> bool {
- let drag_idx = self.position_to_idx(px, py, false);
+ let drag_idx = self.position_to_idx(px, py);
if self.cursor_idx != drag_idx {
self.cursor_idx = drag_idx;
self.just_focused = false;
@@ -992,7 +972,7 @@ impl TextBox {
/// Selection highlight + caret quads, shared by both render branches. `x`/`w` are the
/// (possibly label-inset) horizontal span the branch draws in — the legacy paths differed
- /// (non-rounded used the full base span, rounded inset by the side label).
+ /// (non-rounded and rounded alike use the full base span).
fn selection_quads(&self, x: f32, w: f32, out: &mut Vec<(f32, f32, f32, f32, [f32; 4])>) {
if !(self.editing || self.select_anchor.is_some()) {
return;
@@ -1140,9 +1120,8 @@ impl TextBox {
[0xcc, 0xcc, 0xd4]
};
- let label_x = side_offset(&self.label);
- let x = self.rect.x + label_x;
- let w = self.rect.width - label_x;
+ let x = self.rect.x;
+ let w = self.rect.width;
if self.multiline {
let char_width = self.char_width();
@@ -1211,11 +1190,10 @@ impl TextBox {
return None;
}
let top = self.label_top();
- let label_x = side_offset(&self.label);
let well = Rect {
- x: self.rect.x + label_x,
+ x: self.rect.x,
y: self.rect.y + top,
- width: self.rect.width - label_x,
+ width: self.rect.width,
height: self.rect.height - top,
};
let depth = crate::layout::bevel_width().min(well.height * 0.2);
@@ -1292,10 +1270,6 @@ impl Adapted<TextBox> {
}
impl Layout for TextBox {
- fn inflates_label_rect(&self) -> bool {
- false
- }
-
/// One row for a single-line box; a multiline box has no natural height of its own —
/// the host sizes it, and a layout strategy leaves its assigned rect alone.
@@ -1376,13 +1350,11 @@ impl Paint for TextBox {
}
fn text_bounds(&self, rect: Rect) -> Option<[f32; 4]> {
- // Legacy bounded-text getters clipped to the full base rect, inset on the left by the
- // side label.
+ // Legacy bounded-text getters clipped to the full block rect.
let top = self.label_top();
let base_y = rect.y - top;
let base_h = rect.height + top;
- let label_x = side_offset(&self.label);
- Some([rect.x + label_x, base_y, rect.x + rect.width, base_y + base_h])
+ Some([rect.x, base_y, rect.x + rect.width, base_y + base_h])
}
/// The legacy `prepare_text`: sync font family/size with the live config defaults, then
@@ -1468,8 +1440,7 @@ impl Paint for TextBox {
// one `selection_quads`/`value_labels` compute at paint time.
self.line_glyph_positions.clear();
if self.multiline {
- let label_x = side_offset(&self.label);
- let wrap_w = self.rect.width - label_x;
+ let wrap_w = self.rect.width;
let max_chars = if self.line_wrap_enabled() {
(((wrap_w - 16.0) / self.char_width()).floor() as usize).max(1)
} else {
@@ -1517,7 +1488,7 @@ impl Paint for TextBox {
let _ = (base_y, base_h);
if radius <= 0.0 {
- // Legacy `extra_quads`: full base span (no side-label inset), disabled
+ // Legacy `extra_quads`: full base span, disabled
// special-case with early return.
let mut quads: Vec<(f32, f32, f32, f32, [f32; 4])> = Vec::new();
if self.disabled {
@@ -1547,10 +1518,9 @@ impl Paint for TextBox {
ctx.quad(Rect { x: qx, y: qy, width: qw, height: qh }, qc);
}
} else {
- // Legacy `all_rounded_quads`: side-label inset, no disabled special-case.
- let label_x = side_offset(&self.label);
- let x = self.rect.x + label_x;
- let w = self.rect.width - label_x;
+ // Legacy `all_rounded_quads`: no disabled special-case.
+ let x = self.rect.x;
+ let w = self.rect.width;
// One background regardless of focus (see the flat path above).
let bg_color = crate::colors::textbox_background_color();
@@ -1676,7 +1646,7 @@ impl Input for TextBox {
self.begin_editing();
ectx.request_focus();
}
- let idx = self.position_to_idx(*px, *py, true);
+ let idx = self.position_to_idx(*px, *py);
self.cursor_idx = idx;
self.select_anchor = Some(idx);
self.all_selected = false;
@@ -1688,11 +1658,8 @@ impl Input for TextBox {
Event::MouseButton { button: MouseButton::Left, state: ElementState::Released, x: px, y: py, .. } => {
if self.disabled { return false; }
// Legacy gated releases on the hit test; the adapter delivers them ungated, so
- // re-check containment (plain rect + side inset — row spans approximated).
- let label_x = side_offset(&self.label);
- let top = self.label_top();
- let (bx, by, bw, bh) = (self.rect.x + label_x, self.rect.y, self.rect.width - label_x, self.rect.height);
- let _ = top;
+ // re-check containment (the plain block rect — row spans approximated).
+ let (bx, by, bw, bh) = (self.rect.x, self.rect.y, self.rect.width, self.rect.height);
if !(*px >= bx && *px <= bx + bw && *py >= by && *py <= by + bh) {
return false;
}
diff --git a/src/widget/input/trackpad.rs b/src/widget/input/trackpad.rs
index 8f21ffd..4a64f38 100644
--- a/src/widget/input/trackpad.rs
+++ b/src/widget/input/trackpad.rs
@@ -12,8 +12,8 @@ pub struct Finger {
/// Touchpad visualization/input area (narrow-trait model, Phase 6as leaf sweep). The
/// content rect is cached on assignment (the ParametersBg pattern) because the finger
-/// math runs from events and drags as well as paint; the control label stays
-/// model-drawn (`inline_label`) to keep the legacy detached-top layout byte-identical.
+/// math runs from events and drags as well as paint; the control label is the adapter's
+/// detached one above the pad, like every control's.
#[derive(Debug, Clone)]
pub struct Trackpad {
rect: Rect,
@@ -51,35 +51,10 @@ impl Adapted<Trackpad> {
}
impl Trackpad {
-
- fn label_offset(&self) -> f32 {
- if crate::layout::control_label_layout() == "side" {
- return 0.0;
- }
- if self.label.is_some() {
- let (_, font_size) = crate::layout::control_label_font_detached_parsed();
- font_size + crate::layout::control_label_margin()
- } else {
- 0.0
- }
- }
-
- fn label_x_offset(&self) -> f32 {
- if crate::layout::control_label_layout() == "side" && self.label.is_some() {
- 90.0
- } else {
- 0.0
- }
- }
-
- /// The inner touch area (content rect minus the detached-label reservation).
+ /// The touch area: the cached block rect less the detached-label strip above it.
fn touch_area(&self) -> (f32, f32, f32, f32) {
- let label_x = self.label_x_offset();
- let x = self.rect.x + label_x;
- let w = self.rect.width - label_x;
- let top = self.label_offset();
- let visual_h = self.rect.height - top;
- (x, self.rect.y + top, w, visual_h)
+ let top = crate::widget::input::slider::detached_strip(&self.label);
+ (self.rect.x, self.rect.y + top, self.rect.width, self.rect.height - top)
}
fn finger_at(&self, px: f32, py: f32) -> Finger {
@@ -93,10 +68,6 @@ impl Trackpad {
}
impl Layout for Trackpad {
- fn inline_label(&self) -> bool {
- true
- }
-
fn rect_assigned(&mut self, rect: Rect) {
self.rect = rect;
}
@@ -160,7 +131,7 @@ impl Paint for Trackpad {
);
}
- // 4. Labels ("Touchpad Area" hint + the model-drawn control label)
+ // 4. The "Touchpad Area" hint (the control label is the adapter's).
ctx.text(
"Touchpad Area".to_string(),
x + 12.0,
@@ -168,16 +139,6 @@ impl Paint for Trackpad {
11.0,
[0x73, 0x73, 0x8c],
);
- if let Some(ref label) = self.label {
- let (_, font_size) = crate::layout::control_label_font_detached_parsed();
- ctx.text(
- label.clone(),
- self.rect.x,
- self.rect.y,
- font_size,
- colors::control_label_color_detached_for_state(self.hovered, false),
- );
- }
}
}
diff --git a/src/widget/layout_helper.rs b/src/widget/layout_helper.rs
index d29eb08..ce42c68 100644
--- a/src/widget/layout_helper.rs
+++ b/src/widget/layout_helper.rs
@@ -21,10 +21,10 @@ impl ColumnLayout {
}
}
+ /// `height` is the CONTENT height; the widget's block adds its label strip.
pub fn add_widget(&mut self, widget: &mut dyn WidgetHost, height: f32) {
- let label_off = widget.base().label_offset();
- let total_h = height + label_off;
- widget.set_rect(self.x + self.margin, self.current_y, self.width - 2.0 * self.margin, height);
+ let total_h = height + widget.label_strip();
+ widget.set_rect(self.x + self.margin, self.current_y, self.width - 2.0 * self.margin, total_h);
self.current_y += total_h + self.gap;
}
@@ -36,7 +36,7 @@ impl ColumnLayout {
let mut max_label_off = 0.0;
for &widget_ptr in widgets {
unsafe {
- let off = (*widget_ptr).base().label_offset();
+ let off = (*widget_ptr).label_strip();
if off > max_label_off {
max_label_off = off;
}
@@ -48,7 +48,9 @@ impl ColumnLayout {
let mut curr_x = self.x + self.margin;
for &widget_ptr in widgets {
unsafe {
- (*widget_ptr).set_rect(curr_x, self.current_y, widget_w, height);
+ // Content lines up on one row; a shorter label strip starts lower.
+ let off = (*widget_ptr).label_strip();
+ (*widget_ptr).set_rect(curr_x, self.current_y + max_label_off - off, widget_w, height + off);
}
curr_x += widget_w + gap;
}
diff --git a/src/widget/mod.rs b/src/widget/mod.rs
index 53c503d..74011ca 100644
--- a/src/widget/mod.rs
+++ b/src/widget/mod.rs
@@ -207,12 +207,13 @@ pub trait WidgetHost {
fn preferred_height(&self) -> Option<f32> { None }
/// The height of the detached-label strip above this widget's content: zero for
- /// unlabeled and inline-label widgets. A widget's occupied rect is its content plus
- /// this strip, whichever legacy convention its `set_rect` follows; `layout` lands
- /// the content at the origin and the strip above it. A strategy reserves that
+ /// unlabeled widgets and for those whose base label IS their content
+ /// ([`Layout::inline_label`]). A widget's rect is always its content plus this
+ /// strip — `set_rect` takes that block, `layout` lands the content at the origin
+ /// and hangs the strip above it. A strategy reserves that
/// row above every child's content (`container_layout::label_lead`) and puts
/// `layout::CONTROL_GAP` between the blocks.
- fn label_strip(&self) -> f32 { 0.0 }
+ fn label_strip(&self) -> f32 { self.base().label_offset() }
fn mark_dirty(&mut self, ctx: &mut UiContext) {
let b = self.base_mut();
@@ -268,9 +269,12 @@ pub trait WidgetHost {
Size { width, height }
}
+ /// Land the CONTENT box at `origin`, the label strip hanging above it — the one
+ /// placement contract (`Adapted` repeats it over its measured content size).
fn layout(&mut self, origin: Point, constraints: LayoutConstraints, ctx: &mut UiContext) {
let size = self.measure(constraints, ctx);
- self.set_rect(origin.x, origin.y, size.width, size.height);
+ let strip = self.label_strip();
+ self.set_rect(origin.x, origin.y - strip, size.width, size.height + strip);
}
fn rect(&self) -> (f32, f32, f32, f32) {
@@ -318,10 +322,7 @@ pub trait WidgetHost {
return false;
}
let b = self.base();
- let (mut hx, mut hw) = if b.row_w > 0.0 { (b.row_x, b.row_w) } else { (x, w) };
- let label_x = self.label_x_offset();
- hx += label_x;
- hw -= label_x;
+ let (hx, hw) = if b.row_w > 0.0 { (b.row_x, b.row_w) } else { (x, w) };
px >= hx && px <= hx + hw && py >= y && py <= y + h
}
@@ -344,10 +345,9 @@ pub trait WidgetHost {
} else {
return None;
};
- let label_x = self.label_x_offset();
let b = self.base();
- let hx = if b.row_w > 0.0 { b.row_x } else { b.x } + label_x;
- let hw = if b.row_w > 0.0 { b.row_w } else { b.w } - label_x;
+ let hx = if b.row_w > 0.0 { b.row_x } else { b.x };
+ let hw = if b.row_w > 0.0 { b.row_w } else { b.w };
Some((hx, b.y, hw, b.h, hc))
}
@@ -360,18 +360,6 @@ pub trait WidgetHost {
// inherent `Adapted<W>` reads; index-driven rosters (TI, designer) route them
// through per-slot matches like the other value drains.
- fn label_x_offset(&self) -> f32 {
- let name = self.type_name();
- if name == "Label" || name == "Button" || name == "Checkbox" || name == "Toggle" || name == "Ramp" {
- return 0.0;
- }
- if crate::layout::control_label_layout() == "side" && self.base().label.is_some() {
- 90.0
- } else {
- 0.0
- }
- }
-
fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> { Vec::new() }
fn extra_arcs(&self) -> Vec<(f32, f32, f32, f32, f32, f32, [f32; 4])> { Vec::new() }
fn extra_circles(&self) -> Vec<(f32, f32, f32, [f32; 4])> { Vec::new() }
@@ -668,14 +656,6 @@ pub trait GeomController {
fn take_geom_toggle(&mut self) -> bool;
}
-pub fn label_offset(w: &dyn WidgetHost) -> f32 {
- let name = w.type_name();
- if name == "Label" || name == "Button" || name == "Checkbox" || name == "Toggle" {
- return 0.0;
- }
- w.base().label_offset()
-}
-
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct CornerRadii {
pub top_left: f32,
diff --git a/src/widget/model.rs b/src/widget/model.rs
index c3fb7d2..ce5cc5a 100644
--- a/src/widget/model.rs
+++ b/src/widget/model.rs
@@ -41,21 +41,16 @@ pub trait Layout {
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.
+ /// Whether this widget's base label IS its content — the text a `Button` face, a
+ /// `Checkbox` row or a `Label` draws itself — rather than a control label, which
+ /// the adapter draws detached above the content (the one convention for every
+ /// labeled control: `layout::control_label_strip` tall, at
+ /// [`detached_label_inset`](Layout::detached_label_inset)). Inline-label widgets
+ /// carry no label strip and get no content-rect inset.
fn inline_label(&self) -> bool {
false
}
- /// Whether `set_rect` grows the widget past the assigned rect to make room for a detached
- /// label above (`ProgressBar`'s legacy convention). Sliders keep the assigned rect and let
- /// the label eat into it instead. Irrelevant for inline-label widgets. Default: grow.
- fn inflates_label_rect(&self) -> bool {
- true
- }
-
/// Horizontal inset of the detached base label: [`crate::layout::DETACHED_LABEL_INSET`]
/// for every control, so a column of labels is one line and the carve-out tabs
/// (which hug the label at this inset) sit under their labels.
@@ -72,7 +67,7 @@ pub trait Layout {
}
/// Whether the adapter's hit test substitutes the base row rect (`row_x`/`row_w`, pushed in
- /// by row-layout hosts via `set_row_rect`) plus the side-label inset — the legacy
+ /// by row-layout hosts via `set_row_rect`) — the legacy
/// `WidgetHost::hit_test` default geometry. Migrated controls so far dropped it (accepted
/// drift); TextBox restores it (cce-files' save-name box relies on row hits). Default: off,
/// keeping the other migrated widgets exactly as they shipped.
@@ -710,19 +705,8 @@ impl<W: Layout + Paint + Input + 'static> Adapted<W> {
}
/// 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, or when the widget draws its label
- /// inline — `Widget::label_offset` / [`Layout::inline_label`]).
- /// How far past an assigned rect `set_rect` grows this widget for its detached
- /// label — the legacy inflating convention; zero for the eating convention and
- /// for inline labels.
- fn label_inflation(&self) -> f32 {
- if Layout::inline_label(&self.inner) || !Layout::inflates_label_rect(&self.inner) {
- 0.0
- } else {
- self.base.label_offset()
- }
- }
-
+ /// strip at the top (zero when there is no label, or when the widget's label is its
+ /// content — [`Layout::inline_label`]).
fn content_rect(&self) -> Rect {
let top = if Layout::inline_label(&self.inner) { 0.0 } else { self.base.label_offset() };
Rect {
@@ -1025,13 +1009,6 @@ impl<W: Layout + Paint + Input + 'static> Adapted<W> {
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 = WidgetHost::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 }];
- }
- }
let inset = Layout::detached_label_inset(&self.inner);
return vec![TextLabel { text: label.clone(), x: b.x + inset, y: b.y, font_size, color }];
}
@@ -1116,12 +1093,11 @@ impl<W: Layout + Paint + Input + 'static> WidgetHost for Adapted<W> {
// containers — the ctx-carrying half of the arrangement the model can't do in
// `arrange_children`.
// `origin` is the CONTENT box's top-left and `measure` its height; the detached
- // label hangs in the strip above, so the occupied rect starts `strip` higher.
- // `set_rect` speaks the widget's legacy convention: an inflating widget grows
- // by the strip itself, one whose label eats into its rect needs it included.
+ // label hangs in the strip above, so the block `set_rect` takes starts `strip`
+ // higher and is `strip` taller.
let size = self.measure(constraints, ctx);
let strip = self.label_strip();
- self.set_rect(origin.x, origin.y - strip, size.width, size.height + strip - self.label_inflation());
+ self.set_rect(origin.x, origin.y - strip, size.width, size.height + strip);
let host_id = self.base.id();
Layout::register_embedded_children(&mut self.inner, host_id, ctx);
}
@@ -1156,17 +1132,16 @@ impl<W: Layout + Paint + Input + 'static> WidgetHost for Adapted<W> {
// --- Legacy structural conventions the adapter owns on the widget's behalf ---
- /// 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). Inline-label widgets ([`Layout::inline_label`]) draw the label
- /// inside their rect and get no inflation. Zero-cost when no label is set.
+ /// The assigned rect is the widget's whole block: the detached label strip (if any)
+ /// at its top, the content below (`content_rect`). One convention for every
+ /// control — a caller sizing a labeled widget by hand adds `label_strip` to the
+ /// content height; `layout` does that for it.
fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
let r = Layout::adjust_rect(&self.inner, Rect { x, y, width: w, height: h });
- let inflation = self.label_inflation();
self.base.x = r.x;
self.base.y = r.y;
self.base.w = r.width;
- self.base.h = r.height + inflation;
+ self.base.h = r.height;
// Ungated rect notification (TextBox re-clamps scroll on every assignment, hidden or
// not — the legacy `set_rect` side effect).
let landed = Rect { x: self.base.x, y: self.base.y, width: self.base.w, height: self.base.h };
@@ -1180,9 +1155,7 @@ impl<W: Layout + Paint + Input + 'static> WidgetHost for Adapted<W> {
}
}
- /// The intrinsic content height — the control below the label. Which legacy
- /// `set_rect` convention the widget follows (rect inflated by the label, or the
- /// label eating into it) is `layout`'s business, not the caller's.
+ /// The intrinsic content height — the control below the label.
fn preferred_height(&self) -> Option<f32> {
Layout::intrinsic_size(&self.inner).map(|s| s.height)
}
@@ -1213,7 +1186,7 @@ impl<W: Layout + Paint + Input + 'static> WidgetHost for Adapted<W> {
/// overrides do today) — unless the widget opts back in
/// ([`Paint::legacy_focus_highlight`], TextBox), in which case this replicates the
/// `WidgetHost` default byte-for-byte: primary tint when ctx-focused (or active), secondary
- /// when hovered, over the row-substituted, side-label-inset span.
+ /// when hovered, over the row-substituted span.
fn highlight_quad(&self, ctx: &UiContext) -> Option<(f32, f32, f32, f32, [f32; 4])> {
// A forwarding widget (Paginator → its ButtonStrip) serves the forwarded value here —
// and only here; `all_quads`/`paint_self` gate on `legacy_focus_highlight` instead, so
@@ -1232,9 +1205,8 @@ impl<W: Layout + Paint + Input + 'static> WidgetHost for Adapted<W> {
} else {
return None;
};
- let label_x = WidgetHost::label_x_offset(self);
- let hx = if self.base.row_w > 0.0 { self.base.row_x } else { self.base.x } + label_x;
- let hw = if self.base.row_w > 0.0 { self.base.row_w } else { self.base.w } - label_x;
+ let hx = if self.base.row_w > 0.0 { self.base.row_x } else { self.base.x };
+ let hw = if self.base.row_w > 0.0 { self.base.row_w } else { self.base.w };
Some((hx, self.base.y, hw, self.base.h, hc))
}
@@ -1541,16 +1513,13 @@ impl<W: Layout + Paint + Input + 'static> WidgetHost for Adapted<W> {
}
let (x, y, w, h) = self.rect();
// Row-hit opt-in ([`Layout::hit_row_rect`]): replicate the legacy `hit_test` default's
- // geometry — substitute the host-pushed row span and inset by the side label — before
+ // geometry — substitute the host-pushed row span — before
// the narrow test. The width<=0 reject also comes from that default.
if Layout::hit_row_rect(&self.inner) {
if w <= 0.0 || h <= 0.0 {
return false;
}
- let (mut hx, mut hw) = if self.base.row_w > 0.0 { (self.base.row_x, self.base.row_w) } else { (x, w) };
- let label_x = WidgetHost::label_x_offset(self);
- hx += label_x;
- hw -= label_x;
+ let (hx, hw) = if self.base.row_w > 0.0 { (self.base.row_x, self.base.row_w) } else { (x, w) };
return Input::hit(&self.inner, Rect { x: hx, y, width: hw, height: h }, px, py);
}
Input::hit(&self.inner, Rect { x, y, width: w, height: h }, px, py)
@@ -1698,9 +1667,8 @@ mod tests {
Rect { x, y, width: w, height: h }
}
- /// One rhythm for labeled controls whichever legacy convention they follow: the
- /// preferred height is the CONTENT height for the inflating kind (ProgressBar) and
- /// the eating kind (Slider, Spinbox, Dropdown) alike, and `layout` places that
+ /// One rhythm for labeled controls: the preferred height is the CONTENT height
+ /// for every kind (ProgressBar, Slider, Spinbox, Dropdown), and `layout` places that
/// content at the origin with the label strip hanging above it — so a strategy
/// placing content boxes lines mixed controls up by content, neither squashes a
/// track to the label's leftovers nor lets a label spill into the gap below.
@@ -1728,7 +1696,7 @@ mod tests {
let (_, top, _, landed) = w.rect();
assert!((top - (100.0 - strip)).abs() < 0.01, "{name}: the label hangs above the origin (top {top})");
assert!((landed - (content + strip)).abs() < 0.01, "{name}: occupied {landed} = content + strip");
- let painted = landed - crate::widget::label_offset(w);
+ let painted = landed - w.label_strip();
assert!((painted - content).abs() < 0.01, "{name}: content {painted}, wanted {content}");
}
}