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 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 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 if !self.visibility() && self.parent() == ui.root() {
578 ui.send(self.handle(), WidgetMessage::Visibility(true));
579 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 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 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 ui.send(self.title, WidgetMessage::Remove);
759 }
760
761 if node.is_some() {
762 self.title = *node;
763
764 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 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
890pub struct WindowBuilder {
892 pub widget_builder: WidgetBuilder,
894 pub content: Handle<UiNode>,
896 pub title: Option<WindowTitle>,
898 pub tab_label: String,
900 pub can_close: bool,
902 pub can_minimize: bool,
904 pub can_maximize: bool,
906 pub open: bool,
908 pub close_button: Option<Handle<Button>>,
910 pub minimize_button: Option<Handle<Button>>,
912 pub maximize_button: Option<Handle<Button>>,
914 pub modal: bool,
917 pub can_resize: bool,
919 pub safe_border_size: Option<Vector2<f32>>,
921 pub close_by_esc: bool,
924 pub remove_on_close: bool,
926}
927
928#[derive(Debug, Clone, PartialEq)]
936pub enum WindowTitle {
937 Text {
938 text: String,
940 font: Option<FontResource>,
942 font_size: Option<StyledProperty<f32>>,
945 },
946 Node(Handle<UiNode>),
947}
948
949impl WindowTitle {
950 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 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 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 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 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 pub fn with_content(mut self, content: Handle<impl ObjectOrVariant<UiNode>>) -> Self {
1126 self.content = content.to_base();
1127 self
1128 }
1129
1130 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 pub fn with_minimize_button(mut self, button: Handle<Button>) -> Self {
1143 self.minimize_button = Some(button);
1144 self
1145 }
1146
1147 pub fn with_maximize_button(mut self, button: Handle<Button>) -> Self {
1149 self.minimize_button = Some(button);
1150 self
1151 }
1152
1153 pub fn with_close_button(mut self, button: Handle<Button>) -> Self {
1155 self.close_button = Some(button);
1156 self
1157 }
1158
1159 pub fn can_close(mut self, can_close: bool) -> Self {
1161 self.can_close = can_close;
1162 self
1163 }
1164
1165 pub fn can_minimize(mut self, can_minimize: bool) -> Self {
1167 self.can_minimize = can_minimize;
1168 self
1169 }
1170
1171 pub fn can_maximize(mut self, can_minimize: bool) -> Self {
1173 self.can_maximize = can_minimize;
1174 self
1175 }
1176
1177 pub fn open(mut self, open: bool) -> Self {
1179 self.open = open;
1180 self
1181 }
1182
1183 pub fn modal(mut self, modal: bool) -> Self {
1185 self.modal = modal;
1186 self
1187 }
1188
1189 pub fn can_resize(mut self, can_resize: bool) -> Self {
1191 self.can_resize = can_resize;
1192 self
1193 }
1194
1195 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 pub fn with_close_by_esc(mut self, close: bool) -> Self {
1204 self.close_by_esc = close;
1205 self
1206 }
1207
1208 pub fn with_remove_on_close(mut self, close: bool) -> Self {
1210 self.remove_on_close = close;
1211 self
1212 }
1213
1214 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 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 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}