Add focus history and Alt-Tab window switching
This commit is contained in:
1 parent
a3431cab5b
commit
0162870cca
18 files changed
+571
-8
No files matched your search
@@ -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)));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user