use std::num::NonZeroU32;
use std::sync::Arc;
use anyhow::{Context, Result};
use pixelcoords_core::geometry::{Point, Size};
use winit::dpi::{PhysicalPosition, PhysicalSize};
use winit::event_loop::ActiveEventLoop;
use winit::monitor::MonitorHandle;
use winit::window::{CursorIcon, Window, WindowAttributes};
#[derive(Debug, Clone, Copy)]
pub struct CoordMap {
pub window: Size,
pub capture: Size,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Fit {
pub origin: Point,
pub size: Size,
}
impl CoordMap {
pub fn is_identity(&self) -> bool {
self.window == self.capture
}
pub fn fit(&self) -> Fit {
let scale = f64::min(
f64::from(self.window.w) / f64::from(self.capture.w),
f64::from(self.window.h) / f64::from(self.capture.h),
);
let w = ((f64::from(self.capture.w) * scale) as i32).clamp(1, self.window.w);
let h = ((f64::from(self.capture.h) * scale) as i32).clamp(1, self.window.h);
Fit {
origin: Point::new((self.window.w - w) / 2, (self.window.h - h) / 2),
size: Size::new(w, h),
}
}
pub fn window_to_capture(&self, pos: PhysicalPosition<f64>) -> Point {
let fit = self.fit();
let x =
(pos.x - f64::from(fit.origin.x)) * f64::from(self.capture.w) / f64::from(fit.size.w);
let y =
(pos.y - f64::from(fit.origin.y)) * f64::from(self.capture.h) / f64::from(fit.size.h);
Point::new(
(x as i32).clamp(0, self.capture.w - 1),
(y as i32).clamp(0, self.capture.h - 1),
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Presentation {
Fullscreen,
Windowed,
}
pub struct OverlayView {
window: Arc<Window>,
surface: softbuffer::Surface<Arc<Window>, Arc<Window>>,
capture_size: Size,
scratch: Vec<u32>,
created: std::time::Instant,
warned_mismatch: bool,
}
const SETTLE: std::time::Duration = std::time::Duration::from_secs(1);
impl OverlayView {
pub fn new(
event_loop: &ActiveEventLoop,
monitor: Option<&MonitorHandle>,
capture_size: Size,
presentation: Presentation,
) -> Result<Self> {
let attrs = WindowAttributes::default()
.with_title("pixelcoords")
.with_visible(false)
.with_decorations(false)
.with_inner_size(PhysicalSize::new(
capture_size.w.max(1) as u32,
capture_size.h.max(1) as u32,
));
#[cfg(not(target_os = "macos"))]
let attrs = match presentation {
Presentation::Fullscreen => attrs.with_fullscreen(Some(
winit::window::Fullscreen::Borderless(monitor.cloned()),
)),
Presentation::Windowed => attrs,
};
let window = Arc::new(event_loop.create_window(attrs).context("creating window")?);
if let Some(m) = monitor.filter(|_| presentation == Presentation::Fullscreen) {
window.set_outer_position(m.position());
}
#[cfg(target_os = "macos")]
if presentation == Presentation::Fullscreen {
use winit::platform::macos::WindowExtMacOS;
window.set_simple_fullscreen(true);
}
let context = softbuffer::Context::new(window.clone())
.map_err(|e| anyhow::anyhow!("softbuffer context: {e}"))?;
let surface = softbuffer::Surface::new(&context, window.clone())
.map_err(|e| anyhow::anyhow!("softbuffer surface: {e}"))?;
window.set_cursor(CursorIcon::Crosshair);
window.set_visible(true);
window.focus_window();
Ok(Self {
window,
surface,
capture_size,
scratch: Vec::new(),
created: std::time::Instant::now(),
warned_mismatch: false,
})
}
pub fn id(&self) -> winit::window::WindowId {
self.window.id()
}
pub fn request_redraw(&self) {
self.window.request_redraw();
}
pub fn set_cursor(&self, icon: CursorIcon) {
self.window.set_cursor(icon);
}
pub fn coord_map(&self) -> CoordMap {
let inner = self.window.inner_size();
CoordMap {
window: Size::new(inner.width.max(1) as i32, inner.height.max(1) as i32),
capture: self.capture_size,
}
}
pub fn present(&mut self, compose: impl FnOnce(&mut [u32], Size)) -> Result<()> {
let map = self.coord_map();
let (win_w, win_h) = (map.window.w as u32, map.window.h as u32);
self.surface
.resize(
NonZeroU32::new(win_w).context("zero-width window")?,
NonZeroU32::new(win_h).context("zero-height window")?,
)
.map_err(|e| anyhow::anyhow!("surface resize: {e}"))?;
let mut buffer = self
.surface
.buffer_mut()
.map_err(|e| anyhow::anyhow!("surface buffer: {e}"))?;
if map.is_identity() {
compose(&mut buffer, map.capture);
buffer
.present()
.map_err(|e| anyhow::anyhow!("present: {e}"))?;
return Ok(());
}
let fit = map.fit();
if self.created.elapsed() > SETTLE && !self.warned_mismatch {
log::warn!(
"window {}x{} != capture {}x{}; drawing it letterboxed at {}x{} \
(selections stay in capture space)",
map.window.w,
map.window.h,
map.capture.w,
map.capture.h,
fit.size.w,
fit.size.h
);
self.warned_mismatch = true;
}
let cap_len = (map.capture.w as usize) * (map.capture.h as usize);
self.scratch.resize(cap_len, 0);
compose(&mut self.scratch, map.capture);
letterbox_blit(&self.scratch, map.capture, &mut buffer, map.window, fit);
buffer
.present()
.map_err(|e| anyhow::anyhow!("present: {e}"))?;
Ok(())
}
}
fn letterbox_blit(src: &[u32], src_size: Size, dst: &mut [u32], dst_size: Size, fit: Fit) {
dst.fill(0);
for dy in 0..fit.size.h {
let sy = (i64::from(dy) * i64::from(src_size.h) / i64::from(fit.size.h)) as i32;
let src_row = (sy as usize) * (src_size.w as usize);
let dst_row =
((dy + fit.origin.y) as usize) * (dst_size.w as usize) + fit.origin.x.max(0) as usize;
for dx in 0..fit.size.w {
let sx = (i64::from(dx) * i64::from(src_size.w) / i64::from(fit.size.w)) as i32;
dst[dst_row + dx as usize] = src[src_row + sx as usize];
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn identity_map_passes_coordinates_through() {
let map = CoordMap {
window: Size::new(3024, 1964),
capture: Size::new(3024, 1964),
};
assert!(map.is_identity());
let p = map.window_to_capture(PhysicalPosition::new(100.0, 200.0));
assert_eq!(p, Point::new(100, 200));
}
#[test]
fn mismatched_map_scales_and_clamps() {
let map = CoordMap {
window: Size::new(1512, 982),
capture: Size::new(3024, 1964),
};
let p = map.window_to_capture(PhysicalPosition::new(100.0, 200.0));
assert_eq!(p, Point::new(200, 400));
let edge = map.window_to_capture(PhysicalPosition::new(5000.0, 5000.0));
assert_eq!(edge, Point::new(3023, 1963));
}
#[test]
fn a_picked_window_keeps_its_shape_on_a_screen_shaped_nothing_like_it() {
let map = CoordMap {
window: Size::new(1512, 949),
capture: Size::new(763, 957),
};
assert!(!map.is_identity());
let fit = map.fit();
assert_eq!(fit.size, Size::new(756, 949));
assert_eq!(fit.origin, Point::new(378, 0));
let aspect = f64::from(fit.size.w) / f64::from(fit.size.h);
assert!((aspect - 763.0 / 957.0).abs() < 0.001, "aspect {aspect}");
}
#[test]
fn a_window_clamped_to_the_work_area_shrinks_rather_than_squashes() {
let map = CoordMap {
window: Size::new(763, 917),
capture: Size::new(763, 957),
};
let fit = map.fit();
assert_eq!(fit.size, Size::new(731, 917));
assert_eq!(fit.origin, Point::new(16, 0));
let center = map.window_to_capture(PhysicalPosition::new(
f64::from(fit.origin.x) + f64::from(fit.size.w) / 2.0,
f64::from(fit.size.h) / 2.0,
));
assert_eq!(center, Point::new(381, 478));
}
#[test]
fn a_window_matching_the_capture_needs_no_letterbox() {
let pick = Size::new(763, 957);
let map = CoordMap {
window: pick,
capture: pick,
};
assert!(map.is_identity());
assert_eq!(map.fit().origin, Point::new(0, 0));
assert_eq!(map.fit().size, pick);
assert_eq!(
map.window_to_capture(PhysicalPosition::new(271.0, 417.0)),
Point::new(271, 417)
);
}
#[test]
fn letterbox_blit_2x_upscale_replicates_pixels() {
let src = vec![1u32, 2, 3, 4];
let mut dst = vec![0u32; 16];
let size = Size::new(4, 4);
let fit = Fit {
origin: Point::new(0, 0),
size,
};
letterbox_blit(&src, Size::new(2, 2), &mut dst, size, fit);
assert_eq!(dst[0], 1);
assert_eq!(dst[1], 1);
assert_eq!(dst[2], 2);
assert_eq!(dst[4], 1);
assert_eq!(dst[15], 4);
}
#[test]
fn letterbox_blit_leaves_the_margins_black() {
let src = vec![7u32; 4];
let mut dst = vec![9u32; 16];
let fit = Fit {
origin: Point::new(1, 1),
size: Size::new(2, 2),
};
letterbox_blit(&src, Size::new(2, 2), &mut dst, Size::new(4, 4), fit);
assert_eq!(dst[5], 7);
assert_eq!(dst[6], 7);
assert_eq!(dst[9], 7);
assert_eq!(dst[10], 7);
assert_eq!(dst[0], 0);
assert_eq!(dst[4], 0);
assert_eq!(dst[7], 0);
assert_eq!(dst[15], 0);
}
}