GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
feat: relief scrollbar for multiline TextBox; painter shared
The groove + raised-pill relief painter moves to a free
paint_relief_scrollbar (ScrollBox::paint_scrollbar_relief delegates).
Multiline TextBox draws it persistently when content overflows — an
editor keeps its position indicator, unlike the activity-faded tree
bar. Content height mirrors clamp_scroll (8px pad each end) so the
thumb tracks the scroll range exactly.
Co-Authored-By: Claude Fable 5 <[email protected]>
src/widget/container/scroll_box.rs | 101 +++++++++++++++++++++++--------------
src/widget/input/text_box.rs | 21 ++++++++
2 files changed, 83 insertions(+), 39 deletions(-)
diff --git a/src/widget/container/scroll_box.rs b/src/widget/container/scroll_box.rs
index 91ea477..c2f61e8 100644
--- a/src/widget/container/scroll_box.rs
+++ b/src/widget/container/scroll_box.rs
@@ -7,6 +7,55 @@
use crate::widget::*;
+/// Shared relief-scrollbar painter: the track a carved groove
+/// ([`crate::scene::paint::PaintCtx::recess`]), the thumb a raised rounded
+/// plate riding in it ([`crate::scene::paint::PaintCtx::bevel`], configured
+/// thumb color) — the DE highlight/shadow bevel treatment. `viewport` is the
+/// scrolling area's box; the bar hugs its right edge. No-op while the content
+/// fits.
+pub fn paint_relief_scrollbar(
+ pc: &mut crate::scene::paint::PaintCtx,
+ viewport: crate::scene::layout::Rect,
+ content_h: f32,
+ scroll_y: f32,
+) {
+ if content_h <= viewport.height {
+ return;
+ }
+ let sb_w = crate::layout::scrollbar_width();
+ let sb_x = viewport.x + viewport.width - sb_w - 4.0;
+ let sb_track_h = viewport.height - 8.0;
+ let sb_track_y = viewport.y + 4.0;
+
+ let visible_ratio = viewport.height / content_h;
+ let thumb_h = if sb_track_h <= 20.0 {
+ sb_track_h
+ } else {
+ (sb_track_h * visible_ratio).clamp(20.0, sb_track_h)
+ };
+ let max_scroll = (content_h - viewport.height).max(0.0);
+ let scroll_ratio = if max_scroll > 0.0 { scroll_y / max_scroll } else { 0.0 };
+ let thumb_y = sb_track_y + scroll_ratio * (sb_track_h - thumb_h);
+
+ // Pill radii; the roll width scales to the bar (the DE bevel width would
+ // swallow a 14px-wide thumb whole).
+ let r = sb_w * 0.5;
+ let depth = crate::layout::bevel_width().min(sb_w * 0.35);
+ let radii = (r, r, r, r);
+ use crate::scene::layout::Rect;
+ pc.recess(
+ Rect { x: sb_x, y: sb_track_y, width: sb_w, height: sb_track_h },
+ radii,
+ depth,
+ );
+ pc.bevel(
+ Rect { x: sb_x, y: thumb_y, width: sb_w, height: thumb_h },
+ radii,
+ crate::color::scrollbar_thumb_color(),
+ depth,
+ );
+}
+
#[derive(Debug, Clone)]
pub struct ScrollBox {
pub base: Widget,
@@ -300,46 +349,20 @@ impl ScrollBox {
quads
}
- /// The scrollbar in the DE's relief language: the track a carved groove
- /// ([`PaintCtx::recess`]), the thumb a raised rounded plate riding in it
- /// ([`PaintCtx::bevel`] with the configured thumb color) — the
- /// highlight/shadow bevel treatment. The flat-quad view stays available
- /// through [`extra_quads`](Self::extra_quads) for legacy paths.
+ /// The scrollbar in the DE's relief language — see
+ /// [`paint_relief_scrollbar`]. The flat-quad view stays available through
+ /// [`extra_quads`](Self::extra_quads) for legacy paths.
pub fn paint_scrollbar_relief(&self, pc: &mut crate::scene::paint::PaintCtx) {
- if self.content_h <= self.viewport_h {
- return;
- }
- let sb_w = crate::layout::scrollbar_width();
- let sb_x = self.base.x + self.base.w - sb_w - 4.0;
- let sb_track_h = self.viewport_h - 8.0;
- let sb_track_y = self.viewport_y + 4.0;
-
- let visible_ratio = self.viewport_h / self.content_h;
- let thumb_h = if sb_track_h <= 20.0 {
- sb_track_h
- } else {
- (sb_track_h * visible_ratio).clamp(20.0, sb_track_h)
- };
- let max_scroll = (self.content_h - self.viewport_h).max(0.0);
- let scroll_ratio = if max_scroll > 0.0 { self.scroll_y / max_scroll } else { 0.0 };
- let thumb_y = sb_track_y + scroll_ratio * (sb_track_h - thumb_h);
-
- // Pill radii; the roll width scales to the bar (the DE bevel width
- // would swallow a 14px-wide thumb whole).
- let r = sb_w * 0.5;
- let depth = crate::layout::bevel_width().min(sb_w * 0.35);
- let radii = (r, r, r, r);
- use crate::scene::layout::Rect;
- pc.recess(
- Rect { x: sb_x, y: sb_track_y, width: sb_w, height: sb_track_h },
- radii,
- depth,
- );
- pc.bevel(
- Rect { x: sb_x, y: thumb_y, width: sb_w, height: thumb_h },
- radii,
- crate::color::scrollbar_thumb_color(),
- depth,
+ paint_relief_scrollbar(
+ pc,
+ crate::scene::layout::Rect {
+ x: self.base.x,
+ y: self.viewport_y,
+ width: self.base.w,
+ height: self.viewport_h,
+ },
+ self.content_h,
+ self.scroll_y,
);
}
diff --git a/src/widget/input/text_box.rs b/src/widget/input/text_box.rs
index 8cbd904..28bf28c 100644
--- a/src/widget/input/text_box.rs
+++ b/src/widget/input/text_box.rs
@@ -1326,6 +1326,27 @@ impl Paint for TextBox {
for (qx, qy, qw, qh, qc) in quads {
ctx.quad(Rect { x: qx, y: qy, width: qw, height: qh }, qc);
}
+
+ // 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
+ // 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)
+ } else {
+ 999999
+ };
+ let content_h = self.wrap_text(max_chars).0.len() as f32 * line_height;
+ 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,
+ self.scroll_y,
+ );
+ }
}
for tl in self.value_labels() {