use std::collections::VecDeque;
use std::path::PathBuf;
use std::sync::{Arc, Condvar, Mutex, PoisonError};
use core::time::Duration;
use crcbl_core::input::{
ButtonState, ContactId, DeviceId, Keysym, Modifiers, PointerButton, Scancode, ScrollDelta,
TouchPhase,
};
use crcbl_core::time::{ManualTime, TimeSource};
use crcbl_core::{EventTime, Pool, SurfaceTarget};
use crate::waker::WakeSignal;
use crate::{
ClipboardContent, ClipboardOffer, ClipboardRequestId, CloseReply, CursorIcon, DisplayMode,
KeyCode, LogicalRect, LogicalSize, MimeType, MonitorId, MonitorInfo, PhysicalPoint,
PhysicalRect, PhysicalSize, PointerMode, ReceivedMime, Shell, ShellBackend, ShellCaps,
ShellError, ShellEvent, ShellWaker, SizeConstraints, WindowConfiguration, WindowDesc, WindowId,
WindowState,
};
pub const DEFAULT_MONITOR_SIZE: PhysicalSize = PhysicalSize::new(1920, 1080);
pub const DEFAULT_REFRESH_MILLIHERTZ: u32 = 60_000;
pub const DEFAULT_CONFIGURE_DELAY: u32 = 1;
pub const VIRTUAL_DEVICE: DeviceId = DeviceId(1);
pub const DEFAULT_CLIPBOARD_DEADLINE: u32 = 4;
#[derive(Clone, Debug)]
struct PendingRead {
window: WindowId,
request: ClipboardRequestId,
mime: MimeType,
countdown: u32,
}
#[derive(Clone, Copy, Debug)]
struct PendingConfigure {
size: PhysicalSize,
scale_factor: f64,
mode: DisplayMode,
countdown: u32,
}
#[derive(Clone, Debug)]
struct HeadlessWindow {
title: String,
configuration: Option<WindowConfiguration>,
pending: Option<PendingConfigure>,
requested_mode: DisplayMode,
requested_constraints: SizeConstraints,
focused: bool,
visible: bool,
pointer_mode: PointerMode,
cursor: Option<CursorIcon>,
close_pending: bool,
accept_drops: bool,
parent: Option<WindowId>,
panel_monitor: Option<MonitorId>,
}
impl HeadlessWindow {
fn size(&self) -> Option<PhysicalSize> {
self.configuration.map(|config| config.size)
}
fn scale_factor(&self) -> f64 {
self.configuration
.map(|config| config.scale_factor)
.or_else(|| self.pending.map(|pending| pending.scale_factor))
.unwrap_or(1.0)
}
fn settled_mode(&self) -> DisplayMode {
self.configuration
.map(|config| config.mode)
.or_else(|| self.pending.map(|pending| pending.mode))
.unwrap_or(self.requested_mode)
}
fn base_size(&self) -> PhysicalSize {
self.size()
.or_else(|| self.pending.map(|pending| pending.size))
.unwrap_or(PhysicalSize::new(1, 1))
}
}
#[derive(Debug)]
pub struct HeadlessShell {
caps: ShellCaps,
monitors: Vec<MonitorInfo>,
next_monitor_id: u32,
windows: Pool<HeadlessWindow>,
queue: VecDeque<ShellEvent>,
clock: ManualTime,
modifiers: Modifiers,
clipboard: Vec<(ReceivedMime, Vec<u8>)>,
next_request_id: u32,
reads: Vec<PendingRead>,
clipboard_readable: bool,
clipboard_stalled: bool,
clipboard_needs_interaction: bool,
saw_interaction: bool,
clipboard_deadline: u32,
waits: u32,
configure_delay: u32,
wake: Option<Arc<HeadlessWake>>,
}
#[derive(Debug, Default)]
struct HeadlessWake {
rung: Mutex<bool>,
bell: Condvar,
}
impl HeadlessWake {
fn wait(&self, timeout: Option<Duration>) {
let rung = self.rung.lock().unwrap_or_else(PoisonError::into_inner);
let mut rung = match timeout {
Some(timeout) => {
self.bell
.wait_timeout_while(rung, timeout, |rung| !*rung)
.unwrap_or_else(PoisonError::into_inner)
.0
}
None => self
.bell
.wait_while(rung, |rung| !*rung)
.unwrap_or_else(PoisonError::into_inner),
};
*rung = false;
}
}
impl WakeSignal for HeadlessWake {
fn wake(&self) {
*self.rung.lock().unwrap_or_else(PoisonError::into_inner) = true;
self.bell.notify_all();
}
}
impl Default for HeadlessShell {
fn default() -> Self {
Self::new()
}
}
impl HeadlessShell {
#[must_use]
pub fn new() -> Self {
Self {
caps: ShellCaps::DESKTOP,
monitors: vec![MonitorInfo {
id: MonitorId(1),
name: "HEADLESS-1".to_string(),
bounds: PhysicalRect::new(
0,
0,
DEFAULT_MONITOR_SIZE.width,
DEFAULT_MONITOR_SIZE.height,
),
work_area: PhysicalRect::new(
0,
0,
DEFAULT_MONITOR_SIZE.width,
DEFAULT_MONITOR_SIZE.height,
),
scale_factor: 1.0,
refresh_millihertz: DEFAULT_REFRESH_MILLIHERTZ,
is_primary: true,
}],
next_monitor_id: 2,
windows: Pool::new(),
queue: VecDeque::new(),
clock: ManualTime::new(),
modifiers: Modifiers::empty(),
clipboard: Vec::new(),
next_request_id: 1,
reads: Vec::new(),
clipboard_readable: true,
clipboard_stalled: false,
clipboard_needs_interaction: false,
saw_interaction: false,
clipboard_deadline: DEFAULT_CLIPBOARD_DEADLINE,
waits: 0,
configure_delay: DEFAULT_CONFIGURE_DELAY,
wake: None,
}
}
#[must_use]
pub fn with_caps(mut self, caps: ShellCaps) -> Self {
self.caps = caps;
self
}
#[must_use]
pub fn with_monitors(mut self, monitors: Vec<MonitorInfo>) -> Self {
assert!(!monitors.is_empty(), "a shell needs at least one monitor");
for (index, monitor) in monitors.iter().enumerate() {
assert!(
monitor.id.0 != u32::MAX,
"monitor id {} leaves no id for the next hotplug",
monitor.id.0
);
assert!(
!monitors[..index].iter().any(|seen| seen.id == monitor.id),
"monitor id {} appears twice",
monitor.id.0
);
}
self.next_monitor_id = monitors
.iter()
.map(|monitor| monitor.id.0 + 1)
.max()
.unwrap_or(1);
self.monitors = monitors;
self
}
#[must_use]
pub fn with_scale_factor(mut self, scale_factor: f64) -> Self {
assert!(scale_factor > 0.0, "scale factor must be positive");
self.monitors[0].scale_factor = scale_factor;
self
}
#[must_use]
pub const fn with_configure_delay(mut self, pumps: u32) -> Self {
self.configure_delay = pumps;
self
}
pub const fn set_configure_delay(&mut self, pumps: u32) {
self.configure_delay = pumps;
}
pub fn set_caps(&mut self, caps: ShellCaps) {
self.caps = caps;
}
#[must_use]
pub fn now(&self) -> EventTime {
EventTime::from_duration(self.clock.elapsed())
}
pub fn advance(&mut self, delta: Duration) {
self.clock.advance(delta);
}
pub fn set_time(&mut self, now: Duration) {
self.clock.set(now);
}
pub fn set_modifiers(&mut self, modifiers: Modifiers) {
self.modifiers = modifiers;
}
#[must_use]
pub fn pending_events(&self) -> usize {
self.queue.len()
}
#[must_use]
pub const fn wait_count(&self) -> u32 {
self.waits
}
pub fn inject(&mut self, event: ShellEvent) {
self.queue.push_back(event);
}
pub fn resize(&mut self, window: WindowId, size: PhysicalSize) -> Result<(), ShellError> {
let state = self.window_mut(window)?;
let scale_factor = state.scale_factor();
let mode = state.settled_mode();
let constrained = state.requested_constraints.apply(size, scale_factor);
state.pending = None;
state.configuration = Some(WindowConfiguration {
size: constrained,
scale_factor,
mode,
});
self.queue.push_back(ShellEvent::Resized {
window,
size: constrained,
scale_factor,
});
Ok(())
}
pub fn change_scale_factor(
&mut self,
window: WindowId,
scale_factor: f64,
) -> Result<(), ShellError> {
assert!(scale_factor > 0.0, "scale factor must be positive");
let state = self.window_mut(window)?;
let logical = state.base_size().to_logical(state.scale_factor());
let mode = state.settled_mode();
let size = state
.requested_constraints
.apply(logical.to_physical(scale_factor), scale_factor);
state.pending = None;
state.configuration = Some(WindowConfiguration {
size,
scale_factor,
mode,
});
self.queue.push_back(ShellEvent::ScaleFactorChanged {
window,
scale_factor,
size,
});
self.queue.push_back(ShellEvent::Resized {
window,
size,
scale_factor,
});
Ok(())
}
pub fn set_focus(&mut self, window: WindowId, focused: bool) -> Result<(), ShellError> {
self.window_mut(window)?.focused = focused;
self.queue.push_back(ShellEvent::Focus { window, focused });
Ok(())
}
pub fn request_close(&mut self, window: WindowId) -> Result<(), ShellError> {
self.window_mut(window)?.close_pending = true;
self.queue.push_back(ShellEvent::CloseRequested { window });
Ok(())
}
pub fn key(
&mut self,
window: WindowId,
key: KeyCode,
state: ButtonState,
) -> Result<(), ShellError> {
self.key_with(window, key, state, false)
}
pub fn key_press(&mut self, window: WindowId, key: KeyCode) -> Result<(), ShellError> {
self.key(window, key, ButtonState::Pressed)
}
pub fn key_release(&mut self, window: WindowId, key: KeyCode) -> Result<(), ShellError> {
self.key(window, key, ButtonState::Released)
}
pub fn key_repeat(&mut self, window: WindowId, key: KeyCode) -> Result<(), ShellError> {
self.key_with(window, key, ButtonState::Pressed, true)
}
fn key_with(
&mut self,
window: WindowId,
key: KeyCode,
state: ButtonState,
repeat: bool,
) -> Result<(), ShellError> {
self.check_window(window)?;
self.saw_interaction = true;
let event = ShellEvent::Key {
window,
device: VIRTUAL_DEVICE,
time: self.now(),
scancode: synthetic_scancode(key),
key_code: Some(key),
keysym: synthetic_keysym(key),
state,
repeat,
modifiers: self.modifiers,
};
self.queue.push_back(event);
Ok(())
}
pub fn move_pointer(
&mut self,
window: WindowId,
position: PhysicalPoint,
raw_delta: (f64, f64),
) -> Result<(), ShellError> {
let locked = self.window(window)?.pointer_mode == PointerMode::Locked;
let raw = if self.caps.contains(ShellCaps::RAW_POINTER_MOTION) {
Some(raw_delta)
} else {
None
};
self.saw_interaction = true;
let event = ShellEvent::PointerMotion {
window,
device: VIRTUAL_DEVICE,
time: self.now(),
abs: if locked { None } else { Some(position) },
raw_delta: raw,
};
self.queue.push_back(event);
Ok(())
}
pub fn set_pointer_focus(
&mut self,
window: WindowId,
entered: bool,
position: Option<PhysicalPoint>,
) -> Result<(), ShellError> {
self.check_window(window)?;
let event = ShellEvent::PointerFocus {
window,
device: VIRTUAL_DEVICE,
time: self.now(),
entered,
position: if entered { position } else { None },
};
self.queue.push_back(event);
Ok(())
}
pub fn button(
&mut self,
window: WindowId,
button: PointerButton,
state: ButtonState,
position: Option<PhysicalPoint>,
) -> Result<(), ShellError> {
self.check_window(window)?;
self.saw_interaction = true;
let event = ShellEvent::Button {
window,
device: VIRTUAL_DEVICE,
time: self.now(),
button,
state,
position,
modifiers: self.modifiers,
};
self.queue.push_back(event);
Ok(())
}
pub fn scroll(
&mut self,
window: WindowId,
delta: ScrollDelta,
position: Option<PhysicalPoint>,
) -> Result<(), ShellError> {
self.check_window(window)?;
self.saw_interaction = true;
let event = ShellEvent::Wheel {
window,
device: VIRTUAL_DEVICE,
time: self.now(),
delta,
position,
modifiers: self.modifiers,
};
self.queue.push_back(event);
Ok(())
}
pub fn touch(
&mut self,
window: WindowId,
contact: ContactId,
phase: TouchPhase,
position: PhysicalPoint,
) -> Result<(), ShellError> {
self.check_window(window)?;
if !self.caps.contains(ShellCaps::TOUCH) {
return Err(self.unsupported("touch"));
}
self.saw_interaction = true;
let event = ShellEvent::Touch {
window,
device: VIRTUAL_DEVICE,
time: self.now(),
contact,
phase,
position,
};
self.queue.push_back(event);
Ok(())
}
pub fn commit_text(&mut self, window: WindowId, text: &str) -> Result<(), ShellError> {
assert!(!text.is_empty(), "a text commit is never empty");
self.check_window(window)?;
let event = ShellEvent::TextCommit {
window,
time: self.now(),
text: text.to_string(),
};
self.queue.push_back(event);
Ok(())
}
pub fn drop_file(
&mut self,
window: WindowId,
path: impl Into<PathBuf>,
position: Option<PhysicalPoint>,
) -> Result<(), ShellError> {
if !self.caps.contains(ShellCaps::DRAG_DROP) {
return Err(self.unsupported("drag and drop"));
}
if !self.window(window)?.accept_drops {
return Err(ShellError::Unsupported {
backend: ShellBackend::Headless,
what: "drops on a window created without accept_drops",
});
}
let event = ShellEvent::DroppedFile {
window,
time: self.now(),
path: path.into(),
position,
};
self.queue.push_back(event);
Ok(())
}
pub fn plug_monitor(&mut self, mut info: MonitorInfo) -> MonitorId {
let id = MonitorId(self.next_monitor_id);
self.next_monitor_id += 1;
info.id = id;
self.monitors.push(info);
self.queue.push_back(ShellEvent::MonitorsChanged);
id
}
pub fn unplug_monitor(&mut self, id: MonitorId) -> Result<(), ShellError> {
let index = self
.monitors
.iter()
.position(|monitor| monitor.id == id)
.ok_or(ShellError::NoSuchMonitor(id.0))?;
assert!(
self.monitors.len() > 1,
"a shell needs at least one monitor"
);
let previous = self.monitors.clone();
self.monitors.remove(index);
crate::monitor::queue_removed(&previous, &self.monitors, &mut self.queue);
let panels: Vec<WindowId> = self
.windows
.iter()
.filter(|(_, state)| state.panel_monitor == Some(id) && !state.close_pending)
.map(|(window, _)| window.cast())
.collect();
for panel in panels {
self.request_close(panel)?;
}
self.queue.push_back(ShellEvent::MonitorsChanged);
Ok(())
}
pub fn title(&self, window: WindowId) -> Result<&str, ShellError> {
Ok(self.window(window)?.title.as_str())
}
pub fn cursor(&self, window: WindowId) -> Result<Option<CursorIcon>, ShellError> {
Ok(self.window(window)?.cursor)
}
#[must_use]
pub fn clipboard_bytes(&self, mime: MimeType) -> Option<&[u8]> {
self.clipboard
.iter()
.find(|(offered, _)| offered.matches(mime))
.map(|(_, bytes)| bytes.as_slice())
}
pub fn set_clipboard_readable(&mut self, readable: bool) {
self.clipboard_readable = readable;
}
pub fn set_clipboard_stalled(&mut self, stalled: bool) {
self.clipboard_stalled = stalled;
}
pub fn require_user_interaction(&mut self, required: bool) {
self.clipboard_needs_interaction = required;
}
pub fn clear_user_interaction(&mut self) {
self.saw_interaction = false;
}
pub fn clipboard_deadline(&mut self, pumps: u32) {
self.clipboard_deadline = pumps;
}
#[must_use]
pub fn held_clipboard_reads(&self) -> usize {
self.reads.len()
}
pub fn set_foreign_clipboard(&mut self, offers: Vec<(ReceivedMime, Vec<u8>)>) {
self.clipboard = offers;
}
fn answerable(&self) -> bool {
self.clipboard_readable && !self.clipboard_stalled
}
fn answer(&self, window: WindowId, request: ClipboardRequestId, mime: MimeType) -> ShellEvent {
let found = self
.clipboard
.iter()
.find(|(offered, _)| offered.matches(mime));
let (mime, content) = match found {
Some((offered, bytes)) => (offered.clone(), ClipboardContent::Bytes(bytes.clone())),
None => (ReceivedMime::from(mime), ClipboardContent::Empty),
};
ShellEvent::ClipboardData {
window,
request,
mime,
content,
}
}
fn resolve_reads(&mut self) {
if self.reads.is_empty() {
return;
}
let answerable = self.answerable();
let mut held = Vec::with_capacity(self.reads.len());
for mut read in core::mem::take(&mut self.reads) {
if self.window(read.window).is_err() {
continue;
}
if answerable {
let answer = self.answer(read.window, read.request, read.mime);
self.queue.push_back(answer);
continue;
}
if read.countdown > 0 {
read.countdown -= 1;
held.push(read);
continue;
}
self.queue.push_back(ShellEvent::ClipboardData {
window: read.window,
request: read.request,
mime: ReceivedMime::from(read.mime),
content: ClipboardContent::Unavailable,
});
}
self.reads = held;
}
fn deliver_due_configures(&mut self) {
let mut due: Vec<(WindowId, PendingConfigure, Option<f64>)> = Vec::new();
for (handle, state) in self.windows.iter_mut() {
let Some(pending) = state.pending.as_mut() else {
continue;
};
if pending.countdown > 0 {
pending.countdown -= 1;
continue;
}
let pending = state.pending.take().expect("checked just above");
let previous_scale = state.configuration.map(|config| config.scale_factor);
state.configuration = Some(WindowConfiguration {
size: pending.size,
scale_factor: pending.scale_factor,
mode: pending.mode,
});
due.push((handle.cast(), pending, previous_scale));
}
for (window, pending, previous_scale) in due {
if previous_scale.is_some_and(|previous| previous != pending.scale_factor) {
self.queue.push_back(ShellEvent::ScaleFactorChanged {
window,
scale_factor: pending.scale_factor,
size: pending.size,
});
}
self.queue.push_back(ShellEvent::Resized {
window,
size: pending.size,
scale_factor: pending.scale_factor,
});
}
}
fn window(&self, window: WindowId) -> Result<&HeadlessWindow, ShellError> {
self.windows
.get(window.cast())
.ok_or_else(|| ShellError::invalid_window(window))
}
fn window_mut(&mut self, window: WindowId) -> Result<&mut HeadlessWindow, ShellError> {
self.windows
.get_mut(window.cast())
.ok_or_else(|| ShellError::invalid_window(window))
}
fn check_window(&self, window: WindowId) -> Result<(), ShellError> {
self.window(window).map(|_| ())
}
fn unsupported(&self, what: &'static str) -> ShellError {
ShellError::Unsupported {
backend: ShellBackend::Headless,
what,
}
}
}
fn synthetic_scancode(key: KeyCode) -> Scancode {
let index = KeyCode::ALL
.iter()
.position(|candidate| *candidate == key)
.unwrap_or(0);
Scancode(index as u32 + 8)
}
fn synthetic_keysym(key: KeyCode) -> Keysym {
let name = key.as_str();
if let Some(letter) = name.strip_prefix("Key") {
return Keysym::from_char(letter.to_ascii_lowercase().chars().next().unwrap_or('?'));
}
if let Some(digit) = name.strip_prefix("Digit") {
return Keysym::from_char(digit.chars().next().unwrap_or('?'));
}
match key {
KeyCode::Space => Keysym::from_char(' '),
KeyCode::Minus => Keysym::from_char('-'),
KeyCode::Equal => Keysym::from_char('='),
KeyCode::Comma => Keysym::from_char(','),
KeyCode::Period => Keysym::from_char('.'),
KeyCode::Slash => Keysym::from_char('/'),
KeyCode::Semicolon => Keysym::from_char(';'),
KeyCode::Quote => Keysym::from_char('\''),
KeyCode::Backslash => Keysym::from_char('\\'),
KeyCode::BracketLeft => Keysym::from_char('['),
KeyCode::BracketRight => Keysym::from_char(']'),
KeyCode::Backquote => Keysym::from_char('`'),
_ => Keysym::NONE,
}
}
impl Shell for HeadlessShell {
fn backend(&self) -> ShellBackend {
ShellBackend::Headless
}
fn caps(&self) -> ShellCaps {
self.caps
}
fn create_window(&mut self, desc: &WindowDesc<'_>) -> Result<WindowId, ShellError> {
crate::role::check(desc, self.caps(), self.backend(), |monitor| {
self.monitor(monitor).is_some()
})?;
if !self.windows.is_empty() && !self.caps.contains(ShellCaps::MULTI_WINDOW) {
return Err(self.unsupported("a second window"));
}
if let DisplayMode::Borderless {
monitor: Some(monitor),
} = desc.mode
&& self.monitor(monitor).is_none()
{
return Err(ShellError::NoSuchMonitor(monitor.0));
}
let (size, scale_factor) = match desc.mode {
DisplayMode::Windowed => {
let scale_factor = self.monitors[0].scale_factor;
(
desc.constraints
.apply(desc.size.to_physical(scale_factor), scale_factor),
scale_factor,
)
}
DisplayMode::Borderless { monitor } => {
let monitor = monitor
.and_then(|id| self.monitor(id))
.unwrap_or(&self.monitors[0]);
(monitor.size(), monitor.scale_factor)
}
};
let (parent, panel_monitor) = match desc.role {
crate::WindowRole::Popup(popup) => {
self.window(popup.parent)?;
(Some(popup.parent), None)
}
crate::WindowRole::Panel(panel) => (None, panel.monitor),
crate::WindowRole::Toplevel => (None, None),
};
let countdown = self.configure_delay;
let window: WindowId = self
.windows
.insert(HeadlessWindow {
title: desc.title.to_string(),
configuration: None,
pending: Some(PendingConfigure {
size,
scale_factor,
mode: desc.mode,
countdown,
}),
requested_mode: desc.mode,
requested_constraints: desc.constraints,
focused: false,
visible: desc.visible,
pointer_mode: PointerMode::Free,
cursor: Some(CursorIcon::Default),
close_pending: false,
accept_drops: desc.accept_drops,
parent,
panel_monitor,
})
.cast();
Ok(window)
}
fn destroy_window(&mut self, window: WindowId) -> Result<(), ShellError> {
self.window(window)?;
let popups: Vec<WindowId> = self
.windows
.iter()
.filter(|(_, state)| state.parent == Some(window))
.map(|(id, _)| id.cast())
.collect();
for popup in popups {
self.destroy_window(popup)?;
}
self.windows
.remove(window.cast())
.ok_or_else(|| ShellError::invalid_window(window))?;
self.queue.push_back(ShellEvent::WindowDestroyed { window });
Ok(())
}
fn window_state(&self, window: WindowId) -> Result<WindowState, ShellError> {
let state = self.window(window)?;
Ok(WindowState {
configuration: state.configuration,
requested_mode: state.requested_mode,
requested_constraints: state.requested_constraints,
focused: state.focused,
visible: state.visible,
pointer_mode: state.pointer_mode,
close_pending: state.close_pending,
})
}
fn set_title(&mut self, window: WindowId, title: &str) -> Result<(), ShellError> {
self.window_mut(window)?.title = title.to_string();
Ok(())
}
fn set_visible(&mut self, window: WindowId, visible: bool) -> Result<(), ShellError> {
self.window_mut(window)?.visible = visible;
Ok(())
}
fn set_mode(&mut self, window: WindowId, mode: DisplayMode) -> Result<(), ShellError> {
let target = match mode {
DisplayMode::Windowed => None,
DisplayMode::Borderless { monitor } => {
let monitor = match monitor {
Some(id) => self
.monitor(id)
.ok_or(ShellError::NoSuchMonitor(id.0))?
.clone(),
None => self.monitors[0].clone(),
};
Some(monitor)
}
};
let countdown = self.configure_delay;
let state = self.window_mut(window)?;
state.requested_mode = mode;
let (size, scale_factor) = match &target {
Some(monitor) => (monitor.size(), monitor.scale_factor),
None => (state.base_size(), state.scale_factor()),
};
state.pending = Some(PendingConfigure {
size,
scale_factor,
mode,
countdown,
});
Ok(())
}
fn set_constraints(
&mut self,
window: WindowId,
constraints: SizeConstraints,
) -> Result<(), ShellError> {
let countdown = self.configure_delay;
let state = self.window_mut(window)?;
state.requested_constraints = constraints;
if let Some(pending) = state.pending.as_mut() {
pending.size = constraints.apply(pending.size, pending.scale_factor);
return Ok(());
}
if let Some(config) = state.configuration {
let constrained = constraints.apply(config.size, config.scale_factor);
if constrained != config.size {
state.pending = Some(PendingConfigure {
size: constrained,
scale_factor: config.scale_factor,
mode: config.mode,
countdown,
});
}
}
Ok(())
}
fn reposition_popup(
&mut self,
popup: WindowId,
anchor: LogicalRect,
size: LogicalSize,
) -> Result<(), ShellError> {
let _ = anchor;
let countdown = self.configure_delay;
let state = self.window_mut(popup)?;
if state.parent.is_none() {
return Err(crate::role::not_a_popup(popup));
}
if let Some(pending) = state.pending.as_mut() {
pending.size = size.to_physical(pending.scale_factor);
} else if let Some(config) = state.configuration {
state.pending = Some(PendingConfigure {
size: size.to_physical(config.scale_factor),
scale_factor: config.scale_factor,
mode: config.mode,
countdown,
});
}
Ok(())
}
fn monitors(&self) -> &[MonitorInfo] {
&self.monitors
}
fn pump(&mut self, sink: &mut dyn FnMut(ShellEvent)) {
self.keep_alive();
for _ in 0..self.queue.len() {
let Some(event) = self.queue.pop_front() else {
break;
};
sink(event);
}
}
fn keep_alive(&mut self) {
self.deliver_due_configures();
self.resolve_reads();
}
fn wait_events(&mut self, timeout: Option<Duration>) {
self.waits += 1;
if !self.caps.contains(ShellCaps::EVENT_WAIT) || !self.queue.is_empty() {
return;
}
match (&self.wake, timeout) {
(Some(wake), timeout) => wake.wait(timeout),
(None, Some(timeout)) => std::thread::sleep(timeout),
(None, None) => {}
}
}
fn waker(&mut self) -> Option<ShellWaker> {
let wake = self.wake.get_or_insert_with(Arc::default);
Some(ShellWaker::new(Arc::clone(wake) as Arc<dyn WakeSignal>))
}
fn align_event_clock(&mut self, elapsed: Duration) {
self.set_time(elapsed);
}
fn surface_target(&self, window: WindowId) -> Result<SurfaceTarget, ShellError> {
self.window(window)?;
Ok(SurfaceTarget::Offscreen)
}
fn set_pointer_mode(&mut self, window: WindowId, mode: PointerMode) -> Result<(), ShellError> {
if !self.caps.contains(mode.required_cap()) {
return Err(self.unsupported(mode.as_str()));
}
self.window_mut(window)?.pointer_mode = mode;
Ok(())
}
fn set_cursor(
&mut self,
window: WindowId,
cursor: Option<CursorIcon>,
) -> Result<(), ShellError> {
self.window_mut(window)?.cursor = cursor;
Ok(())
}
fn warp_pointer(
&mut self,
window: WindowId,
position: PhysicalPoint,
) -> Result<(), ShellError> {
if !self.caps.contains(ShellCaps::POINTER_WARP) {
return Err(self.unsupported("pointer warp"));
}
self.check_window(window)?;
let event = ShellEvent::PointerMotion {
window,
device: VIRTUAL_DEVICE,
time: self.now(),
abs: Some(position),
raw_delta: None,
};
self.queue.push_back(event);
Ok(())
}
fn reply_close_request(
&mut self,
window: WindowId,
reply: CloseReply,
) -> Result<(), ShellError> {
let state = self.window_mut(window)?;
if !state.close_pending {
return Err(ShellError::NoPendingCloseRequest { window });
}
state.close_pending = false;
match reply {
CloseReply::Close => self.destroy_window(window),
CloseReply::Keep => Ok(()),
}
}
fn clipboard_offer(
&mut self,
window: WindowId,
offers: &[ClipboardOffer<'_>],
) -> Result<(), ShellError> {
if !self.caps.contains(ShellCaps::CLIPBOARD) {
return Err(self.unsupported("clipboard"));
}
self.check_window(window)?;
if self.clipboard_needs_interaction && !self.saw_interaction {
return Err(ShellError::NeedsUserInteraction {
backend: ShellBackend::Headless,
what: "claiming the clipboard",
});
}
self.clipboard = offers
.iter()
.map(|offer| (ReceivedMime::from(offer.mime), offer.bytes.to_vec()))
.collect();
Ok(())
}
fn clipboard_request(
&mut self,
window: WindowId,
mime: MimeType,
) -> Result<ClipboardRequestId, ShellError> {
if !self.caps.contains(ShellCaps::CLIPBOARD) {
return Err(self.unsupported("clipboard"));
}
self.check_window(window)?;
let request = ClipboardRequestId(self.next_request_id);
self.next_request_id += 1;
if self.answerable() {
let answer = self.answer(window, request, mime);
self.queue.push_back(answer);
} else {
self.reads.push(PendingRead {
window,
request,
mime,
countdown: self.clipboard_deadline,
});
}
Ok(request)
}
fn clipboard_readable(&self, window: WindowId) -> bool {
self.caps.contains(ShellCaps::CLIPBOARD) && self.window(window).is_ok() && self.answerable()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{AspectRatio, LogicalSize};
fn shell_with_window() -> (HeadlessShell, WindowId) {
let mut shell = HeadlessShell::new();
let window = shell
.create_window(&WindowDesc::default())
.expect("headless window creation never fails");
(shell, window)
}
fn shell_with_configured_window() -> (HeadlessShell, WindowId) {
let (mut shell, window) = shell_with_window();
settle(&mut shell);
assert!(shell.window_state(window).expect("state").is_configured());
(shell, window)
}
fn settle(shell: &mut HeadlessShell) {
for _ in 0..8 {
let mut seen = 0;
shell.pump(&mut |_| seen += 1);
if seen == 0 && shell.windows.iter().all(|(_, w)| w.pending.is_none()) {
return;
}
}
panic!("the shell never settled");
}
fn drain(shell: &mut HeadlessShell) -> Vec<ShellEvent> {
let mut events = Vec::new();
shell.pump(&mut |event| events.push(event));
events
}
fn names(events: &[ShellEvent]) -> Vec<&'static str> {
events.iter().map(ShellEvent::name).collect()
}
fn create_swapchain(state: &WindowState) -> Result<PhysicalSize, &'static str> {
let size = state.size().ok_or("window is not configured yet")?;
if size.is_empty() {
return Err("zero-extent swapchain");
}
Ok(size)
}
#[test]
fn a_new_window_has_no_size_until_a_later_pump() {
let (mut shell, window) = shell_with_window();
let state = shell.window_state(window).expect("state");
assert!(!state.is_configured());
assert_eq!(state.size(), None);
assert_eq!(state.scale_factor(), None);
assert_eq!(state.effective_mode(), None);
assert_eq!(state.requested_mode, DisplayMode::Windowed);
assert_eq!(drain(&mut shell), Vec::new());
assert!(!shell.window_state(window).expect("state").is_configured());
let events = drain(&mut shell);
assert_eq!(
events,
vec![ShellEvent::Resized {
window,
size: PhysicalSize::new(1280, 720),
scale_factor: 1.0,
}]
);
let state = shell.window_state(window).expect("state");
assert_eq!(state.size(), Some(PhysicalSize::new(1280, 720)));
assert_eq!(state.scale_factor(), Some(1.0));
assert_eq!(state.effective_mode(), Some(DisplayMode::Windowed));
assert!(state.mode_request_honoured());
}
#[test]
fn keep_alive_takes_a_turn_and_keeps_everything_for_the_next_pump() {
let (mut shell, window) = shell_with_window();
shell.inject(ShellEvent::CloseRequested { window });
for _ in 0..=DEFAULT_CONFIGURE_DELAY {
shell.keep_alive();
}
assert!(
shell.window_state(window).expect("state").is_configured(),
"every turn the configure waits for was taken by keep_alive"
);
assert_eq!(
drain(&mut shell),
vec![
ShellEvent::CloseRequested { window },
ShellEvent::Resized {
window,
size: PhysicalSize::new(1280, 720),
scale_factor: 1.0,
},
],
"nothing keep_alive saw was delivered or dropped"
);
assert_eq!(drain(&mut shell), Vec::new(), "and each arrived once");
}
#[test]
fn a_consumer_that_skips_the_wait_gets_a_clean_error_not_a_wrong_extent() {
let (mut shell, window) = shell_with_window();
assert_eq!(
create_swapchain(&shell.window_state(window).expect("state")),
Err("window is not configured yet")
);
let target = shell.surface_target(window).expect("the handle exists");
assert_eq!(target, SurfaceTarget::Offscreen);
let size = loop {
shell.pump(&mut |_| {});
if let Ok(size) = create_swapchain(&shell.window_state(window).expect("state")) {
break size;
}
};
assert_eq!(size, PhysicalSize::new(1280, 720));
assert_eq!(
shell.surface_target(window).expect("target"),
target,
"the target is invariant under a configure"
);
}
#[test]
fn the_configure_delay_is_a_knob() {
let mut shell = HeadlessShell::new().with_configure_delay(3);
let window = shell.create_window(&WindowDesc::default()).expect("window");
for pump in 0..3 {
assert_eq!(drain(&mut shell), Vec::new(), "pump {pump}");
assert!(!shell.window_state(window).expect("state").is_configured());
}
assert_eq!(names(&drain(&mut shell)), ["Resized"]);
assert!(shell.window_state(window).expect("state").is_configured());
let mut shell = HeadlessShell::new().with_configure_delay(0);
let window = shell.create_window(&WindowDesc::default()).expect("window");
assert_eq!(names(&drain(&mut shell)), ["Resized"]);
assert!(shell.window_state(window).expect("state").is_configured());
shell.set_configure_delay(2);
shell
.set_mode(window, DisplayMode::Borderless { monitor: None })
.expect("request");
assert_eq!(drain(&mut shell), Vec::new());
assert_eq!(drain(&mut shell), Vec::new());
assert_eq!(names(&drain(&mut shell)), ["Resized"]);
}
#[test]
fn set_mode_is_a_request_whose_effect_arrives_later() {
let (mut shell, window) = shell_with_configured_window();
shell
.set_mode(window, DisplayMode::Borderless { monitor: None })
.expect("request");
let state = shell.window_state(window).expect("state");
assert_eq!(
state.requested_mode,
DisplayMode::Borderless { monitor: None }
);
assert_eq!(
state.effective_mode(),
Some(DisplayMode::Windowed),
"still windowed until the window system says otherwise"
);
assert!(
!state.mode_request_honoured(),
"a UI toggle reading the request instead of the effect would lie here"
);
assert_eq!(state.size(), Some(PhysicalSize::new(1280, 720)));
settle(&mut shell);
let state = shell.window_state(window).expect("state");
assert_eq!(
state.effective_mode(),
Some(DisplayMode::Borderless { monitor: None })
);
assert!(state.mode_request_honoured());
assert_eq!(state.size(), Some(PhysicalSize::new(1920, 1080)));
}
#[test]
fn a_refused_mode_is_expressible_through_injection() {
let (mut shell, window) = shell_with_configured_window();
shell
.set_mode(window, DisplayMode::Borderless { monitor: None })
.expect("request");
shell
.resize(window, PhysicalSize::new(1280, 720))
.expect("answer");
let state = shell.window_state(window).expect("state");
assert_eq!(state.effective_mode(), Some(DisplayMode::Windowed));
assert!(!state.mode_request_honoured());
}
#[test]
fn a_repositioned_popup_is_configured_at_its_new_size() {
let (mut shell, window) = shell_with_configured_window();
let anchor = crate::LogicalRect::new(
crate::LogicalPoint::new(10.0, 10.0),
LogicalSize::new(100.0, 20.0),
);
let popup = shell
.create_window(&WindowDesc {
size: LogicalSize::new(200.0, 150.0),
role: crate::WindowRole::Popup(crate::PopupDesc {
parent: window,
anchor,
placement: crate::PopupPlacement::Below,
input: crate::PopupInput::Pointer,
}),
..WindowDesc::default()
})
.expect("a popup");
settle(&mut shell);
let scale = shell
.window_state(popup)
.expect("live")
.scale_factor()
.expect("configured");
let moved = LogicalSize::new(240.0, 90.0);
shell
.reposition_popup(popup, anchor, moved)
.expect("reposition");
settle(&mut shell);
assert_eq!(
shell.window_state(popup).expect("live").size(),
Some(moved.to_physical(scale))
);
assert!(matches!(
shell.reposition_popup(window, anchor, moved),
Err(ShellError::InvalidDescriptor(_))
));
}
#[test]
fn set_constraints_is_a_request_too() {
let (mut shell, window) = shell_with_configured_window();
let constraints = SizeConstraints::min(LogicalSize::new(1600.0, 900.0));
shell.set_constraints(window, constraints).expect("request");
let state = shell.window_state(window).expect("state");
assert_eq!(state.requested_constraints, constraints);
assert_eq!(
state.size(),
Some(PhysicalSize::new(1280, 720)),
"the size only changes when the window system says so"
);
settle(&mut shell);
assert_eq!(
shell.window_state(window).expect("state").size(),
Some(PhysicalSize::new(1600, 900))
);
}
#[test]
fn constraints_do_not_cancel_a_mode_request_that_has_not_landed() {
let (mut shell, window) = shell_with_configured_window();
shell
.set_mode(window, DisplayMode::Borderless { monitor: None })
.expect("mode request");
shell
.set_constraints(window, SizeConstraints::min(LogicalSize::new(320.0, 180.0)))
.expect("constraint request");
settle(&mut shell);
let state = shell.window_state(window).expect("state");
assert_eq!(
state.effective_mode(),
Some(DisplayMode::Borderless { monitor: None })
);
assert!(state.mode_request_honoured());
}
#[test]
fn a_borderless_window_reports_its_own_monitors_scale() {
let mut shell = HeadlessShell::new();
let hidpi = shell.plug_monitor(MonitorInfo {
id: MonitorId(0),
name: "HEADLESS-2".to_string(),
bounds: PhysicalRect::new(1920, 0, 2560, 1440),
work_area: PhysicalRect::new(1920, 0, 2560, 1440),
scale_factor: 2.0,
refresh_millihertz: 143_998,
is_primary: false,
});
let window = shell
.create_window(&WindowDesc {
mode: DisplayMode::Borderless {
monitor: Some(hidpi),
},
..WindowDesc::default()
})
.expect("window");
settle(&mut shell);
let state = shell.window_state(window).expect("state");
assert_eq!(state.size(), Some(PhysicalSize::new(2560, 1440)));
assert_eq!(
state.scale_factor(),
Some(2.0),
"the scale is the borderless monitor's, not the primary's"
);
let other = shell
.create_window(&WindowDesc::default())
.expect("second window");
settle(&mut shell);
shell
.set_mode(
other,
DisplayMode::Borderless {
monitor: Some(hidpi),
},
)
.expect("request");
settle(&mut shell);
assert_eq!(
shell.window_state(other).expect("state").scale_factor(),
shell.window_state(window).expect("state").scale_factor()
);
}
#[test]
fn constraints_set_before_the_first_configure_fold_into_it() {
let mut shell = HeadlessShell::new();
let window = shell.create_window(&WindowDesc::default()).expect("window");
shell
.set_constraints(
window,
SizeConstraints::NONE.with_aspect(AspectRatio::SQUARE),
)
.expect("request");
settle(&mut shell);
assert_eq!(
shell.window_state(window).expect("state").size(),
Some(PhysicalSize::new(720, 720))
);
}
#[test]
fn creation_uses_the_requested_size_and_the_monitors_scale() {
let mut shell = HeadlessShell::new();
let window = shell
.create_window(&WindowDesc {
size: LogicalSize::new(800.0, 600.0),
..WindowDesc::default()
})
.expect("window");
settle(&mut shell);
assert_eq!(
shell.window_state(window).expect("state").size(),
Some(PhysicalSize::new(800, 600))
);
let mut shell = HeadlessShell::new().with_scale_factor(2.0);
let window = shell
.create_window(&WindowDesc {
size: LogicalSize::new(640.0, 360.0),
..WindowDesc::default()
})
.expect("window");
settle(&mut shell);
let state = shell.window_state(window).expect("state");
assert_eq!(state.size(), Some(PhysicalSize::new(1280, 720)));
assert_eq!(state.scale_factor(), Some(2.0));
}
#[test]
fn scale_changes_preserve_logical_size_and_report_both_facts() {
let (mut shell, window) = shell_with_configured_window();
shell
.change_scale_factor(window, 1.5)
.expect("scale change");
let events = drain(&mut shell);
assert_eq!(
events,
vec![
ShellEvent::ScaleFactorChanged {
window,
scale_factor: 1.5,
size: PhysicalSize::new(1920, 1080),
},
ShellEvent::Resized {
window,
size: PhysicalSize::new(1920, 1080),
scale_factor: 1.5,
},
]
);
assert_eq!(
shell.window_state(window).expect("state").size(),
Some(PhysicalSize::new(1920, 1080))
);
}
#[test]
fn a_scale_change_scheduled_by_a_mode_switch_arrives_before_the_resize() {
let mut shell = HeadlessShell::new();
let hidpi = shell.plug_monitor(MonitorInfo {
id: MonitorId(0),
name: "HEADLESS-2".to_string(),
bounds: PhysicalRect::new(1920, 0, 2560, 1440),
work_area: PhysicalRect::new(1920, 0, 2560, 1440),
scale_factor: 2.0,
refresh_millihertz: 143_998,
is_primary: false,
});
assert_eq!(hidpi, MonitorId(2), "ids are never reused");
let window = shell.create_window(&WindowDesc::default()).expect("window");
settle(&mut shell);
shell
.set_mode(
window,
DisplayMode::Borderless {
monitor: Some(hidpi),
},
)
.expect("request");
assert_eq!(drain(&mut shell), Vec::new(), "still only a request");
let events = drain(&mut shell);
assert_eq!(names(&events), ["ScaleFactorChanged", "Resized"]);
let state = shell.window_state(window).expect("state");
assert_eq!(state.size(), Some(PhysicalSize::new(2560, 1440)));
assert_eq!(state.scale_factor(), Some(2.0));
assert!(state.effective_mode().expect("configured").is_borderless());
assert_eq!(
shell.surface_target(window).expect("target"),
SurfaceTarget::Offscreen
);
assert!(matches!(
shell.set_mode(
window,
DisplayMode::Borderless {
monitor: Some(MonitorId(99)),
},
),
Err(ShellError::NoSuchMonitor(99))
));
}
#[test]
fn resizes_honour_constraints_and_inject_bypasses_them() {
let (mut shell, window) = shell_with_configured_window();
shell
.set_constraints(
window,
SizeConstraints::min(LogicalSize::new(640.0, 360.0))
.with_aspect(AspectRatio::WIDESCREEN),
)
.expect("request");
settle(&mut shell);
shell
.resize(window, PhysicalSize::new(1000, 1000))
.expect("resize");
assert_eq!(
drain(&mut shell),
vec![ShellEvent::Resized {
window,
size: PhysicalSize::new(1000, 562),
scale_factor: 1.0,
}]
);
shell.inject(ShellEvent::Resized {
window,
size: PhysicalSize::new(300, 900),
scale_factor: 1.0,
});
assert_eq!(
drain(&mut shell)[0],
ShellEvent::Resized {
window,
size: PhysicalSize::new(300, 900),
scale_factor: 1.0,
}
);
}
#[test]
fn a_compositor_configure_supersedes_a_pending_one() {
let (mut shell, window) = shell_with_configured_window();
shell
.set_mode(window, DisplayMode::Borderless { monitor: None })
.expect("request");
shell
.resize(window, PhysicalSize::new(640, 480))
.expect("answer");
assert_eq!(names(&drain(&mut shell)), ["Resized"]);
assert_eq!(
shell.window_state(window).expect("state").size(),
Some(PhysicalSize::new(640, 480))
);
assert_eq!(drain(&mut shell), Vec::new());
}
#[test]
fn timestamps_come_from_the_shells_clock_not_from_the_pump() {
let (mut shell, window) = shell_with_configured_window();
shell.set_time(Duration::from_secs(2));
shell.key_press(window, KeyCode::Space).expect("press");
shell.advance(Duration::from_millis(180));
shell.key_release(window, KeyCode::Space).expect("release");
let events = drain(&mut shell);
let held = events[1]
.time()
.expect("input")
.saturating_since(events[0].time().expect("input"));
assert_eq!(held, Duration::from_millis(180));
assert_eq!(events[0].time(), Some(EventTime::from_millis(2_000)));
}
#[test]
fn synthesized_key_fields_are_stable_and_plausible() {
let (mut shell, window) = shell_with_configured_window();
shell.set_modifiers(Modifiers::CTRL);
shell.key_press(window, KeyCode::KeyS).expect("press");
let events = drain(&mut shell);
let ShellEvent::Key {
scancode,
key_code,
keysym,
modifiers,
repeat,
device,
..
} = events[0].clone()
else {
panic!("wrong variant");
};
assert_eq!(key_code, Some(KeyCode::KeyS));
assert_eq!(keysym, Keysym::from_char('s'));
assert_eq!(modifiers, Modifiers::CTRL);
assert!(!repeat);
assert_eq!(device, VIRTUAL_DEVICE);
assert!(scancode.0 >= 8, "evdev-shaped scancodes start at 8");
assert_eq!(synthetic_keysym(KeyCode::Digit7), Keysym::from_char('7'));
assert_eq!(synthetic_keysym(KeyCode::F5), Keysym::NONE);
assert_eq!(synthetic_keysym(KeyCode::Slash), Keysym::from_char('/'));
shell.key_repeat(window, KeyCode::KeyS).expect("repeat");
let events = drain(&mut shell);
let ShellEvent::Key { repeat, .. } = events[0] else {
panic!("wrong variant");
};
assert!(repeat);
}
#[test]
fn a_locked_pointer_reports_no_absolute_position() {
let (mut shell, window) = shell_with_configured_window();
shell
.set_pointer_mode(window, PointerMode::Locked)
.expect("lock");
shell
.move_pointer(window, PhysicalPoint::new(10.0, 10.0), (3.0, -4.0))
.expect("motion");
let events = drain(&mut shell);
let ShellEvent::PointerMotion { abs, raw_delta, .. } = events[0] else {
panic!("wrong variant");
};
assert_eq!(abs, None);
assert_eq!(raw_delta, Some((3.0, -4.0)));
}
#[test]
fn a_backend_without_raw_motion_reports_none() {
let mut shell = HeadlessShell::new()
.with_caps(ShellCaps::DESKTOP - ShellCaps::RAW_POINTER_MOTION)
.with_configure_delay(0);
let window = shell.create_window(&WindowDesc::default()).expect("window");
drain(&mut shell);
shell
.move_pointer(window, PhysicalPoint::new(1.0, 2.0), (5.0, 5.0))
.expect("motion");
let events = drain(&mut shell);
let ShellEvent::PointerMotion { abs, raw_delta, .. } = events[0] else {
panic!("wrong variant");
};
assert_eq!(abs, Some(PhysicalPoint::new(1.0, 2.0)));
assert_eq!(raw_delta, None, "no relative-motion source");
assert!(!shell.caps().has_mouselook());
}
#[test]
fn pointer_focus_and_scroll_and_text_are_injectable() {
let (mut shell, window) = shell_with_configured_window();
shell
.set_pointer_focus(window, true, Some(PhysicalPoint::new(1.0, 1.0)))
.expect("enter");
shell
.scroll(window, ScrollDelta::Lines { x: 0.0, y: -1.0 }, None)
.expect("scroll");
shell
.button(
window,
PointerButton::Left,
ButtonState::Pressed,
Some(PhysicalPoint::new(1.0, 1.0)),
)
.expect("click");
shell.commit_text(window, "ありがとう").expect("text");
shell
.set_pointer_focus(window, false, Some(PhysicalPoint::new(1.0, 1.0)))
.expect("leave");
let events = drain(&mut shell);
assert_eq!(
names(&events),
[
"PointerFocus",
"Wheel",
"Button",
"TextCommit",
"PointerFocus"
]
);
let ShellEvent::PointerFocus {
entered, position, ..
} = &events[4]
else {
panic!("wrong variant");
};
assert!(!entered);
assert_eq!(*position, None, "a leave has no position");
assert!(events.iter().all(ShellEvent::is_input));
}
#[test]
fn contacts_are_injectable_and_a_touchless_shell_refuses_them() {
let (mut shell, window) = shell_with_configured_window();
assert!(shell.caps().contains(ShellCaps::TOUCH));
for (contact, phase, x) in [
(1, TouchPhase::Began, 10.0),
(2, TouchPhase::Began, 200.0),
(2, TouchPhase::Moved, 220.0),
(1, TouchPhase::Ended, 10.0),
(2, TouchPhase::Cancelled, 220.0),
] {
shell
.touch(
window,
ContactId(contact),
phase,
PhysicalPoint::new(x, 5.0),
)
.expect("a contact");
}
let events = drain(&mut shell);
assert_eq!(names(&events), ["Touch"; 5]);
let seen: Vec<(ContactId, TouchPhase, f64)> = events
.iter()
.map(|event| {
let ShellEvent::Touch {
contact,
phase,
position,
..
} = event
else {
panic!("wrong variant");
};
(*contact, *phase, position.x)
})
.collect();
assert_eq!(
seen,
vec![
(ContactId(1), TouchPhase::Began, 10.0),
(ContactId(2), TouchPhase::Began, 200.0),
(ContactId(2), TouchPhase::Moved, 220.0),
(ContactId(1), TouchPhase::Ended, 10.0),
(ContactId(2), TouchPhase::Cancelled, 220.0),
]
);
assert!(events.iter().all(ShellEvent::is_input));
let mut touchless = HeadlessShell::new()
.with_caps(ShellCaps::DESKTOP - ShellCaps::TOUCH)
.with_configure_delay(0);
let window = touchless
.create_window(&WindowDesc::default())
.expect("window");
drain(&mut touchless);
assert!(matches!(
touchless.touch(
window,
ContactId(1),
TouchPhase::Began,
PhysicalPoint::ORIGIN
),
Err(ShellError::Unsupported { .. })
));
assert!(drain(&mut touchless).is_empty(), "and queued nothing");
}
#[test]
fn warping_produces_motion_like_every_platform_that_has_it() {
let (mut shell, window) = shell_with_configured_window();
shell
.warp_pointer(window, PhysicalPoint::new(320.0, 180.0))
.expect("warp");
let events = drain(&mut shell);
let ShellEvent::PointerMotion { abs, raw_delta, .. } = events[0] else {
panic!("wrong variant");
};
assert_eq!(abs, Some(PhysicalPoint::new(320.0, 180.0)));
assert_eq!(raw_delta, None, "a warp is not physical motion");
}
#[test]
fn capabilities_gate_the_operations_that_need_them() {
let mut shell = HeadlessShell::new().with_caps(ShellCaps::empty());
let window = shell.create_window(&WindowDesc::default()).expect("window");
assert!(matches!(
shell.warp_pointer(window, PhysicalPoint::ORIGIN),
Err(ShellError::Unsupported { .. })
));
assert!(matches!(
shell.set_pointer_mode(window, PointerMode::Locked),
Err(ShellError::Unsupported { .. })
));
assert!(matches!(
shell.clipboard_request(window, MimeType::TextUtf8),
Err(ShellError::Unsupported { .. })
));
shell
.set_pointer_mode(window, PointerMode::Free)
.expect("Free is always available");
assert!(matches!(
shell.create_window(&WindowDesc::default()),
Err(ShellError::Unsupported { .. })
));
}
#[test]
fn stale_handles_fail_cleanly_on_every_accessor() {
let (mut shell, window) = shell_with_configured_window();
shell.destroy_window(window).expect("destroy");
assert_eq!(
drain(&mut shell),
vec![ShellEvent::WindowDestroyed { window }]
);
assert!(matches!(
shell.window_state(window),
Err(ShellError::InvalidWindow { .. })
));
assert!(matches!(
shell.surface_target(window),
Err(ShellError::InvalidWindow { .. })
));
assert!(matches!(
shell.resize(window, PhysicalSize::new(1, 1)),
Err(ShellError::InvalidWindow { .. })
));
assert!(matches!(
shell.set_title(window, "gone"),
Err(ShellError::InvalidWindow { .. })
));
assert!(matches!(
shell.destroy_window(window),
Err(ShellError::InvalidWindow { .. })
));
assert!(matches!(
shell.key_press(window, KeyCode::KeyA),
Err(ShellError::InvalidWindow { .. })
));
}
#[test]
fn destroying_a_window_cancels_its_pending_configure() {
let (mut shell, window) = shell_with_window();
shell.destroy_window(window).expect("destroy");
assert_eq!(names(&drain(&mut shell)), ["WindowDestroyed"]);
assert_eq!(drain(&mut shell), Vec::new());
assert_eq!(drain(&mut shell), Vec::new());
}
#[test]
fn close_requests_are_questions() {
let (mut shell, window) = shell_with_configured_window();
assert!(matches!(
shell.reply_close_request(window, CloseReply::Close),
Err(ShellError::NoPendingCloseRequest { .. })
));
shell.request_close(window).expect("request");
assert!(shell.window_state(window).expect("state").close_pending);
shell
.reply_close_request(window, CloseReply::Keep)
.expect("keep");
assert!(!shell.window_state(window).expect("state").close_pending);
assert!(shell.window_state(window).is_ok(), "still open");
shell.request_close(window).expect("request again");
shell
.reply_close_request(window, CloseReply::Close)
.expect("close");
assert!(matches!(
shell.window_state(window),
Err(ShellError::InvalidWindow { .. })
));
}
#[test]
fn monitor_hotplug_reports_a_change_and_keeps_ids_unique() {
let mut shell = HeadlessShell::new();
let extra = shell.plug_monitor(MonitorInfo {
id: MonitorId(0),
name: "HEADLESS-2".to_string(),
bounds: PhysicalRect::new(1920, 0, 1920, 1080),
work_area: PhysicalRect::new(1920, 0, 1920, 1080),
scale_factor: 1.0,
refresh_millihertz: 60_000,
is_primary: false,
});
assert_eq!(shell.monitors().len(), 2);
assert_eq!(drain(&mut shell), vec![ShellEvent::MonitorsChanged]);
shell.unplug_monitor(extra).expect("unplug");
assert_eq!(shell.monitors().len(), 1);
assert_eq!(
drain(&mut shell),
vec![
ShellEvent::MonitorRemoved { monitor: extra },
ShellEvent::MonitorsChanged
]
);
assert!(matches!(
shell.unplug_monitor(extra),
Err(ShellError::NoSuchMonitor(_))
));
assert_eq!(
shell.monitor(MonitorId(1)).map(|m| m.name.as_str()),
Some("HEADLESS-1")
);
assert!(shell.monitor(extra).is_none());
}
#[test]
fn unplugging_a_monitor_asks_its_panels_and_only_its_panels_to_close() {
let mut shell = HeadlessShell::new();
let second = shell.plug_monitor(MonitorInfo {
id: MonitorId(0),
name: "HEADLESS-2".to_string(),
bounds: PhysicalRect::new(1920, 0, 1920, 1080),
work_area: PhysicalRect::new(1920, 0, 1920, 1080),
scale_factor: 1.0,
refresh_millihertz: 60_000,
is_primary: false,
});
let first = shell.monitors()[0].id;
let panel_on = |shell: &mut HeadlessShell, monitor| {
shell
.create_window(&WindowDesc {
role: crate::WindowRole::Panel(crate::PanelDesc {
monitor,
..crate::PanelDesc::default()
}),
..WindowDesc::default()
})
.expect("panel")
};
let doomed = panel_on(&mut shell, Some(second));
let already_asked = panel_on(&mut shell, Some(second));
let elsewhere = panel_on(&mut shell, Some(first));
let anywhere = panel_on(&mut shell, None);
let toplevel = shell.create_window(&WindowDesc::default()).expect("window");
shell.request_close(already_asked).expect("close");
settle(&mut shell);
drain(&mut shell);
shell.unplug_monitor(second).expect("unplug");
assert_eq!(
drain(&mut shell),
vec![
ShellEvent::MonitorRemoved { monitor: second },
ShellEvent::CloseRequested { window: doomed },
ShellEvent::MonitorsChanged,
]
);
for window in [elsewhere, anywhere, toplevel] {
assert!(!shell.window_state(window).expect("open").close_pending);
}
}
#[test]
fn drops_require_both_the_capability_and_the_opt_in() {
let mut shell = HeadlessShell::new();
let plain = shell.create_window(&WindowDesc::default()).expect("window");
assert!(matches!(
shell.drop_file(plain, "/tmp/a.gltf", None),
Err(ShellError::Unsupported { .. })
));
let dropper = shell
.create_window(&WindowDesc {
accept_drops: true,
..WindowDesc::default()
})
.expect("window");
settle(&mut shell);
shell
.drop_file(dropper, "/tmp/a.gltf", Some(PhysicalPoint::new(4.0, 5.0)))
.expect("drop");
assert_eq!(names(&drain(&mut shell))[0], "DroppedFile");
}
#[test]
fn a_sink_that_queues_more_events_does_not_spin_the_pump() {
let (mut shell, window) = shell_with_configured_window();
shell.inject(ShellEvent::MonitorsChanged);
let mut extra = Vec::new();
shell.pump(&mut |event| extra.push(event));
assert_eq!(extra.len(), 1);
shell.inject(ShellEvent::CloseRequested { window });
assert_eq!(shell.pending_events(), 1);
}
#[test]
fn accessors_that_no_real_backend_can_provide_are_headless_only() {
let (mut shell, window) = shell_with_configured_window();
shell.set_title(window, "renamed").expect("title");
assert_eq!(shell.title(window).expect("title"), "renamed");
shell.set_cursor(window, None).expect("hide");
assert_eq!(shell.cursor(window).expect("cursor"), None);
shell
.set_cursor(window, Some(CursorIcon::Crosshair))
.expect("shape");
assert_eq!(
shell.cursor(window).expect("cursor"),
Some(CursorIcon::Crosshair)
);
shell.set_visible(window, false).expect("hide window");
assert!(!shell.window_state(window).expect("state").visible);
}
#[test]
fn waiting_is_observable_and_focus_transitions_are_events() {
let (mut shell, window) = shell_with_configured_window();
assert_eq!(shell.wait_count(), 0);
shell.wait_events(Some(Duration::from_millis(5)));
shell.wait_events(None);
assert_eq!(shell.wait_count(), 2);
shell.set_focus(window, true).expect("focus");
assert!(shell.window_state(window).expect("state").focused);
shell.set_focus(window, false).expect("unfocus");
assert_eq!(names(&drain(&mut shell)), ["Focus", "Focus"]);
}
const LONG_WAIT: Duration = Duration::from_secs(60);
const RING_AFTER: Duration = Duration::from_millis(50);
fn ring_later(waker: ShellWaker) -> std::thread::JoinHandle<()> {
std::thread::spawn(move || {
std::thread::sleep(RING_AFTER);
waker.wake();
})
}
#[test]
fn a_wake_from_another_thread_ends_a_long_wait() {
let mut shell = HeadlessShell::new();
let ringer = ring_later(shell.waker().expect("headless has a waker"));
let start = std::time::Instant::now();
shell.wait_events(Some(LONG_WAIT));
let slept = start.elapsed();
ringer.join().expect("the ringer");
assert!(slept < LONG_WAIT / 2, "the wait ran {slept:?}");
}
#[test]
fn a_wake_before_the_wait_ends_it_at_once_and_wakes_coalesce() {
let mut shell = HeadlessShell::new();
let waker = shell.waker().expect("headless has a waker");
for _ in 0..3 {
waker.wake();
}
let start = std::time::Instant::now();
shell.wait_events(Some(LONG_WAIT));
assert!(
start.elapsed() < LONG_WAIT / 2,
"a rung bell must end the wait"
);
let timeout = Duration::from_millis(100);
let start = std::time::Instant::now();
shell.wait_events(Some(timeout));
assert!(
start.elapsed() >= timeout / 2,
"the bell was cleared, so this wait must sleep, but it took {:?}",
start.elapsed()
);
}
#[test]
fn with_a_waker_out_an_untimed_wait_sleeps_until_it_rings() {
let mut shell = HeadlessShell::new();
let ringer = ring_later(shell.waker().expect("headless has a waker"));
let start = std::time::Instant::now();
shell.wait_events(None);
let slept = start.elapsed();
ringer.join().expect("the ringer");
assert!(
slept >= RING_AFTER / 2,
"an untimed wait returned after {slept:?}, before anything rang"
);
}
#[test]
fn the_clipboard_round_trips_through_an_event() {
let (mut shell, window) = shell_with_configured_window();
shell
.clipboard_offer(
window,
&[
ClipboardOffer::text("node"),
ClipboardOffer::ron("(kind:\"node\")"),
],
)
.expect("offer");
assert_eq!(
shell.clipboard_bytes(MimeType::TextUtf8),
Some(&b"node"[..])
);
let request = shell
.clipboard_request(window, MimeType::CrcblRon)
.expect("request");
assert_eq!(
drain(&mut shell)[0],
ShellEvent::ClipboardData {
window,
request,
mime: ReceivedMime::from(MimeType::CrcblRon),
content: ClipboardContent::Bytes(b"(kind:\"node\")".to_vec()),
}
);
let request = shell
.clipboard_request(window, MimeType::UriList)
.expect("request");
assert_eq!(
drain(&mut shell)[0],
ShellEvent::ClipboardData {
window,
request,
mime: ReceivedMime::from(MimeType::UriList),
content: ClipboardContent::Empty,
}
);
shell
.clipboard_offer(window, &[ClipboardOffer::text("")])
.expect("offer");
let request = shell
.clipboard_request(window, MimeType::TextUtf8)
.expect("request");
assert_eq!(
drain(&mut shell)[0],
ShellEvent::ClipboardData {
window,
request,
mime: ReceivedMime::from(MimeType::TextUtf8),
content: ClipboardContent::Bytes(Vec::new()),
}
);
}
#[test]
fn a_read_taken_while_the_clipboard_is_unreadable_is_held_not_answered_empty() {
let (mut shell, window) = shell_with_configured_window();
shell
.clipboard_offer(window, &[ClipboardOffer::text("published")])
.expect("offer");
shell.set_clipboard_readable(false);
assert!(!shell.clipboard_readable(window));
let request = shell
.clipboard_request(window, MimeType::TextUtf8)
.expect("a read is accepted even when it cannot be answered yet");
assert_eq!(shell.held_clipboard_reads(), 1);
assert!(
drain(&mut shell).is_empty(),
"a held read produces no event at all — least of all an empty one"
);
shell.set_clipboard_readable(true);
let events = drain(&mut shell);
assert_eq!(events.len(), 1);
assert_eq!(
events[0],
ShellEvent::ClipboardData {
window,
request,
mime: ReceivedMime::from(MimeType::TextUtf8),
content: ClipboardContent::Bytes(b"published".to_vec()),
}
);
assert_eq!(shell.held_clipboard_reads(), 0);
}
#[test]
fn a_read_that_never_becomes_answerable_ends_as_unavailable() {
let (mut shell, window) = shell_with_configured_window();
shell
.clipboard_offer(window, &[ClipboardOffer::text("unreachable")])
.expect("offer");
shell.set_clipboard_readable(false);
shell.clipboard_deadline(2);
let request = shell
.clipboard_request(window, MimeType::TextUtf8)
.expect("request");
assert!(drain(&mut shell).is_empty(), "pump 1: still held");
assert!(drain(&mut shell).is_empty(), "pump 2: still held");
let events = drain(&mut shell);
assert_eq!(
events[0],
ShellEvent::ClipboardData {
window,
request,
mime: ReceivedMime::from(MimeType::TextUtf8),
content: ClipboardContent::Unavailable,
}
);
assert_eq!(shell.held_clipboard_reads(), 0);
}
#[test]
fn a_peer_that_holds_the_clipboard_and_never_delivers_is_unavailable() {
let (mut shell, window) = shell_with_configured_window();
shell.set_foreign_clipboard(vec![(
ReceivedMime::new("text/plain"),
b"never sent".to_vec(),
)]);
shell.set_clipboard_stalled(true);
shell.clipboard_deadline(1);
assert!(
!shell.clipboard_readable(window),
"a clipboard whose owner will not answer is not readable"
);
let request = shell
.clipboard_request(window, MimeType::TextUtf8)
.expect("request");
assert!(drain(&mut shell).is_empty());
let events = drain(&mut shell);
let ShellEvent::ClipboardData {
request: answered,
content,
..
} = &events[0]
else {
panic!("wrong variant");
};
assert_eq!(*answered, request);
assert_eq!(*content, ClipboardContent::Unavailable);
shell.set_clipboard_stalled(false);
let request = shell
.clipboard_request(window, MimeType::TextUtf8)
.expect("request");
assert_eq!(
drain(&mut shell)[0],
ShellEvent::ClipboardData {
window,
request,
mime: ReceivedMime::new("text/plain"),
content: ClipboardContent::Bytes(b"never sent".to_vec()),
}
);
}
#[test]
fn claiming_the_clipboard_can_require_a_user_interaction() {
let (mut shell, window) = shell_with_configured_window();
shell.require_user_interaction(true);
let refused = shell.clipboard_offer(window, &[ClipboardOffer::text("stolen")]);
assert!(
matches!(
refused,
Err(ShellError::NeedsUserInteraction {
backend: ShellBackend::Headless,
..
})
),
"{refused:?}"
);
assert!(
shell.clipboard_bytes(MimeType::TextUtf8).is_none(),
"a refused claim must not have taken the clipboard anyway"
);
shell.key_press(window, KeyCode::KeyC).expect("key");
shell
.clipboard_offer(window, &[ClipboardOffer::text("copied")])
.expect("with a serial in hand, the claim goes through");
assert_eq!(
shell.clipboard_bytes(MimeType::TextUtf8),
Some(&b"copied"[..])
);
shell.clear_user_interaction();
assert!(matches!(
shell.clipboard_offer(window, &[ClipboardOffer::text("later")]),
Err(ShellError::NeedsUserInteraction { .. })
));
}
#[test]
fn a_held_read_for_a_destroyed_window_is_dropped_rather_than_answered() {
let (mut shell, window) = shell_with_configured_window();
shell.set_clipboard_readable(false);
let _request = shell
.clipboard_request(window, MimeType::TextUtf8)
.expect("request");
assert_eq!(shell.held_clipboard_reads(), 1);
shell.destroy_window(window).expect("destroy");
let events = drain(&mut shell);
assert_eq!(shell.held_clipboard_reads(), 0);
assert!(
!events
.iter()
.any(|event| matches!(event, ShellEvent::ClipboardData { .. })),
"no clipboard answer may name a destroyed window: {events:?}"
);
}
#[test]
fn a_paste_from_a_foreign_application_keeps_the_foreign_spelling() {
let (mut shell, window) = shell_with_configured_window();
shell.set_foreign_clipboard(vec![
(ReceivedMime::new("text/plain"), b"from gedit".to_vec()),
(
ReceivedMime::new("application/vnd.some-editor.scene+json"),
b"{}".to_vec(),
),
]);
let request = shell
.clipboard_request(window, MimeType::TextUtf8)
.expect("request");
let events = drain(&mut shell);
let ShellEvent::ClipboardData {
request: answered,
mime,
content,
..
} = &events[0]
else {
panic!("wrong variant");
};
assert_eq!(*answered, request);
assert_eq!(content.bytes(), Some(&b"from gedit"[..]));
assert_eq!(mime.as_str(), "text/plain");
assert!(mime.matches(MimeType::TextUtf8));
assert_ne!(*mime, ReceivedMime::from(MimeType::TextUtf8));
assert_eq!(
shell.clipboard_bytes(MimeType::Other("application/vnd.some-editor.scene+json")),
Some(&b"{}"[..])
);
}
#[test]
#[should_panic(expected = "a shell needs at least one monitor")]
fn a_shell_with_no_monitors_is_rejected() {
let _ = HeadlessShell::new().with_monitors(Vec::new());
}
#[test]
#[should_panic(expected = "a text commit is never empty")]
fn an_empty_text_commit_is_rejected() {
let (mut shell, window) = shell_with_configured_window();
let _ = shell.commit_text(window, "");
}
}