1use 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#[derive(Default, Clone, Debug)]
38pub struct QuitPrompt {
39 pub border_radius: f32,
41 pub border_thickness: f32,
43 pub background_color: Color,
45 pub border_color: Color,
47 pub font: Font,
49 pub width: u32,
51 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(), 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 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 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#[derive(Default, Clone, Debug)]
128pub struct BindOverlay {
129 pub border_radius: f32,
131 pub border_thickness: f32,
133 pub background_color: Color,
135 pub border_color: Color,
137 pub font: Font,
139 pub width: u32,
141 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: IndexMap<KeybindRepr, Vec<String>>,
209 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 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()); 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(), 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 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 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#[derive(Default, Clone, Debug)]
464pub struct ConfigCrashedMessage {
465 pub border_radius: f32,
467 pub border_thickness: f32,
469 pub background_color: Color,
471 pub border_color: Color,
473 pub font: Font,
475 pub width: u32,
477 pub height: u32,
479 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(), 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(), 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(), 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 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 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#[derive(Debug, Clone)]
576pub struct FocusBorder {
577 pub window: WindowHandle,
579 pub thickness: u32,
581 pub focused_color: Color,
583 pub unfocused_color: Color,
585 pub focused: bool,
587 pub include_titlebar: bool,
589 pub title: String,
591 pub titlebar_height: u32,
593}
594
595#[derive(Clone)]
597pub enum FocusBorderMessage {
598 SetFocused(bool),
600 Maximize,
602 Close,
604 TitleChanged(String),
606}
607
608impl FocusBorder {
609 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 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 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 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#[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}