feat: lay out workspaces per output
This commit is contained in:
1 parent
1934cef705
commit
8d305bf338
3 files changed
+161
-59
No files matched your search
+14
-9
@@ -280,6 +280,7 @@ impl Ferese {
|
||||
let pointer = self.seat.get_pointer().expect("seat has a pointer");
|
||||
let keyboard = self.seat.get_keyboard().expect("seat has a keyboard");
|
||||
let position = pointer.current_location();
|
||||
self.focus_output_at(position);
|
||||
|
||||
if let Some((layer, _, _)) = self.layer_under(position) {
|
||||
if layer.can_receive_keyboard_focus() {
|
||||
@@ -315,14 +316,11 @@ impl Ferese {
|
||||
}
|
||||
|
||||
fn clamp_pointer_position(&self, requested: Point<f64, Logical>) -> Point<f64, Logical> {
|
||||
let Some(output) = self.space.outputs().next() else {
|
||||
return requested;
|
||||
};
|
||||
let Some(geometry) = self.space.output_geometry(output) else {
|
||||
return requested;
|
||||
};
|
||||
|
||||
Point::from((
|
||||
self.space
|
||||
.outputs()
|
||||
.filter_map(|output| self.space.output_geometry(output))
|
||||
.map(|geometry| {
|
||||
let position = Point::from((
|
||||
requested.x.clamp(
|
||||
f64::from(geometry.loc.x),
|
||||
f64::from(geometry.loc.x + geometry.size.w - 1),
|
||||
@@ -331,7 +329,14 @@ impl Ferese {
|
||||
f64::from(geometry.loc.y),
|
||||
f64::from(geometry.loc.y + geometry.size.h - 1),
|
||||
),
|
||||
))
|
||||
));
|
||||
let x = position.x - requested.x;
|
||||
let y = position.y - requested.y;
|
||||
(x * x + y * y, position)
|
||||
})
|
||||
.min_by(|(left, _), (right, _)| left.total_cmp(right))
|
||||
.map(|(_, position)| position)
|
||||
.unwrap_or(requested)
|
||||
}
|
||||
|
||||
fn constrain_pointer_position(
|
||||
|
||||
+105
-50
@@ -219,6 +219,32 @@ impl Ferese {
|
||||
.find_map(|(output, id)| (*id == focused).then_some(output))
|
||||
}
|
||||
|
||||
pub(crate) fn focus_output_at(&mut self, position: Point<f64, Logical>) {
|
||||
let output = self.space.outputs().find_map(|output| {
|
||||
let geometry = self.space.output_geometry(output)?;
|
||||
(position.x >= f64::from(geometry.loc.x)
|
||||
&& position.y >= f64::from(geometry.loc.y)
|
||||
&& position.x < f64::from(geometry.loc.x + geometry.size.w)
|
||||
&& position.y < f64::from(geometry.loc.y + geometry.size.h))
|
||||
.then(|| output.clone())
|
||||
});
|
||||
let Some(output) = output else {
|
||||
return;
|
||||
};
|
||||
let Some(output_id) = self.output_ids.get(&output).copied() else {
|
||||
return;
|
||||
};
|
||||
if self.output_workspaces.focused_output() == Some(output_id) {
|
||||
return;
|
||||
}
|
||||
let Some(workspace) = self.output_workspaces.active_workspace(output_id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.activate_output_workspace(output_id, workspace);
|
||||
self.relayout();
|
||||
}
|
||||
|
||||
pub(crate) fn queue_dmabuf_import(&mut self, dmabuf: Dmabuf, notifier: ImportNotifier) {
|
||||
self.pending_dmabuf_imports.push((dmabuf, notifier));
|
||||
}
|
||||
@@ -593,18 +619,42 @@ impl Ferese {
|
||||
|
||||
pub fn relayout(&mut self) {
|
||||
self.arrange_layers();
|
||||
let Some(bounds) = self.output_bounds() else {
|
||||
return;
|
||||
let outputs = self.space.outputs().cloned().collect::<Vec<_>>();
|
||||
let mut visible = HashSet::new();
|
||||
let mut placements = Vec::new();
|
||||
|
||||
for output in outputs {
|
||||
let Some(output_id) = self.output_ids.get(&output).copied() else {
|
||||
continue;
|
||||
};
|
||||
let fullscreen_bounds = self.full_output_bounds().unwrap_or(bounds);
|
||||
let layout = match self.tiled_layout(bounds) {
|
||||
let Some(workspace_id) = self.output_workspaces.active_workspace(output_id) else {
|
||||
continue;
|
||||
};
|
||||
let Some(workspace) = self.workspaces.workspace(workspace_id) else {
|
||||
continue;
|
||||
};
|
||||
let Some(bounds) = self.output_bounds_for(&output) else {
|
||||
continue;
|
||||
};
|
||||
let fullscreen_bounds = self.full_output_bounds_for(&output).unwrap_or(bounds);
|
||||
let focused = self
|
||||
.focused_window
|
||||
.filter(|window| {
|
||||
self.workspaces.workspace_for_window(*window) == Some(workspace_id)
|
||||
})
|
||||
.or(workspace.last_focused);
|
||||
let layout = match workspace.layout.geometry_with_constraints(
|
||||
bounds,
|
||||
GapConfig::default(),
|
||||
&self.window_constraints(),
|
||||
focused,
|
||||
) {
|
||||
Ok(layout) => layout,
|
||||
Err(error) => {
|
||||
tracing::error!(%error, "failed to compute tiled geometry");
|
||||
return;
|
||||
tracing::error!(%error, ?workspace_id, "failed to compute tiled geometry");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let tiled_geometry = layout.geometry;
|
||||
|
||||
for warning in layout.warnings {
|
||||
tracing::warn!(
|
||||
@@ -615,18 +665,43 @@ impl Ferese {
|
||||
"window size constraint could not be satisfied exactly"
|
||||
);
|
||||
}
|
||||
let active = self.workspaces.active_id();
|
||||
let fullscreen = self.workspaces.active().fullscreen;
|
||||
|
||||
for (window, id) in &self.window_ids {
|
||||
if self.workspaces.workspace_for_window(*id) != Some(workspace_id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let rect = if workspace.fullscreen == Some(*id) {
|
||||
fullscreen_bounds
|
||||
} else if workspace.fullscreen.is_some() {
|
||||
continue;
|
||||
} else {
|
||||
match self.workspaces.placement(*id) {
|
||||
Some(WindowPlacement::Tiled) => {
|
||||
let Some(rect) = layout.geometry.get(id).copied() else {
|
||||
continue;
|
||||
};
|
||||
rect
|
||||
}
|
||||
Some(WindowPlacement::Floating { rect }) => rect,
|
||||
None => continue,
|
||||
}
|
||||
};
|
||||
let is_fullscreen = workspace.fullscreen == Some(*id);
|
||||
let is_floating = matches!(
|
||||
self.workspaces.placement(*id),
|
||||
Some(WindowPlacement::Floating { .. })
|
||||
);
|
||||
|
||||
visible.insert(*id);
|
||||
placements.push((window.clone(), *id, rect, is_fullscreen, is_floating));
|
||||
}
|
||||
}
|
||||
|
||||
let hidden = self
|
||||
.window_ids
|
||||
.iter()
|
||||
.filter(|(_, id)| {
|
||||
self.workspaces.workspace_for_window(**id) != Some(active)
|
||||
|| fullscreen.is_some_and(|fullscreen| fullscreen != **id)
|
||||
|| (fullscreen.is_none()
|
||||
&& self.workspaces.placement(**id) == Some(WindowPlacement::Tiled)
|
||||
&& !tiled_geometry.contains_key(id))
|
||||
})
|
||||
.filter(|(_, id)| !visible.contains(id))
|
||||
.map(|(window, _)| window.clone())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
@@ -634,35 +709,6 @@ impl Ferese {
|
||||
self.space.unmap_elem(&window);
|
||||
}
|
||||
|
||||
let placements = self
|
||||
.window_ids
|
||||
.iter()
|
||||
.filter_map(|(window, id)| {
|
||||
if self.workspaces.workspace_for_window(*id) != Some(active) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let rect = if fullscreen == Some(*id) {
|
||||
fullscreen_bounds
|
||||
} else if fullscreen.is_some() {
|
||||
return None;
|
||||
} else {
|
||||
match self.workspaces.placement(*id)? {
|
||||
WindowPlacement::Tiled => *tiled_geometry.get(id)?,
|
||||
WindowPlacement::Floating { rect } => rect,
|
||||
}
|
||||
};
|
||||
|
||||
let is_fullscreen = fullscreen == Some(*id);
|
||||
let is_floating = matches!(
|
||||
self.workspaces.placement(*id),
|
||||
Some(WindowPlacement::Floating { .. })
|
||||
);
|
||||
|
||||
Some((window.clone(), *id, rect, is_fullscreen, is_floating))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let now = self.start_time.elapsed();
|
||||
|
||||
for (window, id, rect, is_fullscreen, is_floating) in placements {
|
||||
@@ -1021,13 +1067,21 @@ impl Ferese {
|
||||
})
|
||||
.unwrap_or(Axis::Horizontal);
|
||||
|
||||
if let Err(error) = self
|
||||
let destination = match self
|
||||
.workspaces
|
||||
.move_window_to_numeric(window, index, axis, 0.5)
|
||||
{
|
||||
Ok(destination) => destination,
|
||||
Err(error) => {
|
||||
tracing::error!(%error, ?window, index, "failed to move window to workspace");
|
||||
return;
|
||||
}
|
||||
};
|
||||
if let Some(output) = self.output_workspaces.focused_output()
|
||||
&& let Err(error) = self.output_workspaces.assign_workspace(output, destination)
|
||||
{
|
||||
tracing::error!(%error, ?destination, "failed to assign destination workspace");
|
||||
}
|
||||
|
||||
self.focused_window = self.workspaces.active().last_focused;
|
||||
self.relayout();
|
||||
@@ -1054,6 +1108,10 @@ impl Ferese {
|
||||
let output = self
|
||||
.focused_output()
|
||||
.or_else(|| self.space.outputs().next())?;
|
||||
self.output_bounds_for(output)
|
||||
}
|
||||
|
||||
fn output_bounds_for(&self, output: &Output) -> Option<Rect> {
|
||||
let geometry = self.space.output_geometry(output)?;
|
||||
let zone = layer_map_for_output(output).non_exclusive_zone();
|
||||
|
||||
@@ -1065,10 +1123,7 @@ impl Ferese {
|
||||
))
|
||||
}
|
||||
|
||||
fn full_output_bounds(&self) -> Option<Rect> {
|
||||
let output = self
|
||||
.focused_output()
|
||||
.or_else(|| self.space.outputs().next())?;
|
||||
fn full_output_bounds_for(&self, output: &Output) -> Option<Rect> {
|
||||
let geometry = self.space.output_geometry(output)?;
|
||||
|
||||
Some(Rect::new(
|
||||
|
||||
@@ -285,6 +285,30 @@ impl OutputWorkspaceMap {
|
||||
Ok(WorkspaceSwitch::Activated(output))
|
||||
}
|
||||
|
||||
pub fn assign_workspace(
|
||||
&mut self,
|
||||
output: OutputId,
|
||||
workspace: WorkspaceId,
|
||||
) -> Result<OutputId, OutputError> {
|
||||
if !self.outputs.contains_key(&output) {
|
||||
return Err(OutputError::UnknownOutput(output));
|
||||
}
|
||||
if let Some(owner) = self.assignments.get(&workspace).copied() {
|
||||
return Ok(owner);
|
||||
}
|
||||
|
||||
self.assignments.insert(workspace, output);
|
||||
self.outputs
|
||||
.get_mut(&output)
|
||||
.expect("output was checked above")
|
||||
.workspaces
|
||||
.insert(workspace);
|
||||
self.bump_revision(workspace);
|
||||
|
||||
debug_assert!(self.validate());
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
fn migration_target(
|
||||
&self,
|
||||
removed: OutputId,
|
||||
@@ -498,4 +522,22 @@ mod tests {
|
||||
);
|
||||
assert_eq!(outputs.focused_output(), Some(OutputId(2)));
|
||||
}
|
||||
|
||||
#[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();
|
||||
|
||||
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))
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user