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
8pub struct AnchoredState {
10 child_layout_ids: SmallVec<[LayoutId; 4]>,
11}
12
13pub 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
24pub 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 pub fn anchor(mut self, anchor: Anchor) -> Self {
40 self.anchor = anchor;
41 self
42 }
43
44 pub fn position(mut self, anchor: Point<Pixels>) -> Self {
47 self.anchor_position = Some(anchor);
48 self
49 }
50
51 pub fn offset(mut self, offset: Point<Pixels>) -> Self {
54 self.offset = Some(offset);
55 self
56 }
57
58 pub fn position_mode(mut self, mode: AnchoredPositionMode) -> Self {
62 self.position_mode = mode;
63 self
64 }
65
66 pub fn snap_to_window(mut self) -> Self {
68 self.fit_mode = AnchoredFitMode::SnapToWindow;
69 self
70 }
71
72 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 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 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#[derive(Copy, Clone, PartialEq)]
239pub enum AnchoredFitMode {
240 SnapToWindow,
242 SnapToWindowWithMargin(Edges<Pixels>),
244 SwitchAnchor,
246}
247
248#[derive(Copy, Clone, PartialEq)]
250pub enum AnchoredPositionMode {
251 Window,
253 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}