1use densa_core::{Point, Rect, Size};
2use densa_draw::Canvas;
3use densa_ui::{
4 AccessAction, AccessNode, Event, EventCx, Handled, Length, Overflow, PaintCx, PointerButton,
5 PointerKind, Role, ScrollDelta, Style, Widget,
6};
7
8const LINE: f32 = 48.0;
10const BAR: f32 = 6.0;
12const MIN_THUMB: f32 = 24.0;
14
15#[derive(Debug, Default)]
21pub struct ScrollView {
22 drag: Option<(Axis, f32)>,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27enum Axis {
28 Horizontal,
29 Vertical,
30}
31
32impl ScrollView {
33 pub fn new() -> Self {
35 Self::default()
36 }
37
38 pub fn style() -> Style {
41 Style::column()
42 .grow(1.0)
43 .min_size(Length::Px(0.0), Length::Px(0.0))
44 .overflow(Overflow::Scroll)
45 }
46
47 fn scroll_by(cx: &mut EventCx<'_>, dx: f32, dy: f32) -> Handled {
48 let at = cx.scroll();
49 let moved = cx.scroll_to(Point::new(at.x + dx, at.y + dy));
50 (moved != at).into()
51 }
52}
53
54fn thumb(axis: Axis, rect: Rect, content: Size, scroll: Point, scale: f32) -> Option<Rect> {
56 let (shown, total, offset) = match axis {
57 Axis::Vertical => (rect.height(), content.height, scroll.y),
58 Axis::Horizontal => (rect.width(), content.width, scroll.x),
59 };
60 if total <= shown || shown <= 0.0 {
61 return None;
62 }
63 let length = (shown * shown / total).max(MIN_THUMB * scale).min(shown);
64 let start = offset / (total - shown) * (shown - length);
65 let bar = BAR * scale;
66 Some(match axis {
67 Axis::Vertical => Rect::new(rect.right() - bar, rect.top() + start, bar, length),
68 Axis::Horizontal => Rect::new(rect.left() + start, rect.bottom() - bar, length, bar),
69 })
70}
71
72impl Widget for ScrollView {
73 fn accessibility(&self, cx: &PaintCx<'_>, node: &mut AccessNode) {
74 node.set_role(Role::ScrollView);
75 let (at, content, view) = (cx.scroll(), cx.content_size(), cx.rect());
76 let (room_x, room_y) = (content.width - view.width(), content.height - view.height());
77 for (action, can) in [
78 (AccessAction::ScrollUp, at.y > 0.0),
79 (AccessAction::ScrollDown, at.y < room_y),
80 (AccessAction::ScrollLeft, at.x > 0.0),
81 (AccessAction::ScrollRight, at.x < room_x),
82 ] {
83 if can {
84 node.add_action(action);
85 }
86 }
87 }
88
89 fn paint_over(&mut self, cx: &PaintCx<'_>, canvas: &mut Canvas<'_>) {
90 let theme = cx.theme();
91 for axis in [Axis::Vertical, Axis::Horizontal] {
92 let at = thumb(axis, cx.rect(), cx.content_size(), cx.scroll(), cx.scale());
93 if let Some(at) = at {
94 let alpha = if self.drag.is_some_and(|(a, _)| a == axis) {
95 0.65
96 } else {
97 0.45
98 };
99 let color = theme.text_dim.with_alpha(alpha);
100 canvas.fill_rect(at, color);
101 }
102 }
103 }
104
105 fn event(&mut self, cx: &mut EventCx<'_>, event: &Event<'_>) -> Handled {
106 let Event::Pointer(pointer) = event else {
107 return Handled::No;
108 };
109 let at = pointer.position;
110 match pointer.kind {
111 PointerKind::Scroll(ScrollDelta::Lines(dx, dy)) => {
112 let line = cx.px(LINE);
113 Self::scroll_by(cx, dx * line, dy * line)
114 }
115 PointerKind::Scroll(ScrollDelta::Pixels(dx, dy)) => Self::scroll_by(cx, dx, dy),
116 PointerKind::Down(PointerButton::Primary) => {
117 let (rect, content, scroll) = (cx.rect(), cx.content_size(), cx.scroll());
118 for axis in [Axis::Vertical, Axis::Horizontal] {
119 if let Some(bar) = thumb(axis, rect, content, scroll, cx.scale()) {
120 if bar.contains(at) {
121 let grab = match axis {
122 Axis::Vertical => at.y - bar.top(),
123 Axis::Horizontal => at.x - bar.left(),
124 };
125 self.drag = Some((axis, grab));
126 cx.request_paint();
127 return Handled::Yes;
128 }
129 }
130 }
131 Handled::No
132 }
133 PointerKind::Move => {
134 let Some((axis, grab)) = self.drag else {
135 return Handled::No;
136 };
137 let (rect, content, scroll) = (cx.rect(), cx.content_size(), cx.scroll());
138 let Some(bar) = thumb(axis, rect, content, scroll, cx.scale()) else {
139 return Handled::Yes;
140 };
141 let (track, length, total, shown, pointer, start) = match axis {
143 Axis::Vertical => (
144 rect.height(),
145 bar.height(),
146 content.height,
147 rect.height(),
148 at.y,
149 rect.top(),
150 ),
151 Axis::Horizontal => (
152 rect.width(),
153 bar.width(),
154 content.width,
155 rect.width(),
156 at.x,
157 rect.left(),
158 ),
159 };
160 let travel = (track - length).max(1.0);
161 let offset = (pointer - grab - start) / travel * (total - shown);
162 let target = match axis {
163 Axis::Vertical => Point::new(scroll.x, offset),
164 Axis::Horizontal => Point::new(offset, scroll.y),
165 };
166 cx.scroll_to(target);
167 Handled::Yes
168 }
169 PointerKind::Up(PointerButton::Primary) | PointerKind::Cancel
170 if self.drag.take().is_some() =>
171 {
172 cx.request_paint();
173 Handled::Yes
174 }
175 _ => Handled::No,
176 }
177 }
178}