1use crate::geom::LogicalRect;
23use crate::monitor::MonitorId;
24use crate::window::{WindowDesc, WindowId};
25#[cfg(any(target_os = "windows", desktop_unix, test))]
26use crate::{PhysicalRect, PhysicalSize};
27use crate::{ShellBackend, ShellCaps, ShellError};
28
29#[derive(Clone, Copy, Debug, Default, PartialEq)]
31#[non_exhaustive]
32pub enum WindowRole<'a> {
33 #[default]
36 Toplevel,
37 Panel(PanelDesc<'a>),
40 Popup(PopupDesc),
43}
44
45#[derive(Clone, Copy, Debug, Default, PartialEq)]
59pub struct PanelDesc<'a> {
60 pub namespace: &'a str,
64 pub monitor: Option<MonitorId>,
71 pub layer: PanelLayer,
73 pub anchor: PanelAnchor,
76 pub reserve: Reserve,
78 pub margins: Margins,
81 pub keyboard: PanelKeyboard,
83}
84
85#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
87pub enum PanelLayer {
88 Background,
90 Bottom,
92 #[default]
94 Top,
95 Overlay,
102}
103
104bitflags::bitflags! {
105 #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
107 pub struct PanelAnchor: u8 {
108 const TOP = 1 << 0;
110 const BOTTOM = 1 << 1;
112 const LEFT = 1 << 2;
114 const RIGHT = 1 << 3;
116 }
117}
118
119#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
128pub enum Reserve {
129 #[default]
133 None,
134 OwnExtent,
144 IgnoreOthers,
148}
149
150impl PanelDesc<'_> {
151 #[must_use]
155 pub fn reserved_edge(&self) -> Option<PanelAnchor> {
156 let vertical = PanelAnchor::LEFT | PanelAnchor::RIGHT;
157 let horizontal = PanelAnchor::TOP | PanelAnchor::BOTTOM;
158 [
159 (PanelAnchor::TOP, vertical),
160 (PanelAnchor::BOTTOM, vertical),
161 (PanelAnchor::LEFT, horizontal),
162 (PanelAnchor::RIGHT, horizontal),
163 ]
164 .into_iter()
165 .find(|&(edge, beside)| self.anchor == edge || self.anchor == edge | beside)
166 .map(|(edge, _)| edge)
167 }
168}
169
170#[derive(Clone, Copy, Debug, Default, PartialEq)]
172pub struct Margins {
173 pub top: f64,
175 pub right: f64,
177 pub bottom: f64,
179 pub left: f64,
181}
182
183#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
185pub enum PanelKeyboard {
186 #[default]
188 None,
189 OnDemand,
191 Exclusive,
203}
204
205#[derive(Clone, Copy, Debug, PartialEq)]
207pub struct PopupDesc {
208 pub parent: WindowId,
211 pub anchor: LogicalRect,
214 pub placement: PopupPlacement,
217 pub input: PopupInput,
219}
220
221#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
223pub enum PopupInput {
224 #[default]
228 Passthrough,
229 Pointer,
232 Grab,
244}
245
246pub(crate) fn escape_dismisses(popup: Option<PopupInput>, key: &crate::ShellEvent) -> bool {
261 popup == Some(PopupInput::Grab)
262 && matches!(
263 key,
264 crate::ShellEvent::Key {
265 keysym: crcbl_core::input::Keysym::ESCAPE,
266 state: crcbl_core::input::ButtonState::Pressed,
267 repeat: false,
268 ..
269 }
270 )
271}
272
273pub(crate) fn check(
280 desc: &WindowDesc<'_>,
281 caps: ShellCaps,
282 backend: ShellBackend,
283 has_monitor: impl Fn(MonitorId) -> bool,
284) -> Result<(), ShellError> {
285 let unsupported = |what| Err(ShellError::Unsupported { backend, what });
286 if desc.transparent && !caps.contains(ShellCaps::TRANSPARENT) {
287 return unsupported("transparent windows");
288 }
289 match desc.role {
290 WindowRole::Toplevel => Ok(()),
291 WindowRole::Panel(panel) => check_panel(&panel, caps, backend, has_monitor),
292 WindowRole::Popup(_) if !caps.contains(ShellCaps::POPUP) => unsupported("popup windows"),
293 WindowRole::Popup(_) => Ok(()),
294 }
295}
296
297fn check_panel(
302 panel: &PanelDesc<'_>,
303 caps: ShellCaps,
304 backend: ShellBackend,
305 has_monitor: impl Fn(MonitorId) -> bool,
306) -> Result<(), ShellError> {
307 let unsupported = |what| Err(ShellError::Unsupported { backend, what });
308 if !caps.contains(ShellCaps::PANEL) {
309 return unsupported("panel windows");
310 }
311 if panel.reserve == Reserve::OwnExtent && panel.reserved_edge().is_none() {
312 return unsupported("a reserved panel that is not held against one edge");
313 }
314 match panel.monitor {
315 Some(monitor) if !has_monitor(monitor) => Err(ShellError::NoSuchMonitor(monitor.0)),
316 _ => Ok(()),
317 }
318}
319
320pub(crate) fn check_set_panel(
330 window: WindowId,
331 was: Option<PanelDesc<'_>>,
332 panel: &PanelDesc<'_>,
333 caps: ShellCaps,
334 backend: ShellBackend,
335 has_monitor: impl Fn(MonitorId) -> bool,
336) -> Result<(), ShellError> {
337 let Some(was) = was else {
338 return Err(not_a_panel(window));
339 };
340 if panel.namespace != was.namespace {
341 return Err(ShellError::InvalidDescriptor(
342 "set_panel: a panel's namespace is fixed when it is created".to_string(),
343 ));
344 }
345 if panel.monitor != was.monitor {
346 return Err(ShellError::InvalidDescriptor(
347 "set_panel: a panel's monitor is fixed when it is created".to_string(),
348 ));
349 }
350 check_panel(panel, caps, backend, has_monitor)
351}
352
353pub(crate) fn not_a_panel(window: WindowId) -> ShellError {
356 ShellError::InvalidDescriptor(format!("set_panel: {window:?} is not a panel"))
357}
358
359#[derive(Clone, Debug, PartialEq)]
362pub(crate) struct KeptPanel {
363 namespace: String,
364 desc: PanelDesc<'static>,
367}
368
369impl KeptPanel {
370 pub(crate) fn new(desc: &PanelDesc<'_>) -> Self {
371 Self {
372 namespace: desc.namespace.to_string(),
373 desc: PanelDesc {
374 namespace: "",
375 ..*desc
376 },
377 }
378 }
379
380 pub(crate) fn desc(&self) -> PanelDesc<'_> {
382 PanelDesc {
383 namespace: &self.namespace,
384 ..self.desc
385 }
386 }
387}
388
389#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
391pub enum PopupPlacement {
392 #[default]
394 Below,
395 Above,
397 Left,
399 Right,
401}
402
403#[cfg(any(target_os = "windows", target_os = "macos", desktop_unix, test))]
409#[derive(Clone, Copy, Debug, PartialEq)]
410pub(crate) struct Placed {
411 pub x: f64,
412 pub y: f64,
413 pub width: f64,
414 pub height: f64,
415}
416
417pub(crate) fn not_a_popup(window: WindowId) -> ShellError {
427 ShellError::InvalidDescriptor(format!("reposition_popup: {window:?} is not a popup"))
428}
429
430#[cfg(any(target_os = "windows", target_os = "macos", desktop_unix, test))]
431pub(crate) fn place_popup(
432 anchor: Placed,
433 (width, height): (f64, f64),
434 placement: PopupPlacement,
435 area: Placed,
436) -> (f64, f64) {
437 let (left, top) = (anchor.x, anchor.y);
438 let (right, bottom) = (left + anchor.width, top + anchor.height);
439 let (area_right, area_bottom) = (area.x + area.width, area.y + area.height);
440 let side = |near: f64, far: f64, extent: f64, low: f64, high: f64| {
442 if near >= low && near + extent <= high {
443 near
444 } else if far >= low && far + extent <= high {
445 far
446 } else {
447 near
448 }
449 };
450 let slide = |start: f64, extent: f64, low: f64, high: f64| start.min(high - extent).max(low);
451 match placement {
452 PopupPlacement::Below => (
453 slide(left, width, area.x, area_right),
454 side(bottom, top - height, height, area.y, area_bottom),
455 ),
456 PopupPlacement::Above => (
457 slide(left, width, area.x, area_right),
458 side(top - height, bottom, height, area.y, area_bottom),
459 ),
460 PopupPlacement::Left => (
461 side(left - width, right, width, area.x, area_right),
462 slide(top, height, area.y, area_bottom),
463 ),
464 PopupPlacement::Right => (
465 side(right, left - width, width, area.x, area_right),
466 slide(top, height, area.y, area_bottom),
467 ),
468 }
469}
470
471#[cfg(any(target_os = "windows", desktop_unix, test))]
472#[must_use]
476pub(crate) fn popup_rect(
477 anchor: PhysicalRect,
478 size: PhysicalSize,
479 placement: PopupPlacement,
480 area: PhysicalRect,
481) -> PhysicalRect {
482 let placed = |rect: PhysicalRect| Placed {
483 x: f64::from(rect.x),
484 y: f64::from(rect.y),
485 width: f64::from(rect.width),
486 height: f64::from(rect.height),
487 };
488 let (x, y) = place_popup(
489 placed(anchor),
490 (f64::from(size.width), f64::from(size.height)),
491 placement,
492 placed(area),
493 );
494 #[expect(
497 clippy::cast_possible_truncation,
498 reason = "clamped into i32's range before the cast"
499 )]
500 let narrow = |value: f64| value.clamp(f64::from(i32::MIN), f64::from(i32::MAX)) as i32;
501 PhysicalRect::new(narrow(x), narrow(y), size.width, size.height)
502}
503
504#[cfg(any(target_os = "windows", desktop_unix, test))]
505#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
508pub(crate) struct Insets {
509 pub top: i32,
511 pub right: i32,
513 pub bottom: i32,
515 pub left: i32,
517}
518
519#[cfg(any(target_os = "windows", desktop_unix, test))]
520#[must_use]
525pub(crate) fn panel_rect(
526 area: PhysicalRect,
527 anchor: PanelAnchor,
528 size: PhysicalSize,
529 insets: Insets,
530) -> PhysicalRect {
531 let (x, width) = place_on_axis(
532 (area.x, area.width),
533 (
534 anchor.contains(PanelAnchor::LEFT),
535 anchor.contains(PanelAnchor::RIGHT),
536 ),
537 (insets.left, insets.right),
538 size.width,
539 );
540 let (y, height) = place_on_axis(
541 (area.y, area.height),
542 (
543 anchor.contains(PanelAnchor::TOP),
544 anchor.contains(PanelAnchor::BOTTOM),
545 ),
546 (insets.top, insets.bottom),
547 size.height,
548 );
549 PhysicalRect::new(x, y, width, height)
550}
551
552#[cfg(any(target_os = "windows", desktop_unix, test))]
556fn place_on_axis(
557 (area_start, area_length): (i32, u32),
558 (at_start, at_end): (bool, bool),
559 (inset_start, inset_end): (i32, i32),
560 length: u32,
561) -> (i32, u32) {
562 let area_length = i64::from(area_length);
563 let length = i64::from(length);
564 let (offset, length) = match (at_start, at_end) {
565 (true, true) => (
566 i64::from(inset_start),
567 (area_length - i64::from(inset_start) - i64::from(inset_end)).max(1),
568 ),
569 (true, false) => (i64::from(inset_start), length),
570 (false, true) => (area_length - i64::from(inset_end) - length, length),
571 (false, false) => ((area_length - length) / 2, length),
572 };
573 let start = i64::from(area_start) + offset;
574 (
575 i32::try_from(start).unwrap_or(if start < 0 { i32::MIN } else { i32::MAX }),
576 u32::try_from(length).unwrap_or(u32::MAX),
577 )
578}
579
580#[cfg(test)]
581mod tests {
582 use super::{
583 PanelAnchor, PanelDesc, PopupDesc, PopupInput, PopupPlacement, Reserve, WindowRole,
584 };
585 use crate::{
586 HeadlessShell, LogicalRect, MonitorId, Shell, ShellCaps, ShellError, ShellEvent, WindowDesc,
587 };
588 use crate::{PhysicalRect, PhysicalSize};
589
590 #[test]
591 fn a_popup_opens_on_its_side_and_flips_or_slides_to_stay_on_screen() {
592 let area = PhysicalRect::new(0, 0, 1920, 1040);
593 let button = PhysicalRect::new(100, 100, 80, 30);
594 let menu = PhysicalSize::new(200, 300);
595 assert_eq!(
596 super::popup_rect(button, menu, PopupPlacement::Below, area),
597 PhysicalRect::new(100, 130, 200, 300),
598 "below, left edges aligned"
599 );
600 assert_eq!(
601 super::popup_rect(button, menu, PopupPlacement::Above, area),
602 PhysicalRect::new(100, 130, 200, 300),
603 "no room above, so flipped below"
604 );
605 assert_eq!(
606 super::popup_rect(button, menu, PopupPlacement::Right, area),
607 PhysicalRect::new(180, 100, 200, 300),
608 "a submenu, top edges aligned"
609 );
610 assert_eq!(
611 super::popup_rect(button, menu, PopupPlacement::Left, area),
612 PhysicalRect::new(180, 100, 200, 300),
613 "no room to the left, so flipped right"
614 );
615 let low = PhysicalRect::new(1850, 1000, 60, 30);
616 assert_eq!(
617 super::popup_rect(low, menu, PopupPlacement::Below, area),
618 PhysicalRect::new(1720, 700, 200, 300),
619 "flipped above, and slid back onto the screen"
620 );
621 let tall = PhysicalSize::new(200, 2000);
622 assert_eq!(
623 super::popup_rect(button, tall, PopupPlacement::Below, area).y,
624 130,
625 "fits neither side, so it stays on its own"
626 );
627 }
628
629 #[test]
630 fn a_panel_is_placed_as_layer_shell_places_one() {
631 let area = PhysicalRect::new(100, 40, 1920, 1040);
632 let bar = PhysicalSize::new(0, 30);
633 let insets = super::Insets {
634 top: 4,
635 right: 8,
636 bottom: 12,
637 left: 16,
638 };
639 let across_the_top = PanelAnchor::TOP | PanelAnchor::LEFT | PanelAnchor::RIGHT;
640 assert_eq!(
641 super::panel_rect(area, across_the_top, bar, super::Insets::default()),
642 PhysicalRect::new(100, 40, 1920, 30),
643 "stretched between the sides, at its own height"
644 );
645 assert_eq!(
646 super::panel_rect(area, across_the_top, bar, insets),
647 PhysicalRect::new(116, 44, 1920 - 16 - 8, 30),
648 );
649 let dock = PhysicalSize::new(64, 500);
650 assert_eq!(
651 super::panel_rect(area, PanelAnchor::RIGHT, dock, insets),
652 PhysicalRect::new(100 + 1920 - 8 - 64, 40 + (1040 - 500) / 2, 64, 500),
653 "held off the right edge, centred down it"
654 );
655 assert_eq!(
656 super::panel_rect(area, PanelAnchor::BOTTOM | PanelAnchor::LEFT, dock, insets),
657 PhysicalRect::new(116, 40 + 1040 - 12 - 500, 64, 500),
658 "in the corner"
659 );
660 let launcher = PhysicalSize::new(600, 400);
661 assert_eq!(
662 super::panel_rect(area, PanelAnchor::empty(), launcher, insets),
663 PhysicalRect::new(100 + (1920 - 600) / 2, 40 + (1040 - 400) / 2, 600, 400),
664 "centred, and an unanchored panel's insets do nothing"
665 );
666 assert_eq!(
667 super::panel_rect(area, PanelAnchor::all(), launcher, insets),
668 PhysicalRect::new(116, 44, 1920 - 24, 1040 - 16),
669 );
670 }
671
672 #[test]
673 fn only_a_fresh_escape_press_on_a_grabbing_popup_dismisses_it() {
674 use crcbl_core::KeyCode;
675 use crcbl_core::input::{ButtonState, DeviceId, Keysym, Modifiers, Scancode};
676
677 let window = crate::WindowId::from_bits(1 << 32).expect("a valid handle");
678 let key = |key_code, keysym, state, repeat| ShellEvent::Key {
679 window,
680 device: DeviceId(1),
681 time: crcbl_core::EventTime::ZERO,
682 scancode: Scancode(0),
683 key_code,
684 keysym,
685 state,
686 repeat,
687 modifiers: Modifiers::empty(),
688 };
689 let pressed = ButtonState::Pressed;
690 let escape = key(Some(KeyCode::Escape), Keysym::ESCAPE, pressed, false);
691 let grab = Some(PopupInput::Grab);
692 assert!(super::escape_dismisses(grab, &escape));
693 let remapped = key(Some(KeyCode::CapsLock), Keysym::ESCAPE, pressed, false);
696 assert!(super::escape_dismisses(grab, &remapped));
697 let wtype_down = key(Some(KeyCode::Escape), Keysym::DOWN, pressed, false);
698 assert!(!super::escape_dismisses(grab, &wtype_down));
699
700 let released = key(
702 Some(KeyCode::Escape),
703 Keysym::ESCAPE,
704 ButtonState::Released,
705 false,
706 );
707 let repeated = key(Some(KeyCode::Escape), Keysym::ESCAPE, pressed, true);
708 let enter = key(Some(KeyCode::Enter), Keysym::RETURN, pressed, false);
709 let unnamed = key(None, Keysym::NONE, pressed, false);
710 for other in [&released, &repeated, &enter, &unnamed] {
711 assert!(!super::escape_dismisses(grab, other), "{other:?}");
712 }
713 for input in [
716 Some(PopupInput::Pointer),
717 Some(PopupInput::Passthrough),
718 None,
719 ] {
720 assert!(!super::escape_dismisses(input, &escape), "{input:?}");
721 }
722 assert!(!super::escape_dismisses(
724 grab,
725 &ShellEvent::CloseRequested { window }
726 ));
727 }
728
729 fn panel(anchor: PanelAnchor) -> PanelDesc<'static> {
730 PanelDesc {
731 anchor,
732 ..PanelDesc::default()
733 }
734 }
735
736 #[test]
737 fn a_reserved_edge_is_one_edge_alone_or_with_both_neighbours() {
738 let top = PanelAnchor::TOP;
739 let sides = PanelAnchor::LEFT | PanelAnchor::RIGHT;
740 assert_eq!(panel(top).reserved_edge(), Some(top));
741 assert_eq!(panel(top | sides).reserved_edge(), Some(top));
742 let left = PanelAnchor::LEFT;
743 let ends = PanelAnchor::TOP | PanelAnchor::BOTTOM;
744 assert_eq!(panel(left | ends).reserved_edge(), Some(left));
745 assert_eq!(
746 panel(PanelAnchor::BOTTOM).reserved_edge(),
747 Some(PanelAnchor::BOTTOM)
748 );
749 assert_eq!(panel(top | PanelAnchor::LEFT).reserved_edge(), None);
751 assert_eq!(panel(PanelAnchor::empty()).reserved_edge(), None);
752 assert_eq!(panel(PanelAnchor::all()).reserved_edge(), None);
753 assert_eq!(panel(sides).reserved_edge(), None);
754 }
755
756 fn with_role(role: WindowRole<'_>) -> WindowDesc<'_> {
757 WindowDesc {
758 role,
759 ..WindowDesc::default()
760 }
761 }
762
763 fn refused(result: Result<crate::WindowId, ShellError>) -> &'static str {
764 match result {
765 Err(ShellError::Unsupported { what, .. }) => what,
766 other => panic!("expected Unsupported, got {other:?}"),
767 }
768 }
769
770 #[test]
771 fn a_role_or_transparency_the_caps_lack_is_refused_not_downgraded() {
772 let mut shell = HeadlessShell::new().with_caps(
773 ShellCaps::DESKTOP - (ShellCaps::PANEL | ShellCaps::POPUP | ShellCaps::TRANSPARENT),
774 );
775 let parent = shell
776 .create_window(&WindowDesc::default())
777 .expect("a toplevel");
778 assert_eq!(
779 refused(shell.create_window(&with_role(WindowRole::Panel(PanelDesc::default())))),
780 "panel windows"
781 );
782 let popup = WindowRole::Popup(PopupDesc {
783 parent,
784 anchor: LogicalRect::default(),
785 placement: PopupPlacement::Below,
786 input: PopupInput::Grab,
787 });
788 assert_eq!(
789 refused(shell.create_window(&with_role(popup))),
790 "popup windows"
791 );
792 let transparent = WindowDesc {
793 transparent: true,
794 ..WindowDesc::default()
795 };
796 assert_eq!(
797 refused(shell.create_window(&transparent)),
798 "transparent windows"
799 );
800 }
801
802 #[test]
803 fn a_reserved_panel_needs_one_edge_and_a_real_monitor() {
804 let mut shell = HeadlessShell::new();
805 let cornered = PanelDesc {
806 reserve: Reserve::OwnExtent,
807 ..panel(PanelAnchor::TOP | PanelAnchor::LEFT)
808 };
809 assert_eq!(
810 refused(shell.create_window(&with_role(WindowRole::Panel(cornered)))),
811 "a reserved panel that is not held against one edge"
812 );
813 let bar = PanelDesc {
814 reserve: Reserve::OwnExtent,
815 ..panel(PanelAnchor::TOP | PanelAnchor::LEFT | PanelAnchor::RIGHT)
816 };
817 shell
818 .create_window(&with_role(WindowRole::Panel(bar)))
819 .expect("a bar names the top edge");
820 let elsewhere = PanelDesc {
821 monitor: Some(MonitorId(u32::MAX)),
822 ..PanelDesc::default()
823 };
824 assert!(matches!(
825 shell.create_window(&with_role(WindowRole::Panel(elsewhere))),
826 Err(ShellError::NoSuchMonitor(_))
827 ));
828 }
829
830 #[test]
831 fn a_popup_needs_a_live_parent_and_goes_with_it() {
832 let mut shell = HeadlessShell::new();
833 let parent = shell
834 .create_window(&WindowDesc::default())
835 .expect("a toplevel");
836 let popup_of = |parent| {
837 WindowRole::Popup(PopupDesc {
838 parent,
839 anchor: LogicalRect::default(),
840 placement: PopupPlacement::Below,
841 input: PopupInput::Passthrough,
842 })
843 };
844 let popup = shell
845 .create_window(&with_role(popup_of(parent)))
846 .expect("a popup");
847 let submenu = shell
848 .create_window(&with_role(popup_of(popup)))
849 .expect("a popup's popup");
850 shell.pump(&mut |_| {});
851
852 shell.destroy_window(parent).expect("destroy");
853 let mut destroyed = Vec::new();
854 shell.pump(&mut |event| {
855 if let ShellEvent::WindowDestroyed { window } = event {
856 destroyed.push(window);
857 }
858 });
859 assert_eq!(
860 destroyed,
861 [submenu, popup, parent],
862 "children first, each reported"
863 );
864 assert!(
865 shell.create_window(&with_role(popup_of(parent))).is_err(),
866 "a dead parent"
867 );
868 }
869}