added cursor_follows_focus and focus_follows_cursor

This commit is contained in:
skewballfox 2024-09-04 11:13:59 -05:00 committed by Victoria Brekenfeld
parent 52280e9823
commit 7da0bc430a
22 changed files with 844 additions and 384 deletions

View file

@ -5,6 +5,8 @@
pub use input::{AccelProfile, ClickMethod, ScrollMethod, TapButtonMap}; pub use input::{AccelProfile, ClickMethod, ScrollMethod, TapButtonMap};
use serde::{Deserialize, Serialize}; 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)] #[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize)]
pub struct InputConfig { pub struct InputConfig {
pub state: DeviceState, pub state: DeviceState,

View file

@ -23,6 +23,12 @@ pub struct CosmicCompConfig {
pub autotile_behavior: TileBehavior, pub autotile_behavior: TileBehavior,
/// Active hint enabled /// Active hint enabled
pub active_hint: bool, 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 /// Let X11 applications scale themselves
pub descale_xwayland: bool, pub descale_xwayland: bool,
} }
@ -50,6 +56,9 @@ impl Default for CosmicCompConfig {
autotile: Default::default(), autotile: Default::default(),
autotile_behavior: Default::default(), autotile_behavior: Default::default(),
active_hint: true, active_hint: true,
focus_follows_cursor: false,
cursor_follows_focus: false,
focus_follows_cursor_delay: 250,
descale_xwayland: false, descale_xwayland: false,
} }
} }

View file

@ -660,7 +660,7 @@ where
.lock() .lock()
.unwrap() .unwrap()
.is_some(); .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_size = output.geometry().size;
let output_scale = output.current_scale().fractional_scale(); let output_scale = output.current_scale().fractional_scale();
@ -670,7 +670,7 @@ where
.iter() .iter()
.find(|w| w.handle == current.0) .find(|w| w.handle == current.0)
.ok_or(OutputNoMode)?; .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 let has_fullscreen = workspace
.fullscreen .fullscreen
@ -775,7 +775,7 @@ where
.space_for_handle(&previous) .space_for_handle(&previous)
.ok_or(OutputNoMode)?; .ok_or(OutputNoMode)?;
let has_fullscreen = workspace.fullscreen.is_some(); 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 { let percentage = match start {
WorkspaceDelta::Shortcut(st) => ease( WorkspaceDelta::Shortcut(st) => ease(

View file

@ -658,6 +658,24 @@ fn config_changed(config: cosmic_config::Config, keys: Vec<String>, state: &mut
state.common.update_xwayland_scale(); state.common.update_xwayland_scale();
} }
} }
"focus_follows_cursor" => {
let new = get_config::<bool>(&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::<bool>(&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::<u64>(&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;
}
}
_ => {} _ => {}
} }
} }

File diff suppressed because it is too large Load diff

View file

