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_id, Monitor, Placement};
#[cfg(feature = "demux")]
use crate::playback::{Playback, Tick};
use crate::render::{Gpu, Renderer};
#[cfg(feature = "demux")]
use crate::video_source::VideoSource;
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)
}
pub(crate) 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 = monitor_id(&name, position.x, position.y, scale_factor);
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,
}
}
pub struct Viewer {
content: Content,
placement: Placement,
title: String,
decorations: bool,
click_through: bool,
looping: bool,
}
impl Viewer {
pub fn new(frame: Frame, placement: Placement) -> Self {
Viewer::showing(Content::Still(frame), placement)
}
#[cfg(feature = "demux")]
pub fn video(source: VideoSource, placement: Placement) -> Self {
Viewer::showing(
Content::Video(Box::new(Playback::new(
source,
true,
crate::clock::Monotonic::shared(),
None,
))),
placement,
)
}
fn showing(content: Content, placement: Placement) -> Self {
Viewer {
content,
placement,
title: "vtome".to_string(),
decorations: false,
click_through: false,
looping: true,
}
}
pub fn click_through(mut self, click_through: bool) -> Self {
self.click_through = click_through;
self
}
pub fn looping(mut self, looping: bool) -> Self {
self.looping = looping;
self
}
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<Report> {
let event_loop = EventLoop::new().map_err(|error| Error::Render {
reason: format!("the event loop would not start: {error}"),
})?;
event_loop.set_control_flow(match self.content {
Content::Still(_) => ControlFlow::Wait,
#[cfg(feature = "demux")]
Content::Video(_) => ControlFlow::Poll,
});
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(application.viewer.content.report()),
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Report {
pub presented: u64,
pub dropped: u64,
pub repeated: u64,
pub loops: u32,
pub hardware_decode: Option<bool>,
}
enum Content {
Still(Frame),
#[cfg(feature = "demux")]
Video(Box<Playback>),
}
impl Content {
fn still(&self) -> Option<&Frame> {
match self {
Content::Still(frame) => Some(frame),
#[cfg(feature = "demux")]
Content::Video(_) => None,
}
}
fn size(&self) -> (u32, u32) {
match self {
Content::Still(frame) => (frame.width(), frame.height()),
#[cfg(feature = "demux")]
Content::Video(playback) => playback.size(),
}
}
fn report(&self) -> Report {
match self {
Content::Still(_) => Report {
presented: 1,
..Report::default()
},
#[cfg(feature = "demux")]
Content::Video(playback) => {
let (presented, dropped, repeated) = playback.counts();
Report {
presented,
dropped,
repeated,
loops: playback.loops(),
hardware_decode: Some(playback.source().decoder().is_hardware()),
}
}
}
}
}
struct State {
window: Arc<Window>,
surface: wgpu::Surface<'static>,
config: wgpu::SurfaceConfiguration,
gpu: Gpu,
renderer: Renderer,
has_picture: bool,
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 (width, height) = self.viewer.content.size();
let resolved = self.viewer.placement.resolve(width, 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}"),
})?,
);
if self.viewer.click_through {
window
.set_cursor_hittest(false)
.map_err(|error| Error::Render {
reason: format!("this platform will not let clicks through a window: {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 = crate::render::surface_format(&capabilities);
let size = window.inner_size();
let config = 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: crate::render::see_through(&capabilities),
view_formats: vec![],
desired_maximum_frame_latency: 2,
};
surface.configure(&gpu.device, &config);
let mut renderer = Renderer::new(&gpu, format)?;
let mut has_picture = false;
if let Some(frame) = self.viewer.content.still() {
renderer.upload(&gpu, frame)?;
has_picture = true;
}
Ok(State {
window,
surface,
config,
gpu,
renderer,
has_picture,
quad: resolved.quad_in_window(),
opacity: resolved.opacity,
})
}
fn refresh(&mut self, event_loop: &ActiveEventLoop) -> Result<()> {
let Some(state) = self.state.as_mut() else {
return Ok(());
};
#[cfg(feature = "demux")]
if let Content::Video(playback) = &mut self.viewer.content {
playback.looping = self.viewer.looping;
match playback.tick()? {
Tick::Show(frame) => {
state.renderer.upload(&state.gpu, &frame)?;
state.has_picture = true;
}
Tick::Hold => {}
Tick::Ended => {
event_loop.exit();
return Ok(());
}
}
}
if !state.has_picture {
return Ok(());
}
Application::redraw(state)?;
#[cfg(feature = "demux")]
if let Content::Video(playback) = &mut self.viewer.content {
playback.presented();
}
let _ = event_loop;
Ok(())
}
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(),
#[cfg(feature = "demux")]
WindowEvent::KeyboardInput {
event:
KeyEvent {
logical_key: Key::Named(NamedKey::Space),
state: ElementState::Pressed,
repeat: false,
..
},
..
} => {
if let Content::Video(playback) = &mut self.viewer.content {
playback.toggle_pause();
}
}
WindowEvent::Resized(size) => {
state.config.width = size.width.max(1);
state.config.height = size.height.max(1);
state.surface.configure(&state.gpu.device, &state.config);
state.window.request_redraw();
}
WindowEvent::RedrawRequested => {
if let Err(error) = self.refresh(event_loop) {
self.failure = Some(error);
event_loop.exit();
}
}
_ => {}
}
}
fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
#[cfg(feature = "demux")]
if let (Content::Video(_), Some(state)) = (&self.viewer.content, &self.state) {
state.window.request_redraw();
}
}
}