aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/a11y.rs7
-rw-r--r--src/handlers/compositor.rs8
-rw-r--r--src/handlers/xdg_shell.rs8
-rw-r--r--src/input/mod.rs373
-rw-r--r--src/niri.rs154
-rw-r--r--src/ui/mod.rs1
-rw-r--r--src/ui/mru.rs1929
-rw-r--r--src/ui/mru/tests.rs135
-rw-r--r--src/window/mapped.rs12
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 {