1use std::time::Duration;
2
3use zng_ext_config::{AnyConfig as _, CONFIG, ConfigKey, ConfigStatus, ConfigValue};
4use zng_ext_window::{
5 AutoSize, FrameCaptureMode, MONITORS, MonitorQuery, WINDOW_Ext as _, WINDOW_LOAD_EVENT, WINDOWS, WindowButton, WindowIcon,
6 WindowLoadingHandle, WindowState, WindowVars,
7};
8use zng_wgt::prelude::*;
9
10use serde::{Deserialize, Serialize};
11use zng_wgt_layer::adorner_fn;
12
13use super::Window;
14
15fn bind_window_var<T, V>(child: impl UiNode, user_var: impl IntoVar<T>, select: impl Fn(&WindowVars) -> V + Send + 'static) -> impl UiNode
16where
17 T: VarValue + PartialEq,
18 V: Var<T>,
19{
20 #[cfg(feature = "dyn_closure")]
21 let select: Box<dyn Fn(&WindowVars) -> V + Send> = Box::new(select);
22 bind_window_var_impl(child.cfg_boxed(), user_var.into_var(), select).cfg_boxed()
23}
24fn bind_window_var_impl<T, V>(
25 child: impl UiNode,
26 user_var: impl IntoVar<T>,
27 select: impl Fn(&WindowVars) -> V + Send + 'static,
28) -> impl UiNode
29where
30 T: VarValue + PartialEq,
31 V: Var<T>,
32{
33 let user_var = user_var.into_var();
34
35 match_node(child, move |_, op| {
36 if let UiNodeOp::Init = op {
37 let window_var = select(&WINDOW.vars());
38 if !user_var.capabilities().is_always_static() {
39 let binding = user_var.bind_bidi(&window_var);
40 WIDGET.push_var_handles(binding);
41 }
42 window_var.set_from(&user_var).unwrap();
43 }
44 })
45}
46
47macro_rules! set_properties {
49 ($(
50 $ident:ident: $Type:ty,
51 )+) => {
52 $(paste::paste! {
53 #[doc = "Binds the [`"$ident "`](fn@WindowVars::"$ident ") window var with the property value."]
54 #[property(CONTEXT, widget_impl(Window))]
57 pub fn $ident(child: impl UiNode, $ident: impl IntoVar<$Type>) -> impl UiNode {
58 bind_window_var(child, $ident, |w|w.$ident().clone())
59 }
60 })+
61 }
62}
63set_properties! {
64 position: Point,
65 monitor: MonitorQuery,
66
67 state: WindowState,
68
69 size: Size,
70 min_size: Size,
71 max_size: Size,
72
73 font_size: Length,
74
75 chrome: bool,
76 icon: WindowIcon,
77 title: Txt,
78
79 auto_size: AutoSize,
80 auto_size_origin: Point,
81
82 resizable: bool,
83 movable: bool,
84
85 always_on_top: bool,
86
87 visible: bool,
88 taskbar_visible: bool,
89
90 parent: Option<WindowId>,
91 modal: bool,
92
93 color_scheme: Option<ColorScheme>,
94 accent_color: Option<LightDark>,
95
96 frame_capture_mode: FrameCaptureMode,
97
98 enabled_buttons: WindowButton,
99}
100
101macro_rules! map_properties {
102 ($(
103 $ident:ident . $member:ident = $name:ident : $Type:ty,
104 )+) => {$(paste::paste! {
105 #[doc = "Binds the `"$member "` of the [`"$ident "`](fn@WindowVars::"$ident ") window var with the property value."]
106 #[property(CONTEXT, widget_impl(Window))]
109 pub fn $name(child: impl UiNode, $name: impl IntoVar<$Type>) -> impl UiNode {
110 bind_window_var(child, $name, |w|w.$ident().map_ref_bidi(|v| &v.$member, |v|&mut v.$member))
111 }
112 })+}
113}
114map_properties! {
115 position.x = x: Length,
116 position.y = y: Length,
117 size.width = width: Length,
118 size.height = height: Length,
119 min_size.width = min_width: Length,
120 min_size.height = min_height: Length,
121 max_size.width = max_width: Length,
122 max_size.height = max_height: Length,
123}
124
125#[property(CONTEXT, default(colors::WHITE), widget_impl(Window))]
132pub fn clear_color(child: impl UiNode, color: impl IntoVar<Rgba>) -> impl UiNode {
133 let clear_color = color.into_var();
134 match_node(child, move |_, op| match op {
135 UiNodeOp::Init => {
136 WIDGET.sub_var_render_update(&clear_color);
137 }
138 UiNodeOp::Render { frame } => {
139 frame.set_clear_color(clear_color.get());
140 }
141 UiNodeOp::RenderUpdate { update } => {
142 update.set_clear_color(clear_color.get());
143 }
144 _ => {}
145 })
146}
147
148#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
154pub enum SaveState {
155 Enabled {
157 key: Option<ConfigKey>,
164 },
165 Disabled,
167}
168impl Default for SaveState {
169 fn default() -> Self {
171 Self::enabled()
172 }
173}
174impl SaveState {
175 pub const fn enabled() -> Self {
177 Self::Enabled { key: None }
178 }
179
180 pub fn enabled_key(&self) -> Option<ConfigKey> {
187 match self {
188 Self::Enabled { key } => {
189 if key.is_some() {
190 return key.clone();
191 }
192 let mut try_win = true;
193 if let Some(wgt) = WIDGET.try_id() {
194 let name = wgt.name();
195 if !name.is_empty() {
196 return Some(formatx!("wgt-{name}"));
197 }
198 try_win = WIDGET.parent_id().is_none();
199 }
200 if try_win {
201 if let Some(win) = WINDOW.try_id() {
202 let name = win.name();
203 if !name.is_empty() {
204 return Some(formatx!("win-{name}"));
205 }
206 }
207 }
208 None
209 }
210 Self::Disabled => None,
211 }
212 }
213}
214impl_from_and_into_var! {
215 fn from(persist: bool) -> SaveState {
217 if persist {
218 SaveState::default()
219 } else {
220 SaveState::Disabled
221 }
222 }
223}
224
225pub fn save_state_node<S: ConfigValue>(
234 child: impl UiNode,
235 enabled: impl IntoValue<SaveState>,
236 mut on_load_restore: impl FnMut(Option<S>) + Send + 'static,
237 mut on_update_save: impl FnMut(bool) -> Option<S> + Send + 'static,
238) -> impl UiNode {
239 let enabled = enabled.into();
240 enum State<S: ConfigValue> {
241 Disabled,
242 AwaitingLoad,
243 Loaded,
244 LoadedWithCfg(BoxedVar<S>),
245 }
246 let mut state = State::Disabled;
247 match_node(child, move |_, op| match op {
248 UiNodeOp::Init => {
249 if let Some(key) = enabled.enabled_key() {
250 if WINDOW.is_loaded() {
251 if CONFIG.contains_key(key.clone()).get() {
252 let cfg = CONFIG.get(key, on_update_save(true).unwrap());
253 on_load_restore(Some(cfg.get()));
254 state = State::LoadedWithCfg(cfg);
255 } else {
256 on_load_restore(None);
257 state = State::Loaded;
258 }
259 } else {
260 WIDGET.sub_event(&WINDOW_LOAD_EVENT);
261 state = State::AwaitingLoad;
262 }
263 } else {
264 state = State::Disabled;
265 }
266 }
267 UiNodeOp::Deinit => {
268 state = State::Disabled;
269 }
270 UiNodeOp::Event { update } => {
271 if matches!(&state, State::AwaitingLoad) && WINDOW_LOAD_EVENT.has(update) {
272 if let Some(key) = enabled.enabled_key() {
273 if CONFIG.contains_key(key.clone()).get() {
274 let cfg = CONFIG.get(key, on_update_save(true).unwrap());
275 on_load_restore(Some(cfg.get()));
276 state = State::LoadedWithCfg(cfg);
277 } else {
278 on_load_restore(None);
279 state = State::Loaded;
280 }
281 } else {
282 state = State::Disabled;
284 }
285 }
286 }
287 UiNodeOp::Update { .. } => match &mut state {
288 State::LoadedWithCfg(cfg) => {
289 if let Some(new) = on_update_save(false) {
290 let _ = cfg.set(new);
291 }
292 }
293 State::Loaded => {
294 if let Some(new) = on_update_save(false) {
295 if let Some(key) = enabled.enabled_key() {
296 let cfg = CONFIG.insert(key, new.clone());
297 state = State::LoadedWithCfg(cfg);
298 } else {
299 state = State::Disabled;
300 }
301 }
302 }
303 _ => {}
304 },
305 _ => {}
306 })
307}
308
309#[property(CONTEXT, default(SaveState::Disabled), widget_impl(Window))]
319pub fn save_state(child: impl UiNode, enabled: impl IntoValue<SaveState>) -> impl UiNode {
320 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
321 struct WindowStateCfg {
322 state: WindowState,
323 restore_rect: euclid::Rect<f32, Dip>,
324 }
325 save_state_node::<WindowStateCfg>(
326 child,
327 enabled,
328 |cfg| {
329 let vars = WINDOW.vars();
330 let state = vars.state();
331 WIDGET.sub_var(&state).sub_var(&vars.restore_rect());
332
333 if let Some(cfg) = cfg {
334 state.set(cfg.state);
336
337 let restore_rect: DipRect = cfg.restore_rect.cast();
339 let visible = MONITORS.available_monitors().iter().any(|m| m.dip_rect().intersects(&restore_rect));
340 if visible {
341 vars.position().set(restore_rect.origin);
342 }
343 vars.size().set(restore_rect.size);
344 }
345 },
346 |required| {
347 let vars = WINDOW.vars();
348 let state = vars.state();
349 let rect = vars.restore_rect();
350 if required || state.is_new() || rect.is_new() {
351 Some(WindowStateCfg {
352 state: state.get(),
353 restore_rect: rect.get().cast(),
354 })
355 } else {
356 None
357 }
358 },
359 )
360}
361
362#[derive(Clone, Copy, Debug, PartialEq, Eq)]
366pub enum BlockWindowLoad {
367 Enabled {
371 deadline: Deadline,
373 },
374 Disabled,
376}
377impl BlockWindowLoad {
378 pub fn enabled(deadline: impl Into<Deadline>) -> BlockWindowLoad {
380 BlockWindowLoad::Enabled { deadline: deadline.into() }
381 }
382
383 pub fn is_enabled(self) -> bool {
385 matches!(self, Self::Enabled { .. })
386 }
387
388 pub fn is_disabled(self) -> bool {
390 matches!(self, Self::Disabled)
391 }
392
393 pub fn deadline(self) -> Option<Deadline> {
395 match self {
396 BlockWindowLoad::Enabled { deadline } => {
397 if deadline.has_elapsed() {
398 None
399 } else {
400 Some(deadline)
401 }
402 }
403 BlockWindowLoad::Disabled => None,
404 }
405 }
406}
407impl_from_and_into_var! {
408 fn from(enabled: bool) -> BlockWindowLoad {
410 if enabled {
411 BlockWindowLoad::enabled(1.secs())
412 } else {
413 BlockWindowLoad::Disabled
414 }
415 }
416
417 fn from(enabled_timeout: Duration) -> BlockWindowLoad {
419 BlockWindowLoad::enabled(enabled_timeout)
420 }
421}
422
423#[property(CONTEXT, default(false), widget_impl(Window))]
429pub fn config_block_window_load(child: impl UiNode, enabled: impl IntoValue<BlockWindowLoad>) -> impl UiNode {
430 let enabled = enabled.into();
431
432 enum State {
433 Allow,
434 Block {
435 _handle: WindowLoadingHandle,
436 cfg: BoxedVar<ConfigStatus>,
437 },
438 }
439 let mut state = State::Allow;
440
441 match_node(child, move |_, op| match op {
442 UiNodeOp::Init => {
443 if let Some(delay) = enabled.deadline() {
444 let cfg = CONFIG.status();
445 if !cfg.get().is_idle() {
446 if let Some(_handle) = WINDOW.loading_handle(delay) {
447 WIDGET.sub_var(&cfg);
448 state = State::Block { _handle, cfg };
449 }
450 }
451 }
452 }
453 UiNodeOp::Deinit => {
454 state = State::Allow;
455 }
456 UiNodeOp::Update { .. } => {
457 if let State::Block { cfg, .. } = &state {
458 if cfg.get().is_idle() {
459 state = State::Allow;
460 }
461 }
462 }
463 _ => {}
464 })
465}
466
467#[property(EVENT, default(state_var()), widget_impl(Window))]
474pub fn needs_fallback_chrome(child: impl UiNode, needs: impl IntoVar<bool>) -> impl UiNode {
475 zng_wgt::node::bind_state_init(
476 child,
477 || {
478 if WINDOW.mode().is_headless() {
479 LocalVar(false).boxed()
480 } else {
481 let vars = WINDOW.vars();
482 expr_var! {
483 *#{vars.chrome()} && #{WINDOWS.system_chrome()}.needs_custom() && !#{vars.state()}.is_fullscreen()
484 }
485 .boxed()
486 }
487 },
488 needs,
489 )
490}
491
492#[property(EVENT, default(state_var()), widget_impl(Window))]
499pub fn prefer_custom_chrome(child: impl UiNode, prefer: impl IntoVar<bool>) -> impl UiNode {
500 zng_wgt::node::bind_state(child, WINDOWS.system_chrome().map(|c| c.prefer_custom), prefer)
501}
502
503#[property(FILL, default(WidgetFn::nil()), widget_impl(Window))]
512pub fn custom_chrome_adorner_fn(child: impl UiNode, custom_chrome: impl IntoVar<WidgetFn<()>>) -> impl UiNode {
513 adorner_fn(child, custom_chrome)
514}
515
516#[property(CHILD_LAYOUT, default(0), widget_impl(Window))]
520pub fn custom_chrome_padding_fn(child: impl UiNode, padding: impl IntoVar<SideOffsets>) -> impl UiNode {
521 zng_wgt_container::padding(child, padding)
522}