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 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 UiNode, wgt_fn: impl IntoVar<WidgetFn<CategoryItemArgs>>) -> impl 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 UiNode, wgt_fn: impl IntoVar<WidgetFn<CategoriesListArgs>>) -> impl 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 UiNode, wgt_fn: impl IntoVar<WidgetFn<CategoryHeaderArgs>>) -> impl 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 UiNode, wgt_fn: impl IntoVar<WidgetFn<SettingArgs>>) -> impl 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 UiNode, wgt_fn: impl IntoVar<WidgetFn<SettingsArgs>>) -> impl 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 UiNode, wgt_fn: impl IntoVar<WidgetFn<SettingsSearchArgs>>) -> impl 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 UiNode, wgt_fn: impl IntoVar<WidgetFn<PanelArgs>>) -> impl UiNode {
102 with_context_var(child, PANEL_FN_VAR, wgt_fn)
103}
104
105pub 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
115pub 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
126pub 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
165pub 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
184pub 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
238pub 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
255pub 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
268pub 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
279pub 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
290pub struct CategoryItemArgs {
292 pub index: usize,
294 pub category: Category,
296}
297
298pub struct CategoryHeaderArgs {
300 pub category: Category,
302}
303
304pub struct CategoriesListArgs {
308 pub items: UiVec,
310}
311
312pub struct SettingArgs {
314 pub index: usize,
316 pub setting: Setting,
318 pub editor: BoxedUiNode,
320}
321
322pub struct SettingsArgs {
324 pub header: BoxedUiNode,
326 pub items: UiVec,
328}
329
330pub struct SettingsSearchArgs {}
334
335pub struct PanelArgs {
337 pub search: BoxedUiNode,
339 pub categories: BoxedUiNode,
341 pub settings: BoxedUiNode,
343}
344
345pub trait SettingBuilderEditorExt {
347 fn editor_fn(&mut self, editor: WidgetFn<Setting>) -> &mut Self;
351}
352
353pub trait SettingEditorExt {
355 fn editor_fn(&self) -> Option<WidgetFn<Setting>>;
357
358 fn editor(&self) -> BoxedUiNode;
362}
363
364pub trait WidgetInfoSettingExt {
366 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
402pub trait SettingsCtxExt {
404 fn editor_search(&self) -> ContextVar<Txt>;
406
407 fn editor_selected_category(&self) -> ContextVar<CategoryId>;
409
410 fn editor_state(&self) -> ReadOnlyContextVar<Option<SettingsEditorState>>;
412
413 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#[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#[derive(PartialEq, Debug, Clone)]
462pub struct SettingsEditorState {
463 pub clean_search: Txt,
465 pub categories: Vec<Category>,
467 pub selected_cat: Category,
469 pub selected_settings: Vec<Setting>,
471 pub top_match: ConfigKey,
473}