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
39macro_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 #[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 #[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]map_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#[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#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
151pub enum SaveState {
152 Enabled {
154 key: Option<ConfigKey>,
161 },
162 Disabled,
164}
165impl Default for SaveState {
166 fn default() -> Self {
168 Self::enabled()
169 }
170}
171impl SaveState {
172 pub const fn enabled() -> Self {
174 Self::Enabled { key: None }
175 }
176
177 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 fn from(persist: bool) -> SaveState {
212 if persist { SaveState::default() } else { SaveState::Disabled }
213 }
214}
215
216pub 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,
235 AwaitingLoad {
237 _window_load_handle: EventHandle,
238 _config_status_handle: VarHandle,
239 },
240 Loaded,
242 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 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 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 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 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 state = State::Disabled;
338 }
339 }
340 }
341 _ => {}
342 },
343 _ => {}
344 })
345}
346
347#[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 state.set(cfg.state);
374
375 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
406pub enum BlockWindowLoad {
407 Enabled {
411 deadline: Deadline,
413 },
414 Disabled,
416}
417impl BlockWindowLoad {
418 pub fn enabled(deadline: impl Into<Deadline>) -> BlockWindowLoad {
420 BlockWindowLoad::Enabled { deadline: deadline.into() }
421 }
422
423 pub fn is_enabled(self) -> bool {
425 matches!(self, Self::Enabled { .. })
426 }
427
428 pub fn is_disabled(self) -> bool {
430 matches!(self, Self::Disabled)
431 }
432
433 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 fn from(enabled: bool) -> BlockWindowLoad {
450 if enabled {
451 BlockWindowLoad::enabled(1.secs())
452 } else {
453 BlockWindowLoad::Disabled
454 }
455 }
456
457 fn from(enabled_timeout: Duration) -> BlockWindowLoad {
459 BlockWindowLoad::enabled(enabled_timeout)
460 }
461}
462
463#[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#[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#[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#[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#[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}