1use std::collections::VecDeque;
135use std::path::PathBuf;
136use std::sync::{Arc, Condvar, Mutex, PoisonError};
137
138use core::time::Duration;
139
140use crcbl_core::input::{
141 ButtonState, ContactId, DeviceId, Keysym, Modifiers, PointerButton, Scancode, ScrollDelta,
142 TouchPhase,
143};
144use crcbl_core::time::{ManualTime, TimeSource};
145use crcbl_core::{EventTime, Pool, SurfaceTarget};
146
147use crate::waker::WakeSignal;
148use crate::{
149 ClipboardContent, ClipboardOffer, ClipboardRequestId, CloseReply, CursorIcon, DisplayMode,
150 KeyCode, LogicalRect, LogicalSize, MimeType, MonitorId, MonitorInfo, PhysicalPoint,
151 PhysicalRect, PhysicalSize, PointerMode, ReceivedMime, Shell, ShellBackend, ShellCaps,
152 ShellError, ShellEvent, ShellWaker, SizeConstraints, WindowConfiguration, WindowDesc, WindowId,
153 WindowState,
154};
155
156pub const DEFAULT_MONITOR_SIZE: PhysicalSize = PhysicalSize::new(1920, 1080);
161
162pub const DEFAULT_REFRESH_MILLIHERTZ: u32 = 60_000;
164
165pub const DEFAULT_CONFIGURE_DELAY: u32 = 1;
171
172pub const VIRTUAL_DEVICE: DeviceId = DeviceId(1);
178
179pub const DEFAULT_CLIPBOARD_DEADLINE: u32 = 4;
187
188#[derive(Clone, Debug)]
194struct PendingRead {
195 window: WindowId,
196 request: ClipboardRequestId,
197 mime: MimeType,
198 countdown: u32,
200}
201
202#[derive(Clone, Copy, Debug)]
206struct PendingConfigure {
207 size: PhysicalSize,
208 scale_factor: f64,
209 mode: DisplayMode,
210 countdown: u32,
212}
213
214#[derive(Clone, Debug)]
216struct HeadlessWindow {
217 title: String,
218 configuration: Option<WindowConfiguration>,
220 pending: Option<PendingConfigure>,
222 requested_mode: DisplayMode,
223 requested_constraints: SizeConstraints,
224 focused: bool,
225 visible: bool,
226 pointer_mode: PointerMode,
227 cursor: Option<CursorIcon>,
228 close_pending: bool,
229 accept_drops: bool,
230 parent: Option<WindowId>,
232 panel_monitor: Option<MonitorId>,
234}
235
236impl HeadlessWindow {
237 fn size(&self) -> Option<PhysicalSize> {
239 self.configuration.map(|config| config.size)
240 }
241
242 fn scale_factor(&self) -> f64 {
248 self.configuration
249 .map(|config| config.scale_factor)
250 .or_else(|| self.pending.map(|pending| pending.scale_factor))
251 .unwrap_or(1.0)
252 }
253
254 fn settled_mode(&self) -> DisplayMode {
263 self.configuration
264 .map(|config| config.mode)
265 .or_else(|| self.pending.map(|pending| pending.mode))
266 .unwrap_or(self.requested_mode)
267 }
268
269 fn base_size(&self) -> PhysicalSize {
271 self.size()
272 .or_else(|| self.pending.map(|pending| pending.size))
273 .unwrap_or(PhysicalSize::new(1, 1))
274 }
275}
276
277#[derive(Debug)]
282pub struct HeadlessShell {
283 caps: ShellCaps,
284 monitors: Vec<MonitorInfo>,
285 next_monitor_id: u32,
286 windows: Pool<HeadlessWindow>,
287 queue: VecDeque<ShellEvent>,
288 clock: ManualTime,
289 modifiers: Modifiers,
290 clipboard: Vec<(ReceivedMime, Vec<u8>)>,
291 next_request_id: u32,
292 reads: Vec<PendingRead>,
295 clipboard_readable: bool,
299 clipboard_stalled: bool,
301 clipboard_needs_interaction: bool,
303 saw_interaction: bool,
306 clipboard_deadline: u32,
309 waits: u32,
310 configure_delay: u32,
311 wake: Option<Arc<HeadlessWake>>,
313}
314
315#[derive(Debug, Default)]
318struct HeadlessWake {
319 rung: Mutex<bool>,
320 bell: Condvar,
321}
322
323impl HeadlessWake {
324 fn wait(&self, timeout: Option<Duration>) {
327 let rung = self.rung.lock().unwrap_or_else(PoisonError::into_inner);
328 let mut rung = match timeout {
329 Some(timeout) => {
330 self.bell
331 .wait_timeout_while(rung, timeout, |rung| !*rung)
332 .unwrap_or_else(PoisonError::into_inner)
333 .0
334 }
335 None => self
336 .bell
337 .wait_while(rung, |rung| !*rung)
338 .unwrap_or_else(PoisonError::into_inner),
339 };
340 *rung = false;
341 }
342}
343
344impl WakeSignal for HeadlessWake {
345 fn wake(&self) {
346 *self.rung.lock().unwrap_or_else(PoisonError::into_inner) = true;
347 self.bell.notify_all();
348 }
349}
350
351impl Default for HeadlessShell {
352 fn default() -> Self {
353 Self::new()
354 }
355}
356
357impl HeadlessShell {
358 #[must_use]
360 pub fn new() -> Self {
361 Self {
362 caps: ShellCaps::DESKTOP,
363 monitors: vec![MonitorInfo {
364 id: MonitorId(1),
365 name: "HEADLESS-1".to_string(),
366 bounds: PhysicalRect::new(
367 0,
368 0,
369 DEFAULT_MONITOR_SIZE.width,
370 DEFAULT_MONITOR_SIZE.height,
371 ),
372 work_area: PhysicalRect::new(
373 0,
374 0,
375 DEFAULT_MONITOR_SIZE.width,
376 DEFAULT_MONITOR_SIZE.height,
377 ),
378 scale_factor: 1.0,
379 refresh_millihertz: DEFAULT_REFRESH_MILLIHERTZ,
380 is_primary: true,
381 }],
382 next_monitor_id: 2,
383 windows: Pool::new(),
384 queue: VecDeque::new(),
385 clock: ManualTime::new(),
386 modifiers: Modifiers::empty(),
387 clipboard: Vec::new(),
388 next_request_id: 1,
389 reads: Vec::new(),
390 clipboard_readable: true,
391 clipboard_stalled: false,
392 clipboard_needs_interaction: false,
393 saw_interaction: false,
394 clipboard_deadline: DEFAULT_CLIPBOARD_DEADLINE,
395 waits: 0,
396 configure_delay: DEFAULT_CONFIGURE_DELAY,
397 wake: None,
398 }
399 }
400
401 #[must_use]
406 pub fn with_caps(mut self, caps: ShellCaps) -> Self {
407 self.caps = caps;
408 self
409 }
410
411 #[must_use]
426 pub fn with_monitors(mut self, monitors: Vec<MonitorInfo>) -> Self {
427 assert!(!monitors.is_empty(), "a shell needs at least one monitor");
428 for (index, monitor) in monitors.iter().enumerate() {
429 assert!(
430 monitor.id.0 != u32::MAX,
431 "monitor id {} leaves no id for the next hotplug",
432 monitor.id.0
433 );
434 assert!(
435 !monitors[..index].iter().any(|seen| seen.id == monitor.id),
436 "monitor id {} appears twice",
437 monitor.id.0
438 );
439 }
440 self.next_monitor_id = monitors
441 .iter()
442 .map(|monitor| monitor.id.0 + 1)
443 .max()
444 .unwrap_or(1);
445 self.monitors = monitors;
446 self
447 }
448
449 #[must_use]
454 pub fn with_scale_factor(mut self, scale_factor: f64) -> Self {
455 assert!(scale_factor > 0.0, "scale factor must be positive");
456 self.monitors[0].scale_factor = scale_factor;
457 self
458 }
459
460 #[must_use]
465 pub const fn with_configure_delay(mut self, pumps: u32) -> Self {
466 self.configure_delay = pumps;
467 self
468 }
469
470 pub const fn set_configure_delay(&mut self, pumps: u32) {
480 self.configure_delay = pumps;
481 }
482
483 pub fn set_caps(&mut self, caps: ShellCaps) {
489 self.caps = caps;
490 }
491
492 #[must_use]
494 pub fn now(&self) -> EventTime {
495 EventTime::from_duration(self.clock.elapsed())
496 }
497
498 pub fn advance(&mut self, delta: Duration) {
504 self.clock.advance(delta);
505 }
506
507 pub fn set_time(&mut self, now: Duration) {
515 self.clock.set(now);
516 }
517
518 pub fn set_modifiers(&mut self, modifiers: Modifiers) {
523 self.modifiers = modifiers;
524 }
525
526 #[must_use]
528 pub fn pending_events(&self) -> usize {
529 self.queue.len()
530 }
531
532 #[must_use]
538 pub const fn wait_count(&self) -> u32 {
539 self.waits
540 }
541
542 pub fn inject(&mut self, event: ShellEvent) {
550 self.queue.push_back(event);
551 }
552
553 pub fn resize(&mut self, window: WindowId, size: PhysicalSize) -> Result<(), ShellError> {
569 let state = self.window_mut(window)?;
570 let scale_factor = state.scale_factor();
571 let mode = state.settled_mode();
572 let constrained = state.requested_constraints.apply(size, scale_factor);
573 state.pending = None;
574 state.configuration = Some(WindowConfiguration {
575 size: constrained,
576 scale_factor,
577 mode,
578 });
579 self.queue.push_back(ShellEvent::Resized {
580 window,
581 size: constrained,
582 scale_factor,
583 });
584 Ok(())
585 }
586
587 pub fn change_scale_factor(
609 &mut self,
610 window: WindowId,
611 scale_factor: f64,
612 ) -> Result<(), ShellError> {
613 assert!(scale_factor > 0.0, "scale factor must be positive");
614 let state = self.window_mut(window)?;
615 let logical = state.base_size().to_logical(state.scale_factor());
616 let mode = state.settled_mode();
617 let size = state
618 .requested_constraints
619 .apply(logical.to_physical(scale_factor), scale_factor);
620 state.pending = None;
621 state.configuration = Some(WindowConfiguration {
622 size,
623 scale_factor,
624 mode,
625 });
626 self.queue.push_back(ShellEvent::ScaleFactorChanged {
627 window,
628 scale_factor,
629 size,
630 });
631 self.queue.push_back(ShellEvent::Resized {
632 window,
633 size,
634 scale_factor,
635 });
636 Ok(())
637 }
638
639 pub fn set_focus(&mut self, window: WindowId, focused: bool) -> Result<(), ShellError> {
645 self.window_mut(window)?.focused = focused;
646 self.queue.push_back(ShellEvent::Focus { window, focused });
647 Ok(())
648 }
649
650 pub fn request_close(&mut self, window: WindowId) -> Result<(), ShellError> {
659 self.window_mut(window)?.close_pending = true;
660 self.queue.push_back(ShellEvent::CloseRequested { window });
661 Ok(())
662 }
663
664 pub fn key(
677 &mut self,
678 window: WindowId,
679 key: KeyCode,
680 state: ButtonState,
681 ) -> Result<(), ShellError> {
682 self.key_with(window, key, state, false)
683 }
684
685 pub fn key_press(&mut self, window: WindowId, key: KeyCode) -> Result<(), ShellError> {
691 self.key(window, key, ButtonState::Pressed)
692 }
693
694 pub fn key_release(&mut self, window: WindowId, key: KeyCode) -> Result<(), ShellError> {
700 self.key(window, key, ButtonState::Released)
701 }
702
703 pub fn key_repeat(&mut self, window: WindowId, key: KeyCode) -> Result<(), ShellError> {
710 self.key_with(window, key, ButtonState::Pressed, true)
711 }
712
713 fn key_with(
714 &mut self,
715 window: WindowId,
716 key: KeyCode,
717 state: ButtonState,
718 repeat: bool,
719 ) -> Result<(), ShellError> {
720 self.check_window(window)?;
721 self.saw_interaction = true;
722 let event = ShellEvent::Key {
723 window,
724 device: VIRTUAL_DEVICE,
725 time: self.now(),
726 scancode: synthetic_scancode(key),
727 key_code: Some(key),
728 keysym: synthetic_keysym(key),
729 state,
730 repeat,
731 modifiers: self.modifiers,
732 };
733 self.queue.push_back(event);
734 Ok(())
735 }
736
737 pub fn move_pointer(
748 &mut self,
749 window: WindowId,
750 position: PhysicalPoint,
751 raw_delta: (f64, f64),
752 ) -> Result<(), ShellError> {
753 let locked = self.window(window)?.pointer_mode == PointerMode::Locked;
754 let raw = if self.caps.contains(ShellCaps::RAW_POINTER_MOTION) {
755 Some(raw_delta)
756 } else {
757 None
758 };
759 self.saw_interaction = true;
760 let event = ShellEvent::PointerMotion {
761 window,
762 device: VIRTUAL_DEVICE,
763 time: self.now(),
764 abs: if locked { None } else { Some(position) },
765 raw_delta: raw,
766 };
767 self.queue.push_back(event);
768 Ok(())
769 }
770
771 pub fn set_pointer_focus(
777 &mut self,
778 window: WindowId,
779 entered: bool,
780 position: Option<PhysicalPoint>,
781 ) -> Result<(), ShellError> {
782 self.check_window(window)?;
783 let event = ShellEvent::PointerFocus {
784 window,
785 device: VIRTUAL_DEVICE,
786 time: self.now(),
787 entered,
788 position: if entered { position } else { None },
789 };
790 self.queue.push_back(event);
791 Ok(())
792 }
793
794 pub fn button(
800 &mut self,
801 window: WindowId,
802 button: PointerButton,
803 state: ButtonState,
804 position: Option<PhysicalPoint>,
805 ) -> Result<(), ShellError> {
806 self.check_window(window)?;
807 self.saw_interaction = true;
808 let event = ShellEvent::Button {
809 window,
810 device: VIRTUAL_DEVICE,
811 time: self.now(),
812 button,
813 state,
814 position,
815 modifiers: self.modifiers,
816 };
817 self.queue.push_back(event);
818 Ok(())
819 }
820
821 pub fn scroll(
827 &mut self,
828 window: WindowId,
829 delta: ScrollDelta,
830 position: Option<PhysicalPoint>,
831 ) -> Result<(), ShellError> {
832 self.check_window(window)?;
833 self.saw_interaction = true;
834 let event = ShellEvent::Wheel {
835 window,
836 device: VIRTUAL_DEVICE,
837 time: self.now(),
838 delta,
839 position,
840 modifiers: self.modifiers,
841 };
842 self.queue.push_back(event);
843 Ok(())
844 }
845
846 pub fn touch(
874 &mut self,
875 window: WindowId,
876 contact: ContactId,
877 phase: TouchPhase,
878 position: PhysicalPoint,
879 ) -> Result<(), ShellError> {
880 self.check_window(window)?;
881 if !self.caps.contains(ShellCaps::TOUCH) {
882 return Err(self.unsupported("touch"));
883 }
884 self.saw_interaction = true;
885 let event = ShellEvent::Touch {
886 window,
887 device: VIRTUAL_DEVICE,
888 time: self.now(),
889 contact,
890 phase,
891 position,
892 };
893 self.queue.push_back(event);
894 Ok(())
895 }
896
897 pub fn commit_text(&mut self, window: WindowId, text: &str) -> Result<(), ShellError> {
908 assert!(!text.is_empty(), "a text commit is never empty");
909 self.check_window(window)?;
910 let event = ShellEvent::TextCommit {
911 window,
912 time: self.now(),
913 text: text.to_string(),
914 };
915 self.queue.push_back(event);
916 Ok(())
917 }
918
919 pub fn drop_file(
927 &mut self,
928 window: WindowId,
929 path: impl Into<PathBuf>,
930 position: Option<PhysicalPoint>,
931 ) -> Result<(), ShellError> {
932 if !self.caps.contains(ShellCaps::DRAG_DROP) {
933 return Err(self.unsupported("drag and drop"));
934 }
935 if !self.window(window)?.accept_drops {
936 return Err(ShellError::Unsupported {
937 backend: ShellBackend::Headless,
938 what: "drops on a window created without accept_drops",
939 });
940 }
941 let event = ShellEvent::DroppedFile {
942 window,
943 time: self.now(),
944 path: path.into(),
945 position,
946 };
947 self.queue.push_back(event);
948 Ok(())
949 }
950
951 pub fn plug_monitor(&mut self, mut info: MonitorInfo) -> MonitorId {
958 let id = MonitorId(self.next_monitor_id);
959 self.next_monitor_id += 1;
960 info.id = id;
961 self.monitors.push(info);
962 self.queue.push_back(ShellEvent::MonitorsChanged);
963 id
964 }
965
966 pub fn unplug_monitor(&mut self, id: MonitorId) -> Result<(), ShellError> {
980 let index = self
981 .monitors
982 .iter()
983 .position(|monitor| monitor.id == id)
984 .ok_or(ShellError::NoSuchMonitor(id.0))?;
985 assert!(
986 self.monitors.len() > 1,
987 "a shell needs at least one monitor"
988 );
989 let previous = self.monitors.clone();
990 self.monitors.remove(index);
991 crate::monitor::queue_removed(&previous, &self.monitors, &mut self.queue);
992 let panels: Vec<WindowId> = self
993 .windows
994 .iter()
995 .filter(|(_, state)| state.panel_monitor == Some(id) && !state.close_pending)
996 .map(|(window, _)| window.cast())
997 .collect();
998 for panel in panels {
999 self.request_close(panel)?;
1000 }
1001 self.queue.push_back(ShellEvent::MonitorsChanged);
1002 Ok(())
1003 }
1004
1005 pub fn title(&self, window: WindowId) -> Result<&str, ShellError> {
1016 Ok(self.window(window)?.title.as_str())
1017 }
1018
1019 pub fn cursor(&self, window: WindowId) -> Result<Option<CursorIcon>, ShellError> {
1027 Ok(self.window(window)?.cursor)
1028 }
1029
1030 #[must_use]
1034 pub fn clipboard_bytes(&self, mime: MimeType) -> Option<&[u8]> {
1035 self.clipboard
1036 .iter()
1037 .find(|(offered, _)| offered.matches(mime))
1038 .map(|(_, bytes)| bytes.as_slice())
1039 }
1040
1041 pub fn set_clipboard_readable(&mut self, readable: bool) {
1058 self.clipboard_readable = readable;
1059 }
1060
1061 pub fn set_clipboard_stalled(&mut self, stalled: bool) {
1070 self.clipboard_stalled = stalled;
1071 }
1072
1073 pub fn require_user_interaction(&mut self, required: bool) {
1100 self.clipboard_needs_interaction = required;
1101 }
1102
1103 pub fn clear_user_interaction(&mut self) {
1108 self.saw_interaction = false;
1109 }
1110
1111 pub fn clipboard_deadline(&mut self, pumps: u32) {
1117 self.clipboard_deadline = pumps;
1118 }
1119
1120 #[must_use]
1125 pub fn held_clipboard_reads(&self) -> usize {
1126 self.reads.len()
1127 }
1128
1129 pub fn set_foreign_clipboard(&mut self, offers: Vec<(ReceivedMime, Vec<u8>)>) {
1137 self.clipboard = offers;
1138 }
1139
1140 fn answerable(&self) -> bool {
1146 self.clipboard_readable && !self.clipboard_stalled
1147 }
1148
1149 fn answer(&self, window: WindowId, request: ClipboardRequestId, mime: MimeType) -> ShellEvent {
1151 let found = self
1156 .clipboard
1157 .iter()
1158 .find(|(offered, _)| offered.matches(mime));
1159 let (mime, content) = match found {
1160 Some((offered, bytes)) => (offered.clone(), ClipboardContent::Bytes(bytes.clone())),
1161 None => (ReceivedMime::from(mime), ClipboardContent::Empty),
1164 };
1165 ShellEvent::ClipboardData {
1166 window,
1167 request,
1168 mime,
1169 content,
1170 }
1171 }
1172
1173 fn resolve_reads(&mut self) {
1180 if self.reads.is_empty() {
1181 return;
1182 }
1183 let answerable = self.answerable();
1184 let mut held = Vec::with_capacity(self.reads.len());
1185 for mut read in core::mem::take(&mut self.reads) {
1186 if self.window(read.window).is_err() {
1190 continue;
1191 }
1192 if answerable {
1193 let answer = self.answer(read.window, read.request, read.mime);
1194 self.queue.push_back(answer);
1195 continue;
1196 }
1197 if read.countdown > 0 {
1198 read.countdown -= 1;
1199 held.push(read);
1200 continue;
1201 }
1202 self.queue.push_back(ShellEvent::ClipboardData {
1205 window: read.window,
1206 request: read.request,
1207 mime: ReceivedMime::from(read.mime),
1208 content: ClipboardContent::Unavailable,
1209 });
1210 }
1211 self.reads = held;
1212 }
1213
1214 fn deliver_due_configures(&mut self) {
1221 let mut due: Vec<(WindowId, PendingConfigure, Option<f64>)> = Vec::new();
1225 for (handle, state) in self.windows.iter_mut() {
1226 let Some(pending) = state.pending.as_mut() else {
1227 continue;
1228 };
1229 if pending.countdown > 0 {
1230 pending.countdown -= 1;
1231 continue;
1232 }
1233 let pending = state.pending.take().expect("checked just above");
1234 let previous_scale = state.configuration.map(|config| config.scale_factor);
1235 state.configuration = Some(WindowConfiguration {
1236 size: pending.size,
1237 scale_factor: pending.scale_factor,
1238 mode: pending.mode,
1239 });
1240 due.push((handle.cast(), pending, previous_scale));
1241 }
1242
1243 for (window, pending, previous_scale) in due {
1244 if previous_scale.is_some_and(|previous| previous != pending.scale_factor) {
1248 self.queue.push_back(ShellEvent::ScaleFactorChanged {
1249 window,
1250 scale_factor: pending.scale_factor,
1251 size: pending.size,
1252 });
1253 }
1254 self.queue.push_back(ShellEvent::Resized {
1255 window,
1256 size: pending.size,
1257 scale_factor: pending.scale_factor,
1258 });
1259 }
1260 }
1261
1262 fn window(&self, window: WindowId) -> Result<&HeadlessWindow, ShellError> {
1263 self.windows
1264 .get(window.cast())
1265 .ok_or_else(|| ShellError::invalid_window(window))
1266 }
1267
1268 fn window_mut(&mut self, window: WindowId) -> Result<&mut HeadlessWindow, ShellError> {
1269 self.windows
1270 .get_mut(window.cast())
1271 .ok_or_else(|| ShellError::invalid_window(window))
1272 }
1273
1274 fn check_window(&self, window: WindowId) -> Result<(), ShellError> {
1275 self.window(window).map(|_| ())
1276 }
1277
1278 fn unsupported(&self, what: &'static str) -> ShellError {
1279 ShellError::Unsupported {
1280 backend: ShellBackend::Headless,
1281 what,
1282 }
1283 }
1284}
1285
1286fn synthetic_scancode(key: KeyCode) -> Scancode {
1290 let index = KeyCode::ALL
1291 .iter()
1292 .position(|candidate| *candidate == key)
1293 .unwrap_or(0);
1294 Scancode(index as u32 + 8)
1297}
1298
1299fn synthetic_keysym(key: KeyCode) -> Keysym {
1301 let name = key.as_str();
1302 if let Some(letter) = name.strip_prefix("Key") {
1303 return Keysym::from_char(letter.to_ascii_lowercase().chars().next().unwrap_or('?'));
1305 }
1306 if let Some(digit) = name.strip_prefix("Digit") {
1307 return Keysym::from_char(digit.chars().next().unwrap_or('?'));
1308 }
1309 match key {
1310 KeyCode::Space => Keysym::from_char(' '),
1311 KeyCode::Minus => Keysym::from_char('-'),
1312 KeyCode::Equal => Keysym::from_char('='),
1313 KeyCode::Comma => Keysym::from_char(','),
1314 KeyCode::Period => Keysym::from_char('.'),
1315 KeyCode::Slash => Keysym::from_char('/'),
1316 KeyCode::Semicolon => Keysym::from_char(';'),
1317 KeyCode::Quote => Keysym::from_char('\''),
1318 KeyCode::Backslash => Keysym::from_char('\\'),
1319 KeyCode::BracketLeft => Keysym::from_char('['),
1320 KeyCode::BracketRight => Keysym::from_char(']'),
1321 KeyCode::Backquote => Keysym::from_char('`'),
1322 _ => Keysym::NONE,
1326 }
1327}
1328
1329impl Shell for HeadlessShell {
1330 fn backend(&self) -> ShellBackend {
1331 ShellBackend::Headless
1332 }
1333
1334 fn caps(&self) -> ShellCaps {
1335 self.caps
1336 }
1337
1338 fn create_window(&mut self, desc: &WindowDesc<'_>) -> Result<WindowId, ShellError> {
1339 crate::role::check(desc, self.caps(), self.backend(), |monitor| {
1340 self.monitor(monitor).is_some()
1341 })?;
1342 if !self.windows.is_empty() && !self.caps.contains(ShellCaps::MULTI_WINDOW) {
1343 return Err(self.unsupported("a second window"));
1344 }
1345 if let DisplayMode::Borderless {
1346 monitor: Some(monitor),
1347 } = desc.mode
1348 && self.monitor(monitor).is_none()
1349 {
1350 return Err(ShellError::NoSuchMonitor(monitor.0));
1351 }
1352
1353 let (size, scale_factor) = match desc.mode {
1365 DisplayMode::Windowed => {
1366 let scale_factor = self.monitors[0].scale_factor;
1367 (
1368 desc.constraints
1369 .apply(desc.size.to_physical(scale_factor), scale_factor),
1370 scale_factor,
1371 )
1372 }
1373 DisplayMode::Borderless { monitor } => {
1374 let monitor = monitor
1375 .and_then(|id| self.monitor(id))
1376 .unwrap_or(&self.monitors[0]);
1377 (monitor.size(), monitor.scale_factor)
1378 }
1379 };
1380
1381 let (parent, panel_monitor) = match desc.role {
1382 crate::WindowRole::Popup(popup) => {
1383 self.window(popup.parent)?;
1384 (Some(popup.parent), None)
1385 }
1386 crate::WindowRole::Panel(panel) => (None, panel.monitor),
1387 crate::WindowRole::Toplevel => (None, None),
1388 };
1389 let countdown = self.configure_delay;
1390 let window: WindowId = self
1391 .windows
1392 .insert(HeadlessWindow {
1393 title: desc.title.to_string(),
1394 configuration: None,
1397 pending: Some(PendingConfigure {
1398 size,
1399 scale_factor,
1400 mode: desc.mode,
1401 countdown,
1402 }),
1403 requested_mode: desc.mode,
1404 requested_constraints: desc.constraints,
1405 focused: false,
1406 visible: desc.visible,
1407 pointer_mode: PointerMode::Free,
1408 cursor: Some(CursorIcon::Default),
1409 close_pending: false,
1410 accept_drops: desc.accept_drops,
1411 parent,
1412 panel_monitor,
1413 })
1414 .cast();
1415 Ok(window)
1416 }
1417
1418 fn destroy_window(&mut self, window: WindowId) -> Result<(), ShellError> {
1422 self.window(window)?;
1423 let popups: Vec<WindowId> = self
1424 .windows
1425 .iter()
1426 .filter(|(_, state)| state.parent == Some(window))
1427 .map(|(id, _)| id.cast())
1428 .collect();
1429 for popup in popups {
1430 self.destroy_window(popup)?;
1431 }
1432 self.windows
1433 .remove(window.cast())
1434 .ok_or_else(|| ShellError::invalid_window(window))?;
1435 self.queue.push_back(ShellEvent::WindowDestroyed { window });
1436 Ok(())
1437 }
1438
1439 fn window_state(&self, window: WindowId) -> Result<WindowState, ShellError> {
1440 let state = self.window(window)?;
1441 Ok(WindowState {
1442 configuration: state.configuration,
1443 requested_mode: state.requested_mode,
1444 requested_constraints: state.requested_constraints,
1445 focused: state.focused,
1446 visible: state.visible,
1447 pointer_mode: state.pointer_mode,
1448 close_pending: state.close_pending,
1449 })
1450 }
1451
1452 fn set_title(&mut self, window: WindowId, title: &str) -> Result<(), ShellError> {
1453 self.window_mut(window)?.title = title.to_string();
1454 Ok(())
1455 }
1456
1457 fn set_visible(&mut self, window: WindowId, visible: bool) -> Result<(), ShellError> {
1458 self.window_mut(window)?.visible = visible;
1459 Ok(())
1460 }
1461
1462 fn set_mode(&mut self, window: WindowId, mode: DisplayMode) -> Result<(), ShellError> {
1463 let target = match mode {
1464 DisplayMode::Windowed => None,
1465 DisplayMode::Borderless { monitor } => {
1466 let monitor = match monitor {
1467 Some(id) => self
1468 .monitor(id)
1469 .ok_or(ShellError::NoSuchMonitor(id.0))?
1470 .clone(),
1471 None => self.monitors[0].clone(),
1472 };
1473 Some(monitor)
1474 }
1475 };
1476 let countdown = self.configure_delay;
1477
1478 let state = self.window_mut(window)?;
1479 state.requested_mode = mode;
1485 let (size, scale_factor) = match &target {
1486 Some(monitor) => (monitor.size(), monitor.scale_factor),
1487 None => (state.base_size(), state.scale_factor()),
1491 };
1492 state.pending = Some(PendingConfigure {
1493 size,
1494 scale_factor,
1495 mode,
1496 countdown,
1497 });
1498 Ok(())
1499 }
1500
1501 fn set_constraints(
1502 &mut self,
1503 window: WindowId,
1504 constraints: SizeConstraints,
1505 ) -> Result<(), ShellError> {
1506 let countdown = self.configure_delay;
1507 let state = self.window_mut(window)?;
1508 state.requested_constraints = constraints;
1509
1510 if let Some(pending) = state.pending.as_mut() {
1517 pending.size = constraints.apply(pending.size, pending.scale_factor);
1518 return Ok(());
1519 }
1520
1521 if let Some(config) = state.configuration {
1525 let constrained = constraints.apply(config.size, config.scale_factor);
1526 if constrained != config.size {
1527 state.pending = Some(PendingConfigure {
1528 size: constrained,
1529 scale_factor: config.scale_factor,
1530 mode: config.mode,
1531 countdown,
1532 });
1533 }
1534 }
1535 Ok(())
1536 }
1537
1538 fn reposition_popup(
1542 &mut self,
1543 popup: WindowId,
1544 anchor: LogicalRect,
1545 size: LogicalSize,
1546 ) -> Result<(), ShellError> {
1547 let _ = anchor;
1548 let countdown = self.configure_delay;
1549 let state = self.window_mut(popup)?;
1550 if state.parent.is_none() {
1551 return Err(crate::role::not_a_popup(popup));
1552 }
1553 if let Some(pending) = state.pending.as_mut() {
1554 pending.size = size.to_physical(pending.scale_factor);
1555 } else if let Some(config) = state.configuration {
1556 state.pending = Some(PendingConfigure {
1557 size: size.to_physical(config.scale_factor),
1558 scale_factor: config.scale_factor,
1559 mode: config.mode,
1560 countdown,
1561 });
1562 }
1563 Ok(())
1564 }
1565
1566 fn monitors(&self) -> &[MonitorInfo] {
1567 &self.monitors
1568 }
1569
1570 fn pump(&mut self, sink: &mut dyn FnMut(ShellEvent)) {
1571 self.keep_alive();
1572 for _ in 0..self.queue.len() {
1577 let Some(event) = self.queue.pop_front() else {
1578 break;
1579 };
1580 sink(event);
1581 }
1582 }
1583
1584 fn keep_alive(&mut self) {
1592 self.deliver_due_configures();
1593 self.resolve_reads();
1594 }
1595
1596 fn wait_events(&mut self, timeout: Option<Duration>) {
1615 self.waits += 1;
1616 if !self.caps.contains(ShellCaps::EVENT_WAIT) || !self.queue.is_empty() {
1617 return;
1618 }
1619 match (&self.wake, timeout) {
1620 (Some(wake), timeout) => wake.wait(timeout),
1621 (None, Some(timeout)) => std::thread::sleep(timeout),
1622 (None, None) => {}
1623 }
1624 }
1625
1626 fn waker(&mut self) -> Option<ShellWaker> {
1629 let wake = self.wake.get_or_insert_with(Arc::default);
1630 Some(ShellWaker::new(Arc::clone(wake) as Arc<dyn WakeSignal>))
1631 }
1632
1633 fn align_event_clock(&mut self, elapsed: Duration) {
1640 self.set_time(elapsed);
1641 }
1642
1643 fn surface_target(&self, window: WindowId) -> Result<SurfaceTarget, ShellError> {
1644 self.window(window)?;
1650 Ok(SurfaceTarget::Offscreen)
1651 }
1652
1653 fn set_pointer_mode(&mut self, window: WindowId, mode: PointerMode) -> Result<(), ShellError> {
1654 if !self.caps.contains(mode.required_cap()) {
1655 return Err(self.unsupported(mode.as_str()));
1656 }
1657 self.window_mut(window)?.pointer_mode = mode;
1658 Ok(())
1659 }
1660
1661 fn set_cursor(
1662 &mut self,
1663 window: WindowId,
1664 cursor: Option<CursorIcon>,
1665 ) -> Result<(), ShellError> {
1666 self.window_mut(window)?.cursor = cursor;
1667 Ok(())
1668 }
1669
1670 fn warp_pointer(
1671 &mut self,
1672 window: WindowId,
1673 position: PhysicalPoint,
1674 ) -> Result<(), ShellError> {
1675 if !self.caps.contains(ShellCaps::POINTER_WARP) {
1676 return Err(self.unsupported("pointer warp"));
1677 }
1678 self.check_window(window)?;
1679 let event = ShellEvent::PointerMotion {
1682 window,
1683 device: VIRTUAL_DEVICE,
1684 time: self.now(),
1685 abs: Some(position),
1686 raw_delta: None,
1687 };
1688 self.queue.push_back(event);
1689 Ok(())
1690 }
1691
1692 fn reply_close_request(
1693 &mut self,
1694 window: WindowId,
1695 reply: CloseReply,
1696 ) -> Result<(), ShellError> {
1697 let state = self.window_mut(window)?;
1698 if !state.close_pending {
1699 return Err(ShellError::NoPendingCloseRequest { window });
1700 }
1701 state.close_pending = false;
1702 match reply {
1703 CloseReply::Close => self.destroy_window(window),
1704 CloseReply::Keep => Ok(()),
1705 }
1706 }
1707
1708 fn clipboard_offer(
1719 &mut self,
1720 window: WindowId,
1721 offers: &[ClipboardOffer<'_>],
1722 ) -> Result<(), ShellError> {
1723 if !self.caps.contains(ShellCaps::CLIPBOARD) {
1724 return Err(self.unsupported("clipboard"));
1725 }
1726 self.check_window(window)?;
1727 if self.clipboard_needs_interaction && !self.saw_interaction {
1728 return Err(ShellError::NeedsUserInteraction {
1729 backend: ShellBackend::Headless,
1730 what: "claiming the clipboard",
1731 });
1732 }
1733 self.clipboard = offers
1734 .iter()
1735 .map(|offer| (ReceivedMime::from(offer.mime), offer.bytes.to_vec()))
1736 .collect();
1737 Ok(())
1738 }
1739
1740 fn clipboard_request(
1754 &mut self,
1755 window: WindowId,
1756 mime: MimeType,
1757 ) -> Result<ClipboardRequestId, ShellError> {
1758 if !self.caps.contains(ShellCaps::CLIPBOARD) {
1759 return Err(self.unsupported("clipboard"));
1760 }
1761 self.check_window(window)?;
1762 let request = ClipboardRequestId(self.next_request_id);
1763 self.next_request_id += 1;
1764 if self.answerable() {
1765 let answer = self.answer(window, request, mime);
1766 self.queue.push_back(answer);
1770 } else {
1771 self.reads.push(PendingRead {
1772 window,
1773 request,
1774 mime,
1775 countdown: self.clipboard_deadline,
1776 });
1777 }
1778 Ok(request)
1779 }
1780
1781 fn clipboard_readable(&self, window: WindowId) -> bool {
1787 self.caps.contains(ShellCaps::CLIPBOARD) && self.window(window).is_ok() && self.answerable()
1788 }
1789}
1790
1791#[cfg(test)]
1792mod tests {
1793 use super::*;
1794 use crate::{AspectRatio, LogicalSize};
1795
1796 fn shell_with_window() -> (HeadlessShell, WindowId) {
1798 let mut shell = HeadlessShell::new();
1799 let window = shell
1800 .create_window(&WindowDesc::default())
1801 .expect("headless window creation never fails");
1802 (shell, window)
1803 }
1804
1805 fn shell_with_configured_window() -> (HeadlessShell, WindowId) {
1807 let (mut shell, window) = shell_with_window();
1808 settle(&mut shell);
1809 assert!(shell.window_state(window).expect("state").is_configured());
1810 (shell, window)
1811 }
1812
1813 fn settle(shell: &mut HeadlessShell) {
1815 for _ in 0..8 {
1816 let mut seen = 0;
1817 shell.pump(&mut |_| seen += 1);
1818 if seen == 0 && shell.windows.iter().all(|(_, w)| w.pending.is_none()) {
1819 return;
1820 }
1821 }
1822 panic!("the shell never settled");
1823 }
1824
1825 fn drain(shell: &mut HeadlessShell) -> Vec<ShellEvent> {
1826 let mut events = Vec::new();
1827 shell.pump(&mut |event| events.push(event));
1828 events
1829 }
1830
1831 fn names(events: &[ShellEvent]) -> Vec<&'static str> {
1832 events.iter().map(ShellEvent::name).collect()
1833 }
1834
1835 fn create_swapchain(state: &WindowState) -> Result<PhysicalSize, &'static str> {
1838 let size = state.size().ok_or("window is not configured yet")?;
1839 if size.is_empty() {
1840 return Err("zero-extent swapchain");
1841 }
1842 Ok(size)
1843 }
1844
1845 #[test]
1848 fn a_new_window_has_no_size_until_a_later_pump() {
1849 let (mut shell, window) = shell_with_window();
1850
1851 let state = shell.window_state(window).expect("state");
1854 assert!(!state.is_configured());
1855 assert_eq!(state.size(), None);
1856 assert_eq!(state.scale_factor(), None);
1857 assert_eq!(state.effective_mode(), None);
1858 assert_eq!(state.requested_mode, DisplayMode::Windowed);
1859
1860 assert_eq!(drain(&mut shell), Vec::new());
1862 assert!(!shell.window_state(window).expect("state").is_configured());
1863
1864 let events = drain(&mut shell);
1866 assert_eq!(
1867 events,
1868 vec![ShellEvent::Resized {
1869 window,
1870 size: PhysicalSize::new(1280, 720),
1871 scale_factor: 1.0,
1872 }]
1873 );
1874 let state = shell.window_state(window).expect("state");
1875 assert_eq!(state.size(), Some(PhysicalSize::new(1280, 720)));
1876 assert_eq!(state.scale_factor(), Some(1.0));
1877 assert_eq!(state.effective_mode(), Some(DisplayMode::Windowed));
1878 assert!(state.mode_request_honoured());
1879 }
1880
1881 #[test]
1885 fn keep_alive_takes_a_turn_and_keeps_everything_for_the_next_pump() {
1886 let (mut shell, window) = shell_with_window();
1887 shell.inject(ShellEvent::CloseRequested { window });
1888
1889 for _ in 0..=DEFAULT_CONFIGURE_DELAY {
1891 shell.keep_alive();
1892 }
1893 assert!(
1894 shell.window_state(window).expect("state").is_configured(),
1895 "every turn the configure waits for was taken by keep_alive"
1896 );
1897
1898 assert_eq!(
1899 drain(&mut shell),
1900 vec![
1901 ShellEvent::CloseRequested { window },
1902 ShellEvent::Resized {
1903 window,
1904 size: PhysicalSize::new(1280, 720),
1905 scale_factor: 1.0,
1906 },
1907 ],
1908 "nothing keep_alive saw was delivered or dropped"
1909 );
1910 assert_eq!(drain(&mut shell), Vec::new(), "and each arrived once");
1911 }
1912
1913 #[test]
1914 fn a_consumer_that_skips_the_wait_gets_a_clean_error_not_a_wrong_extent() {
1915 let (mut shell, window) = shell_with_window();
1916
1917 assert_eq!(
1919 create_swapchain(&shell.window_state(window).expect("state")),
1920 Err("window is not configured yet")
1921 );
1922
1923 let target = shell.surface_target(window).expect("the handle exists");
1929 assert_eq!(target, SurfaceTarget::Offscreen);
1930
1931 let size = loop {
1933 shell.pump(&mut |_| {});
1934 if let Ok(size) = create_swapchain(&shell.window_state(window).expect("state")) {
1935 break size;
1936 }
1937 };
1938 assert_eq!(size, PhysicalSize::new(1280, 720));
1939 assert_eq!(
1942 shell.surface_target(window).expect("target"),
1943 target,
1944 "the target is invariant under a configure"
1945 );
1946 }
1947
1948 #[test]
1949 fn the_configure_delay_is_a_knob() {
1950 let mut shell = HeadlessShell::new().with_configure_delay(3);
1951 let window = shell.create_window(&WindowDesc::default()).expect("window");
1952 for pump in 0..3 {
1953 assert_eq!(drain(&mut shell), Vec::new(), "pump {pump}");
1954 assert!(!shell.window_state(window).expect("state").is_configured());
1955 }
1956 assert_eq!(names(&drain(&mut shell)), ["Resized"]);
1957 assert!(shell.window_state(window).expect("state").is_configured());
1958
1959 let mut shell = HeadlessShell::new().with_configure_delay(0);
1962 let window = shell.create_window(&WindowDesc::default()).expect("window");
1963 assert_eq!(names(&drain(&mut shell)), ["Resized"]);
1964 assert!(shell.window_state(window).expect("state").is_configured());
1965
1966 shell.set_configure_delay(2);
1967 shell
1968 .set_mode(window, DisplayMode::Borderless { monitor: None })
1969 .expect("request");
1970 assert_eq!(drain(&mut shell), Vec::new());
1971 assert_eq!(drain(&mut shell), Vec::new());
1972 assert_eq!(names(&drain(&mut shell)), ["Resized"]);
1973 }
1974
1975 #[test]
1976 fn set_mode_is_a_request_whose_effect_arrives_later() {
1977 let (mut shell, window) = shell_with_configured_window();
1978 shell
1979 .set_mode(window, DisplayMode::Borderless { monitor: None })
1980 .expect("request");
1981
1982 let state = shell.window_state(window).expect("state");
1984 assert_eq!(
1985 state.requested_mode,
1986 DisplayMode::Borderless { monitor: None }
1987 );
1988 assert_eq!(
1989 state.effective_mode(),
1990 Some(DisplayMode::Windowed),
1991 "still windowed until the window system says otherwise"
1992 );
1993 assert!(
1994 !state.mode_request_honoured(),
1995 "a UI toggle reading the request instead of the effect would lie here"
1996 );
1997 assert_eq!(state.size(), Some(PhysicalSize::new(1280, 720)));
1998
1999 settle(&mut shell);
2000 let state = shell.window_state(window).expect("state");
2001 assert_eq!(
2002 state.effective_mode(),
2003 Some(DisplayMode::Borderless { monitor: None })
2004 );
2005 assert!(state.mode_request_honoured());
2006 assert_eq!(state.size(), Some(PhysicalSize::new(1920, 1080)));
2007 }
2008
2009 #[test]
2010 fn a_refused_mode_is_expressible_through_injection() {
2011 let (mut shell, window) = shell_with_configured_window();
2014 shell
2015 .set_mode(window, DisplayMode::Borderless { monitor: None })
2016 .expect("request");
2017 shell
2019 .resize(window, PhysicalSize::new(1280, 720))
2020 .expect("answer");
2021 let state = shell.window_state(window).expect("state");
2022 assert_eq!(state.effective_mode(), Some(DisplayMode::Windowed));
2023 assert!(!state.mode_request_honoured());
2024 }
2025
2026 #[test]
2029 fn a_repositioned_popup_is_configured_at_its_new_size() {
2030 let (mut shell, window) = shell_with_configured_window();
2031 let anchor = crate::LogicalRect::new(
2032 crate::LogicalPoint::new(10.0, 10.0),
2033 LogicalSize::new(100.0, 20.0),
2034 );
2035 let popup = shell
2036 .create_window(&WindowDesc {
2037 size: LogicalSize::new(200.0, 150.0),
2038 role: crate::WindowRole::Popup(crate::PopupDesc {
2039 parent: window,
2040 anchor,
2041 placement: crate::PopupPlacement::Below,
2042 input: crate::PopupInput::Pointer,
2043 }),
2044 ..WindowDesc::default()
2045 })
2046 .expect("a popup");
2047 settle(&mut shell);
2048 let scale = shell
2049 .window_state(popup)
2050 .expect("live")
2051 .scale_factor()
2052 .expect("configured");
2053
2054 let moved = LogicalSize::new(240.0, 90.0);
2055 shell
2056 .reposition_popup(popup, anchor, moved)
2057 .expect("reposition");
2058 settle(&mut shell);
2059 assert_eq!(
2060 shell.window_state(popup).expect("live").size(),
2061 Some(moved.to_physical(scale))
2062 );
2063 assert!(matches!(
2064 shell.reposition_popup(window, anchor, moved),
2065 Err(ShellError::InvalidDescriptor(_))
2066 ));
2067 }
2068
2069 #[test]
2070 fn set_constraints_is_a_request_too() {
2071 let (mut shell, window) = shell_with_configured_window();
2072 let constraints = SizeConstraints::min(LogicalSize::new(1600.0, 900.0));
2073 shell.set_constraints(window, constraints).expect("request");
2074
2075 let state = shell.window_state(window).expect("state");
2076 assert_eq!(state.requested_constraints, constraints);
2077 assert_eq!(
2078 state.size(),
2079 Some(PhysicalSize::new(1280, 720)),
2080 "the size only changes when the window system says so"
2081 );
2082
2083 settle(&mut shell);
2084 assert_eq!(
2085 shell.window_state(window).expect("state").size(),
2086 Some(PhysicalSize::new(1600, 900))
2087 );
2088 }
2089
2090 #[test]
2091 fn constraints_do_not_cancel_a_mode_request_that_has_not_landed() {
2092 let (mut shell, window) = shell_with_configured_window();
2097 shell
2098 .set_mode(window, DisplayMode::Borderless { monitor: None })
2099 .expect("mode request");
2100 shell
2101 .set_constraints(window, SizeConstraints::min(LogicalSize::new(320.0, 180.0)))
2102 .expect("constraint request");
2103 settle(&mut shell);
2104
2105 let state = shell.window_state(window).expect("state");
2106 assert_eq!(
2107 state.effective_mode(),
2108 Some(DisplayMode::Borderless { monitor: None })
2109 );
2110 assert!(state.mode_request_honoured());
2111 }
2112
2113 #[test]
2114 fn a_borderless_window_reports_its_own_monitors_scale() {
2115 let mut shell = HeadlessShell::new();
2120 let hidpi = shell.plug_monitor(MonitorInfo {
2121 id: MonitorId(0),
2122 name: "HEADLESS-2".to_string(),
2123 bounds: PhysicalRect::new(1920, 0, 2560, 1440),
2124 work_area: PhysicalRect::new(1920, 0, 2560, 1440),
2125 scale_factor: 2.0,
2126 refresh_millihertz: 143_998,
2127 is_primary: false,
2128 });
2129 let window = shell
2130 .create_window(&WindowDesc {
2131 mode: DisplayMode::Borderless {
2132 monitor: Some(hidpi),
2133 },
2134 ..WindowDesc::default()
2135 })
2136 .expect("window");
2137 settle(&mut shell);
2138
2139 let state = shell.window_state(window).expect("state");
2140 assert_eq!(state.size(), Some(PhysicalSize::new(2560, 1440)));
2141 assert_eq!(
2142 state.scale_factor(),
2143 Some(2.0),
2144 "the scale is the borderless monitor's, not the primary's"
2145 );
2146 let other = shell
2149 .create_window(&WindowDesc::default())
2150 .expect("second window");
2151 settle(&mut shell);
2152 shell
2153 .set_mode(
2154 other,
2155 DisplayMode::Borderless {
2156 monitor: Some(hidpi),
2157 },
2158 )
2159 .expect("request");
2160 settle(&mut shell);
2161 assert_eq!(
2162 shell.window_state(other).expect("state").scale_factor(),
2163 shell.window_state(window).expect("state").scale_factor()
2164 );
2165 }
2166
2167 #[test]
2168 fn constraints_set_before_the_first_configure_fold_into_it() {
2169 let mut shell = HeadlessShell::new();
2170 let window = shell.create_window(&WindowDesc::default()).expect("window");
2171 shell
2172 .set_constraints(
2173 window,
2174 SizeConstraints::NONE.with_aspect(AspectRatio::SQUARE),
2175 )
2176 .expect("request");
2177 settle(&mut shell);
2178 assert_eq!(
2181 shell.window_state(window).expect("state").size(),
2182 Some(PhysicalSize::new(720, 720))
2183 );
2184 }
2185
2186 #[test]
2187 fn creation_uses_the_requested_size_and_the_monitors_scale() {
2188 let mut shell = HeadlessShell::new();
2189 let window = shell
2190 .create_window(&WindowDesc {
2191 size: LogicalSize::new(800.0, 600.0),
2192 ..WindowDesc::default()
2193 })
2194 .expect("window");
2195 settle(&mut shell);
2196 assert_eq!(
2197 shell.window_state(window).expect("state").size(),
2198 Some(PhysicalSize::new(800, 600))
2199 );
2200
2201 let mut shell = HeadlessShell::new().with_scale_factor(2.0);
2202 let window = shell
2203 .create_window(&WindowDesc {
2204 size: LogicalSize::new(640.0, 360.0),
2205 ..WindowDesc::default()
2206 })
2207 .expect("window");
2208 settle(&mut shell);
2209 let state = shell.window_state(window).expect("state");
2210 assert_eq!(state.size(), Some(PhysicalSize::new(1280, 720)));
2211 assert_eq!(state.scale_factor(), Some(2.0));
2212 }
2213
2214 #[test]
2217 fn scale_changes_preserve_logical_size_and_report_both_facts() {
2218 let (mut shell, window) = shell_with_configured_window();
2219 shell
2220 .change_scale_factor(window, 1.5)
2221 .expect("scale change");
2222 let events = drain(&mut shell);
2223 assert_eq!(
2224 events,
2225 vec![
2226 ShellEvent::ScaleFactorChanged {
2227 window,
2228 scale_factor: 1.5,
2229 size: PhysicalSize::new(1920, 1080),
2230 },
2231 ShellEvent::Resized {
2234 window,
2235 size: PhysicalSize::new(1920, 1080),
2236 scale_factor: 1.5,
2237 },
2238 ]
2239 );
2240 assert_eq!(
2244 shell.window_state(window).expect("state").size(),
2245 Some(PhysicalSize::new(1920, 1080))
2246 );
2247 }
2248
2249 #[test]
2250 fn a_scale_change_scheduled_by_a_mode_switch_arrives_before_the_resize() {
2251 let mut shell = HeadlessShell::new();
2252 let hidpi = shell.plug_monitor(MonitorInfo {
2253 id: MonitorId(0),
2254 name: "HEADLESS-2".to_string(),
2255 bounds: PhysicalRect::new(1920, 0, 2560, 1440),
2256 work_area: PhysicalRect::new(1920, 0, 2560, 1440),
2257 scale_factor: 2.0,
2258 refresh_millihertz: 143_998,
2259 is_primary: false,
2260 });
2261 assert_eq!(hidpi, MonitorId(2), "ids are never reused");
2262 let window = shell.create_window(&WindowDesc::default()).expect("window");
2263 settle(&mut shell);
2264
2265 shell
2266 .set_mode(
2267 window,
2268 DisplayMode::Borderless {
2269 monitor: Some(hidpi),
2270 },
2271 )
2272 .expect("request");
2273 assert_eq!(drain(&mut shell), Vec::new(), "still only a request");
2274 let events = drain(&mut shell);
2275 assert_eq!(names(&events), ["ScaleFactorChanged", "Resized"]);
2276
2277 let state = shell.window_state(window).expect("state");
2278 assert_eq!(state.size(), Some(PhysicalSize::new(2560, 1440)));
2279 assert_eq!(state.scale_factor(), Some(2.0));
2280 assert!(state.effective_mode().expect("configured").is_borderless());
2281 assert_eq!(
2286 shell.surface_target(window).expect("target"),
2287 SurfaceTarget::Offscreen
2288 );
2289
2290 assert!(matches!(
2291 shell.set_mode(
2292 window,
2293 DisplayMode::Borderless {
2294 monitor: Some(MonitorId(99)),
2295 },
2296 ),
2297 Err(ShellError::NoSuchMonitor(99))
2298 ));
2299 }
2300
2301 #[test]
2302 fn resizes_honour_constraints_and_inject_bypasses_them() {
2303 let (mut shell, window) = shell_with_configured_window();
2304 shell
2305 .set_constraints(
2306 window,
2307 SizeConstraints::min(LogicalSize::new(640.0, 360.0))
2308 .with_aspect(AspectRatio::WIDESCREEN),
2309 )
2310 .expect("request");
2311 settle(&mut shell);
2312
2313 shell
2314 .resize(window, PhysicalSize::new(1000, 1000))
2315 .expect("resize");
2316 assert_eq!(
2317 drain(&mut shell),
2318 vec![ShellEvent::Resized {
2319 window,
2320 size: PhysicalSize::new(1000, 562),
2321 scale_factor: 1.0,
2322 }]
2323 );
2324
2325 shell.inject(ShellEvent::Resized {
2328 window,
2329 size: PhysicalSize::new(300, 900),
2330 scale_factor: 1.0,
2331 });
2332 assert_eq!(
2333 drain(&mut shell)[0],
2334 ShellEvent::Resized {
2335 window,
2336 size: PhysicalSize::new(300, 900),
2337 scale_factor: 1.0,
2338 }
2339 );
2340 }
2341
2342 #[test]
2343 fn a_compositor_configure_supersedes_a_pending_one() {
2344 let (mut shell, window) = shell_with_configured_window();
2345 shell
2346 .set_mode(window, DisplayMode::Borderless { monitor: None })
2347 .expect("request");
2348 shell
2350 .resize(window, PhysicalSize::new(640, 480))
2351 .expect("answer");
2352 assert_eq!(names(&drain(&mut shell)), ["Resized"]);
2353 assert_eq!(
2354 shell.window_state(window).expect("state").size(),
2355 Some(PhysicalSize::new(640, 480))
2356 );
2357 assert_eq!(drain(&mut shell), Vec::new());
2359 }
2360
2361 #[test]
2364 fn timestamps_come_from_the_shells_clock_not_from_the_pump() {
2365 let (mut shell, window) = shell_with_configured_window();
2366 shell.set_time(Duration::from_secs(2));
2367 shell.key_press(window, KeyCode::Space).expect("press");
2368 shell.advance(Duration::from_millis(180));
2369 shell.key_release(window, KeyCode::Space).expect("release");
2370
2371 let events = drain(&mut shell);
2372 let held = events[1]
2373 .time()
2374 .expect("input")
2375 .saturating_since(events[0].time().expect("input"));
2376 assert_eq!(held, Duration::from_millis(180));
2377 assert_eq!(events[0].time(), Some(EventTime::from_millis(2_000)));
2378 }
2379
2380 #[test]
2381 fn synthesized_key_fields_are_stable_and_plausible() {
2382 let (mut shell, window) = shell_with_configured_window();
2383 shell.set_modifiers(Modifiers::CTRL);
2384 shell.key_press(window, KeyCode::KeyS).expect("press");
2385 let events = drain(&mut shell);
2386 let ShellEvent::Key {
2387 scancode,
2388 key_code,
2389 keysym,
2390 modifiers,
2391 repeat,
2392 device,
2393 ..
2394 } = events[0].clone()
2395 else {
2396 panic!("wrong variant");
2397 };
2398 assert_eq!(key_code, Some(KeyCode::KeyS));
2399 assert_eq!(keysym, Keysym::from_char('s'));
2400 assert_eq!(modifiers, Modifiers::CTRL);
2401 assert!(!repeat);
2402 assert_eq!(device, VIRTUAL_DEVICE);
2403 assert!(scancode.0 >= 8, "evdev-shaped scancodes start at 8");
2404 assert_eq!(synthetic_keysym(KeyCode::Digit7), Keysym::from_char('7'));
2405 assert_eq!(synthetic_keysym(KeyCode::F5), Keysym::NONE);
2406 assert_eq!(synthetic_keysym(KeyCode::Slash), Keysym::from_char('/'));
2407
2408 shell.key_repeat(window, KeyCode::KeyS).expect("repeat");
2409 let events = drain(&mut shell);
2410 let ShellEvent::Key { repeat, .. } = events[0] else {
2411 panic!("wrong variant");
2412 };
2413 assert!(repeat);
2414 }
2415
2416 #[test]
2417 fn a_locked_pointer_reports_no_absolute_position() {
2418 let (mut shell, window) = shell_with_configured_window();
2419 shell
2420 .set_pointer_mode(window, PointerMode::Locked)
2421 .expect("lock");
2422 shell
2423 .move_pointer(window, PhysicalPoint::new(10.0, 10.0), (3.0, -4.0))
2424 .expect("motion");
2425 let events = drain(&mut shell);
2426 let ShellEvent::PointerMotion { abs, raw_delta, .. } = events[0] else {
2427 panic!("wrong variant");
2428 };
2429 assert_eq!(abs, None);
2430 assert_eq!(raw_delta, Some((3.0, -4.0)));
2431 }
2432
2433 #[test]
2434 fn a_backend_without_raw_motion_reports_none() {
2435 let mut shell = HeadlessShell::new()
2436 .with_caps(ShellCaps::DESKTOP - ShellCaps::RAW_POINTER_MOTION)
2437 .with_configure_delay(0);
2438 let window = shell.create_window(&WindowDesc::default()).expect("window");
2439 drain(&mut shell);
2440 shell
2441 .move_pointer(window, PhysicalPoint::new(1.0, 2.0), (5.0, 5.0))
2442 .expect("motion");
2443 let events = drain(&mut shell);
2444 let ShellEvent::PointerMotion { abs, raw_delta, .. } = events[0] else {
2445 panic!("wrong variant");
2446 };
2447 assert_eq!(abs, Some(PhysicalPoint::new(1.0, 2.0)));
2448 assert_eq!(raw_delta, None, "no relative-motion source");
2449 assert!(!shell.caps().has_mouselook());
2450 }
2451
2452 #[test]
2453 fn pointer_focus_and_scroll_and_text_are_injectable() {
2454 let (mut shell, window) = shell_with_configured_window();
2455 shell
2456 .set_pointer_focus(window, true, Some(PhysicalPoint::new(1.0, 1.0)))
2457 .expect("enter");
2458 shell
2459 .scroll(window, ScrollDelta::Lines { x: 0.0, y: -1.0 }, None)
2460 .expect("scroll");
2461 shell
2462 .button(
2463 window,
2464 PointerButton::Left,
2465 ButtonState::Pressed,
2466 Some(PhysicalPoint::new(1.0, 1.0)),
2467 )
2468 .expect("click");
2469 shell.commit_text(window, "ありがとう").expect("text");
2470 shell
2471 .set_pointer_focus(window, false, Some(PhysicalPoint::new(1.0, 1.0)))
2472 .expect("leave");
2473
2474 let events = drain(&mut shell);
2475 assert_eq!(
2476 names(&events),
2477 [
2478 "PointerFocus",
2479 "Wheel",
2480 "Button",
2481 "TextCommit",
2482 "PointerFocus"
2483 ]
2484 );
2485 let ShellEvent::PointerFocus {
2486 entered, position, ..
2487 } = &events[4]
2488 else {
2489 panic!("wrong variant");
2490 };
2491 assert!(!entered);
2492 assert_eq!(*position, None, "a leave has no position");
2493 assert!(events.iter().all(ShellEvent::is_input));
2494 }
2495
2496 #[test]
2503 fn contacts_are_injectable_and_a_touchless_shell_refuses_them() {
2504 let (mut shell, window) = shell_with_configured_window();
2505 assert!(shell.caps().contains(ShellCaps::TOUCH));
2506 for (contact, phase, x) in [
2507 (1, TouchPhase::Began, 10.0),
2508 (2, TouchPhase::Began, 200.0),
2509 (2, TouchPhase::Moved, 220.0),
2510 (1, TouchPhase::Ended, 10.0),
2511 (2, TouchPhase::Cancelled, 220.0),
2512 ] {
2513 shell
2514 .touch(
2515 window,
2516 ContactId(contact),
2517 phase,
2518 PhysicalPoint::new(x, 5.0),
2519 )
2520 .expect("a contact");
2521 }
2522 let events = drain(&mut shell);
2523 assert_eq!(names(&events), ["Touch"; 5]);
2524 let seen: Vec<(ContactId, TouchPhase, f64)> = events
2525 .iter()
2526 .map(|event| {
2527 let ShellEvent::Touch {
2528 contact,
2529 phase,
2530 position,
2531 ..
2532 } = event
2533 else {
2534 panic!("wrong variant");
2535 };
2536 (*contact, *phase, position.x)
2537 })
2538 .collect();
2539 assert_eq!(
2540 seen,
2541 vec![
2542 (ContactId(1), TouchPhase::Began, 10.0),
2543 (ContactId(2), TouchPhase::Began, 200.0),
2544 (ContactId(2), TouchPhase::Moved, 220.0),
2545 (ContactId(1), TouchPhase::Ended, 10.0),
2546 (ContactId(2), TouchPhase::Cancelled, 220.0),
2547 ]
2548 );
2549 assert!(events.iter().all(ShellEvent::is_input));
2550
2551 let mut touchless = HeadlessShell::new()
2552 .with_caps(ShellCaps::DESKTOP - ShellCaps::TOUCH)
2553 .with_configure_delay(0);
2554 let window = touchless
2555 .create_window(&WindowDesc::default())
2556 .expect("window");
2557 drain(&mut touchless);
2558 assert!(matches!(
2559 touchless.touch(
2560 window,
2561 ContactId(1),
2562 TouchPhase::Began,
2563 PhysicalPoint::ORIGIN
2564 ),
2565 Err(ShellError::Unsupported { .. })
2566 ));
2567 assert!(drain(&mut touchless).is_empty(), "and queued nothing");
2568 }
2569
2570 #[test]
2571 fn warping_produces_motion_like_every_platform_that_has_it() {
2572 let (mut shell, window) = shell_with_configured_window();
2573 shell
2574 .warp_pointer(window, PhysicalPoint::new(320.0, 180.0))
2575 .expect("warp");
2576 let events = drain(&mut shell);
2577 let ShellEvent::PointerMotion { abs, raw_delta, .. } = events[0] else {
2578 panic!("wrong variant");
2579 };
2580 assert_eq!(abs, Some(PhysicalPoint::new(320.0, 180.0)));
2581 assert_eq!(raw_delta, None, "a warp is not physical motion");
2582 }
2583
2584 #[test]
2587 fn capabilities_gate_the_operations_that_need_them() {
2588 let mut shell = HeadlessShell::new().with_caps(ShellCaps::empty());
2589 let window = shell.create_window(&WindowDesc::default()).expect("window");
2590
2591 assert!(matches!(
2592 shell.warp_pointer(window, PhysicalPoint::ORIGIN),
2593 Err(ShellError::Unsupported { .. })
2594 ));
2595 assert!(matches!(
2596 shell.set_pointer_mode(window, PointerMode::Locked),
2597 Err(ShellError::Unsupported { .. })
2598 ));
2599 assert!(matches!(
2600 shell.clipboard_request(window, MimeType::TextUtf8),
2601 Err(ShellError::Unsupported { .. })
2602 ));
2603 shell
2605 .set_pointer_mode(window, PointerMode::Free)
2606 .expect("Free is always available");
2607 assert!(matches!(
2609 shell.create_window(&WindowDesc::default()),
2610 Err(ShellError::Unsupported { .. })
2611 ));
2612 }
2613
2614 #[test]
2615 fn stale_handles_fail_cleanly_on_every_accessor() {
2616 let (mut shell, window) = shell_with_configured_window();
2617 shell.destroy_window(window).expect("destroy");
2618 assert_eq!(
2619 drain(&mut shell),
2620 vec![ShellEvent::WindowDestroyed { window }]
2621 );
2622
2623 assert!(matches!(
2624 shell.window_state(window),
2625 Err(ShellError::InvalidWindow { .. })
2626 ));
2627 assert!(matches!(
2628 shell.surface_target(window),
2629 Err(ShellError::InvalidWindow { .. })
2630 ));
2631 assert!(matches!(
2632 shell.resize(window, PhysicalSize::new(1, 1)),
2633 Err(ShellError::InvalidWindow { .. })
2634 ));
2635 assert!(matches!(
2636 shell.set_title(window, "gone"),
2637 Err(ShellError::InvalidWindow { .. })
2638 ));
2639 assert!(matches!(
2640 shell.destroy_window(window),
2641 Err(ShellError::InvalidWindow { .. })
2642 ));
2643 assert!(matches!(
2644 shell.key_press(window, KeyCode::KeyA),
2645 Err(ShellError::InvalidWindow { .. })
2646 ));
2647 }
2648
2649 #[test]
2650 fn destroying_a_window_cancels_its_pending_configure() {
2651 let (mut shell, window) = shell_with_window();
2654 shell.destroy_window(window).expect("destroy");
2655 assert_eq!(names(&drain(&mut shell)), ["WindowDestroyed"]);
2656 assert_eq!(drain(&mut shell), Vec::new());
2657 assert_eq!(drain(&mut shell), Vec::new());
2658 }
2659
2660 #[test]
2661 fn close_requests_are_questions() {
2662 let (mut shell, window) = shell_with_configured_window();
2663 assert!(matches!(
2664 shell.reply_close_request(window, CloseReply::Close),
2665 Err(ShellError::NoPendingCloseRequest { .. })
2666 ));
2667
2668 shell.request_close(window).expect("request");
2669 assert!(shell.window_state(window).expect("state").close_pending);
2670 shell
2671 .reply_close_request(window, CloseReply::Keep)
2672 .expect("keep");
2673 assert!(!shell.window_state(window).expect("state").close_pending);
2674 assert!(shell.window_state(window).is_ok(), "still open");
2675
2676 shell.request_close(window).expect("request again");
2677 shell
2678 .reply_close_request(window, CloseReply::Close)
2679 .expect("close");
2680 assert!(matches!(
2681 shell.window_state(window),
2682 Err(ShellError::InvalidWindow { .. })
2683 ));
2684 }
2685
2686 #[test]
2687 fn monitor_hotplug_reports_a_change_and_keeps_ids_unique() {
2688 let mut shell = HeadlessShell::new();
2689 let extra = shell.plug_monitor(MonitorInfo {
2690 id: MonitorId(0),
2691 name: "HEADLESS-2".to_string(),
2692 bounds: PhysicalRect::new(1920, 0, 1920, 1080),
2693 work_area: PhysicalRect::new(1920, 0, 1920, 1080),
2694 scale_factor: 1.0,
2695 refresh_millihertz: 60_000,
2696 is_primary: false,
2697 });
2698 assert_eq!(shell.monitors().len(), 2);
2699 assert_eq!(drain(&mut shell), vec![ShellEvent::MonitorsChanged]);
2700
2701 shell.unplug_monitor(extra).expect("unplug");
2702 assert_eq!(shell.monitors().len(), 1);
2703 assert_eq!(
2704 drain(&mut shell),
2705 vec![
2706 ShellEvent::MonitorRemoved { monitor: extra },
2707 ShellEvent::MonitorsChanged
2708 ]
2709 );
2710 assert!(matches!(
2711 shell.unplug_monitor(extra),
2712 Err(ShellError::NoSuchMonitor(_))
2713 ));
2714 assert_eq!(
2715 shell.monitor(MonitorId(1)).map(|m| m.name.as_str()),
2716 Some("HEADLESS-1")
2717 );
2718 assert!(shell.monitor(extra).is_none());
2719 }
2720
2721 #[test]
2722 fn unplugging_a_monitor_asks_its_panels_and_only_its_panels_to_close() {
2723 let mut shell = HeadlessShell::new();
2724 let second = shell.plug_monitor(MonitorInfo {
2725 id: MonitorId(0),
2726 name: "HEADLESS-2".to_string(),
2727 bounds: PhysicalRect::new(1920, 0, 1920, 1080),
2728 work_area: PhysicalRect::new(1920, 0, 1920, 1080),
2729 scale_factor: 1.0,
2730 refresh_millihertz: 60_000,
2731 is_primary: false,
2732 });
2733 let first = shell.monitors()[0].id;
2734 let panel_on = |shell: &mut HeadlessShell, monitor| {
2735 shell
2736 .create_window(&WindowDesc {
2737 role: crate::WindowRole::Panel(crate::PanelDesc {
2738 monitor,
2739 ..crate::PanelDesc::default()
2740 }),
2741 ..WindowDesc::default()
2742 })
2743 .expect("panel")
2744 };
2745 let doomed = panel_on(&mut shell, Some(second));
2746 let already_asked = panel_on(&mut shell, Some(second));
2747 let elsewhere = panel_on(&mut shell, Some(first));
2748 let anywhere = panel_on(&mut shell, None);
2749 let toplevel = shell.create_window(&WindowDesc::default()).expect("window");
2750 shell.request_close(already_asked).expect("close");
2751 settle(&mut shell);
2752 drain(&mut shell);
2753
2754 shell.unplug_monitor(second).expect("unplug");
2755 assert_eq!(
2756 drain(&mut shell),
2757 vec![
2758 ShellEvent::MonitorRemoved { monitor: second },
2759 ShellEvent::CloseRequested { window: doomed },
2760 ShellEvent::MonitorsChanged,
2761 ]
2762 );
2763 for window in [elsewhere, anywhere, toplevel] {
2764 assert!(!shell.window_state(window).expect("open").close_pending);
2765 }
2766 }
2767
2768 #[test]
2769 fn drops_require_both_the_capability_and_the_opt_in() {
2770 let mut shell = HeadlessShell::new();
2771 let plain = shell.create_window(&WindowDesc::default()).expect("window");
2772 assert!(matches!(
2773 shell.drop_file(plain, "/tmp/a.gltf", None),
2774 Err(ShellError::Unsupported { .. })
2775 ));
2776
2777 let dropper = shell
2778 .create_window(&WindowDesc {
2779 accept_drops: true,
2780 ..WindowDesc::default()
2781 })
2782 .expect("window");
2783 settle(&mut shell);
2784 shell
2785 .drop_file(dropper, "/tmp/a.gltf", Some(PhysicalPoint::new(4.0, 5.0)))
2786 .expect("drop");
2787 assert_eq!(names(&drain(&mut shell))[0], "DroppedFile");
2788 }
2789
2790 #[test]
2791 fn a_sink_that_queues_more_events_does_not_spin_the_pump() {
2792 let (mut shell, window) = shell_with_configured_window();
2793 shell.inject(ShellEvent::MonitorsChanged);
2794 let mut extra = Vec::new();
2795 shell.pump(&mut |event| extra.push(event));
2796 assert_eq!(extra.len(), 1);
2797 shell.inject(ShellEvent::CloseRequested { window });
2798 assert_eq!(shell.pending_events(), 1);
2799 }
2800
2801 #[test]
2802 fn accessors_that_no_real_backend_can_provide_are_headless_only() {
2803 let (mut shell, window) = shell_with_configured_window();
2804 shell.set_title(window, "renamed").expect("title");
2805 assert_eq!(shell.title(window).expect("title"), "renamed");
2806 shell.set_cursor(window, None).expect("hide");
2807 assert_eq!(shell.cursor(window).expect("cursor"), None);
2808 shell
2809 .set_cursor(window, Some(CursorIcon::Crosshair))
2810 .expect("shape");
2811 assert_eq!(
2812 shell.cursor(window).expect("cursor"),
2813 Some(CursorIcon::Crosshair)
2814 );
2815 shell.set_visible(window, false).expect("hide window");
2816 assert!(!shell.window_state(window).expect("state").visible);
2817 }
2818
2819 #[test]
2820 fn waiting_is_observable_and_focus_transitions_are_events() {
2821 let (mut shell, window) = shell_with_configured_window();
2822 assert_eq!(shell.wait_count(), 0);
2823 shell.wait_events(Some(Duration::from_millis(5)));
2824 shell.wait_events(None);
2825 assert_eq!(shell.wait_count(), 2);
2826
2827 shell.set_focus(window, true).expect("focus");
2828 assert!(shell.window_state(window).expect("state").focused);
2829 shell.set_focus(window, false).expect("unfocus");
2830 assert_eq!(names(&drain(&mut shell)), ["Focus", "Focus"]);
2831 }
2832
2833 const LONG_WAIT: Duration = Duration::from_secs(60);
2838 const RING_AFTER: Duration = Duration::from_millis(50);
2840
2841 fn ring_later(waker: ShellWaker) -> std::thread::JoinHandle<()> {
2843 std::thread::spawn(move || {
2844 std::thread::sleep(RING_AFTER);
2845 waker.wake();
2846 })
2847 }
2848
2849 #[test]
2850 fn a_wake_from_another_thread_ends_a_long_wait() {
2851 let mut shell = HeadlessShell::new();
2852 let ringer = ring_later(shell.waker().expect("headless has a waker"));
2853 let start = std::time::Instant::now();
2854 shell.wait_events(Some(LONG_WAIT));
2855 let slept = start.elapsed();
2856 ringer.join().expect("the ringer");
2857 assert!(slept < LONG_WAIT / 2, "the wait ran {slept:?}");
2858 }
2859
2860 #[test]
2861 fn a_wake_before_the_wait_ends_it_at_once_and_wakes_coalesce() {
2862 let mut shell = HeadlessShell::new();
2863 let waker = shell.waker().expect("headless has a waker");
2864 for _ in 0..3 {
2865 waker.wake();
2866 }
2867 let start = std::time::Instant::now();
2868 shell.wait_events(Some(LONG_WAIT));
2869 assert!(
2870 start.elapsed() < LONG_WAIT / 2,
2871 "a rung bell must end the wait"
2872 );
2873
2874 let timeout = Duration::from_millis(100);
2876 let start = std::time::Instant::now();
2877 shell.wait_events(Some(timeout));
2878 assert!(
2879 start.elapsed() >= timeout / 2,
2880 "the bell was cleared, so this wait must sleep, but it took {:?}",
2881 start.elapsed()
2882 );
2883 }
2884
2885 #[test]
2886 fn with_a_waker_out_an_untimed_wait_sleeps_until_it_rings() {
2887 let mut shell = HeadlessShell::new();
2888 let ringer = ring_later(shell.waker().expect("headless has a waker"));
2889 let start = std::time::Instant::now();
2890 shell.wait_events(None);
2891 let slept = start.elapsed();
2892 ringer.join().expect("the ringer");
2893 assert!(
2894 slept >= RING_AFTER / 2,
2895 "an untimed wait returned after {slept:?}, before anything rang"
2896 );
2897 }
2898
2899 #[test]
2902 fn the_clipboard_round_trips_through_an_event() {
2903 let (mut shell, window) = shell_with_configured_window();
2904 shell
2905 .clipboard_offer(
2906 window,
2907 &[
2908 ClipboardOffer::text("node"),
2909 ClipboardOffer::ron("(kind:\"node\")"),
2910 ],
2911 )
2912 .expect("offer");
2913 assert_eq!(
2914 shell.clipboard_bytes(MimeType::TextUtf8),
2915 Some(&b"node"[..])
2916 );
2917
2918 let request = shell
2919 .clipboard_request(window, MimeType::CrcblRon)
2920 .expect("request");
2921 assert_eq!(
2922 drain(&mut shell)[0],
2923 ShellEvent::ClipboardData {
2924 window,
2925 request,
2926 mime: ReceivedMime::from(MimeType::CrcblRon),
2927 content: ClipboardContent::Bytes(b"(kind:\"node\")".to_vec()),
2928 }
2929 );
2930
2931 let request = shell
2934 .clipboard_request(window, MimeType::UriList)
2935 .expect("request");
2936 assert_eq!(
2937 drain(&mut shell)[0],
2938 ShellEvent::ClipboardData {
2939 window,
2940 request,
2941 mime: ReceivedMime::from(MimeType::UriList),
2942 content: ClipboardContent::Empty,
2943 }
2944 );
2945
2946 shell
2949 .clipboard_offer(window, &[ClipboardOffer::text("")])
2950 .expect("offer");
2951 let request = shell
2952 .clipboard_request(window, MimeType::TextUtf8)
2953 .expect("request");
2954 assert_eq!(
2955 drain(&mut shell)[0],
2956 ShellEvent::ClipboardData {
2957 window,
2958 request,
2959 mime: ReceivedMime::from(MimeType::TextUtf8),
2960 content: ClipboardContent::Bytes(Vec::new()),
2961 }
2962 );
2963 }
2964
2965 #[test]
2966 fn a_read_taken_while_the_clipboard_is_unreadable_is_held_not_answered_empty() {
2967 let (mut shell, window) = shell_with_configured_window();
2973 shell
2974 .clipboard_offer(window, &[ClipboardOffer::text("published")])
2975 .expect("offer");
2976 shell.set_clipboard_readable(false);
2977 assert!(!shell.clipboard_readable(window));
2978
2979 let request = shell
2980 .clipboard_request(window, MimeType::TextUtf8)
2981 .expect("a read is accepted even when it cannot be answered yet");
2982 assert_eq!(shell.held_clipboard_reads(), 1);
2983 assert!(
2984 drain(&mut shell).is_empty(),
2985 "a held read produces no event at all — least of all an empty one"
2986 );
2987
2988 shell.set_clipboard_readable(true);
2990 let events = drain(&mut shell);
2991 assert_eq!(events.len(), 1);
2992 assert_eq!(
2993 events[0],
2994 ShellEvent::ClipboardData {
2995 window,
2996 request,
2997 mime: ReceivedMime::from(MimeType::TextUtf8),
2998 content: ClipboardContent::Bytes(b"published".to_vec()),
2999 }
3000 );
3001 assert_eq!(shell.held_clipboard_reads(), 0);
3002 }
3003
3004 #[test]
3005 fn a_read_that_never_becomes_answerable_ends_as_unavailable() {
3006 let (mut shell, window) = shell_with_configured_window();
3010 shell
3011 .clipboard_offer(window, &[ClipboardOffer::text("unreachable")])
3012 .expect("offer");
3013 shell.set_clipboard_readable(false);
3014 shell.clipboard_deadline(2);
3015
3016 let request = shell
3017 .clipboard_request(window, MimeType::TextUtf8)
3018 .expect("request");
3019 assert!(drain(&mut shell).is_empty(), "pump 1: still held");
3020 assert!(drain(&mut shell).is_empty(), "pump 2: still held");
3021 let events = drain(&mut shell);
3022 assert_eq!(
3023 events[0],
3024 ShellEvent::ClipboardData {
3025 window,
3026 request,
3027 mime: ReceivedMime::from(MimeType::TextUtf8),
3028 content: ClipboardContent::Unavailable,
3031 }
3032 );
3033 assert_eq!(shell.held_clipboard_reads(), 0);
3034 }
3035
3036 #[test]
3037 fn a_peer_that_holds_the_clipboard_and_never_delivers_is_unavailable() {
3038 let (mut shell, window) = shell_with_configured_window();
3042 shell.set_foreign_clipboard(vec![(
3043 ReceivedMime::new("text/plain"),
3044 b"never sent".to_vec(),
3045 )]);
3046 shell.set_clipboard_stalled(true);
3047 shell.clipboard_deadline(1);
3048 assert!(
3049 !shell.clipboard_readable(window),
3050 "a clipboard whose owner will not answer is not readable"
3051 );
3052
3053 let request = shell
3054 .clipboard_request(window, MimeType::TextUtf8)
3055 .expect("request");
3056 assert!(drain(&mut shell).is_empty());
3057 let events = drain(&mut shell);
3058 let ShellEvent::ClipboardData {
3059 request: answered,
3060 content,
3061 ..
3062 } = &events[0]
3063 else {
3064 panic!("wrong variant");
3065 };
3066 assert_eq!(*answered, request);
3067 assert_eq!(*content, ClipboardContent::Unavailable);
3068
3069 shell.set_clipboard_stalled(false);
3071 let request = shell
3072 .clipboard_request(window, MimeType::TextUtf8)
3073 .expect("request");
3074 assert_eq!(
3075 drain(&mut shell)[0],
3076 ShellEvent::ClipboardData {
3077 window,
3078 request,
3079 mime: ReceivedMime::new("text/plain"),
3080 content: ClipboardContent::Bytes(b"never sent".to_vec()),
3081 }
3082 );
3083 }
3084
3085 #[test]
3086 fn claiming_the_clipboard_can_require_a_user_interaction() {
3087 let (mut shell, window) = shell_with_configured_window();
3091 shell.require_user_interaction(true);
3092
3093 let refused = shell.clipboard_offer(window, &[ClipboardOffer::text("stolen")]);
3094 assert!(
3095 matches!(
3096 refused,
3097 Err(ShellError::NeedsUserInteraction {
3098 backend: ShellBackend::Headless,
3099 ..
3100 })
3101 ),
3102 "{refused:?}"
3103 );
3104 assert!(
3105 shell.clipboard_bytes(MimeType::TextUtf8).is_none(),
3106 "a refused claim must not have taken the clipboard anyway"
3107 );
3108
3109 shell.key_press(window, KeyCode::KeyC).expect("key");
3112 shell
3113 .clipboard_offer(window, &[ClipboardOffer::text("copied")])
3114 .expect("with a serial in hand, the claim goes through");
3115 assert_eq!(
3116 shell.clipboard_bytes(MimeType::TextUtf8),
3117 Some(&b"copied"[..])
3118 );
3119
3120 shell.clear_user_interaction();
3122 assert!(matches!(
3123 shell.clipboard_offer(window, &[ClipboardOffer::text("later")]),
3124 Err(ShellError::NeedsUserInteraction { .. })
3125 ));
3126 }
3127
3128 #[test]
3129 fn a_held_read_for_a_destroyed_window_is_dropped_rather_than_answered() {
3130 let (mut shell, window) = shell_with_configured_window();
3133 shell.set_clipboard_readable(false);
3134 let _request = shell
3135 .clipboard_request(window, MimeType::TextUtf8)
3136 .expect("request");
3137 assert_eq!(shell.held_clipboard_reads(), 1);
3138
3139 shell.destroy_window(window).expect("destroy");
3140 let events = drain(&mut shell);
3141 assert_eq!(shell.held_clipboard_reads(), 0);
3142 assert!(
3143 !events
3144 .iter()
3145 .any(|event| matches!(event, ShellEvent::ClipboardData { .. })),
3146 "no clipboard answer may name a destroyed window: {events:?}"
3147 );
3148 }
3149
3150 #[test]
3151 fn a_paste_from_a_foreign_application_keeps_the_foreign_spelling() {
3152 let (mut shell, window) = shell_with_configured_window();
3153 shell.set_foreign_clipboard(vec![
3154 (ReceivedMime::new("text/plain"), b"from gedit".to_vec()),
3157 (
3158 ReceivedMime::new("application/vnd.some-editor.scene+json"),
3159 b"{}".to_vec(),
3160 ),
3161 ]);
3162
3163 let request = shell
3165 .clipboard_request(window, MimeType::TextUtf8)
3166 .expect("request");
3167 let events = drain(&mut shell);
3168 let ShellEvent::ClipboardData {
3169 request: answered,
3170 mime,
3171 content,
3172 ..
3173 } = &events[0]
3174 else {
3175 panic!("wrong variant");
3176 };
3177 assert_eq!(*answered, request);
3178 assert_eq!(content.bytes(), Some(&b"from gedit"[..]));
3179 assert_eq!(mime.as_str(), "text/plain");
3181 assert!(mime.matches(MimeType::TextUtf8));
3182 assert_ne!(*mime, ReceivedMime::from(MimeType::TextUtf8));
3183
3184 assert_eq!(
3187 shell.clipboard_bytes(MimeType::Other("application/vnd.some-editor.scene+json")),
3188 Some(&b"{}"[..])
3189 );
3190 }
3191
3192 #[test]
3195 #[should_panic(expected = "a shell needs at least one monitor")]
3196 fn a_shell_with_no_monitors_is_rejected() {
3197 let _ = HeadlessShell::new().with_monitors(Vec::new());
3198 }
3199
3200 #[test]
3201 #[should_panic(expected = "a text commit is never empty")]
3202 fn an_empty_text_commit_is_rejected() {
3203 let (mut shell, window) = shell_with_configured_window();
3204 let _ = shell.commit_text(window, "");
3205 }
3206}