1use crate::{
2 Result, emit,
3 ipc::{self, Rect},
4};
5use serde::Deserialize;
6use smithay_client_toolkit::{
7 compositor::{CompositorHandler, CompositorState},
8 delegate_compositor, delegate_layer, delegate_output, delegate_registry, delegate_shm,
9 output::{OutputHandler, OutputState},
10 registry::{ProvidesRegistryState, RegistryState},
11 shell::{
12 WaylandSurface,
13 wlr_layer::{
14 Anchor, KeyboardInteractivity, Layer, LayerShell, LayerShellHandler, LayerSurface,
15 LayerSurfaceConfigure,
16 },
17 },
18 shm::{
19 Shm, ShmHandler,
20 slot::{Buffer, SlotPool},
21 },
22};
23use std::{
24 fs::File,
25 io::{BufReader, Read},
26 os::unix::net::UnixStream,
27 path::Path,
28 time::{Duration, Instant},
29};
30use wayland_client::{
31 Connection, Dispatch, QueueHandle, delegate_noop,
32 globals::registry_queue_init,
33 protocol::{
34 wl_output::{self, WlOutput},
35 wl_shm,
36 wl_subcompositor::WlSubcompositor,
37 wl_subsurface::WlSubsurface,
38 wl_surface::WlSurface,
39 },
40};
41use wayland_protocols::wp::presentation_time::client::{
42 wp_presentation::WpPresentation,
43 wp_presentation_feedback::{self, WpPresentationFeedback},
44};
45use wayland_protocols::wp::viewporter::client::{
46 wp_viewport::WpViewport, wp_viewporter::WpViewporter,
47};
48
49enum Receipt {
50 Initial(usize),
51 Landed,
52}
53
54#[derive(Clone, Copy, PartialEq)]
55enum Texture {
56 None,
57 Flash,
58 Image,
59}
60
61#[derive(Deserialize)]
62pub struct Options {
63 bounds: Rect,
64 pid: u32,
65 flash: bool,
66 animate: bool,
67}
68#[derive(Deserialize)]
69#[serde(tag = "command", rename_all = "lowercase")]
70enum Command {
71 Close,
72 Animate { title: String, frame: Rect },
73}
74struct Overlay {
75 layer: LayerSurface,
76 image: WlSurface,
77 subsurface: WlSubsurface,
78 viewport: WpViewport,
79 _background: WpViewport,
80 bounds: Rect,
81 configured: bool,
82 presented: bool,
83 frame_pending: bool,
84 receipt_pending: bool,
85 texture: Texture,
86 background_attached: bool,
87}
88struct Feedback {
89 registry: RegistryState,
90 output: OutputState,
91 shm: Shm,
92 presentation: WpPresentation,
93 _pool: SlotPool,
94 overlays: Vec<Overlay>,
95 background: Buffer,
96 image: Buffer,
97 flash: Buffer,
98 image_size: (u32, u32),
99 options: Options,
100 start: Instant,
101 flight: Option<(Rect, Instant)>,
102 destination: Option<ipc::Window>,
103 ready: bool,
104 landed: bool,
105 landing_receipts: usize,
106 done: bool,
107}
108
109/// Clip in logical output space, then map the visible portion to source pixels.
110fn clip(rect: Rect, output: Rect, image: (u32, u32)) -> Option<(Rect, Rect)> {
111 if !rect.valid() || !output.valid() || !rect.intersects(output) {
112 return None;
113 }
114 let x = rect.x.max(output.x);
115 let y = rect.y.max(output.y);
116 let right = (rect.x + rect.width).min(output.x + output.width);
117 let bottom = (rect.y + rect.height).min(output.y + output.height);
118 if right - x < 1. || bottom - y < 1. {
119 return None;
120 }
121 // Wayland source rectangles use 24.8 fixed point. Quantize edges together so rounding
122 // can never put the right/bottom edge outside the image at fractional output scales.
123 let left_px = ((x - rect.x) / rect.width * f64::from(image.0) * 256.).floor() / 256.;
124 let top_px = ((y - rect.y) / rect.height * f64::from(image.1) * 256.).floor() / 256.;
125 let right_px = ((right - rect.x) / rect.width * f64::from(image.0) * 256.).floor() / 256.;
126 let bottom_px = ((bottom - rect.y) / rect.height * f64::from(image.1) * 256.).floor() / 256.;
127 if right_px <= left_px || bottom_px <= top_px {
128 return None;
129 }
130 Some((
131 Rect {
132 x: x - output.x,
133 y: y - output.y,
134 width: right - x,
135 height: bottom - y,
136 },
137 Rect {
138 x: left_px,
139 y: top_px,
140 width: right_px - left_px,
141 height: bottom_px - top_px,
142 },
143 ))
144}
145fn interpolate(from: Rect, to: Rect, progress: f64) -> Rect {
146 let t = 1.0 - (1.0 - progress.clamp(0., 1.)).powi(3);
147 Rect {
148 x: from.x + (to.x - from.x) * t,
149 y: from.y + (to.y - from.y) * t,
150 width: from.width + (to.width - from.width) * t,
151 height: from.height + (to.height - from.height) * t,
152 }
153}
154fn target(bounds: Rect, frame: Rect) -> Result<Rect> {
155 if !bounds.valid()
156 || !frame.valid()
157 || frame.x < 0.
158 || frame.y < 0.
159 || frame.x + frame.width > 1.01
160 || frame.y + frame.height > 1.01
161 {
162 return Err("Invalid capture animation destination.".into());
163 }
164 Ok(Rect {
165 x: bounds.x + frame.x * bounds.width,
166 y: bounds.y + frame.y * bounds.height,
167 width: frame.width * bounds.width,
168 height: frame.height * bounds.height,
169 })
170}
171
172pub fn run(directory: &Path, mut options: Options) -> Result<()> {
173 if !options.bounds.valid() {
174 return Err("Invalid capture bounds.".into());
175 }
176 ipc::ensure_unlocked()?;
177 options.animate &= ipc::animations_enabled();
178 let mut events = UnixStream::connect(ipc::session_directory()?.join(".socket2.sock"))?;
179 events.set_nonblocking(true)?;
180 let mut decoder = png::Decoder::new(BufReader::new(File::open(directory.join("capture.png"))?));
181 decoder.set_limits(png::Limits {
182 bytes: 128 * 1024 * 1024,
183 });
184 let mut reader = decoder.read_info()?;
185 let mut bytes = vec![
186 0;
187 reader
188 .output_buffer_size()
189 .ok_or("Invalid capture image size.")?
190 ];
191 let info = reader.next_frame(&mut bytes)?;
192 if info.color_type != png::ColorType::Rgba || info.bit_depth != png::BitDepth::Eight {
193 return Err("Expected RGBA capture image.".into());
194 }
195 let (connection, mut queue, mut state, compositor, layer_shell, subcompositor, viewporter) = {
196 let connection = Connection::connect_to_env()?;
197 let (globals, queue) = registry_queue_init::<Feedback>(&connection)?;
198 let qh = queue.handle();
199 let compositor = CompositorState::bind(&globals, &qh)?;
200 let layer_shell = LayerShell::bind(&globals, &qh)?;
201 let subcompositor: WlSubcompositor = globals.bind(&qh, 1..=1, ())?;
202 let viewporter: WpViewporter = globals.bind(&qh, 1..=1, ())?;
203 let presentation: WpPresentation = globals.bind(&qh, 1..=1, ())?;
204 let shm = Shm::bind(&globals, &qh)?;
205 let mut pool = SlotPool::new(bytes.len() * 2 + 4, &shm)?;
206 let (background, canvas) = pool.create_buffer(1, 1, 4, wl_shm::Format::Argb8888)?;
207 canvas.fill(0);
208 let (image, canvas) = pool.create_buffer(
209 info.width as i32,
210 info.height as i32,
211 info.width as i32 * 4,
212 wl_shm::Format::Argb8888,
213 )?;
214 // Wayland's ARGB8888 is native-endian and premultiplied; PNG is straight RGBA.
215 for (src, dst) in bytes.chunks_exact(4).zip(canvas.chunks_exact_mut(4)) {
216 let a = u32::from(src[3]);
217 dst.copy_from_slice(&[
218 (u32::from(src[2]) * a / 255) as u8,
219 (u32::from(src[1]) * a / 255) as u8,
220 (u32::from(src[0]) * a / 255) as u8,
221 src[3],
222 ]);
223 }
224 let (flash, canvas) = pool.create_buffer(
225 info.width as i32,
226 info.height as i32,
227 info.width as i32 * 4,
228 wl_shm::Format::Argb8888,
229 )?;
230 for (src, dst) in bytes.chunks_exact(4).zip(canvas.chunks_exact_mut(4)) {
231 let a = u32::from(src[3]);
232 for (i, channel) in [2, 1, 0].into_iter().enumerate() {
233 dst[i] = ((u32::from(src[channel]) * 3 + 255) / 4 * a / 255) as u8;
234 }
235 dst[3] = src[3];
236 }
237 let state = Feedback {
238 registry: RegistryState::new(&globals),
239 output: OutputState::new(&globals, &qh),
240 shm,
241 presentation,
242 _pool: pool,
243 overlays: Vec::new(),
244 background,
245 image,
246 flash,
247 image_size: (info.width, info.height),
248 options,
249 start: Instant::now(),
250 flight: None,
251 destination: None,
252 ready: false,
253 landed: false,
254 landing_receipts: 0,
255 done: false,
256 };
257 (
258 connection,
259 queue,
260 state,
261 compositor,
262 layer_shell,
263 subcompositor,
264 viewporter,
265 )
266 };
267 let qh = queue.handle();
268 queue.roundtrip(&mut state)?;
269 queue.roundtrip(&mut state)?;
270 let region = compositor.wl_compositor().create_region(&qh, ());
271 for output in state.output.outputs() {
272 let data = state
273 .output
274 .info(&output)
275 .ok_or("Missing output information.")?;
276 let (x, y) = data.logical_position.ok_or("Missing output position.")?;
277 let (width, height) = data.logical_size.ok_or("Missing output size.")?;
278 if width <= 0 || height <= 0 {
279 continue;
280 }
281 let surface = compositor.create_surface(&qh);
282 surface.set_input_region(Some(&region));
283 let background = viewporter.get_viewport(&surface, &qh, ());
284 background.set_destination(width, height);
285 let image = compositor.create_surface(&qh);
286 image.set_input_region(Some(&region));
287 let viewport = viewporter.get_viewport(&image, &qh, ());
288 let subsurface = subcompositor.get_subsurface(&image, &surface, &qh, ());
289 let layer = layer_shell.create_layer_surface(
290 &qh,
291 surface,
292 Layer::Overlay,
293 Some("t3-snap-shot"),
294 Some(&output),
295 );
296 layer.set_anchor(Anchor::TOP | Anchor::BOTTOM | Anchor::LEFT | Anchor::RIGHT);
297 layer.set_keyboard_interactivity(KeyboardInteractivity::None);
298 layer.set_exclusive_zone(-1);
299 layer.set_size(width as u32, height as u32);
300 layer.commit();
301 state.overlays.push(Overlay {
302 layer,
303 image,
304 subsurface,
305 viewport,
306 _background: background,
307 bounds: Rect {
308 x: x.into(),
309 y: y.into(),
310 width: width.into(),
311 height: height.into(),
312 },
313 configured: false,
314 presented: !state.options.bounds.intersects(Rect {
315 x: x.into(),
316 y: y.into(),
317 width: width.into(),
318 height: height.into(),
319 }),
320 frame_pending: false,
321 receipt_pending: false,
322 texture: Texture::None,
323 background_attached: false,
324 });
325 }
326 region.destroy();
327 if state.overlays.is_empty() {
328 return Err("No output is available for capture effects.".into());
329 }
330 let stdin = std::io::stdin();
331 if state.overlays.iter().all(|o| o.presented) {
332 return Err("The captured window is outside the visible outputs.".into());
333 }
334 let deadline = Instant::now() + Duration::from_secs(8);
335 let mut commands = Vec::new();
336 while !state.done {
337 queue.dispatch_pending(&mut state)?;
338 if state.done {
339 break;
340 }
341 connection.flush()?;
342 let Some(read) = queue.prepare_read() else {
343 continue;
344 };
345 let timeout = rustix::event::Timespec::try_from(
346 deadline
347 .checked_duration_since(Instant::now())
348 .ok_or("Capture feedback expired.")?,
349 )?;
350 let mut fds = [
351 rustix::event::PollFd::new(&connection, rustix::event::PollFlags::IN),
352 rustix::event::PollFd::new(&stdin, rustix::event::PollFlags::IN),
353 rustix::event::PollFd::new(&events, rustix::event::PollFlags::IN),
354 ];
355 if rustix::event::poll(&mut fds, Some(&timeout))? == 0 {
356 break;
357 }
358 if fds[0].revents().intersects(
359 rustix::event::PollFlags::IN
360 | rustix::event::PollFlags::HUP
361 | rustix::event::PollFlags::ERR,
362 ) {
363 read.read()?;
364 } else {
365 drop(read);
366 }
367 let input_ready = fds[1]
368 .revents()
369 .intersects(rustix::event::PollFlags::IN | rustix::event::PollFlags::HUP);
370 let events_ready = !fds[2].revents().is_empty();
371 drop(fds);
372 if events_ready {
373 let mut bytes = [0; 8192];
374 match events.read(&mut bytes) {
375 Ok(0) => break,
376 Ok(_) => {
377 ipc::ensure_unlocked()?;
378 if let Some(destination) = &state.destination {
379 let current = ipc::destination(
380 ipc::windows()?,
381 state.options.pid,
382 &destination.title,
383 )?;
384 if current.as_ref() != Some(destination) {
385 break;
386 }
387 if ipc::active_window()?.address != destination.address {
388 break;
389 }
390 }
391 }
392 Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {}
393 Err(error) => return Err(error.into()),
394 }
395 }
396 if input_ready {
397 let mut buf = [0; 2048];
398 let count = stdin.lock().read(&mut buf)?;
399 if count == 0 {
400 break;
401 }
402 commands.extend_from_slice(&buf[..count]);
403 if commands.len() > 4096 {
404 return Err("Oversized feedback command.".into());
405 }
406 while let Some(end) = commands.iter().position(|byte| *byte == b'\n') {
407 let line = commands.drain(..=end).collect::<Vec<_>>();
408 match serde_json::from_slice::<Command>(&line)? {
409 Command::Close => state.done = true,
410 Command::Animate { title, frame }
411 if state.options.animate && state.flight.is_none() =>
412 {
413 ipc::ensure_unlocked()?;
414 let window = ipc::destination(ipc::windows()?, state.options.pid, &title)?
415 .ok_or("Capture destination is not visible.")?;
416 let dest = target(window.bounds(), frame)?;
417 state.flight = Some((dest, Instant::now()));
418 state.destination = Some(window);
419 state.draw(&qh)?;
420 }
421 _ => {}
422 }
423 }
424 }
425 }
426 // Dropping the connection destroys every overlay, including cancellation/error paths.
427 emit(serde_json::json!({"event":"done"}))
428}
429
430impl Feedback {
431 fn draw(&mut self, qh: &QueueHandle<Self>) -> Result<()> {
432 let progress = self
433 .flight
434 .map(|(_, start)| start.elapsed().as_secs_f64() / 0.48);
435 let rect = self
436 .flight
437 .map(|(to, _)| interpolate(self.options.bounds, to, progress.unwrap_or(0.)))
438 .unwrap_or(self.options.bounds);
439 let flashing = self.options.flash
440 && self.start.elapsed() < Duration::from_millis(130)
441 && self.flight.is_none();
442 let visible = self.options.animate || flashing;
443 let driver = self
444 .overlays
445 .iter()
446 .position(|o| o.bounds.intersects(rect))
447 .unwrap_or(0);
448 let landing = progress.is_some_and(|p| p >= 1.) && !self.landed;
449 for (index, overlay) in self.overlays.iter_mut().enumerate() {
450 if !overlay.configured {
451 continue;
452 }
453 let surface = overlay.layer.wl_surface();
454 if let Some((dest, src)) =
455 clip(rect, overlay.bounds, self.image_size).filter(|_| visible)
456 {
457 overlay
458 .viewport
459 .set_source(src.x, src.y, src.width, src.height);
460 overlay.viewport.set_destination(
461 dest.width.round().max(1.) as i32,
462 dest.height.round().max(1.) as i32,
463 );
464 overlay
465 .subsurface
466 .set_position(dest.x.round() as i32, dest.y.round() as i32);
467 // Immutable textures are uploaded only when they change. Flight frames update
468 // the viewport and subsurface position, not megabytes of shared-memory pixels.
469 let texture = if flashing {
470 Texture::Flash
471 } else {
472 Texture::Image
473 };
474 if overlay.texture != texture {
475 overlay.image.attach(
476 Some((if flashing { &self.flash } else { &self.image }).wl_buffer()),
477 0,
478 0,
479 );
480 overlay.image.damage_buffer(0, 0, i32::MAX, i32::MAX);
481 overlay.texture = texture;
482 }
483 } else {
484 if overlay.texture != Texture::None {
485 overlay.image.attach(None, 0, 0);
486 overlay.texture = Texture::None;
487 }
488 }
489 overlay.image.commit();
490 if !overlay.background_attached {
491 surface.attach(Some(self.background.wl_buffer()), 0, 0);
492 overlay.background_attached = true;
493 }
494 if !overlay.presented && !overlay.receipt_pending {
495 self.presentation
496 .feedback(surface, qh, Receipt::Initial(index));
497 overlay.receipt_pending = true;
498 }
499 if landing && overlay.bounds.intersects(rect) {
500 self.presentation.feedback(surface, qh, Receipt::Landed);
501 self.landing_receipts += 1;
502 }
503 if !overlay.frame_pending
504 && self.ready
505 && index == driver
506 && (flashing || progress.is_some_and(|p| p < 1.))
507 {
508 surface.frame(qh, surface.clone());
509 overlay.frame_pending = true;
510 }
511 surface.commit();
512 }
513 if landing {
514 // Presentation feedback, not a frame callback, acknowledges the final painted image.
515 self.landed = true;
516 if self.landing_receipts == 0 {
517 self.done = true;
518 }
519 }
520 Ok(())
521 }
522}
523impl CompositorHandler for Feedback {
524 fn scale_factor_changed(
525 &mut self,
526 _: &Connection,
527 _: &QueueHandle<Self>,
528 _: &WlSurface,
529 _: i32,
530 ) {
531 }
532 fn transform_changed(
533 &mut self,
534 _: &Connection,
535 _: &QueueHandle<Self>,
536 _: &WlSurface,
537 _: wl_output::Transform,
538 ) {
539 }
540 fn surface_enter(
541 &mut self,
542 _: &Connection,
543 _: &QueueHandle<Self>,
544 _: &WlSurface,
545 _: &WlOutput,
546 ) {
547 }
548 fn surface_leave(
549 &mut self,
550 _: &Connection,
551 _: &QueueHandle<Self>,
552 _: &WlSurface,
553 _: &WlOutput,
554 ) {
555 }
556 fn frame(&mut self, _: &Connection, qh: &QueueHandle<Self>, surface: &WlSurface, _: u32) {
557 if self.landed {
558 return;
559 }
560 if let Some(overlay) = self
561 .overlays
562 .iter_mut()
563 .find(|o| o.layer.wl_surface() == surface)
564 {
565 overlay.frame_pending = false;
566 }
567 if self.ready && self.draw(qh).is_err() {
568 self.done = true;
569 }
570 }
571}
572impl LayerShellHandler for Feedback {
573 fn closed(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &LayerSurface) {
574 self.done = true;
575 }
576 fn configure(
577 &mut self,
578 _: &Connection,
579 qh: &QueueHandle<Self>,
580 layer: &LayerSurface,
581 config: LayerSurfaceConfigure,
582 _: u32,
583 ) {
584 if let Some(overlay) = self.overlays.iter_mut().find(|o| o.layer == *layer) {
585 if overlay.configured
586 || (config.new_size.0 != 0
587 && config.new_size
588 != (overlay.bounds.width as u32, overlay.bounds.height as u32))
589 {
590 self.done = true;
591 return;
592 }
593 overlay.configured = true;
594 }
595 if self.overlays.iter().all(|o| o.configured) && self.draw(qh).is_err() {
596 self.done = true;
597 }
598 }
599}
600impl OutputHandler for Feedback {
601 fn output_state(&mut self) -> &mut OutputState {
602 &mut self.output
603 }
604 fn new_output(&mut self, _: &Connection, _: &QueueHandle<Self>, _: WlOutput) {
605 if !self.overlays.is_empty() {
606 self.done = true;
607 }
608 }
609 fn update_output(&mut self, _: &Connection, _: &QueueHandle<Self>, _: WlOutput) {
610 if self.ready {
611 self.done = true;
612 }
613 }
614 fn output_destroyed(&mut self, _: &Connection, _: &QueueHandle<Self>, _: WlOutput) {
615 self.done = true;
616 }
617}
618impl ShmHandler for Feedback {
619 fn shm_state(&mut self) -> &mut Shm {
620 &mut self.shm
621 }
622}
623impl ProvidesRegistryState for Feedback {
624 fn registry(&mut self) -> &mut RegistryState {
625 &mut self.registry
626 }
627 smithay_client_toolkit::registry_handlers!(OutputState);
628}
629delegate_registry!(Feedback);
630delegate_output!(Feedback);
631delegate_shm!(Feedback);
632delegate_layer!(Feedback);
633delegate_compositor!(Feedback);
634delegate_noop!(Feedback: ignore WlSubcompositor);
635delegate_noop!(Feedback: ignore WlSubsurface);
636delegate_noop!(Feedback: ignore WpViewporter);
637delegate_noop!(Feedback: ignore WpViewport);
638delegate_noop!(Feedback: ignore WpPresentation);
639impl Dispatch<WpPresentationFeedback, Receipt> for Feedback {
640 fn event(
641 state: &mut Self,
642 _: &WpPresentationFeedback,
643 event: wp_presentation_feedback::Event,
644 receipt: &Receipt,
645 _: &Connection,
646 qh: &QueueHandle<Self>,
647 ) {
648 match event {
649 wp_presentation_feedback::Event::Presented { .. } => {
650 match receipt {
651 Receipt::Initial(index) => {
652 state.overlays[*index].presented = true;
653 if !state.ready && state.overlays.iter().all(|o| o.presented) {
654 state.ready = true;
655 if emit(serde_json::json!({"event":"ready","animate":state.options.animate})).is_err() || state.draw(qh).is_err() {state.done=true;}
656 }
657 }
658 Receipt::Landed => {
659 state.landing_receipts = state.landing_receipts.saturating_sub(1);
660 if state.landing_receipts == 0
661 && emit(serde_json::json!({"event":"landed"})).is_err()
662 {
663 state.done = true;
664 }
665 }
666 }
667 }
668 wp_presentation_feedback::Event::Discarded => state.done = true,
669 _ => {}
670 }
671 }
672}
673delegate_noop!(Feedback: ignore wayland_client::protocol::wl_region::WlRegion);
674
675#[cfg(test)]
676mod tests {
677 use super::*;
678 #[test]
679 fn clips_across_negative_origin_and_scaled_outputs() {
680 let rect = Rect {
681 x: -100.,
682 y: 0.,
683 width: 200.,
684 height: 100.,
685 };
686 let (dest, src) = clip(
687 rect,
688 Rect {
689 x: 0.,
690 y: 0.,
691 width: 1920.,
692 height: 1080.,
693 },
694 (400, 200),
695 )
696 .unwrap();
697 assert_eq!(
698 dest,
699 Rect {
700 x: 0.,
701 y: 0.,
702 width: 100.,
703 height: 100.
704 }
705 );
706 assert_eq!(
707 src,
708 Rect {
709 x: 200.,
710 y: 0.,
711 width: 200.,
712 height: 200.
713 }
714 );
715 assert!(
716 clip(
717 rect,
718 Rect {
719 x: 200.,
720 y: 0.,
721 width: 100.,
722 height: 100.
723 },
724 (400, 200)
725 )
726 .is_none()
727 );
728 }
729 #[test]
730 fn flight_ends_at_exact_composer_bounds() {
731 let a = Rect {
732 x: -300.,
733 y: 0.,
734 width: 600.,
735 height: 500.,
736 };
737 let b = Rect {
738 x: 100.,
739 y: 400.,
740 width: 100.,
741 height: 80.,
742 };
743 assert_eq!(interpolate(a, b, 0.), a);
744 assert_eq!(interpolate(a, b, 1.), b);
745 assert_eq!(
746 target(
747 a,
748 Rect {
749 x: 0.5,
750 y: 0.4,
751 width: 0.2,
752 height: 0.1
753 }
754 )
755 .unwrap(),
756 Rect {
757 x: 0.,
758 y: 200.,
759 width: 120.,
760 height: 50.
761 }
762 );
763 assert!(
764 target(
765 a,
766 Rect {
767 x: 590.,
768 y: 0.,
769 width: 30.,
770 height: 30.
771 }
772 )
773 .is_err()
774 );
775 }
776}