Skip to main content

fyrox_ui/
window.rs

1// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
2//
3// Permission is hereby granted, free of charge, to any person obtaining a copy
4// of this software and associated documentation files (the "Software"), to deal
5// in the Software without restriction, including without limitation the rights
6// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7// copies of the Software, and to permit persons to whom the Software is
8// furnished to do so, subject to the following conditions:
9//
10// The above copyright notice and this permission notice shall be included in all
11// copies or substantial portions of the Software.
12//
13// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19// SOFTWARE.
20
21//! The Window widget provides a standard window that can contain another widget. See [`Window`] docs
22//! for more info and usage examples.
23
24use crate::button::Button;
25use crate::grid::Grid;
26use crate::message::MessageData;
27use crate::vector_image::VectorImage;
28use crate::{
29    border::BorderBuilder,
30    brush::Brush,
31    button::{ButtonBuilder, ButtonMessage},
32    core::{
33        algebra::Vector2, color::Color, log::Log, math::Rect, pool::Handle, reflect::prelude::*,
34        type_traits::prelude::*, uuid_provider, visitor::prelude::*,
35    },
36    decorator::DecoratorBuilder,
37    font::FontResource,
38    grid::{Column, GridBuilder, Row},
39    message::{CursorIcon, KeyCode, UiMessage},
40    navigation::NavigationLayerBuilder,
41    style::{resource::StyleResourceExt, Style, StyledProperty},
42    text::{Text, TextBuilder, TextMessage},
43    vector_image::{Primitive, VectorImageBuilder},
44    widget::{Widget, WidgetBuilder, WidgetMessage},
45    BuildContext, Control, HorizontalAlignment, RestrictionEntry, Thickness, UiNode, UserInterface,
46    VerticalAlignment,
47};
48use fyrox_core::pool::ObjectOrVariant;
49use fyrox_graph::{
50    constructor::{ConstructorProvider, GraphNodeConstructor},
51    SceneGraph,
52};
53use std::cell::RefCell;
54
55#[derive(Debug, Clone, Copy, PartialEq)]
56pub enum WindowAlignment {
57    /// No specific alignment.
58    None,
59    /// Center of the parent widget (in most cases - center of the screen, if the parent is not
60    /// specified).
61    Center,
62    /// Position (in local coordinates) relative to the left top corner of the parent widget
63    /// bounds. In most cases, it is just screen coordinates.
64    Position(Vector2<f32>),
65    /// Relative alignment to the specified widget.
66    Relative {
67        /// A handle of a node to which the sender of this message should be aligned to.
68        relative_to: Handle<UiNode>,
69        /// Horizontal alignment of the widget.
70        horizontal_alignment: HorizontalAlignment,
71        /// Vertical alignment of the widget.
72        vertical_alignment: VerticalAlignment,
73        /// Margins for each side.
74        margin: Thickness,
75    },
76}
77
78/// A set of possible messages that can be used to modify the state of a window or listen to changes
79/// in the window.
80#[derive(Debug, Clone, PartialEq)]
81pub enum WindowMessage {
82    /// Opens a window.
83    Open {
84        /// A flag that defines whether the window should be centered or not.
85        alignment: WindowAlignment,
86        /// Should the window be opened in modal mode or not.
87        modal: bool,
88        /// A flag that defines whether the content of the window should be focused when the window
89        /// is opening.
90        focus_content: bool,
91    },
92
93    /// Closes a window.
94    Close,
95
96    /// Minimizes a window - it differs from classic minimization in window managers,
97    /// instead of putting window in system tray, it just collapses internal content panel.
98    Minimize(bool),
99
100    /// Forces the window to take the inner size of the main application window.
101    Maximize(bool),
102
103    /// Whether window can be minimized by _ mark. false hides _ mark.
104    CanMinimize(bool),
105
106    /// Whether window can be closed by X mark. false hides X mark.
107    CanClose(bool),
108
109    /// Whether window can be resized by resize grips.
110    CanResize(bool),
111
112    /// Indicates that move has been started. You should never send this message by hand.
113    MoveStart,
114
115    /// Moves window to a new position in local coordinates.
116    Move(Vector2<f32>),
117
118    /// Indicated that move has ended. You should never send this message by hand.
119    MoveEnd,
120
121    /// Sets new window title.
122    Title(WindowTitle),
123
124    /// Safe border size defines "part" of a window that should always be on screen when dragged.
125    /// It is used to prevent moving window outside of main application window bounds, to still
126    /// be able to drag it.
127    SafeBorderSize(Option<Vector2<f32>>),
128}
129impl MessageData for WindowMessage {}
130
131/// The state of a window's size, as controlled by the buttons in the top-right corner.
132#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Visit, Reflect)]
133pub enum WindowSizeState {
134    /// The window is neither maximized nor minimized, showing its content and free to move around the screen.
135    #[default]
136    Normal,
137    /// The window is hiding its content.
138    Minimized,
139    /// The window is filling the screen.
140    Maximized,
141}
142
143/// The Window widget provides a standard window that can contain another widget. Based on setting,
144/// windows can be configured so users can do any of the following:
145///
146/// * Movable by the user. Not configurable.
147/// * Have title text on the title bar. Set by the *with_title* function.
148/// * Able to be exited by the user. Set by the *can_close* function.
149/// * Able to be minimized to just the Title bar, and of course maximized again. Set by the
150/// *can_minimize* function.
151/// * Able to resize the window. Set by the *can_resize* function.
152///
153/// As with other UI elements, you create and configure the window using the WindowBuilder.
154///
155/// ```rust,no_run
156/// # use fyrox_ui::{
157/// #     core::{pool::Handle, algebra::Vector2},
158/// #     window::{WindowBuilder, WindowTitle},
159/// #     text::TextBuilder,
160/// #     widget::WidgetBuilder,
161/// #     UiNode,
162/// #     UserInterface
163/// # };
164/// fn create_window(ui: &mut UserInterface) {
165///     WindowBuilder::new(
166///         WidgetBuilder::new()
167///             .with_desired_position(Vector2::new(300.0, 0.0))
168///             .with_width(300.0),
169///     )
170///     .with_content(
171///         TextBuilder::new(WidgetBuilder::new())
172///             .with_text("Example Window content.")
173///             .build(&mut ui.build_ctx())
174///     )
175///     .with_title(WindowTitle::text("Window"))
176///     .can_close(true)
177///     .can_minimize(true)
178///     .open(true)
179///     .can_resize(false)
180///     .build(&mut ui.build_ctx());
181/// }
182/// ```
183///
184/// You will likely want to constrain the initial size of the window to something as shown in the
185/// example by providing a set width and/or height to the base WidgetBuilder. Otherwise, it will
186/// expand to fit its content.
187///
188/// You may also want to set an initial position with the *with_desired_position* function called
189/// on the base WidgetBuilder which sets the position of the window's top-left corner. Otherwise, all
190/// your windows will start with its top-left corner at 0,0 and be stacked on top of each other.
191///
192/// Windows can only contain a single direct child widget, set by using the *with_content* function.
193/// Additional calls to *with_content* replace the widgets given in previous calls, and the old
194/// widgets exist outside the window, so you should delete old widgets before changing a window's
195/// widget. If you want multiple widgets, you need to use one of the layout container widgets like
196/// the Grid, Stack Panel, etc. then add the additional widgets to that widget as needed.
197///
198/// The Window is a user editable object, but can only be affected by UI Messages they trigger if
199/// the message's corresponding variable has been set to true aka what is set by the *can_close*,
200/// *can_minimize*, and *can_resize* functions.
201///
202/// ## Initial Open State
203///
204/// By default, the window will be created in the open, or maximized, state. You can manually set
205/// this state via the *open* function providing a true or false as desired.
206///
207/// ## Styling the Buttons
208///
209/// The window close and minimize buttons can be configured with the *with_close_button* and
210/// *with_minimize_button* functions. You will want to pass them a button widget, but can do anything
211/// else you like past that.
212///
213/// ## Modal (AKA Forced Focus)
214///
215/// A Modal in UI design terms indicates a window or box that has forced focus. The user is not able
216/// to interact with anything else until the modal is dismissed.
217///
218/// Any window can be set and unset as a modal via the *modal* function.
219#[derive(Default, Clone, Visit, Reflect, Debug, ComponentProvider)]
220#[reflect(derived_type = "UiNode")]
221pub struct Window {
222    /// Base widget of the window.
223    pub widget: Widget,
224    /// The text to put in the tab for this window.
225    tab_label: String,
226    /// Mouse click position.
227    pub mouse_click_pos: Vector2<f32>,
228    /// Initial mouse position.
229    pub initial_position: Vector2<f32>,
230    /// Initial size of the window.
231    pub initial_size: Vector2<f32>,
232    /// Whether the window is being dragged or not.
233    pub is_dragging: bool,
234    /// Whether the window is minimized, maximized, or normal.
235    pub size_state: WindowSizeState,
236    /// Whether the window can be minimized or not.
237    pub can_minimize: bool,
238    /// Whether the window can be maximized or not.
239    pub can_maximize: bool,
240    /// Whether the window can be closed or not.
241    pub can_close: bool,
242    /// Whether the window can be resized or not.
243    pub can_resize: bool,
244    /// Handle of a header widget.
245    pub header: Handle<UiNode>,
246    /// Handle of a minimize button.
247    pub minimize_button: Handle<Button>,
248    /// Handle of a maximize button.
249    pub maximize_button: Handle<Button>,
250    /// Handle of a close button.
251    pub close_button: Handle<Button>,
252    /// A distance per each axis when the dragging starts.
253    pub drag_delta: Vector2<f32>,
254    /// Handle of a current content.
255    pub content: Handle<UiNode>,
256    /// Eight grips of the window that are used to resize the window.
257    pub grips: RefCell<[Grip; 8]>,
258    /// Handle of a title widget of the window.
259    pub title: Handle<UiNode>,
260    /// Handle of a container widget of the title.
261    pub title_grid: Handle<Grid>,
262    /// Optional size of the border around the screen in which the window will be forced to stay.
263    pub safe_border_size: Option<Vector2<f32>>,
264    /// Bounds of the window before maximization, it is used to return the window to previous
265    /// size when it is either "unmaximized" or dragged.
266    pub prev_bounds: Option<Rect<f32>>,
267    /// If `true`, then the window can be closed using `Esc` key. Default is `true`. Works only if
268    /// `can_close` is also `true`.
269    pub close_by_esc: bool,
270    /// If `true`, then the window will be deleted after closing.
271    pub remove_on_close: bool,
272}
273
274impl ConstructorProvider<UiNode, UserInterface> for Window {
275    fn constructor() -> GraphNodeConstructor<UiNode, UserInterface> {
276        GraphNodeConstructor::new::<Self>()
277            .with_variant("Window", |ui| {
278                WindowBuilder::new(WidgetBuilder::new().with_name("Window"))
279                    .build(&mut ui.build_ctx())
280                    .to_base()
281                    .into()
282            })
283            .with_group("Layout")
284    }
285}
286
287const GRIP_SIZE: f32 = 6.0;
288const CORNER_GRIP_SIZE: f32 = GRIP_SIZE * 2.0;
289
290/// Kind of a resizing grip.
291#[derive(Copy, Clone, Debug, Visit, Reflect, Default)]
292pub enum GripKind {
293    /// Left-top corner grip.
294    #[default]
295    LeftTopCorner = 0,
296    /// Right-top corner grip.
297    RightTopCorner = 1,
298    /// Right-bottom corner grip.
299    RightBottomCorner = 2,
300    /// Left-bottom corner grip.
301    LeftBottomCorner = 3,
302    /// Left corner grip.
303    Left = 4,
304    /// Top corner grip.
305    Top = 5,
306    /// Right corner grip.
307    Right = 6,
308    /// Bottom corner grip.
309    Bottom = 7,
310}
311
312/// Resizing grip.
313#[derive(Clone, Visit, Default, Debug, Reflect)]
314pub struct Grip {
315    /// Kind of the grip.
316    pub kind: GripKind,
317    /// Bounds of the grip in local-space.
318    pub bounds: Rect<f32>,
319    /// A flag, that is raised when the grip is being dragged.
320    pub is_dragging: bool,
321    /// Cursor type of the grip.
322    pub cursor: CursorIcon,
323}
324
325impl Grip {
326    fn new(kind: GripKind, cursor: CursorIcon) -> Self {
327        Self {
328            kind,
329            bounds: Default::default(),
330            is_dragging: false,
331            cursor,
332        }
333    }
334}
335
336crate::define_widget_deref!(Window);
337
338uuid_provider!(Window = "9331bf32-8614-4005-874c-5239e56bb15e");
339
340impl Control for Window {
341    fn arrange_override(&self, ui: &UserInterface, final_size: Vector2<f32>) -> Vector2<f32> {
342        let size = self.widget.arrange_override(ui, final_size);
343
344        let mut grips = self.grips.borrow_mut();
345
346        // Adjust grips.
347        grips[GripKind::Left as usize].bounds =
348            Rect::new(0.0, GRIP_SIZE, GRIP_SIZE, final_size.y - GRIP_SIZE * 2.0);
349        grips[GripKind::Top as usize].bounds =
350            Rect::new(GRIP_SIZE, 0.0, final_size.x - GRIP_SIZE * 2.0, GRIP_SIZE);
351        grips[GripKind::Right as usize].bounds = Rect::new(
352            final_size.x - GRIP_SIZE,
353            GRIP_SIZE,
354            GRIP_SIZE,
355            final_size.y - GRIP_SIZE * 2.0,
356        );
357        grips[GripKind::Bottom as usize].bounds = Rect::new(
358            GRIP_SIZE,
359            final_size.y - GRIP_SIZE,
360            final_size.x - GRIP_SIZE * 2.0,
361            GRIP_SIZE,
362        );
363
364        // Corners have different size to improve usability.
365        grips[GripKind::LeftTopCorner as usize].bounds =
366            Rect::new(0.0, 0.0, CORNER_GRIP_SIZE, CORNER_GRIP_SIZE);
367        grips[GripKind::RightTopCorner as usize].bounds = Rect::new(
368            final_size.x - GRIP_SIZE,
369            0.0,
370            CORNER_GRIP_SIZE,
371            CORNER_GRIP_SIZE,
372        );
373        grips[GripKind::RightBottomCorner as usize].bounds = Rect::new(
374            final_size.x - CORNER_GRIP_SIZE,
375            final_size.y - CORNER_GRIP_SIZE,
376            CORNER_GRIP_SIZE,
377            CORNER_GRIP_SIZE,
378        );
379        grips[GripKind::LeftBottomCorner as usize].bounds = Rect::new(
380            0.0,
381            final_size.y - CORNER_GRIP_SIZE,
382            CORNER_GRIP_SIZE,
383            CORNER_GRIP_SIZE,
384        );
385
386        size
387    }
388
389    fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
390        self.widget.handle_routed_message(ui, message);
391
392        if let Some(msg) = message.data::<WidgetMessage>() {
393            // Grip interaction have higher priority than other actions.
394            if self.can_resize && !self.is_dragging {
395                match msg {
396                    &WidgetMessage::MouseDown { pos, .. } => {
397                        ui.send(self.handle(), WidgetMessage::Topmost);
398
399                        if !self.maximized() {
400                            // Check grips.
401                            for grip in self.grips.borrow_mut().iter_mut() {
402                                let screen_bounds = grip.bounds.transform(&self.visual_transform);
403                                if screen_bounds.contains(pos) {
404                                    grip.is_dragging = true;
405                                    self.initial_position = self.screen_position();
406                                    self.initial_size = self.actual_local_size();
407                                    self.mouse_click_pos = pos;
408                                    ui.capture_mouse(self.handle());
409                                    break;
410                                }
411                            }
412                        }
413                    }
414                    WidgetMessage::MouseUp { .. } => {
415                        for grip in self.grips.borrow_mut().iter_mut() {
416                            if grip.is_dragging {
417                                ui.release_mouse_capture();
418                                grip.is_dragging = false;
419                                break;
420                            }
421                        }
422                    }
423                    &WidgetMessage::MouseMove { pos, .. } => {
424                        let mut new_cursor = None;
425
426                        if !self.maximized() {
427                            for grip in self.grips.borrow().iter() {
428                                let screen_bounds = grip.bounds.transform(&self.visual_transform);
429                                if grip.is_dragging || screen_bounds.contains(pos) {
430                                    new_cursor = Some(grip.cursor);
431                                }
432
433                                if grip.is_dragging {
434                                    let parent = ui.node(self.parent());
435                                    let click_pos = parent.screen_to_local(self.mouse_click_pos);
436                                    let current_pos = parent.screen_to_local(pos);
437                                    let delta = click_pos - current_pos;
438                                    let (dx, dy, dw, dh) = match grip.kind {
439                                        GripKind::Left => (-1.0, 0.0, 1.0, 0.0),
440                                        GripKind::Top => (0.0, -1.0, 0.0, 1.0),
441                                        GripKind::Right => (0.0, 0.0, -1.0, 0.0),
442                                        GripKind::Bottom => (0.0, 0.0, 0.0, -1.0),
443                                        GripKind::LeftTopCorner => (-1.0, -1.0, 1.0, 1.0),
444                                        GripKind::RightTopCorner => (0.0, -1.0, -1.0, 1.0),
445                                        GripKind::RightBottomCorner => (0.0, 0.0, -1.0, -1.0),
446                                        GripKind::LeftBottomCorner => (-1.0, 0.0, 1.0, -1.0),
447                                    };
448
449                                    let initial_position =
450                                        parent.screen_to_local(self.initial_position);
451                                    let new_pos = if self.minimized() {
452                                        initial_position + Vector2::new(delta.x * dx, 0.0)
453                                    } else {
454                                        initial_position + Vector2::new(delta.x * dx, delta.y * dy)
455                                    };
456                                    let new_size = self.initial_size
457                                        + Vector2::new(delta.x * dw, delta.y * dh);
458                                    let mut clamped_size = self.initial_size;
459                                    let mut clamped_pos = new_pos;
460
461                                    if dw != 0.0 {
462                                        clamped_size.x =
463                                            new_size.x.clamp(self.min_width(), self.max_width());
464                                        if dx != 0.0 {
465                                            let right_edge =
466                                                initial_position.x + self.initial_size.x;
467                                            clamped_pos.x = right_edge - clamped_size.x;
468                                        }
469                                    }
470
471                                    if dh != 0.0 {
472                                        clamped_size.y =
473                                            new_size.y.clamp(self.min_height(), self.max_height());
474                                        if dy != 0.0 {
475                                            let bottom_edge =
476                                                initial_position.y + self.initial_size.y;
477                                            clamped_pos.y = bottom_edge - clamped_size.y;
478                                        }
479                                    }
480
481                                    ui.send_many(
482                                        self.handle(),
483                                        [
484                                            WidgetMessage::DesiredPosition(clamped_pos),
485                                            WidgetMessage::Width(clamped_size.x),
486                                        ],
487                                    );
488                                    if !self.minimized() {
489                                        ui.send(
490                                            self.handle(),
491                                            WidgetMessage::Height(clamped_size.y),
492                                        );
493                                    }
494
495                                    break;
496                                }
497                            }
498                        }
499
500                        self.set_cursor(new_cursor);
501                    }
502                    _ => {}
503                }
504            } else {
505                // The window cannot be resized, so leave the cursor unset.
506                self.set_cursor(None);
507            }
508
509            if (message.destination() == self.header
510                || ui
511                    .node(self.header)
512                    .has_descendant(message.destination(), ui))
513                && !self.maximized()
514                && !message.handled()
515                && !self.has_active_grip()
516            {
517                match msg {
518                    WidgetMessage::MouseDown { pos, .. } => {
519                        self.mouse_click_pos = *pos;
520                        ui.send(self.handle, WindowMessage::MoveStart);
521                        message.set_handled(true);
522                    }
523                    WidgetMessage::MouseUp { .. } => {
524                        ui.send(self.handle, WindowMessage::MoveEnd);
525                        message.set_handled(true);
526                    }
527                    WidgetMessage::MouseMove { pos, .. } => {
528                        if self.is_dragging {
529                            self.drag_delta = *pos - self.mouse_click_pos;
530                            let new_pos = self.initial_position + self.drag_delta;
531                            ui.send(
532                                self.handle(),
533                                WindowMessage::Move(ui.screen_to_root_canvas_space(new_pos)),
534                            );
535                        }
536                        message.set_handled(true);
537                    }
538                    _ => (),
539                }
540            }
541            match msg {
542                WidgetMessage::Unlink => {
543                    if message.destination() == self.handle() {
544                        self.initial_position = self.screen_position();
545                    }
546                }
547                WidgetMessage::KeyDown(key_code)
548                    if self.close_by_esc
549                        && !self.is_docked(ui)
550                        && self.can_close
551                        && *key_code == KeyCode::Escape
552                        && !message.handled() =>
553                {
554                    ui.send(self.handle, WindowMessage::Close);
555                    message.set_handled(true);
556                }
557                _ => {}
558            }
559        } else if let Some(ButtonMessage::Click) = message.data::<ButtonMessage>() {
560            if message.destination() == self.minimize_button {
561                ui.send(self.handle(), WindowMessage::Minimize(!self.minimized()));
562            } else if message.destination() == self.maximize_button {
563                ui.send(self.handle(), WindowMessage::Maximize(!self.maximized()));
564            } else if message.destination() == self.close_button {
565                ui.send(self.handle(), WindowMessage::Close);
566            }
567        } else if let Some(msg) = message.data_for::<WindowMessage>(self.handle()) {
568            match msg {
569                &WindowMessage::Open {
570                    alignment,
571                    modal,
572                    focus_content,
573                } => {
574                    // Only manage this window's visibility if it is at the root.
575                    // Otherwise, it is part of something like a tile, and that parent should decide
576                    // whether the window is visible.
577                    if !self.visibility() && self.parent() == ui.root() {
578                        ui.send(self.handle(), WidgetMessage::Visibility(true));
579                        // If we are opening the window with non-finite width and height, something
580                        // has gone wrong, so correct it.
581                        if !self.width().is_finite() {
582                            Log::err(format!("Window width was {}", self.width()));
583                            self.set_width(200.0);
584                        }
585                        if !self.height().is_finite() {
586                            Log::err(format!("Window height was {}", self.height()));
587                            self.set_height(200.0);
588                        }
589                    }
590                    ui.send(self.handle(), WidgetMessage::Topmost);
591                    if focus_content {
592                        ui.send(self.content_to_focus(), WidgetMessage::Focus);
593                    }
594                    if modal && !ui.restricts_picking(self.handle()) {
595                        ui.push_picking_restriction(RestrictionEntry {
596                            handle: self.handle(),
597                            stop: true,
598                        });
599                    }
600                    match alignment {
601                        WindowAlignment::None => {}
602                        WindowAlignment::Center => {
603                            if self.parent() == ui.root() {
604                                ui.send(self.handle(), WidgetMessage::Center);
605                            }
606                        }
607                        WindowAlignment::Position(position) => {
608                            ui.send(self.handle(), WidgetMessage::DesiredPosition(position));
609                        }
610                        WindowAlignment::Relative {
611                            relative_to,
612                            horizontal_alignment,
613                            vertical_alignment,
614                            margin,
615                        } => {
616                            ui.send(
617                                self.handle(),
618                                WidgetMessage::Align {
619                                    relative_to,
620                                    horizontal_alignment,
621                                    vertical_alignment,
622                                    margin,
623                                },
624                            );
625                        }
626                    }
627                }
628                WindowMessage::Close => {
629                    if self.visibility() {
630                        ui.send(self.handle(), WidgetMessage::Visibility(false));
631                        ui.remove_picking_restriction(self.handle());
632                        if self.remove_on_close {
633                            ui.send(self.handle(), WidgetMessage::Remove);
634                        }
635                    }
636                }
637                &WindowMessage::Minimize(minimized) => {
638                    if minimized {
639                        self.update_size_state(WindowSizeState::Minimized, ui);
640                    } else {
641                        self.update_size_state(WindowSizeState::Normal, ui);
642                    }
643                }
644                &WindowMessage::Maximize(maximized) => {
645                    if maximized {
646                        self.update_size_state(WindowSizeState::Maximized, ui);
647                    } else {
648                        self.update_size_state(WindowSizeState::Normal, ui);
649                    }
650                }
651                &WindowMessage::CanMinimize(value) => {
652                    if self.can_minimize != value {
653                        self.can_minimize = value;
654                        if self.minimize_button.is_some() {
655                            ui.send(self.minimize_button, WidgetMessage::Visibility(value));
656                        }
657                    }
658                }
659                &WindowMessage::CanClose(value) => {
660                    if self.can_close != value {
661                        self.can_close = value;
662                        if self.close_button.is_some() {
663                            ui.send(self.close_button, WidgetMessage::Visibility(value));
664                        }
665                    }
666                }
667                &WindowMessage::CanResize(value) => {
668                    if self.can_resize != value {
669                        self.can_resize = value;
670                        ui.try_send_response(message);
671                    }
672                }
673                &WindowMessage::Move(mut new_pos) => {
674                    if let Some(safe_border) = self.safe_border_size {
675                        // Clamp new position in allowed bounds. This will prevent moving the window outside the main
676                        // application window, thus leaving an opportunity to drag the window to some other place.
677                        new_pos.x = new_pos.x.clamp(
678                            -(self.actual_local_size().x - safe_border.x).abs(),
679                            (ui.screen_size().x - safe_border.x).abs(),
680                        );
681                        new_pos.y = new_pos
682                            .y
683                            .clamp(0.0, (ui.screen_size().y - safe_border.y).abs());
684                    }
685
686                    if self.is_dragging && self.desired_local_position() != new_pos {
687                        ui.send(self.handle(), WidgetMessage::DesiredPosition(new_pos));
688                        ui.try_send_response(message);
689                    }
690                }
691                WindowMessage::MoveStart => {
692                    if !self.is_dragging {
693                        ui.capture_mouse(self.header);
694                        let initial_position = self.screen_position();
695                        self.initial_position = initial_position;
696                        self.is_dragging = true;
697
698                        if self.size_state == WindowSizeState::Maximized {
699                            self.size_state = WindowSizeState::Normal;
700                            if let Some(prev_bounds) = self.prev_bounds.take() {
701                                ui.send_many(
702                                    self.handle,
703                                    [
704                                        WidgetMessage::Width(prev_bounds.w()),
705                                        WidgetMessage::Height(prev_bounds.h()),
706                                    ],
707                                );
708                            }
709                        }
710
711                        ui.try_send_response(message);
712                    }
713                }
714                WindowMessage::MoveEnd => {
715                    if self.is_dragging {
716                        ui.release_mouse_capture();
717                        self.is_dragging = false;
718
719                        ui.try_send_response(message);
720                    }
721                }
722                WindowMessage::Title(title) => {
723                    match title {
724                        WindowTitle::Text {
725                            text,
726                            font,
727                            font_size,
728                        } => {
729                            if ui.try_get_of_type::<Text>(self.title).is_ok() {
730                                // Just modify existing text, this is much faster than
731                                // re-create text everytime.
732                                ui.send(self.title, TextMessage::Text(text.clone()));
733                                if let Some(font) = font {
734                                    ui.send(self.title, TextMessage::Font(font.clone()))
735                                }
736                                if let Some(font_size) = font_size {
737                                    ui.send(self.title, TextMessage::FontSize(font_size.clone()));
738                                }
739                            } else {
740                                ui.send(self.title, WidgetMessage::Remove);
741                                let font = font.clone().unwrap_or_else(|| ui.default_font.clone());
742                                let ctx = &mut ui.build_ctx();
743                                self.title = make_text_title(
744                                    ctx,
745                                    text,
746                                    font,
747                                    font_size
748                                        .clone()
749                                        .unwrap_or_else(|| ctx.style.property(Style::FONT_SIZE)),
750                                )
751                                .to_base();
752                                ui.send(self.title, WidgetMessage::link_with(self.title_grid));
753                            }
754                        }
755                        WindowTitle::Node(node) => {
756                            if self.title.is_some() {
757                                // Remove old title.
758                                ui.send(self.title, WidgetMessage::Remove);
759                            }
760
761                            if node.is_some() {
762                                self.title = *node;
763
764                                // Attach new one.
765                                ui.send(self.title, WidgetMessage::link_with(self.title_grid));
766                            }
767                        }
768                    }
769                }
770                WindowMessage::SafeBorderSize(size) => {
771                    if &self.safe_border_size != size {
772                        self.safe_border_size = *size;
773                        ui.try_send_response(message);
774                    }
775                }
776            }
777        }
778    }
779}
780
781impl Window {
782    pub fn tab_label(&self) -> &str {
783        &self.tab_label
784    }
785    pub fn minimized(&self) -> bool {
786        self.size_state == WindowSizeState::Minimized
787    }
788    pub fn maximized(&self) -> bool {
789        self.size_state == WindowSizeState::Maximized
790    }
791    pub fn normal(&self) -> bool {
792        self.size_state == WindowSizeState::Normal
793    }
794    fn update_size_state(&mut self, new_state: WindowSizeState, ui: &mut UserInterface) {
795        if self.size_state == new_state {
796            return;
797        }
798        if new_state == WindowSizeState::Maximized && !self.can_resize {
799            return;
800        }
801        let current_size = self.actual_global_size();
802        let current_position = self.screen_position();
803        match self.size_state {
804            WindowSizeState::Normal => {
805                self.prev_bounds = Some(Rect::new(
806                    current_position.x,
807                    current_position.y,
808                    current_size.x,
809                    current_size.y,
810                ));
811            }
812            WindowSizeState::Minimized => {
813                self.prev_bounds = Some(Rect::new(
814                    current_position.x,
815                    current_position.y,
816                    current_size.x,
817                    self.prev_bounds.map(|b| b.size.y).unwrap_or(current_size.y),
818                ));
819            }
820            _ => (),
821        }
822        let new_bounds = match new_state {
823            WindowSizeState::Normal | WindowSizeState::Minimized => {
824                self.prev_bounds.unwrap_or_else(|| {
825                    Rect::new(
826                        current_position.x,
827                        current_position.y,
828                        current_size.x,
829                        current_size.y,
830                    )
831                })
832            }
833            WindowSizeState::Maximized => Rect::new(0.0, 0.0, ui.screen_size.x, ui.screen_size.y),
834        };
835
836        if self.content.is_some() {
837            ui.send(
838                self.content,
839                WidgetMessage::Visibility(new_state != WindowSizeState::Minimized),
840            );
841        }
842
843        if new_state == WindowSizeState::Minimized {
844            self.set_desired_local_position(new_bounds.position);
845            if self.can_resize {
846                self.set_width(new_bounds.w());
847            }
848            self.set_height(f32::NAN);
849            self.invalidate_layout();
850        } else {
851            ui.send(
852                self.handle,
853                WidgetMessage::DesiredPosition(new_bounds.position),
854            );
855            if self.can_resize {
856                ui.send_many(
857                    self.handle,
858                    [
859                        WidgetMessage::Width(new_bounds.w()),
860                        WidgetMessage::Height(new_bounds.h()),
861                    ],
862                );
863            }
864        }
865        self.size_state = new_state;
866    }
867    /// Checks whether any resizing grip is active or not.
868    pub fn has_active_grip(&self) -> bool {
869        for grip in self.grips.borrow().iter() {
870            if grip.is_dragging {
871                return true;
872            }
873        }
874        false
875    }
876
877    fn content_to_focus(&self) -> Handle<UiNode> {
878        if self.content.is_some() {
879            self.content
880        } else {
881            self.handle
882        }
883    }
884
885    fn is_docked(&self, ui: &UserInterface) -> bool {
886        self.parent() != ui.root_canvas
887    }
888}
889
890/// Window builder creates [`Window`] instances and adds them to the user interface.
891pub struct WindowBuilder {
892    /// Base widget builder.
893    pub widget_builder: WidgetBuilder,
894    /// Content of the window.
895    pub content: Handle<UiNode>,
896    /// Optional title of the window.
897    pub title: Option<WindowTitle>,
898    /// Label for the window's tab.
899    pub tab_label: String,
900    /// Whether the window can be closed or not.
901    pub can_close: bool,
902    /// Whether the window can be minimized or not.
903    pub can_minimize: bool,
904    /// Whether the window can be maximized or not.
905    pub can_maximize: bool,
906    /// Whether the window should be created open or not.
907    pub open: bool,
908    /// Optional custom closing button, if not specified, then a default button will be created.
909    pub close_button: Option<Handle<Button>>,
910    /// Optional custom minimization button, if not specified, then a default button will be created.
911    pub minimize_button: Option<Handle<Button>>,
912    /// Optional custom maximization button, if not specified, then a default button will be created.
913    pub maximize_button: Option<Handle<Button>>,
914    /// Whether the window should be created as modal or not. Warning: Any independent builders must
915    /// take this into account!
916    pub modal: bool,
917    /// Whether the window should be resizable or not.
918    pub can_resize: bool,
919    /// Optional size of the border around the screen in which the window will be forced to stay.
920    pub safe_border_size: Option<Vector2<f32>>,
921    /// If `true`, then the window can be closed using `Esc` key. Default is `true`. Works only if
922    /// `can_close` is also `true`.
923    pub close_by_esc: bool,
924    /// If `true`, then the window will be deleted after closing.
925    pub remove_on_close: bool,
926}
927
928/// Window title can be either text or node.
929///
930/// If `Text` is used, then builder will automatically create [`Text`] node with specified text,
931/// but with default font.
932///
933/// If you need more flexibility (i.e. put a picture near text) then [`WindowTitle::Node`] option
934/// is for you: it allows to put any UI node hierarchy you want to.
935#[derive(Debug, Clone, PartialEq)]
936pub enum WindowTitle {
937    Text {
938        /// Actual text of the title.
939        text: String,
940        /// Optional font, if [`None`], then the default font will be used.
941        font: Option<FontResource>,
942        /// Optional size of the text. Default is [`None`] (in this case default size defined by the
943        /// current style will be used).
944        font_size: Option<StyledProperty<f32>>,
945    },
946    Node(Handle<UiNode>),
947}
948
949impl WindowTitle {
950    /// A shortcut to create [`WindowTitle::Text`]
951    pub fn text<P: AsRef<str>>(text: P) -> Self {
952        WindowTitle::Text {
953            text: text.as_ref().to_owned(),
954            font: None,
955            font_size: None,
956        }
957    }
958
959    /// A shortcut to create [`WindowTitle::Text`] with custom font.
960    pub fn text_with_font<P: AsRef<str>>(text: P, font: FontResource) -> Self {
961        WindowTitle::Text {
962            text: text.as_ref().to_owned(),
963            font: Some(font),
964            font_size: None,
965        }
966    }
967
968    /// A shortcut to create [`WindowTitle::Text`] with custom font and size.
969    pub fn text_with_font_size<P: AsRef<str>>(
970        text: P,
971        font: FontResource,
972        size: StyledProperty<f32>,
973    ) -> Self {
974        WindowTitle::Text {
975            text: text.as_ref().to_owned(),
976            font: Some(font),
977            font_size: Some(size),
978        }
979    }
980
981    /// A shortcut to create [`WindowTitle::Node`]
982    pub fn node(node: Handle<UiNode>) -> Self {
983        Self::Node(node)
984    }
985}
986
987fn make_text_title(
988    ctx: &mut BuildContext,
989    text: &str,
990    font: FontResource,
991    size: StyledProperty<f32>,
992) -> Handle<Text> {
993    TextBuilder::new(
994        WidgetBuilder::new()
995            .with_margin(Thickness::left(5.0))
996            .on_row(0)
997            .on_column(0),
998    )
999    .with_font_size(size)
1000    .with_font(font)
1001    .with_vertical_text_alignment(VerticalAlignment::Center)
1002    .with_horizontal_text_alignment(HorizontalAlignment::Left)
1003    .with_text(text)
1004    .build(ctx)
1005}
1006
1007enum HeaderButton {
1008    Close,
1009    Minimize,
1010    Maximize,
1011}
1012
1013fn make_mark(ctx: &mut BuildContext, button: HeaderButton) -> Handle<VectorImage> {
1014    let size = 12.0;
1015
1016    VectorImageBuilder::new(
1017        WidgetBuilder::new()
1018            .with_horizontal_alignment(HorizontalAlignment::Center)
1019            .with_vertical_alignment(match button {
1020                HeaderButton::Close => VerticalAlignment::Center,
1021                HeaderButton::Minimize => VerticalAlignment::Bottom,
1022                HeaderButton::Maximize => VerticalAlignment::Center,
1023            })
1024            .with_width(size)
1025            .with_height(size)
1026            .with_foreground(ctx.style.property(Style::BRUSH_BRIGHT)),
1027    )
1028    .with_primitives(match button {
1029        HeaderButton::Close => {
1030            vec![
1031                Primitive::Line {
1032                    begin: Vector2::new(0.0, 0.0),
1033                    end: Vector2::new(size, size),
1034                    thickness: 1.0,
1035                },
1036                Primitive::Line {
1037                    begin: Vector2::new(size, 0.0),
1038                    end: Vector2::new(0.0, size),
1039                    thickness: 1.0,
1040                },
1041            ]
1042        }
1043        HeaderButton::Minimize => {
1044            let bottom_spacing = 3.0;
1045
1046            vec![Primitive::Line {
1047                begin: Vector2::new(0.0, size - bottom_spacing),
1048                end: Vector2::new(size, size - bottom_spacing),
1049                thickness: 1.0,
1050            }]
1051        }
1052        HeaderButton::Maximize => {
1053            let thickness = 1.25;
1054            let half_thickness = thickness * 0.5;
1055
1056            vec![
1057                Primitive::Line {
1058                    begin: Vector2::new(0.0, half_thickness),
1059                    end: Vector2::new(size, half_thickness),
1060                    thickness,
1061                },
1062                Primitive::Line {
1063                    begin: Vector2::new(size - half_thickness, 0.0),
1064                    end: Vector2::new(size - half_thickness, size),
1065                    thickness,
1066                },
1067                Primitive::Line {
1068                    begin: Vector2::new(size, size - half_thickness),
1069                    end: Vector2::new(0.0, size - half_thickness),
1070                    thickness,
1071                },
1072                Primitive::Line {
1073                    begin: Vector2::new(half_thickness, size),
1074                    end: Vector2::new(half_thickness, 0.0),
1075                    thickness,
1076                },
1077            ]
1078        }
1079    })
1080    .build(ctx)
1081}
1082
1083fn make_header_button(ctx: &mut BuildContext, button: HeaderButton) -> Handle<Button> {
1084    ButtonBuilder::new(WidgetBuilder::new().with_margin(Thickness::uniform(2.0)))
1085        .with_back(
1086            DecoratorBuilder::new(
1087                BorderBuilder::new(WidgetBuilder::new())
1088                    .with_stroke_thickness(Thickness::uniform(0.0).into())
1089                    .with_pad_by_corner_radius(false)
1090                    .with_corner_radius(4.0f32.into()),
1091            )
1092            .with_normal_brush(Brush::Solid(Color::TRANSPARENT).into())
1093            .with_hover_brush(ctx.style.property(Style::BRUSH_LIGHT))
1094            .with_pressed_brush(ctx.style.property(Style::BRUSH_LIGHTEST))
1095            .build(ctx),
1096        )
1097        .with_content(make_mark(ctx, button))
1098        .build(ctx)
1099}
1100
1101impl WindowBuilder {
1102    /// Creates new window builder.
1103    pub fn new(widget_builder: WidgetBuilder) -> Self {
1104        Self {
1105            widget_builder,
1106            content: Handle::NONE,
1107            title: None,
1108            tab_label: String::default(),
1109            can_close: true,
1110            can_minimize: true,
1111            can_maximize: true,
1112            open: true,
1113            close_button: None,
1114            minimize_button: None,
1115            maximize_button: None,
1116            modal: false,
1117            can_resize: true,
1118            safe_border_size: Some(Vector2::new(25.0, 20.0)),
1119            close_by_esc: true,
1120            remove_on_close: false,
1121        }
1122    }
1123
1124    /// Sets a desired window content.
1125    pub fn with_content(mut self, content: Handle<impl ObjectOrVariant<UiNode>>) -> Self {
1126        self.content = content.to_base();
1127        self
1128    }
1129
1130    /// Sets a desired window title.
1131    pub fn with_title(mut self, title: WindowTitle) -> Self {
1132        self.title = Some(title);
1133        self
1134    }
1135
1136    pub fn with_tab_label<S: Into<String>>(mut self, label: S) -> Self {
1137        self.tab_label = label.into();
1138        self
1139    }
1140
1141    /// Sets a desired minimization button.
1142    pub fn with_minimize_button(mut self, button: Handle<Button>) -> Self {
1143        self.minimize_button = Some(button);
1144        self
1145    }
1146
1147    /// Sets a desired maximization button.
1148    pub fn with_maximize_button(mut self, button: Handle<Button>) -> Self {
1149        self.minimize_button = Some(button);
1150        self
1151    }
1152
1153    /// Sets a desired closing button.
1154    pub fn with_close_button(mut self, button: Handle<Button>) -> Self {
1155        self.close_button = Some(button);
1156        self
1157    }
1158
1159    /// Sets whether the window can be closed or not.
1160    pub fn can_close(mut self, can_close: bool) -> Self {
1161        self.can_close = can_close;
1162        self
1163    }
1164
1165    /// Sets whether the window can be minimized or not.
1166    pub fn can_minimize(mut self, can_minimize: bool) -> Self {
1167        self.can_minimize = can_minimize;
1168        self
1169    }
1170
1171    /// Sets whether the window can be maximized or not.
1172    pub fn can_maximize(mut self, can_minimize: bool) -> Self {
1173        self.can_maximize = can_minimize;
1174        self
1175    }
1176
1177    /// Sets whether the window should be open or not.
1178    pub fn open(mut self, open: bool) -> Self {
1179        self.open = open;
1180        self
1181    }
1182
1183    /// Sets whether the window should be modal or not.
1184    pub fn modal(mut self, modal: bool) -> Self {
1185        self.modal = modal;
1186        self
1187    }
1188
1189    /// Sets whether the window can be resized or not.
1190    pub fn can_resize(mut self, can_resize: bool) -> Self {
1191        self.can_resize = can_resize;
1192        self
1193    }
1194
1195    /// Sets a desired safe border size.
1196    pub fn with_safe_border_size(mut self, size: Option<Vector2<f32>>) -> Self {
1197        self.safe_border_size = size.map(|s| Vector2::new(s.x.abs(), s.y.abs()));
1198        self
1199    }
1200
1201    /// Defines, whether the window can be closed using `Esc` key or not. Works only if `can_close`
1202    /// is also `true`.
1203    pub fn with_close_by_esc(mut self, close: bool) -> Self {
1204        self.close_by_esc = close;
1205        self
1206    }
1207
1208    /// Defines, whether the window should be deleted after closing or not. Default is `false`.
1209    pub fn with_remove_on_close(mut self, close: bool) -> Self {
1210        self.remove_on_close = close;
1211        self
1212    }
1213
1214    /// Finishes window building and returns its instance.
1215    pub fn build_window(self, ctx: &mut BuildContext) -> Window {
1216        let minimize_button;
1217        let maximize_button;
1218        let close_button;
1219        let title;
1220        let title_grid;
1221        let header = BorderBuilder::new(
1222            WidgetBuilder::new()
1223                .with_horizontal_alignment(HorizontalAlignment::Stretch)
1224                .with_height(22.0)
1225                .with_background(ctx.style.property(Style::BRUSH_DARKER))
1226                .with_child({
1227                    title_grid = GridBuilder::new(
1228                        WidgetBuilder::new()
1229                            .with_child({
1230                                title = match self.title {
1231                                    None => Handle::NONE,
1232                                    Some(window_title) => match window_title {
1233                                        WindowTitle::Node(node) => node,
1234                                        WindowTitle::Text {
1235                                            text,
1236                                            font,
1237                                            font_size,
1238                                        } => make_text_title(
1239                                            ctx,
1240                                            &text,
1241                                            font.unwrap_or_else(|| ctx.default_font()),
1242                                            font_size.unwrap_or_else(|| {
1243                                                ctx.style.property(Style::FONT_SIZE)
1244                                            }),
1245                                        )
1246                                        .to_base(),
1247                                    },
1248                                };
1249                                title
1250                            })
1251                            .with_child({
1252                                minimize_button = self.minimize_button.unwrap_or_else(|| {
1253                                    make_header_button(ctx, HeaderButton::Minimize)
1254                                });
1255                                ctx[minimize_button]
1256                                    .set_visibility(self.can_minimize)
1257                                    .set_width(20.0)
1258                                    .set_row(0)
1259                                    .set_column(1);
1260                                minimize_button
1261                            })
1262                            .with_child({
1263                                maximize_button = self.maximize_button.unwrap_or_else(|| {
1264                                    make_header_button(ctx, HeaderButton::Maximize)
1265                                });
1266                                ctx[maximize_button]
1267                                    .set_visibility(self.can_maximize)
1268                                    .set_width(20.0)
1269                                    .set_row(0)
1270                                    .set_column(2);
1271                                maximize_button
1272                            })
1273                            .with_child({
1274                                close_button = self.close_button.unwrap_or_else(|| {
1275                                    make_header_button(ctx, HeaderButton::Close)
1276                                });
1277                                ctx[close_button]
1278                                    .set_width(20.0)
1279                                    .set_visibility(self.can_close)
1280                                    .set_row(0)
1281                                    .set_column(3);
1282                                close_button
1283                            }),
1284                    )
1285                    .add_column(Column::stretch())
1286                    .add_column(Column::auto())
1287                    .add_column(Column::auto())
1288                    .add_column(Column::auto())
1289                    .add_row(Row::stretch())
1290                    .build(ctx);
1291                    title_grid
1292                })
1293                .on_row(0),
1294        )
1295        .with_pad_by_corner_radius(false)
1296        .with_corner_radius(4.0f32.into())
1297        .with_stroke_thickness(Thickness::uniform(0.0).into())
1298        .build(ctx)
1299        .to_base();
1300
1301        let border = BorderBuilder::new(
1302            WidgetBuilder::new()
1303                .with_foreground(ctx.style.property(Style::BRUSH_DARKER))
1304                .with_child(
1305                    GridBuilder::new(
1306                        WidgetBuilder::new()
1307                            .with_child(
1308                                NavigationLayerBuilder::new(
1309                                    WidgetBuilder::new().on_row(1).with_child(self.content),
1310                                )
1311                                .build(ctx),
1312                            )
1313                            .with_child(header),
1314                    )
1315                    .add_column(Column::stretch())
1316                    .add_row(Row::auto())
1317                    .add_row(Row::stretch())
1318                    .build(ctx),
1319                ),
1320        )
1321        .with_pad_by_corner_radius(false)
1322        .with_corner_radius(4.0f32.into())
1323        .with_stroke_thickness(Thickness::uniform(1.0).into())
1324        .build(ctx);
1325
1326        Window {
1327            widget: self
1328                .widget_builder
1329                .with_visibility(self.open)
1330                .with_child(border)
1331                .build(ctx),
1332            mouse_click_pos: Vector2::default(),
1333            initial_position: Vector2::default(),
1334            initial_size: Default::default(),
1335            is_dragging: false,
1336            size_state: WindowSizeState::default(),
1337            can_minimize: self.can_minimize,
1338            can_maximize: self.can_maximize,
1339            can_close: self.can_close,
1340            can_resize: self.can_resize,
1341            header,
1342            tab_label: self.tab_label,
1343            minimize_button,
1344            maximize_button,
1345            close_button,
1346            drag_delta: Default::default(),
1347            content: self.content,
1348            safe_border_size: self.safe_border_size,
1349            grips: RefCell::new([
1350                // Corners have priority
1351                Grip::new(GripKind::LeftTopCorner, CursorIcon::NwResize),
1352                Grip::new(GripKind::RightTopCorner, CursorIcon::NeResize),
1353                Grip::new(GripKind::RightBottomCorner, CursorIcon::SeResize),
1354                Grip::new(GripKind::LeftBottomCorner, CursorIcon::SwResize),
1355                Grip::new(GripKind::Left, CursorIcon::WResize),
1356                Grip::new(GripKind::Top, CursorIcon::NResize),
1357                Grip::new(GripKind::Right, CursorIcon::EResize),
1358                Grip::new(GripKind::Bottom, CursorIcon::SResize),
1359            ]),
1360            title,
1361            title_grid,
1362            prev_bounds: None,
1363            close_by_esc: self.close_by_esc,
1364            remove_on_close: self.remove_on_close,
1365        }
1366    }
1367
1368    /// Finishes window building and returns its handle.
1369    pub fn build(self, ctx: &mut BuildContext) -> Handle<Window> {
1370        let modal = self.modal;
1371        let open = self.open;
1372
1373        let node = self.build_window(ctx);
1374        let handle = ctx.add(node);
1375
1376        if modal && open {
1377            ctx.push_picking_restriction(RestrictionEntry {
1378                handle: handle.to_base(),
1379                stop: true,
1380            });
1381        }
1382
1383        handle.to_variant()
1384    }
1385}
1386
1387#[cfg(test)]
1388mod test {
1389    use crate::window::WindowBuilder;
1390    use crate::{test::test_widget_deletion, widget::WidgetBuilder};
1391
1392    #[test]
1393    fn test_deletion() {
1394        test_widget_deletion(|ctx| WindowBuilder::new(WidgetBuilder::new()).build(ctx));
1395    }
1396}