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