1use std::collections::VecDeque;
135use std::path::PathBuf;
136use std::sync::{Arc, Condvar, Mutex, PoisonError};
137
138use core::time::Duration;
139
140use crcbl_core::input::{
141 ButtonState, ContactId, DeviceId, Keysym, Modifiers, PointerButton, Scancode, ScrollDelta,
142 TouchPhase,
143};
144use crcbl_core::time::{ManualTime, TimeSource};
145use crcbl_core::{EventTime, Pool, SurfaceTarget};
146
147use crate::waker::WakeSignal;
148use crate::{
149 ClipboardContent, ClipboardOffer, ClipboardRequestId, CloseReply, CursorIcon, DisplayMode,
150 KeyCode, LogicalRect, LogicalSize, MimeType, MonitorId, MonitorInfo, PhysicalPoint,
151 PhysicalRect, PhysicalSize, PointerMode, ReceivedMime, Shell, ShellBackend, ShellCaps,
152 ShellError, ShellEvent, ShellWaker, SizeConstraints, TextPurpose, WindowConfiguration,
153 WindowDesc, WindowId, WindowState,
154};
155
156pub const DEFAULT_MONITOR_SIZE: PhysicalSize = PhysicalSize::new(1920, 1080);
161
162pub const DEFAULT_REFRESH_MILLIHERTZ: u32 = 60_000;
164
165pub const DEFAULT_CONFIGURE_DELAY: u32 = 1;
171
172pub const VIRTUAL_DEVICE: DeviceId = DeviceId(1);
178
179pub const DEFAULT_CLIPBOARD_DEADLINE: u32 = 4;
187
188#[derive(Clone, Debug)]
194struct PendingRead {
195 window: WindowId,
196 request: ClipboardRequestId,
197 mime: MimeType,
198 countdown: u32,
200}
201
202#[derive(Clone, Copy, Debug)]
206struct PendingConfigure {
207 size: PhysicalSize,
208 scale_factor: f64,
209 mode: DisplayMode,
210 countdown: u32,
212}
213
214#[derive(Clone, Debug)]
216struct HeadlessWindow {
217 title: String,
218 configuration: Option<WindowConfiguration>,
220 pending: Option<PendingConfigure>,
222 requested_mode: DisplayMode,
223 requested_constraints: SizeConstraints,
224 focused: bool,
225 visible: bool,
226 pointer_mode: PointerMode,
227 cursor: Option<CursorIcon>,
228 close_pending: bool,
229 accept_drops: bool,
230 parent: Option<WindowId>,
232 panel_monitor: Option<MonitorId>,
234 text_input: Option<TextPurpose>,
236 #[cfg(feature = "accesskit")]
239 accessibility: Option<Vec<accesskit::TreeUpdate>>,
240}
241
242impl HeadlessWindow {
243 fn size(&self) -> Option<PhysicalSize> {
245 self.configuration.map(|config| config.size)
246 }
247
248 fn scale_factor(&self) -> f64 {
254 self.configuration
255 .map(|config| config.scale_factor)
256 .or_else(|| self.pending.map(|pending| pending.scale_factor))
257 .unwrap_or(1.0)
258 }
259
260 fn settled_mode(&self) -> DisplayMode {
269 self.configuration
270 .map(|config| config.mode)
271 .or_else(|| self.pending.map(|pending| pending.mode))
272 .unwrap_or(self.requested_mode)
273 }
274
275 fn base_size(&self) -> PhysicalSize {
277 self.size()
278 .or_else(|| self.pending.map(|pending| pending.size))
279 .unwrap_or(PhysicalSize::new(1, 1))
280 }
281}
282
283#[derive(Debug)]
288pub struct HeadlessShell {
289 caps: ShellCaps,
290 monitors: Vec<MonitorInfo>,
291 next_monitor_id: u32,
292 windows: Pool<HeadlessWindow>,
293 queue: VecDeque<ShellEvent>,
294 clock: ManualTime,
295 modifiers: Modifiers,
296 clipboard: Vec<(ReceivedMime, Vec<u8>)>,
297 next_request_id: u32,
298 reads: Vec<PendingRead>,
301 clipboard_readable: bool,
305 clipboard_stalled: bool,
307 clipboard_needs_interaction: bool,
309 saw_interaction: bool,
312 clipboard_deadline: u32,
315 waits: u32,
316 configure_delay: u32,
317 wake: Option<Arc<HeadlessWake>>,
319}
320
321#[derive(Debug, Default)]
324struct HeadlessWake {
325 rung: Mutex<bool>,
326 bell: Condvar,
327}
328
329impl HeadlessWake {
330 fn wait(&self, timeout: Option<Duration>) {
333 let rung = self.rung.lock().unwrap_or_else(PoisonError::into_inner);
334 let mut rung = match timeout {
335 Some(timeout) => {
336 self.bell
337 .wait_timeout_while(rung, timeout, |rung| !*rung)
338 .unwrap_or_else(PoisonError::into_inner)
339 .0
340 }
341 None => self
342 .bell
343 .wait_while(rung, |rung| !*rung)
344 .unwrap_or_else(PoisonError::into_inner),
345 };
346 *rung = false;
347 }
348}
349
350impl WakeSignal for HeadlessWake {
351 fn wake(&self) {
352 *self.rung.lock().unwrap_or_else(PoisonError::into_inner) = true;
353 self.bell.notify_all();
354 }
355}
356
357impl Default for HeadlessShell {
358 fn default() -> Self {
359 Self::new()
360 }
361}
362
363impl HeadlessShell {
364 #[must_use]
366 pub fn new() -> Self {
367 Self {
368 caps: ShellCaps::DESKTOP,
369 monitors: vec![MonitorInfo {
370 id: MonitorId(1),
371 name: "HEADLESS-1".to_string(),
372 bounds: PhysicalRect::new(
373 0,
374 0,
375 DEFAULT_MONITOR_SIZE.width,
376 DEFAULT_MONITOR_SIZE.height,
377 ),
378 work_area: PhysicalRect::new(
379 0,
380 0,
381 DEFAULT_MONITOR_SIZE.width,
382 DEFAULT_MONITOR_SIZE.height,
383 ),
384 scale_factor: 1.0,
385 refresh_millihertz: DEFAULT_REFRESH_MILLIHERTZ,
386 is_primary: true,
387 }],
388 next_monitor_id: 2,
389 windows: Pool::new(),
390 queue: VecDeque::new(),
391 clock: ManualTime::new(),
392 modifiers: Modifiers::empty(),
393 clipboard: Vec::new(),
394 next_request_id: 1,
395 reads: Vec::new(),
396 clipboard_readable: true,
397 clipboard_stalled: false,
398 clipboard_needs_interaction: false,
399 saw_interaction: false,
400 clipboard_deadline: DEFAULT_CLIPBOARD_DEADLINE,
401 waits: 0,
402 configure_delay: DEFAULT_CONFIGURE_DELAY,
403 wake: None,
404 }
405 }
406
407 #[must_use]
412 pub fn with_caps(mut self, caps: ShellCaps) -> Self {
413 self.caps = caps;
414 self
415 }
416
417 #[must_use]
432 pub fn with_monitors(mut self, monitors: Vec<MonitorInfo>) -> Self {
433 assert!(!monitors.is_empty(), "a shell needs at least one monitor");
434 for (index, monitor) in monitors.iter().enumerate() {
435 assert!(
436 monitor.id.0 != u32::MAX,
437 "monitor id {} leaves no id for the next hotplug",
438 monitor.id.0
439 );
440 assert!(
441 !monitors[..index].iter().any(|seen| seen.id == monitor.id),
442 "monitor id {} appears twice",
443 monitor.id.0
444 );
445 }
446 self.next_monitor_id = monitors
447 .iter()
448 .map(|monitor| monitor.id.0 + 1)
449 .max()
450 .unwrap_or(1);
451 self.monitors = monitors;
452 self
453 }
454
455 #[must_use]
460 pub fn with_scale_factor(mut self, scale_factor: f64) -> Self {
461 assert!(scale_factor > 0.0, "scale factor must be positive");
462 self.monitors[0].scale_factor = scale_factor;
463 self
464 }
465
466 #[must_use]
471 pub const fn with_configure_delay(mut self, pumps: u32) -> Self {
472 self.configure_delay = pumps;
473 self
474 }
475
476 pub const fn set_configure_delay(&mut self, pumps: u32) {
486 self.configure_delay = pumps;
487 }
488
489 pub fn set_caps(&mut self, caps: ShellCaps) {
495 self.caps = caps;
496 }
497
498 #[must_use]
500 pub fn now(&self) -> EventTime {
501 EventTime::from_duration(self.clock.elapsed())
502 }
503
504 pub fn advance(&mut self, delta: Duration) {
510 self.clock.advance(delta);
511 }
512
513 pub fn set_time(&mut self, now: Duration) {
521 self.clock.set(now);
522 }
523
524 pub fn set_modifiers(&mut self, modifiers: Modifiers) {
529 self.modifiers = modifiers;
530 }
531
532 #[must_use]
534 pub fn pending_events(&self) -> usize {
535 self.queue.len()
536 }
537
538 #[must_use]
544 pub const fn wait_count(&self) -> u32 {
545 self.waits
546 }
547
548 pub fn inject(&mut self, event: ShellEvent) {
556 self.queue.push_back(event);
557 }
558
559 pub fn resize(&mut self, window: WindowId, size: PhysicalSize) -> Result<(), ShellError> {
575 let state = self.window_mut(window)?;
576 let scale_factor = state.scale_factor();
577 let mode = state.settled_mode();
578 let constrained = state.requested_constraints.apply(size, scale_factor);
579 state.pending = None;
580 state.configuration = Some(WindowConfiguration {
581 size: constrained,
582 scale_factor,
583 mode,
584 });
585 self.queue.push_back(ShellEvent::Resized {
586 window,
587 size: constrained,
588 scale_factor,
589 });
590 Ok(())
591 }
592
593 pub fn change_scale_factor(
615 &mut self,
616 window: WindowId,
617 scale_factor: f64,
618 ) -> Result<(), ShellError> {
619 assert!(scale_factor > 0.0, "scale factor must be positive");
620 let state = self.window_mut(window)?;
621 let logical = state.base_size().to_logical(state.scale_factor());
622 let mode = state.settled_mode();
623 let size = state
624 .requested_constraints
625 .apply(logical.to_physical(scale_factor), scale_factor);
626 state.pending = None;
627 state.configuration = Some(WindowConfiguration {
628 size,
629 scale_factor,
630 mode,
631 });
632 self.queue.push_back(ShellEvent::ScaleFactorChanged {
633 window,
634 scale_factor,
635 size,
636 });
637 self.queue.push_back(ShellEvent::Resized {
638 window,
639 size,
640 scale_factor,
641 });
642 Ok(())
643 }
644
645 pub fn set_focus(&mut self, window: WindowId, focused: bool) -> Result<(), ShellError> {
651 self.window_mut(window)?.focused = focused;
652 self.queue.push_back(ShellEvent::Focus { window, focused });
653 Ok(())
654 }
655
656 pub fn request_close(&mut self, window: WindowId) -> Result<(), ShellError> {
665 self.window_mut(window)?.close_pending = true;
666 self.queue.push_back(ShellEvent::CloseRequested { window });
667 Ok(())
668 }
669
670 pub fn key(
683 &mut self,
684 window: WindowId,
685 key: KeyCode,
686 state: ButtonState,
687 ) -> Result<(), ShellError> {
688 self.key_with(window, key, state, false)
689 }
690
691 pub fn key_press(&mut self, window: WindowId, key: KeyCode) -> Result<(), ShellError> {
697 self.key(window, key, ButtonState::Pressed)
698 }
699
700 pub fn key_release(&mut self, window: WindowId, key: KeyCode) -> Result<(), ShellError> {
706 self.key(window, key, ButtonState::Released)
707 }
708
709 pub fn key_repeat(&mut self, window: WindowId, key: KeyCode) -> Result<(), ShellError> {
716 self.key_with(window, key, ButtonState::Pressed, true)
717 }
718
719 fn key_with(
720 &mut self,
721 window: WindowId,
722 key: KeyCode,
723 state: ButtonState,
724 repeat: bool,
725 ) -> Result<(), ShellError> {
726 self.check_window(window)?;
727 self.saw_interaction = true;
728 let event = ShellEvent::Key {
729 window,
730 device: VIRTUAL_DEVICE,
731 time: self.now(),
732 scancode: synthetic_scancode(key),
733 key_code: Some(key),
734 keysym: synthetic_keysym(key),
735 state,
736 repeat,
737 modifiers: self.modifiers,
738 };
739 self.queue.push_back(event);
740 Ok(())
741 }
742
743 pub fn move_pointer(
754 &mut self,
755 window: WindowId,
756 position: PhysicalPoint,
757 raw_delta: (f64, f64),
758 ) -> Result<(), ShellError> {
759 let locked = self.window(window)?.pointer_mode == PointerMode::Locked;
760 let raw = if self.caps.contains(ShellCaps::RAW_POINTER_MOTION) {
761 Some(raw_delta)
762 } else {
763 None
764 };
765 self.saw_interaction = true;
766 let event = ShellEvent::PointerMotion {
767 window,
768 device: VIRTUAL_DEVICE,
769 time: self.now(),
770 abs: if locked { None } else { Some(position) },
771 raw_delta: raw,
772 };
773 self.queue.push_back(event);
774 Ok(())
775 }
776
777 pub fn set_pointer_focus(
783 &mut self,
784 window: WindowId,
785 entered: bool,
786 position: Option<PhysicalPoint>,
787 ) -> Result<(), ShellError> {
788 self.check_window(window)?;
789 let event = ShellEvent::PointerFocus {
790 window,
791 device: VIRTUAL_DEVICE,
792 time: self.now(),
793 entered,
794 position: if entered { position } else { None },
795 };
796 self.queue.push_back(event);
797 Ok(())
798 }
799
800 pub fn button(
806 &mut self,
807 window: WindowId,
808 button: PointerButton,
809 state: ButtonState,
810 position: Option<PhysicalPoint>,
811 ) -> Result<(), ShellError> {
812 self.check_window(window)?;
813 self.saw_interaction = true;
814 let event = ShellEvent::Button {
815 window,
816 device: VIRTUAL_DEVICE,
817 time: self.now(),
818 button,
819 state,
820 position,
821 modifiers: self.modifiers,
822 };
823 self.queue.push_back(event);
824 Ok(())
825 }
826
827 pub fn scroll(
833 &mut self,
834 window: WindowId,
835 delta: ScrollDelta,
836 position: Option<PhysicalPoint>,
837 ) -> Result<(), ShellError> {
838 self.check_window(window)?;
839 self.saw_interaction = true;
840 let event = ShellEvent::Wheel {
841 window,
842 device: VIRTUAL_DEVICE,
843 time: self.now(),
844 delta,
845 position,
846 modifiers: self.modifiers,
847 };
848 self.queue.push_back(event);
849 Ok(())
850 }
851
852 pub fn touch(
880 &mut self,
881 window: WindowId,
882 contact: ContactId,
883 phase: TouchPhase,
884 position: PhysicalPoint,
885 ) -> Result<(), ShellError> {
886 self.check_window(window)?;
887 if !self.caps.contains(ShellCaps::TOUCH) {
888 return Err(self.unsupported("touch"));
889 }
890 self.saw_interaction = true;
891 let event = ShellEvent::Touch {
892 window,
893 device: VIRTUAL_DEVICE,
894 time: self.now(),
895 contact,
896 phase,
897 position,
898 };
899 self.queue.push_back(event);
900 Ok(())
901 }
902
903 pub fn commit_text(&mut self, window: WindowId, text: &str) -> Result<(), ShellError> {
914 assert!(!text.is_empty(), "a text commit is never empty");
915 self.check_window(window)?;
916 let event = ShellEvent::TextCommit {
917 window,
918 time: self.now(),
919 text: text.to_string(),
920 };
921 self.queue.push_back(event);
922 Ok(())
923 }
924
925 pub fn drop_file(
933 &mut self,
934 window: WindowId,
935 path: impl Into<PathBuf>,
936 position: Option<PhysicalPoint>,
937 ) -> Result<(), ShellError> {
938 if !self.caps.contains(ShellCaps::DRAG_DROP) {
939 return Err(self.unsupported("drag and drop"));
940 }
941 if !self.window(window)?.accept_drops {
942 return Err(ShellError::Unsupported {
943 backend: ShellBackend::Headless,
944 what: "drops on a window created without accept_drops",
945 });
946 }
947 let event = ShellEvent::DroppedFile {
948 window,
949 time: self.now(),
950 path: path.into(),
951 position,
952 };
953 self.queue.push_back(event);
954 Ok(())
955 }
956
957 pub fn plug_monitor(&mut self, mut info: MonitorInfo) -> MonitorId {
964 let id = MonitorId(self.next_monitor_id);
965 self.next_monitor_id += 1;
966 info.id = id;
967 self.monitors.push(info);
968 self.queue.push_back(ShellEvent::MonitorsChanged);
969 id
970 }
971
972 pub fn unplug_monitor(&mut self, id: MonitorId) -> Result<(), ShellError> {
986 let index = self
987 .monitors
988 .iter()
989 .position(|monitor| monitor.id == id)
990 .ok_or(ShellError::NoSuchMonitor(id.0))?;
991 assert!(
992 self.monitors.len() > 1,
993 "a shell needs at least one monitor"
994 );
995 let previous = self.monitors.clone();
996 self.monitors.remove(index);
997 crate::monitor::queue_removed(&previous, &self.monitors, &mut self.queue);
998 let panels: Vec<WindowId> = self
999 .windows
1000 .iter()
1001 .filter(|(_, state)| state.panel_monitor == Some(id) && !state.close_pending)
1002 .map(|(window, _)| window.cast())
1003 .collect();
1004 for panel in panels {
1005 self.request_close(panel)?;
1006 }
1007 self.queue.push_back(ShellEvent::MonitorsChanged);
1008 Ok(())
1009 }
1010
1011 pub fn title(&self, window: WindowId) -> Result<&str, ShellError> {
1022 Ok(self.window(window)?.title.as_str())
1023 }
1024
1025 pub fn cursor(&self, window: WindowId) -> Result<Option<CursorIcon>, ShellError> {
1033 Ok(self.window(window)?.cursor)
1034 }
1035
1036 pub fn text_input(&self, window: WindowId) -> Result<Option<TextPurpose>, ShellError> {
1045 Ok(self.window(window)?.text_input)
1046 }
1047
1048 #[cfg(feature = "accesskit")]
1057 pub fn activate_accessibility(&mut self, window: WindowId) -> Result<(), ShellError> {
1058 let state = &mut self.window_mut(window)?.accessibility;
1059 if state.is_none() {
1060 *state = Some(Vec::new());
1061 self.queue
1062 .push_back(ShellEvent::AccessibilityActivated { window });
1063 }
1064 Ok(())
1065 }
1066
1067 #[cfg(feature = "accesskit")]
1077 pub fn deactivate_accessibility(&mut self, window: WindowId) -> Result<(), ShellError> {
1078 if self.window_mut(window)?.accessibility.take().is_some() {
1079 self.queue
1080 .push_back(ShellEvent::AccessibilityDeactivated { window });
1081 }
1082 Ok(())
1083 }
1084
1085 #[cfg(feature = "accesskit")]
1099 pub fn request_accessibility_action(
1100 &mut self,
1101 window: WindowId,
1102 request: accesskit::ActionRequest,
1103 ) -> Result<(), ShellError> {
1104 assert!(
1105 self.window(window)?.accessibility.is_some(),
1106 "no assistive technology is reading the window"
1107 );
1108 self.queue
1109 .push_back(ShellEvent::AccessibilityAction { window, request });
1110 Ok(())
1111 }
1112
1113 #[cfg(feature = "accesskit")]
1122 pub fn accessibility_updates(
1123 &self,
1124 window: WindowId,
1125 ) -> Result<Option<&[accesskit::TreeUpdate]>, ShellError> {
1126 Ok(self.window(window)?.accessibility.as_deref())
1127 }
1128
1129 #[must_use]
1133 pub fn clipboard_bytes(&self, mime: MimeType) -> Option<&[u8]> {
1134 self.clipboard
1135 .iter()
1136 .find(|(offered, _)| offered.matches(mime))
1137 .map(|(_, bytes)| bytes.as_slice())
1138 }
1139
1140 pub fn set_clipboard_readable(&mut self, readable: bool) {
1157 self.clipboard_readable = readable;
1158 }
1159
1160 pub fn set_clipboard_stalled(&mut self, stalled: bool) {
1169 self.clipboard_stalled = stalled;
1170 }
1171
1172 pub fn require_user_interaction(&mut self, required: bool) {
1199 self.clipboard_needs_interaction = required;
1200 }
1201
1202 pub fn clear_user_interaction(&mut self) {
1207 self.saw_interaction = false;
1208 }
1209
1210 pub fn clipboard_deadline(&mut self, pumps: u32) {
1216 self.clipboard_deadline = pumps;
1217 }
1218
1219 #[must_use]
1224 pub fn held_clipboard_reads(&self) -> usize {
1225 self.reads.len()
1226 }
1227
1228 pub fn set_foreign_clipboard(&mut self, offers: Vec<(ReceivedMime, Vec<u8>)>) {
1236 self.clipboard = offers;
1237 }
1238
1239 fn answerable(&self) -> bool {
1245 self.clipboard_readable && !self.clipboard_stalled
1246 }
1247
1248 fn answer(&self, window: WindowId, request: ClipboardRequestId, mime: MimeType) -> ShellEvent {
1250 let found = self
1255 .clipboard
1256 .iter()
1257 .find(|(offered, _)| offered.matches(mime));
1258 let (mime, content) = match found {
1259 Some((offered, bytes)) => (offered.clone(), ClipboardContent::Bytes(bytes.clone())),
1260 None => (ReceivedMime::from(mime), ClipboardContent::Empty),
1263 };
1264 ShellEvent::ClipboardData {
1265 window,
1266 request,
1267 mime,
1268 content,
1269 }
1270 }
1271
1272 fn resolve_reads(&mut self) {
1279 if self.reads.is_empty() {
1280 return;
1281 }
1282 let answerable = self.answerable();
1283 let mut held = Vec::with_capacity(self.reads.len());
1284 for mut read in core::mem::take(&mut self.reads) {
1285 if self.window(read.window).is_err() {
1289 continue;
1290 }
1291 if answerable {
1292 let answer = self.answer(read.window, read.request, read.mime);
1293 self.queue.push_back(answer);
1294 continue;
1295 }
1296 if read.countdown > 0 {
1297 read.countdown -= 1;
1298 held.push(read);
1299 continue;
1300 }
1301 self.queue.push_back(ShellEvent::ClipboardData {
1304 window: read.window,
1305 request: read.request,
1306 mime: ReceivedMime::from(read.mime),
1307 content: ClipboardContent::Unavailable,
1308 });
1309 }
1310 self.reads = held;
1311 }
1312
1313 fn deliver_due_configures(&mut self) {
1320 let mut due: Vec<(WindowId, PendingConfigure, Option<f64>)> = Vec::new();
1324 for (handle, state) in self.windows.iter_mut() {
1325 let Some(pending) = state.pending.as_mut() else {
1326 continue;
1327 };
1328 if pending.countdown > 0 {
1329 pending.countdown -= 1;
1330 continue;
1331 }
1332 let pending = state.pending.take().expect("checked just above");
1333 let previous_scale = state.configuration.map(|config| config.scale_factor);
1334 state.configuration = Some(WindowConfiguration {
1335 size: pending.size,
1336 scale_factor: pending.scale_factor,
1337 mode: pending.mode,
1338 });
1339 due.push((handle.cast(), pending, previous_scale));
1340 }
1341
1342 for (window, pending, previous_scale) in due {
1343 if previous_scale.is_some_and(|previous| previous != pending.scale_factor) {
1347 self.queue.push_back(ShellEvent::ScaleFactorChanged {
1348 window,
1349 scale_factor: pending.scale_factor,
1350 size: pending.size,
1351 });
1352 }
1353 self.queue.push_back(ShellEvent::Resized {
1354 window,
1355 size: pending.size,
1356 scale_factor: pending.scale_factor,
1357 });
1358 }
1359 }
1360
1361 fn window(&self, window: WindowId) -> Result<&HeadlessWindow, ShellError> {
1362 self.windows
1363 .get(window.cast())
1364 .ok_or_else(|| ShellError::invalid_window(window))
1365 }
1366
1367 fn window_mut(&mut self, window: WindowId) -> Result<&mut HeadlessWindow, ShellError> {
1368 self.windows
1369 .get_mut(window.cast())
1370 .ok_or_else(|| ShellError::invalid_window(window))
1371 }
1372
1373 fn check_window(&self, window: WindowId) -> Result<(), ShellError> {
1374 self.window(window).map(|_| ())
1375 }
1376
1377 fn unsupported(&self, what: &'static str) -> ShellError {
1378 ShellError::Unsupported {
1379 backend: ShellBackend::Headless,
1380 what,
1381 }
1382 }
1383}
1384
1385fn synthetic_scancode(key: KeyCode) -> Scancode {
1389 let index = KeyCode::ALL
1390 .iter()
1391 .position(|candidate| *candidate == key)
1392 .unwrap_or(0);
1393 Scancode(index as u32 + 8)
1396}
1397
1398fn synthetic_keysym(key: KeyCode) -> Keysym {
1400 let name = key.as_str();
1401 if let Some(letter) = name.strip_prefix("Key") {
1402 return Keysym::from_char(letter.to_ascii_lowercase().chars().next().unwrap_or('?'));
1404 }
1405 if let Some(digit) = name.strip_prefix("Digit") {
1406 return Keysym::from_char(digit.chars().next().unwrap_or('?'));
1407 }
1408 match key {
1409 KeyCode::Space => Keysym::from_char(' '),
1410 KeyCode::Minus => Keysym::from_char('-'),
1411 KeyCode::Equal => Keysym::from_char('='),
1412 KeyCode::Comma => Keysym::from_char(','),
1413 KeyCode::Period => Keysym::from_char('.'),
1414 KeyCode::Slash => Keysym::from_char('/'),
1415 KeyCode::Semicolon => Keysym::from_char(';'),
1416 KeyCode::Quote => Keysym::from_char('\''),
1417 KeyCode::Backslash => Keysym::from_char('\\'),
1418 KeyCode::BracketLeft => Keysym::from_char('['),
1419 KeyCode::BracketRight => Keysym::from_char(']'),
1420 KeyCode::Backquote => Keysym::from_char('`'),
1421 _ => Keysym::NONE,
1425 }
1426}
1427
1428impl Shell for HeadlessShell {
1429 fn backend(&self) -> ShellBackend {
1430 ShellBackend::Headless
1431 }
1432
1433 fn caps(&self) -> ShellCaps {
1434 self.caps
1435 }
1436
1437 fn create_window(&mut self, desc: &WindowDesc<'_>) -> Result<WindowId, ShellError> {
1438 crate::role::check(desc, self.caps(), self.backend(), |monitor| {
1439 self.monitor(monitor).is_some()
1440 })?;
1441 if !self.windows.is_empty() && !self.caps.contains(ShellCaps::MULTI_WINDOW) {
1442 return Err(self.unsupported("a second window"));
1443 }
1444 if let DisplayMode::Borderless {
1445 monitor: Some(monitor),
1446 } = desc.mode
1447 && self.monitor(monitor).is_none()
1448 {
1449 return Err(ShellError::NoSuchMonitor(monitor.0));
1450 }
1451
1452 let (size, scale_factor) = match desc.mode {
1464 DisplayMode::Windowed => {
1465 let scale_factor = self.monitors[0].scale_factor;
1466 (
1467 desc.constraints
1468 .apply(desc.size.to_physical(scale_factor), scale_factor),
1469 scale_factor,
1470 )
1471 }
1472 DisplayMode::Borderless { monitor } => {
1473 let monitor = monitor
1474 .and_then(|id| self.monitor(id))
1475 .unwrap_or(&self.monitors[0]);
1476 (monitor.size(), monitor.scale_factor)
1477 }
1478 };
1479
1480 let (parent, panel_monitor) = match desc.role {
1481 crate::WindowRole::Popup(popup) => {
1482 self.window(popup.parent)?;
1483 (Some(popup.parent), None)
1484 }
1485 crate::WindowRole::Panel(panel) => (None, panel.monitor),
1486 crate::WindowRole::Toplevel => (None, None),
1487 };
1488 let countdown = self.configure_delay;
1489 let window: WindowId = self
1490 .windows
1491 .insert(HeadlessWindow {
1492 title: desc.title.to_string(),
1493 configuration: None,
1496 pending: Some(PendingConfigure {
1497 size,
1498 scale_factor,
1499 mode: desc.mode,
1500 countdown,
1501 }),
1502 requested_mode: desc.mode,
1503 requested_constraints: desc.constraints,
1504 focused: false,
1505 visible: desc.visible,
1506 pointer_mode: PointerMode::Free,
1507 cursor: Some(CursorIcon::Default),
1508 close_pending: false,
1509 accept_drops: desc.accept_drops,
1510 parent,
1511 panel_monitor,
1512 text_input: None,
1513 #[cfg(feature = "accesskit")]
1514 accessibility: None,
1515 })
1516 .cast();
1517 Ok(window)
1518 }
1519
1520 fn destroy_window(&mut self, window: WindowId) -> Result<(), ShellError> {
1524 self.window(window)?;
1525 let popups: Vec<WindowId> = self
1526 .windows
1527 .iter()
1528 .filter(|(_, state)| state.parent == Some(window))
1529 .map(|(id, _)| id.cast())
1530 .collect();
1531 for popup in popups {
1532 self.destroy_window(popup)?;
1533 }
1534 self.windows
1535 .remove(window.cast())
1536 .ok_or_else(|| ShellError::invalid_window(window))?;
1537 self.queue.push_back(ShellEvent::WindowDestroyed { window });
1538 Ok(())
1539 }
1540
1541 fn window_state(&self, window: WindowId) -> Result<WindowState, ShellError> {
1542 let state = self.window(window)?;
1543 Ok(WindowState {
1544 configuration: state.configuration,
1545 requested_mode: state.requested_mode,
1546 requested_constraints: state.requested_constraints,
1547 focused: state.focused,
1548 visible: state.visible,
1549 pointer_mode: state.pointer_mode,
1550 close_pending: state.close_pending,
1551 })
1552 }
1553
1554 fn set_title(&mut self, window: WindowId, title: &str) -> Result<(), ShellError> {
1555 self.window_mut(window)?.title = title.to_string();
1556 Ok(())
1557 }
1558
1559 fn set_visible(&mut self, window: WindowId, visible: bool) -> Result<(), ShellError> {
1560 self.window_mut(window)?.visible = visible;
1561 Ok(())
1562 }
1563
1564 fn set_mode(&mut self, window: WindowId, mode: DisplayMode) -> Result<(), ShellError> {
1565 let target = match mode {
1566 DisplayMode::Windowed => None,
1567 DisplayMode::Borderless { monitor } => {
1568 let monitor = match monitor {
1569 Some(id) => self
1570 .monitor(id)
1571 .ok_or(ShellError::NoSuchMonitor(id.0))?
1572 .clone(),
1573 None => self.monitors[0].clone(),
1574 };
1575 Some(monitor)
1576 }
1577 };
1578 let countdown = self.configure_delay;
1579
1580 let state = self.window_mut(window)?;
1581 state.requested_mode = mode;
1587 let (size, scale_factor) = match &target {
1588 Some(monitor) => (monitor.size(), monitor.scale_factor),
1589 None => (state.base_size(), state.scale_factor()),
1593 };
1594 state.pending = Some(PendingConfigure {
1595 size,
1596 scale_factor,
1597 mode,
1598 countdown,
1599 });
1600 Ok(())
1601 }
1602
1603 fn set_constraints(
1604 &mut self,
1605 window: WindowId,
1606 constraints: SizeConstraints,
1607 ) -> Result<(), ShellError> {
1608 let countdown = self.configure_delay;
1609 let state = self.window_mut(window)?;
1610 state.requested_constraints = constraints;
1611
1612 if let Some(pending) = state.pending.as_mut() {
1619 pending.size = constraints.apply(pending.size, pending.scale_factor);
1620 return Ok(());
1621 }
1622
1623 if let Some(config) = state.configuration {
1627 let constrained = constraints.apply(config.size, config.scale_factor);
1628 if constrained != config.size {
1629 state.pending = Some(PendingConfigure {
1630 size: constrained,
1631 scale_factor: config.scale_factor,
1632 mode: config.mode,
1633 countdown,
1634 });
1635 }
1636 }
1637 Ok(())
1638 }
1639
1640 fn reposition_popup(
1644 &mut self,
1645 popup: WindowId,
1646 anchor: LogicalRect,
1647 size: LogicalSize,
1648 ) -> Result<(), ShellError> {
1649 let _ = anchor;
1650 let countdown = self.configure_delay;
1651 let state = self.window_mut(popup)?;
1652 if state.parent.is_none() {
1653 return Err(crate::role::not_a_popup(popup));
1654 }
1655 if let Some(pending) = state.pending.as_mut() {
1656 pending.size = size.to_physical(pending.scale_factor);
1657 } else if let Some(config) = state.configuration {
1658 state.pending = Some(PendingConfigure {
1659 size: size.to_physical(config.scale_factor),
1660 scale_factor: config.scale_factor,
1661 mode: config.mode,
1662 countdown,
1663 });
1664 }
1665 Ok(())
1666 }
1667
1668 fn monitors(&self) -> &[MonitorInfo] {
1669 &self.monitors
1670 }
1671
1672 fn pump(&mut self, sink: &mut dyn FnMut(ShellEvent)) {
1673 self.keep_alive();
1674 for _ in 0..self.queue.len() {
1679 let Some(event) = self.queue.pop_front() else {
1680 break;
1681 };
1682 sink(event);
1683 }
1684 }
1685
1686 fn keep_alive(&mut self) {
1694 self.deliver_due_configures();
1695 self.resolve_reads();
1696 }
1697
1698 fn wait_events(&mut self, timeout: Option<Duration>) {
1717 self.waits += 1;
1718 if !self.caps.contains(ShellCaps::EVENT_WAIT) || !self.queue.is_empty() {
1719 return;
1720 }
1721 match (&self.wake, timeout) {
1722 (Some(wake), timeout) => wake.wait(timeout),
1723 (None, Some(timeout)) => std::thread::sleep(timeout),
1724 (None, None) => {}
1725 }
1726 }
1727
1728 fn waker(&mut self) -> Option<ShellWaker> {
1731 let wake = self.wake.get_or_insert_with(Arc::default);
1732 Some(ShellWaker::new(Arc::clone(wake) as Arc<dyn WakeSignal>))
1733 }
1734
1735 fn align_event_clock(&mut self, elapsed: Duration) {
1742 self.set_time(elapsed);
1743 }
1744
1745 fn surface_target(&self, window: WindowId) -> Result<SurfaceTarget, ShellError> {
1746 self.window(window)?;
1752 Ok(SurfaceTarget::Offscreen)
1753 }
1754
1755 fn set_pointer_mode(&mut self, window: WindowId, mode: PointerMode) -> Result<(), ShellError> {
1756 if !self.caps.contains(mode.required_cap()) {
1757 return Err(self.unsupported(mode.as_str()));
1758 }
1759 self.window_mut(window)?.pointer_mode = mode;
1760 Ok(())
1761 }
1762
1763 fn set_text_input(
1767 &mut self,
1768 window: WindowId,
1769 field: Option<TextPurpose>,
1770 ) -> Result<(), ShellError> {
1771 self.window_mut(window)?.text_input = field;
1772 Ok(())
1773 }
1774
1775 #[cfg(feature = "accesskit")]
1776 fn update_accessibility(
1777 &mut self,
1778 window: WindowId,
1779 update: accesskit::TreeUpdate,
1780 ) -> Result<(), ShellError> {
1781 if let Some(updates) = &mut self.window_mut(window)?.accessibility {
1782 updates.push(update);
1783 }
1784 Ok(())
1785 }
1786
1787 fn set_cursor(
1788 &mut self,
1789 window: WindowId,
1790 cursor: Option<CursorIcon>,
1791 ) -> Result<(), ShellError> {
1792 self.window_mut(window)?.cursor = cursor;
1793 Ok(())
1794 }
1795
1796 fn warp_pointer(
1797 &mut self,
1798 window: WindowId,
1799 position: PhysicalPoint,
1800 ) -> Result<(), ShellError> {
1801 if !self.caps.contains(ShellCaps::POINTER_WARP) {
1802 return Err(self.unsupported("pointer warp"));
1803 }
1804 self.check_window(window)?;
1805 let event = ShellEvent::PointerMotion {
1808 window,
1809 device: VIRTUAL_DEVICE,
1810 time: self.now(),
1811 abs: Some(position),
1812 raw_delta: None,
1813 };
1814 self.queue.push_back(event);
1815 Ok(())
1816 }
1817
1818 fn reply_close_request(
1819 &mut self,
1820 window: WindowId,
1821 reply: CloseReply,
1822 ) -> Result<(), ShellError> {
1823 let state = self.window_mut(window)?;
1824 if !state.close_pending {
1825 return Err(ShellError::NoPendingCloseRequest { window });
1826 }
1827 state.close_pending = false;
1828 match reply {
1829 CloseReply::Close => self.destroy_window(window),
1830 CloseReply::Keep => Ok(()),
1831 }
1832 }
1833
1834 fn clipboard_offer(
1845 &mut self,
1846 window: WindowId,
1847 offers: &[ClipboardOffer<'_>],
1848 ) -> Result<(), ShellError> {
1849 if !self.caps.contains(ShellCaps::CLIPBOARD) {
1850 return Err(self.unsupported("clipboard"));
1851 }
1852 self.check_window(window)?;
1853 if self.clipboard_needs_interaction && !self.saw_interaction {
1854 return Err(ShellError::NeedsUserInteraction {
1855 backend: ShellBackend::Headless,
1856 what: "claiming the clipboard",
1857 });
1858 }
1859 self.clipboard = offers
1860 .iter()
1861 .map(|offer| (ReceivedMime::from(offer.mime), offer.bytes.to_vec()))
1862 .collect();
1863 Ok(())
1864 }
1865
1866 fn clipboard_request(
1880 &mut self,
1881 window: WindowId,
1882 mime: MimeType,
1883 ) -> Result<ClipboardRequestId, ShellError> {
1884 if !self.caps.contains(ShellCaps::CLIPBOARD) {
1885 return Err(self.unsupported("clipboard"));
1886 }
1887 self.check_window(window)?;
1888 let request = ClipboardRequestId(self.next_request_id);
1889 self.next_request_id += 1;
1890 if self.answerable() {
1891 let answer = self.answer(window, request, mime);
1892 self.queue.push_back(answer);
1896 } else {
1897 self.reads.push(PendingRead {
1898 window,
1899 request,
1900 mime,
1901 countdown: self.clipboard_deadline,
1902 });
1903 }
1904 Ok(request)
1905 }
1906
1907 fn clipboard_readable(&self, window: WindowId) -> bool {
1913 self.caps.contains(ShellCaps::CLIPBOARD) && self.window(window).is_ok() && self.answerable()
1914 }
1915}
1916
1917#[cfg(test)]
1918mod tests {
1919 use super::*;
1920 use crate::{AspectRatio, LogicalSize};
1921
1922 fn shell_with_window() -> (HeadlessShell, WindowId) {
1924 let mut shell = HeadlessShell::new();
1925 let window = shell
1926 .create_window(&WindowDesc::default())
1927 .expect("headless window creation never fails");
1928 (shell, window)
1929 }
1930
1931 fn shell_with_configured_window() -> (HeadlessShell, WindowId) {
1933 let (mut shell, window) = shell_with_window();
1934 settle(&mut shell);
1935 assert!(shell.window_state(window).expect("state").is_configured());
1936 (shell, window)
1937 }
1938
1939 fn settle(shell: &mut HeadlessShell) {
1941 for _ in 0..8 {
1942 let mut seen = 0;
1943 shell.pump(&mut |_| seen += 1);
1944 if seen == 0 && shell.windows.iter().all(|(_, w)| w.pending.is_none()) {
1945 return;
1946 }
1947 }
1948 panic!("the shell never settled");
1949 }
1950
1951 fn drain(shell: &mut HeadlessShell) -> Vec<ShellEvent> {
1952 let mut events = Vec::new();
1953 shell.pump(&mut |event| events.push(event));
1954 events
1955 }
1956
1957 fn names(events: &[ShellEvent]) -> Vec<&'static str> {
1958 events.iter().map(ShellEvent::name).collect()
1959 }
1960
1961 fn create_swapchain(state: &WindowState) -> Result<PhysicalSize, &'static str> {
1964 let size = state.size().ok_or("window is not configured yet")?;
1965 if size.is_empty() {
1966 return Err("zero-extent swapchain");
1967 }
1968 Ok(size)
1969 }
1970
1971 #[cfg(feature = "accesskit")]
1976 #[test]
1977 fn a_tree_reaches_the_reader_only_while_it_reads() {
1978 use accesskit::{Action, ActionRequest, Node, NodeId, Role, TreeId, TreeInfo, TreeUpdate};
1979
1980 let (mut shell, window) = shell_with_configured_window();
1981 let update = |label: &str| TreeUpdate {
1982 nodes: vec![(NodeId(1), {
1983 let mut node = Node::new(Role::Window);
1984 node.set_label(label);
1985 node
1986 })],
1987 tree: Some(TreeInfo::new(NodeId(1))),
1988 tree_id: TreeId::ROOT,
1989 focus: NodeId(1),
1990 };
1991
1992 shell
1993 .update_accessibility(window, update("unread"))
1994 .expect("update");
1995 assert_eq!(shell.accessibility_updates(window).expect("updates"), None);
1996
1997 shell.activate_accessibility(window).expect("activate");
1998 shell
1999 .activate_accessibility(window)
2000 .expect("activate twice");
2001 shell
2002 .update_accessibility(window, update("read"))
2003 .expect("update");
2004 let click = ActionRequest {
2005 action: Action::Click,
2006 target_tree: TreeId::ROOT,
2007 target_node: NodeId(1),
2008 data: None,
2009 };
2010 shell
2011 .request_accessibility_action(window, click.clone())
2012 .expect("action");
2013 assert_eq!(
2014 shell.accessibility_updates(window).expect("updates"),
2015 Some(&[update("read")][..])
2016 );
2017 let events = drain(&mut shell);
2018 assert_eq!(
2019 names(&events),
2020 ["AccessibilityActivated", "AccessibilityAction"]
2021 );
2022 assert_eq!(
2023 events[1],
2024 ShellEvent::AccessibilityAction {
2025 window,
2026 request: click
2027 }
2028 );
2029
2030 shell.deactivate_accessibility(window).expect("deactivate");
2031 shell
2032 .deactivate_accessibility(window)
2033 .expect("deactivate twice");
2034 shell
2035 .update_accessibility(window, update("gone"))
2036 .expect("update");
2037 assert_eq!(shell.accessibility_updates(window).expect("updates"), None);
2038 assert_eq!(names(&drain(&mut shell)), ["AccessibilityDeactivated"]);
2039 }
2040
2041 #[test]
2044 fn a_new_window_has_no_size_until_a_later_pump() {
2045 let (mut shell, window) = shell_with_window();
2046
2047 let state = shell.window_state(window).expect("state");
2050 assert!(!state.is_configured());
2051 assert_eq!(state.size(), None);
2052 assert_eq!(state.scale_factor(), None);
2053 assert_eq!(state.effective_mode(), None);
2054 assert_eq!(state.requested_mode, DisplayMode::Windowed);
2055
2056 assert_eq!(drain(&mut shell), Vec::new());
2058 assert!(!shell.window_state(window).expect("state").is_configured());
2059
2060 let events = drain(&mut shell);
2062 assert_eq!(
2063 events,
2064 vec![ShellEvent::Resized {
2065 window,
2066 size: PhysicalSize::new(1280, 720),
2067 scale_factor: 1.0,
2068 }]
2069 );
2070 let state = shell.window_state(window).expect("state");
2071 assert_eq!(state.size(), Some(PhysicalSize::new(1280, 720)));
2072 assert_eq!(state.scale_factor(), Some(1.0));
2073 assert_eq!(state.effective_mode(), Some(DisplayMode::Windowed));
2074 assert!(state.mode_request_honoured());
2075 }
2076
2077 #[test]
2081 fn keep_alive_takes_a_turn_and_keeps_everything_for_the_next_pump() {
2082 let (mut shell, window) = shell_with_window();
2083 shell.inject(ShellEvent::CloseRequested { window });
2084
2085 for _ in 0..=DEFAULT_CONFIGURE_DELAY {
2087 shell.keep_alive();
2088 }
2089 assert!(
2090 shell.window_state(window).expect("state").is_configured(),
2091 "every turn the configure waits for was taken by keep_alive"
2092 );
2093
2094 assert_eq!(
2095 drain(&mut shell),
2096 vec![
2097 ShellEvent::CloseRequested { window },
2098 ShellEvent::Resized {
2099 window,
2100 size: PhysicalSize::new(1280, 720),
2101 scale_factor: 1.0,
2102 },
2103 ],
2104 "nothing keep_alive saw was delivered or dropped"
2105 );
2106 assert_eq!(drain(&mut shell), Vec::new(), "and each arrived once");
2107 }
2108
2109 #[test]
2110 fn a_consumer_that_skips_the_wait_gets_a_clean_error_not_a_wrong_extent() {
2111 let (mut shell, window) = shell_with_window();
2112
2113 assert_eq!(
2115 create_swapchain(&shell.window_state(window).expect("state")),
2116 Err("window is not configured yet")
2117 );
2118
2119 let target = shell.surface_target(window).expect("the handle exists");
2125 assert_eq!(target, SurfaceTarget::Offscreen);
2126
2127 let size = loop {
2129 shell.pump(&mut |_| {});
2130 if let Ok(size) = create_swapchain(&shell.window_state(window).expect("state")) {
2131 break size;
2132 }
2133 };
2134 assert_eq!(size, PhysicalSize::new(1280, 720));
2135 assert_eq!(
2138 shell.surface_target(window).expect("target"),
2139 target,
2140 "the target is invariant under a configure"
2141 );
2142 }
2143
2144 #[test]
2145 fn the_configure_delay_is_a_knob() {
2146 let mut shell = HeadlessShell::new().with_configure_delay(3);
2147 let window = shell.create_window(&WindowDesc::default()).expect("window");
2148 for pump in 0..3 {
2149 assert_eq!(drain(&mut shell), Vec::new(), "pump {pump}");
2150 assert!(!shell.window_state(window).expect("state").is_configured());
2151 }
2152 assert_eq!(names(&drain(&mut shell)), ["Resized"]);
2153 assert!(shell.window_state(window).expect("state").is_configured());
2154
2155 let mut shell = HeadlessShell::new().with_configure_delay(0);
2158 let window = shell.create_window(&WindowDesc::default()).expect("window");
2159 assert_eq!(names(&drain(&mut shell)), ["Resized"]);
2160 assert!(shell.window_state(window).expect("state").is_configured());
2161
2162 shell.set_configure_delay(2);
2163 shell
2164 .set_mode(window, DisplayMode::Borderless { monitor: None })
2165 .expect("request");
2166 assert_eq!(drain(&mut shell), Vec::new());
2167 assert_eq!(drain(&mut shell), Vec::new());
2168 assert_eq!(names(&drain(&mut shell)), ["Resized"]);
2169 }
2170
2171 #[test]
2172 fn set_mode_is_a_request_whose_effect_arrives_later() {
2173 let (mut shell, window) = shell_with_configured_window();
2174 shell
2175 .set_mode(window, DisplayMode::Borderless { monitor: None })
2176 .expect("request");
2177
2178 let state = shell.window_state(window).expect("state");
2180 assert_eq!(
2181 state.requested_mode,
2182 DisplayMode::Borderless { monitor: None }
2183 );
2184 assert_eq!(
2185 state.effective_mode(),
2186 Some(DisplayMode::Windowed),
2187 "still windowed until the window system says otherwise"
2188 );
2189 assert!(
2190 !state.mode_request_honoured(),
2191 "a UI toggle reading the request instead of the effect would lie here"
2192 );
2193 assert_eq!(state.size(), Some(PhysicalSize::new(1280, 720)));
2194
2195 settle(&mut shell);
2196 let state = shell.window_state(window).expect("state");
2197 assert_eq!(
2198 state.effective_mode(),
2199 Some(DisplayMode::Borderless { monitor: None })
2200 );
2201 assert!(state.mode_request_honoured());
2202 assert_eq!(state.size(), Some(PhysicalSize::new(1920, 1080)));
2203 }
2204
2205 #[test]
2206 fn a_refused_mode_is_expressible_through_injection() {
2207 let (mut shell, window) = shell_with_configured_window();
2210 shell
2211 .set_mode(window, DisplayMode::Borderless { monitor: None })
2212 .expect("request");
2213 shell
2215 .resize(window, PhysicalSize::new(1280, 720))
2216 .expect("answer");
2217 let state = shell.window_state(window).expect("state");
2218 assert_eq!(state.effective_mode(), Some(DisplayMode::Windowed));
2219 assert!(!state.mode_request_honoured());
2220 }
2221
2222 #[test]
2225 fn a_repositioned_popup_is_configured_at_its_new_size() {
2226 let (mut shell, window) = shell_with_configured_window();
2227 let anchor = crate::LogicalRect::new(
2228 crate::LogicalPoint::new(10.0, 10.0),
2229 LogicalSize::new(100.0, 20.0),
2230 );
2231 let popup = shell
2232 .create_window(&WindowDesc {
2233 size: LogicalSize::new(200.0, 150.0),
2234 role: crate::WindowRole::Popup(crate::PopupDesc {
2235 parent: window,
2236 anchor,
2237 placement: crate::PopupPlacement::Below,
2238 input: crate::PopupInput::Pointer,
2239 }),
2240 ..WindowDesc::default()
2241 })
2242 .expect("a popup");
2243 settle(&mut shell);
2244 let scale = shell
2245 .window_state(popup)
2246 .expect("live")
2247 .scale_factor()
2248 .expect("configured");
2249
2250 let moved = LogicalSize::new(240.0, 90.0);
2251 shell
2252 .reposition_popup(popup, anchor, moved)
2253 .expect("reposition");
2254 settle(&mut shell);
2255 assert_eq!(
2256 shell.window_state(popup).expect("live").size(),
2257 Some(moved.to_physical(scale))
2258 );
2259 assert!(matches!(
2260 shell.reposition_popup(window, anchor, moved),
2261 Err(ShellError::InvalidDescriptor(_))
2262 ));
2263 }
2264
2265 #[test]
2266 fn set_constraints_is_a_request_too() {
2267 let (mut shell, window) = shell_with_configured_window();
2268 let constraints = SizeConstraints::min(LogicalSize::new(1600.0, 900.0));
2269 shell.set_constraints(window, constraints).expect("request");
2270
2271 let state = shell.window_state(window).expect("state");
2272 assert_eq!(state.requested_constraints, constraints);
2273 assert_eq!(
2274 state.size(),
2275 Some(PhysicalSize::new(1280, 720)),
2276 "the size only changes when the window system says so"
2277 );
2278
2279 settle(&mut shell);
2280 assert_eq!(
2281 shell.window_state(window).expect("state").size(),
2282 Some(PhysicalSize::new(1600, 900))
2283 );
2284 }
2285
2286 #[test]
2287 fn constraints_do_not_cancel_a_mode_request_that_has_not_landed() {
2288 let (mut shell, window) = shell_with_configured_window();
2293 shell
2294 .set_mode(window, DisplayMode::Borderless { monitor: None })
2295 .expect("mode request");
2296 shell
2297 .set_constraints(window, SizeConstraints::min(LogicalSize::new(320.0, 180.0)))
2298 .expect("constraint request");
2299 settle(&mut shell);
2300
2301 let state = shell.window_state(window).expect("state");
2302 assert_eq!(
2303 state.effective_mode(),
2304 Some(DisplayMode::Borderless { monitor: None })
2305 );
2306 assert!(state.mode_request_honoured());
2307 }
2308
2309 #[test]
2310 fn a_borderless_window_reports_its_own_monitors_scale() {
2311 let mut shell = HeadlessShell::new();
2316 let hidpi = shell.plug_monitor(MonitorInfo {
2317 id: MonitorId(0),
2318 name: "HEADLESS-2".to_string(),
2319 bounds: PhysicalRect::new(1920, 0, 2560, 1440),
2320 work_area: PhysicalRect::new(1920, 0, 2560, 1440),
2321 scale_factor: 2.0,
2322 refresh_millihertz: 143_998,
2323 is_primary: false,
2324 });
2325 let window = shell
2326 .create_window(&WindowDesc {
2327 mode: DisplayMode::Borderless {
2328 monitor: Some(hidpi),
2329 },
2330 ..WindowDesc::default()
2331 })
2332 .expect("window");
2333 settle(&mut shell);
2334
2335 let state = shell.window_state(window).expect("state");
2336 assert_eq!(state.size(), Some(PhysicalSize::new(2560, 1440)));
2337 assert_eq!(
2338 state.scale_factor(),
2339 Some(2.0),
2340 "the scale is the borderless monitor's, not the primary's"
2341 );
2342 let other = shell
2345 .create_window(&WindowDesc::default())
2346 .expect("second window");
2347 settle(&mut shell);
2348 shell
2349 .set_mode(
2350 other,
2351 DisplayMode::Borderless {
2352 monitor: Some(hidpi),
2353 },
2354 )
2355 .expect("request");
2356 settle(&mut shell);
2357 assert_eq!(
2358 shell.window_state(other).expect("state").scale_factor(),
2359 shell.window_state(window).expect("state").scale_factor()
2360 );
2361 }
2362
2363 #[test]
2364 fn constraints_set_before_the_first_configure_fold_into_it() {
2365 let mut shell = HeadlessShell::new();
2366 let window = shell.create_window(&WindowDesc::default()).expect("window");
2367 shell
2368 .set_constraints(
2369 window,
2370 SizeConstraints::NONE.with_aspect(AspectRatio::SQUARE),
2371 )
2372 .expect("request");
2373 settle(&mut shell);
2374 assert_eq!(
2377 shell.window_state(window).expect("state").size(),
2378 Some(PhysicalSize::new(720, 720))
2379 );
2380 }
2381
2382 #[test]
2383 fn creation_uses_the_requested_size_and_the_monitors_scale() {
2384 let mut shell = HeadlessShell::new();
2385 let window = shell
2386 .create_window(&WindowDesc {
2387 size: LogicalSize::new(800.0, 600.0),
2388 ..WindowDesc::default()
2389 })
2390 .expect("window");
2391 settle(&mut shell);
2392 assert_eq!(
2393 shell.window_state(window).expect("state").size(),
2394 Some(PhysicalSize::new(800, 600))
2395 );
2396
2397 let mut shell = HeadlessShell::new().with_scale_factor(2.0);
2398 let window = shell
2399 .create_window(&WindowDesc {
2400 size: LogicalSize::new(640.0, 360.0),
2401 ..WindowDesc::default()
2402 })
2403 .expect("window");
2404 settle(&mut shell);
2405 let state = shell.window_state(window).expect("state");
2406 assert_eq!(state.size(), Some(PhysicalSize::new(1280, 720)));
2407 assert_eq!(state.scale_factor(), Some(2.0));
2408 }
2409
2410 #[test]
2413 fn scale_changes_preserve_logical_size_and_report_both_facts() {
2414 let (mut shell, window) = shell_with_configured_window();
2415 shell
2416 .change_scale_factor(window, 1.5)
2417 .expect("scale change");
2418 let events = drain(&mut shell);
2419 assert_eq!(
2420 events,
2421 vec![
2422 ShellEvent::ScaleFactorChanged {
2423 window,
2424 scale_factor: 1.5,
2425 size: PhysicalSize::new(1920, 1080),
2426 },
2427 ShellEvent::Resized {
2430 window,
2431 size: PhysicalSize::new(1920, 1080),
2432 scale_factor: 1.5,
2433 },
2434 ]
2435 );
2436 assert_eq!(
2440 shell.window_state(window).expect("state").size(),
2441 Some(PhysicalSize::new(1920, 1080))
2442 );
2443 }
2444
2445 #[test]
2446 fn a_scale_change_scheduled_by_a_mode_switch_arrives_before_the_resize() {
2447 let mut shell = HeadlessShell::new();
2448 let hidpi = shell.plug_monitor(MonitorInfo {
2449 id: MonitorId(0),
2450 name: "HEADLESS-2".to_string(),
2451 bounds: PhysicalRect::new(1920, 0, 2560, 1440),
2452 work_area: PhysicalRect::new(1920, 0, 2560, 1440),
2453 scale_factor: 2.0,
2454 refresh_millihertz: 143_998,
2455 is_primary: false,
2456 });
2457 assert_eq!(hidpi, MonitorId(2), "ids are never reused");
2458 let window = shell.create_window(&WindowDesc::default()).expect("window");
2459 settle(&mut shell);
2460
2461 shell
2462 .set_mode(
2463 window,
2464 DisplayMode::Borderless {
2465 monitor: Some(hidpi),
2466 },
2467 )
2468 .expect("request");
2469 assert_eq!(drain(&mut shell), Vec::new(), "still only a request");
2470 let events = drain(&mut shell);
2471 assert_eq!(names(&events), ["ScaleFactorChanged", "Resized"]);
2472
2473 let state = shell.window_state(window).expect("state");
2474 assert_eq!(state.size(), Some(PhysicalSize::new(2560, 1440)));
2475 assert_eq!(state.scale_factor(), Some(2.0));
2476 assert!(state.effective_mode().expect("configured").is_borderless());
2477 assert_eq!(
2482 shell.surface_target(window).expect("target"),
2483 SurfaceTarget::Offscreen
2484 );
2485
2486 assert!(matches!(
2487 shell.set_mode(
2488 window,
2489 DisplayMode::Borderless {
2490 monitor: Some(MonitorId(99)),
2491 },
2492 ),
2493 Err(ShellError::NoSuchMonitor(99))
2494 ));
2495 }
2496
2497 #[test]
2498 fn resizes_honour_constraints_and_inject_bypasses_them() {
2499 let (mut shell, window) = shell_with_configured_window();
2500 shell
2501 .set_constraints(
2502 window,
2503 SizeConstraints::min(LogicalSize::new(640.0, 360.0))
2504 .with_aspect(AspectRatio::WIDESCREEN),
2505 )
2506 .expect("request");
2507 settle(&mut shell);
2508
2509 shell
2510 .resize(window, PhysicalSize::new(1000, 1000))
2511 .expect("resize");
2512 assert_eq!(
2513 drain(&mut shell),
2514 vec![ShellEvent::Resized {
2515 window,
2516 size: PhysicalSize::new(1000, 562),
2517 scale_factor: 1.0,
2518 }]
2519 );
2520
2521 shell.inject(ShellEvent::Resized {
2524 window,
2525 size: PhysicalSize::new(300, 900),
2526 scale_factor: 1.0,
2527 });
2528 assert_eq!(
2529 drain(&mut shell)[0],
2530 ShellEvent::Resized {
2531 window,
2532 size: PhysicalSize::new(300, 900),
2533 scale_factor: 1.0,
2534 }
2535 );
2536 }
2537
2538 #[test]
2539 fn a_compositor_configure_supersedes_a_pending_one() {
2540 let (mut shell, window) = shell_with_configured_window();
2541 shell
2542 .set_mode(window, DisplayMode::Borderless { monitor: None })
2543 .expect("request");
2544 shell
2546 .resize(window, PhysicalSize::new(640, 480))
2547 .expect("answer");
2548 assert_eq!(names(&drain(&mut shell)), ["Resized"]);
2549 assert_eq!(
2550 shell.window_state(window).expect("state").size(),
2551 Some(PhysicalSize::new(640, 480))
2552 );
2553 assert_eq!(drain(&mut shell), Vec::new());
2555 }
2556
2557 #[test]
2560 fn timestamps_come_from_the_shells_clock_not_from_the_pump() {
2561 let (mut shell, window) = shell_with_configured_window();
2562 shell.set_time(Duration::from_secs(2));
2563 shell.key_press(window, KeyCode::Space).expect("press");
2564 shell.advance(Duration::from_millis(180));
2565 shell.key_release(window, KeyCode::Space).expect("release");
2566
2567 let events = drain(&mut shell);
2568 let held = events[1]
2569 .time()
2570 .expect("input")
2571 .saturating_since(events[0].time().expect("input"));
2572 assert_eq!(held, Duration::from_millis(180));
2573 assert_eq!(events[0].time(), Some(EventTime::from_millis(2_000)));
2574 }
2575
2576 #[test]
2577 fn synthesized_key_fields_are_stable_and_plausible() {
2578 let (mut shell, window) = shell_with_configured_window();
2579 shell.set_modifiers(Modifiers::CTRL);
2580 shell.key_press(window, KeyCode::KeyS).expect("press");
2581 let events = drain(&mut shell);
2582 let ShellEvent::Key {
2583 scancode,
2584 key_code,
2585 keysym,
2586 modifiers,
2587 repeat,
2588 device,
2589 ..
2590 } = events[0].clone()
2591 else {
2592 panic!("wrong variant");
2593 };
2594 assert_eq!(key_code, Some(KeyCode::KeyS));
2595 assert_eq!(keysym, Keysym::from_char('s'));
2596 assert_eq!(modifiers, Modifiers::CTRL);
2597 assert!(!repeat);
2598 assert_eq!(device, VIRTUAL_DEVICE);
2599 assert!(scancode.0 >= 8, "evdev-shaped scancodes start at 8");
2600 assert_eq!(synthetic_keysym(KeyCode::Digit7), Keysym::from_char('7'));
2601 assert_eq!(synthetic_keysym(KeyCode::F5), Keysym::NONE);
2602 assert_eq!(synthetic_keysym(KeyCode::Slash), Keysym::from_char('/'));
2603
2604 shell.key_repeat(window, KeyCode::KeyS).expect("repeat");
2605 let events = drain(&mut shell);
2606 let ShellEvent::Key { repeat, .. } = events[0] else {
2607 panic!("wrong variant");
2608 };
2609 assert!(repeat);
2610 }
2611
2612 #[test]
2613 fn a_locked_pointer_reports_no_absolute_position() {
2614 let (mut shell, window) = shell_with_configured_window();
2615 shell
2616 .set_pointer_mode(window, PointerMode::Locked)
2617 .expect("lock");
2618 shell
2619 .move_pointer(window, PhysicalPoint::new(10.0, 10.0), (3.0, -4.0))
2620 .expect("motion");
2621 let events = drain(&mut shell);
2622 let ShellEvent::PointerMotion { abs, raw_delta, .. } = events[0] else {
2623 panic!("wrong variant");
2624 };
2625 assert_eq!(abs, None);
2626 assert_eq!(raw_delta, Some((3.0, -4.0)));
2627 }
2628
2629 #[test]
2630 fn a_backend_without_raw_motion_reports_none() {
2631 let mut shell = HeadlessShell::new()
2632 .with_caps(ShellCaps::DESKTOP - ShellCaps::RAW_POINTER_MOTION)
2633 .with_configure_delay(0);
2634 let window = shell.create_window(&WindowDesc::default()).expect("window");
2635 drain(&mut shell);
2636 shell
2637 .move_pointer(window, PhysicalPoint::new(1.0, 2.0), (5.0, 5.0))
2638 .expect("motion");
2639 let events = drain(&mut shell);
2640 let ShellEvent::PointerMotion { abs, raw_delta, .. } = events[0] else {
2641 panic!("wrong variant");
2642 };
2643 assert_eq!(abs, Some(PhysicalPoint::new(1.0, 2.0)));
2644 assert_eq!(raw_delta, None, "no relative-motion source");
2645 assert!(!shell.caps().has_mouselook());
2646 }
2647
2648 #[test]
2649 fn pointer_focus_and_scroll_and_text_are_injectable() {
2650 let (mut shell, window) = shell_with_configured_window();
2651 shell
2652 .set_pointer_focus(window, true, Some(PhysicalPoint::new(1.0, 1.0)))
2653 .expect("enter");
2654 shell
2655 .scroll(window, ScrollDelta::Lines { x: 0.0, y: -1.0 }, None)
2656 .expect("scroll");
2657 shell
2658 .button(
2659 window,
2660 PointerButton::Left,
2661 ButtonState::Pressed,
2662 Some(PhysicalPoint::new(1.0, 1.0)),
2663 )
2664 .expect("click");
2665 shell.commit_text(window, "ありがとう").expect("text");
2666 shell
2667 .set_pointer_focus(window, false, Some(PhysicalPoint::new(1.0, 1.0)))
2668 .expect("leave");
2669
2670 let events = drain(&mut shell);
2671 assert_eq!(
2672 names(&events),
2673 [
2674 "PointerFocus",
2675 "Wheel",
2676 "Button",
2677 "TextCommit",
2678 "PointerFocus"
2679 ]
2680 );
2681 let ShellEvent::PointerFocus {
2682 entered, position, ..
2683 } = &events[4]
2684 else {
2685 panic!("wrong variant");
2686 };
2687 assert!(!entered);
2688 assert_eq!(*position, None, "a leave has no position");
2689 assert!(events.iter().all(ShellEvent::is_input));
2690 }
2691
2692 #[test]
2699 fn contacts_are_injectable_and_a_touchless_shell_refuses_them() {
2700 let (mut shell, window) = shell_with_configured_window();
2701 assert!(shell.caps().contains(ShellCaps::TOUCH));
2702 for (contact, phase, x) in [
2703 (1, TouchPhase::Began, 10.0),
2704 (2, TouchPhase::Began, 200.0),
2705 (2, TouchPhase::Moved, 220.0),
2706 (1, TouchPhase::Ended, 10.0),
2707 (2, TouchPhase::Cancelled, 220.0),
2708 ] {
2709 shell
2710 .touch(
2711 window,
2712 ContactId(contact),
2713 phase,
2714 PhysicalPoint::new(x, 5.0),
2715 )
2716 .expect("a contact");
2717 }
2718 let events = drain(&mut shell);
2719 assert_eq!(names(&events), ["Touch"; 5]);
2720 let seen: Vec<(ContactId, TouchPhase, f64)> = events
2721 .iter()
2722 .map(|event| {
2723 let ShellEvent::Touch {
2724 contact,
2725 phase,
2726 position,
2727 ..
2728 } = event
2729 else {
2730 panic!("wrong variant");
2731 };
2732 (*contact, *phase, position.x)
2733 })
2734 .collect();
2735 assert_eq!(
2736 seen,
2737 vec![
2738 (ContactId(1), TouchPhase::Began, 10.0),
2739 (ContactId(2), TouchPhase::Began, 200.0),
2740 (ContactId(2), TouchPhase::Moved, 220.0),
2741 (ContactId(1), TouchPhase::Ended, 10.0),
2742 (ContactId(2), TouchPhase::Cancelled, 220.0),
2743 ]
2744 );
2745 assert!(events.iter().all(ShellEvent::is_input));
2746
2747 let mut touchless = HeadlessShell::new()
2748 .with_caps(ShellCaps::DESKTOP - ShellCaps::TOUCH)
2749 .with_configure_delay(0);
2750 let window = touchless
2751 .create_window(&WindowDesc::default())
2752 .expect("window");
2753 drain(&mut touchless);
2754 assert!(matches!(
2755 touchless.touch(
2756 window,
2757 ContactId(1),
2758 TouchPhase::Began,
2759 PhysicalPoint::ORIGIN
2760 ),
2761 Err(ShellError::Unsupported { .. })
2762 ));
2763 assert!(drain(&mut touchless).is_empty(), "and queued nothing");
2764 }
2765
2766 #[test]
2767 fn warping_produces_motion_like_every_platform_that_has_it() {
2768 let (mut shell, window) = shell_with_configured_window();
2769 shell
2770 .warp_pointer(window, PhysicalPoint::new(320.0, 180.0))
2771 .expect("warp");
2772 let events = drain(&mut shell);
2773 let ShellEvent::PointerMotion { abs, raw_delta, .. } = events[0] else {
2774 panic!("wrong variant");
2775 };
2776 assert_eq!(abs, Some(PhysicalPoint::new(320.0, 180.0)));
2777 assert_eq!(raw_delta, None, "a warp is not physical motion");
2778 }
2779
2780 #[test]
2783 fn capabilities_gate_the_operations_that_need_them() {
2784 let mut shell = HeadlessShell::new().with_caps(ShellCaps::empty());
2785 let window = shell.create_window(&WindowDesc::default()).expect("window");
2786
2787 assert!(matches!(
2788 shell.warp_pointer(window, PhysicalPoint::ORIGIN),
2789 Err(ShellError::Unsupported { .. })
2790 ));
2791 assert!(matches!(
2792 shell.set_pointer_mode(window, PointerMode::Locked),
2793 Err(ShellError::Unsupported { .. })
2794 ));
2795 assert!(matches!(
2796 shell.clipboard_request(window, MimeType::TextUtf8),
2797 Err(ShellError::Unsupported { .. })
2798 ));
2799 shell
2801 .set_pointer_mode(window, PointerMode::Free)
2802 .expect("Free is always available");
2803 assert!(matches!(
2805 shell.create_window(&WindowDesc::default()),
2806 Err(ShellError::Unsupported { .. })
2807 ));
2808 }
2809
2810 #[test]
2811 fn stale_handles_fail_cleanly_on_every_accessor() {
2812 let (mut shell, window) = shell_with_configured_window();
2813 shell.destroy_window(window).expect("destroy");
2814 assert_eq!(
2815 drain(&mut shell),
2816 vec![ShellEvent::WindowDestroyed { window }]
2817 );
2818
2819 assert!(matches!(
2820 shell.window_state(window),
2821 Err(ShellError::InvalidWindow { .. })
2822 ));
2823 assert!(matches!(
2824 shell.surface_target(window),
2825 Err(ShellError::InvalidWindow { .. })
2826 ));
2827 assert!(matches!(
2828 shell.resize(window, PhysicalSize::new(1, 1)),
2829 Err(ShellError::InvalidWindow { .. })
2830 ));
2831 assert!(matches!(
2832 shell.set_title(window, "gone"),
2833 Err(ShellError::InvalidWindow { .. })
2834 ));
2835 assert!(matches!(
2836 shell.destroy_window(window),
2837 Err(ShellError::InvalidWindow { .. })
2838 ));
2839 assert!(matches!(
2840 shell.key_press(window, KeyCode::KeyA),
2841 Err(ShellError::InvalidWindow { .. })
2842 ));
2843 }
2844
2845 #[test]
2846 fn destroying_a_window_cancels_its_pending_configure() {
2847 let (mut shell, window) = shell_with_window();
2850 shell.destroy_window(window).expect("destroy");
2851 assert_eq!(names(&drain(&mut shell)), ["WindowDestroyed"]);
2852 assert_eq!(drain(&mut shell), Vec::new());
2853 assert_eq!(drain(&mut shell), Vec::new());
2854 }
2855
2856 #[test]
2857 fn close_requests_are_questions() {
2858 let (mut shell, window) = shell_with_configured_window();
2859 assert!(matches!(
2860 shell.reply_close_request(window, CloseReply::Close),
2861 Err(ShellError::NoPendingCloseRequest { .. })
2862 ));
2863
2864 shell.request_close(window).expect("request");
2865 assert!(shell.window_state(window).expect("state").close_pending);
2866 shell
2867 .reply_close_request(window, CloseReply::Keep)
2868 .expect("keep");
2869 assert!(!shell.window_state(window).expect("state").close_pending);
2870 assert!(shell.window_state(window).is_ok(), "still open");
2871
2872 shell.request_close(window).expect("request again");
2873 shell
2874 .reply_close_request(window, CloseReply::Close)
2875 .expect("close");
2876 assert!(matches!(
2877 shell.window_state(window),
2878 Err(ShellError::InvalidWindow { .. })
2879 ));
2880 }
2881
2882 #[test]
2883 fn monitor_hotplug_reports_a_change_and_keeps_ids_unique() {
2884 let mut shell = HeadlessShell::new();
2885 let extra = shell.plug_monitor(MonitorInfo {
2886 id: MonitorId(0),
2887 name: "HEADLESS-2".to_string(),
2888 bounds: PhysicalRect::new(1920, 0, 1920, 1080),
2889 work_area: PhysicalRect::new(1920, 0, 1920, 1080),
2890 scale_factor: 1.0,
2891 refresh_millihertz: 60_000,
2892 is_primary: false,
2893 });
2894 assert_eq!(shell.monitors().len(), 2);
2895 assert_eq!(drain(&mut shell), vec![ShellEvent::MonitorsChanged]);
2896
2897 shell.unplug_monitor(extra).expect("unplug");
2898 assert_eq!(shell.monitors().len(), 1);
2899 assert_eq!(
2900 drain(&mut shell),
2901 vec![
2902 ShellEvent::MonitorRemoved { monitor: extra },
2903 ShellEvent::MonitorsChanged
2904 ]
2905 );
2906 assert!(matches!(
2907 shell.unplug_monitor(extra),
2908 Err(ShellError::NoSuchMonitor(_))
2909 ));
2910 assert_eq!(
2911 shell.monitor(MonitorId(1)).map(|m| m.name.as_str()),
2912 Some("HEADLESS-1")
2913 );
2914 assert!(shell.monitor(extra).is_none());
2915 }
2916
2917 #[test]
2918 fn unplugging_a_monitor_asks_its_panels_and_only_its_panels_to_close() {
2919 let mut shell = HeadlessShell::new();
2920 let second = shell.plug_monitor(MonitorInfo {
2921 id: MonitorId(0),
2922 name: "HEADLESS-2".to_string(),
2923 bounds: PhysicalRect::new(1920, 0, 1920, 1080),
2924 work_area: PhysicalRect::new(1920, 0, 1920, 1080),
2925 scale_factor: 1.0,
2926 refresh_millihertz: 60_000,
2927 is_primary: false,
2928 });
2929 let first = shell.monitors()[0].id;
2930 let panel_on = |shell: &mut HeadlessShell, monitor| {
2931 shell
2932 .create_window(&WindowDesc {
2933 role: crate::WindowRole::Panel(crate::PanelDesc {
2934 monitor,
2935 ..crate::PanelDesc::default()
2936 }),
2937 ..WindowDesc::default()
2938 })
2939 .expect("panel")
2940 };
2941 let doomed = panel_on(&mut shell, Some(second));
2942 let already_asked = panel_on(&mut shell, Some(second));
2943 let elsewhere = panel_on(&mut shell, Some(first));
2944 let anywhere = panel_on(&mut shell, None);
2945 let toplevel = shell.create_window(&WindowDesc::default()).expect("window");
2946 shell.request_close(already_asked).expect("close");
2947 settle(&mut shell);
2948 drain(&mut shell);
2949
2950 shell.unplug_monitor(second).expect("unplug");
2951 assert_eq!(
2952 drain(&mut shell),
2953 vec![
2954 ShellEvent::MonitorRemoved { monitor: second },
2955 ShellEvent::CloseRequested { window: doomed },
2956 ShellEvent::MonitorsChanged,
2957 ]
2958 );
2959 for window in [elsewhere, anywhere, toplevel] {
2960 assert!(!shell.window_state(window).expect("open").close_pending);
2961 }
2962 }
2963
2964 #[test]
2965 fn drops_require_both_the_capability_and_the_opt_in() {
2966 let mut shell = HeadlessShell::new();
2967 let plain = shell.create_window(&WindowDesc::default()).expect("window");
2968 assert!(matches!(
2969 shell.drop_file(plain, "/tmp/a.gltf", None),
2970 Err(ShellError::Unsupported { .. })
2971 ));
2972
2973 let dropper = shell
2974 .create_window(&WindowDesc {
2975 accept_drops: true,
2976 ..WindowDesc::default()
2977 })
2978 .expect("window");
2979 settle(&mut shell);
2980 shell
2981 .drop_file(dropper, "/tmp/a.gltf", Some(PhysicalPoint::new(4.0, 5.0)))
2982 .expect("drop");
2983 assert_eq!(names(&drain(&mut shell))[0], "DroppedFile");
2984 }
2985
2986 #[test]
2987 fn a_sink_that_queues_more_events_does_not_spin_the_pump() {
2988 let (mut shell, window) = shell_with_configured_window();
2989 shell.inject(ShellEvent::MonitorsChanged);
2990 let mut extra = Vec::new();
2991 shell.pump(&mut |event| extra.push(event));
2992 assert_eq!(extra.len(), 1);
2993 shell.inject(ShellEvent::CloseRequested { window });
2994 assert_eq!(shell.pending_events(), 1);
2995 }
2996
2997 #[test]
2998 fn accessors_that_no_real_backend_can_provide_are_headless_only() {
2999 let (mut shell, window) = shell_with_configured_window();
3000 shell.set_title(window, "renamed").expect("title");
3001 assert_eq!(shell.title(window).expect("title"), "renamed");
3002 shell.set_cursor(window, None).expect("hide");
3003 assert_eq!(shell.cursor(window).expect("cursor"), None);
3004 shell
3005 .set_cursor(window, Some(CursorIcon::Crosshair))
3006 .expect("shape");
3007 assert_eq!(
3008 shell.cursor(window).expect("cursor"),
3009 Some(CursorIcon::Crosshair)
3010 );
3011 shell.set_visible(window, false).expect("hide window");
3012 assert!(!shell.window_state(window).expect("state").visible);
3013 }
3014
3015 #[test]
3016 fn waiting_is_observable_and_focus_transitions_are_events() {
3017 let (mut shell, window) = shell_with_configured_window();
3018 assert_eq!(shell.wait_count(), 0);
3019 shell.wait_events(Some(Duration::from_millis(5)));
3020 shell.wait_events(None);
3021 assert_eq!(shell.wait_count(), 2);
3022
3023 shell.set_focus(window, true).expect("focus");
3024 assert!(shell.window_state(window).expect("state").focused);
3025 shell.set_focus(window, false).expect("unfocus");
3026 assert_eq!(names(&drain(&mut shell)), ["Focus", "Focus"]);
3027 }
3028
3029 const LONG_WAIT: Duration = Duration::from_secs(60);
3034 const RING_AFTER: Duration = Duration::from_millis(50);
3036
3037 fn ring_later(waker: ShellWaker) -> std::thread::JoinHandle<()> {
3039 std::thread::spawn(move || {
3040 std::thread::sleep(RING_AFTER);
3041 waker.wake();
3042 })
3043 }
3044
3045 #[test]
3046 fn a_wake_from_another_thread_ends_a_long_wait() {
3047 let mut shell = HeadlessShell::new();
3048 let ringer = ring_later(shell.waker().expect("headless has a waker"));
3049 let start = std::time::Instant::now();
3050 shell.wait_events(Some(LONG_WAIT));
3051 let slept = start.elapsed();
3052 ringer.join().expect("the ringer");
3053 assert!(slept < LONG_WAIT / 2, "the wait ran {slept:?}");
3054 }
3055
3056 #[test]
3057 fn a_wake_before_the_wait_ends_it_at_once_and_wakes_coalesce() {
3058 let mut shell = HeadlessShell::new();
3059 let waker = shell.waker().expect("headless has a waker");
3060 for _ in 0..3 {
3061 waker.wake();
3062 }
3063 let start = std::time::Instant::now();
3064 shell.wait_events(Some(LONG_WAIT));
3065 assert!(
3066 start.elapsed() < LONG_WAIT / 2,
3067 "a rung bell must end the wait"
3068 );
3069
3070 let timeout = Duration::from_millis(100);
3072 let start = std::time::Instant::now();
3073 shell.wait_events(Some(timeout));
3074 assert!(
3075 start.elapsed() >= timeout / 2,
3076 "the bell was cleared, so this wait must sleep, but it took {:?}",
3077 start.elapsed()
3078 );
3079 }
3080
3081 #[test]
3082 fn with_a_waker_out_an_untimed_wait_sleeps_until_it_rings() {
3083 let mut shell = HeadlessShell::new();
3084 let ringer = ring_later(shell.waker().expect("headless has a waker"));
3085 let start = std::time::Instant::now();
3086 shell.wait_events(None);
3087 let slept = start.elapsed();
3088 ringer.join().expect("the ringer");
3089 assert!(
3090 slept >= RING_AFTER / 2,
3091 "an untimed wait returned after {slept:?}, before anything rang"
3092 );
3093 }
3094
3095 #[test]
3098 fn the_clipboard_round_trips_through_an_event() {
3099 let (mut shell, window) = shell_with_configured_window();
3100 shell
3101 .clipboard_offer(
3102 window,
3103 &[
3104 ClipboardOffer::text("node"),
3105 ClipboardOffer::ron("(kind:\"node\")"),
3106 ],
3107 )
3108 .expect("offer");
3109 assert_eq!(
3110 shell.clipboard_bytes(MimeType::TextUtf8),
3111 Some(&b"node"[..])
3112 );
3113
3114 let request = shell
3115 .clipboard_request(window, MimeType::CrcblRon)
3116 .expect("request");
3117 assert_eq!(
3118 drain(&mut shell)[0],
3119 ShellEvent::ClipboardData {
3120 window,
3121 request,
3122 mime: ReceivedMime::from(MimeType::CrcblRon),
3123 content: ClipboardContent::Bytes(b"(kind:\"node\")".to_vec()),
3124 }
3125 );
3126
3127 let request = shell
3130 .clipboard_request(window, MimeType::UriList)
3131 .expect("request");
3132 assert_eq!(
3133 drain(&mut shell)[0],
3134 ShellEvent::ClipboardData {
3135 window,
3136 request,
3137 mime: ReceivedMime::from(MimeType::UriList),
3138 content: ClipboardContent::Empty,
3139 }
3140 );
3141
3142 shell
3145 .clipboard_offer(window, &[ClipboardOffer::text("")])
3146 .expect("offer");
3147 let request = shell
3148 .clipboard_request(window, MimeType::TextUtf8)
3149 .expect("request");
3150 assert_eq!(
3151 drain(&mut shell)[0],
3152 ShellEvent::ClipboardData {
3153 window,
3154 request,
3155 mime: ReceivedMime::from(MimeType::TextUtf8),
3156 content: ClipboardContent::Bytes(Vec::new()),
3157 }
3158 );
3159 }
3160
3161 #[test]
3162 fn a_read_taken_while_the_clipboard_is_unreadable_is_held_not_answered_empty() {
3163 let (mut shell, window) = shell_with_configured_window();
3169 shell
3170 .clipboard_offer(window, &[ClipboardOffer::text("published")])
3171 .expect("offer");
3172 shell.set_clipboard_readable(false);
3173 assert!(!shell.clipboard_readable(window));
3174
3175 let request = shell
3176 .clipboard_request(window, MimeType::TextUtf8)
3177 .expect("a read is accepted even when it cannot be answered yet");
3178 assert_eq!(shell.held_clipboard_reads(), 1);
3179 assert!(
3180 drain(&mut shell).is_empty(),
3181 "a held read produces no event at all — least of all an empty one"
3182 );
3183
3184 shell.set_clipboard_readable(true);
3186 let events = drain(&mut shell);
3187 assert_eq!(events.len(), 1);
3188 assert_eq!(
3189 events[0],
3190 ShellEvent::ClipboardData {
3191 window,
3192 request,
3193 mime: ReceivedMime::from(MimeType::TextUtf8),
3194 content: ClipboardContent::Bytes(b"published".to_vec()),
3195 }
3196 );
3197 assert_eq!(shell.held_clipboard_reads(), 0);
3198 }
3199
3200 #[test]
3201 fn a_read_that_never_becomes_answerable_ends_as_unavailable() {
3202 let (mut shell, window) = shell_with_configured_window();
3206 shell
3207 .clipboard_offer(window, &[ClipboardOffer::text("unreachable")])
3208 .expect("offer");
3209 shell.set_clipboard_readable(false);
3210 shell.clipboard_deadline(2);
3211
3212 let request = shell
3213 .clipboard_request(window, MimeType::TextUtf8)
3214 .expect("request");
3215 assert!(drain(&mut shell).is_empty(), "pump 1: still held");
3216 assert!(drain(&mut shell).is_empty(), "pump 2: still held");
3217 let events = drain(&mut shell);
3218 assert_eq!(
3219 events[0],
3220 ShellEvent::ClipboardData {
3221 window,
3222 request,
3223 mime: ReceivedMime::from(MimeType::TextUtf8),
3224 content: ClipboardContent::Unavailable,
3227 }
3228 );
3229 assert_eq!(shell.held_clipboard_reads(), 0);
3230 }
3231
3232 #[test]
3233 fn a_peer_that_holds_the_clipboard_and_never_delivers_is_unavailable() {
3234 let (mut shell, window) = shell_with_configured_window();
3238 shell.set_foreign_clipboard(vec![(
3239 ReceivedMime::new("text/plain"),
3240 b"never sent".to_vec(),
3241 )]);
3242 shell.set_clipboard_stalled(true);
3243 shell.clipboard_deadline(1);
3244 assert!(
3245 !shell.clipboard_readable(window),
3246 "a clipboard whose owner will not answer is not readable"
3247 );
3248
3249 let request = shell
3250 .clipboard_request(window, MimeType::TextUtf8)
3251 .expect("request");
3252 assert!(drain(&mut shell).is_empty());
3253 let events = drain(&mut shell);
3254 let ShellEvent::ClipboardData {
3255 request: answered,
3256 content,
3257 ..
3258 } = &events[0]
3259 else {
3260 panic!("wrong variant");
3261 };
3262 assert_eq!(*answered, request);
3263 assert_eq!(*content, ClipboardContent::Unavailable);
3264
3265 shell.set_clipboard_stalled(false);
3267 let request = shell
3268 .clipboard_request(window, MimeType::TextUtf8)
3269 .expect("request");
3270 assert_eq!(
3271 drain(&mut shell)[0],
3272 ShellEvent::ClipboardData {
3273 window,
3274 request,
3275 mime: ReceivedMime::new("text/plain"),
3276 content: ClipboardContent::Bytes(b"never sent".to_vec()),
3277 }
3278 );
3279 }
3280
3281 #[test]
3282 fn claiming_the_clipboard_can_require_a_user_interaction() {
3283 let (mut shell, window) = shell_with_configured_window();
3287 shell.require_user_interaction(true);
3288
3289 let refused = shell.clipboard_offer(window, &[ClipboardOffer::text("stolen")]);
3290 assert!(
3291 matches!(
3292 refused,
3293 Err(ShellError::NeedsUserInteraction {
3294 backend: ShellBackend::Headless,
3295 ..
3296 })
3297 ),
3298 "{refused:?}"
3299 );
3300 assert!(
3301 shell.clipboard_bytes(MimeType::TextUtf8).is_none(),
3302 "a refused claim must not have taken the clipboard anyway"
3303 );
3304
3305 shell.key_press(window, KeyCode::KeyC).expect("key");
3308 shell
3309 .clipboard_offer(window, &[ClipboardOffer::text("copied")])
3310 .expect("with a serial in hand, the claim goes through");
3311 assert_eq!(
3312 shell.clipboard_bytes(MimeType::TextUtf8),
3313 Some(&b"copied"[..])
3314 );
3315
3316 shell.clear_user_interaction();
3318 assert!(matches!(
3319 shell.clipboard_offer(window, &[ClipboardOffer::text("later")]),
3320 Err(ShellError::NeedsUserInteraction { .. })
3321 ));
3322 }
3323
3324 #[test]
3325 fn a_held_read_for_a_destroyed_window_is_dropped_rather_than_answered() {
3326 let (mut shell, window) = shell_with_configured_window();
3329 shell.set_clipboard_readable(false);
3330 let _request = shell
3331 .clipboard_request(window, MimeType::TextUtf8)
3332 .expect("request");
3333 assert_eq!(shell.held_clipboard_reads(), 1);
3334
3335 shell.destroy_window(window).expect("destroy");
3336 let events = drain(&mut shell);
3337 assert_eq!(shell.held_clipboard_reads(), 0);
3338 assert!(
3339 !events
3340 .iter()
3341 .any(|event| matches!(event, ShellEvent::ClipboardData { .. })),
3342 "no clipboard answer may name a destroyed window: {events:?}"
3343 );
3344 }
3345
3346 #[test]
3347 fn a_paste_from_a_foreign_application_keeps_the_foreign_spelling() {
3348 let (mut shell, window) = shell_with_configured_window();
3349 shell.set_foreign_clipboard(vec![
3350 (ReceivedMime::new("text/plain"), b"from gedit".to_vec()),
3353 (
3354 ReceivedMime::new("application/vnd.some-editor.scene+json"),
3355 b"{}".to_vec(),
3356 ),
3357 ]);
3358
3359 let request = shell
3361 .clipboard_request(window, MimeType::TextUtf8)
3362 .expect("request");
3363 let events = drain(&mut shell);
3364 let ShellEvent::ClipboardData {
3365 request: answered,
3366 mime,
3367 content,
3368 ..
3369 } = &events[0]
3370 else {
3371 panic!("wrong variant");
3372 };
3373 assert_eq!(*answered, request);
3374 assert_eq!(content.bytes(), Some(&b"from gedit"[..]));
3375 assert_eq!(mime.as_str(), "text/plain");
3377 assert!(mime.matches(MimeType::TextUtf8));
3378 assert_ne!(*mime, ReceivedMime::from(MimeType::TextUtf8));
3379
3380 assert_eq!(
3383 shell.clipboard_bytes(MimeType::Other("application/vnd.some-editor.scene+json")),
3384 Some(&b"{}"[..])
3385 );
3386 }
3387
3388 #[test]
3391 #[should_panic(expected = "a shell needs at least one monitor")]
3392 fn a_shell_with_no_monitors_is_rejected() {
3393 let _ = HeadlessShell::new().with_monitors(Vec::new());
3394 }
3395
3396 #[test]
3397 #[should_panic(expected = "a text commit is never empty")]
3398 fn an_empty_text_commit_is_rejected() {
3399 let (mut shell, window) = shell_with_configured_window();
3400 let _ = shell.commit_text(window, "");
3401 }
3402}