pub struct ScrollState { /* private fields */ }Expand description
Shared scroll state for a scrollable surface.
Offsets are always clamped to the valid range derived from content_size and viewport_size.
Implementations§
Source§impl ScrollState
impl ScrollState
Sourcepub fn new(content: ScrollSize, viewport: ScrollSize) -> Self
pub fn new(content: ScrollSize, viewport: ScrollSize) -> Self
Create a new ScrollState with the given content and viewport sizes.
Examples found in repository?
69fn main() -> minui::Result<()> {
70 // Shared scroll model (content size + viewport size + offsets).
71 // Seed with non-zero sizes so scrollbar math is sane before first draw.
72 let scroll = Rc::new(RefCell::new(ScrollState::new(
73 ScrollSize::new(1, 1),
74 ScrollSize::new(1, 1),
75 )));
76
77 // Initial scrollbars; sizes are refreshed every draw frame.
78 let vbar = ScrollBar::new(
79 1,
80 1,
81 Rc::clone(&scroll),
82 ScrollBarOptions {
83 orientation: ScrollOrientation::Vertical,
84 show_arrows: true,
85 scroll_step: Some(1),
86 ..ScrollBarOptions::default()
87 },
88 );
89
90 let hbar = ScrollBar::new(
91 1,
92 1,
93 Rc::clone(&scroll),
94 ScrollBarOptions {
95 orientation: ScrollOrientation::Horizontal,
96 show_arrows: true,
97 scroll_step: Some(2),
98 ..ScrollBarOptions::default()
99 },
100 );
101
102 let initial = ScrollDemoState {
103 ui: UiScene::new(),
104 scroll,
105 vbar,
106 hbar,
107 autohide: AutoHide::new(Duration::from_millis(900), 2),
108 };
109
110 let mut app = App::new(initial)?;
111
112 app.run(
113 // ============================
114 // Update: route events
115 // ============================
116 |state, event| {
117 // Apply common scene policies:
118 // - observe mouse position
119 // - tab traversal (if any focusables are registered this frame)
120 // - click-to-focus + mouse capture
121 let _effects = state.ui.apply_policies(&event);
122
123 // Quit (prefer modifier-aware events, with legacy fallback)
124 if let Event::KeyWithModifiers(k) = event {
125 if matches!(k.key, KeyKind::Char('q')) {
126 return false;
127 }
128 }
129 if matches!(event, Event::Character('q')) {
130 return false;
131 }
132
133 // Mouse wheel scrolling:
134 // Route ONLY to the scroll target under cursor (preferred) or the focused scroll target.
135 // This is now centralized in UiScene.
136 match event {
137 Event::MouseScroll { delta } => {
138 if let Some(RouteTarget::Id(_id)) = state.ui.route_wheel_event(&event) {
139 state.autohide.mark_activity();
140
141 // Convention: delta > 0 scrolls up, delta < 0 scrolls down.
142 // We invert to translate "scroll down" into positive offset movement.
143 let dy: i16 = -(delta as i16);
144 state
145 .scroll
146 .borrow_mut()
147 .scroll_by(ScrollOrientation::Vertical, dy);
148 }
149 }
150 Event::MouseScrollHorizontal { delta } => {
151 if let Some(RouteTarget::Id(_id)) = state.ui.route_wheel_event(&event) {
152 state.autohide.mark_activity();
153
154 let dx: i16 = -(delta as i16);
155 state
156 .scroll
157 .borrow_mut()
158 .scroll_by(ScrollOrientation::Horizontal, dx);
159 }
160 }
161 _ => {}
162 }
163
164 // Keyboard scrolling (prefer modifier-aware events, with legacy fallback).
165 match event {
166 Event::KeyWithModifiers(k) => match k.key {
167 KeyKind::Up => state
168 .vbar
169 .scroll_by_fraction(-1.0 / 5.0, ScrollUnit::Viewport),
170 KeyKind::Down => state
171 .vbar
172 .scroll_by_fraction(1.0 / 5.0, ScrollUnit::Viewport),
173 KeyKind::Left => state
174 .hbar
175 .scroll_by_fraction(-1.0 / 5.0, ScrollUnit::Viewport),
176 KeyKind::Right => state
177 .hbar
178 .scroll_by_fraction(1.0 / 5.0, ScrollUnit::Viewport),
179 _ => {}
180 },
181
182 // Legacy fallback.
183 Event::KeyUp => state
184 .vbar
185 .scroll_by_fraction(-1.0 / 5.0, ScrollUnit::Viewport),
186 Event::KeyDown => state
187 .vbar
188 .scroll_by_fraction(1.0 / 5.0, ScrollUnit::Viewport),
189 Event::KeyLeft => state
190 .hbar
191 .scroll_by_fraction(-1.0 / 5.0, ScrollUnit::Viewport),
192 Event::KeyRight => state
193 .hbar
194 .scroll_by_fraction(1.0 / 5.0, ScrollUnit::Viewport),
195
196 _ => {}
197 }
198
199 // Route pointer events into scrollbars (click/drag/arrows), using UiScene owner grouping.
200 //
201 // IMPORTANT:
202 // - Only route if the relevant areas were registered (i.e. visible).
203 // - If capture is active, UiScene ensures drag/release continue to target the captured id.
204 //
205 // We route to an OWNER so the app can forward to the owning ScrollBar instance.
206 if let Some(target) = state.ui.route_mouse_event_to_owner(&event) {
207 match target {
208 RouteTarget::Owner(OWNER_VSCROLL) => {
209 if let Some(entry) = state.ui.get(ID_VSCROLL_ROOT) {
210 let _ = state.vbar.handle_event(&event, entry.area);
211 }
212 }
213 RouteTarget::Owner(OWNER_HSCROLL) => {
214 if let Some(entry) = state.ui.get(ID_HSCROLL_ROOT) {
215 let _ = state.hbar.handle_event(&event, entry.area);
216 }
217 }
218 _ => {}
219 }
220 }
221
222 true
223 },
224 // ============================
225 // Draw: register areas + draw
226 // ============================
227 |state, window| {
228 state.ui.begin_frame();
229
230 let (term_w, term_h) = window.get_size();
231
232 // Layout constants
233 let margin: u16 = 1;
234 let header_h: u16 = 1;
235
236 // Scrollbar sizes (vbar on the right, hbar on bottom)
237 let vbar_w: u16 = 1;
238 let hbar_h: u16 = 1;
239
240 // ScrollBox styling affects its *outer* size vs the *inner* content viewport.
241 // Because the ScrollBox draws a border + padding, we must reserve extra space so
242 // the right/bottom borders don't get overwritten by scrollbars.
243 let box_border: u16 = 1; // single-line border thickness
244 let box_padding: u16 = 1; // we set ContainerPadding::uniform(1) below
245
246 // Compute a safe outer rect for the ScrollBox (what you pass to with_position_and_size)
247 // leaving room for:
248 // - header line
249 // - scrollbars (drawn OUTSIDE the ScrollBox frame)
250 //
251 // IMPORTANT:
252 // - The ScrollBox outer rect must be fully within the terminal bounds.
253 // - The scrollbars are then placed at `outer_x + outer_w` (right) and `outer_y + outer_h` (bottom),
254 // so we must reserve 1 extra column/row for them as well.
255 let outer_x = margin;
256 let outer_y = margin + header_h;
257
258 // Total insets that reduce the usable inner content area.
259 let inner_inset_x = box_border + box_padding;
260 let inner_inset_y = box_border + box_padding;
261
262 // Available space inside terminal margins for the ScrollBox outer rect.
263 // We reserve the scrollbar column/row OUTSIDE the ScrollBox.
264 let max_outer_w = term_w.saturating_sub(margin * 2).saturating_sub(vbar_w);
265 let max_outer_h = term_h
266 .saturating_sub(margin * 2)
267 .saturating_sub(header_h)
268 .saturating_sub(hbar_h);
269
270 // Clamp outer size to available space. Do NOT force a minimum that can exceed terminal bounds.
271 // If the terminal is too small, we'll end up with a very small box (and the scrollbars may be skipped).
272 let outer_w = max_outer_w;
273 let outer_h = max_outer_h;
274
275 // Inner viewport (content area) starts after border+padding.
276 // NOTE: these are currently unused in this example, but kept for clarity.
277 let viewport_x = outer_x + inner_inset_x;
278 let viewport_y = outer_y + inner_inset_y;
279 let viewport_w = outer_w.saturating_sub(inner_inset_x * 2);
280 let viewport_h = outer_h.saturating_sub(inner_inset_y * 2);
281
282 let _ = (viewport_x, viewport_y, viewport_w, viewport_h);
283
284 // Header / help line
285 window.write_str(
286 margin,
287 margin,
288 "Scroll demo: wheel/drag scrollbars • arrows/keys work • press 'q' to quit",
289 )?;
290
291 // Build content each frame (no cloning needed).
292 // Use a row gap to exercise gap-aware content measurement in ScrollBox.
293 let mut content = Container::vertical().with_row_gap(Gap::Pixels(1));
294
295 // Lots of lines (taller than viewport).
296 for i in 0..150u16 {
297 content = content.add_child(
298 Label::new(format!(
299 "Line {:03} | The quick brown fox jumps over the lazy dog. {}",
300 i,
301 if i % 5 == 0 { "[drag the thumb]" } else { "" }
302 ))
303 .with_text_color(Color::White),
304 );
305 }
306
307 // ScrollBox bound to shared state and sized to viewport.
308 let scrollbox = ScrollBox::both()
309 .with_state(Rc::clone(&state.scroll))
310 // IMPORTANT: pass the OUTER rect (includes border/padding), not the inner viewport.
311 .with_position_and_size(outer_x, outer_y, outer_w, outer_h)
312 .with_border()
313 .with_border_chars(BorderChars::single_line())
314 .with_title("Scrollable content")
315 .with_title_alignment(TitleAlignment::Left)
316 .with_padding(minui::widgets::ContainerPadding::uniform(1))
317 .with_row_gap(Gap::Pixels(1))
318 .add_child(content);
319
320 // Register scrollbox interaction regions:
321 // - outer frame as focusable (panel/split-friendly)
322 // - inner viewport as scrollable (wheel routing)
323 let outer_area = WidgetArea::new(outer_x, outer_y, outer_w, outer_h);
324 scrollbox.register_with_ids(
325 state.ui.cache_mut(),
326 outer_area,
327 ID_SCROLLBOX,
328 ID_SCROLLBOX_VIEWPORT,
329 );
330
331 // Draw scrollbox (updates ScrollState sizes + applies offsets via WindowView).
332 scrollbox.draw(window)?;
333
334 // Vertical scrollbar area (right side)
335 // Place it just outside the ScrollBox's outer border so it doesn't overwrite the frame.
336 // This must still be inside the terminal bounds (x < term_w).
337 let vbar_x = outer_x + outer_w;
338 let vbar_y = outer_y;
339 let vbar_h = outer_h;
340
341 // If there isn't room for the scrollbar column (e.g. tiny terminals), just skip drawing it.
342 if vbar_x >= term_w {
343 return Ok(());
344 }
345
346 // Resize + sync existing scrollbar instead of recreating it every frame.
347 // Recreating would reset drag state, making thumb dragging feel broken.
348 state.vbar.set_size(1, vbar_h);
349 state.vbar.set_show_arrows(true);
350 state.vbar.set_scroll_step(Some(1));
351 state.vbar.sync_from_state_and_resize_parts();
352
353 let v_area = WidgetArea::new(vbar_x, vbar_y, 1, vbar_h);
354
355 // Auto-hide policy: show if recently scrolled, mouse is near, or currently dragging.
356 // Reveal policy:
357 // - show when recently scrolled
358 // - show when mouse is near the scrollbar area
359 // - show when mouse is near the scrollbox RIGHT edge strip (even if the bar itself is hidden)
360 // - show while dragging
361 let mouse_pos = state.ui.last_mouse_pos();
362
363 // Only consider proximity to a thin strip along the scrollbox's right edge.
364 // This prevents the scrollbars from staying visible just because the cursor is somewhere
365 // inside/near the scrollbox.
366 let near_scrollbox_right_edge = if let Some((mx, my)) = mouse_pos {
367 // A 1-column strip at the right edge of the scrollbox.
368 let edge_x = outer_x + outer_w.saturating_sub(1);
369 let edge_strip = WidgetArea::new(edge_x, outer_y, 1, outer_h);
370 state.autohide.is_point_near_area(mx, my, edge_strip)
371 } else {
372 false
373 };
374
375 let show_vbar = state
376 .autohide
377 .should_show(v_area, mouse_pos, state.vbar.is_dragging())
378 || near_scrollbox_right_edge;
379
380 if show_vbar {
381 // Register sub-areas so routing can distinguish thumb vs arrows.
382 // Root is scrollable (hover target), thumb/track is draggable, arrows are focusable.
383 // Register areas into the underlying cache (UiScene wraps InteractionCache).
384 state.vbar.register_with_ids(
385 state.ui.cache_mut(),
386 v_area,
387 ID_VSCROLL_ROOT,
388 ID_VSCROLL_THUMB,
389 Some(ID_VSCROLL_ARROW_START),
390 Some(ID_VSCROLL_ARROW_END),
391 );
392
393 // Group all scrollbar sub-ids under a single owner for routing.
394 state.ui.set_owner_for_ids(
395 OWNER_VSCROLL,
396 &[
397 ID_VSCROLL_ROOT,
398 ID_VSCROLL_THUMB,
399 ID_VSCROLL_ARROW_START,
400 ID_VSCROLL_ARROW_END,
401 ],
402 );
403
404 {
405 let mut view = WindowView {
406 window,
407 x_offset: vbar_x,
408 y_offset: vbar_y,
409 scroll_x: 0,
410 scroll_y: 0,
411 width: 1,
412 height: vbar_h,
413 };
414 state.vbar.draw(&mut view)?;
415 }
416 }
417
418 // Horizontal scrollbar area (bottom)
419 // Place it just outside the ScrollBox's outer border so it doesn't overwrite the frame.
420 // This must still be inside the terminal bounds (y < term_h).
421 let hbar_x = outer_x;
422 let hbar_y = outer_y + outer_h;
423 let hbar_w = outer_w;
424
425 // If there isn't room for the scrollbar row (e.g. tiny terminals), just skip drawing it.
426 if hbar_y >= term_h {
427 return Ok(());
428 }
429
430 // Resize + sync existing scrollbar instead of recreating it every frame.
431 // Recreating would reset drag state, making thumb dragging feel broken.
432 state.hbar.set_size(hbar_w, 1);
433 state.hbar.set_show_arrows(true);
434 state.hbar.set_scroll_step(Some(2));
435 state.hbar.sync_from_state_and_resize_parts();
436
437 let h_area = WidgetArea::new(hbar_x, hbar_y, hbar_w, 1);
438
439 // Auto-hide policy: show if recently scrolled, mouse is near, or currently dragging.
440 // Reveal policy:
441 // - show when recently scrolled
442 // - show when mouse is near the scrollbar area
443 // - show when mouse is near the scrollbox BOTTOM edge strip (even if the bar itself is hidden)
444 // - show while dragging
445 let mouse_pos = state.ui.last_mouse_pos();
446
447 // Only consider proximity to a thin strip along the scrollbox's bottom edge.
448 // This prevents the scrollbars from staying visible just because the cursor is somewhere
449 // inside/near the scrollbox.
450 let near_scrollbox_bottom_edge = if let Some((mx, my)) = mouse_pos {
451 // A 1-row strip at the bottom edge of the scrollbox.
452 let edge_y = outer_y + outer_h.saturating_sub(1);
453 let edge_strip = WidgetArea::new(outer_x, edge_y, outer_w, 1);
454 state.autohide.is_point_near_area(mx, my, edge_strip)
455 } else {
456 false
457 };
458
459 let show_hbar = state
460 .autohide
461 .should_show(h_area, mouse_pos, state.hbar.is_dragging())
462 || near_scrollbox_bottom_edge;
463
464 if show_hbar {
465 // Register sub-areas so routing can distinguish thumb vs arrows.
466 // Root is scrollable (hover target), thumb/track is draggable, arrows are focusable.
467 // Register areas into the underlying cache (UiScene wraps InteractionCache).
468 state.hbar.register_with_ids(
469 state.ui.cache_mut(),
470 h_area,
471 ID_HSCROLL_ROOT,
472 ID_HSCROLL_THUMB,
473 Some(ID_HSCROLL_ARROW_START),
474 Some(ID_HSCROLL_ARROW_END),
475 );
476
477 // Group all scrollbar sub-ids under a single owner for routing.
478 state.ui.set_owner_for_ids(
479 OWNER_HSCROLL,
480 &[
481 ID_HSCROLL_ROOT,
482 ID_HSCROLL_THUMB,
483 ID_HSCROLL_ARROW_START,
484 ID_HSCROLL_ARROW_END,
485 ],
486 );
487
488 {
489 let mut view = WindowView {
490 window,
491 x_offset: hbar_x,
492 y_offset: hbar_y,
493 scroll_x: 0,
494 scroll_y: 0,
495 width: hbar_w,
496 height: 1,
497 };
498 state.hbar.draw(&mut view)?;
499 }
500 }
501
502 window.end_frame()?;
503 Ok(())
504 },
505 )?;
506
507 Ok(())
508}Sourcepub fn content_size(&self) -> ScrollSize
pub fn content_size(&self) -> ScrollSize
Returns the content size.
Sourcepub fn viewport_size(&self) -> ScrollSize
pub fn viewport_size(&self) -> ScrollSize
Returns the viewport size.
Sourcepub fn offset(&self) -> ScrollOffset
pub fn offset(&self) -> ScrollOffset
Returns the current scroll offset.
Sourcepub fn offset_for(&self, orientation: ScrollOrientation) -> u16
pub fn offset_for(&self, orientation: ScrollOrientation) -> u16
Returns the current scroll offset along an orientation.
Sourcepub fn set_content_size(&mut self, size: ScrollSize)
pub fn set_content_size(&mut self, size: ScrollSize)
Sets the content size and clamps the current offset.
Sourcepub fn set_viewport_size(&mut self, size: ScrollSize)
pub fn set_viewport_size(&mut self, size: ScrollSize)
Sets the viewport size and clamps the current offset.
Sourcepub fn set_offset(&mut self, offset: ScrollOffset)
pub fn set_offset(&mut self, offset: ScrollOffset)
Sets the offset (x,y) and clamps it.
Sourcepub fn set_offset_for(&mut self, orientation: ScrollOrientation, value: u16)
pub fn set_offset_for(&mut self, orientation: ScrollOrientation, value: u16)
Sets the offset along a single axis and clamps.
Sourcepub fn scroll_by(&mut self, orientation: ScrollOrientation, delta: i16)
pub fn scroll_by(&mut self, orientation: ScrollOrientation, delta: i16)
Scrolls by a signed delta on the given axis.
Negative values scroll up/left, positive scroll down/right.
Examples found in repository?
69fn main() -> minui::Result<()> {
70 // Shared scroll model (content size + viewport size + offsets).
71 // Seed with non-zero sizes so scrollbar math is sane before first draw.
72 let scroll = Rc::new(RefCell::new(ScrollState::new(
73 ScrollSize::new(1, 1),
74 ScrollSize::new(1, 1),
75 )));
76
77 // Initial scrollbars; sizes are refreshed every draw frame.
78 let vbar = ScrollBar::new(
79 1,
80 1,
81 Rc::clone(&scroll),
82 ScrollBarOptions {
83 orientation: ScrollOrientation::Vertical,
84 show_arrows: true,
85 scroll_step: Some(1),
86 ..ScrollBarOptions::default()
87 },
88 );
89
90 let hbar = ScrollBar::new(
91 1,
92 1,
93 Rc::clone(&scroll),
94 ScrollBarOptions {
95 orientation: ScrollOrientation::Horizontal,
96 show_arrows: true,
97 scroll_step: Some(2),
98 ..ScrollBarOptions::default()
99 },
100 );
101
102 let initial = ScrollDemoState {
103 ui: UiScene::new(),
104 scroll,
105 vbar,
106 hbar,
107 autohide: AutoHide::new(Duration::from_millis(900), 2),
108 };
109
110 let mut app = App::new(initial)?;
111
112 app.run(
113 // ============================
114 // Update: route events
115 // ============================
116 |state, event| {
117 // Apply common scene policies:
118 // - observe mouse position
119 // - tab traversal (if any focusables are registered this frame)
120 // - click-to-focus + mouse capture
121 let _effects = state.ui.apply_policies(&event);
122
123 // Quit (prefer modifier-aware events, with legacy fallback)
124 if let Event::KeyWithModifiers(k) = event {
125 if matches!(k.key, KeyKind::Char('q')) {
126 return false;
127 }
128 }
129 if matches!(event, Event::Character('q')) {
130 return false;
131 }
132
133 // Mouse wheel scrolling:
134 // Route ONLY to the scroll target under cursor (preferred) or the focused scroll target.
135 // This is now centralized in UiScene.
136 match event {
137 Event::MouseScroll { delta } => {
138 if let Some(RouteTarget::Id(_id)) = state.ui.route_wheel_event(&event) {
139 state.autohide.mark_activity();
140
141 // Convention: delta > 0 scrolls up, delta < 0 scrolls down.
142 // We invert to translate "scroll down" into positive offset movement.
143 let dy: i16 = -(delta as i16);
144 state
145 .scroll
146 .borrow_mut()
147 .scroll_by(ScrollOrientation::Vertical, dy);
148 }
149 }
150 Event::MouseScrollHorizontal { delta } => {
151 if let Some(RouteTarget::Id(_id)) = state.ui.route_wheel_event(&event) {
152 state.autohide.mark_activity();
153
154 let dx: i16 = -(delta as i16);
155 state
156 .scroll
157 .borrow_mut()
158 .scroll_by(ScrollOrientation::Horizontal, dx);
159 }
160 }
161 _ => {}
162 }
163
164 // Keyboard scrolling (prefer modifier-aware events, with legacy fallback).
165 match event {
166 Event::KeyWithModifiers(k) => match k.key {
167 KeyKind::Up => state
168 .vbar
169 .scroll_by_fraction(-1.0 / 5.0, ScrollUnit::Viewport),
170 KeyKind::Down => state
171 .vbar
172 .scroll_by_fraction(1.0 / 5.0, ScrollUnit::Viewport),
173 KeyKind::Left => state
174 .hbar
175 .scroll_by_fraction(-1.0 / 5.0, ScrollUnit::Viewport),
176 KeyKind::Right => state
177 .hbar
178 .scroll_by_fraction(1.0 / 5.0, ScrollUnit::Viewport),
179 _ => {}
180 },
181
182 // Legacy fallback.
183 Event::KeyUp => state
184 .vbar
185 .scroll_by_fraction(-1.0 / 5.0, ScrollUnit::Viewport),
186 Event::KeyDown => state
187 .vbar
188 .scroll_by_fraction(1.0 / 5.0, ScrollUnit::Viewport),
189 Event::KeyLeft => state
190 .hbar
191 .scroll_by_fraction(-1.0 / 5.0, ScrollUnit::Viewport),
192 Event::KeyRight => state
193 .hbar
194 .scroll_by_fraction(1.0 / 5.0, ScrollUnit::Viewport),
195
196 _ => {}
197 }
198
199 // Route pointer events into scrollbars (click/drag/arrows), using UiScene owner grouping.
200 //
201 // IMPORTANT:
202 // - Only route if the relevant areas were registered (i.e. visible).
203 // - If capture is active, UiScene ensures drag/release continue to target the captured id.
204 //
205 // We route to an OWNER so the app can forward to the owning ScrollBar instance.
206 if let Some(target) = state.ui.route_mouse_event_to_owner(&event) {
207 match target {
208 RouteTarget::Owner(OWNER_VSCROLL) => {
209 if let Some(entry) = state.ui.get(ID_VSCROLL_ROOT) {
210 let _ = state.vbar.handle_event(&event, entry.area);
211 }
212 }
213 RouteTarget::Owner(OWNER_HSCROLL) => {
214 if let Some(entry) = state.ui.get(ID_HSCROLL_ROOT) {
215 let _ = state.hbar.handle_event(&event, entry.area);
216 }
217 }
218 _ => {}
219 }
220 }
221
222 true
223 },
224 // ============================
225 // Draw: register areas + draw
226 // ============================
227 |state, window| {
228 state.ui.begin_frame();
229
230 let (term_w, term_h) = window.get_size();
231
232 // Layout constants
233 let margin: u16 = 1;
234 let header_h: u16 = 1;
235
236 // Scrollbar sizes (vbar on the right, hbar on bottom)
237 let vbar_w: u16 = 1;
238 let hbar_h: u16 = 1;
239
240 // ScrollBox styling affects its *outer* size vs the *inner* content viewport.
241 // Because the ScrollBox draws a border + padding, we must reserve extra space so
242 // the right/bottom borders don't get overwritten by scrollbars.
243 let box_border: u16 = 1; // single-line border thickness
244 let box_padding: u16 = 1; // we set ContainerPadding::uniform(1) below
245
246 // Compute a safe outer rect for the ScrollBox (what you pass to with_position_and_size)
247 // leaving room for:
248 // - header line
249 // - scrollbars (drawn OUTSIDE the ScrollBox frame)
250 //
251 // IMPORTANT:
252 // - The ScrollBox outer rect must be fully within the terminal bounds.
253 // - The scrollbars are then placed at `outer_x + outer_w` (right) and `outer_y + outer_h` (bottom),
254 // so we must reserve 1 extra column/row for them as well.
255 let outer_x = margin;
256 let outer_y = margin + header_h;
257
258 // Total insets that reduce the usable inner content area.
259 let inner_inset_x = box_border + box_padding;
260 let inner_inset_y = box_border + box_padding;
261
262 // Available space inside terminal margins for the ScrollBox outer rect.
263 // We reserve the scrollbar column/row OUTSIDE the ScrollBox.
264 let max_outer_w = term_w.saturating_sub(margin * 2).saturating_sub(vbar_w);
265 let max_outer_h = term_h
266 .saturating_sub(margin * 2)
267 .saturating_sub(header_h)
268 .saturating_sub(hbar_h);
269
270 // Clamp outer size to available space. Do NOT force a minimum that can exceed terminal bounds.
271 // If the terminal is too small, we'll end up with a very small box (and the scrollbars may be skipped).
272 let outer_w = max_outer_w;
273 let outer_h = max_outer_h;
274
275 // Inner viewport (content area) starts after border+padding.
276 // NOTE: these are currently unused in this example, but kept for clarity.
277 let viewport_x = outer_x + inner_inset_x;
278 let viewport_y = outer_y + inner_inset_y;
279 let viewport_w = outer_w.saturating_sub(inner_inset_x * 2);
280 let viewport_h = outer_h.saturating_sub(inner_inset_y * 2);
281
282 let _ = (viewport_x, viewport_y, viewport_w, viewport_h);
283
284 // Header / help line
285 window.write_str(
286 margin,
287 margin,
288 "Scroll demo: wheel/drag scrollbars • arrows/keys work • press 'q' to quit",
289 )?;
290
291 // Build content each frame (no cloning needed).
292 // Use a row gap to exercise gap-aware content measurement in ScrollBox.
293 let mut content = Container::vertical().with_row_gap(Gap::Pixels(1));
294
295 // Lots of lines (taller than viewport).
296 for i in 0..150u16 {
297 content = content.add_child(
298 Label::new(format!(
299 "Line {:03} | The quick brown fox jumps over the lazy dog. {}",
300 i,
301 if i % 5 == 0 { "[drag the thumb]" } else { "" }
302 ))
303 .with_text_color(Color::White),
304 );
305 }
306
307 // ScrollBox bound to shared state and sized to viewport.
308 let scrollbox = ScrollBox::both()
309 .with_state(Rc::clone(&state.scroll))
310 // IMPORTANT: pass the OUTER rect (includes border/padding), not the inner viewport.
311 .with_position_and_size(outer_x, outer_y, outer_w, outer_h)
312 .with_border()
313 .with_border_chars(BorderChars::single_line())
314 .with_title("Scrollable content")
315 .with_title_alignment(TitleAlignment::Left)
316 .with_padding(minui::widgets::ContainerPadding::uniform(1))
317 .with_row_gap(Gap::Pixels(1))
318 .add_child(content);
319
320 // Register scrollbox interaction regions:
321 // - outer frame as focusable (panel/split-friendly)
322 // - inner viewport as scrollable (wheel routing)
323 let outer_area = WidgetArea::new(outer_x, outer_y, outer_w, outer_h);
324 scrollbox.register_with_ids(
325 state.ui.cache_mut(),
326 outer_area,
327 ID_SCROLLBOX,
328 ID_SCROLLBOX_VIEWPORT,
329 );
330
331 // Draw scrollbox (updates ScrollState sizes + applies offsets via WindowView).
332 scrollbox.draw(window)?;
333
334 // Vertical scrollbar area (right side)
335 // Place it just outside the ScrollBox's outer border so it doesn't overwrite the frame.
336 // This must still be inside the terminal bounds (x < term_w).
337 let vbar_x = outer_x + outer_w;
338 let vbar_y = outer_y;
339 let vbar_h = outer_h;
340
341 // If there isn't room for the scrollbar column (e.g. tiny terminals), just skip drawing it.
342 if vbar_x >= term_w {
343 return Ok(());
344 }
345
346 // Resize + sync existing scrollbar instead of recreating it every frame.
347 // Recreating would reset drag state, making thumb dragging feel broken.
348 state.vbar.set_size(1, vbar_h);
349 state.vbar.set_show_arrows(true);
350 state.vbar.set_scroll_step(Some(1));
351 state.vbar.sync_from_state_and_resize_parts();
352
353 let v_area = WidgetArea::new(vbar_x, vbar_y, 1, vbar_h);
354
355 // Auto-hide policy: show if recently scrolled, mouse is near, or currently dragging.
356 // Reveal policy:
357 // - show when recently scrolled
358 // - show when mouse is near the scrollbar area
359 // - show when mouse is near the scrollbox RIGHT edge strip (even if the bar itself is hidden)
360 // - show while dragging
361 let mouse_pos = state.ui.last_mouse_pos();
362
363 // Only consider proximity to a thin strip along the scrollbox's right edge.
364 // This prevents the scrollbars from staying visible just because the cursor is somewhere
365 // inside/near the scrollbox.
366 let near_scrollbox_right_edge = if let Some((mx, my)) = mouse_pos {
367 // A 1-column strip at the right edge of the scrollbox.
368 let edge_x = outer_x + outer_w.saturating_sub(1);
369 let edge_strip = WidgetArea::new(edge_x, outer_y, 1, outer_h);
370 state.autohide.is_point_near_area(mx, my, edge_strip)
371 } else {
372 false
373 };
374
375 let show_vbar = state
376 .autohide
377 .should_show(v_area, mouse_pos, state.vbar.is_dragging())
378 || near_scrollbox_right_edge;
379
380 if show_vbar {
381 // Register sub-areas so routing can distinguish thumb vs arrows.
382 // Root is scrollable (hover target), thumb/track is draggable, arrows are focusable.
383 // Register areas into the underlying cache (UiScene wraps InteractionCache).
384 state.vbar.register_with_ids(
385 state.ui.cache_mut(),
386 v_area,
387 ID_VSCROLL_ROOT,
388 ID_VSCROLL_THUMB,
389 Some(ID_VSCROLL_ARROW_START),
390 Some(ID_VSCROLL_ARROW_END),
391 );
392
393 // Group all scrollbar sub-ids under a single owner for routing.
394 state.ui.set_owner_for_ids(
395 OWNER_VSCROLL,
396 &[
397 ID_VSCROLL_ROOT,
398 ID_VSCROLL_THUMB,
399 ID_VSCROLL_ARROW_START,
400 ID_VSCROLL_ARROW_END,
401 ],
402 );
403
404 {
405 let mut view = WindowView {
406 window,
407 x_offset: vbar_x,
408 y_offset: vbar_y,
409 scroll_x: 0,
410 scroll_y: 0,
411 width: 1,
412 height: vbar_h,
413 };
414 state.vbar.draw(&mut view)?;
415 }
416 }
417
418 // Horizontal scrollbar area (bottom)
419 // Place it just outside the ScrollBox's outer border so it doesn't overwrite the frame.
420 // This must still be inside the terminal bounds (y < term_h).
421 let hbar_x = outer_x;
422 let hbar_y = outer_y + outer_h;
423 let hbar_w = outer_w;
424
425 // If there isn't room for the scrollbar row (e.g. tiny terminals), just skip drawing it.
426 if hbar_y >= term_h {
427 return Ok(());
428 }
429
430 // Resize + sync existing scrollbar instead of recreating it every frame.
431 // Recreating would reset drag state, making thumb dragging feel broken.
432 state.hbar.set_size(hbar_w, 1);
433 state.hbar.set_show_arrows(true);
434 state.hbar.set_scroll_step(Some(2));
435 state.hbar.sync_from_state_and_resize_parts();
436
437 let h_area = WidgetArea::new(hbar_x, hbar_y, hbar_w, 1);
438
439 // Auto-hide policy: show if recently scrolled, mouse is near, or currently dragging.
440 // Reveal policy:
441 // - show when recently scrolled
442 // - show when mouse is near the scrollbar area
443 // - show when mouse is near the scrollbox BOTTOM edge strip (even if the bar itself is hidden)
444 // - show while dragging
445 let mouse_pos = state.ui.last_mouse_pos();
446
447 // Only consider proximity to a thin strip along the scrollbox's bottom edge.
448 // This prevents the scrollbars from staying visible just because the cursor is somewhere
449 // inside/near the scrollbox.
450 let near_scrollbox_bottom_edge = if let Some((mx, my)) = mouse_pos {
451 // A 1-row strip at the bottom edge of the scrollbox.
452 let edge_y = outer_y + outer_h.saturating_sub(1);
453 let edge_strip = WidgetArea::new(outer_x, edge_y, outer_w, 1);
454 state.autohide.is_point_near_area(mx, my, edge_strip)
455 } else {
456 false
457 };
458
459 let show_hbar = state
460 .autohide
461 .should_show(h_area, mouse_pos, state.hbar.is_dragging())
462 || near_scrollbox_bottom_edge;
463
464 if show_hbar {
465 // Register sub-areas so routing can distinguish thumb vs arrows.
466 // Root is scrollable (hover target), thumb/track is draggable, arrows are focusable.
467 // Register areas into the underlying cache (UiScene wraps InteractionCache).
468 state.hbar.register_with_ids(
469 state.ui.cache_mut(),
470 h_area,
471 ID_HSCROLL_ROOT,
472 ID_HSCROLL_THUMB,
473 Some(ID_HSCROLL_ARROW_START),
474 Some(ID_HSCROLL_ARROW_END),
475 );
476
477 // Group all scrollbar sub-ids under a single owner for routing.
478 state.ui.set_owner_for_ids(
479 OWNER_HSCROLL,
480 &[
481 ID_HSCROLL_ROOT,
482 ID_HSCROLL_THUMB,
483 ID_HSCROLL_ARROW_START,
484 ID_HSCROLL_ARROW_END,
485 ],
486 );
487
488 {
489 let mut view = WindowView {
490 window,
491 x_offset: hbar_x,
492 y_offset: hbar_y,
493 scroll_x: 0,
494 scroll_y: 0,
495 width: hbar_w,
496 height: 1,
497 };
498 state.hbar.draw(&mut view)?;
499 }
500 }
501
502 window.end_frame()?;
503 Ok(())
504 },
505 )?;
506
507 Ok(())
508}Sourcepub fn scroll_to(&mut self, orientation: ScrollOrientation, value: u16)
pub fn scroll_to(&mut self, orientation: ScrollOrientation, value: u16)
Scrolls to a specific value on the given axis and clamps.
Sourcepub fn scroll_to_start(&mut self, orientation: ScrollOrientation)
pub fn scroll_to_start(&mut self, orientation: ScrollOrientation)
Scrolls to the start (0) on the given axis.
Sourcepub fn scroll_to_end(&mut self, orientation: ScrollOrientation)
pub fn scroll_to_end(&mut self, orientation: ScrollOrientation)
Scrolls to the end (max) on the given axis.
Sourcepub fn max_offset(&self) -> ScrollOffset
pub fn max_offset(&self) -> ScrollOffset
Returns the maximum scroll offset (x,y).
Sourcepub fn max_offset_for(&self, orientation: ScrollOrientation) -> u16
pub fn max_offset_for(&self, orientation: ScrollOrientation) -> u16
Returns the maximum scroll offset for the given axis.
Sourcepub fn max_offset_x(&self) -> u16
pub fn max_offset_x(&self) -> u16
Returns the maximum horizontal scroll offset.
Sourcepub fn max_offset_y(&self) -> u16
pub fn max_offset_y(&self) -> u16
Returns the maximum vertical scroll offset.
Sourcepub fn can_scroll(&self, orientation: ScrollOrientation) -> bool
pub fn can_scroll(&self, orientation: ScrollOrientation) -> bool
Returns whether there is any scrollable range for the given axis.
Sourcepub fn can_scroll_vertical(&self) -> bool
pub fn can_scroll_vertical(&self) -> bool
Returns whether there is scrollable vertical range.
Sourcepub fn can_scroll_horizontal(&self) -> bool
pub fn can_scroll_horizontal(&self) -> bool
Returns whether there is scrollable horizontal range.
Sourcepub fn at_start(&self, orientation: ScrollOrientation) -> bool
pub fn at_start(&self, orientation: ScrollOrientation) -> bool
Returns whether the current offset is at the start (top/left) on the given axis.
Sourcepub fn at_end(&self, orientation: ScrollOrientation) -> bool
pub fn at_end(&self, orientation: ScrollOrientation) -> bool
Returns whether the current offset is at the end (bottom/right) on the given axis.
Sourcepub fn scroll_ratio(&self, orientation: ScrollOrientation) -> f32
pub fn scroll_ratio(&self, orientation: ScrollOrientation) -> f32
Returns a normalized scroll ratio in [0.0, 1.0] for the given axis.
If there is no scrollable range, this returns 0.0.
Trait Implementations§
Source§impl Clone for ScrollState
impl Clone for ScrollState
Source§fn clone(&self) -> ScrollState
fn clone(&self) -> ScrollState
1.0.0 (const: unstable) · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
source. Read moreimpl Copy for ScrollState
Source§impl Debug for ScrollState
impl Debug for ScrollState
Source§impl Default for ScrollState
impl Default for ScrollState
impl Eq for ScrollState
Source§impl PartialEq for ScrollState
impl PartialEq for ScrollState
Source§fn eq(&self, other: &ScrollState) -> bool
fn eq(&self, other: &ScrollState) -> bool
self and other values to be equal, and is used by ==.