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 }