feather_ui/component/
scroll_area.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: 2025 Fundament Research Institute <https://fundament.institute>
3
4use super::StateMachine;
5use crate::component::{ChildOf, Layout};
6use crate::input::{MouseButton, MouseState, RawEvent, RawEventKind};
7use crate::layout::{Desc, base, fixed};
8use crate::persist::{FnPersist, VectorMap};
9use crate::{
10    AbsRect, AbsVector, Dispatchable, InputResult, PxPoint, PxRect, PxVector, RelDim, RelVector,
11    Slot, SourceID, UNSIZED_AXIS, UnResolve, layout,
12};
13use core::f32;
14use derive_where::derive_where;
15use enum_variant_type::EnumVariantType;
16use feather_macro::Dispatch;
17use smallvec::SmallVec;
18use std::collections::HashMap;
19use std::rc::Rc;
20use std::sync::Arc;
21use winit::event::DeviceId;
22use winit::keyboard::NamedKey;
23
24#[derive(Default, Clone)]
25struct MinimalArea {
26    area: crate::DRect,
27}
28
29impl base::Area for MinimalArea {
30    fn area(&self) -> &crate::DRect {
31        &self.area
32    }
33}
34impl base::Empty for MinimalArea {}
35impl base::ZIndex for MinimalArea {}
36impl base::Margin for MinimalArea {}
37impl base::Anchor for MinimalArea {}
38impl base::Limits for MinimalArea {}
39impl base::RLimits for MinimalArea {}
40impl fixed::Prop for MinimalArea {}
41impl fixed::Child for MinimalArea {}
42
43#[derive(Debug, Dispatch, EnumVariantType, Clone, PartialEq)]
44#[evt(derive(Clone), module = "scroll_area_event")]
45pub enum ScrollAreaEvent {
46    OnScroll(AbsVector),
47}
48
49#[derive(Default, Clone, PartialEq)]
50struct ScrollAreaState {
51    lastdown: HashMap<(DeviceId, u64), (PxPoint, bool)>,
52    scroll: PxVector,
53    stepsize: (Option<f32>, Option<f32>),
54    extension: crate::DAbsRect,
55}
56
57impl ScrollAreaState {
58    fn apply_scroll(
59        &mut self,
60        mut change: PxVector,
61        area: &PxRect,
62        extent: &PxRect,
63        dpi: RelDim,
64    ) -> ScrollAreaEvent {
65        let bounds = area.dim();
66        //let extension = self.extension.resolve(dpi);
67
68        let mut scroll = self.scroll + change;
69        let max = (bounds - extent.dim()).min(crate::PxDim::zero());
70
71        // We should never scroll by a positive amount (this would scroll past topleft
72        // corner), and we should never scroll by an amount that would put us
73        // past the bottomright corner.
74        scroll = scroll.max(max.to_vector().cast_unit());
75        scroll = scroll.min(PxVector::zero());
76
77        change = scroll - self.scroll;
78        self.scroll += change;
79
80        ScrollAreaEvent::OnScroll(change.unresolve(dpi))
81    }
82
83    fn stepvec(&self) -> RelVector {
84        RelVector::new(
85            if self.stepsize.0.is_some() { 1.0 } else { 0.0 },
86            if self.stepsize.1.is_some() { 1.0 } else { 0.0 },
87        )
88    }
89}
90
91impl super::EventRouter for ScrollAreaState {
92    type Input = RawEvent;
93    type Output = ScrollAreaEvent;
94
95    fn process(
96        mut this: crate::AccessCell<Self>,
97        input: Self::Input,
98        area: PxRect,
99        extent: PxRect,
100        dpi: crate::RelDim,
101        _: &std::sync::Weak<crate::Driver>,
102    ) -> InputResult<SmallVec<[Self::Output; 1]>> {
103        match input {
104            RawEvent::Key {
105                down: true,
106                logical_key: winit::keyboard::Key::Named(code),
107                ..
108            } => {
109                if let Some(change) = match (code, this.stepsize.0, this.stepsize.1) {
110                    (NamedKey::ArrowUp, _, Some(y)) => Some(PxVector::new(0.0, -y)),
111                    (NamedKey::ArrowDown, _, Some(y)) => Some(PxVector::new(0.0, y)),
112                    (NamedKey::ArrowLeft, Some(x), _) => Some(PxVector::new(-x, 0.0)),
113                    (NamedKey::ArrowRight, Some(x), _) => Some(PxVector::new(x, 0.0)),
114                    (NamedKey::PageUp, _, Some(_)) => Some(PxVector::new(0.0, -area.dim().height)),
115                    (NamedKey::PageDown, _, Some(_)) => Some(PxVector::new(0.0, area.dim().height)),
116                    _ => None,
117                } {
118                    let e = this.apply_scroll(change, &area, &extent, dpi);
119                    return InputResult::Consume([e].into());
120                }
121            }
122            RawEvent::MouseScroll { delta, .. } => {
123                let change = match delta {
124                    Ok(change) => change,
125                    Err(change) => PxVector::new(
126                        change.x * this.stepsize.0.unwrap_or_default(),
127                        change.y * this.stepsize.1.unwrap_or_default(),
128                    ),
129                };
130
131                let e = this.apply_scroll(change, &area, &extent, dpi);
132                return InputResult::Consume([e].into());
133            }
134            RawEvent::MouseMove { device_id, pos, .. } => {
135                let stepvec = this.stepvec();
136                if let Some((last_pos, drag)) = this.lastdown.get_mut(&(device_id, 0)) {
137                    let diff = (pos - *last_pos).component_mul(stepvec.cast_unit());
138                    if !*drag {
139                        *drag = true;
140                    }
141
142                    if *drag {
143                        *last_pos = pos;
144                        let e = this.apply_scroll(diff, &area, &extent, dpi);
145                        return InputResult::Consume([e].into());
146                    }
147                    return InputResult::Consume(SmallVec::new());
148                }
149            }
150            RawEvent::Mouse {
151                device_id,
152                state,
153                pos,
154                button,
155                ..
156            } => match (state, button) {
157                (MouseState::Down, MouseButton::Left) => {
158                    if area.contains(pos) {
159                        this.lastdown.insert((device_id, 0), (pos, false));
160                        return InputResult::Consume(SmallVec::new());
161                    }
162                }
163                (MouseState::Up, MouseButton::Left) => {
164                    if let Some((last_pos, drag)) = this.lastdown.remove(&(device_id, 0))
165                        && area.contains(pos)
166                    {
167                        let e = this.apply_scroll(pos - last_pos, &area, &extent, dpi);
168                        return InputResult::Consume(if drag {
169                            [e].into()
170                        } else {
171                            SmallVec::new()
172                        });
173                    }
174                }
175                _ => (),
176            },
177            RawEvent::Touch {
178                device_id,
179                index,
180                state,
181                pos,
182                ..
183            } => match state {
184                crate::input::TouchState::Start => {
185                    if area.contains(pos.xy()) {
186                        this.lastdown
187                            .insert((device_id, index as u64), (pos.xy(), false));
188                        return InputResult::Consume(SmallVec::new());
189                    }
190                }
191                crate::input::TouchState::Move => {
192                    let stepvec = this.stepvec();
193                    if let Some((last_pos, drag)) =
194                        this.lastdown.get_mut(&(device_id, index as u64))
195                    {
196                        let diff = (pos.xy() - *last_pos).component_mul(stepvec.cast_unit());
197                        if !*drag {
198                            *drag = true;
199                        }
200
201                        let e = this.apply_scroll(diff, &area, &extent, dpi);
202                        return InputResult::Consume([e].into());
203                    }
204                }
205                crate::input::TouchState::End => {
206                    // TODO: implement kinetic drag
207                    if let Some((last_pos, drag)) = this.lastdown.remove(&(device_id, index as u64))
208                        && area.contains(pos.xy())
209                    {
210                        let stepvec = this.stepvec();
211                        let e = this.apply_scroll(
212                            (pos.xy() - last_pos).component_mul(stepvec.cast_unit()),
213                            &area,
214                            &extent,
215                            dpi,
216                        );
217                        return InputResult::Consume(if drag {
218                            [e].into()
219                        } else {
220                            SmallVec::new()
221                        });
222                    }
223                }
224            },
225            _ => (),
226        }
227
228        InputResult::Forward(SmallVec::new())
229    }
230}
231
232#[derive_where(Clone)]
233pub struct ScrollArea<T> {
234    pub id: Arc<SourceID>,
235    props: Rc<T>,
236    stepsize: (Option<f32>, Option<f32>),
237    extension: crate::DAbsRect,
238    children: im::Vector<Option<Box<ChildOf<dyn fixed::Prop>>>>,
239    slots: [Option<Slot>; ScrollAreaEvent::SIZE],
240}
241
242impl<T: fixed::Prop + 'static> ScrollArea<T> {
243    pub fn new(
244        id: Arc<SourceID>,
245        props: T,
246        stepsize: (Option<f32>, Option<f32>),
247        extension: crate::DAbsRect,
248        children: im::Vector<Option<Box<ChildOf<dyn fixed::Prop>>>>,
249        slots: [Option<Slot>; ScrollAreaEvent::SIZE],
250    ) -> Self {
251        Self {
252            id,
253            props: props.into(),
254            children,
255            slots,
256            stepsize,
257            extension,
258        }
259    }
260}
261
262impl<T: fixed::Prop + 'static> crate::StateMachineChild for ScrollArea<T> {
263    fn id(&self) -> Arc<SourceID> {
264        self.id.clone()
265    }
266    fn init(
267        &self,
268        _: &std::sync::Weak<crate::Driver>,
269    ) -> Result<Box<dyn super::StateMachineWrapper>, crate::Error> {
270        Ok(Box::new(StateMachine {
271            state: ScrollAreaState {
272                ..Default::default()
273            },
274            input_mask: RawEventKind::MouseScroll as u64
275                | RawEventKind::Mouse as u64
276                | RawEventKind::MouseMove as u64
277                | RawEventKind::Touch as u64
278                | RawEventKind::Key as u64,
279            output: self.slots.clone(),
280            changed: true,
281        }))
282    }
283    fn apply_children(
284        &self,
285        f: &mut dyn FnMut(&dyn crate::StateMachineChild) -> eyre::Result<()>,
286    ) -> eyre::Result<()> {
287        self.children
288            .iter()
289            .try_for_each(|x| f(x.as_ref().unwrap().as_ref()))
290    }
291}
292
293impl<T: fixed::Prop + 'static> super::Component for ScrollArea<T>
294where
295    for<'a> &'a T: Into<&'a (dyn fixed::Prop + 'static)>,
296{
297    type Props = T;
298
299    fn layout(
300        &self,
301        manager: &mut crate::StateManager,
302        driver: &crate::graphics::Driver,
303        window: &Arc<SourceID>,
304    ) -> Box<dyn Layout<T> + 'static> {
305        let scroll = manager
306            .get_mut::<StateMachine<ScrollAreaState, { ScrollAreaEvent::SIZE }>>(&self.id)
307            .map(|state| {
308                state.state.stepsize = self.stepsize;
309                state.state.extension = self.extension;
310                state.state.scroll
311            })
312            .unwrap();
313
314        // To create a scroll area, we create an intermediate layout node to hold the
315        // children, which is always unsized, which we then move around to scroll.
316        let mut map = VectorMap::new(crate::persist::Persist::new(
317            |child: &Option<Box<ChildOf<dyn fixed::Prop>>>| -> Option<Box<dyn Layout<<dyn fixed::Prop as Desc>::Child>>> {
318                Some(child.as_ref()?.layout(manager, driver, window))
319            })
320        );
321
322        let (_, children) = map.call(Default::default(), &self.children);
323        let scrollable: Box<dyn layout::Layout<MinimalArea>> =
324            Box::new(layout::Node::<MinimalArea, dyn fixed::Prop> {
325                props: Rc::new(MinimalArea {
326                    area: crate::DRect {
327                        px: PxRect::zero(),
328                        dp: AbsRect::new(scroll.x, scroll.y, 0.0, 0.0),
329                        rel: crate::RelRect::new(
330                            0.0,
331                            0.0,
332                            if self.stepsize.0.is_some() {
333                                UNSIZED_AXIS
334                            } else {
335                                1.0
336                            },
337                            if self.stepsize.1.is_some() {
338                                UNSIZED_AXIS
339                            } else {
340                                1.0
341                            },
342                        ),
343                    },
344                }),
345                children,
346                id: std::sync::Weak::new(),
347                renderable: None,
348                layer: None,
349            });
350
351        let inner: Box<dyn layout::Layout<dyn fixed::Child>> = Box::new(scrollable);
352
353        Box::new(layout::Node::<T, dyn fixed::Prop> {
354            props: self.props.clone(),
355            children: im::vector![Some(inner)],
356            id: Arc::downgrade(&self.id),
357            renderable: None,
358            layer: Some((crate::color::sRGB32::white(), 0.0)),
359        })
360    }
361}
362
363/*
364#[derive_where(Clone)]
365pub struct Node<T: fixed::Prop> {
366    pub id: std::sync::Weak<SourceID>,
367    pub props: Rc<T>,
368    pub children: im::Vector<Option<Box<dyn Layout<dyn fixed::Child>>>>,
369    pub size: Rc<RefCell<PxDim>>,
370    //pub renderable: Rc<dyn crate::render::Renderable>,
371}
372
373impl<T: fixed::Prop + 'static> Layout<T> for Node<T> {
374    fn get_props(&self) -> &T {
375        &self.props
376    }
377
378    fn stage<'a>(
379        &self,
380        area: AbsRect,
381        limits: crate::AbsLimits,
382        window: &mut crate::component::window::WindowState,
383    ) -> Box<dyn layout::Staged + 'a> {
384        let r = <dyn fixed::Prop>::stage(
385            self.props.as_ref().into(),
386            area,
387            limits,
388            &self.children,
389            self.id.clone(),
390            None,
391            window,
392        );
393
394        *self.size.borrow_mut() = r.get_area().dim().0;
395        r
396    }
397}
398*/