@@ -843,11 +843,42 @@ impl WmState { |
| 843 | | 843 | |
| 844 | if !at_edge && let Some(default_target) = Node::find_adjacent(&geoms, from, direction) { | 844 | if !at_edge && let Some(default_target) = Node::find_adjacent(&geoms, from, direction) { |
| 845 | let candidates = Node::adjacent_candidates(&geoms, from, direction); | 845 | let candidates = Node::adjacent_candidates(&geoms, from, direction); |
| | 846 | + let from_rect = geoms.iter().find(|(w, _)| *w == from).map(|(_, r)| *r); |
| | 847 | + let default_rect = geoms |
| | 848 | + .iter() |
| | 849 | + .find(|(w, _)| *w == default_target) |
| | 850 | + .map(|(_, r)| *r); |
| 846 | let target = self | 851 | let target = self |
| 847 | .focus_return_memory | 852 | .focus_return_memory |
| 848 | .get(&(from, direction)) | 853 | .get(&(from, direction)) |
| 849 | .copied() | 854 | .copied() |
| 850 | - .filter(|remembered| candidates.contains(remembered)) | 855 | + .filter(|remembered| { |
| | 856 | + if !candidates.contains(remembered) { |
| | 857 | + return false; |
| | 858 | + } |
| | 859 | + // Only honor remembered target when its distance is |
| | 860 | + // close to the default's. Otherwise topology has |
| | 861 | + // changed (e.g., a new window appeared between |
| | 862 | + // source and remembered target) and the memory is |
| | 863 | + // stale. |
| | 864 | + let Some(remembered_rect) = geoms |
| | 865 | + .iter() |
| | 866 | + .find(|(w, _)| *w == *remembered) |
| | 867 | + .map(|(_, r)| *r) |
| | 868 | + else { |
| | 869 | + return false; |
| | 870 | + }; |
| | 871 | + let (Some(fr), Some(dr)) = (from_rect, default_rect) else { |
| | 872 | + return true; |
| | 873 | + }; |
| | 874 | + let d_default = Node::adjacent_distance(&fr, &dr, direction); |
| | 875 | + let d_remembered = |
| | 876 | + Node::adjacent_distance(&fr, &remembered_rect, direction); |
| | 877 | + // Memory wins when within 32px (a typical gap + |
| | 878 | + // border) of the default's distance — that's the |
| | 879 | + // tie-breaker regime memory is meant for. |
| | 880 | + d_remembered - d_default <= 32.0 |
| | 881 | + }) |
| 851 | .unwrap_or(default_target); | 882 | .unwrap_or(default_target); |
| 852 | self.focus_window(target); | 883 | self.focus_window(target); |
| 853 | self.remember_focus_transition(from, direction, target, &geoms); | 884 | self.remember_focus_transition(from, direction, target, &geoms); |
@@ -3837,6 +3868,72 @@ mod tests { |
| 3837 | ); | 3868 | ); |
| 3838 | } | 3869 | } |
| 3839 | | 3870 | |
| | 3871 | + /// Regression: a stale focus_return_memory entry from a prior layout |
| | 3872 | + /// (when only the far-left window existed as a Left candidate from |
| | 3873 | + /// the source) must not override the obvious nearest-neighbor |
| | 3874 | + /// default after a new window appears between source and the |
| | 3875 | + /// remembered target. Reproduces the widescreen layout where |
| | 3876 | + /// pressing Left from the bottom-right tile would skip the new |
| | 3877 | + /// middle-tall window and land on the far-left tile. |
| | 3878 | + #[test] |
| | 3879 | + fn focus_memory_yields_to_closer_default_when_topology_grows() { |
| | 3880 | + let mut state = WmState::new(); |
| | 3881 | + // Original 3-window layout: far-left tall (1), upper-right (2), |
| | 3882 | + // lower-right (3). From 3, Left has only 1 as a candidate. |
| | 3883 | + let geoms_before = vec![ |
| | 3884 | + (1u32, Rect::new(0.0, 0.0, 500.0, 1000.0)), |
| | 3885 | + (2u32, Rect::new(500.0, 0.0, 500.0, 500.0)), |
| | 3886 | + (3u32, Rect::new(500.0, 500.0, 500.0, 500.0)), |
| | 3887 | + ]; |
| | 3888 | + state.remember_focus_transition(3, Direction::Left, 1, &geoms_before); |
| | 3889 | + assert_eq!( |
| | 3890 | + state.focus_return_memory.get(&(3, Direction::Left)).copied(), |
| | 3891 | + Some(1) |
| | 3892 | + ); |
| | 3893 | + |
| | 3894 | + // New 4-window layout: a middle-tall (4) appears between the |
| | 3895 | + // left column and the right column. Now Left from 3 should |
| | 3896 | + // pick 4, not the stale memorized 1. |
| | 3897 | + let geoms_after = vec![ |
| | 3898 | + (1u32, Rect::new(0.0, 0.0, 250.0, 1000.0)), |
| | 3899 | + (4u32, Rect::new(250.0, 0.0, 250.0, 1000.0)), |
| | 3900 | + (2u32, Rect::new(500.0, 0.0, 500.0, 500.0)), |
| | 3901 | + (3u32, Rect::new(500.0, 500.0, 500.0, 500.0)), |
| | 3902 | + ]; |
| | 3903 | + let from_rect = geoms_after[3].1; |
| | 3904 | + let default_target = Node::find_adjacent(&geoms_after, 3, Direction::Left).unwrap(); |
| | 3905 | + assert_eq!(default_target, 4); |
| | 3906 | + let candidates = Node::adjacent_candidates(&geoms_after, 3, Direction::Left); |
| | 3907 | + let default_rect = geoms_after |
| | 3908 | + .iter() |
| | 3909 | + .find(|(w, _)| *w == default_target) |
| | 3910 | + .map(|(_, r)| *r) |
| | 3911 | + .unwrap(); |
| | 3912 | + let target = state |
| | 3913 | + .focus_return_memory |
| | 3914 | + .get(&(3, Direction::Left)) |
| | 3915 | + .copied() |
| | 3916 | + .filter(|remembered| { |
| | 3917 | + if !candidates.contains(remembered) { |
| | 3918 | + return false; |
| | 3919 | + } |
| | 3920 | + let Some(remembered_rect) = geoms_after |
| | 3921 | + .iter() |
| | 3922 | + .find(|(w, _)| *w == *remembered) |
| | 3923 | + .map(|(_, r)| *r) |
| | 3924 | + else { |
| | 3925 | + return false; |
| | 3926 | + }; |
| | 3927 | + let d_default = |
| | 3928 | + Node::adjacent_distance(&from_rect, &default_rect, Direction::Left); |
| | 3929 | + let d_remembered = |
| | 3930 | + Node::adjacent_distance(&from_rect, &remembered_rect, Direction::Left); |
| | 3931 | + d_remembered - d_default <= 32.0 |
| | 3932 | + }) |
| | 3933 | + .unwrap_or(default_target); |
| | 3934 | + assert_eq!(target, 4, "stale memory should yield to closer default"); |
| | 3935 | + } |
| | 3936 | + |
| 3840 | #[test] | 3937 | #[test] |
| 3841 | fn mouse_hover_on_inactive_stack_sliver_does_not_change_focus() { | 3938 | fn mouse_hover_on_inactive_stack_sliver_does_not_change_focus() { |
| 3842 | let mut state = WmState::new(); | 3939 | let mut state = WmState::new(); |