1use 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#[derive(Default, Clone, Debug)]
23pub struct QuitPrompt {
24 pub border_radius: f32,
26 pub border_thickness: f32,
28 pub background_color: Color,
30 pub border_color: Color,
32 pub font: Font,
34 pub width: u32,
36 pub height: u32,
38}
39
40impl QuitPrompt {
41 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 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(), 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#[derive(Default, Clone, Debug)]
99pub struct BindOverlay {
100 pub border_radius: f32,
102 pub border_thickness: f32,
104 pub background_color: Color,
106 pub border_color: Color,
108 pub font: Font,
110 pub width: u32,
112 pub height: u32,
114}
115
116impl BindOverlay {
117 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 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: IndexMap<KeybindRepr, Vec<String>>,
192 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 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()); 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(), 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#[derive(Default, Clone, Debug)]
411pub struct ConfigCrashedMessage {
412 pub border_radius: f32,
414 pub border_thickness: f32,
416 pub background_color: Color,
418 pub border_color: Color,
420 pub font: Font,
422 pub width: u32,
424 pub height: u32,
426}
427
428impl ConfigCrashedMessage {
429 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 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(), 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(), 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(), 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}