Skip to main content

AutoHide

Struct AutoHide 

Source
pub struct AutoHide { /* private fields */ }
Expand description

A tiny helper for “auto-hide” UI elements (e.g. scrollbars).

This is intended to be used by app code in Phase 1:

  • Update it in your update loop when scroll events occur.
  • Feed it the latest mouse position (if you track mouse movement).
  • Decide whether to draw/register the scrollbars this frame.

This does not perform any drawing or event routing by itself.

Implementations§

Source§

impl AutoHide

Source

pub fn new(reveal_for: Duration, proximity_margin: u16) -> Self

Create a new auto-hide helper.

  • reveal_for: how long to keep the element visible after scrolling/activity.
  • proximity_margin: how many cells around the element count as “near”.
Examples found in repository?
examples/scroll_demo.rs (line 107)
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}
Source

pub fn mark_activity(&mut self)

Record “activity” (e.g. mouse wheel scroll) to keep elements visible for a bit.

Examples found in repository?
examples/scroll_demo.rs (line 139)
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}
Source

pub fn mark_activity_from_event(&mut self, event: &Event) -> bool

Convenience: mark activity if an event is a scroll event.

Returns true if it matched a scroll event.

Source

pub fn is_recently_active(&self) -> bool

Returns whether we are still within the “recent activity” window.

Source

pub fn set_reveal_for(&mut self, reveal_for: Duration)

Set the reveal duration.

Source

pub fn set_proximity_margin(&mut self, proximity_margin: u16)

Set the proximity margin in cells.

Source

pub fn is_point_near_area(&self, x: u16, y: u16, area: WidgetArea) -> bool

Returns whether a point is “near” an area, using proximity_margin.

This is useful for “show when cursor approaches the scrollbar”.

Examples found in repository?
examples/scroll_demo.rs (line 370)
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}
Source

pub fn should_show( &self, area: WidgetArea, mouse_pos: Option<(u16, u16)>, is_dragging: bool, ) -> bool

Decide whether an element should be visible based on:

  • recent scroll activity
  • mouse proximity to its area
  • optional dragging state (callers can pass true while dragging)
Examples found in repository?
examples/scroll_demo.rs (line 377)
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}

Trait Implementations§

Source§

impl Clone for AutoHide

Source§

fn clone(&self) -> AutoHide

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for AutoHide

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Default for AutoHide

Source§

fn default() -> Self

Returns the “default value” for a type. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.