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