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