use minui::MouseButton;
use minui::prelude::*;
use minui::ui::{AutoHide, InteractionCache};
use minui::widgets::controls::scrollbar::{ScrollBar, ScrollBarOptions, ScrollUnit};
use minui::widgets::scroll::{ScrollOrientation, ScrollSize, ScrollState};
use minui::widgets::{WidgetArea, WindowView};
use std::cell::RefCell;
use std::rc::Rc;
use std::time::Duration;
const ID_SCROLLBOX: usize = 1;
const ID_SCROLLBOX_VIEWPORT: usize = 4;
const ID_VSCROLL_ROOT: usize = 2;
const ID_VSCROLL_THUMB: usize = 5;
const ID_VSCROLL_ARROW_START: usize = 6;
const ID_VSCROLL_ARROW_END: usize = 7;
const ID_HSCROLL_ROOT: usize = 3;
const ID_HSCROLL_THUMB: usize = 8;
const ID_HSCROLL_ARROW_START: usize = 9;
const ID_HSCROLL_ARROW_END: usize = 10;
struct ScrollDemoState {
ui: InteractionCache,
scroll: Rc<RefCell<ScrollState>>,
vbar: ScrollBar,
hbar: ScrollBar,
active_drag_id: Option<usize>,
autohide: AutoHide,
}
fn main() -> minui::Result<()> {
let scroll = Rc::new(RefCell::new(ScrollState::new(
ScrollSize::new(1, 1),
ScrollSize::new(1, 1),
)));
let vbar = ScrollBar::new(
1,
1,
Rc::clone(&scroll),
ScrollBarOptions {
orientation: ScrollOrientation::Vertical,
show_arrows: true,
scroll_step: Some(1),
..ScrollBarOptions::default()
},
);
let hbar = ScrollBar::new(
1,
1,
Rc::clone(&scroll),
ScrollBarOptions {
orientation: ScrollOrientation::Horizontal,
show_arrows: true,
scroll_step: Some(2),
..ScrollBarOptions::default()
},
);
let initial = ScrollDemoState {
ui: InteractionCache::new(),
scroll,
vbar,
hbar,
active_drag_id: None,
autohide: AutoHide::new(Duration::from_millis(900), 2),
};
let mut app = App::new(initial)?;
app.run(
|state, event| {
state.ui.observe_event(&event);
if let Event::KeyWithModifiers(k) = event {
if matches!(k.key, KeyKind::Char('q')) {
return false;
}
}
if matches!(event, Event::Character('q')) {
return false;
}
match event {
Event::MouseClick {
x,
y,
button: MouseButton::Left,
} => {
state.active_drag_id = None;
if let Some(hit) = state.ui.hit_test(x, y) {
if hit.flags.draggable {
state.active_drag_id = Some(hit.id);
} else if hit.flags.focusable {
state.ui.focus(hit.id);
}
}
}
Event::MouseRelease {
button: MouseButton::Left,
..
} => {
}
_ => {}
}
match event {
Event::MouseScroll { delta } => {
let mut should_scroll = false;
if let Some((mx, my)) = state.ui.last_mouse_pos() {
if let Some(hit) = state.ui.hit_test(mx, my) {
should_scroll = hit.flags.scrollable;
}
} else if let Some(focused) = state.ui.focused() {
if let Some(entry) = state.ui.get(focused) {
should_scroll = entry.flags.scrollable;
}
}
if should_scroll {
state.autohide.mark_activity();
let dy: i16 = -(delta as i16);
state
.scroll
.borrow_mut()
.scroll_by(ScrollOrientation::Vertical, dy);
}
}
Event::MouseScrollHorizontal { delta } => {
let mut should_scroll = false;
if let Some((mx, my)) = state.ui.last_mouse_pos() {
if let Some(hit) = state.ui.hit_test(mx, my) {
should_scroll = hit.flags.scrollable;
}
} else if let Some(focused) = state.ui.focused() {
if let Some(entry) = state.ui.get(focused) {
should_scroll = entry.flags.scrollable;
}
}
if should_scroll {
state.autohide.mark_activity();
let dx: i16 = -(delta as i16);
state
.scroll
.borrow_mut()
.scroll_by(ScrollOrientation::Horizontal, dx);
}
}
_ => {}
}
match event {
Event::KeyWithModifiers(k) => match k.key {
KeyKind::Up => state
.vbar
.scroll_by_fraction(-1.0 / 5.0, ScrollUnit::Viewport),
KeyKind::Down => state
.vbar
.scroll_by_fraction(1.0 / 5.0, ScrollUnit::Viewport),
KeyKind::Left => state
.hbar
.scroll_by_fraction(-1.0 / 5.0, ScrollUnit::Viewport),
KeyKind::Right => state
.hbar
.scroll_by_fraction(1.0 / 5.0, ScrollUnit::Viewport),
_ => {}
},
Event::KeyUp => state
.vbar
.scroll_by_fraction(-1.0 / 5.0, ScrollUnit::Viewport),
Event::KeyDown => state
.vbar
.scroll_by_fraction(1.0 / 5.0, ScrollUnit::Viewport),
Event::KeyLeft => state
.hbar
.scroll_by_fraction(-1.0 / 5.0, ScrollUnit::Viewport),
Event::KeyRight => state
.hbar
.scroll_by_fraction(1.0 / 5.0, ScrollUnit::Viewport),
_ => {}
}
let captured = state.active_drag_id;
let owner_for = |id: usize| -> Option<&'static str> {
match id {
ID_VSCROLL_ROOT
| ID_VSCROLL_THUMB
| ID_VSCROLL_ARROW_START
| ID_VSCROLL_ARROW_END => Some("v"),
ID_HSCROLL_ROOT
| ID_HSCROLL_THUMB
| ID_HSCROLL_ARROW_START
| ID_HSCROLL_ARROW_END => Some("h"),
_ => None,
}
};
let mut route_vbar = false;
let mut route_hbar = false;
if let Some(id) = captured {
match owner_for(id) {
Some("v") => route_vbar = true,
Some("h") => route_hbar = true,
_ => {}
}
} else if let Some((mx, my)) = state.ui.last_mouse_pos() {
if let Some(hit_id) = state.ui.hit_test_id(mx, my) {
match owner_for(hit_id) {
Some("v") => route_vbar = true,
Some("h") => route_hbar = true,
_ => {}
}
}
}
if route_vbar {
if let Some(entry) = state.ui.get(ID_VSCROLL_ROOT) {
let _ = state.vbar.handle_event(&event, entry.area);
}
}
if route_hbar {
if let Some(entry) = state.ui.get(ID_HSCROLL_ROOT) {
let _ = state.hbar.handle_event(&event, entry.area);
}
}
if matches!(
event,
Event::MouseRelease {
button: MouseButton::Left,
..
}
) {
state.active_drag_id = None;
}
true
},
|state, window| {
state.ui.begin_frame();
let (term_w, term_h) = window.get_size();
let margin: u16 = 1;
let header_h: u16 = 1;
let vbar_w: u16 = 1;
let hbar_h: u16 = 1;
let box_border: u16 = 1; let box_padding: u16 = 1;
let outer_x = margin;
let outer_y = margin + header_h;
let inner_inset_x = box_border + box_padding;
let inner_inset_y = box_border + box_padding;
let max_outer_w = term_w.saturating_sub(margin * 2).saturating_sub(vbar_w);
let max_outer_h = term_h
.saturating_sub(margin * 2)
.saturating_sub(header_h)
.saturating_sub(hbar_h);
let outer_w = max_outer_w;
let outer_h = max_outer_h;
let viewport_x = outer_x + inner_inset_x;
let viewport_y = outer_y + inner_inset_y;
let viewport_w = outer_w.saturating_sub(inner_inset_x * 2);
let viewport_h = outer_h.saturating_sub(inner_inset_y * 2);
let _ = (viewport_x, viewport_y, viewport_w, viewport_h);
window.write_str(
margin,
margin,
"Scroll demo: wheel/drag scrollbars • arrows/keys work • press 'q' to quit",
)?;
let mut content = Container::vertical().with_row_gap(Gap::Pixels(1));
for i in 0..150u16 {
content = content.add_child(
Label::new(format!(
"Line {:03} | The quick brown fox jumps over the lazy dog. {}",
i,
if i % 5 == 0 { "[drag the thumb]" } else { "" }
))
.with_text_color(Color::White),
);
}
let scrollbox = ScrollBox::both()
.with_state(Rc::clone(&state.scroll))
.with_position_and_size(outer_x, outer_y, outer_w, outer_h)
.with_border()
.with_border_chars(BorderChars::single_line())
.with_title("Scrollable content")
.with_title_alignment(TitleAlignment::Left)
.with_padding(minui::widgets::ContainerPadding::uniform(1))
.with_row_gap(Gap::Pixels(1))
.add_child(content);
let outer_area = WidgetArea::new(outer_x, outer_y, outer_w, outer_h);
scrollbox.register_with_ids(
&mut state.ui,
outer_area,
ID_SCROLLBOX,
ID_SCROLLBOX_VIEWPORT,
);
scrollbox.draw(window)?;
let vbar_x = outer_x + outer_w;
let vbar_y = outer_y;
let vbar_h = outer_h;
if vbar_x >= term_w {
return Ok(());
}
state.vbar.set_size(1, vbar_h);
state.vbar.set_show_arrows(true);
state.vbar.set_scroll_step(Some(1));
state.vbar.sync_from_state_and_resize_parts();
let v_area = WidgetArea::new(vbar_x, vbar_y, 1, vbar_h);
let mouse_pos = state.ui.last_mouse_pos();
let near_scrollbox_right_edge = if let Some((mx, my)) = mouse_pos {
let edge_x = outer_x + outer_w.saturating_sub(1);
let edge_strip = WidgetArea::new(edge_x, outer_y, 1, outer_h);
state.autohide.is_point_near_area(mx, my, edge_strip)
} else {
false
};
let show_vbar = state
.autohide
.should_show(v_area, mouse_pos, state.vbar.is_dragging())
|| near_scrollbox_right_edge;
if show_vbar {
state.vbar.register_with_ids(
&mut state.ui,
v_area,
ID_VSCROLL_ROOT,
ID_VSCROLL_THUMB,
Some(ID_VSCROLL_ARROW_START),
Some(ID_VSCROLL_ARROW_END),
);
{
let mut view = WindowView {
window,
x_offset: vbar_x,
y_offset: vbar_y,
scroll_x: 0,
scroll_y: 0,
width: 1,
height: vbar_h,
};
state.vbar.draw(&mut view)?;
}
}
let hbar_x = outer_x;
let hbar_y = outer_y + outer_h;
let hbar_w = outer_w;
if hbar_y >= term_h {
return Ok(());
}
state.hbar.set_size(hbar_w, 1);
state.hbar.set_show_arrows(true);
state.hbar.set_scroll_step(Some(2));
state.hbar.sync_from_state_and_resize_parts();
let h_area = WidgetArea::new(hbar_x, hbar_y, hbar_w, 1);
let mouse_pos = state.ui.last_mouse_pos();
let near_scrollbox_bottom_edge = if let Some((mx, my)) = mouse_pos {
let edge_y = outer_y + outer_h.saturating_sub(1);
let edge_strip = WidgetArea::new(outer_x, edge_y, outer_w, 1);
state.autohide.is_point_near_area(mx, my, edge_strip)
} else {
false
};
let show_hbar = state
.autohide
.should_show(h_area, mouse_pos, state.hbar.is_dragging())
|| near_scrollbox_bottom_edge;
if show_hbar {
state.hbar.register_with_ids(
&mut state.ui,
h_area,
ID_HSCROLL_ROOT,
ID_HSCROLL_THUMB,
Some(ID_HSCROLL_ARROW_START),
Some(ID_HSCROLL_ARROW_END),
);
{
let mut view = WindowView {
window,
x_offset: hbar_x,
y_offset: hbar_y,
scroll_x: 0,
scroll_y: 0,
width: hbar_w,
height: 1,
};
state.hbar.draw(&mut view)?;
}
}
window.end_frame()?;
Ok(())
},
)?;
Ok(())
}