Skip to main content

chrome_lab/
chrome_lab.rs

1use iced::widget::{button, checkbox, column, container, pick_list, row, scrollable, text};
2use iced::{Color, Element, Length, Size, Subscription, Task, application, window};
3
4use iced_window_chrome::{
5    ChromeSettings, MacosTitlebarSeparatorStyle, WindowCornerPreference, WindowsBackdrop,
6    WindowsCapabilities,
7};
8
9const WINDOW_CORNER_CHOICES: [WindowCornerChoice; 4] = [
10    WindowCornerChoice::SystemDefault,
11    WindowCornerChoice::Square,
12    WindowCornerChoice::Round,
13    WindowCornerChoice::RoundSmall,
14];
15
16const WINDOWS_COLOR_CHOICES: [WindowsColorChoice; 6] = [
17    WindowsColorChoice::SystemDefault,
18    WindowsColorChoice::Crimson,
19    WindowsColorChoice::Emerald,
20    WindowsColorChoice::Indigo,
21    WindowsColorChoice::Amber,
22    WindowsColorChoice::White,
23];
24
25const WINDOWS_BACKDROP_CHOICES: [WindowsBackdropChoice; 5] = [
26    WindowsBackdropChoice::SystemDefault,
27    WindowsBackdropChoice::Off,
28    WindowsBackdropChoice::Mica,
29    WindowsBackdropChoice::Acrylic,
30    WindowsBackdropChoice::MicaAlt,
31];
32
33const MACOS_TITLEBAR_HEIGHT_CHOICES: [MacosTitlebarHeightChoice; 6] = [
34    MacosTitlebarHeightChoice::SystemDefault,
35    MacosTitlebarHeightChoice::Compact,
36    MacosTitlebarHeightChoice::Regular,
37    MacosTitlebarHeightChoice::Tall,
38    MacosTitlebarHeightChoice::Hero,
39    MacosTitlebarHeightChoice::Huge,
40];
41
42const MACOS_TRAFFIC_LIGHT_OFFSET_CHOICES: [MacosTrafficLightOffsetChoice; 7] = [
43    MacosTrafficLightOffsetChoice::SystemDefault,
44    MacosTrafficLightOffsetChoice::LiftLarge,
45    MacosTrafficLightOffsetChoice::LiftSmall,
46    MacosTrafficLightOffsetChoice::Aligned,
47    MacosTrafficLightOffsetChoice::DropSmall,
48    MacosTrafficLightOffsetChoice::DropMedium,
49    MacosTrafficLightOffsetChoice::DropLarge,
50];
51
52const MACOS_SEPARATOR_STYLE_CHOICES: [MacosSeparatorStyleChoice; 4] = [
53    MacosSeparatorStyleChoice::SystemDefault,
54    MacosSeparatorStyleChoice::Hidden,
55    MacosSeparatorStyleChoice::Line,
56    MacosSeparatorStyleChoice::Shadow,
57];
58
59fn main() -> iced::Result {
60    application(ChromeLab::boot, update, view)
61        .title(title)
62        .window(window::Settings {
63            size: Size::new(900.0, 760.0),
64            ..window::Settings::default()
65        })
66        .subscription(subscription)
67        .run()
68}
69
70fn title(_: &ChromeLab) -> String {
71    String::from("chrome-lab")
72}
73
74#[derive(Debug, Clone)]
75enum Message {
76    Chrome(iced_window_chrome::Event),
77    ApplyNow,
78    OpenWindow,
79    Ignore,
80    WindowsCaption(bool),
81    WindowsBorder(bool),
82    WindowsClose(bool),
83    WindowsMinimize(bool),
84    WindowsMaximize(bool),
85    WindowsCorner(WindowCornerChoice),
86    WindowsBorderColor(WindowsColorChoice),
87    WindowsTitleBackgroundColor(WindowsColorChoice),
88    WindowsTitleTextColor(WindowsColorChoice),
89    WindowsBackdrop(WindowsBackdropChoice),
90    MacosTitlebar(bool),
91    MacosTitle(bool),
92    MacosTrafficLights(bool),
93    MacosTransparent(bool),
94    MacosFullsize(bool),
95    MacosTitlebarHeight(MacosTitlebarHeightChoice),
96    MacosTrafficLightOffset(MacosTrafficLightOffsetChoice),
97    MacosSeparatorStyle(MacosSeparatorStyleChoice),
98    LinuxDecorations(bool),
99    LinuxClose(bool),
100    LinuxMinimize(bool),
101    LinuxMaximize(bool),
102}
103
104#[derive(Debug, Clone)]
105struct ChromeLab {
106    chrome: ChromeSettings,
107    windows_capabilities: Option<WindowsCapabilities>,
108}
109
110impl ChromeLab {
111    fn boot() -> (Self, Task<Message>) {
112        let state = Self {
113            chrome: ChromeSettings::default(),
114            windows_capabilities: iced_window_chrome::current_windows_capabilities(),
115        };
116
117        (
118            state.clone(),
119            iced_window_chrome::apply_to_latest(state.chrome.clone()),
120        )
121    }
122}
123
124fn update(state: &mut ChromeLab, message: Message) -> Task<Message> {
125    match message {
126        Message::Chrome(event) => iced_window_chrome::handle(event),
127        Message::ApplyNow => reapply(state),
128        Message::OpenWindow => {
129            let (_, task) = window::open(window::Settings {
130                size: Size::new(640.0, 420.0),
131                ..window::Settings::default()
132            });
133
134            task.map(|_| Message::Ignore)
135        }
136        Message::Ignore => Task::none(),
137        Message::WindowsCaption(value) => {
138            state.chrome.windows.caption = value;
139            reapply(state)
140        }
141        Message::WindowsBorder(value) => {
142            state.chrome.windows.border = value;
143            reapply(state)
144        }
145        Message::WindowsClose(value) => {
146            state.chrome.windows.buttons.close = value;
147            reapply(state)
148        }
149        Message::WindowsMinimize(value) => {
150            state.chrome.windows.buttons.minimize = value;
151            reapply(state)
152        }
153        Message::WindowsMaximize(value) => {
154            state.chrome.windows.buttons.maximize = value;
155            reapply(state)
156        }
157        Message::WindowsCorner(value) => {
158            state.chrome.windows.corner_preference = value.into_setting();
159            reapply(state)
160        }
161        Message::WindowsBorderColor(value) => {
162            state.chrome.windows.border_color = value.into_setting();
163            reapply(state)
164        }
165        Message::WindowsTitleBackgroundColor(value) => {
166            state.chrome.windows.title_background_color = value.into_setting();
167            reapply(state)
168        }
169        Message::WindowsTitleTextColor(value) => {
170            state.chrome.windows.title_text_color = value.into_setting();
171            reapply(state)
172        }
173        Message::WindowsBackdrop(value) => {
174            state.chrome.windows.backdrop = value.into_setting();
175            reapply(state)
176        }
177        Message::MacosTitlebar(value) => {
178            state.chrome.macos.titlebar = value;
179            reapply(state)
180        }
181        Message::MacosTitle(value) => {
182            state.chrome.macos.title = value;
183            reapply(state)
184        }
185        Message::MacosTrafficLights(value) => {
186            state.chrome.macos.traffic_lights = value;
187            reapply(state)
188        }
189        Message::MacosTransparent(value) => {
190            state.chrome.macos.titlebar_transparent = value;
191            reapply(state)
192        }
193        Message::MacosFullsize(value) => {
194            state.chrome.macos.fullsize_content_view = value;
195            reapply(state)
196        }
197        Message::MacosTitlebarHeight(value) => {
198            state.chrome.macos.titlebar_height = value.into_setting();
199            reapply(state)
200        }
201        Message::MacosTrafficLightOffset(value) => {
202            state.chrome.macos.traffic_light_offset_y = value.into_setting();
203            reapply(state)
204        }
205        Message::MacosSeparatorStyle(value) => {
206            state.chrome.macos.titlebar_separator_style = value.into_setting();
207            reapply(state)
208        }
209        Message::LinuxDecorations(value) => {
210            state.chrome.linux.decorations = value;
211            reapply(state)
212        }
213        Message::LinuxClose(value) => {
214            state.chrome.linux.buttons.close = value;
215            reapply(state)
216        }
217        Message::LinuxMinimize(value) => {
218            state.chrome.linux.buttons.minimize = value;
219            reapply(state)
220        }
221        Message::LinuxMaximize(value) => {
222            state.chrome.linux.buttons.maximize = value;
223            reapply(state)
224        }
225    }
226}
227
228fn subscription(state: &ChromeLab) -> Subscription<Message> {
229    iced_window_chrome::subscription(state.chrome.clone()).map(Message::Chrome)
230}
231
232fn view(state: &ChromeLab) -> Element<'_, Message> {
233    let windows_visuals_supported = state
234        .windows_capabilities
235        .map(WindowsCapabilities::supports_dwm_visuals)
236        .unwrap_or(false);
237    let windows_backdrop_supported = state
238        .windows_capabilities
239        .map(WindowsCapabilities::supports_system_backdrop)
240        .unwrap_or(false);
241
242    let corner_row: Element<'_, Message> = if windows_visuals_supported {
243        picker_row(
244            "Corner rounding",
245            pick_list(
246                WINDOW_CORNER_CHOICES,
247                Some(WindowCornerChoice::from_setting(
248                    state.chrome.windows.corner_preference,
249                )),
250                Message::WindowsCorner,
251            )
252            .width(180)
253            .into(),
254        )
255    } else {
256        picker_row(
257            "Corner rounding (Win11)",
258            locked_picker(WindowCornerChoice::from_setting(
259                state.chrome.windows.corner_preference,
260            )),
261        )
262    };
263
264    let border_color_row: Element<'_, Message> = if windows_visuals_supported {
265        picker_row(
266            "Border color",
267            pick_list(
268                WINDOWS_COLOR_CHOICES,
269                Some(WindowsColorChoice::from_setting(
270                    state.chrome.windows.border_color,
271                )),
272                Message::WindowsBorderColor,
273            )
274            .width(180)
275            .into(),
276        )
277    } else {
278        picker_row(
279            "Border color (Win11)",
280            locked_picker(WindowsColorChoice::from_setting(
281                state.chrome.windows.border_color,
282            )),
283        )
284    };
285
286    let title_background_row: Element<'_, Message> = if windows_visuals_supported {
287        picker_row(
288            "Title background",
289            pick_list(
290                WINDOWS_COLOR_CHOICES,
291                Some(WindowsColorChoice::from_setting(
292                    state.chrome.windows.title_background_color,
293                )),
294                Message::WindowsTitleBackgroundColor,
295            )
296            .width(180)
297            .into(),
298        )
299    } else {
300        picker_row(
301            "Title background (Win11)",
302            locked_picker(WindowsColorChoice::from_setting(
303                state.chrome.windows.title_background_color,
304            )),
305        )
306    };
307
308    let title_text_row: Element<'_, Message> = if windows_visuals_supported {
309        picker_row(
310            "Title text",
311            pick_list(
312                WINDOWS_COLOR_CHOICES,
313                Some(WindowsColorChoice::from_setting(
314                    state.chrome.windows.title_text_color,
315                )),
316                Message::WindowsTitleTextColor,
317            )
318            .width(180)
319            .into(),
320        )
321    } else {
322        picker_row(
323            "Title text (Win11)",
324            locked_picker(WindowsColorChoice::from_setting(
325                state.chrome.windows.title_text_color,
326            )),
327        )
328    };
329
330    let backdrop_row: Element<'_, Message> = if windows_backdrop_supported {
331        picker_row(
332            "Backdrop material",
333            pick_list(
334                WINDOWS_BACKDROP_CHOICES,
335                Some(WindowsBackdropChoice::from_setting(
336                    state.chrome.windows.backdrop,
337                )),
338                Message::WindowsBackdrop,
339            )
340            .width(180)
341            .into(),
342        )
343    } else {
344        picker_row(
345            "Backdrop material (22621+)",
346            locked_picker(WindowsBackdropChoice::from_setting(
347                state.chrome.windows.backdrop,
348            )),
349        )
350    };
351
352    let windows = column![
353        text("Windows").size(24),
354        text(windows_version_label(state.windows_capabilities)),
355        checkbox(state.chrome.windows.caption)
356            .label("Caption")
357            .on_toggle(Message::WindowsCaption),
358        checkbox(state.chrome.windows.border)
359            .label("Border")
360            .on_toggle(Message::WindowsBorder),
361        checkbox(state.chrome.windows.buttons.close)
362            .label("Close button")
363            .on_toggle(Message::WindowsClose),
364        checkbox(state.chrome.windows.buttons.minimize)
365            .label("Minimize button")
366            .on_toggle(Message::WindowsMinimize),
367        checkbox(state.chrome.windows.buttons.maximize)
368            .label("Maximize button")
369            .on_toggle(Message::WindowsMaximize),
370        corner_row,
371        border_color_row,
372        title_background_row,
373        title_text_row,
374        backdrop_row,
375    ]
376    .spacing(12);
377
378    let macos = column![
379        text("macOS").size(24),
380        checkbox(state.chrome.macos.titlebar)
381            .label("Titlebar")
382            .on_toggle(Message::MacosTitlebar),
383        checkbox(state.chrome.macos.title)
384            .label("Title text")
385            .on_toggle(Message::MacosTitle),
386        checkbox(state.chrome.macos.traffic_lights)
387            .label("Traffic lights")
388            .on_toggle(Message::MacosTrafficLights),
389        checkbox(state.chrome.macos.titlebar_transparent)
390            .label("Transparent titlebar")
391            .on_toggle(Message::MacosTransparent),
392        checkbox(state.chrome.macos.fullsize_content_view)
393            .label("Full-size content view")
394            .on_toggle(Message::MacosFullsize),
395        picker_row(
396            "Separator",
397            pick_list(
398                MACOS_SEPARATOR_STYLE_CHOICES,
399                Some(MacosSeparatorStyleChoice::from_setting(
400                    state.chrome.macos.titlebar_separator_style
401                )),
402                Message::MacosSeparatorStyle,
403            )
404            .width(180)
405            .into(),
406        ),
407        if state.chrome.macos.titlebar || state.chrome.macos.traffic_lights {
408            picker_row(
409                "Titlebar height",
410                pick_list(
411                    MACOS_TITLEBAR_HEIGHT_CHOICES,
412                    Some(MacosTitlebarHeightChoice::from_setting(
413                        state.chrome.macos.titlebar_height,
414                    )),
415                    Message::MacosTitlebarHeight,
416                )
417                .width(180)
418                .into(),
419            )
420        } else {
421            picker_row(
422                "Titlebar height",
423                locked_picker(MacosTitlebarHeightChoice::from_setting(
424                    state.chrome.macos.titlebar_height,
425                )),
426            )
427        },
428        if state.chrome.macos.titlebar || state.chrome.macos.traffic_lights {
429            picker_row(
430                "Traffic light offset",
431                pick_list(
432                    MACOS_TRAFFIC_LIGHT_OFFSET_CHOICES,
433                    Some(MacosTrafficLightOffsetChoice::from_setting(
434                        state.chrome.macos.traffic_light_offset_y,
435                    )),
436                    Message::MacosTrafficLightOffset,
437                )
438                .width(180)
439                .into(),
440            )
441        } else {
442            picker_row(
443                "Traffic light offset",
444                locked_picker(MacosTrafficLightOffsetChoice::from_setting(
445                    state.chrome.macos.traffic_light_offset_y,
446                )),
447            )
448        },
449    ]
450    .spacing(12);
451
452    let linux = column![
453        text("Linux").size(24),
454        text("X11 only"),
455        checkbox(state.chrome.linux.decorations)
456            .label("Decorations")
457            .on_toggle(Message::LinuxDecorations),
458        checkbox(state.chrome.linux.buttons.close)
459            .label("Close button")
460            .on_toggle(Message::LinuxClose),
461        checkbox(state.chrome.linux.buttons.minimize)
462            .label("Minimize button")
463            .on_toggle(Message::LinuxMinimize),
464        checkbox(state.chrome.linux.buttons.maximize)
465            .label("Maximize button")
466            .on_toggle(Message::LinuxMaximize),
467    ]
468    .spacing(12);
469
470    let controls = row![
471        button("Apply to latest window").on_press(Message::ApplyNow),
472        button("Open extra window").on_press(Message::OpenWindow),
473    ]
474    .spacing(12);
475
476    let platform_section: Element<'_, Message> = if cfg!(target_os = "windows") {
477        windows.width(Length::Fill).into()
478    } else if cfg!(target_os = "macos") {
479        macos.width(Length::Fill).into()
480    } else if cfg!(target_os = "linux") {
481        linux.width(Length::Fill).into()
482    } else {
483        column![
484            text("Unsupported platform").size(24),
485            text("This example currently targets Windows, macOS, and Linux."),
486        ]
487        .spacing(12)
488        .width(Length::Fill)
489        .into()
490    };
491
492    let content = column![
493        text("iced-window-chrome").size(34),
494        controls,
495        platform_section,
496    ]
497    .spacing(24)
498    .padding(24);
499
500    container(scrollable(content).width(Length::Fill).height(Length::Fill))
501        .width(Length::Fill)
502        .height(Length::Fill)
503        .into()
504}
505
506fn reapply(state: &ChromeLab) -> Task<Message> {
507    iced_window_chrome::apply_to_latest(state.chrome.clone())
508}
509
510fn picker_row<'a, Message: 'a>(
511    label: &'a str,
512    control: Element<'a, Message>,
513) -> Element<'a, Message> {
514    row![text(label).width(Length::Fill), control]
515        .spacing(12)
516        .into()
517}
518
519fn locked_picker<'a, Message: Clone + 'a>(value: impl ToString) -> Element<'a, Message> {
520    button(
521        row![text(value.to_string()).width(Length::Fill), text("v")]
522            .spacing(8)
523            .width(Length::Fill),
524    )
525    .width(180)
526    .on_press_maybe(None)
527    .into()
528}
529
530fn windows_version_label(capabilities: Option<WindowsCapabilities>) -> String {
531    capabilities
532        .map(|capabilities| format!("Windows {}", capabilities.version))
533        .unwrap_or_else(|| String::from("Windows version unavailable"))
534}
535
536#[derive(Debug, Clone, Copy, PartialEq, Eq)]
537enum WindowCornerChoice {
538    SystemDefault,
539    Square,
540    Round,
541    RoundSmall,
542}
543
544impl WindowCornerChoice {
545    fn from_setting(value: Option<WindowCornerPreference>) -> Self {
546        match value {
547            Some(WindowCornerPreference::DoNotRound) => Self::Square,
548            Some(WindowCornerPreference::Round) => Self::Round,
549            Some(WindowCornerPreference::RoundSmall) => Self::RoundSmall,
550            Some(WindowCornerPreference::Default) | None => Self::SystemDefault,
551        }
552    }
553
554    fn into_setting(self) -> Option<WindowCornerPreference> {
555        match self {
556            Self::SystemDefault => None,
557            Self::Square => Some(WindowCornerPreference::DoNotRound),
558            Self::Round => Some(WindowCornerPreference::Round),
559            Self::RoundSmall => Some(WindowCornerPreference::RoundSmall),
560        }
561    }
562}
563
564impl std::fmt::Display for WindowCornerChoice {
565    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
566        f.write_str(match self {
567            Self::SystemDefault => "System default",
568            Self::Square => "Square",
569            Self::Round => "Round",
570            Self::RoundSmall => "Round small",
571        })
572    }
573}
574
575#[derive(Debug, Clone, Copy, PartialEq, Eq)]
576enum WindowsColorChoice {
577    SystemDefault,
578    Crimson,
579    Emerald,
580    Indigo,
581    Amber,
582    White,
583}
584
585impl WindowsColorChoice {
586    fn from_setting(value: Option<Color>) -> Self {
587        match value.map(Color::into_rgba8) {
588            Some([184, 50, 88, 255]) => Self::Crimson,
589            Some([20, 184, 166, 255]) => Self::Emerald,
590            Some([99, 102, 241, 255]) => Self::Indigo,
591            Some([245, 158, 11, 255]) => Self::Amber,
592            Some([255, 255, 255, 255]) => Self::White,
593            _ => Self::SystemDefault,
594        }
595    }
596
597    fn into_setting(self) -> Option<Color> {
598        match self {
599            Self::SystemDefault => None,
600            Self::Crimson => Some(Color::from_rgb8(184, 50, 88)),
601            Self::Emerald => Some(Color::from_rgb8(20, 184, 166)),
602            Self::Indigo => Some(Color::from_rgb8(99, 102, 241)),
603            Self::Amber => Some(Color::from_rgb8(245, 158, 11)),
604            Self::White => Some(Color::from_rgb8(255, 255, 255)),
605        }
606    }
607}
608
609impl std::fmt::Display for WindowsColorChoice {
610    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
611        f.write_str(match self {
612            Self::SystemDefault => "System default",
613            Self::Crimson => "Crimson",
614            Self::Emerald => "Emerald",
615            Self::Indigo => "Indigo",
616            Self::Amber => "Amber",
617            Self::White => "White",
618        })
619    }
620}
621
622#[derive(Debug, Clone, Copy, PartialEq, Eq)]
623enum WindowsBackdropChoice {
624    SystemDefault,
625    Off,
626    Mica,
627    Acrylic,
628    MicaAlt,
629}
630
631impl WindowsBackdropChoice {
632    fn from_setting(value: Option<WindowsBackdrop>) -> Self {
633        match value {
634            Some(WindowsBackdrop::None) => Self::Off,
635            Some(WindowsBackdrop::Mica) => Self::Mica,
636            Some(WindowsBackdrop::Acrylic) => Self::Acrylic,
637            Some(WindowsBackdrop::MicaAlt) => Self::MicaAlt,
638            None => Self::SystemDefault,
639        }
640    }
641
642    fn into_setting(self) -> Option<WindowsBackdrop> {
643        match self {
644            Self::SystemDefault => None,
645            Self::Off => Some(WindowsBackdrop::None),
646            Self::Mica => Some(WindowsBackdrop::Mica),
647            Self::Acrylic => Some(WindowsBackdrop::Acrylic),
648            Self::MicaAlt => Some(WindowsBackdrop::MicaAlt),
649        }
650    }
651}
652
653impl std::fmt::Display for WindowsBackdropChoice {
654    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
655        f.write_str(match self {
656            Self::SystemDefault => "System default",
657            Self::Off => "Off",
658            Self::Mica => "Mica",
659            Self::Acrylic => "Acrylic",
660            Self::MicaAlt => "Mica Alt",
661        })
662    }
663}
664
665#[derive(Debug, Clone, Copy, PartialEq, Eq)]
666enum MacosTitlebarHeightChoice {
667    SystemDefault,
668    Compact,
669    Regular,
670    Tall,
671    Hero,
672    Huge,
673}
674
675impl MacosTitlebarHeightChoice {
676    fn from_setting(value: Option<f64>) -> Self {
677        match value {
678            Some(value) if approx_eq(value, 28.0) => Self::Compact,
679            Some(value) if approx_eq(value, 36.0) => Self::Regular,
680            Some(value) if approx_eq(value, 48.0) => Self::Tall,
681            Some(value) if approx_eq(value, 60.0) => Self::Hero,
682            Some(value) if approx_eq(value, 72.0) => Self::Huge,
683            _ => Self::SystemDefault,
684        }
685    }
686
687    fn into_setting(self) -> Option<f64> {
688        match self {
689            Self::SystemDefault => None,
690            Self::Compact => Some(28.0),
691            Self::Regular => Some(36.0),
692            Self::Tall => Some(48.0),
693            Self::Hero => Some(60.0),
694            Self::Huge => Some(72.0),
695        }
696    }
697}
698
699impl std::fmt::Display for MacosTitlebarHeightChoice {
700    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
701        f.write_str(match self {
702            Self::SystemDefault => "System default",
703            Self::Compact => "28 pt",
704            Self::Regular => "36 pt",
705            Self::Tall => "48 pt",
706            Self::Hero => "60 pt",
707            Self::Huge => "72 pt",
708        })
709    }
710}
711
712#[derive(Debug, Clone, Copy, PartialEq, Eq)]
713enum MacosTrafficLightOffsetChoice {
714    SystemDefault,
715    LiftLarge,
716    LiftSmall,
717    Aligned,
718    DropSmall,
719    DropMedium,
720    DropLarge,
721}
722
723impl MacosTrafficLightOffsetChoice {
724    fn from_setting(value: Option<f64>) -> Self {
725        match value {
726            Some(value) if approx_eq(value, -12.0) => Self::LiftLarge,
727            Some(value) if approx_eq(value, -6.0) => Self::LiftSmall,
728            Some(value) if approx_eq(value, 0.0) => Self::Aligned,
729            Some(value) if approx_eq(value, 6.0) => Self::DropSmall,
730            Some(value) if approx_eq(value, 12.0) => Self::DropMedium,
731            Some(value) if approx_eq(value, 18.0) => Self::DropLarge,
732            _ => Self::SystemDefault,
733        }
734    }
735
736    fn into_setting(self) -> Option<f64> {
737        match self {
738            Self::SystemDefault => None,
739            Self::LiftLarge => Some(-12.0),
740            Self::LiftSmall => Some(-6.0),
741            Self::Aligned => Some(0.0),
742            Self::DropSmall => Some(6.0),
743            Self::DropMedium => Some(12.0),
744            Self::DropLarge => Some(18.0),
745        }
746    }
747}
748
749impl std::fmt::Display for MacosTrafficLightOffsetChoice {
750    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
751        f.write_str(match self {
752            Self::SystemDefault => "System default",
753            Self::LiftLarge => "-12 pt",
754            Self::LiftSmall => "-6 pt",
755            Self::Aligned => "0 pt",
756            Self::DropSmall => "+6 pt",
757            Self::DropMedium => "+12 pt",
758            Self::DropLarge => "+18 pt",
759        })
760    }
761}
762
763#[derive(Debug, Clone, Copy, PartialEq, Eq)]
764enum MacosSeparatorStyleChoice {
765    SystemDefault,
766    Hidden,
767    Line,
768    Shadow,
769}
770
771impl MacosSeparatorStyleChoice {
772    fn from_setting(value: Option<MacosTitlebarSeparatorStyle>) -> Self {
773        match value {
774            Some(MacosTitlebarSeparatorStyle::None) => Self::Hidden,
775            Some(MacosTitlebarSeparatorStyle::Line) => Self::Line,
776            Some(MacosTitlebarSeparatorStyle::Shadow) => Self::Shadow,
777            Some(MacosTitlebarSeparatorStyle::Automatic) | None => Self::SystemDefault,
778        }
779    }
780
781    fn into_setting(self) -> Option<MacosTitlebarSeparatorStyle> {
782        match self {
783            Self::SystemDefault => None,
784            Self::Hidden => Some(MacosTitlebarSeparatorStyle::None),
785            Self::Line => Some(MacosTitlebarSeparatorStyle::Line),
786            Self::Shadow => Some(MacosTitlebarSeparatorStyle::Shadow),
787        }
788    }
789}
790
791impl std::fmt::Display for MacosSeparatorStyleChoice {
792    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
793        f.write_str(match self {
794            Self::SystemDefault => "System default",
795            Self::Hidden => "Hidden",
796            Self::Line => "Line",
797            Self::Shadow => "Shadow",
798        })
799    }
800}
801
802fn approx_eq(left: f64, right: f64) -> bool {
803    (left - right).abs() < 0.001
804}