From 7da0bc430a1c35f133ff24552c1c9903c65d3457 Mon Sep 17 00:00:00 2001 From: skewballfox Date: Wed, 4 Sep 2024 11:13:59 -0500 Subject: [PATCH] added cursor_follows_focus and focus_follows_cursor --- cosmic-comp-config/src/input.rs | 2 + cosmic-comp-config/src/lib.rs | 9 + src/backend/render/mod.rs | 6 +- src/config/mod.rs | 18 + src/input/mod.rs | 747 +++++++++++--------- src/shell/element/window.rs | 2 +- src/shell/focus/mod.rs | 126 +++- src/shell/grabs/menu/default.rs | 6 +- src/shell/grabs/menu/mod.rs | 1 + src/shell/grabs/moving.rs | 1 + src/shell/layout/floating/mod.rs | 2 +- src/shell/layout/tiling/mod.rs | 5 +- src/shell/mod.rs | 219 +++++- src/shell/seats.rs | 44 ++ src/shell/workspace.rs | 5 +- src/state.rs | 4 +- src/wayland/handlers/compositor.rs | 4 +- src/wayland/handlers/toplevel_management.rs | 4 +- src/wayland/handlers/xdg_activation.rs | 2 +- src/wayland/handlers/xdg_foreign.rs | 5 +- src/wayland/handlers/xdg_shell/mod.rs | 14 +- src/xwayland.rs | 2 +- 22 files changed, 844 insertions(+), 384 deletions(-) diff --git a/cosmic-comp-config/src/input.rs b/cosmic-comp-config/src/input.rs index 5ac4889e..8dc5b761 100644 --- a/cosmic-comp-config/src/input.rs +++ b/cosmic-comp-config/src/input.rs @@ -5,6 +5,8 @@ pub use input::{AccelProfile, ClickMethod, ScrollMethod, TapButtonMap}; use serde::{Deserialize, Serialize}; +// Note: For the following values, None is used to represent the system default +// Configuration for input devices #[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize)] pub struct InputConfig { pub state: DeviceState, diff --git a/cosmic-comp-config/src/lib.rs b/cosmic-comp-config/src/lib.rs index dcc0af6b..5a656e52 100644 --- a/cosmic-comp-config/src/lib.rs +++ b/cosmic-comp-config/src/lib.rs @@ -23,6 +23,12 @@ pub struct CosmicCompConfig { pub autotile_behavior: TileBehavior, /// Active hint enabled pub active_hint: bool, + /// Enables changing keyboard focus to windows when the cursor passes into them + pub focus_follows_cursor: bool, + /// Enables warping the cursor to the focused window when focus changes due to keyboard input + pub cursor_follows_focus: bool, + /// The delay in milliseconds before focus follows mouse (if enabled) + pub focus_follows_cursor_delay: u64, /// Let X11 applications scale themselves pub descale_xwayland: bool, } @@ -50,6 +56,9 @@ impl Default for CosmicCompConfig { autotile: Default::default(), autotile_behavior: Default::default(), active_hint: true, + focus_follows_cursor: false, + cursor_follows_focus: false, + focus_follows_cursor_delay: 250, descale_xwayland: false, } } diff --git a/src/backend/render/mod.rs b/src/backend/render/mod.rs index d1908458..c49929e8 100644 --- a/src/backend/render/mod.rs +++ b/src/backend/render/mod.rs @@ -660,7 +660,7 @@ where .lock() .unwrap() .is_some(); - let active_output = last_active_seat.active_output(); + let focused_output = last_active_seat.focused_or_active_output(); let output_size = output.geometry().size; let output_scale = output.current_scale().fractional_scale(); @@ -670,7 +670,7 @@ where .iter() .find(|w| w.handle == current.0) .ok_or(OutputNoMode)?; - let is_active_space = workspace.outputs().any(|o| o == &active_output); + let is_active_space = workspace.outputs().any(|o| o == &focused_output); let has_fullscreen = workspace .fullscreen @@ -775,7 +775,7 @@ where .space_for_handle(&previous) .ok_or(OutputNoMode)?; let has_fullscreen = workspace.fullscreen.is_some(); - let is_active_space = workspace.outputs().any(|o| o == &active_output); + let is_active_space = workspace.outputs().any(|o| o == &focused_output); let percentage = match start { WorkspaceDelta::Shortcut(st) => ease( diff --git a/src/config/mod.rs b/src/config/mod.rs index 572ea6d5..7ac15044 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -658,6 +658,24 @@ fn config_changed(config: cosmic_config::Config, keys: Vec, state: &mut state.common.update_xwayland_scale(); } } + "focus_follows_cursor" => { + let new = get_config::(&config, "focus_follows_cursor"); + if new != state.common.config.cosmic_conf.focus_follows_cursor { + state.common.config.cosmic_conf.focus_follows_cursor = new; + } + } + "cursor_follows_focus" => { + let new = get_config::(&config, "cursor_follows_focus"); + if new != state.common.config.cosmic_conf.cursor_follows_focus { + state.common.config.cosmic_conf.cursor_follows_focus = new; + } + } + "focus_follows_cursor_delay" => { + let new = get_config::(&config, "focus_follows_cursor_delay"); + if new != state.common.config.cosmic_conf.focus_follows_cursor_delay { + state.common.config.cosmic_conf.focus_follows_cursor_delay = new; + } + } _ => {} } } diff --git a/src/input/mod.rs b/src/input/mod.rs index 30473a1c..74a94234 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -27,7 +27,10 @@ use crate::{ }, }, }; -use calloop::{timer::Timer, RegistrationToken}; +use calloop::{ + timer::{TimeoutAction, Timer}, + RegistrationToken, +}; use cosmic_comp_config::{workspace::WorkspaceLayout, TileBehavior}; use cosmic_config::ConfigSet; use cosmic_settings_config::shortcuts; @@ -86,6 +89,17 @@ use std::{ pub mod gestures; +/// Used for debouncing focus updates due to pointer motion, if after the focus change is +/// triggered the event will cancel if the pointer moves to the original target +#[derive(Debug)] +pub struct PointerFocusState { + //the window under the cursor prior to it's movement + originally_focused_window: Option, + //the window under the cursor after it's movement + scheduled_focused_window: Option, + token: RegistrationToken, +} + #[derive(Default)] pub struct SupressedKeys(RefCell)>>); #[derive(Default)] @@ -203,24 +217,19 @@ impl State { .cloned(); if let Some(seat) = maybe_seat { self.common.idle_notifier_state.notify_activity(&seat); - let current_output = seat.active_output(); - let shortcuts_inhibited = self - .common - .shell - .read() - .unwrap() - .active_space(¤t_output) - .focus_stack - .get(&seat) - .last() - .and_then(|window| { - window.wl_surface().and_then(|surface| { - seat.keyboard_shortcuts_inhibitor_for_surface(&surface) - }) - }) - .map(|inhibitor| inhibitor.is_active()) - .unwrap_or(false); + let current_focus = seat.get_keyboard().unwrap().current_focus(); + //this should fall back to active output since there may not be a focused output + let focused_output = seat.focused_or_active_output(); + + let shortcuts_inhibited = current_focus.is_some_and(|f| { + f.wl_surface() + .and_then(|surface| { + seat.keyboard_shortcuts_inhibitor_for_surface(&surface) + .map(|inhibitor| inhibitor.is_active()) + }) + .unwrap_or(false) + }); let keycode = event.key_code(); let state = event.state(); trace!(?keycode, ?state, "key"); @@ -266,7 +275,7 @@ impl State { shell.set_overview_mode(None, data.common.event_loop_handle.clone()); if let Some(focus) = current_focus { - if let Some(new_descriptor) = shell.workspaces.active(¤t_output).1.node_desc(focus) { + if let Some(new_descriptor) = shell.workspaces.active(&focused_output).1.node_desc(focus) { let mut spaces = shell.workspaces.spaces_mut(); if old_descriptor.handle != new_descriptor.handle { let (mut old_w, mut other_w) = spaces.partition::, _>(|w| w.handle == old_descriptor.handle); @@ -275,7 +284,7 @@ impl State { if let Some(focus) = TilingLayout::swap_trees(&mut old_workspace.tiling_layer, Some(&mut new_workspace.tiling_layer), &old_descriptor, &new_descriptor) { let seat = seat.clone(); data.common.event_loop_handle.insert_idle(move |state| { - Shell::set_focus(state, Some(&focus), &seat, None); + Shell::set_focus(state, Some(&focus), &seat, None,true); }); } old_workspace.refresh_focus_stack(); @@ -288,7 +297,7 @@ impl State { std::mem::drop(spaces); let seat = seat.clone(); data.common.event_loop_handle.insert_idle(move |state| { - Shell::set_focus(state, Some(&focus), &seat, None); + Shell::set_focus(state, Some(&focus), &seat, None,true); }); } workspace.refresh_focus_stack(); @@ -296,7 +305,7 @@ impl State { } } } else { - let new_workspace = shell.workspaces.active(¤t_output).1.handle; + let new_workspace = shell.workspaces.active(&focused_output).1.handle; if new_workspace != old_descriptor.handle { let spaces = shell.workspaces.spaces_mut(); let (mut old_w, mut other_w) = spaces.partition::, _>(|w| w.handle == old_descriptor.handle); @@ -306,7 +315,7 @@ impl State { if let Some(focus) = TilingLayout::move_tree(&mut old_workspace.tiling_layer, &mut new_workspace.tiling_layer, &new_workspace.handle, &seat, new_workspace.focus_stack.get(&seat).iter(), old_descriptor.clone()) { let seat = seat.clone(); data.common.event_loop_handle.insert_idle(move |state| { - Shell::set_focus(state, Some(&focus), &seat, None); + Shell::set_focus(state, Some(&focus), &seat, None,true); }); } old_workspace.refresh_focus_stack(); @@ -559,7 +568,7 @@ impl State { _ => {} }); } - + let original_position = position; position += event.delta().as_global(); let output = shell @@ -587,16 +596,111 @@ impl State { return; } - if ptr.is_grabbed() - && seat + if ptr.is_grabbed() { + if seat .user_data() .get::() .map(|marker| marker.get()) .unwrap_or(false) - { - if output != current_output { - ptr.frame(self); - return; + { + if output != current_output { + ptr.frame(self); + return; + } + } + //If the pointer isn't grabbed, we should check if the focused element should be updated + } else if self.common.config.cosmic_conf.focus_follows_cursor { + let shell = self.common.shell.read().unwrap(); + let (old_keyboard_target, _) = + shell.keyboard_target_from_position(original_position, &seat); + let (new_keyboard_target, _) = + shell.keyboard_target_from_position(position, &seat); + + if old_keyboard_target != new_keyboard_target + && new_keyboard_target.is_some() + { + let create_source = if self.common.pointer_focus_state.is_none() { + true + } else { + let PointerFocusState { + originally_focused_window, + scheduled_focused_window, + token, + } = self.common.pointer_focus_state.as_ref().unwrap(); + + if &new_keyboard_target == originally_focused_window { + //if we moved to the original window, just cancel the event + self.common.event_loop_handle.remove(*token); + //clear the state + self.common.pointer_focus_state = None; + false + } else if &new_keyboard_target != scheduled_focused_window { + //if we moved to a new window, update the scheduled focus + self.common.event_loop_handle.remove(*token); + true + } else { + //the state doesn't need to be updated or cleared + false + } + }; + + if create_source { + // prevent popups from being unfocusable if there is a gap between them and their parent + let delay = calloop::timer::Timer::from_duration( + //default to 250ms + std::time::Duration::from_millis( + self.common.config.cosmic_conf.focus_follows_cursor_delay, + ), + ); + let seat = seat.clone(); + let token = self + .common + .event_loop_handle + .insert_source(delay, move |_, _, state| { + let target = state + .common + .pointer_focus_state + .as_ref() + .unwrap() + .scheduled_focused_window + .clone(); + //clear it prior in case the user twitches in the microsecond it + //takes this function to run + state.common.pointer_focus_state = None; + + Shell::set_focus( + state, + target.as_ref(), + &seat, + Some(SERIAL_COUNTER.next_serial()), + false, + ); + + TimeoutAction::Drop + }) + .ok(); + if token.is_some() { + let originally_focused_window = + if self.common.pointer_focus_state.is_none() { + old_keyboard_target + } else { + // In this case, the pointer has moved to a new window (neither original, nor scheduled) + // so we should preserve the original window for the focus state + self.common + .pointer_focus_state + .as_ref() + .unwrap() + .originally_focused_window + .clone() + }; + + self.common.pointer_focus_state = Some(PointerFocusState { + originally_focused_window, + scheduled_focused_window: new_keyboard_target, + token: token.unwrap(), + }); + } + } } } @@ -780,248 +884,185 @@ impl State { InputEvent::PointerButton { event, .. } => { use smithay::backend::input::{ButtonState, PointerButtonEvent}; - let mut shell = self.common.shell.write().unwrap(); - if let Some(seat) = shell.seats.for_device(&event.device()).cloned() { - self.common.idle_notifier_state.notify_activity(&seat); + // + let Some(seat) = self + .common + .shell + .read() + .unwrap() + .seats + .for_device(&event.device()) + .cloned() + else { + return; + }; + self.common.idle_notifier_state.notify_activity(&seat); - let serial = SERIAL_COUNTER.next_serial(); - let button = event.button_code(); - let mut pass_event = !seat.supressed_buttons().remove(button); - if event.state() == ButtonState::Pressed { - // change the keyboard focus unless the pointer is grabbed - // We test for any matching surface type here but always use the root - // (in case of a window the toplevel) surface for the focus. - // see: https://gitlab.freedesktop.org/wayland/wayland/-/issues/294 - if !seat.get_pointer().unwrap().is_grabbed() { - let output = seat.active_output(); + let serial = SERIAL_COUNTER.next_serial(); + let button = event.button_code(); + let mut pass_event = !seat.supressed_buttons().remove(button); + if event.state() == ButtonState::Pressed { + // change the keyboard focus unless the pointer is grabbed + // We test for any matching surface type here but always use the root + // (in case of a window the toplevel) surface for the focus. + // see: https://gitlab.freedesktop.org/wayland/wayland/-/issues/294 + if !seat.get_pointer().unwrap().is_grabbed() { + let output = seat.active_output(); - let pos = seat.get_pointer().unwrap().current_location().as_global(); - let relative_pos = pos.to_local(&output); - let mut under: Option = None; + let global_position = + seat.get_pointer().unwrap().current_location().as_global(); + let shell = self.common.shell.write().unwrap(); + let (under, trigger_move) = + shell.keyboard_target_from_position(global_position, &seat); + if trigger_move { + // Don't check override redirect windows, because we don't set keyboard focus to them explicitly. + // These cases are handled by the XwaylandKeyboardGrab. + if let Some(target) = shell.element_under(global_position, &output) { + if seat.get_keyboard().unwrap().modifier_state().logo { + if let Some(surface) = target.toplevel().map(Cow::into_owned) { + let seat_clone = seat.clone(); + let mouse_button = PointerButtonEvent::button(&event); - if let Some(session_lock) = shell.session_lock.as_ref() { - under = session_lock - .surfaces - .get(&output) - .map(|lock| lock.clone().into()); - } else if let Some(window) = - shell.active_space(&output).get_fullscreen() - { - let layers = layer_map_for_output(&output); - if let Some(layer) = - layers.layer_under(WlrLayer::Overlay, relative_pos.as_logical()) - { - let layer_loc = layers.layer_geometry(layer).unwrap().loc; - if layer.can_receive_keyboard_focus() - && layer - .surface_under( - relative_pos.as_logical() - layer_loc.to_f64(), - WindowSurfaceType::ALL, - ) - .is_some() - { - under = Some(layer.clone().into()); - } - } else { - under = Some(window.clone().into()); - } - } else { - let done = { - let layers = layer_map_for_output(&output); - if let Some(layer) = layers - .layer_under(WlrLayer::Overlay, relative_pos.as_logical()) - .or_else(|| { - layers.layer_under( - WlrLayer::Top, - relative_pos.as_logical(), - ) - }) - { - let layer_loc = layers.layer_geometry(layer).unwrap().loc; - if layer.can_receive_keyboard_focus() - && layer - .surface_under( - relative_pos.as_logical() - layer_loc.to_f64(), - WindowSurfaceType::ALL, - ) - .is_some() - { - under = Some(layer.clone().into()); - true - } else { - false - } - } else { - false - } - }; - if !done { - // Don't check override redirect windows, because we don't set keyboard focus to them explicitly. - // These cases are handled by the XwaylandKeyboardGrab. - if let Some(target) = shell.element_under(pos, &output) { - if seat.get_keyboard().unwrap().modifier_state().logo { - if let Some(surface) = - target.toplevel().map(Cow::into_owned) - { - let seat_clone = seat.clone(); - let mouse_button = - PointerButtonEvent::button(&event); - - let mut supress_button = || { - // If the logo is held then the pointer event is - // aimed at the compositor and shouldn't be passed - // to the application. - pass_event = false; - seat.supressed_buttons().add(button); - }; - - fn dispatch_grab< - G: PointerGrab + 'static, - >( - grab: Option<( - G, - smithay::input::pointer::Focus, - )>, - seat: Seat, - serial: Serial, - state: &mut State, - ) { - if let Some((target, focus)) = grab { - seat.modifiers_shortcut_queue().clear(); - - seat.get_pointer() - .unwrap() - .set_grab(state, target, serial, focus); - } - } - - if let Some(mouse_button) = mouse_button { - match mouse_button { - smithay::backend::input::MouseButton::Left => { - supress_button(); - self.common.event_loop_handle.insert_idle( - move |state| { - let mut shell = - state.common.shell.write().unwrap(); - let res = shell.move_request( - &surface, - &seat_clone, - serial, - ReleaseMode::NoMouseButtons, - false, - &state.common.config, - &state.common.event_loop_handle, - &state.common.xdg_activation_state, - false, - ); - drop(shell); - dispatch_grab(res, seat_clone, serial, state); - } - ); - }, - smithay::backend::input::MouseButton::Right => { - supress_button(); - self.common.event_loop_handle.insert_idle( - move |state| { - let mut shell = - state.common.shell.write().unwrap(); - let Some(target_elem) = shell.element_for_surface(&surface) else { return }; - let Some(geom) = shell - .space_for(target_elem) - .and_then(|f| f.element_geometry(target_elem)) else { return }; - let geom = geom.to_f64(); - let center = geom.loc + geom.size.downscale(2.0); - let offset = center.to_global(&output) - pos; - let edge = match (offset.x > 0.0, offset.y > 0.0) { - (true, true) => ResizeEdge::TOP_LEFT, - (false, true) => ResizeEdge::TOP_RIGHT, - (true, false) => ResizeEdge::BOTTOM_LEFT, - (false, false) => ResizeEdge::BOTTOM_RIGHT - }; - let res = shell.resize_request( - &surface, - &seat_clone, - serial, - edge, - false, - ); - drop(shell); - dispatch_grab(res, seat_clone, serial, state); - } - ); - }, - _ => {}, - } - } - } - } - under = Some(target); - } else { - let layers = layer_map_for_output(&output); - if let Some(layer) = layers - .layer_under( - WlrLayer::Bottom, - relative_pos.as_logical(), - ) - .or_else(|| { - layers.layer_under( - WlrLayer::Background, - relative_pos.as_logical(), - ) - }) - { - let layer_loc = - layers.layer_geometry(layer).unwrap().loc; - if layer.can_receive_keyboard_focus() - && layer - .surface_under( - relative_pos.as_logical() - - layer_loc.to_f64(), - WindowSurfaceType::ALL, - ) - .is_some() - { - under = Some(layer.clone().into()); - } + let mut supress_button = || { + // If the logo is held then the pointer event is + // aimed at the compositor and shouldn't be passed + // to the application. + pass_event = false; + seat.supressed_buttons().add(button); }; + + fn dispatch_grab + 'static>( + grab: Option<(G, smithay::input::pointer::Focus)>, + seat: Seat, + serial: Serial, + state: &mut State, + ) { + if let Some((target, focus)) = grab { + seat.modifiers_shortcut_queue().clear(); + + seat.get_pointer() + .unwrap() + .set_grab(state, target, serial, focus); + } + } + + if let Some(mouse_button) = mouse_button { + match mouse_button { + smithay::backend::input::MouseButton::Left => { + supress_button(); + self.common.event_loop_handle.insert_idle( + move |state| { + let mut shell = + state.common.shell.write().unwrap(); + let res = shell.move_request( + &surface, + &seat_clone, + serial, + ReleaseMode::NoMouseButtons, + false, + &state.common.config, + &state.common.event_loop_handle, + &state.common.xdg_activation_state, + false, + ); + drop(shell); + dispatch_grab( + res, seat_clone, serial, state, + ); + }, + ); + } + smithay::backend::input::MouseButton::Right => { + supress_button(); + self.common.event_loop_handle.insert_idle( + move |state| { + let mut shell = + state.common.shell.write().unwrap(); + let Some(target_elem) = + shell.element_for_surface(&surface) + else { + return; + }; + let Some(geom) = shell + .space_for(target_elem) + .and_then(|f| { + f.element_geometry(target_elem) + }) + else { + return; + }; + let geom = geom.to_f64(); + let center = + geom.loc + geom.size.downscale(2.0); + let offset = center.to_global(&output) + - global_position; + let edge = match ( + offset.x > 0.0, + offset.y > 0.0, + ) { + (true, true) => { + ResizeEdge::TOP_LEFT + } + (false, true) => { + ResizeEdge::TOP_RIGHT + } + (true, false) => { + ResizeEdge::BOTTOM_LEFT + } + (false, false) => { + ResizeEdge::BOTTOM_RIGHT + } + }; + let res = shell.resize_request( + &surface, + &seat_clone, + serial, + edge, + false, + ); + drop(shell); + dispatch_grab( + res, seat_clone, serial, state, + ); + }, + ); + } + _ => {} + } + } } } } - std::mem::drop(shell); - Shell::set_focus(self, under.as_ref(), &seat, Some(serial)); - } else { - std::mem::drop(shell); - } - } else { - if let Some(Trigger::Pointer(action_button)) = - shell.overview_mode().0.active_trigger() - { - if *action_button == button { - shell - .set_overview_mode(None, self.common.event_loop_handle.clone()); - } } std::mem::drop(shell); - }; - - let ptr = seat.get_pointer().unwrap(); - if pass_event { - ptr.button( - self, - &ButtonEvent { - button, - state: event.state(), - serial, - time: event.time_msec(), - }, - ); - ptr.frame(self); - } else if event.state() == ButtonState::Released { - ptr.unset_grab(self, serial, event.time_msec()) + Shell::set_focus(self, under.as_ref(), &seat, Some(serial), false); } - } + } else { + let mut shell = self.common.shell.write().unwrap(); + if let Some(Trigger::Pointer(action_button)) = + shell.overview_mode().0.active_trigger() + { + if *action_button == button { + shell.set_overview_mode(None, self.common.event_loop_handle.clone()); + } + } + std::mem::drop(shell); + }; + + let ptr = seat.get_pointer().unwrap(); + ptr.button( + self, + &ButtonEvent { + button, + state: event.state(), + serial, + time: event.time_msec(), + }, + ); + ptr.frame(self); } + InputEvent::PointerAxis { event, .. } => { let scroll_factor = if let Some(device) = ::downcast_ref::(&event.device()) { @@ -1121,7 +1162,9 @@ impl State { // Decide on action if first update if first_update { let mut natural_scroll = false; - if let Some(scroll_config) = &self.common.config.cosmic_conf.input_touchpad.scroll_config { + if let Some(scroll_config) = + &self.common.config.cosmic_conf.input_touchpad.scroll_config + { if let Some(natural) = scroll_config.natural_scroll { natural_scroll = natural; } @@ -1157,7 +1200,7 @@ impl State { } else { Some(SwipeAction::PrevWorkspace) } - }, + } Some(Direction::Down) => { if natural_scroll { Some(SwipeAction::PrevWorkspace) @@ -1775,11 +1818,12 @@ impl State { } Action::Close => { - let current_output = seat.active_output(); - let shell = self.common.shell.read().unwrap(); - let workspace = shell.active_space(¤t_output); - if let Some(window) = workspace.focus_stack.get(seat).last() { - window.send_close(); + if let Some(focus_target) = seat.get_keyboard().unwrap().current_focus() { + self.common + .shell + .read() + .unwrap() + .close_focused(&focus_target); } } @@ -1854,7 +1898,9 @@ impl State { } x @ Action::MoveToWorkspace(_) | x @ Action::SendToWorkspace(_) => { - let current_output = seat.active_output(); + let Some(focused_output) = seat.focused_output() else { + return; + }; let follow = matches!(x, Action::MoveToWorkspace(_)); let workspace = match x { Action::MoveToWorkspace(0) | Action::SendToWorkspace(0) => 9, @@ -1863,48 +1909,65 @@ impl State { }; let res = self.common.shell.write().unwrap().move_current_window( seat, - ¤t_output, - (¤t_output, Some(workspace as usize)), + &focused_output, + (&focused_output, Some(workspace as usize)), follow, None, &mut self.common.workspace_state.update(), ); if let Ok(Some((target, _point))) = res { - Shell::set_focus(self, Some(&target), seat, None); + Shell::set_focus( + self, + Some(&target), + seat, + None, + matches!(x, Action::MoveToWorkspace(_)), + ); } } x @ Action::MoveToLastWorkspace | x @ Action::SendToLastWorkspace => { - let current_output = seat.active_output(); + let Some(focused_output) = seat.focused_output() else { + return; + }; let mut shell = self.common.shell.write().unwrap(); - let workspace = shell.workspaces.len(¤t_output).saturating_sub(1); + let workspace = shell.workspaces.len(&focused_output).saturating_sub(1); let res = shell.move_current_window( seat, - ¤t_output, - (¤t_output, Some(workspace)), + &focused_output, + (&focused_output, Some(workspace)), matches!(x, Action::MoveToLastWorkspace), None, &mut self.common.workspace_state.update(), ); + // If the active workspace changed, the cursor_follows_focus should probably be checked if let Ok(Some((target, _point))) = res { std::mem::drop(shell); - Shell::set_focus(self, Some(&target), seat, None); + Shell::set_focus( + self, + Some(&target), + seat, + None, + matches!(x, Action::MoveToLastWorkspace), + ); } } x @ Action::MoveToNextWorkspace | x @ Action::SendToNextWorkspace => { - let current_output = seat.active_output(); + let Some(focused_output) = seat.focused_output() else { + return; + }; let res = { let mut shell = self.common.shell.write().unwrap(); let workspace = shell .workspaces - .active_num(¤t_output) + .active_num(&focused_output) .1 .saturating_add(1); shell.move_current_window( seat, - ¤t_output, - (¤t_output, Some(workspace)), + &focused_output, + (&focused_output, Some(workspace)), matches!(x, Action::MoveToNextWorkspace), direction, &mut self.common.workspace_state.update(), @@ -1913,7 +1976,14 @@ impl State { match res { Ok(Some((target, _point))) => { - Shell::set_focus(self, Some(&target), seat, None); + // If the active workspace changed, the cursor_follows_focus should probably be checked + Shell::set_focus( + self, + Some(&target), + seat, + None, + matches!(x, Action::MoveToNextWorkspace), + ); } Err(_) if propagate => { if let Some(inferred) = pattern.inferred_direction() { @@ -1937,19 +2007,21 @@ impl State { } x @ Action::MoveToPreviousWorkspace | x @ Action::SendToPreviousWorkspace => { - let current_output = seat.active_output(); + let Some(focused_output) = seat.focused_output() else { + return; + }; let res = { let mut shell = self.common.shell.write().unwrap(); let workspace = shell .workspaces - .active_num(¤t_output) + .active_num(&focused_output) .1 .saturating_sub(1); // TODO: Possibly move to prev output, if idx < 0 shell.move_current_window( seat, - ¤t_output, - (¤t_output, Some(workspace)), + &focused_output, + (&focused_output, Some(workspace)), matches!(x, Action::MoveToPreviousWorkspace), direction, &mut self.common.workspace_state.update(), @@ -1958,7 +2030,13 @@ impl State { match res { Ok(Some((target, _point))) => { - Shell::set_focus(self, Some(&target), seat, None); + Shell::set_focus( + self, + Some(&target), + seat, + None, + matches!(x, Action::MoveToPreviousWorkspace), + ); } Err(_) if propagate => { if let Some(inferred) = pattern.inferred_direction() { @@ -2144,14 +2222,16 @@ impl State { _ => unreachable!(), }; - let current_output = seat.active_output(); + let Some(focused_output) = seat.focused_output() else { + return; + }; let mut shell = self.common.shell.write().unwrap(); - let next_output = shell.next_output(¤t_output, direction).cloned(); + let next_output = shell.next_output(&focused_output, direction).cloned(); if let Some(next_output) = next_output { let res = shell.move_current_window( seat, - ¤t_output, + &focused_output, (&next_output, None), is_move_action, Some(direction), @@ -2159,7 +2239,7 @@ impl State { ); if let Ok(Some((target, new_pos))) = res { std::mem::drop(shell); - Shell::set_focus(self, Some(&target), seat, None); + Shell::set_focus(self, Some(&target), seat, None, is_move_action); if let Some(ptr) = seat.get_pointer() { ptr.motion( self, @@ -2208,19 +2288,21 @@ impl State { } x @ Action::MoveToNextOutput | x @ Action::SendToNextOutput => { - let current_output = seat.active_output(); + let Some(focused_output) = seat.focused_output() else { + return; + }; let mut shell = self.common.shell.write().unwrap(); let next_output = shell .outputs() - .skip_while(|o| *o != ¤t_output) + .skip_while(|o| *o != &focused_output) .skip(1) .next() .cloned(); if let Some(next_output) = next_output { let res = shell.move_current_window( seat, - ¤t_output, + &focused_output, (&next_output, None), matches!(x, Action::MoveToNextOutput), direction, @@ -2228,7 +2310,13 @@ impl State { ); if let Ok(Some((target, new_pos))) = res { std::mem::drop(shell); - Shell::set_focus(self, Some(&target), seat, None); + Shell::set_focus( + self, + Some(&target), + seat, + None, + matches!(x, Action::MoveToNextOutput), + ); if let Some(ptr) = seat.get_pointer() { ptr.motion( self, @@ -2246,20 +2334,22 @@ impl State { } x @ Action::MoveToPreviousOutput | x @ Action::SendToPreviousOutput => { - let current_output = seat.active_output(); + let Some(focused_output) = seat.focused_output() else { + return; + }; let mut shell = self.common.shell.write().unwrap(); let prev_output = shell .outputs() .rev() - .skip_while(|o| *o != ¤t_output) + .skip_while(|o| *o != &focused_output) .skip(1) .next() .cloned(); if let Some(prev_output) = prev_output { let res = shell.move_current_window( seat, - ¤t_output, + &focused_output, (&prev_output, None), matches!(x, Action::MoveToPreviousOutput), direction, @@ -2267,7 +2357,13 @@ impl State { ); if let Ok(Some((target, new_pos))) = res { std::mem::drop(shell); - Shell::set_focus(self, Some(&target), seat, None); + Shell::set_focus( + self, + Some(&target), + seat, + None, + matches!(x, Action::MoveToPreviousOutput), + ); if let Some(ptr) = seat.get_pointer() { ptr.motion( self, @@ -2285,58 +2381,58 @@ impl State { } Action::MigrateWorkspaceToNextOutput => { - let current_output = seat.active_output(); + let active_output = seat.active_output(); let (active, next_output) = { let shell = self.common.shell.read().unwrap(); let output = shell .outputs() - .skip_while(|o| *o != ¤t_output) + .skip_while(|o| *o != &active_output) .skip(1) .next() .cloned(); - (shell.active_space(¤t_output).handle, output) + (shell.active_space(&active_output).handle, output) }; if let Some(next_output) = next_output { self.common - .migrate_workspace(¤t_output, &next_output, &active); + .migrate_workspace(&active_output, &next_output, &active); } } Action::MigrateWorkspaceToPreviousOutput => { - let current_output = seat.active_output(); + let active_output = seat.active_output(); let (active, prev_output) = { let shell = self.common.shell.read().unwrap(); let output = shell .outputs() .rev() - .skip_while(|o| *o != ¤t_output) + .skip_while(|o| *o != &active_output) .skip(1) .next() .cloned(); - (shell.active_space(¤t_output).handle, output) + (shell.active_space(&active_output).handle, output) }; if let Some(prev_output) = prev_output { self.common - .migrate_workspace(¤t_output, &prev_output, &active); + .migrate_workspace(&active_output, &prev_output, &active); } } Action::MigrateWorkspaceToOutput(direction) => { - let current_output = seat.active_output(); + let active_output = seat.active_output(); let (active, next_output) = { let shell = self.common.shell.read().unwrap(); ( - shell.active_space(¤t_output).handle, - shell.next_output(¤t_output, direction).cloned(), + shell.active_space(&active_output).handle, + shell.next_output(&active_output, direction).cloned(), ) }; if let Some(next_output) = next_output { self.common - .migrate_workspace(¤t_output, &next_output, &active); + .migrate_workspace(&active_output, &next_output, &active); } } @@ -2367,7 +2463,7 @@ impl State { } FocusResult::Handled => {} FocusResult::Some(target) => { - Shell::set_focus(self, Some(&target), seat, None); + Shell::set_focus(self, Some(&target), seat, None, true); } } } @@ -2390,7 +2486,7 @@ impl State { true, ), MoveResult::ShiftFocus(shift) => { - Shell::set_focus(self, Some(&shift), seat, None); + Shell::set_focus(self, Some(&shift), seat, None, true); } _ => { let current_output = seat.active_output(); @@ -2409,10 +2505,12 @@ impl State { } Action::SwapWindow => { - let current_output = seat.active_output(); + let Some(focused_output) = seat.focused_output() else { + return; + }; let mut shell = self.common.shell.write().unwrap(); - let workspace = shell.active_space_mut(¤t_output); + let workspace = shell.active_space_mut(&focused_output); if workspace.get_fullscreen().is_some() { return; // TODO, is this what we want? Maybe disengage fullscreen instead? } @@ -2433,9 +2531,11 @@ impl State { } Action::Minimize => { - let current_output = seat.active_output(); + let Some(focused_output) = seat.focused_output() else { + return; + }; let mut shell = self.common.shell.write().unwrap(); - let workspace = shell.active_space_mut(¤t_output); + let workspace = shell.active_space_mut(&focused_output); let focus_stack = workspace.focus_stack.get(seat); let focused_window = focus_stack.last().cloned(); if let Some(window) = focused_window { @@ -2444,9 +2544,11 @@ impl State { } Action::Maximize => { - let current_output = seat.active_output(); + let Some(focused_output) = seat.focused_output() else { + return; + }; let mut shell = self.common.shell.write().unwrap(); - let workspace = shell.active_space(¤t_output); + let workspace = shell.active_space(&focused_output); let focus_stack = workspace.focus_stack.get(seat); let focused_window = focus_stack.last().cloned(); if let Some(window) = focused_window { @@ -2459,7 +2561,8 @@ impl State { &self.common.config, self.common.event_loop_handle.clone(), ), - + // NOTE: implementation currently assumes actions that apply to outputs should apply to the active output + // rather than the output that has keyboard focus Action::ToggleOrientation => { let output = seat.active_output(); let mut shell = self.common.shell.write().unwrap(); @@ -2484,7 +2587,7 @@ impl State { .unwrap() .toggle_stacking_focused(seat); if let Some(new_focus) = res { - Shell::set_focus(self, Some(&new_focus), seat, Some(serial)); + Shell::set_focus(self, Some(&new_focus), seat, Some(serial), false); } } @@ -2521,7 +2624,9 @@ impl State { } Action::ToggleWindowFloating => { - let output = seat.active_output(); + let Some(output) = seat.focused_output() else { + return; + }; let mut shell = self.common.shell.write().unwrap(); let workspace = shell.active_space_mut(&output); workspace.toggle_floating_window_focused(seat); diff --git a/src/shell/element/window.rs b/src/shell/element/window.rs index 9fac26b8..24c151eb 100644 --- a/src/shell/element/window.rs +++ b/src/shell/element/window.rs @@ -177,7 +177,7 @@ impl CosmicWindowInternal { pub fn current_focus(&self) -> Option { unsafe { Focus::from_u8(self.pointer_entered.load(Ordering::SeqCst)) } } - + /// returns if the window has any current or pending server-side decorations pub fn has_ssd(&self, pending: bool) -> bool { !self.window.is_decorated(pending) } diff --git a/src/shell/focus/mod.rs b/src/shell/focus/mod.rs index 4915e19f..4adfd063 100644 --- a/src/shell/focus/mod.rs +++ b/src/shell/focus/mod.rs @@ -13,12 +13,12 @@ use smithay::{ utils::{IsAlive, Serial, SERIAL_COUNTER}, wayland::{ seat::WaylandFocus, - selection::data_device::set_data_device_focus, - selection::primary_selection::set_primary_focus, + selection::{data_device::set_data_device_focus, primary_selection::set_primary_focus}, shell::wlr_layer::{KeyboardInteractivity, Layer}, }, }; -use std::{borrow::Cow, sync::Mutex}; +use std::{borrow::Cow, mem, sync::Mutex}; + use tracing::{debug, trace}; use self::target::{KeyboardFocusTarget, WindowGroup}; @@ -31,6 +31,7 @@ pub struct FocusStack<'a>(pub(super) Option<&'a IndexSet>); pub struct FocusStackMut<'a>(pub(super) &'a mut IndexSet); impl<'a> FocusStack<'a> { + /// returns the last unminimized window in the focus stack that is still alive pub fn last(&self) -> Option<&CosmicMapped> { self.0 .as_ref() @@ -93,11 +94,15 @@ impl ActiveFocus { } impl Shell { + /// Set the keyboard focus to the given target + /// Note: `update_cursor` is used to determine whether to update the pointer location if cursor_follows_focus is enabled + /// if the focus change was due to a pointer event, this should be set to false pub fn set_focus( state: &mut State, target: Option<&KeyboardFocusTarget>, seat: &Seat, serial: Option, + update_cursor: bool, ) { let element = match target { Some(KeyboardFocusTarget::Element(mapped)) => Some(mapped.clone()), @@ -115,6 +120,7 @@ impl Shell { if mapped.is_minimized() { return; } + state .common .shell @@ -123,15 +129,7 @@ impl Shell { .append_focus_stack(&mapped, seat); } - // update keyboard focus - if let Some(keyboard) = seat.get_keyboard() { - ActiveFocus::set(seat, target.cloned()); - keyboard.set_focus( - state, - target.cloned(), - serial.unwrap_or_else(|| SERIAL_COUNTER.next_serial()), - ); - } + update_focus_state(seat, target, state, serial, update_cursor); state.common.shell.write().unwrap().update_active(); } @@ -144,7 +142,8 @@ impl Shell { // update FocusStack and notify layouts about new focus (if any window) let workspace = self.space_for_mut(&mapped); let workspace = if workspace.is_none() { - self.active_space_mut(&seat.active_output()) + //should this be the active output or the focused output? + self.active_space_mut(&seat.focused_or_active_output()) } else { workspace.unwrap() }; @@ -218,6 +217,77 @@ impl Shell { } } +/// Internal, used to ensure that ActiveFocus, KeyboardFocusTarget, and FocusedOutput are all in sync +fn update_focus_state( + seat: &Seat, + target: Option<&KeyboardFocusTarget>, + state: &mut State, + serial: Option, + should_update_cursor: bool, +) { + // update keyboard focus + if let Some(keyboard) = seat.get_keyboard() { + if should_update_cursor && state.common.config.cosmic_conf.cursor_follows_focus { + if ActiveFocus::get(seat).as_ref() != target && target.is_some() { + //need to borrow mutably for surface under + let mut shell = state.common.shell.write().unwrap(); + // get the top left corner of the target element + let geometry = shell.focused_geometry(target.unwrap()); + //to avoid the nested mutable borrow of state + if geometry.is_some() { + let top_left = geometry.unwrap().loc.to_f64(); + + // create a pointer target from the target element + let output = shell + .outputs() + .find(|output| output.geometry().to_f64().contains(top_left)) + .cloned() + .unwrap_or(seat.active_output()); + + let focus = shell + .surface_under(top_left, &output) + .map(|(focus, loc)| (focus, loc.as_logical())); + //drop here to avoid multiple mutable borrows + mem::drop(shell); + seat.get_pointer().unwrap().motion( + state, + focus, + &MotionEvent { + location: top_left.as_logical(), + serial: SERIAL_COUNTER.next_serial(), + time: 0, + }, + ); + } + } + } + + ActiveFocus::set(seat, target.cloned()); + keyboard.set_focus( + state, + target.cloned(), + serial.unwrap_or_else(|| SERIAL_COUNTER.next_serial()), + ); + + //update the focused output or set it to the active output + if target.is_some() { + // Get focused output calls visible_output_for_surface internally + // what should happen if the target is some, but it's not visible? + // should this be an error? + seat.set_focused_output( + state + .common + .shell + .read() + .unwrap() + .get_focused_output(target.unwrap()), + ) + } else { + seat.set_focused_output(None); + }; + } +} + fn raise_with_children(floating_layer: &mut FloatingLayout, focused: &CosmicMapped) { if floating_layer.mapped().any(|m| m == focused) { floating_layer.space.raise_element(focused, true); @@ -264,13 +334,16 @@ impl Common { update_pointer_focus(state, &seat); let mut shell = state.common.shell.write().unwrap(); - let output = seat.active_output(); + let output = seat.focused_or_active_output(); + + // If the focused or active output is not in the list of outputs, switch to the first output if !shell.outputs().any(|o| o == &output) { if let Some(other) = shell.outputs().next() { seat.set_active_output(other); } continue; } + let last_known_focus = ActiveFocus::get(&seat); if let Some(target) = last_known_focus { @@ -291,15 +364,14 @@ impl Common { if let Some(new) = popup_grab.current_grab() { trace!("restore focus to previous popup grab"); std::mem::drop(shell); - - if let Some(keyboard) = seat.get_keyboard() { - keyboard.set_focus( - state, - Some(new.clone()), - SERIAL_COUNTER.next_serial(), - ); - } - ActiveFocus::set(&seat, Some(new)); + // TODO: verify whether cursor should be updated at end of popup grab + update_focus_state( + seat, + Some(&new), + state, + Some(SERIAL_COUNTER.next_serial()), + false, + ); seat.user_data() .get_or_insert::(PopupGrabData::default) .set(Some(popup_grab)); @@ -337,12 +409,10 @@ impl Common { // update keyboard focus let target = update_focus_target(&*shell, &seat, &output); std::mem::drop(shell); + //I can probably feature gate this condition + debug!("Restoring focus to {:?}", target.as_ref()); - if let Some(keyboard) = seat.get_keyboard() { - debug!("Restoring focus to {:?}", target.as_ref()); - keyboard.set_focus(state, target.clone(), SERIAL_COUNTER.next_serial()); - ActiveFocus::set(&seat, target); - } + update_focus_state(seat, target.as_ref(), state, None, false); } } diff --git a/src/shell/grabs/menu/default.rs b/src/shell/grabs/menu/default.rs index 1c826cbe..ce227f81 100644 --- a/src/shell/grabs/menu/default.rs +++ b/src/shell/grabs/menu/default.rs @@ -20,7 +20,7 @@ fn toggle_stacking(state: &mut State, mapped: &CosmicMapped) { let seat = shell.seats.last_active().clone(); if let Some(new_focus) = shell.toggle_stacking(&seat, mapped) { std::mem::drop(shell); - Shell::set_focus(state, Some(&new_focus), &seat, None); + Shell::set_focus(state, Some(&new_focus), &seat, None, false); } } @@ -52,7 +52,7 @@ fn move_prev_workspace(state: &mut State, mapped: &CosmicMapped) { ); if let Some((target, _)) = res { std::mem::drop(shell); - Shell::set_focus(state, Some(&target), &seat, None); + Shell::set_focus(state, Some(&target), &seat, None, true); } } } @@ -85,7 +85,7 @@ fn move_next_workspace(state: &mut State, mapped: &CosmicMapped) { ); if let Some((target, _point)) = res { std::mem::drop(shell); - Shell::set_focus(state, Some(&target), &seat, None) + Shell::set_focus(state, Some(&target), &seat, None, true) } } } diff --git a/src/shell/grabs/menu/mod.rs b/src/shell/grabs/menu/mod.rs index 0ced2bb8..d29afc4c 100644 --- a/src/shell/grabs/menu/mod.rs +++ b/src/shell/grabs/menu/mod.rs @@ -161,6 +161,7 @@ impl Item { } } +/// Menu that comes up when right-clicking an application header bar pub struct ContextMenu { items: Vec, selected: AtomicBool, diff --git a/src/shell/grabs/moving.rs b/src/shell/grabs/moving.rs index d92cdc97..785a0c77 100644 --- a/src/shell/grabs/moving.rs +++ b/src/shell/grabs/moving.rs @@ -882,6 +882,7 @@ impl Drop for MoveGrab { Some(&KeyboardFocusTarget::from(mapped)), &seat, Some(serial), + false, ) } }); diff --git a/src/shell/layout/floating/mod.rs b/src/shell/layout/floating/mod.rs index db666277..140fb5d5 100644 --- a/src/shell/layout/floating/mod.rs +++ b/src/shell/layout/floating/mod.rs @@ -728,7 +728,7 @@ impl FloatingLayout { self.space.element_geometry(elem).map(RectExt::as_local) } - pub fn element_under(&mut self, location: Point) -> Option { + pub fn element_under(&self, location: Point) -> Option { self.space .element_under(location.as_logical()) .map(|(mapped, _)| mapped.clone().into()) diff --git a/src/shell/layout/tiling/mod.rs b/src/shell/layout/tiling/mod.rs index 3283ba59..2eb5c708 100644 --- a/src/shell/layout/tiling/mod.rs +++ b/src/shell/layout/tiling/mod.rs @@ -3088,10 +3088,7 @@ impl TilingLayout { None } - pub fn element_under( - &mut self, - location_f64: Point, - ) -> Option { + pub fn element_under(&self, location_f64: Point) -> Option { let location = location_f64.to_i32_round(); for (mapped, geo) in self.mapped() { diff --git a/src/shell/mod.rs b/src/shell/mod.rs index cfe98b49..b48b248a 100644 --- a/src/shell/mod.rs +++ b/src/shell/mod.rs @@ -8,6 +8,7 @@ use std::{ }; use wayland_backend::server::ClientId; +use crate::wayland::protocols::workspace::WorkspaceCapabilities; use cosmic_comp_config::{ workspace::{WorkspaceLayout, WorkspaceMode}, TileBehavior, @@ -50,6 +51,8 @@ use smithay::{ xwayland::X11Surface, }; +use smithay::wayland::shell::wlr_layer::Layer as WlrLayer; + use crate::{ backend::render::animations::spring::{Spring, SpringParams}, config::Config, @@ -69,8 +72,7 @@ use crate::{ toplevel_leave_workspace, ToplevelInfoState, }, workspace::{ - WorkspaceCapabilities, WorkspaceGroupHandle, WorkspaceHandle, WorkspaceState, - WorkspaceUpdateGuard, + WorkspaceGroupHandle, WorkspaceHandle, WorkspaceState, WorkspaceUpdateGuard, }, }, }, @@ -1402,6 +1404,148 @@ impl Shell { self.workspaces.active_mut(output) } + /// get the parent output of the window which has keyboard focus (for a given seat) + pub fn get_focused_output(&self, focus_target: &KeyboardFocusTarget) -> Option<&Output> { + if let Some(focused_surface) = focus_target.wl_surface() { + self.visible_output_for_surface(&focused_surface) + } else { + None + } + } + + /// Derives a keyboard focus target from a global position, and indicates whether the + /// the shell should start a move request event. Used during cursor related focus checks + pub fn keyboard_target_from_position( + &self, + global_position: Point, + seat: &Seat, + ) -> (Option, bool) { + let output = seat.active_output(); + // if not done and super key pressed + let mut grab_conditions_met = false; + let relative_pos = global_position.to_local(&output); + + let mut under: Option = None; + // if the lockscreen is active + if let Some(session_lock) = self.session_lock.as_ref() { + under = session_lock + .surfaces + .get(&output) + .map(|lock| lock.clone().into()); + // if the output can receive keyboard focus + } else if let Some(window) = self.active_space(&output).get_fullscreen() { + let layers = layer_map_for_output(&output); + if let Some(layer) = layers.layer_under(WlrLayer::Overlay, relative_pos.as_logical()) { + let layer_loc = layers.layer_geometry(layer).unwrap().loc; + if layer.can_receive_keyboard_focus() + && layer + .surface_under( + relative_pos.as_logical() - layer_loc.to_f64(), + WindowSurfaceType::ALL, + ) + .is_some() + { + under = Some(layer.clone().into()); + } + } else { + under = Some(window.clone().into()); + } + } else { + let done = { + let layers = layer_map_for_output(&output); + if let Some(layer) = layers + .layer_under(WlrLayer::Overlay, relative_pos.as_logical()) + .or_else(|| layers.layer_under(WlrLayer::Top, relative_pos.as_logical())) + { + let layer_loc = layers.layer_geometry(layer).unwrap().loc; + if layer.can_receive_keyboard_focus() + && layer + .surface_under( + relative_pos.as_logical() - layer_loc.to_f64(), + WindowSurfaceType::ALL, + ) + .is_some() + { + under = Some(layer.clone().into()); + true + } else { + false + } + } else { + false + } + }; + if !done { + // Don't check override redirect windows, because we don't set keyboard focus to them explicitly. + // These cases are handled by the XwaylandKeyboardGrab. + if let Some(target) = self.element_under(global_position, &output) { + if !seat.get_keyboard().unwrap().modifier_state().logo { + under = Some(target); + } else { + grab_conditions_met = true; + } + } else { + let layers = layer_map_for_output(&output); + if let Some(layer) = layers + .layer_under(WlrLayer::Bottom, relative_pos.as_logical()) + .or_else(|| { + layers.layer_under(WlrLayer::Background, relative_pos.as_logical()) + }) + { + let layer_loc = layers.layer_geometry(layer).unwrap().loc; + if layer.can_receive_keyboard_focus() + && layer + .surface_under( + relative_pos.as_logical() - layer_loc.to_f64(), + WindowSurfaceType::ALL, + ) + .is_some() + { + under = Some(layer.clone().into()); + } + }; + } + } + } + (under, grab_conditions_met) + } + + /// Coerce a keyboard focus target into a CosmicMapped element. This is useful when performing window specific + /// actions, such as closing a window + pub fn focused_element(&self, focus_target: &KeyboardFocusTarget) -> Option { + match focus_target { + KeyboardFocusTarget::Fullscreen(surface) => self.element_for_surface(surface).cloned(), + KeyboardFocusTarget::Element(window) => Some(window).cloned(), + KeyboardFocusTarget::Popup(PopupKind::Xdg(popup)) => { + if let Some(parent) = popup.get_parent_surface() { + self.element_for_surface(&parent).cloned() + } else { + None + } + } + KeyboardFocusTarget::Popup(PopupKind::InputMethod(popup)) => { + if let Some(parent) = popup.get_parent() { + self.element_for_surface(&parent.surface).cloned() + } else { + None + } + } + _ => None, + } + } + + /// Close the focused keyboard focus target + pub fn close_focused(&self, focus_target: &KeyboardFocusTarget) { + if let KeyboardFocusTarget::Group(_group) = focus_target { + //TODO: decide if we want close actions to apply to groups + return; + } else { + if let Some(mapped) = self.focused_element(focus_target) { + mapped.send_close(); + } + } + } + pub fn refresh_active_space( &mut self, output: &Output, @@ -2130,11 +2274,11 @@ impl Shell { } pub fn element_under( - &mut self, + &self, location: Point, output: &Output, ) -> Option { - self.workspaces.sets.get_mut(output).and_then(|set| { + self.workspaces.sets.get(output).and_then(|set| { set.sticky_layer .space .element_under(location.to_local(output).as_logical()) @@ -2641,20 +2785,59 @@ impl Shell { Some((grab, Focus::Clear)) } + // Just to avoid a longer lived shell reference + /// Get the window geometry of a keyboard focus target + pub fn focused_geometry(&self, target: &KeyboardFocusTarget) -> Option> { + if let Some(element) = self.focused_element(target) { + self.element_geometry(&element) + } else { + None + } + } + + pub fn element_geometry(&self, mapped: &CosmicMapped) -> Option> { + if let Some(set) = self + .workspaces + .sets + .values() + .find(|set| set.sticky_layer.mapped().any(|m| m == mapped)) + { + let geometry = set + .sticky_layer + .element_geometry(mapped) + .unwrap() + .to_global(&set.output); + Some(geometry) + } else if let Some(workspace) = self.space_for(&mapped) { + let geometry = workspace + .element_geometry(&mapped) + .unwrap() + .to_global(workspace.output()); + Some(geometry) + } else { + None + } + } + #[must_use] pub fn next_focus(&self, direction: FocusDirection, seat: &Seat) -> FocusResult { let overview = self.overview_mode().0; - let output = seat.active_output(); - let Some(target) = seat.get_keyboard().unwrap().current_focus() else { return FocusResult::None; }; + let output = self.get_focused_output(&target).unwrap(); + + let workspace = self.active_space(output); + + if workspace.fullscreen.is_some() { + return FocusResult::None; + } if matches!(target, KeyboardFocusTarget::Fullscreen(_)) { return FocusResult::None; } - let set = self.workspaces.sets.get(&output).unwrap(); + let set = self.workspaces.sets.get(output).unwrap(); let sticky_layer = &set.sticky_layer; let workspace = &set.workspaces[set.active]; @@ -2791,8 +2974,17 @@ impl Shell { } #[must_use] - pub fn move_current_element(&mut self, direction: Direction, seat: &Seat) -> MoveResult { - let output = seat.active_output(); + pub fn move_current_element<'a>( + &mut self, + direction: Direction, + seat: &Seat, + ) -> MoveResult { + let output = seat + .get_keyboard() + .unwrap() + .current_focus() + .and_then(|target| self.get_focused_output(&target).cloned()) + .unwrap(); let workspace = self.active_space(&output); let focus_stack = workspace.focus_stack.get(seat); let Some(last) = focus_stack.last().cloned() else { @@ -3133,7 +3325,9 @@ impl Shell { } pub fn resize(&mut self, seat: &Seat, direction: ResizeDirection, edge: ResizeEdge) { - let output = seat.active_output(); + let Some(output) = seat.focused_output() else { + return; + }; let (_, idx) = self.workspaces.active_num(&output); let Some(focused) = seat.get_keyboard().unwrap().current_focus() else { return; @@ -3234,7 +3428,10 @@ impl Shell { #[must_use] pub fn toggle_stacking_focused(&mut self, seat: &Seat) -> Option { - let set = self.workspaces.sets.get_mut(&seat.active_output()).unwrap(); + let Some(focused_output) = seat.focused_output() else { + return None; + }; + let set = self.workspaces.sets.get_mut(&focused_output).unwrap(); let workspace = &mut set.workspaces[set.active]; let maybe_window = workspace.focus_stack.get(seat).iter().next().cloned(); if let Some(window) = maybe_window { diff --git a/src/shell/seats.rs b/src/shell/seats.rs index ab01fb9f..1073b70f 100644 --- a/src/shell/seats.rs +++ b/src/shell/seats.rs @@ -27,6 +27,10 @@ use super::grabs::{SeatMenuGrabState, SeatMoveGrabState}; crate::utils::id_gen!(next_seat_id, SEAT_ID, SEAT_IDS); +// for more information on seats, see: +// +/// Seats are an abstraction over a set of input devices grouped together, such as a keyboard, pointer and touch device. +/// i.e. Those used by a user to operate the computer. #[derive(Debug)] pub struct Seats { seats: Vec>, @@ -155,8 +159,13 @@ impl Drop for SeatId { #[repr(transparent)] struct SeatId(pub usize); + +/// The output which contains the cursor associated with a seat. struct ActiveOutput(pub Mutex); +/// The output which currently has keyboard focus +struct FocusedOutput(pub Mutex>); + pub fn create_seat( dh: &DisplayHandle, seat_state: &mut SeatState, @@ -175,6 +184,7 @@ pub fn create_seat( userdata.insert_if_missing_threadsafe(SeatMenuGrabState::default); userdata.insert_if_missing_threadsafe(CursorState::default); userdata.insert_if_missing_threadsafe(|| ActiveOutput(Mutex::new(output.clone()))); + userdata.insert_if_missing_threadsafe(|| FocusedOutput(Mutex::new(None))); userdata.insert_if_missing_threadsafe(|| Mutex::new(CursorImageStatus::default_named())); // A lot of clients bind keyboard and pointer unconditionally once on launch.. @@ -213,7 +223,13 @@ pub trait SeatExt { fn id(&self) -> usize; fn active_output(&self) -> Output; + fn focused_output(&self) -> Option; + fn focused_or_active_output(&self) -> Output { + self.focused_output() + .unwrap_or_else(|| self.active_output()) + } fn set_active_output(&self, output: &Output); + fn set_focused_output(&self, output: Option<&Output>); fn devices(&self) -> &Devices; fn supressed_keys(&self) -> &SupressedKeys; fn supressed_buttons(&self) -> &SupressedButtons; @@ -231,6 +247,9 @@ impl SeatExt for Seat { self.user_data().get::().unwrap().0 } + /// Returns the output that contains the cursor associated with a seat. Note that the window which has keyboard focus + /// may be on a different output. Currently, to get the focused output, first get the keyboard focus target and pass + /// it to get_focused_output in the shell. fn active_output(&self) -> Output { self.user_data() .get::() @@ -238,6 +257,21 @@ impl SeatExt for Seat { .unwrap() } + /// Returns the output which currently has keyboard focus. If no window has keyboard focus (e.g. when there are no windows) + /// the focused output will be the same as the active output. + fn focused_output(&self) -> Option { + if self + .get_keyboard() + .is_some_and(|k| k.current_focus().is_some()) + { + self.user_data() + .get::() + .map(|x| x.0.lock().unwrap().clone())? + } else { + None + } + } + fn set_active_output(&self, output: &Output) { *self .user_data() @@ -248,6 +282,16 @@ impl SeatExt for Seat { .unwrap() = output.clone(); } + fn set_focused_output(&self, output: Option<&Output>) { + *self + .user_data() + .get::() + .unwrap() + .0 + .lock() + .unwrap() = output.cloned(); + } + fn devices(&self) -> &Devices { self.user_data().get::().unwrap() } diff --git a/src/shell/workspace.rs b/src/shell/workspace.rs index 6fc25b5d..85589efb 100644 --- a/src/shell/workspace.rs +++ b/src/shell/workspace.rs @@ -177,6 +177,7 @@ impl IsAlive for FullscreenSurface { } } +/// LIFO stack of focus targets #[derive(Debug, Default)] pub struct FocusStacks(HashMap, IndexSet>); @@ -444,7 +445,7 @@ impl Workspace { .find(|e| e.windows().any(|(w, _)| &w == surface)) } - pub fn element_under(&mut self, location: Point) -> Option { + pub fn element_under(&self, location: Point) -> Option { let location = location.to_local(&self.output); self.floating_layer .element_under(location) @@ -784,6 +785,8 @@ impl Workspace { } } + /// Returns the content of the current display if it is alive and + /// not in the process of rendering an animation pub fn get_fullscreen(&self) -> Option<&CosmicSurface> { self.fullscreen .as_ref() diff --git a/src/state.rs b/src/state.rs index 7b817947..11142a2d 100644 --- a/src/state.rs +++ b/src/state.rs @@ -8,7 +8,7 @@ use crate::{ x11::X11State, }, config::{Config, OutputConfig, OutputState}, - input::gestures::GestureState, + input::{gestures::GestureState, PointerFocusState}, shell::{grabs::SeatMoveGrabState, CosmicSurface, SeatExt, Shell}, utils::prelude::OutputExt, wayland::protocols::{ @@ -226,6 +226,7 @@ pub struct Common { pub xwayland_scale: Option, pub xwayland_state: Option, pub xwayland_shell_state: XWaylandShellState, + pub pointer_focus_state: Option, } #[derive(Debug)] @@ -621,6 +622,7 @@ impl State { xwayland_scale: None, xwayland_state: None, xwayland_shell_state, + pointer_focus_state: None, }, backend: BackendData::Unset, ready: Once::new(), diff --git a/src/wayland/handlers/compositor.rs b/src/wayland/handlers/compositor.rs index edd6a143..a71fbb9d 100644 --- a/src/wayland/handlers/compositor.rs +++ b/src/wayland/handlers/compositor.rs @@ -272,7 +272,7 @@ impl State { if let Some(target) = res { let seat = shell.seats.last_active().clone(); std::mem::drop(shell); - Shell::set_focus(self, Some(&target), &seat, None); + Shell::set_focus(self, Some(&target), &seat, None, true); return true; } } @@ -290,7 +290,7 @@ impl State { if let Some(target) = shell.map_layer(&layer_surface) { let seat = shell.seats.last_active().clone(); std::mem::drop(shell); - Shell::set_focus(self, Some(&target), &seat, None); + Shell::set_focus(self, Some(&target), &seat, None, false); } layer_surface.layer_surface().send_configure(); return true; diff --git a/src/wayland/handlers/toplevel_management.rs b/src/wayland/handlers/toplevel_management.rs index a6e468f5..32bdcb9d 100644 --- a/src/wayland/handlers/toplevel_management.rs +++ b/src/wayland/handlers/toplevel_management.rs @@ -87,7 +87,7 @@ impl ToplevelManagementHandler for State { } mapped.focus_window(window); - Shell::set_focus(self, Some(&mapped.clone().into()), &seat, None); + Shell::set_focus(self, Some(&mapped.clone().into()), &seat, None, true); return; } } @@ -133,7 +133,7 @@ impl ToplevelManagementHandler for State { ); if let Some((target, _)) = res { std::mem::drop(shell); - Shell::set_focus(self, Some(&target), &seat, None); + Shell::set_focus(self, Some(&target), &seat, None, true); } return; } diff --git a/src/wayland/handlers/xdg_activation.rs b/src/wayland/handlers/xdg_activation.rs index 4ca6430b..78a488e6 100644 --- a/src/wayland/handlers/xdg_activation.rs +++ b/src/wayland/handlers/xdg_activation.rs @@ -156,7 +156,7 @@ impl XdgActivationHandler for State { let target = element.into(); std::mem::drop(shell); - Shell::set_focus(self, Some(&target), &seat, None); + Shell::set_focus(self, Some(&target), &seat, None, false); } else if let Some(w) = shell.space_for(&element).map(|w| w.handle.clone()) { shell.append_focus_stack(&element, &seat); diff --git a/src/wayland/handlers/xdg_foreign.rs b/src/wayland/handlers/xdg_foreign.rs index b77ecfdd..7749945b 100644 --- a/src/wayland/handlers/xdg_foreign.rs +++ b/src/wayland/handlers/xdg_foreign.rs @@ -1,7 +1,10 @@ // SPDX-License-Identifier: GPL-3.0-only -use smithay::{delegate_xdg_foreign, wayland::xdg_foreign::{XdgForeignHandler, XdgForeignState}}; use crate::state::State; +use smithay::{ + delegate_xdg_foreign, + wayland::xdg_foreign::{XdgForeignHandler, XdgForeignState}, +}; impl XdgForeignHandler for State { fn xdg_foreign_state(&mut self) -> &mut XdgForeignState { diff --git a/src/wayland/handlers/xdg_shell/mod.rs b/src/wayland/handlers/xdg_shell/mod.rs index e97ac671..da9cbb42 100644 --- a/src/wayland/handlers/xdg_shell/mod.rs +++ b/src/wayland/handlers/xdg_shell/mod.rs @@ -97,7 +97,13 @@ impl XdgShellHandler for State { grab.ungrab(PopupUngrabStrategy::All); return; } - Shell::set_focus(self, grab.current_grab().as_ref(), &seat, Some(serial)); + Shell::set_focus( + self, + grab.current_grab().as_ref(), + &seat, + Some(serial), + false, + ); keyboard.set_grab(self, PopupKeyboardGrab::new(&grab), serial); } @@ -226,11 +232,13 @@ impl XdgShellHandler for State { fn fullscreen_request(&mut self, surface: ToplevelSurface, output: Option) { let mut shell = self.common.shell.write().unwrap(); let seat = shell.seats.last_active().clone(); - let active_output = seat.active_output(); + let Some(focused_output) = seat.focused_output() else { + return; + }; let output = output .as_ref() .and_then(Output::from_resource) - .unwrap_or_else(|| active_output.clone()); + .unwrap_or_else(|| focused_output.clone()); if let Some(mapped) = shell.element_for_surface(surface.wl_surface()).cloned() { let from = minimize_rectangle(&output, &mapped.active_window()); diff --git a/src/xwayland.rs b/src/xwayland.rs index bd6371ef..6875c2a2 100644 --- a/src/xwayland.rs +++ b/src/xwayland.rs @@ -390,7 +390,7 @@ impl XwmHandler for State { if let Some(target) = res { let seat = shell.seats.last_active().clone(); std::mem::drop(shell); - Shell::set_focus(self, Some(&target), &seat, None); + Shell::set_focus(self, Some(&target), &seat, None, false); } } }