notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

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(&current.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(&current.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 }