use std::cell::Cell;
use crate::Rectangle;
use crate::widget::Id;
use crate::widget::operation::{self, Operation, Outcome};
thread_local! {
static PREV_FOCUS_CENTER: Cell<Option<(f32, f32)>> = const { Cell::new(None) };
static LAST_FOCUSED: Cell<Option<(usize, Rectangle)>> = const { Cell::new(None) };
static FOCUS_DIRTY: Cell<bool> = const { Cell::new(false) };
}
pub fn mark_focus_dirty() {
FOCUS_DIRTY.set(true);
}
pub fn take_focus_dirty() -> bool {
FOCUS_DIRTY.replace(false)
}
pub trait Focusable {
fn is_focused(&self) -> bool;
fn focus(&mut self);
fn unfocus(&mut self);
fn press(&mut self) -> bool {
false
}
fn consume_direction(&mut self, _direction: FocusDirection) -> bool {
false
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FocusDirection {
Up,
Down,
Left,
Right,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Count {
pub focused: Option<usize>,
pub total: usize,
}
pub fn focus<T>(target: Id) -> impl Operation<T> {
struct Focus {
target: Id,
}
impl<T> Operation<T> for Focus {
fn focusable(&mut self, id: Option<&Id>, _bounds: Rectangle, state: &mut dyn Focusable) {
match id {
Some(id) if id == &self.target => {
state.focus();
mark_focus_dirty();
}
_ => {
state.unfocus();
}
}
}
fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation<T>)) {
operate(self);
}
}
Focus { target }
}
pub fn unfocus<T>() -> impl Operation<T> {
struct Unfocus;
impl<T> Operation<T> for Unfocus {
fn focusable(&mut self, _id: Option<&Id>, _bounds: Rectangle, state: &mut dyn Focusable) {
if state.is_focused() {
state.unfocus();
mark_focus_dirty();
}
}
fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation<T>)) {
operate(self);
}
}
Unfocus
}
pub fn count() -> impl Operation<Count> {
struct CountFocusable {
count: Count,
}
impl Operation<Count> for CountFocusable {
fn focusable(&mut self, _id: Option<&Id>, _bounds: Rectangle, state: &mut dyn Focusable) {
if state.is_focused() {
self.count.focused = Some(self.count.total);
}
self.count.total += 1;
}
fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation<Count>)) {
operate(self);
}
fn finish(&self) -> Outcome<Count> {
Outcome::Some(self.count)
}
}
CountFocusable {
count: Count::default(),
}
}
pub fn focus_previous<T>() -> impl Operation<T>
where
T: Send + 'static,
{
struct FocusPrevious {
count: Count,
current: usize,
}
impl<T> Operation<T> for FocusPrevious {
fn focusable(&mut self, _id: Option<&Id>, _bounds: Rectangle, state: &mut dyn Focusable) {
if self.count.total == 0 {
return;
}
match self.count.focused {
None if self.current == self.count.total - 1 => {
state.focus();
mark_focus_dirty();
}
Some(0) if self.current == 0 => state.unfocus(),
Some(0) => {}
Some(focused) if focused == self.current => state.unfocus(),
Some(focused) if focused - 1 == self.current => {
state.focus();
mark_focus_dirty();
}
_ => {}
}
self.current += 1;
}
fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation<T>)) {
operate(self);
}
}
operation::then(count(), |count| FocusPrevious { count, current: 0 })
}
pub fn focus_next<T>() -> impl Operation<T>
where
T: Send + 'static,
{
struct FocusNext {
count: Count,
current: usize,
}
impl<T> Operation<T> for FocusNext {
fn focusable(&mut self, _id: Option<&Id>, _bounds: Rectangle, state: &mut dyn Focusable) {
match self.count.focused {
None if self.current == 0 => {
state.focus();
mark_focus_dirty();
}
Some(focused) if focused == self.current => state.unfocus(),
Some(focused) if focused + 1 == self.current => {
state.focus();
mark_focus_dirty();
}
_ => {}
}
self.current += 1;
}
fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation<T>)) {
operate(self);
}
}
operation::then(count(), |count| FocusNext { count, current: 0 })
}
pub fn find_focused() -> impl Operation<Id> {
struct FindFocused {
focused: Option<Id>,
}
impl Operation<Id> for FindFocused {
fn focusable(&mut self, id: Option<&Id>, _bounds: Rectangle, state: &mut dyn Focusable) {
if state.is_focused() && id.is_some() {
self.focused = id.cloned();
}
}
fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation<Id>)) {
operate(self);
}
fn finish(&self) -> Outcome<Id> {
if let Some(id) = &self.focused {
Outcome::Some(id.clone())
} else {
Outcome::None
}
}
}
FindFocused { focused: None }
}
pub fn is_focused(target: Id) -> impl Operation<bool> {
struct IsFocused {
target: Id,
is_focused: Option<bool>,
}
impl Operation<bool> for IsFocused {
fn focusable(&mut self, id: Option<&Id>, _bounds: Rectangle, state: &mut dyn Focusable) {
if id.is_some_and(|id| *id == self.target) {
self.is_focused = Some(state.is_focused());
}
}
fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation<bool>)) {
if self.is_focused.is_some() {
return;
}
operate(self);
}
fn finish(&self) -> Outcome<bool> {
self.is_focused.map_or(Outcome::None, Outcome::Some)
}
}
IsFocused {
target,
is_focused: None,
}
}
pub fn press_focused<T>() -> impl Operation<T>
where
T: Send + 'static,
{
struct PressFocused;
impl<T> Operation<T> for PressFocused {
fn focusable(&mut self, _id: Option<&Id>, _bounds: Rectangle, state: &mut dyn Focusable) {
if state.is_focused() {
let _ = state.press();
}
}
fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation<T>)) {
operate(self);
}
}
PressFocused
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct FocusGroup {
pub horizontal_leaves_row: bool,
}
#[derive(Debug, Clone)]
struct Spot {
index: usize,
bounds: Rectangle,
id: Option<Id>,
order: u32,
group: FocusGroup,
}
#[derive(Debug, Clone, Default)]
struct SpatialScan {
widgets: Vec<Spot>,
focused: Option<(usize, Rectangle)>,
total: usize,
direction: Option<FocusDirection>,
wrap: bool,
direction_consumed: bool,
pending_order: Option<u32>,
open_groups: Vec<FocusGroup>,
}
fn spatial_scan(direction: FocusDirection, wrap: bool) -> impl Operation<SpatialScan> {
struct Scan {
result: SpatialScan,
}
impl Operation<SpatialScan> for Scan {
fn focus_order_hint(&mut self, order: u32) {
self.result.pending_order = Some(order);
}
fn enter_focus_group(&mut self, group: FocusGroup) {
self.result.open_groups.push(group);
}
fn exit_focus_group(&mut self) {
let _ = self.result.open_groups.pop();
}
fn focusable(&mut self, id: Option<&Id>, bounds: Rectangle, state: &mut dyn Focusable) {
let idx = self.result.total;
let order = self.result.pending_order.take().unwrap_or(u32::MAX);
self.result.widgets.push(Spot {
index: idx,
bounds,
id: id.cloned(),
order,
group: self.result.open_groups.last().copied().unwrap_or_default(),
});
if state.is_focused() {
self.result.focused = Some((idx, bounds));
if let Some(direction) = self.result.direction
&& state.consume_direction(direction)
{
self.result.direction_consumed = true;
}
}
self.result.total += 1;
}
fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation<SpatialScan>)) {
operate(self);
}
fn finish(&self) -> Outcome<SpatialScan> {
Outcome::Some(self.result.clone())
}
}
Scan {
result: SpatialScan {
direction: Some(direction),
wrap,
..SpatialScan::default()
},
}
}
pub fn focus_directional<T>(direction: FocusDirection) -> impl Operation<T>
where
T: Send + 'static,
{
focus_directional_impl(direction, true)
}
pub fn focus_directional_no_wrap<T>(direction: FocusDirection) -> impl Operation<T>
where
T: Send + 'static,
{
focus_directional_impl(direction, false)
}
fn focus_directional_impl<T>(direction: FocusDirection, wrap: bool) -> impl Operation<T>
where
T: Send + 'static,
{
struct FocusDirectional {
scan: SpatialScan,
target_index: Option<usize>,
current: usize,
}
impl<T> Operation<T> for FocusDirectional {
fn focusable(&mut self, _id: Option<&Id>, _bounds: Rectangle, state: &mut dyn Focusable) {
if let Some(target) = self.target_index {
if self
.scan
.focused
.is_some_and(|(idx, _)| idx == self.current)
{
state.unfocus();
}
if self.current == target {
state.focus();
mark_focus_dirty();
}
}
self.current += 1;
}
fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation<T>)) {
operate(self);
}
}
fn apply_directional(scan: SpatialScan) -> FocusDirectional {
if scan.direction_consumed {
log::trace!("[FocusDir] direction consumed by focused widget — not moving focus");
return FocusDirectional {
scan,
target_index: None,
current: 0,
};
}
let direction = scan.direction.unwrap_or(FocusDirection::Down);
let wrap = scan.wrap;
log::trace!(
"[FocusDir] direction={:?}, {} widgets, focused={:?}",
direction,
scan.widgets.len(),
scan.focused
.map(|(idx, b)| (idx, b.x, b.y, b.width, b.height)),
);
if log::log_enabled!(log::Level::Trace) {
for spot in &scan.widgets {
log::trace!(
"[FocusDir] widget[{}] id={:?} x={:.0} y={:.0} w={:.0} h={:.0}",
spot.index,
spot.id,
spot.bounds.x,
spot.bounds.y,
spot.bounds.width,
spot.bounds.height,
);
}
}
let target_index = find_directional_target(&scan, direction, wrap);
log::trace!(
"[FocusDir] target_index={:?} (from {:?} → {:?})",
target_index,
scan.focused.map(|(i, _)| i),
target_index,
);
if let Some((_idx, bounds)) = scan.focused
&& target_index.is_some()
{
let c = bounds.center();
PREV_FOCUS_CENTER.set(Some((c.x, c.y)));
}
let last = target_index
.and_then(|ti| {
scan.widgets
.iter()
.find(|spot| spot.index == ti)
.map(|spot| (spot.index, spot.bounds))
})
.or(scan.focused);
if let Some(entry) = last {
LAST_FOCUSED.set(Some(entry));
}
FocusDirectional {
scan,
target_index,
current: 0,
}
}
operation::then(spatial_scan(direction, wrap), apply_directional)
}
fn find_directional_target(
scan: &SpatialScan,
direction: FocusDirection,
wrap: bool,
) -> Option<usize> {
if scan.widgets.is_empty() {
return None;
}
let Some((focused_idx, focused_bounds)) = scan.focused else {
if let Some((last_idx, last_bounds)) = LAST_FOCUSED.get()
&& scan
.widgets
.iter()
.any(|spot| spot.index == last_idx && bounds_match(spot.bounds, last_bounds))
{
return Some(last_idx);
}
return Some(edge_widget(scan, direction));
};
let origin = focused_bounds.center();
let prev_center = PREV_FOCUS_CENTER.get();
let group = scan
.widgets
.iter()
.find(|spot| spot.index == focused_idx)
.map_or_else(FocusGroup::default, |spot| spot.group);
let mut best: Option<(usize, bool, f32, f32, f32, f32, u32)> = None;
for &Spot {
index: idx,
bounds,
order,
..
} in &scan.widgets
{
if idx == focused_idx {
continue;
}
let candidate = bounds.center();
let in_direction = match direction {
FocusDirection::Up => candidate.y < origin.y,
FocusDirection::Down => candidate.y > origin.y,
FocusDirection::Left => candidate.x < origin.x,
FocusDirection::Right => candidate.x > origin.x,
};
if !in_direction {
continue;
}
if !group.horizontal_leaves_row
&& matches!(direction, FocusDirection::Left | FocusDirection::Right)
&& !shares_band(focused_bounds, bounds)
{
continue;
}
let (primary_dist, beam_overlap, cross_dist) = match direction {
FocusDirection::Up | FocusDirection::Down => {
let primary = edge_dist(
focused_bounds.y,
focused_bounds.y + focused_bounds.height,
bounds.y,
bounds.y + bounds.height,
);
let beam = axis_overlap(
focused_bounds.x,
focused_bounds.x + focused_bounds.width,
bounds.x,
bounds.x + bounds.width,
);
let cross = edge_dist(
focused_bounds.x,
focused_bounds.x + focused_bounds.width,
bounds.x,
bounds.x + bounds.width,
);
(primary, beam, cross)
}
FocusDirection::Left | FocusDirection::Right => {
let primary = edge_dist(
focused_bounds.x,
focused_bounds.x + focused_bounds.width,
bounds.x,
bounds.x + bounds.width,
);
let beam = axis_overlap(
focused_bounds.y,
focused_bounds.y + focused_bounds.height,
bounds.y,
bounds.y + bounds.height,
);
let cross = edge_dist(
focused_bounds.y,
focused_bounds.y + focused_bounds.height,
bounds.y,
bounds.y + bounds.height,
);
(primary, beam, cross)
}
};
let in_beam = beam_overlap > 0.0;
let history_dist = prev_center.map_or(f32::MAX, |(px, py)| match direction {
FocusDirection::Up | FocusDirection::Down => (candidate.x - px).abs(),
FocusDirection::Left | FocusDirection::Right => (candidate.y - py).abs(),
});
let is_better = match best {
None => true,
Some((_, best_in_beam, best_primary, best_beam, best_cross, best_hist, best_order)) => {
if in_beam != best_in_beam {
in_beam
} else if in_beam {
if order != best_order {
order < best_order
} else if (primary_dist - best_primary).abs() > 0.5 {
primary_dist < best_primary
} else if prev_center.is_some() {
history_dist < best_hist
} else {
beam_overlap > best_beam
}
} else {
(primary_dist + cross_dist * 3.0) < (best_primary + best_cross * 3.0)
}
}
};
if is_better {
best = Some((
idx,
in_beam,
primary_dist,
beam_overlap,
cross_dist,
history_dist,
order,
));
}
}
best.map_or_else(
|| {
if wrap {
Some(wrap_widget(scan, focused_idx, direction))
} else {
None
}
},
|(idx, _, _, _, _, _, _)| Some(idx),
)
}
fn shares_band(a: Rectangle, b: Rectangle) -> bool {
let a_center = a.y + a.height / 2.0;
let b_center = b.y + b.height / 2.0;
(a_center > b.y && a_center < b.y + b.height) || (b_center > a.y && b_center < a.y + a.height)
}
fn edge_dist(a_min: f32, a_max: f32, b_min: f32, b_max: f32) -> f32 {
if a_max <= b_min {
b_min - a_max
} else if b_max <= a_min {
a_min - b_max
} else {
0.0
}
}
fn axis_overlap(a_min: f32, a_max: f32, b_min: f32, b_max: f32) -> f32 {
(a_max.min(b_max) - a_min.max(b_min)).max(0.0)
}
fn bounds_match(a: Rectangle, b: Rectangle) -> bool {
const E: f32 = 1.0;
(a.x - b.x).abs() < E
&& (a.y - b.y).abs() < E
&& (a.width - b.width).abs() < E
&& (a.height - b.height).abs() < E
}
fn edge_widget(scan: &SpatialScan, direction: FocusDirection) -> usize {
let spot = scan
.widgets
.iter()
.max_by(|a, b| {
let val = |w: &Spot| -> f32 {
match direction {
FocusDirection::Up => w.bounds.y + w.bounds.height,
FocusDirection::Down => -(w.bounds.y),
FocusDirection::Left => w.bounds.x + w.bounds.width,
FocusDirection::Right => -(w.bounds.x),
}
};
val(a)
.partial_cmp(&val(b))
.unwrap_or(std::cmp::Ordering::Equal)
})
.unwrap_or(&scan.widgets[0]);
spot.index
}
fn wrap_widget(scan: &SpatialScan, focused_idx: usize, direction: FocusDirection) -> usize {
let has_distinct_positions = {
let tolerance = 1.0_f32;
let first = scan.widgets.first().map(|w| match direction {
FocusDirection::Left | FocusDirection::Right => w.bounds.x,
FocusDirection::Up | FocusDirection::Down => w.bounds.y,
});
first.is_some_and(|first_val| {
scan.widgets.iter().any(|w| {
let val = match direction {
FocusDirection::Left | FocusDirection::Right => w.bounds.x,
FocusDirection::Up | FocusDirection::Down => w.bounds.y,
};
(val - first_val).abs() > tolerance
})
})
};
if !has_distinct_positions {
return focused_idx;
}
let candidate = edge_widget(scan, direction);
if candidate == focused_idx {
focused_idx
} else {
candidate
}
}
#[cfg(test)]
mod tests {
use super::{FocusDirection, FocusGroup, SpatialScan, Spot, find_directional_target};
use crate::Rectangle;
const ROW_LOCKED: FocusGroup = FocusGroup {
horizontal_leaves_row: false,
};
const FREE: FocusGroup = FocusGroup {
horizontal_leaves_row: true,
};
fn rect(x: f32, y: f32, width: f32, height: f32) -> Rectangle {
Rectangle {
x,
y,
width,
height,
}
}
fn scan(widgets: &[Rectangle], focused: usize, group: FocusGroup) -> SpatialScan {
grouped_scan(
&widgets.iter().map(|&b| (b, group)).collect::<Vec<_>>(),
focused,
)
}
fn grouped_scan(widgets: &[(Rectangle, FocusGroup)], focused: usize) -> SpatialScan {
SpatialScan {
widgets: widgets
.iter()
.enumerate()
.map(|(index, &(bounds, group))| Spot {
index,
bounds,
id: None,
order: u32::MAX,
group,
})
.collect(),
focused: Some((focused, widgets[focused].0)),
total: widgets.len(),
..SpatialScan::default()
}
}
fn go(widgets: &[Rectangle], focused: usize, direction: FocusDirection) -> Option<usize> {
find_directional_target(&scan(widgets, focused, ROW_LOCKED), direction, false)
}
#[test]
fn a_horizontal_move_crosses_widgets_of_unequal_height() {
let menu = [
rect(64.0, 36.0, 64.0, 64.0), rect(160.0, 40.0, 56.0, 56.0), rect(248.0, 40.0, 56.0, 56.0), ];
assert_eq!(go(&menu, 0, FocusDirection::Right), Some(1));
assert_eq!(go(&menu, 1, FocusDirection::Right), Some(2));
assert_eq!(go(&menu, 2, FocusDirection::Left), Some(1));
}
#[test]
fn a_horizontal_move_stops_at_the_end_of_its_row() {
let home = [
rect(64.0, 36.0, 64.0, 64.0), rect(336.0, 40.0, 56.0, 56.0), rect(364.0, 247.0, 120.0, 65.0), rect(508.0, 247.0, 120.0, 65.0), rect(796.0, 247.0, 120.0, 65.0), rect(736.0, 400.0, 300.0, 220.0), ];
assert_eq!(go(&home, 2, FocusDirection::Left), None);
assert_eq!(go(&home, 4, FocusDirection::Right), None);
assert_eq!(go(&home, 2, FocusDirection::Right), Some(3));
assert_eq!(go(&home, 4, FocusDirection::Left), Some(3));
}
#[test]
fn a_grid_row_does_not_spill_into_the_row_below() {
let grid = [
rect(64.0, 400.0, 300.0, 220.0), rect(400.0, 400.0, 300.0, 220.0), rect(64.0, 660.0, 300.0, 220.0), rect(400.0, 660.0, 300.0, 220.0), rect(736.0, 660.0, 300.0, 220.0), ];
assert_eq!(go(&grid, 1, FocusDirection::Right), None);
assert_eq!(go(&grid, 0, FocusDirection::Right), Some(1));
}
#[test]
fn a_pixel_of_overlap_is_not_a_shared_row() {
let rows = [
rect(0.0, 0.0, 100.0, 100.0),
rect(200.0, 99.0, 100.0, 100.0),
];
assert_eq!(go(&rows, 0, FocusDirection::Right), None);
}
#[test]
fn a_vertical_move_still_crosses_to_an_unaligned_widget() {
let page = [
rect(64.0, 36.0, 64.0, 64.0), rect(900.0, 400.0, 300.0, 220.0), ];
assert_eq!(go(&page, 0, FocusDirection::Down), Some(1));
assert_eq!(go(&page, 1, FocusDirection::Up), Some(0));
}
#[test]
fn a_group_can_let_a_horizontal_move_leave_its_row() {
let panel = [
rect(64.0, 100.0, 300.0, 400.0), rect(400.0, 560.0, 200.0, 90.0), ];
let free = scan(&panel, 0, FREE);
assert_eq!(
find_directional_target(&free, FocusDirection::Right, false),
Some(1)
);
assert_eq!(go(&panel, 0, FocusDirection::Right), None);
}
#[test]
fn an_inner_group_overrides_the_panel_around_it() {
let page = [
(rect(64.0, 100.0, 300.0, 400.0), FREE), (rect(64.0, 560.0, 300.0, 200.0), ROW_LOCKED), (rect(400.0, 560.0, 300.0, 200.0), ROW_LOCKED), (rect(800.0, 900.0, 300.0, 200.0), FREE), ];
assert_eq!(
find_directional_target(&grouped_scan(&page, 1), FocusDirection::Right, false),
Some(2)
);
assert_eq!(
find_directional_target(&grouped_scan(&page, 2), FocusDirection::Right, false),
None
);
assert_eq!(
find_directional_target(&grouped_scan(&page, 0), FocusDirection::Right, false),
Some(2)
);
}
}