1use std::{rc::Rc, time::Duration};
2
3use gpui::{
4 Animation, AnimationExt as _, AnyElement, App,ClickEvent, DefiniteLength, DismissEvent, Div,
5 EventEmitter, FocusHandle, InteractiveElement as _, IntoElement, KeyBinding, MouseButton,
6 ParentElement, Pixels, RenderOnce, Styled, Window, anchored, div, point,
7 prelude::FluentBuilder as _, px,
8};
9
10use crate::{
11 ActiveTheme, IconName, Placement, Sizable, StyledExt as _, WindowExt as _,
12 actions::Cancel,
13 button::{Button, ButtonVariants as _},
14 dialog::overlay_color,
15 h_flex,
16 scroll::ScrollableElement as _,
17 title_bar::TITLE_BAR_HEIGHT,
18 v_flex,
19};
20
21const CONTEXT: &str = "Sheet";
22pub(crate) fn init(cx: &mut App) {
23 cx.bind_keys([KeyBinding::new("escape", Cancel, Some(CONTEXT))])
24}
25
26#[derive(IntoElement)]
28pub struct Sheet {
29 pub(crate) focus_handle: FocusHandle,
30 pub(crate) placement: Placement,
31 pub(crate) size: DefiniteLength,
32 resizable: bool,
33 on_close: Rc<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
34 title: Option<AnyElement>,
35 footer: Option<AnyElement>,
36 content: Div,
37 margin_top: Pixels,
38 overlay: bool,
39 overlay_closable: bool,
40}
41
42impl Sheet {
43 pub fn new(_: &mut Window, cx: &mut App) -> Self {
45 Self {
46 focus_handle: cx.focus_handle(),
47 placement: Placement::Right,
48 size: DefiniteLength::Absolute(px(350.).into()),
49 resizable: true,
50 title: None,
51 footer: None,
52 content: v_flex().px_4().py_3(),
53 margin_top: TITLE_BAR_HEIGHT,
54 overlay: true,
55 overlay_closable: true,
56 on_close: Rc::new(|_, _, _| {}),
57 }
58 }
59
60 pub fn title(mut self, title: impl IntoElement) -> Self {
62 self.title = Some(title.into_any_element());
63 self
64 }
65
66 pub fn footer(mut self, footer: impl IntoElement) -> Self {
68 self.footer = Some(footer.into_any_element());
69 self
70 }
71
72 pub fn size(mut self, size: impl Into<DefiniteLength>) -> Self {
74 self.size = size.into();
75 self
76 }
77
78 pub fn margin_top(mut self, top: Pixels) -> Self {
82 self.margin_top = top;
83 self
84 }
85
86 pub fn resizable(mut self, resizable: bool) -> Self {
88 self.resizable = resizable;
89 self
90 }
91
92 pub fn overlay(mut self, overlay: bool) -> Self {
94 self.overlay = overlay;
95 self
96 }
97
98 pub fn overlay_closable(mut self, overlay_closable: bool) -> Self {
100 self.overlay_closable = overlay_closable;
101 self
102 }
103
104 pub fn on_close(
106 mut self,
107 on_close: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
108 ) -> Self {
109 self.on_close = Rc::new(on_close);
110 self
111 }
112}
113
114impl EventEmitter<DismissEvent> for Sheet {}
115impl ParentElement for Sheet {
116 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
117 self.content.extend(elements);
118 }
119}
120impl Styled for Sheet {
121 fn style(&mut self) -> &mut gpui::StyleRefinement {
122 self.content.style()
123 }
124}
125
126impl RenderOnce for Sheet {
127 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
128 let placement = self.placement;
129 let titlebar_height = self.margin_top;
130 let window_paddings = crate::window_border::window_paddings(window);
131 let size = window.viewport_size()
132 - gpui::size(
133 window_paddings.left + window_paddings.right,
134 window_paddings.top + window_paddings.bottom,
135 );
136 let on_close = self.on_close.clone();
137
138 anchored()
139 .position(point(
140 window_paddings.left,
141 window_paddings.top + titlebar_height,
142 ))
143 .snap_to_window()
144 .child(
145 div()
146 .occlude()
147 .w(size.width)
148 .h(size.height - titlebar_height)
149 .bg(overlay_color(self.overlay, cx))
150 .when(self.overlay, |this| {
151 this.on_any_mouse_down({
152 let on_close = self.on_close.clone();
153 move |event, window, cx| {
154 cx.stop_propagation();
155
156 if self.overlay_closable && event.button == MouseButton::Left {
157 on_close(&ClickEvent::default(), window, cx);
158 window.close_sheet(cx);
159 }
160 }
161 })
162 })
163 .child(
164 v_flex()
165 .id("sheet")
166 .tab_group()
167 .key_context(CONTEXT)
168 .track_focus(&self.focus_handle)
169 .on_action({
170 let on_close = self.on_close.clone();
171 move |_: &Cancel, window, cx| {
172 cx.propagate();
173
174 on_close(&ClickEvent::default(), window, cx);
175 window.close_sheet(cx);
176 }
177 })
178 .absolute()
179 .occlude()
180 .bg(cx.theme().background)
181 .border_color(cx.theme().border)
182 .shadow_xl()
183 .map(|this| {
184 if placement.is_horizontal() {
186 this.h_full().w(self.size)
187 } else {
188 this.w_full().h(self.size)
189 }
190 })
191 .map(|this| match self.placement {
192 Placement::Top => this.top_0().left_0().right_0().border_b_1(),
193 Placement::Right => this.top_0().right_0().bottom_0().border_l_1(),
194 Placement::Bottom => {
195 this.bottom_0().left_0().right_0().border_t_1()
196 }
197 Placement::Left => this.top_0().left_0().bottom_0().border_r_1(),
198 })
199 .child(
200 h_flex()
202 .justify_between()
203 .pl_4()
204 .pr_3()
205 .py_2()
206 .w_full()
207 .font_semibold()
208 .child(self.title.unwrap_or(div().into_any_element()))
209 .child(
210 Button::new("close")
211 .small()
212 .ghost()
213 .icon(IconName::Close)
214 .on_click(move |_, window, cx| {
215 on_close(&ClickEvent::default(), window, cx);
216 window.close_sheet(cx);
217 }),
218 ),
219 )
220 .child(
221 div().flex_1().overflow_scrollbar().child(self.content),
223 )
224 .when_some(self.footer, |this, footer| {
225 this.child(
227 h_flex()
228 .justify_between()
229 .px_4()
230 .py_3()
231 .w_full()
232 .child(footer),
233 )
234 })
235 .with_animation(
236 "slide",
237 Animation::new(Duration::from_secs_f64(0.15)),
238 move |this, delta| {
239 let y = px(-100.) + delta * px(100.);
240 this.map(|this| match placement {
241 Placement::Top => this.top(y),
242 Placement::Right => this.right(y),
243 Placement::Bottom => this.bottom(y),
244 Placement::Left => this.left(y),
245 })
246 },
247 ),
248 ),
249 )
250 }
251}