feat: lay out workspaces per output
This commit is contained in:
1 parent
1934cef705
commit
8d305bf338
3 files changed
+184
-82
No files matched your search
+22
-17
@@ -280,6 +280,7 @@ impl Ferese {
|
|||||||
let pointer = self.seat.get_pointer().expect("seat has a pointer");
|
let pointer = self.seat.get_pointer().expect("seat has a pointer");
|
||||||
let keyboard = self.seat.get_keyboard().expect("seat has a keyboard");
|
let keyboard = self.seat.get_keyboard().expect("seat has a keyboard");
|
||||||
let position = pointer.current_location();
|
let position = pointer.current_location();
|
||||||
|
self.focus_output_at(position);
|
||||||
|
|
||||||
if let Some((layer, _, _)) = self.layer_under(position) {
|
if let Some((layer, _, _)) = self.layer_under(position) {
|
||||||
if layer.can_receive_keyboard_focus() {
|
if layer.can_receive_keyboard_focus() {
|
||||||
@@ -315,23 +316,27 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn clamp_pointer_position(&self, requested: Point<f64, Logical>) -> Point<f64, Logical> {
|
fn clamp_pointer_position(&self, requested: Point<f64, Logical>) -> Point<f64, Logical> {
|
||||||
let Some(output) = self.space.outputs().next() else {
|
self.space
|
||||||
return requested;
|
.outputs()
|
||||||
};
|
.filter_map(|output| self.space.output_geometry(output))
|
||||||
let Some(geometry) = self.space.output_geometry(output) else {
|
.map(|geometry| {
|
||||||
return requested;
|
let position = Point::from((
|
||||||
};
|
requested.x.clamp(
|
||||||
|
f64::from(geometry.loc.x),
|
||||||
Point::from((
|
f64::from(geometry.loc.x + geometry.size.w - 1),
|
||||||
requested.x.clamp(
|
),
|
||||||
f64::from(geometry.loc.x),
|
requested.y.clamp(
|
||||||
f64::from(geometry.loc.x + geometry.size.w - 1),
|
f64::from(geometry.loc.y),
|
||||||
),
|
f64::from(geometry.loc.y + geometry.size.h - 1),
|
||||||
requested.y.clamp(
|
),
|
||||||
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(
|
fn constrain_pointer_position(
|
||||||
|
|||||||
+120
-65
@@ -219,6 +219,32 @@ impl Ferese {
|
|||||||
.find_map(|(output, id)| (*id == focused).then_some(output))
|
.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) {
|
pub(crate) fn queue_dmabuf_import(&mut self, dmabuf: Dmabuf, notifier: ImportNotifier) {
|
||||||
self.pending_dmabuf_imports.push((dmabuf, notifier));
|
self.pending_dmabuf_imports.push((dmabuf, notifier));
|
||||||
}
|
}
|
||||||
@@ -593,40 +619,89 @@ impl Ferese {
|
|||||||
|
|
||||||
pub fn relayout(&mut self) {
|
pub fn relayout(&mut self) {
|
||||||
self.arrange_layers();
|
self.arrange_layers();
|
||||||
let Some(bounds) = self.output_bounds() else {
|
let outputs = self.space.outputs().cloned().collect::<Vec<_>>();
|
||||||
return;
|
let mut visible = HashSet::new();
|
||||||
};
|
let mut placements = Vec::new();
|
||||||
let fullscreen_bounds = self.full_output_bounds().unwrap_or(bounds);
|
|
||||||
let layout = match self.tiled_layout(bounds) {
|
|
||||||
Ok(layout) => layout,
|
|
||||||
Err(error) => {
|
|
||||||
tracing::error!(%error, "failed to compute tiled geometry");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let tiled_geometry = layout.geometry;
|
|
||||||
|
|
||||||
for warning in layout.warnings {
|
for output in outputs {
|
||||||
tracing::warn!(
|
let Some(output_id) = self.output_ids.get(&output).copied() else {
|
||||||
window = ?warning.window,
|
continue;
|
||||||
kind = ?warning.kind,
|
};
|
||||||
requested = warning.requested,
|
let Some(workspace_id) = self.output_workspaces.active_workspace(output_id) else {
|
||||||
assigned = warning.assigned,
|
continue;
|
||||||
"window size constraint could not be satisfied exactly"
|
};
|
||||||
);
|
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, ?workspace_id, "failed to compute tiled geometry");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for warning in layout.warnings {
|
||||||
|
tracing::warn!(
|
||||||
|
window = ?warning.window,
|
||||||
|
kind = ?warning.kind,
|
||||||
|
requested = warning.requested,
|
||||||
|
assigned = warning.assigned,
|
||||||
|
"window size constraint could not be satisfied exactly"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 active = self.workspaces.active_id();
|
|
||||||
let fullscreen = self.workspaces.active().fullscreen;
|
|
||||||
let hidden = self
|
let hidden = self
|
||||||
.window_ids
|
.window_ids
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|(_, id)| {
|
.filter(|(_, id)| !visible.contains(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))
|
|
||||||
})
|
|
||||||
.map(|(window, _)| window.clone())
|
.map(|(window, _)| window.clone())
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
@@ -634,35 +709,6 @@ impl Ferese {
|
|||||||
self.space.unmap_elem(&window);
|
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();
|
let now = self.start_time.elapsed();
|
||||||
|
|
||||||
for (window, id, rect, is_fullscreen, is_floating) in placements {
|
for (window, id, rect, is_fullscreen, is_floating) in placements {
|
||||||
@@ -1021,12 +1067,20 @@ impl Ferese {
|
|||||||
})
|
})
|
||||||
.unwrap_or(Axis::Horizontal);
|
.unwrap_or(Axis::Horizontal);
|
||||||
|
|
||||||
if let Err(error) = self
|
let destination = match self
|
||||||
.workspaces
|
.workspaces
|
||||||
.move_window_to_numeric(window, index, axis, 0.5)
|
.move_window_to_numeric(window, index, axis, 0.5)
|
||||||
{
|
{
|
||||||
tracing::error!(%error, ?window, index, "failed to move window to workspace");
|
Ok(destination) => destination,
|
||||||
return;
|
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.focused_window = self.workspaces.active().last_focused;
|
||||||
@@ -1054,6 +1108,10 @@ impl Ferese {
|
|||||||
let output = self
|
let output = self
|
||||||
.focused_output()
|
.focused_output()
|
||||||
.or_else(|| self.space.outputs().next())?;
|
.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 geometry = self.space.output_geometry(output)?;
|
||||||
let zone = layer_map_for_output(output).non_exclusive_zone();
|
let zone = layer_map_for_output(output).non_exclusive_zone();
|
||||||
|
|
||||||
@@ -1065,10 +1123,7 @@ impl Ferese {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn full_output_bounds(&self) -> Option<Rect> {
|
fn full_output_bounds_for(&self, output: &Output) -> Option<Rect> {
|
||||||
let output = self
|
|
||||||
.focused_output()
|
|
||||||
.or_else(|| self.space.outputs().next())?;
|
|
||||||
let geometry = self.space.output_geometry(output)?;
|
let geometry = self.space.output_geometry(output)?;
|
||||||
|
|
||||||
Some(Rect::new(
|
Some(Rect::new(
|
||||||
|
|||||||
@@ -285,6 +285,30 @@ impl OutputWorkspaceMap {
|
|||||||
Ok(WorkspaceSwitch::Activated(output))
|
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(
|
fn migration_target(
|
||||||
&self,
|
&self,
|
||||||
removed: OutputId,
|
removed: OutputId,
|
||||||
@@ -498,4 +522,22 @@ mod tests {
|
|||||||
);
|
);
|
||||||
assert_eq!(outputs.focused_output(), Some(OutputId(2)));
|
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