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 {
13 child_layout_ids: SmallVec<[LayoutId; 4]>,
14}
15
16#[derive(derive_setters::Setters)]
21#[setters(generate = false)]
22pub struct Anchored {
23 children: SmallVec<[AnyElement; 2]>,
24 #[setters(generate)]
26 anchor: Anchor,
27 fit_mode: AnchoredFitMode,
28 #[setters(generate, rename = "position", strip_option)]
33 anchor_position: Option<Point<Pixels>>,
34 #[setters(generate)]
39 position_mode: AnchoredPositionMode,
40 #[setters(generate, strip_option)]
45 offset: Option<Point<Pixels>>,
46}
47
48pub fn anchored() -> Anchored {
106 Anchored {
107 children: SmallVec::new(),
108 anchor: Anchor::TopLeft,
109 fit_mode: AnchoredFitMode::SwitchAnchor,
110 anchor_position: None,
111 position_mode: AnchoredPositionMode::Window,
112 offset: None,
113 }
114}
115
116impl Anchored {
117 pub fn snap_to_window(mut self) -> Self {
120 self.fit_mode = AnchoredFitMode::SnapToWindow;
121 self
122 }
123
124 pub fn snap_to_window_with_margin(mut self, edges: impl Into<Edges<Pixels>>) -> Self {
128 self.fit_mode = AnchoredFitMode::SnapToWindowWithMargin(edges.into());
129 self
130 }
131}
132
133impl ParentElement for Anchored {
134 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
135 self.children.extend(elements)
136 }
137}
138
139impl Element for Anchored {
140 type RequestLayoutState = AnchoredState;
141 type PrepaintState = ();
142
143 fn id(&self) -> Option<crate::ElementId> {
144 None
145 }
146
147 fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
148 None
149 }
150
151 fn request_layout(
152 &mut self,
153 _id: Option<&GlobalElementId>,
154 _inspector_id: Option<&InspectorElementId>,
155 window: &mut Window,
156 cx: &mut App,
157 ) -> (crate::LayoutId, Self::RequestLayoutState) {
158 let child_layout_ids = self
159 .children
160 .iter_mut()
161 .map(|child| child.request_layout(window, cx))
162 .collect::<SmallVec<_>>();
163
164 let anchored_style = Style {
165 position: Position::Absolute,
166 display: Display::Flex,
167 ..Style::default()
168 };
169
170 let layout_id = window.request_layout(anchored_style, child_layout_ids.iter().copied(), cx);
171
172 (layout_id, AnchoredState { child_layout_ids })
173 }
174
175 fn prepaint(
176 &mut self,
177 _id: Option<&GlobalElementId>,
178 _inspector_id: Option<&InspectorElementId>,
179 bounds: Bounds<Pixels>,
180 request_layout: &mut Self::RequestLayoutState,
181 window: &mut Window,
182 cx: &mut App,
183 ) {
184 if request_layout.child_layout_ids.is_empty() {
185 return;
186 }
187
188 let children_bounds = request_layout
189 .child_layout_ids
190 .iter()
191 .map(|id| window.layout_bounds(*id))
192 .reduce(|acc, bounds| acc.union(&bounds))
193 .unwrap();
194
195 let (origin, mut desired) = self.position_mode.get_position_and_bounds(
196 self.anchor_position,
197 self.anchor,
198 children_bounds.size,
199 bounds,
200 self.offset,
201 );
202
203 let limits = Bounds {
204 origin: Point::default(),
205 size: window.viewport_size(),
206 };
207
208 if self.fit_mode == AnchoredFitMode::SwitchAnchor {
209 let mut anchor = self.anchor;
210
211 if desired.left() < limits.left() || desired.right() > limits.right() {
212 let switched = Bounds::from_anchor_and_size(
213 anchor.other_side_along(Axis::Horizontal),
214 origin,
215 children_bounds.size,
216 );
217 if !(switched.left() < limits.left() || switched.right() > limits.right()) {
218 anchor = anchor.other_side_along(Axis::Horizontal);
219 desired = switched
220 }
221 }
222
223 if desired.top() < limits.top() || desired.bottom() > limits.bottom() {
224 let switched = Bounds::from_anchor_and_size(
225 anchor.other_side_along(Axis::Vertical),
226 origin,
227 children_bounds.size,
228 );
229 if !(switched.top() < limits.top() || switched.bottom() > limits.bottom()) {
230 desired = switched;
231 }
232 }
233 }
234
235 let client_inset = window.client_inset.unwrap_or(px(0.));
236 let edges = match self.fit_mode {
237 AnchoredFitMode::SnapToWindowWithMargin(edges) => edges,
238 _ => Edges::default(),
239 }
240 .map(|edge| *edge + client_inset);
241
242 if desired.right() > limits.right() {
245 desired.origin.x -= desired.right() - limits.right() + edges.right;
246 }
247 if desired.left() < limits.left() {
248 desired.origin.x = limits.origin.x + edges.left;
249 }
250
251 if desired.bottom() > limits.bottom() {
254 desired.origin.y -= desired.bottom() - limits.bottom() + edges.bottom;
255 }
256 if desired.top() < limits.top() {
257 desired.origin.y = limits.origin.y + edges.top;
258 }
259
260 let offset = desired.origin - bounds.origin;
261 let offset = point(offset.x.round(), offset.y.round());
262
263 window.with_element_offset(offset, |window| {
264 for child in &mut self.children {
265 child.prepaint(window, cx);
266 }
267 })
268 }
269
270 fn paint(
271 &mut self,
272 _id: Option<&GlobalElementId>,
273 _inspector_id: Option<&InspectorElementId>,
274 _bounds: crate::Bounds<crate::Pixels>,
275 _request_layout: &mut Self::RequestLayoutState,
276 _prepaint: &mut Self::PrepaintState,
277 window: &mut Window,
278 cx: &mut App,
279 ) {
280 for child in &mut self.children {
281 child.paint(window, cx);
282 }
283 }
284}
285
286impl IntoElement for Anchored {
287 type Element = Self;
288
289 fn into_element(self) -> Self::Element {
290 self
291 }
292}
293
294#[derive(Copy, Clone, PartialEq)]
296pub enum AnchoredFitMode {
297 SnapToWindow,
299 SnapToWindowWithMargin(Edges<Pixels>),
301 SwitchAnchor,
304}
305
306#[derive(Copy, Clone, PartialEq)]
308pub enum AnchoredPositionMode {
309 Window,
311 Local,
313}
314
315impl AnchoredPositionMode {
316 fn get_position_and_bounds(
317 &self,
318 anchor_position: Option<Point<Pixels>>,
319 anchor: Anchor,
320 size: Size<Pixels>,
321 bounds: Bounds<Pixels>,
322 offset: Option<Point<Pixels>>,
323 ) -> (Point<Pixels>, Bounds<Pixels>) {
324 let offset = offset.unwrap_or_default();
325
326 match self {
327 AnchoredPositionMode::Window => {
328 let anchor_position = anchor_position.unwrap_or(bounds.origin) + offset;
329 let bounds = Bounds::from_anchor_and_size(anchor, anchor_position, size);
330 (anchor_position, bounds)
331 }
332 AnchoredPositionMode::Local => {
333 let anchor_position = bounds.origin + anchor_position.unwrap_or_default() + offset;
334 let bounds = Bounds::from_anchor_and_size(anchor, anchor_position, size);
335 (anchor_position, bounds)
336 }
337 }
338 }
339}
340
341#[cfg(test)]
342mod tests {
343 use crate::{
344 Anchor, Bounds, Context, Pixels, PlatformInput, Point, TestAppContext, Window, deferred,
345 div, point, prelude::*, px, size,
346 };
347
348 #[test]
350 fn anchor_switch_point_includes_offset_and_parent_origin() {
351 let layout_bounds = Bounds::new(point(px(500.), px(100.)), size(px(200.), px(100.)));
352 for (mode, expected) in [
353 (
354 super::AnchoredPositionMode::Window,
355 point(px(125.), px(25.)),
356 ),
357 (
358 super::AnchoredPositionMode::Local,
359 point(px(625.), px(125.)),
360 ),
361 ] {
362 let (origin, bounds) = mode.get_position_and_bounds(
363 Some(point(px(100.), px(20.))),
364 Anchor::TopLeft,
365 size(px(200.), px(300.)),
366 layout_bounds,
367 Some(point(px(25.), px(5.))),
368 );
369 assert_eq!(origin, expected);
370 assert_eq!(bounds.origin, expected);
371 }
372 }
373
374 struct LocalAnchoredTestView;
375
376 impl Render for LocalAnchoredTestView {
377 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
378 div().size_full().child(
379 div()
380 .absolute()
381 .left(px(500.))
382 .top(px(100.))
383 .child(deferred(
384 super::anchored()
385 .position_mode(super::AnchoredPositionMode::Local)
386 .position(point(px(100.), px(20.)))
387 .offset(point(px(25.), px(5.)))
388 .child(
389 div()
390 .debug_selector(|| "LOCAL_MENU".into())
391 .w(px(200.))
392 .h(px(300.)),
393 ),
394 )),
395 )
396 }
397 }
398
399 #[gpui::test]
402 fn test_anchored_local_switch_anchor(cx: &mut TestAppContext) {
403 let window = cx.open_window(size(px(800.), px(600.)), |_, _| LocalAnchoredTestView);
404 cx.run_until_parked();
405
406 let menu_bounds = window
407 .update(cx, |_, window, _| {
408 window
409 .rendered_frame
410 .debug_bounds
411 .get("LOCAL_MENU")
412 .copied()
413 })
414 .unwrap()
415 .expect("LOCAL_MENU debug bounds not found");
416
417 assert_eq!(menu_bounds.origin, point(px(425.), px(125.)));
418 assert_eq!(menu_bounds.size, size(px(200.), px(300.)));
419 }
420
421 struct AnchoredTestView {
422 position: Point<Pixels>,
423 }
424
425 impl Render for AnchoredTestView {
426 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
427 div().size_full().child(
428 div()
429 .id("scroll-container")
430 .overflow_y_scroll()
431 .size_full()
432 .child(div().h(px(2000.)).w_full())
433 .child(
434 deferred(
435 super::anchored()
436 .snap_to_window()
437 .position(self.position)
438 .child(
439 div()
440 .id("menu")
441 .debug_selector(|| "MENU".into())
442 .w(px(200.))
443 .h(px(300.)),
444 ),
445 )
446 .priority(1),
447 ),
448 )
449 }
450 }
451
452 #[gpui::test]
453 fn test_anchored_position_without_scroll(cx: &mut TestAppContext) {
454 let window = cx.open_window(size(px(800.), px(600.)), |_, _| AnchoredTestView {
455 position: point(px(100.), px(100.)),
456 });
457
458 cx.run_until_parked();
459
460 let menu_bounds = window
461 .update(cx, |_, window, _| {
462 window.rendered_frame.debug_bounds.get("MENU").copied()
463 })
464 .unwrap()
465 .expect("MENU debug bounds not found");
466
467 assert_eq!(menu_bounds.origin, point(px(100.), px(100.)));
468 assert_eq!(menu_bounds.size, size(px(200.), px(300.)));
469 }
470
471 #[gpui::test]
472 fn test_anchored_position_when_scrolled(cx: &mut TestAppContext) {
473 let window = cx.open_window(size(px(800.), px(600.)), |_, _| AnchoredTestView {
474 position: point(px(100.), px(100.)),
475 });
476
477 cx.run_until_parked();
478
479 window
480 .update(cx, |_, window, cx| {
481 let event = gpui::ScrollWheelEvent {
482 position: point(px(400.), px(300.)),
483 delta: gpui::ScrollDelta::Pixels(point(px(0.), px(-1000.))),
484 ..Default::default()
485 };
486 window.dispatch_event(PlatformInput::ScrollWheel(event), cx);
487 })
488 .unwrap();
489
490 cx.run_until_parked();
491
492 let menu_bounds = window
493 .update(cx, |_, window, _| {
494 window.rendered_frame.debug_bounds.get("MENU").copied()
495 })
496 .unwrap()
497 .expect("MENU debug bounds not found");
498
499 assert_eq!(menu_bounds.origin, point(px(100.), px(100.)));
500 assert_eq!(menu_bounds.size, size(px(200.), px(300.)));
501 }
502
503 #[gpui::test]
504 fn test_anchored_snaps_to_window(cx: &mut TestAppContext) {
505 let window = cx.open_window(size(px(800.), px(600.)), |_, _| AnchoredTestView {
506 position: point(px(100.), px(500.)),
507 });
508
509 cx.run_until_parked();
510
511 let menu_bounds = window
512 .update(cx, |_, window, _| {
513 window.rendered_frame.debug_bounds.get("MENU").copied()
514 })
515 .unwrap()
516 .expect("MENU debug bounds not found");
517
518 assert_eq!(menu_bounds.origin, point(px(100.), px(300.)));
519 assert_eq!(menu_bounds.size, size(px(200.), px(300.)));
520 }
521}