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, WebUrl, 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 idle_inhibit: bool,
262 #[cfg(feature = "accesskit")]
265 accessibility: Option<Vec<accesskit::TreeUpdate>>,
266}
267
268impl HeadlessWindow {
269 fn size(&self) -> Option<PhysicalSize> {
271 self.configuration.map(|config| config.size)
272 }
273
274 fn scale_factor(&self) -> f64 {
280 self.configuration
281 .map(|config| config.scale_factor)
282 .or_else(|| self.pending.map(|pending| pending.scale_factor))
283 .unwrap_or(1.0)
284 }
285
286 fn settled_mode(&self) -> DisplayMode {
295 self.configuration
296 .map(|config| config.mode)
297 .or_else(|| self.pending.map(|pending| pending.mode))
298 .unwrap_or(self.requested_mode)
299 }
300
301 fn base_size(&self) -> PhysicalSize {
303 self.size()
304 .or_else(|| self.pending.map(|pending| pending.size))
305 .unwrap_or(PhysicalSize::new(1, 1))
306 }
307
308 fn configure_at(&mut self, size: LogicalSize, countdown: u32) {
312 if let Some(pending) = self.pending.as_mut() {
313 pending.size = size.to_physical(pending.scale_factor);
314 } else if let Some(config) = self.configuration {
315 self.pending = Some(PendingConfigure {
316 size: size.to_physical(config.scale_factor),
317 scale_factor: config.scale_factor,
318 mode: config.mode,
319 countdown,
320 });
321 }
322 }
323}
324
325#[derive(Debug)]
330pub struct HeadlessShell {
331 caps: ShellCaps,
332 monitors: Vec<MonitorInfo>,
333 next_monitor_id: u32,
334 windows: Pool<HeadlessWindow>,
335 queue: VecDeque<ShellEvent>,
336 scripts: Vec<ScheduledScript>,
339 clock: ManualTime,
340 modifiers: Modifiers,
341 clipboard: Vec<(ReceivedMime, Vec<u8>)>,
342 next_request_id: u32,
343 reads: Vec<PendingRead>,
346 clipboard_readable: bool,
350 clipboard_stalled: bool,
352 clipboard_needs_interaction: bool,
354 saw_interaction: bool,
357 clipboard_deadline: u32,
360 waits: u32,
361 configure_delay: u32,
362 wake: Option<Arc<HeadlessWake>>,
364 frames: FrameRequests,
366 outputs_powered: bool,
369 opened_urls: Vec<(WindowId, WebUrl)>,
372}
373
374#[derive(Debug, Default)]
377struct HeadlessWake {
378 rung: Mutex<bool>,
379 bell: Condvar,
380}
381
382impl HeadlessWake {
383 fn wait(&self, timeout: Option<Duration>) {
386 let rung = self.rung.lock().unwrap_or_else(PoisonError::into_inner);
387 let mut rung = match timeout {
388 Some(timeout) => {
389 self.bell
390 .wait_timeout_while(rung, timeout, |rung| !*rung)
391 .unwrap_or_else(PoisonError::into_inner)
392 .0
393 }
394 None => self
395 .bell
396 .wait_while(rung, |rung| !*rung)
397 .unwrap_or_else(PoisonError::into_inner),
398 };
399 *rung = false;
400 }
401}
402
403impl WakeSignal for HeadlessWake {
404 fn wake(&self) {
405 *self.rung.lock().unwrap_or_else(PoisonError::into_inner) = true;
406 self.bell.notify_all();
407 }
408}
409
410impl Default for HeadlessShell {
411 fn default() -> Self {
412 Self::new()
413 }
414}
415
416impl HeadlessShell {
417 #[must_use]
419 pub fn new() -> Self {
420 Self {
421 caps: ShellCaps::DESKTOP,
422 monitors: vec![MonitorInfo {
423 id: MonitorId(1),
424 name: "HEADLESS-1".to_string(),
425 bounds: PhysicalRect::new(
426 0,
427 0,
428 DEFAULT_MONITOR_SIZE.width,
429 DEFAULT_MONITOR_SIZE.height,
430 ),
431 work_area: PhysicalRect::new(
432 0,
433 0,
434 DEFAULT_MONITOR_SIZE.width,
435 DEFAULT_MONITOR_SIZE.height,
436 ),
437 scale_factor: 1.0,
438 refresh_millihertz: DEFAULT_REFRESH_MILLIHERTZ,
439 is_primary: true,
440 }],
441 next_monitor_id: 2,
442 windows: Pool::new(),
443 queue: VecDeque::new(),
444 scripts: Vec::new(),
445 clock: ManualTime::new(),
446 modifiers: Modifiers::empty(),
447 clipboard: Vec::new(),
448 next_request_id: 1,
449 reads: Vec::new(),
450 clipboard_readable: true,
451 clipboard_stalled: false,
452 clipboard_needs_interaction: false,
453 saw_interaction: false,
454 clipboard_deadline: DEFAULT_CLIPBOARD_DEADLINE,
455 waits: 0,
456 configure_delay: DEFAULT_CONFIGURE_DELAY,
457 wake: None,
458 frames: FrameRequests::default(),
459 outputs_powered: true,
460 opened_urls: Vec::new(),
461 }
462 }
463
464 #[must_use]
469 pub fn with_caps(mut self, caps: ShellCaps) -> Self {
470 self.caps = caps;
471 self
472 }
473
474 #[must_use]
489 pub fn with_monitors(mut self, monitors: Vec<MonitorInfo>) -> Self {
490 assert!(!monitors.is_empty(), "a shell needs at least one monitor");
491 for (index, monitor) in monitors.iter().enumerate() {
492 assert!(
493 monitor.id.0 != u32::MAX,
494 "monitor id {} leaves no id for the next hotplug",
495 monitor.id.0
496 );
497 assert!(
498 !monitors[..index].iter().any(|seen| seen.id == monitor.id),
499 "monitor id {} appears twice",
500 monitor.id.0
501 );
502 }
503 self.next_monitor_id = monitors
504 .iter()
505 .map(|monitor| monitor.id.0 + 1)
506 .max()
507 .unwrap_or(1);
508 self.monitors = monitors;
509 self
510 }
511
512 #[must_use]
517 pub fn with_scale_factor(mut self, scale_factor: f64) -> Self {
518 assert!(scale_factor > 0.0, "scale factor must be positive");
519 self.monitors[0].scale_factor = scale_factor;
520 self
521 }
522
523 #[must_use]
528 pub const fn with_configure_delay(mut self, pumps: u32) -> Self {
529 self.configure_delay = pumps;
530 self
531 }
532
533 pub const fn set_configure_delay(&mut self, pumps: u32) {
543 self.configure_delay = pumps;
544 }
545
546 pub fn set_caps(&mut self, caps: ShellCaps) {
552 self.caps = caps;
553 }
554
555 #[must_use]
557 pub fn now(&self) -> EventTime {
558 EventTime::from_duration(self.clock.elapsed())
559 }
560
561 pub fn advance(&mut self, delta: Duration) {
567 self.clock.advance(delta);
568 }
569
570 pub fn set_time(&mut self, now: Duration) {
578 self.clock.set(now);
579 }
580
581 pub fn set_modifiers(&mut self, modifiers: Modifiers) {
586 self.modifiers = modifiers;
587 }
588
589 #[must_use]
591 pub fn pending_events(&self) -> usize {
592 self.queue.len()
593 }
594
595 #[must_use]
601 pub const fn wait_count(&self) -> u32 {
602 self.waits
603 }
604
605 pub fn inject(&mut self, event: ShellEvent) {
613 self.queue.push_back(event);
614 }
615
616 pub fn schedule(&mut self, pumps: u32, script: impl FnOnce(&mut Self) + Send + 'static) {
635 self.scripts.push(ScheduledScript {
636 countdown: pumps,
637 script: Box::new(script),
638 });
639 }
640
641 pub fn resize(&mut self, window: WindowId, size: PhysicalSize) -> Result<(), ShellError> {
657 let state = self.window_mut(window)?;
658 let scale_factor = state.scale_factor();
659 let mode = state.settled_mode();
660 let constrained = state.requested_constraints.apply(size, scale_factor);
661 state.pending = None;
662 state.configuration = Some(WindowConfiguration {
663 size: constrained,
664 scale_factor,
665 mode,
666 });
667 self.queue.push_back(ShellEvent::Resized {
668 window,
669 size: constrained,
670 scale_factor,
671 });
672 Ok(())
673 }
674
675 pub fn change_scale_factor(
697 &mut self,
698 window: WindowId,
699 scale_factor: f64,
700 ) -> Result<(), ShellError> {
701 assert!(scale_factor > 0.0, "scale factor must be positive");
702 let state = self.window_mut(window)?;
703 let logical = state.base_size().to_logical(state.scale_factor());
704 let mode = state.settled_mode();
705 let size = state
706 .requested_constraints
707 .apply(logical.to_physical(scale_factor), scale_factor);
708 state.pending = None;
709 state.configuration = Some(WindowConfiguration {
710 size,
711 scale_factor,
712 mode,
713 });
714 self.queue.push_back(ShellEvent::ScaleFactorChanged {
715 window,
716 scale_factor,
717 size,
718 });
719 self.queue.push_back(ShellEvent::Resized {
720 window,
721 size,
722 scale_factor,
723 });
724 Ok(())
725 }
726
727 pub fn set_focus(&mut self, window: WindowId, focused: bool) -> Result<(), ShellError> {
733 self.window_mut(window)?.focused = focused;
734 self.queue.push_back(ShellEvent::Focus { window, focused });
735 Ok(())
736 }
737
738 pub fn request_close(&mut self, window: WindowId) -> Result<(), ShellError> {
747 self.window_mut(window)?.close_pending = true;
748 self.queue.push_back(ShellEvent::CloseRequested { window });
749 Ok(())
750 }
751
752 pub fn key(
765 &mut self,
766 window: WindowId,
767 key: KeyCode,
768 state: ButtonState,
769 ) -> Result<(), ShellError> {
770 self.key_with(window, key, state, false)
771 }
772
773 pub fn key_press(&mut self, window: WindowId, key: KeyCode) -> Result<(), ShellError> {
779 self.key(window, key, ButtonState::Pressed)
780 }
781
782 pub fn key_release(&mut self, window: WindowId, key: KeyCode) -> Result<(), ShellError> {
788 self.key(window, key, ButtonState::Released)
789 }
790
791 pub fn key_repeat(&mut self, window: WindowId, key: KeyCode) -> Result<(), ShellError> {
798 self.key_with(window, key, ButtonState::Pressed, true)
799 }
800
801 fn key_with(
802 &mut self,
803 window: WindowId,
804 key: KeyCode,
805 state: ButtonState,
806 repeat: bool,
807 ) -> Result<(), ShellError> {
808 let popup_input = self.window(window)?.popup_input;
809 self.saw_interaction = true;
810 let event = ShellEvent::Key {
811 window,
812 device: VIRTUAL_DEVICE,
813 time: self.now(),
814 scancode: synthetic_scancode(key),
815 key_code: Some(key),
816 keysym: synthetic_keysym(key),
817 state,
818 repeat,
819 modifiers: self.modifiers,
820 };
821 let dismissed = crate::role::escape_dismisses(popup_input, &event);
822 self.queue.push_back(event);
823 if dismissed && !self.window(window)?.close_pending {
825 self.request_close(window)?;
826 }
827 Ok(())
828 }
829
830 pub fn move_pointer(
841 &mut self,
842 window: WindowId,
843 position: PhysicalPoint,
844 raw_delta: (f64, f64),
845 ) -> Result<(), ShellError> {
846 let locked = self.window(window)?.pointer_mode == PointerMode::Locked;
847 let raw = if self.caps.contains(ShellCaps::RAW_POINTER_MOTION) {
848 Some(raw_delta)
849 } else {
850 None
851 };
852 self.saw_interaction = true;
853 let event = ShellEvent::PointerMotion {
854 window,
855 device: VIRTUAL_DEVICE,
856 time: self.now(),
857 abs: if locked { None } else { Some(position) },
858 raw_delta: raw,
859 };
860 self.queue.push_back(event);
861 Ok(())
862 }
863
864 pub fn set_pointer_focus(
870 &mut self,
871 window: WindowId,
872 entered: bool,
873 position: Option<PhysicalPoint>,
874 ) -> Result<(), ShellError> {
875 self.check_window(window)?;
876 let event = ShellEvent::PointerFocus {
877 window,
878 device: VIRTUAL_DEVICE,
879 time: self.now(),
880 entered,
881 position: if entered { position } else { None },
882 };
883 self.queue.push_back(event);
884 Ok(())
885 }
886
887 pub fn button(
893 &mut self,
894 window: WindowId,
895 button: PointerButton,
896 state: ButtonState,
897 position: Option<PhysicalPoint>,
898 ) -> Result<(), ShellError> {
899 self.check_window(window)?;
900 self.saw_interaction = true;
901 let event = ShellEvent::Button {
902 window,
903 device: VIRTUAL_DEVICE,
904 time: self.now(),
905 button,
906 state,
907 position,
908 modifiers: self.modifiers,
909 };
910 self.queue.push_back(event);
911 Ok(())
912 }
913
914 pub fn scroll(
920 &mut self,
921 window: WindowId,
922 delta: ScrollDelta,
923 position: Option<PhysicalPoint>,
924 ) -> Result<(), ShellError> {
925 self.check_window(window)?;
926 self.saw_interaction = true;
927 let event = ShellEvent::Wheel {
928 window,
929 device: VIRTUAL_DEVICE,
930 time: self.now(),
931 delta,
932 position,
933 modifiers: self.modifiers,
934 };
935 self.queue.push_back(event);
936 Ok(())
937 }
938
939 pub fn touch(
967 &mut self,
968 window: WindowId,
969 contact: ContactId,
970 phase: TouchPhase,
971 position: PhysicalPoint,
972 ) -> Result<(), ShellError> {
973 self.check_window(window)?;
974 if !self.caps.contains(ShellCaps::TOUCH) {
975 return Err(self.unsupported("touch"));
976 }
977 self.saw_interaction = true;
978 let event = ShellEvent::Touch {
979 window,
980 device: VIRTUAL_DEVICE,
981 time: self.now(),
982 contact,
983 phase,
984 position,
985 };
986 self.queue.push_back(event);
987 Ok(())
988 }
989
990 pub fn commit_text(&mut self, window: WindowId, text: &str) -> Result<(), ShellError> {
1001 assert!(!text.is_empty(), "a text commit is never empty");
1002 self.check_window(window)?;
1003 let event = ShellEvent::TextCommit {
1004 window,
1005 time: self.now(),
1006 text: text.to_string(),
1007 };
1008 self.queue.push_back(event);
1009 Ok(())
1010 }
1011
1012 pub fn preedit_text(
1025 &mut self,
1026 window: WindowId,
1027 text: &str,
1028 cursor: Option<usize>,
1029 ) -> Result<(), ShellError> {
1030 assert!(
1031 cursor.is_none_or(|cursor| !text.is_empty() && text.is_char_boundary(cursor)),
1032 "a pre-edit's cursor is a char boundary of a non-empty text"
1033 );
1034 self.check_window(window)?;
1035 let event = ShellEvent::TextPreedit {
1036 window,
1037 time: self.now(),
1038 text: text.to_string(),
1039 cursor,
1040 };
1041 self.queue.push_back(event);
1042 Ok(())
1043 }
1044
1045 pub fn drop_file(
1053 &mut self,
1054 window: WindowId,
1055 path: impl Into<PathBuf>,
1056 position: Option<PhysicalPoint>,
1057 ) -> Result<(), ShellError> {
1058 if !self.caps.contains(ShellCaps::DRAG_DROP) {
1059 return Err(self.unsupported("drag and drop"));
1060 }
1061 if !self.window(window)?.accept_drops {
1062 return Err(ShellError::Unsupported {
1063 backend: ShellBackend::Headless,
1064 what: "drops on a window created without accept_drops",
1065 });
1066 }
1067 let event = ShellEvent::DroppedFile {
1068 window,
1069 time: self.now(),
1070 path: path.into(),
1071 position,
1072 };
1073 self.queue.push_back(event);
1074 Ok(())
1075 }
1076
1077 pub fn plug_monitor(&mut self, mut info: MonitorInfo) -> MonitorId {
1084 let id = MonitorId(self.next_monitor_id);
1085 self.next_monitor_id += 1;
1086 info.id = id;
1087 self.monitors.push(info);
1088 self.queue.push_back(ShellEvent::MonitorsChanged);
1089 id
1090 }
1091
1092 pub fn unplug_monitor(&mut self, id: MonitorId) -> Result<(), ShellError> {
1106 let index = self
1107 .monitors
1108 .iter()
1109 .position(|monitor| monitor.id == id)
1110 .ok_or(ShellError::NoSuchMonitor(id.0))?;
1111 assert!(
1112 self.monitors.len() > 1,
1113 "a shell needs at least one monitor"
1114 );
1115 let previous = self.monitors.clone();
1116 self.monitors.remove(index);
1117 crate::monitor::queue_removed(&previous, &self.monitors, &mut self.queue);
1118 let panels: Vec<WindowId> = self
1119 .windows
1120 .iter()
1121 .filter(|(_, state)| {
1122 state.panel.as_ref().and_then(|panel| panel.desc().monitor) == Some(id)
1123 && !state.close_pending
1124 })
1125 .map(|(window, _)| window.cast())
1126 .collect();
1127 for panel in panels {
1128 self.request_close(panel)?;
1129 }
1130 self.queue.push_back(ShellEvent::MonitorsChanged);
1131 Ok(())
1132 }
1133
1134 pub fn title(&self, window: WindowId) -> Result<&str, ShellError> {
1145 Ok(self.window(window)?.title.as_str())
1146 }
1147
1148 pub fn cursor(&self, window: WindowId) -> Result<Option<CursorIcon>, ShellError> {
1156 Ok(self.window(window)?.cursor)
1157 }
1158
1159 pub fn text_input(&self, window: WindowId) -> Result<Option<TextPurpose>, ShellError> {
1168 Ok(self.window(window)?.text_input)
1169 }
1170
1171 pub fn text_input_area(&self, window: WindowId) -> Result<Option<PhysicalRect>, ShellError> {
1181 Ok(self.window(window)?.text_input_area)
1182 }
1183
1184 pub fn panel(&self, window: WindowId) -> Result<Option<crate::PanelDesc<'_>>, ShellError> {
1193 Ok(self
1194 .window(window)?
1195 .panel
1196 .as_ref()
1197 .map(crate::role::KeptPanel::desc))
1198 }
1199
1200 pub fn input_region(&self, window: WindowId) -> Result<Option<&[PhysicalRect]>, ShellError> {
1211 Ok(self.window(window)?.input_region.as_deref())
1212 }
1213
1214 pub fn frame_requested(&self, window: WindowId) -> Result<bool, ShellError> {
1224 self.window(window)?;
1225 Ok(self.frames.contains(window))
1226 }
1227
1228 pub fn idle_inhibited(&self, window: WindowId) -> Result<bool, ShellError> {
1239 Ok(self.window(window)?.idle_inhibit)
1240 }
1241
1242 #[must_use]
1250 pub fn opened_urls(&self) -> &[(WindowId, WebUrl)] {
1251 &self.opened_urls
1252 }
1253
1254 pub fn set_outputs_powered(&mut self, powered: bool) {
1270 self.outputs_powered = powered;
1271 }
1272
1273 #[cfg(feature = "accesskit")]
1282 pub fn activate_accessibility(&mut self, window: WindowId) -> Result<(), ShellError> {
1283 let state = &mut self.window_mut(window)?.accessibility;
1284 if state.is_none() {
1285 *state = Some(Vec::new());
1286 self.queue
1287 .push_back(ShellEvent::AccessibilityActivated { window });
1288 }
1289 Ok(())
1290 }
1291
1292 #[cfg(feature = "accesskit")]
1302 pub fn deactivate_accessibility(&mut self, window: WindowId) -> Result<(), ShellError> {
1303 if self.window_mut(window)?.accessibility.take().is_some() {
1304 self.queue
1305 .push_back(ShellEvent::AccessibilityDeactivated { window });
1306 }
1307 Ok(())
1308 }
1309
1310 #[cfg(feature = "accesskit")]
1324 pub fn request_accessibility_action(
1325 &mut self,
1326 window: WindowId,
1327 request: accesskit::ActionRequest,
1328 ) -> Result<(), ShellError> {
1329 assert!(
1330 self.window(window)?.accessibility.is_some(),
1331 "no assistive technology is reading the window"
1332 );
1333 self.queue
1334 .push_back(ShellEvent::AccessibilityAction { window, request });
1335 Ok(())
1336 }
1337
1338 #[cfg(feature = "accesskit")]
1347 pub fn accessibility_updates(
1348 &self,
1349 window: WindowId,
1350 ) -> Result<Option<&[accesskit::TreeUpdate]>, ShellError> {
1351 Ok(self.window(window)?.accessibility.as_deref())
1352 }
1353
1354 #[must_use]
1358 pub fn clipboard_bytes(&self, mime: MimeType) -> Option<&[u8]> {
1359 self.clipboard
1360 .iter()
1361 .find(|(offered, _)| offered.matches(mime))
1362 .map(|(_, bytes)| bytes.as_slice())
1363 }
1364
1365 pub fn set_clipboard_readable(&mut self, readable: bool) {
1382 self.clipboard_readable = readable;
1383 }
1384
1385 pub fn set_clipboard_stalled(&mut self, stalled: bool) {
1394 self.clipboard_stalled = stalled;
1395 }
1396
1397 pub fn require_user_interaction(&mut self, required: bool) {
1424 self.clipboard_needs_interaction = required;
1425 }
1426
1427 pub fn clear_user_interaction(&mut self) {
1432 self.saw_interaction = false;
1433 }
1434
1435 pub fn clipboard_deadline(&mut self, pumps: u32) {
1441 self.clipboard_deadline = pumps;
1442 }
1443
1444 #[must_use]
1449 pub fn held_clipboard_reads(&self) -> usize {
1450 self.reads.len()
1451 }
1452
1453 pub fn set_foreign_clipboard(&mut self, offers: Vec<(ReceivedMime, Vec<u8>)>) {
1461 self.clipboard = offers;
1462 }
1463
1464 fn answerable(&self) -> bool {
1470 self.clipboard_readable && !self.clipboard_stalled
1471 }
1472
1473 fn answer(&self, window: WindowId, request: ClipboardRequestId, mime: MimeType) -> ShellEvent {
1475 let found = self
1480 .clipboard
1481 .iter()
1482 .find(|(offered, _)| offered.matches(mime));
1483 let (mime, content) = match found {
1484 Some((offered, bytes)) => (offered.clone(), ClipboardContent::Bytes(bytes.clone())),
1485 None => (ReceivedMime::from(mime), ClipboardContent::Empty),
1488 };
1489 ShellEvent::ClipboardData {
1490 window,
1491 request,
1492 mime,
1493 content,
1494 }
1495 }
1496
1497 fn resolve_reads(&mut self) {
1504 if self.reads.is_empty() {
1505 return;
1506 }
1507 let answerable = self.answerable();
1508 let mut held = Vec::with_capacity(self.reads.len());
1509 for mut read in core::mem::take(&mut self.reads) {
1510 if self.window(read.window).is_err() {
1514 continue;
1515 }
1516 if answerable {
1517 let answer = self.answer(read.window, read.request, read.mime);
1518 self.queue.push_back(answer);
1519 continue;
1520 }
1521 if read.countdown > 0 {
1522 read.countdown -= 1;
1523 held.push(read);
1524 continue;
1525 }
1526 self.queue.push_back(ShellEvent::ClipboardData {
1529 window: read.window,
1530 request: read.request,
1531 mime: ReceivedMime::from(read.mime),
1532 content: ClipboardContent::Unavailable,
1533 });
1534 }
1535 self.reads = held;
1536 }
1537
1538 fn deliver_due_configures(&mut self) {
1545 let mut due: Vec<(WindowId, PendingConfigure, Option<f64>)> = Vec::new();
1549 for (handle, state) in self.windows.iter_mut() {
1550 let Some(pending) = state.pending.as_mut() else {
1551 continue;
1552 };
1553 if pending.countdown > 0 {
1554 pending.countdown -= 1;
1555 continue;
1556 }
1557 let pending = state.pending.take().expect("checked just above");
1558 let previous_scale = state.configuration.map(|config| config.scale_factor);
1559 state.configuration = Some(WindowConfiguration {
1560 size: pending.size,
1561 scale_factor: pending.scale_factor,
1562 mode: pending.mode,
1563 });
1564 due.push((handle.cast(), pending, previous_scale));
1565 }
1566
1567 for (window, pending, previous_scale) in due {
1568 if previous_scale.is_some_and(|previous| previous != pending.scale_factor) {
1572 self.queue.push_back(ShellEvent::ScaleFactorChanged {
1573 window,
1574 scale_factor: pending.scale_factor,
1575 size: pending.size,
1576 });
1577 }
1578 self.queue.push_back(ShellEvent::Resized {
1579 window,
1580 size: pending.size,
1581 scale_factor: pending.scale_factor,
1582 });
1583 }
1584 }
1585
1586 fn run_due_scripts(&mut self) {
1588 if self.scripts.is_empty() {
1589 return;
1590 }
1591 let mut due = Vec::new();
1592 for mut scheduled in core::mem::take(&mut self.scripts) {
1593 if scheduled.countdown > 0 {
1594 scheduled.countdown -= 1;
1595 self.scripts.push(scheduled);
1596 } else {
1597 due.push(scheduled.script);
1598 }
1599 }
1600 for script in due {
1601 script(self);
1602 }
1603 }
1604
1605 fn window(&self, window: WindowId) -> Result<&HeadlessWindow, ShellError> {
1606 self.windows
1607 .get(window.cast())
1608 .ok_or_else(|| ShellError::invalid_window(window))
1609 }
1610
1611 fn window_mut(&mut self, window: WindowId) -> Result<&mut HeadlessWindow, ShellError> {
1612 self.windows
1613 .get_mut(window.cast())
1614 .ok_or_else(|| ShellError::invalid_window(window))
1615 }
1616
1617 fn check_window(&self, window: WindowId) -> Result<(), ShellError> {
1618 self.window(window).map(|_| ())
1619 }
1620
1621 fn unsupported(&self, what: &'static str) -> ShellError {
1622 ShellError::Unsupported {
1623 backend: ShellBackend::Headless,
1624 what,
1625 }
1626 }
1627}
1628
1629fn synthetic_scancode(key: KeyCode) -> Scancode {
1633 let index = KeyCode::ALL
1634 .iter()
1635 .position(|candidate| *candidate == key)
1636 .unwrap_or(0);
1637 Scancode(index as u32 + 8)
1640}
1641
1642fn synthetic_keysym(key: KeyCode) -> Keysym {
1644 let name = key.as_str();
1645 if let Some(letter) = name.strip_prefix("Key") {
1646 return Keysym::from_char(letter.to_ascii_lowercase().chars().next().unwrap_or('?'));
1648 }
1649 if let Some(digit) = name.strip_prefix("Digit") {
1650 return Keysym::from_char(digit.chars().next().unwrap_or('?'));
1651 }
1652 match key {
1653 KeyCode::Space => Keysym::from_char(' '),
1654 KeyCode::Minus => Keysym::from_char('-'),
1655 KeyCode::Equal => Keysym::from_char('='),
1656 KeyCode::Comma => Keysym::from_char(','),
1657 KeyCode::Period => Keysym::from_char('.'),
1658 KeyCode::Slash => Keysym::from_char('/'),
1659 KeyCode::Semicolon => Keysym::from_char(';'),
1660 KeyCode::Quote => Keysym::from_char('\''),
1661 KeyCode::Backslash => Keysym::from_char('\\'),
1662 KeyCode::BracketLeft => Keysym::from_char('['),
1663 KeyCode::BracketRight => Keysym::from_char(']'),
1664 KeyCode::Backquote => Keysym::from_char('`'),
1665 _ => crate::keysym::named_keysym(key).unwrap_or(Keysym::NONE),
1669 }
1670}
1671
1672impl Shell for HeadlessShell {
1673 fn backend(&self) -> ShellBackend {
1674 ShellBackend::Headless
1675 }
1676
1677 fn caps(&self) -> ShellCaps {
1678 self.caps
1679 }
1680
1681 fn create_window(&mut self, desc: &WindowDesc<'_>) -> Result<WindowId, ShellError> {
1682 crate::role::check(desc, self.caps(), self.backend(), |monitor| {
1683 self.monitor(monitor).is_some()
1684 })?;
1685 if !self.windows.is_empty() && !self.caps.contains(ShellCaps::MULTI_WINDOW) {
1686 return Err(self.unsupported("a second window"));
1687 }
1688 if let DisplayMode::Borderless {
1689 monitor: Some(monitor),
1690 } = desc.mode
1691 && self.monitor(monitor).is_none()
1692 {
1693 return Err(ShellError::NoSuchMonitor(monitor.0));
1694 }
1695
1696 let (size, scale_factor) = match desc.mode {
1708 DisplayMode::Windowed => {
1709 let scale_factor = self.monitors[0].scale_factor;
1710 (
1711 desc.constraints
1712 .apply(desc.size.to_physical(scale_factor), scale_factor),
1713 scale_factor,
1714 )
1715 }
1716 DisplayMode::Borderless { monitor } => {
1717 let monitor = monitor
1718 .and_then(|id| self.monitor(id))
1719 .unwrap_or(&self.monitors[0]);
1720 (monitor.size(), monitor.scale_factor)
1721 }
1722 };
1723
1724 let (parent, popup_input, panel) = match desc.role {
1725 crate::WindowRole::Popup(popup) => {
1726 self.window(popup.parent)?;
1727 (Some(popup.parent), Some(popup.input), None)
1728 }
1729 crate::WindowRole::Panel(panel) => {
1730 (None, None, Some(crate::role::KeptPanel::new(&panel)))
1731 }
1732 crate::WindowRole::Toplevel => (None, None, None),
1733 };
1734 let countdown = self.configure_delay;
1735 let window: WindowId = self
1736 .windows
1737 .insert(HeadlessWindow {
1738 title: desc.title.to_string(),
1739 configuration: None,
1742 pending: Some(PendingConfigure {
1743 size,
1744 scale_factor,
1745 mode: desc.mode,
1746 countdown,
1747 }),
1748 requested_mode: desc.mode,
1749 requested_constraints: desc.constraints,
1750 focused: false,
1751 visible: desc.visible,
1752 pointer_mode: PointerMode::Free,
1753 cursor: Some(CursorIcon::Default),
1754 close_pending: false,
1755 accept_drops: desc.accept_drops,
1756 parent,
1757 popup_input,
1758 panel,
1759 text_input: None,
1760 text_input_area: None,
1761 input_region: None,
1762 idle_inhibit: false,
1763 #[cfg(feature = "accesskit")]
1764 accessibility: None,
1765 })
1766 .cast();
1767 Ok(window)
1768 }
1769
1770 fn destroy_window(&mut self, window: WindowId) -> Result<(), ShellError> {
1774 self.window(window)?;
1775 let popups: Vec<WindowId> = self
1776 .windows
1777 .iter()
1778 .filter(|(_, state)| state.parent == Some(window))
1779 .map(|(id, _)| id.cast())
1780 .collect();
1781 for popup in popups {
1782 self.destroy_window(popup)?;
1783 }
1784 self.windows
1785 .remove(window.cast())
1786 .ok_or_else(|| ShellError::invalid_window(window))?;
1787 self.queue.push_back(ShellEvent::WindowDestroyed { window });
1788 Ok(())
1789 }
1790
1791 fn window_state(&self, window: WindowId) -> Result<WindowState, ShellError> {
1792 let state = self.window(window)?;
1793 Ok(WindowState {
1794 configuration: state.configuration,
1795 requested_mode: state.requested_mode,
1796 requested_constraints: state.requested_constraints,
1797 focused: state.focused,
1798 visible: state.visible,
1799 pointer_mode: state.pointer_mode,
1800 close_pending: state.close_pending,
1801 })
1802 }
1803
1804 fn set_title(&mut self, window: WindowId, title: &str) -> Result<(), ShellError> {
1805 self.window_mut(window)?.title = title.to_string();
1806 Ok(())
1807 }
1808
1809 fn set_visible(&mut self, window: WindowId, visible: bool) -> Result<(), ShellError> {
1810 self.window_mut(window)?.visible = visible;
1811 Ok(())
1812 }
1813
1814 fn set_mode(&mut self, window: WindowId, mode: DisplayMode) -> Result<(), ShellError> {
1815 let target = match mode {
1816 DisplayMode::Windowed => None,
1817 DisplayMode::Borderless { monitor } => {
1818 let monitor = match monitor {
1819 Some(id) => self
1820 .monitor(id)
1821 .ok_or(ShellError::NoSuchMonitor(id.0))?
1822 .clone(),
1823 None => self.monitors[0].clone(),
1824 };
1825 Some(monitor)
1826 }
1827 };
1828 let countdown = self.configure_delay;
1829
1830 let state = self.window_mut(window)?;
1831 state.requested_mode = mode;
1837 let (size, scale_factor) = match &target {
1838 Some(monitor) => (monitor.size(), monitor.scale_factor),
1839 None => (state.base_size(), state.scale_factor()),
1843 };
1844 state.pending = Some(PendingConfigure {
1845 size,
1846 scale_factor,
1847 mode,
1848 countdown,
1849 });
1850 Ok(())
1851 }
1852
1853 fn set_constraints(
1854 &mut self,
1855 window: WindowId,
1856 constraints: SizeConstraints,
1857 ) -> Result<(), ShellError> {
1858 let countdown = self.configure_delay;
1859 let state = self.window_mut(window)?;
1860 state.requested_constraints = constraints;
1861
1862 if let Some(pending) = state.pending.as_mut() {
1869 pending.size = constraints.apply(pending.size, pending.scale_factor);
1870 return Ok(());
1871 }
1872
1873 if let Some(config) = state.configuration {
1877 let constrained = constraints.apply(config.size, config.scale_factor);
1878 if constrained != config.size {
1879 state.pending = Some(PendingConfigure {
1880 size: constrained,
1881 scale_factor: config.scale_factor,
1882 mode: config.mode,
1883 countdown,
1884 });
1885 }
1886 }
1887 Ok(())
1888 }
1889
1890 fn reposition_popup(
1894 &mut self,
1895 popup: WindowId,
1896 anchor: LogicalRect,
1897 size: LogicalSize,
1898 ) -> Result<(), ShellError> {
1899 let _ = anchor;
1900 let countdown = self.configure_delay;
1901 let state = self.window_mut(popup)?;
1902 if state.parent.is_none() {
1903 return Err(crate::role::not_a_popup(popup));
1904 }
1905 state.configure_at(size, countdown);
1906 Ok(())
1907 }
1908
1909 fn set_panel(
1913 &mut self,
1914 window: WindowId,
1915 size: LogicalSize,
1916 panel: &crate::PanelDesc<'_>,
1917 ) -> Result<(), ShellError> {
1918 let was = self.window(window)?.panel.as_ref().map(|kept| kept.desc());
1919 crate::role::check_set_panel(window, was, panel, self.caps, self.backend(), |monitor| {
1920 self.monitor(monitor).is_some()
1921 })?;
1922 let countdown = self.configure_delay;
1923 let state = self.window_mut(window)?;
1924 state.panel = Some(crate::role::KeptPanel::new(panel));
1925 state.configure_at(size, countdown);
1926 Ok(())
1927 }
1928
1929 fn monitors(&self) -> &[MonitorInfo] {
1930 &self.monitors
1931 }
1932
1933 fn pump(&mut self, sink: &mut dyn FnMut(ShellEvent)) {
1934 self.keep_alive();
1935 if self.outputs_powered {
1936 let windows = &self.windows;
1937 self.queue.extend(
1938 self.frames
1939 .take_due(|window| windows.contains(window.cast())),
1940 );
1941 }
1942 for _ in 0..self.queue.len() {
1947 let Some(event) = self.queue.pop_front() else {
1948 break;
1949 };
1950 sink(event);
1951 }
1952 }
1953
1954 fn keep_alive(&mut self) {
1963 self.deliver_due_configures();
1964 self.resolve_reads();
1965 self.run_due_scripts();
1966 }
1967
1968 fn wait_events(&mut self, timeout: Option<Duration>) {
1989 self.waits += 1;
1990 if !self.caps.contains(ShellCaps::EVENT_WAIT)
1991 || !self.queue.is_empty()
1992 || !self.scripts.is_empty()
1993 || (self.outputs_powered && self.frames.is_pending())
1994 {
1995 return;
1996 }
1997 match (&self.wake, timeout) {
1998 (Some(wake), timeout) => wake.wait(timeout),
1999 (None, Some(timeout)) => std::thread::sleep(timeout),
2000 (None, None) => {}
2001 }
2002 }
2003
2004 fn waker(&mut self) -> Option<ShellWaker> {
2007 let wake = self.wake.get_or_insert_with(Arc::default);
2008 Some(ShellWaker::new(Arc::clone(wake) as Arc<dyn WakeSignal>))
2009 }
2010
2011 fn align_event_clock(&mut self, elapsed: Duration) {
2018 self.set_time(elapsed);
2019 }
2020
2021 fn surface_target(&self, window: WindowId) -> Result<SurfaceTarget, ShellError> {
2022 self.window(window)?;
2028 Ok(SurfaceTarget::Offscreen)
2029 }
2030
2031 fn set_pointer_mode(&mut self, window: WindowId, mode: PointerMode) -> Result<(), ShellError> {
2032 if !self.caps.contains(mode.required_cap()) {
2033 return Err(self.unsupported(mode.as_str()));
2034 }
2035 self.window_mut(window)?.pointer_mode = mode;
2036 Ok(())
2037 }
2038
2039 fn set_text_input(
2043 &mut self,
2044 window: WindowId,
2045 field: Option<TextPurpose>,
2046 ) -> Result<(), ShellError> {
2047 self.window_mut(window)?.text_input = field;
2048 Ok(())
2049 }
2050
2051 fn set_text_input_area(
2055 &mut self,
2056 window: WindowId,
2057 area: Option<PhysicalRect>,
2058 ) -> Result<(), ShellError> {
2059 self.window_mut(window)?.text_input_area = area;
2060 Ok(())
2061 }
2062
2063 fn set_input_region(
2068 &mut self,
2069 window: WindowId,
2070 region: Option<&[PhysicalRect]>,
2071 ) -> Result<(), ShellError> {
2072 self.window_mut(window)?.input_region = region.map(<[PhysicalRect]>::to_vec);
2073 Ok(())
2074 }
2075
2076 fn set_idle_inhibit(&mut self, window: WindowId, inhibit: bool) -> Result<(), ShellError> {
2081 if !self.caps.contains(ShellCaps::IDLE_INHIBIT) {
2082 return Err(self.unsupported("idle inhibit"));
2083 }
2084 self.window_mut(window)?.idle_inhibit = inhibit;
2085 Ok(())
2086 }
2087
2088 fn open_url(&mut self, window: WindowId, url: &WebUrl) -> Result<(), ShellError> {
2093 if !self.caps.contains(ShellCaps::OPEN_URL) {
2094 return Err(self.unsupported("opening a URL"));
2095 }
2096 self.check_window(window)?;
2097 self.opened_urls.push((window, url.clone()));
2098 Ok(())
2099 }
2100
2101 fn request_frame(&mut self, window: WindowId) -> Result<(), ShellError> {
2106 self.window(window)?;
2107 self.frames.request(window);
2108 Ok(())
2109 }
2110
2111 #[cfg(feature = "accesskit")]
2112 fn update_accessibility(
2113 &mut self,
2114 window: WindowId,
2115 update: accesskit::TreeUpdate,
2116 ) -> Result<(), ShellError> {
2117 if let Some(updates) = &mut self.window_mut(window)?.accessibility {
2118 updates.push(update);
2119 }
2120 Ok(())
2121 }
2122
2123 fn set_cursor(
2124 &mut self,
2125 window: WindowId,
2126 cursor: Option<CursorIcon>,
2127 ) -> Result<(), ShellError> {
2128 self.window_mut(window)?.cursor = cursor;
2129 Ok(())
2130 }
2131
2132 fn warp_pointer(
2133 &mut self,
2134 window: WindowId,
2135 position: PhysicalPoint,
2136 ) -> Result<(), ShellError> {
2137 if !self.caps.contains(ShellCaps::POINTER_WARP) {
2138 return Err(self.unsupported("pointer warp"));
2139 }
2140 self.check_window(window)?;
2141 let event = ShellEvent::PointerMotion {
2144 window,
2145 device: VIRTUAL_DEVICE,
2146 time: self.now(),
2147 abs: Some(position),
2148 raw_delta: None,
2149 };
2150 self.queue.push_back(event);
2151 Ok(())
2152 }
2153
2154 fn reply_close_request(
2155 &mut self,
2156 window: WindowId,
2157 reply: CloseReply,
2158 ) -> Result<(), ShellError> {
2159 let state = self.window_mut(window)?;
2160 if !state.close_pending {
2161 return Err(ShellError::NoPendingCloseRequest { window });
2162 }
2163 state.close_pending = false;
2164 match reply {
2165 CloseReply::Close => self.destroy_window(window),
2166 CloseReply::Keep => Ok(()),
2167 }
2168 }
2169
2170 fn clipboard_offer(
2181 &mut self,
2182 window: WindowId,
2183 offers: &[ClipboardOffer<'_>],
2184 ) -> Result<(), ShellError> {
2185 if !self.caps.contains(ShellCaps::CLIPBOARD) {
2186 return Err(self.unsupported("clipboard"));
2187 }
2188 self.check_window(window)?;
2189 if self.clipboard_needs_interaction && !self.saw_interaction {
2190 return Err(ShellError::NeedsUserInteraction {
2191 backend: ShellBackend::Headless,
2192 what: "claiming the clipboard",
2193 });
2194 }
2195 self.clipboard = offers
2196 .iter()
2197 .map(|offer| (ReceivedMime::from(offer.mime), offer.bytes.to_vec()))
2198 .collect();
2199 Ok(())
2200 }
2201
2202 fn clipboard_request(
2216 &mut self,
2217 window: WindowId,
2218 mime: MimeType,
2219 ) -> Result<ClipboardRequestId, ShellError> {
2220 if !self.caps.contains(ShellCaps::CLIPBOARD) {
2221 return Err(self.unsupported("clipboard"));
2222 }
2223 self.check_window(window)?;
2224 let request = ClipboardRequestId(self.next_request_id);
2225 self.next_request_id += 1;
2226 if self.answerable() {
2227 let answer = self.answer(window, request, mime);
2228 self.queue.push_back(answer);
2232 } else {
2233 self.reads.push(PendingRead {
2234 window,
2235 request,
2236 mime,
2237 countdown: self.clipboard_deadline,
2238 });
2239 }
2240 Ok(request)
2241 }
2242
2243 fn clipboard_readable(&self, window: WindowId) -> bool {
2249 self.caps.contains(ShellCaps::CLIPBOARD) && self.window(window).is_ok() && self.answerable()
2250 }
2251}
2252
2253#[cfg(test)]
2254mod tests {
2255 use super::*;
2256 use crate::{AspectRatio, LogicalSize};
2257
2258 fn shell_with_window() -> (HeadlessShell, WindowId) {
2260 let mut shell = HeadlessShell::new();
2261 let window = shell
2262 .create_window(&WindowDesc::default())
2263 .expect("headless window creation never fails");
2264 (shell, window)
2265 }
2266
2267 fn shell_with_configured_window() -> (HeadlessShell, WindowId) {
2269 let (mut shell, window) = shell_with_window();
2270 settle(&mut shell);
2271 assert!(shell.window_state(window).expect("state").is_configured());
2272 (shell, window)
2273 }
2274
2275 fn settle(shell: &mut HeadlessShell) {
2277 for _ in 0..8 {
2278 let mut seen = 0;
2279 shell.pump(&mut |_| seen += 1);
2280 if seen == 0 && shell.windows.iter().all(|(_, w)| w.pending.is_none()) {
2281 return;
2282 }
2283 }
2284 panic!("the shell never settled");
2285 }
2286
2287 fn drain(shell: &mut HeadlessShell) -> Vec<ShellEvent> {
2288 let mut events = Vec::new();
2289 shell.pump(&mut |event| events.push(event));
2290 events
2291 }
2292
2293 fn names(events: &[ShellEvent]) -> Vec<&'static str> {
2294 events.iter().map(ShellEvent::name).collect()
2295 }
2296
2297 fn create_swapchain(state: &WindowState) -> Result<PhysicalSize, &'static str> {
2300 let size = state.size().ok_or("window is not configured yet")?;
2301 if size.is_empty() {
2302 return Err("zero-extent swapchain");
2303 }
2304 Ok(size)
2305 }
2306
2307 #[test]
2312 fn a_backend_without_activation_answers_none() {
2313 let (mut shell, window) = shell_with_window();
2314 assert_eq!(shell.activation_token(window).expect("a live window"), None);
2315 shell.destroy_window(window).expect("destroy");
2316 assert!(matches!(
2317 shell.activation_token(window),
2318 Err(ShellError::InvalidWindow { .. })
2319 ));
2320 }
2321
2322 #[cfg(feature = "accesskit")]
2327 #[test]
2328 fn a_tree_reaches_the_reader_only_while_it_reads() {
2329 use accesskit::{Action, ActionRequest, Node, NodeId, Role, TreeId, TreeInfo, TreeUpdate};
2330
2331 let (mut shell, window) = shell_with_configured_window();
2332 let update = |label: &str| TreeUpdate {
2333 nodes: vec![(NodeId(1), {
2334 let mut node = Node::new(Role::Window);
2335 node.set_label(label);
2336 node
2337 })],
2338 tree: Some(TreeInfo::new(NodeId(1))),
2339 tree_id: TreeId::ROOT,
2340 focus: NodeId(1),
2341 };
2342
2343 shell
2344 .update_accessibility(window, update("unread"))
2345 .expect("update");
2346 assert_eq!(shell.accessibility_updates(window).expect("updates"), None);
2347
2348 shell.activate_accessibility(window).expect("activate");
2349 shell
2350 .activate_accessibility(window)
2351 .expect("activate twice");
2352 shell
2353 .update_accessibility(window, update("read"))
2354 .expect("update");
2355 let click = ActionRequest {
2356 action: Action::Click,
2357 target_tree: TreeId::ROOT,
2358 target_node: NodeId(1),
2359 data: None,
2360 };
2361 shell
2362 .request_accessibility_action(window, click.clone())
2363 .expect("action");
2364 assert_eq!(
2365 shell.accessibility_updates(window).expect("updates"),
2366 Some(&[update("read")][..])
2367 );
2368 let events = drain(&mut shell);
2369 assert_eq!(
2370 names(&events),
2371 ["AccessibilityActivated", "AccessibilityAction"]
2372 );
2373 assert_eq!(
2374 events[1],
2375 ShellEvent::AccessibilityAction {
2376 window,
2377 request: click
2378 }
2379 );
2380
2381 shell.deactivate_accessibility(window).expect("deactivate");
2382 shell
2383 .deactivate_accessibility(window)
2384 .expect("deactivate twice");
2385 shell
2386 .update_accessibility(window, update("gone"))
2387 .expect("update");
2388 assert_eq!(shell.accessibility_updates(window).expect("updates"), None);
2389 assert_eq!(names(&drain(&mut shell)), ["AccessibilityDeactivated"]);
2390 }
2391
2392 #[test]
2395 fn a_new_window_has_no_size_until_a_later_pump() {
2396 let (mut shell, window) = shell_with_window();
2397
2398 let state = shell.window_state(window).expect("state");
2401 assert!(!state.is_configured());
2402 assert_eq!(state.size(), None);
2403 assert_eq!(state.scale_factor(), None);
2404 assert_eq!(state.effective_mode(), None);
2405 assert_eq!(state.requested_mode, DisplayMode::Windowed);
2406
2407 assert_eq!(drain(&mut shell), Vec::new());
2409 assert!(!shell.window_state(window).expect("state").is_configured());
2410
2411 let events = drain(&mut shell);
2413 assert_eq!(
2414 events,
2415 vec![ShellEvent::Resized {
2416 window,
2417 size: PhysicalSize::new(1280, 720),
2418 scale_factor: 1.0,
2419 }]
2420 );
2421 let state = shell.window_state(window).expect("state");
2422 assert_eq!(state.size(), Some(PhysicalSize::new(1280, 720)));
2423 assert_eq!(state.scale_factor(), Some(1.0));
2424 assert_eq!(state.effective_mode(), Some(DisplayMode::Windowed));
2425 assert!(state.mode_request_honoured());
2426 }
2427
2428 #[test]
2432 fn keep_alive_takes_a_turn_and_keeps_everything_for_the_next_pump() {
2433 let (mut shell, window) = shell_with_window();
2434 shell.inject(ShellEvent::CloseRequested { window });
2435
2436 for _ in 0..=DEFAULT_CONFIGURE_DELAY {
2438 shell.keep_alive();
2439 }
2440 assert!(
2441 shell.window_state(window).expect("state").is_configured(),
2442 "every turn the configure waits for was taken by keep_alive"
2443 );
2444
2445 assert_eq!(
2446 drain(&mut shell),
2447 vec![
2448 ShellEvent::CloseRequested { window },
2449 ShellEvent::Resized {
2450 window,
2451 size: PhysicalSize::new(1280, 720),
2452 scale_factor: 1.0,
2453 },
2454 ],
2455 "nothing keep_alive saw was delivered or dropped"
2456 );
2457 assert_eq!(drain(&mut shell), Vec::new(), "and each arrived once");
2458 }
2459
2460 #[test]
2461 fn a_consumer_that_skips_the_wait_gets_a_clean_error_not_a_wrong_extent() {
2462 let (mut shell, window) = shell_with_window();
2463
2464 assert_eq!(
2466 create_swapchain(&shell.window_state(window).expect("state")),
2467 Err("window is not configured yet")
2468 );
2469
2470 let target = shell.surface_target(window).expect("the handle exists");
2476 assert_eq!(target, SurfaceTarget::Offscreen);
2477
2478 let size = loop {
2480 shell.pump(&mut |_| {});
2481 if let Ok(size) = create_swapchain(&shell.window_state(window).expect("state")) {
2482 break size;
2483 }
2484 };
2485 assert_eq!(size, PhysicalSize::new(1280, 720));
2486 assert_eq!(
2489 shell.surface_target(window).expect("target"),
2490 target,
2491 "the target is invariant under a configure"
2492 );
2493 }
2494
2495 #[test]
2496 fn the_configure_delay_is_a_knob() {
2497 let mut shell = HeadlessShell::new().with_configure_delay(3);
2498 let window = shell.create_window(&WindowDesc::default()).expect("window");
2499 for pump in 0..3 {
2500 assert_eq!(drain(&mut shell), Vec::new(), "pump {pump}");
2501 assert!(!shell.window_state(window).expect("state").is_configured());
2502 }
2503 assert_eq!(names(&drain(&mut shell)), ["Resized"]);
2504 assert!(shell.window_state(window).expect("state").is_configured());
2505
2506 let mut shell = HeadlessShell::new().with_configure_delay(0);
2509 let window = shell.create_window(&WindowDesc::default()).expect("window");
2510 assert_eq!(names(&drain(&mut shell)), ["Resized"]);
2511 assert!(shell.window_state(window).expect("state").is_configured());
2512
2513 shell.set_configure_delay(2);
2514 shell
2515 .set_mode(window, DisplayMode::Borderless { monitor: None })
2516 .expect("request");
2517 assert_eq!(drain(&mut shell), Vec::new());
2518 assert_eq!(drain(&mut shell), Vec::new());
2519 assert_eq!(names(&drain(&mut shell)), ["Resized"]);
2520 }
2521
2522 #[test]
2523 fn set_mode_is_a_request_whose_effect_arrives_later() {
2524 let (mut shell, window) = shell_with_configured_window();
2525 shell
2526 .set_mode(window, DisplayMode::Borderless { monitor: None })
2527 .expect("request");
2528
2529 let state = shell.window_state(window).expect("state");
2531 assert_eq!(
2532 state.requested_mode,
2533 DisplayMode::Borderless { monitor: None }
2534 );
2535 assert_eq!(
2536 state.effective_mode(),
2537 Some(DisplayMode::Windowed),
2538 "still windowed until the window system says otherwise"
2539 );
2540 assert!(
2541 !state.mode_request_honoured(),
2542 "a UI toggle reading the request instead of the effect would lie here"
2543 );
2544 assert_eq!(state.size(), Some(PhysicalSize::new(1280, 720)));
2545
2546 settle(&mut shell);
2547 let state = shell.window_state(window).expect("state");
2548 assert_eq!(
2549 state.effective_mode(),
2550 Some(DisplayMode::Borderless { monitor: None })
2551 );
2552 assert!(state.mode_request_honoured());
2553 assert_eq!(state.size(), Some(PhysicalSize::new(1920, 1080)));
2554 }
2555
2556 #[test]
2557 fn a_refused_mode_is_expressible_through_injection() {
2558 let (mut shell, window) = shell_with_configured_window();
2561 shell
2562 .set_mode(window, DisplayMode::Borderless { monitor: None })
2563 .expect("request");
2564 shell
2566 .resize(window, PhysicalSize::new(1280, 720))
2567 .expect("answer");
2568 let state = shell.window_state(window).expect("state");
2569 assert_eq!(state.effective_mode(), Some(DisplayMode::Windowed));
2570 assert!(!state.mode_request_honoured());
2571 }
2572
2573 #[test]
2579 fn a_reconfigured_panel_is_configured_at_its_new_size() {
2580 let mut shell = HeadlessShell::new();
2581 let monitor = shell.monitors()[0].id;
2582 let bar = crate::PanelDesc {
2583 namespace: "bar",
2584 monitor: Some(monitor),
2585 anchor: crate::PanelAnchor::TOP | crate::PanelAnchor::LEFT | crate::PanelAnchor::RIGHT,
2586 reserve: crate::Reserve::OwnExtent,
2587 ..crate::PanelDesc::default()
2588 };
2589 let window = shell
2590 .create_window(&WindowDesc {
2591 size: LogicalSize::new(400.0, 30.0),
2592 role: crate::WindowRole::Panel(bar),
2593 ..WindowDesc::default()
2594 })
2595 .expect("a panel");
2596 settle(&mut shell);
2597 let scale = shell
2598 .window_state(window)
2599 .expect("live")
2600 .scale_factor()
2601 .expect("configured");
2602
2603 let taller = crate::PanelDesc {
2604 layer: crate::PanelLayer::Overlay,
2605 margins: crate::Margins {
2606 top: 4.0,
2607 ..crate::Margins::default()
2608 },
2609 keyboard: crate::PanelKeyboard::OnDemand,
2610 ..bar
2611 };
2612 let size = LogicalSize::new(400.0, 50.0);
2613 shell.set_panel(window, size, &taller).expect("set_panel");
2614 assert_eq!(shell.panel(window).expect("live"), Some(taller));
2615 assert_eq!(
2616 shell.window_state(window).expect("live").size(),
2617 Some(LogicalSize::new(400.0, 30.0).to_physical(scale)),
2618 "a request: nothing changes before the window system answers"
2619 );
2620 let mut resized = Vec::new();
2621 for _ in 0..8 {
2622 resized.extend(
2623 drain(&mut shell)
2624 .into_iter()
2625 .filter_map(|event| match event {
2626 ShellEvent::Resized {
2627 window: of, size, ..
2628 } if of == window => Some(size),
2629 _ => None,
2630 }),
2631 );
2632 }
2633 assert_eq!(resized, [size.to_physical(scale)]);
2634
2635 let refused = |shell: &mut HeadlessShell, panel: &crate::PanelDesc<'_>| match shell
2636 .set_panel(window, LogicalSize::new(1.0, 1.0), panel)
2637 {
2638 Err(ShellError::InvalidDescriptor(message)) => message,
2639 other => panic!("expected InvalidDescriptor, got {other:?}"),
2640 };
2641 let renamed = crate::PanelDesc {
2642 namespace: "dock",
2643 ..taller
2644 };
2645 assert!(refused(&mut shell, &renamed).contains("namespace"));
2646 let anywhere = crate::PanelDesc {
2647 monitor: None,
2648 ..taller
2649 };
2650 assert!(refused(&mut shell, &anywhere).contains("monitor"));
2651 let toplevel = shell
2652 .create_window(&WindowDesc::default())
2653 .expect("a toplevel");
2654 assert!(matches!(
2655 shell.set_panel(toplevel, size, &taller),
2656 Err(ShellError::InvalidDescriptor(message)) if message.contains("not a panel")
2657 ));
2658 let cornered = crate::PanelDesc {
2659 anchor: crate::PanelAnchor::TOP | crate::PanelAnchor::LEFT,
2660 ..taller
2661 };
2662 assert!(matches!(
2663 shell.set_panel(window, size, &cornered),
2664 Err(ShellError::Unsupported { .. })
2665 ));
2666 assert_eq!(
2667 shell.panel(window).expect("live"),
2668 Some(taller),
2669 "a refused descriptor is not recorded"
2670 );
2671 assert_eq!(shell.panel(toplevel).expect("live"), None);
2672
2673 shell.destroy_window(window).expect("destroy");
2674 assert!(matches!(
2675 shell.set_panel(window, size, &taller),
2676 Err(ShellError::InvalidWindow { .. })
2677 ));
2678 }
2679
2680 #[test]
2683 fn a_repositioned_popup_is_configured_at_its_new_size() {
2684 let (mut shell, window) = shell_with_configured_window();
2685 let anchor = crate::LogicalRect::new(
2686 crate::LogicalPoint::new(10.0, 10.0),
2687 LogicalSize::new(100.0, 20.0),
2688 );
2689 let popup = shell
2690 .create_window(&WindowDesc {
2691 size: LogicalSize::new(200.0, 150.0),
2692 role: crate::WindowRole::Popup(crate::PopupDesc {
2693 parent: window,
2694 anchor,
2695 placement: crate::PopupPlacement::Below,
2696 input: crate::PopupInput::Pointer,
2697 }),
2698 ..WindowDesc::default()
2699 })
2700 .expect("a popup");
2701 settle(&mut shell);
2702 let scale = shell
2703 .window_state(popup)
2704 .expect("live")
2705 .scale_factor()
2706 .expect("configured");
2707
2708 let moved = LogicalSize::new(240.0, 90.0);
2709 shell
2710 .reposition_popup(popup, anchor, moved)
2711 .expect("reposition");
2712 settle(&mut shell);
2713 assert_eq!(
2714 shell.window_state(popup).expect("live").size(),
2715 Some(moved.to_physical(scale))
2716 );
2717 assert!(matches!(
2718 shell.reposition_popup(window, anchor, moved),
2719 Err(ShellError::InvalidDescriptor(_))
2720 ));
2721 }
2722
2723 #[test]
2727 fn escape_on_a_grabbing_popup_is_delivered_then_asks_it_to_close() {
2728 let (mut shell, window) = shell_with_configured_window();
2729 let popup_of = |shell: &mut HeadlessShell, input| {
2730 shell
2731 .create_window(&WindowDesc {
2732 role: crate::WindowRole::Popup(crate::PopupDesc {
2733 parent: window,
2734 anchor: crate::LogicalRect::default(),
2735 placement: crate::PopupPlacement::Below,
2736 input,
2737 }),
2738 ..WindowDesc::default()
2739 })
2740 .expect("a popup")
2741 };
2742 let menu = popup_of(&mut shell, crate::PopupInput::Grab);
2743 let tooltip = popup_of(&mut shell, crate::PopupInput::Pointer);
2744 settle(&mut shell);
2745 let is_close = |event: &ShellEvent| matches!(event, ShellEvent::CloseRequested { .. });
2746
2747 shell.key_press(menu, KeyCode::Escape).expect("press");
2748 let events = drain(&mut shell);
2749 assert!(
2750 matches!(
2751 events.as_slice(),
2752 [
2753 ShellEvent::Key {
2754 key_code: Some(KeyCode::Escape),
2755 ..
2756 },
2757 ShellEvent::CloseRequested { window: closed },
2758 ] if *closed == menu
2759 ),
2760 "the key, then the request: {events:?}"
2761 );
2762 shell.key_press(menu, KeyCode::Escape).expect("press");
2764 shell.key_repeat(menu, KeyCode::Escape).expect("repeat");
2765 assert!(!drain(&mut shell).iter().any(is_close));
2766
2767 shell
2769 .reply_close_request(menu, CloseReply::Keep)
2770 .expect("keep");
2771 shell.key_release(menu, KeyCode::Escape).expect("release");
2772 shell.key_press(menu, KeyCode::Enter).expect("press");
2773 assert!(!drain(&mut shell).iter().any(is_close), "not these");
2774 shell.key_press(menu, KeyCode::Escape).expect("press");
2775 assert!(drain(&mut shell).iter().any(is_close));
2776
2777 shell.key_press(tooltip, KeyCode::Escape).expect("press");
2778 shell.key_press(window, KeyCode::Escape).expect("press");
2779 assert!(!drain(&mut shell).iter().any(is_close));
2780 }
2781
2782 #[test]
2783 fn set_constraints_is_a_request_too() {
2784 let (mut shell, window) = shell_with_configured_window();
2785 let constraints = SizeConstraints::min(LogicalSize::new(1600.0, 900.0));
2786 shell.set_constraints(window, constraints).expect("request");
2787
2788 let state = shell.window_state(window).expect("state");
2789 assert_eq!(state.requested_constraints, constraints);
2790 assert_eq!(
2791 state.size(),
2792 Some(PhysicalSize::new(1280, 720)),
2793 "the size only changes when the window system says so"
2794 );
2795
2796 settle(&mut shell);
2797 assert_eq!(
2798 shell.window_state(window).expect("state").size(),
2799 Some(PhysicalSize::new(1600, 900))
2800 );
2801 }
2802
2803 #[test]
2804 fn constraints_do_not_cancel_a_mode_request_that_has_not_landed() {
2805 let (mut shell, window) = shell_with_configured_window();
2810 shell
2811 .set_mode(window, DisplayMode::Borderless { monitor: None })
2812 .expect("mode request");
2813 shell
2814 .set_constraints(window, SizeConstraints::min(LogicalSize::new(320.0, 180.0)))
2815 .expect("constraint request");
2816 settle(&mut shell);
2817
2818 let state = shell.window_state(window).expect("state");
2819 assert_eq!(
2820 state.effective_mode(),
2821 Some(DisplayMode::Borderless { monitor: None })
2822 );
2823 assert!(state.mode_request_honoured());
2824 }
2825
2826 #[test]
2827 fn a_borderless_window_reports_its_own_monitors_scale() {
2828 let mut shell = HeadlessShell::new();
2833 let hidpi = shell.plug_monitor(MonitorInfo {
2834 id: MonitorId(0),
2835 name: "HEADLESS-2".to_string(),
2836 bounds: PhysicalRect::new(1920, 0, 2560, 1440),
2837 work_area: PhysicalRect::new(1920, 0, 2560, 1440),
2838 scale_factor: 2.0,
2839 refresh_millihertz: 143_998,
2840 is_primary: false,
2841 });
2842 let window = shell
2843 .create_window(&WindowDesc {
2844 mode: DisplayMode::Borderless {
2845 monitor: Some(hidpi),
2846 },
2847 ..WindowDesc::default()
2848 })
2849 .expect("window");
2850 settle(&mut shell);
2851
2852 let state = shell.window_state(window).expect("state");
2853 assert_eq!(state.size(), Some(PhysicalSize::new(2560, 1440)));
2854 assert_eq!(
2855 state.scale_factor(),
2856 Some(2.0),
2857 "the scale is the borderless monitor's, not the primary's"
2858 );
2859 let other = shell
2862 .create_window(&WindowDesc::default())
2863 .expect("second window");
2864 settle(&mut shell);
2865 shell
2866 .set_mode(
2867 other,
2868 DisplayMode::Borderless {
2869 monitor: Some(hidpi),
2870 },
2871 )
2872 .expect("request");
2873 settle(&mut shell);
2874 assert_eq!(
2875 shell.window_state(other).expect("state").scale_factor(),
2876 shell.window_state(window).expect("state").scale_factor()
2877 );
2878 }
2879
2880 #[test]
2881 fn constraints_set_before_the_first_configure_fold_into_it() {
2882 let mut shell = HeadlessShell::new();
2883 let window = shell.create_window(&WindowDesc::default()).expect("window");
2884 shell
2885 .set_constraints(
2886 window,
2887 SizeConstraints::NONE.with_aspect(AspectRatio::SQUARE),
2888 )
2889 .expect("request");
2890 settle(&mut shell);
2891 assert_eq!(
2894 shell.window_state(window).expect("state").size(),
2895 Some(PhysicalSize::new(720, 720))
2896 );
2897 }
2898
2899 #[test]
2900 fn creation_uses_the_requested_size_and_the_monitors_scale() {
2901 let mut shell = HeadlessShell::new();
2902 let window = shell
2903 .create_window(&WindowDesc {
2904 size: LogicalSize::new(800.0, 600.0),
2905 ..WindowDesc::default()
2906 })
2907 .expect("window");
2908 settle(&mut shell);
2909 assert_eq!(
2910 shell.window_state(window).expect("state").size(),
2911 Some(PhysicalSize::new(800, 600))
2912 );
2913
2914 let mut shell = HeadlessShell::new().with_scale_factor(2.0);
2915 let window = shell
2916 .create_window(&WindowDesc {
2917 size: LogicalSize::new(640.0, 360.0),
2918 ..WindowDesc::default()
2919 })
2920 .expect("window");
2921 settle(&mut shell);
2922 let state = shell.window_state(window).expect("state");
2923 assert_eq!(state.size(), Some(PhysicalSize::new(1280, 720)));
2924 assert_eq!(state.scale_factor(), Some(2.0));
2925 }
2926
2927 #[test]
2930 fn scale_changes_preserve_logical_size_and_report_both_facts() {
2931 let (mut shell, window) = shell_with_configured_window();
2932 shell
2933 .change_scale_factor(window, 1.5)
2934 .expect("scale change");
2935 let events = drain(&mut shell);
2936 assert_eq!(
2937 events,
2938 vec![
2939 ShellEvent::ScaleFactorChanged {
2940 window,
2941 scale_factor: 1.5,
2942 size: PhysicalSize::new(1920, 1080),
2943 },
2944 ShellEvent::Resized {
2947 window,
2948 size: PhysicalSize::new(1920, 1080),
2949 scale_factor: 1.5,
2950 },
2951 ]
2952 );
2953 assert_eq!(
2957 shell.window_state(window).expect("state").size(),
2958 Some(PhysicalSize::new(1920, 1080))
2959 );
2960 }
2961
2962 #[test]
2963 fn a_scale_change_scheduled_by_a_mode_switch_arrives_before_the_resize() {
2964 let mut shell = HeadlessShell::new();
2965 let hidpi = shell.plug_monitor(MonitorInfo {
2966 id: MonitorId(0),
2967 name: "HEADLESS-2".to_string(),
2968 bounds: PhysicalRect::new(1920, 0, 2560, 1440),
2969 work_area: PhysicalRect::new(1920, 0, 2560, 1440),
2970 scale_factor: 2.0,
2971 refresh_millihertz: 143_998,
2972 is_primary: false,
2973 });
2974 assert_eq!(hidpi, MonitorId(2), "ids are never reused");
2975 let window = shell.create_window(&WindowDesc::default()).expect("window");
2976 settle(&mut shell);
2977
2978 shell
2979 .set_mode(
2980 window,
2981 DisplayMode::Borderless {
2982 monitor: Some(hidpi),
2983 },
2984 )
2985 .expect("request");
2986 assert_eq!(drain(&mut shell), Vec::new(), "still only a request");
2987 let events = drain(&mut shell);
2988 assert_eq!(names(&events), ["ScaleFactorChanged", "Resized"]);
2989
2990 let state = shell.window_state(window).expect("state");
2991 assert_eq!(state.size(), Some(PhysicalSize::new(2560, 1440)));
2992 assert_eq!(state.scale_factor(), Some(2.0));
2993 assert!(state.effective_mode().expect("configured").is_borderless());
2994 assert_eq!(
2999 shell.surface_target(window).expect("target"),
3000 SurfaceTarget::Offscreen
3001 );
3002
3003 assert!(matches!(
3004 shell.set_mode(
3005 window,
3006 DisplayMode::Borderless {
3007 monitor: Some(MonitorId(99)),
3008 },
3009 ),
3010 Err(ShellError::NoSuchMonitor(99))
3011 ));
3012 }
3013
3014 #[test]
3015 fn resizes_honour_constraints_and_inject_bypasses_them() {
3016 let (mut shell, window) = shell_with_configured_window();
3017 shell
3018 .set_constraints(
3019 window,
3020 SizeConstraints::min(LogicalSize::new(640.0, 360.0))
3021 .with_aspect(AspectRatio::WIDESCREEN),
3022 )
3023 .expect("request");
3024 settle(&mut shell);
3025
3026 shell
3027 .resize(window, PhysicalSize::new(1000, 1000))
3028 .expect("resize");
3029 assert_eq!(
3030 drain(&mut shell),
3031 vec![ShellEvent::Resized {
3032 window,
3033 size: PhysicalSize::new(1000, 562),
3034 scale_factor: 1.0,
3035 }]
3036 );
3037
3038 shell.inject(ShellEvent::Resized {
3041 window,
3042 size: PhysicalSize::new(300, 900),
3043 scale_factor: 1.0,
3044 });
3045 assert_eq!(
3046 drain(&mut shell)[0],
3047 ShellEvent::Resized {
3048 window,
3049 size: PhysicalSize::new(300, 900),
3050 scale_factor: 1.0,
3051 }
3052 );
3053 }
3054
3055 #[test]
3056 fn a_compositor_configure_supersedes_a_pending_one() {
3057 let (mut shell, window) = shell_with_configured_window();
3058 shell
3059 .set_mode(window, DisplayMode::Borderless { monitor: None })
3060 .expect("request");
3061 shell
3063 .resize(window, PhysicalSize::new(640, 480))
3064 .expect("answer");
3065 assert_eq!(names(&drain(&mut shell)), ["Resized"]);
3066 assert_eq!(
3067 shell.window_state(window).expect("state").size(),
3068 Some(PhysicalSize::new(640, 480))
3069 );
3070 assert_eq!(drain(&mut shell), Vec::new());
3072 }
3073
3074 #[test]
3077 fn timestamps_come_from_the_shells_clock_not_from_the_pump() {
3078 let (mut shell, window) = shell_with_configured_window();
3079 shell.set_time(Duration::from_secs(2));
3080 shell.key_press(window, KeyCode::Space).expect("press");
3081 shell.advance(Duration::from_millis(180));
3082 shell.key_release(window, KeyCode::Space).expect("release");
3083
3084 let events = drain(&mut shell);
3085 let held = events[1]
3086 .time()
3087 .expect("input")
3088 .saturating_since(events[0].time().expect("input"));
3089 assert_eq!(held, Duration::from_millis(180));
3090 assert_eq!(events[0].time(), Some(EventTime::from_millis(2_000)));
3091 }
3092
3093 #[test]
3096 fn a_scheduled_script_injects_at_its_pump_with_that_pumps_time() {
3097 let (mut shell, window) = shell_with_configured_window();
3098 shell.schedule(2, move |shell| {
3099 shell.key_press(window, KeyCode::Space).expect("press");
3100 });
3101
3102 for pump in 0..2 {
3103 shell.advance(Duration::from_millis(16));
3104 assert_eq!(drain(&mut shell), Vec::new(), "pump {pump} came early");
3105 }
3106 shell.set_time(Duration::from_secs(3));
3107 let events = drain(&mut shell);
3108 assert_eq!(names(&events), ["Key"]);
3109 assert_eq!(events[0].time(), Some(EventTime::from_millis(3_000)));
3110 assert_eq!(drain(&mut shell), Vec::new(), "and it ran once");
3111 }
3112
3113 #[test]
3116 fn scheduled_scripts_run_in_order_and_a_nested_one_waits_a_pump() {
3117 let (mut shell, window) = shell_with_configured_window();
3118 shell.schedule(0, move |shell| {
3119 shell.key_press(window, KeyCode::KeyA).expect("press");
3120 shell.schedule(0, move |shell| {
3121 shell.key_press(window, KeyCode::KeyC).expect("press");
3122 });
3123 });
3124 shell.schedule(0, move |shell| {
3125 shell.key_press(window, KeyCode::KeyB).expect("press");
3126 });
3127
3128 let keys = |events: Vec<ShellEvent>| -> Vec<Option<KeyCode>> {
3129 events
3130 .into_iter()
3131 .map(|event| match event {
3132 ShellEvent::Key { key_code, .. } => key_code,
3133 other => panic!("not a key: {other:?}"),
3134 })
3135 .collect()
3136 };
3137 assert_eq!(
3138 keys(drain(&mut shell)),
3139 [Some(KeyCode::KeyA), Some(KeyCode::KeyB)]
3140 );
3141 assert_eq!(keys(drain(&mut shell)), [Some(KeyCode::KeyC)]);
3142 }
3143
3144 #[test]
3147 fn keep_alive_runs_a_due_script_and_keeps_its_events() {
3148 let (mut shell, window) = shell_with_configured_window();
3149 shell.schedule(1, move |shell| {
3150 shell.key_press(window, KeyCode::Enter).expect("press");
3151 });
3152 shell.keep_alive();
3153 shell.keep_alive();
3154 assert_eq!(shell.pending_events(), 1, "the script ran in keep_alive");
3155 assert_eq!(names(&drain(&mut shell)), ["Key"]);
3156 }
3157
3158 #[test]
3162 fn a_wait_does_not_sleep_while_a_script_is_scheduled() {
3163 let (mut shell, window) = shell_with_configured_window();
3164 shell.schedule(1, move |shell| {
3165 shell.key_press(window, KeyCode::Space).expect("press");
3166 });
3167 let start = std::time::Instant::now();
3168 shell.wait_events(Some(LONG_WAIT));
3169 assert!(
3170 start.elapsed() < LONG_WAIT / 2,
3171 "the wait slept {:?}",
3172 start.elapsed()
3173 );
3174 }
3175
3176 #[test]
3177 fn synthesized_key_fields_are_stable_and_plausible() {
3178 let (mut shell, window) = shell_with_configured_window();
3179 shell.set_modifiers(Modifiers::CTRL);
3180 shell.key_press(window, KeyCode::KeyS).expect("press");
3181 let events = drain(&mut shell);
3182 let ShellEvent::Key {
3183 scancode,
3184 key_code,
3185 keysym,
3186 modifiers,
3187 repeat,
3188 device,
3189 ..
3190 } = events[0].clone()
3191 else {
3192 panic!("wrong variant");
3193 };
3194 assert_eq!(key_code, Some(KeyCode::KeyS));
3195 assert_eq!(keysym, Keysym::from_char('s'));
3196 assert_eq!(modifiers, Modifiers::CTRL);
3197 assert!(!repeat);
3198 assert_eq!(device, VIRTUAL_DEVICE);
3199 assert!(scancode.0 >= 8, "evdev-shaped scancodes start at 8");
3200 assert_eq!(synthetic_keysym(KeyCode::Digit7), Keysym::from_char('7'));
3201 assert_eq!(synthetic_keysym(KeyCode::F5), Keysym(0xFFC2));
3202 assert_eq!(synthetic_keysym(KeyCode::Enter), Keysym(0xFF0D));
3203 assert_eq!(synthetic_keysym(KeyCode::Escape), Keysym(0xFF1B));
3204 assert_eq!(synthetic_keysym(KeyCode::ArrowLeft), Keysym(0xFF51));
3205 assert_eq!(synthetic_keysym(KeyCode::Slash), Keysym::from_char('/'));
3206
3207 shell.key_repeat(window, KeyCode::KeyS).expect("repeat");
3208 let events = drain(&mut shell);
3209 let ShellEvent::Key { repeat, .. } = events[0] else {
3210 panic!("wrong variant");
3211 };
3212 assert!(repeat);
3213 }
3214
3215 #[test]
3216 fn a_locked_pointer_reports_no_absolute_position() {
3217 let (mut shell, window) = shell_with_configured_window();
3218 shell
3219 .set_pointer_mode(window, PointerMode::Locked)
3220 .expect("lock");
3221 shell
3222 .move_pointer(window, PhysicalPoint::new(10.0, 10.0), (3.0, -4.0))
3223 .expect("motion");
3224 let events = drain(&mut shell);
3225 let ShellEvent::PointerMotion { abs, raw_delta, .. } = events[0] else {
3226 panic!("wrong variant");
3227 };
3228 assert_eq!(abs, None);
3229 assert_eq!(raw_delta, Some((3.0, -4.0)));
3230 }
3231
3232 #[test]
3233 fn a_backend_without_raw_motion_reports_none() {
3234 let mut shell = HeadlessShell::new()
3235 .with_caps(ShellCaps::DESKTOP - ShellCaps::RAW_POINTER_MOTION)
3236 .with_configure_delay(0);
3237 let window = shell.create_window(&WindowDesc::default()).expect("window");
3238 drain(&mut shell);
3239 shell
3240 .move_pointer(window, PhysicalPoint::new(1.0, 2.0), (5.0, 5.0))
3241 .expect("motion");
3242 let events = drain(&mut shell);
3243 let ShellEvent::PointerMotion { abs, raw_delta, .. } = events[0] else {
3244 panic!("wrong variant");
3245 };
3246 assert_eq!(abs, Some(PhysicalPoint::new(1.0, 2.0)));
3247 assert_eq!(raw_delta, None, "no relative-motion source");
3248 assert!(!shell.caps().has_mouselook());
3249 }
3250
3251 #[test]
3252 fn pointer_focus_and_scroll_and_text_are_injectable() {
3253 let (mut shell, window) = shell_with_configured_window();
3254 shell
3255 .set_pointer_focus(window, true, Some(PhysicalPoint::new(1.0, 1.0)))
3256 .expect("enter");
3257 shell
3258 .scroll(window, ScrollDelta::Lines { x: 0.0, y: -1.0 }, None)
3259 .expect("scroll");
3260 shell
3261 .button(
3262 window,
3263 PointerButton::Left,
3264 ButtonState::Pressed,
3265 Some(PhysicalPoint::new(1.0, 1.0)),
3266 )
3267 .expect("click");
3268 shell.commit_text(window, "ありがとう").expect("text");
3269 shell
3270 .set_pointer_focus(window, false, Some(PhysicalPoint::new(1.0, 1.0)))
3271 .expect("leave");
3272
3273 let events = drain(&mut shell);
3274 assert_eq!(
3275 names(&events),
3276 [
3277 "PointerFocus",
3278 "Wheel",
3279 "Button",
3280 "TextCommit",
3281 "PointerFocus"
3282 ]
3283 );
3284 let ShellEvent::PointerFocus {
3285 entered, position, ..
3286 } = &events[4]
3287 else {
3288 panic!("wrong variant");
3289 };
3290 assert!(!entered);
3291 assert_eq!(*position, None, "a leave has no position");
3292 assert!(events.iter().all(ShellEvent::is_input));
3293 }
3294
3295 #[test]
3302 fn contacts_are_injectable_and_a_touchless_shell_refuses_them() {
3303 let (mut shell, window) = shell_with_configured_window();
3304 assert!(shell.caps().contains(ShellCaps::TOUCH));
3305 for (contact, phase, x) in [
3306 (1, TouchPhase::Began, 10.0),
3307 (2, TouchPhase::Began, 200.0),
3308 (2, TouchPhase::Moved, 220.0),
3309 (1, TouchPhase::Ended, 10.0),
3310 (2, TouchPhase::Cancelled, 220.0),
3311 ] {
3312 shell
3313 .touch(
3314 window,
3315 ContactId(contact),
3316 phase,
3317 PhysicalPoint::new(x, 5.0),
3318 )
3319 .expect("a contact");
3320 }
3321 let events = drain(&mut shell);
3322 assert_eq!(names(&events), ["Touch"; 5]);
3323 let seen: Vec<(ContactId, TouchPhase, f64)> = events
3324 .iter()
3325 .map(|event| {
3326 let ShellEvent::Touch {
3327 contact,
3328 phase,
3329 position,
3330 ..
3331 } = event
3332 else {
3333 panic!("wrong variant");
3334 };
3335 (*contact, *phase, position.x)
3336 })
3337 .collect();
3338 assert_eq!(
3339 seen,
3340 vec![
3341 (ContactId(1), TouchPhase::Began, 10.0),
3342 (ContactId(2), TouchPhase::Began, 200.0),
3343 (ContactId(2), TouchPhase::Moved, 220.0),
3344 (ContactId(1), TouchPhase::Ended, 10.0),
3345 (ContactId(2), TouchPhase::Cancelled, 220.0),
3346 ]
3347 );
3348 assert!(events.iter().all(ShellEvent::is_input));
3349
3350 let mut touchless = HeadlessShell::new()
3351 .with_caps(ShellCaps::DESKTOP - ShellCaps::TOUCH)
3352 .with_configure_delay(0);
3353 let window = touchless
3354 .create_window(&WindowDesc::default())
3355 .expect("window");
3356 drain(&mut touchless);
3357 assert!(matches!(
3358 touchless.touch(
3359 window,
3360 ContactId(1),
3361 TouchPhase::Began,
3362 PhysicalPoint::ORIGIN
3363 ),
3364 Err(ShellError::Unsupported { .. })
3365 ));
3366 assert!(drain(&mut touchless).is_empty(), "and queued nothing");
3367 }
3368
3369 #[test]
3370 fn warping_produces_motion_like_every_platform_that_has_it() {
3371 let (mut shell, window) = shell_with_configured_window();
3372 shell
3373 .warp_pointer(window, PhysicalPoint::new(320.0, 180.0))
3374 .expect("warp");
3375 let events = drain(&mut shell);
3376 let ShellEvent::PointerMotion { abs, raw_delta, .. } = events[0] else {
3377 panic!("wrong variant");
3378 };
3379 assert_eq!(abs, Some(PhysicalPoint::new(320.0, 180.0)));
3380 assert_eq!(raw_delta, None, "a warp is not physical motion");
3381 }
3382
3383 #[test]
3386 fn capabilities_gate_the_operations_that_need_them() {
3387 let mut shell = HeadlessShell::new().with_caps(ShellCaps::empty());
3388 let window = shell.create_window(&WindowDesc::default()).expect("window");
3389
3390 assert!(matches!(
3391 shell.warp_pointer(window, PhysicalPoint::ORIGIN),
3392 Err(ShellError::Unsupported { .. })
3393 ));
3394 assert!(matches!(
3395 shell.set_pointer_mode(window, PointerMode::Locked),
3396 Err(ShellError::Unsupported { .. })
3397 ));
3398 assert!(matches!(
3399 shell.clipboard_request(window, MimeType::TextUtf8),
3400 Err(ShellError::Unsupported { .. })
3401 ));
3402 shell
3404 .set_pointer_mode(window, PointerMode::Free)
3405 .expect("Free is always available");
3406 assert!(matches!(
3408 shell.create_window(&WindowDesc::default()),
3409 Err(ShellError::Unsupported { .. })
3410 ));
3411 }
3412
3413 #[test]
3414 fn stale_handles_fail_cleanly_on_every_accessor() {
3415 let (mut shell, window) = shell_with_configured_window();
3416 shell.destroy_window(window).expect("destroy");
3417 assert_eq!(
3418 drain(&mut shell),
3419 vec![ShellEvent::WindowDestroyed { window }]
3420 );
3421
3422 assert!(matches!(
3423 shell.window_state(window),
3424 Err(ShellError::InvalidWindow { .. })
3425 ));
3426 assert!(matches!(
3427 shell.surface_target(window),
3428 Err(ShellError::InvalidWindow { .. })
3429 ));
3430 assert!(matches!(
3431 shell.resize(window, PhysicalSize::new(1, 1)),
3432 Err(ShellError::InvalidWindow { .. })
3433 ));
3434 assert!(matches!(
3435 shell.set_title(window, "gone"),
3436 Err(ShellError::InvalidWindow { .. })
3437 ));
3438 assert!(matches!(
3439 shell.destroy_window(window),
3440 Err(ShellError::InvalidWindow { .. })
3441 ));
3442 assert!(matches!(
3443 shell.key_press(window, KeyCode::KeyA),
3444 Err(ShellError::InvalidWindow { .. })
3445 ));
3446 }
3447
3448 #[test]
3449 fn destroying_a_window_cancels_its_pending_configure() {
3450 let (mut shell, window) = shell_with_window();
3453 shell.destroy_window(window).expect("destroy");
3454 assert_eq!(names(&drain(&mut shell)), ["WindowDestroyed"]);
3455 assert_eq!(drain(&mut shell), Vec::new());
3456 assert_eq!(drain(&mut shell), Vec::new());
3457 }
3458
3459 #[test]
3460 fn close_requests_are_questions() {
3461 let (mut shell, window) = shell_with_configured_window();
3462 assert!(matches!(
3463 shell.reply_close_request(window, CloseReply::Close),
3464 Err(ShellError::NoPendingCloseRequest { .. })
3465 ));
3466
3467 shell.request_close(window).expect("request");
3468 assert!(shell.window_state(window).expect("state").close_pending);
3469 shell
3470 .reply_close_request(window, CloseReply::Keep)
3471 .expect("keep");
3472 assert!(!shell.window_state(window).expect("state").close_pending);
3473 assert!(shell.window_state(window).is_ok(), "still open");
3474
3475 shell.request_close(window).expect("request again");
3476 shell
3477 .reply_close_request(window, CloseReply::Close)
3478 .expect("close");
3479 assert!(matches!(
3480 shell.window_state(window),
3481 Err(ShellError::InvalidWindow { .. })
3482 ));
3483 }
3484
3485 #[test]
3486 fn monitor_hotplug_reports_a_change_and_keeps_ids_unique() {
3487 let mut shell = HeadlessShell::new();
3488 let extra = shell.plug_monitor(MonitorInfo {
3489 id: MonitorId(0),
3490 name: "HEADLESS-2".to_string(),
3491 bounds: PhysicalRect::new(1920, 0, 1920, 1080),
3492 work_area: PhysicalRect::new(1920, 0, 1920, 1080),
3493 scale_factor: 1.0,
3494 refresh_millihertz: 60_000,
3495 is_primary: false,
3496 });
3497 assert_eq!(shell.monitors().len(), 2);
3498 assert_eq!(drain(&mut shell), vec![ShellEvent::MonitorsChanged]);
3499
3500 shell.unplug_monitor(extra).expect("unplug");
3501 assert_eq!(shell.monitors().len(), 1);
3502 assert_eq!(
3503 drain(&mut shell),
3504 vec![
3505 ShellEvent::MonitorRemoved { monitor: extra },
3506 ShellEvent::MonitorsChanged
3507 ]
3508 );
3509 assert!(matches!(
3510 shell.unplug_monitor(extra),
3511 Err(ShellError::NoSuchMonitor(_))
3512 ));
3513 assert_eq!(
3514 shell.monitor(MonitorId(1)).map(|m| m.name.as_str()),
3515 Some("HEADLESS-1")
3516 );
3517 assert!(shell.monitor(extra).is_none());
3518 }
3519
3520 #[test]
3521 fn unplugging_a_monitor_asks_its_panels_and_only_its_panels_to_close() {
3522 let mut shell = HeadlessShell::new();
3523 let second = shell.plug_monitor(MonitorInfo {
3524 id: MonitorId(0),
3525 name: "HEADLESS-2".to_string(),
3526 bounds: PhysicalRect::new(1920, 0, 1920, 1080),
3527 work_area: PhysicalRect::new(1920, 0, 1920, 1080),
3528 scale_factor: 1.0,
3529 refresh_millihertz: 60_000,
3530 is_primary: false,
3531 });
3532 let first = shell.monitors()[0].id;
3533 let panel_on = |shell: &mut HeadlessShell, monitor| {
3534 shell
3535 .create_window(&WindowDesc {
3536 role: crate::WindowRole::Panel(crate::PanelDesc {
3537 monitor,
3538 ..crate::PanelDesc::default()
3539 }),
3540 ..WindowDesc::default()
3541 })
3542 .expect("panel")
3543 };
3544 let doomed = panel_on(&mut shell, Some(second));
3545 let already_asked = panel_on(&mut shell, Some(second));
3546 let elsewhere = panel_on(&mut shell, Some(first));
3547 let anywhere = panel_on(&mut shell, None);
3548 let toplevel = shell.create_window(&WindowDesc::default()).expect("window");
3549 shell.request_close(already_asked).expect("close");
3550 settle(&mut shell);
3551 drain(&mut shell);
3552
3553 shell.unplug_monitor(second).expect("unplug");
3554 assert_eq!(
3555 drain(&mut shell),
3556 vec![
3557 ShellEvent::MonitorRemoved { monitor: second },
3558 ShellEvent::CloseRequested { window: doomed },
3559 ShellEvent::MonitorsChanged,
3560 ]
3561 );
3562 for window in [elsewhere, anywhere, toplevel] {
3563 assert!(!shell.window_state(window).expect("open").close_pending);
3564 }
3565 }
3566
3567 #[test]
3568 fn drops_require_both_the_capability_and_the_opt_in() {
3569 let mut shell = HeadlessShell::new();
3570 let plain = shell.create_window(&WindowDesc::default()).expect("window");
3571 assert!(matches!(
3572 shell.drop_file(plain, "/tmp/a.gltf", None),
3573 Err(ShellError::Unsupported { .. })
3574 ));
3575
3576 let dropper = shell
3577 .create_window(&WindowDesc {
3578 accept_drops: true,
3579 ..WindowDesc::default()
3580 })
3581 .expect("window");
3582 settle(&mut shell);
3583 shell
3584 .drop_file(dropper, "/tmp/a.gltf", Some(PhysicalPoint::new(4.0, 5.0)))
3585 .expect("drop");
3586 assert_eq!(names(&drain(&mut shell))[0], "DroppedFile");
3587 }
3588
3589 #[test]
3590 fn a_sink_that_queues_more_events_does_not_spin_the_pump() {
3591 let (mut shell, window) = shell_with_configured_window();
3592 shell.inject(ShellEvent::MonitorsChanged);
3593 let mut extra = Vec::new();
3594 shell.pump(&mut |event| extra.push(event));
3595 assert_eq!(extra.len(), 1);
3596 shell.inject(ShellEvent::CloseRequested { window });
3597 assert_eq!(shell.pending_events(), 1);
3598 }
3599
3600 #[test]
3601 fn accessors_that_no_real_backend_can_provide_are_headless_only() {
3602 let (mut shell, window) = shell_with_configured_window();
3603 shell.set_title(window, "renamed").expect("title");
3604 assert_eq!(shell.title(window).expect("title"), "renamed");
3605 shell.set_cursor(window, None).expect("hide");
3606 assert_eq!(shell.cursor(window).expect("cursor"), None);
3607 shell
3608 .set_cursor(window, Some(CursorIcon::Crosshair))
3609 .expect("shape");
3610 assert_eq!(
3611 shell.cursor(window).expect("cursor"),
3612 Some(CursorIcon::Crosshair)
3613 );
3614 shell.set_visible(window, false).expect("hide window");
3615 assert!(!shell.window_state(window).expect("state").visible);
3616 }
3617
3618 #[test]
3619 fn an_input_region_is_recorded_replaced_and_given_back() {
3620 let (mut shell, window) = shell_with_configured_window();
3621 assert_eq!(shell.input_region(window).expect("region"), None);
3622 let panel = PhysicalRect::new(0, 0, 400, 300).rounded(12);
3623 shell
3624 .set_input_region(window, Some(&panel))
3625 .expect("a region");
3626 assert_eq!(
3627 shell.input_region(window).expect("region"),
3628 Some(panel.as_slice())
3629 );
3630 shell.set_input_region(window, Some(&[])).expect("nothing");
3631 assert_eq!(shell.input_region(window).expect("region"), Some(&[][..]));
3632 shell.set_input_region(window, None).expect("whole window");
3633 assert_eq!(shell.input_region(window).expect("region"), None);
3634 shell.destroy_window(window).expect("destroy");
3635 assert!(matches!(
3636 shell.set_input_region(window, None),
3637 Err(ShellError::InvalidWindow { .. })
3638 ));
3639 assert!(shell.input_region(window).is_err());
3640 }
3641
3642 #[test]
3643 fn an_idle_inhibit_is_recorded_and_ends_with_its_window() {
3644 let (mut shell, window) = shell_with_configured_window();
3645 assert!(!shell.idle_inhibited(window).expect("live"));
3646 shell.set_idle_inhibit(window, true).expect("inhibit");
3647 shell.set_idle_inhibit(window, true).expect("asking twice");
3648 assert!(shell.idle_inhibited(window).expect("live"));
3649 shell.set_idle_inhibit(window, false).expect("stop");
3650 assert!(!shell.idle_inhibited(window).expect("live"));
3651 shell.set_idle_inhibit(window, true).expect("inhibit again");
3652 shell.destroy_window(window).expect("destroy");
3653 assert!(matches!(
3654 shell.set_idle_inhibit(window, false),
3655 Err(ShellError::InvalidWindow { .. })
3656 ));
3657 assert!(shell.idle_inhibited(window).is_err());
3658 }
3659
3660 #[test]
3661 fn an_idle_inhibit_is_refused_without_the_capability() {
3662 let mut shell =
3663 HeadlessShell::new().with_caps(ShellCaps::DESKTOP - ShellCaps::IDLE_INHIBIT);
3664 let window = shell.create_window(&WindowDesc::default()).expect("window");
3665 assert!(matches!(
3666 shell.set_idle_inhibit(window, true),
3667 Err(ShellError::Unsupported {
3668 backend: ShellBackend::Headless,
3669 ..
3670 })
3671 ));
3672 assert!(!shell.idle_inhibited(window).expect("live"));
3673 }
3674
3675 fn url(text: &str) -> WebUrl {
3676 WebUrl::parse(text).expect("a web URL")
3677 }
3678
3679 #[test]
3680 fn an_opened_url_is_recorded_in_order() {
3681 let (mut shell, window) = shell_with_configured_window();
3682 let other = shell.create_window(&WindowDesc::default()).expect("window");
3683 assert!(shell.opened_urls().is_empty());
3684 let store = url("https://store.example/app/1");
3685 let privacy = url("HTTPS://EXAMPLE.COM/privacy");
3686 shell.open_url(window, &store).expect("store page");
3687 shell.open_url(other, &privacy).expect("privacy policy");
3688 shell
3689 .open_url(window, &store)
3690 .expect("asking twice opens twice");
3691 assert_eq!(
3692 shell.opened_urls(),
3693 [
3694 (window, store.clone()),
3695 (other, privacy),
3696 (window, store.clone())
3697 ]
3698 );
3699 shell.destroy_window(window).expect("destroy");
3700 assert!(matches!(
3701 shell.open_url(window, &store),
3702 Err(ShellError::InvalidWindow { .. })
3703 ));
3704 assert_eq!(
3705 shell.opened_urls().len(),
3706 3,
3707 "the history outlives the window"
3708 );
3709 }
3710
3711 #[test]
3712 fn opening_a_url_is_refused_without_the_capability() {
3713 let mut shell = HeadlessShell::new().with_caps(ShellCaps::DESKTOP - ShellCaps::OPEN_URL);
3714 let window = shell.create_window(&WindowDesc::default()).expect("window");
3715 assert!(matches!(
3716 shell.open_url(window, &url("https://example.com/")),
3717 Err(ShellError::Unsupported {
3718 backend: ShellBackend::Headless,
3719 ..
3720 })
3721 ));
3722 assert!(shell.opened_urls().is_empty());
3723 }
3724
3725 fn frames_for(events: &[ShellEvent], window: WindowId) -> usize {
3726 events
3727 .iter()
3728 .filter(|event| matches!(event, ShellEvent::FrameReady { window: w } if *w == window))
3729 .count()
3730 }
3731
3732 #[test]
3733 fn requests_for_a_frame_share_one_answer_at_the_next_pump() {
3734 let (mut shell, window) = shell_with_configured_window();
3735 assert!(!shell.frame_requested(window).expect("live"));
3736 shell.request_frame(window).expect("a frame");
3737 shell.request_frame(window).expect("the same frame");
3738 assert!(shell.frame_requested(window).expect("live"));
3739 let events = drain(&mut shell);
3740 assert_eq!(frames_for(&events, window), 1, "{:?}", names(&events));
3741 assert!(!shell.frame_requested(window).expect("live"));
3742 assert_eq!(
3743 frames_for(&drain(&mut shell), window),
3744 0,
3745 "nothing asked again"
3746 );
3747 shell.request_frame(window).expect("the next frame");
3748 assert_eq!(frames_for(&drain(&mut shell), window), 1);
3749 }
3750
3751 #[test]
3752 fn a_window_destroyed_with_a_frame_requested_is_not_answered() {
3753 let (mut shell, doomed) = shell_with_configured_window();
3754 let kept = shell
3755 .create_window(&WindowDesc::default())
3756 .expect("a second window");
3757 settle(&mut shell);
3758 shell.request_frame(doomed).expect("a frame");
3759 shell.request_frame(kept).expect("a frame");
3760 shell.destroy_window(doomed).expect("destroy");
3761 let events = drain(&mut shell);
3762 assert_eq!(frames_for(&events, doomed), 0, "{:?}", names(&events));
3763 assert_eq!(frames_for(&events, kept), 1, "the other window still asked");
3764 assert!(matches!(
3765 shell.request_frame(doomed),
3766 Err(ShellError::InvalidWindow { .. })
3767 ));
3768 assert!(shell.frame_requested(doomed).is_err());
3769 }
3770
3771 #[test]
3772 fn a_wait_does_not_sleep_through_a_frame_that_is_due() {
3773 let (mut shell, window) = shell_with_configured_window();
3774 shell.request_frame(window).expect("a frame");
3775 let start = std::time::Instant::now();
3776 shell.wait_events(Some(LONG_WAIT));
3777 assert!(
3778 start.elapsed() < LONG_WAIT / 2,
3779 "slept {:?} with a frame due",
3780 start.elapsed()
3781 );
3782 assert_eq!(frames_for(&drain(&mut shell), window), 1);
3783 }
3784
3785 #[test]
3786 fn an_output_that_is_off_holds_frame_requests_until_it_is_on() {
3787 let (mut shell, window) = shell_with_configured_window();
3788 shell.set_outputs_powered(false);
3789 shell.request_frame(window).expect("a frame");
3790 assert_eq!(frames_for(&drain(&mut shell), window), 0, "held");
3791 assert!(shell.frame_requested(window).expect("live"), "still owed");
3792
3793 let wait = Duration::from_millis(20);
3794 let start = std::time::Instant::now();
3795 shell.wait_events(Some(wait));
3796 assert!(
3797 start.elapsed() >= wait,
3798 "a held request cut the wait short after {:?}",
3799 start.elapsed()
3800 );
3801
3802 shell.set_outputs_powered(true);
3803 assert_eq!(
3804 frames_for(&drain(&mut shell), window),
3805 1,
3806 "answered once on"
3807 );
3808 assert!(!shell.frame_requested(window).expect("live"));
3809 }
3810
3811 #[test]
3812 fn waiting_is_observable_and_focus_transitions_are_events() {
3813 let (mut shell, window) = shell_with_configured_window();
3814 assert_eq!(shell.wait_count(), 0);
3815 shell.wait_events(Some(Duration::from_millis(5)));
3816 shell.wait_events(None);
3817 assert_eq!(shell.wait_count(), 2);
3818
3819 shell.set_focus(window, true).expect("focus");
3820 assert!(shell.window_state(window).expect("state").focused);
3821 shell.set_focus(window, false).expect("unfocus");
3822 assert_eq!(names(&drain(&mut shell)), ["Focus", "Focus"]);
3823 }
3824
3825 const LONG_WAIT: Duration = Duration::from_secs(60);
3830 const RING_AFTER: Duration = Duration::from_millis(50);
3832
3833 fn ring_later(waker: ShellWaker) -> std::thread::JoinHandle<()> {
3835 std::thread::spawn(move || {
3836 std::thread::sleep(RING_AFTER);
3837 waker.wake();
3838 })
3839 }
3840
3841 #[test]
3842 fn a_wake_from_another_thread_ends_a_long_wait() {
3843 let mut shell = HeadlessShell::new();
3844 let ringer = ring_later(shell.waker().expect("headless has a waker"));
3845 let start = std::time::Instant::now();
3846 shell.wait_events(Some(LONG_WAIT));
3847 let slept = start.elapsed();
3848 ringer.join().expect("the ringer");
3849 assert!(slept < LONG_WAIT / 2, "the wait ran {slept:?}");
3850 }
3851
3852 #[test]
3853 fn a_wake_before_the_wait_ends_it_at_once_and_wakes_coalesce() {
3854 let mut shell = HeadlessShell::new();
3855 let waker = shell.waker().expect("headless has a waker");
3856 for _ in 0..3 {
3857 waker.wake();
3858 }
3859 let start = std::time::Instant::now();
3860 shell.wait_events(Some(LONG_WAIT));
3861 assert!(
3862 start.elapsed() < LONG_WAIT / 2,
3863 "a rung bell must end the wait"
3864 );
3865
3866 let timeout = Duration::from_millis(100);
3868 let start = std::time::Instant::now();
3869 shell.wait_events(Some(timeout));
3870 assert!(
3871 start.elapsed() >= timeout / 2,
3872 "the bell was cleared, so this wait must sleep, but it took {:?}",
3873 start.elapsed()
3874 );
3875 }
3876
3877 #[test]
3878 fn with_a_waker_out_an_untimed_wait_sleeps_until_it_rings() {
3879 let mut shell = HeadlessShell::new();
3880 let ringer = ring_later(shell.waker().expect("headless has a waker"));
3881 let start = std::time::Instant::now();
3882 shell.wait_events(None);
3883 let slept = start.elapsed();
3884 ringer.join().expect("the ringer");
3885 assert!(
3886 slept >= RING_AFTER / 2,
3887 "an untimed wait returned after {slept:?}, before anything rang"
3888 );
3889 }
3890
3891 #[test]
3894 fn the_clipboard_round_trips_through_an_event() {
3895 let (mut shell, window) = shell_with_configured_window();
3896 shell
3897 .clipboard_offer(
3898 window,
3899 &[
3900 ClipboardOffer::text("node"),
3901 ClipboardOffer::ron("(kind:\"node\")"),
3902 ],
3903 )
3904 .expect("offer");
3905 assert_eq!(
3906 shell.clipboard_bytes(MimeType::TextUtf8),
3907 Some(&b"node"[..])
3908 );
3909
3910 let request = shell
3911 .clipboard_request(window, MimeType::CrcblRon)
3912 .expect("request");
3913 assert_eq!(
3914 drain(&mut shell)[0],
3915 ShellEvent::ClipboardData {
3916 window,
3917 request,
3918 mime: ReceivedMime::from(MimeType::CrcblRon),
3919 content: ClipboardContent::Bytes(b"(kind:\"node\")".to_vec()),
3920 }
3921 );
3922
3923 let request = shell
3926 .clipboard_request(window, MimeType::UriList)
3927 .expect("request");
3928 assert_eq!(
3929 drain(&mut shell)[0],
3930 ShellEvent::ClipboardData {
3931 window,
3932 request,
3933 mime: ReceivedMime::from(MimeType::UriList),
3934 content: ClipboardContent::Empty,
3935 }
3936 );
3937
3938 shell
3941 .clipboard_offer(window, &[ClipboardOffer::text("")])
3942 .expect("offer");
3943 let request = shell
3944 .clipboard_request(window, MimeType::TextUtf8)
3945 .expect("request");
3946 assert_eq!(
3947 drain(&mut shell)[0],
3948 ShellEvent::ClipboardData {
3949 window,
3950 request,
3951 mime: ReceivedMime::from(MimeType::TextUtf8),
3952 content: ClipboardContent::Bytes(Vec::new()),
3953 }
3954 );
3955 }
3956
3957 #[test]
3958 fn a_read_taken_while_the_clipboard_is_unreadable_is_held_not_answered_empty() {
3959 let (mut shell, window) = shell_with_configured_window();
3965 shell
3966 .clipboard_offer(window, &[ClipboardOffer::text("published")])
3967 .expect("offer");
3968 shell.set_clipboard_readable(false);
3969 assert!(!shell.clipboard_readable(window));
3970
3971 let request = shell
3972 .clipboard_request(window, MimeType::TextUtf8)
3973 .expect("a read is accepted even when it cannot be answered yet");
3974 assert_eq!(shell.held_clipboard_reads(), 1);
3975 assert!(
3976 drain(&mut shell).is_empty(),
3977 "a held read produces no event at all — least of all an empty one"
3978 );
3979
3980 shell.set_clipboard_readable(true);
3982 let events = drain(&mut shell);
3983 assert_eq!(events.len(), 1);
3984 assert_eq!(
3985 events[0],
3986 ShellEvent::ClipboardData {
3987 window,
3988 request,
3989 mime: ReceivedMime::from(MimeType::TextUtf8),
3990 content: ClipboardContent::Bytes(b"published".to_vec()),
3991 }
3992 );
3993 assert_eq!(shell.held_clipboard_reads(), 0);
3994 }
3995
3996 #[test]
3997 fn a_read_that_never_becomes_answerable_ends_as_unavailable() {
3998 let (mut shell, window) = shell_with_configured_window();
4002 shell
4003 .clipboard_offer(window, &[ClipboardOffer::text("unreachable")])
4004 .expect("offer");
4005 shell.set_clipboard_readable(false);
4006 shell.clipboard_deadline(2);
4007
4008 let request = shell
4009 .clipboard_request(window, MimeType::TextUtf8)
4010 .expect("request");
4011 assert!(drain(&mut shell).is_empty(), "pump 1: still held");
4012 assert!(drain(&mut shell).is_empty(), "pump 2: still held");
4013 let events = drain(&mut shell);
4014 assert_eq!(
4015 events[0],
4016 ShellEvent::ClipboardData {
4017 window,
4018 request,
4019 mime: ReceivedMime::from(MimeType::TextUtf8),
4020 content: ClipboardContent::Unavailable,
4023 }
4024 );
4025 assert_eq!(shell.held_clipboard_reads(), 0);
4026 }
4027
4028 #[test]
4029 fn a_peer_that_holds_the_clipboard_and_never_delivers_is_unavailable() {
4030 let (mut shell, window) = shell_with_configured_window();
4034 shell.set_foreign_clipboard(vec![(
4035 ReceivedMime::new("text/plain"),
4036 b"never sent".to_vec(),
4037 )]);
4038 shell.set_clipboard_stalled(true);
4039 shell.clipboard_deadline(1);
4040 assert!(
4041 !shell.clipboard_readable(window),
4042 "a clipboard whose owner will not answer is not readable"
4043 );
4044
4045 let request = shell
4046 .clipboard_request(window, MimeType::TextUtf8)
4047 .expect("request");
4048 assert!(drain(&mut shell).is_empty());
4049 let events = drain(&mut shell);
4050 let ShellEvent::ClipboardData {
4051 request: answered,
4052 content,
4053 ..
4054 } = &events[0]
4055 else {
4056 panic!("wrong variant");
4057 };
4058 assert_eq!(*answered, request);
4059 assert_eq!(*content, ClipboardContent::Unavailable);
4060
4061 shell.set_clipboard_stalled(false);
4063 let request = shell
4064 .clipboard_request(window, MimeType::TextUtf8)
4065 .expect("request");
4066 assert_eq!(
4067 drain(&mut shell)[0],
4068 ShellEvent::ClipboardData {
4069 window,
4070 request,
4071 mime: ReceivedMime::new("text/plain"),
4072 content: ClipboardContent::Bytes(b"never sent".to_vec()),
4073 }
4074 );
4075 }
4076
4077 #[test]
4078 fn claiming_the_clipboard_can_require_a_user_interaction() {
4079 let (mut shell, window) = shell_with_configured_window();
4083 shell.require_user_interaction(true);
4084
4085 let refused = shell.clipboard_offer(window, &[ClipboardOffer::text("stolen")]);
4086 assert!(
4087 matches!(
4088 refused,
4089 Err(ShellError::NeedsUserInteraction {
4090 backend: ShellBackend::Headless,
4091 ..
4092 })
4093 ),
4094 "{refused:?}"
4095 );
4096 assert!(
4097 shell.clipboard_bytes(MimeType::TextUtf8).is_none(),
4098 "a refused claim must not have taken the clipboard anyway"
4099 );
4100
4101 shell.key_press(window, KeyCode::KeyC).expect("key");
4104 shell
4105 .clipboard_offer(window, &[ClipboardOffer::text("copied")])
4106 .expect("with a serial in hand, the claim goes through");
4107 assert_eq!(
4108 shell.clipboard_bytes(MimeType::TextUtf8),
4109 Some(&b"copied"[..])
4110 );
4111
4112 shell.clear_user_interaction();
4114 assert!(matches!(
4115 shell.clipboard_offer(window, &[ClipboardOffer::text("later")]),
4116 Err(ShellError::NeedsUserInteraction { .. })
4117 ));
4118 }
4119
4120 #[test]
4121 fn a_held_read_for_a_destroyed_window_is_dropped_rather_than_answered() {
4122 let (mut shell, window) = shell_with_configured_window();
4125 shell.set_clipboard_readable(false);
4126 let _request = shell
4127 .clipboard_request(window, MimeType::TextUtf8)
4128 .expect("request");
4129 assert_eq!(shell.held_clipboard_reads(), 1);
4130
4131 shell.destroy_window(window).expect("destroy");
4132 let events = drain(&mut shell);
4133 assert_eq!(shell.held_clipboard_reads(), 0);
4134 assert!(
4135 !events
4136 .iter()
4137 .any(|event| matches!(event, ShellEvent::ClipboardData { .. })),
4138 "no clipboard answer may name a destroyed window: {events:?}"
4139 );
4140 }
4141
4142 #[test]
4143 fn a_paste_from_a_foreign_application_keeps_the_foreign_spelling() {
4144 let (mut shell, window) = shell_with_configured_window();
4145 shell.set_foreign_clipboard(vec![
4146 (ReceivedMime::new("text/plain"), b"from gedit".to_vec()),
4149 (
4150 ReceivedMime::new("application/vnd.some-editor.scene+json"),
4151 b"{}".to_vec(),
4152 ),
4153 ]);
4154
4155 let request = shell
4157 .clipboard_request(window, MimeType::TextUtf8)
4158 .expect("request");
4159 let events = drain(&mut shell);
4160 let ShellEvent::ClipboardData {
4161 request: answered,
4162 mime,
4163 content,
4164 ..
4165 } = &events[0]
4166 else {
4167 panic!("wrong variant");
4168 };
4169 assert_eq!(*answered, request);
4170 assert_eq!(content.bytes(), Some(&b"from gedit"[..]));
4171 assert_eq!(mime.as_str(), "text/plain");
4173 assert!(mime.matches(MimeType::TextUtf8));
4174 assert_ne!(*mime, ReceivedMime::from(MimeType::TextUtf8));
4175
4176 assert_eq!(
4179 shell.clipboard_bytes(MimeType::Other("application/vnd.some-editor.scene+json")),
4180 Some(&b"{}"[..])
4181 );
4182 }
4183
4184 #[test]
4187 #[should_panic(expected = "a shell needs at least one monitor")]
4188 fn a_shell_with_no_monitors_is_rejected() {
4189 let _ = HeadlessShell::new().with_monitors(Vec::new());
4190 }
4191
4192 #[test]
4193 #[should_panic(expected = "a text commit is never empty")]
4194 fn an_empty_text_commit_is_rejected() {
4195 let (mut shell, window) = shell_with_configured_window();
4196 let _ = shell.commit_text(window, "");
4197 }
4198}