zng_wgt_window/
window_properties.rs

1use std::time::Duration;
2
3use zng_ext_config::{AnyConfig as _, CONFIG, ConfigKey, ConfigStatus, ConfigValue};
4use zng_ext_window::{
5    AutoSize, FrameCaptureMode, MONITORS, MonitorQuery, WINDOW_Ext as _, WINDOW_LOAD_EVENT, WINDOWS, WindowButton, WindowIcon,
6    WindowLoadingHandle, WindowState, WindowVars,
7};
8use zng_wgt::prelude::*;
9
10use serde::{Deserialize, Serialize};
11use zng_wgt_layer::adorner_fn;
12
13use super::Window;
14
15fn bind_window_var<T, V>(child: impl UiNode, user_var: impl IntoVar<T>, select: impl Fn(&WindowVars) -> V + Send + 'static) -> impl UiNode
16where
17    T: VarValue + PartialEq,
18    V: Var<T>,
19{
20    #[cfg(feature = "dyn_closure")]
21    let select: Box<dyn Fn(&WindowVars) -> V + Send> = Box::new(select);
22    bind_window_var_impl(child.cfg_boxed(), user_var.into_var(), select).cfg_boxed()
23}
24fn bind_window_var_impl<T, V>(
25    child: impl UiNode,
26    user_var: impl IntoVar<T>,
27    select: impl Fn(&WindowVars) -> V + Send + 'static,
28) -> impl UiNode
29where
30    T: VarValue + PartialEq,
31    V: Var<T>,
32{
33    let user_var = user_var.into_var();
34
35    match_node(child, move |_, op| {
36        if let UiNodeOp::Init = op {
37            let window_var = select(&WINDOW.vars());
38            if !user_var.capabilities().is_always_static() {
39                let binding = user_var.bind_bidi(&window_var);
40                WIDGET.push_var_handles(binding);
41            }
42            window_var.set_from(&user_var).unwrap();
43        }
44    })
45}
46
47// Properties that set the full value.
48macro_rules! set_properties {
49    ($(
50        $ident:ident: $Type:ty,
51    )+) => {
52        $(paste::paste! {
53            #[doc = "Binds the [`"$ident "`](fn@WindowVars::"$ident ") window var with the property value."]
54            ///
55            /// The binding is bidirectional and the window variable is assigned on init.
56            #[property(CONTEXT, widget_impl(Window))]
57            pub fn $ident(child: impl UiNode, $ident: impl IntoVar<$Type>) -> impl UiNode {
58                bind_window_var(child, $ident, |w|w.$ident().clone())
59            }
60        })+
61    }
62}
63set_properties! {
64    position: Point,
65    monitor: MonitorQuery,
66
67    state: WindowState,
68
69    size: Size,
70    min_size: Size,
71    max_size: Size,
72
73    font_size: Length,
74
75    chrome: bool,
76    icon: WindowIcon,
77    title: Txt,
78
79    auto_size: AutoSize,
80    auto_size_origin: Point,
81
82    resizable: bool,
83    movable: bool,
84
85    always_on_top: bool,
86
87    visible: bool,
88    taskbar_visible: bool,
89
90    parent: Option<WindowId>,
91    modal: bool,
92
93    color_scheme: Option<ColorScheme>,
94    accent_color: Option<LightDark>,
95
96    frame_capture_mode: FrameCaptureMode,
97
98    enabled_buttons: WindowButton,
99}
100
101macro_rules! map_properties {
102    ($(
103        $ident:ident . $member:ident = $name:ident : $Type:ty,
104    )+) => {$(paste::paste! {
105        #[doc = "Binds the `"$member "` of the [`"$ident "`](fn@WindowVars::"$ident ") window var with the property value."]
106        ///
107        /// The binding is bidirectional and the window variable is assigned on init.
108        #[property(CONTEXT, widget_impl(Window))]
109        pub fn $name(child: impl UiNode, $name: impl IntoVar<$Type>) -> impl UiNode {
110            bind_window_var(child, $name, |w|w.$ident().map_ref_bidi(|v| &v.$member, |v|&mut v.$member))
111        }
112    })+}
113}
114map_properties! {
115    position.x = x: Length,
116    position.y = y: Length,
117    size.width = width: Length,
118    size.height = height: Length,
119    min_size.width = min_width: Length,
120    min_size.height = min_height: Length,
121    max_size.width = max_width: Length,
122    max_size.height = max_height: Length,
123}
124
125/// Window clear color.
126///
127/// Color used to clear the previous frame pixels before rendering a new frame.
128/// It is visible if window content does not completely fill the content area, this
129/// can happen if you do not set a background or the background is semi-transparent, also
130/// can happen during very fast resizes.
131#[property(CONTEXT, default(colors::WHITE), widget_impl(Window))]
132pub fn clear_color(child: impl UiNode, color: impl IntoVar<Rgba>) -> impl UiNode {
133    let clear_color = color.into_var();
134    match_node(child, move |_, op| match op {
135        UiNodeOp::Init => {
136            WIDGET.sub_var_render_update(&clear_color);
137        }
138        UiNodeOp::Render { frame } => {
139            frame.set_clear_color(clear_color.get());
140        }
141        UiNodeOp::RenderUpdate { update } => {
142            update.set_clear_color(clear_color.get());
143        }
144        _ => {}
145    })
146}
147
148/// Window or widget persistence config.
149///
150/// See the [`save_state_node`] for more details.
151///
152/// [`save_state`]: fn@save_state
153#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
154pub enum SaveState {
155    /// Save and restore state.
156    Enabled {
157        /// Config key that identifies the window or widget.
158        ///
159        /// If `None` a key is generated from the widget ID and window ID name, see [`enabled_key`] for
160        /// details about how key generation.
161        ///
162        /// [`enabled_key`]: Self::enabled_key
163        key: Option<ConfigKey>,
164    },
165    /// Don't save nor restore state.
166    Disabled,
167}
168impl Default for SaveState {
169    /// Enabled, no key, delay 1s.
170    fn default() -> Self {
171        Self::enabled()
172    }
173}
174impl SaveState {
175    /// Default, enabled, no key.
176    pub const fn enabled() -> Self {
177        Self::Enabled { key: None }
178    }
179
180    /// Gets the config key if is enabled and can enable on the context.
181    ///
182    /// If is enabled without a key, the key is generated from the widget or window name:
183    ///
184    /// * If the widget ID has a name the key is `"wgt-{name}-state"`.
185    /// * If the context is the window root or just a window and the window ID has a name the key is `"win-{name}-state"`.
186    pub fn enabled_key(&self) -> Option<ConfigKey> {
187        match self {
188            Self::Enabled { key } => {
189                if key.is_some() {
190                    return key.clone();
191                }
192                let mut try_win = true;
193                if let Some(wgt) = WIDGET.try_id() {
194                    let name = wgt.name();
195                    if !name.is_empty() {
196                        return Some(formatx!("wgt-{name}"));
197                    }
198                    try_win = WIDGET.parent_id().is_none();
199                }
200                if try_win {
201                    if let Some(win) = WINDOW.try_id() {
202                        let name = win.name();
203                        if !name.is_empty() {
204                            return Some(formatx!("win-{name}"));
205                        }
206                    }
207                }
208                None
209            }
210            Self::Disabled => None,
211        }
212    }
213}
214impl_from_and_into_var! {
215    /// Convert `true` to default config and `false` to `None`.
216    fn from(persist: bool) -> SaveState {
217        if persist {
218            SaveState::default()
219        } else {
220            SaveState::Disabled
221        }
222    }
223}
224
225/// Helper node for implementing widgets save.
226///
227/// The `on_load_restore` closure is called on window load or on init if the window is already loaded. The argument
228/// is the saved state from a previous instance.
229///
230/// The `on_update_save` closure is called every update after the window loads, if it returns a value the config is updated.
231/// If the argument is `true` the closure must return a value, this value is used as the CONFIG fallback value that is required
232/// by some config backends even when the config is already present.
233pub fn save_state_node<S: ConfigValue>(
234    child: impl UiNode,
235    enabled: impl IntoValue<SaveState>,
236    mut on_load_restore: impl FnMut(Option<S>) + Send + 'static,
237    mut on_update_save: impl FnMut(bool) -> Option<S> + Send + 'static,
238) -> impl UiNode {
239    let enabled = enabled.into();
240    enum State<S: ConfigValue> {
241        Disabled,
242        AwaitingLoad,
243        Loaded,
244        LoadedWithCfg(BoxedVar<S>),
245    }
246    let mut state = State::Disabled;
247    match_node(child, move |_, op| match op {
248        UiNodeOp::Init => {
249            if let Some(key) = enabled.enabled_key() {
250                if WINDOW.is_loaded() {
251                    if CONFIG.contains_key(key.clone()).get() {
252                        let cfg = CONFIG.get(key, on_update_save(true).unwrap());
253                        on_load_restore(Some(cfg.get()));
254                        state = State::LoadedWithCfg(cfg);
255                    } else {
256                        on_load_restore(None);
257                        state = State::Loaded;
258                    }
259                } else {
260                    WIDGET.sub_event(&WINDOW_LOAD_EVENT);
261                    state = State::AwaitingLoad;
262                }
263            } else {
264                state = State::Disabled;
265            }
266        }
267        UiNodeOp::Deinit => {
268            state = State::Disabled;
269        }
270        UiNodeOp::Event { update } => {
271            if matches!(&state, State::AwaitingLoad) && WINDOW_LOAD_EVENT.has(update) {
272                if let Some(key) = enabled.enabled_key() {
273                    if CONFIG.contains_key(key.clone()).get() {
274                        let cfg = CONFIG.get(key, on_update_save(true).unwrap());
275                        on_load_restore(Some(cfg.get()));
276                        state = State::LoadedWithCfg(cfg);
277                    } else {
278                        on_load_restore(None);
279                        state = State::Loaded;
280                    }
281                } else {
282                    // this can happen if the parent widget node is not properly implemented (changed context)
283                    state = State::Disabled;
284                }
285            }
286        }
287        UiNodeOp::Update { .. } => match &mut state {
288            State::LoadedWithCfg(cfg) => {
289                if let Some(new) = on_update_save(false) {
290                    let _ = cfg.set(new);
291                }
292            }
293            State::Loaded => {
294                if let Some(new) = on_update_save(false) {
295                    if let Some(key) = enabled.enabled_key() {
296                        let cfg = CONFIG.insert(key, new.clone());
297                        state = State::LoadedWithCfg(cfg);
298                    } else {
299                        state = State::Disabled;
300                    }
301                }
302            }
303            _ => {}
304        },
305        _ => {}
306    })
307}
308
309/// Save and restore the window state.
310///
311/// If enabled a config entry is created for the window state in [`CONFIG`], and if a config backend is set
312/// the window state is persisted on change and restored when the app reopens.
313///
314/// This property is enabled by default in the `Window!` widget, without a key. Note that without a config key
315/// the state only actually enables if the window root widget ID or the window ID have a name.
316///
317/// [`CONFIG`]: zng_ext_config::CONFIG
318#[property(CONTEXT, default(SaveState::Disabled), widget_impl(Window))]
319pub fn save_state(child: impl UiNode, enabled: impl IntoValue<SaveState>) -> impl UiNode {
320    #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
321    struct WindowStateCfg {
322        state: WindowState,
323        restore_rect: euclid::Rect<f32, Dip>,
324    }
325    save_state_node::<WindowStateCfg>(
326        child,
327        enabled,
328        |cfg| {
329            let vars = WINDOW.vars();
330            let state = vars.state();
331            WIDGET.sub_var(&state).sub_var(&vars.restore_rect());
332
333            if let Some(cfg) = cfg {
334                // restore state
335                state.set(cfg.state);
336
337                // restore normal position if it is valid (visible in a monitor)
338                let restore_rect: DipRect = cfg.restore_rect.cast();
339                let visible = MONITORS.available_monitors().iter().any(|m| m.dip_rect().intersects(&restore_rect));
340                if visible {
341                    vars.position().set(restore_rect.origin);
342                }
343                vars.size().set(restore_rect.size);
344            }
345        },
346        |required| {
347            let vars = WINDOW.vars();
348            let state = vars.state();
349            let rect = vars.restore_rect();
350            if required || state.is_new() || rect.is_new() {
351                Some(WindowStateCfg {
352                    state: state.get(),
353                    restore_rect: rect.get().cast(),
354                })
355            } else {
356                None
357            }
358        },
359    )
360}
361
362/// Defines if a widget load affects the parent window load.
363///
364/// Widgets that support this behavior have a `block_window_load` property.
365#[derive(Clone, Copy, Debug, PartialEq, Eq)]
366pub enum BlockWindowLoad {
367    /// Widget requests a [`WindowLoadingHandle`] and retains it until the widget is loaded.
368    ///
369    /// [`WindowLoadingHandle`]: zng_ext_window::WindowLoadingHandle
370    Enabled {
371        /// Handle expiration deadline, if the widget takes longer than this deadline the window loads anyway.
372        deadline: Deadline,
373    },
374    /// Widget does not hold back window load.
375    Disabled,
376}
377impl BlockWindowLoad {
378    /// Enabled value.
379    pub fn enabled(deadline: impl Into<Deadline>) -> BlockWindowLoad {
380        BlockWindowLoad::Enabled { deadline: deadline.into() }
381    }
382
383    /// Returns `true` if it is enabled.
384    pub fn is_enabled(self) -> bool {
385        matches!(self, Self::Enabled { .. })
386    }
387
388    /// Returns `true` if it is disabled.
389    pub fn is_disabled(self) -> bool {
390        matches!(self, Self::Disabled)
391    }
392
393    /// Returns the block deadline if it is enabled and the deadline has not expired.
394    pub fn deadline(self) -> Option<Deadline> {
395        match self {
396            BlockWindowLoad::Enabled { deadline } => {
397                if deadline.has_elapsed() {
398                    None
399                } else {
400                    Some(deadline)
401                }
402            }
403            BlockWindowLoad::Disabled => None,
404        }
405    }
406}
407impl_from_and_into_var! {
408    /// Converts `true` to `BlockWindowLoad::enabled(1.secs())` and `false` to `BlockWindowLoad::Disabled`.
409    fn from(enabled: bool) -> BlockWindowLoad {
410        if enabled {
411            BlockWindowLoad::enabled(1.secs())
412        } else {
413            BlockWindowLoad::Disabled
414        }
415    }
416
417    /// Converts to enabled with the duration timeout.
418    fn from(enabled_timeout: Duration) -> BlockWindowLoad {
419        BlockWindowLoad::enabled(enabled_timeout)
420    }
421}
422
423/// Block window load until [`CONFIG.status`] is idle.
424///
425/// This property is enabled by default in the `Window!` widget.
426///
427/// [`CONFIG.status`]: CONFIG::status
428#[property(CONTEXT, default(false), widget_impl(Window))]
429pub fn config_block_window_load(child: impl UiNode, enabled: impl IntoValue<BlockWindowLoad>) -> impl UiNode {
430    let enabled = enabled.into();
431
432    enum State {
433        Allow,
434        Block {
435            _handle: WindowLoadingHandle,
436            cfg: BoxedVar<ConfigStatus>,
437        },
438    }
439    let mut state = State::Allow;
440
441    match_node(child, move |_, op| match op {
442        UiNodeOp::Init => {
443            if let Some(delay) = enabled.deadline() {
444                let cfg = CONFIG.status();
445                if !cfg.get().is_idle() {
446                    if let Some(_handle) = WINDOW.loading_handle(delay) {
447                        WIDGET.sub_var(&cfg);
448                        state = State::Block { _handle, cfg };
449                    }
450                }
451            }
452        }
453        UiNodeOp::Deinit => {
454            state = State::Allow;
455        }
456        UiNodeOp::Update { .. } => {
457            if let State::Block { cfg, .. } = &state {
458                if cfg.get().is_idle() {
459                    state = State::Allow;
460                }
461            }
462        }
463        _ => {}
464    })
465}
466
467/// Gets if is not headless, [`chrome`] is `true`, [`state`] is not fullscreen but [`WINDOWS.system_chrome`]
468/// reports the system does not provide window decorations.
469///
470/// [`chrome`]: fn@chrome
471/// [`state`]: fn@state
472/// [`WINDOWS.system_chrome`]: WINDOWS::system_chrome
473#[property(EVENT, default(state_var()), widget_impl(Window))]
474pub fn needs_fallback_chrome(child: impl UiNode, needs: impl IntoVar<bool>) -> impl UiNode {
475    zng_wgt::node::bind_state_init(
476        child,
477        || {
478            if WINDOW.mode().is_headless() {
479                LocalVar(false).boxed()
480            } else {
481                let vars = WINDOW.vars();
482                expr_var! {
483                    *#{vars.chrome()} && #{WINDOWS.system_chrome()}.needs_custom() && !#{vars.state()}.is_fullscreen()
484                }
485                .boxed()
486            }
487        },
488        needs,
489    )
490}
491
492/// Gets if [`WINDOWS.system_chrome`] prefers custom chrome.
493///
494/// Note that you must set [`chrome`] to `false` when using this to provide a custom chrome.
495///
496/// [`chrome`]: fn@chrome
497/// [`WINDOWS.system_chrome`]: WINDOWS::system_chrome
498#[property(EVENT, default(state_var()), widget_impl(Window))]
499pub fn prefer_custom_chrome(child: impl UiNode, prefer: impl IntoVar<bool>) -> impl UiNode {
500    zng_wgt::node::bind_state(child, WINDOWS.system_chrome().map(|c| c.prefer_custom), prefer)
501}
502
503/// Adorner property specific for custom chrome overlays.
504///
505/// This property behaves exactly like [`adorner_fn`]. Using it instead of adorner frees the adorner property
506/// for other usage in the window instance or in derived window types.
507///
508/// Note that you can also set the `custom_chrome_padding_fn` to ensure that the content is not hidden behind the adorner.
509///
510/// [`adorner_fn`]: fn@adorner_fn
511#[property(FILL, default(WidgetFn::nil()), widget_impl(Window))]
512pub fn custom_chrome_adorner_fn(child: impl UiNode, custom_chrome: impl IntoVar<WidgetFn<()>>) -> impl UiNode {
513    adorner_fn(child, custom_chrome)
514}
515
516/// Extra padding for window content in windows that display a [`custom_chrome_adorner_fn`].
517///
518/// [`custom_chrome_adorner_fn`]: fn@custom_chrome_adorner_fn
519#[property(CHILD_LAYOUT, default(0), widget_impl(Window))]
520pub fn custom_chrome_padding_fn(child: impl UiNode, padding: impl IntoVar<SideOffsets>) -> impl UiNode {
521    zng_wgt_container::padding(child, padding)
522}