use cranpose_core::NodeId;
use cranpose_foundation::{SemanticsConfiguration, SemanticsWidgetRole};
use cranpose_ui_graphics::Rect;
use crate::layout::{LayoutBox, LayoutTree};
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct FocusEntry {
pub node_id: NodeId,
pub rect: Rect,
}
impl FocusEntry {
pub fn center(&self) -> (f32, f32) {
(
self.rect.x + self.rect.width * 0.5,
self.rect.y + self.rect.height * 0.5,
)
}
}
pub fn set_focus_order(entries: Vec<FocusEntry>) {
crate::render_state::with_focus_dispatch(|state| state.set_focus_order(entries));
}
pub fn with_focus_order<T>(reader: impl FnOnce(&[FocusEntry]) -> T) -> T {
crate::render_state::with_focus_dispatch(|state| state.with_focus_order(reader))
}
pub fn focus_order_len() -> usize {
with_focus_order(<[FocusEntry]>::len)
}
pub fn collect_focus_order(tree: &LayoutTree) -> Vec<FocusEntry> {
let mut entries = Vec::new();
collect_from_box(focus_root(tree.root()), &mut entries);
entries
}
fn collect_from_box(layout_box: &LayoutBox, entries: &mut Vec<FocusEntry>) {
let config = crate::modifier::collect_semantics_from_modifier(&layout_box.node_data.modifier);
if config.as_ref().is_some_and(|config| config.hidden) {
return;
}
if crate::focus_dispatch::has_focus_target(layout_box.node_id)
&& takes_space(layout_box.rect)
&& config.as_ref().is_none_or(|config| config.enabled)
{
entries.push(FocusEntry {
node_id: layout_box.node_id,
rect: layout_box.rect,
});
}
for child in &layout_box.children {
collect_from_box(child, entries);
}
}
pub(crate) fn takes_space(rect: Rect) -> bool {
rect.width > 0.0
&& rect.height > 0.0
&& rect.x.is_finite()
&& rect.y.is_finite()
&& rect.width.is_finite()
&& rect.height.is_finite()
}
pub fn collect_focus_order_under(tree: &LayoutTree, node_id: NodeId) -> Vec<FocusEntry> {
let mut entries = Vec::new();
if let Some(layout_box) = find_box(focus_root(tree.root()), node_id) {
collect_from_box(layout_box, &mut entries);
}
entries
}
pub fn selectable_group_of(tree: &LayoutTree, node_id: NodeId) -> Option<NodeId> {
group_above(focus_root(tree.root()), node_id, None)
}
pub(crate) fn focus_root(root: &LayoutBox) -> &LayoutBox {
top_modal(root).unwrap_or(root)
}
fn top_modal(layout_box: &LayoutBox) -> Option<&LayoutBox> {
let config = crate::modifier::collect_semantics_from_modifier(&layout_box.node_data.modifier);
if config.as_ref().is_some_and(|config| config.hidden) {
return None;
}
layout_box
.children
.iter()
.rev()
.find_map(top_modal)
.or_else(|| {
(takes_space(layout_box.rect) && config.is_some_and(|config| config.is_modal))
.then_some(layout_box)
})
}
fn group_above(layout_box: &LayoutBox, node_id: NodeId, group: Option<NodeId>) -> Option<NodeId> {
let group = if declares_selectable_group(layout_box) {
Some(layout_box.node_id)
} else {
group
};
if layout_box.node_id == node_id {
return group;
}
layout_box
.children
.iter()
.find_map(|child| group_above(child, node_id, group))
}
fn declares_selectable_group(layout_box: &LayoutBox) -> bool {
crate::modifier::collect_semantics_from_modifier(&layout_box.node_data.modifier)
.as_ref()
.is_some_and(is_selectable_group)
}
pub(crate) fn is_selectable_group(config: &SemanticsConfiguration) -> bool {
config.selectable_group
|| matches!(
config.role,
Some(
SemanticsWidgetRole::Menu
| SemanticsWidgetRole::RadioGroup
| SemanticsWidgetRole::TabBar
)
)
}
fn find_box(layout_box: &LayoutBox, node_id: NodeId) -> Option<&LayoutBox> {
if layout_box.node_id == node_id {
return Some(layout_box);
}
layout_box
.children
.iter()
.find_map(|child| find_box(child, node_id))
}