1use smallvec::SmallVec;
2
3use crate::{
4 AnyElement, App, Axis, Bounds, Corner, 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_corner: Corner,
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_corner: Corner::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: Corner) -> Self {
41 self.anchor_corner = 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 let size = children_bounds.size;
142
143 let (origin, mut desired) = self.position_mode.get_position_and_bounds(
144 self.anchor_position,
145 self.anchor_corner,
146 size,
147 bounds,
148 self.offset,
149 );
150
151 let limits = Bounds {
152 origin: Point::default(),
153 size: window.viewport_size(),
154 };
155
156 if self.fit_mode == AnchoredFitMode::SwitchAnchor {
157 let mut anchor_corner = self.anchor_corner;
158
159 if desired.left() < limits.left() || desired.right() > limits.right() {
160 let switched = Bounds::from_corner_and_size(
161 anchor_corner.other_side_corner_along(Axis::Horizontal),
162 origin,
163 size,
164 );
165 if !(switched.left() < limits.left() || switched.right() > limits.right()) {
166 anchor_corner = anchor_corner.other_side_corner_along(Axis::Horizontal);
167 desired = switched
168 }
169 }
170
171 if desired.top() < limits.top() || desired.bottom() > limits.bottom() {
172 let switched = Bounds::from_corner_and_size(
173 anchor_corner.other_side_corner_along(Axis::Vertical),
174 origin,
175 size,
176 );
177 if !(switched.top() < limits.top() || switched.bottom() > limits.bottom()) {
178 desired = switched;
179 }
180 }
181 }
182
183 let client_inset = window.client_inset.unwrap_or(px(0.));
184 let edges = match self.fit_mode {
185 AnchoredFitMode::SnapToWindowWithMargin(edges) => edges,
186 _ => Edges::default(),
187 }
188 .map(|edge| *edge + client_inset);
189
190 if desired.right() > limits.right() {
193 desired.origin.x -= desired.right() - limits.right() + edges.right;
194 }
195 if desired.left() < limits.left() {
196 desired.origin.x = limits.origin.x + edges.left;
197 }
198
199 if desired.bottom() > limits.bottom() {
202 desired.origin.y -= desired.bottom() - limits.bottom() + edges.bottom;
203 }
204 if desired.top() < limits.top() {
205 desired.origin.y = limits.origin.y + edges.top;
206 }
207
208 let offset = desired.origin - bounds.origin;
209 let offset = point(offset.x.round(), offset.y.round());
210
211 window.with_element_offset(offset, |window| {
212 for child in &mut self.children {
213 child.prepaint(window, cx);
214 }
215 })
216 }
217
218 fn paint(
219 &mut self,
220 _id: Option<&GlobalElementId>,
221 _inspector_id: Option<&InspectorElementId>,
222 _bounds: crate::Bounds<crate::Pixels>,
223 _request_layout: &mut Self::RequestLayoutState,
224 _prepaint: &mut Self::PrepaintState,
225 window: &mut Window,
226 cx: &mut App,
227 ) {
228 for child in &mut self.children {
229 child.paint(window, cx);
230 }
231 }
232}
233
234impl IntoElement for Anchored {
235 type Element = Self;
236
237 fn into_element(self) -> Self::Element {
238 self
239 }
240}
241
242#[derive(Copy, Clone, PartialEq)]
244pub enum AnchoredFitMode {
245 SnapToWindow,
247 SnapToWindowWithMargin(Edges<Pixels>),
249 SwitchAnchor,
251}
252
253#[derive(Copy, Clone, PartialEq)]
255pub enum AnchoredPositionMode {
256 Window,
258 Local,
260}
261
262impl AnchoredPositionMode {
263 fn get_position_and_bounds(
264 &self,
265 anchor_position: Option<Point<Pixels>>,
266 anchor_corner: Corner,
267 size: Size<Pixels>,
268 bounds: Bounds<Pixels>,
269 offset: Option<Point<Pixels>>,
270 ) -> (Point<Pixels>, Bounds<Pixels>) {
271 let offset = offset.unwrap_or_default();
272
273 match self {
274 AnchoredPositionMode::Window => {
275 let anchor_position = anchor_position.unwrap_or(bounds.origin);
276 let bounds =
277 Bounds::from_corner_and_size(anchor_corner, anchor_position + offset, size);
278 (anchor_position, bounds)
279 }
280 AnchoredPositionMode::Local => {
281 let anchor_position = anchor_position.unwrap_or_default();
282 let bounds = Bounds::from_corner_and_size(
283 anchor_corner,
284 bounds.origin + anchor_position + offset,
285 size,
286 );
287 (anchor_position, bounds)
288 }
289 }
290 }
291}
292
293#[cfg(test)]
294mod tests {
295 use crate::{
296 Pixels, PlatformInput, Point, ScrollDelta, ScrollEvent, TestAppContext, Window, deferred,
297 div, point, prelude::*, px, size,
298 };
299
300 struct AnchoredTestView {
301 position: Point<Pixels>,
302 }
303
304 impl Render for AnchoredTestView {
305 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
306 div().size_full().child(
307 div()
308 .id("scroll-container")
309 .overflow_y_scroll()
310 .size_full()
311 .child(div().h(px(2000.)).w_full())
312 .child(
313 deferred(
314 super::anchored()
315 .snap_to_window()
316 .position(self.position)
317 .child(
318 div()
319 .id("menu")
320 .debug_selector(|| "MENU".into())
321 .w(px(200.))
322 .h(px(300.)),
323 ),
324 )
325 .with_priority(1),
326 ),
327 )
328 }
329 }
330
331 fn draw_anchored_test_view(
332 cx: &mut TestAppContext,
333 position: Point<Pixels>,
334 ) -> &mut crate::VisualTestContext {
335 let (_, cx) = cx.add_window_view(|_, _| AnchoredTestView { position });
336 cx.simulate_resize(size(px(800.), px(600.)));
337 cx
338 }
339
340 fn menu_bounds(cx: &mut crate::VisualTestContext) -> crate::Bounds<Pixels> {
341 cx.debug_bounds("MENU")
342 .expect("MENU debug bounds not found")
343 }
344
345 #[gpui::test]
346 fn test_anchored_position_without_scroll(cx: &mut TestAppContext) {
347 let cx = draw_anchored_test_view(cx, point(px(100.), px(100.)));
348
349 let menu_bounds = menu_bounds(cx);
350
351 assert_eq!(menu_bounds.origin, point(px(100.), px(100.)));
352 assert_eq!(menu_bounds.size, size(px(200.), px(300.)));
353 }
354
355 #[gpui::test]
356 fn test_anchored_position_when_scrolled(cx: &mut TestAppContext) {
357 let cx = draw_anchored_test_view(cx, point(px(100.), px(100.)));
358
359 cx.update(|window, cx| {
360 window.dispatch_event(
361 PlatformInput::ScrollWheel(ScrollEvent {
362 position: point(px(400.), px(300.)),
363 delta: ScrollDelta::Pixels(point(px(0.), px(-1000.))),
364 ..Default::default()
365 }),
366 cx,
367 );
368 });
369 cx.run_until_parked();
370
371 let menu_bounds = menu_bounds(cx);
372
373 assert_eq!(menu_bounds.origin, point(px(100.), px(100.)));
374 assert_eq!(menu_bounds.size, size(px(200.), px(300.)));
375 }
376
377 #[gpui::test]
378 fn test_anchored_snaps_to_window(cx: &mut TestAppContext) {
379 let cx = draw_anchored_test_view(cx, point(px(100.), px(500.)));
380
381 let menu_bounds = menu_bounds(cx);
382
383 assert_eq!(menu_bounds.origin, point(px(100.), px(300.)));
384 assert_eq!(menu_bounds.size, size(px(200.), px(300.)));
385 }
386}