Skip to main content

cranpose_ui/modifier/
long_press.rs

1use std::{
2    cell::{Cell, RefCell},
3    hash::{Hash, Hasher},
4    rc::{Rc, Weak},
5};
6
7use cranpose_core::{NodeId, current_runtime_handle, internal::FrameCallbackRegistration};
8use cranpose_foundation::{
9    DRAG_THRESHOLD, DelegatableNode, ModifierNode, ModifierNodeContext, ModifierNodeElement,
10    NodeCapabilities, NodeState, PointerEvent, PointerEventKind, PointerInputNode,
11    gesture_constants::LONG_PRESS_TIMEOUT_NANOS, impl_pointer_input_node,
12};
13use cranpose_ui_graphics::Point;
14
15use super::{Modifier, inspector_metadata};
16use crate::render_state::{AppContext, current_app_context};
17
18impl Modifier {
19    /// Calls `action` when a press rests on this node for the long press
20    /// time without moving, and offers a screen reader the same action
21    /// under `label`, as Compose's `combinedClickable(onLongClick)` does.
22    ///
23    /// The press then belongs to this node: the controls inside it see the
24    /// press cancelled and do not click when it ends.
25    ///
26    /// Example: `Modifier::empty().on_long_press("Show tooltip", move || shown.set(true))`
27    pub fn on_long_press(self, label: impl Into<String>, action: impl Fn() + 'static) -> Self {
28        let action: Rc<dyn Fn()> = Rc::new(action);
29        let reader = Rc::clone(&action);
30        self.then(
31            Self::with_element(LongPressElement { action })
32                .with_inspector_metadata(inspector_metadata("longPress", |_| {})),
33        )
34        .on_long_click(label, move || {
35            reader();
36            true
37        })
38    }
39}
40
41#[derive(Clone)]
42struct LongPressElement {
43    action: Rc<dyn Fn()>,
44}
45
46impl std::fmt::Debug for LongPressElement {
47    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48        formatter.write_str("LongPressElement")
49    }
50}
51
52impl PartialEq for LongPressElement {
53    fn eq(&self, other: &Self) -> bool {
54        Rc::ptr_eq(&self.action, &other.action)
55    }
56}
57
58impl Hash for LongPressElement {
59    fn hash<H: Hasher>(&self, state: &mut H) {
60        "longPress".hash(state);
61    }
62}
63
64impl ModifierNodeElement for LongPressElement {
65    type Node = LongPressNode;
66
67    fn create(&self) -> Self::Node {
68        let press = Rc::new(Press::new(Rc::clone(&self.action)));
69        let dispatch_press = Rc::clone(&press);
70        LongPressNode {
71            dispatch: Rc::new(move |event| Press::on_event(&dispatch_press, &event)),
72            press,
73            state: NodeState::new(),
74        }
75    }
76
77    fn update(&self, node: &mut Self::Node) {
78        *node.press.action.borrow_mut() = Rc::clone(&self.action);
79    }
80
81    fn always_update(&self) -> bool {
82        true
83    }
84
85    fn capabilities(&self) -> NodeCapabilities {
86        NodeCapabilities::POINTER_INPUT
87    }
88}
89
90/// One held press and the frame timer that turns it into a long press.
91struct Press {
92    action: RefCell<Rc<dyn Fn()>>,
93    node: Cell<Option<NodeId>>,
94    /// Where the press went down, while it is held and still a press.
95    origin: Cell<Option<Point>>,
96    /// The first frame time after the press went down: frames and events
97    /// keep different clocks.
98    started_nanos: Cell<Option<u64>>,
99    fired: Cell<bool>,
100    context: RefCell<Weak<AppContext>>,
101    timer: RefCell<Option<FrameCallbackRegistration>>,
102}
103
104impl Press {
105    fn new(action: Rc<dyn Fn()>) -> Self {
106        Self {
107            action: RefCell::new(action),
108            node: Cell::new(None),
109            origin: Cell::new(None),
110            started_nanos: Cell::new(None),
111            fired: Cell::new(false),
112            context: RefCell::new(Weak::new()),
113            timer: RefCell::new(None),
114        }
115    }
116
117    fn on_event(press: &Rc<Self>, event: &PointerEvent) {
118        if event.id != 0 {
119            return;
120        }
121        match event.kind {
122            PointerEventKind::Down if !event.is_consumed() => Self::begin(press, event),
123            PointerEventKind::Move if press.fired.get() => event.consume(),
124            PointerEventKind::Move => {
125                let moved = press.origin.get().is_some_and(|origin| {
126                    let to = event.global_position;
127                    (to.x - origin.x).hypot(to.y - origin.y) > DRAG_THRESHOLD
128                });
129                if moved || event.is_consumed() {
130                    press.end();
131                }
132            }
133            PointerEventKind::Up => {
134                if press.fired.get() {
135                    event.consume();
136                }
137                press.end();
138            }
139            PointerEventKind::Cancel => press.end(),
140            _ => {}
141        }
142    }
143
144    fn begin(press: &Rc<Self>, event: &PointerEvent) {
145        press.end();
146        let Some(runtime) = current_runtime_handle() else {
147            return;
148        };
149        press.origin.set(Some(event.global_position));
150        *press.context.borrow_mut() = current_app_context()
151            .as_ref()
152            .map_or_else(Weak::new, Rc::downgrade);
153        Self::watch(press, &runtime);
154    }
155
156    /// Checks on each frame whether the press has rested long enough.
157    fn watch(press: &Rc<Self>, runtime: &cranpose_core::RuntimeHandle) {
158        let weak = Rc::downgrade(press);
159        let next = runtime.clone();
160        let registration = runtime.frame_clock().with_frame_nanos(move |frame_nanos| {
161            let Some(press) = weak.upgrade() else {
162                return;
163            };
164            press.timer.borrow_mut().take();
165            if press.origin.get().is_none() {
166                return;
167            }
168            let started = press.started_nanos.get().unwrap_or(frame_nanos);
169            press.started_nanos.set(Some(started));
170            if frame_nanos.saturating_sub(started) < LONG_PRESS_TIMEOUT_NANOS {
171                Self::watch(&press, &next);
172                return;
173            }
174            press.fired.set(true);
175            if let (Some(context), Some(node)) =
176                (press.context.borrow().upgrade(), press.node.get())
177            {
178                context.claim_pointer(node);
179            }
180            let action = Rc::clone(&press.action.borrow());
181            action();
182        });
183        *press.timer.borrow_mut() = Some(registration);
184    }
185
186    fn end(&self) {
187        self.timer.borrow_mut().take();
188        self.origin.set(None);
189        self.started_nanos.set(None);
190        self.fired.set(false);
191    }
192}
193
194struct LongPressNode {
195    dispatch: Rc<dyn Fn(PointerEvent)>,
196    press: Rc<Press>,
197    state: NodeState,
198}
199
200impl ModifierNode for LongPressNode {
201    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
202        self.state.set_attached(true);
203        self.press.node.set(context.node_id());
204        context.invalidate(cranpose_foundation::InvalidationKind::PointerInput);
205    }
206
207    fn on_detach(&mut self) {
208        self.state.set_attached(false);
209        self.press.end();
210        self.press.node.set(None);
211    }
212
213    impl_pointer_input_node!();
214}
215
216impl DelegatableNode for LongPressNode {
217    fn node_state(&self) -> &NodeState {
218        &self.state
219    }
220}
221
222impl PointerInputNode for LongPressNode {
223    fn pointer_input_handler(&self) -> Option<Rc<dyn Fn(PointerEvent)>> {
224        Some(Rc::clone(&self.dispatch))
225    }
226}