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 #[cfg(feature = "accesskit")]
2254 #[test]
2255 fn a_tree_reaches_the_reader_only_while_it_reads() {
2256 use accesskit::{Action, ActionRequest, Node, NodeId, Role, TreeId, TreeInfo, TreeUpdate};
2257
2258 let (mut shell, window) = shell_with_configured_window();
2259 let update = |label: &str| TreeUpdate {
2260 nodes: vec![(NodeId(1), {
2261 let mut node = Node::new(Role::Window);
2262 node.set_label(label);
2263 node
2264 })],
2265 tree: Some(TreeInfo::new(NodeId(1))),
2266 tree_id: TreeId::ROOT,
2267 focus: NodeId(1),
2268 };
2269
2270 shell
2271 .update_accessibility(window, update("unread"))
2272 .expect("update");
2273 assert_eq!(shell.accessibility_updates(window).expect("updates"), None);
2274
2275 shell.activate_accessibility(window).expect("activate");
2276 shell
2277 .activate_accessibility(window)
2278 .expect("activate twice");
2279 shell
2280 .update_accessibility(window, update("read"))
2281 .expect("update");
2282 let click = ActionRequest {
2283 action: Action::Click,
2284 target_tree: TreeId::ROOT,
2285 target_node: NodeId(1),
2286 data: None,
2287 };
2288 shell
2289 .request_accessibility_action(window, click.clone())
2290 .expect("action");
2291 assert_eq!(
2292 shell.accessibility_updates(window).expect("updates"),
2293 Some(&[update("read")][..])
2294 );
2295 let events = drain(&mut shell);
2296 assert_eq!(
2297 names(&events),
2298 ["AccessibilityActivated", "AccessibilityAction"]
2299 );
2300 assert_eq!(
2301 events[1],
2302 ShellEvent::AccessibilityAction {
2303 window,
2304 request: click
2305 }
2306 );
2307
2308 shell.deactivate_accessibility(window).expect("deactivate");
2309 shell
2310 .deactivate_accessibility(window)
2311 .expect("deactivate twice");
2312 shell
2313 .update_accessibility(window, update("gone"))
2314 .expect("update");
2315 assert_eq!(shell.accessibility_updates(window).expect("updates"), None);
2316 assert_eq!(names(&drain(&mut shell)), ["AccessibilityDeactivated"]);
2317 }
2318
2319 #[test]
2322 fn a_new_window_has_no_size_until_a_later_pump() {
2323 let (mut shell, window) = shell_with_window();
2324
2325 let state = shell.window_state(window).expect("state");
2328 assert!(!state.is_configured());
2329 assert_eq!(state.size(), None);
2330 assert_eq!(state.scale_factor(), None);
2331 assert_eq!(state.effective_mode(), None);
2332 assert_eq!(state.requested_mode, DisplayMode::Windowed);
2333
2334 assert_eq!(drain(&mut shell), Vec::new());
2336 assert!(!shell.window_state(window).expect("state").is_configured());
2337
2338 let events = drain(&mut shell);
2340 assert_eq!(
2341 events,
2342 vec![ShellEvent::Resized {
2343 window,
2344 size: PhysicalSize::new(1280, 720),
2345 scale_factor: 1.0,
2346 }]
2347 );
2348 let state = shell.window_state(window).expect("state");
2349 assert_eq!(state.size(), Some(PhysicalSize::new(1280, 720)));
2350 assert_eq!(state.scale_factor(), Some(1.0));
2351 assert_eq!(state.effective_mode(), Some(DisplayMode::Windowed));
2352 assert!(state.mode_request_honoured());
2353 }
2354
2355 #[test]
2359 fn keep_alive_takes_a_turn_and_keeps_everything_for_the_next_pump() {
2360 let (mut shell, window) = shell_with_window();
2361 shell.inject(ShellEvent::CloseRequested { window });
2362
2363 for _ in 0..=DEFAULT_CONFIGURE_DELAY {
2365 shell.keep_alive();
2366 }
2367 assert!(
2368 shell.window_state(window).expect("state").is_configured(),
2369 "every turn the configure waits for was taken by keep_alive"
2370 );
2371
2372 assert_eq!(
2373 drain(&mut shell),
2374 vec![
2375 ShellEvent::CloseRequested { window },
2376 ShellEvent::Resized {
2377 window,
2378 size: PhysicalSize::new(1280, 720),
2379 scale_factor: 1.0,
2380 },
2381 ],
2382 "nothing keep_alive saw was delivered or dropped"
2383 );
2384 assert_eq!(drain(&mut shell), Vec::new(), "and each arrived once");
2385 }
2386
2387 #[test]
2388 fn a_consumer_that_skips_the_wait_gets_a_clean_error_not_a_wrong_extent() {
2389 let (mut shell, window) = shell_with_window();
2390
2391 assert_eq!(
2393 create_swapchain(&shell.window_state(window).expect("state")),
2394 Err("window is not configured yet")
2395 );
2396
2397 let target = shell.surface_target(window).expect("the handle exists");
2403 assert_eq!(target, SurfaceTarget::Offscreen);
2404
2405 let size = loop {
2407 shell.pump(&mut |_| {});
2408 if let Ok(size) = create_swapchain(&shell.window_state(window).expect("state")) {
2409 break size;
2410 }
2411 };
2412 assert_eq!(size, PhysicalSize::new(1280, 720));
2413 assert_eq!(
2416 shell.surface_target(window).expect("target"),
2417 target,
2418 "the target is invariant under a configure"
2419 );
2420 }
2421
2422 #[test]
2423 fn the_configure_delay_is_a_knob() {
2424 let mut shell = HeadlessShell::new().with_configure_delay(3);
2425 let window = shell.create_window(&WindowDesc::default()).expect("window");
2426 for pump in 0..3 {
2427 assert_eq!(drain(&mut shell), Vec::new(), "pump {pump}");
2428 assert!(!shell.window_state(window).expect("state").is_configured());
2429 }
2430 assert_eq!(names(&drain(&mut shell)), ["Resized"]);
2431 assert!(shell.window_state(window).expect("state").is_configured());
2432
2433 let mut shell = HeadlessShell::new().with_configure_delay(0);
2436 let window = shell.create_window(&WindowDesc::default()).expect("window");
2437 assert_eq!(names(&drain(&mut shell)), ["Resized"]);
2438 assert!(shell.window_state(window).expect("state").is_configured());
2439
2440 shell.set_configure_delay(2);
2441 shell
2442 .set_mode(window, DisplayMode::Borderless { monitor: None })
2443 .expect("request");
2444 assert_eq!(drain(&mut shell), Vec::new());
2445 assert_eq!(drain(&mut shell), Vec::new());
2446 assert_eq!(names(&drain(&mut shell)), ["Resized"]);
2447 }
2448
2449 #[test]
2450 fn set_mode_is_a_request_whose_effect_arrives_later() {
2451 let (mut shell, window) = shell_with_configured_window();
2452 shell
2453 .set_mode(window, DisplayMode::Borderless { monitor: None })
2454 .expect("request");
2455
2456 let state = shell.window_state(window).expect("state");
2458 assert_eq!(
2459 state.requested_mode,
2460 DisplayMode::Borderless { monitor: None }
2461 );
2462 assert_eq!(
2463 state.effective_mode(),
2464 Some(DisplayMode::Windowed),
2465 "still windowed until the window system says otherwise"
2466 );
2467 assert!(
2468 !state.mode_request_honoured(),
2469 "a UI toggle reading the request instead of the effect would lie here"
2470 );
2471 assert_eq!(state.size(), Some(PhysicalSize::new(1280, 720)));
2472
2473 settle(&mut shell);
2474 let state = shell.window_state(window).expect("state");
2475 assert_eq!(
2476 state.effective_mode(),
2477 Some(DisplayMode::Borderless { monitor: None })
2478 );
2479 assert!(state.mode_request_honoured());
2480 assert_eq!(state.size(), Some(PhysicalSize::new(1920, 1080)));
2481 }
2482
2483 #[test]
2484 fn a_refused_mode_is_expressible_through_injection() {
2485 let (mut shell, window) = shell_with_configured_window();
2488 shell
2489 .set_mode(window, DisplayMode::Borderless { monitor: None })
2490 .expect("request");
2491 shell
2493 .resize(window, PhysicalSize::new(1280, 720))
2494 .expect("answer");
2495 let state = shell.window_state(window).expect("state");
2496 assert_eq!(state.effective_mode(), Some(DisplayMode::Windowed));
2497 assert!(!state.mode_request_honoured());
2498 }
2499
2500 #[test]
2506 fn a_reconfigured_panel_is_configured_at_its_new_size() {
2507 let mut shell = HeadlessShell::new();
2508 let monitor = shell.monitors()[0].id;
2509 let bar = crate::PanelDesc {
2510 namespace: "bar",
2511 monitor: Some(monitor),
2512 anchor: crate::PanelAnchor::TOP | crate::PanelAnchor::LEFT | crate::PanelAnchor::RIGHT,
2513 reserve: crate::Reserve::OwnExtent,
2514 ..crate::PanelDesc::default()
2515 };
2516 let window = shell
2517 .create_window(&WindowDesc {
2518 size: LogicalSize::new(400.0, 30.0),
2519 role: crate::WindowRole::Panel(bar),
2520 ..WindowDesc::default()
2521 })
2522 .expect("a panel");
2523 settle(&mut shell);
2524 let scale = shell
2525 .window_state(window)
2526 .expect("live")
2527 .scale_factor()
2528 .expect("configured");
2529
2530 let taller = crate::PanelDesc {
2531 layer: crate::PanelLayer::Overlay,
2532 margins: crate::Margins {
2533 top: 4.0,
2534 ..crate::Margins::default()
2535 },
2536 keyboard: crate::PanelKeyboard::OnDemand,
2537 ..bar
2538 };
2539 let size = LogicalSize::new(400.0, 50.0);
2540 shell.set_panel(window, size, &taller).expect("set_panel");
2541 assert_eq!(shell.panel(window).expect("live"), Some(taller));
2542 assert_eq!(
2543 shell.window_state(window).expect("live").size(),
2544 Some(LogicalSize::new(400.0, 30.0).to_physical(scale)),
2545 "a request: nothing changes before the window system answers"
2546 );
2547 let mut resized = Vec::new();
2548 for _ in 0..8 {
2549 resized.extend(
2550 drain(&mut shell)
2551 .into_iter()
2552 .filter_map(|event| match event {
2553 ShellEvent::Resized {
2554 window: of, size, ..
2555 } if of == window => Some(size),
2556 _ => None,
2557 }),
2558 );
2559 }
2560 assert_eq!(resized, [size.to_physical(scale)]);
2561
2562 let refused = |shell: &mut HeadlessShell, panel: &crate::PanelDesc<'_>| match shell
2563 .set_panel(window, LogicalSize::new(1.0, 1.0), panel)
2564 {
2565 Err(ShellError::InvalidDescriptor(message)) => message,
2566 other => panic!("expected InvalidDescriptor, got {other:?}"),
2567 };
2568 let renamed = crate::PanelDesc {
2569 namespace: "dock",
2570 ..taller
2571 };
2572 assert!(refused(&mut shell, &renamed).contains("namespace"));
2573 let anywhere = crate::PanelDesc {
2574 monitor: None,
2575 ..taller
2576 };
2577 assert!(refused(&mut shell, &anywhere).contains("monitor"));
2578 let toplevel = shell
2579 .create_window(&WindowDesc::default())
2580 .expect("a toplevel");
2581 assert!(matches!(
2582 shell.set_panel(toplevel, size, &taller),
2583 Err(ShellError::InvalidDescriptor(message)) if message.contains("not a panel")
2584 ));
2585 let cornered = crate::PanelDesc {
2586 anchor: crate::PanelAnchor::TOP | crate::PanelAnchor::LEFT,
2587 ..taller
2588 };
2589 assert!(matches!(
2590 shell.set_panel(window, size, &cornered),
2591 Err(ShellError::Unsupported { .. })
2592 ));
2593 assert_eq!(
2594 shell.panel(window).expect("live"),
2595 Some(taller),
2596 "a refused descriptor is not recorded"
2597 );
2598 assert_eq!(shell.panel(toplevel).expect("live"), None);
2599
2600 shell.destroy_window(window).expect("destroy");
2601 assert!(matches!(
2602 shell.set_panel(window, size, &taller),
2603 Err(ShellError::InvalidWindow { .. })
2604 ));
2605 }
2606
2607 #[test]
2610 fn a_repositioned_popup_is_configured_at_its_new_size() {
2611 let (mut shell, window) = shell_with_configured_window();
2612 let anchor = crate::LogicalRect::new(
2613 crate::LogicalPoint::new(10.0, 10.0),
2614 LogicalSize::new(100.0, 20.0),
2615 );
2616 let popup = shell
2617 .create_window(&WindowDesc {
2618 size: LogicalSize::new(200.0, 150.0),
2619 role: crate::WindowRole::Popup(crate::PopupDesc {
2620 parent: window,
2621 anchor,
2622 placement: crate::PopupPlacement::Below,
2623 input: crate::PopupInput::Pointer,
2624 }),
2625 ..WindowDesc::default()
2626 })
2627 .expect("a popup");
2628 settle(&mut shell);
2629 let scale = shell
2630 .window_state(popup)
2631 .expect("live")
2632 .scale_factor()
2633 .expect("configured");
2634
2635 let moved = LogicalSize::new(240.0, 90.0);
2636 shell
2637 .reposition_popup(popup, anchor, moved)
2638 .expect("reposition");
2639 settle(&mut shell);
2640 assert_eq!(
2641 shell.window_state(popup).expect("live").size(),
2642 Some(moved.to_physical(scale))
2643 );
2644 assert!(matches!(
2645 shell.reposition_popup(window, anchor, moved),
2646 Err(ShellError::InvalidDescriptor(_))
2647 ));
2648 }
2649
2650 #[test]
2654 fn escape_on_a_grabbing_popup_is_delivered_then_asks_it_to_close() {
2655 let (mut shell, window) = shell_with_configured_window();
2656 let popup_of = |shell: &mut HeadlessShell, input| {
2657 shell
2658 .create_window(&WindowDesc {
2659 role: crate::WindowRole::Popup(crate::PopupDesc {
2660 parent: window,
2661 anchor: crate::LogicalRect::default(),
2662 placement: crate::PopupPlacement::Below,
2663 input,
2664 }),
2665 ..WindowDesc::default()
2666 })
2667 .expect("a popup")
2668 };
2669 let menu = popup_of(&mut shell, crate::PopupInput::Grab);
2670 let tooltip = popup_of(&mut shell, crate::PopupInput::Pointer);
2671 settle(&mut shell);
2672 let is_close = |event: &ShellEvent| matches!(event, ShellEvent::CloseRequested { .. });
2673
2674 shell.key_press(menu, KeyCode::Escape).expect("press");
2675 let events = drain(&mut shell);
2676 assert!(
2677 matches!(
2678 events.as_slice(),
2679 [
2680 ShellEvent::Key {
2681 key_code: Some(KeyCode::Escape),
2682 ..
2683 },
2684 ShellEvent::CloseRequested { window: closed },
2685 ] if *closed == menu
2686 ),
2687 "the key, then the request: {events:?}"
2688 );
2689 shell.key_press(menu, KeyCode::Escape).expect("press");
2691 shell.key_repeat(menu, KeyCode::Escape).expect("repeat");
2692 assert!(!drain(&mut shell).iter().any(is_close));
2693
2694 shell
2696 .reply_close_request(menu, CloseReply::Keep)
2697 .expect("keep");
2698 shell.key_release(menu, KeyCode::Escape).expect("release");
2699 shell.key_press(menu, KeyCode::Enter).expect("press");
2700 assert!(!drain(&mut shell).iter().any(is_close), "not these");
2701 shell.key_press(menu, KeyCode::Escape).expect("press");
2702 assert!(drain(&mut shell).iter().any(is_close));
2703
2704 shell.key_press(tooltip, KeyCode::Escape).expect("press");
2705 shell.key_press(window, KeyCode::Escape).expect("press");
2706 assert!(!drain(&mut shell).iter().any(is_close));
2707 }
2708
2709 #[test]
2710 fn set_constraints_is_a_request_too() {
2711 let (mut shell, window) = shell_with_configured_window();
2712 let constraints = SizeConstraints::min(LogicalSize::new(1600.0, 900.0));
2713 shell.set_constraints(window, constraints).expect("request");
2714
2715 let state = shell.window_state(window).expect("state");
2716 assert_eq!(state.requested_constraints, constraints);
2717 assert_eq!(
2718 state.size(),
2719 Some(PhysicalSize::new(1280, 720)),
2720 "the size only changes when the window system says so"
2721 );
2722
2723 settle(&mut shell);
2724 assert_eq!(
2725 shell.window_state(window).expect("state").size(),
2726 Some(PhysicalSize::new(1600, 900))
2727 );
2728 }
2729
2730 #[test]
2731 fn constraints_do_not_cancel_a_mode_request_that_has_not_landed() {
2732 let (mut shell, window) = shell_with_configured_window();
2737 shell
2738 .set_mode(window, DisplayMode::Borderless { monitor: None })
2739 .expect("mode request");
2740 shell
2741 .set_constraints(window, SizeConstraints::min(LogicalSize::new(320.0, 180.0)))
2742 .expect("constraint request");
2743 settle(&mut shell);
2744
2745 let state = shell.window_state(window).expect("state");
2746 assert_eq!(
2747 state.effective_mode(),
2748 Some(DisplayMode::Borderless { monitor: None })
2749 );
2750 assert!(state.mode_request_honoured());
2751 }
2752
2753 #[test]
2754 fn a_borderless_window_reports_its_own_monitors_scale() {
2755 let mut shell = HeadlessShell::new();
2760 let hidpi = shell.plug_monitor(MonitorInfo {
2761 id: MonitorId(0),
2762 name: "HEADLESS-2".to_string(),
2763 bounds: PhysicalRect::new(1920, 0, 2560, 1440),
2764 work_area: PhysicalRect::new(1920, 0, 2560, 1440),
2765 scale_factor: 2.0,
2766 refresh_millihertz: 143_998,
2767 is_primary: false,
2768 });
2769 let window = shell
2770 .create_window(&WindowDesc {
2771 mode: DisplayMode::Borderless {
2772 monitor: Some(hidpi),
2773 },
2774 ..WindowDesc::default()
2775 })
2776 .expect("window");
2777 settle(&mut shell);
2778
2779 let state = shell.window_state(window).expect("state");
2780 assert_eq!(state.size(), Some(PhysicalSize::new(2560, 1440)));
2781 assert_eq!(
2782 state.scale_factor(),
2783 Some(2.0),
2784 "the scale is the borderless monitor's, not the primary's"
2785 );
2786 let other = shell
2789 .create_window(&WindowDesc::default())
2790 .expect("second window");
2791 settle(&mut shell);
2792 shell
2793 .set_mode(
2794 other,
2795 DisplayMode::Borderless {
2796 monitor: Some(hidpi),
2797 },
2798 )
2799 .expect("request");
2800 settle(&mut shell);
2801 assert_eq!(
2802 shell.window_state(other).expect("state").scale_factor(),
2803 shell.window_state(window).expect("state").scale_factor()
2804 );
2805 }
2806
2807 #[test]
2808 fn constraints_set_before_the_first_configure_fold_into_it() {
2809 let mut shell = HeadlessShell::new();
2810 let window = shell.create_window(&WindowDesc::default()).expect("window");
2811 shell
2812 .set_constraints(
2813 window,
2814 SizeConstraints::NONE.with_aspect(AspectRatio::SQUARE),
2815 )
2816 .expect("request");
2817 settle(&mut shell);
2818 assert_eq!(
2821 shell.window_state(window).expect("state").size(),
2822 Some(PhysicalSize::new(720, 720))
2823 );
2824 }
2825
2826 #[test]
2827 fn creation_uses_the_requested_size_and_the_monitors_scale() {
2828 let mut shell = HeadlessShell::new();
2829 let window = shell
2830 .create_window(&WindowDesc {
2831 size: LogicalSize::new(800.0, 600.0),
2832 ..WindowDesc::default()
2833 })
2834 .expect("window");
2835 settle(&mut shell);
2836 assert_eq!(
2837 shell.window_state(window).expect("state").size(),
2838 Some(PhysicalSize::new(800, 600))
2839 );
2840
2841 let mut shell = HeadlessShell::new().with_scale_factor(2.0);
2842 let window = shell
2843 .create_window(&WindowDesc {
2844 size: LogicalSize::new(640.0, 360.0),
2845 ..WindowDesc::default()
2846 })
2847 .expect("window");
2848 settle(&mut shell);
2849 let state = shell.window_state(window).expect("state");
2850 assert_eq!(state.size(), Some(PhysicalSize::new(1280, 720)));
2851 assert_eq!(state.scale_factor(), Some(2.0));
2852 }
2853
2854 #[test]
2857 fn scale_changes_preserve_logical_size_and_report_both_facts() {
2858 let (mut shell, window) = shell_with_configured_window();
2859 shell
2860 .change_scale_factor(window, 1.5)
2861 .expect("scale change");
2862 let events = drain(&mut shell);
2863 assert_eq!(
2864 events,
2865 vec![
2866 ShellEvent::ScaleFactorChanged {
2867 window,
2868 scale_factor: 1.5,
2869 size: PhysicalSize::new(1920, 1080),
2870 },
2871 ShellEvent::Resized {
2874 window,
2875 size: PhysicalSize::new(1920, 1080),
2876 scale_factor: 1.5,
2877 },
2878 ]
2879 );
2880 assert_eq!(
2884 shell.window_state(window).expect("state").size(),
2885 Some(PhysicalSize::new(1920, 1080))
2886 );
2887 }
2888
2889 #[test]
2890 fn a_scale_change_scheduled_by_a_mode_switch_arrives_before_the_resize() {
2891 let mut shell = HeadlessShell::new();
2892 let hidpi = shell.plug_monitor(MonitorInfo {
2893 id: MonitorId(0),
2894 name: "HEADLESS-2".to_string(),
2895 bounds: PhysicalRect::new(1920, 0, 2560, 1440),
2896 work_area: PhysicalRect::new(1920, 0, 2560, 1440),
2897 scale_factor: 2.0,
2898 refresh_millihertz: 143_998,
2899 is_primary: false,
2900 });
2901 assert_eq!(hidpi, MonitorId(2), "ids are never reused");
2902 let window = shell.create_window(&WindowDesc::default()).expect("window");
2903 settle(&mut shell);
2904
2905 shell
2906 .set_mode(
2907 window,
2908 DisplayMode::Borderless {
2909 monitor: Some(hidpi),
2910 },
2911 )
2912 .expect("request");
2913 assert_eq!(drain(&mut shell), Vec::new(), "still only a request");
2914 let events = drain(&mut shell);
2915 assert_eq!(names(&events), ["ScaleFactorChanged", "Resized"]);
2916
2917 let state = shell.window_state(window).expect("state");
2918 assert_eq!(state.size(), Some(PhysicalSize::new(2560, 1440)));
2919 assert_eq!(state.scale_factor(), Some(2.0));
2920 assert!(state.effective_mode().expect("configured").is_borderless());
2921 assert_eq!(
2926 shell.surface_target(window).expect("target"),
2927 SurfaceTarget::Offscreen
2928 );
2929
2930 assert!(matches!(
2931 shell.set_mode(
2932 window,
2933 DisplayMode::Borderless {
2934 monitor: Some(MonitorId(99)),
2935 },
2936 ),
2937 Err(ShellError::NoSuchMonitor(99))
2938 ));
2939 }
2940
2941 #[test]
2942 fn resizes_honour_constraints_and_inject_bypasses_them() {
2943 let (mut shell, window) = shell_with_configured_window();
2944 shell
2945 .set_constraints(
2946 window,
2947 SizeConstraints::min(LogicalSize::new(640.0, 360.0))
2948 .with_aspect(AspectRatio::WIDESCREEN),
2949 )
2950 .expect("request");
2951 settle(&mut shell);
2952
2953 shell
2954 .resize(window, PhysicalSize::new(1000, 1000))
2955 .expect("resize");
2956 assert_eq!(
2957 drain(&mut shell),
2958 vec![ShellEvent::Resized {
2959 window,
2960 size: PhysicalSize::new(1000, 562),
2961 scale_factor: 1.0,
2962 }]
2963 );
2964
2965 shell.inject(ShellEvent::Resized {
2968 window,
2969 size: PhysicalSize::new(300, 900),
2970 scale_factor: 1.0,
2971 });
2972 assert_eq!(
2973 drain(&mut shell)[0],
2974 ShellEvent::Resized {
2975 window,
2976 size: PhysicalSize::new(300, 900),
2977 scale_factor: 1.0,
2978 }
2979 );
2980 }
2981
2982 #[test]
2983 fn a_compositor_configure_supersedes_a_pending_one() {
2984 let (mut shell, window) = shell_with_configured_window();
2985 shell
2986 .set_mode(window, DisplayMode::Borderless { monitor: None })
2987 .expect("request");
2988 shell
2990 .resize(window, PhysicalSize::new(640, 480))
2991 .expect("answer");
2992 assert_eq!(names(&drain(&mut shell)), ["Resized"]);
2993 assert_eq!(
2994 shell.window_state(window).expect("state").size(),
2995 Some(PhysicalSize::new(640, 480))
2996 );
2997 assert_eq!(drain(&mut shell), Vec::new());
2999 }
3000
3001 #[test]
3004 fn timestamps_come_from_the_shells_clock_not_from_the_pump() {
3005 let (mut shell, window) = shell_with_configured_window();
3006 shell.set_time(Duration::from_secs(2));
3007 shell.key_press(window, KeyCode::Space).expect("press");
3008 shell.advance(Duration::from_millis(180));
3009 shell.key_release(window, KeyCode::Space).expect("release");
3010
3011 let events = drain(&mut shell);
3012 let held = events[1]
3013 .time()
3014 .expect("input")
3015 .saturating_since(events[0].time().expect("input"));
3016 assert_eq!(held, Duration::from_millis(180));
3017 assert_eq!(events[0].time(), Some(EventTime::from_millis(2_000)));
3018 }
3019
3020 #[test]
3023 fn a_scheduled_script_injects_at_its_pump_with_that_pumps_time() {
3024 let (mut shell, window) = shell_with_configured_window();
3025 shell.schedule(2, move |shell| {
3026 shell.key_press(window, KeyCode::Space).expect("press");
3027 });
3028
3029 for pump in 0..2 {
3030 shell.advance(Duration::from_millis(16));
3031 assert_eq!(drain(&mut shell), Vec::new(), "pump {pump} came early");
3032 }
3033 shell.set_time(Duration::from_secs(3));
3034 let events = drain(&mut shell);
3035 assert_eq!(names(&events), ["Key"]);
3036 assert_eq!(events[0].time(), Some(EventTime::from_millis(3_000)));
3037 assert_eq!(drain(&mut shell), Vec::new(), "and it ran once");
3038 }
3039
3040 #[test]
3043 fn scheduled_scripts_run_in_order_and_a_nested_one_waits_a_pump() {
3044 let (mut shell, window) = shell_with_configured_window();
3045 shell.schedule(0, move |shell| {
3046 shell.key_press(window, KeyCode::KeyA).expect("press");
3047 shell.schedule(0, move |shell| {
3048 shell.key_press(window, KeyCode::KeyC).expect("press");
3049 });
3050 });
3051 shell.schedule(0, move |shell| {
3052 shell.key_press(window, KeyCode::KeyB).expect("press");
3053 });
3054
3055 let keys = |events: Vec<ShellEvent>| -> Vec<Option<KeyCode>> {
3056 events
3057 .into_iter()
3058 .map(|event| match event {
3059 ShellEvent::Key { key_code, .. } => key_code,
3060 other => panic!("not a key: {other:?}"),
3061 })
3062 .collect()
3063 };
3064 assert_eq!(
3065 keys(drain(&mut shell)),
3066 [Some(KeyCode::KeyA), Some(KeyCode::KeyB)]
3067 );
3068 assert_eq!(keys(drain(&mut shell)), [Some(KeyCode::KeyC)]);
3069 }
3070
3071 #[test]
3074 fn keep_alive_runs_a_due_script_and_keeps_its_events() {
3075 let (mut shell, window) = shell_with_configured_window();
3076 shell.schedule(1, move |shell| {
3077 shell.key_press(window, KeyCode::Enter).expect("press");
3078 });
3079 shell.keep_alive();
3080 shell.keep_alive();
3081 assert_eq!(shell.pending_events(), 1, "the script ran in keep_alive");
3082 assert_eq!(names(&drain(&mut shell)), ["Key"]);
3083 }
3084
3085 #[test]
3089 fn a_wait_does_not_sleep_while_a_script_is_scheduled() {
3090 let (mut shell, window) = shell_with_configured_window();
3091 shell.schedule(1, move |shell| {
3092 shell.key_press(window, KeyCode::Space).expect("press");
3093 });
3094 let start = std::time::Instant::now();
3095 shell.wait_events(Some(LONG_WAIT));
3096 assert!(
3097 start.elapsed() < LONG_WAIT / 2,
3098 "the wait slept {:?}",
3099 start.elapsed()
3100 );
3101 }
3102
3103 #[test]
3104 fn synthesized_key_fields_are_stable_and_plausible() {
3105 let (mut shell, window) = shell_with_configured_window();
3106 shell.set_modifiers(Modifiers::CTRL);
3107 shell.key_press(window, KeyCode::KeyS).expect("press");
3108 let events = drain(&mut shell);
3109 let ShellEvent::Key {
3110 scancode,
3111 key_code,
3112 keysym,
3113 modifiers,
3114 repeat,
3115 device,
3116 ..
3117 } = events[0].clone()
3118 else {
3119 panic!("wrong variant");
3120 };
3121 assert_eq!(key_code, Some(KeyCode::KeyS));
3122 assert_eq!(keysym, Keysym::from_char('s'));
3123 assert_eq!(modifiers, Modifiers::CTRL);
3124 assert!(!repeat);
3125 assert_eq!(device, VIRTUAL_DEVICE);
3126 assert!(scancode.0 >= 8, "evdev-shaped scancodes start at 8");
3127 assert_eq!(synthetic_keysym(KeyCode::Digit7), Keysym::from_char('7'));
3128 assert_eq!(synthetic_keysym(KeyCode::F5), Keysym(0xFFC2));
3129 assert_eq!(synthetic_keysym(KeyCode::Enter), Keysym(0xFF0D));
3130 assert_eq!(synthetic_keysym(KeyCode::Escape), Keysym(0xFF1B));
3131 assert_eq!(synthetic_keysym(KeyCode::ArrowLeft), Keysym(0xFF51));
3132 assert_eq!(synthetic_keysym(KeyCode::Slash), Keysym::from_char('/'));
3133
3134 shell.key_repeat(window, KeyCode::KeyS).expect("repeat");
3135 let events = drain(&mut shell);
3136 let ShellEvent::Key { repeat, .. } = events[0] else {
3137 panic!("wrong variant");
3138 };
3139 assert!(repeat);
3140 }
3141
3142 #[test]
3143 fn a_locked_pointer_reports_no_absolute_position() {
3144 let (mut shell, window) = shell_with_configured_window();
3145 shell
3146 .set_pointer_mode(window, PointerMode::Locked)
3147 .expect("lock");
3148 shell
3149 .move_pointer(window, PhysicalPoint::new(10.0, 10.0), (3.0, -4.0))
3150 .expect("motion");
3151 let events = drain(&mut shell);
3152 let ShellEvent::PointerMotion { abs, raw_delta, .. } = events[0] else {
3153 panic!("wrong variant");
3154 };
3155 assert_eq!(abs, None);
3156 assert_eq!(raw_delta, Some((3.0, -4.0)));
3157 }
3158
3159 #[test]
3160 fn a_backend_without_raw_motion_reports_none() {
3161 let mut shell = HeadlessShell::new()
3162 .with_caps(ShellCaps::DESKTOP - ShellCaps::RAW_POINTER_MOTION)
3163 .with_configure_delay(0);
3164 let window = shell.create_window(&WindowDesc::default()).expect("window");
3165 drain(&mut shell);
3166 shell
3167 .move_pointer(window, PhysicalPoint::new(1.0, 2.0), (5.0, 5.0))
3168 .expect("motion");
3169 let events = drain(&mut shell);
3170 let ShellEvent::PointerMotion { abs, raw_delta, .. } = events[0] else {
3171 panic!("wrong variant");
3172 };
3173 assert_eq!(abs, Some(PhysicalPoint::new(1.0, 2.0)));
3174 assert_eq!(raw_delta, None, "no relative-motion source");
3175 assert!(!shell.caps().has_mouselook());
3176 }
3177
3178 #[test]
3179 fn pointer_focus_and_scroll_and_text_are_injectable() {
3180 let (mut shell, window) = shell_with_configured_window();
3181 shell
3182 .set_pointer_focus(window, true, Some(PhysicalPoint::new(1.0, 1.0)))
3183 .expect("enter");
3184 shell
3185 .scroll(window, ScrollDelta::Lines { x: 0.0, y: -1.0 }, None)
3186 .expect("scroll");
3187 shell
3188 .button(
3189 window,
3190 PointerButton::Left,
3191 ButtonState::Pressed,
3192 Some(PhysicalPoint::new(1.0, 1.0)),
3193 )
3194 .expect("click");
3195 shell.commit_text(window, "ありがとう").expect("text");
3196 shell
3197 .set_pointer_focus(window, false, Some(PhysicalPoint::new(1.0, 1.0)))
3198 .expect("leave");
3199
3200 let events = drain(&mut shell);
3201 assert_eq!(
3202 names(&events),
3203 [
3204 "PointerFocus",
3205 "Wheel",
3206 "Button",
3207 "TextCommit",
3208 "PointerFocus"
3209 ]
3210 );
3211 let ShellEvent::PointerFocus {
3212 entered, position, ..
3213 } = &events[4]
3214 else {
3215 panic!("wrong variant");
3216 };
3217 assert!(!entered);
3218 assert_eq!(*position, None, "a leave has no position");
3219 assert!(events.iter().all(ShellEvent::is_input));
3220 }
3221
3222 #[test]
3229 fn contacts_are_injectable_and_a_touchless_shell_refuses_them() {
3230 let (mut shell, window) = shell_with_configured_window();
3231 assert!(shell.caps().contains(ShellCaps::TOUCH));
3232 for (contact, phase, x) in [
3233 (1, TouchPhase::Began, 10.0),
3234 (2, TouchPhase::Began, 200.0),
3235 (2, TouchPhase::Moved, 220.0),
3236 (1, TouchPhase::Ended, 10.0),
3237 (2, TouchPhase::Cancelled, 220.0),
3238 ] {
3239 shell
3240 .touch(
3241 window,
3242 ContactId(contact),
3243 phase,
3244 PhysicalPoint::new(x, 5.0),
3245 )
3246 .expect("a contact");
3247 }
3248 let events = drain(&mut shell);
3249 assert_eq!(names(&events), ["Touch"; 5]);
3250 let seen: Vec<(ContactId, TouchPhase, f64)> = events
3251 .iter()
3252 .map(|event| {
3253 let ShellEvent::Touch {
3254 contact,
3255 phase,
3256 position,
3257 ..
3258 } = event
3259 else {
3260 panic!("wrong variant");
3261 };
3262 (*contact, *phase, position.x)
3263 })
3264 .collect();
3265 assert_eq!(
3266 seen,
3267 vec![
3268 (ContactId(1), TouchPhase::Began, 10.0),
3269 (ContactId(2), TouchPhase::Began, 200.0),
3270 (ContactId(2), TouchPhase::Moved, 220.0),
3271 (ContactId(1), TouchPhase::Ended, 10.0),
3272 (ContactId(2), TouchPhase::Cancelled, 220.0),
3273 ]
3274 );
3275 assert!(events.iter().all(ShellEvent::is_input));
3276
3277 let mut touchless = HeadlessShell::new()
3278 .with_caps(ShellCaps::DESKTOP - ShellCaps::TOUCH)
3279 .with_configure_delay(0);
3280 let window = touchless
3281 .create_window(&WindowDesc::default())
3282 .expect("window");
3283 drain(&mut touchless);
3284 assert!(matches!(
3285 touchless.touch(
3286 window,
3287 ContactId(1),
3288 TouchPhase::Began,
3289 PhysicalPoint::ORIGIN
3290 ),
3291 Err(ShellError::Unsupported { .. })
3292 ));
3293 assert!(drain(&mut touchless).is_empty(), "and queued nothing");
3294 }
3295
3296 #[test]
3297 fn warping_produces_motion_like_every_platform_that_has_it() {
3298 let (mut shell, window) = shell_with_configured_window();
3299 shell
3300 .warp_pointer(window, PhysicalPoint::new(320.0, 180.0))
3301 .expect("warp");
3302 let events = drain(&mut shell);
3303 let ShellEvent::PointerMotion { abs, raw_delta, .. } = events[0] else {
3304 panic!("wrong variant");
3305 };
3306 assert_eq!(abs, Some(PhysicalPoint::new(320.0, 180.0)));
3307 assert_eq!(raw_delta, None, "a warp is not physical motion");
3308 }
3309
3310 #[test]
3313 fn capabilities_gate_the_operations_that_need_them() {
3314 let mut shell = HeadlessShell::new().with_caps(ShellCaps::empty());
3315 let window = shell.create_window(&WindowDesc::default()).expect("window");
3316
3317 assert!(matches!(
3318 shell.warp_pointer(window, PhysicalPoint::ORIGIN),
3319 Err(ShellError::Unsupported { .. })
3320 ));
3321 assert!(matches!(
3322 shell.set_pointer_mode(window, PointerMode::Locked),
3323 Err(ShellError::Unsupported { .. })
3324 ));
3325 assert!(matches!(
3326 shell.clipboard_request(window, MimeType::TextUtf8),
3327 Err(ShellError::Unsupported { .. })
3328 ));
3329 shell
3331 .set_pointer_mode(window, PointerMode::Free)
3332 .expect("Free is always available");
3333 assert!(matches!(
3335 shell.create_window(&WindowDesc::default()),
3336 Err(ShellError::Unsupported { .. })
3337 ));
3338 }
3339
3340 #[test]
3341 fn stale_handles_fail_cleanly_on_every_accessor() {
3342 let (mut shell, window) = shell_with_configured_window();
3343 shell.destroy_window(window).expect("destroy");
3344 assert_eq!(
3345 drain(&mut shell),
3346 vec![ShellEvent::WindowDestroyed { window }]
3347 );
3348
3349 assert!(matches!(
3350 shell.window_state(window),
3351 Err(ShellError::InvalidWindow { .. })
3352 ));
3353 assert!(matches!(
3354 shell.surface_target(window),
3355 Err(ShellError::InvalidWindow { .. })
3356 ));
3357 assert!(matches!(
3358 shell.resize(window, PhysicalSize::new(1, 1)),
3359 Err(ShellError::InvalidWindow { .. })
3360 ));
3361 assert!(matches!(
3362 shell.set_title(window, "gone"),
3363 Err(ShellError::InvalidWindow { .. })
3364 ));
3365 assert!(matches!(
3366 shell.destroy_window(window),
3367 Err(ShellError::InvalidWindow { .. })
3368 ));
3369 assert!(matches!(
3370 shell.key_press(window, KeyCode::KeyA),
3371 Err(ShellError::InvalidWindow { .. })
3372 ));
3373 }
3374
3375 #[test]
3376 fn destroying_a_window_cancels_its_pending_configure() {
3377 let (mut shell, window) = shell_with_window();
3380 shell.destroy_window(window).expect("destroy");
3381 assert_eq!(names(&drain(&mut shell)), ["WindowDestroyed"]);
3382 assert_eq!(drain(&mut shell), Vec::new());
3383 assert_eq!(drain(&mut shell), Vec::new());
3384 }
3385
3386 #[test]
3387 fn close_requests_are_questions() {
3388 let (mut shell, window) = shell_with_configured_window();
3389 assert!(matches!(
3390 shell.reply_close_request(window, CloseReply::Close),
3391 Err(ShellError::NoPendingCloseRequest { .. })
3392 ));
3393
3394 shell.request_close(window).expect("request");
3395 assert!(shell.window_state(window).expect("state").close_pending);
3396 shell
3397 .reply_close_request(window, CloseReply::Keep)
3398 .expect("keep");
3399 assert!(!shell.window_state(window).expect("state").close_pending);
3400 assert!(shell.window_state(window).is_ok(), "still open");
3401
3402 shell.request_close(window).expect("request again");
3403 shell
3404 .reply_close_request(window, CloseReply::Close)
3405 .expect("close");
3406 assert!(matches!(
3407 shell.window_state(window),
3408 Err(ShellError::InvalidWindow { .. })
3409 ));
3410 }
3411
3412 #[test]
3413 fn monitor_hotplug_reports_a_change_and_keeps_ids_unique() {
3414 let mut shell = HeadlessShell::new();
3415 let extra = shell.plug_monitor(MonitorInfo {
3416 id: MonitorId(0),
3417 name: "HEADLESS-2".to_string(),
3418 bounds: PhysicalRect::new(1920, 0, 1920, 1080),
3419 work_area: PhysicalRect::new(1920, 0, 1920, 1080),
3420 scale_factor: 1.0,
3421 refresh_millihertz: 60_000,
3422 is_primary: false,
3423 });
3424 assert_eq!(shell.monitors().len(), 2);
3425 assert_eq!(drain(&mut shell), vec![ShellEvent::MonitorsChanged]);
3426
3427 shell.unplug_monitor(extra).expect("unplug");
3428 assert_eq!(shell.monitors().len(), 1);
3429 assert_eq!(
3430 drain(&mut shell),
3431 vec![
3432 ShellEvent::MonitorRemoved { monitor: extra },
3433 ShellEvent::MonitorsChanged
3434 ]
3435 );
3436 assert!(matches!(
3437 shell.unplug_monitor(extra),
3438 Err(ShellError::NoSuchMonitor(_))
3439 ));
3440 assert_eq!(
3441 shell.monitor(MonitorId(1)).map(|m| m.name.as_str()),
3442 Some("HEADLESS-1")
3443 );
3444 assert!(shell.monitor(extra).is_none());
3445 }
3446
3447 #[test]
3448 fn unplugging_a_monitor_asks_its_panels_and_only_its_panels_to_close() {
3449 let mut shell = HeadlessShell::new();
3450 let second = shell.plug_monitor(MonitorInfo {
3451 id: MonitorId(0),
3452 name: "HEADLESS-2".to_string(),
3453 bounds: PhysicalRect::new(1920, 0, 1920, 1080),
3454 work_area: PhysicalRect::new(1920, 0, 1920, 1080),
3455 scale_factor: 1.0,
3456 refresh_millihertz: 60_000,
3457 is_primary: false,
3458 });
3459 let first = shell.monitors()[0].id;
3460 let panel_on = |shell: &mut HeadlessShell, monitor| {
3461 shell
3462 .create_window(&WindowDesc {
3463 role: crate::WindowRole::Panel(crate::PanelDesc {
3464 monitor,
3465 ..crate::PanelDesc::default()
3466 }),
3467 ..WindowDesc::default()
3468 })
3469 .expect("panel")
3470 };
3471 let doomed = panel_on(&mut shell, Some(second));
3472 let already_asked = panel_on(&mut shell, Some(second));
3473 let elsewhere = panel_on(&mut shell, Some(first));
3474 let anywhere = panel_on(&mut shell, None);
3475 let toplevel = shell.create_window(&WindowDesc::default()).expect("window");
3476 shell.request_close(already_asked).expect("close");
3477 settle(&mut shell);
3478 drain(&mut shell);
3479
3480 shell.unplug_monitor(second).expect("unplug");
3481 assert_eq!(
3482 drain(&mut shell),
3483 vec![
3484 ShellEvent::MonitorRemoved { monitor: second },
3485 ShellEvent::CloseRequested { window: doomed },
3486 ShellEvent::MonitorsChanged,
3487 ]
3488 );
3489 for window in [elsewhere, anywhere, toplevel] {
3490 assert!(!shell.window_state(window).expect("open").close_pending);
3491 }
3492 }
3493
3494 #[test]
3495 fn drops_require_both_the_capability_and_the_opt_in() {
3496 let mut shell = HeadlessShell::new();
3497 let plain = shell.create_window(&WindowDesc::default()).expect("window");
3498 assert!(matches!(
3499 shell.drop_file(plain, "/tmp/a.gltf", None),
3500 Err(ShellError::Unsupported { .. })
3501 ));
3502
3503 let dropper = shell
3504 .create_window(&WindowDesc {
3505 accept_drops: true,
3506 ..WindowDesc::default()
3507 })
3508 .expect("window");
3509 settle(&mut shell);
3510 shell
3511 .drop_file(dropper, "/tmp/a.gltf", Some(PhysicalPoint::new(4.0, 5.0)))
3512 .expect("drop");
3513 assert_eq!(names(&drain(&mut shell))[0], "DroppedFile");
3514 }
3515
3516 #[test]
3517 fn a_sink_that_queues_more_events_does_not_spin_the_pump() {
3518 let (mut shell, window) = shell_with_configured_window();
3519 shell.inject(ShellEvent::MonitorsChanged);
3520 let mut extra = Vec::new();
3521 shell.pump(&mut |event| extra.push(event));
3522 assert_eq!(extra.len(), 1);
3523 shell.inject(ShellEvent::CloseRequested { window });
3524 assert_eq!(shell.pending_events(), 1);
3525 }
3526
3527 #[test]
3528 fn accessors_that_no_real_backend_can_provide_are_headless_only() {
3529 let (mut shell, window) = shell_with_configured_window();
3530 shell.set_title(window, "renamed").expect("title");
3531 assert_eq!(shell.title(window).expect("title"), "renamed");
3532 shell.set_cursor(window, None).expect("hide");
3533 assert_eq!(shell.cursor(window).expect("cursor"), None);
3534 shell
3535 .set_cursor(window, Some(CursorIcon::Crosshair))
3536 .expect("shape");
3537 assert_eq!(
3538 shell.cursor(window).expect("cursor"),
3539 Some(CursorIcon::Crosshair)
3540 );
3541 shell.set_visible(window, false).expect("hide window");
3542 assert!(!shell.window_state(window).expect("state").visible);
3543 }
3544
3545 #[test]
3546 fn an_input_region_is_recorded_replaced_and_given_back() {
3547 let (mut shell, window) = shell_with_configured_window();
3548 assert_eq!(shell.input_region(window).expect("region"), None);
3549 let panel = PhysicalRect::new(0, 0, 400, 300).rounded(12);
3550 shell
3551 .set_input_region(window, Some(&panel))
3552 .expect("a region");
3553 assert_eq!(
3554 shell.input_region(window).expect("region"),
3555 Some(panel.as_slice())
3556 );
3557 shell.set_input_region(window, Some(&[])).expect("nothing");
3558 assert_eq!(shell.input_region(window).expect("region"), Some(&[][..]));
3559 shell.set_input_region(window, None).expect("whole window");
3560 assert_eq!(shell.input_region(window).expect("region"), None);
3561 shell.destroy_window(window).expect("destroy");
3562 assert!(matches!(
3563 shell.set_input_region(window, None),
3564 Err(ShellError::InvalidWindow { .. })
3565 ));
3566 assert!(shell.input_region(window).is_err());
3567 }
3568
3569 fn frames_for(events: &[ShellEvent], window: WindowId) -> usize {
3570 events
3571 .iter()
3572 .filter(|event| matches!(event, ShellEvent::FrameReady { window: w } if *w == window))
3573 .count()
3574 }
3575
3576 #[test]
3577 fn requests_for_a_frame_share_one_answer_at_the_next_pump() {
3578 let (mut shell, window) = shell_with_configured_window();
3579 assert!(!shell.frame_requested(window).expect("live"));
3580 shell.request_frame(window).expect("a frame");
3581 shell.request_frame(window).expect("the same frame");
3582 assert!(shell.frame_requested(window).expect("live"));
3583 let events = drain(&mut shell);
3584 assert_eq!(frames_for(&events, window), 1, "{:?}", names(&events));
3585 assert!(!shell.frame_requested(window).expect("live"));
3586 assert_eq!(
3587 frames_for(&drain(&mut shell), window),
3588 0,
3589 "nothing asked again"
3590 );
3591 shell.request_frame(window).expect("the next frame");
3592 assert_eq!(frames_for(&drain(&mut shell), window), 1);
3593 }
3594
3595 #[test]
3596 fn a_window_destroyed_with_a_frame_requested_is_not_answered() {
3597 let (mut shell, doomed) = shell_with_configured_window();
3598 let kept = shell
3599 .create_window(&WindowDesc::default())
3600 .expect("a second window");
3601 settle(&mut shell);
3602 shell.request_frame(doomed).expect("a frame");
3603 shell.request_frame(kept).expect("a frame");
3604 shell.destroy_window(doomed).expect("destroy");
3605 let events = drain(&mut shell);
3606 assert_eq!(frames_for(&events, doomed), 0, "{:?}", names(&events));
3607 assert_eq!(frames_for(&events, kept), 1, "the other window still asked");
3608 assert!(matches!(
3609 shell.request_frame(doomed),
3610 Err(ShellError::InvalidWindow { .. })
3611 ));
3612 assert!(shell.frame_requested(doomed).is_err());
3613 }
3614
3615 #[test]
3616 fn a_wait_does_not_sleep_through_a_frame_that_is_due() {
3617 let (mut shell, window) = shell_with_configured_window();
3618 shell.request_frame(window).expect("a frame");
3619 let start = std::time::Instant::now();
3620 shell.wait_events(Some(LONG_WAIT));
3621 assert!(
3622 start.elapsed() < LONG_WAIT / 2,
3623 "slept {:?} with a frame due",
3624 start.elapsed()
3625 );
3626 assert_eq!(frames_for(&drain(&mut shell), window), 1);
3627 }
3628
3629 #[test]
3630 fn an_output_that_is_off_holds_frame_requests_until_it_is_on() {
3631 let (mut shell, window) = shell_with_configured_window();
3632 shell.set_outputs_powered(false);
3633 shell.request_frame(window).expect("a frame");
3634 assert_eq!(frames_for(&drain(&mut shell), window), 0, "held");
3635 assert!(shell.frame_requested(window).expect("live"), "still owed");
3636
3637 let wait = Duration::from_millis(20);
3638 let start = std::time::Instant::now();
3639 shell.wait_events(Some(wait));
3640 assert!(
3641 start.elapsed() >= wait,
3642 "a held request cut the wait short after {:?}",
3643 start.elapsed()
3644 );
3645
3646 shell.set_outputs_powered(true);
3647 assert_eq!(
3648 frames_for(&drain(&mut shell), window),
3649 1,
3650 "answered once on"
3651 );
3652 assert!(!shell.frame_requested(window).expect("live"));
3653 }
3654
3655 #[test]
3656 fn waiting_is_observable_and_focus_transitions_are_events() {
3657 let (mut shell, window) = shell_with_configured_window();
3658 assert_eq!(shell.wait_count(), 0);
3659 shell.wait_events(Some(Duration::from_millis(5)));
3660 shell.wait_events(None);
3661 assert_eq!(shell.wait_count(), 2);
3662
3663 shell.set_focus(window, true).expect("focus");
3664 assert!(shell.window_state(window).expect("state").focused);
3665 shell.set_focus(window, false).expect("unfocus");
3666 assert_eq!(names(&drain(&mut shell)), ["Focus", "Focus"]);
3667 }
3668
3669 const LONG_WAIT: Duration = Duration::from_secs(60);
3674 const RING_AFTER: Duration = Duration::from_millis(50);
3676
3677 fn ring_later(waker: ShellWaker) -> std::thread::JoinHandle<()> {
3679 std::thread::spawn(move || {
3680 std::thread::sleep(RING_AFTER);
3681 waker.wake();
3682 })
3683 }
3684
3685 #[test]
3686 fn a_wake_from_another_thread_ends_a_long_wait() {
3687 let mut shell = HeadlessShell::new();
3688 let ringer = ring_later(shell.waker().expect("headless has a waker"));
3689 let start = std::time::Instant::now();
3690 shell.wait_events(Some(LONG_WAIT));
3691 let slept = start.elapsed();
3692 ringer.join().expect("the ringer");
3693 assert!(slept < LONG_WAIT / 2, "the wait ran {slept:?}");
3694 }
3695
3696 #[test]
3697 fn a_wake_before_the_wait_ends_it_at_once_and_wakes_coalesce() {
3698 let mut shell = HeadlessShell::new();
3699 let waker = shell.waker().expect("headless has a waker");
3700 for _ in 0..3 {
3701 waker.wake();
3702 }
3703 let start = std::time::Instant::now();
3704 shell.wait_events(Some(LONG_WAIT));
3705 assert!(
3706 start.elapsed() < LONG_WAIT / 2,
3707 "a rung bell must end the wait"
3708 );
3709
3710 let timeout = Duration::from_millis(100);
3712 let start = std::time::Instant::now();
3713 shell.wait_events(Some(timeout));
3714 assert!(
3715 start.elapsed() >= timeout / 2,
3716 "the bell was cleared, so this wait must sleep, but it took {:?}",
3717 start.elapsed()
3718 );
3719 }
3720
3721 #[test]
3722 fn with_a_waker_out_an_untimed_wait_sleeps_until_it_rings() {
3723 let mut shell = HeadlessShell::new();
3724 let ringer = ring_later(shell.waker().expect("headless has a waker"));
3725 let start = std::time::Instant::now();
3726 shell.wait_events(None);
3727 let slept = start.elapsed();
3728 ringer.join().expect("the ringer");
3729 assert!(
3730 slept >= RING_AFTER / 2,
3731 "an untimed wait returned after {slept:?}, before anything rang"
3732 );
3733 }
3734
3735 #[test]
3738 fn the_clipboard_round_trips_through_an_event() {
3739 let (mut shell, window) = shell_with_configured_window();
3740 shell
3741 .clipboard_offer(
3742 window,
3743 &[
3744 ClipboardOffer::text("node"),
3745 ClipboardOffer::ron("(kind:\"node\")"),
3746 ],
3747 )
3748 .expect("offer");
3749 assert_eq!(
3750 shell.clipboard_bytes(MimeType::TextUtf8),
3751 Some(&b"node"[..])
3752 );
3753
3754 let request = shell
3755 .clipboard_request(window, MimeType::CrcblRon)
3756 .expect("request");
3757 assert_eq!(
3758 drain(&mut shell)[0],
3759 ShellEvent::ClipboardData {
3760 window,
3761 request,
3762 mime: ReceivedMime::from(MimeType::CrcblRon),
3763 content: ClipboardContent::Bytes(b"(kind:\"node\")".to_vec()),
3764 }
3765 );
3766
3767 let request = shell
3770 .clipboard_request(window, MimeType::UriList)
3771 .expect("request");
3772 assert_eq!(
3773 drain(&mut shell)[0],
3774 ShellEvent::ClipboardData {
3775 window,
3776 request,
3777 mime: ReceivedMime::from(MimeType::UriList),
3778 content: ClipboardContent::Empty,
3779 }
3780 );
3781
3782 shell
3785 .clipboard_offer(window, &[ClipboardOffer::text("")])
3786 .expect("offer");
3787 let request = shell
3788 .clipboard_request(window, MimeType::TextUtf8)
3789 .expect("request");
3790 assert_eq!(
3791 drain(&mut shell)[0],
3792 ShellEvent::ClipboardData {
3793 window,
3794 request,
3795 mime: ReceivedMime::from(MimeType::TextUtf8),
3796 content: ClipboardContent::Bytes(Vec::new()),
3797 }
3798 );
3799 }
3800
3801 #[test]
3802 fn a_read_taken_while_the_clipboard_is_unreadable_is_held_not_answered_empty() {
3803 let (mut shell, window) = shell_with_configured_window();
3809 shell
3810 .clipboard_offer(window, &[ClipboardOffer::text("published")])
3811 .expect("offer");
3812 shell.set_clipboard_readable(false);
3813 assert!(!shell.clipboard_readable(window));
3814
3815 let request = shell
3816 .clipboard_request(window, MimeType::TextUtf8)
3817 .expect("a read is accepted even when it cannot be answered yet");
3818 assert_eq!(shell.held_clipboard_reads(), 1);
3819 assert!(
3820 drain(&mut shell).is_empty(),
3821 "a held read produces no event at all — least of all an empty one"
3822 );
3823
3824 shell.set_clipboard_readable(true);
3826 let events = drain(&mut shell);
3827 assert_eq!(events.len(), 1);
3828 assert_eq!(
3829 events[0],
3830 ShellEvent::ClipboardData {
3831 window,
3832 request,
3833 mime: ReceivedMime::from(MimeType::TextUtf8),
3834 content: ClipboardContent::Bytes(b"published".to_vec()),
3835 }
3836 );
3837 assert_eq!(shell.held_clipboard_reads(), 0);
3838 }
3839
3840 #[test]
3841 fn a_read_that_never_becomes_answerable_ends_as_unavailable() {
3842 let (mut shell, window) = shell_with_configured_window();
3846 shell
3847 .clipboard_offer(window, &[ClipboardOffer::text("unreachable")])
3848 .expect("offer");
3849 shell.set_clipboard_readable(false);
3850 shell.clipboard_deadline(2);
3851
3852 let request = shell
3853 .clipboard_request(window, MimeType::TextUtf8)
3854 .expect("request");
3855 assert!(drain(&mut shell).is_empty(), "pump 1: still held");
3856 assert!(drain(&mut shell).is_empty(), "pump 2: still held");
3857 let events = drain(&mut shell);
3858 assert_eq!(
3859 events[0],
3860 ShellEvent::ClipboardData {
3861 window,
3862 request,
3863 mime: ReceivedMime::from(MimeType::TextUtf8),
3864 content: ClipboardContent::Unavailable,
3867 }
3868 );
3869 assert_eq!(shell.held_clipboard_reads(), 0);
3870 }
3871
3872 #[test]
3873 fn a_peer_that_holds_the_clipboard_and_never_delivers_is_unavailable() {
3874 let (mut shell, window) = shell_with_configured_window();
3878 shell.set_foreign_clipboard(vec![(
3879 ReceivedMime::new("text/plain"),
3880 b"never sent".to_vec(),
3881 )]);
3882 shell.set_clipboard_stalled(true);
3883 shell.clipboard_deadline(1);
3884 assert!(
3885 !shell.clipboard_readable(window),
3886 "a clipboard whose owner will not answer is not readable"
3887 );
3888
3889 let request = shell
3890 .clipboard_request(window, MimeType::TextUtf8)
3891 .expect("request");
3892 assert!(drain(&mut shell).is_empty());
3893 let events = drain(&mut shell);
3894 let ShellEvent::ClipboardData {
3895 request: answered,
3896 content,
3897 ..
3898 } = &events[0]
3899 else {
3900 panic!("wrong variant");
3901 };
3902 assert_eq!(*answered, request);
3903 assert_eq!(*content, ClipboardContent::Unavailable);
3904
3905 shell.set_clipboard_stalled(false);
3907 let request = shell
3908 .clipboard_request(window, MimeType::TextUtf8)
3909 .expect("request");
3910 assert_eq!(
3911 drain(&mut shell)[0],
3912 ShellEvent::ClipboardData {
3913 window,
3914 request,
3915 mime: ReceivedMime::new("text/plain"),
3916 content: ClipboardContent::Bytes(b"never sent".to_vec()),
3917 }
3918 );
3919 }
3920
3921 #[test]
3922 fn claiming_the_clipboard_can_require_a_user_interaction() {
3923 let (mut shell, window) = shell_with_configured_window();
3927 shell.require_user_interaction(true);
3928
3929 let refused = shell.clipboard_offer(window, &[ClipboardOffer::text("stolen")]);
3930 assert!(
3931 matches!(
3932 refused,
3933 Err(ShellError::NeedsUserInteraction {
3934 backend: ShellBackend::Headless,
3935 ..
3936 })
3937 ),
3938 "{refused:?}"
3939 );
3940 assert!(
3941 shell.clipboard_bytes(MimeType::TextUtf8).is_none(),
3942 "a refused claim must not have taken the clipboard anyway"
3943 );
3944
3945 shell.key_press(window, KeyCode::KeyC).expect("key");
3948 shell
3949 .clipboard_offer(window, &[ClipboardOffer::text("copied")])
3950 .expect("with a serial in hand, the claim goes through");
3951 assert_eq!(
3952 shell.clipboard_bytes(MimeType::TextUtf8),
3953 Some(&b"copied"[..])
3954 );
3955
3956 shell.clear_user_interaction();
3958 assert!(matches!(
3959 shell.clipboard_offer(window, &[ClipboardOffer::text("later")]),
3960 Err(ShellError::NeedsUserInteraction { .. })
3961 ));
3962 }
3963
3964 #[test]
3965 fn a_held_read_for_a_destroyed_window_is_dropped_rather_than_answered() {
3966 let (mut shell, window) = shell_with_configured_window();
3969 shell.set_clipboard_readable(false);
3970 let _request = shell
3971 .clipboard_request(window, MimeType::TextUtf8)
3972 .expect("request");
3973 assert_eq!(shell.held_clipboard_reads(), 1);
3974
3975 shell.destroy_window(window).expect("destroy");
3976 let events = drain(&mut shell);
3977 assert_eq!(shell.held_clipboard_reads(), 0);
3978 assert!(
3979 !events
3980 .iter()
3981 .any(|event| matches!(event, ShellEvent::ClipboardData { .. })),
3982 "no clipboard answer may name a destroyed window: {events:?}"
3983 );
3984 }
3985
3986 #[test]
3987 fn a_paste_from_a_foreign_application_keeps_the_foreign_spelling() {
3988 let (mut shell, window) = shell_with_configured_window();
3989 shell.set_foreign_clipboard(vec![
3990 (ReceivedMime::new("text/plain"), b"from gedit".to_vec()),
3993 (
3994 ReceivedMime::new("application/vnd.some-editor.scene+json"),
3995 b"{}".to_vec(),
3996 ),
3997 ]);
3998
3999 let request = shell
4001 .clipboard_request(window, MimeType::TextUtf8)
4002 .expect("request");
4003 let events = drain(&mut shell);
4004 let ShellEvent::ClipboardData {
4005 request: answered,
4006 mime,
4007 content,
4008 ..
4009 } = &events[0]
4010 else {
4011 panic!("wrong variant");
4012 };
4013 assert_eq!(*answered, request);
4014 assert_eq!(content.bytes(), Some(&b"from gedit"[..]));
4015 assert_eq!(mime.as_str(), "text/plain");
4017 assert!(mime.matches(MimeType::TextUtf8));
4018 assert_ne!(*mime, ReceivedMime::from(MimeType::TextUtf8));
4019
4020 assert_eq!(
4023 shell.clipboard_bytes(MimeType::Other("application/vnd.some-editor.scene+json")),
4024 Some(&b"{}"[..])
4025 );
4026 }
4027
4028 #[test]
4031 #[should_panic(expected = "a shell needs at least one monitor")]
4032 fn a_shell_with_no_monitors_is_rejected() {
4033 let _ = HeadlessShell::new().with_monitors(Vec::new());
4034 }
4035
4036 #[test]
4037 #[should_panic(expected = "a text commit is never empty")]
4038 fn an_empty_text_commit_is_rejected() {
4039 let (mut shell, window) = shell_with_configured_window();
4040 let _ = shell.commit_text(window, "");
4041 }
4042}