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
124pub(crate) fn window_safe_insets(window: &Window) -> Edges<Pixels> {
128 let viewport = window.viewport_size();
129 let visible = window.fully_visible_bounds();
130 Edges {
131 top: visible.top(),
132 right: viewport.width - visible.right(),
133 bottom: viewport.height - visible.bottom(),
134 left: visible.left(),
135 }
136}
137
138impl ParentElement for WindowBorder {
139 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
140 self.children.extend(elements);
141 }
142}
143
144impl RenderOnce for WindowBorder {
145 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
146 let decorations = window.window_decorations();
147 let mut children = self.children;
148 if !uses_client_border(decorations) {
149 return match children.len() {
150 1 => children.pop().unwrap(),
151 _ => div().size_full().children(children).into_any_element(),
152 };
153 }
154 let Decorations::Client { tiling } = decorations else {
155 unreachable!("client border requires client decorations");
156 };
157
158 let platform_inset = self.shadow_size;
163 let visual_shadow = if tiling.top && tiling.bottom && tiling.left && tiling.right {
164 px(0.0)
165 } else {
166 self.shadow_size
167 };
168 let resize_hit_size = self.resize_hit_size;
169 window.set_client_inset(platform_inset);
170 let window_size = window.window_bounds().get_bounds().size;
171 let is_window_active = window.is_window_active();
172 let border_color = if cx.theme().is_dark() {
173 Hsla {
174 h: 0.,
175 s: 0.,
176 l: 0.2,
177 a: 1.0,
178 }
179 } else {
180 Hsla {
181 h: 0.,
182 s: 0.,
183 l: 0.8,
184 a: 1.0,
185 }
186 };
187
188 div()
189 .id("window-backdrop")
190 .bg(gpui::transparent_black())
191 .flex()
192 .flex_col()
193 .overflow_hidden()
194 .when(!(tiling.top || tiling.right), |div| {
195 div.rounded_tr(BORDER_RADIUS)
196 })
197 .when(!(tiling.top || tiling.left), |div| {
198 div.rounded_tl(BORDER_RADIUS)
199 })
200 .when(!tiling.top, |div| div.pt(visual_shadow))
201 .when(!tiling.bottom, |div| div.pb(visual_shadow))
202 .when(!tiling.left, |div| div.pl(visual_shadow))
203 .when(!tiling.right, |div| div.pr(visual_shadow))
204 .on_mouse_down(MouseButton::Left, move |_, window, _| {
205 let Decorations::Client { tiling } = window.window_decorations() else {
206 return;
207 };
208 if !window.is_resizable() {
209 return;
210 }
211 if tiling.top && tiling.bottom && tiling.left && tiling.right {
212 return;
213 }
214 let size = window.window_bounds().get_bounds().size;
215 let pos = window.mouse_position();
216 let insets = client_frame_insets(platform_inset, &tiling);
217
218 match resize_edge(pos, size, insets, &tiling, resize_hit_size) {
219 Some(edge) => window.start_window_resize(edge),
220 None => {}
221 };
222 })
223 .size_full()
224 .child(
225 div()
226 .cursor(CursorStyle::default())
227 .flex_1()
228 .min_h_0()
229 .min_w_0()
230 .overflow_hidden()
231 .when(!(tiling.top || tiling.right), |div| {
232 div.rounded_tr(BORDER_RADIUS)
233 })
234 .when(!(tiling.top || tiling.left), |div| {
235 div.rounded_tl(BORDER_RADIUS)
236 })
237 .border_color(border_color)
238 .when(!tiling.top, |div| div.border_t(BORDER_SIZE))
239 .when(!tiling.bottom, |div| div.border_b(BORDER_SIZE))
240 .when(!tiling.left, |div| div.border_l(BORDER_SIZE))
241 .when(!tiling.right, |div| div.border_r(BORDER_SIZE))
242 .when(!tiling.is_tiled(), |div| {
243 let opacity = if is_window_active { 1.0 } else { 0.7 };
244 div.shadow(vec![
245 gpui::BoxShadow {
249 color: Hsla {
250 h: 0.,
251 s: 0.,
252 l: 0.,
253 a: 0.18 * opacity,
254 },
255 blur_radius: px(10.),
258 spread_radius: px(-1.),
259 offset: point(px(0.0), px(2.0)),
260 inset: false,
261 },
262 gpui::BoxShadow {
265 color: Hsla {
266 h: 0.,
267 s: 0.,
268 l: 0.,
269 a: 0.18 * opacity,
270 },
271 blur_radius: px(3.),
272 spread_radius: px(0.),
273 offset: point(px(0.0), px(1.0)),
274 inset: false,
275 },
276 ])
277 })
278 .on_mouse_move(|_e, _, cx| {
279 cx.stop_propagation();
280 })
281 .bg(gpui::transparent_black())
282 .children(children),
283 )
284 .when(window.is_resizable(), |parent| {
285 parent.child(div().absolute().size_full().children(resize_hit_zones(
286 window_size,
287 platform_inset,
288 resize_hit_size,
289 &tiling,
290 )))
291 })
292 .into_any_element()
293 }
294}
295
296fn uses_client_border(decorations: Decorations) -> bool {
297 matches!(decorations, Decorations::Client { .. })
298}
299
300fn cursor_style_for_resize_edge(edge: ResizeEdge) -> CursorStyle {
301 match edge {
302 ResizeEdge::Top | ResizeEdge::Bottom => CursorStyle::ResizeUpDown,
303 ResizeEdge::Left | ResizeEdge::Right => CursorStyle::ResizeLeftRight,
304 ResizeEdge::TopLeft | ResizeEdge::BottomRight => CursorStyle::ResizeUpLeftDownRight,
305 ResizeEdge::TopRight | ResizeEdge::BottomLeft => CursorStyle::ResizeUpRightDownLeft,
306 }
307}
308
309#[cfg(test)]
310mod tests {
311 use super::*;
312
313 #[test]
314 fn only_client_decorated_windows_use_the_client_border() {
315 assert!(!uses_client_border(Decorations::Server));
316 assert!(uses_client_border(Decorations::Client {
317 tiling: Tiling::default(),
318 }));
319 }
320}
321
322fn resize_hit_zones(
326 window_size: Size<Pixels>,
327 shadow_size: Pixels,
328 hit_size: Pixels,
329 tiling: &Tiling,
330) -> Vec<AnyElement> {
331 if tiling.top && tiling.bottom && tiling.left && tiling.right {
332 return Vec::new();
333 }
334
335 let insets = client_frame_insets(shadow_size, tiling);
336 let inner_left = insets.left;
337 let inner_right = window_size.width - insets.right;
338 let inner_top = insets.top;
339 let inner_bottom = window_size.height - insets.bottom;
340 let frame_origin = point(insets.left, insets.top);
342 let band = hit_size + hit_size;
343 let span_x = inner_right - inner_left + band;
344 let span_y = inner_bottom - inner_top + band;
345
346 let mut zones: Vec<AnyElement> = Vec::new();
347
348 let mut push_zone = |edge: ResizeEdge, origin: Point<Pixels>, zone_size: Size<Pixels>| {
349 let origin = origin - frame_origin;
350 zones.push(
351 div()
352 .absolute()
353 .left(origin.x)
354 .top(origin.y)
355 .w(zone_size.width)
356 .h(zone_size.height)
357 .cursor(cursor_style_for_resize_edge(edge))
358 .into_any_element(),
359 );
360 };
361
362 if !tiling.top {
363 push_zone(
364 ResizeEdge::Top,
365 point(inner_left - hit_size, inner_top - hit_size),
366 Size::new(span_x, band),
367 );
368 }
369 if !tiling.bottom {
370 push_zone(
371 ResizeEdge::Bottom,
372 point(inner_left - hit_size, inner_bottom - hit_size),
373 Size::new(span_x, band),
374 );
375 }
376 if !tiling.left {
377 push_zone(
378 ResizeEdge::Left,
379 point(inner_left - hit_size, inner_top - hit_size),
380 Size::new(band, span_y),
381 );
382 }
383 if !tiling.right {
384 push_zone(
385 ResizeEdge::Right,
386 point(inner_right - hit_size, inner_top - hit_size),
387 Size::new(band, span_y),
388 );
389 }
390
391 if !tiling.top && !tiling.left {
393 push_zone(
394 ResizeEdge::TopLeft,
395 point(inner_left - hit_size, inner_top - hit_size),
396 Size::new(band, band),
397 );
398 }
399 if !tiling.top && !tiling.right {
400 push_zone(
401 ResizeEdge::TopRight,
402 point(inner_right - hit_size, inner_top - hit_size),
403 Size::new(band, band),
404 );
405 }
406 if !tiling.bottom && !tiling.left {
407 push_zone(
408 ResizeEdge::BottomLeft,
409 point(inner_left - hit_size, inner_bottom - hit_size),
410 Size::new(band, band),
411 );
412 }
413 if !tiling.bottom && !tiling.right {
414 push_zone(
415 ResizeEdge::BottomRight,
416 point(inner_right - hit_size, inner_bottom - hit_size),
417 Size::new(band, band),
418 );
419 }
420
421 zones
422}
423
424fn resize_edge(
426 pos: Point<Pixels>,
427 size: Size<Pixels>,
428 insets: Edges<Pixels>,
429 tiling: &Tiling,
430 hit_size: Pixels,
431) -> Option<ResizeEdge> {
432 let inner_left = insets.left;
433 let inner_right = size.width - insets.right;
434 let inner_top = insets.top;
435 let inner_bottom = size.height - insets.bottom;
436
437 let on_left = pos.x >= inner_left - hit_size
439 && pos.x <= inner_left + hit_size
440 && pos.y >= inner_top - hit_size
441 && pos.y <= inner_bottom + hit_size;
442 let on_right = pos.x >= inner_right - hit_size
443 && pos.x <= inner_right + hit_size
444 && pos.y >= inner_top - hit_size
445 && pos.y <= inner_bottom + hit_size;
446 let on_top = pos.y >= inner_top - hit_size
447 && pos.y <= inner_top + hit_size
448 && pos.x >= inner_left - hit_size
449 && pos.x <= inner_right + hit_size;
450 let on_bottom = pos.y >= inner_bottom - hit_size
451 && pos.y <= inner_bottom + hit_size
452 && pos.x >= inner_left - hit_size
453 && pos.x <= inner_right + hit_size;
454
455 if !tiling.top && !tiling.left && on_top && on_left {
456 return Some(ResizeEdge::TopLeft);
457 }
458 if !tiling.top && !tiling.right && on_top && on_right {
459 return Some(ResizeEdge::TopRight);
460 }
461 if !tiling.bottom && !tiling.left && on_bottom && on_left {
462 return Some(ResizeEdge::BottomLeft);
463 }
464 if !tiling.bottom && !tiling.right && on_bottom && on_right {
465 return Some(ResizeEdge::BottomRight);
466 }
467 if !tiling.top && on_top {
468 return Some(ResizeEdge::Top);
469 }
470 if !tiling.bottom && on_bottom {
471 return Some(ResizeEdge::Bottom);
472 }
473 if !tiling.left && on_left {
474 return Some(ResizeEdge::Left);
475 }
476 if !tiling.right && on_right {
477 return Some(ResizeEdge::Right);
478 }
479 None
480}