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 indexmap::IndexMap;
8use snowcap_api::{
9    layer::{ExclusiveZone, KeyboardInteractivity, ZLayer},
10    widget::{
11        font::{Family, Font, Weight},
12        Alignment, Color, Column, Container, Length, Padding, Row, Scrollable, Text, WidgetDef,
13    },
14};
15use xkbcommon::xkb::Keysym;
16
17use crate::input::{BindInfoKind, Mod};
18
19/// A quit prompt.
20///
21/// When opened, pressing ENTER will quit the compositor.
22#[derive(Default, Clone, Debug)]
23pub struct QuitPrompt {
24    /// The radius of the prompt's corners.
25    pub border_radius: f32,
26    /// The thickness of the prompt border.
27    pub border_thickness: f32,
28    /// The color of the prompt background.
29    pub background_color: Color,
30    /// The color of the prompt border.
31    pub border_color: Color,
32    /// The font of the prompt.
33    pub font: Font,
34    /// The width of the prompt.
35    pub width: u32,
36    /// The height of the prompt.
37    pub height: u32,
38}
39
40impl QuitPrompt {
41    /// Creates a quit prompt with sane defaults.
42    pub fn new() -> Self {
43        QuitPrompt {
44            border_radius: 12.0,
45            border_thickness: 6.0,
46            background_color: [0.15, 0.03, 0.1, 0.65].into(),
47            border_color: [0.8, 0.2, 0.4].into(),
48            font: Font::new_with_family(Family::Name("Ubuntu".into())),
49            width: 220,
50            height: 120,
51        }
52    }
53
54    /// Shows this quit prompt.
55    pub fn show(&self) {
56        let widget = Container::new(Column::new_with_children([
57            Text::new("Quit Pinnacle?")
58                .font(self.font.clone().weight(Weight::Bold))
59                .size(20.0)
60                .into(),
61            Text::new("").size(8.0).into(), // Spacing because I haven't impl'd that yet
62            Text::new("Press ENTER to confirm, or\nany other key to close this")
63                .font(self.font.clone())
64                .size(14.0)
65                .into(),
66        ]))
67        .width(Length::Fill)
68        .height(Length::Fill)
69        .vertical_alignment(Alignment::Center)
70        .horizontal_alignment(Alignment::Center)
71        .border_radius(self.border_radius)
72        .border_thickness(self.border_thickness)
73        .border_color(self.border_color)
74        .background_color(self.background_color);
75
76        snowcap_api::layer::Layer
77            .new_widget(
78                widget,
79                self.width,
80                self.height,
81                None,
82                KeyboardInteractivity::Exclusive,
83                ExclusiveZone::Respect,
84                ZLayer::Overlay,
85            )
86            .unwrap()
87            .on_key_press(|handle, key, _mods| {
88                if key == Keysym::Return {
89                    crate::pinnacle::quit();
90                } else {
91                    handle.close();
92                }
93            });
94    }
95}
96
97/// A bindings overlay.
98#[derive(Default, Clone, Debug)]
99pub struct BindOverlay {
100    /// The radius of the overlay's corners.
101    pub border_radius: f32,
102    /// The thickness of the overlay border.
103    pub border_thickness: f32,
104    /// The color of the overlay background.
105    pub background_color: Color,
106    /// The color of the overlay border.
107    pub border_color: Color,
108    /// The font of the overlay.
109    pub font: Font,
110    /// The width of the overlay.
111    pub width: u32,
112    /// The height of the overlay.
113    pub height: u32,
114}
115
116impl BindOverlay {
117    /// Creates the default bind overlay.
118    ///
119    /// Some of its characteristics can be changed by setting its fields.
120    pub fn new() -> Self {
121        BindOverlay {
122            border_radius: 12.0,
123            border_thickness: 6.0,
124            background_color: [0.15, 0.15, 0.225, 0.8].into(),
125            border_color: [0.4, 0.4, 0.7].into(),
126            font: Font::new_with_family(Family::Name("Ubuntu".into())),
127            width: 700,
128            height: 500,
129        }
130    }
131
132    /// Shows this bind overlay.
133    pub fn show(&self) {
134        #[derive(PartialEq, Eq, Hash)]
135        struct KeybindRepr {
136            mods: Mod,
137            key_name: String,
138            layer: Option<String>,
139        }
140
141        impl std::fmt::Display for KeybindRepr {
142            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
143                let mods = format_mods(self.mods);
144
145                let layer = self
146                    .layer
147                    .as_ref()
148                    .map(|layer| format!("[{layer}] "))
149                    .unwrap_or_default();
150
151                let bind = mods
152                    .as_deref()
153                    .into_iter()
154                    .chain([self.key_name.as_str()])
155                    .collect::<Vec<_>>()
156                    .join(" + ");
157                write!(f, "{layer}{bind}")
158            }
159        }
160
161        #[derive(PartialEq, Eq, Hash)]
162        struct MousebindRepr {
163            mods: Mod,
164            button_name: String,
165            layer: Option<String>,
166        }
167
168        impl std::fmt::Display for MousebindRepr {
169            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
170                let mods = format_mods(self.mods);
171
172                let layer = self
173                    .layer
174                    .as_ref()
175                    .map(|layer| format!("[{layer}] "))
176                    .unwrap_or_default();
177
178                let bind = mods
179                    .as_deref()
180                    .into_iter()
181                    .chain([self.button_name.as_str()])
182                    .collect::<Vec<_>>()
183                    .join(" + ");
184                write!(f, "{layer}{bind}")
185            }
186        }
187
188        #[derive(Default)]
189        struct GroupBinds {
190            /// keybinds to descriptions
191            keybinds: IndexMap<KeybindRepr, Vec<String>>,
192            /// mousebinds to descriptions
193            mousebinds: IndexMap<MousebindRepr, Vec<String>>,
194        }
195
196        let bind_infos = crate::input::bind_infos();
197
198        let mut groups = IndexMap::<String, GroupBinds>::new();
199
200        for bind_info in bind_infos {
201            let mods = bind_info.mods;
202            let group = bind_info.group;
203            let desc = bind_info.description;
204            let layer = bind_info.layer.name();
205
206            let group = groups.entry(group).or_default();
207
208            match bind_info.kind {
209                BindInfoKind::Key {
210                    key_code: _,
211                    xkb_name,
212                } => {
213                    let repr = KeybindRepr {
214                        mods,
215                        key_name: xkb_name,
216                        layer,
217                    };
218                    let descs = group.keybinds.entry(repr).or_default();
219                    if !desc.is_empty() {
220                        descs.push(desc);
221                    }
222                }
223                BindInfoKind::Mouse { button } => {
224                    let repr = MousebindRepr {
225                        mods,
226                        button_name: match button {
227                            crate::input::MouseButton::Left => "Mouse Left",
228                            crate::input::MouseButton::Right => "Mouse Right",
229                            crate::input::MouseButton::Middle => "Mouse Middle",
230                            crate::input::MouseButton::Side => "Mouse Side",
231                            crate::input::MouseButton::Extra => "Mouse Extra",
232                            crate::input::MouseButton::Forward => "Mouse Forward",
233                            crate::input::MouseButton::Back => "Mouse Back",
234                            crate::input::MouseButton::Other(_) => "Mouse Other",
235                        }
236                        .to_string(),
237                        layer,
238                    };
239                    let descs = group.mousebinds.entry(repr).or_default();
240                    if !desc.is_empty() {
241                        descs.push(desc);
242                    }
243                }
244            }
245        }
246
247        // List keybinds with no group last
248        if let Some(data) = groups.shift_remove("") {
249            groups.insert("".to_string(), data);
250        }
251
252        let sections = groups.into_iter().flat_map(|(group, data)| {
253            let group_title = Text::new(if !group.is_empty() { group } else { "Other".into() })
254                .font(self.font.clone().weight(Weight::Bold))
255                .size(19.0);
256
257            let keybinds = data.keybinds.into_iter().map(|(key, descs)| {
258                if descs.is_empty() {
259                    WidgetDef::from(Text::new(key.to_string()).font(self.font.clone()))
260                } else if descs.len() == 1 {
261                    Row::new_with_children([
262                        Text::new(key.to_string())
263                            .width(Length::FillPortion(1))
264                            .font(self.font.clone())
265                            .into(),
266                        Text::new(descs[0].clone())
267                            .width(Length::FillPortion(2))
268                            .font(self.font.clone())
269                            .into(),
270                    ])
271                    .into()
272                } else {
273                    let mut children = Vec::<WidgetDef>::new();
274                    children.push(
275                        Text::new(key.to_string() + ":")
276                            .font(self.font.clone())
277                            .into(),
278                    );
279
280                    for desc in descs {
281                        children.push(
282                            Text::new(format!("\t{}", desc))
283                                .font(self.font.clone())
284                                .into(),
285                        );
286                    }
287
288                    Column::new_with_children(children).into()
289                }
290            });
291
292            let mousebinds = data.mousebinds.into_iter().map(|(mouse, descs)| {
293                if descs.is_empty() {
294                    WidgetDef::from(Text::new(mouse.to_string()).font(self.font.clone()))
295                } else if descs.len() == 1 {
296                    Row::new_with_children([
297                        Text::new(mouse.to_string())
298                            .width(Length::FillPortion(1))
299                            .font(self.font.clone())
300                            .into(),
301                        Text::new(descs[0].clone())
302                            .width(Length::FillPortion(2))
303                            .font(self.font.clone())
304                            .into(),
305                    ])
306                    .into()
307                } else {
308                    let mut children = Vec::<WidgetDef>::new();
309                    children.push(
310                        Text::new(mouse.to_string() + ":")
311                            .font(self.font.clone())
312                            .into(),
313                    );
314
315                    for desc in descs {
316                        children.push(
317                            Text::new(format!("\t{}", desc))
318                                .font(self.font.clone())
319                                .into(),
320                        );
321                    }
322
323                    Column::new_with_children(children).into()
324                }
325            });
326
327            let mut children = Vec::<WidgetDef>::new();
328            children.push(group_title.into());
329            children.extend(keybinds);
330            children.extend(mousebinds);
331            children.push(Text::new("").size(8.0).into()); // Spacing because I haven't impl'd that yet
332
333            children
334        });
335
336        let scrollable = Scrollable::new(Column::new_with_children(sections))
337            .width(Length::Fill)
338            .height(Length::Fill);
339
340        let widget = Container::new(Column::new_with_children([
341            Text::new("Keybinds")
342                .font(self.font.clone().weight(Weight::Bold))
343                .size(24.0)
344                .width(Length::Fill)
345                .into(),
346            Text::new("").size(8.0).into(), // Spacing because I haven't impl'd that yet
347            scrollable.into(),
348        ]))
349        .width(Length::Fill)
350        .height(Length::Fill)
351        .padding(Padding {
352            top: 16.0,
353            right: 16.0,
354            bottom: 16.0,
355            left: 16.0,
356        })
357        .vertical_alignment(Alignment::Center)
358        .horizontal_alignment(Alignment::Center)
359        .border_radius(self.border_radius)
360        .border_thickness(self.border_thickness)
361        .border_color(self.border_color)
362        .background_color(self.background_color);
363
364        snowcap_api::layer::Layer
365            .new_widget(
366                widget,
367                self.width,
368                self.height,
369                None,
370                KeyboardInteractivity::Exclusive,
371                ExclusiveZone::Respect,
372                ZLayer::Top,
373            )
374            .unwrap()
375            .on_key_press(|handle, _key, _mods| {
376                handle.close();
377            });
378    }
379}
380
381fn format_mods(mods: Mod) -> Option<String> {
382    let mut parts = Vec::new();
383    if mods.contains(Mod::SUPER) {
384        parts.push("Super");
385    }
386    if mods.contains(Mod::CTRL) {
387        parts.push("Ctrl");
388    }
389    if mods.contains(Mod::ALT) {
390        parts.push("Alt");
391    }
392    if mods.contains(Mod::SHIFT) {
393        parts.push("Shift");
394    }
395    if mods.contains(Mod::ISO_LEVEL3_SHIFT) {
396        parts.push("ISO Level 3 Shift");
397    }
398    if mods.contains(Mod::ISO_LEVEL5_SHIFT) {
399        parts.push("ISO Level 5 Shift");
400    }
401
402    if parts.is_empty() {
403        None
404    } else {
405        Some(parts.join(" + "))
406    }
407}
408
409/// A message that the previous config crashed.
410#[derive(Default, Clone, Debug)]
411pub struct ConfigCrashedMessage {
412    /// The radius of the prompt's corners.
413    pub border_radius: f32,
414    /// The thickness of the prompt border.
415    pub border_thickness: f32,
416    /// The color of the prompt background.
417    pub background_color: Color,
418    /// The color of the prompt border.
419    pub border_color: Color,
420    /// The font of the prompt.
421    pub font: Font,
422    /// The width of the prompt.
423    pub width: u32,
424    /// The height of the prompt.
425    pub height: u32,
426}
427
428impl ConfigCrashedMessage {
429    /// Creates an error message.
430    pub fn new() -> Self {
431        ConfigCrashedMessage {
432            border_radius: 12.0,
433            border_thickness: 6.0,
434            background_color: [0.15, 0.03, 0.1, 0.65].into(),
435            border_color: [0.8, 0.2, 0.4].into(),
436            font: Font::new_with_family(Family::Name("Ubuntu".into())),
437            width: 700,
438            height: 400,
439        }
440    }
441
442    /// Shows an error message.
443    pub fn show(&self, message: impl std::fmt::Display) {
444        let widget = Container::new(Column::new_with_children([
445            Text::new("Config crashed!")
446                .font(self.font.clone().weight(Weight::Bold))
447                .size(20.0)
448                .into(),
449            Text::new("").size(8.0).into(), // Spacing because I haven't impl'd that yet
450            Text::new("The previous config crashed with the following error message:")
451                .font(self.font.clone())
452                .size(14.0)
453                .into(),
454            Text::new("").size(8.0).into(), // Spacing because I haven't impl'd that yet
455            Scrollable::new(Text::new(message).font(self.font.clone()).size(14.0))
456                .width(Length::Fill)
457                .height(Length::Fill)
458                .into(),
459            Text::new("").size(8.0).into(), // Spacing because I haven't impl'd that yet
460            Text::new(
461                "ESCAPE/ENTER: Close this window. MOD + S: Bring up the bind overlay.\n\
462                    MOD + CTRL + R: Restart your config.",
463            )
464            .font(self.font.clone())
465            .size(14.0)
466            .into(),
467        ]))
468        .width(Length::Fill)
469        .height(Length::Fill)
470        .padding(Padding {
471            top: 16.0,
472            right: 16.0,
473            bottom: 16.0,
474            left: 16.0,
475        })
476        .vertical_alignment(Alignment::Center)
477        .horizontal_alignment(Alignment::Center)
478        .border_radius(self.border_radius)
479        .border_thickness(self.border_thickness)
480        .border_color(self.border_color)
481        .background_color(self.background_color);
482
483        snowcap_api::layer::Layer
484            .new_widget(
485                widget,
486                self.width,
487                self.height,
488                None,
489                KeyboardInteractivity::Exclusive,
490                ExclusiveZone::Respect,
491                ZLayer::Overlay,
492            )
493            .unwrap()
494            .on_key_press(|handle, key, _mods| {
495                if key == Keysym::Return || key == Keysym::Escape {
496                    handle.close();
497                }
498            });
499    }
500}