pinnacle_api/
snowcap.rs

1//! Integration with the
2//! [Snowcap](https://github.com/pinnacle-comp/pinnacle/tree/main/snowcap) widget system.
3//!
4//! Snowcap is a really-early-in-development widget system, designed for Pinnacle.
5//! This module contains preliminary widgets made with the system.
6
7use std::sync::{Arc, OnceLock};
8
9use indexmap::IndexMap;
10use snowcap_api::{
11    decoration::{DecorationHandle, NewDecorationError},
12    layer::{ExclusiveZone, KeyboardInteractivity, ZLayer},
13    widget::{
14        Alignment, Border, Color, Length, Padding, Program, Radius, WidgetDef,
15        button::{self, Button, Styles},
16        column::Column,
17        container::Container,
18        font::{Family, Font, Weight},
19        image::{Handle, Image},
20        input_region::InputRegion,
21        row::Row,
22        scrollable::Scrollable,
23        text::{self, Text},
24    },
25};
26use xkbcommon::xkb::Keysym;
27
28use crate::{
29    input::{BindInfoKind, Mod},
30    signal::SignalHandle,
31    window::WindowHandle,
32};
33
34/// A quit prompt.
35///
36/// When opened, pressing ENTER will quit the compositor.
37#[derive(Default, Clone, Debug)]
38pub struct QuitPrompt {
39    /// The radius of the prompt's corners.
40    pub border_radius: f32,
41    /// The thickness of the prompt border.
42    pub border_thickness: f32,
43    /// The color of the prompt background.
44    pub background_color: Color,
45    /// The color of the prompt border.
46    pub border_color: Color,
47    /// The font of the prompt.
48    pub font: Font,
49    /// The width of the prompt.
50    pub width: u32,
51    /// The height of the prompt.
52    pub height: u32,
53}
54
55impl Program for QuitPrompt {
56    type Message = ();
57
58    fn update(&mut self, _msg: Self::Message) {}
59
60    fn view(&self) -> WidgetDef<Self::Message> {
61        let widget = Container::new(Column::new_with_children([
62            Text::new("Quit Pinnacle?")
63                .style(
64                    text::Style::new()
65                        .font(self.font.clone().weight(Weight::Bold))
66                        .pixels(20.0),
67                )
68                .into(),
69            Text::new("").style(text::Style::new().pixels(8.0)).into(), // Spacing
70            Text::new("Press ENTER to confirm, or\nany other key to close this")
71                .style(text::Style::new().font(self.font.clone()).pixels(14.0))
72                .into(),
73        ]))
74        .width(Length::Fixed(self.width as f32))
75        .height(Length::Fixed(self.height as f32))
76        .vertical_alignment(Alignment::Center)
77        .horizontal_alignment(Alignment::Center)
78        .style(snowcap_api::widget::container::Style {
79            text_color: None,
80            background_color: Some(self.background_color),
81            border: Some(snowcap_api::widget::Border {
82                color: Some(self.border_color),
83                width: Some(self.border_thickness),
84                radius: Some(self.border_radius.into()),
85            }),
86        });
87
88        widget.into()
89    }
90}
91
92impl QuitPrompt {
93    /// Creates a quit prompt with sane defaults.
94    pub fn new() -> Self {
95        QuitPrompt {
96            border_radius: 12.0,
97            border_thickness: 6.0,
98            background_color: [0.15, 0.03, 0.1, 0.65].into(),
99            border_color: [0.8, 0.2, 0.4].into(),
100            font: Font::new_with_family(Family::Name("Ubuntu".into())),
101            width: 220,
102            height: 120,
103        }
104    }
105
106    /// Shows this quit prompt.
107    pub fn show(self) {
108        snowcap_api::layer::new_widget(
109            self,
110            None,
111            KeyboardInteractivity::Exclusive,
112            ExclusiveZone::Respect,
113            ZLayer::Overlay,
114        )
115        .unwrap()
116        .on_key_press(|handle, key, _mods| {
117            if key == Keysym::Return {
118                crate::pinnacle::quit();
119            } else {
120                handle.close();
121            }
122        });
123    }
124}
125
126/// A bindings overlay.
127#[derive(Default, Clone, Debug)]
128pub struct BindOverlay {
129    /// The radius of the overlay's corners.
130    pub border_radius: f32,
131    /// The thickness of the overlay border.
132    pub border_thickness: f32,
133    /// The color of the overlay background.
134    pub background_color: Color,
135    /// The color of the overlay border.
136    pub border_color: Color,
137    /// The font of the overlay.
138    pub font: Font,
139    /// The width of the overlay.
140    pub width: u32,
141    /// The height of the overlay.
142    pub height: u32,
143}
144
145impl Program for BindOverlay {
146    type Message = ();
147
148    fn update(&mut self, _msg: Self::Message) {}
149
150    fn view(&self) -> WidgetDef<Self::Message> {
151        #[derive(PartialEq, Eq, Hash)]
152        struct KeybindRepr {
153            mods: Mod,
154            key_name: String,
155            layer: Option<String>,
156        }
157
158        impl std::fmt::Display for KeybindRepr {
159            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
160                let mods = format_mods(self.mods);
161
162                let layer = self
163                    .layer
164                    .as_ref()
165                    .map(|layer| format!("[{layer}] "))
166                    .unwrap_or_default();
167
168                let bind = mods
169                    .as_deref()
170                    .into_iter()
171                    .chain([self.key_name.as_str()])
172                    .collect::<Vec<_>>()
173                    .join(" + ");
174                write!(f, "{layer}{bind}")
175            }
176        }
177
178        #[derive(PartialEq, Eq, Hash)]
179        struct MousebindRepr {
180            mods: Mod,
181            button_name: String,
182            layer: Option<String>,
183        }
184
185        impl std::fmt::Display for MousebindRepr {
186            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
187                let mods = format_mods(self.mods);
188
189                let layer = self
190                    .layer
191                    .as_ref()
192                    .map(|layer| format!("[{layer}] "))
193                    .unwrap_or_default();
194
195                let bind = mods
196                    .as_deref()
197                    .into_iter()
198                    .chain([self.button_name.as_str()])
199                    .collect::<Vec<_>>()
200                    .join(" + ");
201                write!(f, "{layer}{bind}")
202            }
203        }
204
205        #[derive(Default)]
206        struct GroupBinds {
207            /// keybinds to descriptions
208            keybinds: IndexMap<KeybindRepr, Vec<String>>,
209            /// mousebinds to descriptions
210            mousebinds: IndexMap<MousebindRepr, Vec<String>>,
211        }
212
213        let bind_infos = crate::input::bind_infos();
214
215        let mut groups = IndexMap::<String, GroupBinds>::new();
216
217        for bind_info in bind_infos {
218            let mods = bind_info.mods;
219            let group = bind_info.group;
220            let desc = bind_info.description;
221            let layer = bind_info.layer.name();
222
223            let group = groups.entry(group).or_default();
224
225            match bind_info.kind {
226                BindInfoKind::Key {
227                    key_code: _,
228                    xkb_name,
229                } => {
230                    let repr = KeybindRepr {
231                        mods,
232                        key_name: xkb_name,
233                        layer,
234                    };
235                    let descs = group.keybinds.entry(repr).or_default();
236                    if !desc.is_empty() {
237                        descs.push(desc);
238                    }
239                }
240                BindInfoKind::Mouse { button } => {
241                    let repr = MousebindRepr {
242                        mods,
243                        button_name: match button {
244                            crate::input::MouseButton::Left => "Mouse Left",
245                            crate::input::MouseButton::Right => "Mouse Right",
246                            crate::input::MouseButton::Middle => "Mouse Middle",
247                            crate::input::MouseButton::Side => "Mouse Side",
248                            crate::input::MouseButton::Extra => "Mouse Extra",
249                            crate::input::MouseButton::Forward => "Mouse Forward",
250                            crate::input::MouseButton::Back => "Mouse Back",
251                            crate::input::MouseButton::Other(_) => "Mouse Other",
252                        }
253                        .to_string(),
254                        layer,
255                    };
256                    let descs = group.mousebinds.entry(repr).or_default();
257                    if !desc.is_empty() {
258                        descs.push(desc);
259                    }
260                }
261            }
262        }
263
264        // List keybinds with no group last
265        if let Some(data) = groups.shift_remove("") {
266            groups.insert("".to_string(), data);
267        }
268
269        let sections = groups.into_iter().flat_map(|(group, data)| {
270            let group_title = Text::new(if !group.is_empty() { group } else { "Other".into() })
271                .style(
272                    text::Style::new()
273                        .font(self.font.clone().weight(Weight::Bold))
274                        .pixels(19.0),
275                );
276
277            let keybinds = data.keybinds.into_iter().map(|(key, descs)| {
278                if descs.is_empty() {
279                    WidgetDef::from(
280                        Text::new(key.to_string())
281                            .style(text::Style::new().font(self.font.clone())),
282                    )
283                } else if descs.len() == 1 {
284                    Row::new_with_children([
285                        Text::new(key.to_string())
286                            .width(Length::FillPortion(1))
287                            .style(text::Style::new().font(self.font.clone()))
288                            .into(),
289                        Text::new(descs[0].clone())
290                            .width(Length::FillPortion(2))
291                            .style(text::Style::new().font(self.font.clone()))
292                            .into(),
293                    ])
294                    .into()
295                } else {
296                    let mut children = Vec::<WidgetDef<()>>::new();
297                    children.push(
298                        Text::new(key.to_string() + ":")
299                            .style(text::Style::new().font(self.font.clone()))
300                            .into(),
301                    );
302
303                    for desc in descs {
304                        children.push(
305                            Text::new(format!("\t{desc}"))
306                                .style(text::Style::new().font(self.font.clone()))
307                                .into(),
308                        );
309                    }
310
311                    Column::new_with_children(children).into()
312                }
313            });
314
315            let mousebinds = data.mousebinds.into_iter().map(|(mouse, descs)| {
316                if descs.is_empty() {
317                    WidgetDef::from(
318                        Text::new(mouse.to_string())
319                            .style(text::Style::new().font(self.font.clone())),
320                    )
321                } else if descs.len() == 1 {
322                    Row::new_with_children([
323                        Text::new(mouse.to_string())
324                            .width(Length::FillPortion(1))
325                            .style(text::Style::new().font(self.font.clone()))
326                            .into(),
327                        Text::new(descs[0].clone())
328                            .width(Length::FillPortion(2))
329                            .style(text::Style::new().font(self.font.clone()))
330                            .into(),
331                    ])
332                    .into()
333                } else {
334                    let mut children = Vec::<WidgetDef<()>>::new();
335                    children.push(
336                        Text::new(mouse.to_string() + ":")
337                            .style(text::Style::new().font(self.font.clone()))
338                            .into(),
339                    );
340
341                    for desc in descs {
342                        children.push(
343                            Text::new(format!("\t{desc}"))
344                                .style(text::Style::new().font(self.font.clone()))
345                                .into(),
346                        );
347                    }
348
349                    Column::new_with_children(children).into()
350                }
351            });
352
353            let mut children = Vec::<WidgetDef<()>>::new();
354            children.push(group_title.into());
355            children.extend(keybinds);
356            children.extend(mousebinds);
357            children.push(Text::new("").style(text::Style::new().pixels(8.0)).into()); // Spacing because I haven't impl'd that yet
358
359            children
360        });
361
362        let scrollable = Scrollable::new(Column::new_with_children(sections))
363            .width(Length::Fill)
364            .height(Length::Fill);
365
366        let widget = Container::new(Column::new_with_children([
367            Text::new("Keybinds")
368                .style(
369                    text::Style::new()
370                        .font(self.font.clone().weight(Weight::Bold))
371                        .pixels(24.0),
372                )
373                .width(Length::Fill)
374                .into(),
375            Text::new("").style(text::Style::new().pixels(8.0)).into(), // Spacing
376            scrollable.into(),
377        ]))
378        .width(Length::Fixed(self.width as f32))
379        .height(Length::Fixed(self.height as f32))
380        .padding(Padding {
381            top: self.border_thickness + 10.0,
382            right: self.border_thickness + 10.0,
383            bottom: self.border_thickness + 10.0,
384            left: self.border_thickness + 10.0,
385        })
386        .vertical_alignment(Alignment::Center)
387        .horizontal_alignment(Alignment::Center)
388        .style(snowcap_api::widget::container::Style {
389            text_color: None,
390            background_color: Some(self.background_color),
391            border: Some(snowcap_api::widget::Border {
392                color: Some(self.border_color),
393                width: Some(self.border_thickness),
394                radius: Some(self.border_radius.into()),
395            }),
396        });
397
398        widget.into()
399    }
400}
401
402impl BindOverlay {
403    /// Creates the default bind overlay.
404    ///
405    /// Some of its characteristics can be changed by setting its fields.
406    pub fn new() -> Self {
407        BindOverlay {
408            border_radius: 12.0,
409            border_thickness: 6.0,
410            background_color: [0.15, 0.15, 0.225, 0.8].into(),
411            border_color: [0.4, 0.4, 0.7].into(),
412            font: Font::new_with_family(Family::Name("Ubuntu".into())),
413            width: 700,
414            height: 500,
415        }
416    }
417
418    /// Shows this bind overlay.
419    pub fn show(self) {
420        snowcap_api::layer::new_widget(
421            self,
422            None,
423            KeyboardInteractivity::Exclusive,
424            ExclusiveZone::Respect,
425            ZLayer::Top,
426        )
427        .unwrap()
428        .on_key_press(|handle, _key, _mods| {
429            handle.close();
430        });
431    }
432}
433
434fn format_mods(mods: Mod) -> Option<String> {
435    let mut parts = Vec::new();
436    if mods.contains(Mod::SUPER) {
437        parts.push("Super");
438    }
439    if mods.contains(Mod::CTRL) {
440        parts.push("Ctrl");
441    }
442    if mods.contains(Mod::ALT) {
443        parts.push("Alt");
444    }
445    if mods.contains(Mod::SHIFT) {
446        parts.push("Shift");
447    }
448    if mods.contains(Mod::ISO_LEVEL3_SHIFT) {
449        parts.push("ISO Level 3 Shift");
450    }
451    if mods.contains(Mod::ISO_LEVEL5_SHIFT) {
452        parts.push("ISO Level 5 Shift");
453    }
454
455    if parts.is_empty() {
456        None
457    } else {
458        Some(parts.join(" + "))
459    }
460}
461
462/// A message that the previous config crashed.
463#[derive(Default, Clone, Debug)]
464pub struct ConfigCrashedMessage {
465    /// The radius of the prompt's corners.
466    pub border_radius: f32,
467    /// The thickness of the prompt border.
468    pub border_thickness: f32,
469    /// The color of the prompt background.
470    pub background_color: Color,
471    /// The color of the prompt border.
472    pub border_color: Color,
473    /// The font of the prompt.
474    pub font: Font,
475    /// The width of the prompt.
476    pub width: u32,
477    /// The height of the prompt.
478    pub height: u32,
479    /// The error message.
480    pub message: String,
481}
482
483impl Program for ConfigCrashedMessage {
484    type Message = ();
485
486    fn update(&mut self, _msg: Self::Message) {}
487
488    fn view(&self) -> WidgetDef<Self::Message> {
489        let widget = Container::new(Column::new_with_children([
490            Text::new("Config crashed!")
491                .style(
492                    text::Style::new()
493                        .font(self.font.clone().weight(Weight::Bold))
494                        .pixels(20.0),
495                )
496                .into(),
497            Text::new("").style(text::Style::new().pixels(8.0)).into(), // Spacing
498            Text::new("The previous config crashed with the following error message:")
499                .style(text::Style::new().font(self.font.clone()).pixels(14.0))
500                .into(),
501            Text::new("").style(text::Style::new().pixels(8.0)).into(), // Spacing
502            Scrollable::new(
503                Text::new(&self.message)
504                    .style(text::Style::new().font(self.font.clone()).pixels(14.0)),
505            )
506            .width(Length::Fill)
507            .height(Length::Fill)
508            .into(),
509            Text::new("").style(text::Style::new().pixels(8.0)).into(), // Spacing
510            Text::new(
511                "ESCAPE/ENTER: Close this window. MOD + S: Bring up the bind overlay.\n\
512                    MOD + CTRL + R: Restart your config.",
513            )
514            .style(text::Style::new().font(self.font.clone()).pixels(14.0))
515            .into(),
516        ]))
517        .width(Length::Fixed(self.width as f32))
518        .height(Length::Fixed(self.height as f32))
519        .padding(Padding {
520            top: 16.0,
521            right: 16.0,
522            bottom: 16.0,
523            left: 16.0,
524        })
525        .vertical_alignment(Alignment::Center)
526        .horizontal_alignment(Alignment::Center)
527        .style(snowcap_api::widget::container::Style {
528            text_color: None,
529            background_color: Some(self.background_color),
530            border: Some(snowcap_api::widget::Border {
531                color: Some(self.border_color),
532                width: Some(self.border_thickness),
533                radius: Some(self.border_radius.into()),
534            }),
535        });
536
537        widget.into()
538    }
539}
540
541impl ConfigCrashedMessage {
542    /// Creates an error message.
543    pub fn new(message: impl std::fmt::Display) -> Self {
544        ConfigCrashedMessage {
545            border_radius: 12.0,
546            border_thickness: 6.0,
547            background_color: [0.15, 0.03, 0.1, 0.65].into(),
548            border_color: [0.8, 0.2, 0.4].into(),
549            font: Font::new_with_family(Family::Name("Ubuntu".into())),
550            width: 700,
551            height: 400,
552            message: message.to_string(),
553        }
554    }
555
556    /// Shows an error message.
557    pub fn show(self) {
558        snowcap_api::layer::new_widget(
559            self,
560            None,
561            KeyboardInteractivity::Exclusive,
562            ExclusiveZone::Respect,
563            ZLayer::Overlay,
564        )
565        .unwrap()
566        .on_key_press(|handle, key, _mods| {
567            if key == Keysym::Return || key == Keysym::Escape {
568                handle.close();
569            }
570        });
571    }
572}
573
574/// A border that shows window focus, with an optional titlebar.
575#[derive(Debug, Clone)]
576pub struct FocusBorder {
577    /// The window this border is decorating.
578    pub window: WindowHandle,
579    /// The thickness of the border, in pixels.
580    pub thickness: u32,
581    /// The color of the border when it's focused.
582    pub focused_color: Color,
583    /// The color of the border when it's unfocused.
584    pub unfocused_color: Color,
585    /// Whether the window this border surrounds is focused.
586    pub focused: bool,
587    /// Whether to draw a titlebar.
588    pub include_titlebar: bool,
589    /// The title of the window.
590    pub title: String,
591    /// The height of the titlebar.
592    pub titlebar_height: u32,
593}
594
595/// A message that changes a [`FocusBorder`].
596#[derive(Clone)]
597pub enum FocusBorderMessage {
598    /// Make this border focused or not.
599    SetFocused(bool),
600    /// Maximize the window this border decorates.
601    Maximize,
602    /// Close the window this border decorates.
603    Close,
604    /// The title changed.
605    TitleChanged(String),
606}
607
608impl FocusBorder {
609    /// Creates a new focus border without a titlebar.
610    pub fn new(window: &WindowHandle) -> Self {
611        Self {
612            window: window.clone(),
613            thickness: 4,
614            focused_color: Color::rgb(0.4, 0.15, 0.7),
615            unfocused_color: Color::rgb(0.15, 0.15, 0.15),
616            focused: window.focused(),
617            include_titlebar: false,
618            title: String::new(),
619            titlebar_height: 0,
620        }
621    }
622
623    /// Creates a new focus border with a titlebar.
624    pub fn new_with_titlebar(window: &WindowHandle) -> Self {
625        Self {
626            window: window.clone(),
627            thickness: 4,
628            focused_color: Color::rgb(0.4, 0.15, 0.7),
629            unfocused_color: Color::rgb(0.15, 0.15, 0.15),
630            focused: window.focused(),
631            include_titlebar: true,
632            title: window.title(),
633            titlebar_height: 16,
634        }
635    }
636
637    /// Decorates the window with this focus border.
638    pub fn decorate(self) -> Result<DecorationHandle<FocusBorderMessage>, NewDecorationError> {
639        let thickness = self.thickness;
640        let titlebar_height = self.titlebar_height;
641        let window = self.window.clone();
642
643        let border = snowcap_api::decoration::new_widget(
644            self,
645            window
646                .foreign_toplevel_list_identifier()
647                .unwrap_or_default(),
648            snowcap_api::decoration::Bounds {
649                left: thickness,
650                right: thickness,
651                top: if titlebar_height > 0 {
652                    thickness * 2 + titlebar_height
653                } else {
654                    thickness
655                },
656                bottom: thickness,
657            },
658            snowcap_api::decoration::Bounds {
659                left: thickness,
660                right: thickness,
661                top: if titlebar_height > 0 {
662                    thickness * 2 + titlebar_height
663                } else {
664                    thickness
665                },
666                bottom: thickness,
667            },
668            20,
669        )?;
670
671        let signal_holder = Arc::new(OnceLock::<SignalHandle>::new());
672        let signal_holder2 = Arc::new(OnceLock::<SignalHandle>::new());
673
674        // We use the foreign toplevel ID to tell if the window is alive
675        let signal =
676            crate::window::connect_signal(crate::signal::WindowSignal::Focused(Box::new({
677                let signal_holder = signal_holder.clone();
678                let signal_holder2 = signal_holder2.clone();
679                let window = window.clone();
680                let border = border.clone();
681                move |focused| {
682                    if window.foreign_toplevel_list_identifier().is_some() {
683                        border.send_message(FocusBorderMessage::SetFocused(&window == focused));
684                    } else {
685                        signal_holder.get().unwrap().disconnect();
686                        signal_holder2.get().unwrap().disconnect();
687                    }
688                }
689            })));
690
691        signal_holder.set(signal).unwrap();
692
693        let signal =
694            crate::window::connect_signal(crate::signal::WindowSignal::TitleChanged(Box::new({
695                let signal_holder = signal_holder.clone();
696                let signal_holder2 = signal_holder2.clone();
697                let window = window.clone();
698                let border = border.clone();
699                move |win, title| {
700                    if window.foreign_toplevel_list_identifier().is_some() {
701                        if &window == win {
702                            border.send_message(FocusBorderMessage::TitleChanged(title.into()));
703                        }
704                    } else {
705                        signal_holder.get().unwrap().disconnect();
706                        signal_holder2.get().unwrap().disconnect();
707                    }
708                }
709            })));
710
711        signal_holder2.set(signal).unwrap();
712
713        Ok(border)
714    }
715}
716
717const B: u32 = 0x000000ff;
718const T: u32 = 0x00000000;
719
720// don't ask lol
721#[rustfmt::skip]
722const EXIT_ICON: &[u32] = &[
723    T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,
724    T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,
725    T,T,B,B,B,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,B,B,B,T,T,
726    T,T,B,B,B,B,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,B,B,B,B,T,T,
727    T,T,B,B,B,B,B,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,B,B,B,B,B,T,T,
728    T,T,T,B,B,B,B,B,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,B,B,B,B,B,T,T,T,
729    T,T,T,T,B,B,B,B,B,T,T,T,T,T,T,T,T,T,T,T,T,T,T,B,B,B,B,B,T,T,T,T,
730    T,T,T,T,T,B,B,B,B,B,T,T,T,T,T,T,T,T,T,T,T,T,B,B,B,B,B,T,T,T,T,T,
731    T,T,T,T,T,T,B,B,B,B,B,T,T,T,T,T,T,T,T,T,T,B,B,B,B,B,T,T,T,T,T,T,
732    T,T,T,T,T,T,T,B,B,B,B,B,T,T,T,T,T,T,T,T,B,B,B,B,B,T,T,T,T,T,T,T,
733    T,T,T,T,T,T,T,T,B,B,B,B,B,T,T,T,T,T,T,B,B,B,B,B,T,T,T,T,T,T,T,T,
734    T,T,T,T,T,T,T,T,T,B,B,B,B,B,T,T,T,T,B,B,B,B,B,T,T,T,T,T,T,T,T,T,
735    T,T,T,T,T,T,T,T,T,T,B,B,B,B,B,T,T,B,B,B,B,B,T,T,T,T,T,T,T,T,T,T,
736    T,T,T,T,T,T,T,T,T,T,T,B,B,B,B,B,B,B,B,B,B,T,T,T,T,T,T,T,T,T,T,T,
737    T,T,T,T,T,T,T,T,T,T,T,T,B,B,B,B,B,B,B,B,T,T,T,T,T,T,T,T,T,T,T,T,
738    T,T,T,T,T,T,T,T,T,T,T,T,T,B,B,B,B,B,B,T,T,T,T,T,T,T,T,T,T,T,T,T,
739    T,T,T,T,T,T,T,T,T,T,T,T,T,B,B,B,B,B,B,T,T,T,T,T,T,T,T,T,T,T,T,T,
740    T,T,T,T,T,T,T,T,T,T,T,T,B,B,B,B,B,B,B,B,T,T,T,T,T,T,T,T,T,T,T,T,
741    T,T,T,T,T,T,T,T,T,T,T,B,B,B,B,B,B,B,B,B,B,T,T,T,T,T,T,T,T,T,T,T,
742    T,T,T,T,T,T,T,T,T,T,B,B,B,B,B,T,T,B,B,B,B,B,T,T,T,T,T,T,T,T,T,T,
743    T,T,T,T,T,T,T,T,T,B,B,B,B,B,T,T,T,T,B,B,B,B,B,T,T,T,T,T,T,T,T,T,
744    T,T,T,T,T,T,T,T,B,B,B,B,B,T,T,T,T,T,T,B,B,B,B,B,T,T,T,T,T,T,T,T,
745    T,T,T,T,T,T,T,B,B,B,B,B,T,T,T,T,T,T,T,T,B,B,B,B,B,T,T,T,T,T,T,T,
746    T,T,T,T,T,T,B,B,B,B,B,T,T,T,T,T,T,T,T,T,T,B,B,B,B,B,T,T,T,T,T,T,
747    T,T,T,T,T,B,B,B,B,B,T,T,T,T,T,T,T,T,T,T,T,T,B,B,B,B,B,T,T,T,T,T,
748    T,T,T,T,B,B,B,B,B,T,T,T,T,T,T,T,T,T,T,T,T,T,T,B,B,B,B,B,T,T,T,T,
749    T,T,T,B,B,B,B,B,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,B,B,B,B,B,T,T,T,
750    T,T,B,B,B,B,B,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,B,B,B,B,B,T,T,
751    T,T,B,B,B,B,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,B,B,B,B,T,T,
752    T,T,B,B,B,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,B,B,B,T,T,
753    T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,
754    T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,
755];
756
757#[rustfmt::skip]
758const MAXIMIZE_ICON: &[u32] = &[
759    T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,
760    T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,
761    T,T,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,T,T,
762    T,T,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,T,T,
763    T,T,B,B,B,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,B,B,B,T,T,
764    T,T,B,B,B,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,B,B,B,T,T,
765    T,T,B,B,B,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,B,B,B,T,T,
766    T,T,B,B,B,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,B,B,B,T,T,
767    T,T,B,B,B,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,B,B,B,T,T,
768    T,T,B,B,B,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,B,B,B,T,T,
769    T,T,B,B,B,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,B,B,B,T,T,
770    T,T,B,B,B,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,B,B,B,T,T,
771    T,T,B,B,B,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,B,B,B,T,T,
772    T,T,B,B,B,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,B,B,B,T,T,
773    T,T,B,B,B,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,B,B,B,T,T,
774    T,T,B,B,B,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,B,B,B,T,T,
775    T,T,B,B,B,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,B,B,B,T,T,
776    T,T,B,B,B,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,B,B,B,T,T,
777    T,T,B,B,B,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,B,B,B,T,T,
778    T,T,B,B,B,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,B,B,B,T,T,
779    T,T,B,B,B,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,B,B,B,T,T,
780    T,T,B,B,B,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,B,B,B,T,T,
781    T,T,B,B,B,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,B,B,B,T,T,
782    T,T,B,B,B,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,B,B,B,T,T,
783    T,T,B,B,B,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,B,B,B,T,T,
784    T,T,B,B,B,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,B,B,B,T,T,
785    T,T,B,B,B,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,B,B,B,T,T,
786    T,T,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,T,T,
787    T,T,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,T,T,
788    T,T,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,T,T,
789    T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,
790    T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,
791];
792
793impl Program for FocusBorder {
794    type Message = FocusBorderMessage;
795
796    fn update(&mut self, msg: Self::Message) {
797        match msg {
798            FocusBorderMessage::SetFocused(focused) => {
799                self.focused = focused;
800            }
801            FocusBorderMessage::Maximize => {
802                self.window.toggle_maximized();
803            }
804            FocusBorderMessage::Close => {
805                self.window.close();
806            }
807            FocusBorderMessage::TitleChanged(title) => {
808                self.title = title;
809            }
810        }
811    }
812
813    fn view(&self) -> WidgetDef<Self::Message> {
814        let mut row = Column::new();
815
816        if self.include_titlebar {
817            let titlebar = Container::new(
818                Row::new_with_children([
819                    Text::new(&self.title)
820                        .style(text::Style {
821                            color: None,
822                            pixels: Some(self.titlebar_height as f32 - 2.0),
823                            font: None,
824                        })
825                        .width(Length::Fill)
826                        .into(),
827                    Button::new(
828                        Image::new(Handle::Rgba {
829                            width: 32,
830                            height: 32,
831                            bytes: MAXIMIZE_ICON
832                                .iter()
833                                .flat_map(|rgba| rgba.to_be_bytes())
834                                .collect(),
835                        })
836                        .width(Length::Fill)
837                        .height(Length::Fill),
838                    )
839                    .width(Length::Fixed((self.titlebar_height) as f32))
840                    .height(Length::Fixed((self.titlebar_height) as f32))
841                    .padding(Padding::from(4.0))
842                    .style(
843                        Styles {
844                            active: Some(button::Style::new().background_color({
845                                let mut color = if self.focused {
846                                    self.focused_color
847                                } else {
848                                    self.unfocused_color
849                                };
850                                color.red += 0.3;
851                                color.green += 0.3;
852                                color.blue += 0.3;
853                                color
854                            })),
855                            hovered: Some(button::Style::new().background_color({
856                                let mut color = if self.focused {
857                                    self.focused_color
858                                } else {
859                                    self.unfocused_color
860                                };
861                                color.red += 0.4;
862                                color.green += 0.4;
863                                color.blue += 0.4;
864                                color
865                            })),
866                            pressed: Some(button::Style::new().background_color({
867                                let mut color = if self.focused {
868                                    self.focused_color
869                                } else {
870                                    self.unfocused_color
871                                };
872                                color.red += 0.5;
873                                color.green += 0.5;
874                                color.blue += 0.5;
875                                color
876                            })),
877                            disabled: None,
878                        }
879                        .border(Border {
880                            color: None,
881                            width: None,
882                            radius: Some(Radius::from(1000.0)),
883                        }),
884                    )
885                    .on_press(FocusBorderMessage::Maximize)
886                    .into(),
887                    Button::new(
888                        Image::new(Handle::Rgba {
889                            width: 32,
890                            height: 32,
891                            bytes: EXIT_ICON
892                                .iter()
893                                .flat_map(|rgba| rgba.to_be_bytes())
894                                .collect(),
895                        })
896                        .width(Length::Fill)
897                        .height(Length::Fill),
898                    )
899                    .width(Length::Fixed((self.titlebar_height) as f32))
900                    .height(Length::Fixed((self.titlebar_height) as f32))
901                    .padding(Padding::from(4.0))
902                    .style(
903                        Styles {
904                            active: Some(button::Style::new().background_color({
905                                let mut color = if self.focused {
906                                    self.focused_color
907                                } else {
908                                    self.unfocused_color
909                                };
910                                color.red += 0.3;
911                                color.green += 0.3;
912                                color.blue += 0.3;
913                                color
914                            })),
915                            hovered: Some(button::Style::new().background_color({
916                                let mut color = if self.focused {
917                                    self.focused_color
918                                } else {
919                                    self.unfocused_color
920                                };
921                                color.red += 0.4;
922                                color.green += 0.4;
923                                color.blue += 0.4;
924                                color
925                            })),
926                            pressed: Some(button::Style::new().background_color({
927                                let mut color = if self.focused {
928                                    self.focused_color
929                                } else {
930                                    self.unfocused_color
931                                };
932                                color.red += 0.5;
933                                color.green += 0.5;
934                                color.blue += 0.5;
935                                color
936                            })),
937                            disabled: None,
938                        }
939                        .border(Border {
940                            color: None,
941                            width: None,
942                            radius: Some(Radius::from(1000.0)),
943                        }),
944                    )
945                    .on_press(FocusBorderMessage::Close)
946                    .into(),
947                ])
948                .item_alignment(Alignment::Start)
949                .spacing(4.0)
950                .width(Length::Fill)
951                .height(Length::Fixed(self.titlebar_height as f32)),
952            )
953            .style(snowcap_api::widget::container::Style {
954                text_color: None,
955                background_color: Some(if self.focused {
956                    self.focused_color
957                } else {
958                    self.unfocused_color
959                }),
960                border: None,
961            })
962            .padding(Padding {
963                top: self.thickness as f32,
964                right: self.thickness as f32,
965                bottom: 0.0,
966                left: self.thickness as f32,
967            });
968
969            row = row.push(titlebar);
970        }
971
972        let focus_border = Container::new(
973            InputRegion::new(false, Row::new())
974                .width(Length::Fill)
975                .height(Length::Fill),
976        )
977        .width(Length::Fill)
978        .height(Length::Fill)
979        .padding(Padding::from(self.thickness as f32))
980        .style(
981            snowcap_api::widget::container::Style::new()
982                .background_color(Color::from([0.0, 0.0, 0.0, 0.0]))
983                .border(snowcap_api::widget::Border {
984                    color: Some(if self.focused {
985                        self.focused_color
986                    } else {
987                        self.unfocused_color
988                    }),
989                    width: Some(self.thickness as f32),
990                    radius: Some(Radius::default()),
991                }),
992        );
993
994        row = row.push(focus_border);
995
996        row.into()
997    }
998}