Add focus history and Alt-Tab window switching

This commit is contained in:
Samuel Onoja committed 2026-10-01 16:00:23 +01:00
1 parent a3431cab5b
commit 0162870cca
18 files changed
+571 -8

No files matched your search

+144
View File
@@ -0,0 +1,144 @@
use ferese_layout::WindowId;
/// Most recent focus first. Layer surfaces and temporary selections do not enter this list.
#[derive(Debug, Default)]
pub struct FocusHistory {
windows: Vec<WindowId>,
}
impl FocusHistory {
pub fn record(&mut self, window: WindowId) {
if self.windows.first() == Some(&window) {
return;
}
self.remove(window);
self.windows.insert(0, window);
}
pub fn remove(&mut self, window: WindowId) {
self.windows.retain(|id| *id != window);
}
pub fn candidates(&self, current: Option<WindowId>, mut available: Vec<WindowId>) -> Vec<WindowId> {
available.sort_unstable_by_key(|id| id.0);
available.dedup();
let mut ordered = Vec::with_capacity(available.len());
for id in current
.into_iter()
.chain(self.windows.iter().copied())
.chain(available.iter().copied())
{
if available.contains(&id) && !ordered.contains(&id) {
ordered.push(id);
}
}
ordered
}
}
/// A frozen traversal order: previewing does not reorder focus history.
#[derive(Debug)]
pub struct FocusCycle {
windows: Vec<WindowId>,
selected: Option<WindowId>,
}
impl FocusCycle {
pub fn new(windows: Vec<WindowId>, current: Option<WindowId>) -> Self {
let selected = current.filter(|id| windows.contains(id));
Self { windows, selected }
}
pub fn windows(&self) -> &[WindowId] {
&self.windows
}
pub fn reconcile(&mut self, available: &[WindowId]) -> Option<WindowId> {
if self.selected.is_some_and(|id| available.contains(&id)) {
self.selected
} else {
self.advance(false, available)
}
}
pub fn advance(&mut self, reverse: bool, available: &[WindowId]) -> Option<WindowId> {
// Keep the old index when the selected window disappears so forward
// traversal selects its successor rather than skipping over it.
let old = self
.selected
.and_then(|id| self.windows.iter().position(|candidate| *candidate == id));
let before = old.map(|index| self.windows[..index].iter().filter(|id| available.contains(id)).count());
let survives = self.selected.is_some_and(|id| available.contains(&id));
self.windows.retain(|id| available.contains(id));
if self.windows.is_empty() {
self.selected = None;
return None;
}
let len = self.windows.len();
let index = match (before, reverse, survives) {
(Some(index), true, _) => (index + len - 1) % len,
(Some(index), false, true) => (index + 1) % len,
(Some(index), false, false) => index % len,
(None, true, _) => len - 1,
(None, false, _) => 0,
};
self.selected = Some(self.windows[index]);
self.selected
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn history_is_unique_filters_unavailable_and_last_focus_toggles() {
let mut history = FocusHistory::default();
for id in [1, 2, 3, 3] {
history.record(WindowId(id));
}
let available = vec![WindowId(1), WindowId(2), WindowId(3), WindowId(4)];
assert_eq!(
history.candidates(Some(WindowId(3)), available.clone()),
[3, 2, 1, 4].map(WindowId)
);
history.record(WindowId(2));
assert_eq!(history.candidates(Some(WindowId(2)), available)[1], WindowId(3));
history.remove(WindowId(3));
assert_eq!(
history.candidates(None, vec![WindowId(1), WindowId(4)]),
[1, 4].map(WindowId)
);
}
#[test]
fn cycle_freezes_order_wraps_reverses_and_skips_closed_windows() {
let available = [3, 2, 1].map(WindowId);
let mut cycle = FocusCycle::new(available.to_vec(), Some(WindowId(3)));
assert_eq!(cycle.advance(false, &available), Some(WindowId(2)));
assert_eq!(cycle.advance(false, &available), Some(WindowId(1)));
assert_eq!(cycle.advance(false, &available), Some(WindowId(3)));
assert_eq!(cycle.advance(true, &available), Some(WindowId(1)));
assert_eq!(cycle.advance(false, &[WindowId(3), WindowId(2)]), Some(WindowId(3)));
assert_eq!(cycle.advance(false, &[]), None);
}
#[test]
fn missing_current_focus_starts_at_most_recent_and_new_windows_do_not_join_cycle() {
let mut cycle = FocusCycle::new(vec![WindowId(2), WindowId(1)], None);
assert_eq!(
cycle.advance(false, &[WindowId(3), WindowId(2), WindowId(1)]),
Some(WindowId(2))
);
assert_eq!(cycle.advance(true, &[WindowId(2)]), Some(WindowId(2)));
}
#[test]
fn closing_the_preview_selects_its_successor_before_commit() {
let available = [3, 2, 1].map(WindowId);
let mut cycle = FocusCycle::new(available.to_vec(), Some(WindowId(3)));
assert_eq!(cycle.advance(false, &available), Some(WindowId(2)));
assert_eq!(cycle.reconcile(&[WindowId(3), WindowId(1)]), Some(WindowId(1)));
assert_eq!(cycle.reconcile(&[WindowId(3), WindowId(1)]), Some(WindowId(1)));
}
}