1#![doc(html_favicon_url = "https://zng-ui.github.io/res/zng-logo-icon.png")]
2#![doc(html_logo_url = "https://zng-ui.github.io/res/zng-logo.png")]
3zng_wgt::enable_widget_macros!();
6
7mod view_fn;
8pub use view_fn::*;
9
10use zng_ext_config::settings::{Category, CategoryId, SETTINGS};
11use zng_ext_input::focus::FOCUS;
12use zng_ext_l10n::l10n;
13use zng_ext_window::{WINDOW_Ext as _, WINDOWS};
14use zng_wgt::{node::VarPresent as _, prelude::*};
15use zng_wgt_input::cmd::SETTINGS_CMD;
16use zng_wgt_size_offset::actual_width;
17use zng_wgt_window::{SaveState, Window, save_state_node};
18
19#[widget($crate::SettingsEditor)]
21pub struct SettingsEditor(WidgetBase);
22impl SettingsEditor {
23 fn widget_intrinsic(&mut self) {
24 widget_set! {
25 self;
26 save_state = SaveState::enabled();
27 zng_wgt_fill::background_color = light_dark(rgb(0.85, 0.85, 0.85), rgb(0.15, 0.15, 0.15));
28 zng_wgt_container::padding = 10;
29
30 when *#actual_width <= 400 && *#actual_width > 1 {
31 panel_fn = WidgetFn::new(default_panel_mobile_fn);
32 categories_list_fn = WidgetFn::new(default_categories_list_mobile_fn);
33 }
34 }
35 self.widget_builder().push_build_action(|wgt| {
36 wgt.set_child(settings_editor_node());
37 wgt.push_intrinsic(NestGroup::EVENT, "command-handler", command_handler);
38 wgt.push_intrinsic(NestGroup::CONTEXT, "editor-vars", |child| {
39 let child = with_context_var_init(child, EDITOR_STATE_VAR, editor_state);
40 let child = with_context_var(child, EDITOR_SEARCH_VAR, var(Txt::from("")));
41 with_context_var(child, EDITOR_SELECTED_CATEGORY_VAR, var(CategoryId::from("")))
42 });
43 });
44 }
45}
46
47pub fn settings_editor_node() -> UiNode {
51 match_node(UiNode::nil(), move |c, op| match op {
52 UiNodeOp::Init => {
53 WIDGET
54 .sub_var(&SETTINGS_FN_VAR)
55 .sub_var(&SETTING_FN_VAR)
56 .sub_var(&SETTINGS_SEARCH_FN_VAR)
57 .sub_var(&CATEGORIES_LIST_FN_VAR)
58 .sub_var(&CATEGORY_HEADER_FN_VAR)
59 .sub_var(&CATEGORY_ITEM_FN_VAR)
60 .sub_var(&PANEL_FN_VAR);
61 *c.node() = settings_view_fn();
62 }
63 UiNodeOp::Deinit => {
64 c.deinit();
65 *c.node() = UiNode::nil();
66 }
67 UiNodeOp::Update { .. }
68 if (PANEL_FN_VAR.is_new()
69 || SETTINGS_FN_VAR.is_new()
70 || SETTING_FN_VAR.is_new()
71 || SETTINGS_SEARCH_FN_VAR.is_new()
72 || CATEGORIES_LIST_FN_VAR.is_new()
73 || CATEGORY_HEADER_FN_VAR.is_new()
74 || CATEGORY_ITEM_FN_VAR.is_new()) =>
75 {
76 c.delegated();
77 c.node().deinit();
78 *c.node() = settings_view_fn();
79 c.node().init();
80 WIDGET.update_info().layout().render();
81 }
82 _ => {}
83 })
84}
85
86fn editor_state() -> Var<Option<SettingsEditorState>> {
87 let clean_search = SETTINGS.editor_search().current_context().map(|s| {
89 let s = s.trim();
90 if !s.starts_with('@') {
91 s.to_lowercase().into()
92 } else {
93 Txt::from_str(s)
94 }
95 });
96
97 let sel_cat = SETTINGS.editor_selected_category().current_context();
98 let r = expr_var! {
99 if #{clean_search}.is_empty() {
100 let (cat, settings) = SETTINGS
102 .get(|_, cat| cat == #{sel_cat}, true)
103 .pop()
104 .unwrap_or_else(|| (Category::unknown(#{sel_cat}.clone()), vec![]));
105 Some(SettingsEditorState {
106 clean_search: #{clean_search}.clone(),
107 categories: SETTINGS.categories(|_| true, false, true),
108 selected_cat: cat,
109 top_match: settings.first().map(|s| s.key().clone()).unwrap_or_default(),
110 selected_settings: settings,
111 })
112 } else {
113 let mut r = SETTINGS.get(|_, _| true, false);
115
116 let mut top_match = (usize::MAX, Txt::from(""));
118 let mut actual_cat = None;
119 r.retain_mut(|(c, s)| {
120 if c.id() == #{sel_cat} {
121 actual_cat = Some(c.clone());
123 s.retain(|s| match s.search_index(#{clean_search}) {
125 Some(i) => {
126 if i < top_match.0 {
127 top_match = (i, s.key().clone());
128 }
129 true
130 }
131 None => false,
132 });
133 !s.is_empty()
134 } else {
135 s.iter().any(|s| match s.search_index(#{clean_search}) {
137 Some(i) => {
138 if i < top_match.0 {
139 top_match = (i, s.key().clone());
140 }
141 true
142 }
143 None => false,
144 })
145 }
146 });
147 let mut r = SettingsEditorState {
148 clean_search: #{clean_search}.clone(),
149 categories: r.iter().map(|(c, _)| c.clone()).collect(),
150 selected_cat: actual_cat.unwrap_or_else(|| Category::unknown(#{sel_cat}.clone())),
151 selected_settings: r
152 .into_iter()
153 .find_map(|(c, s)| if c.id() == #{sel_cat} { Some(s) } else { None })
154 .unwrap_or_default(),
155 top_match: top_match.1,
156 };
157 SETTINGS.sort_categories(&mut r.categories);
158 SETTINGS.sort_settings(&mut r.selected_settings);
159 Some(r)
160 }
161 };
162
163 let sel = SETTINGS.editor_selected_category().current_context();
165 let wk_sel_cat = sel.downgrade();
166 fn correct_sel(options: &[Category], sel: &Var<CategoryId>) {
167 if sel.with(|s| !options.iter().any(|c| c.id() == s))
168 && let Some(first) = options.first()
169 {
170 sel.set(first.id().clone());
171 }
172 }
173 r.hook(move |r| {
174 if let Some(sel) = wk_sel_cat.upgrade() {
175 correct_sel(&r.value().as_ref().unwrap().categories, &sel);
176 true
177 } else {
178 false
179 }
180 })
181 .perm();
182 r.with(|r| {
183 correct_sel(&r.as_ref().unwrap().categories, &sel);
184 });
185
186 r
187}
188
189fn settings_view_fn() -> UiNode {
190 let editor_state = SETTINGS.editor_state().current_context();
191
192 let has_results = editor_state.map(|r| !r.as_ref().unwrap().categories.is_empty());
193
194 let search = SETTINGS_SEARCH_FN_VAR.get()(SettingsSearchArgs {
195 has_results: has_results.clone(),
196 });
197
198 let categories = editor_state
199 .map(|r| r.as_ref().unwrap().categories.clone())
200 .present(wgt_fn!(|categories: Vec<Category>| {
201 let cat_fn = CATEGORY_ITEM_FN_VAR.get();
202 let categories: UiVec = categories
203 .into_iter()
204 .enumerate()
205 .map(|(i, c)| cat_fn(CategoryItemArgs { index: i, category: c }))
206 .collect();
207
208 CATEGORIES_LIST_FN_VAR.get()(CategoriesListArgs { items: categories })
209 }));
210
211 let settings = editor_state.present(wgt_fn!(|state: Option<SettingsEditorState>| {
212 let SettingsEditorState {
213 selected_cat,
214 selected_settings,
215 ..
216 } = state.unwrap();
217 let setting_fn = SETTING_FN_VAR.get();
218
219 let settings: UiVec = selected_settings
220 .into_iter()
221 .enumerate()
222 .map(|(i, s)| {
223 let editor = s.editor();
224 setting_fn(SettingArgs {
225 index: i,
226 setting: s.clone(),
227 editor,
228 })
229 })
230 .collect();
231
232 let header = CATEGORY_HEADER_FN_VAR.get()(CategoryHeaderArgs { category: selected_cat });
233
234 SETTINGS_FN_VAR.get()(SettingsArgs { header, items: settings })
235 }));
236
237 PANEL_FN_VAR.get()(PanelArgs {
238 search,
239 categories,
240 settings,
241 has_results,
242 })
243}
244
245#[property(CONTEXT, widget_impl(SettingsEditor))]
250pub fn save_state(child: impl IntoUiNode, enabled: impl IntoValue<SaveState>) -> UiNode {
251 #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
252 struct SettingsEditorCfg {
253 search: Txt,
254 selected_category: CategoryId,
255 }
256 save_state_node::<SettingsEditorCfg>(
257 child,
258 enabled,
259 |cfg| {
260 let search = SETTINGS.editor_search();
261 let cat = SETTINGS.editor_selected_category();
262 WIDGET.sub_var(&search).sub_var(&cat);
263 if let Some(c) = cfg {
264 search.set(c.search);
265 cat.set(c.selected_category);
266 }
267 },
268 |required| {
269 let search = SETTINGS.editor_search();
270 let cat = SETTINGS.editor_selected_category();
271 if required || search.is_new() || cat.is_new() {
272 Some(SettingsEditorCfg {
273 search: search.get(),
274 selected_category: cat.get(),
275 })
276 } else {
277 None
278 }
279 },
280 )
281}
282
283fn command_handler(child: impl IntoUiNode) -> UiNode {
285 let mut _handle = CommandHandle::dummy();
286 match_node(child, move |c, op| match op {
287 UiNodeOp::Init => {
288 _handle = SETTINGS_CMD.scoped(WIDGET.id()).subscribe(true);
289 }
290 UiNodeOp::Deinit => {
291 _handle = CommandHandle::dummy();
292 }
293 UiNodeOp::Update { updates } => {
294 c.update(updates);
295
296 SETTINGS_CMD.scoped(WIDGET.id()).each_update(true, false, |args| {
297 args.propagation.stop();
298
299 if let Some(id) = args.param::<CategoryId>() {
300 if SETTINGS
301 .editor_state()
302 .with(|s| s.as_ref().unwrap().categories.iter().any(|c| c.id() == id))
303 {
304 SETTINGS.editor_selected_category().set(id.clone());
305 }
306 } else if let Some(key) = args.param::<Txt>() {
307 let search = if SETTINGS.any(|k, _| k == key) {
308 formatx!("@key:{key}")
309 } else {
310 key.clone()
311 };
312 SETTINGS.editor_search().set(search);
313 } else if args.param.is_none() && !FOCUS.is_focus_within(WIDGET.id()).get() {
314 let s = Some(SETTINGS.editor_state().with(|s| s.as_ref().unwrap().top_match.clone()));
316 let info = WIDGET.info();
317 if let Some(w) = info.descendants().find(|w| w.setting_key() == s) {
318 FOCUS.focus_widget_or_enter(w.id(), false, false);
319 } else {
320 FOCUS.focus_widget_or_enter(info.id(), false, false);
321 }
322 }
323 });
324 }
325 _ => {}
326 })
327}
328
329pub fn handle_settings_cmd() {
331 SETTINGS_CMD
332 .on_event(
333 true,
334 true,
335 false,
336 async_hn!(|args| {
337 if !SETTINGS.any(|_, _| true) {
338 return;
339 }
340
341 args.propagation.stop();
342
343 let parent = FOCUS.focused().with(|p| p.as_ref().map(|t| t.window_id()));
344
345 let new_window = WINDOWS.focus_or_open("zng-config-settings-default", async move {
346 if let Some(p) = parent
347 && let Some(p) = WINDOWS.vars(p)
348 {
349 let v = WINDOW.vars();
350 p.icon().set_bind(&v.icon()).perm();
351 }
352
353 Window! {
354 title = l10n!("window.title", "{$app} - Settings", app = zng_env::about().app.clone());
355 parent;
356 child = SettingsEditor! {
357 id = "zng-config-settings-default-editor";
358 };
359 }
360 });
361
362 if let Some(param) = &args.param {
363 let w = new_window.wait_rsp().await;
364 w.instance_state().wait_match(|s| s.is_loaded()).await;
365
366 SETTINGS_CMD
367 .scoped("zng-config-settings-default-editor")
368 .notify_param(param.clone());
369 }
370 }),
371 )
372 .perm();
373}