zng_wgt_settings/
view_fn.rs

1use zng_app::{
2    static_id,
3    widget::{
4        node::{BoxedUiNode, 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, ReadOnlyContextVar};
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, disabled_tooltip, 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 UiNode, wgt_fn: impl IntoVar<WidgetFn<CategoryItemArgs>>) -> impl 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 UiNode, wgt_fn: impl IntoVar<WidgetFn<CategoriesListArgs>>) -> impl 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 UiNode, wgt_fn: impl IntoVar<WidgetFn<CategoryHeaderArgs>>) -> impl 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 UiNode, wgt_fn: impl IntoVar<WidgetFn<SettingArgs>>) -> impl 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 UiNode, wgt_fn: impl IntoVar<WidgetFn<SettingsArgs>>) -> impl 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 UiNode, wgt_fn: impl IntoVar<WidgetFn<SettingsSearchArgs>>) -> impl 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 UiNode, wgt_fn: impl IntoVar<WidgetFn<PanelArgs>>) -> impl 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) -> impl 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) -> impl 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) -> impl UiNode {
130    Container! {
131        child = categories_list(args.items.boxed());
132        child_end = Vr!(zng_wgt::margin = 0), 0;
133    }
134}
135fn categories_list(items: BoxedUiNodeList) -> impl 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) -> impl UiNode {
167    let items = ArcNodeList::new(args.items);
168    Toggle! {
169        zng_wgt::margin = 4;
170        style_fn = zng_wgt_toggle::ComboStyle!();
171        child = Text! {
172            txt = SETTINGS
173                .editor_state()
174                .flat_map(|e| e.as_ref().unwrap().selected_cat.name().clone());
175            font_weight = FontWeight::BOLD;
176            zng_wgt_container::padding = 5;
177        };
178        checked_popup = wgt_fn!(|_| zng_wgt_layer::popup::Popup! {
179            child = categories_list(items.take_on_init().boxed());
180        });
181    }
182}
183
184/// Default setting item view.
185pub fn default_setting_fn(args: SettingArgs) -> impl UiNode {
186    let name = args.setting.name().clone();
187    let description = args.setting.description().clone();
188    let can_reset = args.setting.can_reset();
189    Container! {
190        setting = args.setting.clone();
191
192        zng_wgt_input::focus::focus_scope = true;
193        zng_wgt_input::focus::focus_scope_behavior = zng_ext_input::focus::FocusScopeOnFocus::FirstDescendant;
194
195        child_start = {
196            let s = args.setting;
197            Wgt! {
198                zng_wgt::align = Align::TOP;
199                zng_wgt::visibility = can_reset.map(|c| match c {
200                    true => Visibility::Visible,
201                    false => Visibility::Hidden,
202                });
203                zng_wgt_input::gesture::on_click = hn!(|_| {
204                    s.reset();
205                });
206
207                zng_wgt_fill::background = ICONS.req_or(["settings-reset", "settings-backup-restore"], || Text!("R"));
208                zng_wgt_size_offset::size = 18;
209
210                tooltip = Tip!(Text!("reset"));
211                disabled_tooltip = Tip!(Text!("is default"));
212
213                zng_wgt_input::focus::tab_index = zng_ext_input::focus::TabIndex::SKIP;
214
215                opacity = 70.pct();
216                when *#zng_wgt_input::is_cap_hovered {
217                    opacity = 100.pct();
218                }
219                when *#zng_wgt::is_disabled {
220                    opacity = 30.pct();
221                }
222            }
223        }, 4;
224        child_top = Container! {
225            child_top = Text! {
226                txt = name;
227                font_weight = FontWeight::BOLD;
228            }, 4;
229            child = Markdown! {
230                txt = description;
231                opacity = 70.pct();
232            };
233        }, 5;
234        child = args.editor;
235    }
236}
237
238/// Default settings for a category view.
239pub fn default_settings_fn(args: SettingsArgs) -> impl UiNode {
240    Container! {
241        child_top = args.header, 5;
242        child = Scroll! {
243            mode = ScrollMode::VERTICAL;
244            padding = (0, 20, 20, 10);
245            child_align = Align::FILL_TOP;
246            child = Stack! {
247                direction = StackDirection::top_to_bottom();
248                spacing = 10;
249                children = args.items;
250            };
251        };
252    }
253}
254
255/// Default settings search box.
256pub fn default_settings_search_fn(_: SettingsSearchArgs) -> impl UiNode {
257    Container! {
258        child = TextInput! {
259            txt = SETTINGS.editor_search();
260            style_fn = zng_wgt_text_input::SearchStyle!();
261            zng_wgt_input::focus::focus_shortcut = [shortcut![CTRL+'F'], shortcut![Find]];
262            placeholder_txt = l10n!("search.placeholder", "search settings ({$shortcut})", shortcut = "Ctrl+F");
263        };
264        child_bottom = Hr!(zng_wgt::margin = (10, 10, 0, 10)), 0;
265    }
266}
267
268/// Default editor layout on `actual_width > 400`.
269pub fn default_panel_fn(args: PanelArgs) -> impl UiNode {
270    Container! {
271        child_top = args.search, 0;
272        child = Container! {
273            child_start = args.categories, 0;
274            child = args.settings;
275        };
276    }
277}
278
279/// Default editor layout on `actual_width <= 400`.
280pub fn default_panel_mobile_fn(args: PanelArgs) -> impl UiNode {
281    Container! {
282        child_top = args.search, 0;
283        child = Container! {
284            child_top = args.categories, 0;
285            child = args.settings;
286        };
287    }
288}
289
290/// Arguments for a widget function that makes a category item for a categories list.
291pub struct CategoryItemArgs {
292    /// Index on the list.
293    pub index: usize,
294    /// The category.
295    pub category: Category,
296}
297
298/// Arguments for a widget function that makes a category header in a settings list.
299pub struct CategoryHeaderArgs {
300    /// The category.
301    pub category: Category,
302}
303
304/// Arguments for a widget function that makes a list of category items that can be selected.
305///
306/// The selected category variable is in [`SETTINGS.editor_selected_category`](SettingsCtxExt::editor_selected_category).
307pub struct CategoriesListArgs {
308    /// The item views.
309    pub items: UiVec,
310}
311
312/// Arguments for a widget function that makes a setting container.
313pub struct SettingArgs {
314    /// Index of the setting on the list.
315    pub index: usize,
316    /// The setting.
317    pub setting: Setting,
318    /// The setting value editor.
319    pub editor: BoxedUiNode,
320}
321
322/// Arguments for a widget function that makes a settings for a category list.
323pub struct SettingsArgs {
324    /// The category header.
325    pub header: BoxedUiNode,
326    /// The items.
327    pub items: UiVec,
328}
329
330/// Arguments for a search box widget.
331///
332/// The search variable is in [`SETTINGS.editor_search`](SettingsCtxExt::editor_search).
333pub struct SettingsSearchArgs {}
334
335/// Arguments for the entire settings editor layout.
336pub struct PanelArgs {
337    /// Search box widget.
338    pub search: BoxedUiNode,
339    /// Categories widget.
340    pub categories: BoxedUiNode,
341    /// Settings widget.
342    pub settings: BoxedUiNode,
343}
344
345/// Extends [`SettingBuilder`] to set custom editor metadata.
346pub trait SettingBuilderEditorExt {
347    /// Custom editor for the setting.
348    ///
349    /// If an editor is set the `VAR_EDITOR` service is used to instantiate the editor.
350    fn editor_fn(&mut self, editor: WidgetFn<Setting>) -> &mut Self;
351}
352
353/// Extends [`Setting`] to get custom editor metadata.
354pub trait SettingEditorExt {
355    /// Custom editor for the setting.
356    fn editor_fn(&self) -> Option<WidgetFn<Setting>>;
357
358    /// Instantiate editor.
359    ///
360    /// If an editor is set the [`EDITORS`] service is used to instantiate the editor.
361    fn editor(&self) -> BoxedUiNode;
362}
363
364/// Extends [`WidgetInfo`] to provide the setting config key for setting widgets.
365pub trait WidgetInfoSettingExt {
366    /// Gets the setting config key, if this widget represents a setting item.
367    fn setting_key(&self) -> Option<ConfigKey>;
368}
369
370static_id! {
371    static ref CUSTOM_EDITOR_ID: StateId<WidgetFn<Setting>>;
372    static ref SETTING_KEY_ID: StateId<ConfigKey>;
373}
374
375impl SettingBuilderEditorExt for SettingBuilder<'_> {
376    fn editor_fn(&mut self, editor: WidgetFn<Setting>) -> &mut Self {
377        self.set(*CUSTOM_EDITOR_ID, editor)
378    }
379}
380
381impl SettingEditorExt for Setting {
382    fn editor_fn(&self) -> Option<WidgetFn<Setting>> {
383        self.meta().get_clone(*CUSTOM_EDITOR_ID)
384    }
385
386    fn editor(&self) -> BoxedUiNode {
387        match self.editor_fn() {
388            Some(f) => f(self.clone()),
389            None => EDITOR_SETTING_VAR.with_context_var(ContextInitHandle::current(), Some(self.clone()), || {
390                EDITORS.get(self.value().clone_any())
391            }),
392        }
393    }
394}
395
396impl WidgetInfoSettingExt for WidgetInfo {
397    fn setting_key(&self) -> Option<ConfigKey> {
398        self.meta().get_clone(*SETTING_KEY_ID)
399    }
400}
401
402/// Extends [`SETTINGS`] to provide contextual information in an editor.
403pub trait SettingsCtxExt {
404    /// Gets a read-write context var that tracks the search text.
405    fn editor_search(&self) -> ContextVar<Txt>;
406
407    /// Gets a read-write context var that tracks the selected category.
408    fn editor_selected_category(&self) -> ContextVar<CategoryId>;
409
410    /// Gets a read-only context var that tracks the current editor data state.
411    fn editor_state(&self) -> ReadOnlyContextVar<Option<SettingsEditorState>>;
412
413    /// Gets a read-only context var that tracks the [`Setting`] entry the widget is inside, or will be.
414    fn editor_setting(&self) -> ReadOnlyContextVar<Option<Setting>>;
415}
416impl SettingsCtxExt for SETTINGS {
417    fn editor_search(&self) -> ContextVar<Txt> {
418        EDITOR_SEARCH_VAR
419    }
420
421    fn editor_selected_category(&self) -> ContextVar<CategoryId> {
422        EDITOR_SELECTED_CATEGORY_VAR
423    }
424
425    fn editor_state(&self) -> ReadOnlyContextVar<Option<SettingsEditorState>> {
426        EDITOR_STATE_VAR.read_only()
427    }
428
429    fn editor_setting(&self) -> ReadOnlyContextVar<Option<Setting>> {
430        EDITOR_SETTING_VAR.read_only()
431    }
432}
433
434context_var! {
435    pub(crate) static EDITOR_SEARCH_VAR: Txt = Txt::from_static("");
436    pub(crate) static EDITOR_SELECTED_CATEGORY_VAR: CategoryId = CategoryId(Txt::from_static(""));
437    pub(crate) static EDITOR_STATE_VAR: Option<SettingsEditorState> = None;
438    static EDITOR_SETTING_VAR: Option<Setting> = None;
439}
440
441/// Identifies the [`setting_fn`] widget.
442///
443/// [`setting_fn`]: fn@setting_fn
444#[property(CONTEXT)]
445pub fn setting(child: impl UiNode, setting: impl IntoValue<Setting>) -> impl UiNode {
446    let setting = setting.into();
447
448    let child = match_node(child, |_, op| {
449        if let UiNodeOp::Info { info } = op {
450            info.set_meta(*SETTING_KEY_ID, EDITOR_SETTING_VAR.with(|s| s.as_ref().unwrap().key().clone()));
451        }
452    });
453    with_context_var(child, EDITOR_SETTING_VAR, Some(setting))
454}
455
456/// Represents the current settings data.
457///
458/// Use [`SETTINGS.editor_state`] to get.
459///
460/// [`SETTINGS.editor_state`]: SettingsCtxExt::editor_state
461#[derive(PartialEq, Debug, Clone)]
462pub struct SettingsEditorState {
463    /// The actual text searched.
464    pub clean_search: Txt,
465    /// Categories list.
466    pub categories: Vec<Category>,
467    /// Selected category.
468    pub selected_cat: Category,
469    /// Settings for the selected category that match the search.
470    pub selected_settings: Vec<Setting>,
471    /// Top search match.
472    pub top_match: ConfigKey,
473}