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 LeftTop(Handle<UiNode>),
37
38 RightTop(Handle<UiNode>),
41
42 Center(Handle<UiNode>),
45
46 LeftBottom(Handle<UiNode>),
49
50 RightBottom(Handle<UiNode>),
53
54 Cursor(Handle<UiNode>),
57
58 Position {
60 position: Vector2<f32>,
62
63 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}