1use gpui::{
4 AnyElement, App, CursorStyle, Decorations, Edges, Hsla, InteractiveElement as _, IntoElement,
5 MouseButton, ParentElement, Pixels, Point, RenderOnce, ResizeEdge, Size, Styled as _, Tiling,
6 Window, div, point, prelude::FluentBuilder as _, px,
7};
8
9use crate::ActiveTheme;
10
11#[cfg(not(target_os = "linux"))]
12pub(crate) const SHADOW_SIZE: Pixels = px(0.0);
13#[cfg(target_os = "linux")]
14pub(crate) const SHADOW_SIZE: Pixels = px(20.0);
15const BORDER_SIZE: Pixels = px(1.0);
16const RESIZE_HIT_SIZE: Pixels = px(4.0);
18pub(crate) const BORDER_RADIUS: Pixels = px(0.0);
23
24pub fn window_border() -> WindowBorder {
26 WindowBorder::new()
27}
28
29#[derive(IntoElement)]
31pub struct WindowBorder {
32 shadow_size: Pixels,
33 resize_hit_size: Pixels,
34 children: Vec<AnyElement>,
35}
36
37impl Default for WindowBorder {
38 fn default() -> Self {
39 Self {
40 shadow_size: SHADOW_SIZE,
41 resize_hit_size: RESIZE_HIT_SIZE,
42 children: Vec::new(),
43 }
44 }
45}
46
47impl WindowBorder {
48 pub fn new() -> Self {
49 Self::default()
50 }
51
52 pub fn shadow_size(mut self, size: impl Into<Pixels>) -> Self {
56 self.shadow_size = size.into();
57 self
58 }
59
60 pub fn resize_hit_size(mut self, size: impl Into<Pixels>) -> Self {
64 self.resize_hit_size = size.into();
65 self
66 }
67}
68
69fn client_frame_insets(shadow_size: Pixels, tiling: &Tiling) -> Edges<Pixels> {
71 let mut insets = Edges::all(shadow_size);
72 if tiling.top {
73 insets.top = px(0.0);
74 }
75 if tiling.bottom {
76 insets.bottom = px(0.0);
77 }
78 if tiling.left {
79 insets.left = px(0.0);
80 }
81 if tiling.right {
82 insets.right = px(0.0);
83 }
84 insets
85}
86
87pub fn window_paddings(window: &Window) -> Edges<Pixels> {
89 let shadow_size = window.client_inset().unwrap_or(SHADOW_SIZE);
90 match window.window_decorations() {
91 Decorations::Server => Edges::all(px(0.0)),
92 Decorations::Client { tiling } => client_frame_insets(shadow_size, &tiling),
93 }
94}
95
96pub(crate) fn window_content_insets(window: &Window) -> Edges<Pixels> {
102 let shadow_size = window.client_inset().unwrap_or(SHADOW_SIZE);
103 match window.window_decorations() {
104 Decorations::Server => Edges::all(px(0.0)),
105 Decorations::Client { tiling } => {
106 let mut insets = client_frame_insets(shadow_size, &tiling);
107 if !tiling.top {
108 insets.top += BORDER_SIZE;
109 }
110 if !tiling.bottom {
111 insets.bottom += BORDER_SIZE;
112 }
113 if !tiling.left {
114 insets.left += BORDER_SIZE;
115 }
116 if !tiling.right {
117 insets.right += BORDER_SIZE;
118 }
119 insets
120 }
121 }
122}
123
124impl ParentElement for WindowBorder {
125 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
126 self.children.extend(elements);
127 }
128}
129
130impl RenderOnce for WindowBorder {
131 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
132 let decorations = window.window_decorations();
133 let mut children = self.children;
134 if !uses_client_border(decorations) {
135 return match children.len() {
136 1 => children.pop().unwrap(),
137 _ => div().size_full().children(children).into_any_element(),
138 };
139 }
140 let Decorations::Client { tiling } = decorations else {
141 unreachable!("client border requires client decorations");
142 };
143
144 let platform_inset = self.shadow_size;
149 let visual_shadow = if tiling.top && tiling.bottom && tiling.left && tiling.right {
150 px(0.0)
151 } else {
152 self.shadow_size
153 };
154 let resize_hit_size = self.resize_hit_size;
155 window.set_client_inset(platform_inset);
156 let window_size = window.window_bounds().get_bounds().size;
157 let is_window_active = window.is_window_active();
158 let border_color = if cx.theme().is_dark() {
159 Hsla {
160 h: 0.,
161 s: 0.,
162 l: 0.2,
163 a: 1.0,
164 }
165 } else {
166 Hsla {
167 h: 0.,
168 s: 0.,
169 l: 0.8,
170 a: 1.0,
171 }
172 };
173
174 div()
175 .id("window-backdrop")
176 .bg(gpui::transparent_black())
177 .flex()
178 .flex_col()
179 .overflow_hidden()
180 .when(!(tiling.top || tiling.right), |div| {
181 div.rounded_tr(BORDER_RADIUS)
182 })
183 .when(!(tiling.top || tiling.left), |div| {
184 div.rounded_tl(BORDER_RADIUS)
185 })
186 .when(!tiling.top, |div| div.pt(visual_shadow))
187 .when(!tiling.bottom, |div| div.pb(visual_shadow))
188 .when(!tiling.left, |div| div.pl(visual_shadow))
189 .when(!tiling.right, |div| div.pr(visual_shadow))
190 .on_mouse_down(MouseButton::Left, move |_, window, _| {
191 let Decorations::Client { tiling } = window.window_decorations() else {
192 return;
193 };
194 if !window.is_resizable() {
195 return;
196 }
197 if tiling.top && tiling.bottom && tiling.left && tiling.right {
198 return;
199 }
200 let size = window.window_bounds().get_bounds().size;
201 let pos = window.mouse_position();
202 let insets = client_frame_insets(platform_inset, &tiling);
203
204 match resize_edge(pos, size, insets, &tiling, resize_hit_size) {
205 Some(edge) => window.start_window_resize(edge),
206 None => {}
207 };
208 })
209 .size_full()
210 .child(
211 div()
212 .cursor(CursorStyle::default())
213 .flex_1()
214 .min_h_0()
215 .min_w_0()
216 .overflow_hidden()
217 .when(!(tiling.top || tiling.right), |div| {
218 div.rounded_tr(BORDER_RADIUS)
219 })
220 .when(!(tiling.top || tiling.left), |div| {
221 div.rounded_tl(BORDER_RADIUS)
222 })
223 .border_color(border_color)
224 .when(!tiling.top, |div| div.border_t(BORDER_SIZE))
225 .when(!tiling.bottom, |div| div.border_b(BORDER_SIZE))
226 .when(!tiling.left, |div| div.border_l(BORDER_SIZE))
227 .when(!tiling.right, |div| div.border_r(BORDER_SIZE))
228 .when(!tiling.is_tiled(), |div| {
229 let opacity = if is_window_active { 1.0 } else { 0.7 };
230 div.shadow(vec![
231 gpui::BoxShadow {
235 color: Hsla {
236 h: 0.,
237 s: 0.,
238 l: 0.,
239 a: 0.18 * opacity,
240 },
241 blur_radius: px(10.),
244 spread_radius: px(-1.),
245 offset: point(px(0.0), px(2.0)),
246 inset: false,
247 },
248 gpui::BoxShadow {
251 color: Hsla {
252 h: 0.,
253 s: 0.,
254 l: 0.,
255 a: 0.18 * opacity,
256 },
257 blur_radius: px(3.),
258 spread_radius: px(0.),
259 offset: point(px(0.0), px(1.0)),
260 inset: false,
261 },
262 ])
263 })
264 .on_mouse_move(|_e, _, cx| {
265 cx.stop_propagation();
266 })
267 .bg(gpui::transparent_black())
268 .children(children),
269 )
270 .when(window.is_resizable(), |parent| {
271 parent.child(div().absolute().size_full().children(resize_hit_zones(
272 window_size,
273 platform_inset,
274 resize_hit_size,
275 &tiling,
276 )))
277 })
278 .into_any_element()
279 }
280}
281
282fn uses_client_border(decorations: Decorations) -> bool {
283 matches!(decorations, Decorations::Client { .. })
284}
285
286fn cursor_style_for_resize_edge(edge: ResizeEdge) -> CursorStyle {
287 match edge {
288 ResizeEdge::Top | ResizeEdge::Bottom => CursorStyle::ResizeUpDown,
289 ResizeEdge::Left | ResizeEdge::Right => CursorStyle::ResizeLeftRight,
290 ResizeEdge::TopLeft | ResizeEdge::BottomRight => CursorStyle::ResizeUpLeftDownRight,
291 ResizeEdge::TopRight | ResizeEdge::BottomLeft => CursorStyle::ResizeUpRightDownLeft,
292 }
293}
294
295#[cfg(test)]
296mod tests {
297 use super::*;
298
299 #[test]
300 fn only_client_decorated_windows_use_the_client_border() {
301 assert!(!uses_client_border(Decorations::Server));
302 assert!(uses_client_border(Decorations::Client {
303 tiling: Tiling::default(),
304 }));
305 }
306}
307
308fn resize_hit_zones(
312 window_size: Size<Pixels>,
313 shadow_size: Pixels,
314 hit_size: Pixels,
315 tiling: &Tiling,
316) -> Vec<AnyElement> {
317 if tiling.top && tiling.bottom && tiling.left && tiling.right {
318 return Vec::new();
319 }
320
321 let insets = client_frame_insets(shadow_size, tiling);
322 let inner_left = insets.left;
323 let inner_right = window_size.width - insets.right;
324 let inner_top = insets.top;
325 let inner_bottom = window_size.height - insets.bottom;
326 let frame_origin = point(insets.left, insets.top);
328 let band = hit_size + hit_size;
329 let span_x = inner_right - inner_left + band;
330 let span_y = inner_bottom - inner_top + band;
331
332 let mut zones: Vec<AnyElement> = Vec::new();
333
334 let mut push_zone = |edge: ResizeEdge, origin: Point<Pixels>, zone_size: Size<Pixels>| {
335 let origin = origin - frame_origin;
336 zones.push(
337 div()
338 .absolute()
339 .left(origin.x)
340 .top(origin.y)
341 .w(zone_size.width)
342 .h(zone_size.height)
343 .cursor(cursor_style_for_resize_edge(edge))
344 .into_any_element(),
345 );
346 };
347
348 if !tiling.top {
349 push_zone(
350 ResizeEdge::Top,
351 point(inner_left - hit_size, inner_top - hit_size),
352 Size::new(span_x, band),
353 );
354 }
355 if !tiling.bottom {
356 push_zone(
357 ResizeEdge::Bottom,
358 point(inner_left - hit_size, inner_bottom - hit_size),
359 Size::new(span_x, band),
360 );
361 }
362 if !tiling.left {
363 push_zone(
364 ResizeEdge::Left,
365 point(inner_left - hit_size, inner_top - hit_size),
366 Size::new(band, span_y),
367 );
368 }
369 if !tiling.right {
370 push_zone(
371 ResizeEdge::Right,
372 point(inner_right - hit_size, inner_top - hit_size),
373 Size::new(band, span_y),
374 );
375 }
376
377 if !tiling.top && !tiling.left {
379 push_zone(
380 ResizeEdge::TopLeft,
381 point(inner_left - hit_size, inner_top - hit_size),
382 Size::new(band, band),
383 );
384 }
385 if !tiling.top && !tiling.right {
386 push_zone(
387 ResizeEdge::TopRight,
388 point(inner_right - hit_size, inner_top - hit_size),
389 Size::new(band, band),
390 );
391 }
392 if !tiling.bottom && !tiling.left {
393 push_zone(
394 ResizeEdge::BottomLeft,
395 point(inner_left - hit_size, inner_bottom - hit_size),
396 Size::new(band, band),
397 );
398 }
399 if !tiling.bottom && !tiling.right {
400 push_zone(
401 ResizeEdge::BottomRight,
402 point(inner_right - hit_size, inner_bottom - hit_size),
403 Size::new(band, band),
404 );
405 }
406
407 zones
408}
409
410fn resize_edge(
412 pos: Point<Pixels>,
413 size: Size<Pixels>,
414 insets: Edges<Pixels>,
415 tiling: &Tiling,
416 hit_size: Pixels,
417) -> Option<ResizeEdge> {
418 let inner_left = insets.left;
419 let inner_right = size.width - insets.right;
420 let inner_top = insets.top;
421 let inner_bottom = size.height - insets.bottom;
422
423 let on_left = pos.x >= inner_left - hit_size
425 && pos.x <= inner_left + hit_size
426 && pos.y >= inner_top - hit_size
427 && pos.y <= inner_bottom + hit_size;
428 let on_right = pos.x >= inner_right - hit_size
429 && pos.x <= inner_right + hit_size
430 && pos.y >= inner_top - hit_size
431 && pos.y <= inner_bottom + hit_size;
432 let on_top = pos.y >= inner_top - hit_size
433 && pos.y <= inner_top + hit_size
434 && pos.x >= inner_left - hit_size
435 && pos.x <= inner_right + hit_size;
436 let on_bottom = pos.y >= inner_bottom - hit_size
437 && pos.y <= inner_bottom + hit_size
438 && pos.x >= inner_left - hit_size
439 && pos.x <= inner_right + hit_size;
440
441 if !tiling.top && !tiling.left && on_top && on_left {
442 return Some(ResizeEdge::TopLeft);
443 }
444 if !tiling.top && !tiling.right && on_top && on_right {
445 return Some(ResizeEdge::TopRight);
446 }
447 if !tiling.bottom && !tiling.left && on_bottom && on_left {
448 return Some(ResizeEdge::BottomLeft);
449 }
450 if !tiling.bottom && !tiling.right && on_bottom && on_right {
451 return Some(ResizeEdge::BottomRight);
452 }
453 if !tiling.top && on_top {
454 return Some(ResizeEdge::Top);
455 }
456 if !tiling.bottom && on_bottom {
457 return Some(ResizeEdge::Bottom);
458 }
459 if !tiling.left && on_left {
460 return Some(ResizeEdge::Left);
461 }
462 if !tiling.right && on_right {
463 return Some(ResizeEdge::Right);
464 }
465 None
466}