use std::cell::Cell;
use std::rc::Rc;
use crate::base::Rect;
use crate::layout::{Dimension, Inset, Position, Style as LayoutStyle};
use crate::reactive::{create_root, Scope, Signal};
use crate::theme::TokenSet;
use crate::ui::{
dyn_view, Element, EventCtx, Key, MouseButton, MouseKind, Phase, StyledCanvas, UiEvent, View,
};
use super::list::draw_scrollbar;
thread_local! {
static FOLLOW_FROZEN: Cell<Option<Signal<bool>>> = const { Cell::new(None) };
}
fn follow_frozen_signal() -> Signal<bool> {
FOLLOW_FROZEN.with(|slot| {
if let Some(sig) = slot.get() {
return sig;
}
let (root, sig) = create_root(|cx| cx.signal(false));
std::mem::forget(root);
slot.set(Some(sig));
sig
})
}
pub fn freeze_follow_tail(on: bool) {
follow_frozen_signal().set_if_changed(on);
}
pub fn follow_tail_frozen() -> bool {
follow_frozen_signal().get()
}
pub struct Scroll {
content: View,
content_size: Option<(i32, i32)>,
vertical: bool,
horizontal: bool,
offset_y: Option<Signal<i32>>,
offset_x: Option<Signal<i32>>,
follow: Option<Signal<bool>>,
extent_out: Option<Signal<(i32, i32)>>,
viewport_out: Option<Signal<(i32, i32)>>,
scrollbar_auto_hide: bool,
layout: Option<LayoutStyle>,
}
impl Scroll {
pub fn new(content: View) -> Scroll {
Scroll {
content,
content_size: None,
vertical: true,
horizontal: false,
offset_y: None,
offset_x: None,
follow: None,
extent_out: None,
viewport_out: None,
scrollbar_auto_hide: false,
layout: None,
}
}
pub fn content_size(mut self, w: i32, h: i32) -> Scroll {
self.content_size = Some((w, h));
self
}
pub fn axes(mut self, horizontal: bool, vertical: bool) -> Scroll {
self.horizontal = horizontal;
self.vertical = vertical;
self
}
pub fn offset_y(mut self, sig: Signal<i32>) -> Scroll {
self.offset_y = Some(sig);
self
}
pub fn offset_x(mut self, sig: Signal<i32>) -> Scroll {
self.offset_x = Some(sig);
self
}
pub fn follow_tail(mut self, sig: Signal<bool>) -> Scroll {
self.follow = Some(sig);
self
}
pub fn extent_signal(mut self, sig: Signal<(i32, i32)>) -> Scroll {
self.extent_out = Some(sig);
self
}
pub fn viewport_size_signal(mut self, sig: Signal<(i32, i32)>) -> Scroll {
self.viewport_out = Some(sig);
self
}
pub fn scrollbar_auto_hide(mut self, auto_hide: bool) -> Scroll {
self.scrollbar_auto_hide = auto_hide;
self
}
pub fn layout(mut self, layout: LayoutStyle) -> Scroll {
self.layout = Some(layout);
self
}
pub fn view(self, cx: Scope) -> crate::ui::View {
let t = crate::widgets::theme_tokens(cx);
self.element(cx, &t).build()
}
pub fn element(self, cx: Scope, t: &TokenSet) -> Element {
let track = t.border;
let thumb = t.text_muted;
let ground = t.surface;
let hint = self.content_size;
let viewport_out = self.viewport_out;
let extent: Signal<(i32, i32)> = match self.extent_out {
Some(sig) => {
if let Some(h) = hint {
sig.set_if_changed(h);
}
sig
}
None => cx.signal(hint.unwrap_or((0, 0))),
};
let view_box: Signal<(i32, i32)> = cx.signal((0, 0));
let ox = self.offset_x.unwrap_or_else(|| cx.signal(0i32));
let oy = self.offset_y.unwrap_or_else(|| cx.signal(0i32));
let follow = self.follow;
let (vertical, horizontal) = (self.vertical, self.horizontal);
let layout = self.layout.unwrap_or_else(|| {
LayoutStyle::default().grow(1.0).basis(Dimension::Cells(0))
});
let wrapper_style = move || {
let (w, h) = match hint {
Some((w, h)) => (Dimension::Cells(w.max(1)), Dimension::Cells(h.max(1))),
None => (
if horizontal {
Dimension::Auto
} else {
Dimension::Percent(1.0)
},
if vertical {
Dimension::Auto
} else {
Dimension::Percent(1.0)
},
),
};
let tail_pinned = vertical
&& follow.map(|f| f.get()).unwrap_or(false)
&& oy.get() > 0
&& !follow_frozen_signal().get();
let inset = if tail_pinned {
Inset {
left: Some(-ox.get()),
top: None,
right: None,
bottom: Some(0),
}
} else {
Inset {
left: Some(-ox.get()),
top: Some(-oy.get()),
right: None,
bottom: None,
}
};
LayoutStyle {
position: Position::Absolute,
inset,
width: w,
height: h,
..LayoutStyle::default()
}
};
let mut wrapper = Element::new().style_signal(wrapper_style);
if hint.is_none() {
wrapper = wrapper.draw(size_probe(extent)).probe_when_culled();
}
let wrapper = wrapper.child(self.content);
let viewport = Element::new()
.style(
LayoutStyle::default()
.grow(1.0)
.scroll(),
)
.role(crate::ui::Role::ScrollArea)
.child(wrapper.build())
.draw(size_probe(view_box));
if let Some(out) = viewport_out {
cx.effect(move || {
out.set(view_box.get());
});
}
if let Some(f) = follow {
cx.effect(move || {
if !f.get() {
return; }
if follow_frozen_signal().get() {
return;
}
let content_h = extent.get().1;
let view_h = view_box.get().1;
if view_h > 0 {
let pinned = (content_h - view_h).max(0);
if oy.try_get_untracked() != Some(pinned) {
oy.set(pinned);
}
}
});
}
cx.effect(move || {
let (content_w, content_h) = extent.get();
let (view_w, view_h) = view_box.get();
if hint.is_none() && content_w == 0 && content_h == 0 {
return;
}
if vertical && view_h > 0 {
let max_off = (content_h - view_h).max(0);
if oy.try_get_untracked().is_some_and(|o| o > max_off) {
oy.set(max_off);
}
}
if horizontal && view_w > 0 {
let max_off = (content_w - view_w).max(0);
if ox.try_get_untracked().is_some_and(|o| o > max_off) {
ox.set(max_off);
}
}
});
let derive_follow = move |view_h: i32| {
if let Some(f) = follow {
let max_off = (extent.get_untracked().1 - view_h).max(0);
f.set_if_changed(oy.get_untracked() >= max_off);
}
};
let scroll_by = move |dx: i32, dy: i32, view: Rect| -> bool {
let (content_w, content_h) = extent.get_untracked();
let mut moved = false;
if horizontal && dx != 0 {
let max_off = (content_w - view.w).max(0);
moved |= ox.set_if_changed((ox.get_untracked() + dx).clamp(0, max_off));
}
if vertical && dy != 0 {
let max_off = (content_h - view.h).max(0);
let base = if follow.is_some_and(|f| f.get_untracked())
&& !follow_frozen_signal().get_untracked()
{
max_off
} else {
oy.get_untracked()
};
moved |= oy.set_if_changed((base + dy).clamp(0, max_off));
derive_follow(view.h);
}
moved
};
let handler = move |ctx: &mut EventCtx, ev: &UiEvent| {
let rect = ctx.current_rect();
match ev {
UiEvent::Mouse(m) => {
let (dx, dy) = match m.kind {
MouseKind::ScrollUp => (0, -3),
MouseKind::ScrollDown => (0, 3),
MouseKind::ScrollLeft => (-3, 0),
MouseKind::ScrollRight => (3, 0),
_ => (0, 0),
};
if dx != 0 || dy != 0 {
let unmeasured = hint.is_none() && extent.get_untracked() == (0, 0);
if scroll_by(dx, dy, rect) || unmeasured {
ctx.stop_propagation();
}
}
}
UiEvent::Key(k) => {
let (_content_w, content_h) = extent.get_untracked();
let (dx, dy) = match k.key {
Key::Up => (0, -1),
Key::Down => (0, 1),
Key::Left => (-1, 0),
Key::Right => (1, 0),
Key::PageUp => (0, -rect.h.max(1)),
Key::PageDown => (0, rect.h.max(1)),
Key::Home => (0, -content_h),
Key::End => (0, content_h),
_ => return,
};
scroll_by(dx, dy, rect);
ctx.stop_propagation();
}
_ => {}
}
};
let auto_hide = self.scrollbar_auto_hide;
let bar = dyn_view(
LayoutStyle::default()
.width(Dimension::Cells(1))
.height(Dimension::Percent(1.0)),
move || {
let offset = oy.get();
let content_h = extent.get().1; Element::new()
.style(
LayoutStyle::default()
.width(Dimension::Cells(1))
.height(Dimension::Percent(1.0)),
)
.on(Phase::Bubble, move |ctx: &mut EventCtx, ev: &UiEvent| {
if let UiEvent::Mouse(m) = ev {
let grabbed = matches!(
m.kind,
MouseKind::Down(MouseButton::Left)
| MouseKind::Drag(MouseButton::Left)
);
if grabbed {
let bar = ctx.current_rect();
if auto_hide && content_h <= bar.h {
return; }
let usable = (bar.h - 1).max(1);
let frac = (m.pos.y - bar.y).clamp(0, usable);
let max_off = (content_h - bar.h).max(0);
oy.set((frac * max_off) / usable);
derive_follow(bar.h);
ctx.stop_propagation();
}
}
})
.draw(move |canvas, rect| {
if rect.is_empty() {
return;
}
if auto_hide && content_h <= rect.h {
let blank = crate::render::Style::new().fg(ground).bg(ground);
canvas.fill_styled(rect, ' ', &blank);
return;
}
draw_scrollbar(canvas, rect, offset, content_h, track, thumb, ground);
})
.build()
},
);
let mut root = Element::new()
.style(layout)
.focusable()
.on(Phase::Bubble, handler)
.child(viewport.build());
if vertical {
root = root.child(bar);
}
root
}
}
pub(crate) fn size_probe(sig: Signal<(i32, i32)>) -> impl FnMut(&mut dyn StyledCanvas, Rect) {
let seen: Rc<Cell<(i32, i32)>> = Rc::new(Cell::new((-1, -1)));
let pending: Rc<Cell<bool>> = Rc::new(Cell::new(false));
move |_canvas, rect| {
let size = (rect.w, rect.h);
if seen.get() == size {
return;
}
seen.set(size);
if pending.replace(true) {
return; }
let (seen, pending) = (seen.clone(), pending.clone());
crate::reactive::after(std::time::Duration::ZERO, move || {
pending.set(false);
if sig.try_get_untracked().is_some() {
sig.set_if_changed(seen.get());
}
});
}
}
#[cfg(test)]
#[path = "scroll_tests.rs"]
mod tests;
#[cfg(test)]
#[path = "scroll_extent_tests.rs"]
mod extent_tests;