1use std::collections::VecDeque;
135use std::path::PathBuf;
136use std::sync::{Arc, Condvar, Mutex, PoisonError};
137
138use core::time::Duration;
139
140use crcbl_core::input::{
141 ButtonState, ContactId, DeviceId, Keysym, Modifiers, PointerButton, Scancode, ScrollDelta,
142 TouchPhase,
143};
144use crcbl_core::time::{ManualTime, TimeSource};
145use crcbl_core::{EventTime, Pool, SurfaceTarget};
146
147use crate::waker::WakeSignal;
148use crate::{
149 ClipboardContent, ClipboardOffer, ClipboardRequestId, CloseReply, CursorIcon, DisplayMode,
150 KeyCode, LogicalRect, LogicalSize, MimeType, MonitorId, MonitorInfo, PhysicalPoint,
151 PhysicalRect, PhysicalSize, PointerMode, ReceivedMime, Shell, ShellBackend, ShellCaps,
152 ShellError, ShellEvent, ShellWaker, SizeConstraints, TextPurpose, WindowConfiguration,
153 WindowDesc, WindowId, WindowState,
154};
155
156pub const DEFAULT_MONITOR_SIZE: PhysicalSize = PhysicalSize::new(1920, 1080);
161
162pub const DEFAULT_REFRESH_MILLIHERTZ: u32 = 60_000;
164
165pub const DEFAULT_CONFIGURE_DELAY: u32 = 1;
171
172pub const VIRTUAL_DEVICE: DeviceId = DeviceId(1);
178
179pub const DEFAULT_CLIPBOARD_DEADLINE: u32 = 4;
187
188#[derive(Clone, Debug)]
194struct PendingRead {
195 window: WindowId,
196 request: ClipboardRequestId,
197 mime: MimeType,
198 countdown: u32,
200}
201
202#[derive(Clone, Copy, Debug)]
206struct PendingConfigure {
207 size: PhysicalSize,
208 scale_factor: f64,
209 mode: DisplayMode,
210 countdown: u32,
212}
213
214#[derive(Clone, Debug)]
216struct HeadlessWindow {
217 title: String,
218 configuration: Option<WindowConfiguration>,
220 pending: Option<PendingConfigure>,
222 requested_mode: DisplayMode,
223 requested_constraints: SizeConstraints,
224 focused: bool,
225 visible: bool,
226 pointer_mode: PointerMode,
227 cursor: Option<CursorIcon>,
228 close_pending: bool,
229 accept_drops: bool,
230 parent: Option<WindowId>,
232 popup_input: Option<crate::PopupInput>,
234 panel_monitor: Option<MonitorId>,
236 text_input: Option<TextPurpose>,
238 input_region: Option<Vec<PhysicalRect>>,
240 #[cfg(feature = "accesskit")]
243 accessibility: Option<Vec<accesskit::TreeUpdate>>,
244}
245
246impl HeadlessWindow {
247 fn size(&self) -> Option<PhysicalSize> {
249 self.configuration.map(|config| config.size)
250 }
251
252 fn scale_factor(&self) -> f64 {
258 self.configuration
259 .map(|config| config.scale_factor)
260 .or_else(|| self.pending.map(|pending| pending.scale_factor))
261 .unwrap_or(1.0)
262 }
263
264 fn settled_mode(&self) -> DisplayMode {
273 self.configuration
274 .map(|config| config.mode)
275 .or_else(|| self.pending.map(|pending| pending.mode))
276 .unwrap_or(self.requested_mode)
277 }
278
279 fn base_size(&self) -> PhysicalSize {
281 self.size()
282 .or_else(|| self.pending.map(|pending| pending.size))
283 .unwrap_or(PhysicalSize::new(1, 1))
284 }
285}
286
287#[derive(Debug)]
292pub struct HeadlessShell {
293 caps: ShellCaps,
294 monitors: Vec<MonitorInfo>,
295 next_monitor_id: u32,
296 windows: Pool<HeadlessWindow>,
297 queue: VecDeque<ShellEvent>,
298 clock: ManualTime,
299 modifiers: Modifiers,
300 clipboard: Vec<(ReceivedMime, Vec<u8>)>,
301 next_request_id: u32,
302 reads: Vec<PendingRead>,
305 clipboard_readable: bool,
309 clipboard_stalled: bool,
311 clipboard_needs_interaction: bool,
313 saw_interaction: bool,
316 clipboard_deadline: u32,
319 waits: u32,
320 configure_delay: u32,
321 wake: Option<Arc<HeadlessWake>>,
323}
324
325#[derive(Debug, Default)]
328struct HeadlessWake {
329 rung: Mutex<bool>,
330 bell: Condvar,
331}
332
333impl HeadlessWake {
334 fn wait(&self, timeout: Option<Duration>) {
337 let rung = self.rung.lock().unwrap_or_else(PoisonError::into_inner);
338 let mut rung = match timeout {
339 Some(timeout) => {
340 self.bell
341 .wait_timeout_while(rung, timeout, |rung| !*rung)
342 .unwrap_or_else(PoisonError::into_inner)
343 .0
344 }
345 None => self
346 .bell
347 .wait_while(rung, |rung| !*rung)
348 .unwrap_or_else(PoisonError::into_inner),
349 };
350 *rung = false;
351 }
352}
353
354impl WakeSignal for HeadlessWake {
355 fn wake(&self) {
356 *self.rung.lock().unwrap_or_else(PoisonError::into_inner) = true;
357 self.bell.notify_all();
358 }
359}
360
361impl Default for HeadlessShell {
362 fn default() -> Self {
363 Self::new()
364 }
365}
366
367impl HeadlessShell {
368 #[must_use]
370 pub fn new() -> Self {
371 Self {
372 caps: ShellCaps::DESKTOP,
373 monitors: vec![MonitorInfo {
374 id: MonitorId(1),
375 name: "HEADLESS-1".to_string(),
376 bounds: PhysicalRect::new(
377 0,
378 0,
379 DEFAULT_MONITOR_SIZE.width,
380 DEFAULT_MONITOR_SIZE.height,
381 ),
382 work_area: PhysicalRect::new(
383 0,
384 0,
385 DEFAULT_MONITOR_SIZE.width,
386 DEFAULT_MONITOR_SIZE.height,
387 ),
388 scale_factor: 1.0,
389 refresh_millihertz: DEFAULT_REFRESH_MILLIHERTZ,
390 is_primary: true,
391 }],
392 next_monitor_id: 2,
393 windows: Pool::new(),
394 queue: VecDeque::new(),
395 clock: ManualTime::new(),
396 modifiers: Modifiers::empty(),
397 clipboard: Vec::new(),
398 next_request_id: 1,
399 reads: Vec::new(),
400 clipboard_readable: true,
401 clipboard_stalled: false,
402 clipboard_needs_interaction: false,
403 saw_interaction: false,
404 clipboard_deadline: DEFAULT_CLIPBOARD_DEADLINE,
405 waits: 0,
406 configure_delay: DEFAULT_CONFIGURE_DELAY,
407 wake: None,
408 }
409 }
410
411 #[must_use]
416 pub fn with_caps(mut self, caps: ShellCaps) -> Self {
417 self.caps = caps;
418 self
419 }
420
421 #[must_use]
436 pub fn with_monitors(mut self, monitors: Vec<MonitorInfo>) -> Self {
437 assert!(!monitors.is_empty(), "a shell needs at least one monitor");
438 for (index, monitor) in monitors.iter().enumerate() {
439 assert!(
440 monitor.id.0 != u32::MAX,
441 "monitor id {} leaves no id for the next hotplug",
442 monitor.id.0
443 );
444 assert!(
445 !monitors[..index].iter().any(|seen| seen.id == monitor.id),
446 "monitor id {} appears twice",
447 monitor.id.0
448 );
449 }
450 self.next_monitor_id = monitors
451 .iter()
452 .map(|monitor| monitor.id.0 + 1)
453 .max()
454 .unwrap_or(1);
455 self.monitors = monitors;
456 self
457 }
458
459 #[must_use]
464 pub fn with_scale_factor(mut self, scale_factor: f64) -> Self {
465 assert!(scale_factor > 0.0, "scale factor must be positive");
466 self.monitors[0].scale_factor = scale_factor;
467 self
468 }
469
470 #[must_use]
475 pub const fn with_configure_delay(mut self, pumps: u32) -> Self {
476 self.configure_delay = pumps;
477 self
478 }
479
480 pub const fn set_configure_delay(&mut self, pumps: u32) {
490 self.configure_delay = pumps;
491 }
492
493 pub fn set_caps(&mut self, caps: ShellCaps) {
499 self.caps = caps;
500 }
501
502 #[must_use]
504 pub fn now(&self) -> EventTime {
505 EventTime::from_duration(self.clock.elapsed())
506 }
507
508 pub fn advance(&mut self, delta: Duration) {
514 self.clock.advance(delta);
515 }
516
517 pub fn set_time(&mut self, now: Duration) {
525 self.clock.set(now);
526 }
527
528 pub fn set_modifiers(&mut self, modifiers: Modifiers) {
533 self.modifiers = modifiers;
534 }
535
536 #[must_use]
538 pub fn pending_events(&self) -> usize {
539 self.queue.len()
540 }
541
542 #[must_use]
548 pub const fn wait_count(&self) -> u32 {
549 self.waits
550 }
551
552 pub fn inject(&mut self, event: ShellEvent) {
560 self.queue.push_back(event);
561 }
562
563 pub fn resize(&mut self, window: WindowId, size: PhysicalSize) -> Result<(), ShellError> {
579 let state = self.window_mut(window)?;
580 let scale_factor = state.scale_factor();
581 let mode = state.settled_mode();
582 let constrained = state.requested_constraints.apply(size, scale_factor);
583 state.pending = None;
584 state.configuration = Some(WindowConfiguration {
585 size: constrained,
586 scale_factor,
587 mode,
588 });
589 self.queue.push_back(ShellEvent::Resized {
590 window,
591 size: constrained,
592 scale_factor,
593 });
594 Ok(())
595 }
596
597 pub fn change_scale_factor(
619 &mut self,
620 window: WindowId,
621 scale_factor: f64,
622 ) -> Result<(), ShellError> {
623 assert!(scale_factor > 0.0, "scale factor must be positive");
624 let state = self.window_mut(window)?;
625 let logical = state.base_size().to_logical(state.scale_factor());
626 let mode = state.settled_mode();
627 let size = state
628 .requested_constraints
629 .apply(logical.to_physical(scale_factor), scale_factor);
630 state.pending = None;
631 state.configuration = Some(WindowConfiguration {
632 size,
633 scale_factor,
634 mode,
635 });
636 self.queue.push_back(ShellEvent::ScaleFactorChanged {
637 window,
638 scale_factor,
639 size,
640 });
641 self.queue.push_back(ShellEvent::Resized {
642 window,
643 size,
644 scale_factor,
645 });
646 Ok(())
647 }
648
649 pub fn set_focus(&mut self, window: WindowId, focused: bool) -> Result<(), ShellError> {
655 self.window_mut(window)?.focused = focused;
656 self.queue.push_back(ShellEvent::Focus { window, focused });
657 Ok(())
658 }
659
660 pub fn request_close(&mut self, window: WindowId) -> Result<(), ShellError> {
669 self.window_mut(window)?.close_pending = true;
670 self.queue.push_back(ShellEvent::CloseRequested { window });
671 Ok(())
672 }
673
674 pub fn key(
687 &mut self,
688 window: WindowId,
689 key: KeyCode,
690 state: ButtonState,
691 ) -> Result<(), ShellError> {
692 self.key_with(window, key, state, false)
693 }
694
695 pub fn key_press(&mut self, window: WindowId, key: KeyCode) -> Result<(), ShellError> {
701 self.key(window, key, ButtonState::Pressed)
702 }
703
704 pub fn key_release(&mut self, window: WindowId, key: KeyCode) -> Result<(), ShellError> {
710 self.key(window, key, ButtonState::Released)
711 }
712
713 pub fn key_repeat(&mut self, window: WindowId, key: KeyCode) -> Result<(), ShellError> {
720 self.key_with(window, key, ButtonState::Pressed, true)
721 }
722
723 fn key_with(
724 &mut self,
725 window: WindowId,
726 key: KeyCode,
727 state: ButtonState,
728 repeat: bool,
729 ) -> Result<(), ShellError> {
730 let popup_input = self.window(window)?.popup_input;
731 self.saw_interaction = true;
732 let event = ShellEvent::Key {
733 window,
734 device: VIRTUAL_DEVICE,
735 time: self.now(),
736 scancode: synthetic_scancode(key),
737 key_code: Some(key),
738 keysym: synthetic_keysym(key),
739 state,
740 repeat,
741 modifiers: self.modifiers,
742 };
743 let dismissed = crate::role::escape_dismisses(popup_input, &event);
744 self.queue.push_back(event);
745 if dismissed && !self.window(window)?.close_pending {
747 self.request_close(window)?;
748 }
749 Ok(())
750 }
751
752 pub fn move_pointer(
763 &mut self,
764 window: WindowId,
765 position: PhysicalPoint,
766 raw_delta: (f64, f64),
767 ) -> Result<(), ShellError> {
768 let locked = self.window(window)?.pointer_mode == PointerMode::Locked;
769 let raw = if self.caps.contains(ShellCaps::RAW_POINTER_MOTION) {
770 Some(raw_delta)
771 } else {
772 None
773 };
774 self.saw_interaction = true;
775 let event = ShellEvent::PointerMotion {
776 window,
777 device: VIRTUAL_DEVICE,
778 time: self.now(),
779 abs: if locked { None } else { Some(position) },
780 raw_delta: raw,
781 };
782 self.queue.push_back(event);
783 Ok(())
784 }
785
786 pub fn set_pointer_focus(
792 &mut self,
793 window: WindowId,
794 entered: bool,
795 position: Option<PhysicalPoint>,
796 ) -> Result<(), ShellError> {
797 self.check_window(window)?;
798 let event = ShellEvent::PointerFocus {
799 window,
800 device: VIRTUAL_DEVICE,
801 time: self.now(),
802 entered,
803 position: if entered { position } else { None },
804 };
805 self.queue.push_back(event);
806 Ok(())
807 }
808
809 pub fn button(
815 &mut self,
816 window: WindowId,
817 button: PointerButton,
818 state: ButtonState,
819 position: Option<PhysicalPoint>,
820 ) -> Result<(), ShellError> {
821 self.check_window(window)?;
822 self.saw_interaction = true;
823 let event = ShellEvent::Button {
824 window,
825 device: VIRTUAL_DEVICE,
826 time: self.now(),
827 button,
828 state,
829 position,
830 modifiers: self.modifiers,
831 };
832 self.queue.push_back(event);
833 Ok(())
834 }
835
836 pub fn scroll(
842 &mut self,
843 window: WindowId,
844 delta: ScrollDelta,
845 position: Option<PhysicalPoint>,
846 ) -> Result<(), ShellError> {
847 self.check_window(window)?;
848 self.saw_interaction = true;
849 let event = ShellEvent::Wheel {
850 window,
851 device: VIRTUAL_DEVICE,
852 time: self.now(),
853 delta,
854 position,
855 modifiers: self.modifiers,
856 };
857 self.queue.push_back(event);
858 Ok(())
859 }
860
861 pub fn touch(
889 &mut self,
890 window: WindowId,
891 contact: ContactId,
892 phase: TouchPhase,
893 position: PhysicalPoint,
894 ) -> Result<(), ShellError> {
895 self.check_window(window)?;
896 if !self.caps.contains(ShellCaps::TOUCH) {
897 return Err(self.unsupported("touch"));
898 }
899 self.saw_interaction = true;
900 let event = ShellEvent::Touch {
901 window,
902 device: VIRTUAL_DEVICE,
903 time: self.now(),
904 contact,
905 phase,
906 position,
907 };
908 self.queue.push_back(event);
909 Ok(())
910 }
911
912 pub fn commit_text(&mut self, window: WindowId, text: &str) -> Result<(), ShellError> {
923 assert!(!text.is_empty(), "a text commit is never empty");
924 self.check_window(window)?;
925 let event = ShellEvent::TextCommit {
926 window,
927 time: self.now(),
928 text: text.to_string(),
929 };
930 self.queue.push_back(event);
931 Ok(())
932 }
933
934 pub fn drop_file(
942 &mut self,
943 window: WindowId,
944 path: impl Into<PathBuf>,
945 position: Option<PhysicalPoint>,
946 ) -> Result<(), ShellError> {
947 if !self.caps.contains(ShellCaps::DRAG_DROP) {
948 return Err(self.unsupported("drag and drop"));
949 }
950 if !self.window(window)?.accept_drops {
951 return Err(ShellError::Unsupported {
952 backend: ShellBackend::Headless,
953 what: "drops on a window created without accept_drops",
954 });
955 }
956 let event = ShellEvent::DroppedFile {
957 window,
958 time: self.now(),
959 path: path.into(),
960 position,
961 };
962 self.queue.push_back(event);
963 Ok(())
964 }
965
966 pub fn plug_monitor(&mut self, mut info: MonitorInfo) -> MonitorId {
973 let id = MonitorId(self.next_monitor_id);
974 self.next_monitor_id += 1;
975 info.id = id;
976 self.monitors.push(info);
977 self.queue.push_back(ShellEvent::MonitorsChanged);
978 id
979 }
980
981 pub fn unplug_monitor(&mut self, id: MonitorId) -> Result<(), ShellError> {
995 let index = self
996 .monitors
997 .iter()
998 .position(|monitor| monitor.id == id)
999 .ok_or(ShellError::NoSuchMonitor(id.0))?;
1000 assert!(
1001 self.monitors.len() > 1,
1002 "a shell needs at least one monitor"
1003 );
1004 let previous = self.monitors.clone();
1005 self.monitors.remove(index);
1006 crate::monitor::queue_removed(&previous, &self.monitors, &mut self.queue);
1007 let panels: Vec<WindowId> = self
1008 .windows
1009 .iter()
1010 .filter(|(_, state)| state.panel_monitor == Some(id) && !state.close_pending)
1011 .map(|(window, _)| window.cast())
1012 .collect();
1013 for panel in panels {
1014 self.request_close(panel)?;
1015 }
1016 self.queue.push_back(ShellEvent::MonitorsChanged);
1017 Ok(())
1018 }
1019
1020 pub fn title(&self, window: WindowId) -> Result<&str, ShellError> {
1031 Ok(self.window(window)?.title.as_str())
1032 }
1033
1034 pub fn cursor(&self, window: WindowId) -> Result<Option<CursorIcon>, ShellError> {
1042 Ok(self.window(window)?.cursor)
1043 }
1044
1045 pub fn text_input(&self, window: WindowId) -> Result<Option<TextPurpose>, ShellError> {
1054 Ok(self.window(window)?.text_input)
1055 }
1056
1057 pub fn input_region(&self, window: WindowId) -> Result<Option<&[PhysicalRect]>, ShellError> {
1068 Ok(self.window(window)?.input_region.as_deref())
1069 }
1070
1071 #[cfg(feature = "accesskit")]
1080 pub fn activate_accessibility(&mut self, window: WindowId) -> Result<(), ShellError> {
1081 let state = &mut self.window_mut(window)?.accessibility;
1082 if state.is_none() {
1083 *state = Some(Vec::new());
1084 self.queue
1085 .push_back(ShellEvent::AccessibilityActivated { window });
1086 }
1087 Ok(())
1088 }
1089
1090 #[cfg(feature = "accesskit")]
1100 pub fn deactivate_accessibility(&mut self, window: WindowId) -> Result<(), ShellError> {
1101 if self.window_mut(window)?.accessibility.take().is_some() {
1102 self.queue
1103 .push_back(ShellEvent::AccessibilityDeactivated { window });
1104 }
1105 Ok(())
1106 }
1107
1108 #[cfg(feature = "accesskit")]
1122 pub fn request_accessibility_action(
1123 &mut self,
1124 window: WindowId,
1125 request: accesskit::ActionRequest,
1126 ) -> Result<(), ShellError> {
1127 assert!(
1128 self.window(window)?.accessibility.is_some(),
1129 "no assistive technology is reading the window"
1130 );
1131 self.queue
1132 .push_back(ShellEvent::AccessibilityAction { window, request });
1133 Ok(())
1134 }
1135
1136 #[cfg(feature = "accesskit")]
1145 pub fn accessibility_updates(
1146 &self,
1147 window: WindowId,
1148 ) -> Result<Option<&[accesskit::TreeUpdate]>, ShellError> {
1149 Ok(self.window(window)?.accessibility.as_deref())
1150 }
1151
1152 #[must_use]
1156 pub fn clipboard_bytes(&self, mime: MimeType) -> Option<&[u8]> {
1157 self.clipboard
1158 .iter()
1159 .find(|(offered, _)| offered.matches(mime))
1160 .map(|(_, bytes)| bytes.as_slice())
1161 }
1162
1163 pub fn set_clipboard_readable(&mut self, readable: bool) {
1180 self.clipboard_readable = readable;
1181 }
1182
1183 pub fn set_clipboard_stalled(&mut self, stalled: bool) {
1192 self.clipboard_stalled = stalled;
1193 }
1194
1195 pub fn require_user_interaction(&mut self, required: bool) {
1222 self.clipboard_needs_interaction = required;
1223 }
1224
1225 pub fn clear_user_interaction(&mut self) {
1230 self.saw_interaction = false;
1231 }
1232
1233 pub fn clipboard_deadline(&mut self, pumps: u32) {
1239 self.clipboard_deadline = pumps;
1240 }
1241
1242 #[must_use]
1247 pub fn held_clipboard_reads(&self) -> usize {
1248 self.reads.len()
1249 }
1250
1251 pub fn set_foreign_clipboard(&mut self, offers: Vec<(ReceivedMime, Vec<u8>)>) {
1259 self.clipboard = offers;
1260 }
1261
1262 fn answerable(&self) -> bool {
1268 self.clipboard_readable && !self.clipboard_stalled
1269 }
1270
1271 fn answer(&self, window: WindowId, request: ClipboardRequestId, mime: MimeType) -> ShellEvent {
1273 let found = self
1278 .clipboard
1279 .iter()
1280 .find(|(offered, _)| offered.matches(mime));
1281 let (mime, content) = match found {
1282 Some((offered, bytes)) => (offered.clone(), ClipboardContent::Bytes(bytes.clone())),
1283 None => (ReceivedMime::from(mime), ClipboardContent::Empty),
1286 };
1287 ShellEvent::ClipboardData {
1288 window,
1289 request,
1290 mime,
1291 content,
1292 }
1293 }
1294
1295 fn resolve_reads(&mut self) {
1302 if self.reads.is_empty() {
1303 return;
1304 }
1305 let answerable = self.answerable();
1306 let mut held = Vec::with_capacity(self.reads.len());
1307 for mut read in core::mem::take(&mut self.reads) {
1308 if self.window(read.window).is_err() {
1312 continue;
1313 }
1314 if answerable {
1315 let answer = self.answer(read.window, read.request, read.mime);
1316 self.queue.push_back(answer);
1317 continue;
1318 }
1319 if read.countdown > 0 {
1320 read.countdown -= 1;
1321 held.push(read);
1322 continue;
1323 }
1324 self.queue.push_back(ShellEvent::ClipboardData {
1327 window: read.window,
1328 request: read.request,
1329 mime: ReceivedMime::from(read.mime),
1330 content: ClipboardContent::Unavailable,
1331 });
1332 }
1333 self.reads = held;
1334 }
1335
1336 fn deliver_due_configures(&mut self) {
1343 let mut due: Vec<(WindowId, PendingConfigure, Option<f64>)> = Vec::new();
1347 for (handle, state) in self.windows.iter_mut() {
1348 let Some(pending) = state.pending.as_mut() else {
1349 continue;
1350 };
1351 if pending.countdown > 0 {
1352 pending.countdown -= 1;
1353 continue;
1354 }
1355 let pending = state.pending.take().expect("checked just above");
1356 let previous_scale = state.configuration.map(|config| config.scale_factor);
1357 state.configuration = Some(WindowConfiguration {
1358 size: pending.size,
1359 scale_factor: pending.scale_factor,
1360 mode: pending.mode,
1361 });
1362 due.push((handle.cast(), pending, previous_scale));
1363 }
1364
1365 for (window, pending, previous_scale) in due {
1366 if previous_scale.is_some_and(|previous| previous != pending.scale_factor) {
1370 self.queue.push_back(ShellEvent::ScaleFactorChanged {
1371 window,
1372 scale_factor: pending.scale_factor,
1373 size: pending.size,
1374 });
1375 }
1376 self.queue.push_back(ShellEvent::Resized {
1377 window,
1378 size: pending.size,
1379 scale_factor: pending.scale_factor,
1380 });
1381 }
1382 }
1383
1384 fn window(&self, window: WindowId) -> Result<&HeadlessWindow, ShellError> {
1385 self.windows
1386 .get(window.cast())
1387 .ok_or_else(|| ShellError::invalid_window(window))
1388 }
1389
1390 fn window_mut(&mut self, window: WindowId) -> Result<&mut HeadlessWindow, ShellError> {
1391 self.windows
1392 .get_mut(window.cast())
1393 .ok_or_else(|| ShellError::invalid_window(window))
1394 }
1395
1396 fn check_window(&self, window: WindowId) -> Result<(), ShellError> {
1397 self.window(window).map(|_| ())
1398 }
1399
1400 fn unsupported(&self, what: &'static str) -> ShellError {
1401 ShellError::Unsupported {
1402 backend: ShellBackend::Headless,
1403 what,
1404 }
1405 }
1406}
1407
1408fn synthetic_scancode(key: KeyCode) -> Scancode {
1412 let index = KeyCode::ALL
1413 .iter()
1414 .position(|candidate| *candidate == key)
1415 .unwrap_or(0);
1416 Scancode(index as u32 + 8)
1419}
1420
1421fn synthetic_keysym(key: KeyCode) -> Keysym {
1423 let name = key.as_str();
1424 if let Some(letter) = name.strip_prefix("Key") {
1425 return Keysym::from_char(letter.to_ascii_lowercase().chars().next().unwrap_or('?'));
1427 }
1428 if let Some(digit) = name.strip_prefix("Digit") {
1429 return Keysym::from_char(digit.chars().next().unwrap_or('?'));
1430 }
1431 match key {
1432 KeyCode::Space => Keysym::from_char(' '),
1433 KeyCode::Minus => Keysym::from_char('-'),
1434 KeyCode::Equal => Keysym::from_char('='),
1435 KeyCode::Comma => Keysym::from_char(','),
1436 KeyCode::Period => Keysym::from_char('.'),
1437 KeyCode::Slash => Keysym::from_char('/'),
1438 KeyCode::Semicolon => Keysym::from_char(';'),
1439 KeyCode::Quote => Keysym::from_char('\''),
1440 KeyCode::Backslash => Keysym::from_char('\\'),
1441 KeyCode::BracketLeft => Keysym::from_char('['),
1442 KeyCode::BracketRight => Keysym::from_char(']'),
1443 KeyCode::Backquote => Keysym::from_char('`'),
1444 _ => crate::keysym::named_keysym(key).unwrap_or(Keysym::NONE),
1448 }
1449}
1450
1451impl Shell for HeadlessShell {
1452 fn backend(&self) -> ShellBackend {
1453 ShellBackend::Headless
1454 }
1455
1456 fn caps(&self) -> ShellCaps {
1457 self.caps
1458 }
1459
1460 fn create_window(&mut self, desc: &WindowDesc<'_>) -> Result<WindowId, ShellError> {
1461 crate::role::check(desc, self.caps(), self.backend(), |monitor| {
1462 self.monitor(monitor).is_some()
1463 })?;
1464 if !self.windows.is_empty() && !self.caps.contains(ShellCaps::MULTI_WINDOW) {
1465 return Err(self.unsupported("a second window"));
1466 }
1467 if let DisplayMode::Borderless {
1468 monitor: Some(monitor),
1469 } = desc.mode
1470 && self.monitor(monitor).is_none()
1471 {
1472 return Err(ShellError::NoSuchMonitor(monitor.0));
1473 }
1474
1475 let (size, scale_factor) = match desc.mode {
1487 DisplayMode::Windowed => {
1488 let scale_factor = self.monitors[0].scale_factor;
1489 (
1490 desc.constraints
1491 .apply(desc.size.to_physical(scale_factor), scale_factor),
1492 scale_factor,
1493 )
1494 }
1495 DisplayMode::Borderless { monitor } => {
1496 let monitor = monitor
1497 .and_then(|id| self.monitor(id))
1498 .unwrap_or(&self.monitors[0]);
1499 (monitor.size(), monitor.scale_factor)
1500 }
1501 };
1502
1503 let (parent, popup_input, panel_monitor) = match desc.role {
1504 crate::WindowRole::Popup(popup) => {
1505 self.window(popup.parent)?;
1506 (Some(popup.parent), Some(popup.input), None)
1507 }
1508 crate::WindowRole::Panel(panel) => (None, None, panel.monitor),
1509 crate::WindowRole::Toplevel => (None, None, None),
1510 };
1511 let countdown = self.configure_delay;
1512 let window: WindowId = self
1513 .windows
1514 .insert(HeadlessWindow {
1515 title: desc.title.to_string(),
1516 configuration: None,
1519 pending: Some(PendingConfigure {
1520 size,
1521 scale_factor,
1522 mode: desc.mode,
1523 countdown,
1524 }),
1525 requested_mode: desc.mode,
1526 requested_constraints: desc.constraints,
1527 focused: false,
1528 visible: desc.visible,
1529 pointer_mode: PointerMode::Free,
1530 cursor: Some(CursorIcon::Default),
1531 close_pending: false,
1532 accept_drops: desc.accept_drops,
1533 parent,
1534 popup_input,
1535 panel_monitor,
1536 text_input: None,
1537 input_region: None,
1538 #[cfg(feature = "accesskit")]
1539 accessibility: None,
1540 })
1541 .cast();
1542 Ok(window)
1543 }
1544
1545 fn destroy_window(&mut self, window: WindowId) -> Result<(), ShellError> {
1549 self.window(window)?;
1550 let popups: Vec<WindowId> = self
1551 .windows
1552 .iter()
1553 .filter(|(_, state)| state.parent == Some(window))
1554 .map(|(id, _)| id.cast())
1555 .collect();
1556 for popup in popups {
1557 self.destroy_window(popup)?;
1558 }
1559 self.windows
1560 .remove(window.cast())
1561 .ok_or_else(|| ShellError::invalid_window(window))?;
1562 self.queue.push_back(ShellEvent::WindowDestroyed { window });
1563 Ok(())
1564 }
1565
1566 fn window_state(&self, window: WindowId) -> Result<WindowState, ShellError> {
1567 let state = self.window(window)?;
1568 Ok(WindowState {
1569 configuration: state.configuration,
1570 requested_mode: state.requested_mode,
1571 requested_constraints: state.requested_constraints,
1572 focused: state.focused,
1573 visible: state.visible,
1574 pointer_mode: state.pointer_mode,
1575 close_pending: state.close_pending,
1576 })
1577 }
1578
1579 fn set_title(&mut self, window: WindowId, title: &str) -> Result<(), ShellError> {
1580 self.window_mut(window)?.title = title.to_string();
1581 Ok(())
1582 }
1583
1584 fn set_visible(&mut self, window: WindowId, visible: bool) -> Result<(), ShellError> {
1585 self.window_mut(window)?.visible = visible;
1586 Ok(())
1587 }
1588
1589 fn set_mode(&mut self, window: WindowId, mode: DisplayMode) -> Result<(), ShellError> {
1590 let target = match mode {
1591 DisplayMode::Windowed => None,
1592 DisplayMode::Borderless { monitor } => {
1593 let monitor = match monitor {
1594 Some(id) => self
1595 .monitor(id)
1596 .ok_or(ShellError::NoSuchMonitor(id.0))?
1597 .clone(),
1598 None => self.monitors[0].clone(),
1599 };
1600 Some(monitor)
1601 }
1602 };
1603 let countdown = self.configure_delay;
1604
1605 let state = self.window_mut(window)?;
1606 state.requested_mode = mode;
1612 let (size, scale_factor) = match &target {
1613 Some(monitor) => (monitor.size(), monitor.scale_factor),
1614 None => (state.base_size(), state.scale_factor()),
1618 };
1619 state.pending = Some(PendingConfigure {
1620 size,
1621 scale_factor,
1622 mode,
1623 countdown,
1624 });
1625 Ok(())
1626 }
1627
1628 fn set_constraints(
1629 &mut self,
1630 window: WindowId,
1631 constraints: SizeConstraints,
1632 ) -> Result<(), ShellError> {
1633 let countdown = self.configure_delay;
1634 let state = self.window_mut(window)?;
1635 state.requested_constraints = constraints;
1636
1637 if let Some(pending) = state.pending.as_mut() {
1644 pending.size = constraints.apply(pending.size, pending.scale_factor);
1645 return Ok(());
1646 }
1647
1648 if let Some(config) = state.configuration {
1652 let constrained = constraints.apply(config.size, config.scale_factor);
1653 if constrained != config.size {
1654 state.pending = Some(PendingConfigure {
1655 size: constrained,
1656 scale_factor: config.scale_factor,
1657 mode: config.mode,
1658 countdown,
1659 });
1660 }
1661 }
1662 Ok(())
1663 }
1664
1665 fn reposition_popup(
1669 &mut self,
1670 popup: WindowId,
1671 anchor: LogicalRect,
1672 size: LogicalSize,
1673 ) -> Result<(), ShellError> {
1674 let _ = anchor;
1675 let countdown = self.configure_delay;
1676 let state = self.window_mut(popup)?;
1677 if state.parent.is_none() {
1678 return Err(crate::role::not_a_popup(popup));
1679 }
1680 if let Some(pending) = state.pending.as_mut() {
1681 pending.size = size.to_physical(pending.scale_factor);
1682 } else if let Some(config) = state.configuration {
1683 state.pending = Some(PendingConfigure {
1684 size: size.to_physical(config.scale_factor),
1685 scale_factor: config.scale_factor,
1686 mode: config.mode,
1687 countdown,
1688 });
1689 }
1690 Ok(())
1691 }
1692
1693 fn monitors(&self) -> &[MonitorInfo] {
1694 &self.monitors
1695 }
1696
1697 fn pump(&mut self, sink: &mut dyn FnMut(ShellEvent)) {
1698 self.keep_alive();
1699 for _ in 0..self.queue.len() {
1704 let Some(event) = self.queue.pop_front() else {
1705 break;
1706 };
1707 sink(event);
1708 }
1709 }
1710
1711 fn keep_alive(&mut self) {
1719 self.deliver_due_configures();
1720 self.resolve_reads();
1721 }
1722
1723 fn wait_events(&mut self, timeout: Option<Duration>) {
1742 self.waits += 1;
1743 if !self.caps.contains(ShellCaps::EVENT_WAIT) || !self.queue.is_empty() {
1744 return;
1745 }
1746 match (&self.wake, timeout) {
1747 (Some(wake), timeout) => wake.wait(timeout),
1748 (None, Some(timeout)) => std::thread::sleep(timeout),
1749 (None, None) => {}
1750 }
1751 }
1752
1753 fn waker(&mut self) -> Option<ShellWaker> {
1756 let wake = self.wake.get_or_insert_with(Arc::default);
1757 Some(ShellWaker::new(Arc::clone(wake) as Arc<dyn WakeSignal>))
1758 }
1759
1760 fn align_event_clock(&mut self, elapsed: Duration) {
1767 self.set_time(elapsed);
1768 }
1769
1770 fn surface_target(&self, window: WindowId) -> Result<SurfaceTarget, ShellError> {
1771 self.window(window)?;
1777 Ok(SurfaceTarget::Offscreen)
1778 }
1779
1780 fn set_pointer_mode(&mut self, window: WindowId, mode: PointerMode) -> Result<(), ShellError> {
1781 if !self.caps.contains(mode.required_cap()) {
1782 return Err(self.unsupported(mode.as_str()));
1783 }
1784 self.window_mut(window)?.pointer_mode = mode;
1785 Ok(())
1786 }
1787
1788 fn set_text_input(
1792 &mut self,
1793 window: WindowId,
1794 field: Option<TextPurpose>,
1795 ) -> Result<(), ShellError> {
1796 self.window_mut(window)?.text_input = field;
1797 Ok(())
1798 }
1799
1800 fn set_input_region(
1805 &mut self,
1806 window: WindowId,
1807 region: Option<&[PhysicalRect]>,
1808 ) -> Result<(), ShellError> {
1809 self.window_mut(window)?.input_region = region.map(<[PhysicalRect]>::to_vec);
1810 Ok(())
1811 }
1812
1813 #[cfg(feature = "accesskit")]
1814 fn update_accessibility(
1815 &mut self,
1816 window: WindowId,
1817 update: accesskit::TreeUpdate,
1818 ) -> Result<(), ShellError> {
1819 if let Some(updates) = &mut self.window_mut(window)?.accessibility {
1820 updates.push(update);
1821 }
1822 Ok(())
1823 }
1824
1825 fn set_cursor(
1826 &mut self,
1827 window: WindowId,
1828 cursor: Option<CursorIcon>,
1829 ) -> Result<(), ShellError> {
1830 self.window_mut(window)?.cursor = cursor;
1831 Ok(())
1832 }
1833
1834 fn warp_pointer(
1835 &mut self,
1836 window: WindowId,
1837 position: PhysicalPoint,
1838 ) -> Result<(), ShellError> {
1839 if !self.caps.contains(ShellCaps::POINTER_WARP) {
1840 return Err(self.unsupported("pointer warp"));
1841 }
1842 self.check_window(window)?;
1843 let event = ShellEvent::PointerMotion {
1846 window,
1847 device: VIRTUAL_DEVICE,
1848 time: self.now(),
1849 abs: Some(position),
1850 raw_delta: None,
1851 };
1852 self.queue.push_back(event);
1853 Ok(())
1854 }
1855
1856 fn reply_close_request(
1857 &mut self,
1858 window: WindowId,
1859 reply: CloseReply,
1860 ) -> Result<(), ShellError> {
1861 let state = self.window_mut(window)?;
1862 if !state.close_pending {
1863 return Err(ShellError::NoPendingCloseRequest { window });
1864 }
1865 state.close_pending = false;
1866 match reply {
1867 CloseReply::Close => self.destroy_window(window),
1868 CloseReply::Keep => Ok(()),
1869 }
1870 }
1871
1872 fn clipboard_offer(
1883 &mut self,
1884 window: WindowId,
1885 offers: &[ClipboardOffer<'_>],
1886 ) -> Result<(), ShellError> {
1887 if !self.caps.contains(ShellCaps::CLIPBOARD) {
1888 return Err(self.unsupported("clipboard"));
1889 }
1890 self.check_window(window)?;
1891 if self.clipboard_needs_interaction && !self.saw_interaction {
1892 return Err(ShellError::NeedsUserInteraction {
1893 backend: ShellBackend::Headless,
1894 what: "claiming the clipboard",
1895 });
1896 }
1897 self.clipboard = offers
1898 .iter()
1899 .map(|offer| (ReceivedMime::from(offer.mime), offer.bytes.to_vec()))
1900 .collect();
1901 Ok(())
1902 }
1903
1904 fn clipboard_request(
1918 &mut self,
1919 window: WindowId,
1920 mime: MimeType,
1921 ) -> Result<ClipboardRequestId, ShellError> {
1922 if !self.caps.contains(ShellCaps::CLIPBOARD) {
1923 return Err(self.unsupported("clipboard"));
1924 }
1925 self.check_window(window)?;
1926 let request = ClipboardRequestId(self.next_request_id);
1927 self.next_request_id += 1;
1928 if self.answerable() {
1929 let answer = self.answer(window, request, mime);
1930 self.queue.push_back(answer);
1934 } else {
1935 self.reads.push(PendingRead {
1936 window,
1937 request,
1938 mime,
1939 countdown: self.clipboard_deadline,
1940 });
1941 }
1942 Ok(request)
1943 }
1944
1945 fn clipboard_readable(&self, window: WindowId) -> bool {
1951 self.caps.contains(ShellCaps::CLIPBOARD) && self.window(window).is_ok() && self.answerable()
1952 }
1953}
1954
1955#[cfg(test)]
1956mod tests {
1957 use super::*;
1958 use crate::{AspectRatio, LogicalSize};
1959
1960 fn shell_with_window() -> (HeadlessShell, WindowId) {
1962 let mut shell = HeadlessShell::new();
1963 let window = shell
1964 .create_window(&WindowDesc::default())
1965 .expect("headless window creation never fails");
1966 (shell, window)
1967 }
1968
1969 fn shell_with_configured_window() -> (HeadlessShell, WindowId) {
1971 let (mut shell, window) = shell_with_window();
1972 settle(&mut shell);
1973 assert!(shell.window_state(window).expect("state").is_configured());
1974 (shell, window)
1975 }
1976
1977 fn settle(shell: &mut HeadlessShell) {
1979 for _ in 0..8 {
1980 let mut seen = 0;
1981 shell.pump(&mut |_| seen += 1);
1982 if seen == 0 && shell.windows.iter().all(|(_, w)| w.pending.is_none()) {
1983 return;
1984 }
1985 }
1986 panic!("the shell never settled");
1987 }
1988
1989 fn drain(shell: &mut HeadlessShell) -> Vec<ShellEvent> {
1990 let mut events = Vec::new();
1991 shell.pump(&mut |event| events.push(event));
1992 events
1993 }
1994
1995 fn names(events: &[ShellEvent]) -> Vec<&'static str> {
1996 events.iter().map(ShellEvent::name).collect()
1997 }
1998
1999 fn create_swapchain(state: &WindowState) -> Result<PhysicalSize, &'static str> {
2002 let size = state.size().ok_or("window is not configured yet")?;
2003 if size.is_empty() {
2004 return Err("zero-extent swapchain");
2005 }
2006 Ok(size)
2007 }
2008
2009 #[cfg(feature = "accesskit")]
2014 #[test]
2015 fn a_tree_reaches_the_reader_only_while_it_reads() {
2016 use accesskit::{Action, ActionRequest, Node, NodeId, Role, TreeId, TreeInfo, TreeUpdate};
2017
2018 let (mut shell, window) = shell_with_configured_window();
2019 let update = |label: &str| TreeUpdate {
2020 nodes: vec![(NodeId(1), {
2021 let mut node = Node::new(Role::Window);
2022 node.set_label(label);
2023 node
2024 })],
2025 tree: Some(TreeInfo::new(NodeId(1))),
2026 tree_id: TreeId::ROOT,
2027 focus: NodeId(1),
2028 };
2029
2030 shell
2031 .update_accessibility(window, update("unread"))
2032 .expect("update");
2033 assert_eq!(shell.accessibility_updates(window).expect("updates"), None);
2034
2035 shell.activate_accessibility(window).expect("activate");
2036 shell
2037 .activate_accessibility(window)
2038 .expect("activate twice");
2039 shell
2040 .update_accessibility(window, update("read"))
2041 .expect("update");
2042 let click = ActionRequest {
2043 action: Action::Click,
2044 target_tree: TreeId::ROOT,
2045 target_node: NodeId(1),
2046 data: None,
2047 };
2048 shell
2049 .request_accessibility_action(window, click.clone())
2050 .expect("action");
2051 assert_eq!(
2052 shell.accessibility_updates(window).expect("updates"),
2053 Some(&[update("read")][..])
2054 );
2055 let events = drain(&mut shell);
2056 assert_eq!(
2057 names(&events),
2058 ["AccessibilityActivated", "AccessibilityAction"]
2059 );
2060 assert_eq!(
2061 events[1],
2062 ShellEvent::AccessibilityAction {
2063 window,
2064 request: click
2065 }
2066 );
2067
2068 shell.deactivate_accessibility(window).expect("deactivate");
2069 shell
2070 .deactivate_accessibility(window)
2071 .expect("deactivate twice");
2072 shell
2073 .update_accessibility(window, update("gone"))
2074 .expect("update");
2075 assert_eq!(shell.accessibility_updates(window).expect("updates"), None);
2076 assert_eq!(names(&drain(&mut shell)), ["AccessibilityDeactivated"]);
2077 }
2078
2079 #[test]
2082 fn a_new_window_has_no_size_until_a_later_pump() {
2083 let (mut shell, window) = shell_with_window();
2084
2085 let state = shell.window_state(window).expect("state");
2088 assert!(!state.is_configured());
2089 assert_eq!(state.size(), None);
2090 assert_eq!(state.scale_factor(), None);
2091 assert_eq!(state.effective_mode(), None);
2092 assert_eq!(state.requested_mode, DisplayMode::Windowed);
2093
2094 assert_eq!(drain(&mut shell), Vec::new());
2096 assert!(!shell.window_state(window).expect("state").is_configured());
2097
2098 let events = drain(&mut shell);
2100 assert_eq!(
2101 events,
2102 vec![ShellEvent::Resized {
2103 window,
2104 size: PhysicalSize::new(1280, 720),
2105 scale_factor: 1.0,
2106 }]
2107 );
2108 let state = shell.window_state(window).expect("state");
2109 assert_eq!(state.size(), Some(PhysicalSize::new(1280, 720)));
2110 assert_eq!(state.scale_factor(), Some(1.0));
2111 assert_eq!(state.effective_mode(), Some(DisplayMode::Windowed));
2112 assert!(state.mode_request_honoured());
2113 }
2114
2115 #[test]
2119 fn keep_alive_takes_a_turn_and_keeps_everything_for_the_next_pump() {
2120 let (mut shell, window) = shell_with_window();
2121 shell.inject(ShellEvent::CloseRequested { window });
2122
2123 for _ in 0..=DEFAULT_CONFIGURE_DELAY {
2125 shell.keep_alive();
2126 }
2127 assert!(
2128 shell.window_state(window).expect("state").is_configured(),
2129 "every turn the configure waits for was taken by keep_alive"
2130 );
2131
2132 assert_eq!(
2133 drain(&mut shell),
2134 vec![
2135 ShellEvent::CloseRequested { window },
2136 ShellEvent::Resized {
2137 window,
2138 size: PhysicalSize::new(1280, 720),
2139 scale_factor: 1.0,
2140 },
2141 ],
2142 "nothing keep_alive saw was delivered or dropped"
2143 );
2144 assert_eq!(drain(&mut shell), Vec::new(), "and each arrived once");
2145 }
2146
2147 #[test]
2148 fn a_consumer_that_skips_the_wait_gets_a_clean_error_not_a_wrong_extent() {
2149 let (mut shell, window) = shell_with_window();
2150
2151 assert_eq!(
2153 create_swapchain(&shell.window_state(window).expect("state")),
2154 Err("window is not configured yet")
2155 );
2156
2157 let target = shell.surface_target(window).expect("the handle exists");
2163 assert_eq!(target, SurfaceTarget::Offscreen);
2164
2165 let size = loop {
2167 shell.pump(&mut |_| {});
2168 if let Ok(size) = create_swapchain(&shell.window_state(window).expect("state")) {
2169 break size;
2170 }
2171 };
2172 assert_eq!(size, PhysicalSize::new(1280, 720));
2173 assert_eq!(
2176 shell.surface_target(window).expect("target"),
2177 target,
2178 "the target is invariant under a configure"
2179 );
2180 }
2181
2182 #[test]
2183 fn the_configure_delay_is_a_knob() {
2184 let mut shell = HeadlessShell::new().with_configure_delay(3);
2185 let window = shell.create_window(&WindowDesc::default()).expect("window");
2186 for pump in 0..3 {
2187 assert_eq!(drain(&mut shell), Vec::new(), "pump {pump}");
2188 assert!(!shell.window_state(window).expect("state").is_configured());
2189 }
2190 assert_eq!(names(&drain(&mut shell)), ["Resized"]);
2191 assert!(shell.window_state(window).expect("state").is_configured());
2192
2193 let mut shell = HeadlessShell::new().with_configure_delay(0);
2196 let window = shell.create_window(&WindowDesc::default()).expect("window");
2197 assert_eq!(names(&drain(&mut shell)), ["Resized"]);
2198 assert!(shell.window_state(window).expect("state").is_configured());
2199
2200 shell.set_configure_delay(2);
2201 shell
2202 .set_mode(window, DisplayMode::Borderless { monitor: None })
2203 .expect("request");
2204 assert_eq!(drain(&mut shell), Vec::new());
2205 assert_eq!(drain(&mut shell), Vec::new());
2206 assert_eq!(names(&drain(&mut shell)), ["Resized"]);
2207 }
2208
2209 #[test]
2210 fn set_mode_is_a_request_whose_effect_arrives_later() {
2211 let (mut shell, window) = shell_with_configured_window();
2212 shell
2213 .set_mode(window, DisplayMode::Borderless { monitor: None })
2214 .expect("request");
2215
2216 let state = shell.window_state(window).expect("state");
2218 assert_eq!(
2219 state.requested_mode,
2220 DisplayMode::Borderless { monitor: None }
2221 );
2222 assert_eq!(
2223 state.effective_mode(),
2224 Some(DisplayMode::Windowed),
2225 "still windowed until the window system says otherwise"
2226 );
2227 assert!(
2228 !state.mode_request_honoured(),
2229 "a UI toggle reading the request instead of the effect would lie here"
2230 );
2231 assert_eq!(state.size(), Some(PhysicalSize::new(1280, 720)));
2232
2233 settle(&mut shell);
2234 let state = shell.window_state(window).expect("state");
2235 assert_eq!(
2236 state.effective_mode(),
2237 Some(DisplayMode::Borderless { monitor: None })
2238 );
2239 assert!(state.mode_request_honoured());
2240 assert_eq!(state.size(), Some(PhysicalSize::new(1920, 1080)));
2241 }
2242
2243 #[test]
2244 fn a_refused_mode_is_expressible_through_injection() {
2245 let (mut shell, window) = shell_with_configured_window();
2248 shell
2249 .set_mode(window, DisplayMode::Borderless { monitor: None })
2250 .expect("request");
2251 shell
2253 .resize(window, PhysicalSize::new(1280, 720))
2254 .expect("answer");
2255 let state = shell.window_state(window).expect("state");
2256 assert_eq!(state.effective_mode(), Some(DisplayMode::Windowed));
2257 assert!(!state.mode_request_honoured());
2258 }
2259
2260 #[test]
2263 fn a_repositioned_popup_is_configured_at_its_new_size() {
2264 let (mut shell, window) = shell_with_configured_window();
2265 let anchor = crate::LogicalRect::new(
2266 crate::LogicalPoint::new(10.0, 10.0),
2267 LogicalSize::new(100.0, 20.0),
2268 );
2269 let popup = shell
2270 .create_window(&WindowDesc {
2271 size: LogicalSize::new(200.0, 150.0),
2272 role: crate::WindowRole::Popup(crate::PopupDesc {
2273 parent: window,
2274 anchor,
2275 placement: crate::PopupPlacement::Below,
2276 input: crate::PopupInput::Pointer,
2277 }),
2278 ..WindowDesc::default()
2279 })
2280 .expect("a popup");
2281 settle(&mut shell);
2282 let scale = shell
2283 .window_state(popup)
2284 .expect("live")
2285 .scale_factor()
2286 .expect("configured");
2287
2288 let moved = LogicalSize::new(240.0, 90.0);
2289 shell
2290 .reposition_popup(popup, anchor, moved)
2291 .expect("reposition");
2292 settle(&mut shell);
2293 assert_eq!(
2294 shell.window_state(popup).expect("live").size(),
2295 Some(moved.to_physical(scale))
2296 );
2297 assert!(matches!(
2298 shell.reposition_popup(window, anchor, moved),
2299 Err(ShellError::InvalidDescriptor(_))
2300 ));
2301 }
2302
2303 #[test]
2307 fn escape_on_a_grabbing_popup_is_delivered_then_asks_it_to_close() {
2308 let (mut shell, window) = shell_with_configured_window();
2309 let popup_of = |shell: &mut HeadlessShell, input| {
2310 shell
2311 .create_window(&WindowDesc {
2312 role: crate::WindowRole::Popup(crate::PopupDesc {
2313 parent: window,
2314 anchor: crate::LogicalRect::default(),
2315 placement: crate::PopupPlacement::Below,
2316 input,
2317 }),
2318 ..WindowDesc::default()
2319 })
2320 .expect("a popup")
2321 };
2322 let menu = popup_of(&mut shell, crate::PopupInput::Grab);
2323 let tooltip = popup_of(&mut shell, crate::PopupInput::Pointer);
2324 settle(&mut shell);
2325 let is_close = |event: &ShellEvent| matches!(event, ShellEvent::CloseRequested { .. });
2326
2327 shell.key_press(menu, KeyCode::Escape).expect("press");
2328 let events = drain(&mut shell);
2329 assert!(
2330 matches!(
2331 events.as_slice(),
2332 [
2333 ShellEvent::Key {
2334 key_code: Some(KeyCode::Escape),
2335 ..
2336 },
2337 ShellEvent::CloseRequested { window: closed },
2338 ] if *closed == menu
2339 ),
2340 "the key, then the request: {events:?}"
2341 );
2342 shell.key_press(menu, KeyCode::Escape).expect("press");
2344 shell.key_repeat(menu, KeyCode::Escape).expect("repeat");
2345 assert!(!drain(&mut shell).iter().any(is_close));
2346
2347 shell
2349 .reply_close_request(menu, CloseReply::Keep)
2350 .expect("keep");
2351 shell.key_release(menu, KeyCode::Escape).expect("release");
2352 shell.key_press(menu, KeyCode::Enter).expect("press");
2353 assert!(!drain(&mut shell).iter().any(is_close), "not these");
2354 shell.key_press(menu, KeyCode::Escape).expect("press");
2355 assert!(drain(&mut shell).iter().any(is_close));
2356
2357 shell.key_press(tooltip, KeyCode::Escape).expect("press");
2358 shell.key_press(window, KeyCode::Escape).expect("press");
2359 assert!(!drain(&mut shell).iter().any(is_close));
2360 }
2361
2362 #[test]
2363 fn set_constraints_is_a_request_too() {
2364 let (mut shell, window) = shell_with_configured_window();
2365 let constraints = SizeConstraints::min(LogicalSize::new(1600.0, 900.0));
2366 shell.set_constraints(window, constraints).expect("request");
2367
2368 let state = shell.window_state(window).expect("state");
2369 assert_eq!(state.requested_constraints, constraints);
2370 assert_eq!(
2371 state.size(),
2372 Some(PhysicalSize::new(1280, 720)),
2373 "the size only changes when the window system says so"
2374 );
2375
2376 settle(&mut shell);
2377 assert_eq!(
2378 shell.window_state(window).expect("state").size(),
2379 Some(PhysicalSize::new(1600, 900))
2380 );
2381 }
2382
2383 #[test]
2384 fn constraints_do_not_cancel_a_mode_request_that_has_not_landed() {
2385 let (mut shell, window) = shell_with_configured_window();
2390 shell
2391 .set_mode(window, DisplayMode::Borderless { monitor: None })
2392 .expect("mode request");
2393 shell
2394 .set_constraints(window, SizeConstraints::min(LogicalSize::new(320.0, 180.0)))
2395 .expect("constraint request");
2396 settle(&mut shell);
2397
2398 let state = shell.window_state(window).expect("state");
2399 assert_eq!(
2400 state.effective_mode(),
2401 Some(DisplayMode::Borderless { monitor: None })
2402 );
2403 assert!(state.mode_request_honoured());
2404 }
2405
2406 #[test]
2407 fn a_borderless_window_reports_its_own_monitors_scale() {
2408 let mut shell = HeadlessShell::new();
2413 let hidpi = shell.plug_monitor(MonitorInfo {
2414 id: MonitorId(0),
2415 name: "HEADLESS-2".to_string(),
2416 bounds: PhysicalRect::new(1920, 0, 2560, 1440),
2417 work_area: PhysicalRect::new(1920, 0, 2560, 1440),
2418 scale_factor: 2.0,
2419 refresh_millihertz: 143_998,
2420 is_primary: false,
2421 });
2422 let window = shell
2423 .create_window(&WindowDesc {
2424 mode: DisplayMode::Borderless {
2425 monitor: Some(hidpi),
2426 },
2427 ..WindowDesc::default()
2428 })
2429 .expect("window");
2430 settle(&mut shell);
2431
2432 let state = shell.window_state(window).expect("state");
2433 assert_eq!(state.size(), Some(PhysicalSize::new(2560, 1440)));
2434 assert_eq!(
2435 state.scale_factor(),
2436 Some(2.0),
2437 "the scale is the borderless monitor's, not the primary's"
2438 );
2439 let other = shell
2442 .create_window(&WindowDesc::default())
2443 .expect("second window");
2444 settle(&mut shell);
2445 shell
2446 .set_mode(
2447 other,
2448 DisplayMode::Borderless {
2449 monitor: Some(hidpi),
2450 },
2451 )
2452 .expect("request");
2453 settle(&mut shell);
2454 assert_eq!(
2455 shell.window_state(other).expect("state").scale_factor(),
2456 shell.window_state(window).expect("state").scale_factor()
2457 );
2458 }
2459
2460 #[test]
2461 fn constraints_set_before_the_first_configure_fold_into_it() {
2462 let mut shell = HeadlessShell::new();
2463 let window = shell.create_window(&WindowDesc::default()).expect("window");
2464 shell
2465 .set_constraints(
2466 window,
2467 SizeConstraints::NONE.with_aspect(AspectRatio::SQUARE),
2468 )
2469 .expect("request");
2470 settle(&mut shell);
2471 assert_eq!(
2474 shell.window_state(window).expect("state").size(),
2475 Some(PhysicalSize::new(720, 720))
2476 );
2477 }
2478
2479 #[test]
2480 fn creation_uses_the_requested_size_and_the_monitors_scale() {
2481 let mut shell = HeadlessShell::new();
2482 let window = shell
2483 .create_window(&WindowDesc {
2484 size: LogicalSize::new(800.0, 600.0),
2485 ..WindowDesc::default()
2486 })
2487 .expect("window");
2488 settle(&mut shell);
2489 assert_eq!(
2490 shell.window_state(window).expect("state").size(),
2491 Some(PhysicalSize::new(800, 600))
2492 );
2493
2494 let mut shell = HeadlessShell::new().with_scale_factor(2.0);
2495 let window = shell
2496 .create_window(&WindowDesc {
2497 size: LogicalSize::new(640.0, 360.0),
2498 ..WindowDesc::default()
2499 })
2500 .expect("window");
2501 settle(&mut shell);
2502 let state = shell.window_state(window).expect("state");
2503 assert_eq!(state.size(), Some(PhysicalSize::new(1280, 720)));
2504 assert_eq!(state.scale_factor(), Some(2.0));
2505 }
2506
2507 #[test]
2510 fn scale_changes_preserve_logical_size_and_report_both_facts() {
2511 let (mut shell, window) = shell_with_configured_window();
2512 shell
2513 .change_scale_factor(window, 1.5)
2514 .expect("scale change");
2515 let events = drain(&mut shell);
2516 assert_eq!(
2517 events,
2518 vec![
2519 ShellEvent::ScaleFactorChanged {
2520 window,
2521 scale_factor: 1.5,
2522 size: PhysicalSize::new(1920, 1080),
2523 },
2524 ShellEvent::Resized {
2527 window,
2528 size: PhysicalSize::new(1920, 1080),
2529 scale_factor: 1.5,
2530 },
2531 ]
2532 );
2533 assert_eq!(
2537 shell.window_state(window).expect("state").size(),
2538 Some(PhysicalSize::new(1920, 1080))
2539 );
2540 }
2541
2542 #[test]
2543 fn a_scale_change_scheduled_by_a_mode_switch_arrives_before_the_resize() {
2544 let mut shell = HeadlessShell::new();
2545 let hidpi = shell.plug_monitor(MonitorInfo {
2546 id: MonitorId(0),
2547 name: "HEADLESS-2".to_string(),
2548 bounds: PhysicalRect::new(1920, 0, 2560, 1440),
2549 work_area: PhysicalRect::new(1920, 0, 2560, 1440),
2550 scale_factor: 2.0,
2551 refresh_millihertz: 143_998,
2552 is_primary: false,
2553 });
2554 assert_eq!(hidpi, MonitorId(2), "ids are never reused");
2555 let window = shell.create_window(&WindowDesc::default()).expect("window");
2556 settle(&mut shell);
2557
2558 shell
2559 .set_mode(
2560 window,
2561 DisplayMode::Borderless {
2562 monitor: Some(hidpi),
2563 },
2564 )
2565 .expect("request");
2566 assert_eq!(drain(&mut shell), Vec::new(), "still only a request");
2567 let events = drain(&mut shell);
2568 assert_eq!(names(&events), ["ScaleFactorChanged", "Resized"]);
2569
2570 let state = shell.window_state(window).expect("state");
2571 assert_eq!(state.size(), Some(PhysicalSize::new(2560, 1440)));
2572 assert_eq!(state.scale_factor(), Some(2.0));
2573 assert!(state.effective_mode().expect("configured").is_borderless());
2574 assert_eq!(
2579 shell.surface_target(window).expect("target"),
2580 SurfaceTarget::Offscreen
2581 );
2582
2583 assert!(matches!(
2584 shell.set_mode(
2585 window,
2586 DisplayMode::Borderless {
2587 monitor: Some(MonitorId(99)),
2588 },
2589 ),
2590 Err(ShellError::NoSuchMonitor(99))
2591 ));
2592 }
2593
2594 #[test]
2595 fn resizes_honour_constraints_and_inject_bypasses_them() {
2596 let (mut shell, window) = shell_with_configured_window();
2597 shell
2598 .set_constraints(
2599 window,
2600 SizeConstraints::min(LogicalSize::new(640.0, 360.0))
2601 .with_aspect(AspectRatio::WIDESCREEN),
2602 )
2603 .expect("request");
2604 settle(&mut shell);
2605
2606 shell
2607 .resize(window, PhysicalSize::new(1000, 1000))
2608 .expect("resize");
2609 assert_eq!(
2610 drain(&mut shell),
2611 vec![ShellEvent::Resized {
2612 window,
2613 size: PhysicalSize::new(1000, 562),
2614 scale_factor: 1.0,
2615 }]
2616 );
2617
2618 shell.inject(ShellEvent::Resized {
2621 window,
2622 size: PhysicalSize::new(300, 900),
2623 scale_factor: 1.0,
2624 });
2625 assert_eq!(
2626 drain(&mut shell)[0],
2627 ShellEvent::Resized {
2628 window,
2629 size: PhysicalSize::new(300, 900),
2630 scale_factor: 1.0,
2631 }
2632 );
2633 }
2634
2635 #[test]
2636 fn a_compositor_configure_supersedes_a_pending_one() {
2637 let (mut shell, window) = shell_with_configured_window();
2638 shell
2639 .set_mode(window, DisplayMode::Borderless { monitor: None })
2640 .expect("request");
2641 shell
2643 .resize(window, PhysicalSize::new(640, 480))
2644 .expect("answer");
2645 assert_eq!(names(&drain(&mut shell)), ["Resized"]);
2646 assert_eq!(
2647 shell.window_state(window).expect("state").size(),
2648 Some(PhysicalSize::new(640, 480))
2649 );
2650 assert_eq!(drain(&mut shell), Vec::new());
2652 }
2653
2654 #[test]
2657 fn timestamps_come_from_the_shells_clock_not_from_the_pump() {
2658 let (mut shell, window) = shell_with_configured_window();
2659 shell.set_time(Duration::from_secs(2));
2660 shell.key_press(window, KeyCode::Space).expect("press");
2661 shell.advance(Duration::from_millis(180));
2662 shell.key_release(window, KeyCode::Space).expect("release");
2663
2664 let events = drain(&mut shell);
2665 let held = events[1]
2666 .time()
2667 .expect("input")
2668 .saturating_since(events[0].time().expect("input"));
2669 assert_eq!(held, Duration::from_millis(180));
2670 assert_eq!(events[0].time(), Some(EventTime::from_millis(2_000)));
2671 }
2672
2673 #[test]
2674 fn synthesized_key_fields_are_stable_and_plausible() {
2675 let (mut shell, window) = shell_with_configured_window();
2676 shell.set_modifiers(Modifiers::CTRL);
2677 shell.key_press(window, KeyCode::KeyS).expect("press");
2678 let events = drain(&mut shell);
2679 let ShellEvent::Key {
2680 scancode,
2681 key_code,
2682 keysym,
2683 modifiers,
2684 repeat,
2685 device,
2686 ..
2687 } = events[0].clone()
2688 else {
2689 panic!("wrong variant");
2690 };
2691 assert_eq!(key_code, Some(KeyCode::KeyS));
2692 assert_eq!(keysym, Keysym::from_char('s'));
2693 assert_eq!(modifiers, Modifiers::CTRL);
2694 assert!(!repeat);
2695 assert_eq!(device, VIRTUAL_DEVICE);
2696 assert!(scancode.0 >= 8, "evdev-shaped scancodes start at 8");
2697 assert_eq!(synthetic_keysym(KeyCode::Digit7), Keysym::from_char('7'));
2698 assert_eq!(synthetic_keysym(KeyCode::F5), Keysym(0xFFC2));
2699 assert_eq!(synthetic_keysym(KeyCode::Enter), Keysym(0xFF0D));
2700 assert_eq!(synthetic_keysym(KeyCode::Escape), Keysym(0xFF1B));
2701 assert_eq!(synthetic_keysym(KeyCode::ArrowLeft), Keysym(0xFF51));
2702 assert_eq!(synthetic_keysym(KeyCode::Slash), Keysym::from_char('/'));
2703
2704 shell.key_repeat(window, KeyCode::KeyS).expect("repeat");
2705 let events = drain(&mut shell);
2706 let ShellEvent::Key { repeat, .. } = events[0] else {
2707 panic!("wrong variant");
2708 };
2709 assert!(repeat);
2710 }
2711
2712 #[test]
2713 fn a_locked_pointer_reports_no_absolute_position() {
2714 let (mut shell, window) = shell_with_configured_window();
2715 shell
2716 .set_pointer_mode(window, PointerMode::Locked)
2717 .expect("lock");
2718 shell
2719 .move_pointer(window, PhysicalPoint::new(10.0, 10.0), (3.0, -4.0))
2720 .expect("motion");
2721 let events = drain(&mut shell);
2722 let ShellEvent::PointerMotion { abs, raw_delta, .. } = events[0] else {
2723 panic!("wrong variant");
2724 };
2725 assert_eq!(abs, None);
2726 assert_eq!(raw_delta, Some((3.0, -4.0)));
2727 }
2728
2729 #[test]
2730 fn a_backend_without_raw_motion_reports_none() {
2731 let mut shell = HeadlessShell::new()
2732 .with_caps(ShellCaps::DESKTOP - ShellCaps::RAW_POINTER_MOTION)
2733 .with_configure_delay(0);
2734 let window = shell.create_window(&WindowDesc::default()).expect("window");
2735 drain(&mut shell);
2736 shell
2737 .move_pointer(window, PhysicalPoint::new(1.0, 2.0), (5.0, 5.0))
2738 .expect("motion");
2739 let events = drain(&mut shell);
2740 let ShellEvent::PointerMotion { abs, raw_delta, .. } = events[0] else {
2741 panic!("wrong variant");
2742 };
2743 assert_eq!(abs, Some(PhysicalPoint::new(1.0, 2.0)));
2744 assert_eq!(raw_delta, None, "no relative-motion source");
2745 assert!(!shell.caps().has_mouselook());
2746 }
2747
2748 #[test]
2749 fn pointer_focus_and_scroll_and_text_are_injectable() {
2750 let (mut shell, window) = shell_with_configured_window();
2751 shell
2752 .set_pointer_focus(window, true, Some(PhysicalPoint::new(1.0, 1.0)))
2753 .expect("enter");
2754 shell
2755 .scroll(window, ScrollDelta::Lines { x: 0.0, y: -1.0 }, None)
2756 .expect("scroll");
2757 shell
2758 .button(
2759 window,
2760 PointerButton::Left,
2761 ButtonState::Pressed,
2762 Some(PhysicalPoint::new(1.0, 1.0)),
2763 )
2764 .expect("click");
2765 shell.commit_text(window, "ありがとう").expect("text");
2766 shell
2767 .set_pointer_focus(window, false, Some(PhysicalPoint::new(1.0, 1.0)))
2768 .expect("leave");
2769
2770 let events = drain(&mut shell);
2771 assert_eq!(
2772 names(&events),
2773 [
2774 "PointerFocus",
2775 "Wheel",
2776 "Button",
2777 "TextCommit",
2778 "PointerFocus"
2779 ]
2780 );
2781 let ShellEvent::PointerFocus {
2782 entered, position, ..
2783 } = &events[4]
2784 else {
2785 panic!("wrong variant");
2786 };
2787 assert!(!entered);
2788 assert_eq!(*position, None, "a leave has no position");
2789 assert!(events.iter().all(ShellEvent::is_input));
2790 }
2791
2792 #[test]
2799 fn contacts_are_injectable_and_a_touchless_shell_refuses_them() {
2800 let (mut shell, window) = shell_with_configured_window();
2801 assert!(shell.caps().contains(ShellCaps::TOUCH));
2802 for (contact, phase, x) in [
2803 (1, TouchPhase::Began, 10.0),
2804 (2, TouchPhase::Began, 200.0),
2805 (2, TouchPhase::Moved, 220.0),
2806 (1, TouchPhase::Ended, 10.0),
2807 (2, TouchPhase::Cancelled, 220.0),
2808 ] {
2809 shell
2810 .touch(
2811 window,
2812 ContactId(contact),
2813 phase,
2814 PhysicalPoint::new(x, 5.0),
2815 )
2816 .expect("a contact");
2817 }
2818 let events = drain(&mut shell);
2819 assert_eq!(names(&events), ["Touch"; 5]);
2820 let seen: Vec<(ContactId, TouchPhase, f64)> = events
2821 .iter()
2822 .map(|event| {
2823 let ShellEvent::Touch {
2824 contact,
2825 phase,
2826 position,
2827 ..
2828 } = event
2829 else {
2830 panic!("wrong variant");
2831 };
2832 (*contact, *phase, position.x)
2833 })
2834 .collect();
2835 assert_eq!(
2836 seen,
2837 vec![
2838 (ContactId(1), TouchPhase::Began, 10.0),
2839 (ContactId(2), TouchPhase::Began, 200.0),
2840 (ContactId(2), TouchPhase::Moved, 220.0),
2841 (ContactId(1), TouchPhase::Ended, 10.0),
2842 (ContactId(2), TouchPhase::Cancelled, 220.0),
2843 ]
2844 );
2845 assert!(events.iter().all(ShellEvent::is_input));
2846
2847 let mut touchless = HeadlessShell::new()
2848 .with_caps(ShellCaps::DESKTOP - ShellCaps::TOUCH)
2849 .with_configure_delay(0);
2850 let window = touchless
2851 .create_window(&WindowDesc::default())
2852 .expect("window");
2853 drain(&mut touchless);
2854 assert!(matches!(
2855 touchless.touch(
2856 window,
2857 ContactId(1),
2858 TouchPhase::Began,
2859 PhysicalPoint::ORIGIN
2860 ),
2861 Err(ShellError::Unsupported { .. })
2862 ));
2863 assert!(drain(&mut touchless).is_empty(), "and queued nothing");
2864 }
2865
2866 #[test]
2867 fn warping_produces_motion_like_every_platform_that_has_it() {
2868 let (mut shell, window) = shell_with_configured_window();
2869 shell
2870 .warp_pointer(window, PhysicalPoint::new(320.0, 180.0))
2871 .expect("warp");
2872 let events = drain(&mut shell);
2873 let ShellEvent::PointerMotion { abs, raw_delta, .. } = events[0] else {
2874 panic!("wrong variant");
2875 };
2876 assert_eq!(abs, Some(PhysicalPoint::new(320.0, 180.0)));
2877 assert_eq!(raw_delta, None, "a warp is not physical motion");
2878 }
2879
2880 #[test]
2883 fn capabilities_gate_the_operations_that_need_them() {
2884 let mut shell = HeadlessShell::new().with_caps(ShellCaps::empty());
2885 let window = shell.create_window(&WindowDesc::default()).expect("window");
2886
2887 assert!(matches!(
2888 shell.warp_pointer(window, PhysicalPoint::ORIGIN),
2889 Err(ShellError::Unsupported { .. })
2890 ));
2891 assert!(matches!(
2892 shell.set_pointer_mode(window, PointerMode::Locked),
2893 Err(ShellError::Unsupported { .. })
2894 ));
2895 assert!(matches!(
2896 shell.clipboard_request(window, MimeType::TextUtf8),
2897 Err(ShellError::Unsupported { .. })
2898 ));
2899 shell
2901 .set_pointer_mode(window, PointerMode::Free)
2902 .expect("Free is always available");
2903 assert!(matches!(
2905 shell.create_window(&WindowDesc::default()),
2906 Err(ShellError::Unsupported { .. })
2907 ));
2908 }
2909
2910 #[test]
2911 fn stale_handles_fail_cleanly_on_every_accessor() {
2912 let (mut shell, window) = shell_with_configured_window();
2913 shell.destroy_window(window).expect("destroy");
2914 assert_eq!(
2915 drain(&mut shell),
2916 vec![ShellEvent::WindowDestroyed { window }]
2917 );
2918
2919 assert!(matches!(
2920 shell.window_state(window),
2921 Err(ShellError::InvalidWindow { .. })
2922 ));
2923 assert!(matches!(
2924 shell.surface_target(window),
2925 Err(ShellError::InvalidWindow { .. })
2926 ));
2927 assert!(matches!(
2928 shell.resize(window, PhysicalSize::new(1, 1)),
2929 Err(ShellError::InvalidWindow { .. })
2930 ));
2931 assert!(matches!(
2932 shell.set_title(window, "gone"),
2933 Err(ShellError::InvalidWindow { .. })
2934 ));
2935 assert!(matches!(
2936 shell.destroy_window(window),
2937 Err(ShellError::InvalidWindow { .. })
2938 ));
2939 assert!(matches!(
2940 shell.key_press(window, KeyCode::KeyA),
2941 Err(ShellError::InvalidWindow { .. })
2942 ));
2943 }
2944
2945 #[test]
2946 fn destroying_a_window_cancels_its_pending_configure() {
2947 let (mut shell, window) = shell_with_window();
2950 shell.destroy_window(window).expect("destroy");
2951 assert_eq!(names(&drain(&mut shell)), ["WindowDestroyed"]);
2952 assert_eq!(drain(&mut shell), Vec::new());
2953 assert_eq!(drain(&mut shell), Vec::new());
2954 }
2955
2956 #[test]
2957 fn close_requests_are_questions() {
2958 let (mut shell, window) = shell_with_configured_window();
2959 assert!(matches!(
2960 shell.reply_close_request(window, CloseReply::Close),
2961 Err(ShellError::NoPendingCloseRequest { .. })
2962 ));
2963
2964 shell.request_close(window).expect("request");
2965 assert!(shell.window_state(window).expect("state").close_pending);
2966 shell
2967 .reply_close_request(window, CloseReply::Keep)
2968 .expect("keep");
2969 assert!(!shell.window_state(window).expect("state").close_pending);
2970 assert!(shell.window_state(window).is_ok(), "still open");
2971
2972 shell.request_close(window).expect("request again");
2973 shell
2974 .reply_close_request(window, CloseReply::Close)
2975 .expect("close");
2976 assert!(matches!(
2977 shell.window_state(window),
2978 Err(ShellError::InvalidWindow { .. })
2979 ));
2980 }
2981
2982 #[test]
2983 fn monitor_hotplug_reports_a_change_and_keeps_ids_unique() {
2984 let mut shell = HeadlessShell::new();
2985 let extra = shell.plug_monitor(MonitorInfo {
2986 id: MonitorId(0),
2987 name: "HEADLESS-2".to_string(),
2988 bounds: PhysicalRect::new(1920, 0, 1920, 1080),
2989 work_area: PhysicalRect::new(1920, 0, 1920, 1080),
2990 scale_factor: 1.0,
2991 refresh_millihertz: 60_000,
2992 is_primary: false,
2993 });
2994 assert_eq!(shell.monitors().len(), 2);
2995 assert_eq!(drain(&mut shell), vec![ShellEvent::MonitorsChanged]);
2996
2997 shell.unplug_monitor(extra).expect("unplug");
2998 assert_eq!(shell.monitors().len(), 1);
2999 assert_eq!(
3000 drain(&mut shell),
3001 vec![
3002 ShellEvent::MonitorRemoved { monitor: extra },
3003 ShellEvent::MonitorsChanged
3004 ]
3005 );
3006 assert!(matches!(
3007 shell.unplug_monitor(extra),
3008 Err(ShellError::NoSuchMonitor(_))
3009 ));
3010 assert_eq!(
3011 shell.monitor(MonitorId(1)).map(|m| m.name.as_str()),
3012 Some("HEADLESS-1")
3013 );
3014 assert!(shell.monitor(extra).is_none());
3015 }
3016
3017 #[test]
3018 fn unplugging_a_monitor_asks_its_panels_and_only_its_panels_to_close() {
3019 let mut shell = HeadlessShell::new();
3020 let second = shell.plug_monitor(MonitorInfo {
3021 id: MonitorId(0),
3022 name: "HEADLESS-2".to_string(),
3023 bounds: PhysicalRect::new(1920, 0, 1920, 1080),
3024 work_area: PhysicalRect::new(1920, 0, 1920, 1080),
3025 scale_factor: 1.0,
3026 refresh_millihertz: 60_000,
3027 is_primary: false,
3028 });
3029 let first = shell.monitors()[0].id;
3030 let panel_on = |shell: &mut HeadlessShell, monitor| {
3031 shell
3032 .create_window(&WindowDesc {
3033 role: crate::WindowRole::Panel(crate::PanelDesc {
3034 monitor,
3035 ..crate::PanelDesc::default()
3036 }),
3037 ..WindowDesc::default()
3038 })
3039 .expect("panel")
3040 };
3041 let doomed = panel_on(&mut shell, Some(second));
3042 let already_asked = panel_on(&mut shell, Some(second));
3043 let elsewhere = panel_on(&mut shell, Some(first));
3044 let anywhere = panel_on(&mut shell, None);
3045 let toplevel = shell.create_window(&WindowDesc::default()).expect("window");
3046 shell.request_close(already_asked).expect("close");
3047 settle(&mut shell);
3048 drain(&mut shell);
3049
3050 shell.unplug_monitor(second).expect("unplug");
3051 assert_eq!(
3052 drain(&mut shell),
3053 vec![
3054 ShellEvent::MonitorRemoved { monitor: second },
3055 ShellEvent::CloseRequested { window: doomed },
3056 ShellEvent::MonitorsChanged,
3057 ]
3058 );
3059 for window in [elsewhere, anywhere, toplevel] {
3060 assert!(!shell.window_state(window).expect("open").close_pending);
3061 }
3062 }
3063
3064 #[test]
3065 fn drops_require_both_the_capability_and_the_opt_in() {
3066 let mut shell = HeadlessShell::new();
3067 let plain = shell.create_window(&WindowDesc::default()).expect("window");
3068 assert!(matches!(
3069 shell.drop_file(plain, "/tmp/a.gltf", None),
3070 Err(ShellError::Unsupported { .. })
3071 ));
3072
3073 let dropper = shell
3074 .create_window(&WindowDesc {
3075 accept_drops: true,
3076 ..WindowDesc::default()
3077 })
3078 .expect("window");
3079 settle(&mut shell);
3080 shell
3081 .drop_file(dropper, "/tmp/a.gltf", Some(PhysicalPoint::new(4.0, 5.0)))
3082 .expect("drop");
3083 assert_eq!(names(&drain(&mut shell))[0], "DroppedFile");
3084 }
3085
3086 #[test]
3087 fn a_sink_that_queues_more_events_does_not_spin_the_pump() {
3088 let (mut shell, window) = shell_with_configured_window();
3089 shell.inject(ShellEvent::MonitorsChanged);
3090 let mut extra = Vec::new();
3091 shell.pump(&mut |event| extra.push(event));
3092 assert_eq!(extra.len(), 1);
3093 shell.inject(ShellEvent::CloseRequested { window });
3094 assert_eq!(shell.pending_events(), 1);
3095 }
3096
3097 #[test]
3098 fn accessors_that_no_real_backend_can_provide_are_headless_only() {
3099 let (mut shell, window) = shell_with_configured_window();
3100 shell.set_title(window, "renamed").expect("title");
3101 assert_eq!(shell.title(window).expect("title"), "renamed");
3102 shell.set_cursor(window, None).expect("hide");
3103 assert_eq!(shell.cursor(window).expect("cursor"), None);
3104 shell
3105 .set_cursor(window, Some(CursorIcon::Crosshair))
3106 .expect("shape");
3107 assert_eq!(
3108 shell.cursor(window).expect("cursor"),
3109 Some(CursorIcon::Crosshair)
3110 );
3111 shell.set_visible(window, false).expect("hide window");
3112 assert!(!shell.window_state(window).expect("state").visible);
3113 }
3114
3115 #[test]
3116 fn an_input_region_is_recorded_replaced_and_given_back() {
3117 let (mut shell, window) = shell_with_configured_window();
3118 assert_eq!(shell.input_region(window).expect("region"), None);
3119 let panel = PhysicalRect::new(0, 0, 400, 300).rounded(12);
3120 shell
3121 .set_input_region(window, Some(&panel))
3122 .expect("a region");
3123 assert_eq!(
3124 shell.input_region(window).expect("region"),
3125 Some(panel.as_slice())
3126 );
3127 shell.set_input_region(window, Some(&[])).expect("nothing");
3128 assert_eq!(shell.input_region(window).expect("region"), Some(&[][..]));
3129 shell.set_input_region(window, None).expect("whole window");
3130 assert_eq!(shell.input_region(window).expect("region"), None);
3131 shell.destroy_window(window).expect("destroy");
3132 assert!(matches!(
3133 shell.set_input_region(window, None),
3134 Err(ShellError::InvalidWindow { .. })
3135 ));
3136 assert!(shell.input_region(window).is_err());
3137 }
3138
3139 #[test]
3140 fn waiting_is_observable_and_focus_transitions_are_events() {
3141 let (mut shell, window) = shell_with_configured_window();
3142 assert_eq!(shell.wait_count(), 0);
3143 shell.wait_events(Some(Duration::from_millis(5)));
3144 shell.wait_events(None);
3145 assert_eq!(shell.wait_count(), 2);
3146
3147 shell.set_focus(window, true).expect("focus");
3148 assert!(shell.window_state(window).expect("state").focused);
3149 shell.set_focus(window, false).expect("unfocus");
3150 assert_eq!(names(&drain(&mut shell)), ["Focus", "Focus"]);
3151 }
3152
3153 const LONG_WAIT: Duration = Duration::from_secs(60);
3158 const RING_AFTER: Duration = Duration::from_millis(50);
3160
3161 fn ring_later(waker: ShellWaker) -> std::thread::JoinHandle<()> {
3163 std::thread::spawn(move || {
3164 std::thread::sleep(RING_AFTER);
3165 waker.wake();
3166 })
3167 }
3168
3169 #[test]
3170 fn a_wake_from_another_thread_ends_a_long_wait() {
3171 let mut shell = HeadlessShell::new();
3172 let ringer = ring_later(shell.waker().expect("headless has a waker"));
3173 let start = std::time::Instant::now();
3174 shell.wait_events(Some(LONG_WAIT));
3175 let slept = start.elapsed();
3176 ringer.join().expect("the ringer");
3177 assert!(slept < LONG_WAIT / 2, "the wait ran {slept:?}");
3178 }
3179
3180 #[test]
3181 fn a_wake_before_the_wait_ends_it_at_once_and_wakes_coalesce() {
3182 let mut shell = HeadlessShell::new();
3183 let waker = shell.waker().expect("headless has a waker");
3184 for _ in 0..3 {
3185 waker.wake();
3186 }
3187 let start = std::time::Instant::now();
3188 shell.wait_events(Some(LONG_WAIT));
3189 assert!(
3190 start.elapsed() < LONG_WAIT / 2,
3191 "a rung bell must end the wait"
3192 );
3193
3194 let timeout = Duration::from_millis(100);
3196 let start = std::time::Instant::now();
3197 shell.wait_events(Some(timeout));
3198 assert!(
3199 start.elapsed() >= timeout / 2,
3200 "the bell was cleared, so this wait must sleep, but it took {:?}",
3201 start.elapsed()
3202 );
3203 }
3204
3205 #[test]
3206 fn with_a_waker_out_an_untimed_wait_sleeps_until_it_rings() {
3207 let mut shell = HeadlessShell::new();
3208 let ringer = ring_later(shell.waker().expect("headless has a waker"));
3209 let start = std::time::Instant::now();
3210 shell.wait_events(None);
3211 let slept = start.elapsed();
3212 ringer.join().expect("the ringer");
3213 assert!(
3214 slept >= RING_AFTER / 2,
3215 "an untimed wait returned after {slept:?}, before anything rang"
3216 );
3217 }
3218
3219 #[test]
3222 fn the_clipboard_round_trips_through_an_event() {
3223 let (mut shell, window) = shell_with_configured_window();
3224 shell
3225 .clipboard_offer(
3226 window,
3227 &[
3228 ClipboardOffer::text("node"),
3229 ClipboardOffer::ron("(kind:\"node\")"),
3230 ],
3231 )
3232 .expect("offer");
3233 assert_eq!(
3234 shell.clipboard_bytes(MimeType::TextUtf8),
3235 Some(&b"node"[..])
3236 );
3237
3238 let request = shell
3239 .clipboard_request(window, MimeType::CrcblRon)
3240 .expect("request");
3241 assert_eq!(
3242 drain(&mut shell)[0],
3243 ShellEvent::ClipboardData {
3244 window,
3245 request,
3246 mime: ReceivedMime::from(MimeType::CrcblRon),
3247 content: ClipboardContent::Bytes(b"(kind:\"node\")".to_vec()),
3248 }
3249 );
3250
3251 let request = shell
3254 .clipboard_request(window, MimeType::UriList)
3255 .expect("request");
3256 assert_eq!(
3257 drain(&mut shell)[0],
3258 ShellEvent::ClipboardData {
3259 window,
3260 request,
3261 mime: ReceivedMime::from(MimeType::UriList),
3262 content: ClipboardContent::Empty,
3263 }
3264 );
3265
3266 shell
3269 .clipboard_offer(window, &[ClipboardOffer::text("")])
3270 .expect("offer");
3271 let request = shell
3272 .clipboard_request(window, MimeType::TextUtf8)
3273 .expect("request");
3274 assert_eq!(
3275 drain(&mut shell)[0],
3276 ShellEvent::ClipboardData {
3277 window,
3278 request,
3279 mime: ReceivedMime::from(MimeType::TextUtf8),
3280 content: ClipboardContent::Bytes(Vec::new()),
3281 }
3282 );
3283 }
3284
3285 #[test]
3286 fn a_read_taken_while_the_clipboard_is_unreadable_is_held_not_answered_empty() {
3287 let (mut shell, window) = shell_with_configured_window();
3293 shell
3294 .clipboard_offer(window, &[ClipboardOffer::text("published")])
3295 .expect("offer");
3296 shell.set_clipboard_readable(false);
3297 assert!(!shell.clipboard_readable(window));
3298
3299 let request = shell
3300 .clipboard_request(window, MimeType::TextUtf8)
3301 .expect("a read is accepted even when it cannot be answered yet");
3302 assert_eq!(shell.held_clipboard_reads(), 1);
3303 assert!(
3304 drain(&mut shell).is_empty(),
3305 "a held read produces no event at all — least of all an empty one"
3306 );
3307
3308 shell.set_clipboard_readable(true);
3310 let events = drain(&mut shell);
3311 assert_eq!(events.len(), 1);
3312 assert_eq!(
3313 events[0],
3314 ShellEvent::ClipboardData {
3315 window,
3316 request,
3317 mime: ReceivedMime::from(MimeType::TextUtf8),
3318 content: ClipboardContent::Bytes(b"published".to_vec()),
3319 }
3320 );
3321 assert_eq!(shell.held_clipboard_reads(), 0);
3322 }
3323
3324 #[test]
3325 fn a_read_that_never_becomes_answerable_ends_as_unavailable() {
3326 let (mut shell, window) = shell_with_configured_window();
3330 shell
3331 .clipboard_offer(window, &[ClipboardOffer::text("unreachable")])
3332 .expect("offer");
3333 shell.set_clipboard_readable(false);
3334 shell.clipboard_deadline(2);
3335
3336 let request = shell
3337 .clipboard_request(window, MimeType::TextUtf8)
3338 .expect("request");
3339 assert!(drain(&mut shell).is_empty(), "pump 1: still held");
3340 assert!(drain(&mut shell).is_empty(), "pump 2: still held");
3341 let events = drain(&mut shell);
3342 assert_eq!(
3343 events[0],
3344 ShellEvent::ClipboardData {
3345 window,
3346 request,
3347 mime: ReceivedMime::from(MimeType::TextUtf8),
3348 content: ClipboardContent::Unavailable,
3351 }
3352 );
3353 assert_eq!(shell.held_clipboard_reads(), 0);
3354 }
3355
3356 #[test]
3357 fn a_peer_that_holds_the_clipboard_and_never_delivers_is_unavailable() {
3358 let (mut shell, window) = shell_with_configured_window();
3362 shell.set_foreign_clipboard(vec![(
3363 ReceivedMime::new("text/plain"),
3364 b"never sent".to_vec(),
3365 )]);
3366 shell.set_clipboard_stalled(true);
3367 shell.clipboard_deadline(1);
3368 assert!(
3369 !shell.clipboard_readable(window),
3370 "a clipboard whose owner will not answer is not readable"
3371 );
3372
3373 let request = shell
3374 .clipboard_request(window, MimeType::TextUtf8)
3375 .expect("request");
3376 assert!(drain(&mut shell).is_empty());
3377 let events = drain(&mut shell);
3378 let ShellEvent::ClipboardData {
3379 request: answered,
3380 content,
3381 ..
3382 } = &events[0]
3383 else {
3384 panic!("wrong variant");
3385 };
3386 assert_eq!(*answered, request);
3387 assert_eq!(*content, ClipboardContent::Unavailable);
3388
3389 shell.set_clipboard_stalled(false);
3391 let request = shell
3392 .clipboard_request(window, MimeType::TextUtf8)
3393 .expect("request");
3394 assert_eq!(
3395 drain(&mut shell)[0],
3396 ShellEvent::ClipboardData {
3397 window,
3398 request,
3399 mime: ReceivedMime::new("text/plain"),
3400 content: ClipboardContent::Bytes(b"never sent".to_vec()),
3401 }
3402 );
3403 }
3404
3405 #[test]
3406 fn claiming_the_clipboard_can_require_a_user_interaction() {
3407 let (mut shell, window) = shell_with_configured_window();
3411 shell.require_user_interaction(true);
3412
3413 let refused = shell.clipboard_offer(window, &[ClipboardOffer::text("stolen")]);
3414 assert!(
3415 matches!(
3416 refused,
3417 Err(ShellError::NeedsUserInteraction {
3418 backend: ShellBackend::Headless,
3419 ..
3420 })
3421 ),
3422 "{refused:?}"
3423 );
3424 assert!(
3425 shell.clipboard_bytes(MimeType::TextUtf8).is_none(),
3426 "a refused claim must not have taken the clipboard anyway"
3427 );
3428
3429 shell.key_press(window, KeyCode::KeyC).expect("key");
3432 shell
3433 .clipboard_offer(window, &[ClipboardOffer::text("copied")])
3434 .expect("with a serial in hand, the claim goes through");
3435 assert_eq!(
3436 shell.clipboard_bytes(MimeType::TextUtf8),
3437 Some(&b"copied"[..])
3438 );
3439
3440 shell.clear_user_interaction();
3442 assert!(matches!(
3443 shell.clipboard_offer(window, &[ClipboardOffer::text("later")]),
3444 Err(ShellError::NeedsUserInteraction { .. })
3445 ));
3446 }
3447
3448 #[test]
3449 fn a_held_read_for_a_destroyed_window_is_dropped_rather_than_answered() {
3450 let (mut shell, window) = shell_with_configured_window();
3453 shell.set_clipboard_readable(false);
3454 let _request = shell
3455 .clipboard_request(window, MimeType::TextUtf8)
3456 .expect("request");
3457 assert_eq!(shell.held_clipboard_reads(), 1);
3458
3459 shell.destroy_window(window).expect("destroy");
3460 let events = drain(&mut shell);
3461 assert_eq!(shell.held_clipboard_reads(), 0);
3462 assert!(
3463 !events
3464 .iter()
3465 .any(|event| matches!(event, ShellEvent::ClipboardData { .. })),
3466 "no clipboard answer may name a destroyed window: {events:?}"
3467 );
3468 }
3469
3470 #[test]
3471 fn a_paste_from_a_foreign_application_keeps_the_foreign_spelling() {
3472 let (mut shell, window) = shell_with_configured_window();
3473 shell.set_foreign_clipboard(vec![
3474 (ReceivedMime::new("text/plain"), b"from gedit".to_vec()),
3477 (
3478 ReceivedMime::new("application/vnd.some-editor.scene+json"),
3479 b"{}".to_vec(),
3480 ),
3481 ]);
3482
3483 let request = shell
3485 .clipboard_request(window, MimeType::TextUtf8)
3486 .expect("request");
3487 let events = drain(&mut shell);
3488 let ShellEvent::ClipboardData {
3489 request: answered,
3490 mime,
3491 content,
3492 ..
3493 } = &events[0]
3494 else {
3495 panic!("wrong variant");
3496 };
3497 assert_eq!(*answered, request);
3498 assert_eq!(content.bytes(), Some(&b"from gedit"[..]));
3499 assert_eq!(mime.as_str(), "text/plain");
3501 assert!(mime.matches(MimeType::TextUtf8));
3502 assert_ne!(*mime, ReceivedMime::from(MimeType::TextUtf8));
3503
3504 assert_eq!(
3507 shell.clipboard_bytes(MimeType::Other("application/vnd.some-editor.scene+json")),
3508 Some(&b"{}"[..])
3509 );
3510 }
3511
3512 #[test]
3515 #[should_panic(expected = "a shell needs at least one monitor")]
3516 fn a_shell_with_no_monitors_is_rejected() {
3517 let _ = HeadlessShell::new().with_monitors(Vec::new());
3518 }
3519
3520 #[test]
3521 #[should_panic(expected = "a text commit is never empty")]
3522 fn an_empty_text_commit_is_rejected() {
3523 let (mut shell, window) = shell_with_configured_window();
3524 let _ = shell.commit_text(window, "");
3525 }
3526}