@@ -49,6 +49,7 @@ const CSS: &str = r"
4949.find-bar { background: alpha(@theme_bg_color, 0.95); border-top: 1px solid @borders; padding: 4px 6px; }
5050.find-bar entry { min-height: 28px; }
5151.goto-entry { min-height: 28px; }
52+ .dir-pane-focused { border: 2px solid @accent_color; border-radius: 4px; }
5253" ;
5354
5455const SEP : char = '\x1f' ;
@@ -5322,9 +5323,13 @@ fn build_dir_window(
53225323 }
53235324 } ) ;
53245325
5326+ // Shared selection model — only the focused pane uses it;
5327+ // the other pane uses NoSelection so no highlight is shown.
5328+ let dir_sel = SingleSelection :: new ( Some ( tree_model. clone ( ) ) ) ;
5329+ let no_sel = gtk4:: NoSelection :: new ( Some ( tree_model. clone ( ) ) ) ;
5330+
53255331 // ── Left pane ──────────────────────────────────────────────────
5326- let left_sel = SingleSelection :: new ( Some ( tree_model. clone ( ) ) ) ;
5327- let left_view = ColumnView :: new ( Some ( left_sel. clone ( ) ) ) ;
5332+ let left_view = ColumnView :: new ( Some ( dir_sel. clone ( ) ) ) ;
53285333 left_view. set_show_column_separators ( true ) ;
53295334 {
53305335 let col = ColumnViewColumn :: new ( Some ( "Name" ) , Some ( make_name_factory ( true ) ) ) ;
@@ -5340,8 +5345,7 @@ fn build_dir_window(
53405345 }
53415346
53425347 // ── Right pane ─────────────────────────────────────────────────
5343- let right_sel = SingleSelection :: new ( Some ( tree_model. clone ( ) ) ) ;
5344- let right_view = ColumnView :: new ( Some ( right_sel. clone ( ) ) ) ;
5348+ let right_view = ColumnView :: new ( Some ( no_sel. clone ( ) ) ) ;
53455349 right_view. set_show_column_separators ( true ) ;
53465350 {
53475351 let col = ColumnViewColumn :: new ( Some ( "Name" ) , Some ( make_name_factory ( false ) ) ) ;
@@ -5358,19 +5362,86 @@ fn build_dir_window(
53585362 right_view. append_column ( & col) ;
53595363 }
53605364
5361- // Track which pane was last focused (true = left, false = right)
5365+ // Track which pane was last focused (true = left, false = right).
5366+ // Swap models so only the focused pane shows selection.
53625367 let focused_left = Rc :: new ( Cell :: new ( true ) ) ;
5368+ let sel_syncing = Rc :: new ( Cell :: new ( false ) ) ;
5369+ left_view. add_css_class ( "dir-pane-focused" ) ;
53635370 {
53645371 let fl = focused_left. clone ( ) ;
5372+ let lv = left_view. clone ( ) ;
5373+ let rv = right_view. clone ( ) ;
5374+ let sel = dir_sel. clone ( ) ;
5375+ let ns = no_sel. clone ( ) ;
53655376 let fc = EventControllerFocus :: new ( ) ;
5366- fc. connect_enter ( move |_| fl. set ( true ) ) ;
5377+ fc. connect_enter ( move |_| {
5378+ if fl. get ( ) {
5379+ return ;
5380+ }
5381+ fl. set ( true ) ;
5382+ lv. add_css_class ( "dir-pane-focused" ) ;
5383+ rv. remove_css_class ( "dir-pane-focused" ) ;
5384+ lv. set_model ( Some ( & sel) ) ;
5385+ rv. set_model ( Some ( & ns) ) ;
5386+ let pos = sel. selected ( ) ;
5387+ let v = lv. clone ( ) ;
5388+ gtk4:: glib:: idle_add_local_once ( move || {
5389+ v. scroll_to ( pos, None , gtk4:: ListScrollFlags :: FOCUS , None ) ;
5390+ } ) ;
5391+ } ) ;
53675392 left_view. add_controller ( fc) ;
53685393 let fl = focused_left. clone ( ) ;
5394+ let lv = left_view. clone ( ) ;
5395+ let rv = right_view. clone ( ) ;
5396+ let sel = dir_sel. clone ( ) ;
5397+ let ns = no_sel. clone ( ) ;
53695398 let fc = EventControllerFocus :: new ( ) ;
5370- fc. connect_enter ( move |_| fl. set ( false ) ) ;
5399+ fc. connect_enter ( move |_| {
5400+ if !fl. get ( ) {
5401+ return ;
5402+ }
5403+ fl. set ( false ) ;
5404+ rv. add_css_class ( "dir-pane-focused" ) ;
5405+ lv. remove_css_class ( "dir-pane-focused" ) ;
5406+ rv. set_model ( Some ( & sel) ) ;
5407+ lv. set_model ( Some ( & ns) ) ;
5408+ let pos = sel. selected ( ) ;
5409+ let v = rv. clone ( ) ;
5410+ gtk4:: glib:: idle_add_local_once ( move || {
5411+ v. scroll_to ( pos, None , gtk4:: ListScrollFlags :: FOCUS , None ) ;
5412+ } ) ;
5413+ } ) ;
53715414 right_view. add_controller ( fc) ;
53725415 }
53735416
5417+ // Left/Right arrow keys switch between panes
5418+ {
5419+ let rv = right_view. clone ( ) ;
5420+ let kc = EventControllerKey :: new ( ) ;
5421+ kc. set_propagation_phase ( gtk4:: PropagationPhase :: Capture ) ;
5422+ kc. connect_key_pressed ( move |_, key, _, _| {
5423+ if key == gtk4:: gdk:: Key :: Right {
5424+ rv. grab_focus ( ) ;
5425+ gtk4:: glib:: Propagation :: Stop
5426+ } else {
5427+ gtk4:: glib:: Propagation :: Proceed
5428+ }
5429+ } ) ;
5430+ left_view. add_controller ( kc) ;
5431+ let lv = left_view. clone ( ) ;
5432+ let kc = EventControllerKey :: new ( ) ;
5433+ kc. set_propagation_phase ( gtk4:: PropagationPhase :: Capture ) ;
5434+ kc. connect_key_pressed ( move |_, key, _, _| {
5435+ if key == gtk4:: gdk:: Key :: Left {
5436+ lv. grab_focus ( ) ;
5437+ gtk4:: glib:: Propagation :: Stop
5438+ } else {
5439+ gtk4:: glib:: Propagation :: Proceed
5440+ }
5441+ } ) ;
5442+ right_view. add_controller ( kc) ;
5443+ }
5444+
53745445 // ScrolledWindows + Paned
53755446 let left_scroll = ScrolledWindow :: builder ( )
53765447 . hscrollbar_policy ( PolicyType :: Automatic )
@@ -5449,18 +5520,19 @@ fn build_dir_window(
54495520 let rd = right_dir. clone ( ) ;
54505521 let st = settings. clone ( ) ;
54515522 let tm = tree_model. clone ( ) ;
5452- let ls = left_sel. clone ( ) ;
5453- let rsel = right_sel. clone ( ) ;
5523+ let sel = dir_sel. clone ( ) ;
5524+ let syncing = sel_syncing. clone ( ) ;
5525+ let lv = left_view. clone ( ) ;
5526+ let rv = right_view. clone ( ) ;
54545527 move || {
5455- // Save selected rel_paths
5456- let sel_rel = |sel : & SingleSelection | -> Option < String > {
5457- let pos = sel. selected ( ) ;
5458- let row = tm. item ( pos) ?. downcast :: < TreeListRow > ( ) . ok ( ) ?;
5528+ // Suppress selection sync during rebuild
5529+ syncing. set ( true ) ;
5530+ let saved_pos = sel. selected ( ) ;
5531+ let saved_rel = ( || -> Option < String > {
5532+ let row = tm. item ( saved_pos) ?. downcast :: < TreeListRow > ( ) . ok ( ) ?;
54595533 let obj = row. item ( ) . and_downcast :: < StringObject > ( ) ?;
54605534 Some ( decode_rel_path ( & obj. string ( ) ) . to_string ( ) )
5461- } ;
5462- let left_rel = sel_rel ( & ls) ;
5463- let right_rel = sel_rel ( & rsel) ;
5535+ } ) ( ) ;
54645536
54655537 // Save expanded rel_paths
54665538 let mut expanded: Vec < String > = Vec :: new ( ) ;
@@ -5482,7 +5554,24 @@ fn build_dir_window(
54825554 & mut new_map,
54835555 & st. borrow ( ) . dir_filters ,
54845556 ) ;
5557+ // Skip rebuild if root-level data hasn't changed
5558+ let root_changed = new_store. n_items ( ) != rs. n_items ( )
5559+ || ( 0 ..new_store. n_items ( ) ) . any ( |i| {
5560+ new_store
5561+ . item ( i)
5562+ . and_downcast :: < StringObject > ( )
5563+ . map ( |o| o. string ( ) )
5564+ != rs
5565+ . item ( i)
5566+ . and_downcast :: < StringObject > ( )
5567+ . map ( |o| o. string ( ) )
5568+ } ) ;
5569+ // Always update children_map so re-expanded dirs show fresh data
54855570 * cm. borrow_mut ( ) = new_map;
5571+ if !root_changed {
5572+ syncing. set ( false ) ;
5573+ return ;
5574+ }
54865575 rs. remove_all ( ) ;
54875576 for i in 0 ..new_store. n_items ( ) {
54885577 if let Some ( obj) = new_store. item ( i) {
@@ -5504,22 +5593,34 @@ fn build_dir_window(
55045593 }
55055594 }
55065595
5507- // Restore selection by rel_path
5508- let restore_sel = | sel : & SingleSelection , target : & Option < String > | {
5509- if let Some ( ref rel ) = * target {
5510- for i in 0 ..tm . n_items ( ) {
5511- if let Some ( row ) = tm . item ( i ) . and_then ( |o| o . downcast :: < TreeListRow > ( ) . ok ( ) )
5512- && let Some ( obj ) = row . item ( ) . and_downcast :: < StringObject > ( )
5513- && decode_rel_path ( & obj. string ( ) ) == rel . as_str ( )
5514- {
5515- sel . set_selected ( i ) ;
5516- return ;
5517- }
5596+ // Restore selection on both panes
5597+ let n = tm . n_items ( ) ;
5598+ let mut final_pos = saved_pos . min ( if n > 0 { n - 1 } else { 0 } ) ;
5599+ if let Some ( ref rel ) = saved_rel {
5600+ for i in 0 ..n {
5601+ if let Some ( row ) = tm . item ( i ) . and_then ( |o| o . downcast :: < TreeListRow > ( ) . ok ( ) )
5602+ && let Some ( obj) = row . item ( ) . and_downcast :: < StringObject > ( )
5603+ && decode_rel_path ( & obj . string ( ) ) == rel . as_str ( )
5604+ {
5605+ final_pos = i ;
5606+ break ;
55185607 }
55195608 }
5520- } ;
5521- restore_sel ( & ls, & left_rel) ;
5522- restore_sel ( & rsel, & right_rel) ;
5609+ }
5610+ if n > 0 {
5611+ sel. set_selected ( final_pos) ;
5612+ }
5613+ syncing. set ( false ) ;
5614+
5615+ // Defer scroll_to until after GTK has laid out the new rows,
5616+ // otherwise the ColumnView hasn't realized the items yet.
5617+ let lv2 = lv. clone ( ) ;
5618+ let rv2 = rv. clone ( ) ;
5619+ gtk4:: glib:: idle_add_local_once ( move || {
5620+ let flags = gtk4:: ListScrollFlags :: FOCUS ;
5621+ lv2. scroll_to ( final_pos, None , flags, None ) ;
5622+ rv2. scroll_to ( final_pos, None , flags, None ) ;
5623+ } ) ;
55235624 }
55245625 } ;
55255626
@@ -5536,14 +5637,11 @@ fn build_dir_window(
55365637 } ) ;
55375638 }
55385639
5539- // Helper: get selected row's encoded data from the focused pane
5640+ // Helper: get selected row's encoded data
55405641 let get_selected_row = {
55415642 let tm = tree_model. clone ( ) ;
5542- let ls = left_sel. clone ( ) ;
5543- let rs = right_sel. clone ( ) ;
5544- let fl = focused_left. clone ( ) ;
5643+ let sel = dir_sel. clone ( ) ;
55455644 move || -> Option < String > {
5546- let sel = if fl. get ( ) { & ls } else { & rs } ;
55475645 let pos = sel. selected ( ) ;
55485646 let item = tm. item ( pos) ?;
55495647 let row = item. downcast :: < TreeListRow > ( ) . ok ( ) ?;
@@ -5871,61 +5969,26 @@ fn build_dir_window(
58715969 }
58725970
58735971 // Poll for filesystem changes and reload
5874- let cm_reload = children_map. clone ( ) ;
5875- let rs_reload = root_store. clone ( ) ;
58765972 let tabs_reload = open_tabs. clone ( ) ;
58775973 let ld_reload = left_dir. clone ( ) ;
58785974 let rd_reload = right_dir. clone ( ) ;
5879- let st_reload = settings. clone ( ) ;
5880- gtk4:: glib:: timeout_add_local ( Duration :: from_millis ( 500 ) , move || {
5881- let mut changed = false ;
5882- while fs_rx. try_recv ( ) . is_ok ( ) {
5883- changed = true ;
5884- }
5885- if changed && !SAVING . load ( std:: sync:: atomic:: Ordering :: Relaxed ) {
5886- // Build new tree into a temporary map (not inside borrow_mut)
5887- let mut new_map = HashMap :: new ( ) ;
5888- let ( new_store, _) = scan_level (
5889- Path :: new ( ld_reload. borrow ( ) . as_str ( ) ) ,
5890- Path :: new ( rd_reload. borrow ( ) . as_str ( ) ) ,
5891- "" ,
5892- & mut new_map,
5893- & st_reload. borrow ( ) . dir_filters ,
5894- ) ;
5895- // Only rebuild the tree if root-level data actually changed.
5896- // remove_all()+re-append destroys expansion and selection state,
5897- // so we skip it when the scan results are identical.
5898- let root_changed = new_store. n_items ( ) != rs_reload. n_items ( )
5899- || ( 0 ..new_store. n_items ( ) ) . any ( |i| {
5900- new_store
5901- . item ( i)
5902- . and_downcast :: < StringObject > ( )
5903- . map ( |o| o. string ( ) )
5904- != rs_reload
5905- . item ( i)
5906- . and_downcast :: < StringObject > ( )
5907- . map ( |o| o. string ( ) )
5908- } ) ;
5909- // Always update children_map so re-expanded dirs show fresh data.
5910- * cm_reload. borrow_mut ( ) = new_map;
5911- if root_changed {
5912- // Drop children_map borrow before touching the store.
5913- // Appending to root_store triggers TreeListModel's create_func which
5914- // borrows children_map immutably — so we must not hold a mutable borrow.
5915- rs_reload. remove_all ( ) ;
5916- for i in 0 ..new_store. n_items ( ) {
5917- if let Some ( obj) = new_store. item ( i) {
5918- rs_reload. append ( & obj. downcast :: < StringObject > ( ) . unwrap ( ) ) ;
5919- }
5920- }
5975+ {
5976+ let reload = reload_dir. clone ( ) ;
5977+ gtk4:: glib:: timeout_add_local ( Duration :: from_millis ( 500 ) , move || {
5978+ let mut changed = false ;
5979+ while fs_rx. try_recv ( ) . is_ok ( ) {
5980+ changed = true ;
59215981 }
5922- // Reload open file tabs
5923- for tab in tabs_reload. borrow ( ) . iter ( ) {
5924- reload_file_tab ( tab, & ld_reload. borrow ( ) , & rd_reload. borrow ( ) ) ;
5982+ if changed && !SAVING . load ( std:: sync:: atomic:: Ordering :: Relaxed ) {
5983+ reload ( ) ;
5984+ // Reload open file tabs
5985+ for tab in tabs_reload. borrow ( ) . iter ( ) {
5986+ reload_file_tab ( tab, & ld_reload. borrow ( ) , & rd_reload. borrow ( ) ) ;
5987+ }
59255988 }
5926- }
5927- gtk4 :: glib :: ControlFlow :: Continue
5928- } ) ;
5989+ gtk4 :: glib :: ControlFlow :: Continue
5990+ } ) ;
5991+ }
59295992
59305993 // Window title
59315994 let left_name = Path :: new ( left_dir. borrow ( ) . as_str ( ) )
@@ -5962,6 +6025,7 @@ fn build_dir_window(
59626025 win_actions. add_action ( & action) ;
59636026 }
59646027 window. insert_action_group ( "win" , Some ( & win_actions) ) ;
6028+ window. insert_action_group ( "dir" , Some ( & dir_action_group) ) ;
59656029
59666030 // Register keyboard accelerators
59676031 if let Some ( gtk_app) = window. application ( ) {
@@ -5982,4 +6046,5 @@ fn build_dir_window(
59826046 }
59836047
59846048 window. present ( ) ;
6049+ left_view. grab_focus ( ) ;
59856050}
0 commit comments