1use gpui::{
4 AnyElement, App, Corners, CursorStyle, Decorations, Edges, Hsla, InteractiveElement as _,
5 IntoElement, MouseButton, ParentElement, Pixels, Point, RenderOnce, ResizeEdge, Size,
6 Styled as _, Tiling, 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);
18
19pub fn window_corner_radii(window: &Window, cx: &App) -> Corners<Pixels> {
29 corner_radii(cx.theme().window_radius, window.window_decorations())
30}
31
32fn corner_radii(radius: Pixels, decorations: Decorations) -> Corners<Pixels> {
33 let Decorations::Client { tiling } = decorations else {
34 return Corners::default();
35 };
36 let corner = |rounded: bool| if rounded { radius } else { px(0.0) };
37 Corners {
38 top_left: corner(!tiling.top && !tiling.left),
39 top_right: corner(!tiling.top && !tiling.right),
40 bottom_left: corner(!tiling.bottom && !tiling.left),
41 bottom_right: corner(!tiling.bottom && !tiling.right),
42 }
43}
44
45pub fn window_border() -> WindowBorder {
47 WindowBorder::new()
48}
49
50#[derive(IntoElement)]
52pub struct WindowBorder {
53 shadow_size: Pixels,
54 resize_hit_size: Pixels,
55 children: Vec<AnyElement>,
56}
57
58impl Default for WindowBorder {
59 fn default() -> Self {
60 Self {
61 shadow_size: SHADOW_SIZE,
62 resize_hit_size: RESIZE_HIT_SIZE,
63 children: Vec::new(),
64 }
65 }
66}
67
68impl WindowBorder {
69 pub fn new() -> Self {
70 Self::default()
71 }
72
73 pub fn shadow_size(mut self, size: impl Into<Pixels>) -> Self {
77 self.shadow_size = size.into();
78 self
79 }
80
81 pub fn resize_hit_size(mut self, size: impl Into<Pixels>) -> Self {
85 self.resize_hit_size = size.into();
86 self
87 }
88}
89
90fn client_frame_insets(shadow_size: Pixels, tiling: &Tiling) -> Edges<Pixels> {
92 let mut insets = Edges::all(shadow_size);
93 if tiling.top {
94 insets.top = px(0.0);
95 }
96 if tiling.bottom {
97 insets.bottom = px(0.0);
98 }
99 if tiling.left {
100 insets.left = px(0.0);
101 }
102 if tiling.right {
103 insets.right = px(0.0);
104 }
105 insets
106}
107
108pub fn window_paddings(window: &Window) -> Edges<Pixels> {
110 let shadow_size = window.client_inset().unwrap_or(SHADOW_SIZE);
111 match window.window_decorations() {
112 Decorations::Server => Edges::all(px(0.0)),
113 Decorations::Client { tiling } => client_frame_insets(shadow_size, &tiling),
114 }
115}
116
117pub(crate) fn window_content_insets(window: &Window) -> Edges<Pixels> {
123 let shadow_size = window.client_inset().unwrap_or(SHADOW_SIZE);
124 match window.window_decorations() {
125 Decorations::Server => Edges::all(px(0.0)),
126 Decorations::Client { tiling } => {
127 let mut insets = client_frame_insets(shadow_size, &tiling);
128 if !tiling.top {
129 insets.top += BORDER_SIZE;
130 }
131 if !tiling.bottom {
132 insets.bottom += BORDER_SIZE;
133 }
134 if !tiling.left {
135 insets.left += BORDER_SIZE;
136 }
137 if !tiling.right {
138 insets.right += BORDER_SIZE;
139 }
140 insets
141 }
142 }
143}
144
145pub(crate) fn window_safe_insets(window: &Window) -> Edges<Pixels> {
149 let viewport = window.viewport_size();
150 let visible = window.fully_visible_bounds();
151 Edges {
152 top: visible.top(),
153 right: viewport.width - visible.right(),
154 bottom: viewport.height - visible.bottom(),
155 left: visible.left(),
156 }
157}
158
159impl ParentElement for WindowBorder {
160 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
161 self.children.extend(elements);
162 }
163}
164
165impl RenderOnce for WindowBorder {
166 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
167 let decorations = window.window_decorations();
168 let children = self.children;
169 if !uses_client_border(decorations) {
170 return div()
175 .id("window-backdrop")
176 .size_full()
177 .children(children)
178 .into_any_element();
179 }
180 let Decorations::Client { tiling } = decorations else {
181 unreachable!("client border requires client decorations");
182 };
183
184 let platform_inset = self.shadow_size;
189 let visual_shadow = if tiling.top && tiling.bottom && tiling.left && tiling.right {
190 px(0.0)
191 } else {
192 self.shadow_size
193 };
194 let resize_hit_size = self.resize_hit_size;
195 window.set_client_inset(platform_inset);
196 let radii = window_corner_radii(window, cx);
197 window.set_client_corner_radii(radii);
199 let window_size = window.window_bounds().get_bounds().size;
200 let is_window_active = window.is_window_active();
201 let border_color = cx.theme().window_border;
202
203 div()
204 .id("window-backdrop")
205 .bg(gpui::transparent_black())
206 .flex()
207 .flex_col()
208 .overflow_hidden()
209 .rounded_tl(radii.top_left)
210 .rounded_tr(radii.top_right)
211 .rounded_bl(radii.bottom_left)
212 .rounded_br(radii.bottom_right)
213 .when(!tiling.top, |div| div.pt(visual_shadow))
214 .when(!tiling.bottom, |div| div.pb(visual_shadow))
215 .when(!tiling.left, |div| div.pl(visual_shadow))
216 .when(!tiling.right, |div| div.pr(visual_shadow))
217 .on_mouse_down(MouseButton::Left, move |_, window, _| {
218 let Decorations::Client { tiling } = window.window_decorations() else {
219 return;
220 };
221 if !window.is_resizable() {
222 return;
223 }
224 if tiling.top && tiling.bottom && tiling.left && tiling.right {
225 return;
226 }
227 let size = window.window_bounds().get_bounds().size;
228 let pos = window.mouse_position();
229 let insets = client_frame_insets(platform_inset, &tiling);
230
231 match resize_edge(pos, size, insets, &tiling, resize_hit_size) {
232 Some(edge) => window.start_window_resize(edge),
233 None => {}
234 };
235 })
236 .size_full()
237 .child(
238 div()
239 .cursor(CursorStyle::default())
240 .flex_1()
241 .min_h_0()
242 .min_w_0()
243 .overflow_hidden()
244 .rounded_tl(radii.top_left)
245 .rounded_tr(radii.top_right)
246 .rounded_bl(radii.bottom_left)
247 .rounded_br(radii.bottom_right)
248 .border_color(border_color)
249 .when(!tiling.top, |div| div.border_t(BORDER_SIZE))
250 .when(!tiling.bottom, |div| div.border_b(BORDER_SIZE))
251 .when(!tiling.left, |div| div.border_l(BORDER_SIZE))
252 .when(!tiling.right, |div| div.border_r(BORDER_SIZE))
253 .when(!tiling.is_tiled(), |div| {
254 let opacity = if is_window_active { 1.0 } else { 0.7 };
255 div.shadow(vec![
256 gpui::BoxShadow {
260 color: Hsla {
261 h: 0.,
262 s: 0.,
263 l: 0.,
264 a: 0.18 * opacity,
265 },
266 blur_radius: px(10.),
269 spread_radius: px(-1.),
270 offset: point(px(0.0), px(2.0)),
271 inset: false,
272 },
273 gpui::BoxShadow {
276 color: Hsla {
277 h: 0.,
278 s: 0.,
279 l: 0.,
280 a: 0.18 * opacity,
281 },
282 blur_radius: px(3.),
283 spread_radius: px(0.),
284 offset: point(px(0.0), px(1.0)),
285 inset: false,
286 },
287 ])
288 })
289 .on_mouse_move(|_e, _, cx| {
290 cx.stop_propagation();
291 })
292 .bg(gpui::transparent_black())
293 .children(children),
294 )
295 .when(window.is_resizable(), |parent| {
296 parent.child(div().absolute().size_full().children(resize_hit_zones(
297 window_size,
298 platform_inset,
299 resize_hit_size,
300 &tiling,
301 )))
302 })
303 .into_any_element()
304 }
305}
306
307fn uses_client_border(decorations: Decorations) -> bool {
308 matches!(decorations, Decorations::Client { .. })
309}
310
311fn cursor_style_for_resize_edge(edge: ResizeEdge) -> CursorStyle {
312 match edge {
313 ResizeEdge::Top | ResizeEdge::Bottom => CursorStyle::ResizeUpDown,
314 ResizeEdge::Left | ResizeEdge::Right => CursorStyle::ResizeLeftRight,
315 ResizeEdge::TopLeft | ResizeEdge::BottomRight => CursorStyle::ResizeUpLeftDownRight,
316 ResizeEdge::TopRight | ResizeEdge::BottomLeft => CursorStyle::ResizeUpRightDownLeft,
317 }
318}
319
320#[cfg(test)]
321mod tests {
322 #[test]
323 fn corners_round_only_next_to_untiled_edges() {
324 use super::corner_radii;
325 use gpui::{Corners, Decorations, Tiling, px};
326
327 let radius = px(10.0);
328 assert_eq!(
329 corner_radii(
330 radius,
331 Decorations::Client {
332 tiling: Tiling::default()
333 }
334 ),
335 Corners::all(radius)
336 );
337 let top_tiled = Tiling {
338 top: true,
339 ..Tiling::default()
340 };
341 assert_eq!(
342 corner_radii(radius, Decorations::Client { tiling: top_tiled }),
343 Corners {
344 top_left: px(0.0),
345 top_right: px(0.0),
346 bottom_left: radius,
347 bottom_right: radius,
348 }
349 );
350 assert_eq!(
351 corner_radii(radius, Decorations::Server),
352 Corners::default()
353 );
354 }
355
356 use super::*;
357
358 #[test]
359 fn only_client_decorated_windows_use_the_client_border() {
360 assert!(!uses_client_border(Decorations::Server));
361 assert!(uses_client_border(Decorations::Client {
362 tiling: Tiling::default(),
363 }));
364 }
365}
366
367fn resize_hit_zones(
371 window_size: Size<Pixels>,
372 shadow_size: Pixels,
373 hit_size: Pixels,
374 tiling: &Tiling,
375) -> Vec<AnyElement> {
376 if tiling.top && tiling.bottom && tiling.left && tiling.right {
377 return Vec::new();
378 }
379
380 let insets = client_frame_insets(shadow_size, tiling);
381 let inner_left = insets.left;
382 let inner_right = window_size.width - insets.right;
383 let inner_top = insets.top;
384 let inner_bottom = window_size.height - insets.bottom;
385 let frame_origin = point(insets.left, insets.top);
387 let band = hit_size + hit_size;
388 let span_x = inner_right - inner_left + band;
389 let span_y = inner_bottom - inner_top + band;
390
391 let mut zones: Vec<AnyElement> = Vec::new();
392
393 let mut push_zone = |edge: ResizeEdge, origin: Point<Pixels>, zone_size: Size<Pixels>| {
394 let origin = origin - frame_origin;
395 zones.push(
396 div()
397 .absolute()
398 .left(origin.x)
399 .top(origin.y)
400 .w(zone_size.width)
401 .h(zone_size.height)
402 .cursor(cursor_style_for_resize_edge(edge))
403 .into_any_element(),
404 );
405 };
406
407 if !tiling.top {
408 push_zone(
409 ResizeEdge::Top,
410 point(inner_left - hit_size, inner_top - hit_size),
411 Size::new(span_x, band),
412 );
413 }
414 if !tiling.bottom {
415 push_zone(
416 ResizeEdge::Bottom,
417 point(inner_left - hit_size, inner_bottom - hit_size),
418 Size::new(span_x, band),
419 );
420 }
421 if !tiling.left {
422 push_zone(
423 ResizeEdge::Left,
424 point(inner_left - hit_size, inner_top - hit_size),
425 Size::new(band, span_y),
426 );
427 }
428 if !tiling.right {
429 push_zone(
430 ResizeEdge::Right,
431 point(inner_right - hit_size, inner_top - hit_size),
432 Size::new(band, span_y),
433 );
434 }
435
436 if !tiling.top && !tiling.left {
438 push_zone(
439 ResizeEdge::TopLeft,
440 point(inner_left - hit_size, inner_top - hit_size),
441 Size::new(band, band),
442 );
443 }
444 if !tiling.top && !tiling.right {
445 push_zone(
446 ResizeEdge::TopRight,
447 point(inner_right - hit_size, inner_top - hit_size),
448 Size::new(band, band),
449 );
450 }
451 if !tiling.bottom && !tiling.left {
452 push_zone(
453 ResizeEdge::BottomLeft,
454 point(inner_left - hit_size, inner_bottom - hit_size),
455 Size::new(band, band),
456 );
457 }
458 if !tiling.bottom && !tiling.right {
459 push_zone(
460 ResizeEdge::BottomRight,
461 point(inner_right - hit_size, inner_bottom - hit_size),
462 Size::new(band, band),
463 );
464 }
465
466 zones
467}
468
469fn resize_edge(
471 pos: Point<Pixels>,
472 size: Size<Pixels>,
473 insets: Edges<Pixels>,
474 tiling: &Tiling,
475 hit_size: Pixels,
476) -> Option<ResizeEdge> {
477 let inner_left = insets.left;
478 let inner_right = size.width - insets.right;
479 let inner_top = insets.top;
480 let inner_bottom = size.height - insets.bottom;
481
482 let on_left = pos.x >= inner_left - hit_size
484 && pos.x <= inner_left + hit_size
485 && pos.y >= inner_top - hit_size
486 && pos.y <= inner_bottom + hit_size;
487 let on_right = pos.x >= inner_right - hit_size
488 && pos.x <= inner_right + hit_size
489 && pos.y >= inner_top - hit_size
490 && pos.y <= inner_bottom + hit_size;
491 let on_top = pos.y >= inner_top - hit_size
492 && pos.y <= inner_top + hit_size
493 && pos.x >= inner_left - hit_size
494 && pos.x <= inner_right + hit_size;
495 let on_bottom = pos.y >= inner_bottom - hit_size
496 && pos.y <= inner_bottom + hit_size
497 && pos.x >= inner_left - hit_size
498 && pos.x <= inner_right + hit_size;
499
500 if !tiling.top && !tiling.left && on_top && on_left {
501 return Some(ResizeEdge::TopLeft);
502 }
503 if !tiling.top && !tiling.right && on_top && on_right {
504 return Some(ResizeEdge::TopRight);
505 }
506 if !tiling.bottom && !tiling.left && on_bottom && on_left {
507 return Some(ResizeEdge::BottomLeft);
508 }
509 if !tiling.bottom && !tiling.right && on_bottom && on_right {
510 return Some(ResizeEdge::BottomRight);
511 }
512 if !tiling.top && on_top {
513 return Some(ResizeEdge::Top);
514 }
515 if !tiling.bottom && on_bottom {
516 return Some(ResizeEdge::Bottom);
517 }
518 if !tiling.left && on_left {
519 return Some(ResizeEdge::Left);
520 }
521 if !tiling.right && on_right {
522 return Some(ResizeEdge::Right);
523 }
524 None
525}