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}
86
87fn editor_state() -> Var<Option<SettingsEditorState>> {
88 let clean_search = SETTINGS.editor_search().current_context().map(|s| {
90 let s = s.trim();
91 if !s.starts_with('@') {
92 s.to_lowercase().into()
93 } else {
94 Txt::from_str(s)
95 }
96 });
97
98 let sel_cat = SETTINGS.editor_selected_category().current_context();
99 let r = expr_var! {
100 if #{clean_search}.is_empty() {
101 let (cat, settings) = SETTINGS
103 .get(|_, cat| cat == #{sel_cat}, true)
104 .pop()
105 .unwrap_or_else(|| (Category::unknown(#{sel_cat}.clone()), vec![]));
106 Some(SettingsEditorState {
107 clean_search: #{clean_search}.clone(),
108 categories: SETTINGS.categories(|_| true, false, true),
109 selected_cat: cat,
110 top_match: settings.first().map(|s| s.key().clone()).unwrap_or_default(),
111 selected_settings: settings,
112 })
113 } else {
114 let mut r = SETTINGS.get(|_, _| true, false);
116
117 let mut top_match = (usize::MAX, Txt::from(""));
119 let mut actual_cat = None;
120 r.retain_mut(|(c, s)| {
121 if c.id() == #{sel_cat} {
122 actual_cat = Some(c.clone());
124 s.retain(|s| match s.search_index(#{clean_search}) {
126 Some(i) => {
127 if i < top_match.0 {
128 top_match = (i, s.key().clone());
129 }
130 true
131 }
132 None => false,
133 });
134 !s.is_empty()
135 } else {
136 s.iter().any(|s| match s.search_index(#{clean_search}) {
138 Some(i) => {
139 if i < top_match.0 {
140 top_match = (i, s.key().clone());
141 }
142 true
143 }
144 None => false,
145 })
146 }
147 });
148 let mut r = SettingsEditorState {
149 clean_search: #{clean_search}.clone(),
150 categories: r.iter().map(|(c, _)| c.clone()).collect(),
151 selected_cat: actual_cat.unwrap_or_else(|| Category::unknown(#{sel_cat}.clone())),
152 selected_settings: r
153 .into_iter()
154 .find_map(|(c, s)| if c.id() == #{sel_cat} { Some(s) } else { None })
155 .unwrap_or_default(),
156 top_match: top_match.1,
157 };
158 SETTINGS.sort_categories(&mut r.categories);
159 SETTINGS.sort_settings(&mut r.selected_settings);
160 Some(r)
161 }
162 };
163
164 let sel = SETTINGS.editor_selected_category().current_context();
166 let wk_sel_cat = sel.downgrade();
167 fn correct_sel(options: &[Category], sel: &Var<CategoryId>) {
168 if sel.with(|s| !options.iter().any(|c| c.id() == s))
169 && let Some(first) = options.first()
170 {
171 sel.set(first.id().clone());
172 }
173 }
174 r.hook(move |r| {
175 if let Some(sel) = wk_sel_cat.upgrade() {
176 correct_sel(&r.value().as_ref().unwrap().categories, &sel);
177 true
178 } else {
179 false
180 }
181 })
182 .perm();
183 r.with(|r| {
184 correct_sel(&r.as_ref().unwrap().categories, &sel);
185 });
186
187 r
188}
189
190fn settings_view_fn() -> UiNode {
191 let search = SETTINGS_SEARCH_FN_VAR.get()(SettingsSearchArgs {});
192
193 let editor_state = SETTINGS.editor_state().current_context();
194
195 let categories = editor_state
196 .map(|r| r.as_ref().unwrap().categories.clone())
197 .present(wgt_fn!(|categories: Vec<Category>| {
198 let cat_fn = CATEGORY_ITEM_FN_VAR.get();
199 let categories: UiVec = categories
200 .into_iter()
201 .enumerate()
202 .map(|(i, c)| cat_fn(CategoryItemArgs { index: i, category: c }))
203 .collect();
204
205 CATEGORIES_LIST_FN_VAR.get()(CategoriesListArgs { items: categories })
206 }));
207
208 let settings = editor_state.present(wgt_fn!(|state: Option<SettingsEditorState>| {
209 let SettingsEditorState {
210 selected_cat,
211 selected_settings,
212 ..
213 } = state.unwrap();
214 let setting_fn = SETTING_FN_VAR.get();
215
216 let settings: UiVec = selected_settings
217 .into_iter()
218 .enumerate()
219 .map(|(i, s)| {
220 let editor = s.editor();
221 setting_fn(SettingArgs {
222 index: i,
223 setting: s.clone(),
224 editor,
225 })
226 })
227 .collect();
228
229 let header = CATEGORY_HEADER_FN_VAR.get()(CategoryHeaderArgs { category: selected_cat });
230
231 SETTINGS_FN_VAR.get()(SettingsArgs { header, items: settings })
232 }));
233
234 PANEL_FN_VAR.get()(PanelArgs {
235 search,
236 categories,
237 settings,
238 })
239}
240
241#[property(CONTEXT, widget_impl(SettingsEditor))]
246pub fn save_state(child: impl IntoUiNode, enabled: impl IntoValue<SaveState>) -> UiNode {
247 #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
248 struct SettingsEditorCfg {
249 search: Txt,
250 selected_category: CategoryId,
251 }
252 save_state_node::<SettingsEditorCfg>(
253 child,
254 enabled,
255 |cfg| {
256 let search = SETTINGS.editor_search();
257 let cat = SETTINGS.editor_selected_category();
258 WIDGET.sub_var(&search).sub_var(&cat);
259 if let Some(c) = cfg {
260 search.set(c.search);
261 cat.set(c.selected_category);
262 }
263 },
264 |required| {
265 let search = SETTINGS.editor_search();
266 let cat = SETTINGS.editor_selected_category();
267 if required || search.is_new() || cat.is_new() {
268 Some(SettingsEditorCfg {
269 search: search.get(),
270 selected_category: cat.get(),
271 })
272 } else {
273 None
274 }
275 },
276 )
277}
278
279fn command_handler(child: impl IntoUiNode) -> UiNode {
281 let mut _handle = CommandHandle::dummy();
282 match_node(child, move |c, op| match op {
283 UiNodeOp::Init => {
284 _handle = SETTINGS_CMD.scoped(WIDGET.id()).subscribe(true);
285 }
286 UiNodeOp::Deinit => {
287 _handle = CommandHandle::dummy();
288 }
289 UiNodeOp::Event { update } => {
290 c.event(update);
291
292 if let Some(args) = SETTINGS_CMD.scoped(WIDGET.id()).on_unhandled(update) {
293 args.propagation().stop();
294
295 if let Some(id) = args.param::<CategoryId>() {
296 if SETTINGS
297 .editor_state()
298 .with(|s| s.as_ref().unwrap().categories.iter().any(|c| c.id() == id))
299 {
300 SETTINGS.editor_selected_category().set(id.clone());
301 }
302 } else if let Some(key) = args.param::<Txt>() {
303 let search = if SETTINGS.any(|k, _| k == key) {
304 formatx!("@key:{key}")
305 } else {
306 key.clone()
307 };
308 SETTINGS.editor_search().set(search);
309 } else if args.param.is_none() && !FOCUS.is_focus_within(WIDGET.id()).get() {
310 let s = Some(SETTINGS.editor_state().with(|s| s.as_ref().unwrap().top_match.clone()));
312 let info = WIDGET.info();
313 if let Some(w) = info.descendants().find(|w| w.setting_key() == s) {
314 FOCUS.focus_widget_or_enter(w.id(), false, false);
315 } else {
316 FOCUS.focus_widget_or_enter(info.id(), false, false);
317 }
318 }
319 }
320 }
321 _ => {}
322 })
323}
324
325pub fn handle_settings_cmd() {
327 use zng_app::{event::AnyEventArgs as _, window::WINDOW};
328
329 SETTINGS_CMD
330 .on_event(
331 true,
332 async_hn!(|args| {
333 if args.propagation().is_stopped() || !SETTINGS.any(|_, _| true) {
334 return;
335 }
336
337 args.propagation().stop();
338
339 let parent = WINDOWS.focused_window_id();
340
341 let new_window = WINDOWS.focus_or_open("zng-config-settings-default", async move {
342 if let Some(p) = parent
343 && let Ok(p) = WINDOWS.vars(p)
344 {
345 let v = WINDOW.vars();
346 p.icon().set_bind(&v.icon()).perm();
347 }
348
349 Window! {
350 title = l10n!("window.title", "{$app} - Settings", app = zng_env::about().app.clone());
351 parent;
352 child = SettingsEditor! {
353 id = "zng-config-settings-default-editor";
354 };
355 }
356 });
357
358 if let Some(param) = &args.args.param {
359 if let Some(w) = new_window {
360 WINDOWS.wait_loaded(w.wait_rsp().await, true).await;
361 }
362 SETTINGS_CMD
363 .scoped("zng-config-settings-default-editor")
364 .notify_param(param.clone());
365 }
366 }),
367 )
368 .perm();
369}