diff --git a/src/shell/focus/mod.rs b/src/shell/focus/mod.rs index e2038375..a3b855a3 100644 --- a/src/shell/focus/mod.rs +++ b/src/shell/focus/mod.rs @@ -21,10 +21,12 @@ use std::{borrow::Cow, mem, sync::Mutex}; use tracing::{debug, trace}; +pub use self::order::{render_input_order, Stage}; use self::target::{KeyboardFocusTarget, WindowGroup}; use super::{grabs::SeatMoveGrabState, layout::floating::FloatingLayout, SeatExt}; +mod order; pub mod target; pub struct FocusStack<'a>(pub(super) Option<&'a IndexSet>); diff --git a/src/shell/focus/order.rs b/src/shell/focus/order.rs new file mode 100644 index 00000000..f035d908 --- /dev/null +++ b/src/shell/focus/order.rs @@ -0,0 +1,374 @@ +use std::{ops::ControlFlow, time::Instant}; + +use cosmic_comp_config::workspace::WorkspaceLayout; +use keyframe::{ease, functions::EaseInOutCubic}; +use smithay::{ + desktop::{layer_map_for_output, LayerSurface, PopupKind, PopupManager}, + output::{Output, OutputNoMode}, + utils::{Logical, Point}, + wayland::{session_lock::LockSurface, shell::wlr_layer::Layer}, + xwayland::X11Surface, +}; + +use crate::{ + backend::render::ElementFilter, + shell::{ + layout::{floating::FloatingLayout, tiling::ANIMATION_DURATION}, + Shell, Workspace, WorkspaceDelta, + }, + utils::{geometry::*, prelude::OutputExt, quirks::WORKSPACE_OVERVIEW_NAMESPACE}, + wayland::protocols::workspace::WorkspaceHandle, +}; + +pub enum Stage<'a> { + SessionLock(Option<&'a LockSurface>), + LayerPopup { + layer: LayerSurface, + popup: &'a PopupKind, + location: Point, + }, + LayerSurface { + layer: LayerSurface, + location: Point, + }, + OverrideRedirect { + surface: &'a X11Surface, + location: Point, + }, + StickyPopups(&'a FloatingLayout), + Sticky(&'a FloatingLayout), + WorkspacePopups { + workspace: &'a Workspace, + offset: Point, + }, + Workspace { + workspace: &'a Workspace, + offset: Point, + }, +} + +pub fn render_input_order( + shell: &Shell, + output: &Output, + previous: Option<(WorkspaceHandle, usize, WorkspaceDelta)>, + current: (WorkspaceHandle, usize), + element_filter: ElementFilter, + callback: impl FnMut(Stage) -> ControlFlow, ()>, +) -> Result { + match render_input_order_internal(shell, output, previous, current, element_filter, callback) { + ControlFlow::Break(result) => result, + ControlFlow::Continue(_) => Ok(R::default()), + } +} + +fn render_input_order_internal( + shell: &Shell, + output: &Output, + previous: Option<(WorkspaceHandle, usize, WorkspaceDelta)>, + current: (WorkspaceHandle, usize), + element_filter: ElementFilter, + mut callback: impl FnMut(Stage) -> ControlFlow, ()>, +) -> ControlFlow, ()> { + // Session Lock + if let Some(session_lock) = &shell.session_lock { + return callback(Stage::SessionLock(session_lock.surfaces.get(output))); + } + + // Overlay-level layer shell + // overlay is above everything + for (layer, popup, location) in layer_popups(output, Layer::Overlay, element_filter) { + callback(Stage::LayerPopup { + layer, + popup: &popup, + location, + })?; + } + for (layer, location) in layer_surfaces(output, Layer::Overlay, element_filter) { + callback(Stage::LayerSurface { layer, location })?; + } + + // calculate a bunch of stuff for workspace transitions + + let Some(set) = shell.workspaces.sets.get(output) else { + return ControlFlow::Break(Err(OutputNoMode)); + }; + let Some(workspace) = set.workspaces.iter().find(|w| w.handle == current.0) else { + return ControlFlow::Break(Err(OutputNoMode)); + }; + let output_size = output.geometry().size; + let has_fullscreen = workspace + .fullscreen + .as_ref() + .filter(|f| !f.is_animating()) + .is_some(); + + let (previous, current_offset) = match previous.as_ref() { + Some((previous, previous_idx, start)) => { + let layout = shell.workspaces.layout; + + let Some(workspace) = shell.workspaces.space_for_handle(&previous) else { + return ControlFlow::Break(Err(OutputNoMode)); + }; + let has_fullscreen = workspace.fullscreen.is_some(); + + let percentage = match start { + WorkspaceDelta::Shortcut(st) => ease( + EaseInOutCubic, + 0.0, + 1.0, + Instant::now().duration_since(*st).as_millis() as f32 + / ANIMATION_DURATION.as_millis() as f32, + ), + WorkspaceDelta::Gesture(prog) => *prog as f32, + WorkspaceDelta::GestureEnd(st, spring) => { + (spring.value_at(Instant::now().duration_since(*st)) as f32).clamp(0.0, 1.0) + } + }; + + let offset = Point::::from(match (layout, *previous_idx < current.1) { + (WorkspaceLayout::Vertical, true) => { + (0, (-output_size.h as f32 * percentage).round() as i32) + } + (WorkspaceLayout::Vertical, false) => { + (0, (output_size.h as f32 * percentage).round() as i32) + } + (WorkspaceLayout::Horizontal, true) => { + ((-output_size.w as f32 * percentage).round() as i32, 0) + } + (WorkspaceLayout::Horizontal, false) => { + ((output_size.w as f32 * percentage).round() as i32, 0) + } + }); + + ( + Some((previous, has_fullscreen, offset)), + Point::::from(match (layout, *previous_idx < current.1) { + (WorkspaceLayout::Vertical, true) => (0, output_size.h + offset.y), + (WorkspaceLayout::Vertical, false) => (0, -(output_size.h - offset.y)), + (WorkspaceLayout::Horizontal, true) => (output_size.w + offset.x, 0), + (WorkspaceLayout::Horizontal, false) => (-(output_size.w - offset.x), 0), + }), + ) + } + None => (None, Point::default()), + }; + + // Top-level layer shell popups + if !has_fullscreen { + for (layer, popup, location) in layer_popups(output, Layer::Top, element_filter) { + callback(Stage::LayerPopup { + layer, + popup: &popup, + location, + })?; + } + } + + if element_filter != ElementFilter::LayerShellOnly { + // overlay redirect windows + // they need to be over sticky windows, because they could be popups of sticky windows, + // and we can't differenciate that. + for (surface, location) in shell + .override_redirect_windows + .iter() + .filter(|or| { + (*or) + .geometry() + .as_global() + .intersection(output.geometry()) + .is_some() + }) + .map(|or| (or, or.geometry().loc.as_global())) + { + callback(Stage::OverrideRedirect { surface, location })?; + } + + // sticky window popups + if !has_fullscreen { + callback(Stage::StickyPopups(&set.sticky_layer))?; + } + } + + if element_filter != ElementFilter::LayerShellOnly { + // previous workspace popups + if let Some((previous_handle, _, offset)) = previous.as_ref() { + let Some(workspace) = shell.workspaces.space_for_handle(previous_handle) else { + return ControlFlow::Break(Err(OutputNoMode)); + }; + + callback(Stage::WorkspacePopups { + workspace, + offset: *offset, + })?; + } + + // current workspace popups + let Some(workspace) = shell.workspaces.space_for_handle(¤t.0) else { + return ControlFlow::Break(Err(OutputNoMode)); + }; + + callback(Stage::WorkspacePopups { + workspace, + offset: current_offset, + })?; + } + + if !has_fullscreen { + // bottom layer popups + for (layer, popup, location) in layer_popups(output, Layer::Bottom, element_filter) { + callback(Stage::LayerPopup { + layer, + popup: &popup, + location, + })?; + } + } + + if let Some((_, has_fullscreen, offset)) = previous.as_ref() { + if !has_fullscreen { + // previous bottom layer popups + for (layer, popup, location) in layer_popups(output, Layer::Bottom, element_filter) { + callback(Stage::LayerPopup { + layer, + popup: &popup, + location: location + offset.as_global(), + })?; + } + } + } + + if !has_fullscreen { + // background layer popups + for (layer, popup, location) in layer_popups(output, Layer::Background, element_filter) { + callback(Stage::LayerPopup { + layer, + popup: &popup, + location, + })?; + } + } + + if let Some((_, has_fullscreen, offset)) = previous.as_ref() { + if !has_fullscreen { + // previous background layer popups + for (layer, popup, location) in layer_popups(output, Layer::Background, element_filter) + { + callback(Stage::LayerPopup { + layer, + popup: &popup, + location: location + offset.as_global(), + })?; + } + } + } + + if !has_fullscreen { + // top-layer shell + for (layer, location) in layer_surfaces(output, Layer::Top, element_filter) { + callback(Stage::LayerSurface { layer, location })?; + } + + // sticky windows + if element_filter != ElementFilter::LayerShellOnly { + callback(Stage::Sticky(&set.sticky_layer))?; + } + } + + if element_filter != ElementFilter::LayerShellOnly { + // workspace windows + callback(Stage::Workspace { + workspace, + offset: current_offset, + })?; + + // previous workspace windows + if let Some((previous_handle, _, offset)) = previous.as_ref() { + let Some(workspace) = shell.workspaces.space_for_handle(previous_handle) else { + return ControlFlow::Break(Err(OutputNoMode)); + }; + callback(Stage::Workspace { + workspace, + offset: *offset, + })?; + } + } + + if !has_fullscreen { + // bottom layer + for (layer, mut location) in layer_surfaces(output, Layer::Bottom, element_filter) { + location += current_offset.as_global(); + callback(Stage::LayerSurface { layer, location })?; + } + } + + if let Some((_, has_fullscreen, offset)) = previous.as_ref() { + if !has_fullscreen { + // previous bottom layer + for (layer, mut location) in layer_surfaces(output, Layer::Bottom, element_filter) { + location += offset.as_global(); + callback(Stage::LayerSurface { layer, location })?; + } + } + } + + if !has_fullscreen { + // background layer + for (layer, mut location) in layer_surfaces(output, Layer::Background, element_filter) { + location += current_offset.as_global(); + callback(Stage::LayerSurface { layer, location })?; + } + } + + if let Some((_, has_fullscreen, offset)) = previous.as_ref() { + if !has_fullscreen { + // previous background layer + for (layer, mut location) in layer_surfaces(output, Layer::Background, element_filter) { + location += offset.as_global(); + callback(Stage::LayerSurface { layer, location })?; + } + } + } + + ControlFlow::Continue(()) +} + +fn layer_popups<'a>( + output: &'a Output, + layer: Layer, + element_filter: ElementFilter, +) -> impl Iterator)> + 'a { + layer_surfaces(output, layer, element_filter).flat_map(move |(surface, location)| { + let location_clone = location.clone(); + let surface_clone = surface.clone(); + PopupManager::popups_for_surface(surface.wl_surface()).map(move |(popup, popup_offset)| { + let offset = (popup_offset - popup.geometry().loc) + .as_local() + .to_global(output); + (surface_clone.clone(), popup, (location_clone + offset)) + }) + }) +} + +fn layer_surfaces<'a>( + output: &'a Output, + layer: Layer, + element_filter: ElementFilter, +) -> impl Iterator)> + 'a { + // we want to avoid deadlocks on the layer-map in callbacks, so we need to clone the layer surfaces + let layers = { + let layer_map = layer_map_for_output(output); + layer_map + .layers_on(layer) + .rev() + .map(|s| (s.clone(), layer_map.layer_geometry(s).unwrap())) + .collect::>() + }; + + layers + .into_iter() + .filter(move |(s, _)| { + !(element_filter == ElementFilter::ExcludeWorkspaceOverview + && s.namespace() == WORKSPACE_OVERVIEW_NAMESPACE) + }) + .map(|(surface, geometry)| (surface, geometry.loc.as_local().to_global(output))) +}