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