liora_components/
backtop.rs1use 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}