git.lucas.co / cce-system-interface
system settings
git clone https://git.lucas.co/cce-system-interface.git

src/pages/browser.rs (12.4K)

  1 //! Browser (cce-browser) settings: homepage, search engine, download
  2 //! directory, history recording, navigation-bar position, page color
  3 //! scheme. Edits the
  4 //! browser's own app config (`~/.config/cce/cce-browser/config.kdl`,
  5 //! section "browser") — the browser reloads it when its window regains
  6 //! focus.
  7 
  8 use std::fs;
  9 
 10 use cce_ui::layout::{LayoutStrategy, PageLayoutBuilder};
 11 use cce_ui::widget::input::{Dropdown, Toggle};
 12 use cce_ui::widget::{TextBox, WidgetHost};
 13 
 14 use crate::app::{AppAction, PageContent, SectionContextExt};
 15 use crate::pages::AppPage;
 16 
 17 /// config key, menu label — the browser maps the key onto a query URL.
 18 pub const SEARCH_ENGINES: [(&str, &str); 4] = [
 19     ("duckduckgo", "DuckDuckGo"),
 20     ("google", "Google"),
 21     ("bing", "Bing"),
 22     ("wikipedia", "Wikipedia"),
 23 ];
 24 
 25 /// config key, menu label — the window edge the browser's floating
 26 /// navigation bar is anchored to.
 27 pub const BAR_POSITIONS: [(&str, &str); 2] = [("top", "Top"), ("bottom", "Bottom")];
 28 
 29 /// config key, menu label — the first two are reported to pages as
 30 /// `prefers-color-scheme`, so sites that ship a dark stylesheet use it.
 31 /// "Force Dark" additionally inverts the page, for sites that ship no dark
 32 /// theme; it fights their palette, so it is a deliberate last resort.
 33 pub const COLOR_SCHEMES: [(&str, &str); 3] =
 34     [("dark", "Dark"), ("light", "Light"), ("force-dark", "Force Dark")];
 35 
 36 const DEFAULT_HOMEPAGE: &str = "https://servo.org";
 37 
 38 #[derive(Debug, Clone)]
 39 pub struct BrowserConfig {
 40     pub homepage: String,
 41     pub search: String,
 42     pub download_dir: String,
 43     pub history: bool,
 44     pub bar_position: String,
 45     pub color_scheme: String,
 46 }
 47 
 48 pub struct BrowserState {
 49     pub loaded: bool,
 50     pub homepage: String,
 51     pub search: String,
 52     pub download_dir: String,
 53     pub history: bool,
 54     pub bar_position: String,
 55     pub color_scheme: String,
 56     pub homepage_box: cce_ui::widget::Adapted<TextBox>,
 57     pub search_menu: cce_ui::widget::Adapted<Dropdown>,
 58     pub bar_position_menu: cce_ui::widget::Adapted<Dropdown>,
 59     pub color_scheme_menu: cce_ui::widget::Adapted<Dropdown>,
 60     pub download_dir_box: cce_ui::widget::Adapted<TextBox>,
 61     pub history_toggle: cce_ui::widget::Adapted<Toggle>,
 62 }
 63 
 64 impl Default for BrowserState {
 65     fn default() -> Self {
 66         let config = read_browser_config();
 67         let mut homepage_box = TextBox::new(config.homepage.clone())
 68             .with_draw_bg_border(true)
 69             .with_label("Homepage");
 70         homepage_box.edit_buffer = config.homepage.clone();
 71         let mut download_dir_box = TextBox::new(config.download_dir.clone())
 72             .with_draw_bg_border(true)
 73             .with_label("Download Directory");
 74         download_dir_box.edit_buffer = config.download_dir.clone();
 75         Self {
 76             loaded: true,
 77             homepage: config.homepage,
 78             search: config.search.clone(),
 79             download_dir: config.download_dir,
 80             history: config.history,
 81             bar_position: config.bar_position.clone(),
 82             color_scheme: config.color_scheme.clone(),
 83             homepage_box,
 84             search_menu: Dropdown::new(
 85                 SEARCH_ENGINES.iter().map(|(_, label)| label.to_string()).collect(),
 86                 search_index(&config.search),
 87             )
 88             .with_label("Search Engine"),
 89             bar_position_menu: Dropdown::new(
 90                 BAR_POSITIONS.iter().map(|(_, label)| label.to_string()).collect(),
 91                 bar_position_index(&config.bar_position),
 92             )
 93             .with_label("Navigation Bar Position"),
 94             color_scheme_menu: Dropdown::new(
 95                 COLOR_SCHEMES.iter().map(|(_, label)| label.to_string()).collect(),
 96                 color_scheme_index(&config.color_scheme),
 97             )
 98             .with_label("Page Color Scheme"),
 99             download_dir_box,
100             history_toggle: Toggle::new().with_label("Record History"),
101         }
102     }
103 }
104 
105 fn search_index(key: &str) -> usize {
106     SEARCH_ENGINES.iter().position(|(k, _)| *k == key).unwrap_or(0)
107 }
108 
109 fn bar_position_index(key: &str) -> usize {
110     BAR_POSITIONS.iter().position(|(k, _)| *k == key).unwrap_or(0)
111 }
112 
113 fn color_scheme_index(key: &str) -> usize {
114     COLOR_SCHEMES.iter().position(|(k, _)| *k == key).unwrap_or(0)
115 }
116 
117 #[derive(Debug, Clone)]
118 pub enum BrowserMessage {
119     SetSearch(String),
120     SetBarPosition(String),
121     SetColorScheme(String),
122     ToggleHistory,
123     /// Commit the homepage / download-dir text fields.
124     Apply,
125     Refreshed(BrowserConfig),
126 }
127 
128 /// A TextBox's live contents: the in-progress edit buffer while focused,
129 /// the committed text otherwise (the recurring TextBox landmine).
130 fn live_text(tb: &cce_ui::widget::Adapted<TextBox>) -> String {
131     if tb.editing {
132         tb.edit_buffer.trim().to_string()
133     } else {
134         tb.text.trim().to_string()
135     }
136 }
137 
138 pub fn update(state: &mut BrowserState, msg: BrowserMessage) {
139     match msg {
140         BrowserMessage::SetSearch(key) => {
141             state.search = key.clone();
142             write_config_value("search", &key);
143         }
144         BrowserMessage::SetBarPosition(key) => {
145             state.bar_position = key.clone();
146             write_config_value("bar-position", &key);
147         }
148         BrowserMessage::SetColorScheme(key) => {
149             state.color_scheme = key.clone();
150             write_config_value("color-scheme", &key);
151         }
152         BrowserMessage::ToggleHistory => {
153             state.history = !state.history;
154             write_config_value("history", &state.history.to_string());
155         }
156         BrowserMessage::Apply => {
157             state.homepage = live_text(&state.homepage_box);
158             if state.homepage.is_empty() {
159                 state.homepage = DEFAULT_HOMEPAGE.to_string();
160                 state.homepage_box.text = state.homepage.clone();
161                 state.homepage_box.edit_buffer = state.homepage.clone();
162             }
163             state.download_dir = live_text(&state.download_dir_box);
164             write_config_value("homepage", &state.homepage);
165             write_config_value("download-dir", &state.download_dir);
166         }
167         BrowserMessage::Refreshed(new) => {
168             state.loaded = true;
169             state.search = new.search;
170             state.history = new.history;
171             state.bar_position = new.bar_position;
172             state.color_scheme = new.color_scheme;
173             // Don't clobber fields mid-edit with watcher refreshes.
174             if !state.homepage_box.editing && state.homepage != new.homepage {
175                 state.homepage = new.homepage.clone();
176                 state.homepage_box.text = new.homepage.clone();
177                 state.homepage_box.edit_buffer = new.homepage;
178             }
179             if !state.download_dir_box.editing && state.download_dir != new.download_dir {
180                 state.download_dir = new.download_dir.clone();
181                 state.download_dir_box.text = new.download_dir.clone();
182                 state.download_dir_box.edit_buffer = new.download_dir;
183             }
184         }
185     }
186 }
187 
188 fn get_config_path() -> String {
189     cce_ui::config::get_app_config_path("cce-browser")
190         .to_string_lossy()
191         .into_owned()
192 }
193 
194 pub fn read_browser_config() -> BrowserConfig {
195     let content = fs::read_to_string(get_config_path()).unwrap_or_default();
196     let val = cce_ui::config::parse_kdl_to_json(&content);
197     BrowserConfig {
198         homepage: val["browser"]["homepage"]
199             .as_str()
200             .unwrap_or(DEFAULT_HOMEPAGE)
201             .to_string(),
202         search: val["browser"]["search"].as_str().unwrap_or("duckduckgo").to_string(),
203         download_dir: val["browser"]["download-dir"].as_str().unwrap_or("").to_string(),
204         history: val["browser"]["history"].as_bool().unwrap_or(true),
205         bar_position: val["browser"]["bar-position"].as_str().unwrap_or("top").to_string(),
206         color_scheme: val["browser"]["color-scheme"].as_str().unwrap_or("dark").to_string(),
207     }
208 }
209 
210 fn write_config_value(key: &str, value: &str) {
211     let path = get_config_path();
212     // The per-app config dir may not exist yet.
213     if let Some(dir) = std::path::Path::new(&path).parent() {
214         let _ = fs::create_dir_all(dir);
215     }
216     // Section nesting comes from the dotted key path (the section arg of
217     // write_config_value is vestigial).
218     cce_ui::config::write_config_value(&path, &format!("browser.{key}"), value, "browser");
219 }
220 
221 impl AppPage for BrowserState {
222     // Sections: [Browser Settings]
223     fn section_widgets(&mut self) -> Vec<Vec<cce_ui::widget::WidgetId>> {
224         vec![vec![
225             self.homepage_box.id(),
226             self.search_menu.id(),
227             self.bar_position_menu.id(),
228             self.color_scheme_menu.id(),
229             self.download_dir_box.id(),
230             self.history_toggle.id(),
231         ]]
232     }
233 
234     fn view(
235         &mut self,
236         cx: f32,
237         cy: f32,
238         cw: f32,
239         ch: f32,
240         _root_focused: bool,
241         sec_focused: &[bool],
242         layout: &mut dyn LayoutStrategy,
243         ctx: &mut cce_ui::context::UiContext,
244     ) -> PageContent {
245         let mut final_pc = PageContent::new();
246         let sec_w = 320.0f32;
247         let mut builder = PageLayoutBuilder::new(layout, cx, cy, cw, ch, sec_w).with_section_count(1);
248 
249         builder.add_section(&mut final_pc, "Browser Settings", sec_focused.first().copied().unwrap_or(false), |sec| {
250             let mut stack = sec.vstack(cce_ui::layout::plate_gap());
251             let sec_w = stack.context.cw;
252             let row_w = sec_w - 28.0;
253 
254             self.homepage_box.set_row_rect(stack.context.left + 14.0, row_w);
255             stack.add_widget(&mut self.homepage_box, row_w, 44.0, ctx);
256 
257             self.search_menu.selected = search_index(&self.search);
258             self.search_menu.set_row_rect(stack.context.left + 14.0, row_w);
259             stack.add_widget(&mut self.search_menu, row_w, 44.0, ctx);
260 
261             self.bar_position_menu.selected = bar_position_index(&self.bar_position);
262             self.bar_position_menu.set_row_rect(stack.context.left + 14.0, row_w);
263             stack.add_widget(&mut self.bar_position_menu, row_w, 44.0, ctx);
264 
265             self.color_scheme_menu.selected = color_scheme_index(&self.color_scheme);
266             self.color_scheme_menu.set_row_rect(stack.context.left + 14.0, row_w);
267             stack.add_widget(&mut self.color_scheme_menu, row_w, 44.0, ctx);
268 
269             self.download_dir_box.set_row_rect(stack.context.left + 14.0, row_w);
270             stack.add_widget(&mut self.download_dir_box, row_w, 44.0, ctx);
271 
272             self.history_toggle.set_toggled(self.history);
273             stack.add_widget(&mut self.history_toggle, row_w, cce_ui::layout::toggle_height(), ctx);
274 
275             let btn_h = 32.0;
276             stack.add_row(1, 0.0, btn_h, |ctx, _, x, w| {
277                 ctx.button(
278                     "Apply",
279                     x,
280                     ctx.ay(),
281                     w,
282                     btn_h,
283                     [0.20, 0.40, 0.65, 1.0],
284                     [0.28, 0.50, 0.78, 1.0],
285                     [1.0, 1.0, 1.0, 1.0],
286                     AppAction::Browser(BrowserMessage::Apply),
287                 );
288             });
289         });
290 
291         final_pc
292     }
293 
294     fn propagate_widget_changes(&mut self, actions: &mut Vec<AppAction>) {
295         if self.search_menu.take_change() {
296             let key = SEARCH_ENGINES
297                 .get(self.search_menu.selected)
298                 .map(|(k, _)| k.to_string())
299                 .unwrap_or_else(|| "duckduckgo".to_string());
300             actions.push(AppAction::Browser(BrowserMessage::SetSearch(key)));
301         }
302         if self.bar_position_menu.take_change() {
303             let key = BAR_POSITIONS
304                 .get(self.bar_position_menu.selected)
305                 .map(|(k, _)| k.to_string())
306                 .unwrap_or_else(|| "top".to_string());
307             actions.push(AppAction::Browser(BrowserMessage::SetBarPosition(key)));
308         }
309         if self.color_scheme_menu.take_change() {
310             let key = COLOR_SCHEMES
311                 .get(self.color_scheme_menu.selected)
312                 .map(|(k, _)| k.to_string())
313                 .unwrap_or_else(|| "dark".to_string());
314             actions.push(AppAction::Browser(BrowserMessage::SetColorScheme(key)));
315         }
316         if self.history_toggle.take_change() {
317             actions.push(AppAction::Browser(BrowserMessage::ToggleHistory));
318         }
319         // Enter in either text field commits both.
320         if self.homepage_box.take_change() || self.download_dir_box.take_change() {
321             actions.push(AppAction::Browser(BrowserMessage::Apply));
322         }
323     }
324 }