use crate::model::component_model::ComponentModel;
use crate::model::components_model::SurfaceComponentsModel;
use crate::protocol::common_types::ChildList;
pub const FOCUSABLE_TYPES: &[&str] = &[
"Button",
"TextField",
"CheckBox",
"Slider",
"ChoicePicker",
"DateTimeInput",
"AudioPlayer",
];
pub struct FocusManager {
pub focusable_ids: Vec<String>,
current_index: usize,
}
impl Default for FocusManager {
fn default() -> Self {
Self::new()
}
}
impl FocusManager {
pub fn new() -> Self {
Self {
focusable_ids: Vec::new(),
current_index: 0,
}
}
pub fn rebuild_from_components(&mut self, components: &SurfaceComponentsModel) {
let previously_focused = self.focused_id().map(|s| s.to_string());
self.focusable_ids.clear();
self.current_index = 0;
let child_ids = collect_all_child_ids(components);
let all = components.all();
let mut roots: Vec<&ComponentModel> = all
.values()
.filter(|c| !child_ids.contains(c.id.as_str()))
.collect();
roots.sort_by(|a, b| a.id.cmp(&b.id));
for root in &roots {
self.collect_focusable_depth_first(root, components);
}
if let Some(ref prev_id) = previously_focused {
if let Some(idx) = self.focusable_ids.iter().position(|id| id == prev_id) {
self.current_index = idx;
}
}
}
pub fn focus_next(&mut self) {
if self.focusable_ids.is_empty() {
return;
}
self.current_index = (self.current_index + 1) % self.focusable_ids.len();
}
pub fn focus_prev(&mut self) {
if self.focusable_ids.is_empty() {
return;
}
if self.current_index == 0 {
self.current_index = self.focusable_ids.len() - 1;
} else {
self.current_index -= 1;
}
}
pub fn is_focused(&self, id: &str) -> bool {
self.focusable_ids
.get(self.current_index)
.is_some_and(|focused| focused == id)
}
pub fn focused_id(&self) -> Option<&str> {
self.focusable_ids.get(self.current_index).map(|s| s.as_str())
}
pub fn reset(&mut self) {
self.focusable_ids.clear();
self.current_index = 0;
}
fn collect_focusable_depth_first(
&mut self,
component: &ComponentModel,
components: &SurfaceComponentsModel,
) {
if FOCUSABLE_TYPES.contains(&component.component_type.as_str()) {
self.focusable_ids.push(component.id.clone());
}
if let Some(child_ids) = component.children() {
match child_ids {
ChildList::Static(ids) => {
for cid in &ids {
if let Some(child) = components.get(cid) {
self.collect_focusable_depth_first(child, components);
}
}
}
ChildList::Template { .. } => {
}
}
}
if let Some(single_id) = component.child() {
if let Some(child) = components.get(&single_id) {
self.collect_focusable_depth_first(child, components);
}
}
}
}
fn collect_all_child_ids(components: &SurfaceComponentsModel) -> std::collections::HashSet<String> {
let mut ids = std::collections::HashSet::new();
for component in components.all().values() {
if let Some(ChildList::Static(children)) = component.children() {
for cid in children {
ids.insert(cid.clone());
}
}
if let Some(single_id) = component.child() {
ids.insert(single_id);
}
}
ids
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn make_component(id: &str, component_type: &str) -> ComponentModel {
ComponentModel::from_json(&json!({
"id": id,
"component": component_type,
}))
.unwrap()
}
fn make_container(id: &str, component_type: &str, child_ids: &[&str]) -> ComponentModel {
ComponentModel::from_json(&json!({
"id": id,
"component": component_type,
"children": child_ids,
}))
.unwrap()
}
#[test]
fn collects_focusable_in_dfs_order() {
let mut surface = SurfaceComponentsModel::new();
surface.upsert(make_container("col", "Column", &["btn1", "row"]));
surface.upsert(make_component("btn1", "Button"));
surface.upsert(make_container("row", "Row", &["tf1", "btn2"]));
surface.upsert(make_component("tf1", "TextField"));
surface.upsert(make_component("btn2", "Button"));
let mut fm = FocusManager::new();
fm.rebuild_from_components(&surface);
assert_eq!(fm.focusable_ids, vec!["btn1", "tf1", "btn2"]);
}
#[test]
fn focus_cycles_forward() {
let mut fm = FocusManager::new();
fm.focusable_ids = vec!["a".into(), "b".into(), "c".into()];
assert_eq!(fm.focused_id(), Some("a"));
fm.focus_next();
assert_eq!(fm.focused_id(), Some("b"));
fm.focus_next();
assert_eq!(fm.focused_id(), Some("c"));
fm.focus_next();
assert_eq!(fm.focused_id(), Some("a")); }
#[test]
fn focus_cycles_backward() {
let mut fm = FocusManager::new();
fm.focusable_ids = vec!["a".into(), "b".into(), "c".into()];
fm.focus_prev();
assert_eq!(fm.focused_id(), Some("c")); fm.focus_prev();
assert_eq!(fm.focused_id(), Some("b"));
}
#[test]
fn is_focused_checks_current() {
let mut fm = FocusManager::new();
fm.focusable_ids = vec!["a".into(), "b".into()];
assert!(fm.is_focused("a"));
assert!(!fm.is_focused("b"));
fm.focus_next();
assert!(!fm.is_focused("a"));
assert!(fm.is_focused("b"));
}
#[test]
fn reset_clears_everything() {
let mut fm = FocusManager::new();
fm.focusable_ids = vec!["a".into()];
fm.focus_next();
fm.reset();
assert!(fm.focused_id().is_none());
assert!(fm.focusable_ids.is_empty());
}
#[test]
fn empty_surface_yields_no_focus() {
let surface = SurfaceComponentsModel::new();
let mut fm = FocusManager::new();
fm.rebuild_from_components(&surface);
assert!(fm.focused_id().is_none());
fm.focus_next(); assert!(fm.focused_id().is_none());
}
#[test]
fn rebuild_preserves_focus_if_still_present() {
let mut surface = SurfaceComponentsModel::new();
surface.upsert(make_container("col", "Column", &["btn1", "btn2"]));
surface.upsert(make_component("btn1", "Button"));
surface.upsert(make_component("btn2", "Button"));
let mut fm = FocusManager::new();
fm.rebuild_from_components(&surface);
fm.focus_next(); assert_eq!(fm.focused_id(), Some("btn2"));
fm.rebuild_from_components(&surface);
assert_eq!(fm.focused_id(), Some("btn2"));
}
#[test]
fn non_focusable_types_are_skipped() {
let mut surface = SurfaceComponentsModel::new();
surface.upsert(make_component("txt", "Text"));
surface.upsert(make_component("btn", "Button"));
let mut fm = FocusManager::new();
fm.rebuild_from_components(&surface);
assert_eq!(fm.focusable_ids, vec!["btn"]);
}
}