Skip to main content

cranpose_app_shell/shell_input/
key_activation.rs

1use std::{fmt::Debug, rc::Rc};
2
3use cranpose_core::{NodeId, enter_event_handler_scope, run_in_mutable_snapshot};
4use cranpose_foundation::PointerEventKind;
5use cranpose_render_common::Renderer;
6use cranpose_ui::{Key, KeyEvent, KeyEventType, LayoutBox};
7use cranpose_ui_graphics::{Point, Rect};
8
9use crate::{PointerEventTime, SurfaceMut};
10
11// A press a click key holds on a control until the key comes up.
12pub(crate) struct KeyPress {
13    key: Key,
14    target: PressTarget,
15}
16
17enum PressTarget {
18    Pointer(Press),
19    // A control that clicks through a semantics action or by taking focus
20    // for editing, which happens on the release.
21    Action(Action),
22}
23
24enum Action {
25    Direct(cranpose_foundation::SemanticsCustomAction),
26    Edit(NodeId),
27}
28
29// Where a pointer press lands: the position on screen and in the control,
30// and the control's pointer handlers.
31struct Press {
32    position: Point,
33    local: Point,
34    handlers: Rc<cranpose_ui::ModifierNodeSlices>,
35}
36
37pub(super) enum ActivationTarget {
38    Direct(cranpose_foundation::SemanticsCustomAction),
39    Edit(NodeId),
40    Pointer(NodeId, Option<Rect>),
41}
42
43pub(super) fn activation_target(
44    node: &cranpose_ui::SemanticsNode,
45    node_id: NodeId,
46    canvas_key: Option<u64>,
47) -> Option<ActivationTarget> {
48    if node.hidden {
49        return None;
50    }
51    if node.node_id != node_id {
52        return node
53            .children
54            .iter()
55            .find_map(|child| activation_target(child, node_id, canvas_key));
56    }
57    if !node.enabled {
58        return None;
59    }
60    if let Some(key) = canvas_key {
61        let child = node
62            .details()
63            .canvas_children
64            .iter()
65            .find(|child| child.key == key)?;
66        return (child.enabled && child.clickable)
67            .then_some(ActivationTarget::Pointer(node_id, Some(child.bounds)));
68    }
69    if let Some(action) = &node.on_click {
70        return Some(ActivationTarget::Direct(action.clone()));
71    }
72    if node.details().editable_text && node.focusable {
73        return Some(ActivationTarget::Edit(node_id));
74    }
75    node.actions.first().map(|action| match action {
76        cranpose_ui::SemanticsAction::Click { handler } => {
77            ActivationTarget::Pointer(handler.node_id(), None)
78        }
79    })
80}
81
82pub(super) fn activation_input(
83    root: &LayoutBox,
84    node_id: NodeId,
85    canvas_bounds: Option<Rect>,
86) -> Option<(Point, Point, Rc<cranpose_ui::ModifierNodeSlices>)> {
87    if root.node_id != node_id {
88        return root
89            .children
90            .iter()
91            .find_map(|child| activation_input(child, node_id, canvas_bounds));
92    }
93    let bounds = canvas_bounds.map_or(root.rect, |bounds| Rect {
94        x: root.rect.x + bounds.x,
95        y: root.rect.y + bounds.y,
96        ..bounds
97    });
98    let position = Point {
99        x: bounds.x + bounds.width * 0.5,
100        y: bounds.y + bounds.height * 0.5,
101    };
102    let local = Point {
103        x: position.x - root.rect.x,
104        y: position.y - root.rect.y,
105    };
106    (bounds.width > 0.0 && bounds.height > 0.0 && position.x.is_finite() && position.y.is_finite())
107        .then(|| (position, local, Rc::clone(&root.node_data.modifier_slices)))
108}
109
110pub(super) fn plain_key_down(event: &KeyEvent) -> bool {
111    event.type_() == KeyEventType::KeyDown
112        && !event.modifiers().ctrl
113        && !event.modifiers().meta
114        && !event.modifiers().alt
115}
116
117#[expect(clippy::multiple_inherent_impl, reason = "one file per shell concern")]
118impl<R> SurfaceMut<'_, R>
119where
120    R: Renderer,
121    R::Error: Debug,
122{
123    /// Activates the identified control using its current semantics and geometry.
124    ///
125    /// Hidden, disabled, removed and modal-background controls reject activation.
126    /// `canvas_key` identifies a drawn child of `node_id`. The action bypasses
127    /// overlapping controls and developer overlays without changing pointer capture.
128    pub fn accessibility_activate(&mut self, node_id: NodeId, canvas_key: Option<u64>) -> bool {
129        let event_time = self.shell.app.realtime_pointer_event_time(None);
130        let _event_handler = enter_event_handler_scope();
131        let result = {
132            let _app_context = self.shell.app.app_context.enter_scope();
133            run_in_mutable_snapshot(|| self.activate_node(node_id, canvas_key, event_time))
134                .unwrap_or(false)
135        };
136        if result {
137            self.mark_dirty();
138        }
139        result
140    }
141
142    fn activate_node(
143        &mut self,
144        node_id: NodeId,
145        canvas_key: Option<u64>,
146        event_time: PointerEventTime,
147    ) -> bool {
148        match self.press_target(node_id, canvas_key) {
149            Some(PressTarget::Pointer(press)) => {
150                for kind in [PointerEventKind::Down, PointerEventKind::Up] {
151                    self.send_press(&press, kind, event_time);
152                }
153                true
154            }
155            Some(PressTarget::Action(target)) => self.run_action(target),
156            None => false,
157        }
158    }
159
160    // Presses the control `node_id` as `key` goes down, as Compose's
161    // clickable does: a control a pointer presses takes the press now and
162    // the release, which clicks it, when the key comes up, so it shows its
163    // pressed look and a held key long-presses it. A control that clicks
164    // through a semantics action clicks on the release.
165    pub(super) fn press_by_key(&mut self, node_id: NodeId, key: Key) -> bool {
166        let event_time = self.shell.app.realtime_pointer_event_time(None);
167        let _event_handler = enter_event_handler_scope();
168        let _app_context = self.shell.app.app_context.enter_scope();
169        run_in_mutable_snapshot(|| {
170            self.end_key_press(PointerEventKind::Cancel, event_time);
171            let Some(target) = self.press_target(node_id, None) else {
172                return false;
173            };
174            if let PressTarget::Pointer(press) = &target {
175                self.send_press(press, PointerEventKind::Down, event_time);
176            }
177            self.surface_mut().key_press = Some(KeyPress { key, target });
178            self.mark_dirty();
179            true
180        })
181        .unwrap_or(false)
182    }
183
184    // Releases the press `key` made, which clicks the control. Answers
185    // whether `key` held one.
186    pub(super) fn release_by_key(&mut self, key: Key) -> bool {
187        if self
188            .surface()
189            .key_press
190            .as_ref()
191            .is_none_or(|press| press.key != key)
192        {
193            return false;
194        }
195        let event_time = self.shell.app.realtime_pointer_event_time(None);
196        let _event_handler = enter_event_handler_scope();
197        let _app_context = self.shell.app.app_context.enter_scope();
198        run_in_mutable_snapshot(|| self.end_key_press(PointerEventKind::Up, event_time))
199            .unwrap_or(false)
200    }
201
202    fn end_key_press(&mut self, kind: PointerEventKind, event_time: PointerEventTime) -> bool {
203        let Some(press) = self.surface_mut().key_press.take() else {
204            return false;
205        };
206        self.mark_dirty();
207        match press.target {
208            PressTarget::Pointer(press) => {
209                self.send_press(&press, kind, event_time);
210                true
211            }
212            PressTarget::Action(target) => kind == PointerEventKind::Up && self.run_action(target),
213        }
214    }
215
216    fn press_target(&mut self, node_id: NodeId, canvas_key: Option<u64>) -> Option<PressTarget> {
217        let target = {
218            let (app, surface) = self.parts();
219            surface
220                .semantics_tree_for_input(app)
221                .and_then(|tree| activation_target(tree.root(), node_id, canvas_key))?
222        };
223        let (target_id, canvas_bounds) = match target {
224            ActivationTarget::Pointer(node_id, bounds) => (node_id, bounds),
225            ActivationTarget::Direct(action) => {
226                return Some(PressTarget::Action(Action::Direct(action)));
227            }
228            ActivationTarget::Edit(node_id) => {
229                return Some(PressTarget::Action(Action::Edit(node_id)));
230            }
231        };
232        let (position, local, handlers) =
233            self.with_layout_tree(|tree| activation_input(tree?.root(), target_id, canvas_bounds))?;
234        Some(PressTarget::Pointer(Press {
235            position,
236            local,
237            handlers,
238        }))
239    }
240
241    fn send_press(&self, press: &Press, kind: PointerEventKind, event_time: PointerEventTime) {
242        let event = self.pointer_event(kind, press.local, press.position, event_time);
243        press.handlers.dispatch_pointer_event(event.clone());
244        event.finish_post_dispatch();
245    }
246
247    fn run_action(&mut self, action: Action) -> bool {
248        match action {
249            Action::Direct(action) => {
250                action.invoke();
251                true
252            }
253            Action::Edit(node_id) => {
254                self.activate();
255                cranpose_ui::request_focus_from_platform(node_id)
256            }
257        }
258    }
259}