fyrox_ui/popup.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//! Popup is used to display other widgets in floating panel, that could lock input in its bounds. See [`Popup`] docs
22//! for more info and usage examples.
23
24#![warn(missing_docs)]
25
26use crate::message::MessageData;
27use crate::{
28 border::BorderBuilder,
29 core::{
30 algebra::Vector2, math::Rect, pool::Handle, reflect::prelude::*, type_traits::prelude::*,
31 uuid_provider, variable::InheritableVariable, visitor::prelude::*,
32 },
33 message::{ButtonState, KeyCode, OsEvent, UiMessage},
34 style::{resource::StyleResourceExt, Style},
35 widget::{Widget, WidgetBuilder, WidgetMessage},
36 BuildContext, Control, RestrictionEntry, Thickness, UiNode, UserInterface,
37};
38use fyrox_core::pool::ObjectOrVariant;
39use fyrox_graph::{
40 constructor::{ConstructorProvider, GraphNodeConstructor},
41 SceneGraph,
42};
43
44/// A set of messages for [`Popup`] widget.
45#[derive(Debug, Clone, PartialEq)]
46pub enum PopupMessage {
47 /// Used to open a [`Popup`] widgets. Use [`PopupMessage::Open`] to create the message.
48 Open,
49 /// Used to close a [`Popup`] widgets. Use [`PopupMessage::Close`] to create the message.
50 Close,
51 /// Used to change the content of a [`Popup`] widgets. Use [`PopupMessage::Content`] to create the message.
52 Content(Handle<UiNode>),
53 /// Used to change popup's placement. Use [`PopupMessage::Placement`] to create the message.
54 Placement(Placement),
55 /// Used to adjust the position of a popup widget, so it will be on screen. Use [`PopupMessage::AdjustPosition`] to create
56 /// the message.
57 AdjustPosition,
58 /// Used to set the owner of a Popup. The owner will receive Event messages.
59 Owner(Handle<UiNode>),
60 /// Sent by the Popup to its owner when handling messages from the Popup's children.
61 RelayedMessage(UiMessage),
62}
63
64impl MessageData for PopupMessage {
65 fn need_perform_layout(&self) -> bool {
66 matches!(self, Self::AdjustPosition)
67 }
68}
69
70/// Defines a method of popup placement.
71#[derive(Copy, Clone, PartialEq, Debug, Visit, Reflect)]
72pub enum Placement {
73 /// A popup should be placed relative to given widget at the left top corner of the widget screen bounds.
74 /// Widget handle could be [`Handle::NONE`], in this case the popup will be placed at the left top corner of the screen.
75 LeftTop(Handle<UiNode>),
76
77 /// A popup should be placed relative to given widget at the right top corner of the widget screen bounds.
78 /// Widget handle could be [`Handle::NONE`], in this case the popup will be placed at the right top corner of the screen.
79 RightTop(Handle<UiNode>),
80
81 /// A popup should be placed relative to given widget at the center of the widget screen bounds.
82 /// Widget handle could be [`Handle::NONE`], in this case, the popup will be placed at the center of the screen.
83 Center(Handle<UiNode>),
84
85 /// A popup should be placed relative to given widget at the left bottom corner of the widget screen bounds.
86 /// Widget handle could be [`Handle::NONE`], in this case the popup will be placed at the left bottom corner of the screen.
87 LeftBottom(Handle<UiNode>),
88
89 /// A popup should be placed relative to given widget at the right bottom corner of the widget screen bounds.
90 /// Widget handle could be [`Handle::NONE`], in this case the popup will be placed at the right bottom corner of the screen.
91 RightBottom(Handle<UiNode>),
92
93 /// A popup should be placed at the cursor position. The widget handle could be either [`Handle::NONE`] or a handle of a
94 /// widget that is directly behind the cursor.
95 Cursor(Handle<UiNode>),
96
97 /// A popup should be placed at given screen-space position.
98 Position {
99 /// Screen-space position.
100 position: Vector2<f32>,
101
102 /// A handle of the node that is located behind the given position. Could be [`Handle::NONE`] if there is nothing behind
103 /// given position.
104 target: Handle<UiNode>,
105 },
106}
107
108impl Default for Placement {
109 fn default() -> Self {
110 Self::LeftTop(Default::default())
111 }
112}
113
114impl Placement {
115 /// Returns a handle of the node to which this placement corresponds to.
116 pub fn target(&self) -> Handle<UiNode> {
117 match self {
118 Placement::LeftTop(target)
119 | Placement::RightTop(target)
120 | Placement::Center(target)
121 | Placement::LeftBottom(target)
122 | Placement::RightBottom(target)
123 | Placement::Cursor(target)
124 | Placement::Position { target, .. } => *target,
125 }
126 }
127}
128
129/// Popup is used to display other widgets in floating panel, that could lock input in its bounds.
130///
131/// ## How to create
132///
133/// A simple popup with a button could be created using the following code:
134///
135/// ```rust
136/// # use fyrox_ui::{
137/// # button::ButtonBuilder, core::pool::Handle, popup::{Popup, PopupBuilder}, widget::WidgetBuilder,
138/// # BuildContext, UiNode,
139/// # };
140/// fn create_popup_with_button(ctx: &mut BuildContext) -> Handle<Popup> {
141/// PopupBuilder::new(WidgetBuilder::new())
142/// .with_content(
143/// ButtonBuilder::new(WidgetBuilder::new())
144/// .with_text("Click Me!")
145/// .build(ctx),
146/// )
147/// .build(ctx)
148/// }
149/// ```
150///
151/// Keep in mind, that the popup is closed by default. You need to open it explicitly by sending a [`PopupMessage::Open`] to it,
152/// otherwise you won't see it:
153///
154/// ```rust
155/// # use fyrox_ui::{
156/// # button::ButtonBuilder,
157/// # core::pool::Handle,
158/// # message::MessageDirection,
159/// # popup::{Placement, PopupBuilder, PopupMessage},
160/// # widget::WidgetBuilder,
161/// # UiNode, UserInterface,
162/// # };
163/// # use fyrox_ui::popup::Popup;
164///
165/// fn create_popup_with_button_and_open_it(ui: &mut UserInterface) -> Handle<Popup> {
166/// let popup = PopupBuilder::new(WidgetBuilder::new())
167/// .with_content(
168/// ButtonBuilder::new(WidgetBuilder::new())
169/// .with_text("Click Me!")
170/// .build(&mut ui.build_ctx()),
171/// )
172/// .build(&mut ui.build_ctx());
173///
174/// // Open the popup explicitly.
175/// ui.send(popup, PopupMessage::Open);
176///
177/// popup
178/// }
179/// ```
180///
181/// ## Placement
182///
183/// Since popups are usually used to show useful context-specific information (like context menus, drop-down lists, etc.), they're usually
184/// open above some other widget with specific alignment (right, left, center, etc.).
185///
186/// ```rust
187/// # use fyrox_ui::{
188/// # button::ButtonBuilder,
189/// # core::pool::Handle,
190/// # message::MessageDirection,
191/// # popup::{Placement, PopupBuilder, PopupMessage},
192/// # widget::WidgetBuilder,
193/// # UiNode, UserInterface,
194/// # };
195/// # use fyrox_ui::popup::Popup;
196///
197/// fn create_popup_with_button_and_open_it(ui: &mut UserInterface) -> Handle<Popup> {
198/// let popup = PopupBuilder::new(WidgetBuilder::new())
199/// .with_content(
200/// ButtonBuilder::new(WidgetBuilder::new())
201/// .with_text("Click Me!")
202/// .build(&mut ui.build_ctx()),
203/// )
204/// // Set the placement. For simplicity, it is just a cursor position with Handle::NONE as placement target.
205/// .with_placement(Placement::Cursor(Handle::NONE))
206/// .build(&mut ui.build_ctx());
207///
208/// // Open the popup explicitly at the current placement.
209/// ui.send(popup, PopupMessage::Open);
210///
211/// popup
212/// }
213/// ```
214///
215/// The example uses [`Placement::Cursor`] with [`Handle::NONE`] placement target for simplicity reasons, however in
216/// the real-world usages this handle must be a handle of some widget that is located under the popup. It is very
217/// important to specify it correctly, otherwise you will lose the built-in ability to fetch the actual placement target.
218/// For example, imagine that you're building your own custom [`crate::dropdown_list::DropdownList`] widget and the popup
219/// is used to display content of the list. In this case, you could specify the placement target like this:
220///
221/// ```rust
222/// # use fyrox_ui::{
223/// # button::ButtonBuilder,
224/// # core::pool::Handle,
225/// # message::MessageDirection,
226/// # popup::{Placement, PopupBuilder, PopupMessage},
227/// # widget::WidgetBuilder,
228/// # UiNode, UserInterface,
229/// # };
230/// # use fyrox_ui::popup::Popup;
231///
232/// fn create_popup_with_button_and_open_it(
233/// dropdown_list: Handle<UiNode>,
234/// ui: &mut UserInterface,
235/// ) -> Handle<Popup> {
236/// let popup = PopupBuilder::new(WidgetBuilder::new())
237/// .with_content(
238/// ButtonBuilder::new(WidgetBuilder::new())
239/// .with_text("Click Me!")
240/// .build(&mut ui.build_ctx()),
241/// )
242/// // Set the placement to the dropdown list.
243/// .with_placement(Placement::LeftBottom(dropdown_list))
244/// .build(&mut ui.build_ctx());
245///
246/// // Open the popup explicitly at the current placement.
247/// ui.send(popup, PopupMessage::Open);
248///
249/// popup
250/// }
251/// ```
252///
253/// In this case, the popup will open at the left bottom corner of the dropdown list automatically. Placement target is also
254/// useful to build context menus, especially for lists with multiple items. Each item in the list usually has the same context
255/// menu, and this is an ideal use case for popups, since the single context menu can be shared across multiple list items. To find
256/// which item causes the context menu to open, catch [`PopupMessage::Placement`] and extract the node handle - this will be your
257/// actual item.
258///
259/// ## Opening mode
260///
261/// By default, when you click outside your popup, it will automatically close. It is pretty common behavior in the UI, you
262/// can see it almost every time you use context menus in various apps. There are cases when this behavior is undesired and it
263/// can be turned off:
264///
265/// ```rust
266/// # use fyrox_ui::{
267/// # button::ButtonBuilder, core::pool::Handle, popup::PopupBuilder, widget::WidgetBuilder,
268/// # BuildContext, UiNode,
269/// # };
270/// # use fyrox_ui::popup::Popup;
271///
272/// fn create_popup_with_button(ctx: &mut BuildContext) -> Handle<Popup> {
273/// PopupBuilder::new(WidgetBuilder::new())
274/// .with_content(
275/// ButtonBuilder::new(WidgetBuilder::new())
276/// .with_text("Click Me!")
277/// .build(ctx),
278/// )
279/// // This forces the popup to stay open when clicked outside its bounds
280/// .stays_open(true)
281/// .build(ctx)
282/// }
283/// ```
284///
285/// ## Smart placement
286///
287/// Popup widget can automatically adjust its position to always remain on screen, which is useful for tooltips, dropdown lists,
288/// etc. To enable this option, use [`PopupBuilder::with_smart_placement`] with `true` as the first argument.
289#[derive(Default, Clone, Visit, Debug, Reflect, ComponentProvider)]
290#[reflect(derived_type = "UiNode")]
291pub struct Popup {
292 /// Base widget of the popup.
293 pub widget: Widget,
294 /// Current placement of the popup.
295 pub placement: InheritableVariable<Placement>,
296 /// A flag, that defines whether the popup will stay open if a user click outside its bounds.
297 pub stays_open: InheritableVariable<bool>,
298 /// A flag, that defines whether the popup is open or not.
299 pub is_open: InheritableVariable<bool>,
300 /// Current content of the popup.
301 pub content: InheritableVariable<Handle<UiNode>>,
302 /// Background widget of the popup. It is used as a container for the content.
303 pub body: InheritableVariable<Handle<UiNode>>,
304 /// Smart placement prevents the popup from going outside the screen bounds. It is usually used for tooltips,
305 /// dropdown lists, etc. to prevent the content from being outside the screen.
306 pub smart_placement: InheritableVariable<bool>,
307 /// The destination for Event messages that relay messages from the children of this popup.
308 pub owner: Handle<UiNode>,
309 /// A flag, that defines whether the popup should restrict all the mouse input or not.
310 pub restrict_picking: InheritableVariable<bool>,
311}
312
313impl ConstructorProvider<UiNode, UserInterface> for Popup {
314 fn constructor() -> GraphNodeConstructor<UiNode, UserInterface> {
315 GraphNodeConstructor::new::<Self>()
316 .with_variant("Popup", |ui| {
317 PopupBuilder::new(WidgetBuilder::new().with_name("Popup"))
318 .build(&mut ui.build_ctx())
319 .to_base()
320 .into()
321 })
322 .with_group("Layout")
323 }
324}
325
326crate::define_widget_deref!(Popup);
327
328fn adjust_placement_position(
329 node_screen_bounds: Rect<f32>,
330 screen_size: Vector2<f32>,
331) -> Vector2<f32> {
332 let mut new_position = node_screen_bounds.position;
333 let right_bottom = node_screen_bounds.right_bottom_corner();
334 if right_bottom.x > screen_size.x {
335 new_position.x -= right_bottom.x - screen_size.x;
336 }
337 if right_bottom.y > screen_size.y {
338 new_position.y -= right_bottom.y - screen_size.y;
339 }
340 new_position
341}
342
343impl Popup {
344 fn left_top_placement(&self, ui: &UserInterface, target: Handle<UiNode>) -> Vector2<f32> {
345 ui.try_get_node(target)
346 .map(|n| n.screen_position())
347 .unwrap_or_default()
348 }
349
350 fn right_top_placement(&self, ui: &UserInterface, target: Handle<UiNode>) -> Vector2<f32> {
351 ui.try_get_node(target)
352 .ok()
353 .map(|n| n.screen_position() + Vector2::new(n.actual_global_size().x, 0.0))
354 .unwrap_or_else(|| {
355 Vector2::new(ui.screen_size().x - self.widget.actual_global_size().x, 0.0)
356 })
357 }
358
359 fn center_placement(&self, ui: &UserInterface, target: Handle<UiNode>) -> Vector2<f32> {
360 ui.try_get_node(target)
361 .ok()
362 .map(|n| n.screen_position() + n.actual_global_size().scale(0.5))
363 .unwrap_or_else(|| (ui.screen_size - self.widget.actual_global_size()).scale(0.5))
364 }
365
366 fn left_bottom_placement(&self, ui: &UserInterface, target: Handle<UiNode>) -> Vector2<f32> {
367 ui.try_get_node(target)
368 .ok()
369 .map(|n| n.screen_position() + Vector2::new(0.0, n.actual_global_size().y))
370 .unwrap_or_else(|| {
371 Vector2::new(0.0, ui.screen_size().y - self.widget.actual_global_size().y)
372 })
373 }
374
375 fn right_bottom_placement(&self, ui: &UserInterface, target: Handle<UiNode>) -> Vector2<f32> {
376 ui.try_get_node(target)
377 .ok()
378 .map(|n| n.screen_position() + n.actual_global_size())
379 .unwrap_or_else(|| ui.screen_size - self.widget.actual_global_size())
380 }
381}
382
383uuid_provider!(Popup = "1c641540-59eb-4ccd-a090-2173dab02245");
384
385impl Control for Popup {
386 fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
387 self.widget.handle_routed_message(ui, message);
388
389 if let Some(msg) = message.data_for::<PopupMessage>(self.handle()) {
390 match msg {
391 PopupMessage::Open => {
392 if !*self.is_open {
393 self.is_open.set_value_and_mark_modified(true);
394 ui.send(self.handle(), WidgetMessage::Visibility(true));
395 if *self.restrict_picking {
396 ui.push_picking_restriction(RestrictionEntry {
397 handle: self.handle(),
398 stop: false,
399 });
400 }
401 ui.send(self.handle(), WidgetMessage::Topmost);
402 let position = match *self.placement {
403 Placement::LeftTop(target) => self.left_top_placement(ui, target),
404 Placement::RightTop(target) => self.right_top_placement(ui, target),
405 Placement::Center(target) => self.center_placement(ui, target),
406 Placement::LeftBottom(target) => self.left_bottom_placement(ui, target),
407 Placement::RightBottom(target) => {
408 self.right_bottom_placement(ui, target)
409 }
410 Placement::Cursor(_) => ui.cursor_position(),
411 Placement::Position { position, .. } => position,
412 };
413
414 ui.send(
415 self.handle(),
416 WidgetMessage::DesiredPosition(
417 ui.screen_to_root_canvas_space(position),
418 ),
419 );
420 ui.send(
421 if self.content.is_some() {
422 *self.content
423 } else {
424 self.handle
425 },
426 WidgetMessage::Focus,
427 );
428 if *self.smart_placement {
429 ui.send(self.handle, PopupMessage::AdjustPosition);
430 }
431 ui.send_message(message.reverse());
432 }
433 }
434 PopupMessage::Close => {
435 if *self.is_open {
436 self.is_open.set_value_and_mark_modified(false);
437 ui.send(self.handle(), WidgetMessage::Visibility(false));
438
439 if *self.restrict_picking {
440 ui.remove_picking_restriction(self.handle());
441
442 if let Some(top) = ui.top_picking_restriction() {
443 ui.send(top.handle, WidgetMessage::Focus);
444 }
445 }
446
447 if ui.captured_node() == self.handle() {
448 ui.release_mouse_capture();
449 }
450
451 ui.send_message(message.reverse());
452 }
453 }
454 PopupMessage::Content(content) => {
455 if *self.content != *content {
456 if self.content.is_some() {
457 ui.send(*self.content, WidgetMessage::Remove);
458 }
459 self.content.set_value_and_mark_modified(*content);
460 ui.send(*self.content, WidgetMessage::LinkWith(*self.body));
461
462 ui.send_message(message.reverse());
463 }
464 }
465 PopupMessage::Placement(placement) => {
466 if *self.placement != *placement {
467 self.placement.set_value_and_mark_modified(*placement);
468 self.invalidate_layout();
469
470 ui.send_message(message.reverse());
471 }
472 }
473 PopupMessage::AdjustPosition => {
474 let new_position =
475 adjust_placement_position(self.screen_bounds(), ui.screen_size());
476
477 if new_position != self.screen_position() {
478 ui.send(
479 self.handle,
480 WidgetMessage::DesiredPosition(
481 ui.screen_to_root_canvas_space(new_position),
482 ),
483 );
484 }
485 }
486 PopupMessage::Owner(owner) => {
487 self.owner = *owner;
488 }
489 PopupMessage::RelayedMessage(_) => (),
490 }
491 } else if let Some(WidgetMessage::KeyDown(key)) = message.data() {
492 if !message.handled() && *key == KeyCode::Escape {
493 ui.send(self.handle, PopupMessage::Close);
494 message.set_handled(true);
495 }
496 }
497 if ui.is_valid_handle(self.owner) && !message.handled() {
498 ui.send(self.owner, PopupMessage::RelayedMessage(message.clone()));
499 }
500 }
501
502 fn handle_os_event(
503 &mut self,
504 self_handle: Handle<UiNode>,
505 ui: &mut UserInterface,
506 event: &OsEvent,
507 ) {
508 if let OsEvent::MouseInput { state, .. } = event {
509 if *state != ButtonState::Pressed || !*self.is_open {
510 return;
511 }
512
513 if *self.restrict_picking {
514 if let Some(top_restriction) = ui.top_picking_restriction() {
515 if top_restriction.handle != self_handle {
516 return;
517 }
518 }
519 }
520
521 let pos = ui.cursor_position();
522 if !self.widget.screen_bounds().contains(pos) && !*self.stays_open {
523 ui.send(self.handle(), PopupMessage::Close);
524 }
525 }
526 }
527}
528
529/// Popup widget builder is used to create [`Popup`] widget instances and add them to the user interface.
530pub struct PopupBuilder {
531 widget_builder: WidgetBuilder,
532 placement: Placement,
533 stays_open: bool,
534 content: Handle<UiNode>,
535 smart_placement: bool,
536 owner: Handle<UiNode>,
537 restrict_picking: bool,
538}
539
540impl PopupBuilder {
541 /// Creates new builder instance.
542 pub fn new(widget_builder: WidgetBuilder) -> Self {
543 Self {
544 widget_builder,
545 placement: Placement::Cursor(Default::default()),
546 stays_open: false,
547 content: Default::default(),
548 smart_placement: true,
549 owner: Default::default(),
550 restrict_picking: true,
551 }
552 }
553
554 /// Sets the desired popup placement.
555 pub fn with_placement(mut self, placement: Placement) -> Self {
556 self.placement = placement;
557 self
558 }
559
560 /// Enables or disables smart placement.
561 pub fn with_smart_placement(mut self, smart_placement: bool) -> Self {
562 self.smart_placement = smart_placement;
563 self
564 }
565
566 /// Defines whether to keep the popup open when a user clicks outside its content or not.
567 pub fn stays_open(mut self, value: bool) -> Self {
568 self.stays_open = value;
569 self
570 }
571
572 /// Sets the content of the popup.
573 pub fn with_content(mut self, content: Handle<impl ObjectOrVariant<UiNode>>) -> Self {
574 self.content = content.to_base();
575 self
576 }
577
578 /// Sets the desired owner of the popup, to which the popup will relay its own messages.
579 pub fn with_owner(mut self, owner: Handle<UiNode>) -> Self {
580 self.owner = owner;
581 self
582 }
583
584 /// Sets a flag, that defines whether the popup should restrict all the mouse input or not.
585 pub fn with_restrict_picking(mut self, restrict: bool) -> Self {
586 self.restrict_picking = restrict;
587 self
588 }
589
590 /// Builds the popup widget, but does not add it to the user interface. Could be useful if you're making your
591 /// own derived version of the popup.
592 pub fn build_popup(self, ctx: &mut BuildContext) -> Popup {
593 let style = &ctx.style;
594
595 let body = BorderBuilder::new(
596 WidgetBuilder::new()
597 .with_background(style.property(Style::BRUSH_PRIMARY))
598 .with_foreground(style.property(Style::BRUSH_DARKEST))
599 .with_child(self.content),
600 )
601 .with_stroke_thickness(Thickness::uniform(1.0).into())
602 .build(ctx)
603 .to_base();
604
605 Popup {
606 widget: self
607 .widget_builder
608 .with_child(body)
609 .with_visibility(false)
610 .with_handle_os_events(true)
611 .build(ctx),
612 placement: self.placement.into(),
613 stays_open: self.stays_open.into(),
614 is_open: false.into(),
615 content: self.content.into(),
616 smart_placement: self.smart_placement.into(),
617 body: body.into(),
618 owner: self.owner,
619 restrict_picking: self.restrict_picking.into(),
620 }
621 }
622
623 /// Finishes building the [`Popup`] instance and adds to the user interface and returns its handle.
624 pub fn build(self, ctx: &mut BuildContext) -> Handle<Popup> {
625 let popup = self.build_popup(ctx);
626 ctx.add(popup)
627 }
628}
629
630#[cfg(test)]
631mod test {
632 use crate::popup::PopupBuilder;
633 use crate::{test::test_widget_deletion, widget::WidgetBuilder};
634
635 #[test]
636 fn test_deletion() {
637 test_widget_deletion(|ctx| PopupBuilder::new(WidgetBuilder::new()).build(ctx));
638 }
639}