@@ -8,6 +8,7 @@ use git2::{
88use ratatui:: {
99 Terminal ,
1010 backend:: Backend ,
11+ layout:: { Constraint , Direction , Layout } ,
1112 style:: { Color , Modifier , Style } ,
1213 text:: { Line , Span } ,
1314 widgets:: ListState ,
@@ -23,6 +24,69 @@ use super::{
2324} ;
2425use crate :: commands:: rm:: { LocalBranchStatus , RemoveOutcome } ;
2526
27+ #[ allow( dead_code) ]
28+ #[ derive( Clone , Copy , Debug , Eq , PartialEq ) ]
29+ pub ( crate ) enum ActionPanelOrientation {
30+ Horizontal ,
31+ Vertical ,
32+ }
33+
34+ #[ allow( dead_code) ]
35+ #[ derive( Clone , Debug ) ]
36+ pub ( crate ) struct ActionPanelState {
37+ pub orientation : ActionPanelOrientation ,
38+ pub selected_index : usize ,
39+ pub scroll_offset : usize ,
40+ }
41+
42+ impl ActionPanelState {
43+ pub fn new ( orientation : ActionPanelOrientation ) -> Self {
44+ Self {
45+ orientation,
46+ selected_index : 0 ,
47+ scroll_offset : 0 ,
48+ }
49+ }
50+
51+ pub fn vertical ( ) -> Self {
52+ Self :: new ( ActionPanelOrientation :: Vertical )
53+ }
54+
55+ pub fn ensure_visible ( & mut self , visible_rows : usize , total_items : usize ) {
56+ if total_items == 0 {
57+ self . selected_index = 0 ;
58+ self . scroll_offset = 0 ;
59+ return ;
60+ }
61+
62+ if self . selected_index >= total_items {
63+ self . selected_index = total_items. saturating_sub ( 1 ) ;
64+ }
65+
66+ if visible_rows == 0 {
67+ self . scroll_offset = 0 ;
68+ return ;
69+ }
70+
71+ if self . selected_index < self . scroll_offset {
72+ self . scroll_offset = self . selected_index ;
73+ } else {
74+ let viewport_end = self . scroll_offset + visible_rows;
75+ if self . selected_index >= viewport_end {
76+ self . scroll_offset = self
77+ . selected_index
78+ . saturating_add ( 1 )
79+ . saturating_sub ( visible_rows) ;
80+ }
81+ }
82+
83+ let max_offset = total_items. saturating_sub ( visible_rows) ;
84+ if self . scroll_offset > max_offset {
85+ self . scroll_offset = max_offset;
86+ }
87+ }
88+ }
89+
2690pub struct InteractiveCommand < B , E >
2791where
2892 B : Backend ,
3498 pub ( crate ) worktrees : Vec < WorktreeEntry > ,
3599 pub ( crate ) selected : Option < usize > ,
36100 pub ( crate ) focus : Focus ,
37- pub ( crate ) action_selected : usize ,
101+ pub ( crate ) action_panel : ActionPanelState ,
38102 pub ( crate ) global_action_selected : usize ,
39103 pub ( crate ) branches : Vec < String > ,
40104 pub ( crate ) default_branch : Option < String > ,
67131 worktrees,
68132 selected,
69133 focus : Focus :: Worktrees ,
70- action_selected : 0 ,
134+ action_panel : ActionPanelState :: vertical ( ) ,
71135 global_action_selected : 0 ,
72136 branches,
73137 default_branch,
@@ -275,7 +339,7 @@ where
275339 }
276340 }
277341 Focus :: Actions => {
278- let action = Action :: from_index ( self . action_selected ) ;
342+ let action = Action :: from_index ( self . action_panel . selected_index ) ;
279343 match action {
280344 Action :: Open => {
281345 if let Some ( entry) = self . current_entry ( ) {
@@ -864,9 +928,12 @@ where
864928
865929 fn move_action ( & mut self , delta : isize ) {
866930 let len = Action :: ALL . len ( ) as isize ;
867- let current = self . action_selected as isize ;
931+ let current = self . action_panel . selected_index as isize ;
868932 let next = ( current + delta) . rem_euclid ( len) ;
869- self . action_selected = next as usize ;
933+ self . action_panel . selected_index = next as usize ;
934+ let visible_rows = self . action_panel_visible_rows ( ) ;
935+ self . action_panel
936+ . ensure_visible ( visible_rows, Action :: ALL . len ( ) ) ;
870937 }
871938
872939 fn move_global_action ( & mut self , delta : isize ) {
@@ -879,6 +946,28 @@ where
879946 self . global_action_selected = next as usize ;
880947 }
881948
949+ fn action_panel_visible_rows ( & self ) -> usize {
950+ let Ok ( size) = self . terminal . size ( ) else {
951+ return Action :: ALL . len ( ) ;
952+ } ;
953+
954+ let columns = Layout :: default ( )
955+ . direction ( Direction :: Horizontal )
956+ . constraints ( [ Constraint :: Percentage ( 45 ) , Constraint :: Percentage ( 55 ) ] )
957+ . split ( size) ;
958+
959+ let panel_layout = Layout :: default ( )
960+ . direction ( Direction :: Vertical )
961+ . constraints ( [
962+ Constraint :: Min ( 5 ) ,
963+ Constraint :: Length ( ( Action :: ALL . len ( ) as u16 ) . saturating_add ( 2 ) . max ( 3 ) ) ,
964+ Constraint :: Length ( 3 ) ,
965+ ] )
966+ . split ( columns[ 1 ] ) ;
967+
968+ usize:: from ( panel_layout[ 1 ] . height . saturating_sub ( 2 ) )
969+ }
970+
882971 fn current_entry ( & self ) -> Option < & WorktreeEntry > {
883972 self . selected . and_then ( |idx| self . worktrees . get ( idx) )
884973 }
@@ -938,7 +1027,7 @@ where
9381027 items,
9391028 detail,
9401029 self . focus ,
941- self . action_selected ,
1030+ self . action_panel . clone ( ) ,
9421031 self . global_action_selected ,
9431032 self . status . clone ( ) ,
9441033 dialog,
0 commit comments