1use glam::Vec2;
2use ori_graphics::{Quad, Rect};
3use ori_macro::Build;
4
5use crate::{
6 Axis, BoxConstraints, Children, Context, DrawContext, Event, EventContext, FlexLayout,
7 LayoutContext, Parent, PointerEvent, Style, View,
8};
9
10#[derive(Default, Build)]
11pub struct Scroll {
12 content: Children,
13}
14
15impl Scroll {
16 fn scrollbar_rect(&self, state: &ScrollState, cx: &mut impl Context) -> Rect {
17 let axis = cx.style::<Axis>("direction");
18 let max_width = axis.major(cx.rect().size());
19
20 let width = cx.style_range("scrollbar-width", 0.0..max_width);
21 let padding = cx.style_range("scrollbar-padding", 0.0..max_width - width);
22
23 let max_height = axis.minor(cx.rect().size()) - padding * 2.0;
24 let height = cx.style_range("scrollbar-height", 0.0..max_height);
25
26 let scrollbar_size = axis.pack(height, width);
27 let range = axis.major(cx.rect().size()) - height - padding * 2.0;
28
29 Rect::min_size(
30 axis.pack(
31 axis.major(cx.rect().min) + range * axis.major(state.scroll) + padding,
32 axis.minor(cx.rect().max) - axis.minor(scrollbar_size) - padding,
33 ),
34 scrollbar_size,
35 )
36 }
37
38 fn scrollbar_track_rect(&self, cx: &mut impl Context) -> Rect {
39 let axis = cx.style::<Axis>("direction");
40
41 let max_width = axis.major(cx.rect().size());
42 let width = cx.style_range("scrollbar-width", 0.0..max_width);
43
44 let padding = cx.style_range("scrollbar-padding", 0.0..max_width - width);
45
46 Rect::min_size(
47 axis.pack(
48 axis.major(cx.rect().min) + padding,
49 axis.minor(cx.rect().max) - width - padding,
50 ),
51 axis.pack(axis.major(cx.rect().size()) - padding * 2.0, width),
52 )
53 }
54
55 fn overflow(&self, cx: &mut impl Context) -> Vec2 {
56 self.content.size() - cx.size()
57 }
58
59 fn should_show_scrollbar(&self, cx: &mut impl Context) -> bool {
60 self.overflow(cx).max_element() > 1.0
61 }
62
63 fn handle_pointer_event(
64 &self,
65 state: &mut ScrollState,
66 cx: &mut EventContext,
67 event: &PointerEvent,
68 ) -> bool {
69 let mut handled = false;
70
71 let axis = cx.style::<Axis>("direction");
72
73 if event.scroll_delta != Vec2::ZERO && cx.hovered() {
74 let overflow = self.overflow(cx);
75 state.scroll -= axis.pack(event.scroll_delta.y, 0.0) / overflow * 10.0;
76 state.scroll = state.scroll.clamp(Vec2::ZERO, Vec2::ONE);
77
78 cx.request_redraw();
79
80 handled = true;
81 }
82
83 if !self.should_show_scrollbar(cx) {
84 return handled;
85 }
86
87 let scrollbar_rect = self.scrollbar_track_rect(cx);
88
89 if scrollbar_rect.contains(event.position) && event.is_press() {
90 cx.activate();
91 }
92
93 if event.is_release() {
94 cx.deactivate();
95 }
96
97 if cx.active() {
98 let start = axis.major(scrollbar_rect.min);
99 let end = axis.major(scrollbar_rect.max);
100 let range = end - start;
101
102 let scroll = (axis.major(event.position) - start) / range;
103 let minor = axis.minor(event.position);
104 state.scroll = axis.pack(scroll.clamp(0.0, 1.0), minor);
105
106 cx.request_redraw();
107
108 handled = true;
109 }
110
111 handled
112 }
113}
114
115impl Parent for Scroll {
116 fn add_child(&mut self, child: impl View) {
117 self.content.add_child(child);
118 }
119}
120
121#[derive(Default)]
122pub struct ScrollState {
123 scroll: Vec2,
124}
125
126impl View for Scroll {
127 type State = ScrollState;
128
129 fn build(&self) -> Self::State {
130 ScrollState::default()
131 }
132
133 fn style(&self) -> Style {
134 Style::new("scroll")
135 }
136
137 fn event(&self, state: &mut Self::State, cx: &mut EventContext, event: &Event) {
138 if let Some(pointer_event) = event.get::<PointerEvent>() {
139 if self.handle_pointer_event(state, cx, pointer_event) {
140 event.handle();
141 }
142 }
143
144 self.content.event(cx, event);
145 }
146
147 fn layout(&self, _state: &mut Self::State, cx: &mut LayoutContext, bc: BoxConstraints) -> Vec2 {
148 let axis = cx.style::<Axis>("direction");
149
150 let flex = FlexLayout {
151 axis,
152 justify_content: cx.style("justify-content"),
153 align_items: cx.style("align-items"),
154 gap: cx.style_range("gap", 0.0..bc.max.min_element() / 2.0),
155 ..Default::default()
156 };
157
158 let content_bc = match axis {
159 Axis::Horizontal => bc.loose_x(),
160 Axis::Vertical => bc.loose_y(),
161 };
162 let size = self.content.flex_layout(cx, content_bc, flex);
163
164 cx.style_constraints(bc).constrain(size)
165 }
166
167 fn draw(&self, state: &mut Self::State, cx: &mut DrawContext) {
168 cx.draw_quad();
169
170 let overflow = self.overflow(cx);
171 self.content.set_offset(-state.scroll * overflow);
172
173 let container_rect = cx.rect();
174 cx.layer().clip(container_rect).draw(|cx| {
175 self.content.draw(cx);
176 });
177
178 if !self.should_show_scrollbar(cx) {
179 return;
180 }
181
182 let rect = self.scrollbar_track_rect(cx);
184
185 let max_radius = rect.size().min_element() / 2.0;
186 let radius = cx.style_range("scrollbar-border-radius", 0.0..max_radius);
187
188 let quad = Quad {
189 rect,
190 background: cx.style("scrollbar-track-color"),
191 border_radius: [radius; 4],
192 border_width: cx.style_range("scrollbar-track-border-width", 0.0..max_radius),
193 border_color: cx.style("scrollbar-track-border-color"),
194 };
195
196 cx.layer().depth(100.0).draw(|cx| {
197 cx.draw(quad);
198 });
199
200 let rect = self.scrollbar_rect(state, cx);
202
203 let quad = Quad {
204 rect,
205 background: cx.style("scrollbar-color"),
206 border_radius: [radius; 4],
207 border_width: cx.style_range("scrollbar-border-width", 0.0..max_radius),
208 border_color: cx.style("scrollbar-border-color"),
209 };
210
211 cx.layer().depth(100.0).draw(|cx| {
212 cx.draw(quad);
213 });
214 }
215}