zng_wgt_window/
window_properties.rs

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