ori_core/views/
scroll.rs

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        // draw scrollbar track
183        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        // draw scrollbar
201        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}