1use gpui::{
2 AnyElement, App, Bounds, Context, ElementId, GlobalElementId, InspectorElementId, IntoElement,
3 LayoutId, Pixels, Render, Window, div, prelude::*, px,
4};
5use liora_core::push_passive_portal;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
8pub enum AffixPosition {
9 #[default]
10 Top,
11 Bottom,
12}
13
14pub struct Affix {
15 offset: Pixels,
16 position: AffixPosition,
17 is_fixed: bool,
18 last_bounds: Option<Bounds<Pixels>>,
19 on_change: Option<Box<dyn Fn(bool, &mut Window, &mut App) + 'static>>,
20 content: Arc<dyn Fn(&mut Window, &mut Context<Affix>) -> AnyElement + 'static>,
21}
22
23use std::sync::Arc;
24
25impl Affix {
26 pub fn new() -> Self {
27 Self {
28 offset: px(0.0),
29 position: AffixPosition::Top,
30 is_fixed: false,
31 last_bounds: None,
32 on_change: None,
33 content: Arc::new(|_, _| div().into_any_element()),
34 }
35 }
36
37 pub fn offset(mut self, offset: impl Into<Pixels>) -> Self {
38 self.offset = offset.into();
39 self
40 }
41
42 pub fn offset_md(self) -> Self {
43 self.offset(px(20.0))
44 }
45
46 pub fn offset_lg(self) -> Self {
47 self.offset(px(80.0))
48 }
49
50 pub fn position(mut self, pos: AffixPosition) -> Self {
51 self.position = pos;
52 self
53 }
54
55 pub fn on_change(mut self, f: impl Fn(bool, &mut Window, &mut App) + 'static) -> Self {
56 self.on_change = Some(Box::new(f));
57 self
58 }
59
60 pub fn content<F>(mut self, f: F) -> Self
61 where
62 F: Fn(&mut Window, &mut Context<Affix>) -> AnyElement + 'static,
63 {
64 self.content = Arc::new(f);
65 self
66 }
67}
68
69impl Render for Affix {
70 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
71 let is_fixed = self.is_fixed;
72 let offset = self.offset;
73 let content_fn = self.content.clone();
74 let affix_handle = cx.entity().clone();
75 let last_bounds = self.last_bounds;
76
77 if is_fixed {
78 if let Some(bounds) = last_bounds {
79 let fixed_top = match self.position {
80 AffixPosition::Top => offset,
81 AffixPosition::Bottom => {
82 _window.viewport_size().height - offset - bounds.size.height
83 }
84 };
85 let fixed_left = bounds.left();
86 let fixed_width = bounds.size.width;
87 let fixed_content = content_fn(_window, cx);
88
89 push_passive_portal(
90 move |_, _| {
91 div()
92 .absolute()
93 .top(fixed_top)
94 .left(fixed_left)
95 .w(fixed_width)
96 .child(fixed_content)
97 .into_any_element()
98 },
99 cx,
100 );
101 }
102 }
103
104 let flow_content = if is_fixed {
105 match last_bounds {
106 Some(bounds) => div()
107 .w(bounds.size.width)
108 .h(bounds.size.height)
109 .into_any_element(),
110 None => div().h(px(40.0)).into_any_element(),
111 }
112 } else {
113 content_fn(_window, cx)
114 };
115
116 div().relative().child(BoundsTracker {
117 child: flow_content,
118 on_bounds_change: Box::new(move |bounds, window, cx| {
119 let (offset, position, current_fixed) =
120 affix_handle.update(cx, |this, _| (this.offset, this.position, this.is_fixed));
121
122 let should_be_fixed = match position {
123 AffixPosition::Top => bounds.top() <= offset,
124 AffixPosition::Bottom => {
125 let viewport_h = window.viewport_size().height;
126 bounds.bottom() >= viewport_h - offset
127 }
128 };
129
130 affix_handle.update(cx, |this, _| {
131 this.last_bounds = Some(bounds);
132 });
133
134 if should_be_fixed != current_fixed {
135 affix_handle.update(cx, |this, cx| {
136 this.is_fixed = should_be_fixed;
137 if let Some(ref on_change) = this.on_change {
138 (on_change)(should_be_fixed, window, cx);
139 }
140 cx.notify();
141 });
142 }
143 }),
144 })
145 }
146}
147
148struct BoundsTracker {
149 child: AnyElement,
150 on_bounds_change: Box<dyn Fn(Bounds<Pixels>, &mut Window, &mut App)>,
151}
152
153impl IntoElement for BoundsTracker {
154 type Element = Self;
155 fn into_element(self) -> Self::Element {
156 self
157 }
158}
159
160impl gpui::Element for BoundsTracker {
161 type RequestLayoutState = ();
162 type PrepaintState = ();
163
164 fn id(&self) -> Option<ElementId> {
165 None
166 }
167 fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
168 None
169 }
170
171 fn request_layout(
172 &mut self,
173 _id: Option<&GlobalElementId>,
174 _id2: Option<&InspectorElementId>,
175 window: &mut Window,
176 cx: &mut App,
177 ) -> (LayoutId, ()) {
178 (self.child.request_layout(window, cx), ())
179 }
180
181 fn prepaint(
182 &mut self,
183 _id: Option<&GlobalElementId>,
184 _id2: Option<&InspectorElementId>,
185 bounds: Bounds<Pixels>,
186 _rl: &mut (),
187 window: &mut Window,
188 cx: &mut App,
189 ) -> () {
190 self.child.prepaint_at(bounds.origin, window, cx);
191 }
192
193 fn paint(
194 &mut self,
195 _id: Option<&GlobalElementId>,
196 _id2: Option<&InspectorElementId>,
197 bounds: Bounds<Pixels>,
198 _rl: &mut (),
199 _ps: &mut (),
200 window: &mut Window,
201 cx: &mut App,
202 ) {
203 (self.on_bounds_change)(bounds, window, cx);
204 self.child.paint(window, cx);
205 }
206}