feather-ui 0.4.0

Feather UI library
Documentation
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2025 Fundament Research Institute <https://fundament.institute>

use super::StateMachine;
use crate::component::Layout;
use crate::input::{MouseButton, MouseState, RawEvent, RawEventKind};
use crate::layout::leaf;
use crate::{
    AbsPoint, AbsVector, Dispatchable, InputResult, PxPoint, Slot, SourceID, UnResolve, layout,
};
use core::f32;
use derive_where::derive_where;
use enum_variant_type::EnumVariantType;
use feather_macro::Dispatch;
use smallvec::SmallVec;
use std::collections::HashMap;
use std::rc::Rc;
use std::sync::Arc;
use winit::event::DeviceId;
use winit::keyboard::NamedKey;

/// Represents processed mouse events, transformed into more useful
/// [`MouseAreaEvent::OnClick`], [`MouseAreaEvent::OnDrag`],
/// [`MouseAreaEvent::Hover`], etc. Generated by [`MouseArea`].
#[derive(Debug, Dispatch, EnumVariantType, Clone, PartialEq)]
#[evt(derive(Clone), module = "mouse_area_event")]
pub enum MouseAreaEvent {
    OnClick(MouseButton, AbsPoint),
    OnDblClick(MouseButton, AbsPoint),
    OnDrag(MouseButton, AbsVector),
    Default,
    Hover,
    Active,
}

#[derive(Default, Clone, PartialEq)]
struct MouseAreaState {
    lastdown: HashMap<(DeviceId, u64), (PxPoint, bool)>,
    hover: bool,
    deadzone: f32,
}

impl MouseAreaState {
    fn hover_event(buttons: u16, hover: bool) -> MouseAreaEvent {
        let active = (buttons & MouseButton::Left as u16) != 0;
        match (active, hover) {
            (true, true) => MouseAreaEvent::Active,
            (true, false) => MouseAreaEvent::Hover,
            (false, true) => MouseAreaEvent::Hover,
            (false, false) => MouseAreaEvent::Default,
        }
    }
}

impl super::EventRouter for MouseAreaState {
    type Input = RawEvent;
    type Output = MouseAreaEvent;

    fn process(
        mut this: crate::AccessCell<Self>,
        input: Self::Input,
        area: crate::PxRect,
        _: crate::PxRect,
        dpi: crate::RelDim,
        _: &std::sync::Weak<crate::Driver>,
    ) -> InputResult<SmallVec<[Self::Output; 1]>> {
        match input {
            RawEvent::Key {
                down,
                logical_key: winit::keyboard::Key::Named(code),
                ..
            } => {
                if (code == NamedKey::Enter || code == NamedKey::Accept) && down {
                    return InputResult::Consume(
                        [MouseAreaEvent::OnClick(
                            crate::input::MouseButton::Left,
                            AbsPoint::zero(),
                        )]
                        .into(),
                    );
                }
            }
            RawEvent::MouseOn { all_buttons, .. } | RawEvent::MouseOff { all_buttons, .. } => {
                this.hover = matches!(input, RawEvent::MouseOff { .. });
                let hover = Self::hover_event(all_buttons, this.hover);
                return InputResult::Consume([hover].into());
            }
            RawEvent::MouseMove {
                device_id,
                pos,
                all_buttons,
                ..
            } => {
                let hover = Self::hover_event(all_buttons, this.hover);
                for i in 0..5 {
                    let deadzone = this.deadzone;
                    if let Some((last_pos, drag)) = this.lastdown.get_mut(&(device_id, (1 << i))) {
                        let diff = pos - *last_pos;
                        if !*drag && diff.dot(diff) > deadzone {
                            *drag = true;
                        }

                        let b = match i {
                            0 => MouseButton::Left,
                            1 => MouseButton::Middle,
                            2 => MouseButton::Right,
                            3 => MouseButton::Back,
                            4 => MouseButton::Forward,
                            _ => panic!("Impossible number"),
                        };
                        if *drag {
                            *last_pos = pos;
                            return InputResult::Consume(SmallVec::from_iter([
                                hover,
                                MouseAreaEvent::OnDrag(b, diff.unresolve(dpi)),
                            ]));
                        }
                    }
                }

                return InputResult::Consume([hover].into());
            }
            RawEvent::Mouse {
                device_id,
                state,
                pos,
                button,
                ..
            } => {
                let hover = Self::hover_event(button as u16, this.hover);
                match state {
                    MouseState::Down => {
                        if area.contains(pos) {
                            this.lastdown
                                .insert((device_id, button as u64), (pos, false));
                            return InputResult::Consume([hover].into());
                        }
                    }
                    MouseState::Up => {
                        if let Some((last_pos, drag)) =
                            this.lastdown.remove(&(device_id, button as u64))
                            && area.contains(pos)
                        {
                            return InputResult::Consume(SmallVec::from_iter([
                                if drag {
                                    let diff = pos - last_pos;
                                    MouseAreaEvent::OnDrag(button, diff.unresolve(dpi))
                                } else {
                                    MouseAreaEvent::OnClick(button, pos.unresolve(dpi))
                                },
                                hover,
                            ]));
                        }
                    }
                    MouseState::DblClick => {
                        if let Some((last_pos, drag)) =
                            this.lastdown.remove(&(device_id, button as u64))
                            && area.contains(pos)
                        {
                            return InputResult::Consume(if drag {
                                SmallVec::from_iter([
                                    MouseAreaEvent::OnClick(button, pos.unresolve(dpi)),
                                    MouseAreaEvent::OnDblClick(button, pos.unresolve(dpi)),
                                    hover,
                                ])
                            } else {
                                SmallVec::from_iter([
                                    if drag {
                                        let diff = pos - last_pos;
                                        MouseAreaEvent::OnDrag(button, diff.unresolve(dpi))
                                    } else {
                                        MouseAreaEvent::OnClick(button, pos.unresolve(dpi))
                                    },
                                    hover,
                                ])
                            });
                        }
                    }
                }
            }
            RawEvent::Touch {
                device_id,
                index,
                state,
                pos,
                ..
            } => match state {
                crate::input::TouchState::Start => {
                    let hover = Self::hover_event(MouseButton::Left as u16, this.hover);
                    if area.contains(pos.xy()) {
                        this.lastdown
                            .insert((device_id, index as u64), (pos.xy(), false));
                        return InputResult::Consume([hover].into());
                    }
                }
                crate::input::TouchState::Move => {
                    let deadzone = this.deadzone;
                    let hover = Self::hover_event(MouseButton::Left as u16, this.hover);
                    if let Some((last_pos, drag)) =
                        this.lastdown.get_mut(&(device_id, index as u64))
                    {
                        let diff = pos.xy() - *last_pos;
                        if !*drag && diff.dot(diff) > deadzone {
                            *drag = true;
                        }
                        if *drag {
                            return InputResult::Consume(SmallVec::from_iter([
                                hover,
                                MouseAreaEvent::OnDrag(MouseButton::Left, diff.unresolve(dpi)),
                            ]));
                        }
                        return InputResult::Consume(SmallVec::new());
                    }
                }
                crate::input::TouchState::End => {
                    let hover = Self::hover_event(0, this.hover);
                    if let Some((last_pos, drag)) = this.lastdown.remove(&(device_id, index as u64))
                        && area.contains(pos.xy())
                    {
                        let diff = pos.xy() - last_pos;
                        return InputResult::Consume(SmallVec::from_iter([
                            if drag {
                                MouseAreaEvent::OnDrag(MouseButton::Left, diff.unresolve(dpi))
                            } else {
                                MouseAreaEvent::OnClick(MouseButton::Left, pos.xy().unresolve(dpi))
                            },
                            hover,
                        ]));
                    }
                }
            },
            _ => (),
        }
        InputResult::Forward(SmallVec::new())
    }
}

