Skip to main content

liora_components/
backtop.rs

1use crate::gpui_compat::element_id;
2use crate::motion::pop_in;
3use gpui::{
4    AnyElement, App, Bounds, Context, ElementId, Entity, GlobalElementId, InspectorElementId,
5    IntoElement, LayoutId, Pixels, Render, ScrollHandle, Window, div, point, prelude::*, px,
6};
7use liora_core::Config;
8use liora_icons::Icon;
9use liora_icons_lucide::IconName;
10
11pub struct Backtop {
12    id: gpui::SharedString,
13    scroll_handle: ScrollHandle,
14    visibility_height: Pixels,
15    right: Pixels,
16    bottom: Pixels,
17    is_visible: bool,
18    content: Option<Box<dyn Fn(&mut Window, &mut Context<Backtop>) -> AnyElement + 'static>>,
19}
20
21impl Backtop {
22    pub fn new(scroll_handle: ScrollHandle) -> Self {
23        Self {
24            id: liora_core::unique_id("backtop"),
25            scroll_handle,
26            visibility_height: px(200.0),
27            right: px(40.0),
28            bottom: px(40.0),
29            is_visible: false,
30            content: None,
31        }
32    }
33
34    pub fn id(mut self, id: impl Into<gpui::SharedString>) -> Self {
35        self.id = id.into();
36        self
37    }
38
39    pub fn visibility_height(mut self, h: impl Into<Pixels>) -> Self {
40        self.visibility_height = h.into();
41        self
42    }
43
44    pub fn visibility_height_sm(self) -> Self {
45        self.visibility_height(px(100.0))
46    }
47
48    pub fn right(mut self, r: impl Into<Pixels>) -> Self {
49        self.right = r.into();
50        self
51    }
52
53    pub fn right_lg(self) -> Self {
54        self.right(px(100.0))
55    }
56
57    pub fn bottom(mut self, b: impl Into<Pixels>) -> Self {
58        self.bottom = b.into();
59        self
60    }
61
62    pub fn content<F>(mut self, f: F) -> Self
63    where
64        F: Fn(&mut Window, &mut Context<Backtop>) -> AnyElement + 'static,
65    {
66        self.content = Some(Box::new(f));
67        self
68    }
69}
70
71impl Render for Backtop {
72    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
73        let theme = cx.global::<Config>().theme.clone();
74        let is_visible = self.is_visible;
75
76        let scroll_handle = self.scroll_handle.clone();
77
78        div()
79            .absolute()
80            .top_0()
81            .left_0()
82            .size_full()
83            .child(BacktopVisibilityTracker {
84                backtop: cx.entity().clone(),
85                scroll_handle: self.scroll_handle.clone(),
86                visibility_height: self.visibility_height,
87            })
88            .when(is_visible, |s| {
89                s.child(pop_in(
90                    element_id(format!("{}-btn-motion", self.id)),
91                    div()
92                        .id(element_id(format!("{}-btn", self.id)))
93                        .absolute()
94                        .bottom(self.bottom)
95                        .right(self.right)
96                        .cursor_pointer()
97                        .flex()
98                        .items_center()
99                        .justify_center()
100                        .w(px(40.0))
101                        .h(px(40.0))
102                        .rounded_full()
103                        .bg(theme.neutral.card)
104                        .border_1()
105                        .border_color(theme.neutral.border)
106                        .shadow_lg()
107                        .hover(|s| s.cursor_pointer().bg(theme.neutral.hover))
108                        .on_click(move |_, _, _| {
109                            scroll_handle.set_offset(point(px(0.0), px(0.0)));
110                        })
111                        .child(match &self.content {
112                            Some(content_fn) => (content_fn)(_window, cx),
113                            None => Icon::new(IconName::ChevronUp)
114                                .size(px(20.0))
115                                .color(theme.primary.base)
116                                .into_any_element(),
117                        }),
118                ))
119            })
120    }
121}
122
123struct BacktopVisibilityTracker {
124    backtop: Entity<Backtop>,
125    scroll_handle: ScrollHandle,
126    visibility_height: Pixels,
127}
128
129impl IntoElement for BacktopVisibilityTracker {
130    type Element = Self;
131    fn into_element(self) -> Self::Element {
132        self
133    }
134}
135
136impl gpui::Element for BacktopVisibilityTracker {
137    type RequestLayoutState = ();
138    type PrepaintState = ();
139
140    fn id(&self) -> Option<ElementId> {
141        None
142    }
143
144    fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
145        None
146    }
147
148    fn request_layout(
149        &mut self,
150        _id: Option<&GlobalElementId>,
151        _id2: Option<&InspectorElementId>,
152        window: &mut Window,
153        cx: &mut App,
154    ) -> (LayoutId, Self::RequestLayoutState) {
155        let mut style = gpui::Style::default();
156        style.size.width = px(0.0).into();
157        style.size.height = px(0.0).into();
158        (window.request_layout(style, [], cx), ())
159    }
160
161    fn prepaint(
162        &mut self,
163        _id: Option<&GlobalElementId>,
164        _id2: Option<&InspectorElementId>,
165        _bounds: Bounds<Pixels>,
166        _request_layout: &mut Self::RequestLayoutState,
167        _window: &mut Window,
168        _cx: &mut App,
169    ) -> Self::PrepaintState {
170    }
171
172    fn paint(
173        &mut self,
174        _id: Option<&GlobalElementId>,
175        _id2: Option<&InspectorElementId>,
176        _bounds: Bounds<Pixels>,
177        _request_layout: &mut Self::RequestLayoutState,
178        _prepaint: &mut Self::PrepaintState,
179        _window: &mut Window,
180        cx: &mut App,
181    ) {
182        let visible = -self.scroll_handle.offset().y >= self.visibility_height;
183        self.backtop.update(cx, |this, cx| {
184            if this.is_visible != visible {
185                this.is_visible = visible;
186                cx.notify();
187            }
188        });
189    }
190}