1use std::{rc::Rc, time::Duration};
2
3use rgpui::{
4 Animation, AnimationExt as _, AnyElement, App, ClickEvent, DefiniteLength, DismissEvent, Edges,
5 EventEmitter, FocusHandle, InteractiveElement as _, IntoElement, KeyBinding, MouseButton,
6 ParentElement, Pixels, RenderOnce, StyleRefinement, Styled, Window, WindowControlArea,
7 anchored, div, point, prelude::FluentBuilder as _, px,
8};
9use schemars::JsonSchema;
10use serde::{Deserialize, Serialize};
11
12use crate::{
13 ActiveTheme, FocusTrapElement as _, IconName, Placement, Sizable, StyledExt as _,
14 WindowExt as _,
15 actions::Cancel,
16 button::{Button, ButtonVariants as _},
17 dialog::overlay_color,
18 h_flex,
19 scroll::ScrollableElement as _,
20 title_bar::TITLE_BAR_HEIGHT,
21 v_flex,
22};
23
24const CONTEXT: &str = "Sheet";
25pub(crate) fn init(cx: &mut App) {
26 cx.bind_keys([KeyBinding::new("escape", Cancel, Some(CONTEXT))])
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
31pub struct SheetSettings {
32 pub margin_top: Pixels,
34}
35
36impl Default for SheetSettings {
37 fn default() -> Self {
38 Self {
39 margin_top: TITLE_BAR_HEIGHT,
40 }
41 }
42}
43
44#[derive(IntoElement)]
46pub struct Sheet {
47 pub(crate) focus_handle: FocusHandle,
48 pub(crate) placement: Placement,
49 pub(crate) size: DefiniteLength,
50 resizable: bool,
51 on_close: Rc<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
52 title: Option<AnyElement>,
53 footer: Option<AnyElement>,
54 style: StyleRefinement,
55 children: Vec<AnyElement>,
56 overlay: bool,
57 overlay_closable: bool,
58}
59
60impl Sheet {
61 pub fn new(_: &mut Window, cx: &mut App) -> Self {
63 Self {
64 focus_handle: cx.focus_handle(),
65 placement: Placement::Right,
66 size: DefiniteLength::Absolute(px(350.).into()),
67 resizable: true,
68 title: None,
69 footer: None,
70 style: StyleRefinement::default(),
71 children: Vec::new(),
72 overlay: true,
73 overlay_closable: true,
74 on_close: Rc::new(|_, _, _| {}),
75 }
76 }
77
78 pub fn title(mut self, title: impl IntoElement) -> Self {
80 self.title = Some(title.into_any_element());
81 self
82 }
83
84 pub fn footer(mut self, footer: impl IntoElement) -> Self {
86 self.footer = Some(footer.into_any_element());
87 self
88 }
89
90 pub fn size(mut self, size: impl Into<DefiniteLength>) -> Self {
92 self.size = size.into();
93 self
94 }
95
96 pub fn resizable(mut self, resizable: bool) -> Self {
98 self.resizable = resizable;
99 self
100 }
101
102 pub fn overlay(mut self, overlay: bool) -> Self {
104 self.overlay = overlay;
105 self
106 }
107
108 pub fn overlay_closable(mut self, overlay_closable: bool) -> Self {
110 self.overlay_closable = overlay_closable;
111 self
112 }
113
114 pub fn on_close(
116 mut self,
117 on_close: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
118 ) -> Self {
119 self.on_close = Rc::new(on_close);
120 self
121 }
122}
123
124impl EventEmitter<DismissEvent> for Sheet {}
125impl ParentElement for Sheet {
126 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
127 self.children.extend(elements);
128 }
129}
130impl Styled for Sheet {
131 fn style(&mut self) -> &mut rgpui::StyleRefinement {
132 &mut self.style
133 }
134}
135
136impl RenderOnce for Sheet {
137 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
138 let placement = self.placement;
139 let window_paddings = crate::window_border::window_paddings(window);
140 let size = window.viewport_size()
141 - rgpui::size(
142 window_paddings.left + window_paddings.right,
143 window_paddings.top + window_paddings.bottom,
144 );
145 let top = cx.theme().sheet.margin_top;
146 let on_close = self.on_close.clone();
147
148 let base_size = window.text_style().font_size;
149 let rem_size = window.rem_size();
150 let mut paddings = Edges::all(px(16.));
151 if let Some(pl) = self.style.padding.left {
152 paddings.left = pl.to_pixels(base_size, rem_size);
153 }
154 if let Some(pr) = self.style.padding.right {
155 paddings.right = pr.to_pixels(base_size, rem_size);
156 }
157 if let Some(pt) = self.style.padding.top {
158 paddings.top = pt.to_pixels(base_size, rem_size);
159 }
160 if let Some(pb) = self.style.padding.bottom {
161 paddings.bottom = pb.to_pixels(base_size, rem_size);
162 }
163
164 anchored()
165 .position(point(window_paddings.left, window_paddings.top))
166 .snap_to_window()
167 .child(
168 div()
169 .occlude()
170 .w(size.width)
171 .h(size.height)
172 .bg(overlay_color(self.overlay, cx))
173 .when(self.overlay, |this| {
174 this.window_control_area(WindowControlArea::Drag)
175 .on_any_mouse_down({
176 let on_close = self.on_close.clone();
177 move |event, window, cx| {
178 if event.position.y < top {
179 return;
180 }
181
182 cx.stop_propagation();
183 if self.overlay_closable && event.button == MouseButton::Left {
184 window.close_sheet(cx);
185 on_close(&ClickEvent::default(), window, cx);
186 }
187 }
188 })
189 })
190 .child(
191 v_flex()
192 .id("sheet")
193 .key_context(CONTEXT)
194 .track_focus(&self.focus_handle)
195 .focus_trap("sheet", &self.focus_handle)
196 .on_action({
197 let on_close = self.on_close.clone();
198 move |_: &Cancel, window, cx| {
199 cx.propagate();
200
201 window.close_sheet(cx);
202 on_close(&ClickEvent::default(), window, cx);
203 }
204 })
205 .absolute()
206 .occlude()
207 .bg(cx.theme().background)
208 .border_color(cx.theme().border)
209 .shadow_xl()
210 .refine_style(&self.style)
211 .map(|this| {
212 if placement.is_horizontal() {
214 this.w(self.size)
215 } else {
216 this.h(self.size)
217 }
218 })
219 .map(|this| match self.placement {
220 Placement::Top => this.top(top).left_0().right_0().border_b_1(),
221 Placement::Right => this.top(top).right_0().bottom_0().border_l_1(),
222 Placement::Bottom => {
223 this.bottom_0().left_0().right_0().border_t_1()
224 }
225 Placement::Left => this.top(top).left_0().bottom_0().border_r_1(),
226 })
227 .child(
228 h_flex()
230 .justify_between()
231 .pl_4()
232 .pr_3()
233 .py_2()
234 .w_full()
235 .font_semibold()
236 .child(self.title.unwrap_or(div().into_any_element()))
237 .child(
238 Button::new("close")
239 .small()
240 .ghost()
241 .icon(IconName::Close)
242 .on_click(move |_, window, cx| {
243 window.close_sheet(cx);
244 on_close(&ClickEvent::default(), window, cx);
245 }),
246 ),
247 )
248 .child(
249 div().flex_1().overflow_hidden().child(
250 v_flex()
252 .size_full()
253 .overflow_y_scrollbar()
254 .pl(paddings.left)
255 .pr(paddings.right)
256 .children(self.children),
257 ),
258 )
259 .when_some(self.footer, |this, footer| {
260 this.child(
262 h_flex()
263 .justify_between()
264 .px_4()
265 .py_3()
266 .w_full()
267 .child(footer),
268 )
269 })
270 .on_any_mouse_down({
271 |_, _, cx| {
272 cx.stop_propagation();
273 }
274 })
275 .with_animation(
276 "slide",
277 Animation::new(Duration::from_secs_f64(0.15)),
278 move |this, delta| {
279 let y = px(-100.) + delta * px(100.);
280 this.map(|this| match placement {
281 Placement::Top => this.top(top + y),
282 Placement::Right => this.right(y),
283 Placement::Bottom => this.bottom(y),
284 Placement::Left => this.left(y),
285 })
286 },
287 ),
288 ),
289 )
290 }
291}