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 tiling.top && tiling.bottom && tiling.left && tiling.right {
195 return;
196 }
197 let size = window.window_bounds().get_bounds().size;
198 let pos = window.mouse_position();
199 let insets = client_frame_insets(platform_inset, &tiling);
200
201 match resize_edge(pos, size, insets, &tiling, resize_hit_size) {
202 Some(edge) => window.start_window_resize(edge),
203 None => {}
204 };
205 })
206 .size_full()
207 .child(
208 div()
209 .cursor(CursorStyle::default())
210 .flex_1()
211 .min_h_0()
212 .min_w_0()
213 .overflow_hidden()
214 .when(!(tiling.top || tiling.right), |div| {
215 div.rounded_tr(BORDER_RADIUS)
216 })
217 .when(!(tiling.top || tiling.left), |div| {
218 div.rounded_tl(BORDER_RADIUS)
219 })
220 .border_color(border_color)
221 .when(!tiling.top, |div| div.border_t(BORDER_SIZE))
222 .when(!tiling.bottom, |div| div.border_b(BORDER_SIZE))
223 .when(!tiling.left, |div| div.border_l(BORDER_SIZE))
224 .when(!tiling.right, |div| div.border_r(BORDER_SIZE))
225 .when(!tiling.is_tiled(), |div| {
226 let opacity = if is_window_active { 1.0 } else { 0.7 };
227 div.shadow(vec![
228 gpui::BoxShadow {
232 color: Hsla {
233 h: 0.,
234 s: 0.,
235 l: 0.,
236 a: 0.18 * opacity,
237 },
238 blur_radius: px(10.),
241 spread_radius: px(-1.),
242 offset: point(px(0.0), px(2.0)),
243 inset: false,
244 },
245 gpui::BoxShadow {
248 color: Hsla {
249 h: 0.,
250 s: 0.,
251 l: 0.,
252 a: 0.18 * opacity,
253 },
254 blur_radius: px(3.),
255 spread_radius: px(0.),
256 offset: point(px(0.0), px(1.0)),
257 inset: false,
258 },
259 ])
260 })
261 .on_mouse_move(|_e, _, cx| {
262 cx.stop_propagation();
263 })
264 .bg(gpui::transparent_black())
265 .children(children),
266 )
267 .child(div().absolute().size_full().children(resize_hit_zones(
268 window_size,
269 platform_inset,
270 resize_hit_size,
271 &tiling,
272 )))
273 .into_any_element()
274 }
275}
276
277fn uses_client_border(decorations: Decorations) -> bool {
278 matches!(decorations, Decorations::Client { .. })
279}
280
281fn cursor_style_for_resize_edge(edge: ResizeEdge) -> CursorStyle {
282 match edge {
283 ResizeEdge::Top | ResizeEdge::Bottom => CursorStyle::ResizeUpDown,
284 ResizeEdge::Left | ResizeEdge::Right => CursorStyle::ResizeLeftRight,
285 ResizeEdge::TopLeft | ResizeEdge::BottomRight => CursorStyle::ResizeUpLeftDownRight,
286 ResizeEdge::TopRight | ResizeEdge::BottomLeft => CursorStyle::ResizeUpRightDownLeft,
287 }
288}
289
290#[cfg(test)]
291mod tests {
292 use super::*;
293
294 #[test]
295 fn only_client_decorated_windows_use_the_client_border() {
296 assert!(!uses_client_border(Decorations::Server));
297 assert!(uses_client_border(Decorations::Client {
298 tiling: Tiling::default(),
299 }));
300 }
301}
302
303fn resize_hit_zones(
307 window_size: Size<Pixels>,
308 shadow_size: Pixels,
309 hit_size: Pixels,
310 tiling: &Tiling,
311) -> Vec<AnyElement> {
312 if tiling.top && tiling.bottom && tiling.left && tiling.right {
313 return Vec::new();
314 }
315
316 let insets = client_frame_insets(shadow_size, tiling);
317 let inner_left = insets.left;
318 let inner_right = window_size.width - insets.right;
319 let inner_top = insets.top;
320 let inner_bottom = window_size.height - insets.bottom;
321 let frame_origin = point(insets.left, insets.top);
323 let band = hit_size + hit_size;
324 let span_x = inner_right - inner_left + band;
325 let span_y = inner_bottom - inner_top + band;
326
327 let mut zones: Vec<AnyElement> = Vec::new();
328
329 let mut push_zone = |edge: ResizeEdge, origin: Point<Pixels>, zone_size: Size<Pixels>| {
330 let origin = origin - frame_origin;
331 zones.push(
332 div()
333 .absolute()
334 .left(origin.x)
335 .top(origin.y)
336 .w(zone_size.width)
337 .h(zone_size.height)
338 .cursor(cursor_style_for_resize_edge(edge))
339 .into_any_element(),
340 );
341 };
342
343 if !tiling.top {
344 push_zone(
345 ResizeEdge::Top,
346 point(inner_left - hit_size, inner_top - hit_size),
347 Size::new(span_x, band),
348 );
349 }
350 if !tiling.bottom {
351 push_zone(
352 ResizeEdge::Bottom,
353 point(inner_left - hit_size, inner_bottom - hit_size),
354 Size::new(span_x, band),
355 );
356 }
357 if !tiling.left {
358 push_zone(
359 ResizeEdge::Left,
360 point(inner_left - hit_size, inner_top - hit_size),
361 Size::new(band, span_y),
362 );
363 }
364 if !tiling.right {
365 push_zone(
366 ResizeEdge::Right,
367 point(inner_right - hit_size, inner_top - hit_size),
368 Size::new(band, span_y),
369 );
370 }
371
372 if !tiling.top && !tiling.left {
374 push_zone(
375 ResizeEdge::TopLeft,
376 point(inner_left - hit_size, inner_top - hit_size),
377 Size::new(band, band),
378 );
379 }
380 if !tiling.top && !tiling.right {
381 push_zone(
382 ResizeEdge::TopRight,
383 point(inner_right - hit_size, inner_top - hit_size),
384 Size::new(band, band),
385 );
386 }
387 if !tiling.bottom && !tiling.left {
388 push_zone(
389 ResizeEdge::BottomLeft,
390 point(inner_left - hit_size, inner_bottom - hit_size),
391 Size::new(band, band),
392 );
393 }
394 if !tiling.bottom && !tiling.right {
395 push_zone(
396 ResizeEdge::BottomRight,
397 point(inner_right - hit_size, inner_bottom - hit_size),
398 Size::new(band, band),
399 );
400 }
401
402 zones
403}
404
405fn resize_edge(
407 pos: Point<Pixels>,
408 size: Size<Pixels>,
409 insets: Edges<Pixels>,
410 tiling: &Tiling,
411 hit_size: Pixels,
412) -> Option<ResizeEdge> {
413 let inner_left = insets.left;
414 let inner_right = size.width - insets.right;
415 let inner_top = insets.top;
416 let inner_bottom = size.height - insets.bottom;
417
418 let on_left = pos.x >= inner_left - hit_size
420 && pos.x <= inner_left + hit_size
421 && pos.y >= inner_top - hit_size
422 && pos.y <= inner_bottom + hit_size;
423 let on_right = pos.x >= inner_right - hit_size
424 && pos.x <= inner_right + hit_size
425 && pos.y >= inner_top - hit_size
426 && pos.y <= inner_bottom + hit_size;
427 let on_top = pos.y >= inner_top - hit_size
428 && pos.y <= inner_top + hit_size
429 && pos.x >= inner_left - hit_size
430 && pos.x <= inner_right + hit_size;
431 let on_bottom = pos.y >= inner_bottom - hit_size
432 && pos.y <= inner_bottom + hit_size
433 && pos.x >= inner_left - hit_size
434 && pos.x <= inner_right + hit_size;
435
436 if !tiling.top && !tiling.left && on_top && on_left {
437 return Some(ResizeEdge::TopLeft);
438 }
439 if !tiling.top && !tiling.right && on_top && on_right {
440 return Some(ResizeEdge::TopRight);
441 }
442 if !tiling.bottom && !tiling.left && on_bottom && on_left {
443 return Some(ResizeEdge::BottomLeft);
444 }
445 if !tiling.bottom && !tiling.right && on_bottom && on_right {
446 return Some(ResizeEdge::BottomRight);
447 }
448 if !tiling.top && on_top {
449 return Some(ResizeEdge::Top);
450 }
451 if !tiling.bottom && on_bottom {
452 return Some(ResizeEdge::Bottom);
453 }
454 if !tiling.left && on_left {
455 return Some(ResizeEdge::Left);
456 }
457 if !tiling.right && on_right {
458 return Some(ResizeEdge::Right);
459 }
460 None
461}