1use std::collections::VecDeque;
135use std::path::PathBuf;
136use std::sync::{Arc, Condvar, Mutex, PoisonError};
137
138use core::time::Duration;
139
140use crcbl_core::input::{
141 ButtonState, ContactId, DeviceId, Keysym, Modifiers, PointerButton, Scancode, ScrollDelta,
142 TouchPhase,
143};
144use crcbl_core::time::{ManualTime, TimeSource};
145use crcbl_core::{EventTime, Pool, SurfaceTarget};
146
147use crate::frame::FrameRequests;
148use crate::waker::WakeSignal;
149use crate::{
150 ClipboardContent, ClipboardOffer, ClipboardRequestId, CloseReply, CursorIcon, DisplayMode,
151 KeyCode, LogicalRect, LogicalSize, MimeType, MonitorId, MonitorInfo, PhysicalPoint,
152 PhysicalRect, PhysicalSize, PointerMode, ReceivedMime, Shell, ShellBackend, ShellCaps,
153 ShellError, ShellEvent, ShellWaker, SizeConstraints, TextPurpose, WindowConfiguration,
154 WindowDesc, WindowId, WindowState,
155};
156
157pub const DEFAULT_MONITOR_SIZE: PhysicalSize = PhysicalSize::new(1920, 1080);
162
163pub const DEFAULT_REFRESH_MILLIHERTZ: u32 = 60_000;
165
166pub const DEFAULT_CONFIGURE_DELAY: u32 = 1;
172
173pub const VIRTUAL_DEVICE: DeviceId = DeviceId(1);
179
180pub const DEFAULT_CLIPBOARD_DEADLINE: u32 = 4;
188
189#[derive(Clone, Debug)]
195struct PendingRead {
196 window: WindowId,
197 request: ClipboardRequestId,
198 mime: MimeType,
199 countdown: u32,
201}
202
203#[derive(Clone, Copy, Debug)]
207struct PendingConfigure {
208 size: PhysicalSize,
209 scale_factor: f64,
210 mode: DisplayMode,
211 countdown: u32,
213}
214
215#[derive(Clone, Debug)]
217struct HeadlessWindow {
218 title: String,
219 configuration: Option<WindowConfiguration>,
221 pending: Option<PendingConfigure>,
223 requested_mode: DisplayMode,
224 requested_constraints: SizeConstraints,
225 focused: bool,
226 visible: bool,
227 pointer_mode: PointerMode,
228 cursor: Option<CursorIcon>,
229 close_pending: bool,
230 accept_drops: bool,
231 parent: Option<WindowId>,
233 popup_input: Option<crate::PopupInput>,
235 panel: Option<crate::role::KeptPanel>,
239 text_input: Option<TextPurpose>,
241 text_input_area: Option<PhysicalRect>,
243 input_region: Option<Vec<PhysicalRect>>,
245 #[cfg(feature = "accesskit")]
248 accessibility: Option<Vec<accesskit::TreeUpdate>>,
249}
250
251impl HeadlessWindow {
252 fn size(&self) -> Option<PhysicalSize> {
254 self.configuration.map(|config| config.size)
255 }
256
257 fn scale_factor(&self) -> f64 {
263 self.configuration
264 .map(|config| config.scale_factor)
265 .or_else(|| self.pending.map(|pending| pending.scale_factor))
266 .unwrap_or(1.0)
267 }
268
269 fn settled_mode(&self) -> DisplayMode {
278 self.configuration
279 .map(|config| config.mode)
280 .or_else(|| self.pending.map(|pending| pending.mode))
281 .unwrap_or(self.requested_mode)
282 }
283
284 fn base_size(&self) -> PhysicalSize {
286 self.size()
287 .or_else(|| self.pending.map(|pending| pending.size))
288 .unwrap_or(PhysicalSize::new(1, 1))
289 }
290
291 fn configure_at(&mut self, size: LogicalSize, countdown: u32) {
295 if let Some(pending) = self.pending.as_mut() {
296 pending.size = size.to_physical(pending.scale_factor);
297 } else if let Some(config) = self.configuration {
298 self.pending = Some(PendingConfigure {
299 size: size.to_physical(config.scale_factor),
300 scale_factor: config.scale_factor,
301 mode: config.mode,
302 countdown,
303 });
304 }
305 }
306}
307
308#[derive(Debug)]
313pub struct HeadlessShell {
314 caps: ShellCaps,
315 monitors: Vec<MonitorInfo>,
316 next_monitor_id: u32,
317 windows: Pool<HeadlessWindow>,
318 queue: VecDeque<ShellEvent>,
319 clock: ManualTime,
320 modifiers: Modifiers,
321 clipboard: Vec<(ReceivedMime, Vec<u8>)>,
322 next_request_id: u32,
323 reads: Vec<PendingRead>,
326 clipboard_readable: bool,
330 clipboard_stalled: bool,
332 clipboard_needs_interaction: bool,
334 saw_interaction: bool,
337 clipboard_deadline: u32,
340 waits: u32,
341 configure_delay: u32,
342 wake: Option<Arc<HeadlessWake>>,
344 frames: FrameRequests,
346 outputs_powered: bool,
349}
350
351#[derive(Debug, Default)]
354struct HeadlessWake {
355 rung: Mutex<bool>,
356 bell: Condvar,
357}
358
359impl HeadlessWake {
360 fn wait(&self, timeout: Option<Duration>) {
363 let rung = self.rung.lock().unwrap_or_else(PoisonError::into_inner);
364 let mut rung = match timeout {
365 Some(timeout) => {
366 self.bell
367 .wait_timeout_while(rung, timeout, |rung| !*rung)
368 .unwrap_or_else(PoisonError::into_inner)
369 .0
370 }
371 None => self
372 .bell
373 .wait_while(rung, |rung| !*rung)
374 .unwrap_or_else(PoisonError::into_inner),
375 };
376 *rung = false;
377 }
378}
379
380impl WakeSignal for HeadlessWake {
381 fn wake(&self) {
382 *self.rung.lock().unwrap_or_else(PoisonError::into_inner) = true;
383 self.bell.notify_all();
384 }
385}
386
387impl Default for HeadlessShell {
388 fn default() -> Self {
389 Self::new()
390 }
391}
392
393impl HeadlessShell {
394 #[must_use]
396 pub fn new() -> Self {
397 Self {
398 caps: ShellCaps::DESKTOP,
399 monitors: vec![MonitorInfo {
400 id: MonitorId(1),
401 name: "HEADLESS-1".to_string(),
402 bounds: PhysicalRect::new(
403 0,
404 0,
405 DEFAULT_MONITOR_SIZE.width,
406 DEFAULT_MONITOR_SIZE.height,
407 ),
408 work_area: PhysicalRect::new(
409 0,
410 0,
411 DEFAULT_MONITOR_SIZE.width,
412 DEFAULT_MONITOR_SIZE.height,
413 ),
414 scale_factor: 1.0,
415 refresh_millihertz: DEFAULT_REFRESH_MILLIHERTZ,
416 is_primary: true,
417 }],
418 next_monitor_id: 2,
419 windows: Pool::new(),
420 queue: VecDeque::new(),
421 clock: ManualTime::new(),
422 modifiers: Modifiers::empty(),
423 clipboard: Vec::new(),
424 next_request_id: 1,
425 reads: Vec::new(),
426 clipboard_readable: true,
427 clipboard_stalled: false,
428 clipboard_needs_interaction: false,
429 saw_interaction: false,
430 clipboard_deadline: DEFAULT_CLIPBOARD_DEADLINE,
431 waits: 0,
432 configure_delay: DEFAULT_CONFIGURE_DELAY,
433 wake: None,
434 frames: FrameRequests::default(),
435 outputs_powered: true,
436 }
437 }
438
439 #[must_use]
444 pub fn with_caps(mut self, caps: ShellCaps) -> Self {
445 self.caps = caps;
446 self
447 }
448
449 #[must_use]
464 pub fn with_monitors(mut self, monitors: Vec<MonitorInfo>) -> Self {
465 assert!(!monitors.is_empty(), "a shell needs at least one monitor");
466 for (index, monitor) in monitors.iter().enumerate() {
467 assert!(
468 monitor.id.0 != u32::MAX,
469 "monitor id {} leaves no id for the next hotplug",
470 monitor.id.0
471 );
472 assert!(
473 !monitors[..index].iter().any(|seen| seen.id == monitor.id),
474 "monitor id {} appears twice",
475 monitor.id.0
476 );
477 }
478 self.next_monitor_id = monitors
479 .iter()
480 .map(|monitor| monitor.id.0 + 1)
481 .max()
482 .unwrap_or(1);
483 self.monitors = monitors;
484 self
485 }
486
487 #[must_use]
492 pub fn with_scale_factor(mut self, scale_factor: f64) -> Self {
493 assert!(scale_factor > 0.0, "scale factor must be positive");
494 self.monitors[0].scale_factor = scale_factor;
495 self
496 }
497
498 #[must_use]
503 pub const fn with_configure_delay(mut self, pumps: u32) -> Self {
504 self.configure_delay = pumps;
505 self
506 }
507
508 pub const fn set_configure_delay(&mut self, pumps: u32) {
518 self.configure_delay = pumps;
519 }
520
521 pub fn set_caps(&mut self, caps: ShellCaps) {
527 self.caps = caps;
528 }
529
530 #[must_use]
532 pub fn now(&self) -> EventTime {
533 EventTime::from_duration(self.clock.elapsed())
534 }
535
536 pub fn advance(&mut self, delta: Duration) {
542 self.clock.advance(delta);
543 }
544
545 pub fn set_time(&mut self, now: Duration) {
553 self.clock.set(now);
554 }
555
556 pub fn set_modifiers(&mut self, modifiers: Modifiers) {
561 self.modifiers = modifiers;
562 }
563
564 #[must_use]
566 pub fn pending_events(&self) -> usize {
567 self.queue.len()
568 }
569
570 #[must_use]
576 pub const fn wait_count(&self) -> u32 {
577 self.waits
578 }
579
580 pub fn inject(&mut self, event: ShellEvent) {
588 self.queue.push_back(event);
589 }
590
591 pub fn resize(&mut self, window: WindowId, size: PhysicalSize) -> Result<(), ShellError> {
607 let state = self.window_mut(window)?;
608 let scale_factor = state.scale_factor();
609 let mode = state.settled_mode();
610 let constrained = state.requested_constraints.apply(size, scale_factor);
611 state.pending = None;
612 state.configuration = Some(WindowConfiguration {
613 size: constrained,
614 scale_factor,
615 mode,
616 });
617 self.queue.push_back(ShellEvent::Resized {
618 window,
619 size: constrained,
620 scale_factor,
621 });
622 Ok(())
623 }
624
625 pub fn change_scale_factor(
647 &mut self,
648 window: WindowId,
649 scale_factor: f64,
650 ) -> Result<(), ShellError> {
651 assert!(scale_factor > 0.0, "scale factor must be positive");
652 let state = self.window_mut(window)?;
653 let logical = state.base_size().to_logical(state.scale_factor());
654 let mode = state.settled_mode();
655 let size = state
656 .requested_constraints
657 .apply(logical.to_physical(scale_factor), scale_factor);
658 state.pending = None;
659 state.configuration = Some(WindowConfiguration {
660 size,
661 scale_factor,
662 mode,
663 });
664 self.queue.push_back(ShellEvent::ScaleFactorChanged {
665 window,
666 scale_factor,
667 size,
668 });
669 self.queue.push_back(ShellEvent::Resized {
670 window,
671 size,
672 scale_factor,
673 });
674 Ok(())
675 }
676
677 pub fn set_focus(&mut self, window: WindowId, focused: bool) -> Result<(), ShellError> {
683 self.window_mut(window)?.focused = focused;
684 self.queue.push_back(ShellEvent::Focus { window, focused });
685 Ok(())
686 }
687
688 pub fn request_close(&mut self, window: WindowId) -> Result<(), ShellError> {
697 self.window_mut(window)?.close_pending = true;
698 self.queue.push_back(ShellEvent::CloseRequested { window });
699 Ok(())
700 }
701
702 pub fn key(
715 &mut self,
716 window: WindowId,
717 key: KeyCode,
718 state: ButtonState,
719 ) -> Result<(), ShellError> {
720 self.key_with(window, key, state, false)
721 }
722
723 pub fn key_press(&mut self, window: WindowId, key: KeyCode) -> Result<(), ShellError> {
729 self.key(window, key, ButtonState::Pressed)
730 }
731
732 pub fn key_release(&mut self, window: WindowId, key: KeyCode) -> Result<(), ShellError> {
738 self.key(window, key, ButtonState::Released)
739 }
740
741 pub fn key_repeat(&mut self, window: WindowId, key: KeyCode) -> Result<(), ShellError> {
748 self.key_with(window, key, ButtonState::Pressed, true)
749 }
750
751 fn key_with(
752 &mut self,
753 window: WindowId,
754 key: KeyCode,
755 state: ButtonState,
756 repeat: bool,
757 ) -> Result<(), ShellError> {
758 let popup_input = self.window(window)?.popup_input;
759 self.saw_interaction = true;
760 let event = ShellEvent::Key {
761 window,
762 device: VIRTUAL_DEVICE,
763 time: self.now(),
764 scancode: synthetic_scancode(key),
765 key_code: Some(key),
766 keysym: synthetic_keysym(key),
767 state,
768 repeat,
769 modifiers: self.modifiers,
770 };
771 let dismissed = crate::role::escape_dismisses(popup_input, &event);
772 self.queue.push_back(event);
773 if dismissed && !self.window(window)?.close_pending {
775 self.request_close(window)?;
776 }
777 Ok(())
778 }
779
780 pub fn move_pointer(
791 &mut self,
792 window: WindowId,
793 position: PhysicalPoint,
794 raw_delta: (f64, f64),
795 ) -> Result<(), ShellError> {
796 let locked = self.window(window)?.pointer_mode == PointerMode::Locked;
797 let raw = if self.caps.contains(ShellCaps::RAW_POINTER_MOTION) {
798 Some(raw_delta)
799 } else {
800 None
801 };
802 self.saw_interaction = true;
803 let event = ShellEvent::PointerMotion {
804 window,
805 device: VIRTUAL_DEVICE,
806 time: self.now(),
807 abs: if locked { None } else { Some(position) },
808 raw_delta: raw,
809 };
810 self.queue.push_back(event);
811 Ok(())
812 }
813
814 pub fn set_pointer_focus(
820 &mut self,
821 window: WindowId,
822 entered: bool,
823 position: Option<PhysicalPoint>,
824 ) -> Result<(), ShellError> {
825 self.check_window(window)?;
826 let event = ShellEvent::PointerFocus {
827 window,
828 device: VIRTUAL_DEVICE,
829 time: self.now(),
830 entered,
831 position: if entered { position } else { None },
832 };
833 self.queue.push_back(event);
834 Ok(())
835 }
836
837 pub fn button(
843 &mut self,
844 window: WindowId,
845 button: PointerButton,
846 state: ButtonState,
847 position: Option<PhysicalPoint>,
848 ) -> Result<(), ShellError> {
849 self.check_window(window)?;
850 self.saw_interaction = true;
851 let event = ShellEvent::Button {
852 window,
853 device: VIRTUAL_DEVICE,
854 time: self.now(),
855 button,
856 state,
857 position,
858 modifiers: self.modifiers,
859 };
860 self.queue.push_back(event);
861 Ok(())
862 }
863
864 pub fn scroll(
870 &mut self,
871 window: WindowId,
872 delta: ScrollDelta,
873 position: Option<PhysicalPoint>,
874 ) -> Result<(), ShellError> {
875 self.check_window(window)?;
876 self.saw_interaction = true;
877 let event = ShellEvent::Wheel {
878 window,
879 device: VIRTUAL_DEVICE,
880 time: self.now(),
881 delta,
882 position,
883 modifiers: self.modifiers,
884 };
885 self.queue.push_back(event);
886 Ok(())
887 }
888
889 pub fn touch(
917 &mut self,
918 window: WindowId,
919 contact: ContactId,
920 phase: TouchPhase,
921 position: PhysicalPoint,
922 ) -> Result<(), ShellError> {
923 self.check_window(window)?;
924 if !self.caps.contains(ShellCaps::TOUCH) {
925 return Err(self.unsupported("touch"));
926 }
927 self.saw_interaction = true;
928 let event = ShellEvent::Touch {
929 window,
930 device: VIRTUAL_DEVICE,
931 time: self.now(),
932 contact,
933 phase,
934 position,
935 };
936 self.queue.push_back(event);
937 Ok(())
938 }
939
940 pub fn commit_text(&mut self, window: WindowId, text: &str) -> Result<(), ShellError> {
951 assert!(!text.is_empty(), "a text commit is never empty");
952 self.check_window(window)?;
953 let event = ShellEvent::TextCommit {
954 window,
955 time: self.now(),
956 text: text.to_string(),
957 };
958 self.queue.push_back(event);
959 Ok(())
960 }
961
962 pub fn preedit_text(
975 &mut self,
976 window: WindowId,
977 text: &str,
978 cursor: Option<usize>,
979 ) -> Result<(), ShellError> {
980 assert!(
981 cursor.is_none_or(|cursor| !text.is_empty() && text.is_char_boundary(cursor)),
982 "a pre-edit's cursor is a char boundary of a non-empty text"
983 );
984 self.check_window(window)?;
985 let event = ShellEvent::TextPreedit {
986 window,
987 time: self.now(),
988 text: text.to_string(),
989 cursor,
990 };
991 self.queue.push_back(event);
992 Ok(())
993 }
994
995 pub fn drop_file(
1003 &mut self,
1004 window: WindowId,
1005 path: impl Into<PathBuf>,
1006 position: Option<PhysicalPoint>,
1007 ) -> Result<(), ShellError> {
1008 if !self.caps.contains(ShellCaps::DRAG_DROP) {
1009 return Err(self.unsupported("drag and drop"));
1010 }
1011 if !self.window(window)?.accept_drops {
1012 return Err(ShellError::Unsupported {
1013 backend: ShellBackend::Headless,
1014 what: "drops on a window created without accept_drops",
1015 });
1016 }
1017 let event = ShellEvent::DroppedFile {
1018 window,
1019 time: self.now(),
1020 path: path.into(),
1021 position,
1022 };
1023 self.queue.push_back(event);
1024 Ok(())
1025 }
1026
1027 pub fn plug_monitor(&mut self, mut info: MonitorInfo) -> MonitorId {
1034 let id = MonitorId(self.next_monitor_id);
1035 self.next_monitor_id += 1;
1036 info.id = id;
1037 self.monitors.push(info);
1038 self.queue.push_back(ShellEvent::MonitorsChanged);
1039 id
1040 }
1041
1042 pub fn unplug_monitor(&mut self, id: MonitorId) -> Result<(), ShellError> {
1056 let index = self
1057 .monitors
1058 .iter()
1059 .position(|monitor| monitor.id == id)
1060 .ok_or(ShellError::NoSuchMonitor(id.0))?;
1061 assert!(
1062 self.monitors.len() > 1,
1063 "a shell needs at least one monitor"
1064 );
1065 let previous = self.monitors.clone();
1066 self.monitors.remove(index);
1067 crate::monitor::queue_removed(&previous, &self.monitors, &mut self.queue);
1068 let panels: Vec<WindowId> = self
1069 .windows
1070 .iter()
1071 .filter(|(_, state)| {
1072 state.panel.as_ref().and_then(|panel| panel.desc().monitor) == Some(id)
1073 && !state.close_pending
1074 })
1075 .map(|(window, _)| window.cast())
1076 .collect();
1077 for panel in panels {
1078 self.request_close(panel)?;
1079 }
1080 self.queue.push_back(ShellEvent::MonitorsChanged);
1081 Ok(())
1082 }
1083
1084 pub fn title(&self, window: WindowId) -> Result<&str, ShellError> {
1095 Ok(self.window(window)?.title.as_str())
1096 }
1097
1098 pub fn cursor(&self, window: WindowId) -> Result<Option<CursorIcon>, ShellError> {
1106 Ok(self.window(window)?.cursor)
1107 }
1108
1109 pub fn text_input(&self, window: WindowId) -> Result<Option<TextPurpose>, ShellError> {
1118 Ok(self.window(window)?.text_input)
1119 }
1120
1121 pub fn text_input_area(&self, window: WindowId) -> Result<Option<PhysicalRect>, ShellError> {
1131 Ok(self.window(window)?.text_input_area)
1132 }
1133
1134 pub fn panel(&self, window: WindowId) -> Result<Option<crate::PanelDesc<'_>>, ShellError> {
1143 Ok(self
1144 .window(window)?
1145 .panel
1146 .as_ref()
1147 .map(crate::role::KeptPanel::desc))
1148 }
1149
1150 pub fn input_region(&self, window: WindowId) -> Result<Option<&[PhysicalRect]>, ShellError> {
1161 Ok(self.window(window)?.input_region.as_deref())
1162 }
1163
1164 pub fn frame_requested(&self, window: WindowId) -> Result<bool, ShellError> {
1174 self.window(window)?;
1175 Ok(self.frames.contains(window))
1176 }
1177
1178 pub fn set_outputs_powered(&mut self, powered: bool) {
1194 self.outputs_powered = powered;
1195 }
1196
1197 #[cfg(feature = "accesskit")]
1206 pub fn activate_accessibility(&mut self, window: WindowId) -> Result<(), ShellError> {
1207 let state = &mut self.window_mut(window)?.accessibility;
1208 if state.is_none() {
1209 *state = Some(Vec::new());
1210 self.queue
1211 .push_back(ShellEvent::AccessibilityActivated { window });
1212 }
1213 Ok(())
1214 }
1215
1216 #[cfg(feature = "accesskit")]
1226 pub fn deactivate_accessibility(&mut self, window: WindowId) -> Result<(), ShellError> {
1227 if self.window_mut(window)?.accessibility.take().is_some() {
1228 self.queue
1229 .push_back(ShellEvent::AccessibilityDeactivated { window });
1230 }
1231 Ok(())
1232 }
1233
1234 #[cfg(feature = "accesskit")]
1248 pub fn request_accessibility_action(
1249 &mut self,
1250 window: WindowId,
1251 request: accesskit::ActionRequest,
1252 ) -> Result<(), ShellError> {
1253 assert!(
1254 self.window(window)?.accessibility.is_some(),
1255 "no assistive technology is reading the window"
1256 );
1257 self.queue
1258 .push_back(ShellEvent::AccessibilityAction { window, request });
1259 Ok(())
1260 }
1261
1262 #[cfg(feature = "accesskit")]
1271 pub fn accessibility_updates(
1272 &self,
1273 window: WindowId,
1274 ) -> Result<Option<&[accesskit::TreeUpdate]>, ShellError> {
1275 Ok(self.window(window)?.accessibility.as_deref())
1276 }
1277
1278 #[must_use]
1282 pub fn clipboard_bytes(&self, mime: MimeType) -> Option<&[u8]> {
1283 self.clipboard
1284 .iter()
1285 .find(|(offered, _)| offered.matches(mime))
1286 .map(|(_, bytes)| bytes.as_slice())
1287 }
1288
1289 pub fn set_clipboard_readable(&mut self, readable: bool) {
1306 self.clipboard_readable = readable;
1307 }
1308
1309 pub fn set_clipboard_stalled(&mut self, stalled: bool) {
1318 self.clipboard_stalled = stalled;
1319 }
1320
1321 pub fn require_user_interaction(&mut self, required: bool) {
1348 self.clipboard_needs_interaction = required;
1349 }
1350
1351 pub fn clear_user_interaction(&mut self) {
1356 self.saw_interaction = false;
1357 }
1358
1359 pub fn clipboard_deadline(&mut self, pumps: u32) {
1365 self.clipboard_deadline = pumps;
1366 }
1367
1368 #[must_use]
1373 pub fn held_clipboard_reads(&self) -> usize {
1374 self.reads.len()
1375 }
1376
1377 pub fn set_foreign_clipboard(&mut self, offers: Vec<(ReceivedMime, Vec<u8>)>) {
1385 self.clipboard = offers;
1386 }
1387
1388 fn answerable(&self) -> bool {
1394 self.clipboard_readable && !self.clipboard_stalled
1395 }
1396
1397 fn answer(&self, window: WindowId, request: ClipboardRequestId, mime: MimeType) -> ShellEvent {
1399 let found = self
1404 .clipboard
1405 .iter()
1406 .find(|(offered, _)| offered.matches(mime));
1407 let (mime, content) = match found {
1408 Some((offered, bytes)) => (offered.clone(), ClipboardContent::Bytes(bytes.clone())),
1409 None => (ReceivedMime::from(mime), ClipboardContent::Empty),
1412 };
1413 ShellEvent::ClipboardData {
1414 window,
1415 request,
1416 mime,
1417 content,
1418 }
1419 }
1420
1421 fn resolve_reads(&mut self) {
1428 if self.reads.is_empty() {
1429 return;
1430 }
1431 let answerable = self.answerable();
1432 let mut held = Vec::with_capacity(self.reads.len());
1433 for mut read in core::mem::take(&mut self.reads) {
1434 if self.window(read.window).is_err() {
1438 continue;
1439 }
1440 if answerable {
1441 let answer = self.answer(read.window, read.request, read.mime);
1442 self.queue.push_back(answer);
1443 continue;
1444 }
1445 if read.countdown > 0 {
1446 read.countdown -= 1;
1447 held.push(read);
1448 continue;
1449 }
1450 self.queue.push_back(ShellEvent::ClipboardData {
1453 window: read.window,
1454 request: read.request,
1455 mime: ReceivedMime::from(read.mime),
1456 content: ClipboardContent::Unavailable,
1457 });
1458 }
1459 self.reads = held;
1460 }
1461
1462 fn deliver_due_configures(&mut self) {
1469 let mut due: Vec<(WindowId, PendingConfigure, Option<f64>)> = Vec::new();
1473 for (handle, state) in self.windows.iter_mut() {
1474 let Some(pending) = state.pending.as_mut() else {
1475 continue;
1476 };
1477 if pending.countdown > 0 {
1478 pending.countdown -= 1;
1479 continue;
1480 }
1481 let pending = state.pending.take().expect("checked just above");
1482 let previous_scale = state.configuration.map(|config| config.scale_factor);
1483 state.configuration = Some(WindowConfiguration {
1484 size: pending.size,
1485 scale_factor: pending.scale_factor,
1486 mode: pending.mode,
1487 });
1488 due.push((handle.cast(), pending, previous_scale));
1489 }
1490
1491 for (window, pending, previous_scale) in due {
1492 if previous_scale.is_some_and(|previous| previous != pending.scale_factor) {
1496 self.queue.push_back(ShellEvent::ScaleFactorChanged {
1497 window,
1498 scale_factor: pending.scale_factor,
1499 size: pending.size,
1500 });
1501 }
1502 self.queue.push_back(ShellEvent::Resized {
1503 window,
1504 size: pending.size,
1505 scale_factor: pending.scale_factor,
1506 });
1507 }
1508 }
1509
1510 fn window(&self, window: WindowId) -> Result<&HeadlessWindow, ShellError> {
1511 self.windows
1512 .get(window.cast())
1513 .ok_or_else(|| ShellError::invalid_window(window))
1514 }
1515
1516 fn window_mut(&mut self, window: WindowId) -> Result<&mut HeadlessWindow, ShellError> {
1517 self.windows
1518 .get_mut(window.cast())
1519 .ok_or_else(|| ShellError::invalid_window(window))
1520 }
1521
1522 fn check_window(&self, window: WindowId) -> Result<(), ShellError> {
1523 self.window(window).map(|_| ())
1524 }
1525
1526 fn unsupported(&self, what: &'static str) -> ShellError {
1527 ShellError::Unsupported {
1528 backend: ShellBackend::Headless,
1529 what,
1530 }
1531 }
1532}
1533
1534fn synthetic_scancode(key: KeyCode) -> Scancode {
1538 let index = KeyCode::ALL
1539 .iter()
1540 .position(|candidate| *candidate == key)
1541 .unwrap_or(0);
1542 Scancode(index as u32 + 8)
1545}
1546
1547fn synthetic_keysym(key: KeyCode) -> Keysym {
1549 let name = key.as_str();
1550 if let Some(letter) = name.strip_prefix("Key") {
1551 return Keysym::from_char(letter.to_ascii_lowercase().chars().next().unwrap_or('?'));
1553 }
1554 if let Some(digit) = name.strip_prefix("Digit") {
1555 return Keysym::from_char(digit.chars().next().unwrap_or('?'));
1556 }
1557 match key {
1558 KeyCode::Space => Keysym::from_char(' '),
1559 KeyCode::Minus => Keysym::from_char('-'),
1560 KeyCode::Equal => Keysym::from_char('='),
1561 KeyCode::Comma => Keysym::from_char(','),
1562 KeyCode::Period => Keysym::from_char('.'),
1563 KeyCode::Slash => Keysym::from_char('/'),
1564 KeyCode::Semicolon => Keysym::from_char(';'),
1565 KeyCode::Quote => Keysym::from_char('\''),
1566 KeyCode::Backslash => Keysym::from_char('\\'),
1567 KeyCode::BracketLeft => Keysym::from_char('['),
1568 KeyCode::BracketRight => Keysym::from_char(']'),
1569 KeyCode::Backquote => Keysym::from_char('`'),
1570 _ => crate::keysym::named_keysym(key).unwrap_or(Keysym::NONE),
1574 }
1575}
1576
1577impl Shell for HeadlessShell {
1578 fn backend(&self) -> ShellBackend {
1579 ShellBackend::Headless
1580 }
1581
1582 fn caps(&self) -> ShellCaps {
1583 self.caps
1584 }
1585
1586 fn create_window(&mut self, desc: &WindowDesc<'_>) -> Result<WindowId, ShellError> {
1587 crate::role::check(desc, self.caps(), self.backend(), |monitor| {
1588 self.monitor(monitor).is_some()
1589 })?;
1590 if !self.windows.is_empty() && !self.caps.contains(ShellCaps::MULTI_WINDOW) {
1591 return Err(self.unsupported("a second window"));
1592 }
1593 if let DisplayMode::Borderless {
1594 monitor: Some(monitor),
1595 } = desc.mode
1596 && self.monitor(monitor).is_none()
1597 {
1598 return Err(ShellError::NoSuchMonitor(monitor.0));
1599 }
1600
1601 let (size, scale_factor) = match desc.mode {
1613 DisplayMode::Windowed => {
1614 let scale_factor = self.monitors[0].scale_factor;
1615 (
1616 desc.constraints
1617 .apply(desc.size.to_physical(scale_factor), scale_factor),
1618 scale_factor,
1619 )
1620 }
1621 DisplayMode::Borderless { monitor } => {
1622 let monitor = monitor
1623 .and_then(|id| self.monitor(id))
1624 .unwrap_or(&self.monitors[0]);
1625 (monitor.size(), monitor.scale_factor)
1626 }
1627 };
1628
1629 let (parent, popup_input, panel) = match desc.role {
1630 crate::WindowRole::Popup(popup) => {
1631 self.window(popup.parent)?;
1632 (Some(popup.parent), Some(popup.input), None)
1633 }
1634 crate::WindowRole::Panel(panel) => {
1635 (None, None, Some(crate::role::KeptPanel::new(&panel)))
1636 }
1637 crate::WindowRole::Toplevel => (None, None, None),
1638 };
1639 let countdown = self.configure_delay;
1640 let window: WindowId = self
1641 .windows
1642 .insert(HeadlessWindow {
1643 title: desc.title.to_string(),
1644 configuration: None,
1647 pending: Some(PendingConfigure {
1648 size,
1649 scale_factor,
1650 mode: desc.mode,
1651 countdown,
1652 }),
1653 requested_mode: desc.mode,
1654 requested_constraints: desc.constraints,
1655 focused: false,
1656 visible: desc.visible,
1657 pointer_mode: PointerMode::Free,
1658 cursor: Some(CursorIcon::Default),
1659 close_pending: false,
1660 accept_drops: desc.accept_drops,
1661 parent,
1662 popup_input,
1663 panel,
1664 text_input: None,
1665 text_input_area: None,
1666 input_region: None,
1667 #[cfg(feature = "accesskit")]
1668 accessibility: None,
1669 })
1670 .cast();
1671 Ok(window)
1672 }
1673
1674 fn destroy_window(&mut self, window: WindowId) -> Result<(), ShellError> {
1678 self.window(window)?;
1679 let popups: Vec<WindowId> = self
1680 .windows
1681 .iter()
1682 .filter(|(_, state)| state.parent == Some(window))
1683 .map(|(id, _)| id.cast())
1684 .collect();
1685 for popup in popups {
1686 self.destroy_window(popup)?;
1687 }
1688 self.windows
1689 .remove(window.cast())
1690 .ok_or_else(|| ShellError::invalid_window(window))?;
1691 self.queue.push_back(ShellEvent::WindowDestroyed { window });
1692 Ok(())
1693 }
1694
1695 fn window_state(&self, window: WindowId) -> Result<WindowState, ShellError> {
1696 let state = self.window(window)?;
1697 Ok(WindowState {
1698 configuration: state.configuration,
1699 requested_mode: state.requested_mode,
1700 requested_constraints: state.requested_constraints,
1701 focused: state.focused,
1702 visible: state.visible,
1703 pointer_mode: state.pointer_mode,
1704 close_pending: state.close_pending,
1705 })
1706 }
1707
1708 fn set_title(&mut self, window: WindowId, title: &str) -> Result<(), ShellError> {
1709 self.window_mut(window)?.title = title.to_string();
1710 Ok(())
1711 }
1712
1713 fn set_visible(&mut self, window: WindowId, visible: bool) -> Result<(), ShellError> {
1714 self.window_mut(window)?.visible = visible;
1715 Ok(())
1716 }
1717
1718 fn set_mode(&mut self, window: WindowId, mode: DisplayMode) -> Result<(), ShellError> {
1719 let target = match mode {
1720 DisplayMode::Windowed => None,
1721 DisplayMode::Borderless { monitor } => {
1722 let monitor = match monitor {
1723 Some(id) => self
1724 .monitor(id)
1725 .ok_or(ShellError::NoSuchMonitor(id.0))?
1726 .clone(),
1727 None => self.monitors[0].clone(),
1728 };
1729 Some(monitor)
1730 }
1731 };
1732 let countdown = self.configure_delay;
1733
1734 let state = self.window_mut(window)?;
1735 state.requested_mode = mode;
1741 let (size, scale_factor) = match &target {
1742 Some(monitor) => (monitor.size(), monitor.scale_factor),
1743 None => (state.base_size(), state.scale_factor()),
1747 };
1748 state.pending = Some(PendingConfigure {
1749 size,
1750 scale_factor,
1751 mode,
1752 countdown,
1753 });
1754 Ok(())
1755 }
1756
1757 fn set_constraints(
1758 &mut self,
1759 window: WindowId,
1760 constraints: SizeConstraints,
1761 ) -> Result<(), ShellError> {
1762 let countdown = self.configure_delay;
1763 let state = self.window_mut(window)?;
1764 state.requested_constraints = constraints;
1765
1766 if let Some(pending) = state.pending.as_mut() {
1773 pending.size = constraints.apply(pending.size, pending.scale_factor);
1774 return Ok(());
1775 }
1776
1777 if let Some(config) = state.configuration {
1781 let constrained = constraints.apply(config.size, config.scale_factor);
1782 if constrained != config.size {
1783 state.pending = Some(PendingConfigure {
1784 size: constrained,
1785 scale_factor: config.scale_factor,
1786 mode: config.mode,
1787 countdown,
1788 });
1789 }
1790 }
1791 Ok(())
1792 }
1793
1794 fn reposition_popup(
1798 &mut self,
1799 popup: WindowId,
1800 anchor: LogicalRect,
1801 size: LogicalSize,
1802 ) -> Result<(), ShellError> {
1803 let _ = anchor;
1804 let countdown = self.configure_delay;
1805 let state = self.window_mut(popup)?;
1806 if state.parent.is_none() {
1807 return Err(crate::role::not_a_popup(popup));
1808 }
1809 state.configure_at(size, countdown);
1810 Ok(())
1811 }
1812
1813 fn set_panel(
1817 &mut self,
1818 window: WindowId,
1819 size: LogicalSize,
1820 panel: &crate::PanelDesc<'_>,
1821 ) -> Result<(), ShellError> {
1822 let was = self.window(window)?.panel.as_ref().map(|kept| kept.desc());
1823 crate::role::check_set_panel(window, was, panel, self.caps, self.backend(), |monitor| {
1824 self.monitor(monitor).is_some()
1825 })?;
1826 let countdown = self.configure_delay;
1827 let state = self.window_mut(window)?;
1828 state.panel = Some(crate::role::KeptPanel::new(panel));
1829 state.configure_at(size, countdown);
1830 Ok(())
1831 }
1832
1833 fn monitors(&self) -> &[MonitorInfo] {
1834 &self.monitors
1835 }
1836
1837 fn pump(&mut self, sink: &mut dyn FnMut(ShellEvent)) {
1838 self.keep_alive();
1839 if self.outputs_powered {
1840 let windows = &self.windows;
1841 self.queue.extend(
1842 self.frames
1843 .take_due(|window| windows.contains(window.cast())),
1844 );
1845 }
1846 for _ in 0..self.queue.len() {
1851 let Some(event) = self.queue.pop_front() else {
1852 break;
1853 };
1854 sink(event);
1855 }
1856 }
1857
1858 fn keep_alive(&mut self) {
1866 self.deliver_due_configures();
1867 self.resolve_reads();
1868 }
1869
1870 fn wait_events(&mut self, timeout: Option<Duration>) {
1890 self.waits += 1;
1891 if !self.caps.contains(ShellCaps::EVENT_WAIT)
1892 || !self.queue.is_empty()
1893 || (self.outputs_powered && self.frames.is_pending())
1894 {
1895 return;
1896 }
1897 match (&self.wake, timeout) {
1898 (Some(wake), timeout) => wake.wait(timeout),
1899 (None, Some(timeout)) => std::thread::sleep(timeout),
1900 (None, None) => {}
1901 }
1902 }
1903
1904 fn waker(&mut self) -> Option<ShellWaker> {
1907 let wake = self.wake.get_or_insert_with(Arc::default);
1908 Some(ShellWaker::new(Arc::clone(wake) as Arc<dyn WakeSignal>))
1909 }
1910
1911 fn align_event_clock(&mut self, elapsed: Duration) {
1918 self.set_time(elapsed);
1919 }
1920
1921 fn surface_target(&self, window: WindowId) -> Result<SurfaceTarget, ShellError> {
1922 self.window(window)?;
1928 Ok(SurfaceTarget::Offscreen)
1929 }
1930
1931 fn set_pointer_mode(&mut self, window: WindowId, mode: PointerMode) -> Result<(), ShellError> {
1932 if !self.caps.contains(mode.required_cap()) {
1933 return Err(self.unsupported(mode.as_str()));
1934 }
1935 self.window_mut(window)?.pointer_mode = mode;
1936 Ok(())
1937 }
1938
1939 fn set_text_input(
1943 &mut self,
1944 window: WindowId,
1945 field: Option<TextPurpose>,
1946 ) -> Result<(), ShellError> {
1947 self.window_mut(window)?.text_input = field;
1948 Ok(())
1949 }
1950
1951 fn set_text_input_area(
1955 &mut self,
1956 window: WindowId,
1957 area: Option<PhysicalRect>,
1958 ) -> Result<(), ShellError> {
1959 self.window_mut(window)?.text_input_area = area;
1960 Ok(())
1961 }
1962
1963 fn set_input_region(
1968 &mut self,
1969 window: WindowId,
1970 region: Option<&[PhysicalRect]>,
1971 ) -> Result<(), ShellError> {
1972 self.window_mut(window)?.input_region = region.map(<[PhysicalRect]>::to_vec);
1973 Ok(())
1974 }
1975
1976 fn request_frame(&mut self, window: WindowId) -> Result<(), ShellError> {
1981 self.window(window)?;
1982 self.frames.request(window);
1983 Ok(())
1984 }
1985
1986 #[cfg(feature = "accesskit")]
1987 fn update_accessibility(
1988 &mut self,
1989 window: WindowId,
1990 update: accesskit::TreeUpdate,
1991 ) -> Result<(), ShellError> {
1992 if let Some(updates) = &mut self.window_mut(window)?.accessibility {
1993 updates.push(update);
1994 }
1995 Ok(())
1996 }
1997
1998 fn set_cursor(
1999 &mut self,
2000 window: WindowId,
2001 cursor: Option<CursorIcon>,
2002 ) -> Result<(), ShellError> {
2003 self.window_mut(window)?.cursor = cursor;
2004 Ok(())
2005 }
2006
2007 fn warp_pointer(
2008 &mut self,
2009 window: WindowId,
2010 position: PhysicalPoint,
2011 ) -> Result<(), ShellError> {
2012 if !self.caps.contains(ShellCaps::POINTER_WARP) {
2013 return Err(self.unsupported("pointer warp"));
2014 }
2015 self.check_window(window)?;
2016 let event = ShellEvent::PointerMotion {
2019 window,
2020 device: VIRTUAL_DEVICE,
2021 time: self.now(),
2022 abs: Some(position),
2023 raw_delta: None,
2024 };
2025 self.queue.push_back(event);
2026 Ok(())
2027 }
2028
2029 fn reply_close_request(
2030 &mut self,
2031 window: WindowId,
2032 reply: CloseReply,
2033 ) -> Result<(), ShellError> {
2034 let state = self.window_mut(window)?;
2035 if !state.close_pending {
2036 return Err(ShellError::NoPendingCloseRequest { window });
2037 }
2038 state.close_pending = false;
2039 match reply {
2040 CloseReply::Close => self.destroy_window(window),
2041 CloseReply::Keep => Ok(()),
2042 }
2043 }
2044
2045 fn clipboard_offer(
2056 &mut self,
2057 window: WindowId,
2058 offers: &[ClipboardOffer<'_>],
2059 ) -> Result<(), ShellError> {
2060 if !self.caps.contains(ShellCaps::CLIPBOARD) {
2061 return Err(self.unsupported("clipboard"));
2062 }
2063 self.check_window(window)?;
2064 if self.clipboard_needs_interaction && !self.saw_interaction {
2065 return Err(ShellError::NeedsUserInteraction {
2066 backend: ShellBackend::Headless,
2067 what: "claiming the clipboard",
2068 });
2069 }
2070 self.clipboard = offers
2071 .iter()
2072 .map(|offer| (ReceivedMime::from(offer.mime), offer.bytes.to_vec()))
2073 .collect();
2074 Ok(())
2075 }
2076
2077 fn clipboard_request(
2091 &mut self,
2092 window: WindowId,
2093 mime: MimeType,
2094 ) -> Result<ClipboardRequestId, ShellError> {
2095 if !self.caps.contains(ShellCaps::CLIPBOARD) {
2096 return Err(self.unsupported("clipboard"));
2097 }
2098 self.check_window(window)?;
2099 let request = ClipboardRequestId(self.next_request_id);
2100 self.next_request_id += 1;
2101 if self.answerable() {
2102 let answer = self.answer(window, request, mime);
2103 self.queue.push_back(answer);
2107 } else {
2108 self.reads.push(PendingRead {
2109 window,
2110 request,
2111 mime,
2112 countdown: self.clipboard_deadline,
2113 });
2114 }
2115 Ok(request)
2116 }
2117
2118 fn clipboard_readable(&self, window: WindowId) -> bool {
2124 self.caps.contains(ShellCaps::CLIPBOARD) && self.window(window).is_ok() && self.answerable()
2125 }
2126}
2127
2128#[cfg(test)]
2129mod tests {
2130 use super::*;
2131 use crate::{AspectRatio, LogicalSize};
2132
2133 fn shell_with_window() -> (HeadlessShell, WindowId) {
2135 let mut shell = HeadlessShell::new();
2136 let window = shell
2137 .create_window(&WindowDesc::default())
2138 .expect("headless window creation never fails");
2139 (shell, window)
2140 }
2141
2142 fn shell_with_configured_window() -> (HeadlessShell, WindowId) {
2144 let (mut shell, window) = shell_with_window();
2145 settle(&mut shell);
2146 assert!(shell.window_state(window).expect("state").is_configured());
2147 (shell, window)
2148 }
2149
2150 fn settle(shell: &mut HeadlessShell) {
2152 for _ in 0..8 {
2153 let mut seen = 0;
2154 shell.pump(&mut |_| seen += 1);
2155 if seen == 0 && shell.windows.iter().all(|(_, w)| w.pending.is_none()) {
2156 return;
2157 }
2158 }
2159 panic!("the shell never settled");
2160 }
2161
2162 fn drain(shell: &mut HeadlessShell) -> Vec<ShellEvent> {
2163 let mut events = Vec::new();
2164 shell.pump(&mut |event| events.push(event));
2165 events
2166 }
2167
2168 fn names(events: &[ShellEvent]) -> Vec<&'static str> {
2169 events.iter().map(ShellEvent::name).collect()
2170 }
2171
2172 fn create_swapchain(state: &WindowState) -> Result<PhysicalSize, &'static str> {
2175 let size = state.size().ok_or("window is not configured yet")?;
2176 if size.is_empty() {
2177 return Err("zero-extent swapchain");
2178 }
2179 Ok(size)
2180 }
2181
2182 #[cfg(feature = "accesskit")]
2187 #[test]
2188 fn a_tree_reaches_the_reader_only_while_it_reads() {
2189 use accesskit::{Action, ActionRequest, Node, NodeId, Role, TreeId, TreeInfo, TreeUpdate};
2190
2191 let (mut shell, window) = shell_with_configured_window();
2192 let update = |label: &str| TreeUpdate {
2193 nodes: vec![(NodeId(1), {
2194 let mut node = Node::new(Role::Window);
2195 node.set_label(label);
2196 node
2197 })],
2198 tree: Some(TreeInfo::new(NodeId(1))),
2199 tree_id: TreeId::ROOT,
2200 focus: NodeId(1),
2201 };
2202
2203 shell
2204 .update_accessibility(window, update("unread"))
2205 .expect("update");
2206 assert_eq!(shell.accessibility_updates(window).expect("updates"), None);
2207
2208 shell.activate_accessibility(window).expect("activate");
2209 shell
2210 .activate_accessibility(window)
2211 .expect("activate twice");
2212 shell
2213 .update_accessibility(window, update("read"))
2214 .expect("update");
2215 let click = ActionRequest {
2216 action: Action::Click,
2217 target_tree: TreeId::ROOT,
2218 target_node: NodeId(1),
2219 data: None,
2220 };
2221 shell
2222 .request_accessibility_action(window, click.clone())
2223 .expect("action");
2224 assert_eq!(
2225 shell.accessibility_updates(window).expect("updates"),
2226 Some(&[update("read")][..])
2227 );
2228 let events = drain(&mut shell);
2229 assert_eq!(
2230 names(&events),
2231 ["AccessibilityActivated", "AccessibilityAction"]
2232 );
2233 assert_eq!(
2234 events[1],
2235 ShellEvent::AccessibilityAction {
2236 window,
2237 request: click
2238 }
2239 );
2240
2241 shell.deactivate_accessibility(window).expect("deactivate");
2242 shell
2243 .deactivate_accessibility(window)
2244 .expect("deactivate twice");
2245 shell
2246 .update_accessibility(window, update("gone"))
2247 .expect("update");
2248 assert_eq!(shell.accessibility_updates(window).expect("updates"), None);
2249 assert_eq!(names(&drain(&mut shell)), ["AccessibilityDeactivated"]);
2250 }
2251
2252 #[test]
2255 fn a_new_window_has_no_size_until_a_later_pump() {
2256 let (mut shell, window) = shell_with_window();
2257
2258 let state = shell.window_state(window).expect("state");
2261 assert!(!state.is_configured());
2262 assert_eq!(state.size(), None);
2263 assert_eq!(state.scale_factor(), None);
2264 assert_eq!(state.effective_mode(), None);
2265 assert_eq!(state.requested_mode, DisplayMode::Windowed);
2266
2267 assert_eq!(drain(&mut shell), Vec::new());
2269 assert!(!shell.window_state(window).expect("state").is_configured());
2270
2271 let events = drain(&mut shell);
2273 assert_eq!(
2274 events,
2275 vec![ShellEvent::Resized {
2276 window,
2277 size: PhysicalSize::new(1280, 720),
2278 scale_factor: 1.0,
2279 }]
2280 );
2281 let state = shell.window_state(window).expect("state");
2282 assert_eq!(state.size(), Some(PhysicalSize::new(1280, 720)));
2283 assert_eq!(state.scale_factor(), Some(1.0));
2284 assert_eq!(state.effective_mode(), Some(DisplayMode::Windowed));
2285 assert!(state.mode_request_honoured());
2286 }
2287
2288 #[test]
2292 fn keep_alive_takes_a_turn_and_keeps_everything_for_the_next_pump() {
2293 let (mut shell, window) = shell_with_window();
2294 shell.inject(ShellEvent::CloseRequested { window });
2295
2296 for _ in 0..=DEFAULT_CONFIGURE_DELAY {
2298 shell.keep_alive();
2299 }
2300 assert!(
2301 shell.window_state(window).expect("state").is_configured(),
2302 "every turn the configure waits for was taken by keep_alive"
2303 );
2304
2305 assert_eq!(
2306 drain(&mut shell),
2307 vec![
2308 ShellEvent::CloseRequested { window },
2309 ShellEvent::Resized {
2310 window,
2311 size: PhysicalSize::new(1280, 720),
2312 scale_factor: 1.0,
2313 },
2314 ],
2315 "nothing keep_alive saw was delivered or dropped"
2316 );
2317 assert_eq!(drain(&mut shell), Vec::new(), "and each arrived once");
2318 }
2319
2320 #[test]
2321 fn a_consumer_that_skips_the_wait_gets_a_clean_error_not_a_wrong_extent() {
2322 let (mut shell, window) = shell_with_window();
2323
2324 assert_eq!(
2326 create_swapchain(&shell.window_state(window).expect("state")),
2327 Err("window is not configured yet")
2328 );
2329
2330 let target = shell.surface_target(window).expect("the handle exists");
2336 assert_eq!(target, SurfaceTarget::Offscreen);
2337
2338 let size = loop {
2340 shell.pump(&mut |_| {});
2341 if let Ok(size) = create_swapchain(&shell.window_state(window).expect("state")) {
2342 break size;
2343 }
2344 };
2345 assert_eq!(size, PhysicalSize::new(1280, 720));
2346 assert_eq!(
2349 shell.surface_target(window).expect("target"),
2350 target,
2351 "the target is invariant under a configure"
2352 );
2353 }
2354
2355 #[test]
2356 fn the_configure_delay_is_a_knob() {
2357 let mut shell = HeadlessShell::new().with_configure_delay(3);
2358 let window = shell.create_window(&WindowDesc::default()).expect("window");
2359 for pump in 0..3 {
2360 assert_eq!(drain(&mut shell), Vec::new(), "pump {pump}");
2361 assert!(!shell.window_state(window).expect("state").is_configured());
2362 }
2363 assert_eq!(names(&drain(&mut shell)), ["Resized"]);
2364 assert!(shell.window_state(window).expect("state").is_configured());
2365
2366 let mut shell = HeadlessShell::new().with_configure_delay(0);
2369 let window = shell.create_window(&WindowDesc::default()).expect("window");
2370 assert_eq!(names(&drain(&mut shell)), ["Resized"]);
2371 assert!(shell.window_state(window).expect("state").is_configured());
2372
2373 shell.set_configure_delay(2);
2374 shell
2375 .set_mode(window, DisplayMode::Borderless { monitor: None })
2376 .expect("request");
2377 assert_eq!(drain(&mut shell), Vec::new());
2378 assert_eq!(drain(&mut shell), Vec::new());
2379 assert_eq!(names(&drain(&mut shell)), ["Resized"]);
2380 }
2381
2382 #[test]
2383 fn set_mode_is_a_request_whose_effect_arrives_later() {
2384 let (mut shell, window) = shell_with_configured_window();
2385 shell
2386 .set_mode(window, DisplayMode::Borderless { monitor: None })
2387 .expect("request");
2388
2389 let state = shell.window_state(window).expect("state");
2391 assert_eq!(
2392 state.requested_mode,
2393 DisplayMode::Borderless { monitor: None }
2394 );
2395 assert_eq!(
2396 state.effective_mode(),
2397 Some(DisplayMode::Windowed),
2398 "still windowed until the window system says otherwise"
2399 );
2400 assert!(
2401 !state.mode_request_honoured(),
2402 "a UI toggle reading the request instead of the effect would lie here"
2403 );
2404 assert_eq!(state.size(), Some(PhysicalSize::new(1280, 720)));
2405
2406 settle(&mut shell);
2407 let state = shell.window_state(window).expect("state");
2408 assert_eq!(
2409 state.effective_mode(),
2410 Some(DisplayMode::Borderless { monitor: None })
2411 );
2412 assert!(state.mode_request_honoured());
2413 assert_eq!(state.size(), Some(PhysicalSize::new(1920, 1080)));
2414 }
2415
2416 #[test]
2417 fn a_refused_mode_is_expressible_through_injection() {
2418 let (mut shell, window) = shell_with_configured_window();
2421 shell
2422 .set_mode(window, DisplayMode::Borderless { monitor: None })
2423 .expect("request");
2424 shell
2426 .resize(window, PhysicalSize::new(1280, 720))
2427 .expect("answer");
2428 let state = shell.window_state(window).expect("state");
2429 assert_eq!(state.effective_mode(), Some(DisplayMode::Windowed));
2430 assert!(!state.mode_request_honoured());
2431 }
2432
2433 #[test]
2439 fn a_reconfigured_panel_is_configured_at_its_new_size() {
2440 let mut shell = HeadlessShell::new();
2441 let monitor = shell.monitors()[0].id;
2442 let bar = crate::PanelDesc {
2443 namespace: "bar",
2444 monitor: Some(monitor),
2445 anchor: crate::PanelAnchor::TOP | crate::PanelAnchor::LEFT | crate::PanelAnchor::RIGHT,
2446 reserve: crate::Reserve::OwnExtent,
2447 ..crate::PanelDesc::default()
2448 };
2449 let window = shell
2450 .create_window(&WindowDesc {
2451 size: LogicalSize::new(400.0, 30.0),
2452 role: crate::WindowRole::Panel(bar),
2453 ..WindowDesc::default()
2454 })
2455 .expect("a panel");
2456 settle(&mut shell);
2457 let scale = shell
2458 .window_state(window)
2459 .expect("live")
2460 .scale_factor()
2461 .expect("configured");
2462
2463 let taller = crate::PanelDesc {
2464 layer: crate::PanelLayer::Overlay,
2465 margins: crate::Margins {
2466 top: 4.0,
2467 ..crate::Margins::default()
2468 },
2469 keyboard: crate::PanelKeyboard::OnDemand,
2470 ..bar
2471 };
2472 let size = LogicalSize::new(400.0, 50.0);
2473 shell.set_panel(window, size, &taller).expect("set_panel");
2474 assert_eq!(shell.panel(window).expect("live"), Some(taller));
2475 assert_eq!(
2476 shell.window_state(window).expect("live").size(),
2477 Some(LogicalSize::new(400.0, 30.0).to_physical(scale)),
2478 "a request: nothing changes before the window system answers"
2479 );
2480 let mut resized = Vec::new();
2481 for _ in 0..8 {
2482 resized.extend(
2483 drain(&mut shell)
2484 .into_iter()
2485 .filter_map(|event| match event {
2486 ShellEvent::Resized {
2487 window: of, size, ..
2488 } if of == window => Some(size),
2489 _ => None,
2490 }),
2491 );
2492 }
2493 assert_eq!(resized, [size.to_physical(scale)]);
2494
2495 let refused = |shell: &mut HeadlessShell, panel: &crate::PanelDesc<'_>| match shell
2496 .set_panel(window, LogicalSize::new(1.0, 1.0), panel)
2497 {
2498 Err(ShellError::InvalidDescriptor(message)) => message,
2499 other => panic!("expected InvalidDescriptor, got {other:?}"),
2500 };
2501 let renamed = crate::PanelDesc {
2502 namespace: "dock",
2503 ..taller
2504 };
2505 assert!(refused(&mut shell, &renamed).contains("namespace"));
2506 let anywhere = crate::PanelDesc {
2507 monitor: None,
2508 ..taller
2509 };
2510 assert!(refused(&mut shell, &anywhere).contains("monitor"));
2511 let toplevel = shell
2512 .create_window(&WindowDesc::default())
2513 .expect("a toplevel");
2514 assert!(matches!(
2515 shell.set_panel(toplevel, size, &taller),
2516 Err(ShellError::InvalidDescriptor(message)) if message.contains("not a panel")
2517 ));
2518 let cornered = crate::PanelDesc {
2519 anchor: crate::PanelAnchor::TOP | crate::PanelAnchor::LEFT,
2520 ..taller
2521 };
2522 assert!(matches!(
2523 shell.set_panel(window, size, &cornered),
2524 Err(ShellError::Unsupported { .. })
2525 ));
2526 assert_eq!(
2527 shell.panel(window).expect("live"),
2528 Some(taller),
2529 "a refused descriptor is not recorded"
2530 );
2531 assert_eq!(shell.panel(toplevel).expect("live"), None);
2532
2533 shell.destroy_window(window).expect("destroy");
2534 assert!(matches!(
2535 shell.set_panel(window, size, &taller),
2536 Err(ShellError::InvalidWindow { .. })
2537 ));
2538 }
2539
2540 #[test]
2543 fn a_repositioned_popup_is_configured_at_its_new_size() {
2544 let (mut shell, window) = shell_with_configured_window();
2545 let anchor = crate::LogicalRect::new(
2546 crate::LogicalPoint::new(10.0, 10.0),
2547 LogicalSize::new(100.0, 20.0),
2548 );
2549 let popup = shell
2550 .create_window(&WindowDesc {
2551 size: LogicalSize::new(200.0, 150.0),
2552 role: crate::WindowRole::Popup(crate::PopupDesc {
2553 parent: window,
2554 anchor,
2555 placement: crate::PopupPlacement::Below,
2556 input: crate::PopupInput::Pointer,
2557 }),
2558 ..WindowDesc::default()
2559 })
2560 .expect("a popup");
2561 settle(&mut shell);
2562 let scale = shell
2563 .window_state(popup)
2564 .expect("live")
2565 .scale_factor()
2566 .expect("configured");
2567
2568 let moved = LogicalSize::new(240.0, 90.0);
2569 shell
2570 .reposition_popup(popup, anchor, moved)
2571 .expect("reposition");
2572 settle(&mut shell);
2573 assert_eq!(
2574 shell.window_state(popup).expect("live").size(),
2575 Some(moved.to_physical(scale))
2576 );
2577 assert!(matches!(
2578 shell.reposition_popup(window, anchor, moved),
2579 Err(ShellError::InvalidDescriptor(_))
2580 ));
2581 }
2582
2583 #[test]
2587 fn escape_on_a_grabbing_popup_is_delivered_then_asks_it_to_close() {
2588 let (mut shell, window) = shell_with_configured_window();
2589 let popup_of = |shell: &mut HeadlessShell, input| {
2590 shell
2591 .create_window(&WindowDesc {
2592 role: crate::WindowRole::Popup(crate::PopupDesc {
2593 parent: window,
2594 anchor: crate::LogicalRect::default(),
2595 placement: crate::PopupPlacement::Below,
2596 input,
2597 }),
2598 ..WindowDesc::default()
2599 })
2600 .expect("a popup")
2601 };
2602 let menu = popup_of(&mut shell, crate::PopupInput::Grab);
2603 let tooltip = popup_of(&mut shell, crate::PopupInput::Pointer);
2604 settle(&mut shell);
2605 let is_close = |event: &ShellEvent| matches!(event, ShellEvent::CloseRequested { .. });
2606
2607 shell.key_press(menu, KeyCode::Escape).expect("press");
2608 let events = drain(&mut shell);
2609 assert!(
2610 matches!(
2611 events.as_slice(),
2612 [
2613 ShellEvent::Key {
2614 key_code: Some(KeyCode::Escape),
2615 ..
2616 },
2617 ShellEvent::CloseRequested { window: closed },
2618 ] if *closed == menu
2619 ),
2620 "the key, then the request: {events:?}"
2621 );
2622 shell.key_press(menu, KeyCode::Escape).expect("press");
2624 shell.key_repeat(menu, KeyCode::Escape).expect("repeat");
2625 assert!(!drain(&mut shell).iter().any(is_close));
2626
2627 shell
2629 .reply_close_request(menu, CloseReply::Keep)
2630 .expect("keep");
2631 shell.key_release(menu, KeyCode::Escape).expect("release");
2632 shell.key_press(menu, KeyCode::Enter).expect("press");
2633 assert!(!drain(&mut shell).iter().any(is_close), "not these");
2634 shell.key_press(menu, KeyCode::Escape).expect("press");
2635 assert!(drain(&mut shell).iter().any(is_close));
2636
2637 shell.key_press(tooltip, KeyCode::Escape).expect("press");
2638 shell.key_press(window, KeyCode::Escape).expect("press");
2639 assert!(!drain(&mut shell).iter().any(is_close));
2640 }
2641
2642 #[test]
2643 fn set_constraints_is_a_request_too() {
2644 let (mut shell, window) = shell_with_configured_window();
2645 let constraints = SizeConstraints::min(LogicalSize::new(1600.0, 900.0));
2646 shell.set_constraints(window, constraints).expect("request");
2647
2648 let state = shell.window_state(window).expect("state");
2649 assert_eq!(state.requested_constraints, constraints);
2650 assert_eq!(
2651 state.size(),
2652 Some(PhysicalSize::new(1280, 720)),
2653 "the size only changes when the window system says so"
2654 );
2655
2656 settle(&mut shell);
2657 assert_eq!(
2658 shell.window_state(window).expect("state").size(),
2659 Some(PhysicalSize::new(1600, 900))
2660 );
2661 }
2662
2663 #[test]
2664 fn constraints_do_not_cancel_a_mode_request_that_has_not_landed() {
2665 let (mut shell, window) = shell_with_configured_window();
2670 shell
2671 .set_mode(window, DisplayMode::Borderless { monitor: None })
2672 .expect("mode request");
2673 shell
2674 .set_constraints(window, SizeConstraints::min(LogicalSize::new(320.0, 180.0)))
2675 .expect("constraint request");
2676 settle(&mut shell);
2677
2678 let state = shell.window_state(window).expect("state");
2679 assert_eq!(
2680 state.effective_mode(),
2681 Some(DisplayMode::Borderless { monitor: None })
2682 );
2683 assert!(state.mode_request_honoured());
2684 }
2685
2686 #[test]
2687 fn a_borderless_window_reports_its_own_monitors_scale() {
2688 let mut shell = HeadlessShell::new();
2693 let hidpi = shell.plug_monitor(MonitorInfo {
2694 id: MonitorId(0),
2695 name: "HEADLESS-2".to_string(),
2696 bounds: PhysicalRect::new(1920, 0, 2560, 1440),
2697 work_area: PhysicalRect::new(1920, 0, 2560, 1440),
2698 scale_factor: 2.0,
2699 refresh_millihertz: 143_998,
2700 is_primary: false,
2701 });
2702 let window = shell
2703 .create_window(&WindowDesc {
2704 mode: DisplayMode::Borderless {
2705 monitor: Some(hidpi),
2706 },
2707 ..WindowDesc::default()
2708 })
2709 .expect("window");
2710 settle(&mut shell);
2711
2712 let state = shell.window_state(window).expect("state");
2713 assert_eq!(state.size(), Some(PhysicalSize::new(2560, 1440)));
2714 assert_eq!(
2715 state.scale_factor(),
2716 Some(2.0),
2717 "the scale is the borderless monitor's, not the primary's"
2718 );
2719 let other = shell
2722 .create_window(&WindowDesc::default())
2723 .expect("second window");
2724 settle(&mut shell);
2725 shell
2726 .set_mode(
2727 other,
2728 DisplayMode::Borderless {
2729 monitor: Some(hidpi),
2730 },
2731 )
2732 .expect("request");
2733 settle(&mut shell);
2734 assert_eq!(
2735 shell.window_state(other).expect("state").scale_factor(),
2736 shell.window_state(window).expect("state").scale_factor()
2737 );
2738 }
2739
2740 #[test]
2741 fn constraints_set_before_the_first_configure_fold_into_it() {
2742 let mut shell = HeadlessShell::new();
2743 let window = shell.create_window(&WindowDesc::default()).expect("window");
2744 shell
2745 .set_constraints(
2746 window,
2747 SizeConstraints::NONE.with_aspect(AspectRatio::SQUARE),
2748 )
2749 .expect("request");
2750 settle(&mut shell);
2751 assert_eq!(
2754 shell.window_state(window).expect("state").size(),
2755 Some(PhysicalSize::new(720, 720))
2756 );
2757 }
2758
2759 #[test]
2760 fn creation_uses_the_requested_size_and_the_monitors_scale() {
2761 let mut shell = HeadlessShell::new();
2762 let window = shell
2763 .create_window(&WindowDesc {
2764 size: LogicalSize::new(800.0, 600.0),
2765 ..WindowDesc::default()
2766 })
2767 .expect("window");
2768 settle(&mut shell);
2769 assert_eq!(
2770 shell.window_state(window).expect("state").size(),
2771 Some(PhysicalSize::new(800, 600))
2772 );
2773
2774 let mut shell = HeadlessShell::new().with_scale_factor(2.0);
2775 let window = shell
2776 .create_window(&WindowDesc {
2777 size: LogicalSize::new(640.0, 360.0),
2778 ..WindowDesc::default()
2779 })
2780 .expect("window");
2781 settle(&mut shell);
2782 let state = shell.window_state(window).expect("state");
2783 assert_eq!(state.size(), Some(PhysicalSize::new(1280, 720)));
2784 assert_eq!(state.scale_factor(), Some(2.0));
2785 }
2786
2787 #[test]
2790 fn scale_changes_preserve_logical_size_and_report_both_facts() {
2791 let (mut shell, window) = shell_with_configured_window();
2792 shell
2793 .change_scale_factor(window, 1.5)
2794 .expect("scale change");
2795 let events = drain(&mut shell);
2796 assert_eq!(
2797 events,
2798 vec![
2799 ShellEvent::ScaleFactorChanged {
2800 window,
2801 scale_factor: 1.5,
2802 size: PhysicalSize::new(1920, 1080),
2803 },
2804 ShellEvent::Resized {
2807 window,
2808 size: PhysicalSize::new(1920, 1080),
2809 scale_factor: 1.5,
2810 },
2811 ]
2812 );
2813 assert_eq!(
2817 shell.window_state(window).expect("state").size(),
2818 Some(PhysicalSize::new(1920, 1080))
2819 );
2820 }
2821
2822 #[test]
2823 fn a_scale_change_scheduled_by_a_mode_switch_arrives_before_the_resize() {
2824 let mut shell = HeadlessShell::new();
2825 let hidpi = shell.plug_monitor(MonitorInfo {
2826 id: MonitorId(0),
2827 name: "HEADLESS-2".to_string(),
2828 bounds: PhysicalRect::new(1920, 0, 2560, 1440),
2829 work_area: PhysicalRect::new(1920, 0, 2560, 1440),
2830 scale_factor: 2.0,
2831 refresh_millihertz: 143_998,
2832 is_primary: false,
2833 });
2834 assert_eq!(hidpi, MonitorId(2), "ids are never reused");
2835 let window = shell.create_window(&WindowDesc::default()).expect("window");
2836 settle(&mut shell);
2837
2838 shell
2839 .set_mode(
2840 window,
2841 DisplayMode::Borderless {
2842 monitor: Some(hidpi),
2843 },
2844 )
2845 .expect("request");
2846 assert_eq!(drain(&mut shell), Vec::new(), "still only a request");
2847 let events = drain(&mut shell);
2848 assert_eq!(names(&events), ["ScaleFactorChanged", "Resized"]);
2849
2850 let state = shell.window_state(window).expect("state");
2851 assert_eq!(state.size(), Some(PhysicalSize::new(2560, 1440)));
2852 assert_eq!(state.scale_factor(), Some(2.0));
2853 assert!(state.effective_mode().expect("configured").is_borderless());
2854 assert_eq!(
2859 shell.surface_target(window).expect("target"),
2860 SurfaceTarget::Offscreen
2861 );
2862
2863 assert!(matches!(
2864 shell.set_mode(
2865 window,
2866 DisplayMode::Borderless {
2867 monitor: Some(MonitorId(99)),
2868 },
2869 ),
2870 Err(ShellError::NoSuchMonitor(99))
2871 ));
2872 }
2873
2874 #[test]
2875 fn resizes_honour_constraints_and_inject_bypasses_them() {
2876 let (mut shell, window) = shell_with_configured_window();
2877 shell
2878 .set_constraints(
2879 window,
2880 SizeConstraints::min(LogicalSize::new(640.0, 360.0))
2881 .with_aspect(AspectRatio::WIDESCREEN),
2882 )
2883 .expect("request");
2884 settle(&mut shell);
2885
2886 shell
2887 .resize(window, PhysicalSize::new(1000, 1000))
2888 .expect("resize");
2889 assert_eq!(
2890 drain(&mut shell),
2891 vec![ShellEvent::Resized {
2892 window,
2893 size: PhysicalSize::new(1000, 562),
2894 scale_factor: 1.0,
2895 }]
2896 );
2897
2898 shell.inject(ShellEvent::Resized {
2901 window,
2902 size: PhysicalSize::new(300, 900),
2903 scale_factor: 1.0,
2904 });
2905 assert_eq!(
2906 drain(&mut shell)[0],
2907 ShellEvent::Resized {
2908 window,
2909 size: PhysicalSize::new(300, 900),
2910 scale_factor: 1.0,
2911 }
2912 );
2913 }
2914
2915 #[test]
2916 fn a_compositor_configure_supersedes_a_pending_one() {
2917 let (mut shell, window) = shell_with_configured_window();
2918 shell
2919 .set_mode(window, DisplayMode::Borderless { monitor: None })
2920 .expect("request");
2921 shell
2923 .resize(window, PhysicalSize::new(640, 480))
2924 .expect("answer");
2925 assert_eq!(names(&drain(&mut shell)), ["Resized"]);
2926 assert_eq!(
2927 shell.window_state(window).expect("state").size(),
2928 Some(PhysicalSize::new(640, 480))
2929 );
2930 assert_eq!(drain(&mut shell), Vec::new());
2932 }
2933
2934 #[test]
2937 fn timestamps_come_from_the_shells_clock_not_from_the_pump() {
2938 let (mut shell, window) = shell_with_configured_window();
2939 shell.set_time(Duration::from_secs(2));
2940 shell.key_press(window, KeyCode::Space).expect("press");
2941 shell.advance(Duration::from_millis(180));
2942 shell.key_release(window, KeyCode::Space).expect("release");
2943
2944 let events = drain(&mut shell);
2945 let held = events[1]
2946 .time()
2947 .expect("input")
2948 .saturating_since(events[0].time().expect("input"));
2949 assert_eq!(held, Duration::from_millis(180));
2950 assert_eq!(events[0].time(), Some(EventTime::from_millis(2_000)));
2951 }
2952
2953 #[test]
2954 fn synthesized_key_fields_are_stable_and_plausible() {
2955 let (mut shell, window) = shell_with_configured_window();
2956 shell.set_modifiers(Modifiers::CTRL);
2957 shell.key_press(window, KeyCode::KeyS).expect("press");
2958 let events = drain(&mut shell);
2959 let ShellEvent::Key {
2960 scancode,
2961 key_code,
2962 keysym,
2963 modifiers,
2964 repeat,
2965 device,
2966 ..
2967 } = events[0].clone()
2968 else {
2969 panic!("wrong variant");
2970 };
2971 assert_eq!(key_code, Some(KeyCode::KeyS));
2972 assert_eq!(keysym, Keysym::from_char('s'));
2973 assert_eq!(modifiers, Modifiers::CTRL);
2974 assert!(!repeat);
2975 assert_eq!(device, VIRTUAL_DEVICE);
2976 assert!(scancode.0 >= 8, "evdev-shaped scancodes start at 8");
2977 assert_eq!(synthetic_keysym(KeyCode::Digit7), Keysym::from_char('7'));
2978 assert_eq!(synthetic_keysym(KeyCode::F5), Keysym(0xFFC2));
2979 assert_eq!(synthetic_keysym(KeyCode::Enter), Keysym(0xFF0D));
2980 assert_eq!(synthetic_keysym(KeyCode::Escape), Keysym(0xFF1B));
2981 assert_eq!(synthetic_keysym(KeyCode::ArrowLeft), Keysym(0xFF51));
2982 assert_eq!(synthetic_keysym(KeyCode::Slash), Keysym::from_char('/'));
2983
2984 shell.key_repeat(window, KeyCode::KeyS).expect("repeat");
2985 let events = drain(&mut shell);
2986 let ShellEvent::Key { repeat, .. } = events[0] else {
2987 panic!("wrong variant");
2988 };
2989 assert!(repeat);
2990 }
2991
2992 #[test]
2993 fn a_locked_pointer_reports_no_absolute_position() {
2994 let (mut shell, window) = shell_with_configured_window();
2995 shell
2996 .set_pointer_mode(window, PointerMode::Locked)
2997 .expect("lock");
2998 shell
2999 .move_pointer(window, PhysicalPoint::new(10.0, 10.0), (3.0, -4.0))
3000 .expect("motion");
3001 let events = drain(&mut shell);
3002 let ShellEvent::PointerMotion { abs, raw_delta, .. } = events[0] else {
3003 panic!("wrong variant");
3004 };
3005 assert_eq!(abs, None);
3006 assert_eq!(raw_delta, Some((3.0, -4.0)));
3007 }
3008
3009 #[test]
3010 fn a_backend_without_raw_motion_reports_none() {
3011 let mut shell = HeadlessShell::new()
3012 .with_caps(ShellCaps::DESKTOP - ShellCaps::RAW_POINTER_MOTION)
3013 .with_configure_delay(0);
3014 let window = shell.create_window(&WindowDesc::default()).expect("window");
3015 drain(&mut shell);
3016 shell
3017 .move_pointer(window, PhysicalPoint::new(1.0, 2.0), (5.0, 5.0))
3018 .expect("motion");
3019 let events = drain(&mut shell);
3020 let ShellEvent::PointerMotion { abs, raw_delta, .. } = events[0] else {
3021 panic!("wrong variant");
3022 };
3023 assert_eq!(abs, Some(PhysicalPoint::new(1.0, 2.0)));
3024 assert_eq!(raw_delta, None, "no relative-motion source");
3025 assert!(!shell.caps().has_mouselook());
3026 }
3027
3028 #[test]
3029 fn pointer_focus_and_scroll_and_text_are_injectable() {
3030 let (mut shell, window) = shell_with_configured_window();
3031 shell
3032 .set_pointer_focus(window, true, Some(PhysicalPoint::new(1.0, 1.0)))
3033 .expect("enter");
3034 shell
3035 .scroll(window, ScrollDelta::Lines { x: 0.0, y: -1.0 }, None)
3036 .expect("scroll");
3037 shell
3038 .button(
3039 window,
3040 PointerButton::Left,
3041 ButtonState::Pressed,
3042 Some(PhysicalPoint::new(1.0, 1.0)),
3043 )
3044 .expect("click");
3045 shell.commit_text(window, "ありがとう").expect("text");
3046 shell
3047 .set_pointer_focus(window, false, Some(PhysicalPoint::new(1.0, 1.0)))
3048 .expect("leave");
3049
3050 let events = drain(&mut shell);
3051 assert_eq!(
3052 names(&events),
3053 [
3054 "PointerFocus",
3055 "Wheel",
3056 "Button",
3057 "TextCommit",
3058 "PointerFocus"
3059 ]
3060 );
3061 let ShellEvent::PointerFocus {
3062 entered, position, ..
3063 } = &events[4]
3064 else {
3065 panic!("wrong variant");
3066 };
3067 assert!(!entered);
3068 assert_eq!(*position, None, "a leave has no position");
3069 assert!(events.iter().all(ShellEvent::is_input));
3070 }
3071
3072 #[test]
3079 fn contacts_are_injectable_and_a_touchless_shell_refuses_them() {
3080 let (mut shell, window) = shell_with_configured_window();
3081 assert!(shell.caps().contains(ShellCaps::TOUCH));
3082 for (contact, phase, x) in [
3083 (1, TouchPhase::Began, 10.0),
3084 (2, TouchPhase::Began, 200.0),
3085 (2, TouchPhase::Moved, 220.0),
3086 (1, TouchPhase::Ended, 10.0),
3087 (2, TouchPhase::Cancelled, 220.0),
3088 ] {
3089 shell
3090 .touch(
3091 window,
3092 ContactId(contact),
3093 phase,
3094 PhysicalPoint::new(x, 5.0),
3095 )
3096 .expect("a contact");
3097 }
3098 let events = drain(&mut shell);
3099 assert_eq!(names(&events), ["Touch"; 5]);
3100 let seen: Vec<(ContactId, TouchPhase, f64)> = events
3101 .iter()
3102 .map(|event| {
3103 let ShellEvent::Touch {
3104 contact,
3105 phase,
3106 position,
3107 ..
3108 } = event
3109 else {
3110 panic!("wrong variant");
3111 };
3112 (*contact, *phase, position.x)
3113 })
3114 .collect();
3115 assert_eq!(
3116 seen,
3117 vec![
3118 (ContactId(1), TouchPhase::Began, 10.0),
3119 (ContactId(2), TouchPhase::Began, 200.0),
3120 (ContactId(2), TouchPhase::Moved, 220.0),
3121 (ContactId(1), TouchPhase::Ended, 10.0),
3122 (ContactId(2), TouchPhase::Cancelled, 220.0),
3123 ]
3124 );
3125 assert!(events.iter().all(ShellEvent::is_input));
3126
3127 let mut touchless = HeadlessShell::new()
3128 .with_caps(ShellCaps::DESKTOP - ShellCaps::TOUCH)
3129 .with_configure_delay(0);
3130 let window = touchless
3131 .create_window(&WindowDesc::default())
3132 .expect("window");
3133 drain(&mut touchless);
3134 assert!(matches!(
3135 touchless.touch(
3136 window,
3137 ContactId(1),
3138 TouchPhase::Began,
3139 PhysicalPoint::ORIGIN
3140 ),
3141 Err(ShellError::Unsupported { .. })
3142 ));
3143 assert!(drain(&mut touchless).is_empty(), "and queued nothing");
3144 }
3145
3146 #[test]
3147 fn warping_produces_motion_like_every_platform_that_has_it() {
3148 let (mut shell, window) = shell_with_configured_window();
3149 shell
3150 .warp_pointer(window, PhysicalPoint::new(320.0, 180.0))
3151 .expect("warp");
3152 let events = drain(&mut shell);
3153 let ShellEvent::PointerMotion { abs, raw_delta, .. } = events[0] else {
3154 panic!("wrong variant");
3155 };
3156 assert_eq!(abs, Some(PhysicalPoint::new(320.0, 180.0)));
3157 assert_eq!(raw_delta, None, "a warp is not physical motion");
3158 }
3159
3160 #[test]
3163 fn capabilities_gate_the_operations_that_need_them() {
3164 let mut shell = HeadlessShell::new().with_caps(ShellCaps::empty());
3165 let window = shell.create_window(&WindowDesc::default()).expect("window");
3166
3167 assert!(matches!(
3168 shell.warp_pointer(window, PhysicalPoint::ORIGIN),
3169 Err(ShellError::Unsupported { .. })
3170 ));
3171 assert!(matches!(
3172 shell.set_pointer_mode(window, PointerMode::Locked),
3173 Err(ShellError::Unsupported { .. })
3174 ));
3175 assert!(matches!(
3176 shell.clipboard_request(window, MimeType::TextUtf8),
3177 Err(ShellError::Unsupported { .. })
3178 ));
3179 shell
3181 .set_pointer_mode(window, PointerMode::Free)
3182 .expect("Free is always available");
3183 assert!(matches!(
3185 shell.create_window(&WindowDesc::default()),
3186 Err(ShellError::Unsupported { .. })
3187 ));
3188 }
3189
3190 #[test]
3191 fn stale_handles_fail_cleanly_on_every_accessor() {
3192 let (mut shell, window) = shell_with_configured_window();
3193 shell.destroy_window(window).expect("destroy");
3194 assert_eq!(
3195 drain(&mut shell),
3196 vec![ShellEvent::WindowDestroyed { window }]
3197 );
3198
3199 assert!(matches!(
3200 shell.window_state(window),
3201 Err(ShellError::InvalidWindow { .. })
3202 ));
3203 assert!(matches!(
3204 shell.surface_target(window),
3205 Err(ShellError::InvalidWindow { .. })
3206 ));
3207 assert!(matches!(
3208 shell.resize(window, PhysicalSize::new(1, 1)),
3209 Err(ShellError::InvalidWindow { .. })
3210 ));
3211 assert!(matches!(
3212 shell.set_title(window, "gone"),
3213 Err(ShellError::InvalidWindow { .. })
3214 ));
3215 assert!(matches!(
3216 shell.destroy_window(window),
3217 Err(ShellError::InvalidWindow { .. })
3218 ));
3219 assert!(matches!(
3220 shell.key_press(window, KeyCode::KeyA),
3221 Err(ShellError::InvalidWindow { .. })
3222 ));
3223 }
3224
3225 #[test]
3226 fn destroying_a_window_cancels_its_pending_configure() {
3227 let (mut shell, window) = shell_with_window();
3230 shell.destroy_window(window).expect("destroy");
3231 assert_eq!(names(&drain(&mut shell)), ["WindowDestroyed"]);
3232 assert_eq!(drain(&mut shell), Vec::new());
3233 assert_eq!(drain(&mut shell), Vec::new());
3234 }
3235
3236 #[test]
3237 fn close_requests_are_questions() {
3238 let (mut shell, window) = shell_with_configured_window();
3239 assert!(matches!(
3240 shell.reply_close_request(window, CloseReply::Close),
3241 Err(ShellError::NoPendingCloseRequest { .. })
3242 ));
3243
3244 shell.request_close(window).expect("request");
3245 assert!(shell.window_state(window).expect("state").close_pending);
3246 shell
3247 .reply_close_request(window, CloseReply::Keep)
3248 .expect("keep");
3249 assert!(!shell.window_state(window).expect("state").close_pending);
3250 assert!(shell.window_state(window).is_ok(), "still open");
3251
3252 shell.request_close(window).expect("request again");
3253 shell
3254 .reply_close_request(window, CloseReply::Close)
3255 .expect("close");
3256 assert!(matches!(
3257 shell.window_state(window),
3258 Err(ShellError::InvalidWindow { .. })
3259 ));
3260 }
3261
3262 #[test]
3263 fn monitor_hotplug_reports_a_change_and_keeps_ids_unique() {
3264 let mut shell = HeadlessShell::new();
3265 let extra = shell.plug_monitor(MonitorInfo {
3266 id: MonitorId(0),
3267 name: "HEADLESS-2".to_string(),
3268 bounds: PhysicalRect::new(1920, 0, 1920, 1080),
3269 work_area: PhysicalRect::new(1920, 0, 1920, 1080),
3270 scale_factor: 1.0,
3271 refresh_millihertz: 60_000,
3272 is_primary: false,
3273 });
3274 assert_eq!(shell.monitors().len(), 2);
3275 assert_eq!(drain(&mut shell), vec![ShellEvent::MonitorsChanged]);
3276
3277 shell.unplug_monitor(extra).expect("unplug");
3278 assert_eq!(shell.monitors().len(), 1);
3279 assert_eq!(
3280 drain(&mut shell),
3281 vec![
3282 ShellEvent::MonitorRemoved { monitor: extra },
3283 ShellEvent::MonitorsChanged
3284 ]
3285 );
3286 assert!(matches!(
3287 shell.unplug_monitor(extra),
3288 Err(ShellError::NoSuchMonitor(_))
3289 ));
3290 assert_eq!(
3291 shell.monitor(MonitorId(1)).map(|m| m.name.as_str()),
3292 Some("HEADLESS-1")
3293 );
3294 assert!(shell.monitor(extra).is_none());
3295 }
3296
3297 #[test]
3298 fn unplugging_a_monitor_asks_its_panels_and_only_its_panels_to_close() {
3299 let mut shell = HeadlessShell::new();
3300 let second = shell.plug_monitor(MonitorInfo {
3301 id: MonitorId(0),
3302 name: "HEADLESS-2".to_string(),
3303 bounds: PhysicalRect::new(1920, 0, 1920, 1080),
3304 work_area: PhysicalRect::new(1920, 0, 1920, 1080),
3305 scale_factor: 1.0,
3306 refresh_millihertz: 60_000,
3307 is_primary: false,
3308 });
3309 let first = shell.monitors()[0].id;
3310 let panel_on = |shell: &mut HeadlessShell, monitor| {
3311 shell
3312 .create_window(&WindowDesc {
3313 role: crate::WindowRole::Panel(crate::PanelDesc {
3314 monitor,
3315 ..crate::PanelDesc::default()
3316 }),
3317 ..WindowDesc::default()
3318 })
3319 .expect("panel")
3320 };
3321 let doomed = panel_on(&mut shell, Some(second));
3322 let already_asked = panel_on(&mut shell, Some(second));
3323 let elsewhere = panel_on(&mut shell, Some(first));
3324 let anywhere = panel_on(&mut shell, None);
3325 let toplevel = shell.create_window(&WindowDesc::default()).expect("window");
3326 shell.request_close(already_asked).expect("close");
3327 settle(&mut shell);
3328 drain(&mut shell);
3329
3330 shell.unplug_monitor(second).expect("unplug");
3331 assert_eq!(
3332 drain(&mut shell),
3333 vec![
3334 ShellEvent::MonitorRemoved { monitor: second },
3335 ShellEvent::CloseRequested { window: doomed },
3336 ShellEvent::MonitorsChanged,
3337 ]
3338 );
3339 for window in [elsewhere, anywhere, toplevel] {
3340 assert!(!shell.window_state(window).expect("open").close_pending);
3341 }
3342 }
3343
3344 #[test]
3345 fn drops_require_both_the_capability_and_the_opt_in() {
3346 let mut shell = HeadlessShell::new();
3347 let plain = shell.create_window(&WindowDesc::default()).expect("window");
3348 assert!(matches!(
3349 shell.drop_file(plain, "/tmp/a.gltf", None),
3350 Err(ShellError::Unsupported { .. })
3351 ));
3352
3353 let dropper = shell
3354 .create_window(&WindowDesc {
3355 accept_drops: true,
3356 ..WindowDesc::default()
3357 })
3358 .expect("window");
3359 settle(&mut shell);
3360 shell
3361 .drop_file(dropper, "/tmp/a.gltf", Some(PhysicalPoint::new(4.0, 5.0)))
3362 .expect("drop");
3363 assert_eq!(names(&drain(&mut shell))[0], "DroppedFile");
3364 }
3365
3366 #[test]
3367 fn a_sink_that_queues_more_events_does_not_spin_the_pump() {
3368 let (mut shell, window) = shell_with_configured_window();
3369 shell.inject(ShellEvent::MonitorsChanged);
3370 let mut extra = Vec::new();
3371 shell.pump(&mut |event| extra.push(event));
3372 assert_eq!(extra.len(), 1);
3373 shell.inject(ShellEvent::CloseRequested { window });
3374 assert_eq!(shell.pending_events(), 1);
3375 }
3376
3377 #[test]
3378 fn accessors_that_no_real_backend_can_provide_are_headless_only() {
3379 let (mut shell, window) = shell_with_configured_window();
3380 shell.set_title(window, "renamed").expect("title");
3381 assert_eq!(shell.title(window).expect("title"), "renamed");
3382 shell.set_cursor(window, None).expect("hide");
3383 assert_eq!(shell.cursor(window).expect("cursor"), None);
3384 shell
3385 .set_cursor(window, Some(CursorIcon::Crosshair))
3386 .expect("shape");
3387 assert_eq!(
3388 shell.cursor(window).expect("cursor"),
3389 Some(CursorIcon::Crosshair)
3390 );
3391 shell.set_visible(window, false).expect("hide window");
3392 assert!(!shell.window_state(window).expect("state").visible);
3393 }
3394
3395 #[test]
3396 fn an_input_region_is_recorded_replaced_and_given_back() {
3397 let (mut shell, window) = shell_with_configured_window();
3398 assert_eq!(shell.input_region(window).expect("region"), None);
3399 let panel = PhysicalRect::new(0, 0, 400, 300).rounded(12);
3400 shell
3401 .set_input_region(window, Some(&panel))
3402 .expect("a region");
3403 assert_eq!(
3404 shell.input_region(window).expect("region"),
3405 Some(panel.as_slice())
3406 );
3407 shell.set_input_region(window, Some(&[])).expect("nothing");
3408 assert_eq!(shell.input_region(window).expect("region"), Some(&[][..]));
3409 shell.set_input_region(window, None).expect("whole window");
3410 assert_eq!(shell.input_region(window).expect("region"), None);
3411 shell.destroy_window(window).expect("destroy");
3412 assert!(matches!(
3413 shell.set_input_region(window, None),
3414 Err(ShellError::InvalidWindow { .. })
3415 ));
3416 assert!(shell.input_region(window).is_err());
3417 }
3418
3419 fn frames_for(events: &[ShellEvent], window: WindowId) -> usize {
3420 events
3421 .iter()
3422 .filter(|event| matches!(event, ShellEvent::FrameReady { window: w } if *w == window))
3423 .count()
3424 }
3425
3426 #[test]
3427 fn requests_for_a_frame_share_one_answer_at_the_next_pump() {
3428 let (mut shell, window) = shell_with_configured_window();
3429 assert!(!shell.frame_requested(window).expect("live"));
3430 shell.request_frame(window).expect("a frame");
3431 shell.request_frame(window).expect("the same frame");
3432 assert!(shell.frame_requested(window).expect("live"));
3433 let events = drain(&mut shell);
3434 assert_eq!(frames_for(&events, window), 1, "{:?}", names(&events));
3435 assert!(!shell.frame_requested(window).expect("live"));
3436 assert_eq!(
3437 frames_for(&drain(&mut shell), window),
3438 0,
3439 "nothing asked again"
3440 );
3441 shell.request_frame(window).expect("the next frame");
3442 assert_eq!(frames_for(&drain(&mut shell), window), 1);
3443 }
3444
3445 #[test]
3446 fn a_window_destroyed_with_a_frame_requested_is_not_answered() {
3447 let (mut shell, doomed) = shell_with_configured_window();
3448 let kept = shell
3449 .create_window(&WindowDesc::default())
3450 .expect("a second window");
3451 settle(&mut shell);
3452 shell.request_frame(doomed).expect("a frame");
3453 shell.request_frame(kept).expect("a frame");
3454 shell.destroy_window(doomed).expect("destroy");
3455 let events = drain(&mut shell);
3456 assert_eq!(frames_for(&events, doomed), 0, "{:?}", names(&events));
3457 assert_eq!(frames_for(&events, kept), 1, "the other window still asked");
3458 assert!(matches!(
3459 shell.request_frame(doomed),
3460 Err(ShellError::InvalidWindow { .. })
3461 ));
3462 assert!(shell.frame_requested(doomed).is_err());
3463 }
3464
3465 #[test]
3466 fn a_wait_does_not_sleep_through_a_frame_that_is_due() {
3467 let (mut shell, window) = shell_with_configured_window();
3468 shell.request_frame(window).expect("a frame");
3469 let start = std::time::Instant::now();
3470 shell.wait_events(Some(LONG_WAIT));
3471 assert!(
3472 start.elapsed() < LONG_WAIT / 2,
3473 "slept {:?} with a frame due",
3474 start.elapsed()
3475 );
3476 assert_eq!(frames_for(&drain(&mut shell), window), 1);
3477 }
3478
3479 #[test]
3480 fn an_output_that_is_off_holds_frame_requests_until_it_is_on() {
3481 let (mut shell, window) = shell_with_configured_window();
3482 shell.set_outputs_powered(false);
3483 shell.request_frame(window).expect("a frame");
3484 assert_eq!(frames_for(&drain(&mut shell), window), 0, "held");
3485 assert!(shell.frame_requested(window).expect("live"), "still owed");
3486
3487 let wait = Duration::from_millis(20);
3488 let start = std::time::Instant::now();
3489 shell.wait_events(Some(wait));
3490 assert!(
3491 start.elapsed() >= wait,
3492 "a held request cut the wait short after {:?}",
3493 start.elapsed()
3494 );
3495
3496 shell.set_outputs_powered(true);
3497 assert_eq!(
3498 frames_for(&drain(&mut shell), window),
3499 1,
3500 "answered once on"
3501 );
3502 assert!(!shell.frame_requested(window).expect("live"));
3503 }
3504
3505 #[test]
3506 fn waiting_is_observable_and_focus_transitions_are_events() {
3507 let (mut shell, window) = shell_with_configured_window();
3508 assert_eq!(shell.wait_count(), 0);
3509 shell.wait_events(Some(Duration::from_millis(5)));
3510 shell.wait_events(None);
3511 assert_eq!(shell.wait_count(), 2);
3512
3513 shell.set_focus(window, true).expect("focus");
3514 assert!(shell.window_state(window).expect("state").focused);
3515 shell.set_focus(window, false).expect("unfocus");
3516 assert_eq!(names(&drain(&mut shell)), ["Focus", "Focus"]);
3517 }
3518
3519 const LONG_WAIT: Duration = Duration::from_secs(60);
3524 const RING_AFTER: Duration = Duration::from_millis(50);
3526
3527 fn ring_later(waker: ShellWaker) -> std::thread::JoinHandle<()> {
3529 std::thread::spawn(move || {
3530 std::thread::sleep(RING_AFTER);
3531 waker.wake();
3532 })
3533 }
3534
3535 #[test]
3536 fn a_wake_from_another_thread_ends_a_long_wait() {
3537 let mut shell = HeadlessShell::new();
3538 let ringer = ring_later(shell.waker().expect("headless has a waker"));
3539 let start = std::time::Instant::now();
3540 shell.wait_events(Some(LONG_WAIT));
3541 let slept = start.elapsed();
3542 ringer.join().expect("the ringer");
3543 assert!(slept < LONG_WAIT / 2, "the wait ran {slept:?}");
3544 }
3545
3546 #[test]
3547 fn a_wake_before_the_wait_ends_it_at_once_and_wakes_coalesce() {
3548 let mut shell = HeadlessShell::new();
3549 let waker = shell.waker().expect("headless has a waker");
3550 for _ in 0..3 {
3551 waker.wake();
3552 }
3553 let start = std::time::Instant::now();
3554 shell.wait_events(Some(LONG_WAIT));
3555 assert!(
3556 start.elapsed() < LONG_WAIT / 2,
3557 "a rung bell must end the wait"
3558 );
3559
3560 let timeout = Duration::from_millis(100);
3562 let start = std::time::Instant::now();
3563 shell.wait_events(Some(timeout));
3564 assert!(
3565 start.elapsed() >= timeout / 2,
3566 "the bell was cleared, so this wait must sleep, but it took {:?}",
3567 start.elapsed()
3568 );
3569 }
3570
3571 #[test]
3572 fn with_a_waker_out_an_untimed_wait_sleeps_until_it_rings() {
3573 let mut shell = HeadlessShell::new();
3574 let ringer = ring_later(shell.waker().expect("headless has a waker"));
3575 let start = std::time::Instant::now();
3576 shell.wait_events(None);
3577 let slept = start.elapsed();
3578 ringer.join().expect("the ringer");
3579 assert!(
3580 slept >= RING_AFTER / 2,
3581 "an untimed wait returned after {slept:?}, before anything rang"
3582 );
3583 }
3584
3585 #[test]
3588 fn the_clipboard_round_trips_through_an_event() {
3589 let (mut shell, window) = shell_with_configured_window();
3590 shell
3591 .clipboard_offer(
3592 window,
3593 &[
3594 ClipboardOffer::text("node"),
3595 ClipboardOffer::ron("(kind:\"node\")"),
3596 ],
3597 )
3598 .expect("offer");
3599 assert_eq!(
3600 shell.clipboard_bytes(MimeType::TextUtf8),
3601 Some(&b"node"[..])
3602 );
3603
3604 let request = shell
3605 .clipboard_request(window, MimeType::CrcblRon)
3606 .expect("request");
3607 assert_eq!(
3608 drain(&mut shell)[0],
3609 ShellEvent::ClipboardData {
3610 window,
3611 request,
3612 mime: ReceivedMime::from(MimeType::CrcblRon),
3613 content: ClipboardContent::Bytes(b"(kind:\"node\")".to_vec()),
3614 }
3615 );
3616
3617 let request = shell
3620 .clipboard_request(window, MimeType::UriList)
3621 .expect("request");
3622 assert_eq!(
3623 drain(&mut shell)[0],
3624 ShellEvent::ClipboardData {
3625 window,
3626 request,
3627 mime: ReceivedMime::from(MimeType::UriList),
3628 content: ClipboardContent::Empty,
3629 }
3630 );
3631
3632 shell
3635 .clipboard_offer(window, &[ClipboardOffer::text("")])
3636 .expect("offer");
3637 let request = shell
3638 .clipboard_request(window, MimeType::TextUtf8)
3639 .expect("request");
3640 assert_eq!(
3641 drain(&mut shell)[0],
3642 ShellEvent::ClipboardData {
3643 window,
3644 request,
3645 mime: ReceivedMime::from(MimeType::TextUtf8),
3646 content: ClipboardContent::Bytes(Vec::new()),
3647 }
3648 );
3649 }
3650
3651 #[test]
3652 fn a_read_taken_while_the_clipboard_is_unreadable_is_held_not_answered_empty() {
3653 let (mut shell, window) = shell_with_configured_window();
3659 shell
3660 .clipboard_offer(window, &[ClipboardOffer::text("published")])
3661 .expect("offer");
3662 shell.set_clipboard_readable(false);
3663 assert!(!shell.clipboard_readable(window));
3664
3665 let request = shell
3666 .clipboard_request(window, MimeType::TextUtf8)
3667 .expect("a read is accepted even when it cannot be answered yet");
3668 assert_eq!(shell.held_clipboard_reads(), 1);
3669 assert!(
3670 drain(&mut shell).is_empty(),
3671 "a held read produces no event at all — least of all an empty one"
3672 );
3673
3674 shell.set_clipboard_readable(true);
3676 let events = drain(&mut shell);
3677 assert_eq!(events.len(), 1);
3678 assert_eq!(
3679 events[0],
3680 ShellEvent::ClipboardData {
3681 window,
3682 request,
3683 mime: ReceivedMime::from(MimeType::TextUtf8),
3684 content: ClipboardContent::Bytes(b"published".to_vec()),
3685 }
3686 );
3687 assert_eq!(shell.held_clipboard_reads(), 0);
3688 }
3689
3690 #[test]
3691 fn a_read_that_never_becomes_answerable_ends_as_unavailable() {
3692 let (mut shell, window) = shell_with_configured_window();
3696 shell
3697 .clipboard_offer(window, &[ClipboardOffer::text("unreachable")])
3698 .expect("offer");
3699 shell.set_clipboard_readable(false);
3700 shell.clipboard_deadline(2);
3701
3702 let request = shell
3703 .clipboard_request(window, MimeType::TextUtf8)
3704 .expect("request");
3705 assert!(drain(&mut shell).is_empty(), "pump 1: still held");
3706 assert!(drain(&mut shell).is_empty(), "pump 2: still held");
3707 let events = drain(&mut shell);
3708 assert_eq!(
3709 events[0],
3710 ShellEvent::ClipboardData {
3711 window,
3712 request,
3713 mime: ReceivedMime::from(MimeType::TextUtf8),
3714 content: ClipboardContent::Unavailable,
3717 }
3718 );
3719 assert_eq!(shell.held_clipboard_reads(), 0);
3720 }
3721
3722 #[test]
3723 fn a_peer_that_holds_the_clipboard_and_never_delivers_is_unavailable() {
3724 let (mut shell, window) = shell_with_configured_window();
3728 shell.set_foreign_clipboard(vec![(
3729 ReceivedMime::new("text/plain"),
3730 b"never sent".to_vec(),
3731 )]);
3732 shell.set_clipboard_stalled(true);
3733 shell.clipboard_deadline(1);
3734 assert!(
3735 !shell.clipboard_readable(window),
3736 "a clipboard whose owner will not answer is not readable"
3737 );
3738
3739 let request = shell
3740 .clipboard_request(window, MimeType::TextUtf8)
3741 .expect("request");
3742 assert!(drain(&mut shell).is_empty());
3743 let events = drain(&mut shell);
3744 let ShellEvent::ClipboardData {
3745 request: answered,
3746 content,
3747 ..
3748 } = &events[0]
3749 else {
3750 panic!("wrong variant");
3751 };
3752 assert_eq!(*answered, request);
3753 assert_eq!(*content, ClipboardContent::Unavailable);
3754
3755 shell.set_clipboard_stalled(false);
3757 let request = shell
3758 .clipboard_request(window, MimeType::TextUtf8)
3759 .expect("request");
3760 assert_eq!(
3761 drain(&mut shell)[0],
3762 ShellEvent::ClipboardData {
3763 window,
3764 request,
3765 mime: ReceivedMime::new("text/plain"),
3766 content: ClipboardContent::Bytes(b"never sent".to_vec()),
3767 }
3768 );
3769 }
3770
3771 #[test]
3772 fn claiming_the_clipboard_can_require_a_user_interaction() {
3773 let (mut shell, window) = shell_with_configured_window();
3777 shell.require_user_interaction(true);
3778
3779 let refused = shell.clipboard_offer(window, &[ClipboardOffer::text("stolen")]);
3780 assert!(
3781 matches!(
3782 refused,
3783 Err(ShellError::NeedsUserInteraction {
3784 backend: ShellBackend::Headless,
3785 ..
3786 })
3787 ),
3788 "{refused:?}"
3789 );
3790 assert!(
3791 shell.clipboard_bytes(MimeType::TextUtf8).is_none(),
3792 "a refused claim must not have taken the clipboard anyway"
3793 );
3794
3795 shell.key_press(window, KeyCode::KeyC).expect("key");
3798 shell
3799 .clipboard_offer(window, &[ClipboardOffer::text("copied")])
3800 .expect("with a serial in hand, the claim goes through");
3801 assert_eq!(
3802 shell.clipboard_bytes(MimeType::TextUtf8),
3803 Some(&b"copied"[..])
3804 );
3805
3806 shell.clear_user_interaction();
3808 assert!(matches!(
3809 shell.clipboard_offer(window, &[ClipboardOffer::text("later")]),
3810 Err(ShellError::NeedsUserInteraction { .. })
3811 ));
3812 }
3813
3814 #[test]
3815 fn a_held_read_for_a_destroyed_window_is_dropped_rather_than_answered() {
3816 let (mut shell, window) = shell_with_configured_window();
3819 shell.set_clipboard_readable(false);
3820 let _request = shell
3821 .clipboard_request(window, MimeType::TextUtf8)
3822 .expect("request");
3823 assert_eq!(shell.held_clipboard_reads(), 1);
3824
3825 shell.destroy_window(window).expect("destroy");
3826 let events = drain(&mut shell);
3827 assert_eq!(shell.held_clipboard_reads(), 0);
3828 assert!(
3829 !events
3830 .iter()
3831 .any(|event| matches!(event, ShellEvent::ClipboardData { .. })),
3832 "no clipboard answer may name a destroyed window: {events:?}"
3833 );
3834 }
3835
3836 #[test]
3837 fn a_paste_from_a_foreign_application_keeps_the_foreign_spelling() {
3838 let (mut shell, window) = shell_with_configured_window();
3839 shell.set_foreign_clipboard(vec![
3840 (ReceivedMime::new("text/plain"), b"from gedit".to_vec()),
3843 (
3844 ReceivedMime::new("application/vnd.some-editor.scene+json"),
3845 b"{}".to_vec(),
3846 ),
3847 ]);
3848
3849 let request = shell
3851 .clipboard_request(window, MimeType::TextUtf8)
3852 .expect("request");
3853 let events = drain(&mut shell);
3854 let ShellEvent::ClipboardData {
3855 request: answered,
3856 mime,
3857 content,
3858 ..
3859 } = &events[0]
3860 else {
3861 panic!("wrong variant");
3862 };
3863 assert_eq!(*answered, request);
3864 assert_eq!(content.bytes(), Some(&b"from gedit"[..]));
3865 assert_eq!(mime.as_str(), "text/plain");
3867 assert!(mime.matches(MimeType::TextUtf8));
3868 assert_ne!(*mime, ReceivedMime::from(MimeType::TextUtf8));
3869
3870 assert_eq!(
3873 shell.clipboard_bytes(MimeType::Other("application/vnd.some-editor.scene+json")),
3874 Some(&b"{}"[..])
3875 );
3876 }
3877
3878 #[test]
3881 #[should_panic(expected = "a shell needs at least one monitor")]
3882 fn a_shell_with_no_monitors_is_rejected() {
3883 let _ = HeadlessShell::new().with_monitors(Vec::new());
3884 }
3885
3886 #[test]
3887 #[should_panic(expected = "a text commit is never empty")]
3888 fn an_empty_text_commit_is_rejected() {
3889 let (mut shell, window) = shell_with_configured_window();
3890 let _ = shell.commit_text(window, "");
3891 }
3892}