makepad-widgets 1.0.0

Makepad widgets
Documentation
use crate::{
    Area,
    Cx,
    Event,
    Hit,
    MouseCursor,
    NextFrame,
};

#[derive(Clone, Copy, Debug)]
struct ScrollSample{
    abs: f64,
    time: f64,
}

#[derive(Default, Clone, Debug)]
pub enum ScrollMode {
    #[default]
    DragAndDrop,
    Swipe,
}

#[derive(Default, Clone, Debug)]
enum ScrollState {
    #[default]
    Stopped,
    Drag{samples:Vec<ScrollSample>},
    Flick {delta: f64, next_frame: NextFrame},
    Pulldown {next_frame: NextFrame},
}

#[derive(Default, PartialEq)]
pub enum TouchMotionChange {
    #[default]
    None,
    ScrollStateChanged,
    ScrolledAtChanged,
}

#[derive(Default, Clone)]
pub struct TouchGesture {
    flick_scroll_minimum: f64,
    flick_scroll_maximum: f64,
    flick_scroll_scaling: f64,
    flick_scroll_decay: f64,

    scroll_mode: ScrollMode,
    scroll_state: ScrollState,

    min_scrolled_at: f64,
    max_scrolled_at: f64,
    pulldown_maximum: f64,

    pub scrolled_at: f64,
}

impl TouchGesture {
    pub fn new() -> Self {
        Self {
            flick_scroll_minimum: 0.2,
            flick_scroll_maximum: 80.0,
            flick_scroll_scaling: 0.005,
            flick_scroll_decay: 0.98,

            scroll_state: ScrollState::Stopped,
            scroll_mode: ScrollMode::DragAndDrop,

            scrolled_at: 0.0,
            min_scrolled_at: f64::MIN,
            max_scrolled_at: f64::MAX,
            pulldown_maximum: 60.0,
        }
    }

    pub fn reset_scrolled_at(&mut self) {
        self.scrolled_at = 0.0;
    }

    pub fn set_mode(&mut self, scroll_mode: ScrollMode) {
        self.scroll_mode = scroll_mode;
    }

    pub fn set_range(&mut self, min_offset: f64, max_offset: f64) {
        self.min_scrolled_at = min_offset;
        self.max_scrolled_at = max_offset;
        self.scrolled_at = self.scrolled_at.clamp(
            self.min_scrolled_at - self.pulldown_maximum,
            self.max_scrolled_at + self.pulldown_maximum
        );
    }

    pub fn stop(&mut self) {
        self.scrolled_at = 0.0;
        self.scroll_state = ScrollState::Stopped;
    }

    pub fn is_stopped(&self) -> bool {
        match self.scroll_state {
            ScrollState::Stopped => true,
            _ => false
        }
    }

    pub fn is_dragging(&self) -> bool {
        match self.scroll_state {
            ScrollState::Drag {..} => true,
            _ => false
        }
    }

