| 1 | use crate::{ |
| 2 | Result, emit, |
| 3 | ipc::{self, Rect}, |
| 4 | }; |
| 5 | use serde::Deserialize; |
| 6 | use 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 | }; |
| 23 | use std::{ |
| 24 | fs::File, |
| 25 | io::{BufReader, Read}, |
| 26 | os::unix::net::UnixStream, |
| 27 | path::Path, |
| 28 | time::{Duration, Instant}, |
| 29 | }; |
| 30 | use 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 | }; |
| 41 | use wayland_protocols::wp::presentation_time::client::{ |
| 42 | wp_presentation::WpPresentation, |
| 43 | wp_presentation_feedback::{self, WpPresentationFeedback}, |
| 44 | }; |
| 45 | use wayland_protocols::wp::viewporter::client::{ |
| 46 | wp_viewport::WpViewport, wp_viewporter::WpViewporter, |
| 47 | }; |
| 48 | |
| 49 | enum Receipt { |
| 50 | Initial(usize), |
| 51 | Landed, |
| 52 | } |
| 53 | |
| 54 | #[derive(Clone, Copy, PartialEq)] |
| 55 | enum Texture { |
| 56 | None, |
| 57 | Flash, |
| 58 | Image, |
| 59 | } |
| 60 | |
| 61 | #[derive(Deserialize)] |
| 62 | pub 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")] |
| 70 | enum Command { |
| 71 | Close, |
| 72 | Animate { title: String, frame: Rect }, |
| 73 | } |
| 74 | struct 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 | } |
| 88 | struct 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. |
| 110 | fn 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 | } |
| 145 | fn 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 | } |
| 154 | fn 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 | |
| 172 | pub 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 | |
| 430 | impl 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 | } |
| 523 | impl 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 | } |
| 572 | impl 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 | } |
| 600 | impl 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 | } |
| 618 | impl ShmHandler for Feedback { |
| 619 | fn shm_state(&mut self) -> &mut Shm { |
| 620 | &mut self.shm |
| 621 | } |
| 622 | } |
| 623 | impl ProvidesRegistryState for Feedback { |
| 624 | fn registry(&mut self) -> &mut RegistryState { |
| 625 | &mut self.registry |
| 626 | } |
| 627 | smithay_client_toolkit::registry_handlers!(OutputState); |
| 628 | } |
| 629 | delegate_registry!(Feedback); |
| 630 | delegate_output!(Feedback); |
| 631 | delegate_shm!(Feedback); |
| 632 | delegate_layer!(Feedback); |
| 633 | delegate_compositor!(Feedback); |
| 634 | delegate_noop!(Feedback: ignore WlSubcompositor); |
| 635 | delegate_noop!(Feedback: ignore WlSubsurface); |
| 636 | delegate_noop!(Feedback: ignore WpViewporter); |
| 637 | delegate_noop!(Feedback: ignore WpViewport); |
| 638 | delegate_noop!(Feedback: ignore WpPresentation); |
| 639 | impl 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 | } |
| 673 | delegate_noop!(Feedback: ignore wayland_client::protocol::wl_region::WlRegion); |
| 674 | |
| 675 | #[cfg(test)] |
| 676 | mod 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 | } |