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