1use zng_app::{
2 static_id,
3 widget::{
4 node::{UiNode, UiVec},
5 property,
6 },
7};
8use zng_ext_config::{
9 ConfigKey,
10 settings::{Category, CategoryId, SETTINGS, Setting, SettingBuilder},
11};
12use zng_ext_font::FontWeight;
13use zng_ext_l10n::l10n;
14use zng_var::ContextInitHandle;
15use zng_wgt::{EDITORS, ICONS, Wgt, WidgetFn, node::with_context_var, prelude::*};
16use zng_wgt_container::Container;
17use zng_wgt_filter::opacity;
18use zng_wgt_markdown::Markdown;
19use zng_wgt_rule_line::{hr::Hr, vr::Vr};
20use zng_wgt_scroll::{Scroll, ScrollMode};
21use zng_wgt_stack::{Stack, StackDirection};
22use zng_wgt_style::Style;
23use zng_wgt_text::Text;
24use zng_wgt_text_input::TextInput;
25use zng_wgt_toggle::{Selector, Toggle};
26use zng_wgt_tooltip::{Tip, tooltip};
27
28use crate::SettingsEditor;
29
30context_var! {
31 pub static CATEGORY_ITEM_FN_VAR: WidgetFn<CategoryItemArgs> = WidgetFn::new(default_category_item_fn);
33 pub static CATEGORIES_LIST_FN_VAR: WidgetFn<CategoriesListArgs> = WidgetFn::new(default_categories_list_fn);
35 pub static CATEGORY_HEADER_FN_VAR: WidgetFn<CategoryHeaderArgs> = WidgetFn::new(default_category_header_fn);
37 pub static SETTING_FN_VAR: WidgetFn<SettingArgs> = WidgetFn::new(default_setting_fn);
39 pub static SETTINGS_FN_VAR: WidgetFn<SettingsArgs> = WidgetFn::new(default_settings_fn);
41 pub static SETTINGS_SEARCH_FN_VAR: WidgetFn<SettingsSearchArgs> = WidgetFn::new(default_settings_search_fn);
43 pub static PANEL_FN_VAR: WidgetFn<PanelArgs> = WidgetFn::new(default_panel_fn);
45}
46
47#[property(CONTEXT, default(CATEGORY_ITEM_FN_VAR), widget_impl(SettingsEditor))]
51pub fn category_item_fn(child: impl IntoUiNode, wgt_fn: impl IntoVar<WidgetFn<CategoryItemArgs>>) -> UiNode {
52 with_context_var(child, CATEGORY_ITEM_FN_VAR, wgt_fn)
53}
54
55#[property(CONTEXT, default(CATEGORIES_LIST_FN_VAR), widget_impl(SettingsEditor))]
59pub fn categories_list_fn(child: impl IntoUiNode, wgt_fn: impl IntoVar<WidgetFn<CategoriesListArgs>>) -> UiNode {
60 with_context_var(child, CATEGORIES_LIST_FN_VAR, wgt_fn)
61}
62
63#[property(CONTEXT, default(CATEGORY_HEADER_FN_VAR), widget_impl(SettingsEditor))]
67pub fn category_header_fn(child: impl IntoUiNode, wgt_fn: impl IntoVar<WidgetFn<CategoryHeaderArgs>>) -> UiNode {
68 with_context_var(child, CATEGORY_HEADER_FN_VAR, wgt_fn)
69}
70
71#[property(CONTEXT, default(SETTING_FN_VAR), widget_impl(SettingsEditor))]
79pub fn setting_fn(child: impl IntoUiNode, wgt_fn: impl IntoVar<WidgetFn<SettingArgs>>) -> UiNode {
80 with_context_var(child, SETTING_FN_VAR, wgt_fn)
81}
82
83#[property(CONTEXT, default(SETTINGS_FN_VAR), widget_impl(SettingsEditor))]
87pub fn settings_fn(child: impl IntoUiNode, wgt_fn: impl IntoVar<WidgetFn<SettingsArgs>>) -> UiNode {
88 with_context_var(child, SETTINGS_FN_VAR, wgt_fn)
89}
90
91#[property(CONTEXT, default(SETTINGS_SEARCH_FN_VAR), widget_impl(SettingsEditor))]
95pub fn settings_search_fn(child: impl IntoUiNode, wgt_fn: impl IntoVar<WidgetFn<SettingsSearchArgs>>) -> UiNode {
96 with_context_var(child, SETTINGS_SEARCH_FN_VAR, wgt_fn)
97}
98
99#[property(CONTEXT, default(PANEL_FN_VAR), widget_impl(SettingsEditor))]
101pub fn panel_fn(child: impl IntoUiNode, wgt_fn: impl IntoVar<WidgetFn<PanelArgs>>) -> UiNode {
102 with_context_var(child, PANEL_FN_VAR, wgt_fn)
103}
104
105pub fn default_category_item_fn(args: CategoryItemArgs) -> UiNode {
109 Toggle! {
110 child = Text!(args.category.name().clone());
111 value::<CategoryId> = args.category.id().clone();
112 }
113}
114
115pub fn default_category_header_fn(args: CategoryHeaderArgs) -> UiNode {
119 Text! {
120 txt = args.category.name().clone();
121 font_size = 1.5.em();
122 zng_wgt::margin = (10, 10, 10, 28);
123 }
124}
125
126pub fn default_categories_list_fn(args: CategoriesListArgs) -> UiNode {
130 Container! {
131 child = categories_list(args.items.into_node());
132 child_end = Vr!(zng_wgt::margin = 0);
133 }
134}
135fn categories_list(items: UiNode) -> UiNode {
136 let list = Stack! {
137 zng_wgt_toggle::selector = Selector::single(SETTINGS.editor_selected_category());
138 direction = StackDirection::top_to_bottom();
139 children = items;
140 zng_wgt_toggle::style_fn = Style! {
141 replace = true;
142 opacity = 70.pct();
143 zng_wgt_size_offset::height = 2.em();
144 zng_wgt_container::child_align = Align::START;
145 zng_wgt_input::cursor = zng_wgt_input::CursorIcon::Pointer;
146
147 when *#zng_wgt_input::is_cap_hovered {
148 zng_wgt_text::font_weight = FontWeight::MEDIUM;
149 }
150
151 when *#zng_wgt_toggle::is_checked {
152 zng_wgt_text::font_weight = FontWeight::BOLD;
153 opacity = 100.pct();
154 }
155 };
156 };
157 Scroll! {
158 mode = ScrollMode::VERTICAL;
159 child_align = Align::FILL_TOP;
160 padding = (10, 20);
161 child = list;
162 }
163}
164
165pub fn default_categories_list_mobile_fn(args: CategoriesListArgs) -> UiNode {
167 let items = ArcNode::new(args.items);
168 Toggle! {
169 zng_wgt::margin = 4;
170 style_fn = zng_wgt_toggle::ComboStyle!();
171 child = Text! {
172 txt =
173 SETTINGS
174 .editor_state()
175 .flat_map(|e| e.as_ref().unwrap().selected_cat.name().clone()),
176 ;
177 font_weight = FontWeight::BOLD;
178 zng_wgt_container::padding = 5;
179 };
180 checked_popup = wgt_fn!(|_| zng_wgt_layer::popup::Popup! {
181 child = categories_list(items.take_on_init());
182 });
183 }
184}
185
186pub fn default_setting_fn(args: SettingArgs) -> UiNode {
188 let s = args.setting;
189 let name = s.name().clone();
190 let description = s.description().clone();
191 Container! {
192 setting = s.clone();
193
194 zng_wgt_input::focus::focus_scope = true;
195 zng_wgt_input::focus::focus_scope_behavior = zng_ext_input::focus::FocusScopeOnFocus::FirstDescendant;
196
197 child_spacing = (4, 5);
198 child_start = reset_button(s.can_reset(), hn!(|_| s.reset()));
199 child_top = Container! {
200 child_top = Text! {
201 txt = name;
202 font_weight = FontWeight::BOLD;
203 };
204 child_spacing = 4;
205 child = Markdown! {
206 txt = description;
207 opacity = 70.pct();
208 };
209 };
210 child = args.editor;
211 }
212}
213
214pub fn reset_button(can_reset: Var<bool>, reset: Handler<zng_wgt_input::gesture::ClickArgs>) -> UiNode {
216 Wgt! {
217 zng_wgt::align = Align::TOP;
218 zng_wgt::visibility = can_reset.map(|c| match c {
219 true => Visibility::Visible,
220 false => Visibility::Hidden,
221 });
222 zng_wgt_input::gesture::on_click = reset;
223
224 zng_wgt_fill::background = ICONS.req_or(["settings-reset", "settings-backup-restore"], || Text!("R"));
225 zng_wgt_size_offset::size = 18;
226
227 tooltip = Tip!(Text!(l10n!("reset", "Reset to default")));
229
230 zng_wgt_input::focus::tab_index = zng_ext_input::focus::TabIndex::SKIP;
231
232 opacity = 70.pct();
233 when *#zng_wgt_input::is_cap_hovered {
234 opacity = 100.pct();
235 }
236 }
237}
238
239pub fn default_settings_fn(args: SettingsArgs) -> UiNode {
241 Container! {
242 child_spacing = 5;
243 child_top = args.header;
244 child = Scroll! {
245 mode = ScrollMode::VERTICAL;
246 padding = (0, 20, 20, 10);
247 child_align = Align::FILL_TOP;
248 child = Stack! {
249 direction = StackDirection::top_to_bottom();
250 spacing = 10;
251 children = args.items;
252 };
253 };
254 }
255}
256
257pub fn default_settings_search_fn(_: SettingsSearchArgs) -> UiNode {
259 Container! {
260 child = TextInput! {
261 txt = SETTINGS.editor_search();
262 style_fn = zng_wgt_text_input::SearchStyle!();
263 zng_wgt_input::focus::focus_shortcut = [shortcut![CTRL + 'F'], shortcut![Find]];
264 placeholder_txt = l10n!("search.placeholder", "search settings ({$shortcut})", shortcut = "Ctrl+F");
265 };
266 child_bottom = Hr!(zng_wgt::margin = (10, 10, 0, 10));
267 }
268}
269
270pub fn default_panel_fn(args: PanelArgs) -> UiNode {
272 Container! {
273 child_top = args.search;
274 child = Container! {
275 child_start = args.categories;
276 child = args.settings;
277 };
278 when !*#{args.has_results} {
279 child = Text! {
280 txt = l10n!("search.no_results", "No settings found");
281 txt_align = Align::TOP;
282 zng_wgt::margin = 10;
283 };
284 }
285 }
286}
287
288pub fn default_panel_mobile_fn(args: PanelArgs) -> UiNode {
290 Container! {
291 child_top = args.search;
292 child = Container! {
293 child_top = args.categories;
294 child = args.settings;
295 };
296 }
297}
298
299#[non_exhaustive]
301pub struct CategoryItemArgs {
302 pub index: usize,
304 pub category: Category,
306}
307impl CategoryItemArgs {
308 pub fn new(index: usize, category: Category) -> Self {
310 Self { index, category }
311 }
312}
313
314#[non_exhaustive]
316pub struct CategoryHeaderArgs {
317 pub category: Category,
319}
320impl CategoryHeaderArgs {
321 pub fn new(category: Category) -> Self {
323 Self { category }
324 }
325}
326
327#[non_exhaustive]
331pub struct CategoriesListArgs {
332 pub items: UiVec,
334}
335impl CategoriesListArgs {
336 pub fn new(items: UiVec) -> Self {
338 Self { items }
339 }
340}
341
342#[non_exhaustive]
344pub struct SettingArgs {
345 pub index: usize,
347 pub setting: Setting,
349 pub editor: UiNode,
351}
352impl SettingArgs {
353 pub fn new(index: usize, setting: Setting, editor: UiNode) -> Self {
355 Self { index, setting, editor }
356 }
357}
358
359#[non_exhaustive]
361pub struct SettingsArgs {
362 pub header: UiNode,
364 pub items: UiVec,
366}
367impl SettingsArgs {
368 pub fn new(header: UiNode, items: UiVec) -> Self {
370 Self { header, items }
371 }
372}
373
374#[non_exhaustive]
378pub struct SettingsSearchArgs {
379 pub has_results: Var<bool>,
381}
382impl Default for SettingsSearchArgs {
383 fn default() -> Self {
384 Self {
385 has_results: true.into_var(),
386 }
387 }
388}
389
390#[non_exhaustive]
392pub struct PanelArgs {
393 pub search: UiNode,
395 pub categories: UiNode,
397 pub settings: UiNode,
399
400 pub has_results: Var<bool>,
402}
403impl PanelArgs {
404 pub fn new(search: UiNode, categories: UiNode, settings: UiNode) -> Self {
406 Self {
407 search,
408 categories,
409 settings,
410 has_results: true.into_var(),
411 }
412 }
413}
414
415pub trait SettingBuilderEditorExt {
417 fn editor_fn(&mut self, editor: WidgetFn<Setting>) -> &mut Self;
421}
422
423pub trait SettingEditorExt {
425 fn editor_fn(&self) -> Option<WidgetFn<Setting>>;
427
428 fn editor(&self) -> UiNode;
432}
433
434pub trait WidgetInfoSettingExt {
436 fn setting_key(&self) -> Option<ConfigKey>;
438}
439
440static_id! {
441 static ref CUSTOM_EDITOR_ID: StateId<WidgetFn<Setting>>;
442 static ref SETTING_KEY_ID: StateId<ConfigKey>;
443}
444
445impl SettingBuilderEditorExt for SettingBuilder<'_> {
446 fn editor_fn(&mut self, editor: WidgetFn<Setting>) -> &mut Self {
447 self.set(*CUSTOM_EDITOR_ID, editor)
448 }
449}
450
451impl SettingEditorExt for Setting {
452 fn editor_fn(&self) -> Option<WidgetFn<Setting>> {
453 self.meta().get_clone(*CUSTOM_EDITOR_ID)
454 }
455
456 fn editor(&self) -> UiNode {
457 match self.editor_fn() {
458 Some(f) => f(self.clone()),
459 None => EDITOR_SETTING_VAR.with_context_var(ContextInitHandle::current(), Some(self.clone()), || {
460 EDITORS.get(self.value().clone())
461 }),
462 }
463 }
464}
465
466impl WidgetInfoSettingExt for WidgetInfo {
467 fn setting_key(&self) -> Option<ConfigKey> {
468 self.meta().get_clone(*SETTING_KEY_ID)
469 }
470}
471
472pub trait SettingsCtxExt {
474 fn editor_search(&self) -> ContextVar<Txt>;
476
477 fn editor_selected_category(&self) -> ContextVar<CategoryId>;
479
480 fn editor_state(&self) -> Var<Option<SettingsEditorState>>;
482
483 fn editor_setting(&self) -> Var<Option<Setting>>;
485}
486impl SettingsCtxExt for SETTINGS {
487 fn editor_search(&self) -> ContextVar<Txt> {
488 EDITOR_SEARCH_VAR
489 }
490
491 fn editor_selected_category(&self) -> ContextVar<CategoryId> {
492 EDITOR_SELECTED_CATEGORY_VAR
493 }
494
495 fn editor_state(&self) -> Var<Option<SettingsEditorState>> {
496 EDITOR_STATE_VAR.read_only()
497 }
498
499 fn editor_setting(&self) -> Var<Option<Setting>> {
500 EDITOR_SETTING_VAR.read_only()
501 }
502}
503
504context_var! {
505 pub(crate) static EDITOR_SEARCH_VAR: Txt = Txt::from_static("");
506 pub(crate) static EDITOR_SELECTED_CATEGORY_VAR: CategoryId = CategoryId(Txt::from_static(""));
507 pub(crate) static EDITOR_STATE_VAR: Option<SettingsEditorState> = None;
508 static EDITOR_SETTING_VAR: Option<Setting> = None;
509}
510
511#[property(CONTEXT)]
515pub fn setting(child: impl IntoUiNode, setting: impl IntoValue<Setting>) -> UiNode {
516 let setting = setting.into();
517
518 let child = match_node(child, |_, op| {
519 if let UiNodeOp::Info { info } = op {
520 info.set_meta(*SETTING_KEY_ID, EDITOR_SETTING_VAR.with(|s| s.as_ref().unwrap().key().clone()));
521 }
522 });
523 with_context_var(child, EDITOR_SETTING_VAR, Some(setting))
524}
525
526#[derive(PartialEq, Debug, Clone)]
532#[non_exhaustive]
533pub struct SettingsEditorState {
534 pub clean_search: Txt,
536 pub categories: Vec<Category>,
538 pub selected_cat: Category,
540 pub selected_settings: Vec<Setting>,
542 pub top_match: ConfigKey,
544}