diff --git a/src/layout/mod.rs b/src/layout/mod.rs index 5c4dd639e4..03134c0e7b 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -334,8 +334,10 @@ pub trait LayoutElement { #[derive(Debug)] pub struct Layout { - /// Monitors and workspaes in the layout. + /// Monitors and workspaces in the layout. monitor_set: MonitorSet, + /// Focused outputs ordered from least to most recently focused. + output_focus_stack: Vec, /// Whether the layout should draw as active. /// /// This normally indicates that the layout has keyboard focus, but not always. E.g. when the @@ -688,6 +690,15 @@ impl OverviewProgress { } impl Layout { + pub fn outputs_by_recent_focus(&self) -> impl DoubleEndedIterator { + self.output_focus_stack.iter() + } + + fn note_output_focus(&mut self, output: &Output) { + self.output_focus_stack.retain(|focused| focused != output); + self.output_focus_stack.push(output.clone()); + } + pub fn new(clock: Clock, config: &Config) -> Self { Self::with_options_and_workspaces(clock, config, Options::from_config(config)) } @@ -695,6 +706,7 @@ impl Layout { pub fn with_options(clock: Clock, options: Options) -> Self { Self { monitor_set: MonitorSet::NoOutputs { workspaces: vec![] }, + output_focus_stack: vec![], is_active: true, last_active_workspace_id: HashMap::new(), interactive_move: None, @@ -720,6 +732,7 @@ impl Layout { Self { monitor_set: MonitorSet::NoOutputs { workspaces }, + output_focus_stack: vec![], is_active: true, last_active_workspace_id: HashMap::new(), interactive_move: None, @@ -831,6 +844,9 @@ impl Layout { monitor.overview_open = self.overview_open; monitor.set_overview_progress(self.overview_progress.as_ref()); + self.output_focus_stack.clear(); + self.output_focus_stack.push(monitor.output.clone()); + MonitorSet::Normal { monitors: vec![monitor], primary_idx: 0, @@ -841,6 +857,7 @@ impl Layout { } pub fn remove_output(&mut self, output: &Output) { + self.output_focus_stack.retain(|focused| focused != output); self.monitor_set = match mem::take(&mut self.monitor_set) { MonitorSet::Normal { mut monitors, @@ -918,6 +935,7 @@ impl Layout { if activate { *active_monitor_idx = monitor_idx; + self.note_output_focus(&monitors[monitor_idx].output); } } @@ -1017,6 +1035,7 @@ impl Layout { if activate.map_smart(|| false) { *active_monitor_idx = mon_idx; + self.note_output_focus(&mon.output); } // Set the default height for scrolling windows. @@ -1531,6 +1550,7 @@ impl Layout { for (workspace_idx, ws) in mon.workspaces.iter_mut().enumerate() { if ws.activate_window(window) { *active_monitor_idx = monitor_idx; + self.note_output_focus(&mon.output); // If currently in the middle of a vertical swipe between the target workspace // and some other, don't switch the workspace. @@ -1567,6 +1587,7 @@ impl Layout { for (workspace_idx, ws) in mon.workspaces.iter_mut().enumerate() { if ws.activate_window_without_raising(window) { *active_monitor_idx = monitor_idx; + self.note_output_focus(&mon.output); // If currently in the middle of a vertical swipe between the target workspace // and some other, don't switch the workspace. @@ -3274,6 +3295,7 @@ impl Layout { for (idx, mon) in monitors.iter().enumerate() { if &mon.output == output { *active_monitor_idx = idx; + self.note_output_focus(output); return; } } @@ -3373,6 +3395,7 @@ impl Layout { ); if activate.map_smart(|| false) { *active_monitor_idx = new_idx; + self.note_output_focus(&monitors[new_idx].output); } let mon = &mut monitors[mon_idx]; @@ -3488,6 +3511,7 @@ impl Layout { if activate { *active_monitor_idx = target_idx; + self.note_output_focus(&monitors[target_idx].output); } activate diff --git a/src/niri.rs b/src/niri.rs index ec40e01c14..d46825ee23 100644 --- a/src/niri.rs +++ b/src/niri.rs @@ -3462,6 +3462,52 @@ impl Niri { self.global_space.output_under(pos).next().cloned() } + fn closest_output_in_direction( + &self, + current: &Output, + extended_geo: Rectangle, + metrics: impl Fn(Rectangle, Rectangle) -> Option<(i32, i32)>, + ) -> Option { + let current_geo = self.global_space.output_geometry(current)?; + + let candidates = self + .global_space + .outputs() + .filter(|&output| output != current) + .filter_map(|output| { + let geo = self.global_space.output_geometry(output).unwrap(); + let metrics = metrics(current_geo, geo)?; + geo.overlaps(extended_geo).then_some((output, metrics)) + }) + .collect::>(); + + Self::pick_closest_output(self.layout.outputs_by_recent_focus(), candidates) + } + + fn pick_closest_output<'a>( + recent_focus: impl DoubleEndedIterator, + candidates: impl IntoIterator, + ) -> Option { + let candidates = candidates.into_iter().collect::>(); + let best_edge = candidates.iter().map(|(_, (edge, _))| *edge).max()?; + + recent_focus + .rev() + .find(|focused| { + candidates + .iter() + .any(|(candidate, (edge, _))| *edge == best_edge && *candidate == *focused) + }) + .cloned() + .or_else(|| { + candidates + .iter() + .filter(|(_, (edge, _))| *edge == best_edge) + .min_by_key(|(_, (_, orthogonal_distance))| *orthogonal_distance) + .map(|(output, _)| (*output).clone()) + }) + } + pub fn output_left_of(&self, current: &Output) -> Option { let current_geo = self.global_space.output_geometry(current)?; let extended_geo = Rectangle::new( @@ -3469,13 +3515,12 @@ impl Niri { Size::from((i32::MAX, current_geo.size.h)), ); - self.global_space - .outputs() - .map(|output| (output, self.global_space.output_geometry(output).unwrap())) - .filter(|(_, geo)| center(*geo).x < center(current_geo).x && geo.overlaps(extended_geo)) - .min_by_key(|(_, geo)| center(current_geo).x - center(*geo).x) - .map(|(output, _)| output) - .cloned() + self.closest_output_in_direction(current, extended_geo, |current_geo, geo| { + (center(geo).x < center(current_geo).x).then_some(( + geo.loc.x + geo.size.w, + (center(geo).y - center(current_geo).y).abs(), + )) + }) } pub fn output_right_of(&self, current: &Output) -> Option { @@ -3485,13 +3530,10 @@ impl Niri { Size::from((i32::MAX, current_geo.size.h)), ); - self.global_space - .outputs() - .map(|output| (output, self.global_space.output_geometry(output).unwrap())) - .filter(|(_, geo)| center(*geo).x > center(current_geo).x && geo.overlaps(extended_geo)) - .min_by_key(|(_, geo)| center(*geo).x - center(current_geo).x) - .map(|(output, _)| output) - .cloned() + self.closest_output_in_direction(current, extended_geo, |current_geo, geo| { + (center(geo).x > center(current_geo).x) + .then_some((-geo.loc.x, (center(geo).y - center(current_geo).y).abs())) + }) } pub fn output_up_of(&self, current: &Output) -> Option { @@ -3501,13 +3543,12 @@ impl Niri { Size::from((current_geo.size.w, i32::MAX)), ); - self.global_space - .outputs() - .map(|output| (output, self.global_space.output_geometry(output).unwrap())) - .filter(|(_, geo)| center(*geo).y < center(current_geo).y && geo.overlaps(extended_geo)) - .min_by_key(|(_, geo)| center(current_geo).y - center(*geo).y) - .map(|(output, _)| output) - .cloned() + self.closest_output_in_direction(current, extended_geo, |current_geo, geo| { + (center(geo).y < center(current_geo).y).then_some(( + geo.loc.y + geo.size.h, + (center(geo).x - center(current_geo).x).abs(), + )) + }) } pub fn output_down_of(&self, current: &Output) -> Option { @@ -3517,13 +3558,10 @@ impl Niri { Size::from((current_geo.size.w, i32::MAX)), ); - self.global_space - .outputs() - .map(|output| (output, self.global_space.output_geometry(output).unwrap())) - .filter(|(_, geo)| center(*geo).y > center(current_geo).y && geo.overlaps(extended_geo)) - .min_by_key(|(_, geo)| center(*geo).y - center(current_geo).y) - .map(|(output, _)| output) - .cloned() + self.closest_output_in_direction(current, extended_geo, |current_geo, geo| { + (center(geo).y > center(current_geo).y) + .then_some((-geo.loc.y, (center(geo).x - center(current_geo).x).abs())) + }) } pub fn output_previous_of(&self, current: &Output) -> Option {