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 delta = self.mouse_click_pos - pos;
435                                    let (dx, dy, dw, dh) = match grip.kind {
436                                        GripKind::Left => (-1.0, 0.0, 1.0, 0.0),
437                                        GripKind::Top => (0.0, -1.0, 0.0, 1.0),
438                                        GripKind::Right => (0.0, 0.0, -1.0, 0.0),
439                                        GripKind::Bottom => (0.0, 0.0, 0.0, -1.0),
440                                        GripKind::LeftTopCorner => (-1.0, -1.0, 1.0, 1.0),
441                                        GripKind::RightTopCorner => (0.0, -1.0, -1.0, 1.0),
442                                        GripKind::RightBottomCorner => (0.0, 0.0, -1.0, -1.0),
443                                        GripKind::LeftBottomCorner => (-1.0, 0.0, 1.0, -1.0),
444                                    };
445
446                                    let new_pos = if self.minimized() {
447                                        self.initial_position + Vector2::new(delta.x * dx, 0.0)
448                                    } else {
449                                        self.initial_position
450                                            + Vector2::new(delta.x * dx, delta.y * dy)
451                                    };
452                                    let new_size = self.initial_size
453                                        + Vector2::new(delta.x * dw, delta.y * dh);
454
455                                    if new_size.x > self.min_width()
456                                        && new_size.x < self.max_width()
457                                        && new_size.y > self.min_height()
458                                        && new_size.y < self.max_height()
459                                    {
460                                        ui.send_many(
461                                            self.handle(),
462                                            [
463                                                WidgetMessage::DesiredPosition(
464                                                    ui.screen_to_root_canvas_space(new_pos),
465                                                ),
466                                                WidgetMessage::Width(new_size.x),
467                                            ],
468                                        );
469                                        if !self.minimized() {
470                                            ui.send(
471                                                self.handle(),
472                                                WidgetMessage::Height(new_size.y),
473                                            );
474                                        }
475                                    }
476
477                                    break;
478                                }
479                            }
480                        }
481
482                        self.set_cursor(new_cursor);
483                    }
484                    _ => {}
485                }
486            } else {
487                // The window cannot be resized, so leave the cursor unset.
488                self.set_cursor(None);
489            }
490
491            if (message.destination() == self.header
492                || ui
493                    .node(self.header)
494                    .has_descendant(message.destination(), ui))
495                && !self.maximized()
496                && !message.handled()
497                && !self.has_active_grip()
498            {
499                match msg {
500                    WidgetMessage::MouseDown { pos, .. } => {
501                        self.mouse_click_pos = *pos;
502                        ui.send(self.handle, WindowMessage::MoveStart);
503                        message.set_handled(true);
504                    }
505                    WidgetMessage::MouseUp { .. } => {
506                        ui.send(self.handle, WindowMessage::MoveEnd);
507                        message.set_handled(true);
508                    }
509                    WidgetMessage::MouseMove { pos, .. } => {
510                        if self.is_dragging {
511                            self.drag_delta = *pos - self.mouse_click_pos;
512                            let new_pos = self.initial_position + self.drag_delta;
513                            ui.send(
514                                self.handle(),
515                                WindowMessage::Move(ui.screen_to_root_canvas_space(new_pos)),
516                            );
517                        }
518                        message.set_handled(true);
519                    }
520                    _ => (),
521                }
522            }
523            match msg {
524                WidgetMessage::Unlink => {
525                    if message.destination() == self.handle() {
526                        self.initial_position = self.screen_position();
527                    }
528                }
529                WidgetMessage::KeyDown(key_code)
530                    if self.close_by_esc
531                        && !self.is_docked(ui)
532                        && self.can_close
533                        && *key_code == KeyCode::Escape
534                        && !message.handled() =>
535                {
536                    ui.send(self.handle, WindowMessage::Close);
537                    message.set_handled(true);
538                }
539                _ => {}
540            }
541        } else if let Some(ButtonMessage::Click) = message.data::<ButtonMessage>() {
542            if message.destination() == self.minimize_button {
543                ui.send(self.handle(), WindowMessage::Minimize(!self.minimized()));
544            } else if message.destination() == self.maximize_button {
545                ui.send(self.handle(), WindowMessage::Maximize(!self.maximized()));
546            } else if message.destination() == self.close_button {
547                ui.send(self.handle(), WindowMessage::Close);
548            }
549        } else if let Some(msg) = message.data_for::<WindowMessage>(self.handle()) {
550            match msg {
551                &WindowMessage::Open {
552                    alignment,
553                    modal,
554                    focus_content,
555                } => {
556                    // Only manage this window's visibility if it is at the root.
557                    // Otherwise, it is part of something like a tile, and that parent should decide
558                    // whether the window is visible.
559                    if !self.visibility() && self.parent() == ui.root() {
560                        ui.send(self.handle(), WidgetMessage::Visibility(true));
561                        // If we are opening the window with non-finite width and height, something
562                        // has gone wrong, so correct it.
563                        if !self.width().is_finite() {
564                            Log::err(format!("Window width was {}", self.width()));
565                            self.set_width(200.0);
566                        }
567                        if !self.height().is_finite() {
568                            Log::err(format!("Window height was {}", self.height()));
569                            self.set_height(200.0);
570                        }
571                    }
572                    ui.send(self.handle(), WidgetMessage::Topmost);
573                    if focus_content {
574                        ui.send(self.content_to_focus(), WidgetMessage::Focus);
575                    }
576                    if modal && !ui.restricts_picking(self.handle()) {
577                        ui.push_picking_restriction(RestrictionEntry {
578                            handle: self.handle(),
579                            stop: true,
580                        });
581                    }
582                    match alignment {
583                        WindowAlignment::None => {}
584                        WindowAlignment::Center => {
585                            if self.parent() == ui.root() {
586                                ui.send(self.handle(), WidgetMessage::Center);
587                            }
588                        }
589                        WindowAlignment::Position(position) => {
590                            ui.send(self.handle(), WidgetMessage::DesiredPosition(position));
591                        }
592                        WindowAlignment::Relative {
593                            relative_to,
594                            horizontal_alignment,
595                            vertical_alignment,
596                            margin,
597                        } => {
598                            ui.send(
599                                self.handle(),
600                                WidgetMessage::Align {
601                                    relative_to,
602                                    horizontal_alignment,
603                                    vertical_alignment,
604                                    margin,
605                                },
606                            );
607                        }
608                    }
609                }
610                WindowMessage::Close => {
611                    if self.visibility() {
612                        ui.send(self.handle(), WidgetMessage::Visibility(false));
613                        ui.remove_picking_restriction(self.handle());
614                        if self.remove_on_close {
615                            ui.send(self.handle(), WidgetMessage::Remove);
616                        }
617                    }
618                }
619                &WindowMessage::Minimize(minimized) => {
620                    if minimized {
621                        self.update_size_state(WindowSizeState::Minimized, ui);
622                    } else {
623                        self.update_size_state(WindowSizeState::Normal, ui);
624                    }
625                }
626                &WindowMessage::Maximize(maximized) => {
627                    if maximized {
628                        self.update_size_state(WindowSizeState::Maximized, ui);
629                    } else {
630                        self.update_size_state(WindowSizeState::Normal, ui);
631                    }
632                }
633                &WindowMessage::CanMinimize(value) => {
634                    if self.can_minimize != value {
635                        self.can_minimize = value;
636                        if self.minimize_button.is_some() {
637                            ui.send(self.minimize_button, WidgetMessage::Visibility(value));
638                        }
639                    }
640                }
641                &WindowMessage::CanClose(value) => {
642                    if self.can_close != value {
643                        self.can_close = value;
644                        if self.close_button.is_some() {
645                            ui.send(self.close_button, WidgetMessage::Visibility(value));
646                        }
647                    }
648                }
649                &WindowMessage::CanResize(value) => {
650                    if self.can_resize != value {
651                        self.can_resize = value;
652                        ui.send_message(message.reverse());
653                    }
654                }
655                &WindowMessage::Move(mut new_pos) => {
656                    if let Some(safe_border) = self.safe_border_size {
657                        // Clamp new position in allowed bounds. This will prevent moving the window outside the main
658                        // application window, thus leaving an opportunity to drag the window to some other place.
659                        new_pos.x = new_pos.x.clamp(
660                            -(self.actual_local_size().x - safe_border.x).abs(),
661                            (ui.screen_size().x - safe_border.x).abs(),
662                        );
663                        new_pos.y = new_pos
664                            .y
665                            .clamp(0.0, (ui.screen_size().y - safe_border.y).abs());
666                    }
667
668                    if self.is_dragging && self.desired_local_position() != new_pos {
669                        ui.send(self.handle(), WidgetMessage::DesiredPosition(new_pos));
670                        ui.send_message(message.reverse());
671                    }
672                }
673                WindowMessage::MoveStart => {
674                    if !self.is_dragging {
675                        ui.capture_mouse(self.header);
676                        let initial_position = self.screen_position();
677                        self.initial_position = initial_position;
678                        self.is_dragging = true;
679
680                        if self.size_state == WindowSizeState::Maximized {
681                            self.size_state = WindowSizeState::Normal;
682                            if let Some(prev_bounds) = self.prev_bounds.take() {
683                                ui.send_many(
684                                    self.handle,
685                                    [
686                                        WidgetMessage::Width(prev_bounds.w()),
687                                        WidgetMessage::Height(prev_bounds.h()),
688                                    ],
689                                );
690                            }
691                        }
692
693                        ui.send_message(message.reverse());
694                    }
695                }
696                WindowMessage::MoveEnd => {
697                    if self.is_dragging {
698                        ui.release_mouse_capture();
699                        self.is_dragging = false;
700
701                        ui.send_message(message.reverse());
702                    }
703                }
704                WindowMessage::Title(title) => {
705                    match title {
706                        WindowTitle::Text {
707                            text,
708                            font,
709                            font_size,
710                        } => {
711                            if ui.try_get_of_type::<Text>(self.title).is_ok() {
712                                // Just modify existing text, this is much faster than
713                                // re-create text everytime.
714                                ui.send(self.title, TextMessage::Text(text.clone()));
715                                if let Some(font) = font {
716                                    ui.send(self.title, TextMessage::Font(font.clone()))
717                                }
718                                if let Some(font_size) = font_size {
719                                    ui.send(self.title, TextMessage::FontSize(font_size.clone()));
720                                }
721                            } else {
722                                ui.send(self.title, WidgetMessage::Remove);
723                                let font = font.clone().unwrap_or_else(|| ui.default_font.clone());
724                                let ctx = &mut ui.build_ctx();
725                                self.title = make_text_title(
726                                    ctx,
727                                    text,
728                                    font,
729                                    font_size
730                                        .clone()
731                                        .unwrap_or_else(|| ctx.style.property(Style::FONT_SIZE)),
732                                )
733                                .to_base();
734                                ui.send(self.title, WidgetMessage::link_with(self.title_grid));
735                            }
736                        }
737                        WindowTitle::Node(node) => {
738                            if self.title.is_some() {
739                                // Remove old title.
740                                ui.send(self.title, WidgetMessage::Remove);
741                            }
742
743                            if node.is_some() {
744                                self.title = *node;
745
746                                // Attach new one.
747                                ui.send(self.title, WidgetMessage::link_with(self.title_grid));
748                            }
749                        }
750                    }
751                }
752                WindowMessage::SafeBorderSize(size) => {
753                    if &self.safe_border_size != size {
754                        self.safe_border_size = *size;
755                        ui.send_message(message.reverse());
756                    }
757                }
758            }
759        }
760    }
761}
762
763impl Window {
764    pub fn tab_label(&self) -> &str {
765        &self.tab_label
766    }
767    pub fn minimized(&self) -> bool {
768        self.size_state == WindowSizeState::Minimized
769    }
770    pub fn maximized(&self) -> bool {
771        self.size_state == WindowSizeState::Maximized
772    }
773    pub fn normal(&self) -> bool {
774        self.size_state == WindowSizeState::Normal
775    }
776    fn update_size_state(&mut self, new_state: WindowSizeState, ui: &mut UserInterface) {
777        if self.size_state == new_state {
778            return;
779        }
780        if new_state == WindowSizeState::Maximized && !self.can_resize {
781            return;
782        }
783        let current_size = self.actual_global_size();
784        let current_position = self.screen_position();
785        match self.size_state {
786            WindowSizeState::Normal => {
787                self.prev_bounds = Some(Rect::new(
788                    current_position.x,
789                    current_position.y,
790                    current_size.x,
791                    current_size.y,
792                ));
793            }
794            WindowSizeState::Minimized => {
795                self.prev_bounds = Some(Rect::new(
796                    current_position.x,
797                    current_position.y,
798                    current_size.x,
799                    self.prev_bounds.map(|b| b.size.y).unwrap_or(current_size.y),
800                ));
801            }
802            _ => (),
803        }
804        let new_bounds = match new_state {
805            WindowSizeState::Normal | WindowSizeState::Minimized => {
806                self.prev_bounds.unwrap_or_else(|| {
807                    Rect::new(
808                        current_position.x,
809                        current_position.y,
810                        current_size.x,
811                        current_size.y,
812                    )
813                })
814            }
815            WindowSizeState::Maximized => Rect::new(0.0, 0.0, ui.screen_size.x, ui.screen_size.y),
816        };
817
818        if self.content.is_some() {
819            ui.send(
820                self.content,
821                WidgetMessage::Visibility(new_state != WindowSizeState::Minimized),
822            );
823        }
824
825        if new_state == WindowSizeState::Minimized {
826            self.set_desired_local_position(new_bounds.position);
827            if self.can_resize {
828                self.set_width(new_bounds.w());
829            }
830            self.set_height(f32::NAN);
831            self.invalidate_layout();
832        } else {
833            ui.send(
834                self.handle,
835                WidgetMessage::DesiredPosition(new_bounds.position),
836            );
837            if self.can_resize {
838                ui.send_many(
839                    self.handle,
840                    [
841                        WidgetMessage::Width(new_bounds.w()),
842                        WidgetMessage::Height(new_bounds.h()),
843                    ],
844                );
845            }
846        }
847        self.size_state = new_state;
848    }
849    /// Checks whether any resizing grip is active or not.
850    pub fn has_active_grip(&self) -> bool {
851        for grip in self.grips.borrow().iter() {
852            if grip.is_dragging {
853                return true;
854            }
855        }
856        false
857    }
858
859    fn content_to_focus(&self) -> Handle<UiNode> {
860        if self.content.is_some() {
861            self.content
862        } else {
863            self.handle
864        }
865    }
866
867    fn is_docked(&self, ui: &UserInterface) -> bool {
868        self.parent() != ui.root_canvas
869    }
870}
871
872/// Window builder creates [`Window`] instances and adds them to the user interface.
873pub struct WindowBuilder {
874    /// Base widget builder.
875    pub widget_builder: WidgetBuilder,
876    /// Content of the window.
877    pub content: Handle<UiNode>,
878    /// Optional title of the window.
879    pub title: Option<WindowTitle>,
880    /// Label for the window's tab.
881    pub tab_label: String,
882    /// Whether the window can be closed or not.
883    pub can_close: bool,
884    /// Whether the window can be minimized or not.
885    pub can_minimize: bool,
886    /// Whether the window can be maximized or not.
887    pub can_maximize: bool,
888    /// Whether the window should be created open or not.
889    pub open: bool,
890    /// Optional custom closing button, if not specified, then a default button will be created.
891    pub close_button: Option<Handle<Button>>,
892    /// Optional custom minimization button, if not specified, then a default button will be created.
893    pub minimize_button: Option<Handle<Button>>,
894    /// Optional custom maximization button, if not specified, then a default button will be created.
895    pub maximize_button: Option<Handle<Button>>,
896    /// Whether the window should be created as modal or not. Warning: Any independent builders must
897    /// take this into account!
898    pub modal: bool,
899    /// Whether the window should be resizable or not.
900    pub can_resize: bool,
901    /// Optional size of the border around the screen in which the window will be forced to stay.
902    pub safe_border_size: Option<Vector2<f32>>,
903    /// If `true`, then the window can be closed using `Esc` key. Default is `true`. Works only if
904    /// `can_close` is also `true`.
905    pub close_by_esc: bool,
906    /// If `true`, then the window will be deleted after closing.
907    pub remove_on_close: bool,
908}
909
910/// Window title can be either text or node.
911///
912/// If `Text` is used, then builder will automatically create [`Text`] node with specified text,
913/// but with default font.
914///
915/// If you need more flexibility (i.e. put a picture near text) then [`WindowTitle::Node`] option
916/// is for you: it allows to put any UI node hierarchy you want to.
917#[derive(Debug, Clone, PartialEq)]
918pub enum WindowTitle {
919    Text {
920        /// Actual text of the title.
921        text: String,
922        /// Optional font, if [`None`], then the default font will be used.
923        font: Option<FontResource>,
924        /// Optional size of the text. Default is [`None`] (in this case default size defined by the
925        /// current style will be used).
926        font_size: Option<StyledProperty<f32>>,
927    },
928    Node(Handle<UiNode>),
929}
930
931impl WindowTitle {
932    /// A shortcut to create [`WindowTitle::Text`]
933    pub fn text<P: AsRef<str>>(text: P) -> Self {
934        WindowTitle::Text {
935            text: text.as_ref().to_owned(),
936            font: None,
937            font_size: None,
938        }
939    }
940
941    /// A shortcut to create [`WindowTitle::Text`] with custom font.
942    pub fn text_with_font<P: AsRef<str>>(text: P, font: FontResource) -> Self {
943        WindowTitle::Text {
944            text: text.as_ref().to_owned(),
945            font: Some(font),
946            font_size: None,
947        }
948    }
949
950    /// A shortcut to create [`WindowTitle::Text`] with custom font and size.
951    pub fn text_with_font_size<P: AsRef<str>>(
952        text: P,
953        font: FontResource,
954        size: StyledProperty<f32>,
955    ) -> Self {
956        WindowTitle::Text {
957            text: text.as_ref().to_owned(),
958            font: Some(font),
959            font_size: Some(size),
960        }
961    }
962
963    /// A shortcut to create [`WindowTitle::Node`]
964    pub fn node(node: Handle<UiNode>) -> Self {
965        Self::Node(node)
966    }
967}
968
969fn make_text_title(
970    ctx: &mut BuildContext,
971    text: &str,
972    font: FontResource,
973    size: StyledProperty<f32>,
974) -> Handle<Text> {
975    TextBuilder::new(
976        WidgetBuilder::new()
977            .with_margin(Thickness::left(5.0))
978            .on_row(0)
979            .on_column(0),
980    )
981    .with_font_size(size)
982    .with_font(font)
983    .with_vertical_text_alignment(VerticalAlignment::Center)
984    .with_horizontal_text_alignment(HorizontalAlignment::Left)
985    .with_text(text)
986    .build(ctx)
987}
988
989enum HeaderButton {
990    Close,
991    Minimize,
992    Maximize,
993}
994
995fn make_mark(ctx: &mut BuildContext, button: HeaderButton) -> Handle<VectorImage> {
996    let size = 12.0;
997
998    VectorImageBuilder::new(
999        WidgetBuilder::new()
1000            .with_horizontal_alignment(HorizontalAlignment::Center)
1001            .with_vertical_alignment(match button {
1002                HeaderButton::Close => VerticalAlignment::Center,
1003                HeaderButton::Minimize => VerticalAlignment::Bottom,
1004                HeaderButton::Maximize => VerticalAlignment::Center,
1005            })
1006            .with_width(size)
1007            .with_height(size)
1008            .with_foreground(ctx.style.property(Style::BRUSH_BRIGHT)),
1009    )
1010    .with_primitives(match button {
1011        HeaderButton::Close => {
1012            vec![
1013                Primitive::Line {
1014                    begin: Vector2::new(0.0, 0.0),
1015                    end: Vector2::new(size, size),
1016                    thickness: 1.0,
1017                },
1018                Primitive::Line {
1019                    begin: Vector2::new(size, 0.0),
1020                    end: Vector2::new(0.0, size),
1021                    thickness: 1.0,
1022                },
1023            ]
1024        }
1025        HeaderButton::Minimize => {
1026            let bottom_spacing = 3.0;
1027
1028            vec![Primitive::Line {
1029                begin: Vector2::new(0.0, size - bottom_spacing),
1030                end: Vector2::new(size, size - bottom_spacing),
1031                thickness: 1.0,
1032            }]
1033        }
1034        HeaderButton::Maximize => {
1035            let thickness = 1.25;
1036            let half_thickness = thickness * 0.5;
1037
1038            vec![
1039                Primitive::Line {
1040                    begin: Vector2::new(0.0, half_thickness),
1041                    end: Vector2::new(size, half_thickness),
1042                    thickness,
1043                },
1044                Primitive::Line {
1045                    begin: Vector2::new(size - half_thickness, 0.0),
1046                    end: Vector2::new(size - half_thickness, size),
1047                    thickness,
1048                },
1049                Primitive::Line {
1050                    begin: Vector2::new(size, size - half_thickness),
1051                    end: Vector2::new(0.0, size - half_thickness),
1052                    thickness,
1053                },
1054                Primitive::Line {
1055                    begin: Vector2::new(half_thickness, size),
1056                    end: Vector2::new(half_thickness, 0.0),
1057                    thickness,
1058                },
1059            ]
1060        }
1061    })
1062    .build(ctx)
1063}
1064
1065fn make_header_button(ctx: &mut BuildContext, button: HeaderButton) -> Handle<Button> {
1066    ButtonBuilder::new(WidgetBuilder::new().with_margin(Thickness::uniform(2.0)))
1067        .with_back(
1068            DecoratorBuilder::new(
1069                BorderBuilder::new(WidgetBuilder::new())
1070                    .with_stroke_thickness(Thickness::uniform(0.0).into())
1071                    .with_pad_by_corner_radius(false)
1072                    .with_corner_radius(4.0f32.into()),
1073            )
1074            .with_normal_brush(Brush::Solid(Color::TRANSPARENT).into())
1075            .with_hover_brush(ctx.style.property(Style::BRUSH_LIGHT))
1076            .with_pressed_brush(ctx.style.property(Style::BRUSH_LIGHTEST))
1077            .build(ctx),
1078        )
1079        .with_content(make_mark(ctx, button))
1080        .build(ctx)
1081}
1082
1083impl WindowBuilder {
1084    /// Creates new window builder.
1085    pub fn new(widget_builder: WidgetBuilder) -> Self {
1086        Self {
1087            widget_builder,
1088            content: Handle::NONE,
1089            title: None,
1090            tab_label: String::default(),
1091            can_close: true,
1092            can_minimize: true,
1093            can_maximize: true,
1094            open: true,
1095            close_button: None,
1096            minimize_button: None,
1097            maximize_button: None,
1098            modal: false,
1099            can_resize: true,
1100            safe_border_size: Some(Vector2::new(25.0, 20.0)),
1101            close_by_esc: true,
1102            remove_on_close: false,
1103        }
1104    }
1105
1106    /// Sets a desired window content.
1107    pub fn with_content(mut self, content: Handle<impl ObjectOrVariant<UiNode>>) -> Self {
1108        self.content = content.to_base();
1109        self
1110    }
1111
1112    /// Sets a desired window title.
1113    pub fn with_title(mut self, title: WindowTitle) -> Self {
1114        self.title = Some(title);
1115        self
1116    }
1117
1118    pub fn with_tab_label<S: Into<String>>(mut self, label: S) -> Self {
1119        self.tab_label = label.into();
1120        self
1121    }
1122
1123    /// Sets a desired minimization button.
1124    pub fn with_minimize_button(mut self, button: Handle<Button>) -> Self {
1125        self.minimize_button = Some(button);
1126        self
1127    }
1128
1129    /// Sets a desired maximization button.
1130    pub fn with_maximize_button(mut self, button: Handle<Button>) -> Self {
1131        self.minimize_button = Some(button);
1132        self
1133    }
1134
1135    /// Sets a desired closing button.
1136    pub fn with_close_button(mut self, button: Handle<Button>) -> Self {
1137        self.close_button = Some(button);
1138        self
1139    }
1140
1141    /// Sets whether the window can be closed or not.
1142    pub fn can_close(mut self, can_close: bool) -> Self {
1143        self.can_close = can_close;
1144        self
1145    }
1146
1147    /// Sets whether the window can be minimized or not.
1148    pub fn can_minimize(mut self, can_minimize: bool) -> Self {
1149        self.can_minimize = can_minimize;
1150        self
1151    }
1152
1153    /// Sets whether the window can be maximized or not.
1154    pub fn can_maximize(mut self, can_minimize: bool) -> Self {
1155        self.can_maximize = can_minimize;
1156        self
1157    }
1158
1159    /// Sets whether the window should be open or not.
1160    pub fn open(mut self, open: bool) -> Self {
1161        self.open = open;
1162        self
1163    }
1164
1165    /// Sets whether the window should be modal or not.
1166    pub fn modal(mut self, modal: bool) -> Self {
1167        self.modal = modal;
1168        self
1169    }
1170
1171    /// Sets whether the window can be resized or not.
1172    pub fn can_resize(mut self, can_resize: bool) -> Self {
1173        self.can_resize = can_resize;
1174        self
1175    }
1176
1177    /// Sets a desired safe border size.
1178    pub fn with_safe_border_size(mut self, size: Option<Vector2<f32>>) -> Self {
1179        self.safe_border_size = size.map(|s| Vector2::new(s.x.abs(), s.y.abs()));
1180        self
1181    }
1182
1183    /// Defines, whether the window can be closed using `Esc` key or not. Works only if `can_close`
1184    /// is also `true`.
1185    pub fn with_close_by_esc(mut self, close: bool) -> Self {
1186        self.close_by_esc = close;
1187        self
1188    }
1189
1190    /// Defines, whether the window should be deleted after closing or not. Default is `false`.
1191    pub fn with_remove_on_close(mut self, close: bool) -> Self {
1192        self.remove_on_close = close;
1193        self
1194    }
1195
1196    /// Finishes window building and returns its instance.
1197    pub fn build_window(self, ctx: &mut BuildContext) -> Window {
1198        let minimize_button;
1199        let maximize_button;
1200        let close_button;
1201        let title;
1202        let title_grid;
1203        let header = BorderBuilder::new(
1204            WidgetBuilder::new()
1205                .with_horizontal_alignment(HorizontalAlignment::Stretch)
1206                .with_height(22.0)
1207                .with_background(ctx.style.property(Style::BRUSH_DARKER))
1208                .with_child({
1209                    title_grid = GridBuilder::new(
1210                        WidgetBuilder::new()
1211                            .with_child({
1212                                title = match self.title {
1213                                    None => Handle::NONE,
1214                                    Some(window_title) => match window_title {
1215                                        WindowTitle::Node(node) => node,
1216                                        WindowTitle::Text {
1217                                            text,
1218                                            font,
1219                                            font_size,
1220                                        } => make_text_title(
1221                                            ctx,
1222                                            &text,
1223                                            font.unwrap_or_else(|| ctx.default_font()),
1224                                            font_size.unwrap_or_else(|| {
1225                                                ctx.style.property(Style::FONT_SIZE)
1226                                            }),
1227                                        )
1228                                        .to_base(),
1229                                    },
1230                                };
1231                                title
1232                            })
1233                            .with_child({
1234                                minimize_button = self.minimize_button.unwrap_or_else(|| {
1235                                    make_header_button(ctx, HeaderButton::Minimize)
1236                                });
1237                                ctx[minimize_button]
1238                                    .set_visibility(self.can_minimize)
1239                                    .set_width(20.0)
1240                                    .set_row(0)
1241                                    .set_column(1);
1242                                minimize_button
1243                            })
1244                            .with_child({
1245                                maximize_button = self.maximize_button.unwrap_or_else(|| {
1246                                    make_header_button(ctx, HeaderButton::Maximize)
1247                                });
1248                                ctx[maximize_button]
1249                                    .set_visibility(self.can_maximize)
1250                                    .set_width(20.0)
1251                                    .set_row(0)
1252                                    .set_column(2);
1253                                maximize_button
1254                            })
1255                            .with_child({
1256                                close_button = self.close_button.unwrap_or_else(|| {
1257                                    make_header_button(ctx, HeaderButton::Close)
1258                                });
1259                                ctx[close_button]
1260                                    .set_width(20.0)
1261                                    .set_visibility(self.can_close)
1262                                    .set_row(0)
1263                                    .set_column(3);
1264                                close_button
1265                            }),
1266                    )
1267                    .add_column(Column::stretch())
1268                    .add_column(Column::auto())
1269                    .add_column(Column::auto())
1270                    .add_column(Column::auto())
1271                    .add_row(Row::stretch())
1272                    .build(ctx);
1273                    title_grid
1274                })
1275                .on_row(0),
1276        )
1277        .with_pad_by_corner_radius(false)
1278        .with_corner_radius(4.0f32.into())
1279        .with_stroke_thickness(Thickness::uniform(0.0).into())
1280        .build(ctx)
1281        .to_base();
1282
1283        let border = BorderBuilder::new(
1284            WidgetBuilder::new()
1285                .with_foreground(ctx.style.property(Style::BRUSH_DARKER))
1286                .with_child(
1287                    GridBuilder::new(
1288                        WidgetBuilder::new()
1289                            .with_child(
1290                                NavigationLayerBuilder::new(
1291                                    WidgetBuilder::new().on_row(1).with_child(self.content),
1292                                )
1293                                .build(ctx),
1294                            )
1295                            .with_child(header),
1296                    )
1297                    .add_column(Column::stretch())
1298                    .add_row(Row::auto())
1299                    .add_row(Row::stretch())
1300                    .build(ctx),
1301                ),
1302        )
1303        .with_pad_by_corner_radius(false)
1304        .with_corner_radius(4.0f32.into())
1305        .with_stroke_thickness(Thickness::uniform(1.0).into())
1306        .build(ctx);
1307
1308        Window {
1309            widget: self
1310                .widget_builder
1311                .with_visibility(self.open)
1312                .with_child(border)
1313                .build(ctx),
1314            mouse_click_pos: Vector2::default(),
1315            initial_position: Vector2::default(),
1316            initial_size: Default::default(),
1317            is_dragging: false,
1318            size_state: WindowSizeState::default(),
1319            can_minimize: self.can_minimize,
1320            can_maximize: self.can_maximize,
1321            can_close: self.can_close,
1322            can_resize: self.can_resize,
1323            header,
1324            tab_label: self.tab_label,
1325            minimize_button,
1326            maximize_button,
1327            close_button,
1328            drag_delta: Default::default(),
1329            content: self.content,
1330            safe_border_size: self.safe_border_size,
1331            grips: RefCell::new([
1332                // Corners have priority
1333                Grip::new(GripKind::LeftTopCorner, CursorIcon::NwResize),
1334                Grip::new(GripKind::RightTopCorner, CursorIcon::NeResize),
1335                Grip::new(GripKind::RightBottomCorner, CursorIcon::SeResize),
1336                Grip::new(GripKind::LeftBottomCorner, CursorIcon::SwResize),
1337                Grip::new(GripKind::Left, CursorIcon::WResize),
1338                Grip::new(GripKind::Top, CursorIcon::NResize),
1339                Grip::new(GripKind::Right, CursorIcon::EResize),
1340                Grip::new(GripKind::Bottom, CursorIcon::SResize),
1341            ]),
1342            title,
1343            title_grid,
1344            prev_bounds: None,
1345            close_by_esc: self.close_by_esc,
1346            remove_on_close: self.remove_on_close,
1347        }
1348    }
1349
1350    /// Finishes window building and returns its handle.
1351    pub fn build(self, ctx: &mut BuildContext) -> Handle<Window> {
1352        let modal = self.modal;
1353        let open = self.open;
1354
1355        let node = self.build_window(ctx);
1356        let handle = ctx.add(node);
1357
1358        if modal && open {
1359            ctx.push_picking_restriction(RestrictionEntry {
1360                handle: handle.to_base(),
1361                stop: true,
1362            });
1363        }
1364
1365        handle.to_variant()
1366    }
1367}
1368
1369#[cfg(test)]
1370mod test {
1371    use crate::window::WindowBuilder;
1372    use crate::{test::test_widget_deletion, widget::WidgetBuilder};
1373
1374    #[test]
1375    fn test_deletion() {
1376        test_widget_deletion(|ctx| WindowBuilder::new(WidgetBuilder::new()).build(ctx));
1377    }
1378}