1use std::cmp::Ordering;
2
3use smallvec::SmallVec;
4
5use crate::{
6 Anchor, AnyElement, App, Axis, Bounds, Display, Edges, Element, GlobalElementId,
7 InspectorElementId, IntoElement, LayoutId, ParentElement, Pixels, Point, Position, Size, Style,
8 Window, point, px,
9};
10
11pub struct AnchoredState {
13 child_layout_ids: SmallVec<[LayoutId; 4]>,
14}
15
16pub struct Anchored {
19 children: SmallVec<[AnyElement; 2]>,
20 anchor: Anchor,
21 fit_mode: AnchoredFitMode,
22 placement: Placement,
23 position_mode: AnchoredPositionMode,
24 offset: Option<Point<Pixels>>,
25}
26
27#[derive(Clone, Copy)]
28enum Placement {
29 InPlace,
30 At(Point<Pixels>),
31 Attached {
32 target: Bounds<Pixels>,
33 corner: Anchor,
34 },
35}
36
37pub fn anchored() -> Anchored {
40 Anchored {
41 children: SmallVec::new(),
42 anchor: Anchor::TopLeft,
43 fit_mode: AnchoredFitMode::SwitchAnchor,
44 placement: Placement::InPlace,
45 position_mode: AnchoredPositionMode::Window,
46 offset: None,
47 }
48}
49
50impl Anchored {
51 pub fn anchor(mut self, anchor: Anchor) -> Self {
53 self.anchor = anchor;
54 self
55 }
56
57 pub fn position(mut self, anchor: Point<Pixels>) -> Self {
60 self.placement = Placement::At(anchor);
61 self
62 }
63
64 pub fn attach(mut self, target: Bounds<Pixels>, corner: Anchor) -> Self {
73 self.placement = Placement::Attached { target, corner };
74 self
75 }
76
77 pub fn offset(mut self, offset: Point<Pixels>) -> Self {
80 self.offset = Some(offset);
81 self
82 }
83
84 pub fn position_mode(mut self, mode: AnchoredPositionMode) -> Self {
89 self.position_mode = mode;
90 self
91 }
92
93 pub fn snap_to_window(mut self) -> Self {
95 self.fit_mode = AnchoredFitMode::SnapToWindow;
96 self
97 }
98
99 pub fn snap_to_window_with_margin(mut self, edges: impl Into<Edges<Pixels>>) -> Self {
101 self.fit_mode = AnchoredFitMode::SnapToWindowWithMargin(edges.into());
102 self
103 }
104}
105
106impl ParentElement for Anchored {
107 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
108 self.children.extend(elements)
109 }
110}
111
112impl Element for Anchored {
113 type RequestLayoutState = AnchoredState;
114 type PrepaintState = ();
115
116 fn id(&self) -> Option<crate::ElementId> {
117 None
118 }
119
120 fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
121 None
122 }
123
124 fn request_layout(
125 &mut self,
126 _id: Option<&GlobalElementId>,
127 _inspector_id: Option<&InspectorElementId>,
128 window: &mut Window,
129 cx: &mut App,
130 ) -> (crate::LayoutId, Self::RequestLayoutState) {
131 let child_layout_ids = self
132 .children
133 .iter_mut()
134 .map(|child| child.request_layout(window, cx))
135 .collect::<SmallVec<_>>();
136
137 let anchored_style = Style {
138 position: Position::Absolute,
139 display: Display::Flex,
140 ..Style::default()
141 };
142
143 let layout_id = window.request_layout(anchored_style, child_layout_ids.iter().copied(), cx);
144
145 (layout_id, AnchoredState { child_layout_ids })
146 }
147
148 fn prepaint(
149 &mut self,
150 _id: Option<&GlobalElementId>,
151 _inspector_id: Option<&InspectorElementId>,
152 bounds: Bounds<Pixels>,
153 request_layout: &mut Self::RequestLayoutState,
154 window: &mut Window,
155 cx: &mut App,
156 ) {
157 if request_layout.child_layout_ids.is_empty() {
158 return;
159 }
160
161 let children_bounds = request_layout
162 .child_layout_ids
163 .iter()
164 .map(|id| window.layout_bounds(*id))
165 .reduce(|acc, bounds| acc.union(&bounds))
166 .unwrap();
167
168 let anchor_position = match self.placement {
169 Placement::InPlace => None,
170 Placement::At(position) => Some(position),
171 Placement::Attached { target, corner } => Some(target.corner(corner)),
172 };
173 let (origin, mut desired) = self.position_mode.get_position_and_bounds(
174 anchor_position,
175 self.anchor,
176 children_bounds.size,
177 bounds,
178 self.offset,
179 );
180
181 let limits = Bounds {
182 origin: Point::default(),
183 size: window.viewport_size(),
184 };
185
186 if let Placement::Attached { target, mut corner } = self.placement {
187 let visible = limits.inset(window.client_inset.unwrap_or(px(0.)));
191 let overflow_along = |bounds: Bounds<Pixels>, axis: Axis| match axis {
192 Axis::Horizontal => {
193 (visible.left() - bounds.left()).max(px(0.))
194 + (bounds.right() - visible.right()).max(px(0.))
195 }
196 Axis::Vertical => {
197 (visible.top() - bounds.top()).max(px(0.))
198 + (bounds.bottom() - visible.bottom()).max(px(0.))
199 }
200 };
201 let mut anchor = self.anchor;
202 let mut offset = self.offset.unwrap_or_default();
203 for axis in [Axis::Horizontal, Axis::Vertical] {
204 let corner_side = side_along(corner, axis);
205 let is_outside_target = corner_side != Ordering::Equal
206 && side_along(anchor, axis) == corner_side.reverse();
207 let overflow = overflow_along(desired, axis);
208 if !is_outside_target || overflow == px(0.) {
209 continue;
210 }
211
212 let flipped_corner = corner.other_side_along(axis);
213 let flipped_anchor = anchor.other_side_along(axis);
214 let flipped_offset = match axis {
215 Axis::Horizontal => point(-offset.x, offset.y),
216 Axis::Vertical => point(offset.x, -offset.y),
217 };
218 let (_, flipped) = self.position_mode.get_position_and_bounds(
219 Some(target.corner(flipped_corner)),
220 flipped_anchor,
221 children_bounds.size,
222 bounds,
223 Some(flipped_offset),
224 );
225 if overflow_along(flipped, axis) < overflow {
226 corner = flipped_corner;
227 anchor = flipped_anchor;
228 offset = flipped_offset;
229 desired = flipped;
230 }
231 }
232 } else if self.fit_mode == AnchoredFitMode::SwitchAnchor {
233 let mut anchor = self.anchor;
234
235 if desired.left() < limits.left() || desired.right() > limits.right() {
236 let switched = Bounds::from_anchor_and_size(
237 anchor.other_side_along(Axis::Horizontal),
238 origin,
239 children_bounds.size,
240 );
241 if !(switched.left() < limits.left() || switched.right() > limits.right()) {
242 anchor = anchor.other_side_along(Axis::Horizontal);
243 desired = switched
244 }
245 }
246
247 if desired.top() < limits.top() || desired.bottom() > limits.bottom() {
248 let switched = Bounds::from_anchor_and_size(
249 anchor.other_side_along(Axis::Vertical),
250 origin,
251 children_bounds.size,
252 );
253 if !(switched.top() < limits.top() || switched.bottom() > limits.bottom()) {
254 desired = switched;
255 }
256 }
257 }
258
259 let client_inset = window.client_inset.unwrap_or(px(0.));
260 let edges = match self.fit_mode {
261 AnchoredFitMode::SnapToWindowWithMargin(edges) => edges,
262 _ => Edges::default(),
263 }
264 .map(|edge| *edge + client_inset);
265
266 if desired.right() > limits.right() {
269 desired.origin.x -= desired.right() - limits.right() + edges.right;
270 }
271 if desired.left() < limits.left() {
272 desired.origin.x = limits.origin.x + edges.left;
273 }
274
275 if desired.bottom() > limits.bottom() {
278 desired.origin.y -= desired.bottom() - limits.bottom() + edges.bottom;
279 }
280 if desired.top() < limits.top() {
281 desired.origin.y = limits.origin.y + edges.top;
282 }
283
284 let offset = desired.origin - bounds.origin;
285 let offset = point(offset.x.round(), offset.y.round());
286
287 window.with_element_offset(offset, |window| {
288 for child in &mut self.children {
289 child.prepaint(window, cx);
290 }
291 })
292 }
293
294 fn paint(
295 &mut self,
296 _id: Option<&GlobalElementId>,
297 _inspector_id: Option<&InspectorElementId>,
298 _bounds: crate::Bounds<crate::Pixels>,
299 _request_layout: &mut Self::RequestLayoutState,
300 _prepaint: &mut Self::PrepaintState,
301 window: &mut Window,
302 cx: &mut App,
303 ) {
304 for child in &mut self.children {
305 child.paint(window, cx);
306 }
307 }
308}
309
310impl IntoElement for Anchored {
311 type Element = Self;
312
313 fn into_element(self) -> Self::Element {
314 self
315 }
316}
317
318fn side_along(anchor: Anchor, axis: Axis) -> Ordering {
321 match axis {
322 Axis::Horizontal => match anchor {
323 Anchor::TopLeft | Anchor::BottomLeft | Anchor::LeftCenter => Ordering::Less,
324 Anchor::TopCenter | Anchor::BottomCenter => Ordering::Equal,
325 Anchor::TopRight | Anchor::BottomRight | Anchor::RightCenter => Ordering::Greater,
326 },
327 Axis::Vertical => match anchor {
328 Anchor::TopLeft | Anchor::TopRight | Anchor::TopCenter => Ordering::Less,
329 Anchor::LeftCenter | Anchor::RightCenter => Ordering::Equal,
330 Anchor::BottomLeft | Anchor::BottomRight | Anchor::BottomCenter => Ordering::Greater,
331 },
332 }
333}
334
335#[derive(Copy, Clone, PartialEq)]
337pub enum AnchoredFitMode {
338 SnapToWindow,
340 SnapToWindowWithMargin(Edges<Pixels>),
342 SwitchAnchor,
344}
345
346#[derive(Copy, Clone, PartialEq)]
348pub enum AnchoredPositionMode {
349 Window,
351 Local,
353}
354
355impl AnchoredPositionMode {
356 fn get_position_and_bounds(
357 &self,
358 anchor_position: Option<Point<Pixels>>,
359 anchor: Anchor,
360 size: Size<Pixels>,
361 bounds: Bounds<Pixels>,
362 offset: Option<Point<Pixels>>,
363 ) -> (Point<Pixels>, Bounds<Pixels>) {
364 let offset = offset.unwrap_or_default();
365
366 match self {
367 AnchoredPositionMode::Window => {
368 let anchor_position = anchor_position.unwrap_or(bounds.origin);
369 let bounds = Bounds::from_anchor_and_size(anchor, anchor_position + offset, size);
370 (anchor_position, bounds)
371 }
372 AnchoredPositionMode::Local => {
373 let anchor_position = anchor_position.unwrap_or_default();
374 let bounds = Bounds::from_anchor_and_size(
375 anchor,
376 bounds.origin + anchor_position + offset,
377 size,
378 );
379 (anchor_position, bounds)
380 }
381 }
382 }
383}
384
385#[cfg(test)]
386mod tests {
387 use crate::{
388 Anchor, Bounds, Context, Pixels, PlatformInput, Point, TestAppContext, Window, deferred,
389 div, point, prelude::*, px, size,
390 };
391
392 struct AnchoredTestView {
393 position: Point<Pixels>,
394 }
395
396 impl Render for AnchoredTestView {
397 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
398 div().size_full().child(
399 div()
400 .id("scroll-container")
401 .overflow_y_scroll()
402 .size_full()
403 .child(div().h(px(2000.)).w_full())
404 .child(
405 deferred(
406 super::anchored()
407 .snap_to_window()
408 .position(self.position)
409 .child(
410 div()
411 .id("menu")
412 .debug_selector(|| "MENU".into())
413 .w(px(200.))
414 .h(px(300.)),
415 ),
416 )
417 .with_priority(1),
418 ),
419 )
420 }
421 }
422
423 #[gpui::test]
424 fn test_anchored_position_without_scroll(cx: &mut TestAppContext) {
425 let window = cx.open_window(size(px(800.), px(600.)), |_, _| AnchoredTestView {
426 position: point(px(100.), px(100.)),
427 });
428
429 cx.run_until_parked();
430
431 let menu_bounds = window
432 .update(cx, |_, window, _| {
433 window.rendered_frame.debug_bounds.get("MENU").copied()
434 })
435 .unwrap()
436 .expect("MENU debug bounds not found");
437
438 assert_eq!(menu_bounds.origin, point(px(100.), px(100.)));
439 assert_eq!(menu_bounds.size, size(px(200.), px(300.)));
440 }
441
442 #[gpui::test]
443 fn test_anchored_position_when_scrolled(cx: &mut TestAppContext) {
444 let window = cx.open_window(size(px(800.), px(600.)), |_, _| AnchoredTestView {
445 position: point(px(100.), px(100.)),
446 });
447
448 cx.run_until_parked();
449
450 window
451 .update(cx, |_, window, cx| {
452 let event = gpui::ScrollEvent {
453 position: point(px(400.), px(300.)),
454 delta: gpui::ScrollDelta::Pixels(point(px(0.), px(-1000.))),
455 ..Default::default()
456 };
457 window.dispatch_event(PlatformInput::Scroll(event), cx);
458 })
459 .unwrap();
460
461 cx.run_until_parked();
462
463 let menu_bounds = window
464 .update(cx, |_, window, _| {
465 window.rendered_frame.debug_bounds.get("MENU").copied()
466 })
467 .unwrap()
468 .expect("MENU debug bounds not found");
469
470 assert_eq!(menu_bounds.origin, point(px(100.), px(100.)));
471 assert_eq!(menu_bounds.size, size(px(200.), px(300.)));
472 }
473
474 #[gpui::test]
475 fn test_anchored_snaps_to_window(cx: &mut TestAppContext) {
476 let window = cx.open_window(size(px(800.), px(600.)), |_, _| AnchoredTestView {
477 position: point(px(100.), px(500.)),
478 });
479
480 cx.run_until_parked();
481
482 let menu_bounds = window
483 .update(cx, |_, window, _| {
484 window.rendered_frame.debug_bounds.get("MENU").copied()
485 })
486 .unwrap()
487 .expect("MENU debug bounds not found");
488
489 assert_eq!(menu_bounds.origin, point(px(100.), px(300.)));
490 assert_eq!(menu_bounds.size, size(px(200.), px(300.)));
491 }
492
493 struct AttachedTestView {
494 target: Bounds<Pixels>,
495 corner: Anchor,
496 anchor: Anchor,
497 }
498
499 impl Render for AttachedTestView {
500 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
501 div().size_full().child(
502 deferred(
503 super::anchored()
504 .attach(self.target, self.corner)
505 .anchor(self.anchor)
506 .offset(point(px(-2.), px(4.)))
507 .snap_to_window_with_margin(px(8.))
508 .child(
509 div()
510 .debug_selector(|| "MENU".into())
511 .w(px(200.))
512 .h(px(300.)),
513 ),
514 )
515 .with_priority(1),
516 )
517 }
518 }
519
520 #[gpui::test]
521 fn test_anchored_attach_moves_to_the_side_with_room(cx: &mut TestAppContext) {
522 let trigger = |x: f32, y: f32| Bounds::new(point(px(x), px(y)), size(px(100.), px(20.)));
523 let submenu = (Anchor::TopRight, Anchor::TopLeft);
525 let dropdown = (Anchor::BottomLeft, Anchor::TopLeft);
527 for (target, (corner, anchor), expected_origin) in [
528 (trigger(100., 100.), submenu, point(px(198.), px(104.))),
529 (trigger(550., 100.), submenu, point(px(352.), px(104.))),
530 (trigger(100., 400.), dropdown, point(px(98.), px(96.))),
531 (trigger(498., 100.), submenu, point(px(596.), px(104.))),
534 (
537 Bounds::new(point(px(180.), px(100.)), size(px(500.), px(20.))),
538 submenu,
539 point(px(8.), px(104.)),
540 ),
541 (
544 Bounds::new(point(px(50.), px(100.)), size(px(650.), px(20.))),
545 submenu,
546 point(px(592.), px(104.)),
547 ),
548 (trigger(700., 100.), dropdown, point(px(592.), px(124.))),
551 (
553 trigger(100., 400.),
554 (Anchor::BottomCenter, Anchor::TopCenter),
555 point(px(48.), px(96.)),
556 ),
557 ] {
558 let window = cx.open_window(size(px(800.), px(600.)), |_, _| AttachedTestView {
559 target,
560 corner,
561 anchor,
562 });
563
564 cx.run_until_parked();
565
566 let menu_bounds = window
567 .update(cx, |_, window, _| {
568 window.rendered_frame.debug_bounds.get("MENU").copied()
569 })
570 .unwrap()
571 .expect("MENU debug bounds not found");
572
573 assert_eq!(
574 menu_bounds.origin, expected_origin,
575 "target: {target:?}, corner: {corner:?}, anchor: {anchor:?}"
576 );
577 }
578 }
579}