Skip to main content

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