rg3d_ui/
popup.rs

1use crate::{
2    border::BorderBuilder,
3    core::{algebra::Vector2, math::Rect, pool::Handle},
4    define_constructor,
5    message::{ButtonState, MessageDirection, OsEvent, UiMessage},
6    widget::{Widget, WidgetBuilder, WidgetMessage},
7    BuildContext, Control, NodeHandleMapping, RestrictionEntry, Thickness, UiNode, UserInterface,
8    BRUSH_DARKER, BRUSH_LIGHTER,
9};
10use std::{
11    any::{Any, TypeId},
12    ops::{Deref, DerefMut},
13};
14
15#[derive(Debug, Clone, PartialEq)]
16pub enum PopupMessage {
17    Open,
18    Close,
19    Content(Handle<UiNode>),
20    Placement(Placement),
21    AdjustPosition,
22}
23
24impl PopupMessage {
25    define_constructor!(PopupMessage:Open => fn open(), layout: false);
26    define_constructor!(PopupMessage:Close => fn close(), layout: false);
27    define_constructor!(PopupMessage:Content => fn content(Handle<UiNode>), layout: false);
28    define_constructor!(PopupMessage:Placement => fn placement(Placement), layout: false);
29    define_constructor!(PopupMessage:AdjustPosition => fn adjust_position(), layout: true);
30}
31
32#[derive(Copy, Clone, PartialEq, Debug)]
33pub enum Placement {
34    /// A popup should be placed relative to given widget at the left top corner of the widget screen bounds.
35    /// Widget handle could be `NONE`, in this case the popup will be placed at the left top corner of the screen.
36    LeftTop(Handle<UiNode>),
37
38    /// A popup should be placed relative to given widget at the right top corner of the widget screen bounds.
39    /// Widget handle could be `NONE`, in this case the popup will be placed at the right top corner of the screen.
40    RightTop(Handle<UiNode>),
41
42    /// A popup should be placed relative to given widget at the center of the widget screen bounds.
43    /// Widget handle could be `NONE`, in this case the popup will be placed at the center of the screen.
44    Center(Handle<UiNode>),
45
46    /// A popup should be placed relative to given widget at the left bottom corner of the widget screen bounds.
47    /// Widget handle could be `NONE`, in this case the popup will be placed at the left bottom corner of the screen.
48    LeftBottom(Handle<UiNode>),
49
50    /// A popup should be placed relative to given widget at the right bottom corner of the widget screen bounds.
51    /// Widget handle could be `NONE`, in this case the popup will be placed at the right bottom corner of the screen.
52    RightBottom(Handle<UiNode>),
53
54    /// A popup should be placed at the cursor position. The widget handle could be either `NONE` or a handle of a
55    /// widget that is directly behind the cursor.
56    Cursor(Handle<UiNode>),
57
58    /// A popup should be placed at given screen-space position.
59    Position {
60        /// Screen-space position.
61        position: Vector2<f32>,
62
63        /// A handle of the node that is located behind the given position. Could be `NONE` if there is nothing behind
64        /// given position.
65        target: Handle<UiNode>,
66    },
67}
68
69#[derive(Clone)]
70pub struct Popup {
71    widget: Widget,
72    placement: Placement,
73    stays_open: bool,
74    is_open: bool,
75    content: Handle<UiNode>,
76    body: Handle<UiNode>,
77    smart_placement: bool,
78}
79
80crate::define_widget_deref!(Popup);
81
82fn adjust_placement_position(
83    node_screen_bounds: Rect<f32>,
84    screen_size: Vector2<f32>,
85) -> Vector2<f32> {
86    let mut new_position = node_screen_bounds.position;
87    let right_bottom = node_screen_bounds.right_bottom_corner();
88    if right_bottom.x > screen_size.x {
89        new_position.x -= right_bottom.x - screen_size.x;
90    }
91    if right_bottom.y > screen_size.y {
92        new_position.y -= right_bottom.y - screen_size.y;
93    }
94    new_position
95}
96
97impl Popup {
98    fn left_top_placement(&self, ui: &UserInterface, target: Handle<UiNode>) -> Vector2<f32> {
99        ui.try_get_node(target)
100            .map(|n| n.screen_position())
101            .unwrap_or_default()
102    }
103
104    fn right_top_placement(&self, ui: &UserInterface, target: Handle<UiNode>) -> Vector2<f32> {
105        ui.try_get_node(target)
106            .map(|n| n.screen_position() + Vector2::new(n.actual_size().x, 0.0))
107            .unwrap_or_else(|| Vector2::new(ui.screen_size().x - self.widget.actual_size().x, 0.0))
108    }
109
110    fn center_placement(&self, ui: &UserInterface, target: Handle<UiNode>) -> Vector2<f32> {
111        ui.try_get_node(target)
112            .map(|n| n.screen_position() + n.actual_size().scale(0.5))
113            .unwrap_or_else(|| (ui.screen_size - self.widget.actual_size()).scale(0.5))
114    }
115
116    fn left_bottom_placement(&self, ui: &UserInterface, target: Handle<UiNode>) -> Vector2<f32> {
117        ui.try_get_node(target)
118            .map(|n| n.screen_position() + Vector2::new(0.0, n.actual_size().y))
119            .unwrap_or_else(|| Vector2::new(0.0, ui.screen_size().y - self.widget.actual_size().y))
120    }
121
122    fn right_bottom_placement(&self, ui: &UserInterface, target: Handle<UiNode>) -> Vector2<f32> {
123        ui.try_get_node(target)
124            .map(|n| n.screen_position() + n.actual_size())
125            .unwrap_or_else(|| ui.screen_size - self.widget.actual_size())
126    }
127}
128
129impl Control for Popup {
130    fn query_component(&self, type_id: TypeId) -> Option<&dyn Any> {
131        if type_id == TypeId::of::<Self>() {
132            Some(self)
133        } else {
134            None
135        }
136    }
137
138    fn resolve(&mut self, node_map: &NodeHandleMapping) {
139        node_map.resolve(&mut self.content);
140        node_map.resolve(&mut self.body);
141    }
142
143    fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
144        self.widget.handle_routed_message(ui, message);
145
146        if let Some(msg) = message.data::<PopupMessage>() {
147            if message.destination() == self.handle() {
148                match msg {
149                    PopupMessage::Open => {
150                        if !self.is_open {
151                            self.is_open = true;
152                            ui.send_message(WidgetMessage::visibility(
153                                self.handle(),
154                                MessageDirection::ToWidget,
155                                true,
156                            ));
157                            ui.push_picking_restriction(RestrictionEntry {
158                                handle: self.handle(),
159                                stop: false,
160                            });
161                            ui.send_message(WidgetMessage::topmost(
162                                self.handle(),
163                                MessageDirection::ToWidget,
164                            ));
165                            let position = match self.placement {
166                                Placement::LeftTop(target) => self.left_top_placement(ui, target),
167                                Placement::RightTop(target) => self.right_top_placement(ui, target),
168                                Placement::Center(target) => self.center_placement(ui, target),
169                                Placement::LeftBottom(target) => {
170                                    self.left_bottom_placement(ui, target)
171                                }
172                                Placement::RightBottom(target) => {
173                                    self.right_bottom_placement(ui, target)
174                                }
175                                Placement::Cursor(_) => ui.cursor_position(),
176                                Placement::Position { position, .. } => position,
177                            };
178                            ui.send_message(WidgetMessage::desired_position(
179                                self.handle(),
180                                MessageDirection::ToWidget,
181                                position,
182                            ));
183                            if self.smart_placement {
184                                ui.send_message(PopupMessage::adjust_position(
185                                    self.handle,
186                                    MessageDirection::ToWidget,
187                                ));
188                            }
189                        }
190                    }
191                    PopupMessage::Close => {
192                        if self.is_open {
193                            self.is_open = false;
194                            ui.send_message(WidgetMessage::visibility(
195                                self.handle(),
196                                MessageDirection::ToWidget,
197                                false,
198                            ));
199                            ui.remove_picking_restriction(self.handle());
200                            if ui.captured_node() == self.handle() {
201                                ui.release_mouse_capture();
202                            }
203                        }
204                    }
205                    PopupMessage::Content(content) => {
206                        if self.content.is_some() {
207                            ui.send_message(WidgetMessage::remove(
208                                self.content,
209                                MessageDirection::ToWidget,
210                            ));
211                        }
212                        self.content = *content;
213
214                        ui.send_message(WidgetMessage::link(
215                            self.content,
216                            MessageDirection::ToWidget,
217                            self.body,
218                        ));
219                    }
220                    PopupMessage::Placement(placement) => {
221                        self.placement = *placement;
222                        self.invalidate_layout();
223                    }
224                    PopupMessage::AdjustPosition => {
225                        let new_position =
226                            adjust_placement_position(self.screen_bounds(), ui.screen_size());
227
228                        if new_position != self.screen_position() {
229                            ui.send_message(WidgetMessage::desired_position(
230                                self.handle,
231                                MessageDirection::ToWidget,
232                                new_position,
233                            ));
234                        }
235                    }
236                }
237            }
238        }
239    }
240
241    fn handle_os_event(
242        &mut self,
243        self_handle: Handle<UiNode>,
244        ui: &mut UserInterface,
245        event: &OsEvent,
246    ) {
247        if let OsEvent::MouseInput { state, .. } = event {
248            if let Some(top_restriction) = ui.top_picking_restriction() {
249                if *state == ButtonState::Pressed
250                    && top_restriction.handle == self_handle
251                    && self.is_open
252                {
253                    let pos = ui.cursor_position();
254                    if !self.widget.screen_bounds().contains(pos) && !self.stays_open {
255                        ui.send_message(PopupMessage::close(
256                            self.handle(),
257                            MessageDirection::ToWidget,
258                        ));
259                    }
260                }
261            }
262        }
263    }
264}
265
266pub struct PopupBuilder {
267    widget_builder: WidgetBuilder,
268    placement: Placement,
269    stays_open: bool,
270    content: Handle<UiNode>,
271    smart_placement: bool,
272}
273
274impl PopupBuilder {
275    pub fn new(widget_builder: WidgetBuilder) -> Self {
276        Self {
277            widget_builder,
278            placement: Placement::Cursor(Default::default()),
279            stays_open: false,
280            content: Default::default(),
281            smart_placement: true,
282        }
283    }
284
285    pub fn with_placement(mut self, placement: Placement) -> Self {
286        self.placement = placement;
287        self
288    }
289
290    pub fn with_smart_placement(mut self, smart_placement: bool) -> Self {
291        self.smart_placement = smart_placement;
292        self
293    }
294
295    pub fn stays_open(mut self, value: bool) -> Self {
296        self.stays_open = value;
297        self
298    }
299
300    pub fn with_content(mut self, content: Handle<UiNode>) -> Self {
301        self.content = content;
302        self
303    }
304
305    pub fn build(self, ctx: &mut BuildContext) -> Handle<UiNode> {
306        let body = BorderBuilder::new(
307            WidgetBuilder::new()
308                .with_background(BRUSH_DARKER)
309                .with_foreground(BRUSH_LIGHTER)
310                .with_child(self.content),
311        )
312        .with_stroke_thickness(Thickness::uniform(1.0))
313        .build(ctx);
314
315        let popup = Popup {
316            widget: self
317                .widget_builder
318                .with_child(body)
319                .with_visibility(false)
320                .with_handle_os_events(true)
321                .build(),
322            placement: self.placement,
323            stays_open: self.stays_open,
324            is_open: false,
325            content: self.content,
326            smart_placement: self.smart_placement,
327            body,
328        };
329
330        ctx.add_node(UiNode::new(popup))
331    }
332}