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
43macro_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 #[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 #[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]map_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#[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#[cfg(feature = "config")]
155#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
156pub enum SaveState {
157 Enabled {
159 key: Option<ConfigKey>,
166 },
167 Disabled,
169}
170#[cfg(feature = "config")]
171impl Default for SaveState {
172 fn default() -> Self {
174 Self::enabled()
175 }
176}
177#[cfg(feature = "config")]
178impl SaveState {
179 pub const fn enabled() -> Self {
181 Self::Enabled { key: None }
182 }
183
184 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 fn from(persist: bool) -> SaveState {
220 if persist { SaveState::default() } else { SaveState::Disabled }
221 }
222}
223
224#[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,
244 AwaitingLoad {
246 _window_load_handle: EventHandle,
247 _config_status_handle: VarHandle,
248 },
249 Loaded,
251 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 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 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 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 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 state = State::Disabled;
347 }
348 }
349 }
350 _ => {}
351 },
352 _ => {}
353 })
354}
355
356#[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 state.set(cfg.state);
384
385 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
416pub enum BlockWindowLoad {
417 Enabled {
421 deadline: Deadline,
423 },
424 Disabled,
426}
427impl BlockWindowLoad {
428 pub fn enabled(deadline: impl Into<Deadline>) -> BlockWindowLoad {
430 BlockWindowLoad::Enabled { deadline: deadline.into() }
431 }
432
433 pub fn is_enabled(self) -> bool {
435 matches!(self, Self::Enabled { .. })
436 }
437
438 pub fn is_disabled(self) -> bool {
440 matches!(self, Self::Disabled)
441 }
442
443 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 fn from(enabled: bool) -> BlockWindowLoad {
460 if enabled {
461 BlockWindowLoad::enabled(1.secs())
462 } else {
463 BlockWindowLoad::Disabled
464 }
465 }
466
467 fn from(enabled_timeout: Duration) -> BlockWindowLoad {
469 BlockWindowLoad::enabled(enabled_timeout)
470 }
471}
472
473#[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#[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#[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#[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#[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}