use crate::geom::LogicalRect;
use crate::monitor::MonitorId;
use crate::window::{WindowDesc, WindowId};
use crate::{ShellBackend, ShellCaps, ShellError};
#[derive(Clone, Copy, Debug, Default, PartialEq)]
#[non_exhaustive]
pub enum WindowRole<'a> {
#[default]
Toplevel,
Panel(PanelDesc<'a>),
Popup(PopupDesc),
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct PanelDesc<'a> {
pub namespace: &'a str,
pub monitor: Option<MonitorId>,
pub layer: PanelLayer,
pub anchor: PanelAnchor,
pub reserve: Reserve,
pub margins: Margins,
pub keyboard: PanelKeyboard,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum PanelLayer {
Background,
Bottom,
#[default]
Top,
Overlay,
}
bitflags::bitflags! {
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct PanelAnchor: u8 {
const TOP = 1 << 0;
const BOTTOM = 1 << 1;
const LEFT = 1 << 2;
const RIGHT = 1 << 3;
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum Reserve {
#[default]
None,
OwnExtent,
IgnoreOthers,
}
impl PanelDesc<'_> {
#[must_use]
pub fn reserved_edge(&self) -> Option<PanelAnchor> {
let vertical = PanelAnchor::LEFT | PanelAnchor::RIGHT;
let horizontal = PanelAnchor::TOP | PanelAnchor::BOTTOM;
[
(PanelAnchor::TOP, vertical),
(PanelAnchor::BOTTOM, vertical),
(PanelAnchor::LEFT, horizontal),
(PanelAnchor::RIGHT, horizontal),
]
.into_iter()
.find(|&(edge, beside)| self.anchor == edge || self.anchor == edge | beside)
.map(|(edge, _)| edge)
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct Margins {
pub top: f64,
pub right: f64,
pub bottom: f64,
pub left: f64,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum PanelKeyboard {
#[default]
None,
OnDemand,
Exclusive,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct PopupDesc {
pub parent: WindowId,
pub anchor: LogicalRect,
pub placement: PopupPlacement,
pub input: PopupInput,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum PopupInput {
#[default]
Passthrough,
Pointer,
Grab,
}
pub(crate) fn escape_dismisses(popup: Option<PopupInput>, key: &crate::ShellEvent) -> bool {
popup == Some(PopupInput::Grab)
&& matches!(
key,
crate::ShellEvent::Key {
key_code: Some(crcbl_core::KeyCode::Escape),
state: crcbl_core::input::ButtonState::Pressed,
repeat: false,
..
}
)
}
pub(crate) fn check(
desc: &WindowDesc<'_>,
caps: ShellCaps,
backend: ShellBackend,
has_monitor: impl Fn(MonitorId) -> bool,
) -> Result<(), ShellError> {
let unsupported = |what| Err(ShellError::Unsupported { backend, what });
if desc.transparent && !caps.contains(ShellCaps::TRANSPARENT) {
return unsupported("transparent windows");
}
match desc.role {
WindowRole::Toplevel => Ok(()),
WindowRole::Panel(panel) => check_panel(&panel, caps, backend, has_monitor),
WindowRole::Popup(_) if !caps.contains(ShellCaps::POPUP) => unsupported("popup windows"),
WindowRole::Popup(_) => Ok(()),
}
}
fn check_panel(
panel: &PanelDesc<'_>,
caps: ShellCaps,
backend: ShellBackend,
has_monitor: impl Fn(MonitorId) -> bool,
) -> Result<(), ShellError> {
let unsupported = |what| Err(ShellError::Unsupported { backend, what });
if !caps.contains(ShellCaps::PANEL) {
return unsupported("panel windows");
}
if panel.reserve == Reserve::OwnExtent && panel.reserved_edge().is_none() {
return unsupported("a reserved panel that is not held against one edge");
}
match panel.monitor {
Some(monitor) if !has_monitor(monitor) => Err(ShellError::NoSuchMonitor(monitor.0)),
_ => Ok(()),
}
}
pub(crate) fn check_set_panel(
window: WindowId,
was: Option<PanelDesc<'_>>,
panel: &PanelDesc<'_>,
caps: ShellCaps,
backend: ShellBackend,
has_monitor: impl Fn(MonitorId) -> bool,
) -> Result<(), ShellError> {
let Some(was) = was else {
return Err(not_a_panel(window));
};
if panel.namespace != was.namespace {
return Err(ShellError::InvalidDescriptor(
"set_panel: a panel's namespace is fixed when it is created".to_string(),
));
}
if panel.monitor != was.monitor {
return Err(ShellError::InvalidDescriptor(
"set_panel: a panel's monitor is fixed when it is created".to_string(),
));
}
check_panel(panel, caps, backend, has_monitor)
}
pub(crate) fn not_a_panel(window: WindowId) -> ShellError {
ShellError::InvalidDescriptor(format!("set_panel: {window:?} is not a panel"))
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct KeptPanel {
namespace: String,
desc: PanelDesc<'static>,
}
impl KeptPanel {
pub(crate) fn new(desc: &PanelDesc<'_>) -> Self {
Self {
namespace: desc.namespace.to_string(),
desc: PanelDesc {
namespace: "",
..*desc
},
}
}
pub(crate) fn desc(&self) -> PanelDesc<'_> {
PanelDesc {
namespace: &self.namespace,
..self.desc
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum PopupPlacement {
#[default]
Below,
Above,
Left,
Right,
}
#[cfg(any(target_os = "windows", target_os = "macos", test))]
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct Placed {
pub x: f64,
pub y: f64,
pub width: f64,
pub height: f64,
}
pub(crate) fn not_a_popup(window: WindowId) -> ShellError {
ShellError::InvalidDescriptor(format!("reposition_popup: {window:?} is not a popup"))
}
#[cfg(any(target_os = "windows", target_os = "macos", test))]
pub(crate) fn place_popup(
anchor: Placed,
(width, height): (f64, f64),
placement: PopupPlacement,
area: Placed,
) -> (f64, f64) {
let (left, top) = (anchor.x, anchor.y);
let (right, bottom) = (left + anchor.width, top + anchor.height);
let (area_right, area_bottom) = (area.x + area.width, area.y + area.height);
let side = |near: f64, far: f64, extent: f64, low: f64, high: f64| {
if near >= low && near + extent <= high {
near
} else if far >= low && far + extent <= high {
far
} else {
near
}
};
let slide = |start: f64, extent: f64, low: f64, high: f64| start.min(high - extent).max(low);
match placement {
PopupPlacement::Below => (
slide(left, width, area.x, area_right),
side(bottom, top - height, height, area.y, area_bottom),
),
PopupPlacement::Above => (
slide(left, width, area.x, area_right),
side(top - height, bottom, height, area.y, area_bottom),
),
PopupPlacement::Left => (
side(left - width, right, width, area.x, area_right),
slide(top, height, area.y, area_bottom),
),
PopupPlacement::Right => (
side(right, left - width, width, area.x, area_right),
slide(top, height, area.y, area_bottom),
),
}
}
#[cfg(test)]
mod tests {
use super::{
PanelAnchor, PanelDesc, PopupDesc, PopupInput, PopupPlacement, Reserve, WindowRole,
};
use crate::{
HeadlessShell, LogicalRect, MonitorId, Shell, ShellCaps, ShellError, ShellEvent, WindowDesc,
};
#[test]
fn only_a_fresh_escape_press_on_a_grabbing_popup_dismisses_it() {
use crcbl_core::KeyCode;
use crcbl_core::input::{ButtonState, DeviceId, Keysym, Modifiers, Scancode};
let window = crate::WindowId::from_bits(1 << 32).expect("a valid handle");
let key = |key_code, state, repeat| ShellEvent::Key {
window,
device: DeviceId(1),
time: crcbl_core::EventTime::ZERO,
scancode: Scancode(0),
key_code,
keysym: Keysym::NONE,
state,
repeat,
modifiers: Modifiers::empty(),
};
let escape = key(Some(KeyCode::Escape), ButtonState::Pressed, false);
let grab = Some(PopupInput::Grab);
assert!(super::escape_dismisses(grab, &escape));
let released = key(Some(KeyCode::Escape), ButtonState::Released, false);
let repeated = key(Some(KeyCode::Escape), ButtonState::Pressed, true);
let enter = key(Some(KeyCode::Enter), ButtonState::Pressed, false);
let unnamed = key(None, ButtonState::Pressed, false);
for other in [&released, &repeated, &enter, &unnamed] {
assert!(!super::escape_dismisses(grab, other), "{other:?}");
}
for input in [
Some(PopupInput::Pointer),
Some(PopupInput::Passthrough),
None,
] {
assert!(!super::escape_dismisses(input, &escape), "{input:?}");
}
assert!(!super::escape_dismisses(
grab,
&ShellEvent::CloseRequested { window }
));
}
fn panel(anchor: PanelAnchor) -> PanelDesc<'static> {
PanelDesc {
anchor,
..PanelDesc::default()
}
}
#[test]
fn a_reserved_edge_is_one_edge_alone_or_with_both_neighbours() {
let top = PanelAnchor::TOP;
let sides = PanelAnchor::LEFT | PanelAnchor::RIGHT;
assert_eq!(panel(top).reserved_edge(), Some(top));
assert_eq!(panel(top | sides).reserved_edge(), Some(top));
let left = PanelAnchor::LEFT;
let ends = PanelAnchor::TOP | PanelAnchor::BOTTOM;
assert_eq!(panel(left | ends).reserved_edge(), Some(left));
assert_eq!(
panel(PanelAnchor::BOTTOM).reserved_edge(),
Some(PanelAnchor::BOTTOM)
);
assert_eq!(panel(top | PanelAnchor::LEFT).reserved_edge(), None);
assert_eq!(panel(PanelAnchor::empty()).reserved_edge(), None);
assert_eq!(panel(PanelAnchor::all()).reserved_edge(), None);
assert_eq!(panel(sides).reserved_edge(), None);
}
fn with_role(role: WindowRole<'_>) -> WindowDesc<'_> {
WindowDesc {
role,
..WindowDesc::default()
}
}
fn refused(result: Result<crate::WindowId, ShellError>) -> &'static str {
match result {
Err(ShellError::Unsupported { what, .. }) => what,
other => panic!("expected Unsupported, got {other:?}"),
}
}
#[test]
fn a_role_or_transparency_the_caps_lack_is_refused_not_downgraded() {
let mut shell = HeadlessShell::new().with_caps(
ShellCaps::DESKTOP - (ShellCaps::PANEL | ShellCaps::POPUP | ShellCaps::TRANSPARENT),
);
let parent = shell
.create_window(&WindowDesc::default())
.expect("a toplevel");
assert_eq!(
refused(shell.create_window(&with_role(WindowRole::Panel(PanelDesc::default())))),
"panel windows"
);
let popup = WindowRole::Popup(PopupDesc {
parent,
anchor: LogicalRect::default(),
placement: PopupPlacement::Below,
input: PopupInput::Grab,
});
assert_eq!(
refused(shell.create_window(&with_role(popup))),
"popup windows"
);
let transparent = WindowDesc {
transparent: true,
..WindowDesc::default()
};
assert_eq!(
refused(shell.create_window(&transparent)),
"transparent windows"
);
}
#[test]
fn a_reserved_panel_needs_one_edge_and_a_real_monitor() {
let mut shell = HeadlessShell::new();
let cornered = PanelDesc {
reserve: Reserve::OwnExtent,
..panel(PanelAnchor::TOP | PanelAnchor::LEFT)
};
assert_eq!(
refused(shell.create_window(&with_role(WindowRole::Panel(cornered)))),
"a reserved panel that is not held against one edge"
);
let bar = PanelDesc {
reserve: Reserve::OwnExtent,
..panel(PanelAnchor::TOP | PanelAnchor::LEFT | PanelAnchor::RIGHT)
};
shell
.create_window(&with_role(WindowRole::Panel(bar)))
.expect("a bar names the top edge");
let elsewhere = PanelDesc {
monitor: Some(MonitorId(u32::MAX)),
..PanelDesc::default()
};
assert!(matches!(
shell.create_window(&with_role(WindowRole::Panel(elsewhere))),
Err(ShellError::NoSuchMonitor(_))
));
}
#[test]
fn a_popup_needs_a_live_parent_and_goes_with_it() {
let mut shell = HeadlessShell::new();
let parent = shell
.create_window(&WindowDesc::default())
.expect("a toplevel");
let popup_of = |parent| {
WindowRole::Popup(PopupDesc {
parent,
anchor: LogicalRect::default(),
placement: PopupPlacement::Below,
input: PopupInput::Passthrough,
})
};
let popup = shell
.create_window(&with_role(popup_of(parent)))
.expect("a popup");
let submenu = shell
.create_window(&with_role(popup_of(popup)))
.expect("a popup's popup");
shell.pump(&mut |_| {});
shell.destroy_window(parent).expect("destroy");
let mut destroyed = Vec::new();
shell.pump(&mut |event| {
if let ShellEvent::WindowDestroyed { window } = event {
destroyed.push(window);
}
});
assert_eq!(
destroyed,
[submenu, popup, parent],
"children first, each reported"
);
assert!(
shell.create_window(&with_role(popup_of(parent))).is_err(),
"a dead parent"
);
}
}