1use crate::render::{DmabufImporter, Gpu};
16use crate::stream;
17use crate::theme::Theme;
18use crate::tr;
19use crate::wl;
20use smithay_client_toolkit::{
21 compositor::{CompositorHandler, CompositorState},
22 delegate_compositor, delegate_keyboard, delegate_output, delegate_pointer, delegate_registry,
23 delegate_seat, delegate_xdg_shell, delegate_xdg_window,
24 output::{OutputHandler, OutputState},
25 reexports::calloop::EventLoop,
26 reexports::calloop::channel::{Channel, Event as ChannelEvent, channel},
27 reexports::calloop_wayland_source::WaylandSource,
28 reexports::protocols::xdg::shell::client::xdg_toplevel::ResizeEdge,
29 registry::{ProvidesRegistryState, RegistryState},
30 registry_handlers,
31 seat::{
32 Capability, SeatHandler, SeatState,
33 keyboard::{KeyEvent, KeyboardHandler, Keysym, Modifiers, RawModifiers},
34 pointer::{PointerEvent, PointerEventKind, PointerHandler},
35 },
36 shell::{
37 WaylandSurface,
38 xdg::{
39 XdgShell,
40 window::{Window, WindowConfigure, WindowDecorations, WindowHandler},
41 },
42 },
43};
44use std::time::{Duration, Instant};
45use wayland_client::{
46 Connection, QueueHandle,
47 globals::registry_queue_init,
48 protocol::{wl_keyboard, wl_output, wl_pointer, wl_seat, wl_surface},
49};
50
51const KEY: &str = "pip";
53const DEFAULT_W: u32 = 480;
55const BADGE: u32 = 132;
57const MIN_W: u32 = 120;
59const ACCENT_FRAMES: u32 = 60;
61const GONE_LINGER: Duration = Duration::from_millis(1400);
63const ROUND: Duration = Duration::from_millis(33);
65const APPEAR_GRACE: Duration = Duration::from_secs(5);
67
68struct Content {
70 tex: Option<egui::TextureHandle>,
72 native: Option<(egui::TextureId, egui::Vec2)>,
74 icon: Option<egui::TextureHandle>,
75 pending: Vec<PipMsg>,
77 gone: bool,
78 label: String,
79 theme: Theme,
80 crop_uv: Option<egui::Rect>,
83}
84
85impl Content {
86 fn pump(&mut self, ctx: &egui::Context, importer: &mut dyn DmabufImporter) {
89 for msg in self.pending.drain(..) {
90 match msg {
91 PipMsg::Shm { w, h, rgba } if w > 0 && h > 0 => {
92 let img = egui::ColorImage::from_rgba_unmultiplied([w, h], &rgba);
93 match self.tex.as_mut() {
94 Some(t) => t.set(img, egui::TextureOptions::LINEAR),
95 None => {
96 self.tex =
97 Some(ctx.load_texture(KEY, img, egui::TextureOptions::LINEAR))
98 }
99 }
100 self.native = None; }
102 PipMsg::Dmabuf { frame } => {
103 if let Some(t) = importer.import(KEY, frame) {
104 self.native = Some(t);
105 }
106 }
107 PipMsg::Shm { .. } => {}
108 PipMsg::Gone => self.gone = true,
109 }
110 }
111 }
112
113 fn tex(&self) -> Option<(egui::TextureId, egui::Vec2)> {
115 if let Some(t) = self.native {
116 return Some(t);
117 }
118 self.tex.as_ref().map(|t| (t.id(), t.size_vec2()))
119 }
120
121 #[allow(clippy::too_many_arguments)]
123 fn draw(
124 &self,
125 ui: &mut egui::Ui,
126 size: (f32, f32),
127 collapsed: bool,
128 hovered: bool,
129 accent: bool,
130 frozen: bool,
131 opacity: f32,
132 ) {
133 let (w, h) = size;
134 let full = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(w, h));
135 let t = &self.theme;
136 let tint = egui::Color32::from_white_alpha((opacity.clamp(0.0, 1.0) * 255.0) as u8);
138 egui::CentralPanel::default()
139 .frame(egui::Frame::NONE)
140 .show_inside(ui, |ui| {
141 let p = ui.painter();
142 if let Some((id, ts)) = self.tex() {
146 let uv = self.crop_uv.unwrap_or(egui::Rect::from_min_max(
147 egui::pos2(0.0, 0.0),
148 egui::pos2(1.0, 1.0),
149 ));
150 let src = egui::vec2(ts.x * uv.width(), ts.y * uv.height());
151 let scale = (full.width() / src.x).min(full.height() / src.y);
152 let draw = egui::Rect::from_center_size(full.center(), src * scale);
153 p.image(id, draw, uv, tint);
154 } else if let Some(icon) = &self.icon {
155 let isz = icon.size_vec2();
156 let scale = (full.width() / isz.x).min(full.height() / isz.y).min(1.0);
157 let draw = egui::Rect::from_center_size(full.center(), isz * scale);
158 p.image(
159 icon.id(),
160 draw,
161 egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0)),
162 tint,
163 );
164 } else {
165 p.text(
166 full.center(),
167 egui::Align2::CENTER_CENTER,
168 tr!("loading"),
169 egui::FontId::proportional(16.0),
170 t.text_dim,
171 );
172 }
173
174 if self.gone {
175 p.rect_filled(full, 0.0, egui::Color32::from_black_alpha(180));
176 p.text(
177 full.center(),
178 egui::Align2::CENTER_CENTER,
179 tr!("pip-gone"),
180 egui::FontId::proportional(16.0),
181 t.text,
182 );
183 return;
184 }
185
186 if accent {
188 p.rect_stroke(
189 full,
190 0.0,
191 egui::Stroke::new(2.0, t.window_accent),
192 egui::StrokeKind::Inside,
193 );
194 }
195
196 if frozen {
198 let bar = egui::Stroke::new(3.0, egui::Color32::from_white_alpha(220));
199 let (x, y) = (8.0, 8.0);
200 p.line_segment([egui::pos2(x, y), egui::pos2(x, y + 12.0)], bar);
201 p.line_segment([egui::pos2(x + 6.0, y), egui::pos2(x + 6.0, y + 12.0)], bar);
202 }
203
204 if collapsed {
205 return; }
207
208 if hovered {
209 let (close, collapse) = toolbar_rects(w);
210 let strip = egui::Rect::from_min_max(
211 egui::pos2(0.0, 0.0),
212 egui::pos2(w, close.bottom() + 6.0),
213 );
214 p.rect_filled(strip, 0.0, egui::Color32::from_black_alpha(150));
215 let mut job = egui::text::LayoutJob::simple_singleline(
217 self.label.clone(),
218 egui::FontId::proportional(12.0),
219 egui::Color32::WHITE,
220 );
221 job.wrap = egui::text::TextWrapping::truncate_at_width(
222 (collapse.left() - 12.0).max(0.0),
223 );
224 let galley = ui.painter().layout_job(job);
225 p.galley(
226 egui::pos2(8.0, strip.center().y - galley.size().y / 2.0),
227 galley,
228 egui::Color32::WHITE,
229 );
230 draw_collapse(p, collapse, egui::Color32::WHITE);
232 draw_close(p, close, egui::Color32::WHITE);
233 draw_grip(p, grip_rect(w, h), egui::Color32::from_white_alpha(160));
235 }
236 });
237 }
238}
239
240fn min_size_for(aspect: Option<f32>) -> (u32, u32) {
243 match aspect {
244 Some(a) if a > 0.0 => (MIN_W, ((MIN_W as f32 / a).round() as u32).max(1)),
245 _ => (MIN_W, (MIN_W * 9 / 16).max(1)),
246 }
247}
248
249fn toolbar_rects(w: f32) -> (egui::Rect, egui::Rect) {
251 let s = 22.0;
252 let pad = 6.0;
253 let close = egui::Rect::from_min_size(egui::pos2(w - pad - s, pad), egui::vec2(s, s));
254 let collapse =
255 egui::Rect::from_min_size(egui::pos2(w - pad - 2.0 * s - 6.0, pad), egui::vec2(s, s));
256 (close, collapse)
257}
258
259fn grip_rect(w: f32, h: f32) -> egui::Rect {
261 let s = 18.0;
262 egui::Rect::from_min_size(egui::pos2(w - s, h - s), egui::vec2(s, s))
263}
264
265fn draw_close(p: &egui::Painter, r: egui::Rect, c: egui::Color32) {
266 let r = r.shrink(5.0);
267 let s = egui::Stroke::new(2.0, c);
268 p.line_segment([r.left_top(), r.right_bottom()], s);
269 p.line_segment([r.right_top(), r.left_bottom()], s);
270}
271
272fn draw_collapse(p: &egui::Painter, r: egui::Rect, c: egui::Color32) {
273 let r = r.shrink(5.0);
274 let s = egui::Stroke::new(2.0, c);
275 p.line_segment([r.left_top(), egui::pos2(r.center().x, r.bottom())], s);
277 p.line_segment([egui::pos2(r.center().x, r.bottom()), r.right_top()], s);
278}
279
280fn draw_grip(p: &egui::Painter, r: egui::Rect, c: egui::Color32) {
281 let s = egui::Stroke::new(1.5, c);
282 for f in [0.35_f32, 0.7] {
283 p.line_segment(
284 [
285 egui::pos2(r.right() - r.width() * f, r.bottom()),
286 egui::pos2(r.right(), r.bottom() - r.height() * f),
287 ],
288 s,
289 );
290 }
291}
292
293struct State {
294 registry_state: RegistryState,
295 seat_state: SeatState,
296 output_state: OutputState,
297
298 window: Window,
299 seat: Option<wl_seat::WlSeat>,
300 keyboard: Option<wl_keyboard::WlKeyboard>,
301 pointer: Option<wl_pointer::WlPointer>,
302
303 egui_ctx: egui::Context,
304 gpu: Option<Gpu>,
305 content: Content,
306
307 width: u32,
309 height: u32,
310 scale: u32,
311 aspect: Option<f32>,
312 fixed_aspect: bool,
315 expanded: (u32, u32),
317 collapsed: bool,
318
319 hovered: bool,
320 pointer_pos: egui::Pos2,
321 accent: u32,
322 gone_since: Option<Instant>,
323
324 opacity: f32,
326 frozen: bool,
328
329 start: Instant,
330 closing: bool,
331 configured: bool,
332 relaunch: Vec<String>,
334}
335
336pub enum Source {
338 Toplevel(String),
340 Region {
343 output: String,
345 crop: [f32; 4],
349 region_w: u32,
351 region_h: u32,
353 zoom: f32,
355 },
356 ToplevelRegion {
359 id: String,
361 crop: [f32; 4],
363 region_w: u32,
365 region_h: u32,
367 zoom: f32,
369 },
370}
371
372pub struct Config {
374 pub app_id: String,
376 pub label: String,
378 pub icon: Option<(u32, u32, Vec<u8>)>,
380 pub relaunch: Vec<String>,
383}
384
385pub fn run(source: Source, config: Config) -> anyhow::Result<()> {
387 let conn = Connection::connect_to_env()?;
388 run_on(&conn, source, config)
389}
390
391pub fn run_on(conn: &Connection, source: Source, config: Config) -> anyhow::Result<()> {
396 let Config {
397 app_id,
398 label,
399 icon,
400 relaunch,
401 } = config;
402 let (globals, event_queue) = registry_queue_init::<State>(conn)?;
403 let qh = event_queue.handle();
404 let mut event_loop: EventLoop<State> = EventLoop::try_new()?;
405 let lh = event_loop.handle();
406 WaylandSource::new(conn.clone(), event_queue)
407 .insert(lh.clone())
408 .map_err(|e| anyhow::anyhow!("calloop wayland source: {e}"))?;
409
410 let compositor =
411 CompositorState::bind(&globals, &qh).map_err(|e| anyhow::anyhow!("wl_compositor: {e}"))?;
412 let xdg_shell =
413 XdgShell::bind(&globals, &qh).map_err(|e| anyhow::anyhow!("xdg-shell missing: {e}"))?;
414
415 let (init_w, init_h, fixed_aspect, aspect0, crop_uv) = match &source {
419 Source::Toplevel(_) => (DEFAULT_W, DEFAULT_W * 9 / 16, false, None, None),
420 Source::Region {
421 crop,
422 region_w,
423 region_h,
424 zoom,
425 ..
426 }
427 | Source::ToplevelRegion {
428 crop,
429 region_w,
430 region_h,
431 zoom,
432 ..
433 } => {
434 let w = ((*region_w as f32 * zoom).round() as u32).max(MIN_W);
435 let h = ((*region_h as f32 * zoom).round() as u32).max(1);
436 let aspect = *region_w as f32 / (*region_h).max(1) as f32;
437 let uv = egui::Rect::from_min_max(
438 egui::pos2(crop[0], crop[1]),
439 egui::pos2(crop[2], crop[3]),
440 );
441 (w, h, true, Some(aspect), Some(uv))
442 }
443 };
444 let min0 = min_size_for(aspect0);
445
446 let surface = compositor.create_surface(&qh);
447 let window = xdg_shell.create_window(surface, WindowDecorations::RequestServer, &qh);
448 window.set_app_id(&app_id);
449 window.set_title(label.clone());
450 window.set_min_size(Some(min0));
451 window.commit();
452
453 let stream_source = match &source {
456 Source::Toplevel(id) | Source::ToplevelRegion { id, .. } => {
457 stream::Source::Toplevel(id.clone())
458 }
459 Source::Region { output, .. } => stream::Source::Output(output.clone()),
460 };
461 let (tx, ch): (_, Channel<PipMsg>) = channel();
462 std::thread::spawn(move || capture_thread(stream_source, move |m| tx.send(m).is_ok()));
463 lh.insert_source(ch, |event, _, state: &mut State| {
464 if let ChannelEvent::Msg(m) = event {
465 state.on_msg(m);
466 }
467 })
468 .map_err(|e| anyhow::anyhow!("calloop channel source: {e}"))?;
469
470 let theme = Theme::load();
471 let egui_ctx = egui::Context::default();
472 theme.apply(&egui_ctx);
473 let icon_tex = icon.map(|(w, h, rgba)| {
474 let img = egui::ColorImage::from_rgba_unmultiplied([w as usize, h as usize], &rgba);
475 egui_ctx.load_texture("pip-icon", img, egui::TextureOptions::LINEAR)
476 });
477
478 let mut state = State {
479 registry_state: RegistryState::new(&globals),
480 seat_state: SeatState::new(&globals, &qh),
481 output_state: OutputState::new(&globals, &qh),
482 window,
483 seat: None,
484 keyboard: None,
485 pointer: None,
486 egui_ctx,
487 gpu: None,
488 content: Content {
489 tex: None,
490 native: None,
491 icon: icon_tex,
492 pending: Vec::new(),
493 gone: false,
494 label,
495 theme,
496 crop_uv,
497 },
498 width: init_w,
499 height: init_h,
500 scale: 1,
501 aspect: aspect0,
502 fixed_aspect,
503 expanded: (init_w, init_h),
504 collapsed: false,
505 hovered: false,
506 pointer_pos: egui::Pos2::ZERO,
507 accent: 0,
508 gone_since: None,
509 opacity: 1.0,
510 frozen: false,
511 start: Instant::now(),
512 closing: false,
513 configured: false,
514 relaunch,
515 };
516
517 while !state.closing {
518 event_loop.dispatch(Duration::from_millis(400), &mut state)?;
519 if let Some(t) = state.gone_since
520 && t.elapsed() >= GONE_LINGER
521 {
522 break;
523 }
524 }
525 Ok(())
526}
527
528impl State {
529 fn on_msg(&mut self, m: PipMsg) {
532 match &m {
533 PipMsg::Gone => {
534 self.gone_since.get_or_insert(Instant::now());
535 self.content.pending.push(m);
536 self.redraw();
537 return;
538 }
539 PipMsg::Shm { w, h, .. } => self.on_source_size(*w as u32, *h as u32),
540 PipMsg::Dmabuf { frame } => self.on_source_size(frame.width, frame.height),
541 }
542 if self.frozen {
546 return;
547 }
548 if self.collapsed {
550 self.set_collapsed(false);
551 self.accent = ACCENT_FRAMES;
552 }
553 self.content.pending.push(m);
554 self.redraw();
555 }
556
557 fn on_source_size(&mut self, sw: u32, sh: u32) {
560 if self.fixed_aspect || sw == 0 || sh == 0 {
563 return;
564 }
565 let a = sw as f32 / sh as f32;
566 let first = self.aspect.is_none();
567 self.aspect = Some(a);
568 if !self.collapsed && (first || (self.height as f32 - self.width as f32 / a).abs() > 1.0) {
569 let h = ((self.width as f32 / a).round() as u32).max(1);
570 self.apply_size(self.width, h);
571 self.expanded = (self.width, h);
572 }
573 }
574
575 fn snap(&self, w: u32, h: u32) -> (u32, u32) {
577 match self.aspect {
578 Some(a) if a > 0.0 => (w.max(MIN_W), ((w as f32 / a).round() as u32).max(1)),
579 _ => (w.max(MIN_W), h.max(1)),
580 }
581 }
582
583 fn apply_size(&mut self, w: u32, h: u32) {
586 self.width = w;
587 self.height = h;
588 if let Some(gpu) = &self.gpu {
589 gpu.resize((w * self.scale) as i32, (h * self.scale) as i32);
590 }
591 }
592
593 fn set_collapsed(&mut self, collapsed: bool) {
594 if collapsed == self.collapsed {
595 return;
596 }
597 self.collapsed = collapsed;
598 if collapsed {
599 self.expanded = (self.width, self.height);
600 self.window.set_min_size(Some((BADGE, BADGE)));
601 self.window.set_max_size(Some((BADGE, BADGE)));
602 self.apply_size(BADGE, BADGE);
603 } else {
604 self.window.set_min_size(Some(min_size_for(self.aspect)));
605 self.window.set_max_size(None);
606 let (w, h) = self.expanded;
607 self.apply_size(w, h);
608 }
609 self.window.commit();
610 self.redraw();
611 }
612
613 fn ensure_gpu(&mut self, conn: &Connection) {
614 if self.gpu.is_some() || self.width == 0 {
615 return;
616 }
617 let (pw, ph) = (
618 (self.width * self.scale) as i32,
619 (self.height * self.scale) as i32,
620 );
621 self.gpu = Some(Gpu::new(conn, self.window.wl_surface(), pw, ph));
622 }
623
624 fn redraw(&mut self) {
626 if !self.configured {
627 return;
628 }
629 let (pw, ph) = (self.width * self.scale, self.height * self.scale);
630 let raw_input = egui::RawInput {
631 screen_rect: Some(egui::Rect::from_min_size(
632 egui::Pos2::ZERO,
633 egui::vec2(self.width as f32, self.height as f32),
634 )),
635 time: Some(self.start.elapsed().as_secs_f64()),
636 focused: true,
637 ..Default::default()
638 };
639 let opacity = self.opacity;
640 let backdrop = {
641 let c = self.content.theme.thumb.to_normalized_gamma_f32();
642 [c[0], c[1], c[2], opacity]
643 };
644 let size = (self.width as f32, self.height as f32);
645 let (collapsed, hovered, accent) = (self.collapsed, self.hovered, self.accent > 0);
646 let frozen = self.frozen;
647 let content = &mut self.content;
648 let Some(gpu) = self.gpu.as_mut() else {
649 return;
650 };
651 gpu.render(
652 &self.egui_ctx,
653 raw_input,
654 self.scale as f32,
655 (pw, ph),
656 backdrop,
657 |ui, imp| {
658 let ctx = ui.ctx().clone();
659 content.pump(&ctx, imp);
660 content.draw(ui, size, collapsed, hovered, accent, frozen, opacity);
661 },
662 );
663 if self.accent > 0 {
664 self.accent -= 1;
665 }
666 }
667}
668
669impl CompositorHandler for State {
670 fn scale_factor_changed(
671 &mut self,
672 _: &Connection,
673 _: &QueueHandle<Self>,
674 _: &wl_surface::WlSurface,
675 new_factor: i32,
676 ) {
677 self.scale = new_factor.max(1) as u32;
678 self.window.wl_surface().set_buffer_scale(new_factor.max(1));
679 if let Some(gpu) = &self.gpu {
680 gpu.resize(
681 (self.width * self.scale) as i32,
682 (self.height * self.scale) as i32,
683 );
684 }
685 self.redraw();
686 }
687
688 fn transform_changed(
689 &mut self,
690 _: &Connection,
691 _: &QueueHandle<Self>,
692 _: &wl_surface::WlSurface,
693 _: wayland_client::protocol::wl_output::Transform,
694 ) {
695 }
696
697 fn frame(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &wl_surface::WlSurface, _: u32) {
698 }
700
701 fn surface_enter(
702 &mut self,
703 _: &Connection,
704 _: &QueueHandle<Self>,
705 _: &wl_surface::WlSurface,
706 _: &wl_output::WlOutput,
707 ) {
708 }
709 fn surface_leave(
710 &mut self,
711 _: &Connection,
712 _: &QueueHandle<Self>,
713 _: &wl_surface::WlSurface,
714 _: &wl_output::WlOutput,
715 ) {
716 }
717}
718
719impl WindowHandler for State {
720 fn request_close(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &Window) {
721 self.closing = true;
722 }
723
724 fn configure(
725 &mut self,
726 conn: &Connection,
727 _: &QueueHandle<Self>,
728 _: &Window,
729 configure: WindowConfigure,
730 _: u32,
731 ) {
732 if let (Some(w), Some(h)) = configure.new_size {
733 let (w, h) = if self.collapsed {
734 (BADGE, BADGE)
735 } else {
736 self.snap(w.get(), h.get())
737 };
738 self.apply_size(w, h);
739 if !self.collapsed {
740 self.expanded = (w, h);
741 }
742 }
743 self.ensure_gpu(conn);
744 self.configured = true;
745 self.redraw();
746 }
747}
748
749impl SeatHandler for State {
750 fn seat_state(&mut self) -> &mut SeatState {
751 &mut self.seat_state
752 }
753 fn new_seat(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_seat::WlSeat) {}
754 fn new_capability(
755 &mut self,
756 _: &Connection,
757 qh: &QueueHandle<Self>,
758 seat: wl_seat::WlSeat,
759 cap: Capability,
760 ) {
761 if cap == Capability::Keyboard && self.keyboard.is_none() {
762 self.keyboard = self.seat_state.get_keyboard(qh, &seat, None).ok();
763 }
764 if cap == Capability::Pointer && self.pointer.is_none() {
765 self.pointer = self.seat_state.get_pointer(qh, &seat).ok();
766 }
767 self.seat = Some(seat);
768 }
769 fn remove_capability(
770 &mut self,
771 _: &Connection,
772 _: &QueueHandle<Self>,
773 _: wl_seat::WlSeat,
774 _: Capability,
775 ) {
776 }
777 fn remove_seat(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_seat::WlSeat) {}
778}
779
780impl KeyboardHandler for State {
781 fn enter(
782 &mut self,
783 _: &Connection,
784 _: &QueueHandle<Self>,
785 _: &wl_keyboard::WlKeyboard,
786 _: &wl_surface::WlSurface,
787 _: u32,
788 _: &[u32],
789 _: &[Keysym],
790 ) {
791 }
792 fn leave(
793 &mut self,
794 _: &Connection,
795 _: &QueueHandle<Self>,
796 _: &wl_keyboard::WlKeyboard,
797 _: &wl_surface::WlSurface,
798 _: u32,
799 ) {
800 }
801 fn press_key(
802 &mut self,
803 _: &Connection,
804 _: &QueueHandle<Self>,
805 _: &wl_keyboard::WlKeyboard,
806 _: u32,
807 event: KeyEvent,
808 ) {
809 self.on_key(event.keysym);
810 }
811 fn release_key(
812 &mut self,
813 _: &Connection,
814 _: &QueueHandle<Self>,
815 _: &wl_keyboard::WlKeyboard,
816 _: u32,
817 _: KeyEvent,
818 ) {
819 }
820 fn repeat_key(
821 &mut self,
822 _: &Connection,
823 _: &QueueHandle<Self>,
824 _: &wl_keyboard::WlKeyboard,
825 _: u32,
826 _: KeyEvent,
827 ) {
828 }
829 fn update_modifiers(
830 &mut self,
831 _: &Connection,
832 _: &QueueHandle<Self>,
833 _: &wl_keyboard::WlKeyboard,
834 _: u32,
835 _: Modifiers,
836 _: RawModifiers,
837 _: u32,
838 ) {
839 }
840}
841
842impl PointerHandler for State {
843 fn pointer_frame(
844 &mut self,
845 _: &Connection,
846 _: &QueueHandle<Self>,
847 _: &wl_pointer::WlPointer,
848 events: &[PointerEvent],
849 ) {
850 let Some(seat) = self.seat.clone() else {
851 return;
852 };
853 for e in events {
854 let pos = egui::pos2(e.position.0 as f32, e.position.1 as f32);
855 match e.kind {
856 PointerEventKind::Enter { .. } => {
857 self.pointer_pos = pos;
858 self.hovered = true;
859 self.redraw();
860 }
861 PointerEventKind::Motion { .. } => {
862 self.pointer_pos = pos;
863 }
864 PointerEventKind::Leave { .. } => {
865 self.hovered = false;
866 self.redraw();
867 }
868 PointerEventKind::Press {
870 button: 0x110,
871 serial,
872 ..
873 } => {
874 self.pointer_pos = pos;
875 self.on_press(&seat, serial);
876 }
877 PointerEventKind::Axis { vertical, .. } if vertical.absolute != 0.0 => {
879 let step = if vertical.absolute < 0.0 { 0.1 } else { -0.1 };
880 self.set_opacity(self.opacity + step);
881 }
882 _ => {}
883 }
884 }
885 }
886}
887
888impl State {
889 fn on_press(&mut self, seat: &wl_seat::WlSeat, serial: u32) {
891 if self.collapsed {
892 self.set_collapsed(false);
893 return;
894 }
895 let (w, h) = (self.width as f32, self.height as f32);
896 let (close, collapse) = toolbar_rects(w);
897 let p = self.pointer_pos;
898 if self.hovered && close.contains(p) {
899 self.closing = true;
900 } else if self.hovered && collapse.contains(p) {
901 self.set_collapsed(true);
902 } else if self.hovered && grip_rect(w, h).contains(p) {
903 self.window
904 .xdg_toplevel()
905 .resize(seat, serial, ResizeEdge::BottomRight);
906 } else {
907 self.window.xdg_toplevel()._move(seat, serial);
908 }
909 }
910
911 fn on_key(&mut self, key: Keysym) {
913 match key {
914 Keysym::Escape | Keysym::q => self.closing = true,
915 Keysym::space => {
916 self.frozen = !self.frozen;
917 self.redraw();
918 }
919 Keysym::c => self.set_collapsed(!self.collapsed),
920 Keysym::plus | Keysym::equal | Keysym::KP_Add => self.set_opacity(self.opacity + 0.1),
922 Keysym::minus | Keysym::KP_Subtract => self.set_opacity(self.opacity - 0.1),
923 Keysym::r => self.repick(),
924 _ => {}
925 }
926 }
927
928 fn set_opacity(&mut self, o: f32) {
929 let o = o.clamp(0.2, 1.0);
930 if (o - self.opacity).abs() > f32::EPSILON {
931 self.opacity = o;
932 self.redraw();
933 }
934 }
935
936 fn repick(&mut self) {
940 if self.relaunch.is_empty() {
941 return;
942 }
943 if let Ok(exe) = std::env::current_exe() {
944 let _ = std::process::Command::new(exe).args(&self.relaunch).spawn();
945 }
946 self.closing = true;
947 }
948}
949
950impl OutputHandler for State {
951 fn output_state(&mut self) -> &mut OutputState {
952 &mut self.output_state
953 }
954 fn new_output(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_output::WlOutput) {}
955 fn update_output(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_output::WlOutput) {}
956 fn output_destroyed(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_output::WlOutput) {}
957}
958
959impl ProvidesRegistryState for State {
960 fn registry(&mut self) -> &mut RegistryState {
961 &mut self.registry_state
962 }
963 registry_handlers![OutputState, SeatState];
964}
965
966delegate_compositor!(State);
967delegate_output!(State);
968delegate_seat!(State);
969delegate_keyboard!(State);
970delegate_pointer!(State);
971delegate_xdg_shell!(State);
972delegate_xdg_window!(State);
973delegate_registry!(State);
974pub enum PipMsg {
976 Shm {
978 w: usize,
980 h: usize,
982 rgba: Vec<u8>,
984 },
985 Dmabuf {
987 frame: wl::DmabufFrame,
989 },
990 Gone,
992}
993
994pub fn capture_thread(source: stream::Source, mut sink: impl FnMut(PipMsg) -> bool) {
1002 let mut client = match wl::Client::connect() {
1003 Ok(c) => c,
1004 Err(e) => {
1005 eprintln!("wlr-capture: mirror: {e:#}");
1006 sink(PipMsg::Gone);
1007 return;
1008 }
1009 };
1010
1011 let mut s = stream::Stream::new(source, APPEAR_GRACE);
1012 loop {
1013 let step = s.step(&mut client, ROUND);
1014 for frame in step.frames {
1016 let msg = match frame {
1017 wl::Frame::Shm(img) => PipMsg::Shm {
1018 w: img.width as usize,
1019 h: img.height as usize,
1020 rgba: img.rgba,
1021 },
1022 wl::Frame::Dmabuf(frame) => PipMsg::Dmabuf { frame },
1023 };
1024 if !sink(msg) {
1025 return; }
1027 }
1028 if step.end.is_some() {
1029 sink(PipMsg::Gone);
1030 return;
1031 }
1032 }
1033}