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