use std::sync::Arc;
use winit::application::ApplicationHandler;
use winit::event::{ElementState, KeyEvent, WindowEvent};
use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
use winit::keyboard::{Key, NamedKey};
use winit::window::{Window, WindowId, WindowLevel};
use crate::error::{Error, Result};
use crate::frame::Frame;
use crate::geometry::Rect;
use crate::placement::{Monitor, Placement};
use crate::render::{Gpu, Renderer};
pub fn monitors() -> Result<Vec<Monitor>> {
struct Collector(Vec<Monitor>);
impl ApplicationHandler for Collector {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
self.0 = attached(event_loop);
event_loop.exit();
}
fn window_event(&mut self, _: &ActiveEventLoop, _: WindowId, _: WindowEvent) {}
}
let event_loop = EventLoop::new().map_err(|error| Error::Render {
reason: format!("no event loop, so no monitors to list: {error}"),
})?;
let mut collector = Collector(Vec::new());
event_loop
.run_app(&mut collector)
.map_err(|error| Error::Render {
reason: format!("the event loop stopped before it listed anything: {error}"),
})?;
Ok(collector.0)
}
fn attached(event_loop: &ActiveEventLoop) -> Vec<Monitor> {
let primary = event_loop.primary_monitor();
event_loop
.available_monitors()
.map(|handle| {
let is_primary = primary
.as_ref()
.is_some_and(|candidate| *candidate == handle);
describe(&handle, is_primary)
})
.collect()
}
fn describe(handle: &winit::monitor::MonitorHandle, is_primary: bool) -> Monitor {
let position = handle.position();
let size = handle.size();
let name = handle.name().unwrap_or_default();
let scale_factor = handle.scale_factor();
let refresh_millihertz = handle.refresh_rate_millihertz();
let persistent_id = get_persistent_monitor_id(
&name,
position.x,
position.y,
scale_factor,
refresh_millihertz,
);
Monitor {
name,
bounds: Rect::new(
f64::from(position.x),
f64::from(position.y),
f64::from(size.width),
f64::from(size.height),
),
scale_factor,
refresh_millihertz,
is_primary,
persistent_id,
}
}
fn get_persistent_monitor_id(
name: &str,
x: i32,
y: i32,
scale_factor: f64,
refresh_millihertz: Option<u32>,
) -> String {
#[cfg(target_os = "linux")]
{
if !name.is_empty() {
return format!("xrandr_{}", name);
}
}
hash_monitor_id(name, x, y, scale_factor, refresh_millihertz)
}
fn hash_monitor_id(
name: &str,
x: i32,
y: i32,
scale_factor: f64,
refresh_millihertz: Option<u32>,
) -> String {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
name.hash(&mut hasher);
x.hash(&mut hasher);
y.hash(&mut hasher);
scale_factor.to_bits().hash(&mut hasher);
refresh_millihertz.hash(&mut hasher);
format!("monitor_{:016x}", hasher.finish())
}
pub struct Viewer {
frame: Frame,
placement: Placement,
title: String,
decorations: bool,
}
impl Viewer {
pub fn new(frame: Frame, placement: Placement) -> Self {
Viewer {
frame,
placement,
title: "vtome".to_string(),
decorations: false,
}
}
pub fn title(mut self, title: impl Into<String>) -> Self {
self.title = title.into();
self
}
pub fn decorations(mut self, decorations: bool) -> Self {
self.decorations = decorations;
self
}
pub fn show(self) -> Result<()> {
let event_loop = EventLoop::new().map_err(|error| Error::Render {
reason: format!("the event loop would not start: {error}"),
})?;
event_loop.set_control_flow(ControlFlow::Wait);
let mut application = Application {
viewer: self,
state: None,
failure: None,
};
event_loop
.run_app(&mut application)
.map_err(|error| Error::Render {
reason: format!("the event loop stopped: {error}"),
})?;
match application.failure {
Some(error) => Err(error),
None => Ok(()),
}
}
}
struct State {
window: Arc<Window>,
surface: wgpu::Surface<'static>,
gpu: Gpu,
renderer: Renderer,
quad: crate::geometry::Quad,
opacity: f32,
}
struct Application {
viewer: Viewer,
state: Option<State>,
failure: Option<Error>,
}
impl Application {
fn start(&mut self, event_loop: &ActiveEventLoop) -> Result<State> {
let monitors = attached(event_loop);
let resolved = self.viewer.placement.resolve(
self.viewer.frame.width(),
self.viewer.frame.height(),
&monitors,
)?;
if resolved.fell_back {
eprintln!(
"vtome: {} is not attached — showing on {} instead",
self.viewer.placement.monitor, resolved.monitor.name
);
}
let rect = resolved.window_rect();
let (x, y, width, height) = rect.to_physical();
let attributes = Window::default_attributes()
.with_title(&self.viewer.title)
.with_decorations(self.viewer.decorations)
.with_transparent(true)
.with_position(winit::dpi::PhysicalPosition::new(x, y))
.with_inner_size(winit::dpi::PhysicalSize::new(width.max(1), height.max(1)))
.with_window_level(if resolved.always_on_top {
WindowLevel::AlwaysOnTop
} else {
WindowLevel::Normal
});
let window =
Arc::new(
event_loop
.create_window(attributes)
.map_err(|error| Error::Render {
reason: format!("the window would not open: {error}"),
})?,
);
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_with_display_handle(
Box::new(Arc::clone(&window)),
));
let surface = instance
.create_surface(Arc::clone(&window))
.map_err(|error| Error::Render {
reason: format!("no surface for that window: {error}"),
})?;
let gpu = Gpu::from_instance(instance, Some(&surface))?;
let capabilities = surface.get_capabilities(&gpu.adapter);
let format = capabilities
.formats
.iter()
.copied()
.find(|format| !format.is_srgb())
.unwrap_or(capabilities.formats[0]);
let size = window.inner_size();
surface.configure(
&gpu.device,
&wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format,
color_space: wgpu::SurfaceColorSpace::Srgb,
width: size.width.max(1),
height: size.height.max(1),
present_mode: wgpu::PresentMode::AutoVsync,
alpha_mode: capabilities.alpha_modes[0],
view_formats: vec![],
desired_maximum_frame_latency: 2,
},
);
let mut renderer = Renderer::new(&gpu, format)?;
renderer.upload(&gpu, &self.viewer.frame)?;
Ok(State {
window,
surface,
gpu,
renderer,
quad: resolved.quad_in_window(),
opacity: resolved.opacity,
})
}
fn redraw(state: &mut State) -> Result<()> {
use wgpu::CurrentSurfaceTexture;
let texture = match state.surface.get_current_texture() {
CurrentSurfaceTexture::Success(texture)
| CurrentSurfaceTexture::Suboptimal(texture) => texture,
CurrentSurfaceTexture::Timeout
| CurrentSurfaceTexture::Occluded
| CurrentSurfaceTexture::Outdated
| CurrentSurfaceTexture::Lost => return Ok(()),
other => {
return Err(Error::Render {
reason: format!("no surface texture to draw into: {other:?}"),
})
}
};
let view = texture
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
let size = state.window.inner_size();
state.renderer.draw(
&state.gpu,
&view,
size.width.max(1),
size.height.max(1),
state.quad,
state.opacity,
)?;
state.gpu.queue.present(texture);
Ok(())
}
}
impl ApplicationHandler for Application {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
if self.state.is_some() {
return;
}
match self.start(event_loop) {
Ok(state) => {
state.window.request_redraw();
self.state = Some(state);
}
Err(error) => {
self.failure = Some(error);
event_loop.exit();
}
}
}
fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
let Some(state) = self.state.as_mut() else {
return;
};
match event {
WindowEvent::CloseRequested
| WindowEvent::KeyboardInput {
event:
KeyEvent {
logical_key: Key::Named(NamedKey::Escape),
state: ElementState::Pressed,
..
},
..
} => event_loop.exit(),
WindowEvent::Resized(size) => {
let capabilities = state.surface.get_capabilities(&state.gpu.adapter);
state.surface.configure(
&state.gpu.device,
&wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format: state.renderer.format(),
color_space: wgpu::SurfaceColorSpace::Srgb,
width: size.width.max(1),
height: size.height.max(1),
present_mode: wgpu::PresentMode::AutoVsync,
alpha_mode: capabilities.alpha_modes[0],
view_formats: vec![],
desired_maximum_frame_latency: 2,
},
);
state.window.request_redraw();
}
WindowEvent::RedrawRequested => {
if let Err(error) = Application::redraw(state) {
self.failure = Some(error);
event_loop.exit();
}
}
_ => {}
}
}
}