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 outputs_powered: bool,
347}
348
349#[derive(Debug, Default)]
352struct HeadlessWake {
353 rung: Mutex<bool>,
354 bell: Condvar,
355}
356
357impl HeadlessWake {
358 fn wait(&self, timeout: Option<Duration>) {
361 let rung = self.rung.lock().unwrap_or_else(PoisonError::into_inner);
362 let mut rung = match timeout {
363 Some(timeout) => {
364 self.bell
365 .wait_timeout_while(rung, timeout, |rung| !*rung)
366 .unwrap_or_else(PoisonError::into_inner)
367 .0
368 }
369 None => self
370 .bell
371 .wait_while(rung, |rung| !*rung)
372 .unwrap_or_else(PoisonError::into_inner),
373 };
374 *rung = false;
375 }
376}
377
378impl WakeSignal for HeadlessWake {
379 fn wake(&self) {
380 *self.rung.lock().unwrap_or_else(PoisonError::into_inner) = true;
381 self.bell.notify_all();
382 }
383}
384
385impl Default for HeadlessShell {
386 fn default() -> Self {
387 Self::new()
388 }
389}
390
391impl HeadlessShell {
392 #[must_use]
394 pub fn new() -> Self {
395 Self {
396 caps: ShellCaps::DESKTOP,
397 monitors: vec![MonitorInfo {
398 id: MonitorId(1),
399 name: "HEADLESS-1".to_string(),
400 bounds: PhysicalRect::new(
401 0,
402 0,
403 DEFAULT_MONITOR_SIZE.width,
404 DEFAULT_MONITOR_SIZE.height,
405 ),
406 work_area: PhysicalRect::new(
407 0,
408 0,
409 DEFAULT_MONITOR_SIZE.width,
410 DEFAULT_MONITOR_SIZE.height,
411 ),
412 scale_factor: 1.0,
413 refresh_millihertz: DEFAULT_REFRESH_MILLIHERTZ,
414 is_primary: true,
415 }],
416 next_monitor_id: 2,
417 windows: Pool::new(),
418 queue: VecDeque::new(),
419 clock: ManualTime::new(),
420 modifiers: Modifiers::empty(),
421 clipboard: Vec::new(),
422 next_request_id: 1,
423 reads: Vec::new(),
424 clipboard_readable: true,
425 clipboard_stalled: false,
426 clipboard_needs_interaction: false,
427 saw_interaction: false,
428 clipboard_deadline: DEFAULT_CLIPBOARD_DEADLINE,
429 waits: 0,
430 configure_delay: DEFAULT_CONFIGURE_DELAY,
431 wake: None,
432 frames: FrameRequests::default(),
433 outputs_powered: true,
434 }
435 }
436
437 #[must_use]
442 pub fn with_caps(mut self, caps: ShellCaps) -> Self {
443 self.caps = caps;
444 self
445 }
446
447 #[must_use]
462 pub fn with_monitors(mut self, monitors: Vec<MonitorInfo>) -> Self {
463 assert!(!monitors.is_empty(), "a shell needs at least one monitor");
464 for (index, monitor) in monitors.iter().enumerate() {
465 assert!(
466 monitor.id.0 != u32::MAX,
467 "monitor id {} leaves no id for the next hotplug",
468 monitor.id.0
469 );
470 assert!(
471 !monitors[..index].iter().any(|seen| seen.id == monitor.id),
472 "monitor id {} appears twice",
473 monitor.id.0
474 );
475 }
476 self.next_monitor_id = monitors
477 .iter()
478 .map(|monitor| monitor.id.0 + 1)
479 .max()
480 .unwrap_or(1);
481 self.monitors = monitors;
482 self
483 }
484
485 #[must_use]
490 pub fn with_scale_factor(mut self, scale_factor: f64) -> Self {
491 assert!(scale_factor > 0.0, "scale factor must be positive");
492 self.monitors[0].scale_factor = scale_factor;
493 self
494 }
495
496 #[must_use]
501 pub const fn with_configure_delay(mut self, pumps: u32) -> Self {
502 self.configure_delay = pumps;
503 self
504 }
505
506 pub const fn set_configure_delay(&mut self, pumps: u32) {
516 self.configure_delay = pumps;
517 }
518
519 pub fn set_caps(&mut self, caps: ShellCaps) {
525 self.caps = caps;
526 }
527
528 #[must_use]
530 pub fn now(&self) -> EventTime {
531 EventTime::from_duration(self.clock.elapsed())
532 }
533
534 pub fn advance(&mut self, delta: Duration) {
540 self.clock.advance(delta);
541 }
542
543 pub fn set_time(&mut self, now: Duration) {
551 self.clock.set(now);
552 }
553
554 pub fn set_modifiers(&mut self, modifiers: Modifiers) {
559 self.modifiers = modifiers;
560 }
561
562 #[must_use]
564 pub fn pending_events(&self) -> usize {
565 self.queue.len()
566 }
567
568 #[must_use]
574 pub const fn wait_count(&self) -> u32 {
575 self.waits
576 }
577
578 pub fn inject(&mut self, event: ShellEvent) {
586 self.queue.push_back(event);
587 }
588
589 pub fn resize(&mut self, window: WindowId, size: PhysicalSize) -> Result<(), ShellError> {
605 let state = self.window_mut(window)?;
606 let scale_factor = state.scale_factor();
607 let mode = state.settled_mode();
608 let constrained = state.requested_constraints.apply(size, scale_factor);
609 state.pending = None;
610 state.configuration = Some(WindowConfiguration {
611 size: constrained,
612 scale_factor,
613 mode,
614 });
615 self.queue.push_back(ShellEvent::Resized {
616 window,
617 size: constrained,
618 scale_factor,
619 });
620 Ok(())
621 }
622
623 pub fn change_scale_factor(
645 &mut self,
646 window: WindowId,
647 scale_factor: f64,
648 ) -> Result<(), ShellError> {
649 assert!(scale_factor > 0.0, "scale factor must be positive");
650 let state = self.window_mut(window)?;
651 let logical = state.base_size().to_logical(state.scale_factor());
652 let mode = state.settled_mode();
653 let size = state
654 .requested_constraints
655 .apply(logical.to_physical(scale_factor), scale_factor);
656 state.pending = None;
657 state.configuration = Some(WindowConfiguration {
658 size,
659 scale_factor,
660 mode,
661 });
662 self.queue.push_back(ShellEvent::ScaleFactorChanged {
663 window,
664 scale_factor,
665 size,
666 });
667 self.queue.push_back(ShellEvent::Resized {
668 window,
669 size,
670 scale_factor,
671 });
672 Ok(())
673 }
674
675 pub fn set_focus(&mut self, window: WindowId, focused: bool) -> Result<(), ShellError> {
681 self.window_mut(window)?.focused = focused;
682 self.queue.push_back(ShellEvent::Focus { window, focused });
683 Ok(())
684 }
685
686 pub fn request_close(&mut self, window: WindowId) -> Result<(), ShellError> {
695 self.window_mut(window)?.close_pending = true;
696 self.queue.push_back(ShellEvent::CloseRequested { window });
697 Ok(())
698 }
699
700 pub fn key(
713 &mut self,
714 window: WindowId,
715 key: KeyCode,
716 state: ButtonState,
717 ) -> Result<(), ShellError> {
718 self.key_with(window, key, state, false)
719 }
720
721 pub fn key_press(&mut self, window: WindowId, key: KeyCode) -> Result<(), ShellError> {
727 self.key(window, key, ButtonState::Pressed)
728 }
729
730 pub fn key_release(&mut self, window: WindowId, key: KeyCode) -> Result<(), ShellError> {
736 self.key(window, key, ButtonState::Released)
737 }
738
739 pub fn key_repeat(&mut self, window: WindowId, key: KeyCode) -> Result<(), ShellError> {
746 self.key_with(window, key, ButtonState::Pressed, true)
747 }
748
749 fn key_with(
750 &mut self,
751 window: WindowId,
752 key: KeyCode,
753 state: ButtonState,
754 repeat: bool,
755 ) -> Result<(), ShellError> {
756 let popup_input = self.window(window)?.popup_input;
757 self.saw_interaction = true;
758 let event = ShellEvent::Key {
759 window,
760 device: VIRTUAL_DEVICE,
761 time: self.now(),
762 scancode: synthetic_scancode(key),
763 key_code: Some(key),
764 keysym: synthetic_keysym(key),
765 state,
766 repeat,
767 modifiers: self.modifiers,
768 };
769 let dismissed = crate::role::escape_dismisses(popup_input, &event);
770 self.queue.push_back(event);
771 if dismissed && !self.window(window)?.close_pending {
773 self.request_close(window)?;
774 }
775 Ok(())
776 }
777
778 pub fn move_pointer(
789 &mut self,
790 window: WindowId,
791 position: PhysicalPoint,
792 raw_delta: (f64, f64),
793 ) -> Result<(), ShellError> {
794 let locked = self.window(window)?.pointer_mode == PointerMode::Locked;
795 let raw = if self.caps.contains(ShellCaps::RAW_POINTER_MOTION) {
796 Some(raw_delta)
797 } else {
798 None
799 };
800 self.saw_interaction = true;
801 let event = ShellEvent::PointerMotion {
802 window,
803 device: VIRTUAL_DEVICE,
804 time: self.now(),
805 abs: if locked { None } else { Some(position) },
806 raw_delta: raw,
807 };
808 self.queue.push_back(event);
809 Ok(())
810 }
811
812 pub fn set_pointer_focus(
818 &mut self,
819 window: WindowId,
820 entered: bool,
821 position: Option<PhysicalPoint>,
822 ) -> Result<(), ShellError> {
823 self.check_window(window)?;
824 let event = ShellEvent::PointerFocus {
825 window,
826 device: VIRTUAL_DEVICE,
827 time: self.now(),
828 entered,
829 position: if entered { position } else { None },
830 };
831 self.queue.push_back(event);
832 Ok(())
833 }
834
835 pub fn button(
841 &mut self,
842 window: WindowId,
843 button: PointerButton,
844 state: ButtonState,
845 position: Option<PhysicalPoint>,
846 ) -> Result<(), ShellError> {
847 self.check_window(window)?;
848 self.saw_interaction = true;
849 let event = ShellEvent::Button {
850 window,
851 device: VIRTUAL_DEVICE,
852 time: self.now(),
853 button,
854 state,
855 position,
856 modifiers: self.modifiers,
857 };
858 self.queue.push_back(event);
859 Ok(())
860 }
861
862 pub fn scroll(
868 &mut self,
869 window: WindowId,
870 delta: ScrollDelta,
871 position: Option<PhysicalPoint>,
872 ) -> Result<(), ShellError> {
873 self.check_window(window)?;
874 self.saw_interaction = true;
875 let event = ShellEvent::Wheel {
876 window,
877 device: VIRTUAL_DEVICE,
878 time: self.now(),
879 delta,
880 position,
881 modifiers: self.modifiers,
882 };
883 self.queue.push_back(event);
884 Ok(())
885 }
886
887 pub fn touch(
915 &mut self,
916 window: WindowId,
917 contact: ContactId,
918 phase: TouchPhase,
919 position: PhysicalPoint,
920 ) -> Result<(), ShellError> {
921 self.check_window(window)?;
922 if !self.caps.contains(ShellCaps::TOUCH) {
923 return Err(self.unsupported("touch"));
924 }
925 self.saw_interaction = true;
926 let event = ShellEvent::Touch {
927 window,
928 device: VIRTUAL_DEVICE,
929 time: self.now(),
930 contact,
931 phase,
932 position,
933 };
934 self.queue.push_back(event);
935 Ok(())
936 }
937
938 pub fn commit_text(&mut self, window: WindowId, text: &str) -> Result<(), ShellError> {
949 assert!(!text.is_empty(), "a text commit is never empty");
950 self.check_window(window)?;
951 let event = ShellEvent::TextCommit {
952 window,
953 time: self.now(),
954 text: text.to_string(),
955 };
956 self.queue.push_back(event);
957 Ok(())
958 }
959
960 pub fn drop_file(
968 &mut self,
969 window: WindowId,
970 path: impl Into<PathBuf>,
971 position: Option<PhysicalPoint>,
972 ) -> Result<(), ShellError> {
973 if !self.caps.contains(ShellCaps::DRAG_DROP) {
974 return Err(self.unsupported("drag and drop"));
975 }
976 if !self.window(window)?.accept_drops {
977 return Err(ShellError::Unsupported {
978 backend: ShellBackend::Headless,
979 what: "drops on a window created without accept_drops",
980 });
981 }
982 let event = ShellEvent::DroppedFile {
983 window,
984 time: self.now(),
985 path: path.into(),
986 position,
987 };
988 self.queue.push_back(event);
989 Ok(())
990 }
991
992 pub fn plug_monitor(&mut self, mut info: MonitorInfo) -> MonitorId {
999 let id = MonitorId(self.next_monitor_id);
1000 self.next_monitor_id += 1;
1001 info.id = id;
1002 self.monitors.push(info);
1003 self.queue.push_back(ShellEvent::MonitorsChanged);
1004 id
1005 }
1006
1007 pub fn unplug_monitor(&mut self, id: MonitorId) -> Result<(), ShellError> {
1021 let index = self
1022 .monitors
1023 .iter()
1024 .position(|monitor| monitor.id == id)
1025 .ok_or(ShellError::NoSuchMonitor(id.0))?;
1026 assert!(
1027 self.monitors.len() > 1,
1028 "a shell needs at least one monitor"
1029 );
1030 let previous = self.monitors.clone();
1031 self.monitors.remove(index);
1032 crate::monitor::queue_removed(&previous, &self.monitors, &mut self.queue);
1033 let panels: Vec<WindowId> = self
1034 .windows
1035 .iter()
1036 .filter(|(_, state)| {
1037 state.panel.as_ref().and_then(|panel| panel.desc().monitor) == Some(id)
1038 && !state.close_pending
1039 })
1040 .map(|(window, _)| window.cast())
1041 .collect();
1042 for panel in panels {
1043 self.request_close(panel)?;
1044 }
1045 self.queue.push_back(ShellEvent::MonitorsChanged);
1046 Ok(())
1047 }
1048
1049 pub fn title(&self, window: WindowId) -> Result<&str, ShellError> {
1060 Ok(self.window(window)?.title.as_str())
1061 }
1062
1063 pub fn cursor(&self, window: WindowId) -> Result<Option<CursorIcon>, ShellError> {
1071 Ok(self.window(window)?.cursor)
1072 }
1073
1074 pub fn text_input(&self, window: WindowId) -> Result<Option<TextPurpose>, ShellError> {
1083 Ok(self.window(window)?.text_input)
1084 }
1085
1086 pub fn panel(&self, window: WindowId) -> Result<Option<crate::PanelDesc<'_>>, ShellError> {
1095 Ok(self
1096 .window(window)?
1097 .panel
1098 .as_ref()
1099 .map(crate::role::KeptPanel::desc))
1100 }
1101
1102 pub fn input_region(&self, window: WindowId) -> Result<Option<&[PhysicalRect]>, ShellError> {
1113 Ok(self.window(window)?.input_region.as_deref())
1114 }
1115
1116 pub fn frame_requested(&self, window: WindowId) -> Result<bool, ShellError> {
1126 self.window(window)?;
1127 Ok(self.frames.contains(window))
1128 }
1129
1130 pub fn set_outputs_powered(&mut self, powered: bool) {
1146 self.outputs_powered = powered;
1147 }
1148
1149 #[cfg(feature = "accesskit")]
1158 pub fn activate_accessibility(&mut self, window: WindowId) -> Result<(), ShellError> {
1159 let state = &mut self.window_mut(window)?.accessibility;
1160 if state.is_none() {
1161 *state = Some(Vec::new());
1162 self.queue
1163 .push_back(ShellEvent::AccessibilityActivated { window });
1164 }
1165 Ok(())
1166 }
1167
1168 #[cfg(feature = "accesskit")]
1178 pub fn deactivate_accessibility(&mut self, window: WindowId) -> Result<(), ShellError> {
1179 if self.window_mut(window)?.accessibility.take().is_some() {
1180 self.queue
1181 .push_back(ShellEvent::AccessibilityDeactivated { window });
1182 }
1183 Ok(())
1184 }
1185
1186 #[cfg(feature = "accesskit")]
1200 pub fn request_accessibility_action(
1201 &mut self,
1202 window: WindowId,
1203 request: accesskit::ActionRequest,
1204 ) -> Result<(), ShellError> {
1205 assert!(
1206 self.window(window)?.accessibility.is_some(),
1207 "no assistive technology is reading the window"
1208 );
1209 self.queue
1210 .push_back(ShellEvent::AccessibilityAction { window, request });
1211 Ok(())
1212 }
1213
1214 #[cfg(feature = "accesskit")]
1223 pub fn accessibility_updates(
1224 &self,
1225 window: WindowId,
1226 ) -> Result<Option<&[accesskit::TreeUpdate]>, ShellError> {
1227 Ok(self.window(window)?.accessibility.as_deref())
1228 }
1229
1230 #[must_use]
1234 pub fn clipboard_bytes(&self, mime: MimeType) -> Option<&[u8]> {
1235 self.clipboard
1236 .iter()
1237 .find(|(offered, _)| offered.matches(mime))
1238 .map(|(_, bytes)| bytes.as_slice())
1239 }
1240
1241 pub fn set_clipboard_readable(&mut self, readable: bool) {
1258 self.clipboard_readable = readable;
1259 }
1260
1261 pub fn set_clipboard_stalled(&mut self, stalled: bool) {
1270 self.clipboard_stalled = stalled;
1271 }
1272
1273 pub fn require_user_interaction(&mut self, required: bool) {
1300 self.clipboard_needs_interaction = required;
1301 }
1302
1303 pub fn clear_user_interaction(&mut self) {
1308 self.saw_interaction = false;
1309 }
1310
1311 pub fn clipboard_deadline(&mut self, pumps: u32) {
1317 self.clipboard_deadline = pumps;
1318 }
1319
1320 #[must_use]
1325 pub fn held_clipboard_reads(&self) -> usize {
1326 self.reads.len()
1327 }
1328
1329 pub fn set_foreign_clipboard(&mut self, offers: Vec<(ReceivedMime, Vec<u8>)>) {
1337 self.clipboard = offers;
1338 }
1339
1340 fn answerable(&self) -> bool {
1346 self.clipboard_readable && !self.clipboard_stalled
1347 }
1348
1349 fn answer(&self, window: WindowId, request: ClipboardRequestId, mime: MimeType) -> ShellEvent {
1351 let found = self
1356 .clipboard
1357 .iter()
1358 .find(|(offered, _)| offered.matches(mime));
1359 let (mime, content) = match found {
1360 Some((offered, bytes)) => (offered.clone(), ClipboardContent::Bytes(bytes.clone())),
1361 None => (ReceivedMime::from(mime), ClipboardContent::Empty),
1364 };
1365 ShellEvent::ClipboardData {
1366 window,
1367 request,
1368 mime,
1369 content,
1370 }
1371 }
1372
1373 fn resolve_reads(&mut self) {
1380 if self.reads.is_empty() {
1381 return;
1382 }
1383 let answerable = self.answerable();
1384 let mut held = Vec::with_capacity(self.reads.len());
1385 for mut read in core::mem::take(&mut self.reads) {
1386 if self.window(read.window).is_err() {
1390 continue;
1391 }
1392 if answerable {
1393 let answer = self.answer(read.window, read.request, read.mime);
1394 self.queue.push_back(answer);
1395 continue;
1396 }
1397 if read.countdown > 0 {
1398 read.countdown -= 1;
1399 held.push(read);
1400 continue;
1401 }
1402 self.queue.push_back(ShellEvent::ClipboardData {
1405 window: read.window,
1406 request: read.request,
1407 mime: ReceivedMime::from(read.mime),
1408 content: ClipboardContent::Unavailable,
1409 });
1410 }
1411 self.reads = held;
1412 }
1413
1414 fn deliver_due_configures(&mut self) {
1421 let mut due: Vec<(WindowId, PendingConfigure, Option<f64>)> = Vec::new();
1425 for (handle, state) in self.windows.iter_mut() {
1426 let Some(pending) = state.pending.as_mut() else {
1427 continue;
1428 };
1429 if pending.countdown > 0 {
1430 pending.countdown -= 1;
1431 continue;
1432 }
1433 let pending = state.pending.take().expect("checked just above");
1434 let previous_scale = state.configuration.map(|config| config.scale_factor);
1435 state.configuration = Some(WindowConfiguration {
1436 size: pending.size,
1437 scale_factor: pending.scale_factor,
1438 mode: pending.mode,
1439 });
1440 due.push((handle.cast(), pending, previous_scale));
1441 }
1442
1443 for (window, pending, previous_scale) in due {
1444 if previous_scale.is_some_and(|previous| previous != pending.scale_factor) {
1448 self.queue.push_back(ShellEvent::ScaleFactorChanged {
1449 window,
1450 scale_factor: pending.scale_factor,
1451 size: pending.size,
1452 });
1453 }
1454 self.queue.push_back(ShellEvent::Resized {
1455 window,
1456 size: pending.size,
1457 scale_factor: pending.scale_factor,
1458 });
1459 }
1460 }
1461
1462 fn window(&self, window: WindowId) -> Result<&HeadlessWindow, ShellError> {
1463 self.windows
1464 .get(window.cast())
1465 .ok_or_else(|| ShellError::invalid_window(window))
1466 }
1467
1468 fn window_mut(&mut self, window: WindowId) -> Result<&mut HeadlessWindow, ShellError> {
1469 self.windows
1470 .get_mut(window.cast())
1471 .ok_or_else(|| ShellError::invalid_window(window))
1472 }
1473
1474 fn check_window(&self, window: WindowId) -> Result<(), ShellError> {
1475 self.window(window).map(|_| ())
1476 }
1477
1478 fn unsupported(&self, what: &'static str) -> ShellError {
1479 ShellError::Unsupported {
1480 backend: ShellBackend::Headless,
1481 what,
1482 }
1483 }
1484}
1485
1486fn synthetic_scancode(key: KeyCode) -> Scancode {
1490 let index = KeyCode::ALL
1491 .iter()
1492 .position(|candidate| *candidate == key)
1493 .unwrap_or(0);
1494 Scancode(index as u32 + 8)
1497}
1498
1499fn synthetic_keysym(key: KeyCode) -> Keysym {
1501 let name = key.as_str();
1502 if let Some(letter) = name.strip_prefix("Key") {
1503 return Keysym::from_char(letter.to_ascii_lowercase().chars().next().unwrap_or('?'));
1505 }
1506 if let Some(digit) = name.strip_prefix("Digit") {
1507 return Keysym::from_char(digit.chars().next().unwrap_or('?'));
1508 }
1509 match key {
1510 KeyCode::Space => Keysym::from_char(' '),
1511 KeyCode::Minus => Keysym::from_char('-'),
1512 KeyCode::Equal => Keysym::from_char('='),
1513 KeyCode::Comma => Keysym::from_char(','),
1514 KeyCode::Period => Keysym::from_char('.'),
1515 KeyCode::Slash => Keysym::from_char('/'),
1516 KeyCode::Semicolon => Keysym::from_char(';'),
1517 KeyCode::Quote => Keysym::from_char('\''),
1518 KeyCode::Backslash => Keysym::from_char('\\'),
1519 KeyCode::BracketLeft => Keysym::from_char('['),
1520 KeyCode::BracketRight => Keysym::from_char(']'),
1521 KeyCode::Backquote => Keysym::from_char('`'),
1522 _ => crate::keysym::named_keysym(key).unwrap_or(Keysym::NONE),
1526 }
1527}
1528
1529impl Shell for HeadlessShell {
1530 fn backend(&self) -> ShellBackend {
1531 ShellBackend::Headless
1532 }
1533
1534 fn caps(&self) -> ShellCaps {
1535 self.caps
1536 }
1537
1538 fn create_window(&mut self, desc: &WindowDesc<'_>) -> Result<WindowId, ShellError> {
1539 crate::role::check(desc, self.caps(), self.backend(), |monitor| {
1540 self.monitor(monitor).is_some()
1541 })?;
1542 if !self.windows.is_empty() && !self.caps.contains(ShellCaps::MULTI_WINDOW) {
1543 return Err(self.unsupported("a second window"));
1544 }
1545 if let DisplayMode::Borderless {
1546 monitor: Some(monitor),
1547 } = desc.mode
1548 && self.monitor(monitor).is_none()
1549 {
1550 return Err(ShellError::NoSuchMonitor(monitor.0));
1551 }
1552
1553 let (size, scale_factor) = match desc.mode {
1565 DisplayMode::Windowed => {
1566 let scale_factor = self.monitors[0].scale_factor;
1567 (
1568 desc.constraints
1569 .apply(desc.size.to_physical(scale_factor), scale_factor),
1570 scale_factor,
1571 )
1572 }
1573 DisplayMode::Borderless { monitor } => {
1574 let monitor = monitor
1575 .and_then(|id| self.monitor(id))
1576 .unwrap_or(&self.monitors[0]);
1577 (monitor.size(), monitor.scale_factor)
1578 }
1579 };
1580
1581 let (parent, popup_input, panel) = match desc.role {
1582 crate::WindowRole::Popup(popup) => {
1583 self.window(popup.parent)?;
1584 (Some(popup.parent), Some(popup.input), None)
1585 }
1586 crate::WindowRole::Panel(panel) => {
1587 (None, None, Some(crate::role::KeptPanel::new(&panel)))
1588 }
1589 crate::WindowRole::Toplevel => (None, None, None),
1590 };
1591 let countdown = self.configure_delay;
1592 let window: WindowId = self
1593 .windows
1594 .insert(HeadlessWindow {
1595 title: desc.title.to_string(),
1596 configuration: None,
1599 pending: Some(PendingConfigure {
1600 size,
1601 scale_factor,
1602 mode: desc.mode,
1603 countdown,
1604 }),
1605 requested_mode: desc.mode,
1606 requested_constraints: desc.constraints,
1607 focused: false,
1608 visible: desc.visible,
1609 pointer_mode: PointerMode::Free,
1610 cursor: Some(CursorIcon::Default),
1611 close_pending: false,
1612 accept_drops: desc.accept_drops,
1613 parent,
1614 popup_input,
1615 panel,
1616 text_input: None,
1617 input_region: None,
1618 #[cfg(feature = "accesskit")]
1619 accessibility: None,
1620 })
1621 .cast();
1622 Ok(window)
1623 }
1624
1625 fn destroy_window(&mut self, window: WindowId) -> Result<(), ShellError> {
1629 self.window(window)?;
1630 let popups: Vec<WindowId> = self
1631 .windows
1632 .iter()
1633 .filter(|(_, state)| state.parent == Some(window))
1634 .map(|(id, _)| id.cast())
1635 .collect();
1636 for popup in popups {
1637 self.destroy_window(popup)?;
1638 }
1639 self.windows
1640 .remove(window.cast())
1641 .ok_or_else(|| ShellError::invalid_window(window))?;
1642 self.queue.push_back(ShellEvent::WindowDestroyed { window });
1643 Ok(())
1644 }
1645
1646 fn window_state(&self, window: WindowId) -> Result<WindowState, ShellError> {
1647 let state = self.window(window)?;
1648 Ok(WindowState {
1649 configuration: state.configuration,
1650 requested_mode: state.requested_mode,
1651 requested_constraints: state.requested_constraints,
1652 focused: state.focused,
1653 visible: state.visible,
1654 pointer_mode: state.pointer_mode,
1655 close_pending: state.close_pending,
1656 })
1657 }
1658
1659 fn set_title(&mut self, window: WindowId, title: &str) -> Result<(), ShellError> {
1660 self.window_mut(window)?.title = title.to_string();
1661 Ok(())
1662 }
1663
1664 fn set_visible(&mut self, window: WindowId, visible: bool) -> Result<(), ShellError> {
1665 self.window_mut(window)?.visible = visible;
1666 Ok(())
1667 }
1668
1669 fn set_mode(&mut self, window: WindowId, mode: DisplayMode) -> Result<(), ShellError> {
1670 let target = match mode {
1671 DisplayMode::Windowed => None,
1672 DisplayMode::Borderless { monitor } => {
1673 let monitor = match monitor {
1674 Some(id) => self
1675 .monitor(id)
1676 .ok_or(ShellError::NoSuchMonitor(id.0))?
1677 .clone(),
1678 None => self.monitors[0].clone(),
1679 };
1680 Some(monitor)
1681 }
1682 };
1683 let countdown = self.configure_delay;
1684
1685 let state = self.window_mut(window)?;
1686 state.requested_mode = mode;
1692 let (size, scale_factor) = match &target {
1693 Some(monitor) => (monitor.size(), monitor.scale_factor),
1694 None => (state.base_size(), state.scale_factor()),
1698 };
1699 state.pending = Some(PendingConfigure {
1700 size,
1701 scale_factor,
1702 mode,
1703 countdown,
1704 });
1705 Ok(())
1706 }
1707
1708 fn set_constraints(
1709 &mut self,
1710 window: WindowId,
1711 constraints: SizeConstraints,
1712 ) -> Result<(), ShellError> {
1713 let countdown = self.configure_delay;
1714 let state = self.window_mut(window)?;
1715 state.requested_constraints = constraints;
1716
1717 if let Some(pending) = state.pending.as_mut() {
1724 pending.size = constraints.apply(pending.size, pending.scale_factor);
1725 return Ok(());
1726 }
1727
1728 if let Some(config) = state.configuration {
1732 let constrained = constraints.apply(config.size, config.scale_factor);
1733 if constrained != config.size {
1734 state.pending = Some(PendingConfigure {
1735 size: constrained,
1736 scale_factor: config.scale_factor,
1737 mode: config.mode,
1738 countdown,
1739 });
1740 }
1741 }
1742 Ok(())
1743 }
1744
1745 fn reposition_popup(
1749 &mut self,
1750 popup: WindowId,
1751 anchor: LogicalRect,
1752 size: LogicalSize,
1753 ) -> Result<(), ShellError> {
1754 let _ = anchor;
1755 let countdown = self.configure_delay;
1756 let state = self.window_mut(popup)?;
1757 if state.parent.is_none() {
1758 return Err(crate::role::not_a_popup(popup));
1759 }
1760 state.configure_at(size, countdown);
1761 Ok(())
1762 }
1763
1764 fn set_panel(
1768 &mut self,
1769 window: WindowId,
1770 size: LogicalSize,
1771 panel: &crate::PanelDesc<'_>,
1772 ) -> Result<(), ShellError> {
1773 let was = self.window(window)?.panel.as_ref().map(|kept| kept.desc());
1774 crate::role::check_set_panel(window, was, panel, self.caps, self.backend(), |monitor| {
1775 self.monitor(monitor).is_some()
1776 })?;
1777 let countdown = self.configure_delay;
1778 let state = self.window_mut(window)?;
1779 state.panel = Some(crate::role::KeptPanel::new(panel));
1780 state.configure_at(size, countdown);
1781 Ok(())
1782 }
1783
1784 fn monitors(&self) -> &[MonitorInfo] {
1785 &self.monitors
1786 }
1787
1788 fn pump(&mut self, sink: &mut dyn FnMut(ShellEvent)) {
1789 self.keep_alive();
1790 if self.outputs_powered {
1791 let windows = &self.windows;
1792 self.queue.extend(
1793 self.frames
1794 .take_due(|window| windows.contains(window.cast())),
1795 );
1796 }
1797 for _ in 0..self.queue.len() {
1802 let Some(event) = self.queue.pop_front() else {
1803 break;
1804 };
1805 sink(event);
1806 }
1807 }
1808
1809 fn keep_alive(&mut self) {
1817 self.deliver_due_configures();
1818 self.resolve_reads();
1819 }
1820
1821 fn wait_events(&mut self, timeout: Option<Duration>) {
1841 self.waits += 1;
1842 if !self.caps.contains(ShellCaps::EVENT_WAIT)
1843 || !self.queue.is_empty()
1844 || (self.outputs_powered && self.frames.is_pending())
1845 {
1846 return;
1847 }
1848 match (&self.wake, timeout) {
1849 (Some(wake), timeout) => wake.wait(timeout),
1850 (None, Some(timeout)) => std::thread::sleep(timeout),
1851 (None, None) => {}
1852 }
1853 }
1854
1855 fn waker(&mut self) -> Option<ShellWaker> {
1858 let wake = self.wake.get_or_insert_with(Arc::default);
1859 Some(ShellWaker::new(Arc::clone(wake) as Arc<dyn WakeSignal>))
1860 }
1861
1862 fn align_event_clock(&mut self, elapsed: Duration) {
1869 self.set_time(elapsed);
1870 }
1871
1872 fn surface_target(&self, window: WindowId) -> Result<SurfaceTarget, ShellError> {
1873 self.window(window)?;
1879 Ok(SurfaceTarget::Offscreen)
1880 }
1881
1882 fn set_pointer_mode(&mut self, window: WindowId, mode: PointerMode) -> Result<(), ShellError> {
1883 if !self.caps.contains(mode.required_cap()) {
1884 return Err(self.unsupported(mode.as_str()));
1885 }
1886 self.window_mut(window)?.pointer_mode = mode;
1887 Ok(())
1888 }
1889
1890 fn set_text_input(
1894 &mut self,
1895 window: WindowId,
1896 field: Option<TextPurpose>,
1897 ) -> Result<(), ShellError> {
1898 self.window_mut(window)?.text_input = field;
1899 Ok(())
1900 }
1901
1902 fn set_input_region(
1907 &mut self,
1908 window: WindowId,
1909 region: Option<&[PhysicalRect]>,
1910 ) -> Result<(), ShellError> {
1911 self.window_mut(window)?.input_region = region.map(<[PhysicalRect]>::to_vec);
1912 Ok(())
1913 }
1914
1915 fn request_frame(&mut self, window: WindowId) -> Result<(), ShellError> {
1920 self.window(window)?;
1921 self.frames.request(window);
1922 Ok(())
1923 }
1924
1925 #[cfg(feature = "accesskit")]
1926 fn update_accessibility(
1927 &mut self,
1928 window: WindowId,
1929 update: accesskit::TreeUpdate,
1930 ) -> Result<(), ShellError> {
1931 if let Some(updates) = &mut self.window_mut(window)?.accessibility {
1932 updates.push(update);
1933 }
1934 Ok(())
1935 }
1936
1937 fn set_cursor(
1938 &mut self,
1939 window: WindowId,
1940 cursor: Option<CursorIcon>,
1941 ) -> Result<(), ShellError> {
1942 self.window_mut(window)?.cursor = cursor;
1943 Ok(())
1944 }
1945
1946 fn warp_pointer(
1947 &mut self,
1948 window: WindowId,
1949 position: PhysicalPoint,
1950 ) -> Result<(), ShellError> {
1951 if !self.caps.contains(ShellCaps::POINTER_WARP) {
1952 return Err(self.unsupported("pointer warp"));
1953 }
1954 self.check_window(window)?;
1955 let event = ShellEvent::PointerMotion {
1958 window,
1959 device: VIRTUAL_DEVICE,
1960 time: self.now(),
1961 abs: Some(position),
1962 raw_delta: None,
1963 };
1964 self.queue.push_back(event);
1965 Ok(())
1966 }
1967
1968 fn reply_close_request(
1969 &mut self,
1970 window: WindowId,
1971 reply: CloseReply,
1972 ) -> Result<(), ShellError> {
1973 let state = self.window_mut(window)?;
1974 if !state.close_pending {
1975 return Err(ShellError::NoPendingCloseRequest { window });
1976 }
1977 state.close_pending = false;
1978 match reply {
1979 CloseReply::Close => self.destroy_window(window),
1980 CloseReply::Keep => Ok(()),
1981 }
1982 }
1983
1984 fn clipboard_offer(
1995 &mut self,
1996 window: WindowId,
1997 offers: &[ClipboardOffer<'_>],
1998 ) -> Result<(), ShellError> {
1999 if !self.caps.contains(ShellCaps::CLIPBOARD) {
2000 return Err(self.unsupported("clipboard"));
2001 }
2002 self.check_window(window)?;
2003 if self.clipboard_needs_interaction && !self.saw_interaction {
2004 return Err(ShellError::NeedsUserInteraction {
2005 backend: ShellBackend::Headless,
2006 what: "claiming the clipboard",
2007 });
2008 }
2009 self.clipboard = offers
2010 .iter()
2011 .map(|offer| (ReceivedMime::from(offer.mime), offer.bytes.to_vec()))
2012 .collect();
2013 Ok(())
2014 }
2015
2016 fn clipboard_request(
2030 &mut self,
2031 window: WindowId,
2032 mime: MimeType,
2033 ) -> Result<ClipboardRequestId, ShellError> {
2034 if !self.caps.contains(ShellCaps::CLIPBOARD) {
2035 return Err(self.unsupported("clipboard"));
2036 }
2037 self.check_window(window)?;
2038 let request = ClipboardRequestId(self.next_request_id);
2039 self.next_request_id += 1;
2040 if self.answerable() {
2041 let answer = self.answer(window, request, mime);
2042 self.queue.push_back(answer);
2046 } else {
2047 self.reads.push(PendingRead {
2048 window,
2049 request,
2050 mime,
2051 countdown: self.clipboard_deadline,
2052 });
2053 }
2054 Ok(request)
2055 }
2056
2057 fn clipboard_readable(&self, window: WindowId) -> bool {
2063 self.caps.contains(ShellCaps::CLIPBOARD) && self.window(window).is_ok() && self.answerable()
2064 }
2065}
2066
2067#[cfg(test)]
2068mod tests {
2069 use super::*;
2070 use crate::{AspectRatio, LogicalSize};
2071
2072 fn shell_with_window() -> (HeadlessShell, WindowId) {
2074 let mut shell = HeadlessShell::new();
2075 let window = shell
2076 .create_window(&WindowDesc::default())
2077 .expect("headless window creation never fails");
2078 (shell, window)
2079 }
2080
2081 fn shell_with_configured_window() -> (HeadlessShell, WindowId) {
2083 let (mut shell, window) = shell_with_window();
2084 settle(&mut shell);
2085 assert!(shell.window_state(window).expect("state").is_configured());
2086 (shell, window)
2087 }
2088
2089 fn settle(shell: &mut HeadlessShell) {
2091 for _ in 0..8 {
2092 let mut seen = 0;
2093 shell.pump(&mut |_| seen += 1);
2094 if seen == 0 && shell.windows.iter().all(|(_, w)| w.pending.is_none()) {
2095 return;
2096 }
2097 }
2098 panic!("the shell never settled");
2099 }
2100
2101 fn drain(shell: &mut HeadlessShell) -> Vec<ShellEvent> {
2102 let mut events = Vec::new();
2103 shell.pump(&mut |event| events.push(event));
2104 events
2105 }
2106
2107 fn names(events: &[ShellEvent]) -> Vec<&'static str> {
2108 events.iter().map(ShellEvent::name).collect()
2109 }
2110
2111 fn create_swapchain(state: &WindowState) -> Result<PhysicalSize, &'static str> {
2114 let size = state.size().ok_or("window is not configured yet")?;
2115 if size.is_empty() {
2116 return Err("zero-extent swapchain");
2117 }
2118 Ok(size)
2119 }
2120
2121 #[cfg(feature = "accesskit")]
2126 #[test]
2127 fn a_tree_reaches_the_reader_only_while_it_reads() {
2128 use accesskit::{Action, ActionRequest, Node, NodeId, Role, TreeId, TreeInfo, TreeUpdate};
2129
2130 let (mut shell, window) = shell_with_configured_window();
2131 let update = |label: &str| TreeUpdate {
2132 nodes: vec![(NodeId(1), {
2133 let mut node = Node::new(Role::Window);
2134 node.set_label(label);
2135 node
2136 })],
2137 tree: Some(TreeInfo::new(NodeId(1))),
2138 tree_id: TreeId::ROOT,
2139 focus: NodeId(1),
2140 };
2141
2142 shell
2143 .update_accessibility(window, update("unread"))
2144 .expect("update");
2145 assert_eq!(shell.accessibility_updates(window).expect("updates"), None);
2146
2147 shell.activate_accessibility(window).expect("activate");
2148 shell
2149 .activate_accessibility(window)
2150 .expect("activate twice");
2151 shell
2152 .update_accessibility(window, update("read"))
2153 .expect("update");
2154 let click = ActionRequest {
2155 action: Action::Click,
2156 target_tree: TreeId::ROOT,
2157 target_node: NodeId(1),
2158 data: None,
2159 };
2160 shell
2161 .request_accessibility_action(window, click.clone())
2162 .expect("action");
2163 assert_eq!(
2164 shell.accessibility_updates(window).expect("updates"),
2165 Some(&[update("read")][..])
2166 );
2167 let events = drain(&mut shell);
2168 assert_eq!(
2169 names(&events),
2170 ["AccessibilityActivated", "AccessibilityAction"]
2171 );
2172 assert_eq!(
2173 events[1],
2174 ShellEvent::AccessibilityAction {
2175 window,
2176 request: click
2177 }
2178 );
2179
2180 shell.deactivate_accessibility(window).expect("deactivate");
2181 shell
2182 .deactivate_accessibility(window)
2183 .expect("deactivate twice");
2184 shell
2185 .update_accessibility(window, update("gone"))
2186 .expect("update");
2187 assert_eq!(shell.accessibility_updates(window).expect("updates"), None);
2188 assert_eq!(names(&drain(&mut shell)), ["AccessibilityDeactivated"]);
2189 }
2190
2191 #[test]
2194 fn a_new_window_has_no_size_until_a_later_pump() {
2195 let (mut shell, window) = shell_with_window();
2196
2197 let state = shell.window_state(window).expect("state");
2200 assert!(!state.is_configured());
2201 assert_eq!(state.size(), None);
2202 assert_eq!(state.scale_factor(), None);
2203 assert_eq!(state.effective_mode(), None);
2204 assert_eq!(state.requested_mode, DisplayMode::Windowed);
2205
2206 assert_eq!(drain(&mut shell), Vec::new());
2208 assert!(!shell.window_state(window).expect("state").is_configured());
2209
2210 let events = drain(&mut shell);
2212 assert_eq!(
2213 events,
2214 vec![ShellEvent::Resized {
2215 window,
2216 size: PhysicalSize::new(1280, 720),
2217 scale_factor: 1.0,
2218 }]
2219 );
2220 let state = shell.window_state(window).expect("state");
2221 assert_eq!(state.size(), Some(PhysicalSize::new(1280, 720)));
2222 assert_eq!(state.scale_factor(), Some(1.0));
2223 assert_eq!(state.effective_mode(), Some(DisplayMode::Windowed));
2224 assert!(state.mode_request_honoured());
2225 }
2226
2227 #[test]
2231 fn keep_alive_takes_a_turn_and_keeps_everything_for_the_next_pump() {
2232 let (mut shell, window) = shell_with_window();
2233 shell.inject(ShellEvent::CloseRequested { window });
2234
2235 for _ in 0..=DEFAULT_CONFIGURE_DELAY {
2237 shell.keep_alive();
2238 }
2239 assert!(
2240 shell.window_state(window).expect("state").is_configured(),
2241 "every turn the configure waits for was taken by keep_alive"
2242 );
2243
2244 assert_eq!(
2245 drain(&mut shell),
2246 vec![
2247 ShellEvent::CloseRequested { window },
2248 ShellEvent::Resized {
2249 window,
2250 size: PhysicalSize::new(1280, 720),
2251 scale_factor: 1.0,
2252 },
2253 ],
2254 "nothing keep_alive saw was delivered or dropped"
2255 );
2256 assert_eq!(drain(&mut shell), Vec::new(), "and each arrived once");
2257 }
2258
2259 #[test]
2260 fn a_consumer_that_skips_the_wait_gets_a_clean_error_not_a_wrong_extent() {
2261 let (mut shell, window) = shell_with_window();
2262
2263 assert_eq!(
2265 create_swapchain(&shell.window_state(window).expect("state")),
2266 Err("window is not configured yet")
2267 );
2268
2269 let target = shell.surface_target(window).expect("the handle exists");
2275 assert_eq!(target, SurfaceTarget::Offscreen);
2276
2277 let size = loop {
2279 shell.pump(&mut |_| {});
2280 if let Ok(size) = create_swapchain(&shell.window_state(window).expect("state")) {
2281 break size;
2282 }
2283 };
2284 assert_eq!(size, PhysicalSize::new(1280, 720));
2285 assert_eq!(
2288 shell.surface_target(window).expect("target"),
2289 target,
2290 "the target is invariant under a configure"
2291 );
2292 }
2293
2294 #[test]
2295 fn the_configure_delay_is_a_knob() {
2296 let mut shell = HeadlessShell::new().with_configure_delay(3);
2297 let window = shell.create_window(&WindowDesc::default()).expect("window");
2298 for pump in 0..3 {
2299 assert_eq!(drain(&mut shell), Vec::new(), "pump {pump}");
2300 assert!(!shell.window_state(window).expect("state").is_configured());
2301 }
2302 assert_eq!(names(&drain(&mut shell)), ["Resized"]);
2303 assert!(shell.window_state(window).expect("state").is_configured());
2304
2305 let mut shell = HeadlessShell::new().with_configure_delay(0);
2308 let window = shell.create_window(&WindowDesc::default()).expect("window");
2309 assert_eq!(names(&drain(&mut shell)), ["Resized"]);
2310 assert!(shell.window_state(window).expect("state").is_configured());
2311
2312 shell.set_configure_delay(2);
2313 shell
2314 .set_mode(window, DisplayMode::Borderless { monitor: None })
2315 .expect("request");
2316 assert_eq!(drain(&mut shell), Vec::new());
2317 assert_eq!(drain(&mut shell), Vec::new());
2318 assert_eq!(names(&drain(&mut shell)), ["Resized"]);
2319 }
2320
2321 #[test]
2322 fn set_mode_is_a_request_whose_effect_arrives_later() {
2323 let (mut shell, window) = shell_with_configured_window();
2324 shell
2325 .set_mode(window, DisplayMode::Borderless { monitor: None })
2326 .expect("request");
2327
2328 let state = shell.window_state(window).expect("state");
2330 assert_eq!(
2331 state.requested_mode,
2332 DisplayMode::Borderless { monitor: None }
2333 );
2334 assert_eq!(
2335 state.effective_mode(),
2336 Some(DisplayMode::Windowed),
2337 "still windowed until the window system says otherwise"
2338 );
2339 assert!(
2340 !state.mode_request_honoured(),
2341 "a UI toggle reading the request instead of the effect would lie here"
2342 );
2343 assert_eq!(state.size(), Some(PhysicalSize::new(1280, 720)));
2344
2345 settle(&mut shell);
2346 let state = shell.window_state(window).expect("state");
2347 assert_eq!(
2348 state.effective_mode(),
2349 Some(DisplayMode::Borderless { monitor: None })
2350 );
2351 assert!(state.mode_request_honoured());
2352 assert_eq!(state.size(), Some(PhysicalSize::new(1920, 1080)));
2353 }
2354
2355 #[test]
2356 fn a_refused_mode_is_expressible_through_injection() {
2357 let (mut shell, window) = shell_with_configured_window();
2360 shell
2361 .set_mode(window, DisplayMode::Borderless { monitor: None })
2362 .expect("request");
2363 shell
2365 .resize(window, PhysicalSize::new(1280, 720))
2366 .expect("answer");
2367 let state = shell.window_state(window).expect("state");
2368 assert_eq!(state.effective_mode(), Some(DisplayMode::Windowed));
2369 assert!(!state.mode_request_honoured());
2370 }
2371
2372 #[test]
2378 fn a_reconfigured_panel_is_configured_at_its_new_size() {
2379 let mut shell = HeadlessShell::new();
2380 let monitor = shell.monitors()[0].id;
2381 let bar = crate::PanelDesc {
2382 namespace: "bar",
2383 monitor: Some(monitor),
2384 anchor: crate::PanelAnchor::TOP | crate::PanelAnchor::LEFT | crate::PanelAnchor::RIGHT,
2385 reserve: crate::Reserve::OwnExtent,
2386 ..crate::PanelDesc::default()
2387 };
2388 let window = shell
2389 .create_window(&WindowDesc {
2390 size: LogicalSize::new(400.0, 30.0),
2391 role: crate::WindowRole::Panel(bar),
2392 ..WindowDesc::default()
2393 })
2394 .expect("a panel");
2395 settle(&mut shell);
2396 let scale = shell
2397 .window_state(window)
2398 .expect("live")
2399 .scale_factor()
2400 .expect("configured");
2401
2402 let taller = crate::PanelDesc {
2403 layer: crate::PanelLayer::Overlay,
2404 margins: crate::Margins {
2405 top: 4.0,
2406 ..crate::Margins::default()
2407 },
2408 keyboard: crate::PanelKeyboard::OnDemand,
2409 ..bar
2410 };
2411 let size = LogicalSize::new(400.0, 50.0);
2412 shell.set_panel(window, size, &taller).expect("set_panel");
2413 assert_eq!(shell.panel(window).expect("live"), Some(taller));
2414 assert_eq!(
2415 shell.window_state(window).expect("live").size(),
2416 Some(LogicalSize::new(400.0, 30.0).to_physical(scale)),
2417 "a request: nothing changes before the window system answers"
2418 );
2419 let mut resized = Vec::new();
2420 for _ in 0..8 {
2421 resized.extend(
2422 drain(&mut shell)
2423 .into_iter()
2424 .filter_map(|event| match event {
2425 ShellEvent::Resized {
2426 window: of, size, ..
2427 } if of == window => Some(size),
2428 _ => None,
2429 }),
2430 );
2431 }
2432 assert_eq!(resized, [size.to_physical(scale)]);
2433
2434 let refused = |shell: &mut HeadlessShell, panel: &crate::PanelDesc<'_>| match shell
2435 .set_panel(window, LogicalSize::new(1.0, 1.0), panel)
2436 {
2437 Err(ShellError::InvalidDescriptor(message)) => message,
2438 other => panic!("expected InvalidDescriptor, got {other:?}"),
2439 };
2440 let renamed = crate::PanelDesc {
2441 namespace: "dock",
2442 ..taller
2443 };
2444 assert!(refused(&mut shell, &renamed).contains("namespace"));
2445 let anywhere = crate::PanelDesc {
2446 monitor: None,
2447 ..taller
2448 };
2449 assert!(refused(&mut shell, &anywhere).contains("monitor"));
2450 let toplevel = shell
2451 .create_window(&WindowDesc::default())
2452 .expect("a toplevel");
2453 assert!(matches!(
2454 shell.set_panel(toplevel, size, &taller),
2455 Err(ShellError::InvalidDescriptor(message)) if message.contains("not a panel")
2456 ));
2457 let cornered = crate::PanelDesc {
2458 anchor: crate::PanelAnchor::TOP | crate::PanelAnchor::LEFT,
2459 ..taller
2460 };
2461 assert!(matches!(
2462 shell.set_panel(window, size, &cornered),
2463 Err(ShellError::Unsupported { .. })
2464 ));
2465 assert_eq!(
2466 shell.panel(window).expect("live"),
2467 Some(taller),
2468 "a refused descriptor is not recorded"
2469 );
2470 assert_eq!(shell.panel(toplevel).expect("live"), None);
2471
2472 shell.destroy_window(window).expect("destroy");
2473 assert!(matches!(
2474 shell.set_panel(window, size, &taller),
2475 Err(ShellError::InvalidWindow { .. })
2476 ));
2477 }
2478
2479 #[test]
2482 fn a_repositioned_popup_is_configured_at_its_new_size() {
2483 let (mut shell, window) = shell_with_configured_window();
2484 let anchor = crate::LogicalRect::new(
2485 crate::LogicalPoint::new(10.0, 10.0),
2486 LogicalSize::new(100.0, 20.0),
2487 );
2488 let popup = shell
2489 .create_window(&WindowDesc {
2490 size: LogicalSize::new(200.0, 150.0),
2491 role: crate::WindowRole::Popup(crate::PopupDesc {
2492 parent: window,
2493 anchor,
2494 placement: crate::PopupPlacement::Below,
2495 input: crate::PopupInput::Pointer,
2496 }),
2497 ..WindowDesc::default()
2498 })
2499 .expect("a popup");
2500 settle(&mut shell);
2501 let scale = shell
2502 .window_state(popup)
2503 .expect("live")
2504 .scale_factor()
2505 .expect("configured");
2506
2507 let moved = LogicalSize::new(240.0, 90.0);
2508 shell
2509 .reposition_popup(popup, anchor, moved)
2510 .expect("reposition");
2511 settle(&mut shell);
2512 assert_eq!(
2513 shell.window_state(popup).expect("live").size(),
2514 Some(moved.to_physical(scale))
2515 );
2516 assert!(matches!(
2517 shell.reposition_popup(window, anchor, moved),
2518 Err(ShellError::InvalidDescriptor(_))
2519 ));
2520 }
2521
2522 #[test]
2526 fn escape_on_a_grabbing_popup_is_delivered_then_asks_it_to_close() {
2527 let (mut shell, window) = shell_with_configured_window();
2528 let popup_of = |shell: &mut HeadlessShell, input| {
2529 shell
2530 .create_window(&WindowDesc {
2531 role: crate::WindowRole::Popup(crate::PopupDesc {
2532 parent: window,
2533 anchor: crate::LogicalRect::default(),
2534 placement: crate::PopupPlacement::Below,
2535 input,
2536 }),
2537 ..WindowDesc::default()
2538 })
2539 .expect("a popup")
2540 };
2541 let menu = popup_of(&mut shell, crate::PopupInput::Grab);
2542 let tooltip = popup_of(&mut shell, crate::PopupInput::Pointer);
2543 settle(&mut shell);
2544 let is_close = |event: &ShellEvent| matches!(event, ShellEvent::CloseRequested { .. });
2545
2546 shell.key_press(menu, KeyCode::Escape).expect("press");
2547 let events = drain(&mut shell);
2548 assert!(
2549 matches!(
2550 events.as_slice(),
2551 [
2552 ShellEvent::Key {
2553 key_code: Some(KeyCode::Escape),
2554 ..
2555 },
2556 ShellEvent::CloseRequested { window: closed },
2557 ] if *closed == menu
2558 ),
2559 "the key, then the request: {events:?}"
2560 );
2561 shell.key_press(menu, KeyCode::Escape).expect("press");
2563 shell.key_repeat(menu, KeyCode::Escape).expect("repeat");
2564 assert!(!drain(&mut shell).iter().any(is_close));
2565
2566 shell
2568 .reply_close_request(menu, CloseReply::Keep)
2569 .expect("keep");
2570 shell.key_release(menu, KeyCode::Escape).expect("release");
2571 shell.key_press(menu, KeyCode::Enter).expect("press");
2572 assert!(!drain(&mut shell).iter().any(is_close), "not these");
2573 shell.key_press(menu, KeyCode::Escape).expect("press");
2574 assert!(drain(&mut shell).iter().any(is_close));
2575
2576 shell.key_press(tooltip, KeyCode::Escape).expect("press");
2577 shell.key_press(window, KeyCode::Escape).expect("press");
2578 assert!(!drain(&mut shell).iter().any(is_close));
2579 }
2580
2581 #[test]
2582 fn set_constraints_is_a_request_too() {
2583 let (mut shell, window) = shell_with_configured_window();
2584 let constraints = SizeConstraints::min(LogicalSize::new(1600.0, 900.0));
2585 shell.set_constraints(window, constraints).expect("request");
2586
2587 let state = shell.window_state(window).expect("state");
2588 assert_eq!(state.requested_constraints, constraints);
2589 assert_eq!(
2590 state.size(),
2591 Some(PhysicalSize::new(1280, 720)),
2592 "the size only changes when the window system says so"
2593 );
2594
2595 settle(&mut shell);
2596 assert_eq!(
2597 shell.window_state(window).expect("state").size(),
2598 Some(PhysicalSize::new(1600, 900))
2599 );
2600 }
2601
2602 #[test]
2603 fn constraints_do_not_cancel_a_mode_request_that_has_not_landed() {
2604 let (mut shell, window) = shell_with_configured_window();
2609 shell
2610 .set_mode(window, DisplayMode::Borderless { monitor: None })
2611 .expect("mode request");
2612 shell
2613 .set_constraints(window, SizeConstraints::min(LogicalSize::new(320.0, 180.0)))
2614 .expect("constraint request");
2615 settle(&mut shell);
2616
2617 let state = shell.window_state(window).expect("state");
2618 assert_eq!(
2619 state.effective_mode(),
2620 Some(DisplayMode::Borderless { monitor: None })
2621 );
2622 assert!(state.mode_request_honoured());
2623 }
2624
2625 #[test]
2626 fn a_borderless_window_reports_its_own_monitors_scale() {
2627 let mut shell = HeadlessShell::new();
2632 let hidpi = shell.plug_monitor(MonitorInfo {
2633 id: MonitorId(0),
2634 name: "HEADLESS-2".to_string(),
2635 bounds: PhysicalRect::new(1920, 0, 2560, 1440),
2636 work_area: PhysicalRect::new(1920, 0, 2560, 1440),
2637 scale_factor: 2.0,
2638 refresh_millihertz: 143_998,
2639 is_primary: false,
2640 });
2641 let window = shell
2642 .create_window(&WindowDesc {
2643 mode: DisplayMode::Borderless {
2644 monitor: Some(hidpi),
2645 },
2646 ..WindowDesc::default()
2647 })
2648 .expect("window");
2649 settle(&mut shell);
2650
2651 let state = shell.window_state(window).expect("state");
2652 assert_eq!(state.size(), Some(PhysicalSize::new(2560, 1440)));
2653 assert_eq!(
2654 state.scale_factor(),
2655 Some(2.0),
2656 "the scale is the borderless monitor's, not the primary's"
2657 );
2658 let other = shell
2661 .create_window(&WindowDesc::default())
2662 .expect("second window");
2663 settle(&mut shell);
2664 shell
2665 .set_mode(
2666 other,
2667 DisplayMode::Borderless {
2668 monitor: Some(hidpi),
2669 },
2670 )
2671 .expect("request");
2672 settle(&mut shell);
2673 assert_eq!(
2674 shell.window_state(other).expect("state").scale_factor(),
2675 shell.window_state(window).expect("state").scale_factor()
2676 );
2677 }
2678
2679 #[test]
2680 fn constraints_set_before_the_first_configure_fold_into_it() {
2681 let mut shell = HeadlessShell::new();
2682 let window = shell.create_window(&WindowDesc::default()).expect("window");
2683 shell
2684 .set_constraints(
2685 window,
2686 SizeConstraints::NONE.with_aspect(AspectRatio::SQUARE),
2687 )
2688 .expect("request");
2689 settle(&mut shell);
2690 assert_eq!(
2693 shell.window_state(window).expect("state").size(),
2694 Some(PhysicalSize::new(720, 720))
2695 );
2696 }
2697
2698 #[test]
2699 fn creation_uses_the_requested_size_and_the_monitors_scale() {
2700 let mut shell = HeadlessShell::new();
2701 let window = shell
2702 .create_window(&WindowDesc {
2703 size: LogicalSize::new(800.0, 600.0),
2704 ..WindowDesc::default()
2705 })
2706 .expect("window");
2707 settle(&mut shell);
2708 assert_eq!(
2709 shell.window_state(window).expect("state").size(),
2710 Some(PhysicalSize::new(800, 600))
2711 );
2712
2713 let mut shell = HeadlessShell::new().with_scale_factor(2.0);
2714 let window = shell
2715 .create_window(&WindowDesc {
2716 size: LogicalSize::new(640.0, 360.0),
2717 ..WindowDesc::default()
2718 })
2719 .expect("window");
2720 settle(&mut shell);
2721 let state = shell.window_state(window).expect("state");
2722 assert_eq!(state.size(), Some(PhysicalSize::new(1280, 720)));
2723 assert_eq!(state.scale_factor(), Some(2.0));
2724 }
2725
2726 #[test]
2729 fn scale_changes_preserve_logical_size_and_report_both_facts() {
2730 let (mut shell, window) = shell_with_configured_window();
2731 shell
2732 .change_scale_factor(window, 1.5)
2733 .expect("scale change");
2734 let events = drain(&mut shell);
2735 assert_eq!(
2736 events,
2737 vec![
2738 ShellEvent::ScaleFactorChanged {
2739 window,
2740 scale_factor: 1.5,
2741 size: PhysicalSize::new(1920, 1080),
2742 },
2743 ShellEvent::Resized {
2746 window,
2747 size: PhysicalSize::new(1920, 1080),
2748 scale_factor: 1.5,
2749 },
2750 ]
2751 );
2752 assert_eq!(
2756 shell.window_state(window).expect("state").size(),
2757 Some(PhysicalSize::new(1920, 1080))
2758 );
2759 }
2760
2761 #[test]
2762 fn a_scale_change_scheduled_by_a_mode_switch_arrives_before_the_resize() {
2763 let mut shell = HeadlessShell::new();
2764 let hidpi = shell.plug_monitor(MonitorInfo {
2765 id: MonitorId(0),
2766 name: "HEADLESS-2".to_string(),
2767 bounds: PhysicalRect::new(1920, 0, 2560, 1440),
2768 work_area: PhysicalRect::new(1920, 0, 2560, 1440),
2769 scale_factor: 2.0,
2770 refresh_millihertz: 143_998,
2771 is_primary: false,
2772 });
2773 assert_eq!(hidpi, MonitorId(2), "ids are never reused");
2774 let window = shell.create_window(&WindowDesc::default()).expect("window");
2775 settle(&mut shell);
2776
2777 shell
2778 .set_mode(
2779 window,
2780 DisplayMode::Borderless {
2781 monitor: Some(hidpi),
2782 },
2783 )
2784 .expect("request");
2785 assert_eq!(drain(&mut shell), Vec::new(), "still only a request");
2786 let events = drain(&mut shell);
2787 assert_eq!(names(&events), ["ScaleFactorChanged", "Resized"]);
2788
2789 let state = shell.window_state(window).expect("state");
2790 assert_eq!(state.size(), Some(PhysicalSize::new(2560, 1440)));
2791 assert_eq!(state.scale_factor(), Some(2.0));
2792 assert!(state.effective_mode().expect("configured").is_borderless());
2793 assert_eq!(
2798 shell.surface_target(window).expect("target"),
2799 SurfaceTarget::Offscreen
2800 );
2801
2802 assert!(matches!(
2803 shell.set_mode(
2804 window,
2805 DisplayMode::Borderless {
2806 monitor: Some(MonitorId(99)),
2807 },
2808 ),
2809 Err(ShellError::NoSuchMonitor(99))
2810 ));
2811 }
2812
2813 #[test]
2814 fn resizes_honour_constraints_and_inject_bypasses_them() {
2815 let (mut shell, window) = shell_with_configured_window();
2816 shell
2817 .set_constraints(
2818 window,
2819 SizeConstraints::min(LogicalSize::new(640.0, 360.0))
2820 .with_aspect(AspectRatio::WIDESCREEN),
2821 )
2822 .expect("request");
2823 settle(&mut shell);
2824
2825 shell
2826 .resize(window, PhysicalSize::new(1000, 1000))
2827 .expect("resize");
2828 assert_eq!(
2829 drain(&mut shell),
2830 vec![ShellEvent::Resized {
2831 window,
2832 size: PhysicalSize::new(1000, 562),
2833 scale_factor: 1.0,
2834 }]
2835 );
2836
2837 shell.inject(ShellEvent::Resized {
2840 window,
2841 size: PhysicalSize::new(300, 900),
2842 scale_factor: 1.0,
2843 });
2844 assert_eq!(
2845 drain(&mut shell)[0],
2846 ShellEvent::Resized {
2847 window,
2848 size: PhysicalSize::new(300, 900),
2849 scale_factor: 1.0,
2850 }
2851 );
2852 }
2853
2854 #[test]
2855 fn a_compositor_configure_supersedes_a_pending_one() {
2856 let (mut shell, window) = shell_with_configured_window();
2857 shell
2858 .set_mode(window, DisplayMode::Borderless { monitor: None })
2859 .expect("request");
2860 shell
2862 .resize(window, PhysicalSize::new(640, 480))
2863 .expect("answer");
2864 assert_eq!(names(&drain(&mut shell)), ["Resized"]);
2865 assert_eq!(
2866 shell.window_state(window).expect("state").size(),
2867 Some(PhysicalSize::new(640, 480))
2868 );
2869 assert_eq!(drain(&mut shell), Vec::new());
2871 }
2872
2873 #[test]
2876 fn timestamps_come_from_the_shells_clock_not_from_the_pump() {
2877 let (mut shell, window) = shell_with_configured_window();
2878 shell.set_time(Duration::from_secs(2));
2879 shell.key_press(window, KeyCode::Space).expect("press");
2880 shell.advance(Duration::from_millis(180));
2881 shell.key_release(window, KeyCode::Space).expect("release");
2882
2883 let events = drain(&mut shell);
2884 let held = events[1]
2885 .time()
2886 .expect("input")
2887 .saturating_since(events[0].time().expect("input"));
2888 assert_eq!(held, Duration::from_millis(180));
2889 assert_eq!(events[0].time(), Some(EventTime::from_millis(2_000)));
2890 }
2891
2892 #[test]
2893 fn synthesized_key_fields_are_stable_and_plausible() {
2894 let (mut shell, window) = shell_with_configured_window();
2895 shell.set_modifiers(Modifiers::CTRL);
2896 shell.key_press(window, KeyCode::KeyS).expect("press");
2897 let events = drain(&mut shell);
2898 let ShellEvent::Key {
2899 scancode,
2900 key_code,
2901 keysym,
2902 modifiers,
2903 repeat,
2904 device,
2905 ..
2906 } = events[0].clone()
2907 else {
2908 panic!("wrong variant");
2909 };
2910 assert_eq!(key_code, Some(KeyCode::KeyS));
2911 assert_eq!(keysym, Keysym::from_char('s'));
2912 assert_eq!(modifiers, Modifiers::CTRL);
2913 assert!(!repeat);
2914 assert_eq!(device, VIRTUAL_DEVICE);
2915 assert!(scancode.0 >= 8, "evdev-shaped scancodes start at 8");
2916 assert_eq!(synthetic_keysym(KeyCode::Digit7), Keysym::from_char('7'));
2917 assert_eq!(synthetic_keysym(KeyCode::F5), Keysym(0xFFC2));
2918 assert_eq!(synthetic_keysym(KeyCode::Enter), Keysym(0xFF0D));
2919 assert_eq!(synthetic_keysym(KeyCode::Escape), Keysym(0xFF1B));
2920 assert_eq!(synthetic_keysym(KeyCode::ArrowLeft), Keysym(0xFF51));
2921 assert_eq!(synthetic_keysym(KeyCode::Slash), Keysym::from_char('/'));
2922
2923 shell.key_repeat(window, KeyCode::KeyS).expect("repeat");
2924 let events = drain(&mut shell);
2925 let ShellEvent::Key { repeat, .. } = events[0] else {
2926 panic!("wrong variant");
2927 };
2928 assert!(repeat);
2929 }
2930
2931 #[test]
2932 fn a_locked_pointer_reports_no_absolute_position() {
2933 let (mut shell, window) = shell_with_configured_window();
2934 shell
2935 .set_pointer_mode(window, PointerMode::Locked)
2936 .expect("lock");
2937 shell
2938 .move_pointer(window, PhysicalPoint::new(10.0, 10.0), (3.0, -4.0))
2939 .expect("motion");
2940 let events = drain(&mut shell);
2941 let ShellEvent::PointerMotion { abs, raw_delta, .. } = events[0] else {
2942 panic!("wrong variant");
2943 };
2944 assert_eq!(abs, None);
2945 assert_eq!(raw_delta, Some((3.0, -4.0)));
2946 }
2947
2948 #[test]
2949 fn a_backend_without_raw_motion_reports_none() {
2950 let mut shell = HeadlessShell::new()
2951 .with_caps(ShellCaps::DESKTOP - ShellCaps::RAW_POINTER_MOTION)
2952 .with_configure_delay(0);
2953 let window = shell.create_window(&WindowDesc::default()).expect("window");
2954 drain(&mut shell);
2955 shell
2956 .move_pointer(window, PhysicalPoint::new(1.0, 2.0), (5.0, 5.0))
2957 .expect("motion");
2958 let events = drain(&mut shell);
2959 let ShellEvent::PointerMotion { abs, raw_delta, .. } = events[0] else {
2960 panic!("wrong variant");
2961 };
2962 assert_eq!(abs, Some(PhysicalPoint::new(1.0, 2.0)));
2963 assert_eq!(raw_delta, None, "no relative-motion source");
2964 assert!(!shell.caps().has_mouselook());
2965 }
2966
2967 #[test]
2968 fn pointer_focus_and_scroll_and_text_are_injectable() {
2969 let (mut shell, window) = shell_with_configured_window();
2970 shell
2971 .set_pointer_focus(window, true, Some(PhysicalPoint::new(1.0, 1.0)))
2972 .expect("enter");
2973 shell
2974 .scroll(window, ScrollDelta::Lines { x: 0.0, y: -1.0 }, None)
2975 .expect("scroll");
2976 shell
2977 .button(
2978 window,
2979 PointerButton::Left,
2980 ButtonState::Pressed,
2981 Some(PhysicalPoint::new(1.0, 1.0)),
2982 )
2983 .expect("click");
2984 shell.commit_text(window, "ありがとう").expect("text");
2985 shell
2986 .set_pointer_focus(window, false, Some(PhysicalPoint::new(1.0, 1.0)))
2987 .expect("leave");
2988
2989 let events = drain(&mut shell);
2990 assert_eq!(
2991 names(&events),
2992 [
2993 "PointerFocus",
2994 "Wheel",
2995 "Button",
2996 "TextCommit",
2997 "PointerFocus"
2998 ]
2999 );
3000 let ShellEvent::PointerFocus {
3001 entered, position, ..
3002 } = &events[4]
3003 else {
3004 panic!("wrong variant");
3005 };
3006 assert!(!entered);
3007 assert_eq!(*position, None, "a leave has no position");
3008 assert!(events.iter().all(ShellEvent::is_input));
3009 }
3010
3011 #[test]
3018 fn contacts_are_injectable_and_a_touchless_shell_refuses_them() {
3019 let (mut shell, window) = shell_with_configured_window();
3020 assert!(shell.caps().contains(ShellCaps::TOUCH));
3021 for (contact, phase, x) in [
3022 (1, TouchPhase::Began, 10.0),
3023 (2, TouchPhase::Began, 200.0),
3024 (2, TouchPhase::Moved, 220.0),
3025 (1, TouchPhase::Ended, 10.0),
3026 (2, TouchPhase::Cancelled, 220.0),
3027 ] {
3028 shell
3029 .touch(
3030 window,
3031 ContactId(contact),
3032 phase,
3033 PhysicalPoint::new(x, 5.0),
3034 )
3035 .expect("a contact");
3036 }
3037 let events = drain(&mut shell);
3038 assert_eq!(names(&events), ["Touch"; 5]);
3039 let seen: Vec<(ContactId, TouchPhase, f64)> = events
3040 .iter()
3041 .map(|event| {
3042 let ShellEvent::Touch {
3043 contact,
3044 phase,
3045 position,
3046 ..
3047 } = event
3048 else {
3049 panic!("wrong variant");
3050 };
3051 (*contact, *phase, position.x)
3052 })
3053 .collect();
3054 assert_eq!(
3055 seen,
3056 vec![
3057 (ContactId(1), TouchPhase::Began, 10.0),
3058 (ContactId(2), TouchPhase::Began, 200.0),
3059 (ContactId(2), TouchPhase::Moved, 220.0),
3060 (ContactId(1), TouchPhase::Ended, 10.0),
3061 (ContactId(2), TouchPhase::Cancelled, 220.0),
3062 ]
3063 );
3064 assert!(events.iter().all(ShellEvent::is_input));
3065
3066 let mut touchless = HeadlessShell::new()
3067 .with_caps(ShellCaps::DESKTOP - ShellCaps::TOUCH)
3068 .with_configure_delay(0);
3069 let window = touchless
3070 .create_window(&WindowDesc::default())
3071 .expect("window");
3072 drain(&mut touchless);
3073 assert!(matches!(
3074 touchless.touch(
3075 window,
3076 ContactId(1),
3077 TouchPhase::Began,
3078 PhysicalPoint::ORIGIN
3079 ),
3080 Err(ShellError::Unsupported { .. })
3081 ));
3082 assert!(drain(&mut touchless).is_empty(), "and queued nothing");
3083 }
3084
3085 #[test]
3086 fn warping_produces_motion_like_every_platform_that_has_it() {
3087 let (mut shell, window) = shell_with_configured_window();
3088 shell
3089 .warp_pointer(window, PhysicalPoint::new(320.0, 180.0))
3090 .expect("warp");
3091 let events = drain(&mut shell);
3092 let ShellEvent::PointerMotion { abs, raw_delta, .. } = events[0] else {
3093 panic!("wrong variant");
3094 };
3095 assert_eq!(abs, Some(PhysicalPoint::new(320.0, 180.0)));
3096 assert_eq!(raw_delta, None, "a warp is not physical motion");
3097 }
3098
3099 #[test]
3102 fn capabilities_gate_the_operations_that_need_them() {
3103 let mut shell = HeadlessShell::new().with_caps(ShellCaps::empty());
3104 let window = shell.create_window(&WindowDesc::default()).expect("window");
3105
3106 assert!(matches!(
3107 shell.warp_pointer(window, PhysicalPoint::ORIGIN),
3108 Err(ShellError::Unsupported { .. })
3109 ));
3110 assert!(matches!(
3111 shell.set_pointer_mode(window, PointerMode::Locked),
3112 Err(ShellError::Unsupported { .. })
3113 ));
3114 assert!(matches!(
3115 shell.clipboard_request(window, MimeType::TextUtf8),
3116 Err(ShellError::Unsupported { .. })
3117 ));
3118 shell
3120 .set_pointer_mode(window, PointerMode::Free)
3121 .expect("Free is always available");
3122 assert!(matches!(
3124 shell.create_window(&WindowDesc::default()),
3125 Err(ShellError::Unsupported { .. })
3126 ));
3127 }
3128
3129 #[test]
3130 fn stale_handles_fail_cleanly_on_every_accessor() {
3131 let (mut shell, window) = shell_with_configured_window();
3132 shell.destroy_window(window).expect("destroy");
3133 assert_eq!(
3134 drain(&mut shell),
3135 vec![ShellEvent::WindowDestroyed { window }]
3136 );
3137
3138 assert!(matches!(
3139 shell.window_state(window),
3140 Err(ShellError::InvalidWindow { .. })
3141 ));
3142 assert!(matches!(
3143 shell.surface_target(window),
3144 Err(ShellError::InvalidWindow { .. })
3145 ));
3146 assert!(matches!(
3147 shell.resize(window, PhysicalSize::new(1, 1)),
3148 Err(ShellError::InvalidWindow { .. })
3149 ));
3150 assert!(matches!(
3151 shell.set_title(window, "gone"),
3152 Err(ShellError::InvalidWindow { .. })
3153 ));
3154 assert!(matches!(
3155 shell.destroy_window(window),
3156 Err(ShellError::InvalidWindow { .. })
3157 ));
3158 assert!(matches!(
3159 shell.key_press(window, KeyCode::KeyA),
3160 Err(ShellError::InvalidWindow { .. })
3161 ));
3162 }
3163
3164 #[test]
3165 fn destroying_a_window_cancels_its_pending_configure() {
3166 let (mut shell, window) = shell_with_window();
3169 shell.destroy_window(window).expect("destroy");
3170 assert_eq!(names(&drain(&mut shell)), ["WindowDestroyed"]);
3171 assert_eq!(drain(&mut shell), Vec::new());
3172 assert_eq!(drain(&mut shell), Vec::new());
3173 }
3174
3175 #[test]
3176 fn close_requests_are_questions() {
3177 let (mut shell, window) = shell_with_configured_window();
3178 assert!(matches!(
3179 shell.reply_close_request(window, CloseReply::Close),
3180 Err(ShellError::NoPendingCloseRequest { .. })
3181 ));
3182
3183 shell.request_close(window).expect("request");
3184 assert!(shell.window_state(window).expect("state").close_pending);
3185 shell
3186 .reply_close_request(window, CloseReply::Keep)
3187 .expect("keep");
3188 assert!(!shell.window_state(window).expect("state").close_pending);
3189 assert!(shell.window_state(window).is_ok(), "still open");
3190
3191 shell.request_close(window).expect("request again");
3192 shell
3193 .reply_close_request(window, CloseReply::Close)
3194 .expect("close");
3195 assert!(matches!(
3196 shell.window_state(window),
3197 Err(ShellError::InvalidWindow { .. })
3198 ));
3199 }
3200
3201 #[test]
3202 fn monitor_hotplug_reports_a_change_and_keeps_ids_unique() {
3203 let mut shell = HeadlessShell::new();
3204 let extra = shell.plug_monitor(MonitorInfo {
3205 id: MonitorId(0),
3206 name: "HEADLESS-2".to_string(),
3207 bounds: PhysicalRect::new(1920, 0, 1920, 1080),
3208 work_area: PhysicalRect::new(1920, 0, 1920, 1080),
3209 scale_factor: 1.0,
3210 refresh_millihertz: 60_000,
3211 is_primary: false,
3212 });
3213 assert_eq!(shell.monitors().len(), 2);
3214 assert_eq!(drain(&mut shell), vec![ShellEvent::MonitorsChanged]);
3215
3216 shell.unplug_monitor(extra).expect("unplug");
3217 assert_eq!(shell.monitors().len(), 1);
3218 assert_eq!(
3219 drain(&mut shell),
3220 vec![
3221 ShellEvent::MonitorRemoved { monitor: extra },
3222 ShellEvent::MonitorsChanged
3223 ]
3224 );
3225 assert!(matches!(
3226 shell.unplug_monitor(extra),
3227 Err(ShellError::NoSuchMonitor(_))
3228 ));
3229 assert_eq!(
3230 shell.monitor(MonitorId(1)).map(|m| m.name.as_str()),
3231 Some("HEADLESS-1")
3232 );
3233 assert!(shell.monitor(extra).is_none());
3234 }
3235
3236 #[test]
3237 fn unplugging_a_monitor_asks_its_panels_and_only_its_panels_to_close() {
3238 let mut shell = HeadlessShell::new();
3239 let second = shell.plug_monitor(MonitorInfo {
3240 id: MonitorId(0),
3241 name: "HEADLESS-2".to_string(),
3242 bounds: PhysicalRect::new(1920, 0, 1920, 1080),
3243 work_area: PhysicalRect::new(1920, 0, 1920, 1080),
3244 scale_factor: 1.0,
3245 refresh_millihertz: 60_000,
3246 is_primary: false,
3247 });
3248 let first = shell.monitors()[0].id;
3249 let panel_on = |shell: &mut HeadlessShell, monitor| {
3250 shell
3251 .create_window(&WindowDesc {
3252 role: crate::WindowRole::Panel(crate::PanelDesc {
3253 monitor,
3254 ..crate::PanelDesc::default()
3255 }),
3256 ..WindowDesc::default()
3257 })
3258 .expect("panel")
3259 };
3260 let doomed = panel_on(&mut shell, Some(second));
3261 let already_asked = panel_on(&mut shell, Some(second));
3262 let elsewhere = panel_on(&mut shell, Some(first));
3263 let anywhere = panel_on(&mut shell, None);
3264 let toplevel = shell.create_window(&WindowDesc::default()).expect("window");
3265 shell.request_close(already_asked).expect("close");
3266 settle(&mut shell);
3267 drain(&mut shell);
3268
3269 shell.unplug_monitor(second).expect("unplug");
3270 assert_eq!(
3271 drain(&mut shell),
3272 vec![
3273 ShellEvent::MonitorRemoved { monitor: second },
3274 ShellEvent::CloseRequested { window: doomed },
3275 ShellEvent::MonitorsChanged,
3276 ]
3277 );
3278 for window in [elsewhere, anywhere, toplevel] {
3279 assert!(!shell.window_state(window).expect("open").close_pending);
3280 }
3281 }
3282
3283 #[test]
3284 fn drops_require_both_the_capability_and_the_opt_in() {
3285 let mut shell = HeadlessShell::new();
3286 let plain = shell.create_window(&WindowDesc::default()).expect("window");
3287 assert!(matches!(
3288 shell.drop_file(plain, "/tmp/a.gltf", None),
3289 Err(ShellError::Unsupported { .. })
3290 ));
3291
3292 let dropper = shell
3293 .create_window(&WindowDesc {
3294 accept_drops: true,
3295 ..WindowDesc::default()
3296 })
3297 .expect("window");
3298 settle(&mut shell);
3299 shell
3300 .drop_file(dropper, "/tmp/a.gltf", Some(PhysicalPoint::new(4.0, 5.0)))
3301 .expect("drop");
3302 assert_eq!(names(&drain(&mut shell))[0], "DroppedFile");
3303 }
3304
3305 #[test]
3306 fn a_sink_that_queues_more_events_does_not_spin_the_pump() {
3307 let (mut shell, window) = shell_with_configured_window();
3308 shell.inject(ShellEvent::MonitorsChanged);
3309 let mut extra = Vec::new();
3310 shell.pump(&mut |event| extra.push(event));
3311 assert_eq!(extra.len(), 1);
3312 shell.inject(ShellEvent::CloseRequested { window });
3313 assert_eq!(shell.pending_events(), 1);
3314 }
3315
3316 #[test]
3317 fn accessors_that_no_real_backend_can_provide_are_headless_only() {
3318 let (mut shell, window) = shell_with_configured_window();
3319 shell.set_title(window, "renamed").expect("title");
3320 assert_eq!(shell.title(window).expect("title"), "renamed");
3321 shell.set_cursor(window, None).expect("hide");
3322 assert_eq!(shell.cursor(window).expect("cursor"), None);
3323 shell
3324 .set_cursor(window, Some(CursorIcon::Crosshair))
3325 .expect("shape");
3326 assert_eq!(
3327 shell.cursor(window).expect("cursor"),
3328 Some(CursorIcon::Crosshair)
3329 );
3330 shell.set_visible(window, false).expect("hide window");
3331 assert!(!shell.window_state(window).expect("state").visible);
3332 }
3333
3334 #[test]
3335 fn an_input_region_is_recorded_replaced_and_given_back() {
3336 let (mut shell, window) = shell_with_configured_window();
3337 assert_eq!(shell.input_region(window).expect("region"), None);
3338 let panel = PhysicalRect::new(0, 0, 400, 300).rounded(12);
3339 shell
3340 .set_input_region(window, Some(&panel))
3341 .expect("a region");
3342 assert_eq!(
3343 shell.input_region(window).expect("region"),
3344 Some(panel.as_slice())
3345 );
3346 shell.set_input_region(window, Some(&[])).expect("nothing");
3347 assert_eq!(shell.input_region(window).expect("region"), Some(&[][..]));
3348 shell.set_input_region(window, None).expect("whole window");
3349 assert_eq!(shell.input_region(window).expect("region"), None);
3350 shell.destroy_window(window).expect("destroy");
3351 assert!(matches!(
3352 shell.set_input_region(window, None),
3353 Err(ShellError::InvalidWindow { .. })
3354 ));
3355 assert!(shell.input_region(window).is_err());
3356 }
3357
3358 fn frames_for(events: &[ShellEvent], window: WindowId) -> usize {
3359 events
3360 .iter()
3361 .filter(|event| matches!(event, ShellEvent::FrameReady { window: w } if *w == window))
3362 .count()
3363 }
3364
3365 #[test]
3366 fn requests_for_a_frame_share_one_answer_at_the_next_pump() {
3367 let (mut shell, window) = shell_with_configured_window();
3368 assert!(!shell.frame_requested(window).expect("live"));
3369 shell.request_frame(window).expect("a frame");
3370 shell.request_frame(window).expect("the same frame");
3371 assert!(shell.frame_requested(window).expect("live"));
3372 let events = drain(&mut shell);
3373 assert_eq!(frames_for(&events, window), 1, "{:?}", names(&events));
3374 assert!(!shell.frame_requested(window).expect("live"));
3375 assert_eq!(
3376 frames_for(&drain(&mut shell), window),
3377 0,
3378 "nothing asked again"
3379 );
3380 shell.request_frame(window).expect("the next frame");
3381 assert_eq!(frames_for(&drain(&mut shell), window), 1);
3382 }
3383
3384 #[test]
3385 fn a_window_destroyed_with_a_frame_requested_is_not_answered() {
3386 let (mut shell, doomed) = shell_with_configured_window();
3387 let kept = shell
3388 .create_window(&WindowDesc::default())
3389 .expect("a second window");
3390 settle(&mut shell);
3391 shell.request_frame(doomed).expect("a frame");
3392 shell.request_frame(kept).expect("a frame");
3393 shell.destroy_window(doomed).expect("destroy");
3394 let events = drain(&mut shell);
3395 assert_eq!(frames_for(&events, doomed), 0, "{:?}", names(&events));
3396 assert_eq!(frames_for(&events, kept), 1, "the other window still asked");
3397 assert!(matches!(
3398 shell.request_frame(doomed),
3399 Err(ShellError::InvalidWindow { .. })
3400 ));
3401 assert!(shell.frame_requested(doomed).is_err());
3402 }
3403
3404 #[test]
3405 fn a_wait_does_not_sleep_through_a_frame_that_is_due() {
3406 let (mut shell, window) = shell_with_configured_window();
3407 shell.request_frame(window).expect("a frame");
3408 let start = std::time::Instant::now();
3409 shell.wait_events(Some(LONG_WAIT));
3410 assert!(
3411 start.elapsed() < LONG_WAIT / 2,
3412 "slept {:?} with a frame due",
3413 start.elapsed()
3414 );
3415 assert_eq!(frames_for(&drain(&mut shell), window), 1);
3416 }
3417
3418 #[test]
3419 fn an_output_that_is_off_holds_frame_requests_until_it_is_on() {
3420 let (mut shell, window) = shell_with_configured_window();
3421 shell.set_outputs_powered(false);
3422 shell.request_frame(window).expect("a frame");
3423 assert_eq!(frames_for(&drain(&mut shell), window), 0, "held");
3424 assert!(shell.frame_requested(window).expect("live"), "still owed");
3425
3426 let wait = Duration::from_millis(20);
3427 let start = std::time::Instant::now();
3428 shell.wait_events(Some(wait));
3429 assert!(
3430 start.elapsed() >= wait,
3431 "a held request cut the wait short after {:?}",
3432 start.elapsed()
3433 );
3434
3435 shell.set_outputs_powered(true);
3436 assert_eq!(
3437 frames_for(&drain(&mut shell), window),
3438 1,
3439 "answered once on"
3440 );
3441 assert!(!shell.frame_requested(window).expect("live"));
3442 }
3443
3444 #[test]
3445 fn waiting_is_observable_and_focus_transitions_are_events() {
3446 let (mut shell, window) = shell_with_configured_window();
3447 assert_eq!(shell.wait_count(), 0);
3448 shell.wait_events(Some(Duration::from_millis(5)));
3449 shell.wait_events(None);
3450 assert_eq!(shell.wait_count(), 2);
3451
3452 shell.set_focus(window, true).expect("focus");
3453 assert!(shell.window_state(window).expect("state").focused);
3454 shell.set_focus(window, false).expect("unfocus");
3455 assert_eq!(names(&drain(&mut shell)), ["Focus", "Focus"]);
3456 }
3457
3458 const LONG_WAIT: Duration = Duration::from_secs(60);
3463 const RING_AFTER: Duration = Duration::from_millis(50);
3465
3466 fn ring_later(waker: ShellWaker) -> std::thread::JoinHandle<()> {
3468 std::thread::spawn(move || {
3469 std::thread::sleep(RING_AFTER);
3470 waker.wake();
3471 })
3472 }
3473
3474 #[test]
3475 fn a_wake_from_another_thread_ends_a_long_wait() {
3476 let mut shell = HeadlessShell::new();
3477 let ringer = ring_later(shell.waker().expect("headless has a waker"));
3478 let start = std::time::Instant::now();
3479 shell.wait_events(Some(LONG_WAIT));
3480 let slept = start.elapsed();
3481 ringer.join().expect("the ringer");
3482 assert!(slept < LONG_WAIT / 2, "the wait ran {slept:?}");
3483 }
3484
3485 #[test]
3486 fn a_wake_before_the_wait_ends_it_at_once_and_wakes_coalesce() {
3487 let mut shell = HeadlessShell::new();
3488 let waker = shell.waker().expect("headless has a waker");
3489 for _ in 0..3 {
3490 waker.wake();
3491 }
3492 let start = std::time::Instant::now();
3493 shell.wait_events(Some(LONG_WAIT));
3494 assert!(
3495 start.elapsed() < LONG_WAIT / 2,
3496 "a rung bell must end the wait"
3497 );
3498
3499 let timeout = Duration::from_millis(100);
3501 let start = std::time::Instant::now();
3502 shell.wait_events(Some(timeout));
3503 assert!(
3504 start.elapsed() >= timeout / 2,
3505 "the bell was cleared, so this wait must sleep, but it took {:?}",
3506 start.elapsed()
3507 );
3508 }
3509
3510 #[test]
3511 fn with_a_waker_out_an_untimed_wait_sleeps_until_it_rings() {
3512 let mut shell = HeadlessShell::new();
3513 let ringer = ring_later(shell.waker().expect("headless has a waker"));
3514 let start = std::time::Instant::now();
3515 shell.wait_events(None);
3516 let slept = start.elapsed();
3517 ringer.join().expect("the ringer");
3518 assert!(
3519 slept >= RING_AFTER / 2,
3520 "an untimed wait returned after {slept:?}, before anything rang"
3521 );
3522 }
3523
3524 #[test]
3527 fn the_clipboard_round_trips_through_an_event() {
3528 let (mut shell, window) = shell_with_configured_window();
3529 shell
3530 .clipboard_offer(
3531 window,
3532 &[
3533 ClipboardOffer::text("node"),
3534 ClipboardOffer::ron("(kind:\"node\")"),
3535 ],
3536 )
3537 .expect("offer");
3538 assert_eq!(
3539 shell.clipboard_bytes(MimeType::TextUtf8),
3540 Some(&b"node"[..])
3541 );
3542
3543 let request = shell
3544 .clipboard_request(window, MimeType::CrcblRon)
3545 .expect("request");
3546 assert_eq!(
3547 drain(&mut shell)[0],
3548 ShellEvent::ClipboardData {
3549 window,
3550 request,
3551 mime: ReceivedMime::from(MimeType::CrcblRon),
3552 content: ClipboardContent::Bytes(b"(kind:\"node\")".to_vec()),
3553 }
3554 );
3555
3556 let request = shell
3559 .clipboard_request(window, MimeType::UriList)
3560 .expect("request");
3561 assert_eq!(
3562 drain(&mut shell)[0],
3563 ShellEvent::ClipboardData {
3564 window,
3565 request,
3566 mime: ReceivedMime::from(MimeType::UriList),
3567 content: ClipboardContent::Empty,
3568 }
3569 );
3570
3571 shell
3574 .clipboard_offer(window, &[ClipboardOffer::text("")])
3575 .expect("offer");
3576 let request = shell
3577 .clipboard_request(window, MimeType::TextUtf8)
3578 .expect("request");
3579 assert_eq!(
3580 drain(&mut shell)[0],
3581 ShellEvent::ClipboardData {
3582 window,
3583 request,
3584 mime: ReceivedMime::from(MimeType::TextUtf8),
3585 content: ClipboardContent::Bytes(Vec::new()),
3586 }
3587 );
3588 }
3589
3590 #[test]
3591 fn a_read_taken_while_the_clipboard_is_unreadable_is_held_not_answered_empty() {
3592 let (mut shell, window) = shell_with_configured_window();
3598 shell
3599 .clipboard_offer(window, &[ClipboardOffer::text("published")])
3600 .expect("offer");
3601 shell.set_clipboard_readable(false);
3602 assert!(!shell.clipboard_readable(window));
3603
3604 let request = shell
3605 .clipboard_request(window, MimeType::TextUtf8)
3606 .expect("a read is accepted even when it cannot be answered yet");
3607 assert_eq!(shell.held_clipboard_reads(), 1);
3608 assert!(
3609 drain(&mut shell).is_empty(),
3610 "a held read produces no event at all — least of all an empty one"
3611 );
3612
3613 shell.set_clipboard_readable(true);
3615 let events = drain(&mut shell);
3616 assert_eq!(events.len(), 1);
3617 assert_eq!(
3618 events[0],
3619 ShellEvent::ClipboardData {
3620 window,
3621 request,
3622 mime: ReceivedMime::from(MimeType::TextUtf8),
3623 content: ClipboardContent::Bytes(b"published".to_vec()),
3624 }
3625 );
3626 assert_eq!(shell.held_clipboard_reads(), 0);
3627 }
3628
3629 #[test]
3630 fn a_read_that_never_becomes_answerable_ends_as_unavailable() {
3631 let (mut shell, window) = shell_with_configured_window();
3635 shell
3636 .clipboard_offer(window, &[ClipboardOffer::text("unreachable")])
3637 .expect("offer");
3638 shell.set_clipboard_readable(false);
3639 shell.clipboard_deadline(2);
3640
3641 let request = shell
3642 .clipboard_request(window, MimeType::TextUtf8)
3643 .expect("request");
3644 assert!(drain(&mut shell).is_empty(), "pump 1: still held");
3645 assert!(drain(&mut shell).is_empty(), "pump 2: still held");
3646 let events = drain(&mut shell);
3647 assert_eq!(
3648 events[0],
3649 ShellEvent::ClipboardData {
3650 window,
3651 request,
3652 mime: ReceivedMime::from(MimeType::TextUtf8),
3653 content: ClipboardContent::Unavailable,
3656 }
3657 );
3658 assert_eq!(shell.held_clipboard_reads(), 0);
3659 }
3660
3661 #[test]
3662 fn a_peer_that_holds_the_clipboard_and_never_delivers_is_unavailable() {
3663 let (mut shell, window) = shell_with_configured_window();
3667 shell.set_foreign_clipboard(vec![(
3668 ReceivedMime::new("text/plain"),
3669 b"never sent".to_vec(),
3670 )]);
3671 shell.set_clipboard_stalled(true);
3672 shell.clipboard_deadline(1);
3673 assert!(
3674 !shell.clipboard_readable(window),
3675 "a clipboard whose owner will not answer is not readable"
3676 );
3677
3678 let request = shell
3679 .clipboard_request(window, MimeType::TextUtf8)
3680 .expect("request");
3681 assert!(drain(&mut shell).is_empty());
3682 let events = drain(&mut shell);
3683 let ShellEvent::ClipboardData {
3684 request: answered,
3685 content,
3686 ..
3687 } = &events[0]
3688 else {
3689 panic!("wrong variant");
3690 };
3691 assert_eq!(*answered, request);
3692 assert_eq!(*content, ClipboardContent::Unavailable);
3693
3694 shell.set_clipboard_stalled(false);
3696 let request = shell
3697 .clipboard_request(window, MimeType::TextUtf8)
3698 .expect("request");
3699 assert_eq!(
3700 drain(&mut shell)[0],
3701 ShellEvent::ClipboardData {
3702 window,
3703 request,
3704 mime: ReceivedMime::new("text/plain"),
3705 content: ClipboardContent::Bytes(b"never sent".to_vec()),
3706 }
3707 );
3708 }
3709
3710 #[test]
3711 fn claiming_the_clipboard_can_require_a_user_interaction() {
3712 let (mut shell, window) = shell_with_configured_window();
3716 shell.require_user_interaction(true);
3717
3718 let refused = shell.clipboard_offer(window, &[ClipboardOffer::text("stolen")]);
3719 assert!(
3720 matches!(
3721 refused,
3722 Err(ShellError::NeedsUserInteraction {
3723 backend: ShellBackend::Headless,
3724 ..
3725 })
3726 ),
3727 "{refused:?}"
3728 );
3729 assert!(
3730 shell.clipboard_bytes(MimeType::TextUtf8).is_none(),
3731 "a refused claim must not have taken the clipboard anyway"
3732 );
3733
3734 shell.key_press(window, KeyCode::KeyC).expect("key");
3737 shell
3738 .clipboard_offer(window, &[ClipboardOffer::text("copied")])
3739 .expect("with a serial in hand, the claim goes through");
3740 assert_eq!(
3741 shell.clipboard_bytes(MimeType::TextUtf8),
3742 Some(&b"copied"[..])
3743 );
3744
3745 shell.clear_user_interaction();
3747 assert!(matches!(
3748 shell.clipboard_offer(window, &[ClipboardOffer::text("later")]),
3749 Err(ShellError::NeedsUserInteraction { .. })
3750 ));
3751 }
3752
3753 #[test]
3754 fn a_held_read_for_a_destroyed_window_is_dropped_rather_than_answered() {
3755 let (mut shell, window) = shell_with_configured_window();
3758 shell.set_clipboard_readable(false);
3759 let _request = shell
3760 .clipboard_request(window, MimeType::TextUtf8)
3761 .expect("request");
3762 assert_eq!(shell.held_clipboard_reads(), 1);
3763
3764 shell.destroy_window(window).expect("destroy");
3765 let events = drain(&mut shell);
3766 assert_eq!(shell.held_clipboard_reads(), 0);
3767 assert!(
3768 !events
3769 .iter()
3770 .any(|event| matches!(event, ShellEvent::ClipboardData { .. })),
3771 "no clipboard answer may name a destroyed window: {events:?}"
3772 );
3773 }
3774
3775 #[test]
3776 fn a_paste_from_a_foreign_application_keeps_the_foreign_spelling() {
3777 let (mut shell, window) = shell_with_configured_window();
3778 shell.set_foreign_clipboard(vec![
3779 (ReceivedMime::new("text/plain"), b"from gedit".to_vec()),
3782 (
3783 ReceivedMime::new("application/vnd.some-editor.scene+json"),
3784 b"{}".to_vec(),
3785 ),
3786 ]);
3787
3788 let request = shell
3790 .clipboard_request(window, MimeType::TextUtf8)
3791 .expect("request");
3792 let events = drain(&mut shell);
3793 let ShellEvent::ClipboardData {
3794 request: answered,
3795 mime,
3796 content,
3797 ..
3798 } = &events[0]
3799 else {
3800 panic!("wrong variant");
3801 };
3802 assert_eq!(*answered, request);
3803 assert_eq!(content.bytes(), Some(&b"from gedit"[..]));
3804 assert_eq!(mime.as_str(), "text/plain");
3806 assert!(mime.matches(MimeType::TextUtf8));
3807 assert_ne!(*mime, ReceivedMime::from(MimeType::TextUtf8));
3808
3809 assert_eq!(
3812 shell.clipboard_bytes(MimeType::Other("application/vnd.some-editor.scene+json")),
3813 Some(&b"{}"[..])
3814 );
3815 }
3816
3817 #[test]
3820 #[should_panic(expected = "a shell needs at least one monitor")]
3821 fn a_shell_with_no_monitors_is_rejected() {
3822 let _ = HeadlessShell::new().with_monitors(Vec::new());
3823 }
3824
3825 #[test]
3826 #[should_panic(expected = "a text commit is never empty")]
3827 fn an_empty_text_commit_is_rejected() {
3828 let (mut shell, window) = shell_with_configured_window();
3829 let _ = shell.commit_text(window, "");
3830 }
3831}