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    }
279}
280
281/// Default editor layout on `actual_width <= 400`.
282pub 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/// Arguments for a widget function that makes a category item for a categories list.
293#[non_exhaustive]
294pub struct CategoryItemArgs {
295    /// Index on the list.
296    pub index: usize,
297    /// The category.
298    pub category: Category,
299}
300impl CategoryItemArgs {
301    /// New args.
302    pub fn new(index: usize, category: Category) -> Self {
303        Self { index, category }
304    }
305}
306
307/// Arguments for a widget function that makes a category header in a settings list.
308#[non_exhaustive]
309pub struct CategoryHeaderArgs {
310    /// The category.
311    pub category: Category,
312}
313impl CategoryHeaderArgs {
314    /// New args.
315    pub fn new(category: Category) -> Self {
316        Self { category }
317    }
318}
319
320/// Arguments for a widget function that makes a list of category items that can be selected.
321///
322/// The selected category variable is in [`SETTINGS.editor_selected_category`](SettingsCtxExt::editor_selected_category).
323#[non_exhaustive]
324pub struct CategoriesListArgs {
325    /// The item views.
326    pub items: UiVec,
327}
328impl CategoriesListArgs {
329    /// New args.
330    pub fn new(items: UiVec) -> Self {
331        Self { items }
332    }
333}
334
335/// Arguments for a widget function that makes a setting container.
336#[non_exhaustive]
337pub struct SettingArgs {
338    /// Index of the setting on the list.
339    pub index: usize,
340    /// The setting.
341    pub setting: Setting,
342    /// The setting value editor.
343    pub editor: UiNode,
344}
345impl SettingArgs {
346    /// New args.
347    pub fn new(index: usize, setting: Setting, editor: UiNode) -> Self {
348        Self { index, setting, editor }
349    }
350}
351
352/// Arguments for a widget function that makes a settings for a category list.
353#[non_exhaustive]
354pub struct SettingsArgs {
355    /// The category header.
356    pub header: UiNode,
357    /// The items.
358    pub items: UiVec,
359}
360impl SettingsArgs {
361    /// New args.
362    pub fn new(header: UiNode, items: UiVec) -> Self {
363        Self { header, items }
364    }
365}
366
367/// Arguments for a search box widget.
368///
369/// The search variable is in [`SETTINGS.editor_search`](SettingsCtxExt::editor_search).
370#[derive(Default)]
371#[non_exhaustive]
372pub struct SettingsSearchArgs {}
373
374/// Arguments for the entire settings editor layout.
375#[non_exhaustive]
376pub struct PanelArgs {
377    /// Search box widget.
378    pub search: UiNode,
379    /// Categories widget.
380    pub categories: UiNode,
381    /// Settings widget.
382    pub settings: UiNode,
383}
384impl PanelArgs {
385    /// New args.
386    pub fn new(search: UiNode, categories: UiNode, settings: UiNode) -> Self {
387        Self {
388            search,
389            categories,
390            settings,
391        }
392    }
393}
394
395/// Extends [`SettingBuilder`] to set custom editor metadata.
396pub trait SettingBuilderEditorExt {
397    /// Custom editor for the setting.
398    ///
399    /// If no editor is set the [`EDITORS`] service is used to instantiate the editor.
400    fn editor_fn(&mut self, editor: WidgetFn<Setting>) -> &mut Self;
401}
402
403/// Extends [`Setting`] to get custom editor metadata.
404pub trait SettingEditorExt {
405    /// Custom editor for the setting.
406    fn editor_fn(&self) -> Option<WidgetFn<Setting>>;
407
408    /// Instantiate editor.
409    ///
410    /// If no editor is set the [`EDITORS`] service is used to instantiate the editor.
411    fn editor(&self) -> UiNode;
412}
413
414/// Extends [`WidgetInfo`] to provide the setting config key for setting widgets.
415pub trait WidgetInfoSettingExt {
416    /// Gets the setting config key, if this widget represents a setting item.
417    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
452/// Extends [`SETTINGS`] to provide contextual information in an editor.
453pub trait SettingsCtxExt {
454    /// Gets a read-write context var that tracks the search text.
455    fn editor_search(&self) -> ContextVar<Txt>;
456
457    /// Gets a read-write context var that tracks the selected category.
458    fn editor_selected_category(&self) -> ContextVar<CategoryId>;
459
460    /// Gets a read-only context var that tracks the current editor data state.
461    fn editor_state(&self) -> Var<Option<SettingsEditorState>>;
462
463    /// Gets a read-only context var that tracks the [`Setting`] entry the widget is inside, or will be.
464    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/// Identifies the [`setting_fn`] widget.
492///
493/// [`setting_fn`]: fn@setting_fn
494#[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/// Represents the current settings data.
507///
508/// Use [`SETTINGS.editor_state`] to get.
509///
510/// [`SETTINGS.editor_state`]: SettingsCtxExt::editor_state
511#[derive(PartialEq, Debug, Clone)]
512#[non_exhaustive]
513pub struct SettingsEditorState {
514    /// The actual text searched.
515    pub clean_search: Txt,
516    /// Categories list.
517    pub categories: Vec<Category>,
518    /// Selected category.
519    pub selected_cat: Category,
520    /// Settings for the selected category that match the search.
521    pub selected_settings: Vec<Setting>,
522    /// Top search match.
523    pub top_match: ConfigKey,
524}