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}