1use 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 None,
59 Center,
62 Position(Vector2<f32>),
65 Relative {
67 relative_to: Handle<UiNode>,
69 horizontal_alignment: HorizontalAlignment,
71 vertical_alignment: VerticalAlignment,
73 margin: Thickness,
75 },
76}
77
78#[derive(Debug, Clone, PartialEq)]
81pub enum WindowMessage {
82 Open {
84 alignment: WindowAlignment,
86 modal: bool,
88 focus_content: bool,
91 },
92
93 Close,
95
96 Minimize(bool),
99
100 Maximize(bool),
102
103 CanMinimize(bool),
105
106 CanClose(bool),
108
109 CanResize(bool),
111
112 MoveStart,
114
115 Move(Vector2<f32>),
117
118 MoveEnd,
120
121 Title(WindowTitle),
123
124 SafeBorderSize(Option<Vector2<f32>>),
128}
129impl MessageData for WindowMessage {}
130
131#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Visit, Reflect)]
133pub enum WindowSizeState {
134 #[default]
136 Normal,
137 Minimized,
139 Maximized,
141}
142
143#[derive(Default, Clone, Visit, Reflect, Debug, ComponentProvider)]
220#[reflect(derived_type = "UiNode")]
221pub struct Window {
222 pub widget: Widget,
224 tab_label: String,
226 pub mouse_click_pos: Vector2<f32>,
228 pub initial_position: Vector2<f32>,
230 pub initial_size: Vector2<f32>,
232 pub is_dragging: bool,
234 pub size_state: WindowSizeState,
236 pub can_minimize: bool,
238 pub can_maximize: bool,
240 pub can_close: bool,
242 pub can_resize: bool,
244 pub header: Handle<UiNode>,
246 pub minimize_button: Handle<Button>,
248 pub maximize_button: Handle<Button>,
250 pub close_button: Handle<Button>,
252 pub drag_delta: Vector2<f32>,
254 pub content: Handle<UiNode>,
256 pub grips: RefCell<[Grip; 8]>,
258 pub title: Handle<UiNode>,
260 pub title_grid: Handle<Grid>,
262 pub safe_border_size: Option<Vector2<f32>>,
264 pub prev_bounds: Option<Rect<f32>>,
267 pub close_by_esc: bool,
270 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#[derive(Copy, Clone, Debug, Visit, Reflect, Default)]
292pub enum GripKind {
293 #[default]
295 LeftTopCorner = 0,
296 RightTopCorner = 1,
298 RightBottomCorner = 2,
300 LeftBottomCorner = 3,
302 Left = 4,
304 Top = 5,
306 Right = 6,
308 Bottom = 7,
310}
311
312#[derive(Clone, Visit, Default, Debug, Reflect)]
314pub struct Grip {
315 pub kind: GripKind,
317 pub bounds: Rect<f32>,
319 pub is_dragging: bool,
321 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 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 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 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 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 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 if !self.visibility() && self.parent() == ui.root() {
560 ui.send(self.handle(), WidgetMessage::Visibility(true));
561 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 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 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 ui.send(self.title, WidgetMessage::Remove);
741 }
742
743 if node.is_some() {
744 self.title = *node;
745
746 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 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
872pub struct WindowBuilder {
874 pub widget_builder: WidgetBuilder,
876 pub content: Handle<UiNode>,
878 pub title: Option<WindowTitle>,
880 pub tab_label: String,
882 pub can_close: bool,
884 pub can_minimize: bool,
886 pub can_maximize: bool,
888 pub open: bool,
890 pub close_button: Option<Handle<Button>>,
892 pub minimize_button: Option<Handle<Button>>,
894 pub maximize_button: Option<Handle<Button>>,
896 pub modal: bool,
899 pub can_resize: bool,
901 pub safe_border_size: Option<Vector2<f32>>,
903 pub close_by_esc: bool,
906 pub remove_on_close: bool,
908}
909
910#[derive(Debug, Clone, PartialEq)]
918pub enum WindowTitle {
919 Text {
920 text: String,
922 font: Option<FontResource>,
924 font_size: Option<StyledProperty<f32>>,
927 },
928 Node(Handle<UiNode>),
929}
930
931impl WindowTitle {
932 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 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 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 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 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 pub fn with_content(mut self, content: Handle<impl ObjectOrVariant<UiNode>>) -> Self {
1108 self.content = content.to_base();
1109 self
1110 }
1111
1112 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 pub fn with_minimize_button(mut self, button: Handle<Button>) -> Self {
1125 self.minimize_button = Some(button);
1126 self
1127 }
1128
1129 pub fn with_maximize_button(mut self, button: Handle<Button>) -> Self {
1131 self.minimize_button = Some(button);
1132 self
1133 }
1134
1135 pub fn with_close_button(mut self, button: Handle<Button>) -> Self {
1137 self.close_button = Some(button);
1138 self
1139 }
1140
1141 pub fn can_close(mut self, can_close: bool) -> Self {
1143 self.can_close = can_close;
1144 self
1145 }
1146
1147 pub fn can_minimize(mut self, can_minimize: bool) -> Self {
1149 self.can_minimize = can_minimize;
1150 self
1151 }
1152
1153 pub fn can_maximize(mut self, can_minimize: bool) -> Self {
1155 self.can_maximize = can_minimize;
1156 self
1157 }
1158
1159 pub fn open(mut self, open: bool) -> Self {
1161 self.open = open;
1162 self
1163 }
1164
1165 pub fn modal(mut self, modal: bool) -> Self {
1167 self.modal = modal;
1168 self
1169 }
1170
1171 pub fn can_resize(mut self, can_resize: bool) -> Self {
1173 self.can_resize = can_resize;
1174 self
1175 }
1176
1177 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 pub fn with_close_by_esc(mut self, close: bool) -> Self {
1186 self.close_by_esc = close;
1187 self
1188 }
1189
1190 pub fn with_remove_on_close(mut self, close: bool) -> Self {
1192 self.remove_on_close = close;
1193 self
1194 }
1195
1196 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 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 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}