| mod asset; |
| mod render; |
| |
| use anyhow::Result; |
| use asset::ASSETS; |
| use byte_unit::Byte; |
| use clap::Parser; |
| use dialoguer::Confirm; |
| use render::render_svg; |
| use std::path::PathBuf; |
| use std::time::Instant; |
| use vello::{ |
| kurbo::{Affine, Vec2}, |
| util::RenderContext, |
| Renderer, Scene, SceneBuilder, |
| }; |
| use winit::{ |
| dpi::LogicalSize, |
| event_loop::EventLoop, |
| window::{Window, WindowBuilder}, |
| }; |
| |
| #[derive(Parser, Debug)] |
| #[command(about, long_about = None)] |
| struct Args { |
| /// Input files for rendering. Will use builtin SVGs if empty. |
| files: Vec<PathBuf>, |
| } |
| |
| // Check if all the known assets have been downloaded. |
| // If some haven't been downloaded (or if the checksums don't match), find |
| // their combined size and licenses. Ask the user if they want to download |
| // the SVG files. |
| // If yes, download the files and return normally. |
| // If no, exit with status code -1 |
| fn fetch_missing_assets() -> Result<()> { |
| let missing_assets = ASSETS |
| .iter() |
| .filter(|asset| !asset.fetched()) |
| .collect::<Vec<_>>(); |
| |
| if !missing_assets.is_empty() { |
| let total_size = Byte::from_bytes(missing_assets.iter().map(|asset| asset.size).sum()) |
| .get_appropriate_unit(true); |
| let mut licenses: Vec<_> = missing_assets.iter().map(|asset| asset.license).collect(); |
| licenses.sort(); |
| licenses.dedup(); |
| |
| println!("Some SVG assets are missing. Let me download them for you."); |
| println!( |
| "They'll take up {total_size} and are available under these licenses: {}", |
| licenses.join(", ") |
| ); |
| |
| if Confirm::new() |
| .with_prompt("Do you want to continue?") |
| .interact()? |
| { |
| println!("Looks like you want to continue."); |
| for missing in missing_assets { |
| missing.fetch()? |
| } |
| } else { |
| println!("Nevermind then."); |
| std::process::exit(1) |
| } |
| } |
| Ok(()) |
| } |
| |
| async fn run(event_loop: EventLoop<()>, window: Window, svg_files: Vec<PathBuf>) { |
| use winit::{event::*, event_loop::ControlFlow}; |
| let mut render_cx = RenderContext::new().unwrap(); |
| let size = window.inner_size(); |
| let mut surface = render_cx |
| .create_surface(&window, size.width, size.height) |
| .await; |
| let device_handle = &render_cx.devices[surface.dev_id]; |
| let mut renderer = Renderer::new(&device_handle.device).unwrap(); |
| let mut current_frame = 0usize; |
| let mut scene = Scene::new(); |
| let mut cached_svg_scene = vec![]; |
| cached_svg_scene.resize_with(svg_files.len(), || None); |
| let mut transform = Affine::IDENTITY; |
| let mut mouse_down = false; |
| let mut prior_position = Vec2::default(); |
| let mut last_title_update = Instant::now(); |
| // We allow looping left and right through the svgs, so use a signed index |
| let mut svg_ix: i32 = 0; |
| // These are set after choosing the svg, as they overwrite the defaults specified there |
| event_loop.run(move |event, _, control_flow| match event { |
| Event::WindowEvent { |
| ref event, |
| window_id, |
| } if window_id == window.id() => match event { |
| WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit, |
| WindowEvent::KeyboardInput { input, .. } => { |
| if input.state == ElementState::Pressed { |
| match input.virtual_keycode { |
| Some(VirtualKeyCode::Left) => { |
| svg_ix = svg_ix.saturating_sub(1); |
| transform = Affine::IDENTITY |
| } |
| Some(VirtualKeyCode::Right) => { |
| svg_ix = svg_ix.saturating_add(1); |
| transform = Affine::IDENTITY |
| } |
| Some(VirtualKeyCode::Space) => transform = Affine::IDENTITY, |
| Some(VirtualKeyCode::Escape) => { |
| *control_flow = ControlFlow::Exit; |
| } |
| _ => {} |
| } |
| } |
| } |
| WindowEvent::Resized(size) => { |
| render_cx.resize_surface(&mut surface, size.width, size.height); |
| window.request_redraw(); |
| } |
| WindowEvent::MouseInput { state, button, .. } => { |
| if button == &MouseButton::Left { |
| mouse_down = state == &ElementState::Pressed; |
| } |
| } |
| WindowEvent::MouseWheel { delta, .. } => { |
| const BASE: f64 = 1.05; |
| const PIXELS_PER_LINE: f64 = 20.0; |
| let exponent = if let MouseScrollDelta::PixelDelta(delta) = delta { |
| delta.y / PIXELS_PER_LINE |
| } else if let MouseScrollDelta::LineDelta(_, y) = delta { |
| *y as f64 |
| } else { |
| 0.0 |
| }; |
| transform = Affine::translate(prior_position) |
| * Affine::scale(BASE.powf(exponent)) |
| * Affine::translate(-prior_position) |
| * transform; |
| } |
| WindowEvent::CursorMoved { position, .. } => { |
| let position = Vec2::new(position.x, position.y); |
| if mouse_down { |
| transform = Affine::translate(position - prior_position) * transform; |
| } |
| prior_position = position; |
| } |
| _ => {} |
| }, |
| Event::MainEventsCleared => { |
| window.request_redraw(); |
| } |
| Event::RedrawRequested(_) => { |
| current_frame += 1; |
| let width = surface.config.width; |
| let height = surface.config.height; |
| let device_handle = &render_cx.devices[surface.dev_id]; |
| let mut builder = SceneBuilder::for_scene(&mut scene); |
| |
| // Allow looping forever |
| let svg_ix = svg_ix.rem_euclid(svg_files.len() as i32) as usize; |
| |
| render_svg( |
| &mut builder, |
| &mut cached_svg_scene[svg_ix], |
| transform, |
| &svg_files[svg_ix], |
| ); |
| |
| builder.finish(); |
| let surface_texture = surface |
| .surface |
| .get_current_texture() |
| .expect("failed to get surface texture"); |
| renderer |
| .render_to_surface( |
| &device_handle.device, |
| &device_handle.queue, |
| &scene, |
| &surface_texture, |
| width, |
| height, |
| ) |
| .expect("failed to render to surface"); |
| surface_texture.present(); |
| |
| if current_frame % 60 == 0 { |
| let now = Instant::now(); |
| let duration = now.duration_since(last_title_update); |
| let fps = 60.0 / duration.as_secs_f64(); |
| window.set_title(&format!("usvg viewer - fps: {:.1}", fps)); |
| last_title_update = now; |
| } |
| device_handle.device.poll(wgpu::Maintain::Wait); |
| } |
| _ => {} |
| }) |
| } |
| |
| fn main() -> Result<()> { |
| let args = Args::parse(); |
| let paths = if args.files.is_empty() { |
| fetch_missing_assets()?; |
| ASSETS.iter().map(|asset| asset.local_path()).collect() |
| } else { |
| args.files |
| }; |
| let event_loop = EventLoop::new(); |
| let window = WindowBuilder::new() |
| .with_inner_size(LogicalSize::new(1044, 800)) |
| .with_resizable(true) |
| .with_title("Vello usvg viewer") |
| .build(&event_loop)?; |
| pollster::block_on(run(event_loop, window, paths)); |
| Ok(()) |
| } |