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 let mut applier = self.composition.applier_mut();
47 for target in targets {
48 let node_id = target.node_id();
49 target.dispatch_with_applier(&mut applier, event.clone());
50 log::trace!(
51 target: "cranpose::input",
52 "dispatch {:?} node={} consumed={} stop_on_consume={}",
53 event.kind,
54 node_id,
55 event.is_consumed(),
56 stop_on_consume,
57 );
58 if stop_on_consume && event.is_consumed() {
59 break;
60 }
61 }
62 }
63
64 pub fn set_cursor(&mut self, x: f32, y: f32) -> bool {
65 let _event_handler = enter_event_handler_scope();
66 let app_context = Rc::clone(&self.app_context);
67 let result = app_context
68 .enter(|| run_in_mutable_snapshot(|| self.set_cursor_inner(x, y)).unwrap_or(false));
69 if result {
70 self.mark_dirty();
71 }
72 log::trace!(
73 target: "cranpose::input",
74 "set_cursor ({x:.2},{y:.2}) -> {result}"
75 );
76 result
77 }
78
79 fn set_cursor_inner(&mut self, x: f32, y: f32) -> bool {
80 self.cursor = (x, y);
81
82 if self.buttons_pressed != PointerButtons::NONE {
86 if self.hit_path_tracker.has_path(PointerId::PRIMARY) {
87 let targets = self.resolve_gesture_targets(PointerId::PRIMARY);
88 if !targets.is_empty() {
89 let event =
90 PointerEvent::new(PointerEventKind::Move, Point { x, y }, Point { x, y })
91 .with_buttons(self.buttons_pressed);
92 self.dispatch_targets(targets, event, false);
93 return true;
94 }
95
96 return false;
97 }
98
99 return false;
102 }
103
104 let hits = self.renderer.scene().hit_test(x, y);
107 let new_ids: Vec<NodeId> = hits.iter().map(|h| h.node_id()).collect();
108
109 let pos = Point { x, y };
111 let previously_hovered = self.hovered_nodes.clone();
112 for old_id in previously_hovered {
113 if !new_ids.contains(&old_id) {
114 if let Some(target) = self.renderer.scene().find_target(old_id) {
115 let exit_event = PointerEvent::new(PointerEventKind::Exit, pos, pos)
116 .with_buttons(self.buttons_pressed);
117 self.dispatch_targets(std::iter::once(target), exit_event, false);
118 }
119 }
120 }
121
122 for hit in &hits {
124 if !self.hovered_nodes.contains(&hit.node_id()) {
125 let enter_event = PointerEvent::new(PointerEventKind::Enter, pos, pos)
126 .with_buttons(self.buttons_pressed);
127 self.dispatch_targets(std::iter::once(hit.clone()), enter_event, false);
128 }
129 }
130
131 self.hovered_nodes = new_ids;
132
133 if !hits.is_empty() {
134 let event = PointerEvent::new(PointerEventKind::Move, pos, pos)
135 .with_buttons(self.buttons_pressed);
136 self.dispatch_targets(hits, event, true);
137 true
138 } else {
139 false
140 }
141 }
142
143 pub fn pointer_pressed(&mut self) -> bool {
144 let _event_handler = enter_event_handler_scope();
145 let app_context = Rc::clone(&self.app_context);
146 let result = app_context
147 .enter(|| run_in_mutable_snapshot(|| self.pointer_pressed_inner()).unwrap_or(false));
148 if result {
149 self.mark_dirty();
150 }
151 log::trace!(target: "cranpose::input", "pointer_pressed -> {result}");
152 result
153 }
154
155 fn pointer_pressed_inner(&mut self) -> bool {
156 self.buttons_pressed.insert(PointerButton::Primary);
158
159 let hits = self.renderer.scene().hit_test(self.cursor.0, self.cursor.1);
167 if hits.is_empty() {
168 self.hit_path_tracker.remove_path(PointerId::PRIMARY);
169 false
170 } else {
171 let event = PointerEvent::new(
172 PointerEventKind::Down,
173 Point {
174 x: self.cursor.0,
175 y: self.cursor.1,
176 },
177 Point {
178 x: self.cursor.0,
179 y: self.cursor.1,
180 },
181 )
182 .with_buttons(self.buttons_pressed);
183
184 let mut delivered_capture_paths = Vec::new();
185 let mut applier = self.composition.applier_mut();
186 for hit in hits {
187 let node_id = hit.node_id();
188 delivered_capture_paths.push(hit.capture_path());
189 hit.dispatch_with_applier(&mut applier, event.clone());
190 log::trace!(
191 target: "cranpose::input",
192 "dispatch {:?} node={} consumed={} stop_on_consume=true",
193 event.kind,
194 node_id,
195 event.is_consumed(),
196 );
197 if event.is_consumed() {
198 break;
199 }
200 }
201
202 self.hit_path_tracker
203 .add_hit_path(PointerId::PRIMARY, delivered_capture_paths);
204 log::trace!(
205 target: "cranpose::input",
206 "pointer_pressed_inner cached_hit_path={:?}",
207 self.hit_path_tracker.get_path(PointerId::PRIMARY),
208 );
209
210 true
211 }
212 }
213
214 pub fn pointer_released(&mut self) -> bool {
215 let _event_handler = enter_event_handler_scope();
216 let app_context = Rc::clone(&self.app_context);
217 let result = app_context
218 .enter(|| run_in_mutable_snapshot(|| self.pointer_released_inner()).unwrap_or(false));
219 if result {
220 self.mark_dirty();
221 }
222 log::trace!(target: "cranpose::input", "pointer_released -> {result}");
223 result
224 }
225
226 fn pointer_released_inner(&mut self) -> bool {
227 self.buttons_pressed.remove(PointerButton::Primary);
230 let corrected_buttons = self.buttons_pressed;
231 let targets = self.resolve_gesture_targets(PointerId::PRIMARY);
232
233 self.hit_path_tracker.remove_path(PointerId::PRIMARY);
235
236 if !targets.is_empty() {
237 let event = PointerEvent::new(
238 PointerEventKind::Up,
239 Point {
240 x: self.cursor.0,
241 y: self.cursor.1,
242 },
243 Point {
244 x: self.cursor.0,
245 y: self.cursor.1,
246 },
247 )
248 .with_buttons(corrected_buttons);
249
250 self.dispatch_targets(targets, event, false);
251 true
252 } else {
253 false
254 }
255 }
256
257 pub fn pointer_scrolled(&mut self, delta_x: f32, delta_y: f32) -> bool {
261 let _event_handler = enter_event_handler_scope();
262 let app_context = Rc::clone(&self.app_context);
263 let result = app_context.enter(|| {
264 run_in_mutable_snapshot(|| self.pointer_scrolled_inner(delta_x, delta_y))
265 .unwrap_or(false)
266 });
267 if result {
268 self.mark_dirty();
269 }
270 log::trace!(
271 target: "cranpose::input",
272 "pointer_scrolled ({delta_x:.2},{delta_y:.2}) -> {result}"
273 );
274 result
275 }
276
277 fn pointer_scrolled_inner(&mut self, delta_x: f32, delta_y: f32) -> bool {
278 if delta_x.abs() <= f32::EPSILON && delta_y.abs() <= f32::EPSILON {
279 return false;
280 }
281
282 let hits = self.renderer.scene().hit_test(self.cursor.0, self.cursor.1);
283 if hits.is_empty() {
284 return false;
285 }
286
287 let event = PointerEvent::new(
288 PointerEventKind::Scroll,
289 Point {
290 x: self.cursor.0,
291 y: self.cursor.1,
292 },
293 Point {
294 x: self.cursor.0,
295 y: self.cursor.1,
296 },
297 )
298 .with_buttons(self.buttons_pressed)
299 .with_scroll_delta(Point {
300 x: delta_x,
301 y: delta_y,
302 });
303
304 let capture_paths = hits
305 .iter()
306 .map(|hit| hit.capture_path())
307 .collect::<Vec<_>>();
308 let targets = crate::hit_path_tracker::dispatch_order_for_paths(&capture_paths)
309 .into_iter()
310 .filter_map(|node_id| self.renderer.scene().find_target(node_id))
311 .collect::<Vec<_>>();
312
313 self.dispatch_targets(targets, event.clone(), true);
314
315 event.is_consumed()
316 }
317
318 pub fn cancel_gesture(&mut self) {
324 let _event_handler = enter_event_handler_scope();
325 let app_context = Rc::clone(&self.app_context);
326 let _ = app_context.enter(|| {
327 run_in_mutable_snapshot(|| {
328 self.cancel_gesture_inner();
329 })
330 });
331 }
332
333 fn cancel_gesture_inner(&mut self) {
334 let targets = self.resolve_gesture_targets(PointerId::PRIMARY);
335
336 self.hit_path_tracker.clear();
338 self.buttons_pressed = PointerButtons::NONE;
339
340 if !targets.is_empty() {
341 let event = PointerEvent::new(
342 PointerEventKind::Cancel,
343 Point {
344 x: self.cursor.0,
345 y: self.cursor.1,
346 },
347 Point {
348 x: self.cursor.0,
349 y: self.cursor.1,
350 },
351 );
352
353 self.dispatch_targets(targets, event, false);
354 }
355
356 let pos = Point {
358 x: self.cursor.0,
359 y: self.cursor.1,
360 };
361 let hovered_nodes = self.hovered_nodes.clone();
362 for node_id in hovered_nodes {
363 if let Some(target) = self.renderer.scene().find_target(node_id) {
364 let exit_event = PointerEvent::new(PointerEventKind::Exit, pos, pos);
365 self.dispatch_targets(std::iter::once(target), exit_event, false);
366 }
367 }
368 self.hovered_nodes.clear();
369 }
370
371 pub fn on_key_event(&mut self, event: &KeyEvent) -> bool {
378 let _event_handler = enter_event_handler_scope();
379 let app_context = Rc::clone(&self.app_context);
380 app_context.enter(|| self.on_key_event_inner(event))
381 }
382
383 fn on_key_event_inner(&mut self, event: &KeyEvent) -> bool {
385 use KeyEventType::KeyDown;
386
387 if event.event_type == KeyDown && event.modifiers.command_or_ctrl() {
389 #[cfg(all(
391 feature = "clipboard-native",
392 not(target_arch = "wasm32"),
393 not(target_os = "android"),
394 not(target_os = "ios")
395 ))]
396 {
397 match event.key_code {
398 KeyCode::C => {
400 let text = self.on_copy_inner();
402 if let (Some(text), Some(clipboard)) = (text, self.clipboard.as_mut()) {
403 let _ = clipboard.set_text(&text);
404 return true;
405 }
406 }
407 KeyCode::X => {
409 let text = self.on_cut_inner();
411 if let (Some(text), Some(clipboard)) = (text, self.clipboard.as_mut()) {
412 let _ = clipboard.set_text(&text);
413 self.mark_dirty();
414 self.request_layout_pass();
415 return true;
416 }
417 }
418 KeyCode::V => {
420 let text = self.clipboard.as_mut().and_then(|cb| cb.get_text().ok());
422 if let Some(text) = text {
423 if self.on_paste_inner(&text) {
424 return true;
425 }
426 }
427 }
428 _ => {}
429 }
430 }
431 }
432
433 if !cranpose_ui::text_field_focus::has_focused_field() {
435 return false;
436 }
437
438 let handled = run_in_mutable_snapshot(|| {
442 cranpose_ui::text_field_focus::dispatch_key_event(event)
445 })
446 .unwrap_or(false);
447
448 if handled {
449 self.mark_dirty();
451 self.request_layout_pass();
452 }
453
454 handled
455 }
456
457 pub fn on_paste(&mut self, text: &str) -> bool {
461 let _event_handler = enter_event_handler_scope();
462 let app_context = Rc::clone(&self.app_context);
463 app_context.enter(|| self.on_paste_inner(text))
464 }
465
466 fn on_paste_inner(&mut self, text: &str) -> bool {
467 let handled =
471 run_in_mutable_snapshot(|| cranpose_ui::text_field_focus::dispatch_paste(text))
472 .unwrap_or(false);
473
474 if handled {
475 self.mark_dirty();
476 self.request_layout_pass();
477 }
478
479 handled
480 }
481
482 pub fn on_copy(&mut self) -> Option<String> {
486 let app_context = Rc::clone(&self.app_context);
487 app_context.enter(|| self.on_copy_inner())
488 }
489
490 fn on_copy_inner(&mut self) -> Option<String> {
491 cranpose_ui::text_field_focus::dispatch_copy()
493 }
494
495 pub fn on_cut(&mut self) -> Option<String> {
499 let _event_handler = enter_event_handler_scope();
500 let app_context = Rc::clone(&self.app_context);
501 app_context.enter(|| self.on_cut_inner())
502 }
503
504 fn on_cut_inner(&mut self) -> Option<String> {
505 let text =
506 run_in_mutable_snapshot(cranpose_ui::text_field_focus::dispatch_cut).unwrap_or(None);
507
508 if text.is_some() {
509 self.mark_dirty();
510 self.request_layout_pass();
511 }
512
513 text
514 }
515
516 #[cfg(all(
520 feature = "clipboard-native",
521 target_os = "linux",
522 not(target_arch = "wasm32")
523 ))]
524 pub fn set_primary_selection(&mut self, text: &str) {
525 use arboard::{LinuxClipboardKind, SetExtLinux};
526 if let Some(ref mut clipboard) = self.clipboard {
527 let result = clipboard
528 .set()
529 .clipboard(LinuxClipboardKind::Primary)
530 .text(text.to_string());
531 if let Err(e) = result {
532 log::debug!("Primary selection set failed: {:?}", e);
534 }
535 }
536 }
537
538 #[cfg(not(all(
539 feature = "clipboard-native",
540 target_os = "linux",
541 not(target_arch = "wasm32")
542 )))]
543 pub fn set_primary_selection(&mut self, _text: &str) {}
544
545 #[cfg(all(
548 feature = "clipboard-native",
549 target_os = "linux",
550 not(target_arch = "wasm32")
551 ))]
552 pub fn get_primary_selection(&mut self) -> Option<String> {
553 use arboard::{GetExtLinux, LinuxClipboardKind};
554 if let Some(ref mut clipboard) = self.clipboard {
555 clipboard
556 .get()
557 .clipboard(LinuxClipboardKind::Primary)
558 .text()
559 .ok()
560 } else {
561 None
562 }
563 }
564
565 #[cfg(not(all(
566 feature = "clipboard-native",
567 target_os = "linux",
568 not(target_arch = "wasm32")
569 )))]
570 pub fn get_primary_selection(&mut self) -> Option<String> {
571 None
572 }
573
574 pub fn sync_selection_to_primary(&mut self) {
577 #[cfg(all(target_os = "linux", not(target_arch = "wasm32")))]
578 {
579 if let Some(text) = self.on_copy() {
580 self.set_primary_selection(&text);
581 }
582 }
583 }
584
585 pub fn on_ime_preedit(&mut self, text: &str, cursor: Option<(usize, usize)>) -> bool {
593 let _event_handler = enter_event_handler_scope();
594 let app_context = Rc::clone(&self.app_context);
595 app_context.enter(|| self.on_ime_preedit_inner(text, cursor))
596 }
597
598 fn on_ime_preedit_inner(&mut self, text: &str, cursor: Option<(usize, usize)>) -> bool {
599 let handled = run_in_mutable_snapshot(|| {
601 cranpose_ui::text_field_focus::dispatch_ime_preedit(text, cursor)
602 })
603 .unwrap_or(false);
604
605 if handled {
606 self.mark_dirty();
607 self.request_layout_pass();
609 }
610
611 handled
612 }
613
614 pub fn on_ime_delete_surrounding(&mut self, before_bytes: usize, after_bytes: usize) -> bool {
617 let _event_handler = enter_event_handler_scope();
618 let app_context = Rc::clone(&self.app_context);
619 app_context.enter(|| self.on_ime_delete_surrounding_inner(before_bytes, after_bytes))
620 }
621
622 fn on_ime_delete_surrounding_inner(&mut self, before_bytes: usize, after_bytes: usize) -> bool {
623 let handled = run_in_mutable_snapshot(|| {
624 cranpose_ui::text_field_focus::dispatch_delete_surrounding(before_bytes, after_bytes)
625 })
626 .unwrap_or(false);
627
628 if handled {
629 self.mark_dirty();
630 self.request_layout_pass();
631 }
632
633 handled
634 }
635}