11use super :: WorktreeEntry ;
22
3+ /// Calculates initial scroll position to center default branch
4+ fn calculate_initial_scroll (
5+ selected_line : Option < usize > ,
6+ total_lines : usize ,
7+ visible_height : usize ,
8+ ) -> usize {
9+ let selected = selected_line. unwrap_or ( 0 ) ;
10+ let ideal_center = selected. saturating_sub ( visible_height / 2 ) ;
11+ let max_scroll = total_lines. saturating_sub ( visible_height) ;
12+ ideal_center. min ( max_scroll)
13+ }
14+
15+ /// Represents different types of lines in the scrollable branch list
16+ #[ derive( Debug , Clone , PartialEq ) ]
17+ pub ( crate ) enum LineType {
18+ /// A group header like "Branches" or "Worktrees"
19+ GroupHeader { title : String } ,
20+
21+ /// A selectable branch option
22+ BranchOption {
23+ /// Index into base_groups
24+ group_idx : usize ,
25+ /// Index into base_groups[group_idx].options
26+ option_idx : usize ,
27+ } ,
28+
29+ /// Empty spacing line between groups
30+ EmptyLine ,
31+ }
32+
333#[ derive( Clone , Copy , Debug , PartialEq , Eq ) ]
434pub ( crate ) enum CreateDialogFocus {
535 Name ,
@@ -28,6 +58,19 @@ pub(crate) struct CreateDialog {
2858 pub ( crate ) base_indices : Vec < ( usize , usize ) > ,
2959 pub ( crate ) base_selected : usize ,
3060 pub ( crate ) error : Option < String > ,
61+
62+ // Scroll state fields
63+ /// Flattened list of all renderable lines (headers + branches + spacing)
64+ pub ( crate ) flat_lines : Vec < LineType > ,
65+
66+ /// Index of the first visible line in the viewport
67+ pub ( crate ) scroll_offset : usize ,
68+
69+ /// Last known viewport height (for detecting resize)
70+ pub ( crate ) last_known_height : u16 ,
71+
72+ /// Last known content height (viewport minus indicators)
73+ pub ( crate ) last_known_content_height : usize ,
3174}
3275
3376impl CreateDialog {
@@ -107,6 +150,44 @@ impl CreateDialog {
107150 base_selected = 0 ;
108151 }
109152
153+ // Build flat_lines from groups
154+ let mut flat_lines = Vec :: new ( ) ;
155+ for ( group_idx, group) in groups. iter ( ) . enumerate ( ) {
156+ flat_lines. push ( LineType :: GroupHeader {
157+ title : group. title . clone ( ) ,
158+ } ) ;
159+
160+ for ( option_idx, _) in group. options . iter ( ) . enumerate ( ) {
161+ flat_lines. push ( LineType :: BranchOption {
162+ group_idx,
163+ option_idx,
164+ } ) ;
165+ }
166+
167+ // Add spacing between groups (but not after last)
168+ if group_idx < groups. len ( ) - 1 {
169+ flat_lines. push ( LineType :: EmptyLine ) ;
170+ }
171+ }
172+
173+ // Calculate initial scroll position to center default branch
174+ let selected_line = base_indices
175+ . get ( base_selected)
176+ . and_then ( |( target_group, target_option) | {
177+ flat_lines. iter ( ) . position ( |line| {
178+ matches ! (
179+ line,
180+ LineType :: BranchOption { group_idx, option_idx }
181+ if group_idx == target_group && option_idx == target_option
182+ )
183+ } )
184+ } ) ;
185+
186+ // Use reasonable default for initial visible height
187+ let initial_content_height = 6 ; // Conservative estimate
188+ let scroll_offset = calculate_initial_scroll ( selected_line, flat_lines. len ( ) , initial_content_height) ;
189+ let last_known_height = 0 ; // Will be set on first render
190+
110191 Self {
111192 name_input : String :: new ( ) ,
112193 focus : CreateDialogFocus :: Name ,
@@ -115,6 +196,10 @@ impl CreateDialog {
115196 base_indices,
116197 base_selected,
117198 error : None ,
199+ flat_lines,
200+ scroll_offset,
201+ last_known_height,
202+ last_known_content_height : initial_content_height,
118203 }
119204 }
120205
@@ -149,6 +234,53 @@ impl CreateDialog {
149234 let current = self . base_selected as isize ;
150235 let next = ( current + delta) . rem_euclid ( len) ;
151236 self . base_selected = next as usize ;
237+
238+ // Update scroll position to keep selection visible
239+ // Use last known content height from rendering
240+ self . ensure_selected_visible ( self . last_known_content_height ) ;
241+ }
242+
243+ pub ( crate ) fn find_selected_line ( & self ) -> Option < usize > {
244+ let ( target_group, target_option) = self . base_indices . get ( self . base_selected ) ?;
245+
246+ self . flat_lines . iter ( ) . position ( |line| {
247+ matches ! (
248+ line,
249+ LineType :: BranchOption { group_idx, option_idx }
250+ if group_idx == target_group && option_idx == target_option
251+ )
252+ } )
253+ }
254+
255+ pub ( crate ) fn ensure_selected_visible ( & mut self , visible_height : usize ) {
256+ const MARGIN : usize = 2 ;
257+
258+ // If viewport is too small, just ensure selection is in range
259+ if visible_height == 0 {
260+ return ;
261+ }
262+
263+ let Some ( selected_line) = self . find_selected_line ( ) else {
264+ return ;
265+ } ;
266+
267+ let max_scroll = self . flat_lines . len ( ) . saturating_sub ( visible_height) ;
268+
269+ // Calculate safe lower bound (handle case where visible_height < MARGIN)
270+ let viewport_end = self . scroll_offset + visible_height;
271+ let safe_bottom = viewport_end. saturating_sub ( MARGIN ) ;
272+
273+ // Scroll down if selection below viewport
274+ if selected_line >= safe_bottom {
275+ self . scroll_offset = ( selected_line + MARGIN + 1 )
276+ . saturating_sub ( visible_height)
277+ . min ( max_scroll) ;
278+ }
279+
280+ // Scroll up if selection above viewport
281+ if selected_line < self . scroll_offset + MARGIN {
282+ self . scroll_offset = selected_line. saturating_sub ( MARGIN ) ;
283+ }
152284 }
153285}
154286
@@ -161,6 +293,8 @@ pub(crate) struct CreateDialogView {
161293 pub ( crate ) base_selected : usize ,
162294 pub ( crate ) base_indices : Vec < ( usize , usize ) > ,
163295 pub ( crate ) error : Option < String > ,
296+ pub ( crate ) flat_lines : Vec < LineType > ,
297+ pub ( crate ) scroll_offset : usize ,
164298}
165299
166300impl From < & CreateDialog > for CreateDialogView {
@@ -173,6 +307,8 @@ impl From<&CreateDialog> for CreateDialogView {
173307 base_selected : dialog. base_selected ,
174308 base_indices : dialog. base_indices . clone ( ) ,
175309 error : dialog. error . clone ( ) ,
310+ flat_lines : dialog. flat_lines . clone ( ) ,
311+ scroll_offset : dialog. scroll_offset ,
176312 }
177313 }
178314}
0 commit comments