@ -177,7 +177,7 @@ impl CosmicWindowInternal {
pub fn current_focus(&self) -> Option<Focus> { pub fn current_focus(&self) -> Option<Focus> {
unsafe { Focus::from_u8(self.pointer_entered.load(Ordering::SeqCst)) } 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 { pub fn has_ssd(&self, pending: bool) -> bool {
!self.window.is_decorated(pending) !self.window.is_decorated(pending)
} }

View file

@ -13,12 +13,12 @@ use smithay::{
utils::{IsAlive, Serial, SERIAL_COUNTER}, utils::{IsAlive, Serial, SERIAL_COUNTER},
wayland::{ wayland::{
seat::WaylandFocus, seat::WaylandFocus,
selection::data_device::set_data_device_focus, selection::{data_device::set_data_device_focus, primary_selection::set_primary_focus},
selection::primary_selection::set_primary_focus,
shell::wlr_layer::{KeyboardInteractivity, Layer}, shell::wlr_layer::{KeyboardInteractivity, Layer},
}, },
}; };
use std::{borrow::Cow, sync::Mutex}; use std::{borrow::Cow, mem, sync::Mutex};
use tracing::{debug, trace}; use tracing::{debug, trace};
use self::target::{KeyboardFocusTarget, WindowGroup}; use self::target::{KeyboardFocusTarget, WindowGroup};
@ -31,6 +31,7 @@ pub struct FocusStack<'a>(pub(super) Option<&'a IndexSet<CosmicMapped>>);
pub struct FocusStackMut<'a>(pub(super) &'a mut IndexSet<CosmicMapped>); pub struct FocusStackMut<'a>(pub(super) &'a mut IndexSet<CosmicMapped>);
impl<'a> FocusStack<'a> { impl<'a> FocusStack<'a> {
/// returns the last unminimized window in the focus stack that is still alive
pub fn last(&self) -> Option<&CosmicMapped> { pub fn last(&self) -> Option<&CosmicMapped> {
self.0 self.0
.as_ref() .as_ref()
@ -93,11 +94,15 @@ impl ActiveFocus {
} }
impl Shell { 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( pub fn set_focus(
state: &mut State, state: &mut State,
target: Option<&KeyboardFocusTarget>, target: Option<&KeyboardFocusTarget>,
seat: &Seat<State>, seat: &Seat<State>,
serial: Option<Serial>, serial: Option<Serial>,
update_cursor: bool,
) { ) {
let element = match target { let element = match target {
Some(KeyboardFocusTarget::Element(mapped)) => Some(mapped.clone()), Some(KeyboardFocusTarget::Element(mapped)) => Some(mapped.clone()),
@ -115,6 +120,7 @@ impl Shell {
if mapped.is_minimized() { if mapped.is_minimized() {
return; return;
} }
state state
.common .common
.shell .shell
@ -123,15 +129,7 @@ impl Shell {
.append_focus_stack(&mapped, seat); .append_focus_stack(&mapped, seat);
} }
// update keyboard focus update_focus_state(seat, target, state, serial, update_cursor);
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()),
);
}
state.common.shell.write().unwrap().update_active(); state.common.shell.write().unwrap().update_active();
} }
@ -144,7 +142,8 @@ impl Shell {
// update FocusStack and notify layouts about new focus (if any window) // update FocusStack and notify layouts about new focus (if any window)
let workspace = self.space_for_mut(&mapped); let workspace = self.space_for_mut(&mapped);
let workspace = if workspace.is_none() { 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 { } else {
workspace.unwrap() 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<State>,
target: Option<&KeyboardFocusTarget>,
state: &mut State,
serial: Option<Serial>,
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) { fn raise_with_children(floating_layer: &mut FloatingLayout, focused: &CosmicMapped) {
if floating_layer.mapped().any(|m| m == focused) { if floating_layer.mapped().any(|m| m == focused) {
floating_layer.space.raise_element(focused, true); floating_layer.space.raise_element(focused, true);
@ -264,13 +334,16 @@ impl Common {
update_pointer_focus(state, &seat); update_pointer_focus(state, &seat);
let mut shell = state.common.shell.write().unwrap(); 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 !shell.outputs().any(|o| o == &output) {
if let Some(other) = shell.outputs().next() { if let Some(other) = shell.outputs().next() {
seat.set_active_output(other); seat.set_active_output(other);
} }
continue; continue;
} }
let last_known_focus = ActiveFocus::get(&seat); let last_known_focus = ActiveFocus::get(&seat);
if let Some(target) = last_known_focus { if let Some(target) = last_known_focus {
@ -291,15 +364,14 @@ impl Common {
if let Some(new) = popup_grab.current_grab() { if let Some(new) = popup_grab.current_grab() {
trace!("restore focus to previous popup grab"); trace!("restore focus to previous popup grab");
std::mem::drop(shell); std::mem::drop(shell);
// TODO: verify whether cursor should be updated at end of popup grab
if let Some(keyboard) = seat.get_keyboard() { update_focus_state(
keyboard.set_focus( seat,
state, Some(&new),
Some(new.clone()), state,
SERIAL_COUNTER.next_serial(), Some(SERIAL_COUNTER.next_serial()),
); false,
} );
ActiveFocus::set(&seat, Some(new));
seat.user_data() seat.user_data()
.get_or_insert::<PopupGrabData, _>(PopupGrabData::default) .get_or_insert::<PopupGrabData, _>(PopupGrabData::default)
.set(Some(popup_grab)); .set(Some(popup_grab));
@ -337,12 +409,10 @@ impl Common {
// update keyboard focus // update keyboard focus
let target = update_focus_target(&*shell, &seat, &output); let target = update_focus_target(&*shell, &seat, &output);
std::mem::drop(shell); 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() { update_focus_state(seat, target.as_ref(), state, None, false);
debug!("Restoring focus to {:?}", target.as_ref());
keyboard.set_focus(state, target.clone(), SERIAL_COUNTER.next_serial());
ActiveFocus::set(&seat, target);
}
} }
} }

View file

@ -20,7 +20,7 @@ fn toggle_stacking(state: &mut State, mapped: &CosmicMapped) {
let seat = shell.seats.last_active().clone(); let seat = shell.seats.last_active().clone();
if let Some(new_focus) = shell.toggle_stacking(&seat, mapped) { if let Some(new_focus) = shell.toggle_stacking(&seat, mapped) {
std::mem::drop(shell); 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 { if let Some((target, _)) = res {
std::mem::drop(shell); 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 { if let Some((target, _point)) = res {
std::mem::drop(shell); std::mem::drop(shell);
Shell::set_focus(state, Some(&target), &seat, None) Shell::set_focus(state, Some(&target), &seat, None, true)
} }
} }
} }

View file

@ -161,6 +161,7 @@ impl Item {
} }
} }
/// Menu that comes up when right-clicking an application header bar
pub struct ContextMenu { pub struct ContextMenu {
items: Vec<Item>, items: Vec<Item>,
selected: AtomicBool, selected: AtomicBool,

View file

@ -882,6 +882,7 @@ impl Drop for MoveGrab {
Some(&KeyboardFocusTarget::from(mapped)), Some(&KeyboardFocusTarget::from(mapped)),
&seat, &seat,
Some(serial), Some(serial),
false,
) )
} }
}); });

View file

@ -728,7 +728,7 @@ impl FloatingLayout {
self.space.element_geometry(elem).map(RectExt::as_local) self.space.element_geometry(elem).map(RectExt::as_local)
} }
pub fn element_under(&mut self, location: Point<f64, Local>) -> Option<KeyboardFocusTarget> { pub fn element_under(&self, location: Point<f64, Local>) -> Option<KeyboardFocusTarget> {
self.space self.space
.element_under(location.as_logical()) .element_under(location.as_logical())
.map(|(mapped, _)| mapped.clone().into()) .map(|(mapped, _)| mapped.clone().into())

View file

@ -3088,10 +3088,7 @@ impl TilingLayout {
None None
} }
pub fn element_under( pub fn element_under(&self, location_f64: Point<f64, Local>) -> Option<KeyboardFocusTarget> {
&mut self,
location_f64: Point<f64, Local>,
) -> Option<KeyboardFocusTarget> {
let location = location_f64.to_i32_round(); let location = location_f64.to_i32_round();
for (mapped, geo) in self.mapped() { for (mapped, geo) in self.mapped() {

View file

@ -8,6 +8,7 @@ use std::{
}; };
use wayland_backend::server::ClientId; use wayland_backend::server::ClientId;
use crate::wayland::protocols::workspace::WorkspaceCapabilities;
use cosmic_comp_config::{ use cosmic_comp_config::{
workspace::{WorkspaceLayout, WorkspaceMode}, workspace::{WorkspaceLayout, WorkspaceMode},
TileBehavior, TileBehavior,
@ -50,6 +51,8 @@ use smithay::{
xwayland::X11Surface, xwayland::X11Surface,
}; };
use smithay::wayland::shell::wlr_layer::Layer as WlrLayer;
use crate::{ use crate::{
backend::render::animations::spring::{Spring, SpringParams}, backend::render::animations::spring::{Spring, SpringParams},
config::Config, config::Config,
@ -69,8 +72,7 @@ use crate::{
toplevel_leave_workspace, ToplevelInfoState, toplevel_leave_workspace, ToplevelInfoState,
}, },
workspace::{ workspace::{
WorkspaceCapabilities, WorkspaceGroupHandle, WorkspaceHandle, WorkspaceState, WorkspaceGroupHandle, WorkspaceHandle, WorkspaceState, WorkspaceUpdateGuard,
WorkspaceUpdateGuard,
}, },
}, },
}, },
@ -1402,6 +1404,148 @@ impl Shell {
self.workspaces.active_mut(output) 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<f64, Global>,
seat: &Seat<State>,
) -> (Option<KeyboardFocusTarget>, 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<KeyboardFocusTarget> = 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<CosmicMapped> {
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( pub fn refresh_active_space(
&mut self, &mut self,
output: &Output, output: &Output,
@ -2130,11 +2274,11 @@ impl Shell {
} }
pub fn element_under( pub fn element_under(
&mut self, &self,
location: Point<f64, Global>, location: Point<f64, Global>,
output: &Output, output: &Output,
) -> Option<KeyboardFocusTarget> { ) -> Option<KeyboardFocusTarget> {
self.workspaces.sets.get_mut(output).and_then(|set| { self.workspaces.sets.get(output).and_then(|set| {
set.sticky_layer set.sticky_layer
.space .space
.element_under(location.to_local(output).as_logical()) .element_under(location.to_local(output).as_logical())
@ -2641,20 +2785,59 @@ impl Shell {
Some((grab, Focus::Clear)) 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<Rectangle<i32, Global>> {
if let Some(element) = self.focused_element(target) {
self.element_geometry(&element)
} else {
None
}
}
pub fn element_geometry(&self, mapped: &CosmicMapped) -> Option<Rectangle<i32, Global>> {
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] #[must_use]
pub fn next_focus(&self, direction: FocusDirection, seat: &Seat<State>) -> FocusResult { pub fn next_focus(&self, direction: FocusDirection, seat: &Seat<State>) -> FocusResult {
let overview = self.overview_mode().0; let overview = self.overview_mode().0;
let output = seat.active_output();
let Some(target) = seat.get_keyboard().unwrap().current_focus() else { let Some(target) = seat.get_keyboard().unwrap().current_focus() else {
return FocusResult::None; 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(_)) { if matches!(target, KeyboardFocusTarget::Fullscreen(_)) {
return FocusResult::None; 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 sticky_layer = &set.sticky_layer;
let workspace = &set.workspaces[set.active]; let workspace = &set.workspaces[set.active];
@ -2791,8 +2974,17 @@ impl Shell {
} }
#[must_use] #[must_use]
pub fn move_current_element(&mut self, direction: Direction, seat: &Seat<State>) -> MoveResult { pub fn move_current_element<'a>(
let output = seat.active_output(); &mut self,
direction: Direction,
seat: &Seat<State>,
) -> 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 workspace = self.active_space(&output);
let focus_stack = workspace.focus_stack.get(seat); let focus_stack = workspace.focus_stack.get(seat);
let Some(last) = focus_stack.last().cloned() else { let Some(last) = focus_stack.last().cloned() else {
@ -3133,7 +3325,9 @@ impl Shell {
} }
pub fn resize(&mut self, seat: &Seat<State>, direction: ResizeDirection, edge: ResizeEdge) { pub fn resize(&mut self, seat: &Seat<State>, 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 (_, idx) = self.workspaces.active_num(&output);
let Some(focused) = seat.get_keyboard().unwrap().current_focus() else { let Some(focused) = seat.get_keyboard().unwrap().current_focus() else {
return; return;
@ -3234,7 +3428,10 @@ impl Shell {
#[must_use] #[must_use]
pub fn toggle_stacking_focused(&mut self, seat: &Seat<State>) -> Option<KeyboardFocusTarget> { pub fn toggle_stacking_focused(&mut self, seat: &Seat<State>) -> Option<KeyboardFocusTarget> {
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 workspace = &mut set.workspaces[set.active];
let maybe_window = workspace.focus_stack.get(seat).iter().next().cloned(); let maybe_window = workspace.focus_stack.get(seat).iter().next().cloned();
if let Some(window) = maybe_window { if let Some(window) = maybe_window {

View file

@ -27,6 +27,10 @@ use super::grabs::{SeatMenuGrabState, SeatMoveGrabState};
crate::utils::id_gen!(next_seat_id, SEAT_ID, SEAT_IDS); crate::utils::id_gen!(next_seat_id, SEAT_ID, SEAT_IDS);
// for more information on seats, see:
// <https://wayland-book.com/print.html#seats-handling-input>
/// 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)] #[derive(Debug)]
pub struct Seats { pub struct Seats {
seats: Vec<Seat<State>>, seats: Vec<Seat<State>>,
@ -155,8 +159,13 @@ impl Drop for SeatId {
#[repr(transparent)] #[repr(transparent)]
struct SeatId(pub usize); struct SeatId(pub usize);
/// The output which contains the cursor associated with a seat.
struct ActiveOutput(pub Mutex<Output>); struct ActiveOutput(pub Mutex<Output>);
/// The output which currently has keyboard focus
struct FocusedOutput(pub Mutex<Option<Output>>);
pub fn create_seat( pub fn create_seat(
dh: &DisplayHandle, dh: &DisplayHandle,
seat_state: &mut SeatState<State>, seat_state: &mut SeatState<State>,
@ -175,6 +184,7 @@ pub fn create_seat(
userdata.insert_if_missing_threadsafe(SeatMenuGrabState::default); userdata.insert_if_missing_threadsafe(SeatMenuGrabState::default);
userdata.insert_if_missing_threadsafe(CursorState::default); userdata.insert_if_missing_threadsafe(CursorState::default);
userdata.insert_if_missing_threadsafe(|| ActiveOutput(Mutex::new(output.clone()))); 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())); userdata.insert_if_missing_threadsafe(|| Mutex::new(CursorImageStatus::default_named()));
// A lot of clients bind keyboard and pointer unconditionally once on launch.. // A lot of clients bind keyboard and pointer unconditionally once on launch..
@ -213,7 +223,13 @@ pub trait SeatExt {
fn id(&self) -> usize; fn id(&self) -> usize;
fn active_output(&self) -> Output; fn active_output(&self) -> Output;
fn focused_output(&self) -> Option<Output>;
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_active_output(&self, output: &Output);
fn set_focused_output(&self, output: Option<&Output>);
fn devices(&self) -> &Devices; fn devices(&self) -> &Devices;
fn supressed_keys(&self) -> &SupressedKeys; fn supressed_keys(&self) -> &SupressedKeys;
fn supressed_buttons(&self) -> &SupressedButtons; fn supressed_buttons(&self) -> &SupressedButtons;
@ -231,6 +247,9 @@ impl SeatExt for Seat<State> {
self.user_data().get::<SeatId>().unwrap().0 self.user_data().get::<SeatId>().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 { fn active_output(&self) -> Output {
self.user_data() self.user_data()
.get::<ActiveOutput>() .get::<ActiveOutput>()
@ -238,6 +257,21 @@ impl SeatExt for Seat<State> {
.unwrap() .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<Output> {
if self
.get_keyboard()
.is_some_and(|k| k.current_focus().is_some())
{
self.user_data()
.get::<FocusedOutput>()
.map(|x| x.0.lock().unwrap().clone())?
} else {
None
}
}
fn set_active_output(&self, output: &Output) { fn set_active_output(&self, output: &Output) {
*self *self
.user_data() .user_data()
@ -248,6 +282,16 @@ impl SeatExt for Seat<State> {
.unwrap() = output.clone(); .unwrap() = output.clone();
} }
fn set_focused_output(&self, output: Option<&Output>) {
*self
.user_data()
.get::<FocusedOutput>()
.unwrap()
.0
.lock()
.unwrap() = output.cloned();
}
fn devices(&self) -> &Devices { fn devices(&self) -> &Devices {
self.user_data().get::<Devices>().unwrap() self.user_data().get::<Devices>().unwrap()
} }

View file

@ -177,6 +177,7 @@ impl IsAlive for FullscreenSurface {
} }
} }
/// LIFO stack of focus targets
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct FocusStacks(HashMap<Seat<State>, IndexSet<CosmicMapped>>); pub struct FocusStacks(HashMap<Seat<State>, IndexSet<CosmicMapped>>);
@ -444,7 +445,7 @@ impl Workspace {
.find(|e| e.windows().any(|(w, _)| &w == surface)) .find(|e| e.windows().any(|(w, _)| &w == surface))
} }
pub fn element_under(&mut self, location: Point<f64, Global>) -> Option<KeyboardFocusTarget> { pub fn element_under(&self, location: Point<f64, Global>) -> Option<KeyboardFocusTarget> {
let location = location.to_local(&self.output); let location = location.to_local(&self.output);
self.floating_layer self.floating_layer
.element_under(location) .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> { pub fn get_fullscreen(&self) -> Option<&CosmicSurface> {
self.fullscreen self.fullscreen
.as_ref() .as_ref()

View file

@ -8,7 +8,7 @@ use crate::{
x11::X11State, x11::X11State,
}, },
config::{Config, OutputConfig, OutputState}, config::{Config, OutputConfig, OutputState},
input::gestures::GestureState, input::{gestures::GestureState, PointerFocusState},
shell::{grabs::SeatMoveGrabState, CosmicSurface, SeatExt, Shell}, shell::{grabs::SeatMoveGrabState, CosmicSurface, SeatExt, Shell},
utils::prelude::OutputExt, utils::prelude::OutputExt,
wayland::protocols::{ wayland::protocols::{
@ -226,6 +226,7 @@ pub struct Common {
pub xwayland_scale: Option<i32>, pub xwayland_scale: Option<i32>,
pub xwayland_state: Option<XWaylandState>, pub xwayland_state: Option<XWaylandState>,
pub xwayland_shell_state: XWaylandShellState, pub xwayland_shell_state: XWaylandShellState,
pub pointer_focus_state: Option<PointerFocusState>,
} }
#[derive(Debug)] #[derive(Debug)]
@ -621,6 +622,7 @@ impl State {
xwayland_scale: None, xwayland_scale: None,
xwayland_state: None, xwayland_state: None,
xwayland_shell_state, xwayland_shell_state,
pointer_focus_state: None,
}, },
backend: BackendData::Unset, backend: BackendData::Unset,
ready: Once::new(), ready: Once::new(),

View file

@ -272,7 +272,7 @@ impl State {
if let Some(target) = res { if let Some(target) = res {
let seat = shell.seats.last_active().clone(); let seat = shell.seats.last_active().clone();
std::mem::drop(shell); std::mem::drop(shell);
Shell::set_focus(self, Some(&target), &seat, None); Shell::set_focus(self, Some(&target), &seat, None, true);
return true; return true;
} }
} }
@ -290,7 +290,7 @@ impl State {
if let Some(target) = shell.map_layer(&layer_surface) { if let Some(target) = shell.map_layer(&layer_surface) {
let seat = shell.seats.last_active().clone(); let seat = shell.seats.last_active().clone();
std::mem::drop(shell); 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(); layer_surface.layer_surface().send_configure();
return true; return true;

View file

@ -87,7 +87,7 @@ impl ToplevelManagementHandler for State {
} }
mapped.focus_window(window); 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; return;
} }
} }
@ -133,7 +133,7 @@ impl ToplevelManagementHandler for State {
); );
if let Some((target, _)) = res { if let Some((target, _)) = res {
std::mem::drop(shell); std::mem::drop(shell);
Shell::set_focus(self, Some(&target), &seat, None); Shell::set_focus(self, Some(&target), &seat, None, true);
} }
return; return;
} }

View file

@ -156,7 +156,7 @@ impl XdgActivationHandler for State {
let target = element.into(); let target = element.into();
std::mem::drop(shell); 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()) } else if let Some(w) = shell.space_for(&element).map(|w| w.handle.clone())
{ {
shell.append_focus_stack(&element, &seat); shell.append_focus_stack(&element, &seat);

View file

@ -1,7 +1,10 @@
// SPDX-License-Identifier: GPL-3.0-only // SPDX-License-Identifier: GPL-3.0-only
use smithay::{delegate_xdg_foreign, wayland::xdg_foreign::{XdgForeignHandler, XdgForeignState}};
use crate::state::State; use crate::state::State;
use smithay::{
delegate_xdg_foreign,
wayland::xdg_foreign::{XdgForeignHandler, XdgForeignState},
};
impl XdgForeignHandler for State { impl XdgForeignHandler for State {
fn xdg_foreign_state(&mut self) -> &mut XdgForeignState { fn xdg_foreign_state(&mut self) -> &mut XdgForeignState {

View file

@ -97,7 +97,13 @@ impl XdgShellHandler for State {
grab.ungrab(PopupUngrabStrategy::All); grab.ungrab(PopupUngrabStrategy::All);
return; 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); 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<WlOutput>) { fn fullscreen_request(&mut self, surface: ToplevelSurface, output: Option<WlOutput>) {
let mut shell = self.common.shell.write().unwrap(); let mut shell = self.common.shell.write().unwrap();
let seat = shell.seats.last_active().clone(); 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 let output = output
.as_ref() .as_ref()
.and_then(Output::from_resource) .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() { if let Some(mapped) = shell.element_for_surface(surface.wl_surface()).cloned() {
let from = minimize_rectangle(&output, &mapped.active_window()); let from = minimize_rectangle(&output, &mapped.active_window());

View file

@ -390,7 +390,7 @@ impl XwmHandler for State {
if let Some(target) = res { if let Some(target) = res {
let seat = shell.seats.last_active().clone(); let seat = shell.seats.last_active().clone();
std::mem::drop(shell); std::mem::drop(shell);
Shell::set_focus(self, Some(&target), &seat, None); Shell::set_focus(self, Some(&target), &seat, None, false);
} }
} }
} }