Files
feresaria/compositor/src/state/floating.rs
T

438 lines
17 KiB
Rust

use super::*;
impl Ferese {
pub fn toggle_focused_floating(&mut self) {
let Some(window) = self.focused_window else {
return;
};
let Some(bounds) = self.output_bounds() else {
return;
};
let mut floating_rect = match self.workspaces.placement(window) {
Some(WindowPlacement::Tiled) => self
.tiled_layout(bounds)
.ok()
.and_then(|layout| layout.geometry.get(&window).copied())
.unwrap_or_else(|| centered_floating_rect(bounds)),
Some(WindowPlacement::Floating { rect }) => rect,
None => return,
};
if let Some(handle) = self
.window_ids
.iter()
.find_map(|(handle, id)| (*id == window).then_some(handle.clone()))
{
if matches!(
self.workspaces.placement(window),
Some(WindowPlacement::Floating { .. })
) {
self.remember_floating(&handle);
} else {
floating_rect = self.place_floating(
&handle,
Some(window),
bounds,
(floating_rect.width, floating_rect.height),
None,
);
}
}
let axis = self
.workspaces
.active()
.layout
.automatic_axis(Some(window), bounds)
.unwrap_or(Axis::Horizontal);
if let Err(error) = self.workspaces.toggle_floating(window, floating_rect, axis, 0.5) {
tracing::error!(%error, ?window, "failed to toggle floating window");
return;
}
self.relayout();
}
pub fn toggle_focused_fullscreen(&mut self) {
let Some(window) = self.focused_window else {
return;
};
if let Err(error) = self.workspaces.toggle_fullscreen(window) {
tracing::error!(%error, ?window, "failed to toggle fullscreen window");
return;
}
self.relayout();
}
pub fn toggle_focused_maximized(&mut self) {
let Some(window) = self.focused_window else {
return;
};
// Super+F from true fullscreen enters decorated maximization.
let fullscreen = self
.workspaces
.workspace_for_window(window)
.and_then(|workspace| self.workspaces.workspace(workspace))
.is_some_and(|workspace| workspace.fullscreen == Some(window));
let enabled = fullscreen || !self.maximized_windows.contains(&window);
self.set_window_maximized(window, enabled);
}
pub fn set_window_maximized(&mut self, window: WindowId, enabled: bool) {
if self.workspaces.workspace_for_window(window).is_none() {
return;
}
if enabled {
let _ = self.workspaces.set_fullscreen(window, false);
self.maximized_windows.insert(window);
} else {
self.maximized_windows.remove(&window);
}
if self.workspaces.placement(window) == Some(WindowPlacement::Tiled) {
let workspace_id = self.workspaces.workspace_for_window(window).unwrap();
if let Some(workspace) = self.workspaces.workspace_mut(workspace_id)
&& let WorkspaceLayout::Scrolling(layout) = &mut workspace.layout
{
if enabled {
if let Some(column) = layout.columns().iter().find(|column| column.windows.contains(&window)) {
self.maximized_column_widths.entry(window).or_insert(column.width);
}
let _ = layout.set_column_width(window, ColumnWidth::Proportion(1.0));
} else if let Some(width) = self.maximized_column_widths.remove(&window) {
let _ = layout.set_column_width(window, width);
}
}
}
self.relayout();
}
pub(crate) fn output_has_fullscreen(&self, output: &Output) -> bool {
!self.overview.is_presenting()
&& self
.output_id(output)
.and_then(|id| self.output_workspaces.active_workspace(id))
.and_then(|workspace| self.workspaces.workspace(workspace))
.is_some_and(|workspace| workspace.fullscreen.is_some())
}
pub fn set_window_fullscreen(&mut self, window: WindowId, enabled: bool) {
match self.workspaces.set_fullscreen(window, enabled) {
Ok(true) => self.relayout(),
Ok(false) => {}
Err(error) => {
tracing::error!(%error, ?window, enabled, "failed to set fullscreen window")
}
}
}
pub fn is_floating_window(&self, window: &Window) -> bool {
self.window_ids
.get(window)
.and_then(|id| self.workspaces.placement(*id))
.is_some_and(|placement| matches!(placement, WindowPlacement::Floating { .. }))
}
pub fn set_floating_window_geometry(
&mut self,
window: &Window,
location: Point<i32, Logical>,
size: Size<i32, Logical>,
) {
let Some(id) = self.window_ids.get(window).copied() else {
return;
};
self.floating_resize_anchors.remove(&id);
let rect = Rect::new(
location.x as f64,
location.y as f64,
size.w.max(1) as f64,
size.h.max(1) as f64,
);
if let Err(error) = self.workspaces.set_floating_rect(id, rect) {
tracing::error!(%error, ?id, "failed to update floating window geometry");
return;
}
// Direct manipulation follows the pointer, including while the client is
// still drawing its next buffer. Rendering and hit testing share this rect.
self.resize_transactions.remove(&id);
self.resize_snapshots.remove(&id);
let geometry = self
.window_geometry
.entry(id)
.or_insert_with(|| WindowGeometry::new(rect, client_size(window)));
if let Some(size) = geometry.follow_pointer(rect, self.start_time.elapsed())
&& let Some(toplevel) = window.toplevel()
{
toplevel.with_pending_state(|state| {
state.size = Some((size.width, size.height).into());
});
toplevel.send_pending_configure();
}
self.scrolling_world_x.remove(&id);
self.viewport_coupled_widths.remove(&id);
self.space.map_element(window.clone(), location, false);
self.sync_window_stacking();
// Move/resize/cancel callbacks run with Smithay's pointer mutex held.
// Cursor rendering reads current_location(), which would lock it again.
// The event-loop epilogue coalesces this redraw after the grab returns.
self.cursor_redraw_pending = true;
}
pub(super) fn output_bounds(&self) -> Option<Rect> {
let output = self.focused_output().or_else(|| self.space.outputs().next())?;
self.output_bounds_for(output)
}
pub(super) fn floating_outputs(&self) -> Vec<(String, Rect)> {
self.space
.outputs()
.filter_map(|output| self.output_bounds_for(output).map(|work| (output.name(), work)))
.collect()
}
pub(super) fn floating_metadata(window: &Window) -> (Option<String>, Option<WlSurface>) {
window
.toplevel()
.map(|top| {
let app = with_states(top.wl_surface(), |states| {
states
.data_map
.get::<XdgToplevelSurfaceData>()
.and_then(|data| data.lock().ok().and_then(|data| data.app_id.clone()))
});
(app, top.parent())
})
.unwrap_or_default()
}
pub(super) fn floating_obstacles(&self, exclude: Option<WindowId>, work: Rect) -> Vec<(u64, Rect)> {
self.space
.elements()
.filter_map(|window| {
let id = *self.window_ids.get(window)?;
if Some(id) == exclude {
return None;
}
let workspace = self.workspaces.workspace_for_window(id)?;
let owner = self.output_workspaces.output_for_workspace(workspace)?;
if self.output_workspaces.active_workspace(owner) != Some(workspace) {
return None;
}
if self
.workspaces
.workspace(workspace)?
.fullscreen
.is_some_and(|full| full != id)
&& !matches!(self.workspaces.placement(id), Some(WindowPlacement::Floating { .. }))
{
return None;
}
let rect = self.presented_window_rect(id)?;
crate::floating::intersection(rect, work).map(|visible| (id.0, visible))
})
.collect()
}
pub(super) fn place_floating(
&mut self,
window: &Window,
id: Option<WindowId>,
work: Rect,
size: (f64, f64),
overrides: Option<(Option<f64>, Option<f64>)>,
) -> Rect {
use crate::floating::{centered, clamp_to_work, intersection, min_overlap};
let outputs = self.floating_outputs();
let output = outputs
.iter()
.find(|(_, area)| *area == work)
.map(|(name, _)| name.as_str())
.unwrap_or("unknown");
let (app, parent) = Self::floating_metadata(window);
let parent_rect = parent
.as_ref()
.and_then(|surface| {
self.window_ids.iter().find_map(|(window, id)| {
window
.toplevel()
.is_some_and(|top| top.wl_surface() == surface)
.then_some(*id)
})
})
.and_then(|parent| self.presented_window_rect(parent))
.and_then(|rect| intersection(rect, work));
let dialog = crate::window_rules::is_native_dialog(app.as_deref());
let remembered = id
.and_then(|id| self.floating_window_memory.get(&id))
.or_else(|| {
if parent.is_none() {
app.as_deref().and_then(|app| self.floating_memory.get(app))
} else {
None
}
})
.filter(|entry| !dialog && entry.output == output)
.and_then(|entry| entry.restore(&outputs));
let (width, height) = overrides.unwrap_or_default();
let constraints = window
.toplevel()
.map(|top| {
with_states(top.wl_surface(), |states| {
let mut cache = states.cached_state.get::<SurfaceCachedState>();
let state = cache.current();
SizeConstraints {
min_width: state.min_size.w.max(1) as f64,
min_height: state.min_size.h.max(1) as f64,
max_width: (state.max_size.w > 0).then_some(state.max_size.w as f64),
max_height: (state.max_size.h > 0).then_some(state.max_size.h as f64),
}
})
})
.unwrap_or_default();
let stored_size = if parent_rect.is_none() {
remembered.map(|r| (r.width, r.height)).unwrap_or(size)
} else {
size
};
let sized = constrained_floating_rect(
Rect::new(0., 0., width.unwrap_or(stored_size.0), height.unwrap_or(stored_size.1)),
constraints,
);
let dimensions = (sized.width, sized.height);
let rect = if dialog {
centered(dimensions, work)
} else if let Some(parent) = parent_rect {
centered(dimensions, parent)
} else if let Some(mut rect) = remembered {
rect.width = dimensions.0;
rect.height = dimensions.1;
rect
} else {
let others = self
.floating_obstacles(id, work)
.into_iter()
.map(|(_, r)| r)
.collect::<Vec<_>>();
let anchor = id
.and_then(|id| self.floating_placement_anchors.get(&id).copied())
.or_else(|| {
self.focused_window
.filter(|focused| Some(*focused) != id)
.and_then(|focused| self.presented_window_rect(focused))
})
.and_then(|rect| intersection(rect, work))
.unwrap_or(work);
min_overlap(
dimensions,
work,
&others,
(anchor.x + anchor.width / 2., anchor.y + anchor.height / 2.),
)
.unwrap_or_else(|| self.floating_cascade.place(output, work, dimensions))
};
clamp_to_work(rect, work)
}
pub(crate) fn remember_floating(&mut self, window: &Window) {
let Some(id) = self.window_ids.get(window).copied() else {
return;
};
let Some(WindowPlacement::Floating { rect }) = self.workspaces.placement(id) else {
return;
};
let (app, parent) = Self::floating_metadata(window);
if parent.is_some() || crate::window_rules::is_native_dialog(app.as_deref()) {
return;
}
// A drag can finish on another output without changing workspace ownership.
let Some((output, work)) = self
.floating_outputs()
.into_iter()
.max_by(|(_, a), (_, b)| {
let area = |work| crate::floating::intersection(rect, work).map_or(0., |r| r.width * r.height);
area(*a).total_cmp(&area(*b))
})
.filter(|(_, work)| crate::floating::intersection(rect, *work).is_some())
else {
return;
};
let entry = crate::floating::Remembered::new(output, rect, work);
self.floating_window_memory.insert(id, entry.clone());
if let Some(app) = app {
self.floating_memory.save(app, entry);
}
if self.nested_backend.is_none()
&& let Some(path) = crate::floating::Memory::path()
&& let Err(error) = self.floating_save_worker.queue(path, &self.floating_memory)
{
tracing::warn!(%error,"could not queue floating window placement save");
}
}
pub(crate) fn floating_snap_lines(
&self,
window: &Window,
raw: Rect,
) -> (Vec<crate::floating::SnapLine>, Vec<crate::floating::SnapLine>) {
let id = self.window_ids.get(window).copied();
let work = self
.floating_outputs()
.into_iter()
.max_by(|(_, a), (_, b)| {
let area = |work| crate::floating::intersection(raw, work).map_or(0., |r| r.width * r.height);
area(*a).total_cmp(&area(*b))
})
.filter(|(_, work)| crate::floating::intersection(raw, *work).is_some())
.map(|(_, work)| work)
.or_else(|| id.and_then(|id| self.floating_bounds_for_window(id)));
let Some(work) = work else {
return Default::default();
};
crate::floating::snap_lines(raw, work, &self.floating_obstacles(id, work))
}
pub(crate) fn floating_snap_bypassed(&self) -> bool {
self.seat
.get_keyboard()
.is_some_and(|keyboard| keyboard.modifier_state().shift)
}
pub(super) fn floating_bounds_for_window(&self, window: WindowId) -> Option<Rect> {
let output_id = self
.workspaces
.workspace_for_window(window)
.and_then(|workspace| self.output_workspaces.output_for_workspace(workspace));
self.output_ids
.iter()
.find(|(_, id)| Some(**id) == output_id)
.and_then(|(output, _)| self.output_bounds_for(output))
.or_else(|| self.output_bounds())
}
pub(crate) 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();
Some(Rect::new(
(geometry.loc.x + zone.loc.x) as f64,
(geometry.loc.y + zone.loc.y) as f64,
zone.size.w.max(0) as f64,
zone.size.h.max(0) as f64,
))
}
pub(super) fn full_output_bounds_for(&self, output: &Output) -> Option<Rect> {
let geometry = self.space.output_geometry(output)?;
Some(Rect::new(
geometry.loc.x as f64,
geometry.loc.y as f64,
geometry.size.w as f64,
geometry.size.h as f64,
))
}
}