use std::fmt::Debug;
use cranpose_core::{enter_event_handler_scope, run_in_mutable_snapshot};
use cranpose_render_common::Renderer;
use cranpose_ui::{FocusDirection, Key, KeyEvent, KeyEventType, request_render_invalidation};
use super::key_activation::plain_key_down;
use crate::SurfaceMut;
#[expect(clippy::multiple_inherent_impl, reason = "one file per shell concern")]
impl<R> SurfaceMut<'_, R>
where
R: Renderer,
R::Error: Debug,
{
pub(super) fn on_focus_key(&mut self, event: &KeyEvent) -> bool {
if !plain_key_down(event) || event.key() != Key::Tab {
return false;
}
self.on_group_navigation_key(event)
}
fn on_group_navigation_key(&mut self, event: &KeyEvent) -> bool {
let focused = cranpose_ui::active_focus_target();
let target = self.with_layout_tree(|tree| {
cranpose_ui::keyboard_focus_target(tree?, focused, event.key(), event.modifiers().shift)
});
let Some(target) = target else {
return false;
};
let moved =
run_in_mutable_snapshot(|| cranpose_ui::request_focus_from_platform(target.node_id))
.unwrap_or(false);
if moved {
self.mark_dirty();
self.note_focus_moved_by_keyboard(true);
if target.activate {
self.accessibility_activate(target.node_id, None);
}
}
moved
}
pub(super) fn on_activation_key(&mut self, event: &KeyEvent) -> bool {
if !matches!(
event.key(),
Key::Enter | Key::NumPadEnter | Key::Spacebar | Key::DirectionCenter
) || cranpose_ui::text_field_focus::has_focused_field()
{
return false;
}
if event.type_() == KeyEventType::KeyUp {
return self.release_by_key(event.key());
}
if !plain_key_down(event) {
return false;
}
let Some(focused) = cranpose_ui::active_focus_target() else {
return false;
};
if event.native_key_event().repeat_count > 0 {
return true;
}
let pressed = self.press_by_key(focused, event.key());
self.note_focus_moved_by_keyboard(true);
pressed
}
pub(super) fn on_field_dpad_key(&mut self, event: &KeyEvent) -> bool {
if !self.surface().dpad_navigation || !cranpose_ui::text_field_focus::has_focused_field() {
return false;
}
match event.key() {
Key::DirectionCenter => {
if event.type_() == KeyEventType::KeyDown
&& event.native_key_event().repeat_count == 0
{
cranpose_ui::text_field_focus::show_keyboard();
}
true
}
Key::DirectionUp | Key::DirectionDown | Key::DirectionLeft | Key::DirectionRight => {
self.on_navigation_key(event)
}
_ => false,
}
}
pub(super) fn on_unconsumed_key(&mut self, event: &KeyEvent) -> bool {
if self.on_navigation_key(event) {
return true;
}
if cranpose_ui::text_field_focus::has_focused_field() {
return false;
}
let handled = run_in_mutable_snapshot(|| cranpose_ui::dispatch_unhandled_key_event(event))
.unwrap_or(false);
if handled {
self.mark_dirty();
self.shell.app.request_layout_pass();
}
handled
}
fn on_navigation_key(&mut self, event: &KeyEvent) -> bool {
let direction = match event.key() {
Key::DirectionLeft => Some(FocusDirection::Left),
Key::DirectionRight => Some(FocusDirection::Right),
Key::DirectionUp => Some(FocusDirection::Up),
Key::DirectionDown => Some(FocusDirection::Down),
Key::MoveHome | Key::MoveEnd => None,
_ => return false,
};
if !plain_key_down(event)
|| (cranpose_ui::text_field_focus::has_focused_field()
&& !self.surface().dpad_navigation)
{
return false;
}
let frame = Some(self.surface().frames);
if event.native_key_event().repeat_count > 0 && self.surface().key_moved_focus_at == frame {
return true;
}
let moved = self.on_group_navigation_key(event)
|| direction.is_some_and(|direction| {
self.move_focus_by_arrow(direction) || self.scroll_by_arrow(direction)
});
if moved {
self.surface_mut().key_moved_focus_at = frame;
}
moved
}
fn scroll_by_arrow(&mut self, direction: FocusDirection) -> bool {
let Some(focused) = cranpose_ui::active_focus_target() else {
return false;
};
let vertical = matches!(direction, FocusDirection::Up | FocusDirection::Down);
let forward = matches!(direction, FocusDirection::Down | FocusDirection::Right);
let container = {
let (app, surface) = self.parts();
surface.semantics_tree_for_input(app).and_then(|tree| {
let root = tree.root();
crate::focus_reveal::scroll_ancestors(root, focused)?
.into_iter()
.find_map(|ancestor| {
let node = crate::focus_reveal::semantics_of(root, ancestor)?;
let details = node.details();
let range = if vertical {
details.vertical_scroll
} else {
details.horizontal_scroll
}?;
let can_move = if forward != range.reverse {
range.can_scroll_forward()
} else {
range.can_scroll_backward()
};
let action = details.scroll_by.clone()?;
can_move.then_some((ancestor, action, range.reverse))
})
})
};
let Some((ancestor, action, reverse)) = container else {
return false;
};
let Some(viewport) =
self.with_layout_tree(|tree| crate::focus_reveal::bounds_of(tree?.root(), ancestor))
else {
return false;
};
let half = if vertical {
viewport.height
} else {
viewport.width
} * 0.5;
let delta = if forward != reverse { half } else { -half };
let (dx, dy) = if vertical { (0.0, delta) } else { (delta, 0.0) };
let moved = run_in_mutable_snapshot(|| action.invoke(dx, dy)).unwrap_or(false);
if moved {
self.mark_dirty();
self.shell.app.request_layout_pass();
}
moved
}
pub(crate) fn keep_focus(&mut self) {
if cranpose_ui::active_focus_target().is_none() && cranpose_ui::has_focus_targets() {
self.move_focus_by_arrow(FocusDirection::Down);
}
}
#[inline(never)]
fn move_focus_by_arrow(&mut self, direction: FocusDirection) -> bool {
let target = self.with_layout_tree(|layout_tree| {
cranpose_ui::arrow_focus_target(layout_tree?, direction)
});
let Some(target) = target else {
return false;
};
let request = if self.surface().dpad_navigation {
cranpose_ui::request_focus_without_keyboard
} else {
cranpose_ui::request_focus_from_platform
};
let moved = run_in_mutable_snapshot(|| request(target)).unwrap_or(false);
if moved {
self.mark_dirty();
self.note_focus_moved_by_keyboard(true);
}
moved
}
pub(super) fn note_focus_moved_by_keyboard(&mut self, keyboard: bool) {
if cranpose_ui::set_keyboard_focus_visible(keyboard) {
request_render_invalidation();
self.mark_dirty();
}
}
pub(super) fn on_escape_key(&mut self, event: &KeyEvent) -> bool {
if event.type_() != KeyEventType::KeyDown || event.key() != Key::Escape {
return false;
}
self.dismiss_top_modal_in_context()
}
pub fn dismiss_top_modal(&mut self) -> bool {
let _event_handler = enter_event_handler_scope();
let _app_context = self.shell.app.app_context.enter_scope();
self.dismiss_top_modal_in_context()
}
fn dismiss_top_modal_in_context(&mut self) -> bool {
let closed = run_in_mutable_snapshot(|| {
if cranpose_ui::modal_depth() > 0 {
cranpose_ui::dispatch_modal_back()
} else {
cranpose_ui::dismiss_top_popup()
}
})
.unwrap_or(false);
if closed {
self.mark_dirty();
}
closed
}
pub fn move_focus_in_context(&mut self, direction: FocusDirection) -> bool {
let order = self.with_layout_tree(|layout_tree| {
layout_tree
.map(cranpose_ui::collect_focus_order)
.unwrap_or_default()
});
self.move_focus_in_order(order, direction)
}
fn move_focus_in_order(
&mut self,
order: Vec<cranpose_ui::FocusEntry>,
direction: FocusDirection,
) -> bool {
cranpose_ui::set_focus_order(order);
let moved = run_in_mutable_snapshot(|| cranpose_ui::FocusManager.move_focus(direction))
.unwrap_or(false);
if moved {
self.mark_dirty();
self.note_focus_moved_by_keyboard(true);
}
moved
}
}