#[derive_where(Clone)]
pub struct MouseArea<T> {
    pub id: Arc<SourceID>,
    props: Rc<T>,
    deadzone: f32, // A deadzone of infinity disables drag events
    slots: [Option<Slot>; MouseAreaEvent::SIZE],
}

impl<T: leaf::Prop> MouseArea<T> {
    pub fn new(
        id: Arc<SourceID>,
        props: T,
        deadzone: Option<f32>,
        slots: [Option<Slot>; MouseAreaEvent::SIZE],
    ) -> Self {
        Self {
            id,
            props: props.into(),
            deadzone: deadzone.unwrap_or(f32::INFINITY),
            slots,
        }
    }
}

impl<T: leaf::Prop> crate::StateMachineChild for MouseArea<T> {
    fn id(&self) -> Arc<SourceID> {
        self.id.clone()
    }
    fn init(
        &self,
        _: &std::sync::Weak<crate::Driver>,
    ) -> Result<Box<dyn super::StateMachineWrapper>, crate::Error> {
        Ok(Box::new(StateMachine {
            state: MouseAreaState {
                lastdown: HashMap::new(),
                hover: false,
                deadzone: f32::INFINITY,
            },
            input_mask: RawEventKind::Mouse as u64
                | RawEventKind::MouseMove as u64
                | RawEventKind::Touch as u64
                | RawEventKind::Key as u64,
            output: self.slots.clone(),
            changed: true,
        }))
    }
}

impl<T: leaf::Prop + 'static> super::Component for MouseArea<T>
where
    for<'a> &'a T: Into<&'a (dyn leaf::Prop + 'static)>,
{
    type Props = T;

    fn layout(
        &self,
        manager: &mut crate::StateManager,
        _: &crate::graphics::Driver,
        _: &Arc<SourceID>,
    ) -> Box<dyn Layout<T>> {
        // TODO: allow layout to return a Result
        manager
            .get_mut::<StateMachine<MouseAreaState, { MouseAreaEvent::SIZE }>>(&self.id)
            .map(|state| {
                state.state.deadzone = self.deadzone;
            })
            .unwrap();

        Box::new(layout::Node::<T, dyn leaf::Prop> {
            props: self.props.clone(),
            children: Default::default(),
            id: Arc::downgrade(&self.id),
            renderable: None,
            layer: None,
        })
    }
}