Skip to main content

liora_components/
affix.rs

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}