zng_wgt_settings/
view_fn.rs

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    /// Category in a category list.
32    pub static CATEGORY_ITEM_FN_VAR: WidgetFn<CategoryItemArgs> = WidgetFn::new(default_category_item_fn);
33    /// Categories list.
34    pub static CATEGORIES_LIST_FN_VAR: WidgetFn<CategoriesListArgs> = WidgetFn::new(default_categories_list_fn);
35    /// Category header on the settings list.
36    pub static CATEGORY_HEADER_FN_VAR: WidgetFn<CategoryHeaderArgs> = WidgetFn::new(default_category_header_fn);
37    /// Setting item.
38    pub static SETTING_FN_VAR: WidgetFn<SettingArgs> = WidgetFn::new(default_setting_fn);
39    /// Settings list for a category.
40    pub static SETTINGS_FN_VAR: WidgetFn<SettingsArgs> = WidgetFn::new(default_settings_fn);
41    /// Settings search area.
42    pub static SETTINGS_SEARCH_FN_VAR: WidgetFn<SettingsSearchArgs> = WidgetFn::new(default_settings_search_fn);
43    /// Editor layout.
44    pub static PANEL_FN_VAR: WidgetFn<PanelArgs> = WidgetFn::new(default_panel_fn);
45}
46
47/// Widget function that converts [`CategoryItemArgs`] to a category item on a category list.
48///
49/// Sets the [`CATEGORY_ITEM_FN_VAR`].
50#[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/// Widget function that converts [`CategoriesListArgs`] to a category list.
56///
57/// Sets the [`CATEGORIES_LIST_FN_VAR`].
58#[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/// Widget function that converts [`CategoryHeaderArgs`] to a category settings header.
64///
65/// Sets the [`CATEGORY_HEADER_FN_VAR`].
66#[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/// Widget function that converts [`SettingArgs`] to a setting editor entry on a settings list.
72///
73/// Note that the widget must set [`setting`] or some features will not work.
74///
75/// Sets the [`SETTING_FN_VAR`].
76///
77/// [`setting`]: fn@setting
78#[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/// Widget function that converts [`SettingsArgs`] to a settings list.
84///
85/// Sets the [`SETTINGS_FN_VAR`].
86#[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/// Widget function that converts [`SettingsSearchArgs`] to a search box.
92///
93/// Sets the [`SETTINGS_SEARCH_FN_VAR`].
94#[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/// Widget that defines the editor layout, bringing together the other component widgets.
100#[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
105/// Default category item view.
106///
107/// See [`CATEGORY_ITEM_FN_VAR`] for more details.
108pub 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
115/// Default category item view.
116///
117/// See [`CATEGORY_HEADER_FN_VAR`] for more details.
118pub 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
126/// Default categories list view on `actual_width > 400`.
127///
128/// See [`CATEGORIES_LIST_FN_VAR`] for more details.
129pub 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
165/// Default categories list view on `actual_width <= 400`.
166pub 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
186/// Default setting item view.
187pub 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
214/// Reset button used by [`default_setting_fn`].
215pub 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        // l10n-# tip on hover of the reset arrow button
228        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
239/// Default settings for a category view.
240pub 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
257/// Default settings search box.
258pub 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
270/// Default editor layout on `actual_width > 400`.
271pub 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
288/// Default editor layout on `actual_width <= 400`.
289pub 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/// Arguments for a widget function that makes a category item for a categories list.
300#[non_exhaustive]
301pub struct CategoryItemArgs {
302    /// Index on the list.
303    pub index: usize,
304    /// The category.
305    pub category: Category,
306}
307impl CategoryItemArgs {
308    /// New args.
309    pub fn new(index: usize, category: Category) -> Self {
310        Self { index, category }
311    }
312}
313
314/// Arguments for a widget function that makes a category header in a settings list.
315#[non_exhaustive]
316pub struct CategoryHeaderArgs {
317    /// The category.
318    pub category: Category,
319}
320impl CategoryHeaderArgs {
321    /// New args.
322    pub fn new(category: Category) -> Self {
323        Self { category }
324    }
325}
326
327/// Arguments for a widget function that makes a list of category items that can be selected.
328///
329/// The selected category variable is in [`SETTINGS.editor_selected_category`](SettingsCtxExt::editor_selected_category).
330#[non_exhaustive]
331pub struct CategoriesListArgs {
332    /// The item views.
333    pub items: UiVec,
334}
335impl CategoriesListArgs {
336    /// New args.
337    pub fn new(items: UiVec) -> Self {
338        Self { items }
339    }
340}
341
342/// Arguments for a widget function that makes a setting container.
343#[non_exhaustive]
344pub struct SettingArgs {
345    /// Index of the setting on the list.
346    pub index: usize,
347    /// The setting.
348    pub setting: Setting,
349    /// The setting value editor.
350    pub editor: UiNode,
351}
352impl SettingArgs {
353    /// New args.
354    pub fn new(index: usize, setting: Setting, editor: UiNode) -> Self {
355        Self { index, setting, editor }
356    }
357}
358
359/// Arguments for a widget function that makes a settings for a category list.
360#[non_exhaustive]
361pub struct SettingsArgs {
362    /// The category header.
363    pub header: UiNode,
364    /// The items.
365    pub items: UiVec,
366}
367impl SettingsArgs {
368    /// New args.
369    pub fn new(header: UiNode, items: UiVec) -> Self {
370        Self { header, items }
371    }
372}
373
374/// Arguments for a search box widget.
375///
376/// The search variable is in [`SETTINGS.editor_search`](SettingsCtxExt::editor_search).
377#[non_exhaustive]
378pub struct SettingsSearchArgs {
379    /// If any settings matched the current search.
380    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/// Arguments for the entire settings editor layout.
391#[non_exhaustive]
392pub struct PanelArgs {
393    /// Search box widget.
394    pub search: UiNode,
395    /// Categories widget.
396    pub categories: UiNode,
397    /// Settings widget.
398    pub settings: UiNode,
399
400    /// If any settings matched the current search.
401    pub has_results: Var<bool>,
402}
403impl PanelArgs {
404    /// New args.
405    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
415/// Extends [`SettingBuilder`] to set custom editor metadata.
416pub trait SettingBuilderEditorExt {
417    /// Custom editor for the setting.
418    ///
419    /// If no editor is set the [`EDITORS`] service is used to instantiate the editor.
420    fn editor_fn(&mut self, editor: WidgetFn<Setting>) -> &mut Self;
421}
422
423/// Extends [`Setting`] to get custom editor metadata.
424pub trait SettingEditorExt {
425    /// Custom editor for the setting.
426    fn editor_fn(&self) -> Option<WidgetFn<Setting>>;
427
428    /// Instantiate editor.
429    ///
430    /// If no editor is set the [`EDITORS`] service is used to instantiate the editor.
431    fn editor(&self) -> UiNode;
432}
433
434/// Extends [`WidgetInfo`] to provide the setting config key for setting widgets.
435pub trait WidgetInfoSettingExt {
436    /// Gets the setting config key, if this widget represents a setting item.
437    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
472/// Extends [`SETTINGS`] to provide contextual information in an editor.
473pub trait SettingsCtxExt {
474    /// Gets a read-write context var that tracks the search text.
475    fn editor_search(&self) -> ContextVar<Txt>;
476
477    /// Gets a read-write context var that tracks the selected category.
478    fn editor_selected_category(&self) -> ContextVar<CategoryId>;
479
480    /// Gets a read-only context var that tracks the current editor data state.
481    fn editor_state(&self) -> Var<Option<SettingsEditorState>>;
482
483    /// Gets a read-only context var that tracks the [`Setting`] entry the widget is inside, or will be.
484    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/// Identifies the [`setting_fn`] widget.
512///
513/// [`setting_fn`]: fn@setting_fn
514#[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/// Represents the current settings data.
527///
528/// Use [`SETTINGS.editor_state`] to get.
529///
530/// [`SETTINGS.editor_state`]: SettingsCtxExt::editor_state
531#[derive(PartialEq, Debug, Clone)]
532#[non_exhaustive]
533pub struct SettingsEditorState {
534    /// The actual text searched.
535    pub clean_search: Txt,
536    /// Categories list.
537    pub categories: Vec<Category>,
538    /// Selected category.
539    pub selected_cat: Category,
540    /// Settings for the selected category that match the search.
541    pub selected_settings: Vec<Setting>,
542    /// Top search match.
543    pub top_match: ConfigKey,
544}