settings.rs (9171B)
1 use crate::config::{AiProvider, DaveSettings}; 2 use crate::ui::keybind_hint::keybind_hint; 3 4 /// Tracks the state of the settings panel 5 pub struct DaveSettingsPanel { 6 /// Whether the panel is currently open 7 open: bool, 8 /// Working copy of settings being edited 9 editing: DaveSettings, 10 /// Custom model input (when user wants to type a model not in the list) 11 custom_model: String, 12 /// Whether to use custom model input 13 use_custom_model: bool, 14 } 15 16 /// Actions that can result from the settings panel 17 #[derive(Debug)] 18 pub enum SettingsPanelAction { 19 /// User saved the settings 20 Save(DaveSettings), 21 /// User cancelled the settings panel 22 Cancel, 23 } 24 25 impl Default for DaveSettingsPanel { 26 fn default() -> Self { 27 Self::new() 28 } 29 } 30 31 impl DaveSettingsPanel { 32 pub fn new() -> Self { 33 DaveSettingsPanel { 34 open: false, 35 editing: DaveSettings::default(), 36 custom_model: String::new(), 37 use_custom_model: false, 38 } 39 } 40 41 pub fn is_open(&self) -> bool { 42 self.open 43 } 44 45 /// Open the panel with a copy of current settings to edit 46 pub fn open(&mut self, current: &DaveSettings) { 47 self.editing = current.clone(); 48 self.custom_model = current.model.clone(); 49 // Check if current model is in the available list 50 self.use_custom_model = !current 51 .provider 52 .available_models() 53 .contains(¤t.model.as_str()); 54 self.open = true; 55 } 56 57 pub fn close(&mut self) { 58 self.open = false; 59 } 60 61 /// Prepare editing state for overlay mode 62 pub fn prepare_edit(&mut self, current: &DaveSettings) { 63 if !self.open { 64 self.editing = current.clone(); 65 self.custom_model = current.model.clone(); 66 self.use_custom_model = !current 67 .provider 68 .available_models() 69 .contains(¤t.model.as_str()); 70 self.open = true; 71 } 72 } 73 74 /// Render settings as a full-panel overlay (replaces the main content) 75 pub fn overlay_ui( 76 &mut self, 77 ui: &mut egui::Ui, 78 current: &DaveSettings, 79 ) -> Option<SettingsPanelAction> { 80 // Initialize editing state if not already set 81 self.prepare_edit(current); 82 83 let mut action: Option<SettingsPanelAction> = None; 84 let is_narrow = notedeck::ui::is_narrow(ui.ctx()); 85 let ctrl_held = ui.input(|i| i.modifiers.ctrl); 86 87 // Handle Ctrl+S to save 88 if ui.input(|i| i.modifiers.ctrl && i.key_pressed(egui::Key::S)) { 89 action = Some(SettingsPanelAction::Save(self.editing.clone())); 90 } 91 92 // Full panel frame with padding 93 egui::Frame::new() 94 .fill(ui.visuals().panel_fill) 95 .inner_margin(egui::Margin::symmetric(if is_narrow { 16 } else { 40 }, 20)) 96 .show(ui, |ui| { 97 // Header with back button 98 ui.horizontal(|ui| { 99 if ui.button("< Back").clicked() { 100 action = Some(SettingsPanelAction::Cancel); 101 } 102 if ctrl_held { 103 keybind_hint(ui, "Esc"); 104 } 105 ui.add_space(16.0); 106 ui.heading("Settings"); 107 }); 108 109 ui.add_space(24.0); 110 111 // Centered content container (max width for readability on desktop) 112 let max_content_width = if is_narrow { 113 ui.available_width() 114 } else { 115 500.0 116 }; 117 ui.allocate_ui_with_layout( 118 egui::vec2(max_content_width, ui.available_height()), 119 egui::Layout::top_down(egui::Align::LEFT), 120 |ui| { 121 self.settings_form(ui); 122 123 ui.add_space(24.0); 124 125 // Action buttons with keyboard hints 126 ui.horizontal(|ui| { 127 if ui.button("Save").clicked() { 128 action = Some(SettingsPanelAction::Save(self.editing.clone())); 129 } 130 if ctrl_held { 131 keybind_hint(ui, "S"); 132 } 133 ui.add_space(8.0); 134 if ui.button("Cancel").clicked() { 135 action = Some(SettingsPanelAction::Cancel); 136 } 137 if ctrl_held { 138 keybind_hint(ui, "Esc"); 139 } 140 }); 141 }, 142 ); 143 }); 144 145 // Handle Escape key 146 if ui 147 .ctx() 148 .input_mut(|i| i.consume_key(egui::Modifiers::NONE, egui::Key::Escape)) 149 { 150 action = Some(SettingsPanelAction::Cancel); 151 } 152 153 if action.is_some() { 154 self.close(); 155 } 156 157 action 158 } 159 160 /// Render the settings form content (shared between overlay and window modes) 161 fn settings_form(&mut self, ui: &mut egui::Ui) { 162 egui::Grid::new("settings_grid") 163 .num_columns(2) 164 .spacing([10.0, 12.0]) 165 .show(ui, |ui| { 166 // Provider dropdown 167 ui.label("Provider:"); 168 let prev_provider = self.editing.provider; 169 egui::ComboBox::from_id_salt("provider_combo") 170 .selected_text(self.editing.provider.name()) 171 .show_ui(ui, |ui| { 172 for provider in AiProvider::ALL { 173 ui.selectable_value( 174 &mut self.editing.provider, 175 provider, 176 provider.name(), 177 ); 178 } 179 }); 180 ui.end_row(); 181 182 // If provider changed, reset to provider defaults 183 if self.editing.provider != prev_provider { 184 self.editing.model = self.editing.provider.default_model().to_string(); 185 self.editing.endpoint = self 186 .editing 187 .provider 188 .default_endpoint() 189 .map(|s| s.to_string()); 190 self.custom_model = self.editing.model.clone(); 191 self.use_custom_model = false; 192 } 193 194 // Model selection 195 ui.label("Model:"); 196 ui.vertical(|ui| { 197 // Checkbox for custom model 198 ui.checkbox(&mut self.use_custom_model, "Custom model"); 199 200 if self.use_custom_model { 201 // Custom text input 202 let response = ui.text_edit_singleline(&mut self.custom_model); 203 if response.changed() { 204 self.editing.model = self.custom_model.clone(); 205 } 206 } else { 207 // Dropdown with available models 208 egui::ComboBox::from_id_salt("model_combo") 209 .selected_text(&self.editing.model) 210 .show_ui(ui, |ui| { 211 for model in self.editing.provider.available_models() { 212 ui.selectable_value( 213 &mut self.editing.model, 214 model.to_string(), 215 *model, 216 ); 217 } 218 }); 219 } 220 }); 221 ui.end_row(); 222 223 // Endpoint field 224 ui.label("Endpoint:"); 225 let mut endpoint_str = self.editing.endpoint.clone().unwrap_or_default(); 226 if ui.text_edit_singleline(&mut endpoint_str).changed() { 227 self.editing.endpoint = if endpoint_str.is_empty() { 228 None 229 } else { 230 Some(endpoint_str) 231 }; 232 } 233 ui.end_row(); 234 235 // API Key field (only shown when required) 236 if self.editing.provider.requires_api_key() { 237 ui.label("API Key:"); 238 let mut key_str = self.editing.api_key.clone().unwrap_or_default(); 239 if ui 240 .add(egui::TextEdit::singleline(&mut key_str).password(true)) 241 .changed() 242 { 243 self.editing.api_key = if key_str.is_empty() { 244 None 245 } else { 246 Some(key_str) 247 }; 248 } 249 ui.end_row(); 250 } 251 }); 252 } 253 }