#![forbid(unsafe_code)]
use ahash::AHashMap;
use ftui_core::event::KeyCode;
use super::indicator::FocusIndicator;
use super::spatial;
use super::{FocusGraph, FocusId, NavDirection};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FocusEvent {
FocusGained { id: FocusId },
FocusLost { id: FocusId },
FocusMoved { from: FocusId, to: FocusId },
}
#[derive(Debug, Clone)]
pub struct FocusGroup {
pub id: u32,
pub members: Vec<FocusId>,
pub wrap: bool,
pub exit_key: Option<KeyCode>,
}
impl FocusGroup {
#[must_use]
pub fn new(id: u32, members: Vec<FocusId>) -> Self {
Self {
id,
members,
wrap: true,
exit_key: None,
}
}
#[must_use]
pub fn with_wrap(mut self, wrap: bool) -> Self {
self.wrap = wrap;
self
}
#[must_use]
pub fn with_exit_key(mut self, key: KeyCode) -> Self {
self.exit_key = Some(key);
self
}
fn contains(&self, id: FocusId) -> bool {
self.members.contains(&id)
}
}
#[derive(Debug, Clone, Copy)]
pub struct FocusTrap {
pub group_id: u32,
pub return_focus: Option<FocusId>,
}
#[derive(Debug)]
pub struct FocusManager {
graph: FocusGraph,
current: Option<FocusId>,
host_focused: bool,
pending_focus_on_host_gain: Option<FocusId>,
history: Vec<FocusId>,
trap_stack: Vec<FocusTrap>,
groups: AHashMap<u32, FocusGroup>,
last_event: Option<FocusEvent>,
indicator: FocusIndicator,
focus_change_count: u64,
}
impl Default for FocusManager {
fn default() -> Self {
Self {
graph: FocusGraph::default(),
current: None,
host_focused: true,
pending_focus_on_host_gain: None,
history: Vec::new(),
trap_stack: Vec::new(),
groups: AHashMap::new(),
last_event: None,
indicator: FocusIndicator::default(),
focus_change_count: 0,
}
}
}
impl FocusManager {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn graph(&self) -> &FocusGraph {
&self.graph
}
pub fn graph_mut(&mut self) -> &mut FocusGraph {
&mut self.graph
}
#[inline]
#[must_use]
pub fn current(&self) -> Option<FocusId> {
self.current
}
#[must_use]
pub(crate) fn host_focused(&self) -> bool {
self.host_focused
}
pub(crate) fn set_host_focused(&mut self, focused: bool) {
self.host_focused = focused;
if focused {
self.pending_focus_on_host_gain = None;
}
}
#[must_use]
pub fn is_focused(&self, id: FocusId) -> bool {
self.current == Some(id)
}
pub fn focus(&mut self, id: FocusId) -> Option<FocusId> {
if !self.can_focus(id) || !self.allowed_by_trap(id) {
return None;
}
let prev = self.active_focus_target();
if prev == Some(id) {
return prev;
}
self.set_focus(id);
prev
}
pub fn blur(&mut self) -> Option<FocusId> {
let prev = self.current.take();
if let Some(id) = prev {
#[cfg(feature = "tracing")]
tracing::debug!(from_widget = id, trigger = "blur", "focus.change");
self.last_event = Some(FocusEvent::FocusLost { id });
self.focus_change_count += 1;
}
prev
}
pub fn apply_host_focus(&mut self, focused: bool) -> bool {
if !focused {
if let Some(current) = self.current {
self.pending_focus_on_host_gain = Some(current);
}
self.host_focused = false;
return self.blur().is_some();
}
self.host_focused = true;
let had_current = self.current.is_some();
if let Some(current) = self.current
&& self.can_focus(current)
&& self.allowed_by_trap(current)
{
self.pending_focus_on_host_gain = None;
return false;
}
let pending_focus = self.pending_focus_on_host_gain.take();
if let Some(id) = pending_focus
&& self.can_focus(id)
&& self.allowed_by_trap(id)
{
return self.set_focus_without_history(id);
}
if let Some(group_id) = self.active_trap_group()
&& self.focus_first_in_group_without_history(group_id)
{
return true;
}
if self.focus_first_without_history() {
return true;
}
if had_current {
return self.blur().is_some();
}
false
}
pub fn navigate(&mut self, dir: NavDirection) -> bool {
match dir {
NavDirection::Next => self.focus_next(),
NavDirection::Prev => self.focus_prev(),
_ => {
let Some(current) = self.active_focus_target() else {
return false;
};
let target = self
.graph
.navigate(current, dir)
.or_else(|| spatial::spatial_navigate(&self.graph, current, dir));
let Some(target) = target else {
return false;
};
if !self.allowed_by_trap(target) {
return false;
}
self.set_focus(target)
}
}
}
pub fn focus_next(&mut self) -> bool {
self.move_in_tab_order(true)
}
pub fn focus_prev(&mut self) -> bool {
self.move_in_tab_order(false)
}
pub fn focus_first(&mut self) -> bool {
let order = self.active_tab_order();
let Some(first) = order.first().copied() else {
return false;
};
self.set_focus(first)
}
pub fn focus_last(&mut self) -> bool {
let order = self.active_tab_order();
let Some(last) = order.last().copied() else {
return false;
};
self.set_focus(last)
}
pub fn focus_back(&mut self) -> bool {
let active_focus = self.active_focus_target();
while let Some(id) = self.history.pop() {
if active_focus == Some(id) {
continue;
}
if self.can_focus(id) && self.allowed_by_trap(id) {
if !self.host_focused {
return self.set_pending_focus_target(id);
}
let prev = self.current;
self.current = Some(id);
self.last_event = Some(match prev {
Some(from) => FocusEvent::FocusMoved { from, to: id },
None => FocusEvent::FocusGained { id },
});
self.focus_change_count += 1;
return true;
}
}
false
}
pub fn clear_history(&mut self) {
self.history.clear();
}
pub fn push_trap(&mut self, group_id: u32) -> bool {
let return_focus = if self.host_focused {
self.current
} else {
self.current.or(self.deferred_focus_target())
};
if !self.push_trap_with_return_focus(group_id, return_focus) {
#[cfg(feature = "tracing")]
tracing::warn!(group_id, "focus.trap_push rejected: group missing or empty");
return false;
}
if self.host_focused && !self.is_current_focusable_in_group(group_id) {
self.focus_first_in_group_without_history(group_id);
} else if !self.host_focused {
self.pending_focus_on_host_gain = self.group_primary_focus_target(group_id);
}
true
}
pub fn pop_trap(&mut self) -> bool {
let Some(trap) = self.trap_stack.pop() else {
return false;
};
let had_current = self.current.is_some();
#[cfg(feature = "tracing")]
tracing::debug!(
group_id = trap.group_id,
return_focus = ?trap.return_focus,
"focus.trap_pop"
);
if !self.host_focused {
self.pending_focus_on_host_gain = trap
.return_focus
.filter(|id| self.can_focus(*id) && self.allowed_by_trap(*id))
.or_else(|| {
self.active_trap_group()
.and_then(|group_id| self.group_primary_focus_target(group_id))
});
return if had_current {
self.blur().is_some()
} else {
false
};
}
if let Some(id) = trap.return_focus
&& self.can_focus(id)
&& self.allowed_by_trap(id)
{
return self.set_focus_without_history(id);
}
if let Some(active) = self.active_trap_group() {
return self.focus_first_in_group_without_history(active);
}
if trap.return_focus.is_none() {
return if had_current {
self.blur().is_some()
} else {
false
};
}
if self.focus_first_without_history() {
return true;
}
if had_current && self.current.is_some_and(|id| !self.can_focus(id)) {
return self.blur().is_some();
}
false
}
#[must_use]
pub fn is_trapped(&self) -> bool {
self.active_trap_group().is_some()
}
pub fn clear_traps(&mut self) {
self.trap_stack.clear();
}
pub fn create_group(&mut self, id: u32, members: Vec<FocusId>) {
let members = self.filter_focusable(members);
self.groups.insert(id, FocusGroup::new(id, members));
self.repair_focus_after_group_change();
}
pub(crate) fn create_group_preserving_members(&mut self, id: u32, members: Vec<FocusId>) {
let members = self.dedup_members(members);
self.groups.insert(id, FocusGroup::new(id, members));
self.repair_focus_after_group_change();
}
pub fn add_to_group(&mut self, group_id: u32, widget_id: FocusId) {
if !self.can_focus(widget_id) {
return;
}
let group = self
.groups
.entry(group_id)
.or_insert_with(|| FocusGroup::new(group_id, Vec::new()));
if !group.contains(widget_id) {
group.members.push(widget_id);
}
self.repair_focus_after_group_change();
}
pub fn remove_from_group(&mut self, group_id: u32, widget_id: FocusId) {
let Some(group) = self.groups.get_mut(&group_id) else {
return;
};
group.members.retain(|id| *id != widget_id);
self.repair_focus_after_group_change();
}
pub fn remove_group(&mut self, group_id: u32) {
if self.groups.remove(&group_id).is_none() {
return;
}
self.trap_stack.retain(|trap| trap.group_id != group_id);
self.repair_focus_after_group_change();
}
#[must_use]
pub fn focus_event(&self) -> Option<&FocusEvent> {
self.last_event.as_ref()
}
#[must_use]
pub fn take_focus_event(&mut self) -> Option<FocusEvent> {
self.last_event.take()
}
#[inline]
#[must_use]
pub fn indicator(&self) -> &FocusIndicator {
&self.indicator
}
pub fn set_indicator(&mut self, indicator: FocusIndicator) {
self.indicator = indicator;
}
#[inline]
#[must_use]
pub fn focus_change_count(&self) -> u64 {
self.focus_change_count
}
#[cfg(test)]
#[must_use]
pub(crate) fn group_count(&self) -> usize {
self.groups.len()
}
#[must_use]
pub(crate) fn has_group(&self, group_id: u32) -> bool {
self.groups.contains_key(&group_id)
}
#[must_use]
pub(crate) fn group_members(&self, group_id: u32) -> Vec<FocusId> {
self.groups
.get(&group_id)
.map(|group| group.members.clone())
.unwrap_or_default()
}
#[cfg(test)]
#[must_use]
pub(crate) fn base_trap_return_focus(&self) -> Option<Option<FocusId>> {
self.trap_stack.first().map(|trap| trap.return_focus)
}
#[must_use]
pub(crate) fn deferred_focus_target(&self) -> Option<FocusId> {
if let Some(id) = self.active_focus_target() {
return Some(id);
}
self.active_trap_group()
.and_then(|group_id| self.group_primary_focus_target(group_id))
}
#[must_use]
pub(crate) fn logical_focus_target(&self) -> Option<FocusId> {
self.active_focus_target()
}
pub(crate) fn focus_without_history(&mut self, id: FocusId) -> bool {
self.set_focus_without_history(id)
}
pub(crate) fn focus_first_without_history_for_restore(&mut self) -> bool {
self.focus_first_without_history()
}
pub(crate) fn replace_deferred_focus_target(&mut self, target: Option<FocusId>) {
self.current = None;
self.pending_focus_on_host_gain =
target.filter(|id| self.can_focus(*id) && self.allowed_by_trap(*id));
}
pub(crate) fn remove_group_without_repair(&mut self, group_id: u32) -> bool {
if self.groups.remove(&group_id).is_none() {
return false;
}
self.trap_stack.retain(|trap| trap.group_id != group_id);
true
}
pub(crate) fn push_trap_with_return_focus(
&mut self,
group_id: u32,
return_focus: Option<FocusId>,
) -> bool {
if !self.group_has_focusable_member(group_id) {
return false;
}
#[cfg(feature = "tracing")]
tracing::debug!(
group_id,
return_focus = ?return_focus,
"focus.trap_push"
);
self.trap_stack.push(FocusTrap {
group_id,
return_focus,
});
true
}
pub(crate) fn repair_focus_after_excluding_ids(&mut self, excluded: &[FocusId]) {
if !self.host_focused {
if self.current.is_some_and(|id| excluded.contains(&id)) {
let _ = self.blur();
}
return;
}
if self.is_trapped() || !self.current.is_some_and(|id| excluded.contains(&id)) {
return;
}
for id in self.graph.tab_order() {
if excluded.contains(&id) {
continue;
}
if self.set_focus_without_history(id) {
return;
}
}
let _ = self.blur();
}
pub(crate) fn clear_deferred_focus_if_excluded(&mut self, excluded: &[FocusId]) {
if self
.pending_focus_on_host_gain
.is_some_and(|id| excluded.contains(&id))
{
self.pending_focus_on_host_gain = None;
}
}
pub(crate) fn restore_focus_after_invalid_current(&mut self) {
if !self.host_focused {
return;
}
if let Some(group_id) = self.active_trap_group()
&& self.focus_first_in_group_without_history(group_id)
{
return;
}
let _ = self.focus_first_without_history();
}
fn set_focus(&mut self, id: FocusId) -> bool {
self.set_focus_target(id, true)
}
fn set_focus_without_history(&mut self, id: FocusId) -> bool {
self.set_focus_target(id, false)
}
fn set_focus_target(&mut self, id: FocusId, record_history: bool) -> bool {
if !self.host_focused {
return self.set_pending_focus_target(id);
}
self.set_focus_internal(id, record_history)
}
fn set_focus_internal(&mut self, id: FocusId, record_history: bool) -> bool {
if !self.can_focus(id) || !self.allowed_by_trap(id) {
return false;
}
if self.current == Some(id) {
return false;
}
let prev = self.current;
if let Some(prev_id) = prev {
if record_history && Some(prev_id) != self.history.last().copied() {
self.history.push(prev_id);
}
let event = FocusEvent::FocusMoved {
from: prev_id,
to: id,
};
#[cfg(feature = "tracing")]
tracing::debug!(
from_widget = prev_id,
to_widget = id,
trigger = "navigate",
"focus.change"
);
self.last_event = Some(event);
} else {
#[cfg(feature = "tracing")]
tracing::debug!(to_widget = id, trigger = "initial", "focus.change");
self.last_event = Some(FocusEvent::FocusGained { id });
}
self.current = Some(id);
self.focus_change_count += 1;
true
}
fn can_focus(&self, id: FocusId) -> bool {
self.graph.get(id).map(|n| n.is_focusable).unwrap_or(false)
}
fn active_focus_target(&self) -> Option<FocusId> {
if let Some(current) = self.current
&& self.can_focus(current)
&& self.allowed_by_trap(current)
{
return Some(current);
}
if self.host_focused {
return None;
}
self.pending_focus_on_host_gain
.filter(|id| self.can_focus(*id) && self.allowed_by_trap(*id))
}
fn set_pending_focus_target(&mut self, id: FocusId) -> bool {
if !self.can_focus(id) || !self.allowed_by_trap(id) {
return false;
}
let prev = self.active_focus_target();
self.current = None;
if prev == Some(id) {
return false;
}
self.pending_focus_on_host_gain = Some(id);
true
}
fn active_trap_group(&self) -> Option<u32> {
self.trap_stack
.iter()
.rev()
.find(|trap| self.group_has_focusable_member(trap.group_id))
.map(|trap| trap.group_id)
}
fn allowed_by_trap(&self, id: FocusId) -> bool {
let Some(group_id) = self.active_trap_group() else {
return true;
};
self.groups
.get(&group_id)
.map(|g| g.contains(id))
.unwrap_or(false)
}
fn group_has_focusable_member(&self, group_id: u32) -> bool {
self.groups
.get(&group_id)
.is_some_and(|group| group.members.iter().any(|id| self.can_focus(*id)))
}
fn repair_focus_after_group_change(&mut self) {
if !self.host_focused {
if self.current.is_some() {
let _ = self.blur();
}
return;
}
match self.active_trap_group() {
Some(group_id) => {
let current_allowed = self
.current
.is_some_and(|id| self.can_focus(id) && self.allowed_by_trap(id));
if !current_allowed {
let _ = self.focus_first_in_group_without_history(group_id);
}
}
None => {
if self.current.is_some_and(|id| !self.can_focus(id))
&& !self.focus_first_without_history()
{
let _ = self.blur();
}
}
}
}
fn is_current_focusable_in_group(&self, group_id: u32) -> bool {
let Some(current) = self.current else {
return false;
};
self.can_focus(current)
&& self
.groups
.get(&group_id)
.map(|g| g.contains(current))
.unwrap_or(false)
}
fn active_tab_order(&self) -> Vec<FocusId> {
if let Some(group_id) = self.active_trap_group() {
return self.group_tab_order(group_id);
}
self.graph.tab_order()
}
fn group_tab_order(&self, group_id: u32) -> Vec<FocusId> {
let Some(group) = self.groups.get(&group_id) else {
return Vec::new();
};
let order = self.graph.tab_order();
order.into_iter().filter(|id| group.contains(*id)).collect()
}
pub(crate) fn group_primary_focus_target(&self, group_id: u32) -> Option<FocusId> {
self.group_tab_order(group_id).first().copied().or_else(|| {
self.groups
.get(&group_id)
.and_then(|group| group.members.iter().copied().find(|id| self.can_focus(*id)))
})
}
fn focus_first_in_group_without_history(&mut self, group_id: u32) -> bool {
let Some(first) = self.group_primary_focus_target(group_id) else {
return false;
};
self.set_focus_without_history(first)
}
fn focus_first_without_history(&mut self) -> bool {
let order = self.active_tab_order();
let Some(first) = order.first().copied() else {
return false;
};
self.set_focus_without_history(first)
}
fn move_in_tab_order(&mut self, forward: bool) -> bool {
let order = self.active_tab_order();
if order.is_empty() {
return false;
}
let first = order[0];
let last = order[order.len() - 1];
let fallback = if forward { first } else { last };
let wrap = self
.active_trap_group()
.and_then(|id| self.groups.get(&id).map(|g| g.wrap))
.unwrap_or(true);
let next = match self.active_focus_target() {
None => fallback,
Some(current) => {
let pos = order.iter().position(|id| *id == current);
match pos {
None => fallback,
Some(idx) if forward => {
if idx + 1 < order.len() {
order[idx + 1]
} else if wrap {
order[0]
} else {
return false;
}
}
Some(idx) => {
if idx > 0 {
order[idx - 1]
} else if wrap {
last
} else {
return false;
}
}
}
}
};
self.set_focus(next)
}
fn dedup_members(&self, ids: Vec<FocusId>) -> Vec<FocusId> {
let mut out = Vec::new();
for id in ids {
if !out.contains(&id) {
out.push(id);
}
}
out
}
fn filter_focusable(&self, ids: Vec<FocusId>) -> Vec<FocusId> {
self.dedup_members(ids)
.into_iter()
.filter(|id| self.can_focus(*id))
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::focus::FocusNode;
use ftui_core::geometry::Rect;
fn node(id: FocusId, tab: i32) -> FocusNode {
FocusNode::new(id, Rect::new(0, 0, 1, 1)).with_tab_index(tab)
}
#[test]
fn focus_basic() {
let mut fm = FocusManager::new();
fm.graph_mut().insert(node(1, 0));
fm.graph_mut().insert(node(2, 1));
assert!(fm.focus(1).is_none());
assert_eq!(fm.current(), Some(1));
assert_eq!(fm.focus(2), Some(1));
assert_eq!(fm.current(), Some(2));
assert_eq!(fm.blur(), Some(2));
assert_eq!(fm.current(), None);
}
#[test]
fn focus_history_back() {
let mut fm = FocusManager::new();
fm.graph_mut().insert(node(1, 0));
fm.graph_mut().insert(node(2, 1));
fm.graph_mut().insert(node(3, 2));
fm.focus(1);
fm.focus(2);
fm.focus(3);
assert!(fm.focus_back());
assert_eq!(fm.current(), Some(2));
assert!(fm.focus_back());
assert_eq!(fm.current(), Some(1));
}
#[test]
fn focus_back_skips_current_id_in_history() {
let mut fm = FocusManager::new();
fm.graph_mut().insert(node(1, 0));
fm.graph_mut().insert(node(2, 1));
fm.focus(1);
fm.focus(2);
assert_eq!(fm.current(), Some(2));
assert_eq!(fm.blur(), Some(2));
assert_eq!(fm.current(), None);
fm.focus(1);
assert_eq!(fm.current(), Some(1));
let _ = fm.take_focus_event();
let before = fm.focus_change_count();
assert!(!fm.focus_back());
assert_eq!(fm.current(), Some(1));
assert!(fm.take_focus_event().is_none());
assert_eq!(fm.focus_change_count(), before);
}
#[test]
fn focus_next_prev() {
let mut fm = FocusManager::new();
fm.graph_mut().insert(node(1, 0));
fm.graph_mut().insert(node(2, 1));
fm.graph_mut().insert(node(3, 2));
assert!(fm.focus_next());
assert_eq!(fm.current(), Some(1));
assert!(fm.focus_next());
assert_eq!(fm.current(), Some(2));
assert!(fm.focus_prev());
assert_eq!(fm.current(), Some(1));
}
#[test]
fn apply_host_focus_loss_blurs_current() {
let mut fm = FocusManager::new();
fm.graph_mut().insert(node(1, 0));
fm.focus(1);
let _ = fm.take_focus_event();
assert!(fm.apply_host_focus(false));
assert_eq!(fm.current(), None);
assert_eq!(fm.take_focus_event(), Some(FocusEvent::FocusLost { id: 1 }));
}
#[test]
fn apply_host_focus_gain_focuses_first_when_unfocused() {
let mut fm = FocusManager::new();
fm.graph_mut().insert(node(10, 1));
fm.graph_mut().insert(node(5, 0));
assert!(fm.apply_host_focus(true));
assert_eq!(fm.current(), Some(5));
assert_eq!(
fm.take_focus_event(),
Some(FocusEvent::FocusGained { id: 5 })
);
}
#[test]
fn apply_host_focus_gain_preserves_valid_current() {
let mut fm = FocusManager::new();
fm.graph_mut().insert(node(1, 0));
fm.graph_mut().insert(node(2, 1));
fm.focus(2);
let _ = fm.take_focus_event();
assert!(!fm.apply_host_focus(true));
assert_eq!(fm.current(), Some(2));
assert!(fm.take_focus_event().is_none());
}
#[test]
fn apply_host_focus_gain_clears_invalid_current_when_restore_fails() {
let mut fm = FocusManager::new();
fm.graph_mut().insert(node(1, 0));
fm.focus(1);
let _ = fm.take_focus_event();
let _ = fm.graph_mut().remove(1);
assert!(fm.apply_host_focus(true));
assert_eq!(fm.current(), None);
assert_eq!(fm.take_focus_event(), Some(FocusEvent::FocusLost { id: 1 }));
}
#[test]
fn apply_host_focus_gain_respects_trap_order() {
let mut fm = FocusManager::new();
fm.graph_mut().insert(node(1, 0));
fm.graph_mut().insert(node(2, 1));
fm.graph_mut().insert(node(3, 2));
fm.create_group(42, vec![2, 3]);
fm.push_trap(42);
let _ = fm.take_focus_event();
fm.blur();
let _ = fm.take_focus_event();
assert!(fm.apply_host_focus(true));
assert_eq!(fm.current(), Some(2));
}
#[test]
fn apply_host_focus_gain_restores_previously_selected_trapped_focus() {
let mut fm = FocusManager::new();
fm.graph_mut().insert(node(1, 0));
fm.graph_mut().insert(node(2, 1));
fm.graph_mut().insert(node(3, 2));
fm.create_group(42, vec![2, 3]);
assert!(fm.push_trap(42));
assert_eq!(fm.current(), Some(2));
assert_eq!(fm.focus(3), Some(2));
let _ = fm.take_focus_event();
assert!(fm.apply_host_focus(false));
assert_eq!(fm.current(), None);
let _ = fm.take_focus_event();
assert!(fm.apply_host_focus(true));
assert_eq!(fm.current(), Some(3));
assert_eq!(
fm.take_focus_event(),
Some(FocusEvent::FocusGained { id: 3 })
);
}
#[test]
fn push_trap_while_host_blurred_without_prior_focus_restores_none_on_pop() {
let mut fm = FocusManager::new();
fm.graph_mut().insert(node(1, 0));
fm.graph_mut().insert(node(2, 1));
assert!(!fm.apply_host_focus(false));
fm.create_group(42, vec![2]);
assert!(fm.push_trap(42));
assert!(fm.apply_host_focus(true));
assert_eq!(fm.current(), Some(2));
assert!(fm.pop_trap());
assert_eq!(fm.current(), None);
assert_eq!(fm.take_focus_event(), Some(FocusEvent::FocusLost { id: 2 }));
}
#[test]
fn push_trap_does_not_autofocus_while_host_blurred() {
let mut fm = FocusManager::new();
fm.graph_mut().insert(node(1, 0));
fm.graph_mut().insert(node(2, 1));
fm.focus(1);
assert!(fm.apply_host_focus(false));
fm.create_group(42, vec![2]);
assert!(fm.push_trap(42));
assert_eq!(fm.current(), None);
assert!(fm.apply_host_focus(true));
assert_eq!(fm.current(), Some(2));
}
#[test]
fn focus_while_host_blurred_updates_deferred_target_without_restoring_current() {
let mut fm = FocusManager::new();
fm.graph_mut().insert(node(1, 0));
fm.graph_mut().insert(node(2, 1));
fm.graph_mut().insert(node(3, 2));
fm.focus(1);
let _ = fm.take_focus_event();
assert!(fm.apply_host_focus(false));
assert_eq!(fm.current(), None);
assert_eq!(fm.focus(3), Some(1));
assert_eq!(fm.current(), None);
assert_eq!(fm.take_focus_event(), Some(FocusEvent::FocusLost { id: 1 }));
assert!(fm.apply_host_focus(true));
assert_eq!(fm.current(), Some(3));
assert_eq!(
fm.take_focus_event(),
Some(FocusEvent::FocusGained { id: 3 })
);
}
#[test]
fn focus_next_while_host_blurred_advances_deferred_target() {
let mut fm = FocusManager::new();
fm.graph_mut().insert(node(1, 0));
fm.graph_mut().insert(node(2, 1));
fm.graph_mut().insert(node(3, 2));
fm.focus(1);
assert_eq!(fm.focus(2), Some(1));
let _ = fm.take_focus_event();
assert!(fm.apply_host_focus(false));
assert_eq!(fm.current(), None);
assert!(fm.focus_next());
assert_eq!(fm.current(), None);
assert_eq!(fm.take_focus_event(), Some(FocusEvent::FocusLost { id: 2 }));
assert!(fm.apply_host_focus(true));
assert_eq!(fm.current(), Some(3));
assert_eq!(
fm.take_focus_event(),
Some(FocusEvent::FocusGained { id: 3 })
);
}
#[test]
fn focus_trap_push_pop() {
let mut fm = FocusManager::new();
fm.graph_mut().insert(node(1, 0));
fm.graph_mut().insert(node(2, 1));
fm.graph_mut().insert(node(3, 2));
fm.focus(3);
fm.create_group(7, vec![1, 2]);
fm.push_trap(7);
assert!(fm.is_trapped());
assert_eq!(fm.current(), Some(1));
fm.pop_trap();
assert!(!fm.is_trapped());
assert_eq!(fm.current(), Some(3));
}
#[test]
fn focus_group_wrap_respected() {
let mut fm = FocusManager::new();
fm.graph_mut().insert(node(1, 0));
fm.graph_mut().insert(node(2, 1));
fm.create_group(9, vec![1, 2]);
fm.groups.get_mut(&9).unwrap().wrap = false;
fm.push_trap(9);
fm.focus(2);
assert!(!fm.focus_next());
assert_eq!(fm.current(), Some(2));
}
#[test]
fn focus_event_generation() {
let mut fm = FocusManager::new();
fm.graph_mut().insert(node(1, 0));
fm.graph_mut().insert(node(2, 1));
fm.focus(1);
assert_eq!(
fm.take_focus_event(),
Some(FocusEvent::FocusGained { id: 1 })
);
fm.focus(2);
assert_eq!(
fm.take_focus_event(),
Some(FocusEvent::FocusMoved { from: 1, to: 2 })
);
fm.blur();
assert_eq!(fm.take_focus_event(), Some(FocusEvent::FocusLost { id: 2 }));
}
#[test]
fn trap_prevents_focus_outside_group() {
let mut fm = FocusManager::new();
fm.graph_mut().insert(node(1, 0));
fm.graph_mut().insert(node(2, 1));
fm.graph_mut().insert(node(3, 2));
fm.create_group(5, vec![1, 2]);
fm.push_trap(5);
assert_eq!(fm.current(), Some(1));
assert!(fm.focus(3).is_none());
assert_ne!(fm.current(), Some(3));
}
fn spatial_node(id: FocusId, x: u16, y: u16, w: u16, h: u16, tab: i32) -> FocusNode {
FocusNode::new(id, Rect::new(x, y, w, h)).with_tab_index(tab)
}
#[test]
fn navigate_spatial_fallback() {
let mut fm = FocusManager::new();
fm.graph_mut().insert(spatial_node(1, 0, 0, 10, 3, 0));
fm.graph_mut().insert(spatial_node(2, 20, 0, 10, 3, 1));
fm.focus(1);
assert!(fm.navigate(NavDirection::Right));
assert_eq!(fm.current(), Some(2));
assert!(fm.navigate(NavDirection::Left));
assert_eq!(fm.current(), Some(1));
}
#[test]
fn navigate_explicit_edge_overrides_spatial() {
let mut fm = FocusManager::new();
fm.graph_mut().insert(spatial_node(1, 0, 0, 10, 3, 0));
fm.graph_mut().insert(spatial_node(2, 20, 0, 10, 3, 1)); fm.graph_mut().insert(spatial_node(3, 40, 0, 10, 3, 2));
fm.graph_mut().connect(1, NavDirection::Right, 3);
fm.focus(1);
assert!(fm.navigate(NavDirection::Right));
assert_eq!(fm.current(), Some(3));
}
#[test]
fn navigate_spatial_respects_trap() {
let mut fm = FocusManager::new();
fm.graph_mut().insert(spatial_node(1, 0, 0, 10, 3, 0));
fm.graph_mut().insert(spatial_node(2, 20, 0, 10, 3, 1));
fm.graph_mut().insert(spatial_node(3, 40, 0, 10, 3, 2));
fm.create_group(1, vec![1, 2]);
fm.focus(2);
fm.push_trap(1);
assert!(!fm.navigate(NavDirection::Right));
assert_eq!(fm.current(), Some(2));
}
#[test]
fn navigate_spatial_grid_round_trip() {
let mut fm = FocusManager::new();
fm.graph_mut().insert(spatial_node(1, 0, 0, 10, 3, 0));
fm.graph_mut().insert(spatial_node(2, 20, 0, 10, 3, 1));
fm.graph_mut().insert(spatial_node(3, 0, 6, 10, 3, 2));
fm.graph_mut().insert(spatial_node(4, 20, 6, 10, 3, 3));
fm.focus(1);
assert!(fm.navigate(NavDirection::Right));
assert_eq!(fm.current(), Some(2));
assert!(fm.navigate(NavDirection::Down));
assert_eq!(fm.current(), Some(4));
assert!(fm.navigate(NavDirection::Left));
assert_eq!(fm.current(), Some(3));
assert!(fm.navigate(NavDirection::Up));
assert_eq!(fm.current(), Some(1));
}
#[test]
fn navigate_spatial_no_candidate() {
let mut fm = FocusManager::new();
fm.graph_mut().insert(spatial_node(1, 0, 0, 10, 3, 0));
fm.focus(1);
assert!(!fm.navigate(NavDirection::Right));
assert!(!fm.navigate(NavDirection::Up));
assert_eq!(fm.current(), Some(1));
}
#[test]
fn new_manager_has_no_focus() {
let fm = FocusManager::new();
assert_eq!(fm.current(), None);
assert!(!fm.is_trapped());
}
#[test]
fn default_and_new_are_equivalent() {
let a = FocusManager::new();
let b = FocusManager::default();
assert_eq!(a.current(), b.current());
assert_eq!(a.is_trapped(), b.is_trapped());
assert_eq!(a.host_focused(), b.host_focused());
}
#[test]
fn is_focused_returns_true_for_current() {
let mut fm = FocusManager::new();
fm.graph_mut().insert(node(1, 0));
fm.focus(1);
assert!(fm.is_focused(1));
assert!(!fm.is_focused(2));
}
#[test]
fn is_focused_returns_false_when_no_focus() {
let fm = FocusManager::new();
assert!(!fm.is_focused(1));
}
#[test]
fn focus_non_existent_node_returns_none() {
let mut fm = FocusManager::new();
assert!(fm.focus(999).is_none());
assert_eq!(fm.current(), None);
}
#[test]
fn focus_already_focused_returns_same_id() {
let mut fm = FocusManager::new();
fm.graph_mut().insert(node(1, 0));
fm.focus(1);
assert_eq!(fm.focus(1), Some(1));
assert_eq!(fm.current(), Some(1));
}
#[test]
fn blur_when_no_focus_returns_none() {
let mut fm = FocusManager::new();
assert_eq!(fm.blur(), None);
}
#[test]
fn blur_generates_focus_lost_event() {
let mut fm = FocusManager::new();
fm.graph_mut().insert(node(1, 0));
fm.focus(1);
let _ = fm.take_focus_event(); fm.blur();
assert_eq!(fm.take_focus_event(), Some(FocusEvent::FocusLost { id: 1 }));
}
#[test]
fn focus_first_selects_lowest_tab_index() {
let mut fm = FocusManager::new();
fm.graph_mut().insert(node(3, 2));
fm.graph_mut().insert(node(1, 0));
fm.graph_mut().insert(node(2, 1));
assert!(fm.focus_first());
assert_eq!(fm.current(), Some(1));
}
#[test]
fn focus_last_selects_highest_tab_index() {
let mut fm = FocusManager::new();
fm.graph_mut().insert(node(1, 0));
fm.graph_mut().insert(node(2, 1));
fm.graph_mut().insert(node(3, 2));
assert!(fm.focus_last());
assert_eq!(fm.current(), Some(3));
}
#[test]
fn focus_first_on_empty_graph_returns_false() {
let mut fm = FocusManager::new();
assert!(!fm.focus_first());
}
#[test]
fn focus_last_on_empty_graph_returns_false() {
let mut fm = FocusManager::new();
assert!(!fm.focus_last());
}
#[test]
fn focus_next_wraps_at_end() {
let mut fm = FocusManager::new();
fm.graph_mut().insert(node(1, 0));
fm.graph_mut().insert(node(2, 1));
fm.focus(2);
assert!(fm.focus_next()); assert_eq!(fm.current(), Some(1));
}
#[test]
fn focus_prev_wraps_at_start() {
let mut fm = FocusManager::new();
fm.graph_mut().insert(node(1, 0));
fm.graph_mut().insert(node(2, 1));
fm.focus(1);
assert!(fm.focus_prev()); assert_eq!(fm.current(), Some(2));
}
#[test]
fn focus_next_with_no_current_selects_first() {
let mut fm = FocusManager::new();
fm.graph_mut().insert(node(1, 0));
fm.graph_mut().insert(node(2, 1));
assert!(fm.focus_next());
assert_eq!(fm.current(), Some(1));
}
#[test]
fn focus_prev_with_no_current_selects_last() {
let mut fm = FocusManager::new();
fm.graph_mut().insert(node(1, 0));
fm.graph_mut().insert(node(2, 1));
assert!(fm.focus_prev());
assert_eq!(fm.current(), Some(2));
}
#[test]
fn focus_prev_with_stale_current_selects_last() {
let mut fm = FocusManager::new();
fm.graph_mut().insert(node(1, 0));
fm.graph_mut().insert(node(2, 1));
fm.graph_mut().insert(node(3, 2));
fm.focus(2);
let _ = fm.graph_mut().remove(2);
assert!(fm.focus_prev());
assert_eq!(fm.current(), Some(3));
}
#[test]
fn focus_next_on_empty_returns_false() {
let mut fm = FocusManager::new();
assert!(!fm.focus_next());
}
#[test]
fn focus_back_on_empty_history_returns_false() {
let mut fm = FocusManager::new();
fm.graph_mut().insert(node(1, 0));
fm.focus(1);
assert!(!fm.focus_back());
}
#[test]
fn clear_history_prevents_back() {
let mut fm = FocusManager::new();
fm.graph_mut().insert(node(1, 0));
fm.graph_mut().insert(node(2, 1));
fm.focus(1);
fm.focus(2);
fm.clear_history();
assert!(!fm.focus_back());
assert_eq!(fm.current(), Some(2));
}
#[test]
fn focus_back_skips_removed_nodes() {
let mut fm = FocusManager::new();
fm.graph_mut().insert(node(1, 0));
fm.graph_mut().insert(node(2, 1));
fm.graph_mut().insert(node(3, 2));
fm.focus(1);
fm.focus(2);
fm.focus(3);
let _ = fm.graph_mut().remove(2);
assert!(fm.focus_back());
assert_eq!(fm.current(), Some(1));
}
#[test]
fn create_group_filters_non_focusable() {
let mut fm = FocusManager::new();
fm.graph_mut().insert(node(1, 0));
fm.create_group(1, vec![1, 999]);
let group = fm.groups.get(&1).unwrap();
assert_eq!(group.members.len(), 1);
assert!(group.contains(1));
}
#[test]
fn add_to_group_creates_group_if_needed() {
let mut fm = FocusManager::new();
fm.graph_mut().insert(node(1, 0));
fm.add_to_group(42, 1);
assert!(fm.groups.contains_key(&42));
assert!(fm.groups.get(&42).unwrap().contains(1));
}
#[test]
fn add_to_group_skips_unfocusable() {
let mut fm = FocusManager::new();
fm.add_to_group(1, 999); if let Some(group) = fm.groups.get(&1) {
assert!(!group.contains(999));
}
}
#[test]
fn add_to_group_no_duplicates() {
let mut fm = FocusManager::new();
fm.graph_mut().insert(node(1, 0));
fm.add_to_group(1, 1);
fm.add_to_group(1, 1);
assert_eq!(fm.groups.get(&1).unwrap().members.len(), 1);
}
#[test]
fn remove_from_group() {
let mut fm = FocusManager::new();
fm.graph_mut().insert(node(1, 0));
fm.graph_mut().insert(node(2, 1));
fm.create_group(1, vec![1, 2]);
fm.remove_from_group(1, 1);
assert!(!fm.groups.get(&1).unwrap().contains(1));
assert!(fm.groups.get(&1).unwrap().contains(2));
}
#[test]
fn removing_focused_member_from_active_trap_refocuses_remaining_member() {
let mut fm = FocusManager::new();
fm.graph_mut().insert(node(1, 0));
fm.graph_mut().insert(node(2, 1));
fm.graph_mut().insert(node(3, 2));
fm.create_group(1, vec![1, 2]);
fm.focus(2);
assert!(fm.push_trap(1));
assert_eq!(fm.current(), Some(2));
fm.remove_from_group(1, 2);
assert_eq!(fm.current(), Some(1));
assert!(fm.is_trapped());
assert!(fm.focus(3).is_none());
assert_eq!(fm.current(), Some(1));
}
#[test]
fn removing_last_member_from_active_trap_allows_focus_escape() {
let mut fm = FocusManager::new();
fm.graph_mut().insert(node(1, 0));
fm.graph_mut().insert(node(2, 1));
fm.create_group(1, vec![1]);
fm.focus(1);
assert!(fm.push_trap(1));
assert!(fm.is_trapped());
fm.remove_from_group(1, 1);
assert!(!fm.is_trapped());
assert_eq!(fm.current(), Some(1));
assert_eq!(fm.focus(2), Some(1));
assert_eq!(fm.current(), Some(2));
}
#[test]
fn removing_active_inner_trap_member_falls_back_to_outer_trap() {
let mut fm = FocusManager::new();
fm.graph_mut().insert(node(1, 0));
fm.graph_mut().insert(node(2, 1));
fm.graph_mut().insert(node(3, 2));
fm.create_group(10, vec![1, 2]);
fm.create_group(20, vec![3]);
fm.focus(1);
assert!(fm.push_trap(10));
assert!(fm.push_trap(20));
assert_eq!(fm.current(), Some(3));
fm.remove_from_group(20, 3);
assert!(fm.is_trapped());
assert_eq!(fm.current(), Some(1));
assert!(fm.focus(3).is_none());
assert_eq!(fm.focus(2), Some(1));
assert_eq!(fm.current(), Some(2));
}
#[test]
fn adding_member_to_invalidated_trap_restores_confinement() {
let mut fm = FocusManager::new();
fm.graph_mut().insert(node(1, 0));
fm.graph_mut().insert(node(2, 1));
fm.create_group(1, vec![1]);
fm.focus(1);
assert!(fm.push_trap(1));
fm.remove_from_group(1, 1);
assert!(!fm.is_trapped());
fm.add_to_group(1, 2);
assert!(fm.is_trapped());
assert_eq!(fm.current(), Some(2));
assert!(fm.focus(1).is_none());
}
#[test]
fn remove_from_nonexistent_group_is_noop() {
let mut fm = FocusManager::new();
fm.remove_from_group(999, 1); }
#[test]
fn remove_group_deletes_group() {
let mut fm = FocusManager::new();
fm.graph_mut().insert(node(1, 0));
fm.create_group(42, vec![1]);
fm.remove_group(42);
assert!(!fm.groups.contains_key(&42));
}
#[test]
fn remove_group_from_active_inner_trap_falls_back_to_outer_trap() {
let mut fm = FocusManager::new();
fm.graph_mut().insert(node(1, 0));
fm.graph_mut().insert(node(2, 1));
fm.graph_mut().insert(node(3, 2));
fm.create_group(10, vec![1, 2]);
fm.create_group(20, vec![3]);
fm.focus(1);
assert!(fm.push_trap(10));
assert!(fm.push_trap(20));
assert_eq!(fm.current(), Some(3));
fm.remove_group(20);
assert!(fm.is_trapped());
assert_eq!(fm.current(), Some(1));
assert!(fm.focus(3).is_none());
}
#[test]
fn remove_group_clears_stale_trap_entries() {
let mut fm = FocusManager::new();
fm.graph_mut().insert(node(1, 0));
fm.create_group(10, vec![1]);
fm.focus(1);
assert!(fm.push_trap(10));
assert!(fm.is_trapped());
fm.remove_group(10);
assert!(!fm.is_trapped());
assert!(!fm.pop_trap());
}
#[test]
fn remove_group_blurs_invalid_current_when_no_fallback_exists() {
let mut fm = FocusManager::new();
fm.graph_mut().insert(node(1, 0));
fm.create_group(10, vec![1]);
fm.focus(1);
let _ = fm.graph_mut().remove(1);
fm.remove_group(10);
assert_eq!(fm.current(), None);
assert_eq!(fm.take_focus_event(), Some(FocusEvent::FocusLost { id: 1 }));
}
#[test]
fn focus_group_with_wrap() {
let group = FocusGroup::new(1, vec![1, 2]).with_wrap(false);
assert!(!group.wrap);
}
#[test]
fn focus_group_with_exit_key() {
let group = FocusGroup::new(1, vec![]).with_exit_key(KeyCode::Escape);
assert_eq!(group.exit_key, Some(KeyCode::Escape));
}
#[test]
fn focus_group_default_wraps() {
let group = FocusGroup::new(1, vec![]);
assert!(group.wrap);
assert_eq!(group.exit_key, None);
}
#[test]
fn nested_traps() {
let mut fm = FocusManager::new();
fm.graph_mut().insert(node(1, 0));
fm.graph_mut().insert(node(2, 1));
fm.graph_mut().insert(node(3, 2));
fm.graph_mut().insert(node(4, 3));
fm.create_group(10, vec![1, 2]);
fm.create_group(20, vec![3, 4]);
fm.focus(1);
fm.push_trap(10);
assert!(fm.is_trapped());
fm.push_trap(20);
assert_eq!(fm.current(), Some(3));
fm.pop_trap();
assert!(fm.is_trapped());
fm.pop_trap();
assert!(!fm.is_trapped());
}
#[test]
fn trap_push_pop_does_not_pollute_focus_history() {
let mut fm = FocusManager::new();
fm.graph_mut().insert(node(1, 0));
fm.graph_mut().insert(node(2, 1));
fm.graph_mut().insert(node(3, 2));
fm.create_group(10, vec![2]);
fm.focus(1);
fm.focus(3);
assert_eq!(fm.current(), Some(3));
assert!(fm.push_trap(10));
assert_eq!(fm.current(), Some(2));
assert!(fm.pop_trap());
assert_eq!(fm.current(), Some(3));
assert!(fm.focus_back());
assert_eq!(fm.current(), Some(1));
assert!(!fm.focus_back());
}
#[test]
fn pop_trap_restores_none_when_modal_opened_without_focus() {
let mut fm = FocusManager::new();
fm.graph_mut().insert(node(1, 0));
fm.create_group(10, vec![1]);
assert!(fm.push_trap(10));
assert_eq!(fm.current(), Some(1));
assert!(fm.pop_trap());
assert_eq!(fm.current(), None);
assert_eq!(fm.take_focus_event(), Some(FocusEvent::FocusLost { id: 1 }));
}
#[test]
fn pop_trap_on_empty_returns_false() {
let mut fm = FocusManager::new();
assert!(!fm.pop_trap());
}
#[test]
fn push_trap_rejects_missing_group() {
let mut fm = FocusManager::new();
fm.graph_mut().insert(node(1, 0));
fm.focus(1);
assert!(!fm.push_trap(999));
assert!(!fm.is_trapped());
assert_eq!(fm.current(), Some(1));
}
#[test]
fn push_trap_rejects_empty_group() {
let mut fm = FocusManager::new();
fm.graph_mut().insert(node(1, 0));
fm.focus(1);
fm.create_group(42, vec![]);
assert!(!fm.push_trap(42));
assert!(!fm.is_trapped());
assert_eq!(fm.current(), Some(1));
}
#[test]
fn push_trap_autofocuses_negative_tabindex_member_when_group_has_no_tabbable_nodes() {
let mut fm = FocusManager::new();
fm.graph_mut().insert(node(1, 0));
fm.graph_mut().insert(node(2, -1));
fm.focus(1);
fm.create_group(42, vec![2]);
assert!(fm.push_trap(42));
assert!(fm.is_trapped());
assert_eq!(fm.current(), Some(2));
}
#[test]
fn push_trap_blurred_restores_negative_tabindex_member_on_focus_gain() {
let mut fm = FocusManager::new();
fm.graph_mut().insert(node(1, 0));
fm.graph_mut().insert(node(2, -1));
fm.focus(1);
assert!(fm.apply_host_focus(false));
fm.create_group(42, vec![2]);
assert!(fm.push_trap(42));
assert_eq!(fm.current(), None);
assert!(fm.apply_host_focus(true));
assert_eq!(fm.current(), Some(2));
}
#[test]
fn push_trap_retargets_when_current_group_member_becomes_unfocusable() {
let mut fm = FocusManager::new();
fm.graph_mut().insert(node(1, 0));
fm.graph_mut().insert(node(2, 1));
fm.focus(1);
fm.create_group(10, vec![1, 2]);
fm.graph_mut().insert(node(1, 0).with_focusable(false));
assert!(fm.push_trap(10));
assert_eq!(fm.current(), Some(2));
}
#[test]
fn take_focus_event_clears_it() {
let mut fm = FocusManager::new();
fm.graph_mut().insert(node(1, 0));
fm.focus(1);
assert!(fm.take_focus_event().is_some());
assert!(fm.take_focus_event().is_none());
}
#[test]
fn focus_event_accessor() {
let mut fm = FocusManager::new();
fm.graph_mut().insert(node(1, 0));
fm.focus(1);
assert_eq!(fm.focus_event(), Some(&FocusEvent::FocusGained { id: 1 }));
}
#[test]
fn navigate_direction_with_no_current_returns_false() {
let mut fm = FocusManager::new();
fm.graph_mut().insert(spatial_node(1, 0, 0, 10, 3, 0));
assert!(!fm.navigate(NavDirection::Right));
}
#[test]
fn graph_accessor_returns_reference() {
let mut fm = FocusManager::new();
fm.graph_mut().insert(node(1, 0));
assert!(fm.graph().get(1).is_some());
}
#[test]
fn default_indicator_is_reverse() {
let fm = FocusManager::new();
assert!(fm.indicator().is_visible());
assert_eq!(
fm.indicator().kind(),
crate::focus::FocusIndicatorKind::StyleOverlay
);
}
#[test]
fn set_indicator() {
let mut fm = FocusManager::new();
fm.set_indicator(crate::focus::FocusIndicator::underline());
assert_eq!(
fm.indicator().kind(),
crate::focus::FocusIndicatorKind::Underline
);
}
#[test]
fn focus_change_count_increments() {
let mut fm = FocusManager::new();
fm.graph_mut().insert(node(1, 0));
fm.graph_mut().insert(node(2, 1));
assert_eq!(fm.focus_change_count(), 0);
fm.focus(1);
assert_eq!(fm.focus_change_count(), 1);
fm.focus(2);
assert_eq!(fm.focus_change_count(), 2);
fm.blur();
assert_eq!(fm.focus_change_count(), 3);
}
#[test]
fn focus_change_count_zero_on_no_op() {
let mut fm = FocusManager::new();
fm.graph_mut().insert(node(1, 0));
fm.focus(1);
assert_eq!(fm.focus_change_count(), 1);
fm.focus(1);
assert_eq!(fm.focus_change_count(), 1);
}
#[test]
fn focus_back_increments_focus_change_count() {
let mut fm = FocusManager::new();
fm.graph_mut().insert(node(1, 0));
fm.graph_mut().insert(node(2, 1));
fm.focus(1);
fm.focus(2);
assert_eq!(fm.focus_change_count(), 2);
assert!(fm.focus_back());
assert_eq!(fm.current(), Some(1));
assert_eq!(fm.focus_change_count(), 3);
}
}