Skip to main content

gpui_authoring/elements/
anchored.rs

1use smallvec::SmallVec;
2
3use crate::{
4    Anchor, AnyElement, App, Axis, Bounds, Display, Edges, Element, GlobalElementId, IntoElement,
5    LayoutId, ParentElement, Pixels, Point, Position, Size, Style, Window, point, px,
6};
7
8/// The state that the anchored element element uses to track its children.
9pub struct AnchoredState {
10    child_layout_ids: SmallVec<[LayoutId; 4]>,
11}
12
13/// An anchored element that can be used to display UI that
14/// will avoid overflowing the window bounds.
15pub struct Anchored {
16    children: SmallVec<[AnyElement; 2]>,
17    anchor: Anchor,
18    fit_mode: AnchoredFitMode,
19    anchor_position: Option<Point<Pixels>>,
20    position_mode: AnchoredPositionMode,
21    offset: Option<Point<Pixels>>,
22}
23
24/// anchored gives you an element that will avoid overflowing the window bounds.
25/// Its children should have no margin to avoid measurement issues.
26pub fn anchored() -> Anchored {
27    Anchored {
28        children: SmallVec::new(),
29        anchor: Anchor::TopLeft,
30        fit_mode: AnchoredFitMode::SwitchAnchor,
31        anchor_position: None,
32        position_mode: AnchoredPositionMode::Window,
33        offset: None,
34    }
35}
36
37impl Anchored {
38    /// Sets which corner of the anchored element should be anchored to the current position.
39    pub fn anchor(mut self, anchor: Anchor) -> Self {
40        self.anchor = anchor;
41        self
42    }
43
44    /// Sets the position in window coordinates
45    /// (otherwise the location the anchored element is rendered is used)
46    pub fn position(mut self, anchor: Point<Pixels>) -> Self {
47        self.anchor_position = Some(anchor);
48        self
49    }
50
51    /// Offset the final position by this amount.
52    /// Useful when you want to anchor to an element but offset from it, such as in PopoverMenu.
53    pub fn offset(mut self, offset: Point<Pixels>) -> Self {
54        self.offset = Some(offset);
55        self
56    }
57
58    /// Sets the position mode for this anchored element. Local will have this
59    /// interpret its [`Anchored::position`] as relative to the parent element.
60    /// While Window will have it interpret the position as relative to the window.
61    pub fn position_mode(mut self, mode: AnchoredPositionMode) -> Self {
62        self.position_mode = mode;
63        self
64    }
65
66    /// Snap to window edge instead of switching anchor corner when an overflow would occur.
67    pub fn snap_to_window(mut self) -> Self {
68        self.fit_mode = AnchoredFitMode::SnapToWindow;
69        self
70    }
71
72    /// Snap to window edge and leave some margins.
73    pub fn snap_to_window_with_margin(mut self, edges: impl Into<Edges<Pixels>>) -> Self {
74        self.fit_mode = AnchoredFitMode::SnapToWindowWithMargin(edges.into());
75        self
76    }
77}
78
79impl ParentElement for Anchored {
80    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
81        self.children.extend(elements)
82    }
83}
84
85impl Element for Anchored {
86    type RequestLayoutState = AnchoredState;
87    type PrepaintState = ();
88
89    fn id(&self) -> Option<crate::ElementId> {
90        None
91    }
92
93    fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
94        None
95    }
96
97    fn request_layout(
98        &mut self,
99        _id: Option<&GlobalElementId>,
100        window: &mut Window,
101        cx: &mut App,
102    ) -> (crate::LayoutId, Self::RequestLayoutState) {
103        let child_layout_ids = self
104            .children
105            .iter_mut()
106            .map(|child| child.request_layout(window, cx))
107            .collect::<SmallVec<_>>();
108
109        let anchored_style = Style {
110            position: Position::Absolute,
111            display: Display::Flex,
112            ..Style::default()
113        };
114
115        let layout_id = window.request_layout(anchored_style, child_layout_ids.iter().copied(), cx);
116
117        (layout_id, AnchoredState { child_layout_ids })
118    }
119
120    fn prepaint(
121        &mut self,
122        _id: Option<&GlobalElementId>,
123        bounds: Bounds<Pixels>,
124        request_layout: &mut Self::RequestLayoutState,
125        window: &mut Window,
126        cx: &mut App,
127    ) {
128        if request_layout.child_layout_ids.is_empty() {
129            return;
130        }
131
132        let children_bounds = request_layout
133            .child_layout_ids
134            .iter()
135            .map(|id| window.layout_bounds(*id))
136            .reduce(|acc, bounds| acc.union(&bounds))
137            .unwrap();
138
139        let (origin, mut desired) = self.position_mode.get_position_and_bounds(
140            self.anchor_position,
141            self.anchor,
142            children_bounds.size,
143            bounds,
144            self.offset,
145        );
146
147        let limits = Bounds {
148            origin: Point::default(),
149            size: window.viewport_size(),
150        };
151
152        if self.fit_mode == AnchoredFitMode::SwitchAnchor {
153            let mut anchor = self.anchor;
154
155            if desired.left() < limits.left() || desired.right() > limits.right() {
156                let switched = Bounds::from_anchor_and_size(
157                    anchor.other_side_along(Axis::Horizontal),
158                    origin,
159                    children_bounds.size,
160                );
161                if !(switched.left() < limits.left() || switched.right() > limits.right()) {
162                    anchor = anchor.other_side_along(Axis::Horizontal);
163                    desired = switched
164                }
165            }
166
167            if desired.top() < limits.top() || desired.bottom() > limits.bottom() {
168                let switched = Bounds::from_anchor_and_size(
169                    anchor.other_side_along(Axis::Vertical),
170                    origin,
171                    children_bounds.size,
172                );
173                if !(switched.top() < limits.top() || switched.bottom() > limits.bottom()) {
174                    desired = switched;
175                }
176            }
177        }
178
179        let client_inset = window.core.client_inset.unwrap_or(px(0.));
180        let edges = match self.fit_mode {
181            AnchoredFitMode::SnapToWindowWithMargin(edges) => edges,
182            _ => Edges::default(),
183        }
184        .map(|edge| *edge + client_inset);
185
186        // Snap the horizontal edges of the anchored element to the horizontal edges of the window if
187        // its horizontal bounds overflow, aligning to the left if it is wider than the limits.
188        if desired.right() > limits.right() {
189            desired.origin.x -= desired.right() - limits.right() + edges.right;
190        }
191        if desired.left() < limits.left() {
192            desired.origin.x = limits.origin.x + edges.left;
193        }
194
195        // Snap the vertical edges of the anchored element to the vertical edges of the window if
196        // its vertical bounds overflow, aligning to the top if it is taller than the limits.
197        if desired.bottom() > limits.bottom() {
198            desired.origin.y -= desired.bottom() - limits.bottom() + edges.bottom;
199        }
200        if desired.top() < limits.top() {
201            desired.origin.y = limits.origin.y + edges.top;
202        }
203
204        let offset = desired.origin - bounds.origin;
205        let offset = point(offset.x.round(), offset.y.round());
206
207        window.with_element_offset(offset, |window| {
208            for child in &mut self.children {
209                child.prepaint(window, cx);
210            }
211        })
212    }
213
214    fn paint(
215        &mut self,
216        _id: Option<&GlobalElementId>,
217        _bounds: crate::Bounds<crate::Pixels>,
218        _request_layout: &mut Self::RequestLayoutState,
219        _prepaint: &mut Self::PrepaintState,
220        window: &mut Window,
221        cx: &mut App,
222    ) {
223        for child in &mut self.children {
224            child.paint(window, cx);
225        }
226    }
227}
228
229impl IntoElement for Anchored {
230    type Element = Self;
231
232    fn into_element(self) -> Self::Element {
233        self
234    }
235}
236
237/// Which algorithm to use when fitting the anchored element to be inside the window.
238#[derive(Copy, Clone, PartialEq)]
239pub enum AnchoredFitMode {
240    /// Snap the anchored element to the window edge.
241    SnapToWindow,
242    /// Snap to window edge and leave some margins.
243    SnapToWindowWithMargin(Edges<Pixels>),
244    /// Switch which corner anchor this anchored element is attached to.
245    SwitchAnchor,
246}
247
248/// Which algorithm to use when positioning the anchored element.
249#[derive(Copy, Clone, PartialEq)]
250pub enum AnchoredPositionMode {
251    /// Position the anchored element relative to the window.
252    Window,
253    /// Position the anchored element relative to its parent.
254    Local,
255}
256
257impl AnchoredPositionMode {
258    fn get_position_and_bounds(
259        &self,
260        anchor_position: Option<Point<Pixels>>,
261        anchor: Anchor,
262        size: Size<Pixels>,
263        bounds: Bounds<Pixels>,
264        offset: Option<Point<Pixels>>,
265    ) -> (Point<Pixels>, Bounds<Pixels>) {
266        let offset = offset.unwrap_or_default();
267
268        match self {
269            AnchoredPositionMode::Window => {
270                let anchor_position = anchor_position.unwrap_or(bounds.origin);
271                let bounds = Bounds::from_anchor_and_size(anchor, anchor_position + offset, size);
272                (anchor_position, bounds)
273            }
274            AnchoredPositionMode::Local => {
275                let anchor_position = anchor_position.unwrap_or_default();
276                let bounds = Bounds::from_anchor_and_size(
277                    anchor,
278                    bounds.origin + anchor_position + offset,
279                    size,
280                );
281                (anchor_position, bounds)
282            }
283        }
284    }
285}
286
287#[cfg(test)]
288mod tests {
289    use crate::{
290        Context, Pixels, PlatformInput, Point, TestAppContext, Window, deferred, div, point,
291        prelude::*, px, size,
292    };
293
294    struct AnchoredTestView {
295        position: Point<Pixels>,
296    }
297
298    impl Render for AnchoredTestView {
299        fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
300            div().size_full().child(
301                div()
302                    .id("scroll-container")
303                    .overflow_y_scroll()
304                    .size_full()
305                    .child(div().h(px(2000.)).w_full())
306                    .child(
307                        deferred(
308                            super::anchored()
309                                .snap_to_window()
310                                .position(self.position)
311                                .child(
312                                    div()
313                                        .id("menu")
314                                        .debug_selector(|| "MENU".into())
315                                        .w(px(200.))
316                                        .h(px(300.)),
317                                ),
318                        )
319                        .with_priority(1),
320                    ),
321            )
322        }
323    }
324
325    #[gpui::test]
326    fn test_anchored_position_without_scroll(cx: &mut TestAppContext) {
327        let window = cx.open_window(size(px(800.), px(600.)), |_, _| AnchoredTestView {
328            position: point(px(100.), px(100.)),
329        });
330
331        cx.run_until_parked();
332
333        let menu_bounds = window
334            .update(cx, |_, window, _| {
335                window
336                    .frame_state
337                    .rendered_frame
338                    .debug_bounds
339                    .get("MENU")
340                    .copied()
341            })
342            .unwrap()
343            .expect("MENU debug bounds not found");
344
345        assert_eq!(menu_bounds.origin, point(px(100.), px(100.)));
346        assert_eq!(menu_bounds.size, size(px(200.), px(300.)));
347    }
348
349    #[gpui::test]
350    fn test_anchored_position_when_scrolled(cx: &mut TestAppContext) {
351        let window = cx.open_window(size(px(800.), px(600.)), |_, _| AnchoredTestView {
352            position: point(px(100.), px(100.)),
353        });
354
355        cx.run_until_parked();
356
357        window
358            .update(cx, |_, window, cx| {
359                let event = gpui::ScrollWheelEvent {
360                    position: point(px(400.), px(300.)),
361                    delta: gpui::ScrollDelta::Pixels(point(px(0.), px(-1000.))),
362                    ..Default::default()
363                };
364                window.dispatch_event(PlatformInput::ScrollWheel(event), cx);
365            })
366            .unwrap();
367
368        cx.run_until_parked();
369
370        let menu_bounds = window
371            .update(cx, |_, window, _| {
372                window
373                    .frame_state
374                    .rendered_frame
375                    .debug_bounds
376                    .get("MENU")
377                    .copied()
378            })
379            .unwrap()
380            .expect("MENU debug bounds not found");
381
382        assert_eq!(menu_bounds.origin, point(px(100.), px(100.)));
383        assert_eq!(menu_bounds.size, size(px(200.), px(300.)));
384    }
385
386    #[gpui::test]
387    fn test_anchored_snaps_to_window(cx: &mut TestAppContext) {
388        let window = cx.open_window(size(px(800.), px(600.)), |_, _| AnchoredTestView {
389            position: point(px(100.), px(500.)),
390        });
391
392        cx.run_until_parked();
393
394        let menu_bounds = window
395            .update(cx, |_, window, _| {
396                window
397                    .frame_state
398                    .rendered_frame
399                    .debug_bounds
400                    .get("MENU")
401                    .copied()
402            })
403            .unwrap()
404            .expect("MENU debug bounds not found");
405
406        assert_eq!(menu_bounds.origin, point(px(100.), px(300.)));
407        assert_eq!(menu_bounds.size, size(px(200.), px(300.)));
408    }
409}