Skip to main content

ui/popover/
submenu.rs

1//! Where the panel a [`crate::menu::Item::Submenu`] row drops goes: beside the
2//! card that dropped it, on whichever side the window has room for.
3//!
4//! gpui's `anchored` fits a layer to the window around a *point*. Both of its
5//! modes are wrong for a submenu, because the point it would work from is the
6//! parent card's own right edge: snapping slides the panel left until it fits,
7//! straight over the card, and `SwitchAnchor` mirrors it about that edge, which
8//! lands it on the card almost exactly. A submenu has to clear a rectangle, so
9//! [`place`] works from one.
10//!
11//! The rectangle is not known when the element tree is built — the panel's
12//! width is a measurement and the card's edges move with whatever fitting the
13//! card itself went through. A canvas on the row reports it during prepaint,
14//! one deferred round before the panel is placed.
15
16use std::{cell::Cell, rc::Rc};
17
18use gpui::{
19    AnyElement, App, Bounds, Display, Element, GlobalElementId, InspectorElementId, IntoElement,
20    LayoutId, Pixels, Point, Position, Size, Style, Window, canvas, div, point, prelude::*, px,
21};
22
23use super::{MENU_PAD, SNAP};
24
25/// Which way a panel opens from the card it hangs on.
26#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
27pub enum Side {
28    #[default]
29    Right,
30    Left,
31}
32
33impl Side {
34    fn other(self) -> Self {
35        match self {
36            Side::Right => Side::Left,
37            Side::Left => Side::Right,
38        }
39    }
40}
41
42/// The side every panel of one open menu opens to.
43///
44/// Shared down the chain rather than settled per panel: a panel that had to
45/// flip is one whose own children have no room to the right either, and a
46/// deeper panel deciding on its own would open back across its parent.
47///
48/// A frame's worth of state, not a menu's. [`crate::menu::card`] makes one per
49/// render and every panel settles the side again from where the cards actually
50/// landed, so nothing here outlives the geometry it was read from.
51#[derive(Clone, Default)]
52pub struct Chain(Rc<Cell<Side>>);
53
54/// Where a panel of `size` goes, and which side it took.
55///
56/// `anchor` is the parent card's horizontal span at the top the panel's first
57/// row should line up with. A panel's edge sits on the card's with nothing
58/// between: a strip of nothing between the two is a strip the pointer crosses
59/// on its way in, and it would land on a sibling row and close what it was
60/// reaching for.
61///
62/// `prefer` is the side the rest of the chain took, kept unless the window has
63/// no room for it. `margin` is the gap held at the window edge when neither
64/// side fits and the panel is snapped instead.
65pub fn place(
66    anchor: Bounds<Pixels>,
67    size: Size<Pixels>,
68    viewport: Size<Pixels>,
69    margin: Pixels,
70    prefer: Side,
71) -> (Point<Pixels>, Side) {
72    let x = |side: Side| match side {
73        Side::Right => anchor.right(),
74        Side::Left => anchor.left() - size.width,
75    };
76    let fits = |side: Side| {
77        let left = x(side);
78        left >= margin && left + size.width + margin <= viewport.width
79    };
80    let side = match (fits(prefer), fits(prefer.other())) {
81        (false, true) => prefer.other(),
82        // Neither side fits: the preferred one is snapped below, which is what
83        // a window narrower than two panels can do.
84        _ => prefer,
85    };
86
87    let mut origin = point(x(side), anchor.origin.y);
88    if origin.x + size.width > viewport.width {
89        origin.x -= origin.x + size.width - viewport.width + margin;
90    }
91    if origin.x < Pixels::ZERO {
92        origin.x = margin;
93    }
94    // Vertically a panel slides rather than flipping, the way every menu does:
95    // its first row lines up with the row it hangs on, and a panel too tall for
96    // the room below that row rides up until it fits.
97    if origin.y + size.height > viewport.height {
98        origin.y -= origin.y + size.height - viewport.height + margin;
99    }
100    if origin.y < Pixels::ZERO {
101        origin.y = margin;
102    }
103    (origin, side)
104}
105
106/// The panel a submenu row drops, ready to be mounted on that row — which must
107/// be `relative()`, since everything here is placed against it.
108///
109/// `content` arrives already wrapped in its surface and its entrance: this
110/// positions it and nothing else.
111pub(super) fn layer(content: AnyElement, chain: &Chain) -> AnyElement {
112    let anchor = Rc::new(Cell::new(None));
113    let measure = {
114        let anchor = anchor.clone();
115        canvas(
116            move |bounds, _, _| anchor.set(Some(bounds)),
117            |_, _, _, _| {},
118        )
119    };
120    div()
121        .absolute()
122        .inset_0()
123        // The card's span at the row's top: the row is a block child of a card
124        // inset by `MENU_PAD` on every side, so its own box pushed out by that
125        // much on three sides is the card's, and the top is where the panel's
126        // first row has to start to line up with this one.
127        .child(
128            measure
129                .absolute()
130                .top(px(-MENU_PAD))
131                .left(px(-MENU_PAD))
132                .right(px(-MENU_PAD))
133                .h_0(),
134        )
135        .child(
136            // The panel's own layout context: a zero-size absolute box, which
137            // is what it is measured inside today. Nothing is read off this
138            // box's position but the placement it falls back to.
139            div()
140                .absolute()
141                .top(px(-MENU_PAD))
142                .right(px(-MENU_PAD))
143                .size_0()
144                .child(
145                    gpui::deferred(Panel {
146                        child: content,
147                        anchor,
148                        chain: chain.clone(),
149                    })
150                    .priority(1),
151                ),
152        )
153        .into_any_element()
154}
155
156/// The placed panel: gpui's `anchored` with [`place`]'s fitting instead of its
157/// own, and its layout — absolute, so the panel sizes to itself rather than to
158/// the row it hangs on.
159struct Panel {
160    child: AnyElement,
161    /// The rectangle the canvas measured this frame. `None` only if the panel
162    /// is prepainted before the row it hangs on, which the deferred rounds
163    /// rule out.
164    anchor: Rc<Cell<Option<Bounds<Pixels>>>>,
165    chain: Chain,
166}
167
168impl Element for Panel {
169    type RequestLayoutState = LayoutId;
170    type PrepaintState = ();
171
172    fn id(&self) -> Option<gpui::ElementId> {
173        None
174    }
175
176    fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
177        None
178    }
179
180    fn request_layout(
181        &mut self,
182        _id: Option<&GlobalElementId>,
183        _inspector_id: Option<&InspectorElementId>,
184        window: &mut Window,
185        cx: &mut App,
186    ) -> (LayoutId, LayoutId) {
187        let child = self.child.request_layout(window, cx);
188        let style = Style {
189            position: Position::Absolute,
190            display: Display::Flex,
191            ..Style::default()
192        };
193        (window.request_layout(style, [child], cx), child)
194    }
195
196    fn prepaint(
197        &mut self,
198        _id: Option<&GlobalElementId>,
199        _inspector_id: Option<&InspectorElementId>,
200        bounds: Bounds<Pixels>,
201        child: &mut LayoutId,
202        window: &mut Window,
203        cx: &mut App,
204    ) {
205        let size = window.layout_bounds(*child).size;
206        let origin = match self.anchor.get() {
207            Some(anchor) => {
208                let margin = px(SNAP) + window.client_inset().unwrap_or(Pixels::ZERO);
209                let (origin, side) = place(
210                    anchor,
211                    size,
212                    window.viewport_size(),
213                    margin,
214                    self.chain.0.get(),
215                );
216                self.chain.0.set(side);
217                origin
218            }
219            None => bounds.origin,
220        };
221        let offset = origin - bounds.origin;
222        window.with_element_offset(point(offset.x.round(), offset.y.round()), |window| {
223            self.child.prepaint(window, cx);
224        });
225    }
226
227    fn paint(
228        &mut self,
229        _id: Option<&GlobalElementId>,
230        _inspector_id: Option<&InspectorElementId>,
231        _bounds: Bounds<Pixels>,
232        _request_layout: &mut Self::RequestLayoutState,
233        _prepaint: &mut Self::PrepaintState,
234        window: &mut Window,
235        cx: &mut App,
236    ) {
237        self.child.paint(window, cx);
238    }
239}
240
241impl IntoElement for Panel {
242    type Element = Self;
243
244    fn into_element(self) -> Self::Element {
245        self
246    }
247}