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 editor_state = SETTINGS.editor_state().current_context();
192
193 let has_results = editor_state.map(|r| !r.as_ref().unwrap().categories.is_empty());
194
195 let search = SETTINGS_SEARCH_FN_VAR.get()(SettingsSearchArgs {
196 has_results: has_results.clone(),
197 });
198
199 let categories = editor_state
200 .map(|r| r.as_ref().unwrap().categories.clone())
201 .present(wgt_fn!(|categories: Vec<Category>| {
202 let cat_fn = CATEGORY_ITEM_FN_VAR.get();
203 let categories: UiVec = categories
204 .into_iter()
205 .enumerate()
206 .map(|(i, c)| cat_fn(CategoryItemArgs { index: i, category: c }))
207 .collect();
208
209 CATEGORIES_LIST_FN_VAR.get()(CategoriesListArgs { items: categories })
210 }));
211
212 let settings = editor_state.present(wgt_fn!(|state: Option<SettingsEditorState>| {
213 let SettingsEditorState {
214 selected_cat,
215 selected_settings,
216 ..
217 } = state.unwrap();
218 let setting_fn = SETTING_FN_VAR.get();
219
220 let settings: UiVec = selected_settings
221 .into_iter()
222 .enumerate()
223 .map(|(i, s)| {
224 let editor = s.editor();
225 setting_fn(SettingArgs {
226 index: i,
227 setting: s.clone(),
228 editor,
229 })
230 })
231 .collect();
232
233 let header = CATEGORY_HEADER_FN_VAR.get()(CategoryHeaderArgs { category: selected_cat });
234
235 SETTINGS_FN_VAR.get()(SettingsArgs { header, items: settings })
236 }));
237
238 PANEL_FN_VAR.get()(PanelArgs {
239 search,
240 categories,
241 settings,
242 has_results,
243 })
244}
245
246#[property(CONTEXT, widget_impl(SettingsEditor))]
251pub fn save_state(child: impl IntoUiNode, enabled: impl IntoValue<SaveState>) -> UiNode {
252 #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
253 struct SettingsEditorCfg {
254 search: Txt,
255 selected_category: CategoryId,
256 }
257 save_state_node::<SettingsEditorCfg>(
258 child,
259 enabled,
260 |cfg| {
261 let search = SETTINGS.editor_search();
262 let cat = SETTINGS.editor_selected_category();
263 WIDGET.sub_var(&search).sub_var(&cat);
264 if let Some(c) = cfg {
265 search.set(c.search);
266 cat.set(c.selected_category);
267 }
268 },
269 |required| {
270 let search = SETTINGS.editor_search();
271 let cat = SETTINGS.editor_selected_category();
272 if required || search.is_new() || cat.is_new() {
273 Some(SettingsEditorCfg {
274 search: search.get(),
275 selected_category: cat.get(),
276 })
277 } else {
278 None
279 }
280 },
281 )
282}
283
284fn command_handler(child: impl IntoUiNode) -> UiNode {
286 let mut _handle = CommandHandle::dummy();
287 match_node(child, move |c, op| match op {
288 UiNodeOp::Init => {
289 _handle = SETTINGS_CMD.scoped(WIDGET.id()).subscribe(true);
290 }
291 UiNodeOp::Deinit => {
292 _handle = CommandHandle::dummy();
293 }
294 UiNodeOp::Event { update } => {
295 c.event(update);
296
297 if let Some(args) = SETTINGS_CMD.scoped(WIDGET.id()).on_unhandled(update) {
298 args.propagation().stop();
299
300 if let Some(id) = args.param::<CategoryId>() {
301 if SETTINGS
302 .editor_state()
303 .with(|s| s.as_ref().unwrap().categories.iter().any(|c| c.id() == id))
304 {
305 SETTINGS.editor_selected_category().set(id.clone());
306 }
307 } else if let Some(key) = args.param::<Txt>() {
308 let search = if SETTINGS.any(|k, _| k == key) {
309 formatx!("@key:{key}")
310 } else {
311 key.clone()
312 };
313 SETTINGS.editor_search().set(search);
314 } else if args.param.is_none() && !FOCUS.is_focus_within(WIDGET.id()).get() {
315 let s = Some(SETTINGS.editor_state().with(|s| s.as_ref().unwrap().top_match.clone()));
317 let info = WIDGET.info();
318 if let Some(w) = info.descendants().find(|w| w.setting_key() == s) {
319 FOCUS.focus_widget_or_enter(w.id(), false, false);
320 } else {
321 FOCUS.focus_widget_or_enter(info.id(), false, false);
322 }
323 }
324 }
325 }
326 _ => {}
327 })
328}
329
330pub fn handle_settings_cmd() {
332 use zng_app::{event::AnyEventArgs as _, window::WINDOW};
333
334 SETTINGS_CMD
335 .on_event(
336 true,
337 async_hn!(|args| {
338 if args.propagation().is_stopped() || !SETTINGS.any(|_, _| true) {
339 return;
340 }
341
342 args.propagation().stop();
343
344 let parent = WINDOWS.focused_window_id();
345
346 let new_window = WINDOWS.focus_or_open("zng-config-settings-default", async move {
347 if let Some(p) = parent
348 && let Ok(p) = WINDOWS.vars(p)
349 {
350 let v = WINDOW.vars();
351 p.icon().set_bind(&v.icon()).perm();
352 }
353
354 Window! {
355 title = l10n!("window.title", "{$app} - Settings", app = zng_env::about().app.clone());
356 parent;
357 child = SettingsEditor! {
358 id = "zng-config-settings-default-editor";
359 };
360 }
361 });
362
363 if let Some(param) = &args.args.param {
364 if let Some(w) = new_window {
365 WINDOWS.wait_loaded(w.wait_rsp().await, true).await;
366 }
367 SETTINGS_CMD
368 .scoped("zng-config-settings-default-editor")
369 .notify_param(param.clone());
370 }
371 }),
372 )
373 .perm();
374}