zng_wgt_window/
window_properties.rs

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