Fix compositor output routing and pointer hints; format imports
This commit is contained in:
1 parent
88e4f3c135
commit
46509f880a
108 files changed
+3682
-8371
No files matched your search
@@ -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()));
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user