system settings
git clone https://git.lucas.co/cce-system-interface.git
fix(page): give the page scrollbar its own lane
The page scrollbar sat inside the page's 12px right margin, over the
right column's wall and content. While the page scrolls it is now laid
out narrower by the bar's width plus its stand-off, so the right column
clears the bar by the same margin the left column keeps from the window
edge. A page that fits keeps its full width and shows no bar.
The lane follows the page's measured height. A flip re-lays the page out
on the next frame, not twice in one, because a view registers widgets
and popovers as it draws. It cannot oscillate, since narrowing never
shortens a page.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
src/renderer.rs | 23 ++++++++++++++++++-----
src/scroll_bar.rs | 18 ++++++++++++++++++
2 files changed, 36 insertions(+), 5 deletions(-)
diff --git a/src/renderer.rs b/src/renderer.rs
index 894c584..2d55fb8 100644
--- a/src/renderer.rs
+++ b/src/renderer.rs
@@ -176,12 +176,11 @@ impl SystemInterface {
logical_sh - self.header_height - self.status_height
};
// The page scrollbar, on the designer parameter-pane geometry: the DE
- // width widened (the bar rides over page content and reads too slim at
- // stock width), stood off the window's right edge by the configured
+ // width widened, stood off the window's right edge by the configured
// inset instead of hugging it. Updated with LAST frame's content
// height — the legacy window pass also ran before this frame's content
// was measured.
- let sb_w = cce_ui::layout::scrollbar_width() * 1.6;
+ let sb_w = crate::scroll_bar::ScrollBar::width();
// TODO(style): the bar's 4px vertical stand-off pairs with the
// toolkit's `scrollbar_inset()` knob, not a rung of the ladder.
self.page_scroll_bar.set_rect(
@@ -352,6 +351,16 @@ impl SystemInterface {
}
self.content_h = max_y;
self.page_scroll_bar.update(self.scroll_y, max_y, lch);
+ // Give the bar its lane exactly while it shows. The flip lays the page
+ // out once more on the next frame (`needs_rebuild` below) rather than
+ // twice in this one: a view registers widgets and popovers as it
+ // draws. It cannot oscillate — a page that overflows at full width
+ // only grows taller in the narrower lane, and one that fits in the
+ // lane fits at full width too.
+ let lane_flipped = (max_y > lch) != self.page_scroll_bar.lane;
+ if lane_flipped {
+ self.page_scroll_bar.lane = !self.page_scroll_bar.lane;
+ }
let scroll_offset_y = self.scroll_y;
@@ -798,7 +807,7 @@ impl SystemInterface {
self.ui_context.set_focused_id(id);
}
}
- self.needs_rebuild = false;
+ self.needs_rebuild = lane_flipped;
self.laid_out_page = Some(self.app.current_page);
self.last_scroll_y = self.scroll_y;
self.ui_context.clear_dirty();
@@ -814,7 +823,11 @@ impl SystemInterface {
let margin = cce_ui::layout::root_plate_inset();
let cx = cx + margin;
let cy = cy + margin;
- let cw = (cw - 2.0 * margin).max(1.0);
+ // While the page scrolls its right margin starts at the scrollbar's
+ // inner edge, not the window's: the bar no longer covers the right
+ // column's wall and content.
+ let lane = if self.page_scroll_bar.lane { crate::scroll_bar::ScrollBar::lane_width() } else { 0.0 };
+ let cw = (cw - 2.0 * margin - lane).max(1.0);
let ch = (ch - 2.0 * margin).max(1.0);
let mut layout = AdaptiveGrid::new(cce_ui::layout::grid_min_col_width(), cce_ui::layout::root_plate_gap());
// Page root dissolved (6u): the ctrl-nav entry focuses section 0, so root focus is
diff --git a/src/scroll_bar.rs b/src/scroll_bar.rs
index 62d63e7..964ea15 100644
--- a/src/scroll_bar.rs
+++ b/src/scroll_bar.rs
@@ -15,6 +15,11 @@ pub struct ScrollBar {
pub content_h: f32,
pub viewport_h: f32,
pub dragging: bool,
+ /// The page is laid out narrower by [`ScrollBar::lane_width`] so the bar
+ /// has a lane of its own beside the sections instead of riding over the
+ /// right column. Set from the page's measured height each layout (see
+ /// `rebuild_layout_inner`), so it is on exactly while the bar shows.
+ pub lane: bool,
hovered: bool,
/// The shared raise/sink hysteresis (the designer parameter-pane treatment):
/// idle the bar sinks behind the translucent window plate and takes no
@@ -29,11 +34,24 @@ impl ScrollBar {
content_h: 0.0,
viewport_h: 0.0,
dragging: false,
+ lane: false,
hovered: false,
activity: ScrollbarActivity::new(),
})
}
+ /// The bar's width: the DE scrollbar width widened (at stock width it
+ /// reads too slim against the page's wells).
+ pub fn width() -> f32 {
+ cce_ui::layout::scrollbar_width() * 1.6
+ }
+
+ /// The window's right-edge strip the bar occupies — its width plus its
+ /// stand-off from the window edge. The page reserves it while it scrolls.
+ pub fn lane_width() -> f32 {
+ Self::width() + cce_ui::layout::scrollbar_inset()
+ }
+
pub fn update(&mut self, scroll_y: f32, content_h: f32, viewport_h: f32) {
self.scroll_y = scroll_y;
self.content_h = content_h;