Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions cosmic-comp-config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,15 @@ pub struct CosmicCompConfig {
pub cursor_follows_focus: bool,
/// The delay in milliseconds before focus follows mouse (if enabled)
pub focus_follows_cursor_delay: u64,
/// When `focus_follows_cursor` is enabled, controls whether the window
/// that gains keyboard focus from the pointer passing over it is also
/// raised to the front of the stack.
///
/// `true` = historical behaviour: hovering focuses AND raises.
/// `false` = "sloppy focus": hovering focuses but leaves stacking order
/// alone; a window only rises when explicitly acted upon
/// (clicked, keyboard-focused, or activated by an app).
pub focus_follows_cursor_raise: bool,
/// Let X11 applications scale themselves
pub descale_xwayland: XwaylandDescaling,
/// Let X11 applications snoop on certain key-presses to allow for global shortcuts
Expand Down Expand Up @@ -133,6 +142,9 @@ impl Default for CosmicCompConfig {
focus_follows_cursor: false,
cursor_follows_focus: false,
focus_follows_cursor_delay: 250,
// Default true preserves the historical focus-follows-cursor
// behaviour (raise on hover) for everyone who has not opted out.
focus_follows_cursor_raise: true,
descale_xwayland: XwaylandDescaling::Fractional,
xwayland_eavesdropping: XwaylandEavesdropping::default(),
edge_snap_threshold: 0,
Expand Down Expand Up @@ -247,3 +259,19 @@ pub enum XwaylandDescaling {
#[default]
Fractional,
}

#[cfg(test)]
mod tests {
use super::CosmicCompConfig;

/// The new "raise on focus-follows-cursor" option must default to `true`
/// so that upgrading users keep the historical behaviour (focus follows
/// the pointer AND raises). Only users who explicitly opt out get the
/// new "sloppy focus" behaviour.
#[test]
fn focus_follows_cursor_raise_defaults_to_true() {
// Build the whole config with its Default impl and check just the one field.
let config = CosmicCompConfig::default();
assert!(config.focus_follows_cursor_raise);
}
}
11 changes: 11 additions & 0 deletions src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -924,6 +924,17 @@ fn config_changed(config: cosmic_config::Config, keys: Vec<String>, state: &mut
state.common.config.cosmic_conf.focus_follows_cursor_delay = new;
}
}
// Live-reload the "raise on focus-follows-cursor" toggle. Reading the
// key and comparing before assigning matches the surrounding arms and
// avoids a needless write when the value is unchanged. No further work
// is required on change: the flag is consulted the next time focus
// follows the pointer, so the new setting takes effect immediately.
"focus_follows_cursor_raise" => {
let new = get_config::<bool>(&config, "focus_follows_cursor_raise");
if new != state.common.config.cosmic_conf.focus_follows_cursor_raise {
state.common.config.cosmic_conf.focus_follows_cursor_raise = new;
}
}
"edge_snap_threshold" => {
let new = get_config::<u32>(&config, "edge_snap_threshold");
if new != state.common.config.cosmic_conf.edge_snap_threshold {
Expand Down
55 changes: 47 additions & 8 deletions src/input/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
use crate::{
config::{Action, PrivateAction},
shell::{
FocusResult, InvalidWorkspaceIndex, MoveResult, SeatExt, Trigger, WorkspaceDelta,
FocusResult, InvalidWorkspaceIndex, MoveResult, Raise, SeatExt, Trigger, WorkspaceDelta,
focus::{FocusTarget, target::KeyboardFocusTarget},
layout::tiling::SwapWindowGrab,
},
Expand Down Expand Up @@ -306,6 +306,7 @@ impl State {
seat,
None,
matches!(x, Action::MoveToWorkspace(_)),
Raise::Yes,
);
}
}
Expand Down Expand Up @@ -333,6 +334,7 @@ impl State {
seat,
None,
matches!(x, Action::MoveToLastWorkspace),
Raise::Yes,
);
}
}
Expand Down Expand Up @@ -381,6 +383,7 @@ impl State {
seat,
None,
matches!(x, Action::MoveToNextWorkspace),
Raise::Yes,
);
}
Ok(None) => {}
Expand Down Expand Up @@ -472,6 +475,7 @@ impl State {
seat,
None,
matches!(x, Action::MoveToPreviousWorkspace),
Raise::Yes,
);
}
Ok(None) => {}
Expand Down Expand Up @@ -565,7 +569,14 @@ impl State {
std::mem::drop(shell);

let update_cursor = self.common.config.cosmic_conf.cursor_follows_focus;
Shell::set_focus(self, new_target.as_ref(), seat, None, update_cursor);
Shell::set_focus(
self,
new_target.as_ref(),
seat,
None,
update_cursor,
Raise::Yes,
);

if let Some(ptr) = seat.get_pointer() {
// Update cursor position if `set_focus` didn't already
Expand Down Expand Up @@ -640,7 +651,14 @@ impl State {

if let Ok(Some((target, new_pos))) = res {
std::mem::drop(shell);
Shell::set_focus(self, Some(&target), seat, None, is_move_action);
Shell::set_focus(
self,
Some(&target),
seat,
None,
is_move_action,
Raise::Yes,
);
if let Some(ptr) = seat.get_pointer() {
ptr.motion(
self,
Expand Down Expand Up @@ -787,7 +805,7 @@ impl State {
}
FocusResult::Handled => {}
FocusResult::Some(target) => {
Shell::set_focus(self, Some(&target), seat, None, true);
Shell::set_focus(self, Some(&target), seat, None, true, Raise::Yes);
}
}
}
Expand Down Expand Up @@ -843,7 +861,7 @@ impl State {
)
}
MoveResult::ShiftFocus(shift) => {
Shell::set_focus(self, Some(&shift), seat, None, true);
Shell::set_focus(self, Some(&shift), seat, None, true, Raise::Yes);
}
_ => {
let current_output = seat.active_output();
Expand Down Expand Up @@ -919,15 +937,29 @@ impl State {
&self.common.event_loop_handle,
) {
std::mem::drop(shell);
Shell::set_focus(self, Some(&target), seat, Some(serial), true);
Shell::set_focus(
self,
Some(&target),
seat,
Some(serial),
true,
Raise::Yes,
);
}
}
Some(KeyboardFocusTarget::Fullscreen(surface)) => {
if let Some(target) =
shell.unfullscreen_request(&surface, &self.common.event_loop_handle)
{
std::mem::drop(shell);
Shell::set_focus(self, Some(&target), seat, Some(serial), true);
Shell::set_focus(
self,
Some(&target),
seat,
Some(serial),
true,
Raise::Yes,
);
}
}
_ => {}
Expand Down Expand Up @@ -964,7 +996,14 @@ impl State {
.write()
.toggle_stacking_focused(seat, &self.common.event_loop_handle);
if let Some(new_focus) = res {
Shell::set_focus(self, Some(&new_focus), seat, Some(serial), false);
Shell::set_focus(
self,
Some(&new_focus),
seat,
Some(serial),
false,
Raise::Yes,
);
}
}

Expand Down
39 changes: 34 additions & 5 deletions src/input/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use crate::{
},
input::gestures::{GestureState, SwipeAction},
shell::{
LastModifierChange, SeatExt, Trigger,
LastModifierChange, Raise, SeatExt, Trigger,
focus::{
Stage, render_input_order,
target::{KeyboardFocusTarget, PointerFocusTarget},
Expand Down Expand Up @@ -462,12 +462,27 @@ impl State {
//takes this function to run
state.common.pointer_focus_state = None;

// Focus is following the pointer here. Raise
// only if the user has left raise-on-hover on;
// otherwise pass Raise::No so the window gains
// focus without being lifted (sloppy focus).
let raise = if state
.common
.config
.cosmic_conf
.focus_follows_cursor_raise
{
Raise::Yes
} else {
Raise::No
};
Shell::set_focus(
state,
target.as_ref(),
&seat,
Some(SERIAL_COUNTER.next_serial()),
false,
raise,
);

TimeoutAction::Drop
Expand Down Expand Up @@ -909,7 +924,14 @@ impl State {
}
}

Shell::set_focus(self, Some(&target), &seat, Some(serial), false);
Shell::set_focus(
self,
Some(&target),
&seat,
Some(serial),
false,
Raise::Yes,
);
}
}
} else {
Expand Down Expand Up @@ -2036,7 +2058,14 @@ impl State {
) {
let seat = seat.clone();
self.common.event_loop_handle.insert_idle(move |state| {
Shell::set_focus(state, Some(&focus), &seat, None, true);
Shell::set_focus(
state,
Some(&focus),
&seat,
None,
true,
Raise::Yes,
);
});
}
old_workspace.refresh_focus_stack();
Expand All @@ -2052,7 +2081,7 @@ impl State {
std::mem::drop(spaces);
let seat = seat.clone();
self.common.event_loop_handle.insert_idle(move |state| {
Shell::set_focus(state, Some(&focus), &seat, None, true);
Shell::set_focus(state, Some(&focus), &seat, None, true, Raise::Yes);
});
}
workspace.refresh_focus_stack();
Expand Down Expand Up @@ -2088,7 +2117,7 @@ impl State {
) {
let seat = seat.clone();
self.common.event_loop_handle.insert_idle(move |state| {
Shell::set_focus(state, Some(&focus), &seat, None, true);
Shell::set_focus(state, Some(&focus), &seat, None, true, Raise::Yes);
});
}
old_workspace.refresh_focus_stack();
Expand Down
61 changes: 60 additions & 1 deletion src/shell/focus/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,27 @@ pub enum FocusTarget {
Fullscreen(CosmicSurface),
}

/// Whether a focus change should also raise the focused window to the top of
/// its layer's stacking order.
///
/// This exists to keep raising separate from focusing. Historically every
/// focus change raised the window; with focus-follows-cursor that means a
/// window jumps to the front merely because the pointer crossed it. Passing
/// `Raise::No` lets focus move without disturbing stacking order (sloppy
/// focus). A named enum is used instead of a bare `bool` because `set_focus`
/// already takes another boolean (`update_cursor`); two adjacent bools at a
/// call site are easy to transpose, whereas `Raise::Yes`/`Raise::No` is
/// self-documenting.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Raise {
/// Raise the window. Used for explicit focus intent: a click, a keyboard
/// action, or an application activating itself.
Yes,
/// Do not raise. Used when focus arrived passively (the pointer moved onto
/// the window) and the user has opted out of raise-on-hover.
No,
}

impl PartialEq<CosmicMapped> for FocusTarget {
fn eq(&self, other: &CosmicMapped) -> bool {
matches!(self, FocusTarget::Window(mapped) if mapped == other)
Expand Down Expand Up @@ -194,6 +215,10 @@ impl Shell {
seat: &Seat<State>,
serial: Option<Serial>,
update_cursor: bool,
// Whether this focus change should also raise the window. Explicit
// focus passes Raise::Yes; passive focus-follows-cursor passes the
// user's configured choice.
raise: Raise,
) {
let focus_target = match target {
Some(KeyboardFocusTarget::Element(mapped)) => Some(FocusTarget::Window(mapped.clone())),
Expand All @@ -209,7 +234,24 @@ impl Shell {

update_focus_state(seat, target, state, serial, update_cursor);

state.common.shell.write().update_active();
// Record or clear the "focused but do not auto-raise" mark under the
// same write lock we use to run update_active(), so the two stay
// consistent. On explicit focus (Raise::Yes) we clear any prior mark
// so the window raises normally. On passive focus with raising off
// (Raise::No) we mark the focused window — but only if it is an actual
// toplevel element; fullscreen and layer-surface targets are never in
// the floating stack, so there is nothing to suppress for them.
{
let mut shell = state.common.shell.write();
shell.no_raise_window = match raise {
Raise::No => match target {
Some(KeyboardFocusTarget::Element(mapped)) => Some(mapped.clone()),
_ => None,
},
Raise::Yes => None,
};
shell.update_active();
}
}

pub fn append_focus_stack(&mut self, target: impl Into<FocusTarget>, seat: &Seat<State>) {
Expand Down Expand Up @@ -269,9 +311,22 @@ impl Shell {
})
.collect::<Vec<_>>();

// Snapshot the no-raise mark before the loops below borrow `self`
// mutably (they call `self.workspaces...get_mut`). Cloning a
// CosmicMapped is cheap (it is reference-counted) and releasing the
// borrow here keeps the borrow checker happy.
let no_raise = self.no_raise_window.clone();

for output in self.outputs().cloned().collect::<Vec<_>>().into_iter() {
let set = self.workspaces.sets.get_mut(&output).unwrap();
for focused in focused_windows.iter() {
// Skip raising a window the user is only hovering over (sloppy
// focus): raising it would defeat focus-follows-cursor-without-
// raise. All other (explicit) focus changes cleared the mark,
// so they fall through and raise as before.
if no_raise.as_ref() == Some(focused) {
continue;
}
raise_with_children(&mut set.sticky_layer, focused);
}
for window in set.sticky_layer.mapped() {
Expand Down Expand Up @@ -302,6 +357,10 @@ impl Shell {
fs.surface.send_configure();
}
for focused in focused_windows.iter() {
// Same sloppy-focus guard as the sticky layer above.
if no_raise.as_ref() == Some(focused) {
continue;
}
raise_with_children(&mut workspace.floating_layer, focused);
}
for window in workspace.mapped() {
Expand Down
Loading