diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/a11y.rs | 7 | ||||
| -rw-r--r-- | src/handlers/compositor.rs | 8 | ||||
| -rw-r--r-- | src/handlers/xdg_shell.rs | 8 | ||||
| -rw-r--r-- | src/input/mod.rs | 373 | ||||
| -rw-r--r-- | src/niri.rs | 154 | ||||
| -rw-r--r-- | src/ui/mod.rs | 1 | ||||
| -rw-r--r-- | src/ui/mru.rs | 1929 | ||||
| -rw-r--r-- | src/ui/mru/tests.rs | 135 | ||||
| -rw-r--r-- | src/window/mapped.rs | 12 |
9 files changed, 2550 insertions, 77 deletions
diff --git a/src/a11y.rs b/src/a11y.rs index 04b92dbf..f6553138 100644 --- a/src/a11y.rs +++ b/src/a11y.rs @@ -16,6 +16,7 @@ const ID_ANNOUNCEMENT: NodeId = NodeId(1); const ID_SCREENSHOT_UI: NodeId = NodeId(2); const ID_EXIT_CONFIRM_DIALOG: NodeId = NodeId(3); const ID_OVERVIEW: NodeId = NodeId(4); +const ID_MRU: NodeId = NodeId(5); pub struct A11y { event_loop: LoopHandle<'static, State>, @@ -205,6 +206,7 @@ impl Niri { KeyboardFocus::ScreenshotUi => ID_SCREENSHOT_UI, KeyboardFocus::ExitConfirmDialog => ID_EXIT_CONFIRM_DIALOG, KeyboardFocus::Overview => ID_OVERVIEW, + KeyboardFocus::Mru => ID_MRU, _ => ID_ROOT, } } @@ -237,12 +239,16 @@ impl Niri { let mut overview = Node::new(Role::Group); overview.set_label("Overview"); + let mut mru = Node::new(Role::Group); + mru.set_label("Recent windows"); + let mut root = Node::new(Role::Window); root.set_children(vec![ ID_ANNOUNCEMENT, ID_SCREENSHOT_UI, ID_EXIT_CONFIRM_DIALOG, ID_OVERVIEW, + ID_MRU, ]); let tree = Tree { @@ -260,6 +266,7 @@ impl Niri { (ID_SCREENSHOT_UI, screenshot_ui), (ID_EXIT_CONFIRM_DIALOG, exit_confirm_dialog), (ID_OVERVIEW, overview), + (ID_MRU, mru), ], tree: Some(tree), focus, diff --git a/src/handlers/compositor.rs b/src/handlers/compositor.rs index a7761824..dd5bb761 100644 --- a/src/handlers/compositor.rs +++ b/src/handlers/compositor.rs @@ -291,6 +291,7 @@ impl CompositorHandler for State { self.niri .stop_casts_for_target(CastTarget::Window { id: id.get() }); + self.niri.window_mru_ui.remove_window(id); self.niri.layout.remove_window(&window, transaction.clone()); self.add_default_dmabuf_pre_commit_hook(surface); @@ -311,6 +312,7 @@ impl CompositorHandler for State { if let Some(output) = output { self.niri.queue_redraw(&output); + self.niri.queue_redraw_mru_output(); } return; } @@ -337,6 +339,7 @@ impl CompositorHandler for State { } // The toplevel remains mapped. + self.niri.window_mru_ui.update_window(&self.niri.layout, id); self.niri.layout.update_window(&window, serial); // Move the toplevel according to the attach offset. @@ -357,6 +360,7 @@ impl CompositorHandler for State { if let Some(output) = output { self.niri.queue_redraw(&output); + self.niri.queue_redraw_mru_output(); } return; } @@ -370,9 +374,13 @@ impl CompositorHandler for State { let window = mapped.window.clone(); let output = output.cloned(); window.on_commit(); + self.niri + .window_mru_ui + .update_window(&self.niri.layout, mapped.id()); self.niri.layout.update_window(&window, None); if let Some(output) = output { self.niri.queue_redraw(&output); + self.niri.queue_redraw_mru_output(); } return; } diff --git a/src/handlers/xdg_shell.rs b/src/handlers/xdg_shell.rs index e5d91f16..20f348ba 100644 --- a/src/handlers/xdg_shell.rs +++ b/src/handlers/xdg_shell.rs @@ -864,9 +864,9 @@ impl XdgShellHandler for State { let window = mapped.window.clone(); let output = output.cloned(); - self.niri.stop_casts_for_target(CastTarget::Window { - id: mapped.id().get(), - }); + let id = mapped.id(); + self.niri + .stop_casts_for_target(CastTarget::Window { id: id.get() }); self.backend.with_primary_renderer(|renderer| { self.niri.layout.store_unmap_snapshot(renderer, &window); @@ -883,6 +883,7 @@ impl XdgShellHandler for State { let active_window = self.niri.layout.focus().map(|m| &m.window); let was_active = active_window == Some(&window); + self.niri.window_mru_ui.remove_window(id); self.niri.layout.remove_window(&window, transaction.clone()); self.add_default_dmabuf_pre_commit_hook(surface.wl_surface()); @@ -898,6 +899,7 @@ impl XdgShellHandler for State { if let Some(output) = output { self.niri.queue_redraw(&output); + self.niri.queue_redraw_mru_output(); } } diff --git a/src/input/mod.rs b/src/input/mod.rs index 5e9e321e..a6fc549f 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -6,7 +6,9 @@ use std::time::Duration; use calloop::timer::{TimeoutAction, Timer}; use input::event::gesture::GestureEventCoordinates as _; -use niri_config::{Action, Bind, Binds, Key, ModKey, Modifiers, SwitchBinds, Trigger}; +use niri_config::{ + Action, Bind, Binds, Config, Key, ModKey, Modifiers, MruDirection, SwitchBinds, Trigger, +}; use niri_ipc::LayoutSwitchTarget; use smithay::backend::input::{ AbsolutePositionEvent, Axis, AxisSource, ButtonState, Device, DeviceCapability, Event, @@ -43,6 +45,7 @@ use self::spatial_movement_grab::SpatialMovementGrab; use crate::layout::scrolling::ScrollDirection; use crate::layout::{ActivateWindow, LayoutElement as _}; use crate::niri::{CastTarget, PointerVisibility, State}; +use crate::ui::mru::{WindowMru, WindowMruUi}; use crate::ui::screenshot_ui::ScreenshotUi; use crate::utils::spawning::{spawn, spawn_sh}; use crate::utils::{center, get_monotonic_time, ResizeEdge}; @@ -385,6 +388,7 @@ impl State { let key_code = event.key_code(); let modified = keysym.modified_sym(); let raw = keysym.raw_latin_sym_or_raw_current_sym(); + let modifiers = modifiers_from_state(*mods); if this.niri.exit_confirm_dialog.is_open() && pressed { if raw == Some(Keysym::Return) { @@ -397,6 +401,18 @@ impl State { return FilterResult::Intercept(None); } + // Check if all modifiers were released while the MRU UI was open. If so, close the + // UI (which will also transfer the focus to the current MRU UI selection). + if this.niri.window_mru_ui.is_open() && !pressed && modifiers.is_empty() { + this.do_action(Action::MruConfirm, false); + + if this.niri.suppressed_keys.remove(&key_code) { + return FilterResult::Intercept(None); + } else { + return FilterResult::Forward; + } + } + if pressed && raw == Some(Keysym::Escape) && (this.niri.pick_window.is_some() || this.niri.pick_color.is_some()) @@ -416,20 +432,25 @@ impl State { this.niri.screenshot_ui.set_space_down(pressed); } - let bindings = &this.niri.config.borrow().binds; - let res = should_intercept_key( - &mut this.niri.suppressed_keys, - &bindings.0, - mod_key, - key_code, - modified, - raw, - pressed, - *mods, - &this.niri.screenshot_ui, - this.niri.config.borrow().input.disable_power_key_handling, - is_inhibiting_shortcuts, - ); + let res = { + let config = this.niri.config.borrow(); + let bindings = + make_binds_iter(&config, &mut this.niri.window_mru_ui, modifiers); + + should_intercept_key( + &mut this.niri.suppressed_keys, + bindings, + mod_key, + key_code, + modified, + raw, + pressed, + *mods, + &this.niri.screenshot_ui, + this.niri.config.borrow().input.disable_power_key_handling, + is_inhibiting_shortcuts, + ) + }; if matches!(res, FilterResult::Forward) { // If we didn't find any bind, try other hardcoded keys. @@ -440,6 +461,10 @@ impl State { return FilterResult::Intercept(Some(bind)); } } + + // Interaction with the active window, immediately update the active window's + // focus timestamp without waiting for a possible pending MRU lock-in delay. + this.niri.mru_apply_keyboard_commit(); } res @@ -641,6 +666,7 @@ impl State { } Action::Screenshot(show_cursor, path) => { self.open_screenshot_ui(show_cursor, path); + self.niri.cancel_mru(); } Action::ScreenshotWindow(write_to_disk, path) => { let focus = self.niri.layout.focus_with_output(); @@ -2179,6 +2205,90 @@ impl State { watcher.load_config(); } } + Action::MruConfirm => { + self.confirm_mru(); + } + Action::MruCancel => { + self.niri.cancel_mru(); + } + Action::MruAdvance { + direction, + scope, + filter, + } => { + if self.niri.window_mru_ui.is_open() { + self.niri.window_mru_ui.advance(direction, filter); + self.niri.queue_redraw_mru_output(); + } else if self.niri.config.borrow().recent_windows.on { + self.niri.mru_apply_keyboard_commit(); + + let config = self.niri.config.borrow(); + let scope = scope.unwrap_or(self.niri.window_mru_ui.scope()); + + let mut wmru = WindowMru::new(&self.niri); + if !wmru.is_empty() { + wmru.set_scope(scope); + if let Some(filter) = filter { + wmru.set_filter(filter); + } + + if let Some(output) = self.niri.layout.active_output() { + self.niri.window_mru_ui.open( + self.niri.clock.clone(), + wmru, + output.clone(), + ); + + // Only select the *next* window if some window (which should be the + // first one) is already focused. If nothing is focused, keep the first + // window (which is logically the "previously selected" one). + let keep_first = direction == MruDirection::Forward + && self.niri.layout.focus().is_none(); + if !keep_first { + self.niri.window_mru_ui.advance(direction, None); + } + + drop(config); + self.niri.queue_redraw_all(); + } + } + } + } + Action::MruCloseCurrentWindow => { + if self.niri.window_mru_ui.is_open() { + if let Some(id) = self.niri.window_mru_ui.current_window_id() { + if let Some(w) = self.niri.find_window_by_id(id) { + if let Some(tl) = w.toplevel() { + tl.send_close(); + } + } + } + } + } + Action::MruFirst => { + if self.niri.window_mru_ui.is_open() { + self.niri.window_mru_ui.first(); + self.niri.queue_redraw_mru_output(); + } + } + Action::MruLast => { + if self.niri.window_mru_ui.is_open() { + self.niri.window_mru_ui.last(); + self.niri.queue_redraw_mru_output(); + } + } + Action::MruSetScope(scope) => { + if self.niri.window_mru_ui.is_open() { + self.niri.window_mru_ui.set_scope(scope); + self.niri.queue_redraw_mru_output(); + } + } + Action::MruCycleScope => { + if self.niri.window_mru_ui.is_open() { + self.niri.window_mru_ui.cycle_scope(); + self.niri.queue_redraw_mru_output(); + } + } } } @@ -2301,6 +2411,14 @@ impl State { self.niri.screenshot_ui.pointer_motion(point, None); } + if let Some(mru_output) = self.niri.window_mru_ui.output() { + if let Some((output, pos_within_output)) = self.niri.output_under(new_pos) { + if mru_output == output { + self.niri.window_mru_ui.pointer_motion(pos_within_output); + } + } + } + let under = self.niri.contents_under(new_pos); // Handle confined pointer. @@ -2431,6 +2549,14 @@ impl State { self.niri.screenshot_ui.pointer_motion(point, None); } + if let Some(mru_output) = self.niri.window_mru_ui.output() { + if let Some((output, pos_within_output)) = self.niri.output_under(pos) { + if mru_output == output { + self.niri.window_mru_ui.pointer_motion(pos_within_output); + } + } + } + let under = self.niri.contents_under(pos); self.niri.handle_focus_follows_mouse(&under); @@ -2509,7 +2635,29 @@ impl State { let mods = self.niri.seat.get_keyboard().unwrap().modifier_state(); let modifiers = modifiers_from_state(mods); - if self.niri.mods_with_mouse_binds.contains(&modifiers) { + let mut is_mru_open = false; + if let Some(mru_output) = self.niri.window_mru_ui.output() { + is_mru_open = true; + if let Some(MouseButton::Left) = button { + let location = pointer.current_location(); + let (output, pos_within_output) = self.niri.output_under(location).unwrap(); + if mru_output == output { + let id = self.niri.window_mru_ui.pointer_motion(pos_within_output); + if id.is_some() { + self.confirm_mru(); + } else { + self.niri.cancel_mru(); + } + } else { + self.niri.cancel_mru(); + } + + self.niri.suppressed_buttons.insert(button_code); + return; + } + } + + if is_mru_open || self.niri.mods_with_mouse_binds.contains(&modifiers) { if let Some(bind) = match button { Some(MouseButton::Left) => Some(Trigger::MouseLeft), Some(MouseButton::Right) => Some(Trigger::MouseRight), @@ -2520,7 +2668,8 @@ impl State { } .and_then(|trigger| { let config = self.niri.config.borrow(); - let bindings = &config.binds.0; + let bindings = + make_binds_iter(&config, &mut self.niri.window_mru_ui, modifiers); find_configured_bind(bindings, mod_key, trigger, mods) }) { self.niri.suppressed_buttons.insert(button_code); @@ -2824,59 +2973,66 @@ impl State { false }; + let is_mru_open = self.niri.window_mru_ui.is_open(); + // Handle wheel scroll bindings. if source == AxisSource::Wheel { // If we have a scroll bind with current modifiers, then accumulate and don't pass to // Wayland. If there's no bind, reset the accumulator. let mods = self.niri.seat.get_keyboard().unwrap().modifier_state(); let modifiers = modifiers_from_state(mods); - let should_handle = - should_handle_in_overview || self.niri.mods_with_wheel_binds.contains(&modifiers); + let should_handle = should_handle_in_overview + || is_mru_open + || self.niri.mods_with_wheel_binds.contains(&modifiers); if should_handle { let horizontal = horizontal_amount_v120.unwrap_or(0.); let ticks = self.niri.horizontal_wheel_tracker.accumulate(horizontal); if ticks != 0 { - let (bind_left, bind_right) = if should_handle_in_overview - && modifiers.is_empty() - { - let bind_left = Some(Bind { - key: Key { - trigger: Trigger::WheelScrollLeft, - modifiers: Modifiers::empty(), - }, - action: Action::FocusColumnLeftUnderMouse, - repeat: true, - cooldown: None, - allow_when_locked: false, - allow_inhibiting: false, - hotkey_overlay_title: None, - }); - let bind_right = Some(Bind { - key: Key { - trigger: Trigger::WheelScrollRight, - modifiers: Modifiers::empty(), - }, - action: Action::FocusColumnRightUnderMouse, - repeat: true, - cooldown: None, - allow_when_locked: false, - allow_inhibiting: false, - hotkey_overlay_title: None, - }); - (bind_left, bind_right) - } else { - let config = self.niri.config.borrow(); - let bindings = &config.binds.0; - let bind_left = - find_configured_bind(bindings, mod_key, Trigger::WheelScrollLeft, mods); - let bind_right = find_configured_bind( - bindings, - mod_key, - Trigger::WheelScrollRight, - mods, - ); - (bind_left, bind_right) - }; + let (bind_left, bind_right) = + if should_handle_in_overview && modifiers.is_empty() { + let bind_left = Some(Bind { + key: Key { + trigger: Trigger::WheelScrollLeft, + modifiers: Modifiers::empty(), + }, + action: Action::FocusColumnLeftUnderMouse, + repeat: true, + cooldown: None, + allow_when_locked: false, + allow_inhibiting: false, + hotkey_overlay_title: None, + }); + let bind_right = Some(Bind { + key: Key { + trigger: Trigger::WheelScrollRight, + modifiers: Modifiers::empty(), + }, + action: Action::FocusColumnRightUnderMouse, + repeat: true, + cooldown: None, + allow_when_locked: false, + allow_inhibiting: false, + hotkey_overlay_title: None, + }); + (bind_left, bind_right) + } else { + let config = self.niri.config.borrow(); + let bindings = + make_binds_iter(&config, &mut self.niri.window_mru_ui, modifiers); + let bind_left = find_configured_bind( + bindings.clone(), + mod_key, + Trigger::WheelScrollLeft, + mods, + ); + let bind_right = find_configured_bind( + bindings, + mod_key, + Trigger::WheelScrollRight, + mods, + ); + (bind_left, bind_right) + }; if let Some(right) = bind_right { for _ in 0..ticks { @@ -2948,9 +3104,14 @@ impl State { (bind_up, bind_down) } else { let config = self.niri.config.borrow(); - let bindings = &config.binds.0; - let bind_up = - find_configured_bind(bindings, mod_key, Trigger::WheelScrollUp, mods); + let bindings = + make_binds_iter(&config, &mut self.niri.window_mru_ui, modifiers); + let bind_up = find_configured_bind( + bindings.clone(), + mod_key, + Trigger::WheelScrollUp, + mods, + ); let bind_down = find_configured_bind(bindings, mod_key, Trigger::WheelScrollDown, mods); (bind_up, bind_down) @@ -3081,16 +3242,21 @@ impl State { } } - if self.niri.mods_with_finger_scroll_binds.contains(&modifiers) { + if is_mru_open || self.niri.mods_with_finger_scroll_binds.contains(&modifiers) { let ticks = self .niri .horizontal_finger_scroll_tracker .accumulate(horizontal); if ticks != 0 { let config = self.niri.config.borrow(); - let bindings = &config.binds.0; - let bind_left = - find_configured_bind(bindings, mod_key, Trigger::TouchpadScrollLeft, mods); + let bindings = + make_binds_iter(&config, &mut self.niri.window_mru_ui, modifiers); + let bind_left = find_configured_bind( + bindings.clone(), + mod_key, + Trigger::TouchpadScrollLeft, + mods, + ); let bind_right = find_configured_bind(bindings, mod_key, Trigger::TouchpadScrollRight, mods); drop(config); @@ -3113,9 +3279,14 @@ impl State { .accumulate(vertical); if ticks != 0 { let config = self.niri.config.borrow(); - let bindings = &config.binds.0; - let bind_up = - find_configured_bind(bindings, mod_key, Trigger::TouchpadScrollUp, mods); + let bindings = + make_binds_iter(&config, &mut self.niri.window_mru_ui, modifiers); + let bind_up = find_configured_bind( + bindings.clone(), + mod_key, + Trigger::TouchpadScrollUp, + mods, + ); let bind_down = find_configured_bind(bindings, mod_key, Trigger::TouchpadScrollDown, mods); drop(config); @@ -3234,6 +3405,14 @@ impl State { self.niri.screenshot_ui.pointer_motion(point, None); } + if let Some(mru_output) = self.niri.window_mru_ui.output() { + if let Some((output, pos_within_output)) = self.niri.output_under(pos) { + if mru_output == output { + self.niri.window_mru_ui.pointer_motion(pos_within_output); + } + } + } + let under = self.niri.contents_under(pos); let tablet_seat = self.niri.seat.tablet_seat(); @@ -3311,6 +3490,19 @@ impl State { self.niri.queue_redraw_all(); } } + } else if let Some(mru_output) = self.niri.window_mru_ui.output() { + if let Some((output, pos_within_output)) = self.niri.output_under(pos) { + if mru_output == output { + let id = self.niri.window_mru_ui.pointer_motion(pos_within_output); + if id.is_some() { + self.confirm_mru(); + } else { + self.niri.cancel_mru(); + } + } else { + self.niri.cancel_mru(); + } + } } else if let Some((window, _)) = under.window { if let Some(output) = is_overview_open.then_some(under.output).flatten() { let mut workspaces = self.niri.layout.workspaces(); @@ -3425,6 +3617,11 @@ impl State { } fn on_gesture_swipe_begin<I: InputBackend>(&mut self, event: I::GestureSwipeBeginEvent) { + if self.niri.window_mru_ui.is_open() { + // Don't start swipe gestures while in the MRU. + return; + } + if event.fingers() == 3 { self.niri.gesture_swipe_3f_cumulative = Some((0., 0.)); @@ -3772,6 +3969,19 @@ impl State { self.niri.queue_redraw_all(); } } + } else if let Some(mru_output) = self.niri.window_mru_ui.output() { + if let Some((output, pos_within_output)) = self.niri.output_under(pos) { + if mru_output == output { + let id = self.niri.window_mru_ui.pointer_motion(pos_within_output); + if id.is_some() { + self.confirm_mru(); + } else { + self.niri.cancel_mru(); + } + } else { + self.niri.cancel_mru(); + } + } } else if !handle.is_grabbed() { let mods = self.niri.seat.get_keyboard().unwrap().modifier_state(); let mods = modifiers_from_state(mods); @@ -4696,6 +4906,29 @@ fn grab_allows_hot_corner(grab: &(dyn PointerGrab<State> + 'static)) -> bool { true } +/// Returns an iterator over bindings. +/// +/// Includes dynamically populated bindings like the MRU UI. +fn make_binds_iter<'a>( + config: &'a Config, + mru: &'a mut WindowMruUi, + mods: Modifiers, +) -> impl Iterator<Item = &'a Bind> + Clone { + // Figure out the binds to use depending on whether the MRU is enabled and/or open. + let general_binds = (!mru.is_open()).then_some(config.binds.0.iter()); + let general_binds = general_binds.into_iter().flatten(); + + let mru_binds = + (config.recent_windows.on || mru.is_open()).then_some(config.recent_windows.binds.iter()); + let mru_binds = mru_binds.into_iter().flatten(); + + let mru_open_binds = mru.is_open().then(|| mru.opened_bindings(mods)); + let mru_open_binds = mru_open_binds.into_iter().flatten(); + + // MRU binds take precedence over general ones. + mru_binds.chain(mru_open_binds).chain(general_binds) +} + #[cfg(test)] mod tests { use std::cell::Cell; diff --git a/src/niri.rs b/src/niri.rs index 551439c3..b6db2a25 100644 --- a/src/niri.rs +++ b/src/niri.rs @@ -16,7 +16,7 @@ use calloop::futures::Scheduler; use niri_config::debug::PreviewRender; use niri_config::{ Config, FloatOrInt, Key, Modifiers, OutputName, TrackLayout, WarpMouseToFocusMode, - WorkspaceReference, Xkb, + WorkspaceReference, Xkb, DEFAULT_MRU_COMMIT_MS, }; use smithay::backend::allocator::Fourcc; use smithay::backend::input::Keycode; @@ -165,6 +165,7 @@ use crate::render_helpers::{ use crate::ui::config_error_notification::ConfigErrorNotification; use crate::ui::exit_confirm_dialog::{ExitConfirmDialog, ExitConfirmDialogRenderElement}; use crate::ui::hotkey_overlay::HotkeyOverlay; +use crate::ui::mru::{MruCloseRequest, WindowMruUi, WindowMruUiRenderElement}; use crate::ui::screen_transition::{self, ScreenTransition}; use crate::ui::screenshot_ui::{OutputScreenshot, ScreenshotUi, ScreenshotUiRenderElement}; use crate::utils::scale::{closest_representable_scale, guess_monitor_scale}; @@ -384,6 +385,9 @@ pub struct Niri { pub hotkey_overlay: HotkeyOverlay, pub exit_confirm_dialog: ExitConfirmDialog, + pub window_mru_ui: WindowMruUi, + pub pending_mru_commit: Option<PendingMruCommit>, + pub pick_window: Option<async_channel::Sender<Option<MappedId>>>, pub pick_color: Option<async_channel::Sender<Option<niri_ipc::PickedColor>>>, @@ -520,6 +524,7 @@ pub enum KeyboardFocus { ScreenshotUi, ExitConfirmDialog, Overview, + Mru, } #[derive(Default, Clone, PartialEq)] @@ -582,6 +587,14 @@ pub enum CastTarget { Window { id: u64 }, } +/// Pending update to a window's focus timestamp. +#[derive(Debug)] +pub struct PendingMruCommit { + id: MappedId, + token: RegistrationToken, + stamp: Duration, +} + impl RedrawState { fn queue_redraw(self) -> Self { match self { @@ -620,6 +633,7 @@ impl KeyboardFocus { KeyboardFocus::ScreenshotUi => None, KeyboardFocus::ExitConfirmDialog => None, KeyboardFocus::Overview => None, + KeyboardFocus::Mru => None, } } @@ -631,6 +645,7 @@ impl KeyboardFocus { KeyboardFocus::ScreenshotUi => None, KeyboardFocus::ExitConfirmDialog => None, KeyboardFocus::Overview => None, + KeyboardFocus::Mru => None, } } @@ -939,6 +954,12 @@ impl State { self.niri.queue_redraw_all(); } + pub fn confirm_mru(&mut self) { + if let Some(window) = self.niri.close_mru(MruCloseRequest::Confirm) { + self.focus_window(&window); + } + } + pub fn maybe_warp_cursor_to_focus(&mut self) -> bool { let focused = match self.niri.config.borrow().input.warp_mouse_to_focus { None => return false, @@ -1099,6 +1120,8 @@ impl State { } } else if self.niri.screenshot_ui.is_open() { KeyboardFocus::ScreenshotUi + } else if self.niri.window_mru_ui.is_open() { + KeyboardFocus::Mru } else if let Some(output) = self.niri.layout.active_output() { let mon = self.niri.layout.monitor_for_output(output).unwrap(); let layers = layer_map_for_output(output); @@ -1225,6 +1248,38 @@ impl State { { if let Some((mapped, _)) = self.niri.layout.find_window_and_output_mut(surface) { mapped.set_is_focused(true); + + // If `mapped` does not have a focus timestamp, then the window is newly + // created/mapped and a timestamp is unconditionally created. + // + // If `mapped` already has a timestamp only update it after the focus lock-in + // period has gone by without the focus having elsewhere. + let stamp = get_monotonic_time(); + + if mapped.get_focus_timestamp().is_none() { + mapped.set_focus_timestamp(stamp); + } else { + let timer = + Timer::from_duration(Duration::from_millis(DEFAULT_MRU_COMMIT_MS)); + + let focus_token = self + .niri + .event_loop + .insert_source(timer, move |_, _, state| { + state.niri.mru_apply_keyboard_commit(); + TimeoutAction::Drop + }) + .unwrap(); + if let Some(PendingMruCommit { token, .. }) = + self.niri.pending_mru_commit.replace(PendingMruCommit { + id: mapped.id(), + token: focus_token, + stamp, + }) + { + self.niri.event_loop.remove(token); + } + } } } @@ -1411,6 +1466,7 @@ impl State { let mut layer_rules_changed = false; let mut shaders_changed = false; let mut cursor_inactivity_timeout_changed = false; + let mut recent_windows_changed = false; let mut xwls_changed = false; let mut old_config = self.niri.config.borrow_mut(); @@ -1459,8 +1515,9 @@ impl State { preserved_output_config = Some(mem::take(&mut old_config.outputs)); } + let binds_changed = config.binds != old_config.binds; let new_mod_key = self.backend.mod_key(&config); - if new_mod_key != self.backend.mod_key(&old_config) || config.binds != old_config.binds { + if new_mod_key != self.backend.mod_key(&old_config) || binds_changed { self.niri .hotkey_overlay .on_hotkey_config_updated(new_mod_key); @@ -1530,6 +1587,10 @@ impl State { output_config_changed = true; } + if config.recent_windows != old_config.recent_windows { + recent_windows_changed = true; + } + if config.xwayland_satellite != old_config.xwayland_satellite { xwls_changed = true; } @@ -1600,6 +1661,14 @@ impl State { self.niri.reset_pointer_inactivity_timer(); } + if binds_changed { + self.niri.window_mru_ui.update_binds(); + } + + if recent_windows_changed { + self.niri.window_mru_ui.update_config(); + } + if xwls_changed { // If xwl-s was previously working and is now off, we don't try to kill it or stop // watching the sockets, for simplicity's sake. @@ -2552,6 +2621,7 @@ impl Niri { let mods_with_finger_scroll_binds = mods_with_finger_scroll_binds(mod_key, &config_.binds); let screenshot_ui = ScreenshotUi::new(animation_clock.clone(), config.clone()); + let window_mru_ui = WindowMruUi::new(config.clone()); let config_error_notification = ConfigErrorNotification::new(animation_clock.clone(), config.clone()); @@ -2753,6 +2823,9 @@ impl Niri { hotkey_overlay, exit_confirm_dialog, + window_mru_ui, + pending_mru_commit: None, + pick_window: None, pick_color: None, @@ -3109,6 +3182,10 @@ impl Niri { .set_cursor_image(CursorImageStatus::default_named()); self.queue_redraw_all(); } + + if self.window_mru_ui.output() == Some(output) { + self.cancel_mru(); + } } pub fn output_resized(&mut self, output: &Output) { @@ -3376,7 +3453,11 @@ impl Niri { |
