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