1use super::*;
2
3impl<R> AppShell<R>
4where
5 R: Renderer,
6 R::Error: Debug,
7{
8 fn resolve_gesture_targets(
9 &self,
10 pointer: PointerId,
11 ) -> Vec<<<R as Renderer>::Scene as RenderScene>::HitTarget> {
12 self.resolve_hit_path(pointer)
13 }
14
15 fn resolve_hit_path(
22 &self,
23 pointer: PointerId,
24 ) -> Vec<<<R as Renderer>::Scene as RenderScene>::HitTarget> {
25 let Some(node_ids) = self.hit_path_tracker.dispatch_order(pointer) else {
26 return Vec::new();
27 };
28
29 let scene = self.renderer.scene();
30 let targets: Vec<_> = node_ids
31 .iter()
32 .filter_map(|&id| scene.find_target(id))
33 .collect();
34 log::trace!(
35 target: "cranpose::input",
36 "resolve_hit_path pointer={pointer:?} cached={node_ids:?} resolved_count={}",
37 targets.len()
38 );
39 targets
40 }
41
42 fn dispatch_targets<I>(&mut self, targets: I, event: PointerEvent, stop_on_consume: bool)
43 where
44 I: IntoIterator<Item = <<R as Renderer>::Scene as RenderScene>::HitTarget>,
45 {
46 for target in targets {
47 let node_id = target.node_id();
48 target.dispatch(event.clone());
49 log::trace!(
50 target: "cranpose::input",
51 "dispatch {:?} node={} consumed={} stop_on_consume={}",
52 event.kind,
53 node_id,
54 event.is_consumed(),
55 stop_on_consume,
56 );
57 if stop_on_consume && event.is_consumed() {
58 break;
59 }
60 }
61 }
62
63 pub fn set_cursor(&mut self, x: f32, y: f32) -> bool {
64 let _event_handler = enter_event_handler_scope();
65 let app_context = Rc::clone(&self.app_context);
66 let result = app_context
67 .enter(|| run_in_mutable_snapshot(|| self.set_cursor_inner(x, y)).unwrap_or(false));
68 if result {
69 self.mark_dirty();
70 }
71 log::trace!(
72 target: "cranpose::input",
73 "set_cursor ({x:.2},{y:.2}) -> {result}"
74 );
75 result
76 }
77
78 fn set_cursor_inner(&mut self, x: f32, y: f32) -> bool {
79 self.cursor = (x, y);
80
81 if self.buttons_pressed != PointerButtons::NONE {
85 if self.hit_path_tracker.has_path(PointerId::PRIMARY) {
86 let targets = self.resolve_gesture_targets(PointerId::PRIMARY);
87 if !targets.is_empty() {
88 let event =
89 PointerEvent::new(PointerEventKind::Move, Point { x, y }, Point { x, y })
90 .with_buttons(self.buttons_pressed);
91 self.dispatch_targets(targets, event, false);
92 return true;
93 }
94
95 return false;
96 }
97
98 return false;
101 }
102
103 let hits = self.renderer.scene().hit_test(x, y);
106 let new_ids: Vec<NodeId> = hits.iter().map(|h| h.node_id()).collect();
107
108 let pos = Point { x, y };
110 let previously_hovered = self.hovered_nodes.clone();
111 for old_id in previously_hovered {
112 if !new_ids.contains(&old_id) {
113 if let Some(target) = self.renderer.scene().find_target(old_id) {
114 let exit_event = PointerEvent::new(PointerEventKind::Exit, pos, pos)
115 .with_buttons(self.buttons_pressed);
116 self.dispatch_targets(std::iter::once(target), exit_event, false);
117 }
118 }
119 }
120
121 for hit in &hits {
123 if !self.hovered_nodes.contains(&hit.node_id()) {
124 let enter_event = PointerEvent::new(PointerEventKind::Enter, pos, pos)
125 .with_buttons(self.buttons_pressed);
126 self.dispatch_targets(std::iter::once(hit.clone()), enter_event, false);
127 }
128 }
129
130 self.hovered_nodes = new_ids;
131
132 if !hits.is_empty() {
133 let event = PointerEvent::new(PointerEventKind::Move, pos, pos)
134 .with_buttons(self.buttons_pressed);
135 self.dispatch_targets(hits, event, true);
136 true
137 } else {
138 false
139 }
140 }
141
142 pub fn pointer_pressed(&mut self) -> bool {
143 let _event_handler = enter_event_handler_scope();
144 let app_context = Rc::clone(&self.app_context);
145 let result = app_context
146 .enter(|| run_in_mutable_snapshot(|| self.pointer_pressed_inner()).unwrap_or(false));
147 if result {
148 self.mark_dirty();
149 }
150 log::trace!(target: "cranpose::input", "pointer_pressed -> {result}");
151 result
152 }
153
154 fn pointer_pressed_inner(&mut self) -> bool {
155 self.buttons_pressed.insert(PointerButton::Primary);
157
158 let hits = self.renderer.scene().hit_test(self.cursor.0, self.cursor.1);
166 if hits.is_empty() {
167 self.hit_path_tracker.remove_path(PointerId::PRIMARY);
168 false
169 } else {
170 let event = PointerEvent::new(
171 PointerEventKind::Down,
172 Point {
173 x: self.cursor.0,
174 y: self.cursor.1,
175 },
176 Point {
177 x: self.cursor.0,
178 y: self.cursor.1,
179 },
180 )
181 .with_buttons(self.buttons_pressed);
182
183 let mut delivered_capture_paths = Vec::new();
184 for hit in hits {
185 let node_id = hit.node_id();
186 delivered_capture_paths.push(hit.capture_path());
187 hit.dispatch(event.clone());
188 log::trace!(
189 target: "cranpose::input",
190 "dispatch {:?} node={} consumed={} stop_on_consume=true",
191 event.kind,
192 node_id,
193 event.is_consumed(),
194 );
195 if event.is_consumed() {
196 break;
197 }
198 }
199
200 self.hit_path_tracker
201 .add_hit_path(PointerId::PRIMARY, delivered_capture_paths);
202 log::trace!(
203 target: "cranpose::input",
204 "pointer_pressed_inner cached_hit_path={:?}",
205 self.hit_path_tracker.get_path(PointerId::PRIMARY),
206 );
207
208 true
209 }
210 }
211
212 pub fn pointer_released(&mut self) -> bool {
213 let _event_handler = enter_event_handler_scope();
214 let app_context = Rc::clone(&self.app_context);
215 let result = app_context
216 .enter(|| run_in_mutable_snapshot(|| self.pointer_released_inner()).unwrap_or(false));
217 if result {
218 self.mark_dirty();
219 }
220 log::trace!(target: "cranpose::input", "pointer_released -> {result}");
221 result
222 }
223
224 fn pointer_released_inner(&mut self) -> bool {
225 self.buttons_pressed.remove(PointerButton::Primary);
228 let corrected_buttons = self.buttons_pressed;
229 let targets = self.resolve_gesture_targets(PointerId::PRIMARY);
230
231 self.hit_path_tracker.remove_path(PointerId::PRIMARY);
233
234 if !targets.is_empty() {
235 let event = PointerEvent::new(
236 PointerEventKind::Up,
237 Point {
238 x: self.cursor.0,
239 y: self.cursor.1,
240 },
241 Point {
242 x: self.cursor.0,
243 y: self.cursor.1,
244 },
245 )
246 .with_buttons(corrected_buttons);
247
248 self.dispatch_targets(targets, event, false);
249 true
250 } else {
251 false
252 }
253 }
254
255 pub fn pointer_scrolled(&mut self, delta_x: f32, delta_y: f32) -> bool {
259 let _event_handler = enter_event_handler_scope();
260 let app_context = Rc::clone(&self.app_context);
261 let result = app_context.enter(|| {
262 run_in_mutable_snapshot(|| self.pointer_scrolled_inner(delta_x, delta_y))
263 .unwrap_or(false)
264 });
265 if result {
266 self.mark_dirty();
267 }
268 log::trace!(
269 target: "cranpose::input",
270 "pointer_scrolled ({delta_x:.2},{delta_y:.2}) -> {result}"
271 );
272 result
273 }
274
275 fn pointer_scrolled_inner(&mut self, delta_x: f32, delta_y: f32) -> bool {
276 if delta_x.abs() <= f32::EPSILON && delta_y.abs() <= f32::EPSILON {
277 return false;
278 }
279
280 let hits = self.renderer.scene().hit_test(self.cursor.0, self.cursor.1);
281 if hits.is_empty() {
282 return false;
283 }
284
285 let event = PointerEvent::new(
286 PointerEventKind::Scroll,
287 Point {
288 x: self.cursor.0,
289 y: self.cursor.1,
290 },
291 Point {
292 x: self.cursor.0,
293 y: self.cursor.1,
294 },
295 )
296 .with_buttons(self.buttons_pressed)
297 .with_scroll_delta(Point {
298 x: delta_x,
299 y: delta_y,
300 });
301
302 self.dispatch_targets(hits, event.clone(), true);
303
304 event.is_consumed()
305 }
306
307 pub fn cancel_gesture(&mut self) {
313 let _event_handler = enter_event_handler_scope();
314 let app_context = Rc::clone(&self.app_context);
315 let _ = app_context.enter(|| {
316 run_in_mutable_snapshot(|| {
317 self.cancel_gesture_inner();
318 })
319 });
320 }
321
322 fn cancel_gesture_inner(&mut self) {
323 let targets = self.resolve_gesture_targets(PointerId::PRIMARY);
324
325 self.hit_path_tracker.clear();
327 self.buttons_pressed = PointerButtons::NONE;
328
329 if !targets.is_empty() {
330 let event = PointerEvent::new(
331 PointerEventKind::Cancel,
332 Point {
333 x: self.cursor.0,
334 y: self.cursor.1,
335 },
336 Point {
337 x: self.cursor.0,
338 y: self.cursor.1,
339 },
340 );
341
342 self.dispatch_targets(targets, event, false);
343 }
344
345 let pos = Point {
347 x: self.cursor.0,
348 y: self.cursor.1,
349 };
350 let hovered_nodes = self.hovered_nodes.clone();
351 for node_id in hovered_nodes {
352 if let Some(target) = self.renderer.scene().find_target(node_id) {
353 let exit_event = PointerEvent::new(PointerEventKind::Exit, pos, pos);
354 self.dispatch_targets(std::iter::once(target), exit_event, false);
355 }
356 }
357 self.hovered_nodes.clear();
358 }
359
360 pub fn on_key_event(&mut self, event: &KeyEvent) -> bool {
367 let _event_handler = enter_event_handler_scope();
368 let app_context = Rc::clone(&self.app_context);
369 app_context.enter(|| self.on_key_event_inner(event))
370 }
371
372 fn on_key_event_inner(&mut self, event: &KeyEvent) -> bool {
374 use KeyEventType::KeyDown;
375
376 if event.event_type == KeyDown && event.modifiers.command_or_ctrl() {
378 #[cfg(all(
380 feature = "clipboard-native",
381 not(target_arch = "wasm32"),
382 not(target_os = "android"),
383 not(target_os = "ios")
384 ))]
385 {
386 match event.key_code {
387 KeyCode::C => {
389 let text = self.on_copy_inner();
391 if let (Some(text), Some(clipboard)) = (text, self.clipboard.as_mut()) {
392 let _ = clipboard.set_text(&text);
393 return true;
394 }
395 }
396 KeyCode::X => {
398 let text = self.on_cut_inner();
400 if let (Some(text), Some(clipboard)) = (text, self.clipboard.as_mut()) {
401 let _ = clipboard.set_text(&text);
402 self.mark_dirty();
403 self.request_layout_pass();
404 return true;
405 }
406 }
407 KeyCode::V => {
409 let text = self.clipboard.as_mut().and_then(|cb| cb.get_text().ok());
411 if let Some(text) = text {
412 if self.on_paste_inner(&text) {
413 return true;
414 }
415 }
416 }
417 _ => {}
418 }
419 }
420 }
421
422 if !cranpose_ui::text_field_focus::has_focused_field() {
424 return false;
425 }
426
427 let handled = run_in_mutable_snapshot(|| {
431 cranpose_ui::text_field_focus::dispatch_key_event(event)
434 })
435 .unwrap_or(false);
436
437 if handled {
438 self.mark_dirty();
440 self.request_layout_pass();
441 }
442
443 handled
444 }
445
446 pub fn on_paste(&mut self, text: &str) -> bool {
450 let _event_handler = enter_event_handler_scope();
451 let app_context = Rc::clone(&self.app_context);
452 app_context.enter(|| self.on_paste_inner(text))
453 }
454
455 fn on_paste_inner(&mut self, text: &str) -> bool {
456 let handled =
460 run_in_mutable_snapshot(|| cranpose_ui::text_field_focus::dispatch_paste(text))
461 .unwrap_or(false);
462
463 if handled {
464 self.mark_dirty();
465 self.request_layout_pass();
466 }
467
468 handled
469 }
470
471 pub fn on_copy(&mut self) -> Option<String> {
475 let app_context = Rc::clone(&self.app_context);
476 app_context.enter(|| self.on_copy_inner())
477 }
478
479 fn on_copy_inner(&mut self) -> Option<String> {
480 cranpose_ui::text_field_focus::dispatch_copy()
482 }
483
484 pub fn on_cut(&mut self) -> Option<String> {
488 let _event_handler = enter_event_handler_scope();
489 let app_context = Rc::clone(&self.app_context);
490 app_context.enter(|| self.on_cut_inner())
491 }
492
493 fn on_cut_inner(&mut self) -> Option<String> {
494 let text =
495 run_in_mutable_snapshot(cranpose_ui::text_field_focus::dispatch_cut).unwrap_or(None);
496
497 if text.is_some() {
498 self.mark_dirty();
499 self.request_layout_pass();
500 }
501
502 text
503 }
504
505 #[cfg(all(
509 feature = "clipboard-native",
510 target_os = "linux",
511 not(target_arch = "wasm32")
512 ))]
513 pub fn set_primary_selection(&mut self, text: &str) {
514 use arboard::{LinuxClipboardKind, SetExtLinux};
515 if let Some(ref mut clipboard) = self.clipboard {
516 let result = clipboard
517 .set()
518 .clipboard(LinuxClipboardKind::Primary)
519 .text(text.to_string());
520 if let Err(e) = result {
521 log::debug!("Primary selection set failed: {:?}", e);
523 }
524 }
525 }
526
527 #[cfg(not(all(
528 feature = "clipboard-native",
529 target_os = "linux",
530 not(target_arch = "wasm32")
531 )))]
532 pub fn set_primary_selection(&mut self, _text: &str) {}
533
534 #[cfg(all(
537 feature = "clipboard-native",
538 target_os = "linux",
539 not(target_arch = "wasm32")
540 ))]
541 pub fn get_primary_selection(&mut self) -> Option<String> {
542 use arboard::{GetExtLinux, LinuxClipboardKind};
543 if let Some(ref mut clipboard) = self.clipboard {
544 clipboard
545 .get()
546 .clipboard(LinuxClipboardKind::Primary)
547 .text()
548 .ok()
549 } else {
550 None
551 }
552 }
553
554 #[cfg(not(all(
555 feature = "clipboard-native",
556 target_os = "linux",
557 not(target_arch = "wasm32")
558 )))]
559 pub fn get_primary_selection(&mut self) -> Option<String> {
560 None
561 }
562
563 pub fn sync_selection_to_primary(&mut self) {
566 #[cfg(all(target_os = "linux", not(target_arch = "wasm32")))]
567 {
568 if let Some(text) = self.on_copy() {
569 self.set_primary_selection(&text);
570 }
571 }
572 }
573
574 pub fn on_ime_preedit(&mut self, text: &str, cursor: Option<(usize, usize)>) -> bool {
582 let _event_handler = enter_event_handler_scope();
583 let app_context = Rc::clone(&self.app_context);
584 app_context.enter(|| self.on_ime_preedit_inner(text, cursor))
585 }
586
587 fn on_ime_preedit_inner(&mut self, text: &str, cursor: Option<(usize, usize)>) -> bool {
588 let handled = run_in_mutable_snapshot(|| {
590 cranpose_ui::text_field_focus::dispatch_ime_preedit(text, cursor)
591 })
592 .unwrap_or(false);
593
594 if handled {
595 self.mark_dirty();
596 self.request_layout_pass();
598 }
599
600 handled
601 }
602
603 pub fn on_ime_delete_surrounding(&mut self, before_bytes: usize, after_bytes: usize) -> bool {
606 let _event_handler = enter_event_handler_scope();
607 let app_context = Rc::clone(&self.app_context);
608 app_context.enter(|| self.on_ime_delete_surrounding_inner(before_bytes, after_bytes))
609 }
610
611 fn on_ime_delete_surrounding_inner(&mut self, before_bytes: usize, after_bytes: usize) -> bool {
612 let handled = run_in_mutable_snapshot(|| {
613 cranpose_ui::text_field_focus::dispatch_delete_surrounding(before_bytes, after_bytes)
614 })
615 .unwrap_or(false);
616
617 if handled {
618 self.mark_dirty();
619 self.request_layout_pass();
620 }
621
622 handled
623 }
624}