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
44macro_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 #[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 #[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]map_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#[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#[cfg(feature = "config")]
159#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
160pub enum SaveState {
161 Enabled {
163 key: Option<ConfigKey>,
170 },
171 Disabled,
173}
174#[cfg(feature = "config")]
175impl Default for SaveState {
176 fn default() -> Self {
178 Self::enabled()
179 }
180}
181#[cfg(feature = "config")]
182impl SaveState {
183 pub const fn enabled() -> Self {
185 Self::Enabled { key: None }
186 }
187
188 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 fn from(persist: bool) -> SaveState {
224 if persist { SaveState::default() } else { SaveState::Disabled }
225 }
226}
227
228#[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,
248 AwaitingLoad {
250 _window_load_handle: VarHandle,
251 _config_status_handle: VarHandle,
252 },
253 Loaded,
255 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 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 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 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 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 state = State::Disabled;
350 }
351 }
352 }
353 }
354 _ => {}
355 },
356 _ => {}
357 })
358}
359
360#[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 cfg.state = WindowState::Normal;
391 }
392
393 state.set(cfg.state);
395
396 let restore_rect: DipRect = cfg.restore_rect.cast();
398 vars.position().set(restore_rect.origin);
399 vars.size().set(restore_rect.size);
400
401 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 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
525pub enum BlockWindowLoad {
526 Enabled {
530 deadline: Deadline,
532 },
533 Disabled,
535}
536impl BlockWindowLoad {
537 pub fn enabled(deadline: impl Into<Deadline>) -> BlockWindowLoad {
539 BlockWindowLoad::Enabled { deadline: deadline.into() }
540 }
541
542 pub fn is_enabled(self) -> bool {
544 matches!(self, Self::Enabled { .. })
545 }
546
547 pub fn is_disabled(self) -> bool {
549 matches!(self, Self::Disabled)
550 }
551
552 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 fn from(enabled: bool) -> BlockWindowLoad {
569 if enabled {
570 BlockWindowLoad::enabled(1.secs())
571 } else {
572 BlockWindowLoad::Disabled
573 }
574 }
575
576 fn from(enabled_timeout: Duration) -> BlockWindowLoad {
578 BlockWindowLoad::enabled(enabled_timeout)
579 }
580}
581
582#[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#[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 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#[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#[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}