use crate::{
core::layout::LayoutStack,
pop_where,
pure::{
diff::{ScreenState, Snapshot},
geometry::{Rect, RelativeRect, RelativeTo},
workspace::check_workspace_invariants,
Position, Screen, Stack, Workspace,
},
stack, Error, Result, Xid,
};
use std::{
cmp::Ordering,
collections::{HashMap, VecDeque},
hash::Hash,
mem::{swap, take},
};
use tracing::debug;
#[derive(Default, Debug, Clone)]
pub struct StackSet<C>
where
C: Clone + PartialEq + Eq + Hash,
{
pub(crate) screens: Stack<Screen<C>>, pub(crate) hidden: VecDeque<Workspace<C>>, pub(crate) floating: HashMap<C, RelativeRect>, pub(crate) previous_tag: String, pub(crate) invisible_tags: Vec<String>, pub(crate) killed_clients: Vec<C>, }
impl<C> StackSet<C>
where
C: Clone + PartialEq + Eq + Hash,
{
pub fn try_new<I, J, T>(layouts: LayoutStack, ws_tags: I, screen_details: J) -> Result<Self>
where
T: Into<String>,
I: IntoIterator<Item = T>,
J: IntoIterator<Item = Rect>,
{
let workspaces: Vec<Workspace<C>> = ws_tags
.into_iter()
.enumerate()
.map(|(i, tag)| Workspace::new(i, tag, layouts.clone(), None))
.collect();
let screen_details: Vec<Rect> = screen_details.into_iter().collect();
Self::try_new_concrete(workspaces, screen_details, HashMap::new())
}
pub(crate) fn try_new_concrete(
mut workspaces: Vec<Workspace<C>>,
screen_details: Vec<Rect>,
floating: HashMap<C, RelativeRect>,
) -> Result<Self> {
check_workspace_invariants(&workspaces)?;
match (workspaces.len(), screen_details.len()) {
(_, 0) => return Err(Error::NoScreens),
(n_ws, n_screens) if n_ws < n_screens => {
return Err(Error::InsufficientWorkspaces { n_ws, n_screens })
}
_ => (),
}
let hidden: VecDeque<Workspace<C>> = workspaces
.split_off(screen_details.len())
.into_iter()
.collect();
let screens =
Stack::from_iter_unchecked(workspaces.into_iter().zip(screen_details).enumerate().map(
|(index, (workspace, r))| Screen {
workspace,
index,
r,
},
));
let previous_tag = screens.focus.workspace.tag.clone();
Ok(Self {
screens,
hidden,
floating,
previous_tag,
invisible_tags: vec![],
killed_clients: vec![],
})
}
pub fn focus_screen(&mut self, screen_index: usize) {
let current = self.screens.focus.index;
if current == screen_index {
return;
}
loop {
self.screens.focus_down();
if [current, screen_index].contains(&self.screens.focus.index) {
break;
}
}
}
fn update_previous_tag(&mut self, new: String) {
if self.invisible_tags.contains(&new) {
return;
}
self.previous_tag = new;
}
pub fn focus_tag(&mut self, tag: impl AsRef<str>) {
let tag = tag.as_ref();
if self.screens.focus.workspace.tag == tag || self.invisible_tags.iter().any(|t| t == tag) {
return;
}
if !self.try_cycle_screen_to_tag(tag) {
self.try_swap_on_screen_workspace_with_hidden(tag);
}
}
fn try_cycle_screen_to_tag(&mut self, tag: &str) -> bool {
let current_tag = self.screens.focus.workspace.tag.clone();
loop {
self.screens.focus_down();
match &self.screens.focus.workspace.tag {
t if t == tag => {
self.update_previous_tag(current_tag);
return true;
}
t if t == ¤t_tag => return false,
_ => (),
}
}
}
fn try_swap_on_screen_workspace_with_hidden(&mut self, tag: &str) {
if let Some(mut w) = pop_where!(self, hidden, |w: &Workspace<C>| w.tag == tag) {
self.update_previous_tag(self.screens.focus.workspace.tag.clone());
swap(&mut w, &mut self.screens.focus.workspace);
self.hidden.push_back(w);
}
}
fn try_swap_focused_workspace_with_tag(&mut self, tag: &str) -> bool {
if self.screens.focus.workspace.tag == tag {
return false;
}
let p = |s: &&mut Screen<C>| s.workspace.tag == tag;
let in_up = self.screens.up.iter_mut().find(p);
let in_down = self.screens.down.iter_mut().find(p);
if let Some(s) = in_up.or(in_down) {
swap(&mut self.screens.focus.workspace, &mut s.workspace);
return true;
}
false
}
pub fn pull_tag_to_screen(&mut self, tag: impl AsRef<str>) {
let tag = tag.as_ref();
if self.screens.focus.workspace.tag == tag {
return;
}
if !self.try_swap_focused_workspace_with_tag(tag) {
self.try_swap_on_screen_workspace_with_hidden(tag);
}
}
pub fn toggle_tag(&mut self) {
self.focus_tag(self.previous_tag.clone());
}
pub fn focus_client(&mut self, client: &C) {
if self.current_client() == Some(client) {
return; }
let tag = match self.tag_for_client(client) {
Some(tag) if self.invisible_tags.iter().any(|t| t == tag) => return,
None => return, Some(tag) => tag.to_string(),
};
self.focus_tag(&tag);
debug_assert_eq!(
self.current_tag(),
tag,
"attempt to focus unknown or invisible tag for client"
);
while self.current_client() != Some(client) {
self.focus_up()
}
}
pub fn insert(&mut self, client: C) {
self.insert_at(Position::default(), client)
}
pub fn insert_at(&mut self, pos: Position, client: C) {
if self.contains(&client) {
return;
}
self.modify(|current_stack| match current_stack {
Some(mut s) => {
s.insert_at(pos, client);
Some(s)
}
None => Some(stack!(client)),
})
}
pub(crate) fn float_unchecked<R: RelativeTo>(&mut self, client: C, r: R) {
let screen = self.screen_for_client(&client).expect("client to be known");
let r = r.relative_to(&screen.r);
debug!(?r, "setting floating position");
self.floating.insert(client, r);
}
pub fn sink(&mut self, client: &C) -> Option<Rect> {
self.floating
.remove(client)
.map(|rr| rr.applied_to(&self.screens.focus.r))
}
pub fn is_floating(&self, client: &C) -> bool {
self.floating.contains_key(client)
}
pub fn has_floating_windows(&self, tag: impl AsRef<str>) -> bool {
self.workspace(tag.as_ref())
.map(|w| w.clients().any(|id| self.floating.contains_key(id)))
.unwrap_or(false)
}
pub fn remove_client(&mut self, client: &C) -> Option<C> {
self.sink(client);
self.workspaces_mut()
.map(|w| w.remove(client))
.find(|opt| opt.is_some())
.flatten()
}
pub fn remove_focused(&mut self) -> Option<C> {
let client = self.current_client()?.clone();
self.remove_client(&client)
}
pub fn kill_focused(&mut self) {
if let Some(client) = self.remove_focused() {
self.killed_clients.push(client);
}
}
pub fn move_focused_to_tag(&mut self, tag: impl AsRef<str>) {
let tag = tag.as_ref();
if self.current_tag() == tag || !self.contains_tag(tag) {
return;
}
let c = match self.screens.focus.workspace.remove_focused() {
None => return,
Some(c) => c,
};
self.insert_as_focus_for(tag, c)
}
pub fn move_focused_to_screen(&mut self, screen: usize) {
if self.screens.focus.index == screen {
return;
}
let c = match self.screens.focus.workspace.remove_focused() {
None => return,
Some(c) => c,
};
if let Some(s) = self.screens.iter_mut().find(|s| s.index == screen) {
s.workspace.insert_as_focus(c)
}
}
pub fn move_client_to_tag(&mut self, client: &C, tag: impl AsRef<str>) {
let tag = tag.as_ref();
if !self.contains_tag(tag) {
return;
}
let maybe_removed = self
.workspaces_mut()
.map(|w| w.remove(client))
.find(|opt| opt.is_some())
.flatten();
let c = match maybe_removed {
None => return,
Some(c) => c,
};
self.insert_as_focus_for(tag, c)
}
pub fn move_client_to_current_tag(&mut self, client: &C) {
self.move_client_to_tag(client, self.screens.focus.workspace.tag.clone());
}
pub(crate) fn insert_as_focus_for(&mut self, tag: &str, c: C) {
self.modify_workspace(tag, |w| w.insert_as_focus(c));
}
pub fn contains_tag(&self, tag: &str) -> bool {
self.workspaces().any(|w| w.tag == tag)
}
pub fn ordered_tags(&self) -> Vec<String> {
let mut indexed: Vec<_> = self
.workspaces()
.map(|w| (w.id, w.tag.clone()))
.filter(|(_, t)| !self.invisible_tags.contains(t))
.collect();
indexed.sort_by_key(|(id, _)| *id);
indexed.into_iter().map(|(_, tag)| tag).collect()
}
pub fn ordered_workspaces(&self) -> impl Iterator<Item = &Workspace<C>> {
let mut wss: Vec<_> = self
.workspaces()
.filter(|w| !self.invisible_tags.contains(&w.tag))
.collect();
wss.sort_by_key(|w| w.id());
wss.into_iter()
}
fn focus_adjacent_workspace(&mut self, tags: Vec<String>) {
let cur_tag = self.current_tag();
let mut it = tags.iter().skip_while(|t| *t != cur_tag);
if let Some(new_tag) = it.nth(1).or(tags.first()) {
self.pull_tag_to_screen(new_tag)
}
}
pub fn focus_next_workspace(&mut self) {
self.focus_adjacent_workspace(self.ordered_tags())
}
pub fn focus_previous_workspace(&mut self) {
let mut tags = self.ordered_tags();
tags.reverse();
self.focus_adjacent_workspace(tags)
}
pub fn tag_for_screen(&self, index: usize) -> Option<&str> {
self.screens()
.find(|s| s.index == index)
.map(|s| s.workspace.tag.as_str())
}
pub fn tag_for_client(&self, client: &C) -> Option<&str> {
self.workspaces()
.find(|w| {
w.stack
.as_ref()
.map(|s| s.iter().any(|elem| elem == client))
.unwrap_or(false)
})
.map(|w| w.tag.as_str())
}
pub fn screen_for_client(&self, client: &C) -> Option<&Screen<C>> {
self.screens.iter().find(|s| s.workspace.contains(client))
}
pub fn tag_for_workspace_id(&self, id: usize) -> Option<String> {
self.workspaces()
.find(|w| w.id == id)
.map(|w| w.tag.clone())
}
pub fn contains(&self, client: &C) -> bool {
self.clients().any(|c| c == client)
}
pub fn current_client(&self) -> Option<&C> {
self.screens
.focus
.workspace
.stack
.as_ref()
.map(|s| &s.focus)
}
pub fn current_screen(&self) -> &Screen<C> {
&self.screens.focus
}
pub fn current_workspace(&self) -> &Workspace<C> {
&self.screens.focus.workspace
}
pub fn current_workspace_mut(&mut self) -> &mut Workspace<C> {
&mut self.screens.focus.workspace
}
pub fn current_stack(&self) -> Option<&Stack<C>> {
self.screens.focus.workspace.stack.as_ref()
}
pub fn current_tag(&self) -> &str {
&self.screens.focus.workspace.tag
}
pub fn try_rename_workspace(
&mut self,
old_tag: impl AsRef<str>,
new_tag: impl Into<String>,
) -> Result<()> {
let new_tag = new_tag.into();
if old_tag.as_ref() == new_tag {
return Ok(());
}
if self.contains_tag(&new_tag) {
return Err(Error::NonUniqueTags {
tags: vec![new_tag],
});
}
match self.workspace_mut(old_tag.as_ref()) {
Some(ws) => {
ws.tag = new_tag;
Ok(())
}
None => Err(Error::UnknownWorkspace(old_tag.as_ref().to_string())),
}
}
pub fn add_workspace<T>(&mut self, tag: T, layouts: LayoutStack) -> Result<()>
where
T: Into<String>,
{
let tag = tag.into();
if self.contains_tag(&tag) {
return Err(Error::NonUniqueTags { tags: vec![tag] });
}
let id = self
.workspaces()
.map(|w| w.id)
.max()
.expect("at least one workspace")
+ 1;
let ws = Workspace::new(id, tag, layouts, None);
self.hidden.push_front(ws);
Ok(())
}
pub fn remove_workspace(&mut self, tag: impl AsRef<str>) -> Option<Workspace<C>> {
let tag = tag.as_ref();
for s in self.screens.iter_mut() {
if s.workspace.tag != tag {
continue;
}
let mut ws = self.hidden.pop_front()?;
swap(&mut ws, &mut s.workspace);
if self.previous_tag == tag {
self.previous_tag = s.workspace.tag.clone();
}
return Some(ws);
}
let opt = pop_where!(self, hidden, |w: &Workspace<C>| w.tag == tag);
if self.previous_tag == tag {
self.previous_tag = self.screens.focus.workspace.tag.clone();
}
opt
}
pub fn add_invisible_workspace<T>(&mut self, tag: T) -> Result<()>
where
T: Into<String>,
{
let tag = tag.into();
self.add_workspace(tag.clone(), LayoutStack::default())?;
self.invisible_tags.push(tag);
Ok(())
}
pub fn workspace(&self, tag: &str) -> Option<&Workspace<C>> {
self.workspaces().find(|w| w.tag == tag)
}
pub fn workspace_mut(&mut self, tag: &str) -> Option<&mut Workspace<C>> {
self.workspaces_mut().find(|w| w.tag == tag)
}
pub fn next_layout(&mut self) {
self.screens.focus.workspace.next_layout()
}
pub fn previous_layout(&mut self) {
self.screens.focus.workspace.previous_layout()
}
pub fn set_layout_by_name(&mut self, layout: impl AsRef<str>) {
self.screens
.focus
.workspace
.set_layout_by_name(layout.as_ref())
}
pub fn next_screen(&mut self) {
if self.screens.len() == 1 {
return;
}
self.update_previous_tag(self.screens.focus.workspace.tag.clone());
self.screens.focus_down();
}
pub fn previous_screen(&mut self) {
if self.screens.len() == 1 {
return;
}
self.update_previous_tag(self.screens.focus.workspace.tag.clone());
self.screens.focus_up();
}
pub fn drag_workspace_forward(&mut self) {
if self.screens.len() == 1 {
return;
}
let true_previous_tag = self.previous_tag.clone();
self.next_screen();
self.try_swap_focused_workspace_with_tag(&self.previous_tag.clone());
self.previous_tag = true_previous_tag;
}
pub fn drag_workspace_backward(&mut self) {
if self.screens.len() == 1 {
return;
}
let true_previous_tag = self.previous_tag.clone();
self.previous_screen();
self.try_swap_focused_workspace_with_tag(&self.previous_tag.clone());
self.previous_tag = true_previous_tag;
}
pub fn with<T, F>(&self, default: T, f: F) -> T
where
F: Fn(&Stack<C>) -> T,
{
self.current_stack().map(f).unwrap_or_else(|| default)
}
pub fn modify<F>(&mut self, f: F)
where
F: FnOnce(Option<Stack<C>>) -> Option<Stack<C>>,
{
self.screens.focus.workspace.stack = f(take(&mut self.screens.focus.workspace.stack));
}
pub fn modify_occupied<F>(&mut self, f: F)
where
F: FnOnce(Stack<C>) -> Stack<C>,
{
self.modify(|s| s.map(f))
}
fn modify_workspace<F>(&mut self, tag: &str, f: F)
where
F: FnOnce(&mut Workspace<C>),
{
self.workspaces_mut().find(|w| w.tag == tag).map(f);
}
pub fn screens(&self) -> impl Iterator<Item = &Screen<C>> {
self.screens.iter()
}
pub fn screens_mut(&mut self) -> impl Iterator<Item = &mut Screen<C>> {
self.screens.iter_mut()
}
pub fn workspaces(&self) -> impl Iterator<Item = &Workspace<C>> {
self.screens
.iter()
.map(|s| &s.workspace)
.chain(self.hidden.iter())
}
pub fn non_hidden_workspaces(&self) -> impl Iterator<Item = &Workspace<C>> {
self.workspaces()
.filter(|w| !self.invisible_tags.contains(&w.tag))
}
pub fn workspaces_mut(&mut self) -> impl Iterator<Item = &mut Workspace<C>> {
self.screens
.iter_mut()
.map(|s| &mut s.workspace)
.chain(self.hidden.iter_mut())
}
pub fn on_screen_workspaces(&self) -> impl Iterator<Item = &Workspace<C>> {
self.screens.iter().map(|s| &s.workspace)
}
pub fn hidden_workspaces(&self) -> impl Iterator<Item = &Workspace<C>> {
self.hidden.iter()
}
pub fn hidden_workspaces_mut(&mut self) -> impl Iterator<Item = &mut Workspace<C>> {
self.hidden.iter_mut()
}
pub fn clients(&self) -> impl Iterator<Item = &C> {
self.workspaces().flat_map(|w| w.clients())
}
pub fn on_screen_workspace_clients(&self) -> impl Iterator<Item = &C> {
self.on_screen_workspaces().flat_map(|w| w.clients())
}
pub fn hidden_workspace_clients(&self) -> impl Iterator<Item = &C> {
self.hidden_workspaces().flat_map(|w| w.clients())
}
}
#[cfg(test)]
impl StackSet<Xid> {
pub(crate) fn visible_client_positions(&self) -> Vec<(Xid, Rect)> {
let mut s = crate::core::State {
client_set: self.clone(),
config: Default::default(),
extensions: anymap::AnyMap::new(),
root: Xid(0),
mapped: Default::default(),
pending_unmap: Default::default(),
current_event: None,
diff: Default::default(),
running: false,
held_mouse_state: None,
};
s.visible_client_positions(&crate::x::StubXConn)
}
pub(crate) fn position_and_snapshot(&mut self) -> Snapshot<Xid> {
let positions = self.visible_client_positions();
self.snapshot(positions)
}
}
impl StackSet<Xid> {
pub fn float(&mut self, client: Xid, r: Rect) -> Result<()> {
if !self.contains(&client) {
return Err(Error::UnknownClient(client));
}
if self.screen_for_client(&client).is_none() {
return Err(Error::ClientIsNotVisible(client));
}
self.float_unchecked(client, r);
Ok(())
}
pub fn toggle_floating_state(&mut self, client: Xid, r: Rect) -> Result<Option<Rect>> {
let rect = if self.is_floating(&client) {
if self.screen_for_client(&client).is_none() {
return Err(Error::ClientIsNotVisible(client));
}
self.sink(&client)
} else {
self.float(client, r)?;
None
};
Ok(rect)
}
pub(crate) fn update_screens(&mut self, rects: Vec<Rect>) -> Result<()> {
let n_old = self.screens.len();
let n_new = rects.len();
if n_new == 0 {
return Err(Error::NoScreens);
}
match n_new.cmp(&n_old) {
Ordering::Equal => (),
Ordering::Greater => {
let padding = self.take_from_hidden(n_new - n_old);
for (n, w) in padding.into_iter().enumerate() {
self.screens.insert_at(
Position::Tail,
Screen {
workspace: w,
index: n_old + n,
r: Rect::default(),
},
);
}
}
Ordering::Less => {
let mut raw = take(&mut self.screens).flatten();
let removed = raw.split_off(n_new);
self.hidden.extend(removed.into_iter().map(|s| s.workspace));
self.screens = Stack::from_iter_unchecked(raw);
}
}
for (s, r) in self.screens.iter_mut().zip(rects) {
s.r = r;
}
Ok(())
}
fn take_from_hidden(&mut self, n: usize) -> Vec<Workspace<Xid>> {
let next_id = self.workspaces().map(|w| w.id).max().unwrap_or(0) + 1;
let mut tmp = Vec::with_capacity(self.hidden.len());
let mut hidden = VecDeque::new();
for w in take(&mut self.hidden) {
if self.invisible_tags.contains(&w.tag) {
hidden.push_front(w);
} else {
tmp.push(w);
}
}
tmp.sort_by_key(|w| w.id);
if tmp.len() < n {
for m in 0..(n - tmp.len()) {
tmp.push(Workspace::new_default(next_id + m));
}
} else {
let extra = tmp.split_off(n);
hidden.extend(extra);
}
self.hidden = hidden;
tmp
}
}
impl<C> StackSet<C>
where
C: Copy + Clone + PartialEq + Eq + Hash,
{
pub fn snapshot(&mut self, positions: Vec<(C, Rect)>) -> Snapshot<C> {
let visible = self
.screens
.unravel()
.skip(1) .map(ScreenState::from)
.collect();
Snapshot {
focused_client: self.current_client().copied(),
focused: ScreenState::from(&self.screens.focus),
visible,
positions,
hidden_clients: self.hidden_workspace_clients().copied().collect(),
killed_clients: take(&mut self.killed_clients),
}
}
}
macro_rules! defer_to_current_stack {
($(
$(#[$doc_str:meta])*
$method:ident
),+) => {
impl<C> StackSet<C>
where
C: Clone + PartialEq + Eq + Hash
{
$(
$(#[$doc_str])*
pub fn $method(&mut self) {
if let Some(ref mut stack) = self.screens.focus.workspace.stack {
stack.$method();
}
}
)+
}
}
}
defer_to_current_stack!(
focus_up,
focus_down,
swap_up,
swap_down,
rotate_up,
rotate_down,
rotate_focus_to_head,
focus_head,
swap_focus_and_head
);
#[cfg(test)]
pub mod tests {
use super::*;
use simple_test_case::test_case;
fn _test_stack_set<C>(n_tags: usize, n_screens: usize) -> StackSet<C>
where
C: Copy + Clone + PartialEq + Eq + Hash,
{
let tags = (1..=n_tags).map(|n| n.to_string());
let screens: Vec<Rect> = (0..(n_screens as u32))
.map(|k| Rect::new(k as i32 * 1000, k as i32 * 2000, 1000, 2000))
.collect();
StackSet::try_new(LayoutStack::default(), tags, screens).unwrap()
}
pub fn test_stack_set(n_tags: usize, n_screens: usize) -> StackSet<u8> {
_test_stack_set(n_tags, n_screens)
}
pub fn test_xid_stack_set(n_tags: usize, n_screens: usize) -> StackSet<Xid> {
_test_stack_set(n_tags, n_screens)
}
pub fn test_stack_set_with_stacks<C>(stacks: Vec<Option<Stack<C>>>, n: usize) -> StackSet<C>
where
C: Copy + Clone + PartialEq + Eq + Hash,
{
let workspaces: Vec<Workspace<C>> = stacks
.into_iter()
.enumerate()
.map(|(i, s)| Workspace::new(i, (i + 1).to_string(), LayoutStack::default(), s))
.collect();
match StackSet::try_new_concrete(
workspaces,
(0..(n as u32))
.map(|k| Rect::new(k as i32 * 1000, k as i32 * 2000, 1000, 2000))
.collect(),
HashMap::new(),
) {
Ok(s) => s,
Err(e) => panic!("{e}"),
}
}
#[test_case("1", &["1", "2"]; "current focused workspace")]
#[test_case("2", &["1", "2"]; "visible on other screen")]
#[test_case("3", &["3", "2"]; "currently hidden")]
#[test]
fn focus_tag_sets_correct_visible_workspaces(target: &str, vis: &[&str]) {
let mut s = test_stack_set(5, 2);
s.focus_tag(target);
let visible_tags: Vec<&str> = s.screens().map(|s| s.workspace.tag.as_ref()).collect();
assert_eq!(s.screens.focus.workspace.tag, target);
assert_eq!(visible_tags, vis);
}
#[test]
fn invisible_tags_cant_be_focused() {
let mut s = test_stack_set(5, 2);
let visible_tags: Vec<&str> = s.screens().map(|s| s.workspace.tag.as_ref()).collect();
assert_eq!(s.screens.focus.workspace.tag, "1");
assert_eq!(visible_tags, &["1", "2"]);
s.add_invisible_workspace("invisible").unwrap();
s.focus_tag("invisible");
let visible_tags: Vec<&str> = s.screens().map(|s| s.workspace.tag.as_ref()).collect();
assert_eq!(s.screens.focus.workspace.tag, "1");
assert_eq!(visible_tags, &["1", "2"]);
}
#[test_case("1", "2", "2", Some("1"); "focused")]
#[test_case("2", "1", "1", Some("2"); "ws on other screen")]
#[test_case("3", "1", "1", Some("3"); "hidden")]
#[test_case("1", "1", "3", Some("1"); "focused when that is previous tag")]
#[test_case("3", "3", "1", Some("3"); "hidden when that is previous tag")]
#[test_case("?", "7", "7", None; "unknown tag")]
#[test]
fn remove_workspace_works(
tag: &str,
prev_tag: &str,
new_prev_tag: &str,
expected: Option<&str>,
) {
let mut s = test_stack_set(5, 2);
s.previous_tag = prev_tag.to_string();
let maybe_ws = s.remove_workspace(tag);
assert_eq!(maybe_ws.map(|w| w.tag).as_deref(), expected);
assert_eq!(s.previous_tag, new_prev_tag);
}
#[derive(Debug, PartialEq)]
enum RmWs {
Valid,
Unknown,
Conflict,
}
#[test_case("1", "foo", RmWs::Valid; "known to new")]
#[test_case("1", "1", RmWs::Valid; "known to itself")]
#[test_case("?", "foo", RmWs::Unknown; "unknown")]
#[test_case("1", "2", RmWs::Conflict; "conflicting")]
#[test_case("?", "1", RmWs::Conflict; "unknown to conflicting")]
#[test_case("invisible", "foo", RmWs::Valid; "invisible to new")]
#[test_case("invisible", "1", RmWs::Conflict; "invisible to conflicting")]
#[test]
fn try_rename_workspace_works(old_tag: &str, new_tag: &str, expected: RmWs) {
let mut s = test_stack_set(5, 2);
s.add_invisible_workspace("invisible").unwrap();
let res = s.try_rename_workspace(old_tag, new_tag);
match res {
Ok(_) => {
assert_eq!(expected, RmWs::Valid);
assert!(s.contains_tag(new_tag), "should contain the new tag");
if old_tag != new_tag {
assert!(!s.contains_tag(old_tag), "should not contain the old tag");
}
}
Err(Error::UnknownWorkspace(_)) => assert_eq!(expected, RmWs::Unknown),
Err(Error::NonUniqueTags { .. }) => assert_eq!(expected, RmWs::Conflict),
_ => panic!("unexpected error returned: {res:?}"),
}
}
#[test]
fn clients_on_invisible_workspaces_cant_be_focused() {
let mut s = test_stack_set_with_stacks(
vec![
Some(stack!([1, 2], 3, [4, 5])),
Some(stack!(6, [7, 8])),
Some(stack!([9], 10)),
],
1,
);
s.add_invisible_workspace("invisible").unwrap();
assert_eq!(s.current_client(), Some(&3));
s.focus_client(&7);
assert_eq!(s.current_client(), Some(&7));
s.move_client_to_tag(&1, "invisible");
s.focus_client(&1);
assert_eq!(s.current_client(), Some(&7));
}
#[test_case(0, Some("1"), Some("3"); "initial focus")]
#[test_case(1, Some("2"), Some("2"); "other screen")]
#[test_case(2, None, None; "out of bounds")]
#[test]
fn tag_for_screen_works(index: usize, before: Option<&str>, after: Option<&str>) {
let mut s = test_stack_set(5, 2);
assert_eq!(s.tag_for_screen(index), before);
s.focus_tag("3");
assert_eq!(s.tag_for_screen(index), after);
}
#[test_case(5, Some("1"); "in down")]
#[test_case(6, Some("2"); "focus")]
#[test_case(9, Some("3"); "in up")]
#[test_case(42, None; "unknown")]
#[test]
fn tag_for_client_works(client: u8, expected: Option<&str>) {
let s = test_stack_set_with_stacks(
vec![
Some(stack!([1, 2], 3, [4, 5])),
Some(stack!(6, [7, 8])),
Some(stack!([9], 10)),
],
1,
);
assert_eq!(s.tag_for_client(&client), expected);
}
#[test_case(None; "empty current stack")]
#[test_case(Some(stack!(1)); "current stack with one element")]
#[test_case(Some(stack!([2], 1)); "current stack with up")]
#[test_case(Some(stack!(1, [3])); "current stack with down")]
#[test_case(Some(stack!([2], 1, [3])); "current stack with up and down")]
#[test]
fn insert(stack: Option<Stack<u8>>) {
let mut s = test_stack_set_with_stacks(vec![stack], 1);
s.insert(42);
assert!(s.contains(&42))
}
fn test_iter_stack_set() -> StackSet<u8> {
test_stack_set_with_stacks(
vec![
Some(stack!(1)),
Some(stack!([2], 3)),
Some(stack!(4, [5])),
None,
Some(stack!([6], 7, [8])),
],
3,
)
}
#[test]
fn iter_screens_returns_all_screens() {
let s = test_iter_stack_set();
let mut screen_indices: Vec<usize> = s.screens().map(|s| s.index).collect();
screen_indices.sort();
assert_eq!(screen_indices, vec![0, 1, 2])
}
#[test]
fn iter_screens_mut_returns_all_screens() {
let mut s = test_iter_stack_set();
let mut screen_indices: Vec<usize> = s.screens_mut().map(|s| s.index).collect();
screen_indices.sort();
assert_eq!(screen_indices, vec![0, 1, 2])
}
#[test]
fn iter_workspaces_returns_all_workspaces() {
let s = test_iter_stack_set();
let mut tags: Vec<&str> = s.workspaces().map(|w| w.tag.as_str()).collect();
tags.sort();
assert_eq!(tags, vec!["1", "2", "3", "4", "5"])
}
#[test]
fn iter_workspaces_mut_returns_all_workspaces() {
let mut s = test_iter_stack_set();
let mut tags: Vec<&str> = s.workspaces_mut().map(|w| w.tag.as_str()).collect();
tags.sort();
assert_eq!(tags, vec!["1", "2", "3", "4", "5"])
}
#[test]
fn iter_clients_returns_all_clients() {
let s = test_iter_stack_set();
let mut clients: Vec<u8> = s.clients().copied().collect();
clients.sort();
assert_eq!(clients, vec![1, 2, 3, 4, 5, 6, 7, 8])
}
#[test_case(stack!(1); "current stack with one element")]
#[test_case(stack!([2], 1); "current stack with up")]
#[test_case(stack!(1, [3]); "current stack with down")]
#[test_case(stack!([2], 1, [3]); "current stack with up and down")]
#[test]
fn contains(stack: Stack<u8>) {
let s = test_stack_set_with_stacks(vec![Some(stack)], 1);
assert!(s.contains(&1))
}
#[test]
fn changing_workspace_retains_clients() {
let mut s = test_stack_set_with_stacks(vec![Some(stack!(1)), Some(stack!(2, 3)), None], 1);
let clients = |s: &StackSet<u8>| {
let mut cs: Vec<_> = s.clients().copied().collect();
cs.sort();
cs
};
assert_eq!(clients(&s), vec![1, 2, 3]);
s.focus_tag("2");
assert_eq!(clients(&s), vec![1, 2, 3]);
}
#[test_case(true, 1; "forward")]
#[test_case(false, 2; "backward")]
#[test]
fn screen_change_focuses_new_screen(forward: bool, expected_index: usize) {
let mut s = test_stack_set(5, 3);
assert_eq!(s.current_screen().index(), 0);
if forward {
s.next_screen();
} else {
s.previous_screen();
}
assert_eq!(s.current_screen().index(), expected_index);
}
#[test_case(1, true, "1"; "single screen forward")]
#[test_case(1, false, "1"; "single screen backward")]
#[test_case(2, true, "3"; "two screens forward")]
#[test_case(2, false, "3"; "two screens backward")]
#[test]
fn screen_change_sets_expected_previous_tag(n_screens: usize, forward: bool, tag: &str) {
let mut s = test_stack_set(5, n_screens);
s.focus_tag("3");
assert_eq!(s.current_tag(), "3");
assert_eq!(s.previous_tag, "1");
if forward {
s.next_screen();
} else {
s.previous_screen();
}
assert_eq!(s.previous_tag, tag);
}
#[test_case(true, 1; "forward")]
#[test_case(false, 2; "backward")]
#[test]
fn drag_workspace_focuses_new_screen(forward: bool, expected_index: usize) {
let mut s = test_stack_set(5, 3);
assert_eq!(s.screens.focus.workspace.tag, "1");
assert_eq!(s.screens.focus.index, 0);
if forward {
s.drag_workspace_forward();
} else {
s.drag_workspace_backward();
}
assert_eq!(s.screens.focus.workspace.tag, "1");
assert_eq!(s.screens.focus.index, expected_index);
}
#[test_case(1, true; "single screen forward")]
#[test_case(1, false; "single screen backward")]
#[test_case(2, true; "two screens forward")]
#[test_case(2, false; "two screens backward")]
#[test]
fn drag_workspace_maintains_previous_tag(n_screens: usize, forward: bool) {
let mut s = test_stack_set(5, n_screens);
s.focus_tag("3");
"PREVIOUS".clone_into(&mut s.previous_tag);
assert_eq!(s.screens.focus.workspace.tag, "3");
assert_eq!(s.previous_tag, "PREVIOUS");
if forward {
s.drag_workspace_forward();
} else {
s.drag_workspace_backward();
}
assert_eq!(s.screens.focus.workspace.tag, "3");
assert_eq!(s.previous_tag, "PREVIOUS");
}
#[test_case("2", true, "3"; "forward non-wrapping")]
#[test_case("5", true, "1"; "forward wrapping")]
#[test_case("3", false, "2"; "backward non-wrapping")]
#[test_case("1", false, "5"; "backward wrapping")]
#[test]
fn focus_next_prev_workspace_identifies_the_correct_tag(
initial_tag: &str,
next: bool,
expected_tag: &str,
) {
let mut s = test_stack_set(5, 3);
s.focus_tag(initial_tag);
if next {
s.focus_next_workspace();
} else {
s.focus_previous_workspace();
}
assert_eq!(s.current_tag(), expected_tag);
}
#[test]
fn floating_layer_clients_hold_focus() {
let mut s = test_stack_set(5, 3);
for n in 1..5 {
s.insert(n);
}
s.float_unchecked(4, Rect::default());
assert_eq!(s.current_client(), Some(&4));
}
#[test_case("1", 0, Ok(None); "non floating visible")]
#[test_case("1", 1, Ok(Some(Rect::new(0, 0, 10, 10))); "floating visible")]
#[test_case("1", 42, Err(Error::UnknownClient(Xid(42))); "unknown client")]
#[test_case("2", 1, Err(Error::ClientIsNotVisible(Xid(1))); "floating client not visible")]
#[test_case("2", 0, Err(Error::ClientIsNotVisible(Xid(0))); "non floating client not visible")]
#[test]
fn toggle_floating_state(focused_tag: &str, client: u32, expected: Result<Option<Rect>>) {
let mut ss: StackSet<Xid> = StackSet::try_new(
LayoutStack::default(),
["1", "2", "3"],
vec![Rect::new(0, 0, 10, 10)],
)
.expect("enough workspaces to cover the number of initial screens");
ss.insert(Xid(0));
ss.insert(Xid(1));
ss.float_unchecked(Xid(1), Rect::new(0, 0, 10, 10));
ss.focus_tag(focused_tag);
let res = ss.toggle_floating_state(Xid(client), Rect::new(1, 2, 3, 4));
match (expected, res) {
(Ok(None), Ok(None)) => (),
(Ok(Some(r1)), Ok(Some(r2))) => assert_eq!(r1, r2),
(Err(Error::UnknownClient(c1)), Err(Error::UnknownClient(c2))) => {
assert_eq!(c1, c2, "unknown client")
}
(Err(Error::ClientIsNotVisible(c1)), Err(Error::ClientIsNotVisible(c2))) => {
assert_eq!(c1, c2, "client not visible")
}
(Err(e1), Err(e2)) => panic!("expected {e1:?} but got {e2:?}"),
(Ok(Some(r)), Ok(None)) => panic!("expected floating position {r:?} but got None"),
(Ok(None), Ok(Some(r))) => panic!("expected None but got floating position {r:?}"),
(Err(e), Ok(r)) => panic!("expected error {e:?} but got {r:?}"),
(Ok(_), Err(e)) => panic!("unexpected error {e:?}"),
}
}
#[test_case(1, "1"; "current focus to current tag")]
#[test_case(2, "1"; "from current tag to current tag")]
#[test_case(6, "1"; "from other tag to current tag")]
#[test_case(6, "2"; "from other tag to same tag")]
#[test_case(0, "2"; "from current tag to other tag")]
#[test_case(7, "3"; "from other tag to other tag")]
#[test_case(7, "4"; "from other tag to empty tag")]
#[test]
fn move_client_to_tag(client: u8, tag: &str) {
let mut s = test_stack_set_with_stacks(
vec![
Some(stack!([0], 1, [2, 3])),
Some(stack!([6, 7], 8)),
Some(stack!(4, [5])),
None,
],
1,
);
s.move_client_to_tag(&client, tag);
assert_eq!(s.workspace(tag).unwrap().focus(), Some(&client));
}
fn focused_tags(ss: &StackSet<Xid>) -> Vec<&String> {
ss.screens.iter().map(|s| &s.workspace.tag).collect()
}
#[test_case(1, 1, 0, vec!["1"], vec!["1"]; "single to single")]
#[test_case(1, 2, 0, vec!["1"], vec!["1", "2"]; "single to multiple no padding")]
#[test_case(1, 3, 0, vec!["1"], vec!["1", "2", "WS-3"]; "single to multiple with padding")]
#[test_case(2, 1, 0, vec!["1", "2"], vec!["1"]; "multiple to single")]
#[test_case(2, 2, 1, vec!["1", "2"], vec!["1", "2"]; "multiple to same count")]
#[test_case(2, 3, 1, vec!["1", "2"], vec!["1", "2", "WS-3"]; "multiple to more with padding")]
#[test]
fn update_screens(
n_before: usize,
n_after: usize,
focus_after: usize,
tags_before: Vec<&str>,
tags_after: Vec<&str>,
) {
let mut ss: StackSet<Xid> = StackSet::try_new(
LayoutStack::default(),
["1", "2"],
vec![Rect::default(); n_before],
)
.expect("enough workspaces to cover the number of initial screens");
ss.add_invisible_workspace("INVISIBLE")
.expect("no tag collisions");
ss.focus_screen(n_before - 1);
assert_eq!(ss.screens.len(), n_before);
assert_eq!(focused_tags(&ss), tags_before);
ss.update_screens(vec![Rect::default(); n_after]).unwrap();
assert_eq!(ss.screens.len(), n_after);
assert_eq!(ss.screens.focus.index, focus_after);
assert_eq!(focused_tags(&ss), tags_after);
let expected = std::cmp::max(2, n_after) + 1;
assert_eq!(ss.workspaces().count(), expected);
}
#[test]
fn update_screens_with_empty_vec_is_an_error() {
let mut ss: StackSet<Xid> =
StackSet::try_new(LayoutStack::default(), ["1", "2"], vec![Rect::default(); 2])
.expect("enough workspaces to cover the number of screens");
let res = ss.update_screens(vec![]);
assert!(matches!(res, Err(Error::NoScreens)));
}
}
#[cfg(test)]
#[allow(missing_docs)]
mod quickcheck_tests {
use super::{tests::test_stack_set_with_stacks, *};
use quickcheck::{Arbitrary, Gen};
use quickcheck_macros::quickcheck;
use std::collections::HashSet;
impl<C> Stack<C>
where
C: Copy + Clone + PartialEq + Eq + Hash,
{
pub fn try_from_arbitrary_vec(mut up: Vec<C>, g: &mut Gen) -> Option<Self> {
let focus = match up.len() {
0 => return None,
1 => return Some(stack!(up.remove(0))),
_ => up.remove(0),
};
let split_at = usize::arbitrary(g) % (up.len());
let down = up.split_off(split_at);
Some(Self::new(up, focus, down))
}
}
impl StackSet<Xid> {
pub fn minimal_unknown_client(&self) -> Xid {
let mut c = 0;
while self.contains(&Xid(c)) {
c += 1;
}
Xid(c)
}
pub fn first_hidden_tag(&self) -> Option<String> {
self.hidden.iter().map(|w| w.tag.clone()).next()
}
pub fn last_tag(&self) -> String {
self.workspaces()
.last()
.expect("at least one workspace")
.tag
.clone()
}
pub fn last_visible_client(&self) -> Option<&Xid> {
self.screens
.down
.back()
.unwrap_or(&self.screens.focus)
.workspace
.stack
.iter()
.flat_map(|s| s.iter())
.last()
}
}
impl Arbitrary for Xid {
fn arbitrary(g: &mut Gen) -> Self {
Xid(u32::arbitrary(g))
}
}
impl Arbitrary for StackSet<Xid> {
fn arbitrary(g: &mut Gen) -> Self {
let n_stacks = usize::arbitrary(g) % 10;
let mut stacks = Vec::with_capacity(n_stacks);
let mut clients: Vec<Xid> = HashSet::<Xid>::arbitrary(g).into_iter().collect();
for _ in 0..n_stacks {
if clients.is_empty() {
stacks.push(None);
continue;
}
let split_at = usize::arbitrary(g) % (clients.len());
let stack_clients = clients.split_off(split_at);
stacks.push(Stack::try_from_arbitrary_vec(stack_clients, g));
}
stacks.push(Stack::try_from_arbitrary_vec(clients, g));
let n_screens = if n_stacks == 0 {
1
} else {
std::cmp::max(usize::arbitrary(g) % n_stacks, 1)
};
test_stack_set_with_stacks(stacks, n_screens)
}
}
#[quickcheck]
fn insert_pushes_to_current_stack(mut s: StackSet<Xid>) -> bool {
let new_focus = s.minimal_unknown_client();
s.insert(new_focus);
s.current_client() == Some(&new_focus)
}
#[quickcheck]
fn focus_client_focused_the_enclosing_workspace(mut s: StackSet<Xid>) -> bool {
let target = match s.clients().max() {
Some(target) => *target,
None => return true, };
let expected = s
.tag_for_client(&target)
.expect("client is known so tag is Some")
.to_owned();
s.focus_client(&target);
s.current_tag() == expected
}
#[quickcheck]
fn move_focused_to_tag(mut s: StackSet<Xid>) -> bool {
let tag = s.last_tag();
let c = match s.current_client() {
Some(&c) => c,
None => return true, };
s.move_focused_to_tag(&tag);
s.focus_tag(&tag);
s.current_client() == Some(&c)
}
#[quickcheck]
fn move_client_to_tag(mut s: StackSet<Xid>) -> bool {
let tag = s.last_tag();
let c = match s.last_visible_client() {
Some(&c) => c,
None => return true, };
s.move_client_to_tag(&c, &tag);
s.focus_tag(&tag);
s.current_client() == Some(&c)
}
#[quickcheck]
fn focus_next_workspace_always_changes_workspace(mut s: StackSet<Xid>) -> bool {
if s.ordered_tags().len() == 1 {
return true; };
let current_tag = s.current_tag().to_string();
s.focus_next_workspace();
s.current_tag() != current_tag
}
#[quickcheck]
fn focus_previous_workspace_always_changes_workspace(mut s: StackSet<Xid>) -> bool {
if s.ordered_tags().len() == 1 {
return true; };
let current_tag = s.current_tag().to_string();
s.focus_previous_workspace();
s.current_tag() != current_tag
}
}