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 }
279}
280
281pub fn default_panel_mobile_fn(args: PanelArgs) -> UiNode {
283 Container! {
284 child_top = args.search;
285 child = Container! {
286 child_top = args.categories;
287 child = args.settings;
288 };
289 }
290}
291
292#[non_exhaustive]
294pub struct CategoryItemArgs {
295 pub index: usize,
297 pub category: Category,
299}
300impl CategoryItemArgs {
301 pub fn new(index: usize, category: Category) -> Self {
303 Self { index, category }
304 }
305}
306
307#[non_exhaustive]
309pub struct CategoryHeaderArgs {
310 pub category: Category,
312}
313impl CategoryHeaderArgs {
314 pub fn new(category: Category) -> Self {
316 Self { category }
317 }
318}
319
320#[non_exhaustive]
324pub struct CategoriesListArgs {
325 pub items: UiVec,
327}
328impl CategoriesListArgs {
329 pub fn new(items: UiVec) -> Self {
331 Self { items }
332 }
333}
334
335#[non_exhaustive]
337pub struct SettingArgs {
338 pub index: usize,
340 pub setting: Setting,
342 pub editor: UiNode,
344}
345impl SettingArgs {
346 pub fn new(index: usize, setting: Setting, editor: UiNode) -> Self {
348 Self { index, setting, editor }
349 }
350}
351
352#[non_exhaustive]
354pub struct SettingsArgs {
355 pub header: UiNode,
357 pub items: UiVec,
359}
360impl SettingsArgs {
361 pub fn new(header: UiNode, items: UiVec) -> Self {
363 Self { header, items }
364 }
365}
366
367#[derive(Default)]
371#[non_exhaustive]
372pub struct SettingsSearchArgs {}
373
374#[non_exhaustive]
376pub struct PanelArgs {
377 pub search: UiNode,
379 pub categories: UiNode,
381 pub settings: UiNode,
383}
384impl PanelArgs {
385 pub fn new(search: UiNode, categories: UiNode, settings: UiNode) -> Self {
387 Self {
388 search,
389 categories,
390 settings,
391 }
392 }
393}
394
395pub trait SettingBuilderEditorExt {
397 fn editor_fn(&mut self, editor: WidgetFn<Setting>) -> &mut Self;
401}
402
403pub trait SettingEditorExt {
405 fn editor_fn(&self) -> Option<WidgetFn<Setting>>;
407
408 fn editor(&self) -> UiNode;
412}
413
414pub trait WidgetInfoSettingExt {
416 fn setting_key(&self) -> Option<ConfigKey>;
418}
419
420static_id! {
421 static ref CUSTOM_EDITOR_ID: StateId<WidgetFn<Setting>>;
422 static ref SETTING_KEY_ID: StateId<ConfigKey>;
423}
424
425impl SettingBuilderEditorExt for SettingBuilder<'_> {
426 fn editor_fn(&mut self, editor: WidgetFn<Setting>) -> &mut Self {
427 self.set(*CUSTOM_EDITOR_ID, editor)
428 }
429}
430
431impl SettingEditorExt for Setting {
432 fn editor_fn(&self) -> Option<WidgetFn<Setting>> {
433 self.meta().get_clone(*CUSTOM_EDITOR_ID)
434 }
435
436 fn editor(&self) -> UiNode {
437 match self.editor_fn() {
438 Some(f) => f(self.clone()),
439 None => EDITOR_SETTING_VAR.with_context_var(ContextInitHandle::current(), Some(self.clone()), || {
440 EDITORS.get(self.value().clone())
441 }),
442 }
443 }
444}
445
446impl WidgetInfoSettingExt for WidgetInfo {
447 fn setting_key(&self) -> Option<ConfigKey> {
448 self.meta().get_clone(*SETTING_KEY_ID)
449 }
450}
451
452pub trait SettingsCtxExt {
454 fn editor_search(&self) -> ContextVar<Txt>;
456
457 fn editor_selected_category(&self) -> ContextVar<CategoryId>;
459
460 fn editor_state(&self) -> Var<Option<SettingsEditorState>>;
462
463 fn editor_setting(&self) -> Var<Option<Setting>>;
465}
466impl SettingsCtxExt for SETTINGS {
467 fn editor_search(&self) -> ContextVar<Txt> {
468 EDITOR_SEARCH_VAR
469 }
470
471 fn editor_selected_category(&self) -> ContextVar<CategoryId> {
472 EDITOR_SELECTED_CATEGORY_VAR
473 }
474
475 fn editor_state(&self) -> Var<Option<SettingsEditorState>> {
476 EDITOR_STATE_VAR.read_only()
477 }
478
479 fn editor_setting(&self) -> Var<Option<Setting>> {
480 EDITOR_SETTING_VAR.read_only()
481 }
482}
483
484context_var! {
485 pub(crate) static EDITOR_SEARCH_VAR: Txt = Txt::from_static("");
486 pub(crate) static EDITOR_SELECTED_CATEGORY_VAR: CategoryId = CategoryId(Txt::from_static(""));
487 pub(crate) static EDITOR_STATE_VAR: Option<SettingsEditorState> = None;
488 static EDITOR_SETTING_VAR: Option<Setting> = None;
489}
490
491#[property(CONTEXT)]
495pub fn setting(child: impl IntoUiNode, setting: impl IntoValue<Setting>) -> UiNode {
496 let setting = setting.into();
497
498 let child = match_node(child, |_, op| {
499 if let UiNodeOp::Info { info } = op {
500 info.set_meta(*SETTING_KEY_ID, EDITOR_SETTING_VAR.with(|s| s.as_ref().unwrap().key().clone()));
501 }
502 });
503 with_context_var(child, EDITOR_SETTING_VAR, Some(setting))
504}
505
506#[derive(PartialEq, Debug, Clone)]
512#[non_exhaustive]
513pub struct SettingsEditorState {
514 pub clean_search: Txt,
516 pub categories: Vec<Category>,
518 pub selected_cat: Category,
520 pub selected_settings: Vec<Setting>,
522 pub top_match: ConfigKey,
524}