settings.rs (9110B)
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.ctx().input(|i| i.key_pressed(egui::Key::Escape)) { 147 action = Some(SettingsPanelAction::Cancel); 148 } 149 150 if action.is_some() { 151 self.close(); 152 } 153 154 action 155 } 156 157 /// Render the settings form content (shared between overlay and window modes) 158 fn settings_form(&mut self, ui: &mut egui::Ui) { 159 egui::Grid::new("settings_grid") 160 .num_columns(2) 161 .spacing([10.0, 12.0]) 162 .show(ui, |ui| { 163 // Provider dropdown 164 ui.label("Provider:"); 165 let prev_provider = self.editing.provider; 166 egui::ComboBox::from_id_salt("provider_combo") 167 .selected_text(self.editing.provider.name()) 168 .show_ui(ui, |ui| { 169 for provider in AiProvider::ALL { 170 ui.selectable_value( 171 &mut self.editing.provider, 172 provider, 173 provider.name(), 174 ); 175 } 176 }); 177 ui.end_row(); 178 179 // If provider changed, reset to provider defaults 180 if self.editing.provider != prev_provider { 181 self.editing.model = self.editing.provider.default_model().to_string(); 182 self.editing.endpoint = self 183 .editing 184 .provider 185 .default_endpoint() 186 .map(|s| s.to_string()); 187 self.custom_model = self.editing.model.clone(); 188 self.use_custom_model = false; 189 } 190 191 // Model selection 192 ui.label("Model:"); 193 ui.vertical(|ui| { 194 // Checkbox for custom model 195 ui.checkbox(&mut self.use_custom_model, "Custom model"); 196 197 if self.use_custom_model { 198 // Custom text input 199 let response = ui.text_edit_singleline(&mut self.custom_model); 200 if response.changed() { 201 self.editing.model = self.custom_model.clone(); 202 } 203 } else { 204 // Dropdown with available models 205 egui::ComboBox::from_id_salt("model_combo") 206 .selected_text(&self.editing.model) 207 .show_ui(ui, |ui| { 208 for model in self.editing.provider.available_models() { 209 ui.selectable_value( 210 &mut self.editing.model, 211 model.to_string(), 212 *model, 213 ); 214 } 215 }); 216 } 217 }); 218 ui.end_row(); 219 220 // Endpoint field 221 ui.label("Endpoint:"); 222 let mut endpoint_str = self.editing.endpoint.clone().unwrap_or_default(); 223 if ui.text_edit_singleline(&mut endpoint_str).changed() { 224 self.editing.endpoint = if endpoint_str.is_empty() { 225 None 226 } else { 227 Some(endpoint_str) 228 }; 229 } 230 ui.end_row(); 231 232 // API Key field (only shown when required) 233 if self.editing.provider.requires_api_key() { 234 ui.label("API Key:"); 235 let mut key_str = self.editing.api_key.clone().unwrap_or_default(); 236 if ui 237 .add(egui::TextEdit::singleline(&mut key_str).password(true)) 238 .changed() 239 { 240 self.editing.api_key = if key_str.is_empty() { 241 None 242 } else { 243 Some(key_str) 244 }; 245 } 246 ui.end_row(); 247 } 248 }); 249 } 250 }