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::frame::FrameRequests;
148use crate::waker::WakeSignal;
149use crate::{
150 ClipboardContent, ClipboardOffer, ClipboardRequestId, CloseReply, CursorIcon, DisplayMode,
151 KeyCode, LogicalRect, LogicalSize, MimeType, MonitorId, MonitorInfo, PhysicalPoint,
152 PhysicalRect, PhysicalSize, PointerMode, ReceivedMime, Shell, ShellBackend, ShellCaps,
153 ShellError, ShellEvent, ShellWaker, SizeConstraints, TextPurpose, WindowConfiguration,
154 WindowDesc, WindowId, WindowState,
155};
156
157pub const DEFAULT_MONITOR_SIZE: PhysicalSize = PhysicalSize::new(1920, 1080);
162
163pub const DEFAULT_REFRESH_MILLIHERTZ: u32 = 60_000;
165
166pub const DEFAULT_CONFIGURE_DELAY: u32 = 1;
172
173pub const VIRTUAL_DEVICE: DeviceId = DeviceId(1);
179
180pub const DEFAULT_CLIPBOARD_DEADLINE: u32 = 4;
188
189#[derive(Clone, Debug)]
195struct PendingRead {
196 window: WindowId,
197 request: ClipboardRequestId,
198 mime: MimeType,
199 countdown: u32,
201}
202
203#[derive(Clone, Copy, Debug)]
207struct PendingConfigure {
208 size: PhysicalSize,
209 scale_factor: f64,
210 mode: DisplayMode,
211 countdown: u32,
213}
214
215struct ScheduledScript {
217 countdown: u32,
219 script: Box<dyn FnOnce(&mut HeadlessShell) + Send>,
220}
221
222impl core::fmt::Debug for ScheduledScript {
223 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
224 f.debug_struct("ScheduledScript")
225 .field("countdown", &self.countdown)
226 .finish_non_exhaustive()
227 }
228}
229
230#[derive(Clone, Debug)]
232struct HeadlessWindow {
233 title: String,
234 configuration: Option<WindowConfiguration>,
236 pending: Option<PendingConfigure>,
238 requested_mode: DisplayMode,
239 requested_constraints: SizeConstraints,
240 focused: bool,
241 visible: bool,
242 pointer_mode: PointerMode,
243 cursor: Option<CursorIcon>,
244 close_pending: bool,
245 accept_drops: bool,
246 parent: Option<WindowId>,
248 popup_input: Option<crate::PopupInput>,
250 panel: Option<crate::role::KeptPanel>,
254 text_input: Option<TextPurpose>,
256 text_input_area: Option<PhysicalRect>,
258 input_region: Option<Vec<PhysicalRect>>,
260 #[cfg(feature = "accesskit")]
263 accessibility: Option<Vec<accesskit::TreeUpdate>>,
264}
265
266impl HeadlessWindow {
267 fn size(&self) -> Option<PhysicalSize> {
269 self.configuration.map(|config| config.size)
270 }
271
272 fn scale_factor(&self) -> f64 {
278 self.configuration
279 .map(|config| config.scale_factor)
280 .or_else(|| self.pending.map(|pending| pending.scale_factor))
281 .unwrap_or(1.0)
282 }
283
284 fn settled_mode(&self) -> DisplayMode {
293 self.configuration
294 .map(|config| config.mode)
295 .or_else(|| self.pending.map(|pending| pending.mode))
296 .unwrap_or(self.requested_mode)
297 }
298
299 fn base_size(&self) -> PhysicalSize {
301 self.size()
302 .or_else(|| self.pending.map(|pending| pending.size))
303 .unwrap_or(PhysicalSize::new(1, 1))
304 }
305
306 fn configure_at(&mut self, size: LogicalSize, countdown: u32) {
310 if let Some(pending) = self.pending.as_mut() {
311 pending.size = size.to_physical(pending.scale_factor);
312 } else if let Some(config) = self.configuration {
313 self.pending = Some(PendingConfigure {
314 size: size.to_physical(config.scale_factor),
315 scale_factor: config.scale_factor,
316 mode: config.mode,
317 countdown,
318 });
319 }
320 }
321}
322
323#[derive(Debug)]
328pub struct HeadlessShell {
329 caps: ShellCaps,
330 monitors: Vec<MonitorInfo>,
331 next_monitor_id: u32,
332 windows: Pool<HeadlessWindow>,
333 queue: VecDeque<ShellEvent>,
334 scripts: Vec<ScheduledScript>,
337 clock: ManualTime,
338 modifiers: Modifiers,
339 clipboard: Vec<(ReceivedMime, Vec<u8>)>,
340 next_request_id: u32,
341 reads: Vec<PendingRead>,
344 clipboard_readable: bool,
348 clipboard_stalled: bool,
350 clipboard_needs_interaction: bool,
352 saw_interaction: bool,
355 clipboard_deadline: u32,
358 waits: u32,
359 configure_delay: u32,
360 wake: Option<Arc<HeadlessWake>>,
362 frames: FrameRequests,
364 outputs_powered: bool,
367}
368
369#[derive(Debug, Default)]
372struct HeadlessWake {
373 rung: Mutex<bool>,
374 bell: Condvar,
375}
376
377impl HeadlessWake {
378 fn wait(&self, timeout: Option<Duration>) {
381 let rung = self.rung.lock().unwrap_or_else(PoisonError::into_inner);
382 let mut rung = match timeout {
383 Some(timeout) => {
384 self.bell
385 .wait_timeout_while(rung, timeout, |rung| !*rung)
386 .unwrap_or_else(PoisonError::into_inner)
387 .0
388 }
389 None => self
390 .bell
391 .wait_while(rung, |rung| !*rung)
392 .unwrap_or_else(PoisonError::into_inner),
393 };
394 *rung = false;
395 }
396}
397
398impl WakeSignal for HeadlessWake {
399 fn wake(&self) {
400 *self.rung.lock().unwrap_or_else(PoisonError::into_inner) = true;
401 self.bell.notify_all();
402 }
403}
404
405impl Default for HeadlessShell {
406 fn default() -> Self {
407 Self::new()
408 }
409}
410
411impl HeadlessShell {
412 #[must_use]
414 pub fn new() -> Self {
415 Self {
416 caps: ShellCaps::DESKTOP,
417 monitors: vec![MonitorInfo {
418 id: MonitorId(1),
419 name: "HEADLESS-1".to_string(),
420 bounds: PhysicalRect::new(
421 0,
422 0,
423 DEFAULT_MONITOR_SIZE.width,
424 DEFAULT_MONITOR_SIZE.height,
425 ),
426 work_area: PhysicalRect::new(
427 0,
428 0,
429 DEFAULT_MONITOR_SIZE.width,
430 DEFAULT_MONITOR_SIZE.height,
431 ),
432 scale_factor: 1.0,
433 refresh_millihertz: DEFAULT_REFRESH_MILLIHERTZ,
434 is_primary: true,
435 }],
436 next_monitor_id: 2,
437 windows: Pool::new(),
438 queue: VecDeque::new(),
439 scripts: Vec::new(),
440 clock: ManualTime::new(),
441 modifiers: Modifiers::empty(),
442 clipboard: Vec::new(),
443 next_request_id: 1,
444 reads: Vec::new(),
445 clipboard_readable: true,
446 clipboard_stalled: false,
447 clipboard_needs_interaction: false,
448 saw_interaction: false,
449 clipboard_deadline: DEFAULT_CLIPBOARD_DEADLINE,
450 waits: 0,
451 configure_delay: DEFAULT_CONFIGURE_DELAY,
452 wake: None,
453 frames: FrameRequests::default(),
454 outputs_powered: true,
455 }
456 }
457
458 #[must_use]
463 pub fn with_caps(mut self, caps: ShellCaps) -> Self {
464 self.caps = caps;
465 self
466 }
467
468 #[must_use]
483 pub fn with_monitors(mut self, monitors: Vec<MonitorInfo>) -> Self {
484 assert!(!monitors.is_empty(), "a shell needs at least one monitor");
485 for (index, monitor) in monitors.iter().enumerate() {
486 assert!(
487 monitor.id.0 != u32::MAX,
488 "monitor id {} leaves no id for the next hotplug",
489 monitor.id.0
490 );
491 assert!(
492 !monitors[..index].iter().any(|seen| seen.id == monitor.id),
493 "monitor id {} appears twice",
494 monitor.id.0
495 );
496 }
497 self.next_monitor_id = monitors
498 .iter()
499 .map(|monitor| monitor.id.0 + 1)
500 .max()
501 .unwrap_or(1);
502 self.monitors = monitors;
503 self
504 }
505
506 #[must_use]
511 pub fn with_scale_factor(mut self, scale_factor: f64) -> Self {
512 assert!(scale_factor > 0.0, "scale factor must be positive");
513 self.monitors[0].scale_factor = scale_factor;
514 self
515 }
516
517 #[must_use]
522 pub const fn with_configure_delay(mut self, pumps: u32) -> Self {
523 self.configure_delay = pumps;
524 self
525 }
526
527 pub const fn set_configure_delay(&mut self, pumps: u32) {
537 self.configure_delay = pumps;
538 }
539
540 pub fn set_caps(&mut self, caps: ShellCaps) {
546 self.caps = caps;
547 }
548
549 #[must_use]
551 pub fn now(&self) -> EventTime {
552 EventTime::from_duration(self.clock.elapsed())
553 }
554
555 pub fn advance(&mut self, delta: Duration) {
561 self.clock.advance(delta);
562 }
563
564 pub fn set_time(&mut self, now: Duration) {
572 self.clock.set(now);
573 }
574
575 pub fn set_modifiers(&mut self, modifiers: Modifiers) {
580 self.modifiers = modifiers;
581 }
582
583 #[must_use]
585 pub fn pending_events(&self) -> usize {
586 self.queue.len()
587 }
588
589 #[must_use]
595 pub const fn wait_count(&self) -> u32 {
596 self.waits
597 }
598
599 pub fn inject(&mut self, event: ShellEvent) {
607 self.queue.push_back(event);
608 }
609
610 pub fn schedule(&mut self, pumps: u32, script: impl FnOnce(&mut Self) + Send + 'static) {
629 self.scripts.push(ScheduledScript {
630 countdown: pumps,
631 script: Box::new(script),
632 });
633 }
634
635 pub fn resize(&mut self, window: WindowId, size: PhysicalSize) -> Result<(), ShellError> {
651 let state = self.window_mut(window)?;
652 let scale_factor = state.scale_factor();
653 let mode = state.settled_mode();
654 let constrained = state.requested_constraints.apply(size, scale_factor);
655 state.pending = None;
656 state.configuration = Some(WindowConfiguration {
657 size: constrained,
658 scale_factor,
659 mode,
660 });
661 self.queue.push_back(ShellEvent::Resized {
662 window,
663 size: constrained,
664 scale_factor,
665 });
666 Ok(())
667 }
668
669 pub fn change_scale_factor(
691 &mut self,
692 window: WindowId,
693 scale_factor: f64,
694 ) -> Result<(), ShellError> {
695 assert!(scale_factor > 0.0, "scale factor must be positive");
696 let state = self.window_mut(window)?;
697 let logical = state.base_size().to_logical(state.scale_factor());
698 let mode = state.settled_mode();
699 let size = state
700 .requested_constraints
701 .apply(logical.to_physical(scale_factor), scale_factor);
702 state.pending = None;
703 state.configuration = Some(WindowConfiguration {
704 size,
705 scale_factor,
706 mode,
707 });
708 self.queue.push_back(ShellEvent::ScaleFactorChanged {
709 window,
710 scale_factor,
711 size,
712 });
713 self.queue.push_back(ShellEvent::Resized {
714 window,
715 size,
716 scale_factor,
717 });
718 Ok(())
719 }
720
721 pub fn set_focus(&mut self, window: WindowId, focused: bool) -> Result<(), ShellError> {
727 self.window_mut(window)?.focused = focused;
728 self.queue.push_back(ShellEvent::Focus { window, focused });
729 Ok(())
730 }
731
732 pub fn request_close(&mut self, window: WindowId) -> Result<(), ShellError> {
741 self.window_mut(window)?.close_pending = true;
742 self.queue.push_back(ShellEvent::CloseRequested { window });
743 Ok(())
744 }
745
746 pub fn key(
759 &mut self,
760 window: WindowId,
761 key: KeyCode,
762 state: ButtonState,
763 ) -> Result<(), ShellError> {
764 self.key_with(window, key, state, false)
765 }
766
767 pub fn key_press(&mut self, window: WindowId, key: KeyCode) -> Result<(), ShellError> {
773 self.key(window, key, ButtonState::Pressed)
774 }
775
776 pub fn key_release(&mut self, window: WindowId, key: KeyCode) -> Result<(), ShellError> {
782 self.key(window, key, ButtonState::Released)
783 }
784
785 pub fn key_repeat(&mut self, window: WindowId, key: KeyCode) -> Result<(), ShellError> {
792 self.key_with(window, key, ButtonState::Pressed, true)
793 }
794
795 fn key_with(
796 &mut self,
797 window: WindowId,
798 key: KeyCode,
799 state: ButtonState,
800 repeat: bool,
801 ) -> Result<(), ShellError> {
802 let popup_input = self.window(window)?.popup_input;
803 self.saw_interaction = true;
804 let event = ShellEvent::Key {
805 window,
806 device: VIRTUAL_DEVICE,
807 time: self.now(),
808 scancode: synthetic_scancode(key),
809 key_code: Some(key),
810 keysym: synthetic_keysym(key),
811 state,
812 repeat,
813 modifiers: self.modifiers,
814 };
815 let dismissed = crate::role::escape_dismisses(popup_input, &event);
816 self.queue.push_back(event);
817 if dismissed && !self.window(window)?.close_pending {
819 self.request_close(window)?;
820 }
821 Ok(())
822 }
823
824 pub fn move_pointer(
835 &mut self,
836 window: WindowId,
837 position: PhysicalPoint,
838 raw_delta: (f64, f64),
839 ) -> Result<(), ShellError> {
840 let locked = self.window(window)?.pointer_mode == PointerMode::Locked;
841 let raw = if self.caps.contains(ShellCaps::RAW_POINTER_MOTION) {
842 Some(raw_delta)
843 } else {
844 None
845 };
846 self.saw_interaction = true;
847 let event = ShellEvent::PointerMotion {
848 window,
849 device: VIRTUAL_DEVICE,
850 time: self.now(),
851 abs: if locked { None } else { Some(position) },
852 raw_delta: raw,
853 };
854 self.queue.push_back(event);
855 Ok(())
856 }
857
858 pub fn set_pointer_focus(
864 &mut self,
865 window: WindowId,
866 entered: bool,
867 position: Option<PhysicalPoint>,
868 ) -> Result<(), ShellError> {
869 self.check_window(window)?;
870 let event = ShellEvent::PointerFocus {
871 window,
872 device: VIRTUAL_DEVICE,
873 time: self.now(),
874 entered,
875 position: if entered { position } else { None },
876 };
877 self.queue.push_back(event);
878 Ok(())
879 }
880
881 pub fn button(
887 &mut self,
888 window: WindowId,
889 button: PointerButton,
890 state: ButtonState,
891 position: Option<PhysicalPoint>,
892 ) -> Result<(), ShellError> {
893 self.check_window(window)?;
894 self.saw_interaction = true;
895 let event = ShellEvent::Button {
896 window,
897 device: VIRTUAL_DEVICE,
898 time: self.now(),
899 button,
900 state,
901 position,
902 modifiers: self.modifiers,
903 };
904 self.queue.push_back(event);
905 Ok(())
906 }
907
908 pub fn scroll(
914 &mut self,
915 window: WindowId,
916 delta: ScrollDelta,
917 position: Option<PhysicalPoint>,
918 ) -> Result<(), ShellError> {
919 self.check_window(window)?;
920 self.saw_interaction = true;
921 let event = ShellEvent::Wheel {
922 window,
923 device: VIRTUAL_DEVICE,
924 time: self.now(),
925 delta,
926 position,
927 modifiers: self.modifiers,
928 };
929 self.queue.push_back(event);
930 Ok(())
931 }
932
933 pub fn touch(
961 &mut self,
962 window: WindowId,
963 contact: ContactId,
964 phase: TouchPhase,
965 position: PhysicalPoint,
966 ) -> Result<(), ShellError> {
967 self.check_window(window)?;
968 if !self.caps.contains(ShellCaps::TOUCH) {
969 return Err(self.unsupported("touch"));
970 }
971 self.saw_interaction = true;
972 let event = ShellEvent::Touch {
973 window,
974 device: VIRTUAL_DEVICE,
975 time: self.now(),
976 contact,
977 phase,
978 position,
979 };
980 self.queue.push_back(event);
981 Ok(())
982 }
983
984 pub fn commit_text(&mut self, window: WindowId, text: &str) -> Result<(), ShellError> {
995 assert!(!text.is_empty(), "a text commit is never empty");
996 self.check_window(window)?;
997 let event = ShellEvent::TextCommit {
998 window,
999 time: self.now(),
1000 text: text.to_string(),
1001 };
1002 self.queue.push_back(event);
1003 Ok(())
1004 }
1005
1006 pub fn preedit_text(
1019 &mut self,
1020 window: WindowId,
1021 text: &str,
1022 cursor: Option<usize>,
1023 ) -> Result<(), ShellError> {
1024 assert!(
1025 cursor.is_none_or(|cursor| !text.is_empty() && text.is_char_boundary(cursor)),
1026 "a pre-edit's cursor is a char boundary of a non-empty text"
1027 );
1028 self.check_window(window)?;
1029 let event = ShellEvent::TextPreedit {
1030 window,
1031 time: self.now(),
1032 text: text.to_string(),
1033 cursor,
1034 };
1035 self.queue.push_back(event);
1036 Ok(())
1037 }
1038
1039 pub fn drop_file(
1047 &mut self,
1048 window: WindowId,
1049 path: impl Into<PathBuf>,
1050 position: Option<PhysicalPoint>,
1051 ) -> Result<(), ShellError> {
1052 if !self.caps.contains(ShellCaps::DRAG_DROP) {
1053 return Err(self.unsupported("drag and drop"));
1054 }
1055 if !self.window(window)?.accept_drops {
1056 return Err(ShellError::Unsupported {
1057 backend: ShellBackend::Headless,
1058 what: "drops on a window created without accept_drops",
1059 });
1060 }
1061 let event = ShellEvent::DroppedFile {
1062 window,
1063 time: self.now(),
1064 path: path.into(),
1065 position,
1066 };
1067 self.queue.push_back(event);
1068 Ok(())
1069 }
1070
1071 pub fn plug_monitor(&mut self, mut info: MonitorInfo) -> MonitorId {
1078 let id = MonitorId(self.next_monitor_id);
1079 self.next_monitor_id += 1;
1080 info.id = id;
1081 self.monitors.push(info);
1082 self.queue.push_back(ShellEvent::MonitorsChanged);
1083 id
1084 }
1085
1086 pub fn unplug_monitor(&mut self, id: MonitorId) -> Result<(), ShellError> {
1100 let index = self
1101 .monitors
1102 .iter()
1103 .position(|monitor| monitor.id == id)
1104 .ok_or(ShellError::NoSuchMonitor(id.0))?;
1105 assert!(
1106 self.monitors.len() > 1,
1107 "a shell needs at least one monitor"
1108 );
1109 let previous = self.monitors.clone();
1110 self.monitors.remove(index);
1111 crate::monitor::queue_removed(&previous, &self.monitors, &mut self.queue);
1112 let panels: Vec<WindowId> = self
1113 .windows
1114 .iter()
1115 .filter(|(_, state)| {
1116 state.panel.as_ref().and_then(|panel| panel.desc().monitor) == Some(id)
1117 && !state.close_pending
1118 })
1119 .map(|(window, _)| window.cast())
1120 .collect();
1121 for panel in panels {
1122 self.request_close(panel)?;
1123 }
1124 self.queue.push_back(ShellEvent::MonitorsChanged);
1125 Ok(())
1126 }
1127
1128 pub fn title(&self, window: WindowId) -> Result<&str, ShellError> {
1139 Ok(self.window(window)?.title.as_str())
1140 }
1141
1142 pub fn cursor(&self, window: WindowId) -> Result<Option<CursorIcon>, ShellError> {
1150 Ok(self.window(window)?.cursor)
1151 }
1152
1153 pub fn text_input(&self, window: WindowId) -> Result<Option<TextPurpose>, ShellError> {
1162 Ok(self.window(window)?.text_input)
1163 }
1164
1165 pub fn text_input_area(&self, window: WindowId) -> Result<Option<PhysicalRect>, ShellError> {
1175 Ok(self.window(window)?.text_input_area)
1176 }
1177
1178 pub fn panel(&self, window: WindowId) -> Result<Option<crate::PanelDesc<'_>>, ShellError> {
1187 Ok(self
1188 .window(window)?
1189 .panel
1190 .as_ref()
1191 .map(crate::role::KeptPanel::desc))
1192 }
1193
1194 pub fn input_region(&self, window: WindowId) -> Result<Option<&[PhysicalRect]>, ShellError> {
1205 Ok(self.window(window)?.input_region.as_deref())
1206 }
1207
1208 pub fn frame_requested(&self, window: WindowId) -> Result<bool, ShellError> {
1218 self.window(window)?;
1219 Ok(self.frames.contains(window))
1220 }
1221
1222 pub fn set_outputs_powered(&mut self, powered: bool) {
1238 self.outputs_powered = powered;
1239 }
1240
1241 #[cfg(feature = "accesskit")]
1250 pub fn activate_accessibility(&mut self, window: WindowId) -> Result<(), ShellError> {
1251 let state = &mut self.window_mut(window)?.accessibility;
1252 if state.is_none() {
1253 *state = Some(Vec::new());
1254 self.queue
1255 .push_back(ShellEvent::AccessibilityActivated { window });
1256 }
1257 Ok(())
1258 }
1259
1260 #[cfg(feature = "accesskit")]
1270 pub fn deactivate_accessibility(&mut self, window: WindowId) -> Result<(), ShellError> {
1271 if self.window_mut(window)?.accessibility.take().is_some() {
1272 self.queue
1273 .push_back(ShellEvent::AccessibilityDeactivated { window });
1274 }
1275 Ok(())
1276 }
1277
1278 #[cfg(feature = "accesskit")]
1292 pub fn request_accessibility_action(
1293 &mut self,
1294 window: WindowId,
1295 request: accesskit::ActionRequest,
1296 ) -> Result<(), ShellError> {
1297 assert!(
1298 self.window(window)?.accessibility.is_some(),
1299 "no assistive technology is reading the window"
1300 );
1301 self.queue
1302 .push_back(ShellEvent::AccessibilityAction { window, request });
1303 Ok(())
1304 }
1305
1306 #[cfg(feature = "accesskit")]
1315 pub fn accessibility_updates(
1316 &self,
1317 window: WindowId,
1318 ) -> Result<Option<&[accesskit::TreeUpdate]>, ShellError> {
1319 Ok(self.window(window)?.accessibility.as_deref())
1320 }
1321
1322 #[must_use]
1326 pub fn clipboard_bytes(&self, mime: MimeType) -> Option<&[u8]> {
1327 self.clipboard
1328 .iter()
1329 .find(|(offered, _)| offered.matches(mime))
1330 .map(|(_, bytes)| bytes.as_slice())
1331 }
1332
1333 pub fn set_clipboard_readable(&mut self, readable: bool) {
1350 self.clipboard_readable = readable;
1351 }
1352
1353 pub fn set_clipboard_stalled(&mut self, stalled: bool) {
1362 self.clipboard_stalled = stalled;
1363 }
1364
1365 pub fn require_user_interaction(&mut self, required: bool) {
1392 self.clipboard_needs_interaction = required;
1393 }
1394
1395 pub fn clear_user_interaction(&mut self) {
1400 self.saw_interaction = false;
1401 }
1402
1403 pub fn clipboard_deadline(&mut self, pumps: u32) {
1409 self.clipboard_deadline = pumps;
1410 }
1411
1412 #[must_use]
1417 pub fn held_clipboard_reads(&self) -> usize {
1418 self.reads.len()
1419 }
1420
1421 pub fn set_foreign_clipboard(&mut self, offers: Vec<(ReceivedMime, Vec<u8>)>) {
1429 self.clipboard = offers;
1430 }
1431
1432 fn answerable(&self) -> bool {
1438 self.clipboard_readable && !self.clipboard_stalled
1439 }
1440
1441 fn answer(&self, window: WindowId, request: ClipboardRequestId, mime: MimeType) -> ShellEvent {
1443 let found = self
1448 .clipboard
1449 .iter()
1450 .find(|(offered, _)| offered.matches(mime));
1451 let (mime, content) = match found {
1452 Some((offered, bytes)) => (offered.clone(), ClipboardContent::Bytes(bytes.clone())),
1453 None => (ReceivedMime::from(mime), ClipboardContent::Empty),
1456 };
1457 ShellEvent::ClipboardData {
1458 window,
1459 request,
1460 mime,
1461 content,
1462 }
1463 }
1464
1465 fn resolve_reads(&mut self) {
1472 if self.reads.is_empty() {
1473 return;
1474 }
1475 let answerable = self.answerable();
1476 let mut held = Vec::with_capacity(self.reads.len());
1477 for mut read in core::mem::take(&mut self.reads) {
1478 if self.window(read.window).is_err() {
1482 continue;
1483 }
1484 if answerable {
1485 let answer = self.answer(read.window, read.request, read.mime);
1486 self.queue.push_back(answer);
1487 continue;
1488 }
1489 if read.countdown > 0 {
1490 read.countdown -= 1;
1491 held.push(read);
1492 continue;
1493 }
1494 self.queue.push_back(ShellEvent::ClipboardData {
1497 window: read.window,
1498 request: read.request,
1499 mime: ReceivedMime::from(read.mime),
1500 content: ClipboardContent::Unavailable,
1501 });
1502 }
1503 self.reads = held;
1504 }
1505
1506 fn deliver_due_configures(&mut self) {
1513 let mut due: Vec<(WindowId, PendingConfigure, Option<f64>)> = Vec::new();
1517 for (handle, state) in self.windows.iter_mut() {
1518 let Some(pending) = state.pending.as_mut() else {
1519 continue;
1520 };
1521 if pending.countdown > 0 {
1522 pending.countdown -= 1;
1523 continue;
1524 }
1525 let pending = state.pending.take().expect("checked just above");
1526 let previous_scale = state.configuration.map(|config| config.scale_factor);
1527 state.configuration = Some(WindowConfiguration {
1528 size: pending.size,
1529 scale_factor: pending.scale_factor,
1530 mode: pending.mode,
1531 });
1532 due.push((handle.cast(), pending, previous_scale));
1533 }
1534
1535 for (window, pending, previous_scale) in due {
1536 if previous_scale.is_some_and(|previous| previous != pending.scale_factor) {
1540 self.queue.push_back(ShellEvent::ScaleFactorChanged {
1541 window,
1542 scale_factor: pending.scale_factor,
1543 size: pending.size,
1544 });
1545 }
1546 self.queue.push_back(ShellEvent::Resized {
1547 window,
1548 size: pending.size,
1549 scale_factor: pending.scale_factor,
1550 });
1551 }
1552 }
1553
1554 fn run_due_scripts(&mut self) {
1556 if self.scripts.is_empty() {
1557 return;
1558 }
1559 let mut due = Vec::new();
1560 for mut scheduled in core::mem::take(&mut self.scripts) {
1561 if scheduled.countdown > 0 {
1562 scheduled.countdown -= 1;
1563 self.scripts.push(scheduled);
1564 } else {
1565 due.push(scheduled.script);
1566 }
1567 }
1568 for script in due {
1569 script(self);
1570 }
1571 }
1572
1573 fn window(&self, window: WindowId) -> Result<&HeadlessWindow, ShellError> {
1574 self.windows
1575 .get(window.cast())
1576 .ok_or_else(|| ShellError::invalid_window(window))
1577 }
1578
1579 fn window_mut(&mut self, window: WindowId) -> Result<&mut HeadlessWindow, ShellError> {
1580 self.windows
1581 .get_mut(window.cast())
1582 .ok_or_else(|| ShellError::invalid_window(window))
1583 }
1584
1585 fn check_window(&self, window: WindowId) -> Result<(), ShellError> {
1586 self.window(window).map(|_| ())
1587 }
1588
1589 fn unsupported(&self, what: &'static str) -> ShellError {
1590 ShellError::Unsupported {
1591 backend: ShellBackend::Headless,
1592 what,
1593 }
1594 }
1595}
1596
1597fn synthetic_scancode(key: KeyCode) -> Scancode {
1601 let index = KeyCode::ALL
1602 .iter()
1603 .position(|candidate| *candidate == key)
1604 .unwrap_or(0);
1605 Scancode(index as u32 + 8)
1608}
1609
1610fn synthetic_keysym(key: KeyCode) -> Keysym {
1612 let name = key.as_str();
1613 if let Some(letter) = name.strip_prefix("Key") {
1614 return Keysym::from_char(letter.to_ascii_lowercase().chars().next().unwrap_or('?'));
1616 }
1617 if let Some(digit) = name.strip_prefix("Digit") {
1618 return Keysym::from_char(digit.chars().next().unwrap_or('?'));
1619 }
1620 match key {
1621 KeyCode::Space => Keysym::from_char(' '),
1622 KeyCode::Minus => Keysym::from_char('-'),
1623 KeyCode::Equal => Keysym::from_char('='),
1624 KeyCode::Comma => Keysym::from_char(','),
1625 KeyCode::Period => Keysym::from_char('.'),
1626 KeyCode::Slash => Keysym::from_char('/'),
1627 KeyCode::Semicolon => Keysym::from_char(';'),
1628 KeyCode::Quote => Keysym::from_char('\''),
1629 KeyCode::Backslash => Keysym::from_char('\\'),
1630 KeyCode::BracketLeft => Keysym::from_char('['),
1631 KeyCode::BracketRight => Keysym::from_char(']'),
1632 KeyCode::Backquote => Keysym::from_char('`'),
1633 _ => crate::keysym::named_keysym(key).unwrap_or(Keysym::NONE),
1637 }
1638}
1639
1640impl Shell for HeadlessShell {
1641 fn backend(&self) -> ShellBackend {
1642 ShellBackend::Headless
1643 }
1644
1645 fn caps(&self) -> ShellCaps {
1646 self.caps
1647 }
1648
1649 fn create_window(&mut self, desc: &WindowDesc<'_>) -> Result<WindowId, ShellError> {
1650 crate::role::check(desc, self.caps(), self.backend(), |monitor| {
1651 self.monitor(monitor).is_some()
1652 })?;
1653 if !self.windows.is_empty() && !self.caps.contains(ShellCaps::MULTI_WINDOW) {
1654 return Err(self.unsupported("a second window"));
1655 }
1656 if let DisplayMode::Borderless {
1657 monitor: Some(monitor),
1658 } = desc.mode
1659 && self.monitor(monitor).is_none()
1660 {
1661 return Err(ShellError::NoSuchMonitor(monitor.0));
1662 }
1663
1664 let (size, scale_factor) = match desc.mode {
1676 DisplayMode::Windowed => {
1677 let scale_factor = self.monitors[0].scale_factor;
1678 (
1679 desc.constraints
1680 .apply(desc.size.to_physical(scale_factor), scale_factor),
1681 scale_factor,
1682 )
1683 }
1684 DisplayMode::Borderless { monitor } => {
1685 let monitor = monitor
1686 .and_then(|id| self.monitor(id))
1687 .unwrap_or(&self.monitors[0]);
1688 (monitor.size(), monitor.scale_factor)
1689 }
1690 };
1691
1692 let (parent, popup_input, panel) = match desc.role {
1693 crate::WindowRole::Popup(popup) => {
1694 self.window(popup.parent)?;
1695 (Some(popup.parent), Some(popup.input), None)
1696 }
1697 crate::WindowRole::Panel(panel) => {
1698 (None, None, Some(crate::role::KeptPanel::new(&panel)))
1699 }
1700 crate::WindowRole::Toplevel => (None, None, None),
1701 };
1702 let countdown = self.configure_delay;
1703 let window: WindowId = self
1704 .windows
1705 .insert(HeadlessWindow {
1706 title: desc.title.to_string(),
1707 configuration: None,
1710 pending: Some(PendingConfigure {
1711 size,
1712 scale_factor,
1713 mode: desc.mode,
1714 countdown,
1715 }),
1716 requested_mode: desc.mode,
1717 requested_constraints: desc.constraints,
1718 focused: false,
1719 visible: desc.visible,
1720 pointer_mode: PointerMode::Free,
1721 cursor: Some(CursorIcon::Default),
1722 close_pending: false,
1723 accept_drops: desc.accept_drops,
1724 parent,
1725 popup_input,
1726 panel,
1727 text_input: None,
1728 text_input_area: None,
1729 input_region: None,
1730 #[cfg(feature = "accesskit")]
1731 accessibility: None,
1732 })
1733 .cast();
1734 Ok(window)
1735 }
1736
1737 fn destroy_window(&mut self, window: WindowId) -> Result<(), ShellError> {
1741 self.window(window)?;
1742 let popups: Vec<WindowId> = self
1743 .windows
1744 .iter()
1745 .filter(|(_, state)| state.parent == Some(window))
1746 .map(|(id, _)| id.cast())
1747 .collect();
1748 for popup in popups {
1749 self.destroy_window(popup)?;
1750 }
1751 self.windows
1752 .remove(window.cast())
1753 .ok_or_else(|| ShellError::invalid_window(window))?;
1754 self.queue.push_back(ShellEvent::WindowDestroyed { window });
1755 Ok(())
1756 }
1757
1758 fn window_state(&self, window: WindowId) -> Result<WindowState, ShellError> {
1759 let state = self.window(window)?;
1760 Ok(WindowState {
1761 configuration: state.configuration,
1762 requested_mode: state.requested_mode,
1763 requested_constraints: state.requested_constraints,
1764 focused: state.focused,
1765 visible: state.visible,
1766 pointer_mode: state.pointer_mode,
1767 close_pending: state.close_pending,
1768 })
1769 }
1770
1771 fn set_title(&mut self, window: WindowId, title: &str) -> Result<(), ShellError> {
1772 self.window_mut(window)?.title = title.to_string();
1773 Ok(())
1774 }
1775
1776 fn set_visible(&mut self, window: WindowId, visible: bool) -> Result<(), ShellError> {
1777 self.window_mut(window)?.visible = visible;
1778 Ok(())
1779 }
1780
1781 fn set_mode(&mut self, window: WindowId, mode: DisplayMode) -> Result<(), ShellError> {
1782 let target = match mode {
1783 DisplayMode::Windowed => None,
1784 DisplayMode::Borderless { monitor } => {
1785 let monitor = match monitor {
1786 Some(id) => self
1787 .monitor(id)
1788 .ok_or(ShellError::NoSuchMonitor(id.0))?
1789 .clone(),
1790 None => self.monitors[0].clone(),
1791 };
1792 Some(monitor)
1793 }
1794 };
1795 let countdown = self.configure_delay;
1796
1797 let state = self.window_mut(window)?;
1798 state.requested_mode = mode;
1804 let (size, scale_factor) = match &target {
1805 Some(monitor) => (monitor.size(), monitor.scale_factor),
1806 None => (state.base_size(), state.scale_factor()),
1810 };
1811 state.pending = Some(PendingConfigure {
1812 size,
1813 scale_factor,
1814 mode,
1815 countdown,
1816 });
1817 Ok(())
1818 }
1819
1820 fn set_constraints(
1821 &mut self,
1822 window: WindowId,
1823 constraints: SizeConstraints,
1824 ) -> Result<(), ShellError> {
1825 let countdown = self.configure_delay;
1826 let state = self.window_mut(window)?;
1827 state.requested_constraints = constraints;
1828
1829 if let Some(pending) = state.pending.as_mut() {
1836 pending.size = constraints.apply(pending.size, pending.scale_factor);
1837 return Ok(());
1838 }
1839
1840 if let Some(config) = state.configuration {
1844 let constrained = constraints.apply(config.size, config.scale_factor);
1845 if constrained != config.size {
1846 state.pending = Some(PendingConfigure {
1847 size: constrained,
1848 scale_factor: config.scale_factor,
1849 mode: config.mode,
1850 countdown,
1851 });
1852 }
1853 }
1854 Ok(())
1855 }
1856
1857 fn reposition_popup(
1861 &mut self,
1862 popup: WindowId,
1863 anchor: LogicalRect,
1864 size: LogicalSize,
1865 ) -> Result<(), ShellError> {
1866 let _ = anchor;
1867 let countdown = self.configure_delay;
1868 let state = self.window_mut(popup)?;
1869 if state.parent.is_none() {
1870 return Err(crate::role::not_a_popup(popup));
1871 }
1872 state.configure_at(size, countdown);
1873 Ok(())
1874 }
1875
1876 fn set_panel(
1880 &mut self,
1881 window: WindowId,
1882 size: LogicalSize,
1883 panel: &crate::PanelDesc<'_>,
1884 ) -> Result<(), ShellError> {
1885 let was = self.window(window)?.panel.as_ref().map(|kept| kept.desc());
1886 crate::role::check_set_panel(window, was, panel, self.caps, self.backend(), |monitor| {
1887 self.monitor(monitor).is_some()
1888 })?;
1889 let countdown = self.configure_delay;
1890 let state = self.window_mut(window)?;
1891 state.panel = Some(crate::role::KeptPanel::new(panel));
1892 state.configure_at(size, countdown);
1893 Ok(())
1894 }
1895
1896 fn monitors(&self) -> &[MonitorInfo] {
1897 &self.monitors
1898 }
1899
1900 fn pump(&mut self, sink: &mut dyn FnMut(ShellEvent)) {
1901 self.keep_alive();
1902 if self.outputs_powered {
1903 let windows = &self.windows;
1904 self.queue.extend(
1905 self.frames
1906 .take_due(|window| windows.contains(window.cast())),
1907 );
1908 }
1909 for _ in 0..self.queue.len() {
1914 let Some(event) = self.queue.pop_front() else {
1915 break;
1916 };
1917 sink(event);
1918 }
1919 }
1920
1921 fn keep_alive(&mut self) {
1930 self.deliver_due_configures();
1931 self.resolve_reads();
1932 self.run_due_scripts();
1933 }
1934
1935 fn wait_events(&mut self, timeout: Option<Duration>) {
1956 self.waits += 1;
1957 if !self.caps.contains(ShellCaps::EVENT_WAIT)
1958 || !self.queue.is_empty()
1959 || !self.scripts.is_empty()
1960 || (self.outputs_powered && self.frames.is_pending())
1961 {
1962 return;
1963 }
1964 match (&self.wake, timeout) {
1965 (Some(wake), timeout) => wake.wait(timeout),
1966 (None, Some(timeout)) => std::thread::sleep(timeout),
1967 (None, None) => {}
1968 }
1969 }
1970
1971 fn waker(&mut self) -> Option<ShellWaker> {
1974 let wake = self.wake.get_or_insert_with(Arc::default);
1975 Some(ShellWaker::new(Arc::clone(wake) as Arc<dyn WakeSignal>))
1976 }
1977
1978 fn align_event_clock(&mut self, elapsed: Duration) {
1985 self.set_time(elapsed);
1986 }
1987
1988 fn surface_target(&self, window: WindowId) -> Result<SurfaceTarget, ShellError> {
1989 self.window(window)?;
1995 Ok(SurfaceTarget::Offscreen)
1996 }
1997
1998 fn set_pointer_mode(&mut self, window: WindowId, mode: PointerMode) -> Result<(), ShellError> {
1999 if !self.caps.contains(mode.required_cap()) {
2000 return Err(self.unsupported(mode.as_str()));
2001 }
2002 self.window_mut(window)?.pointer_mode = mode;
2003 Ok(())
2004 }
2005
2006 fn set_text_input(
2010 &mut self,
2011 window: WindowId,
2012 field: Option<TextPurpose>,
2013 ) -> Result<(), ShellError> {
2014 self.window_mut(window)?.text_input = field;
2015 Ok(())
2016 }
2017
2018 fn set_text_input_area(
2022 &mut self,
2023 window: WindowId,
2024 area: Option<PhysicalRect>,
2025 ) -> Result<(), ShellError> {
2026 self.window_mut(window)?.text_input_area = area;
2027 Ok(())
2028 }
2029
2030 fn set_input_region(
2035 &mut self,
2036 window: WindowId,
2037 region: Option<&[PhysicalRect]>,
2038 ) -> Result<(), ShellError> {
2039 self.window_mut(window)?.input_region = region.map(<[PhysicalRect]>::to_vec);
2040 Ok(())
2041 }
2042
2043 fn request_frame(&mut self, window: WindowId) -> Result<(), ShellError> {
2048 self.window(window)?;
2049 self.frames.request(window);
2050 Ok(())
2051 }
2052
2053 #[cfg(feature = "accesskit")]
2054 fn update_accessibility(
2055 &mut self,
2056 window: WindowId,
2057 update: accesskit::TreeUpdate,
2058 ) -> Result<(), ShellError> {
2059 if let Some(updates) = &mut self.window_mut(window)?.accessibility {
2060 updates.push(update);
2061 }
2062 Ok(())
2063 }
2064
2065 fn set_cursor(
2066 &mut self,
2067 window: WindowId,
2068 cursor: Option<CursorIcon>,
2069 ) -> Result<(), ShellError> {
2070 self.window_mut(window)?.cursor = cursor;
2071 Ok(())
2072 }
2073
2074 fn warp_pointer(
2075 &mut self,
2076 window: WindowId,
2077 position: PhysicalPoint,
2078 ) -> Result<(), ShellError> {
2079 if !self.caps.contains(ShellCaps::POINTER_WARP) {
2080 return Err(self.unsupported("pointer warp"));
2081 }
2082 self.check_window(window)?;
2083 let event = ShellEvent::PointerMotion {
2086 window,
2087 device: VIRTUAL_DEVICE,
2088 time: self.now(),
2089 abs: Some(position),
2090 raw_delta: None,
2091 };
2092 self.queue.push_back(event);
2093 Ok(())
2094 }
2095
2096 fn reply_close_request(
2097 &mut self,
2098 window: WindowId,
2099 reply: CloseReply,
2100 ) -> Result<(), ShellError> {
2101 let state = self.window_mut(window)?;
2102 if !state.close_pending {
2103 return Err(ShellError::NoPendingCloseRequest { window });
2104 }
2105 state.close_pending = false;
2106 match reply {
2107 CloseReply::Close => self.destroy_window(window),
2108 CloseReply::Keep => Ok(()),
2109 }
2110 }
2111
2112 fn clipboard_offer(
2123 &mut self,
2124 window: WindowId,
2125 offers: &[ClipboardOffer<'_>],
2126 ) -> Result<(), ShellError> {
2127 if !self.caps.contains(ShellCaps::CLIPBOARD) {
2128 return Err(self.unsupported("clipboard"));
2129 }
2130 self.check_window(window)?;
2131 if self.clipboard_needs_interaction && !self.saw_interaction {
2132 return Err(ShellError::NeedsUserInteraction {
2133 backend: ShellBackend::Headless,
2134 what: "claiming the clipboard",
2135 });
2136 }
2137 self.clipboard = offers
2138 .iter()
2139 .map(|offer| (ReceivedMime::from(offer.mime), offer.bytes.to_vec()))
2140 .collect();
2141 Ok(())
2142 }
2143
2144 fn clipboard_request(
2158 &mut self,
2159 window: WindowId,
2160 mime: MimeType,
2161 ) -> Result<ClipboardRequestId, ShellError> {
2162 if !self.caps.contains(ShellCaps::CLIPBOARD) {
2163 return Err(self.unsupported("clipboard"));
2164 }
2165 self.check_window(window)?;
2166 let request = ClipboardRequestId(self.next_request_id);
2167 self.next_request_id += 1;
2168 if self.answerable() {
2169 let answer = self.answer(window, request, mime);
2170 self.queue.push_back(answer);
2174 } else {
2175 self.reads.push(PendingRead {
2176 window,
2177 request,
2178 mime,
2179 countdown: self.clipboard_deadline,
2180 });
2181 }
2182 Ok(request)
2183 }
2184
2185 fn clipboard_readable(&self, window: WindowId) -> bool {
2191 self.caps.contains(ShellCaps::CLIPBOARD) && self.window(window).is_ok() && self.answerable()
2192 }
2193}
2194
2195#[cfg(test)]
2196mod tests {
2197 use super::*;
2198 use crate::{AspectRatio, LogicalSize};
2199
2200 fn shell_with_window() -> (HeadlessShell, WindowId) {
2202 let mut shell = HeadlessShell::new();
2203 let window = shell
2204 .create_window(&WindowDesc::default())
2205 .expect("headless window creation never fails");
2206 (shell, window)
2207 }
2208
2209 fn shell_with_configured_window() -> (HeadlessShell, WindowId) {
2211 let (mut shell, window) = shell_with_window();
2212 settle(&mut shell);
2213 assert!(shell.window_state(window).expect("state").is_configured());
2214 (shell, window)
2215 }
2216
2217 fn settle(shell: &mut HeadlessShell) {
2219 for _ in 0..8 {
2220 let mut seen = 0;
2221 shell.pump(&mut |_| seen += 1);
2222 if seen == 0 && shell.windows.iter().all(|(_, w)| w.pending.is_none()) {
2223 return;
2224 }
2225 }
2226 panic!("the shell never settled");
2227 }
2228
2229 fn drain(shell: &mut HeadlessShell) -> Vec<ShellEvent> {
2230 let mut events = Vec::new();
2231 shell.pump(&mut |event| events.push(event));
2232 events
2233 }
2234
2235 fn names(events: &[ShellEvent]) -> Vec<&'static str> {
2236 events.iter().map(ShellEvent::name).collect()
2237 }
2238
2239 fn create_swapchain(state: &WindowState) -> Result<PhysicalSize, &'static str> {
2242 let size = state.size().ok_or("window is not configured yet")?;
2243 if size.is_empty() {
2244 return Err("zero-extent swapchain");
2245 }
2246 Ok(size)
2247 }
2248
2249 #[test]
2254 fn a_backend_without_activation_answers_none() {
2255 let (mut shell, window) = shell_with_window();
2256 assert_eq!(shell.activation_token(window).expect("a live window"), None);
2257 shell.destroy_window(window).expect("destroy");
2258 assert!(matches!(
2259 shell.activation_token(window),
2260 Err(ShellError::InvalidWindow { .. })
2261 ));
2262 }
2263
2264 #[cfg(feature = "accesskit")]
2269 #[test]
2270 fn a_tree_reaches_the_reader_only_while_it_reads() {
2271 use accesskit::{Action, ActionRequest, Node, NodeId, Role, TreeId, TreeInfo, TreeUpdate};
2272
2273 let (mut shell, window) = shell_with_configured_window();
2274 let update = |label: &str| TreeUpdate {
2275 nodes: vec![(NodeId(1), {
2276 let mut node = Node::new(Role::Window);
2277 node.set_label(label);
2278 node
2279 })],
2280 tree: Some(TreeInfo::new(NodeId(1))),
2281 tree_id: TreeId::ROOT,
2282 focus: NodeId(1),
2283 };
2284
2285 shell
2286 .update_accessibility(window, update("unread"))
2287 .expect("update");
2288 assert_eq!(shell.accessibility_updates(window).expect("updates"), None);
2289
2290 shell.activate_accessibility(window).expect("activate");
2291 shell
2292 .activate_accessibility(window)
2293 .expect("activate twice");
2294 shell
2295 .update_accessibility(window, update("read"))
2296 .expect("update");
2297 let click = ActionRequest {
2298 action: Action::Click,
2299 target_tree: TreeId::ROOT,
2300 target_node: NodeId(1),
2301 data: None,
2302 };
2303 shell
2304 .request_accessibility_action(window, click.clone())
2305 .expect("action");
2306 assert_eq!(
2307 shell.accessibility_updates(window).expect("updates"),
2308 Some(&[update("read")][..])
2309 );
2310 let events = drain(&mut shell);
2311 assert_eq!(
2312 names(&events),
2313 ["AccessibilityActivated", "AccessibilityAction"]
2314 );
2315 assert_eq!(
2316 events[1],
2317 ShellEvent::AccessibilityAction {
2318 window,
2319 request: click
2320 }
2321 );
2322
2323 shell.deactivate_accessibility(window).expect("deactivate");
2324 shell
2325 .deactivate_accessibility(window)
2326 .expect("deactivate twice");
2327 shell
2328 .update_accessibility(window, update("gone"))
2329 .expect("update");
2330 assert_eq!(shell.accessibility_updates(window).expect("updates"), None);
2331 assert_eq!(names(&drain(&mut shell)), ["AccessibilityDeactivated"]);
2332 }
2333
2334 #[test]
2337 fn a_new_window_has_no_size_until_a_later_pump() {
2338 let (mut shell, window) = shell_with_window();
2339
2340 let state = shell.window_state(window).expect("state");
2343 assert!(!state.is_configured());
2344 assert_eq!(state.size(), None);
2345 assert_eq!(state.scale_factor(), None);
2346 assert_eq!(state.effective_mode(), None);
2347 assert_eq!(state.requested_mode, DisplayMode::Windowed);
2348
2349 assert_eq!(drain(&mut shell), Vec::new());
2351 assert!(!shell.window_state(window).expect("state").is_configured());
2352
2353 let events = drain(&mut shell);
2355 assert_eq!(
2356 events,
2357 vec![ShellEvent::Resized {
2358 window,
2359 size: PhysicalSize::new(1280, 720),
2360 scale_factor: 1.0,
2361 }]
2362 );
2363 let state = shell.window_state(window).expect("state");
2364 assert_eq!(state.size(), Some(PhysicalSize::new(1280, 720)));
2365 assert_eq!(state.scale_factor(), Some(1.0));
2366 assert_eq!(state.effective_mode(), Some(DisplayMode::Windowed));
2367 assert!(state.mode_request_honoured());
2368 }
2369
2370 #[test]
2374 fn keep_alive_takes_a_turn_and_keeps_everything_for_the_next_pump() {
2375 let (mut shell, window) = shell_with_window();
2376 shell.inject(ShellEvent::CloseRequested { window });
2377
2378 for _ in 0..=DEFAULT_CONFIGURE_DELAY {
2380 shell.keep_alive();
2381 }
2382 assert!(
2383 shell.window_state(window).expect("state").is_configured(),
2384 "every turn the configure waits for was taken by keep_alive"
2385 );
2386
2387 assert_eq!(
2388 drain(&mut shell),
2389 vec![
2390 ShellEvent::CloseRequested { window },
2391 ShellEvent::Resized {
2392 window,
2393 size: PhysicalSize::new(1280, 720),
2394 scale_factor: 1.0,
2395 },
2396 ],
2397 "nothing keep_alive saw was delivered or dropped"
2398 );
2399 assert_eq!(drain(&mut shell), Vec::new(), "and each arrived once");
2400 }
2401
2402 #[test]
2403 fn a_consumer_that_skips_the_wait_gets_a_clean_error_not_a_wrong_extent() {
2404 let (mut shell, window) = shell_with_window();
2405
2406 assert_eq!(
2408 create_swapchain(&shell.window_state(window).expect("state")),
2409 Err("window is not configured yet")
2410 );
2411
2412 let target = shell.surface_target(window).expect("the handle exists");
2418 assert_eq!(target, SurfaceTarget::Offscreen);
2419
2420 let size = loop {
2422 shell.pump(&mut |_| {});
2423 if let Ok(size) = create_swapchain(&shell.window_state(window).expect("state")) {
2424 break size;
2425 }
2426 };
2427 assert_eq!(size, PhysicalSize::new(1280, 720));
2428 assert_eq!(
2431 shell.surface_target(window).expect("target"),
2432 target,
2433 "the target is invariant under a configure"
2434 );
2435 }
2436
2437 #[test]
2438 fn the_configure_delay_is_a_knob() {
2439 let mut shell = HeadlessShell::new().with_configure_delay(3);
2440 let window = shell.create_window(&WindowDesc::default()).expect("window");
2441 for pump in 0..3 {
2442 assert_eq!(drain(&mut shell), Vec::new(), "pump {pump}");
2443 assert!(!shell.window_state(window).expect("state").is_configured());
2444 }
2445 assert_eq!(names(&drain(&mut shell)), ["Resized"]);
2446 assert!(shell.window_state(window).expect("state").is_configured());
2447
2448 let mut shell = HeadlessShell::new().with_configure_delay(0);
2451 let window = shell.create_window(&WindowDesc::default()).expect("window");
2452 assert_eq!(names(&drain(&mut shell)), ["Resized"]);
2453 assert!(shell.window_state(window).expect("state").is_configured());
2454
2455 shell.set_configure_delay(2);
2456 shell
2457 .set_mode(window, DisplayMode::Borderless { monitor: None })
2458 .expect("request");
2459 assert_eq!(drain(&mut shell), Vec::new());
2460 assert_eq!(drain(&mut shell), Vec::new());
2461 assert_eq!(names(&drain(&mut shell)), ["Resized"]);
2462 }
2463
2464 #[test]
2465 fn set_mode_is_a_request_whose_effect_arrives_later() {
2466 let (mut shell, window) = shell_with_configured_window();
2467 shell
2468 .set_mode(window, DisplayMode::Borderless { monitor: None })
2469 .expect("request");
2470
2471 let state = shell.window_state(window).expect("state");
2473 assert_eq!(
2474 state.requested_mode,
2475 DisplayMode::Borderless { monitor: None }
2476 );
2477 assert_eq!(
2478 state.effective_mode(),
2479 Some(DisplayMode::Windowed),
2480 "still windowed until the window system says otherwise"
2481 );
2482 assert!(
2483 !state.mode_request_honoured(),
2484 "a UI toggle reading the request instead of the effect would lie here"
2485 );
2486 assert_eq!(state.size(), Some(PhysicalSize::new(1280, 720)));
2487
2488 settle(&mut shell);
2489 let state = shell.window_state(window).expect("state");
2490 assert_eq!(
2491 state.effective_mode(),
2492 Some(DisplayMode::Borderless { monitor: None })
2493 );
2494 assert!(state.mode_request_honoured());
2495 assert_eq!(state.size(), Some(PhysicalSize::new(1920, 1080)));
2496 }
2497
2498 #[test]
2499 fn a_refused_mode_is_expressible_through_injection() {
2500 let (mut shell, window) = shell_with_configured_window();
2503 shell
2504 .set_mode(window, DisplayMode::Borderless { monitor: None })
2505 .expect("request");
2506 shell
2508 .resize(window, PhysicalSize::new(1280, 720))
2509 .expect("answer");
2510 let state = shell.window_state(window).expect("state");
2511 assert_eq!(state.effective_mode(), Some(DisplayMode::Windowed));
2512 assert!(!state.mode_request_honoured());
2513 }
2514
2515 #[test]
2521 fn a_reconfigured_panel_is_configured_at_its_new_size() {
2522 let mut shell = HeadlessShell::new();
2523 let monitor = shell.monitors()[0].id;
2524 let bar = crate::PanelDesc {
2525 namespace: "bar",
2526 monitor: Some(monitor),
2527 anchor: crate::PanelAnchor::TOP | crate::PanelAnchor::LEFT | crate::PanelAnchor::RIGHT,
2528 reserve: crate::Reserve::OwnExtent,
2529 ..crate::PanelDesc::default()
2530 };
2531 let window = shell
2532 .create_window(&WindowDesc {
2533 size: LogicalSize::new(400.0, 30.0),
2534 role: crate::WindowRole::Panel(bar),
2535 ..WindowDesc::default()
2536 })
2537 .expect("a panel");
2538 settle(&mut shell);
2539 let scale = shell
2540 .window_state(window)
2541 .expect("live")
2542 .scale_factor()
2543 .expect("configured");
2544
2545 let taller = crate::PanelDesc {
2546 layer: crate::PanelLayer::Overlay,
2547 margins: crate::Margins {
2548 top: 4.0,
2549 ..crate::Margins::default()
2550 },
2551 keyboard: crate::PanelKeyboard::OnDemand,
2552 ..bar
2553 };
2554 let size = LogicalSize::new(400.0, 50.0);
2555 shell.set_panel(window, size, &taller).expect("set_panel");
2556 assert_eq!(shell.panel(window).expect("live"), Some(taller));
2557 assert_eq!(
2558 shell.window_state(window).expect("live").size(),
2559 Some(LogicalSize::new(400.0, 30.0).to_physical(scale)),
2560 "a request: nothing changes before the window system answers"
2561 );
2562 let mut resized = Vec::new();
2563 for _ in 0..8 {
2564 resized.extend(
2565 drain(&mut shell)
2566 .into_iter()
2567 .filter_map(|event| match event {
2568 ShellEvent::Resized {
2569 window: of, size, ..
2570 } if of == window => Some(size),
2571 _ => None,
2572 }),
2573 );
2574 }
2575 assert_eq!(resized, [size.to_physical(scale)]);
2576
2577 let refused = |shell: &mut HeadlessShell, panel: &crate::PanelDesc<'_>| match shell
2578 .set_panel(window, LogicalSize::new(1.0, 1.0), panel)
2579 {
2580 Err(ShellError::InvalidDescriptor(message)) => message,
2581 other => panic!("expected InvalidDescriptor, got {other:?}"),
2582 };
2583 let renamed = crate::PanelDesc {
2584 namespace: "dock",
2585 ..taller
2586 };
2587 assert!(refused(&mut shell, &renamed).contains("namespace"));
2588 let anywhere = crate::PanelDesc {
2589 monitor: None,
2590 ..taller
2591 };
2592 assert!(refused(&mut shell, &anywhere).contains("monitor"));
2593 let toplevel = shell
2594 .create_window(&WindowDesc::default())
2595 .expect("a toplevel");
2596 assert!(matches!(
2597 shell.set_panel(toplevel, size, &taller),
2598 Err(ShellError::InvalidDescriptor(message)) if message.contains("not a panel")
2599 ));
2600 let cornered = crate::PanelDesc {
2601 anchor: crate::PanelAnchor::TOP | crate::PanelAnchor::LEFT,
2602 ..taller
2603 };
2604 assert!(matches!(
2605 shell.set_panel(window, size, &cornered),
2606 Err(ShellError::Unsupported { .. })
2607 ));
2608 assert_eq!(
2609 shell.panel(window).expect("live"),
2610 Some(taller),
2611 "a refused descriptor is not recorded"
2612 );
2613 assert_eq!(shell.panel(toplevel).expect("live"), None);
2614
2615 shell.destroy_window(window).expect("destroy");
2616 assert!(matches!(
2617 shell.set_panel(window, size, &taller),
2618 Err(ShellError::InvalidWindow { .. })
2619 ));
2620 }
2621
2622 #[test]
2625 fn a_repositioned_popup_is_configured_at_its_new_size() {
2626 let (mut shell, window) = shell_with_configured_window();
2627 let anchor = crate::LogicalRect::new(
2628 crate::LogicalPoint::new(10.0, 10.0),
2629 LogicalSize::new(100.0, 20.0),
2630 );
2631 let popup = shell
2632 .create_window(&WindowDesc {
2633 size: LogicalSize::new(200.0, 150.0),
2634 role: crate::WindowRole::Popup(crate::PopupDesc {
2635 parent: window,
2636 anchor,
2637 placement: crate::PopupPlacement::Below,
2638 input: crate::PopupInput::Pointer,
2639 }),
2640 ..WindowDesc::default()
2641 })
2642 .expect("a popup");
2643 settle(&mut shell);
2644 let scale = shell
2645 .window_state(popup)
2646 .expect("live")
2647 .scale_factor()
2648 .expect("configured");
2649
2650 let moved = LogicalSize::new(240.0, 90.0);
2651 shell
2652 .reposition_popup(popup, anchor, moved)
2653 .expect("reposition");
2654 settle(&mut shell);
2655 assert_eq!(
2656 shell.window_state(popup).expect("live").size(),
2657 Some(moved.to_physical(scale))
2658 );
2659 assert!(matches!(
2660 shell.reposition_popup(window, anchor, moved),
2661 Err(ShellError::InvalidDescriptor(_))
2662 ));
2663 }
2664
2665 #[test]
2669 fn escape_on_a_grabbing_popup_is_delivered_then_asks_it_to_close() {
2670 let (mut shell, window) = shell_with_configured_window();
2671 let popup_of = |shell: &mut HeadlessShell, input| {
2672 shell
2673 .create_window(&WindowDesc {
2674 role: crate::WindowRole::Popup(crate::PopupDesc {
2675 parent: window,
2676 anchor: crate::LogicalRect::default(),
2677 placement: crate::PopupPlacement::Below,
2678 input,
2679 }),
2680 ..WindowDesc::default()
2681 })
2682 .expect("a popup")
2683 };
2684 let menu = popup_of(&mut shell, crate::PopupInput::Grab);
2685 let tooltip = popup_of(&mut shell, crate::PopupInput::Pointer);
2686 settle(&mut shell);
2687 let is_close = |event: &ShellEvent| matches!(event, ShellEvent::CloseRequested { .. });
2688
2689 shell.key_press(menu, KeyCode::Escape).expect("press");
2690 let events = drain(&mut shell);
2691 assert!(
2692 matches!(
2693 events.as_slice(),
2694 [
2695 ShellEvent::Key {
2696 key_code: Some(KeyCode::Escape),
2697 ..
2698 },
2699 ShellEvent::CloseRequested { window: closed },
2700 ] if *closed == menu
2701 ),
2702 "the key, then the request: {events:?}"
2703 );
2704 shell.key_press(menu, KeyCode::Escape).expect("press");
2706 shell.key_repeat(menu, KeyCode::Escape).expect("repeat");
2707 assert!(!drain(&mut shell).iter().any(is_close));
2708
2709 shell
2711 .reply_close_request(menu, CloseReply::Keep)
2712 .expect("keep");
2713 shell.key_release(menu, KeyCode::Escape).expect("release");
2714 shell.key_press(menu, KeyCode::Enter).expect("press");
2715 assert!(!drain(&mut shell).iter().any(is_close), "not these");
2716 shell.key_press(menu, KeyCode::Escape).expect("press");
2717 assert!(drain(&mut shell).iter().any(is_close));
2718
2719 shell.key_press(tooltip, KeyCode::Escape).expect("press");
2720 shell.key_press(window, KeyCode::Escape).expect("press");
2721 assert!(!drain(&mut shell).iter().any(is_close));
2722 }
2723
2724 #[test]
2725 fn set_constraints_is_a_request_too() {
2726 let (mut shell, window) = shell_with_configured_window();
2727 let constraints = SizeConstraints::min(LogicalSize::new(1600.0, 900.0));
2728 shell.set_constraints(window, constraints).expect("request");
2729
2730 let state = shell.window_state(window).expect("state");
2731 assert_eq!(state.requested_constraints, constraints);
2732 assert_eq!(
2733 state.size(),
2734 Some(PhysicalSize::new(1280, 720)),
2735 "the size only changes when the window system says so"
2736 );
2737
2738 settle(&mut shell);
2739 assert_eq!(
2740 shell.window_state(window).expect("state").size(),
2741 Some(PhysicalSize::new(1600, 900))
2742 );
2743 }
2744
2745 #[test]
2746 fn constraints_do_not_cancel_a_mode_request_that_has_not_landed() {
2747 let (mut shell, window) = shell_with_configured_window();
2752 shell
2753 .set_mode(window, DisplayMode::Borderless { monitor: None })
2754 .expect("mode request");
2755 shell
2756 .set_constraints(window, SizeConstraints::min(LogicalSize::new(320.0, 180.0)))
2757 .expect("constraint request");
2758 settle(&mut shell);
2759
2760 let state = shell.window_state(window).expect("state");
2761 assert_eq!(
2762 state.effective_mode(),
2763 Some(DisplayMode::Borderless { monitor: None })
2764 );
2765 assert!(state.mode_request_honoured());
2766 }
2767
2768 #[test]
2769 fn a_borderless_window_reports_its_own_monitors_scale() {
2770 let mut shell = HeadlessShell::new();
2775 let hidpi = shell.plug_monitor(MonitorInfo {
2776 id: MonitorId(0),
2777 name: "HEADLESS-2".to_string(),
2778 bounds: PhysicalRect::new(1920, 0, 2560, 1440),
2779 work_area: PhysicalRect::new(1920, 0, 2560, 1440),
2780 scale_factor: 2.0,
2781 refresh_millihertz: 143_998,
2782 is_primary: false,
2783 });
2784 let window = shell
2785 .create_window(&WindowDesc {
2786 mode: DisplayMode::Borderless {
2787 monitor: Some(hidpi),
2788 },
2789 ..WindowDesc::default()
2790 })
2791 .expect("window");
2792 settle(&mut shell);
2793
2794 let state = shell.window_state(window).expect("state");
2795 assert_eq!(state.size(), Some(PhysicalSize::new(2560, 1440)));
2796 assert_eq!(
2797 state.scale_factor(),
2798 Some(2.0),
2799 "the scale is the borderless monitor's, not the primary's"
2800 );
2801 let other = shell
2804 .create_window(&WindowDesc::default())
2805 .expect("second window");
2806 settle(&mut shell);
2807 shell
2808 .set_mode(
2809 other,
2810 DisplayMode::Borderless {
2811 monitor: Some(hidpi),
2812 },
2813 )
2814 .expect("request");
2815 settle(&mut shell);
2816 assert_eq!(
2817 shell.window_state(other).expect("state").scale_factor(),
2818 shell.window_state(window).expect("state").scale_factor()
2819 );
2820 }
2821
2822 #[test]
2823 fn constraints_set_before_the_first_configure_fold_into_it() {
2824 let mut shell = HeadlessShell::new();
2825 let window = shell.create_window(&WindowDesc::default()).expect("window");
2826 shell
2827 .set_constraints(
2828 window,
2829 SizeConstraints::NONE.with_aspect(AspectRatio::SQUARE),
2830 )
2831 .expect("request");
2832 settle(&mut shell);
2833 assert_eq!(
2836 shell.window_state(window).expect("state").size(),
2837 Some(PhysicalSize::new(720, 720))
2838 );
2839 }
2840
2841 #[test]
2842 fn creation_uses_the_requested_size_and_the_monitors_scale() {
2843 let mut shell = HeadlessShell::new();
2844 let window = shell
2845 .create_window(&WindowDesc {
2846 size: LogicalSize::new(800.0, 600.0),
2847 ..WindowDesc::default()
2848 })
2849 .expect("window");
2850 settle(&mut shell);
2851 assert_eq!(
2852 shell.window_state(window).expect("state").size(),
2853 Some(PhysicalSize::new(800, 600))
2854 );
2855
2856 let mut shell = HeadlessShell::new().with_scale_factor(2.0);
2857 let window = shell
2858 .create_window(&WindowDesc {
2859 size: LogicalSize::new(640.0, 360.0),
2860 ..WindowDesc::default()
2861 })
2862 .expect("window");
2863 settle(&mut shell);
2864 let state = shell.window_state(window).expect("state");
2865 assert_eq!(state.size(), Some(PhysicalSize::new(1280, 720)));
2866 assert_eq!(state.scale_factor(), Some(2.0));
2867 }
2868
2869 #[test]
2872 fn scale_changes_preserve_logical_size_and_report_both_facts() {
2873 let (mut shell, window) = shell_with_configured_window();
2874 shell
2875 .change_scale_factor(window, 1.5)
2876 .expect("scale change");
2877 let events = drain(&mut shell);
2878 assert_eq!(
2879 events,
2880 vec![
2881 ShellEvent::ScaleFactorChanged {
2882 window,
2883 scale_factor: 1.5,
2884 size: PhysicalSize::new(1920, 1080),
2885 },
2886 ShellEvent::Resized {
2889 window,
2890 size: PhysicalSize::new(1920, 1080),
2891 scale_factor: 1.5,
2892 },
2893 ]
2894 );
2895 assert_eq!(
2899 shell.window_state(window).expect("state").size(),
2900 Some(PhysicalSize::new(1920, 1080))
2901 );
2902 }
2903
2904 #[test]
2905 fn a_scale_change_scheduled_by_a_mode_switch_arrives_before_the_resize() {
2906 let mut shell = HeadlessShell::new();
2907 let hidpi = shell.plug_monitor(MonitorInfo {
2908 id: MonitorId(0),
2909 name: "HEADLESS-2".to_string(),
2910 bounds: PhysicalRect::new(1920, 0, 2560, 1440),
2911 work_area: PhysicalRect::new(1920, 0, 2560, 1440),
2912 scale_factor: 2.0,
2913 refresh_millihertz: 143_998,
2914 is_primary: false,
2915 });
2916 assert_eq!(hidpi, MonitorId(2), "ids are never reused");
2917 let window = shell.create_window(&WindowDesc::default()).expect("window");
2918 settle(&mut shell);
2919
2920 shell
2921 .set_mode(
2922 window,
2923 DisplayMode::Borderless {
2924 monitor: Some(hidpi),
2925 },
2926 )
2927 .expect("request");
2928 assert_eq!(drain(&mut shell), Vec::new(), "still only a request");
2929 let events = drain(&mut shell);
2930 assert_eq!(names(&events), ["ScaleFactorChanged", "Resized"]);
2931
2932 let state = shell.window_state(window).expect("state");
2933 assert_eq!(state.size(), Some(PhysicalSize::new(2560, 1440)));
2934 assert_eq!(state.scale_factor(), Some(2.0));
2935 assert!(state.effective_mode().expect("configured").is_borderless());
2936 assert_eq!(
2941 shell.surface_target(window).expect("target"),
2942 SurfaceTarget::Offscreen
2943 );
2944
2945 assert!(matches!(
2946 shell.set_mode(
2947 window,
2948 DisplayMode::Borderless {
2949 monitor: Some(MonitorId(99)),
2950 },
2951 ),
2952 Err(ShellError::NoSuchMonitor(99))
2953 ));
2954 }
2955
2956 #[test]
2957 fn resizes_honour_constraints_and_inject_bypasses_them() {
2958 let (mut shell, window) = shell_with_configured_window();
2959 shell
2960 .set_constraints(
2961 window,
2962 SizeConstraints::min(LogicalSize::new(640.0, 360.0))
2963 .with_aspect(AspectRatio::WIDESCREEN),
2964 )
2965 .expect("request");
2966 settle(&mut shell);
2967
2968 shell
2969 .resize(window, PhysicalSize::new(1000, 1000))
2970 .expect("resize");
2971 assert_eq!(
2972 drain(&mut shell),
2973 vec![ShellEvent::Resized {
2974 window,
2975 size: PhysicalSize::new(1000, 562),
2976 scale_factor: 1.0,
2977 }]
2978 );
2979
2980 shell.inject(ShellEvent::Resized {
2983 window,
2984 size: PhysicalSize::new(300, 900),
2985 scale_factor: 1.0,
2986 });
2987 assert_eq!(
2988 drain(&mut shell)[0],
2989 ShellEvent::Resized {
2990 window,
2991 size: PhysicalSize::new(300, 900),
2992 scale_factor: 1.0,
2993 }
2994 );
2995 }
2996
2997 #[test]
2998 fn a_compositor_configure_supersedes_a_pending_one() {
2999 let (mut shell, window) = shell_with_configured_window();
3000 shell
3001 .set_mode(window, DisplayMode::Borderless { monitor: None })
3002 .expect("request");
3003 shell
3005 .resize(window, PhysicalSize::new(640, 480))
3006 .expect("answer");
3007 assert_eq!(names(&drain(&mut shell)), ["Resized"]);
3008 assert_eq!(
3009 shell.window_state(window).expect("state").size(),
3010 Some(PhysicalSize::new(640, 480))
3011 );
3012 assert_eq!(drain(&mut shell), Vec::new());
3014 }
3015
3016 #[test]
3019 fn timestamps_come_from_the_shells_clock_not_from_the_pump() {
3020 let (mut shell, window) = shell_with_configured_window();
3021 shell.set_time(Duration::from_secs(2));
3022 shell.key_press(window, KeyCode::Space).expect("press");
3023 shell.advance(Duration::from_millis(180));
3024 shell.key_release(window, KeyCode::Space).expect("release");
3025
3026 let events = drain(&mut shell);
3027 let held = events[1]
3028 .time()
3029 .expect("input")
3030 .saturating_since(events[0].time().expect("input"));
3031 assert_eq!(held, Duration::from_millis(180));
3032 assert_eq!(events[0].time(), Some(EventTime::from_millis(2_000)));
3033 }
3034
3035 #[test]
3038 fn a_scheduled_script_injects_at_its_pump_with_that_pumps_time() {
3039 let (mut shell, window) = shell_with_configured_window();
3040 shell.schedule(2, move |shell| {
3041 shell.key_press(window, KeyCode::Space).expect("press");
3042 });
3043
3044 for pump in 0..2 {
3045 shell.advance(Duration::from_millis(16));
3046 assert_eq!(drain(&mut shell), Vec::new(), "pump {pump} came early");
3047 }
3048 shell.set_time(Duration::from_secs(3));
3049 let events = drain(&mut shell);
3050 assert_eq!(names(&events), ["Key"]);
3051 assert_eq!(events[0].time(), Some(EventTime::from_millis(3_000)));
3052 assert_eq!(drain(&mut shell), Vec::new(), "and it ran once");
3053 }
3054
3055 #[test]
3058 fn scheduled_scripts_run_in_order_and_a_nested_one_waits_a_pump() {
3059 let (mut shell, window) = shell_with_configured_window();
3060 shell.schedule(0, move |shell| {
3061 shell.key_press(window, KeyCode::KeyA).expect("press");
3062 shell.schedule(0, move |shell| {
3063 shell.key_press(window, KeyCode::KeyC).expect("press");
3064 });
3065 });
3066 shell.schedule(0, move |shell| {
3067 shell.key_press(window, KeyCode::KeyB).expect("press");
3068 });
3069
3070 let keys = |events: Vec<ShellEvent>| -> Vec<Option<KeyCode>> {
3071 events
3072 .into_iter()
3073 .map(|event| match event {
3074 ShellEvent::Key { key_code, .. } => key_code,
3075 other => panic!("not a key: {other:?}"),
3076 })
3077 .collect()
3078 };
3079 assert_eq!(
3080 keys(drain(&mut shell)),
3081 [Some(KeyCode::KeyA), Some(KeyCode::KeyB)]
3082 );
3083 assert_eq!(keys(drain(&mut shell)), [Some(KeyCode::KeyC)]);
3084 }
3085
3086 #[test]
3089 fn keep_alive_runs_a_due_script_and_keeps_its_events() {
3090 let (mut shell, window) = shell_with_configured_window();
3091 shell.schedule(1, move |shell| {
3092 shell.key_press(window, KeyCode::Enter).expect("press");
3093 });
3094 shell.keep_alive();
3095 shell.keep_alive();
3096 assert_eq!(shell.pending_events(), 1, "the script ran in keep_alive");
3097 assert_eq!(names(&drain(&mut shell)), ["Key"]);
3098 }
3099
3100 #[test]
3104 fn a_wait_does_not_sleep_while_a_script_is_scheduled() {
3105 let (mut shell, window) = shell_with_configured_window();
3106 shell.schedule(1, move |shell| {
3107 shell.key_press(window, KeyCode::Space).expect("press");
3108 });
3109 let start = std::time::Instant::now();
3110 shell.wait_events(Some(LONG_WAIT));
3111 assert!(
3112 start.elapsed() < LONG_WAIT / 2,
3113 "the wait slept {:?}",
3114 start.elapsed()
3115 );
3116 }
3117
3118 #[test]
3119 fn synthesized_key_fields_are_stable_and_plausible() {
3120 let (mut shell, window) = shell_with_configured_window();
3121 shell.set_modifiers(Modifiers::CTRL);
3122 shell.key_press(window, KeyCode::KeyS).expect("press");
3123 let events = drain(&mut shell);
3124 let ShellEvent::Key {
3125 scancode,
3126 key_code,
3127 keysym,
3128 modifiers,
3129 repeat,
3130 device,
3131 ..
3132 } = events[0].clone()
3133 else {
3134 panic!("wrong variant");
3135 };
3136 assert_eq!(key_code, Some(KeyCode::KeyS));
3137 assert_eq!(keysym, Keysym::from_char('s'));
3138 assert_eq!(modifiers, Modifiers::CTRL);
3139 assert!(!repeat);
3140 assert_eq!(device, VIRTUAL_DEVICE);
3141 assert!(scancode.0 >= 8, "evdev-shaped scancodes start at 8");
3142 assert_eq!(synthetic_keysym(KeyCode::Digit7), Keysym::from_char('7'));
3143 assert_eq!(synthetic_keysym(KeyCode::F5), Keysym(0xFFC2));
3144 assert_eq!(synthetic_keysym(KeyCode::Enter), Keysym(0xFF0D));
3145 assert_eq!(synthetic_keysym(KeyCode::Escape), Keysym(0xFF1B));
3146 assert_eq!(synthetic_keysym(KeyCode::ArrowLeft), Keysym(0xFF51));
3147 assert_eq!(synthetic_keysym(KeyCode::Slash), Keysym::from_char('/'));
3148
3149 shell.key_repeat(window, KeyCode::KeyS).expect("repeat");
3150 let events = drain(&mut shell);
3151 let ShellEvent::Key { repeat, .. } = events[0] else {
3152 panic!("wrong variant");
3153 };
3154 assert!(repeat);
3155 }
3156
3157 #[test]
3158 fn a_locked_pointer_reports_no_absolute_position() {
3159 let (mut shell, window) = shell_with_configured_window();
3160 shell
3161 .set_pointer_mode(window, PointerMode::Locked)
3162 .expect("lock");
3163 shell
3164 .move_pointer(window, PhysicalPoint::new(10.0, 10.0), (3.0, -4.0))
3165 .expect("motion");
3166 let events = drain(&mut shell);
3167 let ShellEvent::PointerMotion { abs, raw_delta, .. } = events[0] else {
3168 panic!("wrong variant");
3169 };
3170 assert_eq!(abs, None);
3171 assert_eq!(raw_delta, Some((3.0, -4.0)));
3172 }
3173
3174 #[test]
3175 fn a_backend_without_raw_motion_reports_none() {
3176 let mut shell = HeadlessShell::new()
3177 .with_caps(ShellCaps::DESKTOP - ShellCaps::RAW_POINTER_MOTION)
3178 .with_configure_delay(0);
3179 let window = shell.create_window(&WindowDesc::default()).expect("window");
3180 drain(&mut shell);
3181 shell
3182 .move_pointer(window, PhysicalPoint::new(1.0, 2.0), (5.0, 5.0))
3183 .expect("motion");
3184 let events = drain(&mut shell);
3185 let ShellEvent::PointerMotion { abs, raw_delta, .. } = events[0] else {
3186 panic!("wrong variant");
3187 };
3188 assert_eq!(abs, Some(PhysicalPoint::new(1.0, 2.0)));
3189 assert_eq!(raw_delta, None, "no relative-motion source");
3190 assert!(!shell.caps().has_mouselook());
3191 }
3192
3193 #[test]
3194 fn pointer_focus_and_scroll_and_text_are_injectable() {
3195 let (mut shell, window) = shell_with_configured_window();
3196 shell
3197 .set_pointer_focus(window, true, Some(PhysicalPoint::new(1.0, 1.0)))
3198 .expect("enter");
3199 shell
3200 .scroll(window, ScrollDelta::Lines { x: 0.0, y: -1.0 }, None)
3201 .expect("scroll");
3202 shell
3203 .button(
3204 window,
3205 PointerButton::Left,
3206 ButtonState::Pressed,
3207 Some(PhysicalPoint::new(1.0, 1.0)),
3208 )
3209 .expect("click");
3210 shell.commit_text(window, "ありがとう").expect("text");
3211 shell
3212 .set_pointer_focus(window, false, Some(PhysicalPoint::new(1.0, 1.0)))
3213 .expect("leave");
3214
3215 let events = drain(&mut shell);
3216 assert_eq!(
3217 names(&events),
3218 [
3219 "PointerFocus",
3220 "Wheel",
3221 "Button",
3222 "TextCommit",
3223 "PointerFocus"
3224 ]
3225 );
3226 let ShellEvent::PointerFocus {
3227 entered, position, ..
3228 } = &events[4]
3229 else {
3230 panic!("wrong variant");
3231 };
3232 assert!(!entered);
3233 assert_eq!(*position, None, "a leave has no position");
3234 assert!(events.iter().all(ShellEvent::is_input));
3235 }
3236
3237 #[test]
3244 fn contacts_are_injectable_and_a_touchless_shell_refuses_them() {
3245 let (mut shell, window) = shell_with_configured_window();
3246 assert!(shell.caps().contains(ShellCaps::TOUCH));
3247 for (contact, phase, x) in [
3248 (1, TouchPhase::Began, 10.0),
3249 (2, TouchPhase::Began, 200.0),
3250 (2, TouchPhase::Moved, 220.0),
3251 (1, TouchPhase::Ended, 10.0),
3252 (2, TouchPhase::Cancelled, 220.0),
3253 ] {
3254 shell
3255 .touch(
3256 window,
3257 ContactId(contact),
3258 phase,
3259 PhysicalPoint::new(x, 5.0),
3260 )
3261 .expect("a contact");
3262 }
3263 let events = drain(&mut shell);
3264 assert_eq!(names(&events), ["Touch"; 5]);
3265 let seen: Vec<(ContactId, TouchPhase, f64)> = events
3266 .iter()
3267 .map(|event| {
3268 let ShellEvent::Touch {
3269 contact,
3270 phase,
3271 position,
3272 ..
3273 } = event
3274 else {
3275 panic!("wrong variant");
3276 };
3277 (*contact, *phase, position.x)
3278 })
3279 .collect();
3280 assert_eq!(
3281 seen,
3282 vec![
3283 (ContactId(1), TouchPhase::Began, 10.0),
3284 (ContactId(2), TouchPhase::Began, 200.0),
3285 (ContactId(2), TouchPhase::Moved, 220.0),
3286 (ContactId(1), TouchPhase::Ended, 10.0),
3287 (ContactId(2), TouchPhase::Cancelled, 220.0),
3288 ]
3289 );
3290 assert!(events.iter().all(ShellEvent::is_input));
3291
3292 let mut touchless = HeadlessShell::new()
3293 .with_caps(ShellCaps::DESKTOP - ShellCaps::TOUCH)
3294 .with_configure_delay(0);
3295 let window = touchless
3296 .create_window(&WindowDesc::default())
3297 .expect("window");
3298 drain(&mut touchless);
3299 assert!(matches!(
3300 touchless.touch(
3301 window,
3302 ContactId(1),
3303 TouchPhase::Began,
3304 PhysicalPoint::ORIGIN
3305 ),
3306 Err(ShellError::Unsupported { .. })
3307 ));
3308 assert!(drain(&mut touchless).is_empty(), "and queued nothing");
3309 }
3310
3311 #[test]
3312 fn warping_produces_motion_like_every_platform_that_has_it() {
3313 let (mut shell, window) = shell_with_configured_window();
3314 shell
3315 .warp_pointer(window, PhysicalPoint::new(320.0, 180.0))
3316 .expect("warp");
3317 let events = drain(&mut shell);
3318 let ShellEvent::PointerMotion { abs, raw_delta, .. } = events[0] else {
3319 panic!("wrong variant");
3320 };
3321 assert_eq!(abs, Some(PhysicalPoint::new(320.0, 180.0)));
3322 assert_eq!(raw_delta, None, "a warp is not physical motion");
3323 }
3324
3325 #[test]
3328 fn capabilities_gate_the_operations_that_need_them() {
3329 let mut shell = HeadlessShell::new().with_caps(ShellCaps::empty());
3330 let window = shell.create_window(&WindowDesc::default()).expect("window");
3331
3332 assert!(matches!(
3333 shell.warp_pointer(window, PhysicalPoint::ORIGIN),
3334 Err(ShellError::Unsupported { .. })
3335 ));
3336 assert!(matches!(
3337 shell.set_pointer_mode(window, PointerMode::Locked),
3338 Err(ShellError::Unsupported { .. })
3339 ));
3340 assert!(matches!(
3341 shell.clipboard_request(window, MimeType::TextUtf8),
3342 Err(ShellError::Unsupported { .. })
3343 ));
3344 shell
3346 .set_pointer_mode(window, PointerMode::Free)
3347 .expect("Free is always available");
3348 assert!(matches!(
3350 shell.create_window(&WindowDesc::default()),
3351 Err(ShellError::Unsupported { .. })
3352 ));
3353 }
3354
3355 #[test]
3356 fn stale_handles_fail_cleanly_on_every_accessor() {
3357 let (mut shell, window) = shell_with_configured_window();
3358 shell.destroy_window(window).expect("destroy");
3359 assert_eq!(
3360 drain(&mut shell),
3361 vec![ShellEvent::WindowDestroyed { window }]
3362 );
3363
3364 assert!(matches!(
3365 shell.window_state(window),
3366 Err(ShellError::InvalidWindow { .. })
3367 ));
3368 assert!(matches!(
3369 shell.surface_target(window),
3370 Err(ShellError::InvalidWindow { .. })
3371 ));
3372 assert!(matches!(
3373 shell.resize(window, PhysicalSize::new(1, 1)),
3374 Err(ShellError::InvalidWindow { .. })
3375 ));
3376 assert!(matches!(
3377 shell.set_title(window, "gone"),
3378 Err(ShellError::InvalidWindow { .. })
3379 ));
3380 assert!(matches!(
3381 shell.destroy_window(window),
3382 Err(ShellError::InvalidWindow { .. })
3383 ));
3384 assert!(matches!(
3385 shell.key_press(window, KeyCode::KeyA),
3386 Err(ShellError::InvalidWindow { .. })
3387 ));
3388 }
3389
3390 #[test]
3391 fn destroying_a_window_cancels_its_pending_configure() {
3392 let (mut shell, window) = shell_with_window();
3395 shell.destroy_window(window).expect("destroy");
3396 assert_eq!(names(&drain(&mut shell)), ["WindowDestroyed"]);
3397 assert_eq!(drain(&mut shell), Vec::new());
3398 assert_eq!(drain(&mut shell), Vec::new());
3399 }
3400
3401 #[test]
3402 fn close_requests_are_questions() {
3403 let (mut shell, window) = shell_with_configured_window();
3404 assert!(matches!(
3405 shell.reply_close_request(window, CloseReply::Close),
3406 Err(ShellError::NoPendingCloseRequest { .. })
3407 ));
3408
3409 shell.request_close(window).expect("request");
3410 assert!(shell.window_state(window).expect("state").close_pending);
3411 shell
3412 .reply_close_request(window, CloseReply::Keep)
3413 .expect("keep");
3414 assert!(!shell.window_state(window).expect("state").close_pending);
3415 assert!(shell.window_state(window).is_ok(), "still open");
3416
3417 shell.request_close(window).expect("request again");
3418 shell
3419 .reply_close_request(window, CloseReply::Close)
3420 .expect("close");
3421 assert!(matches!(
3422 shell.window_state(window),
3423 Err(ShellError::InvalidWindow { .. })
3424 ));
3425 }
3426
3427 #[test]
3428 fn monitor_hotplug_reports_a_change_and_keeps_ids_unique() {
3429 let mut shell = HeadlessShell::new();
3430 let extra = shell.plug_monitor(MonitorInfo {
3431 id: MonitorId(0),
3432 name: "HEADLESS-2".to_string(),
3433 bounds: PhysicalRect::new(1920, 0, 1920, 1080),
3434 work_area: PhysicalRect::new(1920, 0, 1920, 1080),
3435 scale_factor: 1.0,
3436 refresh_millihertz: 60_000,
3437 is_primary: false,
3438 });
3439 assert_eq!(shell.monitors().len(), 2);
3440 assert_eq!(drain(&mut shell), vec![ShellEvent::MonitorsChanged]);
3441
3442 shell.unplug_monitor(extra).expect("unplug");
3443 assert_eq!(shell.monitors().len(), 1);
3444 assert_eq!(
3445 drain(&mut shell),
3446 vec![
3447 ShellEvent::MonitorRemoved { monitor: extra },
3448 ShellEvent::MonitorsChanged
3449 ]
3450 );
3451 assert!(matches!(
3452 shell.unplug_monitor(extra),
3453 Err(ShellError::NoSuchMonitor(_))
3454 ));
3455 assert_eq!(
3456 shell.monitor(MonitorId(1)).map(|m| m.name.as_str()),
3457 Some("HEADLESS-1")
3458 );
3459 assert!(shell.monitor(extra).is_none());
3460 }
3461
3462 #[test]
3463 fn unplugging_a_monitor_asks_its_panels_and_only_its_panels_to_close() {
3464 let mut shell = HeadlessShell::new();
3465 let second = shell.plug_monitor(MonitorInfo {
3466 id: MonitorId(0),
3467 name: "HEADLESS-2".to_string(),
3468 bounds: PhysicalRect::new(1920, 0, 1920, 1080),
3469 work_area: PhysicalRect::new(1920, 0, 1920, 1080),
3470 scale_factor: 1.0,
3471 refresh_millihertz: 60_000,
3472 is_primary: false,
3473 });
3474 let first = shell.monitors()[0].id;
3475 let panel_on = |shell: &mut HeadlessShell, monitor| {
3476 shell
3477 .create_window(&WindowDesc {
3478 role: crate::WindowRole::Panel(crate::PanelDesc {
3479 monitor,
3480 ..crate::PanelDesc::default()
3481 }),
3482 ..WindowDesc::default()
3483 })
3484 .expect("panel")
3485 };
3486 let doomed = panel_on(&mut shell, Some(second));
3487 let already_asked = panel_on(&mut shell, Some(second));
3488 let elsewhere = panel_on(&mut shell, Some(first));
3489 let anywhere = panel_on(&mut shell, None);
3490 let toplevel = shell.create_window(&WindowDesc::default()).expect("window");
3491 shell.request_close(already_asked).expect("close");
3492 settle(&mut shell);
3493 drain(&mut shell);
3494
3495 shell.unplug_monitor(second).expect("unplug");
3496 assert_eq!(
3497 drain(&mut shell),
3498 vec![
3499 ShellEvent::MonitorRemoved { monitor: second },
3500 ShellEvent::CloseRequested { window: doomed },
3501 ShellEvent::MonitorsChanged,
3502 ]
3503 );
3504 for window in [elsewhere, anywhere, toplevel] {
3505 assert!(!shell.window_state(window).expect("open").close_pending);
3506 }
3507 }
3508
3509 #[test]
3510 fn drops_require_both_the_capability_and_the_opt_in() {
3511 let mut shell = HeadlessShell::new();
3512 let plain = shell.create_window(&WindowDesc::default()).expect("window");
3513 assert!(matches!(
3514 shell.drop_file(plain, "/tmp/a.gltf", None),
3515 Err(ShellError::Unsupported { .. })
3516 ));
3517
3518 let dropper = shell
3519 .create_window(&WindowDesc {
3520 accept_drops: true,
3521 ..WindowDesc::default()
3522 })
3523 .expect("window");
3524 settle(&mut shell);
3525 shell
3526 .drop_file(dropper, "/tmp/a.gltf", Some(PhysicalPoint::new(4.0, 5.0)))
3527 .expect("drop");
3528 assert_eq!(names(&drain(&mut shell))[0], "DroppedFile");
3529 }
3530
3531 #[test]
3532 fn a_sink_that_queues_more_events_does_not_spin_the_pump() {
3533 let (mut shell, window) = shell_with_configured_window();
3534 shell.inject(ShellEvent::MonitorsChanged);
3535 let mut extra = Vec::new();
3536 shell.pump(&mut |event| extra.push(event));
3537 assert_eq!(extra.len(), 1);
3538 shell.inject(ShellEvent::CloseRequested { window });
3539 assert_eq!(shell.pending_events(), 1);
3540 }
3541
3542 #[test]
3543 fn accessors_that_no_real_backend_can_provide_are_headless_only() {
3544 let (mut shell, window) = shell_with_configured_window();
3545 shell.set_title(window, "renamed").expect("title");
3546 assert_eq!(shell.title(window).expect("title"), "renamed");
3547 shell.set_cursor(window, None).expect("hide");
3548 assert_eq!(shell.cursor(window).expect("cursor"), None);
3549 shell
3550 .set_cursor(window, Some(CursorIcon::Crosshair))
3551 .expect("shape");
3552 assert_eq!(
3553 shell.cursor(window).expect("cursor"),
3554 Some(CursorIcon::Crosshair)
3555 );
3556 shell.set_visible(window, false).expect("hide window");
3557 assert!(!shell.window_state(window).expect("state").visible);
3558 }
3559
3560 #[test]
3561 fn an_input_region_is_recorded_replaced_and_given_back() {
3562 let (mut shell, window) = shell_with_configured_window();
3563 assert_eq!(shell.input_region(window).expect("region"), None);
3564 let panel = PhysicalRect::new(0, 0, 400, 300).rounded(12);
3565 shell
3566 .set_input_region(window, Some(&panel))
3567 .expect("a region");
3568 assert_eq!(
3569 shell.input_region(window).expect("region"),
3570 Some(panel.as_slice())
3571 );
3572 shell.set_input_region(window, Some(&[])).expect("nothing");
3573 assert_eq!(shell.input_region(window).expect("region"), Some(&[][..]));
3574 shell.set_input_region(window, None).expect("whole window");
3575 assert_eq!(shell.input_region(window).expect("region"), None);
3576 shell.destroy_window(window).expect("destroy");
3577 assert!(matches!(
3578 shell.set_input_region(window, None),
3579 Err(ShellError::InvalidWindow { .. })
3580 ));
3581 assert!(shell.input_region(window).is_err());
3582 }
3583
3584 fn frames_for(events: &[ShellEvent], window: WindowId) -> usize {
3585 events
3586 .iter()
3587 .filter(|event| matches!(event, ShellEvent::FrameReady { window: w } if *w == window))
3588 .count()
3589 }
3590
3591 #[test]
3592 fn requests_for_a_frame_share_one_answer_at_the_next_pump() {
3593 let (mut shell, window) = shell_with_configured_window();
3594 assert!(!shell.frame_requested(window).expect("live"));
3595 shell.request_frame(window).expect("a frame");
3596 shell.request_frame(window).expect("the same frame");
3597 assert!(shell.frame_requested(window).expect("live"));
3598 let events = drain(&mut shell);
3599 assert_eq!(frames_for(&events, window), 1, "{:?}", names(&events));
3600 assert!(!shell.frame_requested(window).expect("live"));
3601 assert_eq!(
3602 frames_for(&drain(&mut shell), window),
3603 0,
3604 "nothing asked again"
3605 );
3606 shell.request_frame(window).expect("the next frame");
3607 assert_eq!(frames_for(&drain(&mut shell), window), 1);
3608 }
3609
3610 #[test]
3611 fn a_window_destroyed_with_a_frame_requested_is_not_answered() {
3612 let (mut shell, doomed) = shell_with_configured_window();
3613 let kept = shell
3614 .create_window(&WindowDesc::default())
3615 .expect("a second window");
3616 settle(&mut shell);
3617 shell.request_frame(doomed).expect("a frame");
3618 shell.request_frame(kept).expect("a frame");
3619 shell.destroy_window(doomed).expect("destroy");
3620 let events = drain(&mut shell);
3621 assert_eq!(frames_for(&events, doomed), 0, "{:?}", names(&events));
3622 assert_eq!(frames_for(&events, kept), 1, "the other window still asked");
3623 assert!(matches!(
3624 shell.request_frame(doomed),
3625 Err(ShellError::InvalidWindow { .. })
3626 ));
3627 assert!(shell.frame_requested(doomed).is_err());
3628 }
3629
3630 #[test]
3631 fn a_wait_does_not_sleep_through_a_frame_that_is_due() {
3632 let (mut shell, window) = shell_with_configured_window();
3633 shell.request_frame(window).expect("a frame");
3634 let start = std::time::Instant::now();
3635 shell.wait_events(Some(LONG_WAIT));
3636 assert!(
3637 start.elapsed() < LONG_WAIT / 2,
3638 "slept {:?} with a frame due",
3639 start.elapsed()
3640 );
3641 assert_eq!(frames_for(&drain(&mut shell), window), 1);
3642 }
3643
3644 #[test]
3645 fn an_output_that_is_off_holds_frame_requests_until_it_is_on() {
3646 let (mut shell, window) = shell_with_configured_window();
3647 shell.set_outputs_powered(false);
3648 shell.request_frame(window).expect("a frame");
3649 assert_eq!(frames_for(&drain(&mut shell), window), 0, "held");
3650 assert!(shell.frame_requested(window).expect("live"), "still owed");
3651
3652 let wait = Duration::from_millis(20);
3653 let start = std::time::Instant::now();
3654 shell.wait_events(Some(wait));
3655 assert!(
3656 start.elapsed() >= wait,
3657 "a held request cut the wait short after {:?}",
3658 start.elapsed()
3659 );
3660
3661 shell.set_outputs_powered(true);
3662 assert_eq!(
3663 frames_for(&drain(&mut shell), window),
3664 1,
3665 "answered once on"
3666 );
3667 assert!(!shell.frame_requested(window).expect("live"));
3668 }
3669
3670 #[test]
3671 fn waiting_is_observable_and_focus_transitions_are_events() {
3672 let (mut shell, window) = shell_with_configured_window();
3673 assert_eq!(shell.wait_count(), 0);
3674 shell.wait_events(Some(Duration::from_millis(5)));
3675 shell.wait_events(None);
3676 assert_eq!(shell.wait_count(), 2);
3677
3678 shell.set_focus(window, true).expect("focus");
3679 assert!(shell.window_state(window).expect("state").focused);
3680 shell.set_focus(window, false).expect("unfocus");
3681 assert_eq!(names(&drain(&mut shell)), ["Focus", "Focus"]);
3682 }
3683
3684 const LONG_WAIT: Duration = Duration::from_secs(60);
3689 const RING_AFTER: Duration = Duration::from_millis(50);
3691
3692 fn ring_later(waker: ShellWaker) -> std::thread::JoinHandle<()> {
3694 std::thread::spawn(move || {
3695 std::thread::sleep(RING_AFTER);
3696 waker.wake();
3697 })
3698 }
3699
3700 #[test]
3701 fn a_wake_from_another_thread_ends_a_long_wait() {
3702 let mut shell = HeadlessShell::new();
3703 let ringer = ring_later(shell.waker().expect("headless has a waker"));
3704 let start = std::time::Instant::now();
3705 shell.wait_events(Some(LONG_WAIT));
3706 let slept = start.elapsed();
3707 ringer.join().expect("the ringer");
3708 assert!(slept < LONG_WAIT / 2, "the wait ran {slept:?}");
3709 }
3710
3711 #[test]
3712 fn a_wake_before_the_wait_ends_it_at_once_and_wakes_coalesce() {
3713 let mut shell = HeadlessShell::new();
3714 let waker = shell.waker().expect("headless has a waker");
3715 for _ in 0..3 {
3716 waker.wake();
3717 }
3718 let start = std::time::Instant::now();
3719 shell.wait_events(Some(LONG_WAIT));
3720 assert!(
3721 start.elapsed() < LONG_WAIT / 2,
3722 "a rung bell must end the wait"
3723 );
3724
3725 let timeout = Duration::from_millis(100);
3727 let start = std::time::Instant::now();
3728 shell.wait_events(Some(timeout));
3729 assert!(
3730 start.elapsed() >= timeout / 2,
3731 "the bell was cleared, so this wait must sleep, but it took {:?}",
3732 start.elapsed()
3733 );
3734 }
3735
3736 #[test]
3737 fn with_a_waker_out_an_untimed_wait_sleeps_until_it_rings() {
3738 let mut shell = HeadlessShell::new();
3739 let ringer = ring_later(shell.waker().expect("headless has a waker"));
3740 let start = std::time::Instant::now();
3741 shell.wait_events(None);
3742 let slept = start.elapsed();
3743 ringer.join().expect("the ringer");
3744 assert!(
3745 slept >= RING_AFTER / 2,
3746 "an untimed wait returned after {slept:?}, before anything rang"
3747 );
3748 }
3749
3750 #[test]
3753 fn the_clipboard_round_trips_through_an_event() {
3754 let (mut shell, window) = shell_with_configured_window();
3755 shell
3756 .clipboard_offer(
3757 window,
3758 &[
3759 ClipboardOffer::text("node"),
3760 ClipboardOffer::ron("(kind:\"node\")"),
3761 ],
3762 )
3763 .expect("offer");
3764 assert_eq!(
3765 shell.clipboard_bytes(MimeType::TextUtf8),
3766 Some(&b"node"[..])
3767 );
3768
3769 let request = shell
3770 .clipboard_request(window, MimeType::CrcblRon)
3771 .expect("request");
3772 assert_eq!(
3773 drain(&mut shell)[0],
3774 ShellEvent::ClipboardData {
3775 window,
3776 request,
3777 mime: ReceivedMime::from(MimeType::CrcblRon),
3778 content: ClipboardContent::Bytes(b"(kind:\"node\")".to_vec()),
3779 }
3780 );
3781
3782 let request = shell
3785 .clipboard_request(window, MimeType::UriList)
3786 .expect("request");
3787 assert_eq!(
3788 drain(&mut shell)[0],
3789 ShellEvent::ClipboardData {
3790 window,
3791 request,
3792 mime: ReceivedMime::from(MimeType::UriList),
3793 content: ClipboardContent::Empty,
3794 }
3795 );
3796
3797 shell
3800 .clipboard_offer(window, &[ClipboardOffer::text("")])
3801 .expect("offer");
3802 let request = shell
3803 .clipboard_request(window, MimeType::TextUtf8)
3804 .expect("request");
3805 assert_eq!(
3806 drain(&mut shell)[0],
3807 ShellEvent::ClipboardData {
3808 window,
3809 request,
3810 mime: ReceivedMime::from(MimeType::TextUtf8),
3811 content: ClipboardContent::Bytes(Vec::new()),
3812 }
3813 );
3814 }
3815
3816 #[test]
3817 fn a_read_taken_while_the_clipboard_is_unreadable_is_held_not_answered_empty() {
3818 let (mut shell, window) = shell_with_configured_window();
3824 shell
3825 .clipboard_offer(window, &[ClipboardOffer::text("published")])
3826 .expect("offer");
3827 shell.set_clipboard_readable(false);
3828 assert!(!shell.clipboard_readable(window));
3829
3830 let request = shell
3831 .clipboard_request(window, MimeType::TextUtf8)
3832 .expect("a read is accepted even when it cannot be answered yet");
3833 assert_eq!(shell.held_clipboard_reads(), 1);
3834 assert!(
3835 drain(&mut shell).is_empty(),
3836 "a held read produces no event at all — least of all an empty one"
3837 );
3838
3839 shell.set_clipboard_readable(true);
3841 let events = drain(&mut shell);
3842 assert_eq!(events.len(), 1);
3843 assert_eq!(
3844 events[0],
3845 ShellEvent::ClipboardData {
3846 window,
3847 request,
3848 mime: ReceivedMime::from(MimeType::TextUtf8),
3849 content: ClipboardContent::Bytes(b"published".to_vec()),
3850 }
3851 );
3852 assert_eq!(shell.held_clipboard_reads(), 0);
3853 }
3854
3855 #[test]
3856 fn a_read_that_never_becomes_answerable_ends_as_unavailable() {
3857 let (mut shell, window) = shell_with_configured_window();
3861 shell
3862 .clipboard_offer(window, &[ClipboardOffer::text("unreachable")])
3863 .expect("offer");
3864 shell.set_clipboard_readable(false);
3865 shell.clipboard_deadline(2);
3866
3867 let request = shell
3868 .clipboard_request(window, MimeType::TextUtf8)
3869 .expect("request");
3870 assert!(drain(&mut shell).is_empty(), "pump 1: still held");
3871 assert!(drain(&mut shell).is_empty(), "pump 2: still held");
3872 let events = drain(&mut shell);
3873 assert_eq!(
3874 events[0],
3875 ShellEvent::ClipboardData {
3876 window,
3877 request,
3878 mime: ReceivedMime::from(MimeType::TextUtf8),
3879 content: ClipboardContent::Unavailable,
3882 }
3883 );
3884 assert_eq!(shell.held_clipboard_reads(), 0);
3885 }
3886
3887 #[test]
3888 fn a_peer_that_holds_the_clipboard_and_never_delivers_is_unavailable() {
3889 let (mut shell, window) = shell_with_configured_window();
3893 shell.set_foreign_clipboard(vec![(
3894 ReceivedMime::new("text/plain"),
3895 b"never sent".to_vec(),
3896 )]);
3897 shell.set_clipboard_stalled(true);
3898 shell.clipboard_deadline(1);
3899 assert!(
3900 !shell.clipboard_readable(window),
3901 "a clipboard whose owner will not answer is not readable"
3902 );
3903
3904 let request = shell
3905 .clipboard_request(window, MimeType::TextUtf8)
3906 .expect("request");
3907 assert!(drain(&mut shell).is_empty());
3908 let events = drain(&mut shell);
3909 let ShellEvent::ClipboardData {
3910 request: answered,
3911 content,
3912 ..
3913 } = &events[0]
3914 else {
3915 panic!("wrong variant");
3916 };
3917 assert_eq!(*answered, request);
3918 assert_eq!(*content, ClipboardContent::Unavailable);
3919
3920 shell.set_clipboard_stalled(false);
3922 let request = shell
3923 .clipboard_request(window, MimeType::TextUtf8)
3924 .expect("request");
3925 assert_eq!(
3926 drain(&mut shell)[0],
3927 ShellEvent::ClipboardData {
3928 window,
3929 request,
3930 mime: ReceivedMime::new("text/plain"),
3931 content: ClipboardContent::Bytes(b"never sent".to_vec()),
3932 }
3933 );
3934 }
3935
3936 #[test]
3937 fn claiming_the_clipboard_can_require_a_user_interaction() {
3938 let (mut shell, window) = shell_with_configured_window();
3942 shell.require_user_interaction(true);
3943
3944 let refused = shell.clipboard_offer(window, &[ClipboardOffer::text("stolen")]);
3945 assert!(
3946 matches!(
3947 refused,
3948 Err(ShellError::NeedsUserInteraction {
3949 backend: ShellBackend::Headless,
3950 ..
3951 })
3952 ),
3953 "{refused:?}"
3954 );
3955 assert!(
3956 shell.clipboard_bytes(MimeType::TextUtf8).is_none(),
3957 "a refused claim must not have taken the clipboard anyway"
3958 );
3959
3960 shell.key_press(window, KeyCode::KeyC).expect("key");
3963 shell
3964 .clipboard_offer(window, &[ClipboardOffer::text("copied")])
3965 .expect("with a serial in hand, the claim goes through");
3966 assert_eq!(
3967 shell.clipboard_bytes(MimeType::TextUtf8),
3968 Some(&b"copied"[..])
3969 );
3970
3971 shell.clear_user_interaction();
3973 assert!(matches!(
3974 shell.clipboard_offer(window, &[ClipboardOffer::text("later")]),
3975 Err(ShellError::NeedsUserInteraction { .. })
3976 ));
3977 }
3978
3979 #[test]
3980 fn a_held_read_for_a_destroyed_window_is_dropped_rather_than_answered() {
3981 let (mut shell, window) = shell_with_configured_window();
3984 shell.set_clipboard_readable(false);
3985 let _request = shell
3986 .clipboard_request(window, MimeType::TextUtf8)
3987 .expect("request");
3988 assert_eq!(shell.held_clipboard_reads(), 1);
3989
3990 shell.destroy_window(window).expect("destroy");
3991 let events = drain(&mut shell);
3992 assert_eq!(shell.held_clipboard_reads(), 0);
3993 assert!(
3994 !events
3995 .iter()
3996 .any(|event| matches!(event, ShellEvent::ClipboardData { .. })),
3997 "no clipboard answer may name a destroyed window: {events:?}"
3998 );
3999 }
4000
4001 #[test]
4002 fn a_paste_from_a_foreign_application_keeps_the_foreign_spelling() {
4003 let (mut shell, window) = shell_with_configured_window();
4004 shell.set_foreign_clipboard(vec![
4005 (ReceivedMime::new("text/plain"), b"from gedit".to_vec()),
4008 (
4009 ReceivedMime::new("application/vnd.some-editor.scene+json"),
4010 b"{}".to_vec(),
4011 ),
4012 ]);
4013
4014 let request = shell
4016 .clipboard_request(window, MimeType::TextUtf8)
4017 .expect("request");
4018 let events = drain(&mut shell);
4019 let ShellEvent::ClipboardData {
4020 request: answered,
4021 mime,
4022 content,
4023 ..
4024 } = &events[0]
4025 else {
4026 panic!("wrong variant");
4027 };
4028 assert_eq!(*answered, request);
4029 assert_eq!(content.bytes(), Some(&b"from gedit"[..]));
4030 assert_eq!(mime.as_str(), "text/plain");
4032 assert!(mime.matches(MimeType::TextUtf8));
4033 assert_ne!(*mime, ReceivedMime::from(MimeType::TextUtf8));
4034
4035 assert_eq!(
4038 shell.clipboard_bytes(MimeType::Other("application/vnd.some-editor.scene+json")),
4039 Some(&b"{}"[..])
4040 );
4041 }
4042
4043 #[test]
4046 #[should_panic(expected = "a shell needs at least one monitor")]
4047 fn a_shell_with_no_monitors_is_rejected() {
4048 let _ = HeadlessShell::new().with_monitors(Vec::new());
4049 }
4050
4051 #[test]
4052 #[should_panic(expected = "a text commit is never empty")]
4053 fn an_empty_text_commit_is_rejected() {
4054 let (mut shell, window) = shell_with_configured_window();
4055 let _ = shell.commit_text(window, "");
4056 }
4057}