Skip to content

Commit 2b97c7d

Browse files
committed
popover: Improve to support auto switch corner when if overflow window bounds.
1 parent d0abcb2 commit 2b97c7d

File tree

3 files changed

+127
-38
lines changed

3 files changed

+127
-38
lines changed

crates/story/src/form_story.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use gpui::{
33
InteractiveElement, IntoElement, ParentElement as _, Render, Styled, Window,
44
};
55
use gpui_component::{
6-
button::{Button, ButtonGroup},
6+
button::{Button, ButtonGroup, ButtonVariants},
77
checkbox::Checkbox,
88
date_picker::DatePicker,
99
divider::Divider,
@@ -123,6 +123,7 @@ impl Render for FormStory {
123123
.child(
124124
ButtonGroup::new("size")
125125
.small()
126+
.outline()
126127
.child(
127128
Button::new("large")
128129
.selected(self.size == Size::Large)

crates/story/src/popup_story.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ impl Render for PopupStory {
225225
)
226226
.child(
227227
Popover::new("info-top-right")
228-
.anchor(Corner::TopRight)
228+
// .anchor(Corner::TopRight)
229229
.trigger(Button::new("info-top-right").label("Top Right"))
230230
.content(|window, cx| {
231231
cx.new(|cx| {
@@ -327,13 +327,13 @@ impl Render for PopupStory {
327327
.justify_between()
328328
.child(
329329
Popover::new("info-bottom-left")
330-
.anchor(Corner::BottomLeft)
330+
// .anchor(Corner::BottomLeft)
331331
.trigger(Button::new("pop").label("Popup with Form").w(px(300.)))
332332
.content(move |_, _| form.clone()),
333333
)
334334
.child(
335335
Popover::new("info-bottom-right")
336-
.anchor(Corner::BottomRight)
336+
// .anchor(Corner::BottomRight)
337337
.mouse_button(MouseButton::Right)
338338
.trigger(Button::new("pop").label("Mouse Right Click").w(px(300.)))
339339
.content(|window, cx| {

crates/ui/src/popover.rs

Lines changed: 122 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
use gpui::{
2-
actions, anchored, deferred, div, prelude::FluentBuilder as _, px, AnyElement, App, Bounds,
3-
Context, Corner, DismissEvent, DispatchPhase, Element, ElementId, Entity, EventEmitter,
4-
FocusHandle, Focusable, GlobalElementId, Hitbox, InteractiveElement as _, IntoElement,
5-
KeyBinding, LayoutId, ManagedView, MouseButton, MouseDownEvent, ParentElement, Pixels, Point,
6-
Render, Style, StyleRefinement, Styled, Window,
2+
actions, anchored, canvas, deferred, div, prelude::FluentBuilder as _, px, AnyElement, App,
3+
Axis, Bounds, Context, Corner, DismissEvent, DispatchPhase, Element, ElementId, Entity,
4+
EventEmitter, FocusHandle, Focusable, GlobalElementId, Hitbox, InteractiveElement as _,
5+
IntoElement, KeyBinding, LayoutId, ManagedView, MouseButton, MouseDownEvent, ParentElement,
6+
Pixels, Point, Render, Style, StyleRefinement, Styled, Window,
77
};
88
use std::{cell::RefCell, rc::Rc};
99

1010
use crate::{Selectable, StyledExt as _};
1111

1212
const CONTEXT: &str = "Popover";
13+
const WINDOW_EDGE_MARGIN: Pixels = px(8.);
1314

1415
actions!(popover, [Escape]);
1516

@@ -91,6 +92,22 @@ where
9192
}
9293
}
9394

95+
/// The anchor point of the popover relative to the trigger element.
96+
///
97+
/// Default: [`Corner::TopLeft`]
98+
///
99+
/// The anchor you can imagine an arrow corner of the Popover.
100+
///
101+
/// For example if use [`Corner::TopRight`]
102+
///
103+
/// Then the Popover will placement on trigger element's bottom left corner, like this:
104+
///
105+
/// ```
106+
/// [Trigger Button]
107+
/// |------------------------------------|
108+
/// | Popover with Corner::TopRight |
109+
/// |------------------------------------|
110+
/// ```
94111
pub fn anchor(mut self, anchor: Corner) -> Self {
95112
self.anchor = anchor;
96113
self
@@ -102,6 +119,10 @@ where
102119
self
103120
}
104121

122+
/// Set the trigger element of the popover.
123+
///
124+
/// The Trigger must impl [`Selectable`] trait,
125+
/// used for display selected state when popover is open.
105126
pub fn trigger<T>(mut self, trigger: T) -> Self
106127
where
107128
T: Selectable + IntoElement + 'static,
@@ -119,12 +140,12 @@ where
119140

120141
/// Set the content of the popover.
121142
///
122-
/// The `content` is a closure that returns an `AnyElement`.
123-
pub fn content<C>(mut self, content: C) -> Self
143+
/// The `builder` is a closure that returns an `AnyElement`.
144+
pub fn content<C>(mut self, builder: C) -> Self
124145
where
125146
C: Fn(&mut Window, &mut App) -> Entity<M> + 'static,
126147
{
127-
self.content = Some(Rc::new(content));
148+
self.content = Some(Rc::new(builder));
128149
self
129150
}
130151

@@ -147,8 +168,56 @@ where
147168
(trigger)(open, window, cx)
148169
}
149170

150-
fn resolved_corner(&self, bounds: Bounds<Pixels>) -> Point<Pixels> {
151-
bounds.corner(match self.anchor {
171+
fn resolved_corner(
172+
&self,
173+
trigger_bounds: Bounds<Pixels>,
174+
content_bounds: Option<Bounds<Pixels>>,
175+
window: &Window,
176+
) -> Point<Pixels> {
177+
let mut anchor = self.anchor;
178+
179+
// Switch corner based on content bounds if it overflows the window bounds.
180+
if let Some(content_bounds) = content_bounds {
181+
let window_size =
182+
window.bounds().size - gpui::size(WINDOW_EDGE_MARGIN, WINDOW_EDGE_MARGIN);
183+
184+
match anchor {
185+
Corner::TopLeft => {
186+
if content_bounds.right() >= window_size.width {
187+
anchor = anchor.other_side_corner_along(Axis::Horizontal);
188+
};
189+
if content_bounds.bottom() >= window_size.height {
190+
anchor = anchor.other_side_corner_along(Axis::Vertical);
191+
};
192+
}
193+
Corner::TopRight => {
194+
if content_bounds.left() <= WINDOW_EDGE_MARGIN {
195+
anchor = anchor.other_side_corner_along(Axis::Horizontal);
196+
};
197+
if content_bounds.bottom() >= window_size.height {
198+
anchor = anchor.other_side_corner_along(Axis::Vertical);
199+
};
200+
}
201+
Corner::BottomLeft => {
202+
if content_bounds.right() >= window_size.width {
203+
anchor = anchor.other_side_corner_along(Axis::Horizontal);
204+
};
205+
if content_bounds.top() <= WINDOW_EDGE_MARGIN {
206+
anchor = anchor.other_side_corner_along(Axis::Vertical);
207+
};
208+
}
209+
Corner::BottomRight => {
210+
if content_bounds.left() <= WINDOW_EDGE_MARGIN {
211+
anchor = anchor.other_side_corner_along(Axis::Horizontal);
212+
};
213+
if content_bounds.top() <= WINDOW_EDGE_MARGIN {
214+
anchor = anchor.other_side_corner_along(Axis::Vertical);
215+
};
216+
}
217+
}
218+
}
219+
220+
trigger_bounds.corner(match anchor {
152221
Corner::TopLeft => Corner::BottomLeft,
153222
Corner::TopRight => Corner::BottomRight,
154223
Corner::BottomLeft => Corner::TopLeft,
@@ -193,6 +262,7 @@ pub struct PopoverElementState<M> {
193262
content_view: Rc<RefCell<Option<Entity<M>>>>,
194263
/// Trigger bounds for positioning the popover.
195264
trigger_bounds: Option<Bounds<Pixels>>,
265+
content_bounds: Rc<RefCell<Option<Bounds<Pixels>>>>,
196266
}
197267

198268
impl<M> Default for PopoverElementState<M> {
@@ -204,6 +274,7 @@ impl<M> Default for PopoverElementState<M> {
204274
trigger_element: None,
205275
content_view: Rc::new(RefCell::new(None)),
206276
trigger_bounds: None,
277+
content_bounds: Rc::new(RefCell::new(None)),
207278
}
208279
}
209280
}
@@ -255,39 +326,56 @@ impl<M: ManagedView> Element for Popover<M> {
255326
if let Some(content_view) = element_state.content_view.borrow_mut().as_mut() {
256327
is_open = true;
257328

258-
let mut anchored = anchored()
259-
.snap_to_window_with_margin(px(8.))
260-
.anchor(view.anchor);
329+
let mut anchored = anchored().anchor(view.anchor);
261330
if let Some(trigger_bounds) = element_state.trigger_bounds {
262-
anchored = anchored.position(view.resolved_corner(trigger_bounds));
331+
let content_bounds = element_state.content_bounds.borrow();
332+
anchored = anchored.position(view.resolved_corner(
333+
trigger_bounds,
334+
*content_bounds,
335+
window,
336+
));
263337
}
264338

265339
let mut element = {
266340
let content_view_mut = element_state.content_view.clone();
267341
let anchor = view.anchor;
268342
let no_style = view.no_style;
343+
let content_bounds = element_state.content_bounds.clone();
344+
269345
deferred(
270-
anchored.child(
271-
div()
272-
.size_full()
273-
.occlude()
274-
.when(!no_style, |this| this.popover_style(cx))
275-
.map(|this| match anchor {
276-
Corner::TopLeft | Corner::TopRight => this.top_1p5(),
277-
Corner::BottomLeft | Corner::BottomRight => {
278-
this.bottom_1p5()
279-
}
280-
})
281-
.child(content_view.clone())
282-
.when(!no_style, |this| {
283-
this.on_mouse_down_out(move |_, window, _| {
284-
// Update the element_state.content_view to `None`,
285-
// so that the `paint`` method will not paint it.
286-
*content_view_mut.borrow_mut() = None;
287-
window.refresh();
346+
anchored
347+
.snap_to_window_with_margin(WINDOW_EDGE_MARGIN)
348+
.child(
349+
div()
350+
.size_full()
351+
.occlude()
352+
.when(!no_style, |this| this.popover_style(cx))
353+
.map(|this| match anchor {
354+
Corner::TopLeft | Corner::TopRight => this.top_1p5(),
355+
Corner::BottomLeft | Corner::BottomRight => {
356+
this.bottom_1p5()
357+
}
358+
})
359+
.child(content_view.clone())
360+
.when(!no_style, |this| {
361+
this.on_mouse_down_out(move |_, window, _| {
362+
// Update the element_state.content_view to `None`,
363+
// so that the `paint`` method will not paint it.
364+
*content_view_mut.borrow_mut() = None;
365+
window.refresh();
366+
})
288367
})
289-
}),
290-
),
368+
.child(
369+
canvas(
370+
|_, _, _| {},
371+
move |bounds, _, _, _| {
372+
content_bounds.borrow_mut().replace(bounds);
373+
},
374+
)
375+
.size_full()
376+
.absolute(),
377+
),
378+
),
291379
)
292380
.with_priority(1)
293381
.into_any()

0 commit comments

Comments
 (0)