1use crate::color::ColorRole::{self, *};
29use crate::theme::color;
30use crate::tokens::state;
31use gpui::{
32 AnyElement, App, Bounds, Decorations, ElementId, InteractiveElement as _, IntoElement,
33 MouseButton, ParentElement as _, PathBuilder, Pixels, RenderOnce, Rgba, SharedString,
34 StatefulInteractiveElement as _, Styled, Window, WindowControlArea, canvas, div, fill, point,
35 prelude::FluentBuilder as _, px, size,
36};
37use std::rc::Rc;
38
39#[derive(Clone, Copy, Debug, PartialEq, Eq)]
41pub enum Caption {
42 System,
45 Windows,
47 Linux,
50}
51
52impl Caption {
53 pub fn detect(window: &Window) -> Self {
55 if cfg!(target_os = "windows") {
56 Caption::Windows
57 } else if cfg!(any(target_os = "linux", target_os = "freebsd"))
58 && matches!(window.window_decorations(), Decorations::Client { .. })
59 {
60 Caption::Linux
61 } else {
62 Caption::System
63 }
64 }
65}
66
67const WINDOWS_BUTTON_WIDTH: f32 = 46.;
72const WINDOWS_BUTTON_HEIGHT: f32 = 32.;
73const CAPTION_PRIORITY: usize = 50;
75const LINUX_BUTTON: f32 = 32.;
78const LINUX_SHAPE: f32 = 24.;
79const LINUX_GAP: f32 = 4.;
80const LINUX_END: f32 = 12.;
81const WINDOWS_GLYPH: f32 = 10.;
85const WINDOWS_STROKE: f32 = 1.;
86const LINUX_GLYPH: f32 = 8.;
87const LINUX_STROKE: f32 = 1.5;
88
89const WINDOWS_CLOSE_RED: Rgba = Rgba {
94 r: 0xc4 as f32 / 255.,
95 g: 0x2b as f32 / 255.,
96 b: 0x1c as f32 / 255.,
97 a: 1.,
98};
99const WINDOWS_CLOSE_GLYPH: Rgba = Rgba {
101 r: 1.,
102 g: 1.,
103 b: 1.,
104 a: 1.,
105};
106
107fn close_hover() -> Rgba {
108 WINDOWS_CLOSE_RED
109}
110
111fn close_pressed() -> Rgba {
112 Rgba {
113 a: 0.9,
114 ..WINDOWS_CLOSE_RED
115 }
116}
117
118#[derive(Clone, Copy, Debug, PartialEq, Eq)]
120pub enum CaptionButton {
121 Minimize,
122 Maximize,
123 Restore,
124 Close,
125}
126
127impl CaptionButton {
128 fn id(self) -> &'static str {
129 match self {
130 Self::Minimize => "caption-minimize",
131 Self::Maximize => "caption-maximize",
132 Self::Restore => "caption-restore",
133 Self::Close => "caption-close",
134 }
135 }
136
137 fn area(self) -> WindowControlArea {
138 match self {
139 Self::Minimize => WindowControlArea::Min,
140 Self::Maximize | Self::Restore => WindowControlArea::Max,
141 Self::Close => WindowControlArea::Close,
142 }
143 }
144}
145
146#[derive(Clone, Debug)]
153pub struct CaptionLabels {
154 pub minimize: SharedString,
155 pub maximize: SharedString,
156 pub restore: SharedString,
157 pub close: SharedString,
158}
159
160impl CaptionLabels {
161 fn get(&self, button: CaptionButton) -> SharedString {
162 match button {
163 CaptionButton::Minimize => self.minimize.clone(),
164 CaptionButton::Maximize => self.maximize.clone(),
165 CaptionButton::Restore => self.restore.clone(),
166 CaptionButton::Close => self.close.clone(),
167 }
168 }
169}
170
171fn preview_buttons(caption: Caption) -> Vec<CaptionButton> {
174 if caption == Caption::System {
175 return Vec::new();
176 }
177 vec![
178 CaptionButton::Minimize,
179 CaptionButton::Maximize,
180 CaptionButton::Close,
181 ]
182}
183
184fn buttons(caption: Caption, window: &Window) -> Vec<CaptionButton> {
186 if caption == Caption::System || window.is_fullscreen() {
187 return Vec::new();
188 }
189 let (minimize, maximize) = match caption {
190 Caption::Windows => (true, true),
192 _ => {
193 let supported = window.window_controls();
194 (supported.minimize, supported.maximize)
195 }
196 };
197 let zoom = if window.is_maximized() {
198 CaptionButton::Restore
199 } else {
200 CaptionButton::Maximize
201 };
202 [
203 minimize.then_some(CaptionButton::Minimize),
204 maximize.then_some(zoom),
205 Some(CaptionButton::Close),
206 ]
207 .into_iter()
208 .flatten()
209 .collect()
210}
211
212fn width_of(caption: Caption, count: usize) -> f32 {
213 match (caption, count) {
214 (_, 0) | (Caption::System, _) => 0.,
215 (Caption::Windows, count) => WINDOWS_BUTTON_WIDTH * count as f32,
216 (Caption::Linux, count) => {
217 LINUX_BUTTON * count as f32 + LINUX_GAP * (count - 1) as f32 + LINUX_END
218 }
219 }
220}
221
222fn trailing_for(caption_width: f32) -> f32 {
223 if caption_width > 0. {
224 caption_width + 8.
225 } else {
226 16.
227 }
228}
229
230pub(super) type CloseRequest = Rc<dyn Fn(&mut Window, &mut App) -> bool>;
231
232#[derive(gpui::IntoElement)]
240pub struct CaptionButtons {
241 height: Pixels,
242 labels: CaptionLabels,
243 caption: Option<Caption>,
244 on_close: Option<CloseRequest>,
245 preview: Option<ElementId>,
246}
247
248impl CaptionButtons {
249 pub fn new(height: Pixels, labels: CaptionLabels) -> Self {
251 Self {
252 height,
253 labels,
254 caption: None,
255 on_close: None,
256 preview: None,
257 }
258 }
259
260 pub fn preview(mut self, id: impl Into<ElementId>) -> Self {
267 self.preview = Some(id.into());
268 self
269 }
270
271 pub fn labels(mut self, labels: CaptionLabels) -> Self {
272 self.labels = labels;
273 self
274 }
275
276 pub fn caption(mut self, caption: Caption) -> Self {
278 self.caption = Some(caption);
279 self
280 }
281
282 pub fn on_close(mut self, request: impl Fn(&mut Window, &mut App) -> bool + 'static) -> Self {
286 self.on_close = Some(Rc::new(request));
287 self
288 }
289
290 pub fn width(window: &Window) -> f32 {
292 let caption = Caption::detect(window);
293 width_of(caption, buttons(caption, window).len())
294 }
295
296 pub fn trailing(window: &Window) -> f32 {
299 trailing_for(Self::width(window))
300 }
301
302 pub(super) fn trailing_for_style(caption: Caption, window: &Window) -> f32 {
304 trailing_for(width_of(caption, buttons(caption, window).len()))
305 }
306}
307
308impl RenderOnce for CaptionButtons {
309 fn render(self, window: &mut Window, _: &mut App) -> impl IntoElement {
310 let caption = self.caption.unwrap_or_else(|| Caption::detect(window));
311 let live = self.preview.is_none();
312 let buttons = if live {
313 buttons(caption, window)
314 } else {
315 preview_buttons(caption)
316 };
317 if buttons.is_empty() {
318 return div().into_any_element();
319 }
320 let glyph_color = color(if window.is_window_active() {
321 ON_SURFACE
322 } else {
323 ON_SURFACE_VARIANT
324 });
325 let id = self
326 .preview
327 .clone()
328 .unwrap_or_else(|| ElementId::from("window-caption"));
329 let row = gpui_base::h_flex()
330 .id(id)
331 .flex_shrink_0()
332 .text_color(glyph_color);
333 let row = match caption {
334 Caption::Windows => row.h(self.height).items_center().children(
335 buttons
336 .into_iter()
337 .map(|b| windows_button(b, &self.labels, self.height, live)),
338 ),
339 _ => {
340 row.h(self.height)
341 .items_center()
342 .pr(px(LINUX_END))
343 .gap(px(LINUX_GAP))
344 .children(buttons.into_iter().map(|button| {
345 linux_button(button, &self.labels, live, self.on_close.clone())
346 }))
347 }
348 };
349 if live {
350 caption_layer(row, window)
351 } else {
352 row.into_any_element()
353 }
354 }
355}
356
357pub fn title_bar_control<E: gpui::InteractiveElement>(element: E) -> E {
362 element.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
363}
364
365fn glyph_rects(button: CaptionButton, glyph: f32, stroke: f32) -> Vec<Bounds<Pixels>> {
370 let rect = |x: f32, y: f32, width: f32, height: f32| Bounds {
371 origin: point(px(x), px(y)),
372 size: size(px(width), px(height)),
373 };
374 let frame = |x: f32, y: f32, side: f32| {
375 [
376 rect(x, y, side, stroke),
377 rect(x, y + side - stroke, side, stroke),
378 rect(x, y + stroke, stroke, side - 2. * stroke),
379 rect(x + side - stroke, y + stroke, stroke, side - 2. * stroke),
380 ]
381 };
382 match button {
383 CaptionButton::Minimize => vec![rect(0., (glyph - stroke) / 2., glyph, stroke)],
384 CaptionButton::Maximize => frame(0., 0., glyph).to_vec(),
385 CaptionButton::Restore => {
386 let offset = (glyph * 0.2).round();
389 let mut rects = frame(0., offset, glyph - offset).to_vec();
390 rects.push(rect(offset, 0., glyph - offset, stroke));
391 rects.push(rect(
392 glyph - stroke,
393 stroke,
394 stroke,
395 glyph - offset - stroke,
396 ));
397 rects
398 }
399 CaptionButton::Close => Vec::new(),
400 }
401}
402
403fn glyph(button: CaptionButton, glyph: f32, stroke: f32) -> impl IntoElement {
406 let ink_box = div()
407 .when(cfg!(test), move |ink_box| {
408 ink_box.debug_selector(move || format!("{}-glyph", button.id()))
409 })
410 .flex_shrink_0()
411 .size(px(glyph));
412 ink_box.child(
413 canvas(
414 |_, _, _| {},
415 move |bounds, _, window, _| {
416 let ink = window.text_style().color;
417 let scale = window.scale_factor();
418 let snap = |value: f32| (value * scale).round() / scale;
419 let stroke = ((stroke * scale).round() / scale).max(1. / scale);
420 let origin = point(
421 px(snap(f32::from(bounds.origin.x))),
422 px(snap(f32::from(bounds.origin.y))),
423 );
424 for rect in glyph_rects(button, glyph, stroke) {
425 let snapped = Bounds {
426 origin: point(
427 origin.x + px(snap(f32::from(rect.origin.x))),
428 origin.y + px(snap(f32::from(rect.origin.y))),
429 ),
430 size: rect.size,
431 };
432 window.paint_quad(fill(snapped, ink));
433 }
434 if button == CaptionButton::Close {
435 let side = px(glyph);
436 for (from, to) in [
437 (point(px(0.), px(0.)), point(side, side)),
438 (point(side, px(0.)), point(px(0.), side)),
439 ] {
440 let mut path = PathBuilder::stroke(px(stroke));
441 path.move_to(origin + from);
442 path.line_to(origin + to);
443 if let Ok(path) = path.build() {
444 window.paint_path(path, ink);
445 }
446 }
447 }
448 },
449 )
450 .size_full(),
451 )
452}
453
454fn caption_layer(row: impl IntoElement, window: &Window) -> AnyElement {
456 let trailing = gpui::point(window.viewport_size().width, px(0.));
457 gpui::deferred(
458 gpui::anchored()
459 .anchor(gpui::Anchor::TopRight)
460 .position(trailing)
461 .child(row),
462 )
463 .with_priority(CAPTION_PRIORITY)
464 .into_any_element()
465}
466
467fn windows_button(
470 button: CaptionButton,
471 labels: &CaptionLabels,
472 bar_height: Pixels,
473 live: bool,
474) -> gpui::Stateful<gpui::Div> {
475 let close = button == CaptionButton::Close;
476 let ink = color(ON_SURFACE);
477 let (hover, pressed) = if close {
478 (close_hover(), close_pressed())
479 } else {
480 let clear = Rgba { a: 0., ..ink };
481 (state::hovered(clear, ink), state::pressed(clear, ink))
482 };
483 let on_close = WINDOWS_CLOSE_GLYPH;
484 div()
485 .id(button.id())
486 .role(gpui::Role::Button)
487 .aria_label(labels.get(button))
488 .occlude()
489 .when(live, |button_area| {
490 button_area.window_control_area(button.area())
491 })
492 .flex()
493 .flex_shrink_0()
494 .items_center()
495 .justify_center()
496 .w(px(WINDOWS_BUTTON_WIDTH))
497 .h(px(WINDOWS_BUTTON_HEIGHT).min(bar_height))
498 .hover(move |style| {
499 let style = style.bg(hover);
500 if close {
501 style.text_color(on_close)
502 } else {
503 style
504 }
505 })
506 .active(move |style| {
507 let style = style.bg(pressed);
508 if close {
509 style.text_color(on_close)
510 } else {
511 style
512 }
513 })
514 .when(cfg!(test), |area| {
515 area.debug_selector(move || button.id().to_string())
516 })
517 .child(glyph(button, WINDOWS_GLYPH, WINDOWS_STROKE))
518}
519
520fn linux_button(
525 button: CaptionButton,
526 labels: &CaptionLabels,
527 live: bool,
528 on_close: Option<CloseRequest>,
529) -> gpui::Stateful<gpui::Div> {
530 let group = button.id();
531 let shape: ColorRole = SURFACE_CONTAINER_HIGHEST;
532 let (hover, pressed) = (
533 state::hovered(color(shape), color(ON_SURFACE)),
534 state::pressed(color(shape), color(ON_SURFACE)),
535 );
536 div()
537 .id(button.id())
538 .group(group)
539 .role(gpui::Role::Button)
540 .aria_label(labels.get(button))
541 .occlude()
542 .flex()
543 .flex_shrink_0()
544 .items_center()
545 .justify_center()
546 .size(px(LINUX_BUTTON))
547 .child(
548 div()
549 .id("shape")
550 .flex()
551 .items_center()
552 .justify_center()
553 .size(px(LINUX_SHAPE))
554 .rounded_full()
555 .bg(color(shape))
556 .group_hover(group, move |style| style.bg(hover))
557 .group_active(group, move |style| style.bg(pressed))
558 .when(cfg!(test), |shape| {
559 shape.debug_selector(move || format!("{}-shape", button.id()))
560 })
561 .child(glyph(button, LINUX_GLYPH, LINUX_STROKE)),
562 )
563 .when(cfg!(test), |target| {
564 target.debug_selector(move || button.id().to_string())
565 })
566 .on_mouse_down(MouseButton::Left, |_, window, cx| {
567 window.prevent_default();
568 cx.stop_propagation();
569 })
570 .on_click(move |_, window, cx| {
571 cx.stop_propagation();
572 if !live {
573 return;
574 }
575 match button {
576 CaptionButton::Minimize => window.minimize_window(),
577 CaptionButton::Maximize | CaptionButton::Restore => window.zoom_window(),
578 CaptionButton::Close => {
579 if on_close.as_ref().is_none_or(|request| request(window, cx)) {
580 window.remove_window();
581 }
582 }
583 }
584 })
585}
586
587#[cfg(test)]
588pub(crate) mod tests {
589 use super::*;
590 use gpui::{Context, Modifiers, Render, TestAppContext};
591
592 pub(crate) fn test_labels() -> CaptionLabels {
593 CaptionLabels {
594 minimize: "Minimize".into(),
595 maximize: "Maximize".into(),
596 restore: "Restore".into(),
597 close: "Close".into(),
598 }
599 }
600
601 struct DialogHarness {
604 backdrop_presses: usize,
605 menu_presses: usize,
606 }
607
608 impl Render for DialogHarness {
609 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
610 let backdrop = div()
611 .id("backdrop")
612 .absolute()
613 .top_0()
614 .left_0()
615 .size_full()
616 .occlude()
617 .window_control_area(WindowControlArea::Drag)
618 .on_mouse_down(
619 MouseButton::Left,
620 cx.listener(|this, _, _, _| this.backdrop_presses += 1),
621 );
622 let menu = div()
625 .id("menu")
626 .absolute()
627 .top(px(8.))
628 .right(px(8.))
629 .w(px(20.))
630 .h(px(200.))
631 .occlude()
632 .on_mouse_down(
633 MouseButton::Left,
634 cx.listener(|this, _, _, _| this.menu_presses += 1),
635 );
636 let labels = test_labels();
637 let row = gpui_base::h_flex().h(px(WINDOWS_BUTTON_HEIGHT)).children(
638 [
639 CaptionButton::Minimize,
640 CaptionButton::Maximize,
641 CaptionButton::Close,
642 ]
643 .map(|button| windows_button(button, &labels, px(WINDOWS_BUTTON_HEIGHT), true)),
644 );
645 div()
646 .size_full()
647 .child(gpui::deferred(backdrop).with_priority(10))
648 .child(caption_layer(row, window))
649 .child(gpui::deferred(menu).with_priority(gpui_base::POPUP_PRIORITY))
650 }
651 }
652
653 #[gpui::test]
654 fn caption_buttons_take_presses_over_a_dialog_backdrop(cx: &mut TestAppContext) {
655 let (view, cx) = cx.add_window_view(|_, _| DialogHarness {
656 backdrop_presses: 0,
657 menu_presses: 0,
658 });
659 cx.run_until_parked();
660 let width = cx.update(|window, _| f32::from(window.viewport_size().width));
661 for offset in [36., 69., 115.] {
664 cx.simulate_click(
665 gpui::point(px(width - offset), px(16.)),
666 Modifiers::default(),
667 );
668 }
669 cx.update(|_, cx| assert_eq!(view.read(cx).backdrop_presses, 0));
670 cx.simulate_click(gpui::point(px(width - 160.), px(16.)), Modifiers::default());
672 cx.simulate_click(gpui::point(px(width - 69.), px(40.)), Modifiers::default());
674 cx.update(|_, cx| assert_eq!(view.read(cx).backdrop_presses, 2));
675 cx.simulate_click(gpui::point(px(width - 18.), px(16.)), Modifiers::default());
677 cx.update(|_, cx| {
678 assert_eq!(view.read(cx).menu_presses, 1);
679 assert_eq!(view.read(cx).backdrop_presses, 2);
680 });
681 }
682
683 struct PreviewHarness {
685 page_presses: usize,
686 }
687
688 impl Render for PreviewHarness {
689 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
690 div()
691 .id("page")
692 .size_full()
693 .flex()
694 .flex_col()
695 .items_start()
696 .on_mouse_down(
697 MouseButton::Left,
698 cx.listener(|this, _, _, _| this.page_presses += 1),
699 )
700 .child(
701 div()
702 .debug_selector(|| "windows-preview".into())
703 .flex()
704 .child(
705 CaptionButtons::new(px(48.), test_labels())
706 .caption(Caption::Windows)
707 .preview("windows-preview"),
708 ),
709 )
710 .child(
711 div()
712 .debug_selector(|| "linux-preview".into())
713 .flex()
714 .child(
715 CaptionButtons::new(px(48.), test_labels())
716 .caption(Caption::Linux)
717 .on_close(|_, _| panic!("a preview must not close"))
718 .preview("linux-preview"),
719 ),
720 )
721 .child(
722 div().debug_selector(|| "system-preview".into()).child(
723 CaptionButtons::new(px(48.), test_labels())
724 .caption(Caption::System)
725 .preview("system-preview"),
726 ),
727 )
728 }
729 }
730
731 #[gpui::test]
732 fn a_preview_lays_out_in_place_and_does_not_act_on_the_window(cx: &mut TestAppContext) {
733 let (view, cx) = cx.add_window_view(|_, _| PreviewHarness { page_presses: 0 });
734 cx.run_until_parked();
735 let windows = cx.debug_bounds("windows-preview").unwrap();
736 let linux = cx.debug_bounds("linux-preview").unwrap();
737 let system = cx.debug_bounds("system-preview").unwrap();
738 assert_eq!(windows.origin, gpui::point(px(0.), px(0.)));
740 assert_eq!(f32::from(windows.size.width), width_of(Caption::Windows, 3));
741 assert_eq!(windows.size.height, px(48.));
742 assert_eq!(linux.origin.y, windows.size.height);
743 assert_eq!(f32::from(linux.size.width), width_of(Caption::Linux, 3));
744 assert_eq!(system.size.height, px(0.));
745 let close = gpui::point(
748 linux.origin.x + px(width_of(Caption::Linux, 3) - LINUX_END - LINUX_BUTTON / 2.),
749 linux.origin.y + px(24.),
750 );
751 cx.simulate_click(close, Modifiers::default());
752 cx.update(|window, cx| {
753 assert!(window.viewport_size().width > px(0.));
754 assert_eq!(view.read(cx).page_presses, 0);
755 });
756 }
757
758 fn assert_centered(
761 cx: &mut gpui::VisualTestContext,
762 (width, height): (f32, f32),
763 glyph_size: f32,
764 round: bool,
765 ) {
766 let bar = cx.debug_bounds("bar").expect("the bar is rendered");
767 for button in [
768 CaptionButton::Minimize,
769 CaptionButton::Maximize,
770 CaptionButton::Close,
771 ] {
772 let id = button.id();
773 let selector = |suffix: &str| -> &'static str {
774 Box::leak(format!("{id}{suffix}").into_boxed_str())
775 };
776 let target = cx.debug_bounds(id).expect("the button is rendered");
777 assert_eq!(target.size, size(px(width), px(height)), "{id}");
778 assert_eq!(
779 target.center().y,
780 bar.center().y,
781 "{id} on the bar's center"
782 );
783 let glyph = cx.debug_bounds(selector("-glyph")).expect("a glyph");
784 assert_eq!(glyph.size, size(px(glyph_size), px(glyph_size)), "{id}");
785 assert_eq!(glyph.center(), target.center(), "{id}'s glyph is centered");
786 if round {
787 let shape = cx.debug_bounds(selector("-shape")).expect("a shape");
788 assert_eq!(shape.center(), target.center(), "{id}'s shape is centered");
789 }
790 }
791 }
792
793 struct BarHarness(Caption, Pixels);
795
796 impl Render for BarHarness {
797 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
798 div()
799 .debug_selector(|| "bar".into())
800 .flex()
801 .w_full()
802 .h(self.1)
803 .justify_end()
804 .child(
805 CaptionButtons::new(self.1, test_labels())
806 .caption(self.0)
807 .preview("preview"),
808 )
809 }
810 }
811
812 #[gpui::test]
813 fn windows_buttons_and_glyphs_center_in_the_bar(cx: &mut TestAppContext) {
814 for height in [32., 40., 48., 56., 64.] {
815 let (_, cx) = cx.add_window_view(|_, _| BarHarness(Caption::Windows, px(height)));
816 cx.run_until_parked();
817 assert_centered(cx, (46., 32.), 10., false);
818 }
819 }
820
821 #[gpui::test]
822 fn linux_buttons_and_glyphs_center_in_the_bar(cx: &mut TestAppContext) {
823 for height in [32., 40., 48., 56., 64.] {
824 let (_, cx) = cx.add_window_view(|_, _| BarHarness(Caption::Linux, px(height)));
825 cx.run_until_parked();
826 assert_centered(cx, (32., 32.), 8., true);
827 }
828 }
829
830 #[gpui::test]
831 fn a_bar_shorter_than_the_windows_button_clamps_it(cx: &mut TestAppContext) {
832 let (_, cx) = cx.add_window_view(|_, _| BarHarness(Caption::Windows, px(28.)));
833 cx.run_until_parked();
834 assert_centered(cx, (46., 28.), 10., false);
835 }
836
837 #[test]
838 fn every_glyph_fills_its_box_symmetrically() {
839 for (glyph, stroke) in [(10., 1.), (8., 2.), (8., 1.5)] {
840 for button in [
841 CaptionButton::Minimize,
842 CaptionButton::Maximize,
843 CaptionButton::Restore,
844 ] {
845 let rects = glyph_rects(button, glyph, stroke);
846 let (mut left, mut top, mut right, mut bottom) = (glyph, glyph, 0_f32, 0_f32);
847 for rect in &rects {
848 left = left.min(f32::from(rect.origin.x));
849 top = top.min(f32::from(rect.origin.y));
850 right = right.max(f32::from(rect.origin.x + rect.size.width));
851 bottom = bottom.max(f32::from(rect.origin.y + rect.size.height));
852 }
853 assert_eq!((left, right), (0., glyph), "{button:?} spans the box");
854 assert_eq!(
855 (top + bottom) / 2.,
856 glyph / 2.,
857 "{button:?} is centered vertically"
858 );
859 if button != CaptionButton::Minimize {
860 assert_eq!((top, bottom), (0., glyph), "{button:?} fills the box");
861 }
862 }
863 }
864 assert!(glyph_rects(CaptionButton::Close, 10., 1.).is_empty());
866 }
867
868 #[test]
869 fn a_preview_shows_the_full_set_whatever_the_window_offers() {
870 assert_eq!(
871 preview_buttons(Caption::Windows),
872 [
873 CaptionButton::Minimize,
874 CaptionButton::Maximize,
875 CaptionButton::Close
876 ]
877 );
878 assert_eq!(preview_buttons(Caption::Linux).len(), 3);
879 assert!(preview_buttons(Caption::System).is_empty());
880 }
881
882 #[test]
883 fn windows_buttons_take_the_system_width_and_leave_open_space_before_them() {
884 assert_eq!(width_of(Caption::Windows, 3), 138.);
885 assert_eq!(trailing_for(width_of(Caption::Windows, 3)), 146.);
886 }
887
888 #[test]
889 fn a_system_title_bar_keeps_the_ordinary_inset() {
890 assert_eq!(width_of(Caption::System, 3), 0.);
891 assert_eq!(trailing_for(0.), 16.);
892 }
893
894 #[test]
895 fn linux_buttons_follow_what_the_window_manager_offers() {
896 assert_eq!(width_of(Caption::Linux, 1), 44.);
898 assert_eq!(width_of(Caption::Linux, 3), 32. * 3. + 4. * 2. + 12.);
899 }
900}