feather_ui/component/
mouse_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::Layout;
6use crate::input::{MouseButton, MouseState, RawEvent, RawEventKind};
7use crate::layout::leaf;
8use crate::{
9    AbsPoint, AbsVector, Dispatchable, InputResult, PxPoint, Slot, SourceID, UnResolve, layout,
10};
11use core::f32;
12use derive_where::derive_where;
13use enum_variant_type::EnumVariantType;
14use feather_macro::Dispatch;
15use smallvec::SmallVec;
16use std::collections::HashMap;
17use std::rc::Rc;
18use std::sync::Arc;
19use winit::event::DeviceId;
20use winit::keyboard::NamedKey;
21
22/// Represents processed mouse events, transformed into more useful
23/// [`MouseAreaEvent::OnClick`], [`MouseAreaEvent::OnDrag`],
24/// [`MouseAreaEvent::Hover`], etc. Generated by [`MouseArea`].
25#[derive(Debug, Dispatch, EnumVariantType, Clone, PartialEq)]
26#[evt(derive(Clone), module = "mouse_area_event")]
27pub enum MouseAreaEvent {
28    OnClick(MouseButton, AbsPoint),
29    OnDblClick(MouseButton, AbsPoint),
30    OnDrag(MouseButton, AbsVector),
31    Default,
32    Hover,
33    Active,
34}
35
36#[derive(Default, Clone, PartialEq)]
37struct MouseAreaState {
38    lastdown: HashMap<(DeviceId, u64), (PxPoint, bool)>,
39    hover: bool,
40    deadzone: f32,
41}
42
43impl MouseAreaState {
44    fn hover_event(buttons: u16, hover: bool) -> MouseAreaEvent {
45        let active = (buttons & MouseButton::Left as u16) != 0;
46        match (active, hover) {
47            (true, true) => MouseAreaEvent::Active,
48            (true, false) => MouseAreaEvent::Hover,
49            (false, true) => MouseAreaEvent::Hover,
50            (false, false) => MouseAreaEvent::Default,
51        }
52    }
53}
54
55impl super::EventRouter for MouseAreaState {
56    type Input = RawEvent;
57    type Output = MouseAreaEvent;
58
59    fn process(
60        mut this: crate::AccessCell<Self>,
61        input: Self::Input,
62        area: crate::PxRect,
63        _: crate::PxRect,
64        dpi: crate::RelDim,
65        _: &std::sync::Weak<crate::Driver>,
66    ) -> InputResult<SmallVec<[Self::Output; 1]>> {
67        match input {
68            RawEvent::Key {
69                down,
70                logical_key: winit::keyboard::Key::Named(code),
71                ..
72            } => {
73                if (code == NamedKey::Enter || code == NamedKey::Accept) && down {
74                    return InputResult::Consume(
75                        [MouseAreaEvent::OnClick(
76                            crate::input::MouseButton::Left,
77                            AbsPoint::zero(),
78                        )]
79                        .into(),
80                    );
81                }
82            }
83            RawEvent::MouseOn { all_buttons, .. } | RawEvent::MouseOff { all_buttons, .. } => {
84                this.hover = matches!(input, RawEvent::MouseOff { .. });
85                let hover = Self::hover_event(all_buttons, this.hover);
86                return InputResult::Consume([hover].into());
87            }
88            RawEvent::MouseMove {
89                device_id,
90                pos,
91                all_buttons,
92                ..
93            } => {
94                let hover = Self::hover_event(all_buttons, this.hover);
95                for i in 0..5 {
96                    let deadzone = this.deadzone;
97                    if let Some((last_pos, drag)) = this.lastdown.get_mut(&(device_id, (1 << i))) {
98                        let diff = pos - *last_pos;
99                        if !*drag && diff.dot(diff) > deadzone {
100                            *drag = true;
101                        }
102
103                        let b = match i {
104                            0 => MouseButton::Left,
105                            1 => MouseButton::Middle,
106                            2 => MouseButton::Right,
107                            3 => MouseButton::Back,
108                            4 => MouseButton::Forward,
109                            _ => panic!("Impossible number"),
110                        };
111                        if *drag {
112                            *last_pos = pos;
113                            return InputResult::Consume(SmallVec::from_iter([
114                                hover,
115                                MouseAreaEvent::OnDrag(b, diff.unresolve(dpi)),
116                            ]));
117                        }
118                    }
119                }
120
121                return InputResult::Consume([hover].into());
122            }
123            RawEvent::Mouse {
124                device_id,
125                state,
126                pos,
127                button,
128                ..
129            } => {
130                let hover = Self::hover_event(button as u16, this.hover);
131                match state {
132                    MouseState::Down => {
133                        if area.contains(pos) {
134                            this.lastdown
135                                .insert((device_id, button as u64), (pos, false));
136                            return InputResult::Consume([hover].into());
137                        }
138                    }
139                    MouseState::Up => {
140                        if let Some((last_pos, drag)) =
141                            this.lastdown.remove(&(device_id, button as u64))
142                            && area.contains(pos)
143                        {
144                            return InputResult::Consume(SmallVec::from_iter([
145                                if drag {
146                                    let diff = pos - last_pos;
147                                    MouseAreaEvent::OnDrag(button, diff.unresolve(dpi))
148                                } else {
149                                    MouseAreaEvent::OnClick(button, pos.unresolve(dpi))
150                                },
151                                hover,
152                            ]));
153                        }
154                    }
155                    MouseState::DblClick => {
156                        if let Some((last_pos, drag)) =
157                            this.lastdown.remove(&(device_id, button as u64))
158                            && area.contains(pos)
159                        {
160                            return InputResult::Consume(if drag {
161                                SmallVec::from_iter([
162                                    MouseAreaEvent::OnClick(button, pos.unresolve(dpi)),
163                                    MouseAreaEvent::OnDblClick(button, pos.unresolve(dpi)),
164                                    hover,
165                                ])
166                            } else {
167                                SmallVec::from_iter([
168                                    if drag {
169                                        let diff = pos - last_pos;
170                                        MouseAreaEvent::OnDrag(button, diff.unresolve(dpi))
171                                    } else {
172                                        MouseAreaEvent::OnClick(button, pos.unresolve(dpi))
173                                    },
174                                    hover,
175                                ])
176                            });
177                        }
178                    }
179                }
180            }
181            RawEvent::Touch {
182                device_id,
183                index,
184                state,
185                pos,
186                ..
187            } => match state {
188                crate::input::TouchState::Start => {
189                    let hover = Self::hover_event(MouseButton::Left as u16, this.hover);
190                    if area.contains(pos.xy()) {
191                        this.lastdown
192                            .insert((device_id, index as u64), (pos.xy(), false));
193                        return InputResult::Consume([hover].into());
194                    }
195                }
196                crate::input::TouchState::Move => {
197                    let deadzone = this.deadzone;
198                    let hover = Self::hover_event(MouseButton::Left as u16, this.hover);
199                    if let Some((last_pos, drag)) =
200                        this.lastdown.get_mut(&(device_id, index as u64))
201                    {
202                        let diff = pos.xy() - *last_pos;
203                        if !*drag && diff.dot(diff) > deadzone {
204                            *drag = true;
205                        }
206                        if *drag {
207                            return InputResult::Consume(SmallVec::from_iter([
208                                hover,
209                                MouseAreaEvent::OnDrag(MouseButton::Left, diff.unresolve(dpi)),
210                            ]));
211                        }
212                        return InputResult::Consume(SmallVec::new());
213                    }
214                }
215                crate::input::TouchState::End => {
216                    let hover = Self::hover_event(0, this.hover);
217                    if let Some((last_pos, drag)) = this.lastdown.remove(&(device_id, index as u64))
218                        && area.contains(pos.xy())
219                    {
220                        let diff = pos.xy() - last_pos;
221                        return InputResult::Consume(SmallVec::from_iter([
222                            if drag {
223                                MouseAreaEvent::OnDrag(MouseButton::Left, diff.unresolve(dpi))
224                            } else {
225                                MouseAreaEvent::OnClick(MouseButton::Left, pos.xy().unresolve(dpi))
226                            },
227                            hover,
228                        ]));
229                    }
230                }
231            },
232            _ => (),
233        }
234        InputResult::Forward(SmallVec::new())
235    }
236}
237
238#[derive_where(Clone)]
239pub struct MouseArea<T> {
240    pub id: Arc<SourceID>,
241    props: Rc<T>,
242    deadzone: f32, // A deadzone of infinity disables drag events
243    slots: [Option<Slot>; MouseAreaEvent::SIZE],
244}
245
246impl<T: leaf::Prop> MouseArea<T> {
247    pub fn new(
248        id: Arc<SourceID>,
249        props: T,
250        deadzone: Option<f32>,
251        slots: [Option<Slot>; MouseAreaEvent::SIZE],
252    ) -> Self {
253        Self {
254            id,
255            props: props.into(),
256            deadzone: deadzone.unwrap_or(f32::INFINITY),
257            slots,
258        }
259    }
260}
261
262impl<T: leaf::Prop> crate::StateMachineChild for MouseArea<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: MouseAreaState {
272                lastdown: HashMap::new(),
273                hover: false,
274                deadzone: f32::INFINITY,
275            },
276            input_mask: RawEventKind::Mouse as u64
277                | RawEventKind::MouseMove as u64
278                | RawEventKind::Touch as u64
279                | RawEventKind::Key as u64,
280            output: self.slots.clone(),
281            changed: true,
282        }))
283    }
284}
285
286impl<T: leaf::Prop + 'static> super::Component for MouseArea<T>
287where
288    for<'a> &'a T: Into<&'a (dyn leaf::Prop + 'static)>,
289{
290    type Props = T;
291
292    fn layout(
293        &self,
294        manager: &mut crate::StateManager,
295        _: &crate::graphics::Driver,
296        _: &Arc<SourceID>,
297    ) -> Box<dyn Layout<T>> {
298        // TODO: allow layout to return a Result
299        manager
300            .get_mut::<StateMachine<MouseAreaState, { MouseAreaEvent::SIZE }>>(&self.id)
301            .map(|state| {
302                state.state.deadzone = self.deadzone;
303            })
304            .unwrap();
305
306        Box::new(layout::Node::<T, dyn leaf::Prop> {
307            props: self.props.clone(),
308            children: Default::default(),
309            id: Arc::downgrade(&self.id),
310            renderable: None,
311            layer: None,
312        })
313    }
314}