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