#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
#![warn(clippy::all)]
use std::{
env,
io::{self, IsTerminal, Read},
iter, panic,
path::PathBuf,
sync::{Arc, Mutex},
thread,
};
use cgmath::Vector2;
use cli::get_clap_command;
use egui::ViewportId;
use serde::{Deserialize, Serialize};
mod app;
use app::{
App,
op_queue::Op,
preferences::{PREFERENCES, Preferences},
};
mod icon;
mod rect;
mod util;
use util::UserEvent;
use winit::{
event::{Event, WindowEvent},
event_loop::{ControlFlow, EventLoop, EventLoopProxy},
keyboard::{Key, ModifiersState},
window::{Fullscreen, Window, WindowAttributes},
};
mod cli;
mod image_io;
#[derive(Serialize, Deserialize, Debug, Default)]
struct Config {
maximized: bool,
preferences: Preferences,
}
pub struct WgpuState {
pub window: Arc<Window>,
pub adapter: wgpu::Adapter,
pub surface: wgpu::Surface<'static>,
pub device: wgpu::Device,
pub queue: wgpu::Queue,
pub config: wgpu::SurfaceConfiguration,
pub scale_factor: f64,
}
pub struct WindowHandler {
pub wgpu: WgpuState,
pub event_loop: EventLoop<UserEvent>,
pub proxy: EventLoopProxy<UserEvent>,
pub egui_winit: egui_winit::State,
pub egui_renderer: egui_wgpu::Renderer,
pub egui_shapes: Vec<egui::ClippedPrimitive>,
pub app: App,
}
impl WindowHandler {
pub async fn new(class: &str, fullscreen: bool, zen_mode: bool, no_cache: bool) -> Self {
let mut config: Config = confy::load("simp", None).unwrap_or_default();
config.preferences.clamp();
let event_loop: EventLoop<UserEvent> = EventLoop::with_user_event().build().unwrap();
let proxy = event_loop.create_proxy();
let fullscreen = if fullscreen || config.preferences.open_in_fullscreen {
Some(Fullscreen::Borderless(None))
} else {
None
};
*PREFERENCES.lock().unwrap() = config.preferences;
let builder = WindowAttributes::default()
.with_title(String::from("Simp"))
.with_visible(false)
.with_min_inner_size(winit::dpi::LogicalSize::new(640f64, 400f64))
.with_inner_size(winit::dpi::LogicalSize::new(1100f64, 720f64))
.with_maximized(config.maximized)
.with_fullscreen(fullscreen)
.with_window_icon(Some(icon::get_icon()));
#[cfg(all(unix, not(target_os = "macos")))]
let builder = {
use winit::platform::{wayland, x11};
let builder = wayland::WindowAttributesExtWayland::with_name(builder, class, class);
x11::WindowAttributesExtX11::with_name(builder, class, class)
};
let _ = class;
#[allow(deprecated)]
let window = Arc::new(event_loop.create_window(builder).unwrap());
let size = window.inner_size();
let mut backends = if cfg!(windows) {
wgpu::Backends::DX12
} else if cfg!(target_os = "macos") {
wgpu::Backends::PRIMARY
} else {
wgpu::Backends::all()
};
if let Ok(gpu_backend) = env::var("SIMP_GPU_BACKEND") {
backends = wgpu::util::parse_backends_from_comma_list(&gpu_backend);
} else if let Ok(gpu_backend) = env::var("WGPU_BACKEND") {
backends = wgpu::util::parse_backends_from_comma_list(&gpu_backend);
};
let instance_descriptor = wgpu::InstanceDescriptor {
backends,
..Default::default()
};
let instance = wgpu::Instance::new(instance_descriptor);
let surface = instance.create_surface(window.clone()).unwrap();
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::HighPerformance,
compatible_surface: Some(&surface),
force_fallback_adapter: false,
})
.await
.expect("Unable to create adapter");
let limits = wgpu::Limits::default();
let (device, queue) = adapter
.request_device(
&wgpu::DeviceDescriptor {
label: None,
required_features: wgpu::Features::default(),
required_limits: limits.clone(),
memory_hints: wgpu::MemoryHints::default(),
},
None,
)
.await
.unwrap();
let surface_caps = surface.get_capabilities(&adapter);
let surface_format = surface_caps
.formats
.iter()
.copied()
.find(|f| f.is_srgb())
.unwrap_or(surface_caps.formats[0]);
let config = wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format: surface_format,
width: size.width,
height: size.height,
present_mode: wgpu::PresentMode::AutoVsync,
alpha_mode: wgpu::CompositeAlphaMode::Auto,
view_formats: Vec::new(),
desired_maximum_frame_latency: 2,
};
surface.configure(&device, &config);
let mut egui_winit = egui_winit::State::new(
egui::Context::default(),
ViewportId::ROOT,
&window.clone(),
None,
None,
None,
);
egui_winit.set_max_texture_side(limits.max_texture_dimension_2d as usize);
let egui_renderer = egui_wgpu::Renderer::new(&device, surface_format, None, 1, false);
let egui_shapes = Vec::new();
{
let repaint_proxy = Arc::new(Mutex::new(event_loop.create_proxy()));
egui_winit
.egui_ctx()
.set_request_repaint_callback(move |info| {
let _ = repaint_proxy
.lock()
.unwrap()
.send_event(UserEvent::RepaintRequest(info));
});
}
egui_winit.egui_ctx().style_mut(|style| {
style.spacing.slider_width = 200.0;
});
let wgpu = WgpuState {
window: window.clone(),
adapter,
surface,
device,
queue,
config,
scale_factor: 1.0,
};
let app = App::new(
&wgpu,
proxy.clone(),
[size.width as f32, size.height as f32],
zen_mode,
no_cache,
);
let ctrl_proxy = proxy.clone();
ctrlc::set_handler(move || {
let _ = ctrl_proxy.send_event(UserEvent::Exit);
})
.unwrap();
Self {
event_loop,
proxy,
app,
wgpu,
egui_winit,
egui_renderer,
egui_shapes,
}
}
}
impl WindowHandler {
pub fn main_loop(mut self) {
let WindowHandler {
event_loop,
mut app,
proxy,
mut egui_winit,
mut egui_renderer,
ref mut egui_shapes,
mut wgpu,
} = self;
#[allow(deprecated)]
let _ = event_loop.run(move |event, event_loop| match event {
Event::Resumed => wgpu.window.set_visible(true),
Event::NewEvents(..) => wgpu.window.request_redraw(),
Event::WindowEvent { event, .. } => match event {
WindowEvent::CloseRequested => {
let _ = proxy.send_event(UserEvent::Exit);
}
WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
wgpu.scale_factor = scale_factor;
}
WindowEvent::Resized(size) => {
if size.width > 0 && size.height > 0 {
wgpu.config.width = size.width;
wgpu.config.height = size.height;
wgpu.surface.configure(&wgpu.device, &wgpu.config);
app.resize(size);
}
}
WindowEvent::RedrawRequested => {
{
let raw_input = egui_winit.take_egui_input(&wgpu.window);
let egui_output = egui_winit.egui_ctx().run(raw_input, |ctx| {
app.handle_ui(&wgpu, ctx);
});
egui_winit
.handle_platform_output(&wgpu.window, egui_output.platform_output);
for (id, image_delta) in egui_output.textures_delta.set {
egui_renderer.update_texture(
&wgpu.device,
&wgpu.queue,
id,
&image_delta,
);
}
for id in egui_output.textures_delta.free {
egui_renderer.free_texture(&id);
}
let pixels_per_point = egui_winit.egui_ctx().pixels_per_point();
*egui_shapes = egui_winit
.egui_ctx()
.tessellate(egui_output.shapes, pixels_per_point);
}
let (exit, repaint_after) = app.update(&wgpu);
if exit {
event_loop.exit();
}
let control_flow = ControlFlow::wait_duration(repaint_after);
event_loop.set_control_flow(control_flow);
let output = wgpu.surface.get_current_texture().unwrap();
let view = output
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
let mut encoder =
wgpu.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("Render Encoder"),
});
{
{
let clear_color = ((44_f64) / 255.0).powf(2.2);
let mut rpass =
encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Image render pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color {
r: clear_color,
g: clear_color,
b: clear_color,
a: 1.0,
}),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
});
if let Some(image) = app.image_view.as_mut() {
let uniform = image.get_uniform(Vector2::new(
wgpu.config.width as f32,
wgpu.config.height as f32,
));
app.image_renderer.prepare(&wgpu, uniform);
app.image_renderer.render(&mut rpass, image);
let window_size = Vector2::new(
wgpu.config.width as f32,
wgpu.config.height as f32,
);
if let Some(uniform) = image.crop.get_uniform(
window_size,
image.position,
image.rotated_size(),
image.scale,
) {
app.crop_renderer.prepare(&wgpu, uniform);
app.crop_renderer.render(&mut rpass);
}
}
}
{
let screen_descriptor = egui_wgpu::ScreenDescriptor {
pixels_per_point: wgpu.window.scale_factor() as f32,
size_in_pixels: [wgpu.config.width, wgpu.config.height],
};
let cmd_buffers = egui_renderer.update_buffers(
&wgpu.device,
&wgpu.queue,
&mut encoder,
egui_shapes,
&screen_descriptor,
);
wgpu.queue.submit(cmd_buffers);
let mut pass = encoder
.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Gui Render Pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
occlusion_query_set: None,
timestamp_writes: None,
})
.forget_lifetime();
egui_renderer.render(&mut pass, egui_shapes, &screen_descriptor);
}
wgpu.queue.submit(iter::once(encoder.finish()));
wgpu.window.pre_present_notify();
output.present();
}
}
event => {
match &event {
WindowEvent::ModifiersChanged(modifiers) => {
app.modifiers = modifiers.state()
}
WindowEvent::KeyboardInput { event, .. } => {
if app.modifiers.contains(ModifiersState::CONTROL)
&& let Key::Character(ch) = &event.logical_key
{
match ch.as_str() {
"v" => app.handle_paste(),
"+" => return,
"-" => return,
_ => (),
}
}
}
_ => (),
}
let res = egui_winit.on_window_event(&wgpu.window, &event);
if !res.consumed || matches!(event, WindowEvent::ModifiersChanged(_)) {
app.handle_window_event(&wgpu, &event);
}
if res.repaint {
wgpu.window.request_redraw();
}
}
},
Event::LoopExiting => {
wgpu.window.set_visible(false);
let data = Config {
maximized: wgpu.window.is_maximized(),
preferences: PREFERENCES.lock().unwrap().clone(),
};
confy::store("simp", None, data).unwrap();
std::process::exit(0);
}
Event::UserEvent(mut event) => app.handle_user_event(&wgpu, &mut event),
_ => (),
});
}
}
fn generate_man() {
let cmd = get_clap_command();
let man = clap_mangen::Man::new(cmd);
let mut buffer: Vec<u8> = Default::default();
man.render(&mut buffer).unwrap();
std::io::Write::write_all(&mut std::io::stdout(), &buffer).unwrap();
}
fn main() {
panic::set_hook(Box::new(|info| {
let backtrace = std::backtrace::Backtrace::force_capture();
let panic_info = format!("{backtrace}\n{info}");
let _ = std::fs::write("panic.txt", &panic_info);
println!("{}", panic_info);
}));
let matches = cli::get_clap_command().get_matches();
if matches.get_flag("generate-man") {
generate_man();
return;
}
let path: Option<&String> = matches.get_one("file");
let fullscreen: bool = matches.get_flag("fullscreen");
let zen_mode: bool = matches.get_flag("zen-mode");
let no_cache: bool = matches.get_flag("no-cache");
let class: &String = matches.get_one("class").unwrap();
let mut window_handler =
pollster::block_on(WindowHandler::new(class, fullscreen, zen_mode, no_cache));
if !io::stdin().is_terminal() {
let proxy = window_handler.proxy.clone();
thread::spawn(move || {
let mut buffer = Vec::new();
let _ = io::stdin().read_to_end(&mut buffer);
if !buffer.is_empty() {
let _ = proxy.send_event(UserEvent::LoadBytes(buffer));
}
});
}
if let Some(path) = path {
window_handler
.app
.queue(Op::LoadPath(PathBuf::from(path), true))
}
window_handler.main_loop();
}