    pub fn handle_event(&mut self, cx: &mut Cx, event: &Event, area: Area) -> TouchMotionChange {
        let needs_pulldown_when_flicking = self.needs_pulldown_when_flicking();
        let needs_pulldown = self.needs_pulldown();

        match &mut self.scroll_state {
            ScrollState::Flick {delta, next_frame} => {
                if let Some(_) = next_frame.is_event(event) {
                    *delta = *delta * self.flick_scroll_decay;
                    if needs_pulldown_when_flicking {
                        self.scroll_state = ScrollState::Pulldown {next_frame: cx.new_next_frame()};
                        return TouchMotionChange::ScrollStateChanged
                    } else if delta.abs() > self.flick_scroll_minimum {
                        *next_frame = cx.new_next_frame();
                        let delta = *delta;

                        let new_offset = self.scrolled_at - delta;
                        self.scrolled_at = new_offset.clamp(
                            self.min_scrolled_at - self.pulldown_maximum,
                            self.max_scrolled_at + self.pulldown_maximum
                        );

                        return TouchMotionChange::ScrolledAtChanged
                    } else {
                        if needs_pulldown {
                            self.scroll_state = ScrollState::Pulldown {next_frame: cx.new_next_frame()};
                        } else {
                            self.scroll_state = ScrollState::Stopped;
                        }

                        return TouchMotionChange::ScrollStateChanged
                    }
                }
            }
            ScrollState::Pulldown {next_frame} => {
                if let Some(_) = next_frame.is_event(event) {
                    if self.scrolled_at < self.min_scrolled_at {
                        self.scrolled_at += (self.min_scrolled_at - self.scrolled_at) * 0.1;
                        if self.min_scrolled_at - self.scrolled_at < 1.0 {
                            self.scrolled_at = self.min_scrolled_at + 0.5;
                        }
                        else {
                            *next_frame = cx.new_next_frame();
                        }

                        return TouchMotionChange::ScrolledAtChanged
                    }
                    else if self.scrolled_at > self.max_scrolled_at {
                        self.scrolled_at -= (self.scrolled_at - self.max_scrolled_at) * 0.1;
                        if self.scrolled_at - self.max_scrolled_at < 1.0 {
                            self.scrolled_at = self.max_scrolled_at - 0.5;

                            return TouchMotionChange::ScrolledAtChanged
                        }
                        else {
                            *next_frame = cx.new_next_frame();
                        }

                        return TouchMotionChange::ScrolledAtChanged
                    }
                    else {
                        self.scroll_state = ScrollState::Stopped;
                        return TouchMotionChange::ScrollStateChanged
                    }
                }
            }
            _=>()
        }

        match event.hits_with_capture_overload(cx, area, true) {
            Hit::FingerDown(e) => {
                self.scroll_state = ScrollState::Drag {
                    samples: vec![ScrollSample{abs: e.abs.y, time: e.time}]
                };

                return TouchMotionChange::ScrollStateChanged
            }
            Hit::FingerMove(e) => {
                cx.set_cursor(MouseCursor::Default);
                match &mut self.scroll_state {
                    ScrollState::Drag {samples}=>{
                        let new_abs = e.abs.y;
                        let old_sample = *samples.last().unwrap();
                        samples.push(ScrollSample{abs: new_abs, time: e.time});
                        if samples.len() > 4 {
                            samples.remove(0);
                        }
                        let new_offset = self.scrolled_at + old_sample.abs - new_abs;
                        self.scrolled_at = new_offset.clamp(
                            self.min_scrolled_at - self.pulldown_maximum,
                            self.max_scrolled_at + self.pulldown_maximum
                        );

                        return TouchMotionChange::ScrolledAtChanged
                    }
                    _=>()
                }
            }
            Hit::FingerUp(_e) => {
                match &mut self.scroll_state {
                    ScrollState::Drag {samples} => {
                        match self.scroll_mode {
                            ScrollMode::Swipe => {
                                let mut last = None;
                                let mut scaled_delta = 0.0;
                                let mut total_delta = 0.0;
                                for sample in samples.iter().rev() {
                                    if last.is_none() {
                                        last = Some(sample);
                                    }
                                    else {
                                        total_delta += last.unwrap().abs - sample.abs;
                                        scaled_delta += (last.unwrap().abs - sample.abs)/ (last.unwrap().time - sample.time)
                                    }
                                }
                                scaled_delta *= self.flick_scroll_scaling;

                                if self.needs_pulldown() {
                                    self.scroll_state = ScrollState::Pulldown {next_frame: cx.new_next_frame()};
                                }
                                else if total_delta.abs() > 10.0 && scaled_delta.abs() > self.flick_scroll_minimum {
                                    self.scroll_state = ScrollState::Flick {
                                        delta: scaled_delta.min(self.flick_scroll_maximum).max(-self.flick_scroll_maximum),
                                        next_frame: cx.new_next_frame()
                                    };
                                } else {
                                    self.scroll_state = ScrollState::Stopped;
                                }

                                return TouchMotionChange::ScrollStateChanged
                            }
                            ScrollMode::DragAndDrop => {
                                self.scroll_state = ScrollState::Stopped;
                                return TouchMotionChange::ScrollStateChanged
                            }
                        }
                    }
                    _=>()
                }
            }
            _ => ()
        }

        TouchMotionChange::None
    }

    fn needs_pulldown(&self) -> bool {
        self.scrolled_at < self.min_scrolled_at || self.scrolled_at > self.max_scrolled_at
    }

    fn needs_pulldown_when_flicking(&self) -> bool {
        self.scrolled_at - 0.5 < self.min_scrolled_at - self.pulldown_maximum ||
            self.scrolled_at + 0.5 > self.max_scrolled_at + self.pulldown_maximum
    }
}

impl TouchMotionChange {
    pub fn has_changed(&self) -> bool {
        match self {
            TouchMotionChange::None => false,
            _ => true
        }
    }
}