GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
fix(text_box): text starts on the well's floor and clips there, not on the relief wall
A TextBox inset its text a fixed 8px, but a tall box's relief wall is the
full bevel_width deep (~14px in at 9.3), so a multiline box -- cce-fonts'
preview -- drew its text over its own wall. `pad()` is now 8px or the
wall's inner edge, whichever is further in, everywhere the inset was used.
The clip was no help either: the adapter's label bridge discarded the
prim's bounds for `text_bounds`, which TextBox widened to the whole block
because the detached label shared it. The bridge now applies `text_bounds`
to the content text only (the label in the strip above is left unclipped,
which also stops Graph's clipping its label away), and TextBox's bounds
are the well's floor.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
src/widget/input/text_box.rs | 160 ++++++++++++++++++++++++++++---------------
src/widget/model.rs | 67 +++++++++++++-----
2 files changed, 155 insertions(+), 72 deletions(-)
diff --git a/src/widget/input/text_box.rs b/src/widget/input/text_box.rs
index 8d70d19..733ef61 100644
--- a/src/widget/input/text_box.rs
+++ b/src/widget/input/text_box.rs
@@ -254,7 +254,8 @@ impl TextBox {
}
fn map_x_to_idx(&self, click_x: f32) -> usize {
- let relative_x = click_x - (self.rect.x + 8.0) + self.scroll_x;
+ let pad = self.pad();
+ let relative_x = click_x - (self.rect.x + pad) + self.scroll_x;
if self.glyph_positions.is_empty() {
let char_width = self.char_width();
return ((relative_x / char_width).round() as isize)
@@ -490,6 +491,37 @@ impl TextBox {
}
}
+ /// The inset from the box's edge to its text — the caret, selection,
+ /// hit-testing, wrap and scroll range all measure from it. 8px, or the
+ /// well's floor when the relief wall reaches further in: the wall is
+ /// `bevel_width` deep once the box is tall enough (`well`'s 20% cap), so a
+ /// fixed 8px sat a multiline box's text on its wall.
+ fn pad(&self) -> f32 {
+ 8.0f32.max(self.wall_inset())
+ }
+
+ /// How far in from the box's edge its relief wall ends (the outline's own
+ /// inset from the carve plus the wall's depth); zero with no relief.
+ fn wall_inset(&self) -> f32 {
+ self.well().map_or(0.0, |f| f.rect.x - self.rect.x + f.depth)
+ }
+
+ /// The value text's clip, `[x1, y1, x2, y2]`, from the content rect (the
+ /// box below its label strip): the well's floor, so scrolled text slides
+ /// under the relief wall rather than over it. Only a multiline box is
+ /// clipped in y: a single line is centred, never scrolls vertically, and a
+ /// short box's floor is shallower than its line.
+ fn text_clip(&self, content: Rect) -> [f32; 4] {
+ let inset = self.wall_inset();
+ let inset_y = if self.multiline { inset } else { 0.0 };
+ [
+ content.x + inset,
+ content.y + inset_y,
+ content.x + content.width - inset,
+ content.y + content.height - inset_y,
+ ]
+ }
+
pub fn set_placeholder(&mut self, placeholder: &str) {
self.placeholder = Some(placeholder.to_string());
}
@@ -726,10 +758,11 @@ impl TextBox {
}
pub fn clamp_scroll(&mut self) {
+ let pad = self.pad();
let char_width = self.char_width();
let line_height = self.line_height();
let max_chars = if self.line_wrap_enabled() {
- (((self.rect.width - 16.0) / char_width).floor() as usize).max(1)
+ (((self.rect.width - 2.0 * pad) / char_width).floor() as usize).max(1)
} else {
999999
};
@@ -742,7 +775,7 @@ impl TextBox {
if self.multiline {
let content_h = lines.len() as f32 * line_height;
- let max_scroll = (content_h - (self.rect.height - 16.0)).max(0.0);
+ let max_scroll = (content_h - (self.rect.height - 2.0 * pad)).max(0.0);
self.scroll_y = self.scroll_y.clamp(0.0, max_scroll);
} else {
self.scroll_y = 0.0;
@@ -750,7 +783,7 @@ impl TextBox {
if !self.line_wrap_enabled() {
let content_w = self.content_width(&lines);
- let max_scroll_x = (content_w - (self.rect.width - 16.0)).max(0.0);
+ let max_scroll_x = (content_w - (self.rect.width - 2.0 * pad)).max(0.0);
self.scroll_x = self.scroll_x.clamp(0.0, max_scroll_x);
} else {
self.scroll_x = 0.0;
@@ -758,10 +791,11 @@ impl TextBox {
}
pub fn scroll_to_cursor(&mut self) {
+ let pad = self.pad();
let char_width = self.char_width();
let line_height = self.line_height();
let max_chars = if self.line_wrap_enabled() {
- (((self.rect.width - 16.0) / char_width).floor() as usize).max(1)
+ (((self.rect.width - 2.0 * pad) / char_width).floor() as usize).max(1)
} else {
999999
};
@@ -781,11 +815,11 @@ impl TextBox {
let (line_idx, col_idx) = index_map[cursor_idx];
let top = self.label_top();
- let viewport_w = self.rect.width - 16.0;
- let viewport_h = self.rect.height - top - 16.0;
+ let viewport_w = self.rect.width - 2.0 * pad;
+ let viewport_h = self.rect.height - top - 2.0 * pad;
if self.multiline {
- let line_y = top + 8.0 + (line_idx as f32 * line_height);
+ let line_y = top + pad + (line_idx as f32 * line_height);
if line_y < self.scroll_y + 10.0 {
self.scroll_y = (line_y - 20.0).max(0.0);
} else if line_y + line_height > self.scroll_y + viewport_h - 10.0 {
@@ -914,18 +948,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) -> usize {
+ let pad = self.pad();
let char_width = self.char_width();
let top = self.label_top();
if self.multiline {
let line_height = self.line_height();
let max_chars = if self.line_wrap_enabled() {
- (((self.rect.width - 16.0) / char_width).floor() as usize).max(1)
+ (((self.rect.width - 2.0 * pad) / 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 + 8.0) + self.scroll_x;
+ let click_line = (((py - (self.rect.y + top + pad) + self.scroll_y) / line_height).floor() as isize).max(0) as usize;
+ let rel_x = px - (self.rect.x + pad) + 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 {
@@ -952,6 +987,7 @@ impl TextBox {
/// Port of the legacy `keyboard_input` body.
fn handle_key(&mut self, event: &KeyEvent) -> bool {
+ let pad = self.pad();
if !self.editing || self.disabled { return false; }
if event.state != ElementState::Pressed { return false; }
// While an input method composes, its keys are its own: a shell does
@@ -1004,7 +1040,7 @@ impl TextBox {
}
if self.multiline {
let char_width = self.char_width();
- let max_chars = (((self.rect.width - 16.0) / char_width).floor() as usize).max(1);
+ let max_chars = (((self.rect.width - 2.0 * pad) / char_width).floor() as usize).max(1);
let (lines, index_map) = self.wrap_text(max_chars);
let (cursor_l, cursor_c) = index_map[state.cursor_idx.min(index_map.len() - 1)];
if cursor_l > 0 {
@@ -1029,7 +1065,7 @@ impl TextBox {
}
if self.multiline {
let char_width = self.char_width();
- let max_chars = (((self.rect.width - 16.0) / char_width).floor() as usize).max(1);
+ let max_chars = (((self.rect.width - 2.0 * pad) / char_width).floor() as usize).max(1);
let (lines, index_map) = self.wrap_text(max_chars);
let (cursor_l, cursor_c) = index_map[state.cursor_idx.min(index_map.len() - 1)];
if cursor_l < lines.len() - 1 {
@@ -1147,12 +1183,13 @@ impl TextBox {
/// Port of the legacy `mouse_wheel` body (scroll the multiline/no-wrap viewports).
fn handle_wheel(&mut self, delta: &MouseScrollDelta) -> bool {
+ let pad = self.pad();
if self.disabled { return false; }
let char_width = self.char_width();
let line_height = self.line_height();
let max_chars = if self.line_wrap_enabled() {
- (((self.rect.width - 16.0) / char_width).floor() as usize).max(1)
+ (((self.rect.width - 2.0 * pad) / char_width).floor() as usize).max(1)
} else {
999999
};
@@ -1168,7 +1205,7 @@ impl TextBox {
let mut max_scroll_y = 0.0;
if self.multiline {
let content_h = lines.len() as f32 * line_height;
- max_scroll_y = (content_h - (self.rect.height - 16.0)).max(0.0);
+ max_scroll_y = (content_h - (self.rect.height - 2.0 * pad)).max(0.0);
dy_px = match *delta {
MouseScrollDelta::LineDelta(_, dy) => -dy * line_height * 2.0,
MouseScrollDelta::PixelDelta(pos) => -pos.y as f32,
@@ -1179,7 +1216,7 @@ impl TextBox {
let mut max_scroll_x = 0.0;
if !self.line_wrap_enabled() {
let content_w = self.content_width(&lines);
- max_scroll_x = (content_w - (self.rect.width - 16.0)).max(0.0);
+ max_scroll_x = (content_w - (self.rect.width - 2.0 * pad)).max(0.0);
let natural = crate::layout::touchpad_natural_scroll();
let scroll_amt_x = match *delta {
MouseScrollDelta::LineDelta(dx, dy) => {
@@ -1227,6 +1264,7 @@ impl TextBox {
/// the caret, as quads. Returns the caret's rect while editing (unclipped,
/// in the box's coordinates), which the painter reports to the input method.
fn selection_quads(&self, x: f32, w: f32, out: &mut Vec<(f32, f32, f32, f32, [f32; 4])>) -> Option<[f32; 4]> {
+ let pad = self.pad();
if !(self.editing || self.select_anchor.is_some()) {
return None;
}
@@ -1247,7 +1285,7 @@ impl TextBox {
if self.multiline {
let max_chars = if self.line_wrap_enabled() {
- (((w - 16.0) / char_width).floor() as usize).max(1)
+ (((w - 2.0 * pad) / char_width).floor() as usize).max(1)
} else {
999999
};
@@ -1277,13 +1315,13 @@ impl TextBox {
}
}
if let (Some(sc), Some(ec)) = (line_start_col, line_end_col) {
- let highlight_x = x + 8.0 + self.line_col_x(line_idx, sc) - self.scroll_x;
+ let highlight_x = x + pad + self.line_col_x(line_idx, sc) - self.scroll_x;
let highlight_w = self.line_col_x(line_idx, ec + 1) - self.line_col_x(line_idx, sc);
- let highlight_y = self.rect.y + top + 8.0 + (line_idx as f32 * line_height) - self.scroll_y;
+ let highlight_y = self.rect.y + top + pad + (line_idx as f32 * line_height) - self.scroll_y;
let clipped_y = highlight_y.max(view_top);
let clipped_bottom = (highlight_y + line_height).min(view_bottom);
- let h_left = highlight_x.max(x + 8.0);
- let h_right = (highlight_x + highlight_w).min(x + w - 8.0);
+ let h_left = highlight_x.max(x + pad);
+ let h_right = (highlight_x + highlight_w).min(x + w - pad);
if h_left < h_right && clipped_y < clipped_bottom {
out.push((h_left, clipped_y, h_right - h_left, clipped_bottom - clipped_y, highlight_color));
}
@@ -1301,11 +1339,11 @@ impl TextBox {
.filter_map(|i| index_map.get(i).filter(|p| p.0 == line_idx).map(|p| p.1))
.collect();
if let (Some(&a), Some(&b)) = (cols.iter().min(), cols.iter().max()) {
- let ux = x + 8.0 + self.line_col_x(line_idx, a) - self.scroll_x;
+ let ux = x + pad + self.line_col_x(line_idx, a) - self.scroll_x;
let uw = self.line_col_x(line_idx, b + 1) - self.line_col_x(line_idx, a);
- let uy = self.rect.y + top + 8.0 + ((line_idx + 1) as f32 * line_height) - 2.0 - self.scroll_y;
- let left = ux.max(x + 8.0);
- let right = (ux + uw).min(x + w - 8.0);
+ let uy = self.rect.y + top + pad + ((line_idx + 1) as f32 * line_height) - 2.0 - self.scroll_y;
+ let left = ux.max(x + pad);
+ let right = (ux + uw).min(x + w - pad);
if left < right && uy >= view_top && uy + 1.5 <= view_bottom {
out.push((left, uy, right - left, 1.5, cursor_color));
}
@@ -1316,12 +1354,12 @@ impl TextBox {
if self.editing {
let caret_h = self.font_size * 1.15;
let (cursor_l, cursor_c) = index_map[self.cursor_idx.min(index_map.len() - 1)];
- let cursor_x = x + 8.0 + self.line_col_x(cursor_l, cursor_c) - self.scroll_x;
- let cursor_y = self.rect.y + top + 8.0 + (cursor_l as f32 * line_height) + (line_height - caret_h) / 2.0 - self.scroll_y;
+ let cursor_x = x + pad + self.line_col_x(cursor_l, cursor_c) - self.scroll_x;
+ let cursor_y = self.rect.y + top + pad + (cursor_l as f32 * line_height) + (line_height - caret_h) / 2.0 - self.scroll_y;
caret = Some([cursor_x, cursor_y, 1.5, caret_h]);
let clipped_y = cursor_y.max(view_top);
let clipped_bottom = (cursor_y + caret_h).min(view_bottom);
- if cursor_x >= x + 8.0 && cursor_x <= x + w - 8.0 {
+ if cursor_x >= x + pad && cursor_x <= x + w - pad {
if clipped_y < clipped_bottom {
out.push((cursor_x, clipped_y, 1.5, clipped_bottom - clipped_y, cursor_color));
}
@@ -1332,9 +1370,9 @@ impl TextBox {
if start != end {
let h_left_offset = self.glyph_positions.get(start).copied().unwrap_or_else(|| start as f32 * char_width);
let h_right_offset = self.glyph_positions.get(end).copied().unwrap_or_else(|| end as f32 * char_width);
- let highlight_x = x + 8.0 + h_left_offset - self.scroll_x;
- let h_left = highlight_x.max(x + 8.0);
- let h_right = (x + 8.0 + h_right_offset - self.scroll_x).min(x + w - 8.0);
+ let highlight_x = x + pad + h_left_offset - self.scroll_x;
+ let h_left = highlight_x.max(x + pad);
+ let h_right = (x + pad + h_right_offset - self.scroll_x).min(x + w - pad);
if h_left < h_right {
out.push((
h_left,
@@ -1348,8 +1386,8 @@ impl TextBox {
if let Some((cs, cl)) = self.composing {
let at = |i: usize| self.glyph_positions.get(i).copied().unwrap_or(i as f32 * char_width);
- let left = (x + 8.0 + at(cs) - self.scroll_x).max(x + 8.0);
- let right = (x + 8.0 + at(cs + cl) - self.scroll_x).min(x + w - 8.0);
+ let left = (x + pad + at(cs) - self.scroll_x).max(x + pad);
+ let right = (x + pad + at(cs + cl) - self.scroll_x).min(x + w - pad);
if left < right {
let text_y = crate::layout::align_text_y(self.rect.y, self.rect.height, self.font_size, top);
out.push((left, text_y + self.font_size + 1.0, right - left, 1.5, cursor_color));
@@ -1362,11 +1400,11 @@ impl TextBox {
} else {
self.cursor_x_offset
};
- let cursor_x = x + 8.0 + offset - self.scroll_x;
+ let cursor_x = x + pad + offset - self.scroll_x;
let text_y = crate::layout::align_text_y(self.rect.y, self.rect.height, self.font_size, top);
let cursor_y = text_y + (self.font_size - caret_h) / 2.0;
caret = Some([cursor_x, cursor_y, 1.5, caret_h]);
- if cursor_x >= x + 8.0 && cursor_x <= x + w - 8.0 {
+ if cursor_x >= x + pad && cursor_x <= x + w - pad {
out.push((cursor_x, cursor_y, 1.5, caret_h, cursor_color));
}
}
@@ -1377,6 +1415,7 @@ impl TextBox {
/// The value/placeholder text lines — the legacy `text_labels` body minus the control
/// label (the adapter's base-label machinery draws that).
fn value_labels(&self) -> Vec<TextLabel> {
+ let pad = self.pad();
let mut labels = Vec::new();
let top = self.label_top();
let mut val_text = if self.editing {
@@ -1416,7 +1455,7 @@ impl TextBox {
let char_width = self.char_width();
let line_height = self.line_height();
let max_chars = if self.line_wrap_enabled() {
- (((w - 16.0) / char_width).floor() as usize).max(1)
+ (((w - 2.0 * pad) / char_width).floor() as usize).max(1)
} else {
999999
};
@@ -1446,8 +1485,8 @@ impl TextBox {
for (line_idx, line_text) in lines_to_draw.iter().enumerate() {
labels.push(TextLabel {
text: line_text.clone(),
- x: x + 8.0 - self.scroll_x,
- y: self.rect.y + top + 8.0 + (line_idx as f32 * line_height) + (line_height - self.font_size) / 2.0 - self.scroll_y,
+ x: x + pad - self.scroll_x,
+ y: self.rect.y + top + pad + (line_idx as f32 * line_height) + (line_height - self.font_size) / 2.0 - self.scroll_y,
font_size: self.font_size,
color: label_color,
});
@@ -1455,7 +1494,7 @@ impl TextBox {
} else {
labels.push(TextLabel {
text: display_text,
- x: x + 8.0 - self.scroll_x,
+ x: x + pad - self.scroll_x,
y: crate::layout::align_text_y(self.rect.y, self.rect.height, self.font_size, top),
font_size: self.font_size,
color: label_color,
@@ -1648,16 +1687,13 @@ impl Paint for TextBox {
}
fn text_bounds(&self, rect: Rect) -> Option<[f32; 4]> {
- // 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;
- Some([rect.x, base_y, rect.x + rect.width, base_y + base_h])
+ Some(self.text_clip(rect))
}
/// The legacy `prepare_text`: sync font family/size with the live config defaults, then
/// shape the display text and record per-glyph advances (`map_x_to_idx` reads them).
fn prepare_text(&mut self, fs: &mut cosmic_text::FontSystem, _rect: Rect) {
+ let pad = self.pad();
// The input method's composition, shown before the buffer is shaped.
self.sync_preedit();
let (style_family, style_size) = crate::layout::control_label_font_detached_parsed();
@@ -1703,7 +1739,7 @@ impl Paint for TextBox {
// paint time.
let wrap = self.multiline.then(|| {
if self.line_wrap_enabled() {
- (((self.rect.width - 16.0) / self.char_width()).floor() as usize).max(1)
+ (((self.rect.width - 2.0 * pad) / self.char_width()).floor() as usize).max(1)
} else {
999999
}
@@ -1730,6 +1766,7 @@ impl Paint for TextBox {
}
fn paint(&self, rect: Rect, ctx: &mut PaintCtx) {
+ let pad = self.pad();
let top = self.label_top();
let base_y = rect.y - top;
let base_h = rect.height + top;
@@ -1822,12 +1859,12 @@ impl Paint for TextBox {
// Relief scrollbar for overflowing multiline content — the shared
// groove + raised-pill painter (the TreeList treatment), persistent
// rather than activity-faded: an editor keeps its position
- // indicator. Content height mirrors clamp_scroll's math (8px pad
+ // indicator. Content height mirrors clamp_scroll's math (`pad`
// top and bottom), so the thumb tracks the scroll range exactly.
if self.multiline {
let line_height = self.line_height();
let max_chars = if self.line_wrap_enabled() {
- (((self.rect.width - 16.0) / self.char_width()).floor() as usize).max(1)
+ (((self.rect.width - 2.0 * pad) / self.char_width()).floor() as usize).max(1)
} else {
999999
};
@@ -1835,7 +1872,7 @@ impl Paint for TextBox {
crate::widget::container::scroll_box::paint_relief_scrollbar(
ctx,
Rect { x, y: self.rect.y + top, width: w, height: visual_h },
- content_h + 16.0,
+ content_h + 2.0 * pad,
self.scroll_y,
);
}
@@ -1843,13 +1880,9 @@ impl Paint for TextBox {
// The content is whatever has been typed, so a line longer than the
// well is routine rather than exceptional; the well scrolls, but
- // nothing stopped the glyphs drawing outside it.
- let well = Some([
- self.rect.x,
- self.rect.y,
- self.rect.x + self.rect.width,
- self.rect.y + self.rect.height,
- ]);
+ // nothing stopped the glyphs drawing outside it. (The adapter's label
+ // bridge swaps this for `text_bounds` — the same clip.)
+ let well = Some(self.text_clip(Rect { x: self.rect.x, y: self.rect.y + top, width: self.rect.width, height: visual_h }));
let font = self.value_font();
for tl in self.value_labels() {
ctx.text_with(tl.text, tl.x, tl.y, tl.font_size, tl.color, font.clone(), well);
@@ -2631,6 +2664,23 @@ mod tests {
assert_eq!(tb_single.border_width(), 1.0);
assert_eq!(tb_multi.border_width(), configured);
}
+ /// A tall recessed box's wall is the full `bevel_width`, deeper than the
+ /// old fixed 8px inset: its text starts on the well's floor, past the
+ /// wall, not on it (cce-fonts' preview box drew its sample over its relief).
+ #[test]
+ fn tall_box_text_starts_past_its_relief_wall() {
+ let _dummy = crate::context::UiContext::new();
+ let mut tb = TextBox::new("The quick brown fox".to_string())
+ .with_multiline(true)
+ .with_recessed(true);
+ tb.set_rect(10.0, 10.0, 400.0, 180.0);
+ let field = tb.well().expect("a recessed box with rounded corners carves a well");
+ let floor_x = field.rect.x + field.depth;
+ let floor_y = field.rect.y + field.depth;
+ let first = &tb.value_labels()[0];
+ assert!(first.x >= floor_x, "text x {} is on the wall (floor at {})", first.x, floor_x);
+ assert!(first.y >= floor_y, "text y {} is on the wall (floor at {})", first.y, floor_y);
+ }
/// A password box never hands its text to the clipboard -- not from a
/// selection, not by Ctrl+X, not through the menu -- and its menu offers
/// no Cut or Copy. Its Ctrl+X leaves the text in place.
diff --git a/src/widget/model.rs b/src/widget/model.rs
index 15b0d85..30f614f 100644
--- a/src/widget/model.rs
+++ b/src/widget/model.rs
@@ -1037,28 +1037,25 @@ impl<W: Layout + Paint + Input + 'static> Adapted<W> {
// uses (a TreeList's rows, a Breadcrumb's segments) or does not declare. A
// widget with no `widget_font` used to fall back to the engine's sans default
// here, so half the gallery's labels were in a different face.
+ //
+ // `text_bounds` clips the widget's content text only. The detached label sits
+ // in the strip above the content, so a widget clipping its content to its own
+ // rect (Graph) clipped its label away, and one clipping to the inside of its
+ // relief (TextBox's well floor) could not do so at all while the label shared
+ // the clip — TextBox widened it to the whole block, and its text ran over its
+ // wall. (The legacy scroll-ancestor clamp ended here too: always a no-op since
+ // Phase 6av — ScrollBox, the last scroll ancestor type, never appeared as a
+ // tree parent.)
let base_font = Some(crate::layout::control_label_font_detached());
- let mut fonted: Vec<(TextLabel, Option<String>)> = Vec::new();
+ let bounds = Paint::text_bounds(&self.inner, self.content_rect());
+ let mut out: Vec<(TextLabel, Option<String>, Option<[f32; 4]>)> = Vec::new();
if self.visible() {
- fonted.extend(painted.into_iter().map(|l| (l, prim_font.clone())));
+ out.extend(painted.into_iter().map(|l| (l, prim_font.clone(), bounds)));
if !Layout::inline_label(&self.inner) {
- fonted.extend(self.base_label_fallback().into_iter().map(|l| (l, base_font.clone())));
+ out.extend(self.base_label_fallback().into_iter().map(|l| (l, base_font.clone(), None)));
}
}
-
- if let Some(bounds) = Paint::text_bounds(&self.inner, self.content_rect()) {
- return fonted
- .into_iter()
- .map(|(l, font)| (l, font, Some(bounds)))
- .collect();
- }
-
- // The legacy scroll-ancestor clamp ended here: always a no-op since Phase 6av —
- // ScrollBox (the last scroll ancestor type) never appeared as a tree parent.
- fonted
- .into_iter()
- .map(|(l, font)| (l, font, None::<[f32; 4]>))
- .collect::<Vec<_>>()
+ out
}
/// The base-label text of a *detached*-label widget — a replica of the legacy default
@@ -2022,4 +2019,40 @@ mod tests {
assert_eq!(texts[0].2, Some([10.0, 20.0, 110.0, 50.0]), "text_bounds attached");
}
+ /// `text_bounds` clips the content text only: the detached label sits in the
+ /// strip above the content rect, so the content's clip would cut it away.
+ #[test]
+ fn text_bounds_leave_the_detached_label_unclipped() {
+ struct Tag;
+ impl Layout for Tag {}
+ impl Paint for Tag {
+ fn color(&self) -> [f32; 4] {
+ [0.0; 4]
+ }
+ fn paint(&self, rect: Rect, ctx: &mut PaintCtx) {
+ ctx.text("hi", rect.x + 2.0, rect.y + 2.0, 12.0, [1, 2, 3]);
+ }
+ fn text_bounds(&self, rect: Rect) -> Option<[f32; 4]> {
+ Some([rect.x, rect.y, rect.x + rect.width, rect.y + rect.height])
+ }
+ }
+ impl Input for Tag {}
+
+ let mut ctx = UiContext::new();
+ let mut w = Box::new(Adapted::new(Tag).with_label("Name"));
+ let (id, ptr) = (w.id(), w.as_ptr_mut());
+ ctx.register_widget(id, ptr);
+ unsafe { (*ptr).set_rect(10.0, 20.0, 100.0, 60.0) };
+
+ let list = paint_tree(&ctx, unsafe { &*ptr });
+ let bounds_of = |want: &str| {
+ list.items.iter().find_map(|it| match &it.prim {
+ Prim::Text { text, bounds, .. } if text == want => Some(*bounds),
+ _ => None,
+ })
+ };
+ assert!(matches!(bounds_of("hi"), Some(Some(_))), "content text is clipped");
+ assert_eq!(bounds_of("Name"), Some(None), "the label is not");
+ }
+
}