cranpose_ui/modifier/
long_press.rs1use 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 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
90struct Press {
92 action: RefCell<Rc<dyn Fn()>>,
93 node: Cell<Option<NodeId>>,
94 origin: Cell<Option<Point>>,
96 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 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}