Fix compositor output routing and pointer hints; format imports

This commit is contained in:
Samuel Onoja committed 2026-09-30 06:39:58 +01:00
1 parent 88e4f3c135
commit 46509f880a
108 files changed
+3682 -8371

No files matched your search

+14 -29
View File
@@ -81,9 +81,7 @@ impl Default for StickyNote {
impl StickyNote {
pub fn on_output(&self, name: Option<&str>) -> bool {
self.enabled
&& (self.outputs.is_empty()
|| name.is_some_and(|name| self.outputs.iter().any(|s| s == name)))
self.enabled && (self.outputs.is_empty() || name.is_some_and(|name| self.outputs.iter().any(|s| s == name)))
}
/// Text/style changes repaint in place, never remap the desktop card.
pub fn same_surface(&self, other: &Self) -> bool {
@@ -99,12 +97,7 @@ impl StickyNote {
}
pub fn validate(&self) -> Result<(), String> {
let invalid = |field: &str| {
Err(format!(
"desktop_widgets.notes[{}].{field} is invalid",
self.id
))
};
let invalid = |field: &str| Err(format!("desktop_widgets.notes[{}].{field} is invalid", self.id));
if self.id.is_empty()
|| self.id.len() > 64
|| !self
@@ -138,9 +131,9 @@ impl StickyNote {
for color in [&self.color, &self.background].into_iter().flatten() {
if !color.is_empty()
&& color != "theme"
&& !color.strip_prefix('#').is_some_and(|hex| {
matches!(hex.len(), 6 | 8) && hex.bytes().all(|b| b.is_ascii_hexdigit())
})
&& !color
.strip_prefix('#')
.is_some_and(|hex| matches!(hex.len(), 6 | 8) && hex.bytes().all(|b| b.is_ascii_hexdigit()))
{
return invalid("color/background");
}
@@ -253,8 +246,7 @@ impl Default for Clock {
impl Clock {
pub fn on_output(&self, name: Option<&str>) -> bool {
self.enabled
&& (self.outputs.is_empty()
|| name.is_some_and(|name| self.outputs.iter().any(|output| output == name)))
&& (self.outputs.is_empty() || name.is_some_and(|name| self.outputs.iter().any(|output| output == name)))
}
pub fn labels(&self, now: &jiff::Zoned) -> Result<(String, String), String> {
@@ -264,10 +256,8 @@ impl Clock {
}
_ => now.clone(),
};
let time =
jiff::fmt::strtime::format(&self.time_format, &zoned).map_err(|e| e.to_string())?;
let date =
jiff::fmt::strtime::format(&self.date_format, &zoned).map_err(|e| e.to_string())?;
let time = jiff::fmt::strtime::format(&self.time_format, &zoned).map_err(|e| e.to_string())?;
let date = jiff::fmt::strtime::format(&self.date_format, &zoned).map_err(|e| e.to_string())?;
Ok(if self.lowercase {
(time.to_lowercase(), date.to_lowercase())
} else {
@@ -295,16 +285,14 @@ impl Clock {
return invalid(name);
}
}
for color in [&self.color, &self.date_color, &self.background]
.into_iter()
.flatten()
{
for color in [&self.color, &self.date_color, &self.background].into_iter().flatten() {
if color.is_empty() {
continue;
}
if !color.strip_prefix('#').is_some_and(|hex| {
matches!(hex.len(), 6 | 8) && hex.bytes().all(|b| b.is_ascii_hexdigit())
}) {
if !color
.strip_prefix('#')
.is_some_and(|hex| matches!(hex.len(), 6 | 8) && hex.bytes().all(|b| b.is_ascii_hexdigit()))
{
return invalid("color");
}
}
@@ -414,9 +402,6 @@ mod tests {
date_format: "%-d %b".into(),
..Clock::default()
};
assert_eq!(
clock.labels(&now).unwrap(),
("10:05 pm".into(), "26 sep".into())
);
assert_eq!(clock.labels(&now).unwrap(), ("10:05 pm".into(), "26 sep".into()));
}
}
+2 -2
View File
@@ -5,6 +5,6 @@ mod workspace;
pub use output::{OutputError, OutputGeometry, OutputId, OutputWorkspaceMap, WorkspaceSwitch};
pub use workspace::{
LayoutMode, WindowPlacement, Workspace, WorkspaceError, WorkspaceId, WorkspaceLayout,
WorkspaceSet, workspace_order_key,
LayoutMode, WindowPlacement, Workspace, WorkspaceError, WorkspaceId, WorkspaceLayout, WorkspaceSet,
workspace_order_key,
};
+45 -158
View File
@@ -1,8 +1,6 @@
use std::{
collections::{HashMap, HashSet},
error::Error,
fmt,
};
use std::collections::{HashMap, HashSet};
use std::error::Error;
use std::fmt;
use crate::WorkspaceId;
@@ -19,12 +17,7 @@ pub struct OutputGeometry {
impl OutputGeometry {
pub const fn new(x: i32, y: i32, width: i32, height: i32) -> Self {
Self {
x,
y,
width,
height,
}
Self { x, y, width, height }
}
fn center(self) -> (i64, i64) {
@@ -39,10 +32,7 @@ impl OutputGeometry {
pub enum OutputError {
AlreadyConnected(OutputId),
UnknownOutput(OutputId),
WorkspaceAlreadyAssigned {
workspace: WorkspaceId,
output: OutputId,
},
WorkspaceAlreadyAssigned { workspace: WorkspaceId, output: OutputId },
}
impl fmt::Display for OutputError {
@@ -51,10 +41,7 @@ impl fmt::Display for OutputError {
Self::AlreadyConnected(output) => write!(formatter, "output {output:?} is connected"),
Self::UnknownOutput(output) => write!(formatter, "unknown output {output:?}"),
Self::WorkspaceAlreadyAssigned { workspace, output } => {
write!(
formatter,
"workspace {workspace:?} is assigned to {output:?}"
)
write!(formatter, "workspace {workspace:?} is assigned to {output:?}")
}
}
}
@@ -111,11 +98,7 @@ impl OutputWorkspaceMap {
}
pub fn forget_workspace(&mut self, workspace: WorkspaceId) -> bool {
if self
.outputs
.values()
.any(|output| output.active == workspace)
{
if self.outputs.values().any(|output| output.active == workspace) {
return false;
}
if let Some(owner) = self.assignments.remove(&workspace)
@@ -125,9 +108,7 @@ impl OutputWorkspaceMap {
}
self.revisions.remove(&workspace);
self.evacuations.retain(|_, record| {
record
.workspaces
.retain(|entry| entry.workspace != workspace);
record.workspaces.retain(|entry| entry.workspace != workspace);
if record.active == workspace {
if let Some(entry) = record.workspaces.first() {
record.active = entry.workspace;
@@ -165,8 +146,7 @@ impl OutputWorkspaceMap {
let mut reclaimed = Vec::new();
if let Some(record) = &record {
for evacuated in &record.workspaces {
let assignment_matches =
self.assignments.get(&evacuated.workspace).copied() == evacuated.target;
let assignment_matches = self.assignments.get(&evacuated.workspace).copied() == evacuated.target;
let revision_matches = self.revision(evacuated.workspace) == evacuated.revision;
if assignment_matches && revision_matches {
@@ -193,12 +173,7 @@ impl OutputWorkspaceMap {
let active = record
.as_ref()
.and_then(|record| reclaimed.contains(&record.active).then_some(record.active))
.or_else(|| {
reclaimed
.iter()
.copied()
.min_by_key(|workspace| workspace.0)
});
.or_else(|| reclaimed.iter().copied().min_by_key(|workspace| workspace.0));
let active = match active {
Some(active) => active,
None => {
@@ -229,10 +204,7 @@ impl OutputWorkspaceMap {
}
pub fn disconnect(&mut self, output: OutputId) -> Result<Option<OutputId>, OutputError> {
let removed = self
.outputs
.remove(&output)
.ok_or(OutputError::UnknownOutput(output))?;
let removed = self.outputs.remove(&output).ok_or(OutputError::UnknownOutput(output))?;
let target = self.migration_target(output, removed.geometry);
let removed_had_focus = self.focused == Some(output);
let mut evacuated = removed.workspaces.into_iter().collect::<Vec<_>>();
@@ -309,10 +281,7 @@ impl OutputWorkspaceMap {
}
let reassigned = self.assignments.insert(workspace, output) != Some(output);
let state = self
.outputs
.get_mut(&output)
.expect("output was checked above");
let state = self.outputs.get_mut(&output).expect("output was checked above");
state.workspaces.insert(workspace);
state.active = workspace;
self.focused = Some(output);
@@ -324,11 +293,7 @@ impl OutputWorkspaceMap {
Ok(WorkspaceSwitch::Activated(output))
}
pub fn assign_workspace(
&mut self,
output: OutputId,
workspace: WorkspaceId,
) -> Result<OutputId, OutputError> {
pub fn assign_workspace(&mut self, output: OutputId, workspace: WorkspaceId) -> Result<OutputId, OutputError> {
if !self.outputs.contains_key(&output) {
return Err(OutputError::UnknownOutput(output));
}
@@ -348,11 +313,7 @@ impl OutputWorkspaceMap {
Ok(output)
}
fn migration_target(
&self,
removed: OutputId,
removed_geometry: OutputGeometry,
) -> Option<OutputId> {
fn migration_target(&self, removed: OutputId, removed_geometry: OutputGeometry) -> Option<OutputId> {
if let Some(focused) = self.focused
&& focused != removed
&& self.outputs.contains_key(&focused)
@@ -382,10 +343,7 @@ impl OutputWorkspaceMap {
}
fn validate(&self) -> bool {
if self
.focused
.is_some_and(|focused| !self.outputs.contains_key(&focused))
{
if self.focused.is_some_and(|focused| !self.outputs.contains_key(&focused)) {
return false;
}
@@ -423,9 +381,7 @@ mod tests {
assert!(outputs.forget_workspace(first));
assert_eq!(outputs.output_for_workspace(first), None);
outputs.disconnect(display).unwrap();
outputs
.connect(display, geometry, super::WorkspaceId(3))
.unwrap();
outputs.connect(display, geometry, super::WorkspaceId(3)).unwrap();
assert_eq!(outputs.active_workspace(display), Some(second));
assert_eq!(outputs.output_for_workspace(first), None);
assert!(outputs.validate());
@@ -462,47 +418,28 @@ mod tests {
outputs.connect(OutputId(2), geometry(1_920), WorkspaceId(2)),
Ok(WorkspaceId(2))
);
assert_eq!(
outputs.output_for_workspace(WorkspaceId(1)),
Some(OutputId(1))
);
assert_eq!(
outputs.output_for_workspace(WorkspaceId(2)),
Some(OutputId(2))
);
assert_eq!(outputs.output_for_workspace(WorkspaceId(1)), Some(OutputId(1)));
assert_eq!(outputs.output_for_workspace(WorkspaceId(2)), Some(OutputId(2)));
}
#[test]
fn removal_prefers_a_remaining_focused_output_without_replacing_its_workspace() {
let mut outputs = OutputWorkspaceMap::default();
outputs
.connect(OutputId(1), geometry(0), WorkspaceId(1))
.unwrap();
outputs
.connect(OutputId(2), geometry(1_920), WorkspaceId(2))
.unwrap();
outputs.connect(OutputId(1), geometry(0), WorkspaceId(1)).unwrap();
outputs.connect(OutputId(2), geometry(1_920), WorkspaceId(2)).unwrap();
outputs.focus_output(OutputId(1)).unwrap();
assert_eq!(outputs.disconnect(OutputId(2)), Ok(Some(OutputId(1))));
assert_eq!(outputs.active_workspace(OutputId(1)), Some(WorkspaceId(1)));
assert_eq!(
outputs.output_for_workspace(WorkspaceId(2)),
Some(OutputId(1))
);
assert_eq!(outputs.output_for_workspace(WorkspaceId(2)), Some(OutputId(1)));
}
#[test]
fn removing_the_focused_output_activates_its_workspace_on_the_nearest_output() {
let mut outputs = OutputWorkspaceMap::default();
outputs
.connect(OutputId(1), geometry(0), WorkspaceId(1))
.unwrap();
outputs
.connect(OutputId(2), geometry(1_920), WorkspaceId(2))
.unwrap();
outputs
.connect(OutputId(3), geometry(3_840), WorkspaceId(3))
.unwrap();
outputs.connect(OutputId(1), geometry(0), WorkspaceId(1)).unwrap();
outputs.connect(OutputId(2), geometry(1_920), WorkspaceId(2)).unwrap();
outputs.connect(OutputId(3), geometry(3_840), WorkspaceId(3)).unwrap();
outputs.focus_output(OutputId(2)).unwrap();
assert_eq!(outputs.disconnect(OutputId(2)), Ok(Some(OutputId(1))));
@@ -513,12 +450,8 @@ mod tests {
#[test]
fn reconnect_reclaims_automatically_evacuated_workspaces() {
let mut outputs = OutputWorkspaceMap::default();
outputs
.connect(OutputId(1), geometry(0), WorkspaceId(1))
.unwrap();
outputs
.connect(OutputId(2), geometry(1_920), WorkspaceId(2))
.unwrap();
outputs.connect(OutputId(1), geometry(0), WorkspaceId(1)).unwrap();
outputs.connect(OutputId(2), geometry(1_920), WorkspaceId(2)).unwrap();
outputs.focus_output(OutputId(1)).unwrap();
outputs.disconnect(OutputId(2)).unwrap();
@@ -526,29 +459,20 @@ mod tests {
outputs.connect(OutputId(2), geometry(1_920), WorkspaceId(3)),
Ok(WorkspaceId(2))
);
assert_eq!(
outputs.output_for_workspace(WorkspaceId(2)),
Some(OutputId(2))
);
assert_eq!(outputs.output_for_workspace(WorkspaceId(2)), Some(OutputId(2)));
assert_eq!(outputs.active_workspace(OutputId(2)), Some(WorkspaceId(2)));
}
#[test]
fn reconnect_restores_the_migration_targets_remaining_workspace() {
let mut outputs = OutputWorkspaceMap::default();
outputs
.connect(OutputId(1), geometry(0), WorkspaceId(1))
.unwrap();
outputs
.connect(OutputId(2), geometry(1_920), WorkspaceId(2))
.unwrap();
outputs.connect(OutputId(1), geometry(0), WorkspaceId(1)).unwrap();
outputs.connect(OutputId(2), geometry(1_920), WorkspaceId(2)).unwrap();
outputs.focus_output(OutputId(2)).unwrap();
outputs.disconnect(OutputId(2)).unwrap();
assert_eq!(outputs.active_workspace(OutputId(1)), Some(WorkspaceId(2)));
outputs
.connect(OutputId(2), geometry(1_920), WorkspaceId(3))
.unwrap();
outputs.connect(OutputId(2), geometry(1_920), WorkspaceId(3)).unwrap();
assert_eq!(outputs.active_workspace(OutputId(1)), Some(WorkspaceId(1)));
assert_eq!(outputs.active_workspace(OutputId(2)), Some(WorkspaceId(2)));
@@ -557,37 +481,24 @@ mod tests {
#[test]
fn using_an_evacuated_workspace_does_not_prevent_automatic_return() {
let mut outputs = OutputWorkspaceMap::default();
outputs
.connect(OutputId(1), geometry(0), WorkspaceId(1))
.unwrap();
outputs
.connect(OutputId(2), geometry(1_920), WorkspaceId(2))
.unwrap();
outputs.connect(OutputId(1), geometry(0), WorkspaceId(1)).unwrap();
outputs.connect(OutputId(2), geometry(1_920), WorkspaceId(2)).unwrap();
outputs.focus_output(OutputId(1)).unwrap();
outputs.disconnect(OutputId(2)).unwrap();
outputs
.switch_workspace(OutputId(1), WorkspaceId(2))
.unwrap();
outputs.switch_workspace(OutputId(1), WorkspaceId(2)).unwrap();
assert_eq!(
outputs.connect(OutputId(2), geometry(1_920), WorkspaceId(3)),
Ok(WorkspaceId(2))
);
assert_eq!(
outputs.output_for_workspace(WorkspaceId(2)),
Some(OutputId(2))
);
assert_eq!(outputs.output_for_workspace(WorkspaceId(2)), Some(OutputId(2)));
}
#[test]
fn repeated_lid_and_monitor_handoffs_restore_both_workspace_owners() {
let mut outputs = OutputWorkspaceMap::default();
outputs
.connect(OutputId(1), geometry(0), WorkspaceId(1))
.unwrap();
outputs
.connect(OutputId(2), geometry(1920), WorkspaceId(2))
.unwrap();
outputs.connect(OutputId(1), geometry(0), WorkspaceId(1)).unwrap();
outputs.connect(OutputId(2), geometry(1920), WorkspaceId(2)).unwrap();
for removed in [OutputId(1), OutputId(2), OutputId(1), OutputId(2)] {
let remaining = if removed == OutputId(1) {
OutputId(2)
@@ -596,14 +507,8 @@ mod tests {
};
outputs.focus_output(removed).unwrap();
outputs.disconnect(removed).unwrap();
assert_eq!(
outputs.output_for_workspace(WorkspaceId(1)),
Some(remaining)
);
assert_eq!(
outputs.output_for_workspace(WorkspaceId(2)),
Some(remaining)
);
assert_eq!(outputs.output_for_workspace(WorkspaceId(1)), Some(remaining));
assert_eq!(outputs.output_for_workspace(WorkspaceId(2)), Some(remaining));
// Switching between handed-off workspaces is normal use, not an
// explicit change of their home output.
outputs.switch_workspace(remaining, WorkspaceId(1)).unwrap();
@@ -615,14 +520,8 @@ mod tests {
WorkspaceId(3),
)
.unwrap();
assert_eq!(
outputs.output_for_workspace(WorkspaceId(1)),
Some(OutputId(1))
);
assert_eq!(
outputs.output_for_workspace(WorkspaceId(2)),
Some(OutputId(2))
);
assert_eq!(outputs.output_for_workspace(WorkspaceId(1)), Some(OutputId(1)));
assert_eq!(outputs.output_for_workspace(WorkspaceId(2)), Some(OutputId(2)));
assert_eq!(outputs.active_workspace(OutputId(1)), Some(WorkspaceId(1)));
assert_eq!(outputs.active_workspace(OutputId(2)), Some(WorkspaceId(2)));
}
@@ -631,12 +530,8 @@ mod tests {
#[test]
fn selecting_a_workspace_owned_by_another_output_focuses_that_output() {
let mut outputs = OutputWorkspaceMap::default();
outputs
.connect(OutputId(1), geometry(0), WorkspaceId(1))
.unwrap();
outputs
.connect(OutputId(2), geometry(1_920), WorkspaceId(2))
.unwrap();
outputs.connect(OutputId(1), geometry(0), WorkspaceId(1)).unwrap();
outputs.connect(OutputId(2), geometry(1_920), WorkspaceId(2)).unwrap();
assert_eq!(
outputs.switch_workspace(OutputId(1), WorkspaceId(2)),
@@ -648,18 +543,10 @@ mod tests {
#[test]
fn assigning_an_unused_workspace_does_not_change_the_active_workspace() {
let mut outputs = OutputWorkspaceMap::default();
outputs
.connect(OutputId(1), geometry(0), WorkspaceId(1))
.unwrap();
outputs.connect(OutputId(1), geometry(0), WorkspaceId(1)).unwrap();
assert_eq!(
outputs.assign_workspace(OutputId(1), WorkspaceId(2)),
Ok(OutputId(1))
);
assert_eq!(outputs.assign_workspace(OutputId(1), WorkspaceId(2)), Ok(OutputId(1)));
assert_eq!(outputs.active_workspace(OutputId(1)), Some(WorkspaceId(1)));
assert_eq!(
outputs.output_for_workspace(WorkspaceId(2)),
Some(OutputId(1))
);
assert_eq!(outputs.output_for_workspace(WorkspaceId(2)), Some(OutputId(1)));
}
}
+91 -335
View File
@@ -3,8 +3,8 @@ use std::error::Error;
use std::fmt;
use ferese_layout::{
Axis, ColumnWidth, Direction, GapConfig, LayoutError, LayoutResult, LayoutTree, Rect,
ScrollingLayout, SizeConstraints, ViewportFocusStrategy, WindowId,
Axis, ColumnWidth, Direction, GapConfig, LayoutError, LayoutResult, LayoutTree, Rect, ScrollingLayout,
SizeConstraints, ViewportFocusStrategy, WindowId,
};
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
@@ -44,11 +44,7 @@ impl Default for WorkspaceLayout {
}
impl WorkspaceLayout {
pub fn new(
mode: LayoutMode,
default_column_width: ColumnWidth,
focus_strategy: ViewportFocusStrategy,
) -> Self {
pub fn new(mode: LayoutMode, default_column_width: ColumnWidth, focus_strategy: ViewportFocusStrategy) -> Self {
match mode {
LayoutMode::Scrolling => {
let mut layout = ScrollingLayout::with_default_width(default_column_width);
@@ -161,17 +157,11 @@ impl WorkspaceLayout {
constraints,
focused.filter(|window| layout.contains(*window)),
),
Self::Tree(layout) => {
layout.geometry_with_constraints(bounds, gaps, constraints, focused)
}
Self::Tree(layout) => layout.geometry_with_constraints(bounds, gaps, constraints, focused),
}
}
pub fn automatic_axis(
&self,
focused: Option<WindowId>,
bounds: Rect,
) -> Result<Axis, LayoutError> {
pub fn automatic_axis(&self, focused: Option<WindowId>, bounds: Rect) -> Result<Axis, LayoutError> {
match self {
Self::Scrolling(_) => Ok(Axis::Horizontal),
Self::Tree(layout) => layout.automatic_axis(focused, bounds),
@@ -190,24 +180,14 @@ impl WorkspaceLayout {
}
}
pub fn move_window(
&mut self,
window: WindowId,
direction: Direction,
bounds: Rect,
) -> Result<bool, LayoutError> {
pub fn move_window(&mut self, window: WindowId, direction: Direction, bounds: Rect) -> Result<bool, LayoutError> {
match self {
Self::Scrolling(layout) => layout.move_window(window, direction),
Self::Tree(layout) => layout.move_window(window, direction, bounds),
}
}
pub fn resize_window(
&mut self,
window: WindowId,
direction: Direction,
amount: f64,
) -> Result<bool, LayoutError> {
pub fn resize_window(&mut self, window: WindowId, direction: Direction, amount: f64) -> Result<bool, LayoutError> {
match self {
Self::Scrolling(layout) => layout.resize_window(window, direction, amount),
Self::Tree(layout) => layout.resize_window(window, direction, amount),
@@ -229,11 +209,7 @@ impl WorkspaceLayout {
}
}
pub fn cycle_column_width(
&mut self,
window: WindowId,
presets: &[ColumnWidth],
) -> Result<bool, LayoutError> {
pub fn cycle_column_width(&mut self, window: WindowId, presets: &[ColumnWidth]) -> Result<bool, LayoutError> {
match self {
Self::Scrolling(layout) => layout.cycle_column_width(window, presets),
Self::Tree(_) => Ok(false),
@@ -280,11 +256,7 @@ impl WorkspaceLayout {
for column in columns {
for (row, window) in column.windows.into_iter().enumerate() {
let axis = if row == 0 {
Axis::Horizontal
} else {
Axis::Vertical
};
let axis = if row == 0 { Axis::Horizontal } else { Axis::Vertical };
tree.insert(window, previous, axis, 0.5)?;
previous = Some(window);
}
@@ -444,12 +416,7 @@ impl WorkspaceSet {
}
/// Update policy without moving windows or replacing existing column widths.
pub fn reconfigure_defaults(
&mut self,
mode: LayoutMode,
width: ColumnWidth,
strategy: ViewportFocusStrategy,
) {
pub fn reconfigure_defaults(&mut self, mode: LayoutMode, width: ColumnWidth, strategy: ViewportFocusStrategy) {
self.default_layout_mode = mode;
self.default_column_width = width;
self.scrolling_focus_strategy = strategy;
@@ -470,11 +437,7 @@ impl WorkspaceSet {
let workspace = Workspace {
id: active,
name: "1".to_owned(),
layout: WorkspaceLayout::new(
default_layout_mode,
default_column_width,
scrolling_focus_strategy,
),
layout: WorkspaceLayout::new(default_layout_mode, default_column_width, scrolling_focus_strategy),
floating: Vec::new(),
last_focused: None,
fullscreen: None,
@@ -520,12 +483,8 @@ impl WorkspaceSet {
debug_assert!(self.validate().is_ok());
let focused = self.active().last_focused;
let visible_floating = focused.filter(|window| {
matches!(
self.placement(*window),
Some(WindowPlacement::Floating { .. })
)
});
let visible_floating =
focused.filter(|window| matches!(self.placement(*window), Some(WindowPlacement::Floating { .. })));
Ok(visible_floating.or(self.active().fullscreen).or(focused))
}
@@ -543,8 +502,7 @@ impl WorkspaceSet {
pub fn ordered(&self) -> Vec<&Workspace> {
let mut workspaces = self.iter().collect::<Vec<_>>();
workspaces
.sort_unstable_by_key(|workspace| workspace_order_key(&workspace.name, workspace.id));
workspaces.sort_unstable_by_key(|workspace| workspace_order_key(&workspace.name, workspace.id));
workspaces
}
@@ -552,20 +510,12 @@ impl WorkspaceSet {
let mut empty = self
.workspaces
.values()
.filter(|workspace| {
workspace.layout.window_ids().next().is_none() && workspace.floating.is_empty()
})
.filter(|workspace| workspace.layout.window_ids().next().is_none() && workspace.floating.is_empty())
.map(|workspace| workspace.id)
.collect::<Vec<_>>();
empty.sort_by_key(|id| id.0);
let visible_empty = empty
.iter()
.any(|id| *id == self.active || protected.contains(id));
let spare = if visible_empty {
None
} else {
empty.first().copied()
};
let visible_empty = empty.iter().any(|id| *id == self.active || protected.contains(id));
let spare = if visible_empty { None } else { empty.first().copied() };
let mut removed = Vec::new();
for id in empty {
if id == self.active || protected.contains(&id) || Some(id) == spare {
@@ -588,32 +538,20 @@ impl WorkspaceSet {
self.placements.get(&window).copied()
}
pub fn set_active_layout_mode(
&mut self,
mode: LayoutMode,
bounds: Rect,
) -> Result<bool, WorkspaceError> {
pub fn set_active_layout_mode(&mut self, mode: LayoutMode, bounds: Rect) -> Result<bool, WorkspaceError> {
let default_column_width = self.default_column_width;
let focus_strategy = self.scrolling_focus_strategy;
let workspace = self.active_mut();
let focused = tiled_focus(workspace);
let changed = workspace.layout.set_mode(
mode,
bounds,
focused,
default_column_width,
focus_strategy,
)?;
let changed = workspace
.layout
.set_mode(mode, bounds, focused, default_column_width, focus_strategy)?;
debug_assert!(self.validate().is_ok());
Ok(changed)
}
pub fn set_floating_rect(
&mut self,
window: WindowId,
rect: Rect,
) -> Result<(), WorkspaceError> {
pub fn set_floating_rect(&mut self, window: WindowId, rect: Rect) -> Result<(), WorkspaceError> {
let placement = self
.placements
.get_mut(&window)
@@ -639,35 +577,23 @@ impl WorkspaceSet {
self.focus_window_with_reveal(window, false)
}
fn focus_window_with_reveal(
&mut self,
window: WindowId,
reveal: bool,
) -> Result<(), WorkspaceError> {
fn focus_window_with_reveal(&mut self, window: WindowId, reveal: bool) -> Result<(), WorkspaceError> {
if self.workspace_for_window(window) != Some(self.active) {
return Err(WorkspaceError::InvalidState(
"focused window is not on the active workspace",
));
}
if self.active().fullscreen.is_some_and(|fullscreen| {
fullscreen != window
&& !matches!(
self.placement(window),
Some(WindowPlacement::Floating { .. })
)
fullscreen != window && !matches!(self.placement(window), Some(WindowPlacement::Floating { .. }))
}) {
return Err(WorkspaceError::InvalidState(
"focused window is hidden by fullscreen",
));
return Err(WorkspaceError::InvalidState("focused window is hidden by fullscreen"));
}
if self.placement(window) == Some(WindowPlacement::Tiled) {
if reveal {
self.active_mut().layout.activate_window(window)?;
} else {
self.active_mut()
.layout
.activate_window_without_reveal(window)?;
self.active_mut().layout.activate_window_without_reveal(window)?;
}
}
self.active_mut().last_focused = Some(window);
@@ -676,11 +602,7 @@ impl WorkspaceSet {
Ok(())
}
pub fn stack_window(
&mut self,
window: WindowId,
target: WindowId,
) -> Result<(), WorkspaceError> {
pub fn stack_window(&mut self, window: WindowId, target: WindowId) -> Result<(), WorkspaceError> {
let workspace = self
.workspace_for_window(window)
.ok_or(LayoutError::UnknownWindow(window))?;
@@ -721,11 +643,7 @@ impl WorkspaceSet {
Ok(changed)
}
pub fn cycle_column_width(
&mut self,
window: WindowId,
presets: &[ColumnWidth],
) -> Result<bool, WorkspaceError> {
pub fn cycle_column_width(&mut self, window: WindowId, presets: &[ColumnWidth]) -> Result<bool, WorkspaceError> {
if self.workspace_for_window(window) != Some(self.active)
|| self.placement(window) != Some(WindowPlacement::Tiled)
{
@@ -734,10 +652,7 @@ impl WorkspaceSet {
));
}
let changed = self
.active_mut()
.layout
.cycle_column_width(window, presets)?;
let changed = self.active_mut().layout.cycle_column_width(window, presets)?;
debug_assert!(self.validate().is_ok());
Ok(changed)
}
@@ -804,12 +719,7 @@ impl WorkspaceSet {
self.activate(workspace)
}
pub fn insert_window(
&mut self,
window: WindowId,
axis: Axis,
ratio: f64,
) -> Result<(), WorkspaceError> {
pub fn insert_window(&mut self, window: WindowId, axis: Axis, ratio: f64) -> Result<(), WorkspaceError> {
if self.window_workspaces.contains_key(&window) {
return Err(WorkspaceError::DuplicateWindow(window));
}
@@ -1004,11 +914,7 @@ impl WorkspaceSet {
Ok(enabled)
}
pub fn set_fullscreen(
&mut self,
window: WindowId,
enabled: bool,
) -> Result<bool, WorkspaceError> {
pub fn set_fullscreen(&mut self, window: WindowId, enabled: bool) -> Result<bool, WorkspaceError> {
let workspace_id = self
.workspace_for_window(window)
.ok_or(LayoutError::UnknownWindow(window))?;
@@ -1038,18 +944,14 @@ impl WorkspaceSet {
return Err(WorkspaceError::InvalidState("active workspace is missing"));
}
if self.names.len() != self.workspaces.len() {
return Err(WorkspaceError::InvalidState(
"workspace name index is inconsistent",
));
return Err(WorkspaceError::InvalidState("workspace name index is inconsistent"));
}
let mut seen_windows = HashSet::new();
for (id, workspace) in &self.workspaces {
if workspace.id != *id || self.names.get(&workspace.name) != Some(id) {
return Err(WorkspaceError::InvalidState(
"workspace index is inconsistent",
));
return Err(WorkspaceError::InvalidState("workspace index is inconsistent"));
}
workspace.layout.validate()?;
@@ -1059,9 +961,7 @@ impl WorkspaceSet {
|| self.window_workspaces.get(&window) != Some(id)
|| self.placements.get(&window) != Some(&WindowPlacement::Tiled)
{
return Err(WorkspaceError::InvalidState(
"window ownership is inconsistent",
));
return Err(WorkspaceError::InvalidState("window ownership is inconsistent"));
}
}
@@ -1071,10 +971,7 @@ impl WorkspaceSet {
if !seen_floating.insert(*window)
|| !seen_windows.insert(*window)
|| self.window_workspaces.get(window) != Some(id)
|| !matches!(
self.placements.get(window),
Some(WindowPlacement::Floating { .. })
)
|| !matches!(self.placements.get(window), Some(WindowPlacement::Floating { .. }))
{
return Err(WorkspaceError::InvalidState(
"floating window ownership is inconsistent",
@@ -1086,9 +983,7 @@ impl WorkspaceSet {
.last_focused
.is_some_and(|window| self.window_workspaces.get(&window) != Some(id))
{
return Err(WorkspaceError::InvalidState(
"workspace focus is inconsistent",
));
return Err(WorkspaceError::InvalidState("workspace focus is inconsistent"));
}
if workspace
@@ -1101,12 +996,8 @@ impl WorkspaceSet {
}
}
if seen_windows.len() != self.window_workspaces.len()
|| self.placements.len() != self.window_workspaces.len()
{
return Err(WorkspaceError::InvalidState(
"window index retains stale entries",
));
if seen_windows.len() != self.window_workspaces.len() || self.placements.len() != self.window_workspaces.len() {
return Err(WorkspaceError::InvalidState("window index retains stale entries"));
}
Ok(())
@@ -1114,13 +1005,10 @@ impl WorkspaceSet {
}
fn first_window(workspace: &Workspace) -> Option<WindowId> {
workspace.layout.preferred_window().or_else(|| {
workspace
.floating
.iter()
.copied()
.min_by_key(|window| window.0)
})
workspace
.layout
.preferred_window()
.or_else(|| workspace.floating.iter().copied().min_by_key(|window| window.0))
}
fn tiled_focus(workspace: &Workspace) -> Option<WindowId> {
@@ -1151,10 +1039,7 @@ mod tests {
let original_first = set.active_id();
let second = set.ensure_numeric(2).unwrap();
set.activate(second).unwrap();
assert_eq!(
set.prune_empty(&std::collections::HashSet::new()),
vec![original_first]
);
assert_eq!(set.prune_empty(&std::collections::HashSet::new()), vec![original_first]);
let tenth = set.ensure_numeric(10).unwrap();
let first = set.ensure_numeric(1).unwrap();
@@ -1188,19 +1073,13 @@ mod tests {
.unwrap();
let spare = set.ensure_numeric(3).unwrap();
let excess = set.ensure_numeric(4).unwrap();
assert_eq!(
set.prune_empty(&std::collections::HashSet::new()),
vec![excess]
);
assert_eq!(set.prune_empty(&std::collections::HashSet::new()), vec![excess]);
assert!(set.workspace(first).is_some());
assert!(set.workspace(floating).is_some());
assert!(set.workspace(spare).is_some());
set.activate(spare).unwrap();
set.remove_window(super::WindowId(1)).unwrap();
assert_eq!(
set.prune_empty(&std::collections::HashSet::new()),
vec![first]
);
assert_eq!(set.prune_empty(&std::collections::HashSet::new()), vec![first]);
assert!(set.validate().is_ok());
assert_ne!(set.ensure_numeric(1).unwrap(), first);
}
@@ -1222,9 +1101,7 @@ mod tests {
fn live_policy_updates_existing_layouts_and_default_widths_not_custom_widths() {
let mut workspaces = WorkspaceSet::default();
for id in 1..=3 {
workspaces
.insert_window(WindowId(id), Axis::Horizontal, 0.5)
.unwrap();
workspaces.insert_window(WindowId(id), Axis::Horizontal, 0.5).unwrap();
}
let WorkspaceLayout::Scrolling(layout) = &mut workspaces.active_mut().layout else {
panic!()
@@ -1263,30 +1140,18 @@ mod tests {
#[test]
fn reconfiguration_preserves_windows_and_current_widths_but_updates_defaults() {
let mut workspaces = WorkspaceSet::default();
workspaces
.insert_window(WindowId(1), Axis::Horizontal, 0.5)
.unwrap();
workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap();
let workspace = workspaces.active_id();
workspaces.reconfigure_defaults(
LayoutMode::Tree,
ColumnWidth::Full,
ViewportFocusStrategy::Minimal,
);
workspaces.reconfigure_defaults(LayoutMode::Tree, ColumnWidth::Full, ViewportFocusStrategy::Minimal);
let WorkspaceLayout::Scrolling(layout) = &workspaces.active().layout else {
panic!("existing layout must survive");
};
assert_eq!(layout.columns()[0].width, ColumnWidth::Proportion(0.5));
assert_eq!(layout.default_width(), ColumnWidth::Full);
assert_eq!(
workspaces.workspace_for_window(WindowId(1)),
Some(workspace)
);
assert_eq!(workspaces.workspace_for_window(WindowId(1)), Some(workspace));
workspaces.switch_to_numeric(2).unwrap();
assert_eq!(workspaces.active().layout.mode(), LayoutMode::Tree);
assert_eq!(
workspaces.workspace_for_window(WindowId(1)),
Some(workspace)
);
assert_eq!(workspaces.workspace_for_window(WindowId(1)), Some(workspace));
}
use super::*;
@@ -1304,14 +1169,9 @@ mod tests {
#[test]
fn configured_layout_defaults_apply_to_new_workspaces() {
let mut workspaces = WorkspaceSet::new(
LayoutMode::Scrolling,
ColumnWidth::Full,
ViewportFocusStrategy::Minimal,
);
workspaces
.insert_window(WindowId(1), Axis::Horizontal, 0.5)
.unwrap();
let mut workspaces =
WorkspaceSet::new(LayoutMode::Scrolling, ColumnWidth::Full, ViewportFocusStrategy::Minimal);
workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap();
let first = workspaces
.active_mut()
.layout
@@ -1323,10 +1183,7 @@ mod tests {
)
.unwrap();
assert_eq!(
first.geometry[&WindowId(1)],
Rect::new(4.0, 4.0, 992.0, 792.0)
);
assert_eq!(first.geometry[&WindowId(1)], Rect::new(4.0, 4.0, 992.0, 792.0));
workspaces.switch_to_numeric(2).unwrap();
assert_eq!(workspaces.active().layout.mode(), LayoutMode::Scrolling);
}
@@ -1334,14 +1191,10 @@ mod tests {
#[test]
fn switching_restores_workspace_focus() {
let mut workspaces = WorkspaceSet::default();
workspaces
.insert_window(WindowId(1), Axis::Horizontal, 0.5)
.unwrap();
workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap();
assert_eq!(workspaces.switch_to_numeric(2).unwrap(), None);
workspaces
.insert_window(WindowId(2), Axis::Horizontal, 0.5)
.unwrap();
workspaces.insert_window(WindowId(2), Axis::Horizontal, 0.5).unwrap();
assert_eq!(workspaces.switch_to_numeric(1).unwrap(), Some(WindowId(1)));
assert_eq!(workspaces.switch_to_numeric(2).unwrap(), Some(WindowId(2)));
@@ -1351,12 +1204,8 @@ mod tests {
#[test]
fn moving_a_window_updates_membership_without_switching() {
let mut workspaces = WorkspaceSet::default();
workspaces
.insert_window(WindowId(1), Axis::Horizontal, 0.5)
.unwrap();
workspaces
.insert_window(WindowId(2), Axis::Horizontal, 0.5)
.unwrap();
workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap();
workspaces.insert_window(WindowId(2), Axis::Horizontal, 0.5).unwrap();
let first = workspaces.active_id();
let second = workspaces
.move_window_to_numeric(WindowId(2), 2, Axis::Horizontal, 0.5)
@@ -1364,32 +1213,16 @@ mod tests {
assert_eq!(workspaces.active_id(), first);
assert_eq!(workspaces.workspace_for_window(WindowId(2)), Some(second));
assert!(
!workspaces
.workspace(first)
.unwrap()
.layout
.contains(WindowId(2))
);
assert!(
workspaces
.workspace(second)
.unwrap()
.layout
.contains(WindowId(2))
);
assert!(!workspaces.workspace(first).unwrap().layout.contains(WindowId(2)));
assert!(workspaces.workspace(second).unwrap().layout.contains(WindowId(2)));
assert!(workspaces.validate().is_ok());
}
#[test]
fn removing_focus_chooses_a_deterministic_remaining_window() {
let mut workspaces = WorkspaceSet::default();
workspaces
.insert_window(WindowId(2), Axis::Horizontal, 0.5)
.unwrap();
workspaces
.insert_window(WindowId(1), Axis::Horizontal, 0.5)
.unwrap();
workspaces.insert_window(WindowId(2), Axis::Horizontal, 0.5).unwrap();
workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap();
workspaces.remove_window(WindowId(1)).unwrap();
assert_eq!(workspaces.active().last_focused, Some(WindowId(2)));
@@ -1400,9 +1233,7 @@ mod tests {
fn closing_a_scrolling_column_focuses_its_adjacent_column() {
let mut workspaces = WorkspaceSet::default();
for id in 1..=4 {
workspaces
.insert_window(WindowId(id), Axis::Horizontal, 0.5)
.unwrap();
workspaces.insert_window(WindowId(id), Axis::Horizontal, 0.5).unwrap();
}
workspaces.focus_window(WindowId(3)).unwrap();
@@ -1414,9 +1245,7 @@ mod tests {
#[test]
fn focus_rejects_a_window_on_an_inactive_workspace() {
let mut workspaces = WorkspaceSet::default();
workspaces
.insert_window(WindowId(1), Axis::Horizontal, 0.5)
.unwrap();
workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap();
workspaces
.move_window_to_numeric(WindowId(1), 2, Axis::Horizontal, 0.5)
.unwrap();
@@ -1433,9 +1262,7 @@ mod tests {
#[test]
fn floating_toggle_removes_and_restores_tiled_membership() {
let mut workspaces = WorkspaceSet::default();
workspaces
.insert_window(WindowId(1), Axis::Horizontal, 0.5)
.unwrap();
workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap();
let rect = Rect::new(10.0, 20.0, 640.0, 480.0);
assert_eq!(
@@ -1461,21 +1288,14 @@ mod tests {
#[test]
fn floating_geometry_updates_are_authoritative_and_validated() {
let mut workspaces = WorkspaceSet::default();
workspaces
.insert_window(WindowId(1), Axis::Horizontal, 0.5)
.unwrap();
workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap();
assert_eq!(
workspaces.set_floating_rect(WindowId(1), Rect::default()),
Err(WorkspaceError::InvalidState("window is not floating"))
);
workspaces
.toggle_floating(
WindowId(1),
Rect::new(10.0, 20.0, 640.0, 480.0),
Axis::Horizontal,
0.5,
)
.toggle_floating(WindowId(1), Rect::new(10.0, 20.0, 640.0, 480.0), Axis::Horizontal, 0.5)
.unwrap();
workspaces
.set_floating_rect(WindowId(1), Rect::new(30.0, 40.0, f64::NAN, -2.0))
@@ -1497,15 +1317,9 @@ mod tests {
ViewportFocusStrategy::Center,
ViewportFocusStrategy::Paged,
] {
let mut workspaces = WorkspaceSet::new(
LayoutMode::Scrolling,
ColumnWidth::Proportion(0.5),
strategy,
);
let mut workspaces = WorkspaceSet::new(LayoutMode::Scrolling, ColumnWidth::Proportion(0.5), strategy);
for id in 1..=3 {
workspaces
.insert_window(WindowId(id), Axis::Horizontal, 0.5)
.unwrap();
workspaces.insert_window(WindowId(id), Axis::Horizontal, 0.5).unwrap();
}
let bounds = Rect::new(0.0, 0.0, 1000.0, 800.0);
let gaps = GapConfig {
@@ -1555,20 +1369,13 @@ mod tests {
ViewportFocusStrategy::Paged,
);
for id in 1..=4 {
workspaces
.insert_window(WindowId(id), Axis::Horizontal, 0.5)
.unwrap();
workspaces.insert_window(WindowId(id), Axis::Horizontal, 0.5).unwrap();
}
let bounds = Rect::new(0.0, 0.0, 1000.0, 800.0);
let before = workspaces
.active_mut()
.layout
.geometry_with_constraints(
bounds,
GapConfig::default(),
&HashMap::new(),
Some(WindowId(4)),
)
.geometry_with_constraints(bounds, GapConfig::default(), &HashMap::new(), Some(WindowId(4)))
.unwrap();
let scroll = workspaces.active().layout.viewport_x();
workspaces
@@ -1582,12 +1389,7 @@ mod tests {
let after = workspaces
.active_mut()
.layout
.geometry_with_constraints(
bounds,
GapConfig::default(),
&HashMap::new(),
Some(WindowId(5)),
)
.geometry_with_constraints(bounds, GapConfig::default(), &HashMap::new(), Some(WindowId(5)))
.unwrap();
assert_eq!(before.geometry, after.geometry);
assert_eq!(scroll, workspaces.active().layout.viewport_x());
@@ -1598,9 +1400,7 @@ mod tests {
#[test]
fn floating_window_can_follow_its_parent_workspace_without_focus() {
let mut workspaces = WorkspaceSet::default();
workspaces
.insert_window(WindowId(1), Axis::Horizontal, 0.5)
.unwrap();
workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap();
let parent_workspace = workspaces.active_id();
workspaces.switch_to_numeric(2).unwrap();
workspaces
@@ -1612,10 +1412,7 @@ mod tests {
)
.unwrap();
assert_eq!(
workspaces.workspace_for_window(WindowId(2)),
Some(parent_workspace)
);
assert_eq!(workspaces.workspace_for_window(WindowId(2)), Some(parent_workspace));
assert_eq!(
workspaces.workspace(parent_workspace).unwrap().last_focused,
Some(WindowId(1))
@@ -1626,9 +1423,7 @@ mod tests {
#[test]
fn fullscreen_preserves_underlying_placement() {
let mut workspaces = WorkspaceSet::default();
workspaces
.insert_window(WindowId(1), Axis::Horizontal, 0.5)
.unwrap();
workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap();
let before = workspaces.placement(WindowId(1));
assert!(workspaces.toggle_fullscreen(WindowId(1)).unwrap());
@@ -1646,13 +1441,9 @@ mod tests {
#[test]
fn mapping_under_fullscreen_does_not_change_focus() {
let mut workspaces = WorkspaceSet::default();
workspaces
.insert_window(WindowId(1), Axis::Horizontal, 0.5)
.unwrap();
workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap();
workspaces.toggle_fullscreen(WindowId(1)).unwrap();
workspaces
.insert_window(WindowId(2), Axis::Horizontal, 0.5)
.unwrap();
workspaces.insert_window(WindowId(2), Axis::Horizontal, 0.5).unwrap();
assert_eq!(workspaces.active().last_focused, Some(WindowId(1)));
assert_eq!(workspaces.active().fullscreen, Some(WindowId(1)));
@@ -1664,17 +1455,10 @@ mod tests {
fn floating_dialog_can_focus_above_fullscreen() {
let mut workspaces = WorkspaceSet::default();
let workspace = workspaces.active_id();
workspaces
.insert_window(WindowId(1), Axis::Horizontal, 0.5)
.unwrap();
workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap();
workspaces.toggle_fullscreen(WindowId(1)).unwrap();
workspaces
.insert_floating_window(
WindowId(2),
workspace,
Rect::new(100.0, 100.0, 500.0, 400.0),
true,
)
.insert_floating_window(WindowId(2), workspace, Rect::new(100.0, 100.0, 500.0, 400.0), true)
.unwrap();
assert_eq!(workspaces.active().last_focused, Some(WindowId(2)));
@@ -1688,14 +1472,10 @@ mod tests {
#[test]
fn switching_to_fullscreen_restores_the_visible_window() {
let mut workspaces = WorkspaceSet::default();
workspaces
.insert_window(WindowId(1), Axis::Horizontal, 0.5)
.unwrap();
workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap();
workspaces.toggle_fullscreen(WindowId(1)).unwrap();
workspaces.switch_to_numeric(2).unwrap();
workspaces
.insert_window(WindowId(2), Axis::Horizontal, 0.5)
.unwrap();
workspaces.insert_window(WindowId(2), Axis::Horizontal, 0.5).unwrap();
workspaces
.move_window_to_numeric(WindowId(2), 1, Axis::Horizontal, 0.5)
.unwrap();
@@ -1704,9 +1484,7 @@ mod tests {
assert_eq!(workspaces.active().last_focused, Some(WindowId(1)));
assert_eq!(
workspaces.focus_window(WindowId(2)),
Err(WorkspaceError::InvalidState(
"focused window is hidden by fullscreen"
))
Err(WorkspaceError::InvalidState("focused window is hidden by fullscreen"))
);
assert!(workspaces.validate().is_ok());
}
@@ -1714,12 +1492,8 @@ mod tests {
#[test]
fn grouping_tiled_windows_preserves_workspace_membership() {
let mut workspaces = WorkspaceSet::default();
workspaces
.insert_window(WindowId(1), Axis::Horizontal, 0.5)
.unwrap();
workspaces
.insert_window(WindowId(2), Axis::Horizontal, 0.5)
.unwrap();
workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap();
workspaces.insert_window(WindowId(2), Axis::Horizontal, 0.5).unwrap();
workspaces.stack_window(WindowId(2), WindowId(1)).unwrap();
assert_eq!(workspaces.active().last_focused, Some(WindowId(2)));
@@ -1740,26 +1514,16 @@ mod tests {
fn layout_mode_conversion_preserves_membership_and_focus() {
let mut workspaces = WorkspaceSet::default();
for id in 1..=4 {
workspaces
.insert_window(WindowId(id), Axis::Horizontal, 0.5)
.unwrap();
workspaces.insert_window(WindowId(id), Axis::Horizontal, 0.5).unwrap();
}
workspaces.focus_window(WindowId(2)).unwrap();
let bounds = Rect::new(0.0, 0.0, 1_000.0, 800.0);
assert!(
workspaces
.set_active_layout_mode(LayoutMode::Tree, bounds)
.unwrap()
);
assert!(workspaces.set_active_layout_mode(LayoutMode::Tree, bounds).unwrap());
assert_eq!(workspaces.active().layout.mode(), LayoutMode::Tree);
assert_eq!(workspaces.active().last_focused, Some(WindowId(2)));
assert_eq!(
workspaces
.active()
.layout
.window_ids()
.collect::<HashSet<_>>(),
workspaces.active().layout.window_ids().collect::<HashSet<_>>(),
HashSet::from([WindowId(1), WindowId(2), WindowId(3), WindowId(4)])
);
@@ -1777,19 +1541,14 @@ mod tests {
fn scrolling_column_commands_preserve_workspace_state() {
let mut workspaces = WorkspaceSet::default();
for id in 1..=3 {
workspaces
.insert_window(WindowId(id), Axis::Horizontal, 0.5)
.unwrap();
workspaces.insert_window(WindowId(id), Axis::Horizontal, 0.5).unwrap();
}
workspaces.stack_window(WindowId(3), WindowId(2)).unwrap();
assert!(workspaces.extract_window(WindowId(3)).unwrap());
assert!(
workspaces
.cycle_column_width(
WindowId(3),
&[ColumnWidth::Proportion(0.5), ColumnWidth::Full],
)
.cycle_column_width(WindowId(3), &[ColumnWidth::Proportion(0.5), ColumnWidth::Full],)
.unwrap()
);
let bounds = Rect::new(0.0, 0.0, 1_000.0, 800.0);
@@ -1817,9 +1576,7 @@ mod tests {
0 if windows.len() < 48 => {
let window = WindowId(next_window);
next_window += 1;
workspaces
.insert_window(window, random_axis(random), 0.5)
.unwrap();
workspaces.insert_window(window, random_axis(random), 0.5).unwrap();
windows.push(window);
}
1 if !windows.is_empty() => {
@@ -1866,8 +1623,7 @@ mod tests {
})
.collect::<Vec<_>>();
if let Some(window) = candidates.get(random as usize % candidates.len().max(1))
{
if let Some(window) = candidates.get(random as usize % candidates.len().max(1)) {
workspaces.focus_window(*window).unwrap();
}
}