From a74a57819896660ed2c587e60ad11e170eda3e4b Mon Sep 17 00:00:00 2001 From: Ridan Vandenbergh Date: Thu, 21 Nov 2024 12:48:51 +0100 Subject: [PATCH] Add `focus-window-previous` action (#811) * Add `FocusWindowPrevious` action * remove [` * track previous focus in Niri instead of every window --------- Co-authored-by: Ivan Molodetskikh --- niri-config/src/lib.rs | 2 ++ niri-ipc/src/lib.rs | 2 ++ src/input/mod.rs | 22 ++++++------------- src/niri.rs | 48 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 58 insertions(+), 16 deletions(-) diff --git a/niri-config/src/lib.rs b/niri-config/src/lib.rs index 5c5f2cf76..0e506d80f 100644 --- a/niri-config/src/lib.rs +++ b/niri-config/src/lib.rs @@ -1163,6 +1163,7 @@ pub enum Action { FullscreenWindowById(u64), #[knuffel(skip)] FocusWindow(u64), + FocusWindowPrevious, FocusColumnLeft, FocusColumnRight, FocusColumnFirst, @@ -1269,6 +1270,7 @@ impl From for Action { niri_ipc::Action::FullscreenWindow { id: None } => Self::FullscreenWindow, niri_ipc::Action::FullscreenWindow { id: Some(id) } => Self::FullscreenWindowById(id), niri_ipc::Action::FocusWindow { id } => Self::FocusWindow(id), + niri_ipc::Action::FocusWindowPrevious {} => Self::FocusWindowPrevious, niri_ipc::Action::FocusColumnLeft {} => Self::FocusColumnLeft, niri_ipc::Action::FocusColumnRight {} => Self::FocusColumnRight, niri_ipc::Action::FocusColumnFirst {} => Self::FocusColumnFirst, diff --git a/niri-ipc/src/lib.rs b/niri-ipc/src/lib.rs index b4dad8dff..3df0eb4f1 100644 --- a/niri-ipc/src/lib.rs +++ b/niri-ipc/src/lib.rs @@ -204,6 +204,8 @@ pub enum Action { #[cfg_attr(feature = "clap", arg(long))] id: u64, }, + /// Focus the previously focused window. + FocusWindowPrevious {}, /// Focus the column to the left. FocusColumnLeft {}, /// Focus the column to the right. diff --git a/src/input/mod.rs b/src/input/mod.rs index 6a2428909..864bfe7f5 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -636,22 +636,12 @@ impl State { let window = self.niri.layout.windows().find(|(_, m)| m.id().get() == id); let window = window.map(|(_, m)| m.window.clone()); if let Some(window) = window { - let active_output = self.niri.layout.active_output().cloned(); - - self.niri.layout.activate_window(&window); - - let new_active = self.niri.layout.active_output().cloned(); - #[allow(clippy::collapsible_if)] - if new_active != active_output { - if !self.maybe_warp_cursor_to_focus_centered() { - self.move_cursor_to_output(&new_active.unwrap()); - } - } else { - self.maybe_warp_cursor_to_focus(); - } - - // FIXME: granular - self.niri.queue_redraw_all(); + self.focus_window(&window); + } + } + Action::FocusWindowPrevious => { + if let Some(window) = self.niri.previously_focused_window.clone() { + self.focus_window(&window); } } Action::SwitchLayout(action) => { diff --git a/src/niri.rs b/src/niri.rs index 47f92d0a6..40c8ae25d 100644 --- a/src/niri.rs +++ b/src/niri.rs @@ -266,6 +266,7 @@ pub struct Niri { pub bind_repeat_timer: Option, pub keyboard_focus: KeyboardFocus, pub layer_shell_on_demand_focus: Option, + pub previously_focused_window: Option, pub idle_inhibiting_surfaces: HashSet, pub is_fdo_idle_inhibited: Arc, @@ -682,6 +683,27 @@ impl State { rv } + /// Focus a specific window, taking care of a potential active output change and cursor + /// warp. + pub fn focus_window(&mut self, window: &Window) { + let active_output = self.niri.layout.active_output().cloned(); + + self.niri.layout.activate_window(window); + + let new_active = self.niri.layout.active_output().cloned(); + #[allow(clippy::collapsible_if)] + if new_active != active_output { + if !self.maybe_warp_cursor_to_focus_centered() { + self.move_cursor_to_output(&new_active.unwrap()); + } + } else { + self.maybe_warp_cursor_to_focus(); + } + + // FIXME: granular + self.niri.queue_redraw_all(); + } + pub fn maybe_warp_cursor_to_focus(&mut self) -> bool { if !self.niri.config.borrow().input.warp_mouse_to_focus { return false; @@ -891,12 +913,14 @@ impl State { ); // Tell the windows their new focus state for window rule purposes. + let mut previous_focus = None; if let KeyboardFocus::Layout { surface: Some(surface), } = &self.niri.keyboard_focus { if let Some((mapped, _)) = self.niri.layout.find_window_and_output_mut(surface) { mapped.set_is_focused(false); + previous_focus = Some(mapped.window.clone()); } } if let KeyboardFocus::Layout { @@ -908,6 +932,29 @@ impl State { } } + // Update the previous focus but only when staying focused on the layout. + // + // Case 1: opening and closing exclusive-keyboard layer-shell (e.g. app launcher). This + // involves going from Layout to LayerShell, then from LayerShell to Layout. The + // previously focused window should stay unchanged. + // + // Case 1.5: opening layer-shell, in the background switching layout focus, closing + // layer-shell. With the current logic, this won't update the previously focused + // window, which is incorrect. But this case should be rare. + // + // Case 2: switching to an empty workspace, then hitting FocusWindowPrevious. The focus + // should go to the window that was just focused. The keyboard focus goes from Layout + // (with Some surface) to Layout (with None surface), so we update the previously + // focused window. + // + // FIXME: Ideally this should happen inside Layout itself, then there wouldn't be any + // problems with layer-shell, etc. + if matches!(self.niri.keyboard_focus, KeyboardFocus::Layout { .. }) + && matches!(focus, KeyboardFocus::Layout { .. }) + { + self.niri.previously_focused_window = previous_focus; + } + if let Some(grab) = self.niri.popup_grab.as_mut() { if Some(&grab.root) != focus.surface() { trace!( @@ -1908,6 +1955,7 @@ impl Niri { seat, keyboard_focus: KeyboardFocus::Layout { surface: None }, layer_shell_on_demand_focus: None, + previously_focused_window: None, idle_inhibiting_surfaces: HashSet::new(), is_fdo_idle_inhibited: Arc::new(AtomicBool::new(false)), cursor_manager,