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 input_region: Option<Vec<PhysicalRect>>,
243 #[cfg(feature = "accesskit")]
246 accessibility: Option<Vec<accesskit::TreeUpdate>>,
247}
248
249impl HeadlessWindow {
250 fn size(&self) -> Option<PhysicalSize> {
252 self.configuration.map(|config| config.size)
253 }
254
255 fn scale_factor(&self) -> f64 {
261 self.configuration
262 .map(|config| config.scale_factor)
263 .or_else(|| self.pending.map(|pending| pending.scale_factor))
264 .unwrap_or(1.0)
265 }
266
267 fn settled_mode(&self) -> DisplayMode {
276 self.configuration
277 .map(|config| config.mode)
278 .or_else(|| self.pending.map(|pending| pending.mode))
279 .unwrap_or(self.requested_mode)
280 }
281
282 fn base_size(&self) -> PhysicalSize {
284 self.size()
285 .or_else(|| self.pending.map(|pending| pending.size))
286 .unwrap_or(PhysicalSize::new(1, 1))
287 }
288
289 fn configure_at(&mut self, size: LogicalSize, countdown: u32) {
293 if let Some(pending) = self.pending.as_mut() {
294 pending.size = size.to_physical(pending.scale_factor);
295 } else if let Some(config) = self.configuration {
296 self.pending = Some(PendingConfigure {
297 size: size.to_physical(config.scale_factor),
298 scale_factor: config.scale_factor,
299 mode: config.mode,
300 countdown,
301 });
302 }
303 }
304}
305
306#[derive(Debug)]
311pub struct HeadlessShell {
312 caps: ShellCaps,
313 monitors: Vec<MonitorInfo>,
314 next_monitor_id: u32,
315 windows: Pool<HeadlessWindow>,
316 queue: VecDeque<ShellEvent>,
317 clock: ManualTime,
318 modifiers: Modifiers,
319 clipboard: Vec<(ReceivedMime, Vec<u8>)>,
320 next_request_id: u32,
321 reads: Vec<PendingRead>,
324 clipboard_readable: bool,
328 clipboard_stalled: bool,
330 clipboard_needs_interaction: bool,
332 saw_interaction: bool,
335 clipboard_deadline: u32,
338 waits: u32,
339 configure_delay: u32,
340 wake: Option<Arc<HeadlessWake>>,
342 frames: FrameRequests,
344}
345
346#[derive(Debug, Default)]
349struct HeadlessWake {
350 rung: Mutex<bool>,
351 bell: Condvar,
352}
353
354impl HeadlessWake {
355 fn wait(&self, timeout: Option<Duration>) {
358 let rung = self.rung.lock().unwrap_or_else(PoisonError::into_inner);
359 let mut rung = match timeout {
360 Some(timeout) => {
361 self.bell
362 .wait_timeout_while(rung, timeout, |rung| !*rung)
363 .unwrap_or_else(PoisonError::into_inner)
364 .0
365 }
366 None => self
367 .bell
368 .wait_while(rung, |rung| !*rung)
369 .unwrap_or_else(PoisonError::into_inner),
370 };
371 *rung = false;
372 }
373}
374
375impl WakeSignal for HeadlessWake {
376 fn wake(&self) {
377 *self.rung.lock().unwrap_or_else(PoisonError::into_inner) = true;
378 self.bell.notify_all();
379 }
380}
381
382impl Default for HeadlessShell {
383 fn default() -> Self {
384 Self::new()
385 }
386}
387
388impl HeadlessShell {
389 #[must_use]
391 pub fn new() -> Self {
392 Self {
393 caps: ShellCaps::DESKTOP,
394 monitors: vec![MonitorInfo {
395 id: MonitorId(1),
396 name: "HEADLESS-1".to_string(),
397 bounds: PhysicalRect::new(
398 0,
399 0,
400 DEFAULT_MONITOR_SIZE.width,
401 DEFAULT_MONITOR_SIZE.height,
402 ),
403 work_area: PhysicalRect::new(
404 0,
405 0,
406 DEFAULT_MONITOR_SIZE.width,
407 DEFAULT_MONITOR_SIZE.height,
408 ),
409 scale_factor: 1.0,
410 refresh_millihertz: DEFAULT_REFRESH_MILLIHERTZ,
411 is_primary: true,
412 }],
413 next_monitor_id: 2,
414 windows: Pool::new(),
415 queue: VecDeque::new(),
416 clock: ManualTime::new(),
417 modifiers: Modifiers::empty(),
418 clipboard: Vec::new(),
419 next_request_id: 1,
420 reads: Vec::new(),
421 clipboard_readable: true,
422 clipboard_stalled: false,
423 clipboard_needs_interaction: false,
424 saw_interaction: false,
425 clipboard_deadline: DEFAULT_CLIPBOARD_DEADLINE,
426 waits: 0,
427 configure_delay: DEFAULT_CONFIGURE_DELAY,
428 wake: None,
429 frames: FrameRequests::default(),
430 }
431 }
432
433 #[must_use]
438 pub fn with_caps(mut self, caps: ShellCaps) -> Self {
439 self.caps = caps;
440 self
441 }
442
443 #[must_use]
458 pub fn with_monitors(mut self, monitors: Vec<MonitorInfo>) -> Self {
459 assert!(!monitors.is_empty(), "a shell needs at least one monitor");
460 for (index, monitor) in monitors.iter().enumerate() {
461 assert!(
462 monitor.id.0 != u32::MAX,
463 "monitor id {} leaves no id for the next hotplug",
464 monitor.id.0
465 );
466 assert!(
467 !monitors[..index].iter().any(|seen| seen.id == monitor.id),
468 "monitor id {} appears twice",
469 monitor.id.0
470 );
471 }
472 self.next_monitor_id = monitors
473 .iter()
474 .map(|monitor| monitor.id.0 + 1)
475 .max()
476 .unwrap_or(1);
477 self.monitors = monitors;
478 self
479 }
480
481 #[must_use]
486 pub fn with_scale_factor(mut self, scale_factor: f64) -> Self {
487 assert!(scale_factor > 0.0, "scale factor must be positive");
488 self.monitors[0].scale_factor = scale_factor;
489 self
490 }
491
492 #[must_use]
497 pub const fn with_configure_delay(mut self, pumps: u32) -> Self {
498 self.configure_delay = pumps;
499 self
500 }
501
502 pub const fn set_configure_delay(&mut self, pumps: u32) {
512 self.configure_delay = pumps;
513 }
514
515 pub fn set_caps(&mut self, caps: ShellCaps) {
521 self.caps = caps;
522 }
523
524 #[must_use]
526 pub fn now(&self) -> EventTime {
527 EventTime::from_duration(self.clock.elapsed())
528 }
529
530 pub fn advance(&mut self, delta: Duration) {
536 self.clock.advance(delta);
537 }
538
539 pub fn set_time(&mut self, now: Duration) {
547 self.clock.set(now);
548 }
549
550 pub fn set_modifiers(&mut self, modifiers: Modifiers) {
555 self.modifiers = modifiers;
556 }
557
558 #[must_use]
560 pub fn pending_events(&self) -> usize {
561 self.queue.len()
562 }
563
564 #[must_use]
570 pub const fn wait_count(&self) -> u32 {
571 self.waits
572 }
573
574 pub fn inject(&mut self, event: ShellEvent) {
582 self.queue.push_back(event);
583 }
584
585 pub fn resize(&mut self, window: WindowId, size: PhysicalSize) -> Result<(), ShellError> {
601 let state = self.window_mut(window)?;
602 let scale_factor = state.scale_factor();
603 let mode = state.settled_mode();
604 let constrained = state.requested_constraints.apply(size, scale_factor);
605 state.pending = None;
606 state.configuration = Some(WindowConfiguration {
607 size: constrained,
608 scale_factor,
609 mode,
610 });
611 self.queue.push_back(ShellEvent::Resized {
612 window,
613 size: constrained,
614 scale_factor,
615 });
616 Ok(())
617 }
618
619 pub fn change_scale_factor(
641 &mut self,
642 window: WindowId,
643 scale_factor: f64,
644 ) -> Result<(), ShellError> {
645 assert!(scale_factor > 0.0, "scale factor must be positive");
646 let state = self.window_mut(window)?;
647 let logical = state.base_size().to_logical(state.scale_factor());
648 let mode = state.settled_mode();
649 let size = state
650 .requested_constraints
651 .apply(logical.to_physical(scale_factor), scale_factor);
652 state.pending = None;
653 state.configuration = Some(WindowConfiguration {
654 size,
655 scale_factor,
656 mode,
657 });
658 self.queue.push_back(ShellEvent::ScaleFactorChanged {
659 window,
660 scale_factor,
661 size,
662 });
663 self.queue.push_back(ShellEvent::Resized {
664 window,
665 size,
666 scale_factor,
667 });
668 Ok(())
669 }
670
671 pub fn set_focus(&mut self, window: WindowId, focused: bool) -> Result<(), ShellError> {
677 self.window_mut(window)?.focused = focused;
678 self.queue.push_back(ShellEvent::Focus { window, focused });
679 Ok(())
680 }
681
682 pub fn request_close(&mut self, window: WindowId) -> Result<(), ShellError> {
691 self.window_mut(window)?.close_pending = true;
692 self.queue.push_back(ShellEvent::CloseRequested { window });
693 Ok(())
694 }
695
696 pub fn key(
709 &mut self,
710 window: WindowId,
711 key: KeyCode,
712 state: ButtonState,
713 ) -> Result<(), ShellError> {
714 self.key_with(window, key, state, false)
715 }
716
717 pub fn key_press(&mut self, window: WindowId, key: KeyCode) -> Result<(), ShellError> {
723 self.key(window, key, ButtonState::Pressed)
724 }
725
726 pub fn key_release(&mut self, window: WindowId, key: KeyCode) -> Result<(), ShellError> {
732 self.key(window, key, ButtonState::Released)
733 }
734
735 pub fn key_repeat(&mut self, window: WindowId, key: KeyCode) -> Result<(), ShellError> {
742 self.key_with(window, key, ButtonState::Pressed, true)
743 }
744
745 fn key_with(
746 &mut self,
747 window: WindowId,
748 key: KeyCode,
749 state: ButtonState,
750 repeat: bool,
751 ) -> Result<(), ShellError> {
752 let popup_input = self.window(window)?.popup_input;
753 self.saw_interaction = true;
754 let event = ShellEvent::Key {
755 window,
756 device: VIRTUAL_DEVICE,
757 time: self.now(),
758 scancode: synthetic_scancode(key),
759 key_code: Some(key),
760 keysym: synthetic_keysym(key),
761 state,
762 repeat,
763 modifiers: self.modifiers,
764 };
765 let dismissed = crate::role::escape_dismisses(popup_input, &event);
766 self.queue.push_back(event);
767 if dismissed && !self.window(window)?.close_pending {
769 self.request_close(window)?;
770 }
771 Ok(())
772 }
773
774 pub fn move_pointer(
785 &mut self,
786 window: WindowId,
787 position: PhysicalPoint,
788 raw_delta: (f64, f64),
789 ) -> Result<(), ShellError> {
790 let locked = self.window(window)?.pointer_mode == PointerMode::Locked;
791 let raw = if self.caps.contains(ShellCaps::RAW_POINTER_MOTION) {
792 Some(raw_delta)
793 } else {
794 None
795 };
796 self.saw_interaction = true;
797 let event = ShellEvent::PointerMotion {
798 window,
799 device: VIRTUAL_DEVICE,
800 time: self.now(),
801 abs: if locked { None } else { Some(position) },
802 raw_delta: raw,
803 };
804 self.queue.push_back(event);
805 Ok(())
806 }
807
808 pub fn set_pointer_focus(
814 &mut self,
815 window: WindowId,
816 entered: bool,
817 position: Option<PhysicalPoint>,
818 ) -> Result<(), ShellError> {
819 self.check_window(window)?;
820 let event = ShellEvent::PointerFocus {
821 window,
822 device: VIRTUAL_DEVICE,
823 time: self.now(),
824 entered,
825 position: if entered { position } else { None },
826 };
827 self.queue.push_back(event);
828 Ok(())
829 }
830
831 pub fn button(
837 &mut self,
838 window: WindowId,
839 button: PointerButton,
840 state: ButtonState,
841 position: Option<PhysicalPoint>,
842 ) -> Result<(), ShellError> {
843 self.check_window(window)?;
844 self.saw_interaction = true;
845 let event = ShellEvent::Button {
846 window,
847 device: VIRTUAL_DEVICE,
848 time: self.now(),
849 button,
850 state,
851 position,
852 modifiers: self.modifiers,
853 };
854 self.queue.push_back(event);
855 Ok(())
856 }
857
858 pub fn scroll(
864 &mut self,
865 window: WindowId,
866 delta: ScrollDelta,
867 position: Option<PhysicalPoint>,
868 ) -> Result<(), ShellError> {
869 self.check_window(window)?;
870 self.saw_interaction = true;
871 let event = ShellEvent::Wheel {
872 window,
873 device: VIRTUAL_DEVICE,
874 time: self.now(),
875 delta,
876 position,
877 modifiers: self.modifiers,
878 };
879 self.queue.push_back(event);
880 Ok(())
881 }
882
883 pub fn touch(
911 &mut self,
912 window: WindowId,
913 contact: ContactId,
914 phase: TouchPhase,
915 position: PhysicalPoint,
916 ) -> Result<(), ShellError> {
917 self.check_window(window)?;
918 if !self.caps.contains(ShellCaps::TOUCH) {
919 return Err(self.unsupported("touch"));
920 }
921 self.saw_interaction = true;
922 let event = ShellEvent::Touch {
923 window,
924 device: VIRTUAL_DEVICE,
925 time: self.now(),
926 contact,
927 phase,
928 position,
929 };
930 self.queue.push_back(event);
931 Ok(())
932 }
933
934 pub fn commit_text(&mut self, window: WindowId, text: &str) -> Result<(), ShellError> {
945 assert!(!text.is_empty(), "a text commit is never empty");
946 self.check_window(window)?;
947 let event = ShellEvent::TextCommit {
948 window,
949 time: self.now(),
950 text: text.to_string(),
951 };
952 self.queue.push_back(event);
953 Ok(())
954 }
955
956 pub fn drop_file(
964 &mut self,
965 window: WindowId,
966 path: impl Into<PathBuf>,
967 position: Option<PhysicalPoint>,
968 ) -> Result<(), ShellError> {
969 if !self.caps.contains(ShellCaps::DRAG_DROP) {
970 return Err(self.unsupported("drag and drop"));
971 }
972 if !self.window(window)?.accept_drops {
973 return Err(ShellError::Unsupported {
974 backend: ShellBackend::Headless,
975 what: "drops on a window created without accept_drops",
976 });
977 }
978 let event = ShellEvent::DroppedFile {
979 window,
980 time: self.now(),
981 path: path.into(),
982 position,
983 };
984 self.queue.push_back(event);
985 Ok(())
986 }
987
988 pub fn plug_monitor(&mut self, mut info: MonitorInfo) -> MonitorId {
995 let id = MonitorId(self.next_monitor_id);
996 self.next_monitor_id += 1;
997 info.id = id;
998 self.monitors.push(info);
999 self.queue.push_back(ShellEvent::MonitorsChanged);
1000 id
1001 }
1002
1003 pub fn unplug_monitor(&mut self, id: MonitorId) -> Result<(), ShellError> {
1017 let index = self
1018 .monitors
1019 .iter()
1020 .position(|monitor| monitor.id == id)
1021 .ok_or(ShellError::NoSuchMonitor(id.0))?;
1022 assert!(
1023 self.monitors.len() > 1,
1024 "a shell needs at least one monitor"
1025 );
1026 let previous = self.monitors.clone();
1027 self.monitors.remove(index);
1028 crate::monitor::queue_removed(&previous, &self.monitors, &mut self.queue);
1029 let panels: Vec<WindowId> = self
1030 .windows
1031 .iter()
1032 .filter(|(_, state)| {
1033 state.panel.as_ref().and_then(|panel| panel.desc().monitor) == Some(id)
1034 && !state.close_pending
1035 })
1036 .map(|(window, _)| window.cast())
1037 .collect();
1038 for panel in panels {
1039 self.request_close(panel)?;
1040 }
1041 self.queue.push_back(ShellEvent::MonitorsChanged);
1042 Ok(())
1043 }
1044
1045 pub fn title(&self, window: WindowId) -> Result<&str, ShellError> {
1056 Ok(self.window(window)?.title.as_str())
1057 }
1058
1059 pub fn cursor(&self, window: WindowId) -> Result<Option<CursorIcon>, ShellError> {
1067 Ok(self.window(window)?.cursor)
1068 }
1069
1070 pub fn text_input(&self, window: WindowId) -> Result<Option<TextPurpose>, ShellError> {
1079 Ok(self.window(window)?.text_input)
1080 }
1081
1082 pub fn panel(&self, window: WindowId) -> Result<Option<crate::PanelDesc<'_>>, ShellError> {
1091 Ok(self
1092 .window(window)?
1093 .panel
1094 .as_ref()
1095 .map(crate::role::KeptPanel::desc))
1096 }
1097
1098 pub fn input_region(&self, window: WindowId) -> Result<Option<&[PhysicalRect]>, ShellError> {
1109 Ok(self.window(window)?.input_region.as_deref())
1110 }
1111
1112 pub fn frame_requested(&self, window: WindowId) -> Result<bool, ShellError> {
1122 self.window(window)?;
1123 Ok(self.frames.contains(window))
1124 }
1125
1126 #[cfg(feature = "accesskit")]
1135 pub fn activate_accessibility(&mut self, window: WindowId) -> Result<(), ShellError> {
1136 let state = &mut self.window_mut(window)?.accessibility;
1137 if state.is_none() {
1138 *state = Some(Vec::new());
1139 self.queue
1140 .push_back(ShellEvent::AccessibilityActivated { window });
1141 }
1142 Ok(())
1143 }
1144
1145 #[cfg(feature = "accesskit")]
1155 pub fn deactivate_accessibility(&mut self, window: WindowId) -> Result<(), ShellError> {
1156 if self.window_mut(window)?.accessibility.take().is_some() {
1157 self.queue
1158 .push_back(ShellEvent::AccessibilityDeactivated { window });
1159 }
1160 Ok(())
1161 }
1162
1163 #[cfg(feature = "accesskit")]
1177 pub fn request_accessibility_action(
1178 &mut self,
1179 window: WindowId,
1180 request: accesskit::ActionRequest,
1181 ) -> Result<(), ShellError> {
1182 assert!(
1183 self.window(window)?.accessibility.is_some(),
1184 "no assistive technology is reading the window"
1185 );
1186 self.queue
1187 .push_back(ShellEvent::AccessibilityAction { window, request });
1188 Ok(())
1189 }
1190
1191 #[cfg(feature = "accesskit")]
1200 pub fn accessibility_updates(
1201 &self,
1202 window: WindowId,
1203 ) -> Result<Option<&[accesskit::TreeUpdate]>, ShellError> {
1204 Ok(self.window(window)?.accessibility.as_deref())
1205 }
1206
1207 #[must_use]
1211 pub fn clipboard_bytes(&self, mime: MimeType) -> Option<&[u8]> {
1212 self.clipboard
1213 .iter()
1214 .find(|(offered, _)| offered.matches(mime))
1215 .map(|(_, bytes)| bytes.as_slice())
1216 }
1217
1218 pub fn set_clipboard_readable(&mut self, readable: bool) {
1235 self.clipboard_readable = readable;
1236 }
1237
1238 pub fn set_clipboard_stalled(&mut self, stalled: bool) {
1247 self.clipboard_stalled = stalled;
1248 }
1249
1250 pub fn require_user_interaction(&mut self, required: bool) {
1277 self.clipboard_needs_interaction = required;
1278 }
1279
1280 pub fn clear_user_interaction(&mut self) {
1285 self.saw_interaction = false;
1286 }
1287
1288 pub fn clipboard_deadline(&mut self, pumps: u32) {
1294 self.clipboard_deadline = pumps;
1295 }
1296
1297 #[must_use]
1302 pub fn held_clipboard_reads(&self) -> usize {
1303 self.reads.len()
1304 }
1305
1306 pub fn set_foreign_clipboard(&mut self, offers: Vec<(ReceivedMime, Vec<u8>)>) {
1314 self.clipboard = offers;
1315 }
1316
1317 fn answerable(&self) -> bool {
1323 self.clipboard_readable && !self.clipboard_stalled
1324 }
1325
1326 fn answer(&self, window: WindowId, request: ClipboardRequestId, mime: MimeType) -> ShellEvent {
1328 let found = self
1333 .clipboard
1334 .iter()
1335 .find(|(offered, _)| offered.matches(mime));
1336 let (mime, content) = match found {
1337 Some((offered, bytes)) => (offered.clone(), ClipboardContent::Bytes(bytes.clone())),
1338 None => (ReceivedMime::from(mime), ClipboardContent::Empty),
1341 };
1342 ShellEvent::ClipboardData {
1343 window,
1344 request,
1345 mime,
1346 content,
1347 }
1348 }
1349
1350 fn resolve_reads(&mut self) {
1357 if self.reads.is_empty() {
1358 return;
1359 }
1360 let answerable = self.answerable();
1361 let mut held = Vec::with_capacity(self.reads.len());
1362 for mut read in core::mem::take(&mut self.reads) {
1363 if self.window(read.window).is_err() {
1367 continue;
1368 }
1369 if answerable {
1370 let answer = self.answer(read.window, read.request, read.mime);
1371 self.queue.push_back(answer);
1372 continue;
1373 }
1374 if read.countdown > 0 {
1375 read.countdown -= 1;
1376 held.push(read);
1377 continue;
1378 }
1379 self.queue.push_back(ShellEvent::ClipboardData {
1382 window: read.window,
1383 request: read.request,
1384 mime: ReceivedMime::from(read.mime),
1385 content: ClipboardContent::Unavailable,
1386 });
1387 }
1388 self.reads = held;
1389 }
1390
1391 fn deliver_due_configures(&mut self) {
1398 let mut due: Vec<(WindowId, PendingConfigure, Option<f64>)> = Vec::new();
1402 for (handle, state) in self.windows.iter_mut() {
1403 let Some(pending) = state.pending.as_mut() else {
1404 continue;
1405 };
1406 if pending.countdown > 0 {
1407 pending.countdown -= 1;
1408 continue;
1409 }
1410 let pending = state.pending.take().expect("checked just above");
1411 let previous_scale = state.configuration.map(|config| config.scale_factor);
1412 state.configuration = Some(WindowConfiguration {
1413 size: pending.size,
1414 scale_factor: pending.scale_factor,
1415 mode: pending.mode,
1416 });
1417 due.push((handle.cast(), pending, previous_scale));
1418 }
1419
1420 for (window, pending, previous_scale) in due {
1421 if previous_scale.is_some_and(|previous| previous != pending.scale_factor) {
1425 self.queue.push_back(ShellEvent::ScaleFactorChanged {
1426 window,
1427 scale_factor: pending.scale_factor,
1428 size: pending.size,
1429 });
1430 }
1431 self.queue.push_back(ShellEvent::Resized {
1432 window,
1433 size: pending.size,
1434 scale_factor: pending.scale_factor,
1435 });
1436 }
1437 }
1438
1439 fn window(&self, window: WindowId) -> Result<&HeadlessWindow, ShellError> {
1440 self.windows
1441 .get(window.cast())
1442 .ok_or_else(|| ShellError::invalid_window(window))
1443 }
1444
1445 fn window_mut(&mut self, window: WindowId) -> Result<&mut HeadlessWindow, ShellError> {
1446 self.windows
1447 .get_mut(window.cast())
1448 .ok_or_else(|| ShellError::invalid_window(window))
1449 }
1450
1451 fn check_window(&self, window: WindowId) -> Result<(), ShellError> {
1452 self.window(window).map(|_| ())
1453 }
1454
1455 fn unsupported(&self, what: &'static str) -> ShellError {
1456 ShellError::Unsupported {
1457 backend: ShellBackend::Headless,
1458 what,
1459 }
1460 }
1461}
1462
1463fn synthetic_scancode(key: KeyCode) -> Scancode {
1467 let index = KeyCode::ALL
1468 .iter()
1469 .position(|candidate| *candidate == key)
1470 .unwrap_or(0);
1471 Scancode(index as u32 + 8)
1474}
1475
1476fn synthetic_keysym(key: KeyCode) -> Keysym {
1478 let name = key.as_str();
1479 if let Some(letter) = name.strip_prefix("Key") {
1480 return Keysym::from_char(letter.to_ascii_lowercase().chars().next().unwrap_or('?'));
1482 }
1483 if let Some(digit) = name.strip_prefix("Digit") {
1484 return Keysym::from_char(digit.chars().next().unwrap_or('?'));
1485 }
1486 match key {
1487 KeyCode::Space => Keysym::from_char(' '),
1488 KeyCode::Minus => Keysym::from_char('-'),
1489 KeyCode::Equal => Keysym::from_char('='),
1490 KeyCode::Comma => Keysym::from_char(','),
1491 KeyCode::Period => Keysym::from_char('.'),
1492 KeyCode::Slash => Keysym::from_char('/'),
1493 KeyCode::Semicolon => Keysym::from_char(';'),
1494 KeyCode::Quote => Keysym::from_char('\''),
1495 KeyCode::Backslash => Keysym::from_char('\\'),
1496 KeyCode::BracketLeft => Keysym::from_char('['),
1497 KeyCode::BracketRight => Keysym::from_char(']'),
1498 KeyCode::Backquote => Keysym::from_char('`'),
1499 _ => crate::keysym::named_keysym(key).unwrap_or(Keysym::NONE),
1503 }
1504}
1505
1506impl Shell for HeadlessShell {
1507 fn backend(&self) -> ShellBackend {
1508 ShellBackend::Headless
1509 }
1510
1511 fn caps(&self) -> ShellCaps {
1512 self.caps
1513 }
1514
1515 fn create_window(&mut self, desc: &WindowDesc<'_>) -> Result<WindowId, ShellError> {
1516 crate::role::check(desc, self.caps(), self.backend(), |monitor| {
1517 self.monitor(monitor).is_some()
1518 })?;
1519 if !self.windows.is_empty() && !self.caps.contains(ShellCaps::MULTI_WINDOW) {
1520 return Err(self.unsupported("a second window"));
1521 }
1522 if let DisplayMode::Borderless {
1523 monitor: Some(monitor),
1524 } = desc.mode
1525 && self.monitor(monitor).is_none()
1526 {
1527 return Err(ShellError::NoSuchMonitor(monitor.0));
1528 }
1529
1530 let (size, scale_factor) = match desc.mode {
1542 DisplayMode::Windowed => {
1543 let scale_factor = self.monitors[0].scale_factor;
1544 (
1545 desc.constraints
1546 .apply(desc.size.to_physical(scale_factor), scale_factor),
1547 scale_factor,
1548 )
1549 }
1550 DisplayMode::Borderless { monitor } => {
1551 let monitor = monitor
1552 .and_then(|id| self.monitor(id))
1553 .unwrap_or(&self.monitors[0]);
1554 (monitor.size(), monitor.scale_factor)
1555 }
1556 };
1557
1558 let (parent, popup_input, panel) = match desc.role {
1559 crate::WindowRole::Popup(popup) => {
1560 self.window(popup.parent)?;
1561 (Some(popup.parent), Some(popup.input), None)
1562 }
1563 crate::WindowRole::Panel(panel) => {
1564 (None, None, Some(crate::role::KeptPanel::new(&panel)))
1565 }
1566 crate::WindowRole::Toplevel => (None, None, None),
1567 };
1568 let countdown = self.configure_delay;
1569 let window: WindowId = self
1570 .windows
1571 .insert(HeadlessWindow {
1572 title: desc.title.to_string(),
1573 configuration: None,
1576 pending: Some(PendingConfigure {
1577 size,
1578 scale_factor,
1579 mode: desc.mode,
1580 countdown,
1581 }),
1582 requested_mode: desc.mode,
1583 requested_constraints: desc.constraints,
1584 focused: false,
1585 visible: desc.visible,
1586 pointer_mode: PointerMode::Free,
1587 cursor: Some(CursorIcon::Default),
1588 close_pending: false,
1589 accept_drops: desc.accept_drops,
1590 parent,
1591 popup_input,
1592 panel,
1593 text_input: None,
1594 input_region: None,
1595 #[cfg(feature = "accesskit")]
1596 accessibility: None,
1597 })
1598 .cast();
1599 Ok(window)
1600 }
1601
1602 fn destroy_window(&mut self, window: WindowId) -> Result<(), ShellError> {
1606 self.window(window)?;
1607 let popups: Vec<WindowId> = self
1608 .windows
1609 .iter()
1610 .filter(|(_, state)| state.parent == Some(window))
1611 .map(|(id, _)| id.cast())
1612 .collect();
1613 for popup in popups {
1614 self.destroy_window(popup)?;
1615 }
1616 self.windows
1617 .remove(window.cast())
1618 .ok_or_else(|| ShellError::invalid_window(window))?;
1619 self.queue.push_back(ShellEvent::WindowDestroyed { window });
1620 Ok(())
1621 }
1622
1623 fn window_state(&self, window: WindowId) -> Result<WindowState, ShellError> {
1624 let state = self.window(window)?;
1625 Ok(WindowState {
1626 configuration: state.configuration,
1627 requested_mode: state.requested_mode,
1628 requested_constraints: state.requested_constraints,
1629 focused: state.focused,
1630 visible: state.visible,
1631 pointer_mode: state.pointer_mode,
1632 close_pending: state.close_pending,
1633 })
1634 }
1635
1636 fn set_title(&mut self, window: WindowId, title: &str) -> Result<(), ShellError> {
1637 self.window_mut(window)?.title = title.to_string();
1638 Ok(())
1639 }
1640
1641 fn set_visible(&mut self, window: WindowId, visible: bool) -> Result<(), ShellError> {
1642 self.window_mut(window)?.visible = visible;
1643 Ok(())
1644 }
1645
1646 fn set_mode(&mut self, window: WindowId, mode: DisplayMode) -> Result<(), ShellError> {
1647 let target = match mode {
1648 DisplayMode::Windowed => None,
1649 DisplayMode::Borderless { monitor } => {
1650 let monitor = match monitor {
1651 Some(id) => self
1652 .monitor(id)
1653 .ok_or(ShellError::NoSuchMonitor(id.0))?
1654 .clone(),
1655 None => self.monitors[0].clone(),
1656 };
1657 Some(monitor)
1658 }
1659 };
1660 let countdown = self.configure_delay;
1661
1662 let state = self.window_mut(window)?;
1663 state.requested_mode = mode;
1669 let (size, scale_factor) = match &target {
1670 Some(monitor) => (monitor.size(), monitor.scale_factor),
1671 None => (state.base_size(), state.scale_factor()),
1675 };
1676 state.pending = Some(PendingConfigure {
1677 size,
1678 scale_factor,
1679 mode,
1680 countdown,
1681 });
1682 Ok(())
1683 }
1684
1685 fn set_constraints(
1686 &mut self,
1687 window: WindowId,
1688 constraints: SizeConstraints,
1689 ) -> Result<(), ShellError> {
1690 let countdown = self.configure_delay;
1691 let state = self.window_mut(window)?;
1692 state.requested_constraints = constraints;
1693
1694 if let Some(pending) = state.pending.as_mut() {
1701 pending.size = constraints.apply(pending.size, pending.scale_factor);
1702 return Ok(());
1703 }
1704
1705 if let Some(config) = state.configuration {
1709 let constrained = constraints.apply(config.size, config.scale_factor);
1710 if constrained != config.size {
1711 state.pending = Some(PendingConfigure {
1712 size: constrained,
1713 scale_factor: config.scale_factor,
1714 mode: config.mode,
1715 countdown,
1716 });
1717 }
1718 }
1719 Ok(())
1720 }
1721
1722 fn reposition_popup(
1726 &mut self,
1727 popup: WindowId,
1728 anchor: LogicalRect,
1729 size: LogicalSize,
1730 ) -> Result<(), ShellError> {
1731 let _ = anchor;
1732 let countdown = self.configure_delay;
1733 let state = self.window_mut(popup)?;
1734 if state.parent.is_none() {
1735 return Err(crate::role::not_a_popup(popup));
1736 }
1737 state.configure_at(size, countdown);
1738 Ok(())
1739 }
1740
1741 fn set_panel(
1745 &mut self,
1746 window: WindowId,
1747 size: LogicalSize,
1748 panel: &crate::PanelDesc<'_>,
1749 ) -> Result<(), ShellError> {
1750 let was = self.window(window)?.panel.as_ref().map(|kept| kept.desc());
1751 crate::role::check_set_panel(window, was, panel, self.caps, self.backend(), |monitor| {
1752 self.monitor(monitor).is_some()
1753 })?;
1754 let countdown = self.configure_delay;
1755 let state = self.window_mut(window)?;
1756 state.panel = Some(crate::role::KeptPanel::new(panel));
1757 state.configure_at(size, countdown);
1758 Ok(())
1759 }
1760
1761 fn monitors(&self) -> &[MonitorInfo] {
1762 &self.monitors
1763 }
1764
1765 fn pump(&mut self, sink: &mut dyn FnMut(ShellEvent)) {
1766 self.keep_alive();
1767 let windows = &self.windows;
1768 self.queue.extend(
1769 self.frames
1770 .take_due(|window| windows.contains(window.cast())),
1771 );
1772 for _ in 0..self.queue.len() {
1777 let Some(event) = self.queue.pop_front() else {
1778 break;
1779 };
1780 sink(event);
1781 }
1782 }
1783
1784 fn keep_alive(&mut self) {
1792 self.deliver_due_configures();
1793 self.resolve_reads();
1794 }
1795
1796 fn wait_events(&mut self, timeout: Option<Duration>) {
1816 self.waits += 1;
1817 if !self.caps.contains(ShellCaps::EVENT_WAIT)
1818 || !self.queue.is_empty()
1819 || self.frames.is_pending()
1820 {
1821 return;
1822 }
1823 match (&self.wake, timeout) {
1824 (Some(wake), timeout) => wake.wait(timeout),
1825 (None, Some(timeout)) => std::thread::sleep(timeout),
1826 (None, None) => {}
1827 }
1828 }
1829
1830 fn waker(&mut self) -> Option<ShellWaker> {
1833 let wake = self.wake.get_or_insert_with(Arc::default);
1834 Some(ShellWaker::new(Arc::clone(wake) as Arc<dyn WakeSignal>))
1835 }
1836
1837 fn align_event_clock(&mut self, elapsed: Duration) {
1844 self.set_time(elapsed);
1845 }
1846
1847 fn surface_target(&self, window: WindowId) -> Result<SurfaceTarget, ShellError> {
1848 self.window(window)?;
1854 Ok(SurfaceTarget::Offscreen)
1855 }
1856
1857 fn set_pointer_mode(&mut self, window: WindowId, mode: PointerMode) -> Result<(), ShellError> {
1858 if !self.caps.contains(mode.required_cap()) {
1859 return Err(self.unsupported(mode.as_str()));
1860 }
1861 self.window_mut(window)?.pointer_mode = mode;
1862 Ok(())
1863 }
1864
1865 fn set_text_input(
1869 &mut self,
1870 window: WindowId,
1871 field: Option<TextPurpose>,
1872 ) -> Result<(), ShellError> {
1873 self.window_mut(window)?.text_input = field;
1874 Ok(())
1875 }
1876
1877 fn set_input_region(
1882 &mut self,
1883 window: WindowId,
1884 region: Option<&[PhysicalRect]>,
1885 ) -> Result<(), ShellError> {
1886 self.window_mut(window)?.input_region = region.map(<[PhysicalRect]>::to_vec);
1887 Ok(())
1888 }
1889
1890 fn request_frame(&mut self, window: WindowId) -> Result<(), ShellError> {
1895 self.window(window)?;
1896 self.frames.request(window);
1897 Ok(())
1898 }
1899
1900 #[cfg(feature = "accesskit")]
1901 fn update_accessibility(
1902 &mut self,
1903 window: WindowId,
1904 update: accesskit::TreeUpdate,
1905 ) -> Result<(), ShellError> {
1906 if let Some(updates) = &mut self.window_mut(window)?.accessibility {
1907 updates.push(update);
1908 }
1909 Ok(())
1910 }
1911
1912 fn set_cursor(
1913 &mut self,
1914 window: WindowId,
1915 cursor: Option<CursorIcon>,
1916 ) -> Result<(), ShellError> {
1917 self.window_mut(window)?.cursor = cursor;
1918 Ok(())
1919 }
1920
1921 fn warp_pointer(
1922 &mut self,
1923 window: WindowId,
1924 position: PhysicalPoint,
1925 ) -> Result<(), ShellError> {
1926 if !self.caps.contains(ShellCaps::POINTER_WARP) {
1927 return Err(self.unsupported("pointer warp"));
1928 }
1929 self.check_window(window)?;
1930 let event = ShellEvent::PointerMotion {
1933 window,
1934 device: VIRTUAL_DEVICE,
1935 time: self.now(),
1936 abs: Some(position),
1937 raw_delta: None,
1938 };
1939 self.queue.push_back(event);
1940 Ok(())
1941 }
1942
1943 fn reply_close_request(
1944 &mut self,
1945 window: WindowId,
1946 reply: CloseReply,
1947 ) -> Result<(), ShellError> {
1948 let state = self.window_mut(window)?;
1949 if !state.close_pending {
1950 return Err(ShellError::NoPendingCloseRequest { window });
1951 }
1952 state.close_pending = false;
1953 match reply {
1954 CloseReply::Close => self.destroy_window(window),
1955 CloseReply::Keep => Ok(()),
1956 }
1957 }
1958
1959 fn clipboard_offer(
1970 &mut self,
1971 window: WindowId,
1972 offers: &[ClipboardOffer<'_>],
1973 ) -> Result<(), ShellError> {
1974 if !self.caps.contains(ShellCaps::CLIPBOARD) {
1975 return Err(self.unsupported("clipboard"));
1976 }
1977 self.check_window(window)?;
1978 if self.clipboard_needs_interaction && !self.saw_interaction {
1979 return Err(ShellError::NeedsUserInteraction {
1980 backend: ShellBackend::Headless,
1981 what: "claiming the clipboard",
1982 });
1983 }
1984 self.clipboard = offers
1985 .iter()
1986 .map(|offer| (ReceivedMime::from(offer.mime), offer.bytes.to_vec()))
1987 .collect();
1988 Ok(())
1989 }
1990
1991 fn clipboard_request(
2005 &mut self,
2006 window: WindowId,
2007 mime: MimeType,
2008 ) -> Result<ClipboardRequestId, ShellError> {
2009 if !self.caps.contains(ShellCaps::CLIPBOARD) {
2010 return Err(self.unsupported("clipboard"));
2011 }
2012 self.check_window(window)?;
2013 let request = ClipboardRequestId(self.next_request_id);
2014 self.next_request_id += 1;
2015 if self.answerable() {
2016 let answer = self.answer(window, request, mime);
2017 self.queue.push_back(answer);
2021 } else {
2022 self.reads.push(PendingRead {
2023 window,
2024 request,
2025 mime,
2026 countdown: self.clipboard_deadline,
2027 });
2028 }
2029 Ok(request)
2030 }
2031
2032 fn clipboard_readable(&self, window: WindowId) -> bool {
2038 self.caps.contains(ShellCaps::CLIPBOARD) && self.window(window).is_ok() && self.answerable()
2039 }
2040}
2041
2042#[cfg(test)]
2043mod tests {
2044 use super::*;
2045 use crate::{AspectRatio, LogicalSize};
2046
2047 fn shell_with_window() -> (HeadlessShell, WindowId) {
2049 let mut shell = HeadlessShell::new();
2050 let window = shell
2051 .create_window(&WindowDesc::default())
2052 .expect("headless window creation never fails");
2053 (shell, window)
2054 }
2055
2056 fn shell_with_configured_window() -> (HeadlessShell, WindowId) {
2058 let (mut shell, window) = shell_with_window();
2059 settle(&mut shell);
2060 assert!(shell.window_state(window).expect("state").is_configured());
2061 (shell, window)
2062 }
2063
2064 fn settle(shell: &mut HeadlessShell) {
2066 for _ in 0..8 {
2067 let mut seen = 0;
2068 shell.pump(&mut |_| seen += 1);
2069 if seen == 0 && shell.windows.iter().all(|(_, w)| w.pending.is_none()) {
2070 return;
2071 }
2072 }
2073 panic!("the shell never settled");
2074 }
2075
2076 fn drain(shell: &mut HeadlessShell) -> Vec<ShellEvent> {
2077 let mut events = Vec::new();
2078 shell.pump(&mut |event| events.push(event));
2079 events
2080 }
2081
2082 fn names(events: &[ShellEvent]) -> Vec<&'static str> {
2083 events.iter().map(ShellEvent::name).collect()
2084 }
2085
2086 fn create_swapchain(state: &WindowState) -> Result<PhysicalSize, &'static str> {
2089 let size = state.size().ok_or("window is not configured yet")?;
2090 if size.is_empty() {
2091 return Err("zero-extent swapchain");
2092 }
2093 Ok(size)
2094 }
2095
2096 #[cfg(feature = "accesskit")]
2101 #[test]
2102 fn a_tree_reaches_the_reader_only_while_it_reads() {
2103 use accesskit::{Action, ActionRequest, Node, NodeId, Role, TreeId, TreeInfo, TreeUpdate};
2104
2105 let (mut shell, window) = shell_with_configured_window();
2106 let update = |label: &str| TreeUpdate {
2107 nodes: vec![(NodeId(1), {
2108 let mut node = Node::new(Role::Window);
2109 node.set_label(label);
2110 node
2111 })],
2112 tree: Some(TreeInfo::new(NodeId(1))),
2113 tree_id: TreeId::ROOT,
2114 focus: NodeId(1),
2115 };
2116
2117 shell
2118 .update_accessibility(window, update("unread"))
2119 .expect("update");
2120 assert_eq!(shell.accessibility_updates(window).expect("updates"), None);
2121
2122 shell.activate_accessibility(window).expect("activate");
2123 shell
2124 .activate_accessibility(window)
2125 .expect("activate twice");
2126 shell
2127 .update_accessibility(window, update("read"))
2128 .expect("update");
2129 let click = ActionRequest {
2130 action: Action::Click,
2131 target_tree: TreeId::ROOT,
2132 target_node: NodeId(1),
2133 data: None,
2134 };
2135 shell
2136 .request_accessibility_action(window, click.clone())
2137 .expect("action");
2138 assert_eq!(
2139 shell.accessibility_updates(window).expect("updates"),
2140 Some(&[update("read")][..])
2141 );
2142 let events = drain(&mut shell);
2143 assert_eq!(
2144 names(&events),
2145 ["AccessibilityActivated", "AccessibilityAction"]
2146 );
2147 assert_eq!(
2148 events[1],
2149 ShellEvent::AccessibilityAction {
2150 window,
2151 request: click
2152 }
2153 );
2154
2155 shell.deactivate_accessibility(window).expect("deactivate");
2156 shell
2157 .deactivate_accessibility(window)
2158 .expect("deactivate twice");
2159 shell
2160 .update_accessibility(window, update("gone"))
2161 .expect("update");
2162 assert_eq!(shell.accessibility_updates(window).expect("updates"), None);
2163 assert_eq!(names(&drain(&mut shell)), ["AccessibilityDeactivated"]);
2164 }
2165
2166 #[test]
2169 fn a_new_window_has_no_size_until_a_later_pump() {
2170 let (mut shell, window) = shell_with_window();
2171
2172 let state = shell.window_state(window).expect("state");
2175 assert!(!state.is_configured());
2176 assert_eq!(state.size(), None);
2177 assert_eq!(state.scale_factor(), None);
2178 assert_eq!(state.effective_mode(), None);
2179 assert_eq!(state.requested_mode, DisplayMode::Windowed);
2180
2181 assert_eq!(drain(&mut shell), Vec::new());
2183 assert!(!shell.window_state(window).expect("state").is_configured());
2184
2185 let events = drain(&mut shell);
2187 assert_eq!(
2188 events,
2189 vec![ShellEvent::Resized {
2190 window,
2191 size: PhysicalSize::new(1280, 720),
2192 scale_factor: 1.0,
2193 }]
2194 );
2195 let state = shell.window_state(window).expect("state");
2196 assert_eq!(state.size(), Some(PhysicalSize::new(1280, 720)));
2197 assert_eq!(state.scale_factor(), Some(1.0));
2198 assert_eq!(state.effective_mode(), Some(DisplayMode::Windowed));
2199 assert!(state.mode_request_honoured());
2200 }
2201
2202 #[test]
2206 fn keep_alive_takes_a_turn_and_keeps_everything_for_the_next_pump() {
2207 let (mut shell, window) = shell_with_window();
2208 shell.inject(ShellEvent::CloseRequested { window });
2209
2210 for _ in 0..=DEFAULT_CONFIGURE_DELAY {
2212 shell.keep_alive();
2213 }
2214 assert!(
2215 shell.window_state(window).expect("state").is_configured(),
2216 "every turn the configure waits for was taken by keep_alive"
2217 );
2218
2219 assert_eq!(
2220 drain(&mut shell),
2221 vec![
2222 ShellEvent::CloseRequested { window },
2223 ShellEvent::Resized {
2224 window,
2225 size: PhysicalSize::new(1280, 720),
2226 scale_factor: 1.0,
2227 },
2228 ],
2229 "nothing keep_alive saw was delivered or dropped"
2230 );
2231 assert_eq!(drain(&mut shell), Vec::new(), "and each arrived once");
2232 }
2233
2234 #[test]
2235 fn a_consumer_that_skips_the_wait_gets_a_clean_error_not_a_wrong_extent() {
2236 let (mut shell, window) = shell_with_window();
2237
2238 assert_eq!(
2240 create_swapchain(&shell.window_state(window).expect("state")),
2241 Err("window is not configured yet")
2242 );
2243
2244 let target = shell.surface_target(window).expect("the handle exists");
2250 assert_eq!(target, SurfaceTarget::Offscreen);
2251
2252 let size = loop {
2254 shell.pump(&mut |_| {});
2255 if let Ok(size) = create_swapchain(&shell.window_state(window).expect("state")) {
2256 break size;
2257 }
2258 };
2259 assert_eq!(size, PhysicalSize::new(1280, 720));
2260 assert_eq!(
2263 shell.surface_target(window).expect("target"),
2264 target,
2265 "the target is invariant under a configure"
2266 );
2267 }
2268
2269 #[test]
2270 fn the_configure_delay_is_a_knob() {
2271 let mut shell = HeadlessShell::new().with_configure_delay(3);
2272 let window = shell.create_window(&WindowDesc::default()).expect("window");
2273 for pump in 0..3 {
2274 assert_eq!(drain(&mut shell), Vec::new(), "pump {pump}");
2275 assert!(!shell.window_state(window).expect("state").is_configured());
2276 }
2277 assert_eq!(names(&drain(&mut shell)), ["Resized"]);
2278 assert!(shell.window_state(window).expect("state").is_configured());
2279
2280 let mut shell = HeadlessShell::new().with_configure_delay(0);
2283 let window = shell.create_window(&WindowDesc::default()).expect("window");
2284 assert_eq!(names(&drain(&mut shell)), ["Resized"]);
2285 assert!(shell.window_state(window).expect("state").is_configured());
2286
2287 shell.set_configure_delay(2);
2288 shell
2289 .set_mode(window, DisplayMode::Borderless { monitor: None })
2290 .expect("request");
2291 assert_eq!(drain(&mut shell), Vec::new());
2292 assert_eq!(drain(&mut shell), Vec::new());
2293 assert_eq!(names(&drain(&mut shell)), ["Resized"]);
2294 }
2295
2296 #[test]
2297 fn set_mode_is_a_request_whose_effect_arrives_later() {
2298 let (mut shell, window) = shell_with_configured_window();
2299 shell
2300 .set_mode(window, DisplayMode::Borderless { monitor: None })
2301 .expect("request");
2302
2303 let state = shell.window_state(window).expect("state");
2305 assert_eq!(
2306 state.requested_mode,
2307 DisplayMode::Borderless { monitor: None }
2308 );
2309 assert_eq!(
2310 state.effective_mode(),
2311 Some(DisplayMode::Windowed),
2312 "still windowed until the window system says otherwise"
2313 );
2314 assert!(
2315 !state.mode_request_honoured(),
2316 "a UI toggle reading the request instead of the effect would lie here"
2317 );
2318 assert_eq!(state.size(), Some(PhysicalSize::new(1280, 720)));
2319
2320 settle(&mut shell);
2321 let state = shell.window_state(window).expect("state");
2322 assert_eq!(
2323 state.effective_mode(),
2324 Some(DisplayMode::Borderless { monitor: None })
2325 );
2326 assert!(state.mode_request_honoured());
2327 assert_eq!(state.size(), Some(PhysicalSize::new(1920, 1080)));
2328 }
2329
2330 #[test]
2331 fn a_refused_mode_is_expressible_through_injection() {
2332 let (mut shell, window) = shell_with_configured_window();
2335 shell
2336 .set_mode(window, DisplayMode::Borderless { monitor: None })
2337 .expect("request");
2338 shell
2340 .resize(window, PhysicalSize::new(1280, 720))
2341 .expect("answer");
2342 let state = shell.window_state(window).expect("state");
2343 assert_eq!(state.effective_mode(), Some(DisplayMode::Windowed));
2344 assert!(!state.mode_request_honoured());
2345 }
2346
2347 #[test]
2353 fn a_reconfigured_panel_is_configured_at_its_new_size() {
2354 let mut shell = HeadlessShell::new();
2355 let monitor = shell.monitors()[0].id;
2356 let bar = crate::PanelDesc {
2357 namespace: "bar",
2358 monitor: Some(monitor),
2359 anchor: crate::PanelAnchor::TOP | crate::PanelAnchor::LEFT | crate::PanelAnchor::RIGHT,
2360 reserve: crate::Reserve::OwnExtent,
2361 ..crate::PanelDesc::default()
2362 };
2363 let window = shell
2364 .create_window(&WindowDesc {
2365 size: LogicalSize::new(400.0, 30.0),
2366 role: crate::WindowRole::Panel(bar),
2367 ..WindowDesc::default()
2368 })
2369 .expect("a panel");
2370 settle(&mut shell);
2371 let scale = shell
2372 .window_state(window)
2373 .expect("live")
2374 .scale_factor()
2375 .expect("configured");
2376
2377 let taller = crate::PanelDesc {
2378 layer: crate::PanelLayer::Overlay,
2379 margins: crate::Margins {
2380 top: 4.0,
2381 ..crate::Margins::default()
2382 },
2383 keyboard: crate::PanelKeyboard::OnDemand,
2384 ..bar
2385 };
2386 let size = LogicalSize::new(400.0, 50.0);
2387 shell.set_panel(window, size, &taller).expect("set_panel");
2388 assert_eq!(shell.panel(window).expect("live"), Some(taller));
2389 assert_eq!(
2390 shell.window_state(window).expect("live").size(),
2391 Some(LogicalSize::new(400.0, 30.0).to_physical(scale)),
2392 "a request: nothing changes before the window system answers"
2393 );
2394 let mut resized = Vec::new();
2395 for _ in 0..8 {
2396 resized.extend(
2397 drain(&mut shell)
2398 .into_iter()
2399 .filter_map(|event| match event {
2400 ShellEvent::Resized {
2401 window: of, size, ..
2402 } if of == window => Some(size),
2403 _ => None,
2404 }),
2405 );
2406 }
2407 assert_eq!(resized, [size.to_physical(scale)]);
2408
2409 let refused = |shell: &mut HeadlessShell, panel: &crate::PanelDesc<'_>| match shell
2410 .set_panel(window, LogicalSize::new(1.0, 1.0), panel)
2411 {
2412 Err(ShellError::InvalidDescriptor(message)) => message,
2413 other => panic!("expected InvalidDescriptor, got {other:?}"),
2414 };
2415 let renamed = crate::PanelDesc {
2416 namespace: "dock",
2417 ..taller
2418 };
2419 assert!(refused(&mut shell, &renamed).contains("namespace"));
2420 let anywhere = crate::PanelDesc {
2421 monitor: None,
2422 ..taller
2423 };
2424 assert!(refused(&mut shell, &anywhere).contains("monitor"));
2425 let toplevel = shell
2426 .create_window(&WindowDesc::default())
2427 .expect("a toplevel");
2428 assert!(matches!(
2429 shell.set_panel(toplevel, size, &taller),
2430 Err(ShellError::InvalidDescriptor(message)) if message.contains("not a panel")
2431 ));
2432 let cornered = crate::PanelDesc {
2433 anchor: crate::PanelAnchor::TOP | crate::PanelAnchor::LEFT,
2434 ..taller
2435 };
2436 assert!(matches!(
2437 shell.set_panel(window, size, &cornered),
2438 Err(ShellError::Unsupported { .. })
2439 ));
2440 assert_eq!(
2441 shell.panel(window).expect("live"),
2442 Some(taller),
2443 "a refused descriptor is not recorded"
2444 );
2445 assert_eq!(shell.panel(toplevel).expect("live"), None);
2446
2447 shell.destroy_window(window).expect("destroy");
2448 assert!(matches!(
2449 shell.set_panel(window, size, &taller),
2450 Err(ShellError::InvalidWindow { .. })
2451 ));
2452 }
2453
2454 #[test]
2457 fn a_repositioned_popup_is_configured_at_its_new_size() {
2458 let (mut shell, window) = shell_with_configured_window();
2459 let anchor = crate::LogicalRect::new(
2460 crate::LogicalPoint::new(10.0, 10.0),
2461 LogicalSize::new(100.0, 20.0),
2462 );
2463 let popup = shell
2464 .create_window(&WindowDesc {
2465 size: LogicalSize::new(200.0, 150.0),
2466 role: crate::WindowRole::Popup(crate::PopupDesc {
2467 parent: window,
2468 anchor,
2469 placement: crate::PopupPlacement::Below,
2470 input: crate::PopupInput::Pointer,
2471 }),
2472 ..WindowDesc::default()
2473 })
2474 .expect("a popup");
2475 settle(&mut shell);
2476 let scale = shell
2477 .window_state(popup)
2478 .expect("live")
2479 .scale_factor()
2480 .expect("configured");
2481
2482 let moved = LogicalSize::new(240.0, 90.0);
2483 shell
2484 .reposition_popup(popup, anchor, moved)
2485 .expect("reposition");
2486 settle(&mut shell);
2487 assert_eq!(
2488 shell.window_state(popup).expect("live").size(),
2489 Some(moved.to_physical(scale))
2490 );
2491 assert!(matches!(
2492 shell.reposition_popup(window, anchor, moved),
2493 Err(ShellError::InvalidDescriptor(_))
2494 ));
2495 }
2496
2497 #[test]
2501 fn escape_on_a_grabbing_popup_is_delivered_then_asks_it_to_close() {
2502 let (mut shell, window) = shell_with_configured_window();
2503 let popup_of = |shell: &mut HeadlessShell, input| {
2504 shell
2505 .create_window(&WindowDesc {
2506 role: crate::WindowRole::Popup(crate::PopupDesc {
2507 parent: window,
2508 anchor: crate::LogicalRect::default(),
2509 placement: crate::PopupPlacement::Below,
2510 input,
2511 }),
2512 ..WindowDesc::default()
2513 })
2514 .expect("a popup")
2515 };
2516 let menu = popup_of(&mut shell, crate::PopupInput::Grab);
2517 let tooltip = popup_of(&mut shell, crate::PopupInput::Pointer);
2518 settle(&mut shell);
2519 let is_close = |event: &ShellEvent| matches!(event, ShellEvent::CloseRequested { .. });
2520
2521 shell.key_press(menu, KeyCode::Escape).expect("press");
2522 let events = drain(&mut shell);
2523 assert!(
2524 matches!(
2525 events.as_slice(),
2526 [
2527 ShellEvent::Key {
2528 key_code: Some(KeyCode::Escape),
2529 ..
2530 },
2531 ShellEvent::CloseRequested { window: closed },
2532 ] if *closed == menu
2533 ),
2534 "the key, then the request: {events:?}"
2535 );
2536 shell.key_press(menu, KeyCode::Escape).expect("press");
2538 shell.key_repeat(menu, KeyCode::Escape).expect("repeat");
2539 assert!(!drain(&mut shell).iter().any(is_close));
2540
2541 shell
2543 .reply_close_request(menu, CloseReply::Keep)
2544 .expect("keep");
2545 shell.key_release(menu, KeyCode::Escape).expect("release");
2546 shell.key_press(menu, KeyCode::Enter).expect("press");
2547 assert!(!drain(&mut shell).iter().any(is_close), "not these");
2548 shell.key_press(menu, KeyCode::Escape).expect("press");
2549 assert!(drain(&mut shell).iter().any(is_close));
2550
2551 shell.key_press(tooltip, KeyCode::Escape).expect("press");
2552 shell.key_press(window, KeyCode::Escape).expect("press");
2553 assert!(!drain(&mut shell).iter().any(is_close));
2554 }
2555
2556 #[test]
2557 fn set_constraints_is_a_request_too() {
2558 let (mut shell, window) = shell_with_configured_window();
2559 let constraints = SizeConstraints::min(LogicalSize::new(1600.0, 900.0));
2560 shell.set_constraints(window, constraints).expect("request");
2561
2562 let state = shell.window_state(window).expect("state");
2563 assert_eq!(state.requested_constraints, constraints);
2564 assert_eq!(
2565 state.size(),
2566 Some(PhysicalSize::new(1280, 720)),
2567 "the size only changes when the window system says so"
2568 );
2569
2570 settle(&mut shell);
2571 assert_eq!(
2572 shell.window_state(window).expect("state").size(),
2573 Some(PhysicalSize::new(1600, 900))
2574 );
2575 }
2576
2577 #[test]
2578 fn constraints_do_not_cancel_a_mode_request_that_has_not_landed() {
2579 let (mut shell, window) = shell_with_configured_window();
2584 shell
2585 .set_mode(window, DisplayMode::Borderless { monitor: None })
2586 .expect("mode request");
2587 shell
2588 .set_constraints(window, SizeConstraints::min(LogicalSize::new(320.0, 180.0)))
2589 .expect("constraint request");
2590 settle(&mut shell);
2591
2592 let state = shell.window_state(window).expect("state");
2593 assert_eq!(
2594 state.effective_mode(),
2595 Some(DisplayMode::Borderless { monitor: None })
2596 );
2597 assert!(state.mode_request_honoured());
2598 }
2599
2600 #[test]
2601 fn a_borderless_window_reports_its_own_monitors_scale() {
2602 let mut shell = HeadlessShell::new();
2607 let hidpi = shell.plug_monitor(MonitorInfo {
2608 id: MonitorId(0),
2609 name: "HEADLESS-2".to_string(),
2610 bounds: PhysicalRect::new(1920, 0, 2560, 1440),
2611 work_area: PhysicalRect::new(1920, 0, 2560, 1440),
2612 scale_factor: 2.0,
2613 refresh_millihertz: 143_998,
2614 is_primary: false,
2615 });
2616 let window = shell
2617 .create_window(&WindowDesc {
2618 mode: DisplayMode::Borderless {
2619 monitor: Some(hidpi),
2620 },
2621 ..WindowDesc::default()
2622 })
2623 .expect("window");
2624 settle(&mut shell);
2625
2626 let state = shell.window_state(window).expect("state");
2627 assert_eq!(state.size(), Some(PhysicalSize::new(2560, 1440)));
2628 assert_eq!(
2629 state.scale_factor(),
2630 Some(2.0),
2631 "the scale is the borderless monitor's, not the primary's"
2632 );
2633 let other = shell
2636 .create_window(&WindowDesc::default())
2637 .expect("second window");
2638 settle(&mut shell);
2639 shell
2640 .set_mode(
2641 other,
2642 DisplayMode::Borderless {
2643 monitor: Some(hidpi),
2644 },
2645 )
2646 .expect("request");
2647 settle(&mut shell);
2648 assert_eq!(
2649 shell.window_state(other).expect("state").scale_factor(),
2650 shell.window_state(window).expect("state").scale_factor()
2651 );
2652 }
2653
2654 #[test]
2655 fn constraints_set_before_the_first_configure_fold_into_it() {
2656 let mut shell = HeadlessShell::new();
2657 let window = shell.create_window(&WindowDesc::default()).expect("window");
2658 shell
2659 .set_constraints(
2660 window,
2661 SizeConstraints::NONE.with_aspect(AspectRatio::SQUARE),
2662 )
2663 .expect("request");
2664 settle(&mut shell);
2665 assert_eq!(
2668 shell.window_state(window).expect("state").size(),
2669 Some(PhysicalSize::new(720, 720))
2670 );
2671 }
2672
2673 #[test]
2674 fn creation_uses_the_requested_size_and_the_monitors_scale() {
2675 let mut shell = HeadlessShell::new();
2676 let window = shell
2677 .create_window(&WindowDesc {
2678 size: LogicalSize::new(800.0, 600.0),
2679 ..WindowDesc::default()
2680 })
2681 .expect("window");
2682 settle(&mut shell);
2683 assert_eq!(
2684 shell.window_state(window).expect("state").size(),
2685 Some(PhysicalSize::new(800, 600))
2686 );
2687
2688 let mut shell = HeadlessShell::new().with_scale_factor(2.0);
2689 let window = shell
2690 .create_window(&WindowDesc {
2691 size: LogicalSize::new(640.0, 360.0),
2692 ..WindowDesc::default()
2693 })
2694 .expect("window");
2695 settle(&mut shell);
2696 let state = shell.window_state(window).expect("state");
2697 assert_eq!(state.size(), Some(PhysicalSize::new(1280, 720)));
2698 assert_eq!(state.scale_factor(), Some(2.0));
2699 }
2700
2701 #[test]
2704 fn scale_changes_preserve_logical_size_and_report_both_facts() {
2705 let (mut shell, window) = shell_with_configured_window();
2706 shell
2707 .change_scale_factor(window, 1.5)
2708 .expect("scale change");
2709 let events = drain(&mut shell);
2710 assert_eq!(
2711 events,
2712 vec![
2713 ShellEvent::ScaleFactorChanged {
2714 window,
2715 scale_factor: 1.5,
2716 size: PhysicalSize::new(1920, 1080),
2717 },
2718 ShellEvent::Resized {
2721 window,
2722 size: PhysicalSize::new(1920, 1080),
2723 scale_factor: 1.5,
2724 },
2725 ]
2726 );
2727 assert_eq!(
2731 shell.window_state(window).expect("state").size(),
2732 Some(PhysicalSize::new(1920, 1080))
2733 );
2734 }
2735
2736 #[test]
2737 fn a_scale_change_scheduled_by_a_mode_switch_arrives_before_the_resize() {
2738 let mut shell = HeadlessShell::new();
2739 let hidpi = shell.plug_monitor(MonitorInfo {
2740 id: MonitorId(0),
2741 name: "HEADLESS-2".to_string(),
2742 bounds: PhysicalRect::new(1920, 0, 2560, 1440),
2743 work_area: PhysicalRect::new(1920, 0, 2560, 1440),
2744 scale_factor: 2.0,
2745 refresh_millihertz: 143_998,
2746 is_primary: false,
2747 });
2748 assert_eq!(hidpi, MonitorId(2), "ids are never reused");
2749 let window = shell.create_window(&WindowDesc::default()).expect("window");
2750 settle(&mut shell);
2751
2752 shell
2753 .set_mode(
2754 window,
2755 DisplayMode::Borderless {
2756 monitor: Some(hidpi),
2757 },
2758 )
2759 .expect("request");
2760 assert_eq!(drain(&mut shell), Vec::new(), "still only a request");
2761 let events = drain(&mut shell);
2762 assert_eq!(names(&events), ["ScaleFactorChanged", "Resized"]);
2763
2764 let state = shell.window_state(window).expect("state");
2765 assert_eq!(state.size(), Some(PhysicalSize::new(2560, 1440)));
2766 assert_eq!(state.scale_factor(), Some(2.0));
2767 assert!(state.effective_mode().expect("configured").is_borderless());
2768 assert_eq!(
2773 shell.surface_target(window).expect("target"),
2774 SurfaceTarget::Offscreen
2775 );
2776
2777 assert!(matches!(
2778 shell.set_mode(
2779 window,
2780 DisplayMode::Borderless {
2781 monitor: Some(MonitorId(99)),
2782 },
2783 ),
2784 Err(ShellError::NoSuchMonitor(99))
2785 ));
2786 }
2787
2788 #[test]
2789 fn resizes_honour_constraints_and_inject_bypasses_them() {
2790 let (mut shell, window) = shell_with_configured_window();
2791 shell
2792 .set_constraints(
2793 window,
2794 SizeConstraints::min(LogicalSize::new(640.0, 360.0))
2795 .with_aspect(AspectRatio::WIDESCREEN),
2796 )
2797 .expect("request");
2798 settle(&mut shell);
2799
2800 shell
2801 .resize(window, PhysicalSize::new(1000, 1000))
2802 .expect("resize");
2803 assert_eq!(
2804 drain(&mut shell),
2805 vec![ShellEvent::Resized {
2806 window,
2807 size: PhysicalSize::new(1000, 562),
2808 scale_factor: 1.0,
2809 }]
2810 );
2811
2812 shell.inject(ShellEvent::Resized {
2815 window,
2816 size: PhysicalSize::new(300, 900),
2817 scale_factor: 1.0,
2818 });
2819 assert_eq!(
2820 drain(&mut shell)[0],
2821 ShellEvent::Resized {
2822 window,
2823 size: PhysicalSize::new(300, 900),
2824 scale_factor: 1.0,
2825 }
2826 );
2827 }
2828
2829 #[test]
2830 fn a_compositor_configure_supersedes_a_pending_one() {
2831 let (mut shell, window) = shell_with_configured_window();
2832 shell
2833 .set_mode(window, DisplayMode::Borderless { monitor: None })
2834 .expect("request");
2835 shell
2837 .resize(window, PhysicalSize::new(640, 480))
2838 .expect("answer");
2839 assert_eq!(names(&drain(&mut shell)), ["Resized"]);
2840 assert_eq!(
2841 shell.window_state(window).expect("state").size(),
2842 Some(PhysicalSize::new(640, 480))
2843 );
2844 assert_eq!(drain(&mut shell), Vec::new());
2846 }
2847
2848 #[test]
2851 fn timestamps_come_from_the_shells_clock_not_from_the_pump() {
2852 let (mut shell, window) = shell_with_configured_window();
2853 shell.set_time(Duration::from_secs(2));
2854 shell.key_press(window, KeyCode::Space).expect("press");
2855 shell.advance(Duration::from_millis(180));
2856 shell.key_release(window, KeyCode::Space).expect("release");
2857
2858 let events = drain(&mut shell);
2859 let held = events[1]
2860 .time()
2861 .expect("input")
2862 .saturating_since(events[0].time().expect("input"));
2863 assert_eq!(held, Duration::from_millis(180));
2864 assert_eq!(events[0].time(), Some(EventTime::from_millis(2_000)));
2865 }
2866
2867 #[test]
2868 fn synthesized_key_fields_are_stable_and_plausible() {
2869 let (mut shell, window) = shell_with_configured_window();
2870 shell.set_modifiers(Modifiers::CTRL);
2871 shell.key_press(window, KeyCode::KeyS).expect("press");
2872 let events = drain(&mut shell);
2873 let ShellEvent::Key {
2874 scancode,
2875 key_code,
2876 keysym,
2877 modifiers,
2878 repeat,
2879 device,
2880 ..
2881 } = events[0].clone()
2882 else {
2883 panic!("wrong variant");
2884 };
2885 assert_eq!(key_code, Some(KeyCode::KeyS));
2886 assert_eq!(keysym, Keysym::from_char('s'));
2887 assert_eq!(modifiers, Modifiers::CTRL);
2888 assert!(!repeat);
2889 assert_eq!(device, VIRTUAL_DEVICE);
2890 assert!(scancode.0 >= 8, "evdev-shaped scancodes start at 8");
2891 assert_eq!(synthetic_keysym(KeyCode::Digit7), Keysym::from_char('7'));
2892 assert_eq!(synthetic_keysym(KeyCode::F5), Keysym(0xFFC2));
2893 assert_eq!(synthetic_keysym(KeyCode::Enter), Keysym(0xFF0D));
2894 assert_eq!(synthetic_keysym(KeyCode::Escape), Keysym(0xFF1B));
2895 assert_eq!(synthetic_keysym(KeyCode::ArrowLeft), Keysym(0xFF51));
2896 assert_eq!(synthetic_keysym(KeyCode::Slash), Keysym::from_char('/'));
2897
2898 shell.key_repeat(window, KeyCode::KeyS).expect("repeat");
2899 let events = drain(&mut shell);
2900 let ShellEvent::Key { repeat, .. } = events[0] else {
2901 panic!("wrong variant");
2902 };
2903 assert!(repeat);
2904 }
2905
2906 #[test]
2907 fn a_locked_pointer_reports_no_absolute_position() {
2908 let (mut shell, window) = shell_with_configured_window();
2909 shell
2910 .set_pointer_mode(window, PointerMode::Locked)
2911 .expect("lock");
2912 shell
2913 .move_pointer(window, PhysicalPoint::new(10.0, 10.0), (3.0, -4.0))
2914 .expect("motion");
2915 let events = drain(&mut shell);
2916 let ShellEvent::PointerMotion { abs, raw_delta, .. } = events[0] else {
2917 panic!("wrong variant");
2918 };
2919 assert_eq!(abs, None);
2920 assert_eq!(raw_delta, Some((3.0, -4.0)));
2921 }
2922
2923 #[test]
2924 fn a_backend_without_raw_motion_reports_none() {
2925 let mut shell = HeadlessShell::new()
2926 .with_caps(ShellCaps::DESKTOP - ShellCaps::RAW_POINTER_MOTION)
2927 .with_configure_delay(0);
2928 let window = shell.create_window(&WindowDesc::default()).expect("window");
2929 drain(&mut shell);
2930 shell
2931 .move_pointer(window, PhysicalPoint::new(1.0, 2.0), (5.0, 5.0))
2932 .expect("motion");
2933 let events = drain(&mut shell);
2934 let ShellEvent::PointerMotion { abs, raw_delta, .. } = events[0] else {
2935 panic!("wrong variant");
2936 };
2937 assert_eq!(abs, Some(PhysicalPoint::new(1.0, 2.0)));
2938 assert_eq!(raw_delta, None, "no relative-motion source");
2939 assert!(!shell.caps().has_mouselook());
2940 }
2941
2942 #[test]
2943 fn pointer_focus_and_scroll_and_text_are_injectable() {
2944 let (mut shell, window) = shell_with_configured_window();
2945 shell
2946 .set_pointer_focus(window, true, Some(PhysicalPoint::new(1.0, 1.0)))
2947 .expect("enter");
2948 shell
2949 .scroll(window, ScrollDelta::Lines { x: 0.0, y: -1.0 }, None)
2950 .expect("scroll");
2951 shell
2952 .button(
2953 window,
2954 PointerButton::Left,
2955 ButtonState::Pressed,
2956 Some(PhysicalPoint::new(1.0, 1.0)),
2957 )
2958 .expect("click");
2959 shell.commit_text(window, "ありがとう").expect("text");
2960 shell
2961 .set_pointer_focus(window, false, Some(PhysicalPoint::new(1.0, 1.0)))
2962 .expect("leave");
2963
2964 let events = drain(&mut shell);
2965 assert_eq!(
2966 names(&events),
2967 [
2968 "PointerFocus",
2969 "Wheel",
2970 "Button",
2971 "TextCommit",
2972 "PointerFocus"
2973 ]
2974 );
2975 let ShellEvent::PointerFocus {
2976 entered, position, ..
2977 } = &events[4]
2978 else {
2979 panic!("wrong variant");
2980 };
2981 assert!(!entered);
2982 assert_eq!(*position, None, "a leave has no position");
2983 assert!(events.iter().all(ShellEvent::is_input));
2984 }
2985
2986 #[test]
2993 fn contacts_are_injectable_and_a_touchless_shell_refuses_them() {
2994 let (mut shell, window) = shell_with_configured_window();
2995 assert!(shell.caps().contains(ShellCaps::TOUCH));
2996 for (contact, phase, x) in [
2997 (1, TouchPhase::Began, 10.0),
2998 (2, TouchPhase::Began, 200.0),
2999 (2, TouchPhase::Moved, 220.0),
3000 (1, TouchPhase::Ended, 10.0),
3001 (2, TouchPhase::Cancelled, 220.0),
3002 ] {
3003 shell
3004 .touch(
3005 window,
3006 ContactId(contact),
3007 phase,
3008 PhysicalPoint::new(x, 5.0),
3009 )
3010 .expect("a contact");
3011 }
3012 let events = drain(&mut shell);
3013 assert_eq!(names(&events), ["Touch"; 5]);
3014 let seen: Vec<(ContactId, TouchPhase, f64)> = events
3015 .iter()
3016 .map(|event| {
3017 let ShellEvent::Touch {
3018 contact,
3019 phase,
3020 position,
3021 ..
3022 } = event
3023 else {
3024 panic!("wrong variant");
3025 };
3026 (*contact, *phase, position.x)
3027 })
3028 .collect();
3029 assert_eq!(
3030 seen,
3031 vec![
3032 (ContactId(1), TouchPhase::Began, 10.0),
3033 (ContactId(2), TouchPhase::Began, 200.0),
3034 (ContactId(2), TouchPhase::Moved, 220.0),
3035 (ContactId(1), TouchPhase::Ended, 10.0),
3036 (ContactId(2), TouchPhase::Cancelled, 220.0),
3037 ]
3038 );
3039 assert!(events.iter().all(ShellEvent::is_input));
3040
3041 let mut touchless = HeadlessShell::new()
3042 .with_caps(ShellCaps::DESKTOP - ShellCaps::TOUCH)
3043 .with_configure_delay(0);
3044 let window = touchless
3045 .create_window(&WindowDesc::default())
3046 .expect("window");
3047 drain(&mut touchless);
3048 assert!(matches!(
3049 touchless.touch(
3050 window,
3051 ContactId(1),
3052 TouchPhase::Began,
3053 PhysicalPoint::ORIGIN
3054 ),
3055 Err(ShellError::Unsupported { .. })
3056 ));
3057 assert!(drain(&mut touchless).is_empty(), "and queued nothing");
3058 }
3059
3060 #[test]
3061 fn warping_produces_motion_like_every_platform_that_has_it() {
3062 let (mut shell, window) = shell_with_configured_window();
3063 shell
3064 .warp_pointer(window, PhysicalPoint::new(320.0, 180.0))
3065 .expect("warp");
3066 let events = drain(&mut shell);
3067 let ShellEvent::PointerMotion { abs, raw_delta, .. } = events[0] else {
3068 panic!("wrong variant");
3069 };
3070 assert_eq!(abs, Some(PhysicalPoint::new(320.0, 180.0)));
3071 assert_eq!(raw_delta, None, "a warp is not physical motion");
3072 }
3073
3074 #[test]
3077 fn capabilities_gate_the_operations_that_need_them() {
3078 let mut shell = HeadlessShell::new().with_caps(ShellCaps::empty());
3079 let window = shell.create_window(&WindowDesc::default()).expect("window");
3080
3081 assert!(matches!(
3082 shell.warp_pointer(window, PhysicalPoint::ORIGIN),
3083 Err(ShellError::Unsupported { .. })
3084 ));
3085 assert!(matches!(
3086 shell.set_pointer_mode(window, PointerMode::Locked),
3087 Err(ShellError::Unsupported { .. })
3088 ));
3089 assert!(matches!(
3090 shell.clipboard_request(window, MimeType::TextUtf8),
3091 Err(ShellError::Unsupported { .. })
3092 ));
3093 shell
3095 .set_pointer_mode(window, PointerMode::Free)
3096 .expect("Free is always available");
3097 assert!(matches!(
3099 shell.create_window(&WindowDesc::default()),
3100 Err(ShellError::Unsupported { .. })
3101 ));
3102 }
3103
3104 #[test]
3105 fn stale_handles_fail_cleanly_on_every_accessor() {
3106 let (mut shell, window) = shell_with_configured_window();
3107 shell.destroy_window(window).expect("destroy");
3108 assert_eq!(
3109 drain(&mut shell),
3110 vec![ShellEvent::WindowDestroyed { window }]
3111 );
3112
3113 assert!(matches!(
3114 shell.window_state(window),
3115 Err(ShellError::InvalidWindow { .. })
3116 ));
3117 assert!(matches!(
3118 shell.surface_target(window),
3119 Err(ShellError::InvalidWindow { .. })
3120 ));
3121 assert!(matches!(
3122 shell.resize(window, PhysicalSize::new(1, 1)),
3123 Err(ShellError::InvalidWindow { .. })
3124 ));
3125 assert!(matches!(
3126 shell.set_title(window, "gone"),
3127 Err(ShellError::InvalidWindow { .. })
3128 ));
3129 assert!(matches!(
3130 shell.destroy_window(window),
3131 Err(ShellError::InvalidWindow { .. })
3132 ));
3133 assert!(matches!(
3134 shell.key_press(window, KeyCode::KeyA),
3135 Err(ShellError::InvalidWindow { .. })
3136 ));
3137 }
3138
3139 #[test]
3140 fn destroying_a_window_cancels_its_pending_configure() {
3141 let (mut shell, window) = shell_with_window();
3144 shell.destroy_window(window).expect("destroy");
3145 assert_eq!(names(&drain(&mut shell)), ["WindowDestroyed"]);
3146 assert_eq!(drain(&mut shell), Vec::new());
3147 assert_eq!(drain(&mut shell), Vec::new());
3148 }
3149
3150 #[test]
3151 fn close_requests_are_questions() {
3152 let (mut shell, window) = shell_with_configured_window();
3153 assert!(matches!(
3154 shell.reply_close_request(window, CloseReply::Close),
3155 Err(ShellError::NoPendingCloseRequest { .. })
3156 ));
3157
3158 shell.request_close(window).expect("request");
3159 assert!(shell.window_state(window).expect("state").close_pending);
3160 shell
3161 .reply_close_request(window, CloseReply::Keep)
3162 .expect("keep");
3163 assert!(!shell.window_state(window).expect("state").close_pending);
3164 assert!(shell.window_state(window).is_ok(), "still open");
3165
3166 shell.request_close(window).expect("request again");
3167 shell
3168 .reply_close_request(window, CloseReply::Close)
3169 .expect("close");
3170 assert!(matches!(
3171 shell.window_state(window),
3172 Err(ShellError::InvalidWindow { .. })
3173 ));
3174 }
3175
3176 #[test]
3177 fn monitor_hotplug_reports_a_change_and_keeps_ids_unique() {
3178 let mut shell = HeadlessShell::new();
3179 let extra = shell.plug_monitor(MonitorInfo {
3180 id: MonitorId(0),
3181 name: "HEADLESS-2".to_string(),
3182 bounds: PhysicalRect::new(1920, 0, 1920, 1080),
3183 work_area: PhysicalRect::new(1920, 0, 1920, 1080),
3184 scale_factor: 1.0,
3185 refresh_millihertz: 60_000,
3186 is_primary: false,
3187 });
3188 assert_eq!(shell.monitors().len(), 2);
3189 assert_eq!(drain(&mut shell), vec![ShellEvent::MonitorsChanged]);
3190
3191 shell.unplug_monitor(extra).expect("unplug");
3192 assert_eq!(shell.monitors().len(), 1);
3193 assert_eq!(
3194 drain(&mut shell),
3195 vec![
3196 ShellEvent::MonitorRemoved { monitor: extra },
3197 ShellEvent::MonitorsChanged
3198 ]
3199 );
3200 assert!(matches!(
3201 shell.unplug_monitor(extra),
3202 Err(ShellError::NoSuchMonitor(_))
3203 ));
3204 assert_eq!(
3205 shell.monitor(MonitorId(1)).map(|m| m.name.as_str()),
3206 Some("HEADLESS-1")
3207 );
3208 assert!(shell.monitor(extra).is_none());
3209 }
3210
3211 #[test]
3212 fn unplugging_a_monitor_asks_its_panels_and_only_its_panels_to_close() {
3213 let mut shell = HeadlessShell::new();
3214 let second = shell.plug_monitor(MonitorInfo {
3215 id: MonitorId(0),
3216 name: "HEADLESS-2".to_string(),
3217 bounds: PhysicalRect::new(1920, 0, 1920, 1080),
3218 work_area: PhysicalRect::new(1920, 0, 1920, 1080),
3219 scale_factor: 1.0,
3220 refresh_millihertz: 60_000,
3221 is_primary: false,
3222 });
3223 let first = shell.monitors()[0].id;
3224 let panel_on = |shell: &mut HeadlessShell, monitor| {
3225 shell
3226 .create_window(&WindowDesc {
3227 role: crate::WindowRole::Panel(crate::PanelDesc {
3228 monitor,
3229 ..crate::PanelDesc::default()
3230 }),
3231 ..WindowDesc::default()
3232 })
3233 .expect("panel")
3234 };
3235 let doomed = panel_on(&mut shell, Some(second));
3236 let already_asked = panel_on(&mut shell, Some(second));
3237 let elsewhere = panel_on(&mut shell, Some(first));
3238 let anywhere = panel_on(&mut shell, None);
3239 let toplevel = shell.create_window(&WindowDesc::default()).expect("window");
3240 shell.request_close(already_asked).expect("close");
3241 settle(&mut shell);
3242 drain(&mut shell);
3243
3244 shell.unplug_monitor(second).expect("unplug");
3245 assert_eq!(
3246 drain(&mut shell),
3247 vec![
3248 ShellEvent::MonitorRemoved { monitor: second },
3249 ShellEvent::CloseRequested { window: doomed },
3250 ShellEvent::MonitorsChanged,
3251 ]
3252 );
3253 for window in [elsewhere, anywhere, toplevel] {
3254 assert!(!shell.window_state(window).expect("open").close_pending);
3255 }
3256 }
3257
3258 #[test]
3259 fn drops_require_both_the_capability_and_the_opt_in() {
3260 let mut shell = HeadlessShell::new();
3261 let plain = shell.create_window(&WindowDesc::default()).expect("window");
3262 assert!(matches!(
3263 shell.drop_file(plain, "/tmp/a.gltf", None),
3264 Err(ShellError::Unsupported { .. })
3265 ));
3266
3267 let dropper = shell
3268 .create_window(&WindowDesc {
3269 accept_drops: true,
3270 ..WindowDesc::default()
3271 })
3272 .expect("window");
3273 settle(&mut shell);
3274 shell
3275 .drop_file(dropper, "/tmp/a.gltf", Some(PhysicalPoint::new(4.0, 5.0)))
3276 .expect("drop");
3277 assert_eq!(names(&drain(&mut shell))[0], "DroppedFile");
3278 }
3279
3280 #[test]
3281 fn a_sink_that_queues_more_events_does_not_spin_the_pump() {
3282 let (mut shell, window) = shell_with_configured_window();
3283 shell.inject(ShellEvent::MonitorsChanged);
3284 let mut extra = Vec::new();
3285 shell.pump(&mut |event| extra.push(event));
3286 assert_eq!(extra.len(), 1);
3287 shell.inject(ShellEvent::CloseRequested { window });
3288 assert_eq!(shell.pending_events(), 1);
3289 }
3290
3291 #[test]
3292 fn accessors_that_no_real_backend_can_provide_are_headless_only() {
3293 let (mut shell, window) = shell_with_configured_window();
3294 shell.set_title(window, "renamed").expect("title");
3295 assert_eq!(shell.title(window).expect("title"), "renamed");
3296 shell.set_cursor(window, None).expect("hide");
3297 assert_eq!(shell.cursor(window).expect("cursor"), None);
3298 shell
3299 .set_cursor(window, Some(CursorIcon::Crosshair))
3300 .expect("shape");
3301 assert_eq!(
3302 shell.cursor(window).expect("cursor"),
3303 Some(CursorIcon::Crosshair)
3304 );
3305 shell.set_visible(window, false).expect("hide window");
3306 assert!(!shell.window_state(window).expect("state").visible);
3307 }
3308
3309 #[test]
3310 fn an_input_region_is_recorded_replaced_and_given_back() {
3311 let (mut shell, window) = shell_with_configured_window();
3312 assert_eq!(shell.input_region(window).expect("region"), None);
3313 let panel = PhysicalRect::new(0, 0, 400, 300).rounded(12);
3314 shell
3315 .set_input_region(window, Some(&panel))
3316 .expect("a region");
3317 assert_eq!(
3318 shell.input_region(window).expect("region"),
3319 Some(panel.as_slice())
3320 );
3321 shell.set_input_region(window, Some(&[])).expect("nothing");
3322 assert_eq!(shell.input_region(window).expect("region"), Some(&[][..]));
3323 shell.set_input_region(window, None).expect("whole window");
3324 assert_eq!(shell.input_region(window).expect("region"), None);
3325 shell.destroy_window(window).expect("destroy");
3326 assert!(matches!(
3327 shell.set_input_region(window, None),
3328 Err(ShellError::InvalidWindow { .. })
3329 ));
3330 assert!(shell.input_region(window).is_err());
3331 }
3332
3333 fn frames_for(events: &[ShellEvent], window: WindowId) -> usize {
3334 events
3335 .iter()
3336 .filter(|event| matches!(event, ShellEvent::FrameReady { window: w } if *w == window))
3337 .count()
3338 }
3339
3340 #[test]
3341 fn requests_for_a_frame_share_one_answer_at_the_next_pump() {
3342 let (mut shell, window) = shell_with_configured_window();
3343 assert!(!shell.frame_requested(window).expect("live"));
3344 shell.request_frame(window).expect("a frame");
3345 shell.request_frame(window).expect("the same frame");
3346 assert!(shell.frame_requested(window).expect("live"));
3347 let events = drain(&mut shell);
3348 assert_eq!(frames_for(&events, window), 1, "{:?}", names(&events));
3349 assert!(!shell.frame_requested(window).expect("live"));
3350 assert_eq!(
3351 frames_for(&drain(&mut shell), window),
3352 0,
3353 "nothing asked again"
3354 );
3355 shell.request_frame(window).expect("the next frame");
3356 assert_eq!(frames_for(&drain(&mut shell), window), 1);
3357 }
3358
3359 #[test]
3360 fn a_window_destroyed_with_a_frame_requested_is_not_answered() {
3361 let (mut shell, doomed) = shell_with_configured_window();
3362 let kept = shell
3363 .create_window(&WindowDesc::default())
3364 .expect("a second window");
3365 settle(&mut shell);
3366 shell.request_frame(doomed).expect("a frame");
3367 shell.request_frame(kept).expect("a frame");
3368 shell.destroy_window(doomed).expect("destroy");
3369 let events = drain(&mut shell);
3370 assert_eq!(frames_for(&events, doomed), 0, "{:?}", names(&events));
3371 assert_eq!(frames_for(&events, kept), 1, "the other window still asked");
3372 assert!(matches!(
3373 shell.request_frame(doomed),
3374 Err(ShellError::InvalidWindow { .. })
3375 ));
3376 assert!(shell.frame_requested(doomed).is_err());
3377 }
3378
3379 #[test]
3380 fn a_wait_does_not_sleep_through_a_frame_that_is_due() {
3381 let (mut shell, window) = shell_with_configured_window();
3382 shell.request_frame(window).expect("a frame");
3383 let start = std::time::Instant::now();
3384 shell.wait_events(Some(LONG_WAIT));
3385 assert!(
3386 start.elapsed() < LONG_WAIT / 2,
3387 "slept {:?} with a frame due",
3388 start.elapsed()
3389 );
3390 assert_eq!(frames_for(&drain(&mut shell), window), 1);
3391 }
3392
3393 #[test]
3394 fn waiting_is_observable_and_focus_transitions_are_events() {
3395 let (mut shell, window) = shell_with_configured_window();
3396 assert_eq!(shell.wait_count(), 0);
3397 shell.wait_events(Some(Duration::from_millis(5)));
3398 shell.wait_events(None);
3399 assert_eq!(shell.wait_count(), 2);
3400
3401 shell.set_focus(window, true).expect("focus");
3402 assert!(shell.window_state(window).expect("state").focused);
3403 shell.set_focus(window, false).expect("unfocus");
3404 assert_eq!(names(&drain(&mut shell)), ["Focus", "Focus"]);
3405 }
3406
3407 const LONG_WAIT: Duration = Duration::from_secs(60);
3412 const RING_AFTER: Duration = Duration::from_millis(50);
3414
3415 fn ring_later(waker: ShellWaker) -> std::thread::JoinHandle<()> {
3417 std::thread::spawn(move || {
3418 std::thread::sleep(RING_AFTER);
3419 waker.wake();
3420 })
3421 }
3422
3423 #[test]
3424 fn a_wake_from_another_thread_ends_a_long_wait() {
3425 let mut shell = HeadlessShell::new();
3426 let ringer = ring_later(shell.waker().expect("headless has a waker"));
3427 let start = std::time::Instant::now();
3428 shell.wait_events(Some(LONG_WAIT));
3429 let slept = start.elapsed();
3430 ringer.join().expect("the ringer");
3431 assert!(slept < LONG_WAIT / 2, "the wait ran {slept:?}");
3432 }
3433
3434 #[test]
3435 fn a_wake_before_the_wait_ends_it_at_once_and_wakes_coalesce() {
3436 let mut shell = HeadlessShell::new();
3437 let waker = shell.waker().expect("headless has a waker");
3438 for _ in 0..3 {
3439 waker.wake();
3440 }
3441 let start = std::time::Instant::now();
3442 shell.wait_events(Some(LONG_WAIT));
3443 assert!(
3444 start.elapsed() < LONG_WAIT / 2,
3445 "a rung bell must end the wait"
3446 );
3447
3448 let timeout = Duration::from_millis(100);
3450 let start = std::time::Instant::now();
3451 shell.wait_events(Some(timeout));
3452 assert!(
3453 start.elapsed() >= timeout / 2,
3454 "the bell was cleared, so this wait must sleep, but it took {:?}",
3455 start.elapsed()
3456 );
3457 }
3458
3459 #[test]
3460 fn with_a_waker_out_an_untimed_wait_sleeps_until_it_rings() {
3461 let mut shell = HeadlessShell::new();
3462 let ringer = ring_later(shell.waker().expect("headless has a waker"));
3463 let start = std::time::Instant::now();
3464 shell.wait_events(None);
3465 let slept = start.elapsed();
3466 ringer.join().expect("the ringer");
3467 assert!(
3468 slept >= RING_AFTER / 2,
3469 "an untimed wait returned after {slept:?}, before anything rang"
3470 );
3471 }
3472
3473 #[test]
3476 fn the_clipboard_round_trips_through_an_event() {
3477 let (mut shell, window) = shell_with_configured_window();
3478 shell
3479 .clipboard_offer(
3480 window,
3481 &[
3482 ClipboardOffer::text("node"),
3483 ClipboardOffer::ron("(kind:\"node\")"),
3484 ],
3485 )
3486 .expect("offer");
3487 assert_eq!(
3488 shell.clipboard_bytes(MimeType::TextUtf8),
3489 Some(&b"node"[..])
3490 );
3491
3492 let request = shell
3493 .clipboard_request(window, MimeType::CrcblRon)
3494 .expect("request");
3495 assert_eq!(
3496 drain(&mut shell)[0],
3497 ShellEvent::ClipboardData {
3498 window,
3499 request,
3500 mime: ReceivedMime::from(MimeType::CrcblRon),
3501 content: ClipboardContent::Bytes(b"(kind:\"node\")".to_vec()),
3502 }
3503 );
3504
3505 let request = shell
3508 .clipboard_request(window, MimeType::UriList)
3509 .expect("request");
3510 assert_eq!(
3511 drain(&mut shell)[0],
3512 ShellEvent::ClipboardData {
3513 window,
3514 request,
3515 mime: ReceivedMime::from(MimeType::UriList),
3516 content: ClipboardContent::Empty,
3517 }
3518 );
3519
3520 shell
3523 .clipboard_offer(window, &[ClipboardOffer::text("")])
3524 .expect("offer");
3525 let request = shell
3526 .clipboard_request(window, MimeType::TextUtf8)
3527 .expect("request");
3528 assert_eq!(
3529 drain(&mut shell)[0],
3530 ShellEvent::ClipboardData {
3531 window,
3532 request,
3533 mime: ReceivedMime::from(MimeType::TextUtf8),
3534 content: ClipboardContent::Bytes(Vec::new()),
3535 }
3536 );
3537 }
3538
3539 #[test]
3540 fn a_read_taken_while_the_clipboard_is_unreadable_is_held_not_answered_empty() {
3541 let (mut shell, window) = shell_with_configured_window();
3547 shell
3548 .clipboard_offer(window, &[ClipboardOffer::text("published")])
3549 .expect("offer");
3550 shell.set_clipboard_readable(false);
3551 assert!(!shell.clipboard_readable(window));
3552
3553 let request = shell
3554 .clipboard_request(window, MimeType::TextUtf8)
3555 .expect("a read is accepted even when it cannot be answered yet");
3556 assert_eq!(shell.held_clipboard_reads(), 1);
3557 assert!(
3558 drain(&mut shell).is_empty(),
3559 "a held read produces no event at all — least of all an empty one"
3560 );
3561
3562 shell.set_clipboard_readable(true);
3564 let events = drain(&mut shell);
3565 assert_eq!(events.len(), 1);
3566 assert_eq!(
3567 events[0],
3568 ShellEvent::ClipboardData {
3569 window,
3570 request,
3571 mime: ReceivedMime::from(MimeType::TextUtf8),
3572 content: ClipboardContent::Bytes(b"published".to_vec()),
3573 }
3574 );
3575 assert_eq!(shell.held_clipboard_reads(), 0);
3576 }
3577
3578 #[test]
3579 fn a_read_that_never_becomes_answerable_ends_as_unavailable() {
3580 let (mut shell, window) = shell_with_configured_window();
3584 shell
3585 .clipboard_offer(window, &[ClipboardOffer::text("unreachable")])
3586 .expect("offer");
3587 shell.set_clipboard_readable(false);
3588 shell.clipboard_deadline(2);
3589
3590 let request = shell
3591 .clipboard_request(window, MimeType::TextUtf8)
3592 .expect("request");
3593 assert!(drain(&mut shell).is_empty(), "pump 1: still held");
3594 assert!(drain(&mut shell).is_empty(), "pump 2: still held");
3595 let events = drain(&mut shell);
3596 assert_eq!(
3597 events[0],
3598 ShellEvent::ClipboardData {
3599 window,
3600 request,
3601 mime: ReceivedMime::from(MimeType::TextUtf8),
3602 content: ClipboardContent::Unavailable,
3605 }
3606 );
3607 assert_eq!(shell.held_clipboard_reads(), 0);
3608 }
3609
3610 #[test]
3611 fn a_peer_that_holds_the_clipboard_and_never_delivers_is_unavailable() {
3612 let (mut shell, window) = shell_with_configured_window();
3616 shell.set_foreign_clipboard(vec![(
3617 ReceivedMime::new("text/plain"),
3618 b"never sent".to_vec(),
3619 )]);
3620 shell.set_clipboard_stalled(true);
3621 shell.clipboard_deadline(1);
3622 assert!(
3623 !shell.clipboard_readable(window),
3624 "a clipboard whose owner will not answer is not readable"
3625 );
3626
3627 let request = shell
3628 .clipboard_request(window, MimeType::TextUtf8)
3629 .expect("request");
3630 assert!(drain(&mut shell).is_empty());
3631 let events = drain(&mut shell);
3632 let ShellEvent::ClipboardData {
3633 request: answered,
3634 content,
3635 ..
3636 } = &events[0]
3637 else {
3638 panic!("wrong variant");
3639 };
3640 assert_eq!(*answered, request);
3641 assert_eq!(*content, ClipboardContent::Unavailable);
3642
3643 shell.set_clipboard_stalled(false);
3645 let request = shell
3646 .clipboard_request(window, MimeType::TextUtf8)
3647 .expect("request");
3648 assert_eq!(
3649 drain(&mut shell)[0],
3650 ShellEvent::ClipboardData {
3651 window,
3652 request,
3653 mime: ReceivedMime::new("text/plain"),
3654 content: ClipboardContent::Bytes(b"never sent".to_vec()),
3655 }
3656 );
3657 }
3658
3659 #[test]
3660 fn claiming_the_clipboard_can_require_a_user_interaction() {
3661 let (mut shell, window) = shell_with_configured_window();
3665 shell.require_user_interaction(true);
3666
3667 let refused = shell.clipboard_offer(window, &[ClipboardOffer::text("stolen")]);
3668 assert!(
3669 matches!(
3670 refused,
3671 Err(ShellError::NeedsUserInteraction {
3672 backend: ShellBackend::Headless,
3673 ..
3674 })
3675 ),
3676 "{refused:?}"
3677 );
3678 assert!(
3679 shell.clipboard_bytes(MimeType::TextUtf8).is_none(),
3680 "a refused claim must not have taken the clipboard anyway"
3681 );
3682
3683 shell.key_press(window, KeyCode::KeyC).expect("key");
3686 shell
3687 .clipboard_offer(window, &[ClipboardOffer::text("copied")])
3688 .expect("with a serial in hand, the claim goes through");
3689 assert_eq!(
3690 shell.clipboard_bytes(MimeType::TextUtf8),
3691 Some(&b"copied"[..])
3692 );
3693
3694 shell.clear_user_interaction();
3696 assert!(matches!(
3697 shell.clipboard_offer(window, &[ClipboardOffer::text("later")]),
3698 Err(ShellError::NeedsUserInteraction { .. })
3699 ));
3700 }
3701
3702 #[test]
3703 fn a_held_read_for_a_destroyed_window_is_dropped_rather_than_answered() {
3704 let (mut shell, window) = shell_with_configured_window();
3707 shell.set_clipboard_readable(false);
3708 let _request = shell
3709 .clipboard_request(window, MimeType::TextUtf8)
3710 .expect("request");
3711 assert_eq!(shell.held_clipboard_reads(), 1);
3712
3713 shell.destroy_window(window).expect("destroy");
3714 let events = drain(&mut shell);
3715 assert_eq!(shell.held_clipboard_reads(), 0);
3716 assert!(
3717 !events
3718 .iter()
3719 .any(|event| matches!(event, ShellEvent::ClipboardData { .. })),
3720 "no clipboard answer may name a destroyed window: {events:?}"
3721 );
3722 }
3723
3724 #[test]
3725 fn a_paste_from_a_foreign_application_keeps_the_foreign_spelling() {
3726 let (mut shell, window) = shell_with_configured_window();
3727 shell.set_foreign_clipboard(vec![
3728 (ReceivedMime::new("text/plain"), b"from gedit".to_vec()),
3731 (
3732 ReceivedMime::new("application/vnd.some-editor.scene+json"),
3733 b"{}".to_vec(),
3734 ),
3735 ]);
3736
3737 let request = shell
3739 .clipboard_request(window, MimeType::TextUtf8)
3740 .expect("request");
3741 let events = drain(&mut shell);
3742 let ShellEvent::ClipboardData {
3743 request: answered,
3744 mime,
3745 content,
3746 ..
3747 } = &events[0]
3748 else {
3749 panic!("wrong variant");
3750 };
3751 assert_eq!(*answered, request);
3752 assert_eq!(content.bytes(), Some(&b"from gedit"[..]));
3753 assert_eq!(mime.as_str(), "text/plain");
3755 assert!(mime.matches(MimeType::TextUtf8));
3756 assert_ne!(*mime, ReceivedMime::from(MimeType::TextUtf8));
3757
3758 assert_eq!(
3761 shell.clipboard_bytes(MimeType::Other("application/vnd.some-editor.scene+json")),
3762 Some(&b"{}"[..])
3763 );
3764 }
3765
3766 #[test]
3769 #[should_panic(expected = "a shell needs at least one monitor")]
3770 fn a_shell_with_no_monitors_is_rejected() {
3771 let _ = HeadlessShell::new().with_monitors(Vec::new());
3772 }
3773
3774 #[test]
3775 #[should_panic(expected = "a text commit is never empty")]
3776 fn an_empty_text_commit_is_rejected() {
3777 let (mut shell, window) = shell_with_configured_window();
3778 let _ = shell.commit_text(window, "");
3779 }
3780}