@@ -64,6 +64,7 @@ enum InputMode {
6464 RepoPicker ,
6565 DiscardConfirm ,
6666 BranchSwitcher ,
67+ Settings ,
6768}
6869
6970struct State {
@@ -127,6 +128,9 @@ struct State {
127128 window_controls : Vec < WindowControlButton > ,
128129 toolbar_buttons : Vec < ToolbarButton > ,
129130
131+ // ── Settings modal ───────────────────────────────────
132+ settings_index : usize ,
133+
130134 // ── Panel split ratio ────────────────────────────────
131135 file_pane_ratio : f32 ,
132136 divider_dragging : bool ,
@@ -465,6 +469,7 @@ impl State {
465469 mouse_pos : PhysicalPosition :: new ( 0.0 , 0.0 ) ,
466470 window_controls : Vec :: new ( ) ,
467471 toolbar_buttons : Vec :: new ( ) ,
472+ settings_index : 0 ,
468473 file_pane_ratio : 0.30 ,
469474 divider_dragging : false ,
470475 zoom_level : 1.0 ,
@@ -651,6 +656,15 @@ impl State {
651656 self . set_status ( StatusKind :: Prompt , message) ;
652657 }
653658
659+ fn prompt_settings ( & mut self ) {
660+ self . input_mode = InputMode :: Settings ;
661+ self . settings_index = 0 ;
662+ self . set_status (
663+ StatusKind :: Prompt ,
664+ "Settings: Up/Down navigate Left/Right change Esc close" ,
665+ ) ;
666+ }
667+
654668 fn cancel_input_mode ( & mut self ) {
655669 self . input_mode = InputMode :: Normal ;
656670 self . commit_summary . clear ( ) ;
@@ -1047,6 +1061,90 @@ impl State {
10471061 }
10481062 }
10491063
1064+ /// Total number of rows in the settings modal.
1065+ const SETTINGS_COUNT : usize = 3 ;
1066+
1067+ fn handle_settings_input ( & mut self , key : & Key ) -> anyhow:: Result < bool > {
1068+ match key {
1069+ Key :: Named ( NamedKey :: Escape ) => {
1070+ self . cancel_input_mode ( ) ;
1071+ Ok ( true )
1072+ }
1073+ Key :: Named ( NamedKey :: ArrowUp ) => {
1074+ if self . settings_index > 0 {
1075+ self . settings_index -= 1 ;
1076+ }
1077+ Ok ( true )
1078+ }
1079+ Key :: Named ( NamedKey :: ArrowDown ) => {
1080+ if self . settings_index + 1 < Self :: SETTINGS_COUNT {
1081+ self . settings_index += 1 ;
1082+ }
1083+ Ok ( true )
1084+ }
1085+ Key :: Named ( NamedKey :: ArrowRight | NamedKey :: Enter ) => {
1086+ self . settings_adjust ( 1 ) ?;
1087+ Ok ( true )
1088+ }
1089+ Key :: Named ( NamedKey :: ArrowLeft ) => {
1090+ self . settings_adjust ( -1 ) ?;
1091+ Ok ( true )
1092+ }
1093+ _ => Ok ( false ) ,
1094+ }
1095+ }
1096+
1097+ /// Adjust the setting at `self.settings_index` by the given direction.
1098+ fn settings_adjust ( & mut self , direction : i32 ) -> anyhow:: Result < ( ) > {
1099+ match self . settings_index {
1100+ // 0 = Diff backend
1101+ 0 => {
1102+ match self . git . toggle_diff_backend ( ) {
1103+ Ok ( _) => {
1104+ self . refresh_document_from_git ( ) ?;
1105+ }
1106+ Err ( e) => {
1107+ self . set_status ( StatusKind :: Error , format ! ( "{e}" ) ) ;
1108+ }
1109+ }
1110+ }
1111+ // 1 = Pane ratio
1112+ 1 => {
1113+ let delta = direction as f32 * 0.05 ;
1114+ self . adjust_pane_ratio ( delta) ;
1115+ }
1116+ // 2 = Zoom level
1117+ 2 => {
1118+ let delta = direction as f32 * 0.10 ;
1119+ self . apply_zoom ( delta) ;
1120+ }
1121+ _ => { }
1122+ }
1123+ self . geometry_dirty = true ;
1124+ Ok ( ( ) )
1125+ }
1126+
1127+ /// Build the label and current value for each settings row.
1128+ fn settings_row_label ( & self , index : usize ) -> ( & ' static str , String ) {
1129+ match index {
1130+ 0 => (
1131+ "Diff backend" ,
1132+ self . git . diff_backend ( ) . label ( ) . to_string ( ) ,
1133+ ) ,
1134+ 1 => (
1135+ "Pane split" ,
1136+ format ! ( "{:.0}% / {:.0}%" ,
1137+ self . file_pane_ratio * 100.0 ,
1138+ ( 1.0 - self . file_pane_ratio) * 100.0 ) ,
1139+ ) ,
1140+ 2 => (
1141+ "Zoom level" ,
1142+ format ! ( "{:.0}%" , self . zoom_level * 100.0 ) ,
1143+ ) ,
1144+ _ => ( "" , String :: new ( ) ) ,
1145+ }
1146+ }
1147+
10501148 fn execute_action < F > ( & mut self , label : & str , action : F ) -> bool
10511149 where
10521150 F : FnOnce ( & mut Self ) -> anyhow:: Result < ( ) > ,
@@ -1239,6 +1337,9 @@ impl State {
12391337 InputMode :: BranchSwitcher => {
12401338 self . build_branch_switcher_overlay_geometry ( text_vertices, rect_instances)
12411339 }
1340+ InputMode :: Settings => {
1341+ self . build_settings_overlay_geometry ( text_vertices, rect_instances)
1342+ }
12421343 InputMode :: Normal => Ok ( ( ) ) ,
12431344 }
12441345 }
@@ -1643,6 +1744,115 @@ impl State {
16431744 Ok ( ( ) )
16441745 }
16451746
1747+ fn build_settings_overlay_geometry (
1748+ & mut self ,
1749+ text_vertices : & mut Vec < TextVertex > ,
1750+ rect_instances : & mut Vec < StyledRectInstance > ,
1751+ ) -> anyhow:: Result < ( ) > {
1752+ let row_count = Self :: SETTINGS_COUNT ;
1753+ let panel_h =
1754+ self . ui ( 92.0 ) + row_count as f32 * ( self . line_height + self . ui ( 6.0 ) ) ;
1755+ let panel = self . modal_panel_rect ( panel_h) ;
1756+ push_styled_rect (
1757+ rect_instances,
1758+ panel,
1759+ theme:: MODAL_BG_TOP ,
1760+ theme:: MODAL_BG_BOTTOM ,
1761+ theme:: MODAL_BORDER ,
1762+ [ 0.0 , 0.0 , 0.0 , 0.28 ] ,
1763+ self . ui ( 12.0 ) ,
1764+ 1.0 ,
1765+ 1.0 ,
1766+ self . ui ( 16.0 ) ,
1767+ [ 0.0 , self . ui ( 4.0 ) ] ,
1768+ self . ui ( 2.0 ) ,
1769+ ) ;
1770+
1771+ // Title
1772+ let mut x = panel[ 0 ] + self . ui ( 16.0 ) ;
1773+ let mut y = panel[ 1 ] + self . ui ( 18.0 ) ;
1774+ self . append_text_run (
1775+ text_vertices,
1776+ & mut x,
1777+ y + self . ascent ,
1778+ "Settings" ,
1779+ [ 0.92 , 0.96 , 1.0 , 1.0 ] ,
1780+ ) ?;
1781+
1782+ y += self . line_height * 1.4 ;
1783+ let row_x = panel[ 0 ] + self . ui ( 14.0 ) ;
1784+ let row_w = panel[ 2 ] - self . ui ( 28.0 ) ;
1785+
1786+ for idx in 0 ..row_count {
1787+ let row_y = y + idx as f32 * ( self . line_height + self . ui ( 6.0 ) ) ;
1788+ let selected = idx == self . settings_index ;
1789+
1790+ let ( fill_top, fill_bottom, stroke) = if selected {
1791+ (
1792+ [ 0.25 , 0.33 , 0.50 , 1.0 ] ,
1793+ [ 0.18 , 0.24 , 0.38 , 1.0 ] ,
1794+ [ 0.52 , 0.68 , 0.98 , 0.70 ] ,
1795+ )
1796+ } else {
1797+ (
1798+ [ 0.16 , 0.18 , 0.26 , 1.0 ] ,
1799+ [ 0.12 , 0.14 , 0.20 , 1.0 ] ,
1800+ [ 0.30 , 0.36 , 0.48 , 0.40 ] ,
1801+ )
1802+ } ;
1803+
1804+ push_styled_rect (
1805+ rect_instances,
1806+ [ row_x, row_y, row_w, self . line_height + self . ui ( 4.0 ) ] ,
1807+ fill_top,
1808+ fill_bottom,
1809+ stroke,
1810+ [ 0.0 , 0.0 , 0.0 , 0.0 ] ,
1811+ self . ui ( 8.0 ) ,
1812+ 1.0 ,
1813+ 1.0 ,
1814+ 0.0 ,
1815+ [ 0.0 , 0.0 ] ,
1816+ 0.0 ,
1817+ ) ;
1818+
1819+ let ( label, value) = self . settings_row_label ( idx) ;
1820+ let arrows = if selected { "< > " } else { " " } ;
1821+ let display = format ! ( "{label}: {arrows}{value}" ) ;
1822+
1823+ let text_color = if selected {
1824+ [ 1.0 , 1.0 , 1.0 , 1.0 ]
1825+ } else {
1826+ [ 0.86 , 0.90 , 0.96 , 1.0 ]
1827+ } ;
1828+
1829+ let mut tx = row_x + self . ui ( 10.0 ) ;
1830+ self . append_text_run (
1831+ text_vertices,
1832+ & mut tx,
1833+ row_y + self . ascent + 2.0 ,
1834+ & compact_status_message (
1835+ & display,
1836+ ( ( row_w - self . ui ( 20.0 ) ) / self . cell_width ) . max ( 1.0 ) as usize ,
1837+ ) ,
1838+ text_color,
1839+ ) ?;
1840+ }
1841+
1842+ // Hint bar
1843+ let mut hint_x = panel[ 0 ] + self . ui ( 16.0 ) ;
1844+ let hint_y = panel[ 1 ] + panel[ 3 ] - self . line_height + self . ui ( 4.0 ) ;
1845+ self . append_text_run (
1846+ text_vertices,
1847+ & mut hint_x,
1848+ hint_y + self . ascent ,
1849+ "Left/Right change Up/Down navigate Esc close" ,
1850+ [ 0.76 , 0.82 , 0.92 , 1.0 ] ,
1851+ ) ?;
1852+
1853+ Ok ( ( ) )
1854+ }
1855+
16461856 fn refresh_document_from_git ( & mut self ) -> anyhow:: Result < ( ) > {
16471857 let ( file_doc, meta, diff_doc) = self . git . build_split_documents ( ) ?;
16481858 let ( file_line_to_index, file_index_to_line) =
@@ -3060,6 +3270,22 @@ impl State {
30603270 Ok ( true )
30613271 }
30623272 } ,
3273+ "d" => {
3274+ Ok ( self . execute_action ( "Diff backend toggled" , |state| {
3275+ let backend = state. git . toggle_diff_backend ( ) ?;
3276+ state. refresh_document_from_git ( ) ?;
3277+ state. set_status (
3278+ StatusKind :: Success ,
3279+ format ! ( "Diff backend: {}" , backend. label( ) ) ,
3280+ ) ;
3281+ Ok ( ( ) )
3282+ } ) )
3283+ }
3284+ "," => {
3285+ self . prompt_settings ( ) ;
3286+ self . geometry_dirty = true ;
3287+ Ok ( true )
3288+ }
30633289 "r" => self . handle_toolbar_action ( ToolbarAction :: Refresh ) ,
30643290 "s" => self . handle_toolbar_action ( ToolbarAction :: Stage ) ,
30653291 "u" => self . handle_toolbar_action ( ToolbarAction :: Unstage ) ,
@@ -3317,6 +3543,9 @@ impl ApplicationHandler for App {
33173543 InputMode :: BranchSwitcher => {
33183544 st. handle_branch_switcher_input ( & event. logical_key )
33193545 }
3546+ InputMode :: Settings => {
3547+ st. handle_settings_input ( & event. logical_key )
3548+ }
33203549 InputMode :: Normal => Ok ( false ) ,
33213550 }
33223551 } else {
@@ -3350,6 +3579,7 @@ impl ApplicationHandler for App {
33503579 InputMode :: RepoPicker => "Repo switch failed" ,
33513580 InputMode :: DiscardConfirm => "Discard failed" ,
33523581 InputMode :: BranchSwitcher => "Branch switch failed" ,
3582+ InputMode :: Settings => "Settings failed" ,
33533583 InputMode :: Normal => "Input handling failed" ,
33543584 } ;
33553585 st. set_status ( StatusKind :: Error , format ! ( "{label}: {err}" ) ) ;
0 commit comments