notedeck

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

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