@@ -843,11 +843,42 @@ impl WmState { |
| 843 | 843 | |
| 844 | 844 | if !at_edge && let Some(default_target) = Node::find_adjacent(&geoms, from, direction) { |
| 845 | 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 | 851 | let target = self |
| 847 | 852 | .focus_return_memory |
| 848 | 853 | .get(&(from, direction)) |
| 849 | 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 | 882 | .unwrap_or(default_target); |
| 852 | 883 | self.focus_window(target); |
| 853 | 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 | 3937 | #[test] |
| 3841 | 3938 | fn mouse_hover_on_inactive_stack_sliver_does_not_change_focus() { |
| 3842 | 3939 | let mut state = WmState::new(); |