Krolyxon
Commit f2bd31a

Branches

Tags

Refs →

remagesrcapp.rs

568 lines · 19.9 KB · Rust

1//! winit `ApplicationHandler`: routes events, owns the state and the renderer.
2
3use std::collections::{HashMap, HashSet, VecDeque};
4use std::path::{Path, PathBuf};
5use std::sync::atomic::{AtomicU32, Ordering};
6use std::sync::{Arc, Mutex};
7use std::time::{Duration, Instant};
8
9use winit::application::ApplicationHandler;
10use winit::dpi::LogicalSize;
11use winit::event::{ElementState, MouseButton, MouseScrollDelta, StartCause, WindowEvent};
12use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoopProxy};
13use winit::keyboard::{ModifiersState, PhysicalKey};
14use winit::window::{Fullscreen, Window, WindowId};
15
16use crate::action::Action;
17use crate::cache::ImageCache;
18use crate::config::theme::Theme;
19use crate::decode::{self, DecodedImage};
20use crate::input::{self, Input};
21use crate::render::{Renderer, text};
22use crate::state::view::Dir;
23use crate::state::{AppState, Effects, MsgKind};
24use crate::ui;
25use crate::worker::{JobKey, Pool, Priority};
26
27/// GPU upload budget per event-loop turn, so preloads never cause a hitch.
28const UPLOAD_BUDGET: usize = 32 << 20;
29/// Show the window once the first image is ready, or after this long regardless.
30const SHOW_TIMEOUT: Duration = Duration::from_millis(300);
31/// Wheel pixels per zoom step for touchpads.
32const PIXELS_PER_STEP: f32 = 60.0;
33
34pub enum UserEvent {
35 Decoded {
36 path: PathBuf,
37 result: Result<Arc<DecodedImage>, String>,
38 },
39 /// Unknown-extension files that turned out to be images.
40 Found(Vec<PathBuf>),
41 /// Embedded plus system fonts, loaded off-thread after the first frame.
42 Fonts(glyphon::fontdb::Database),
43}
44
45pub struct Options {
46 pub fullscreen: bool,
47 pub geometry: Option<(u32, u32)>,
48 pub class: String,
49 pub cache_mb: usize,
50}
51
52struct Gfx {
53 renderer: Renderer,
54 // Dropped after the renderer, whose surface borrows the window.
55 window: Arc<Window>,
56}
57
58pub struct App {
59 opts: Options,
60 state: AppState,
61 theme: Theme,
62 input: Input,
63 pool: Pool,
64 proxy: EventLoopProxy<UserEvent>,
65 cache: ImageCache,
66 max_dim: Arc<AtomicU32>,
67 /// Decoded images whose result event is still in the loop's queue.
68 arriving: Arc<Mutex<HashSet<PathBuf>>>,
69 gfx: Option<Gfx>,
70 uploads: VecDeque<(PathBuf, Arc<DecodedImage>)>,
71 modifiers: ModifiersState,
72 cursor: [f32; 2],
73 dragging: bool,
74 held: HashMap<PhysicalKey, Dir>,
75 started: Instant,
76 shown_window: bool,
77 pub error: Option<anyhow::Error>,
78}
79
80impl App {
81 pub fn new(opts: Options, state: AppState, proxy: EventLoopProxy<UserEvent>) -> Self {
82 let cache = ImageCache::new(opts.cache_mb << 20);
83 let mut app = Self {
84 opts,
85 state,
86 theme: Theme::nothing_dark(),
87 input: Input::default(),
88 pool: Pool::new(),
89 proxy,
90 cache,
91 // Until the GPU reports its limit; uploads re-check.
92 max_dim: Arc::new(AtomicU32::new(16384)),
93 arriving: Arc::default(),
94 gfx: None,
95 uploads: VecDeque::new(),
96 modifiers: ModifiersState::empty(),
97 cursor: [0.0, 0.0],
98 dragging: false,
99 held: HashMap::new(),
100 started: Instant::now(),
101 shown_window: false,
102 error: None,
103 };
104 // Decode the first image while the window and GPU come up.
105 app.sync(true);
106 app
107 }
108
109 fn request_redraw(&self) {
110 if let Some(g) = &self.gfx {
111 g.window.request_redraw();
112 }
113 }
114
115 fn decode(&self, path: &Path, prio: Priority) {
116 if lock(&self.arriving).contains(path) {
117 return;
118 }
119 let proxy = self.proxy.clone();
120 let max_dim = self.max_dim.clone();
121 let arriving = self.arriving.clone();
122 let path = path.to_path_buf();
123 self.pool
124 .submit(JobKey::Decode(path.clone()), prio, move || {
125 let t = Instant::now();
126 let result = decode::decode(&path, max_dim.load(Ordering::Relaxed))
127 .map(Arc::new)
128 .map_err(|e| e.to_string());
129 log::debug!("decoded {} in {:.1?}", path.display(), t.elapsed());
130 lock(&arriving).insert(path.clone());
131 let _ = proxy.send_event(UserEvent::Decoded { path, result });
132 });
133 }
134
135 /// The current file and its preload neighbours, current first.
136 fn window_paths(&self) -> Vec<PathBuf> {
137 let q = &self.state.queue;
138 let Some(cur) = q.current() else {
139 return Vec::new();
140 };
141 let mut out = vec![cur.to_path_buf()];
142 for i in q.neighbours(self.state.settings.preload, self.state.settings.wrap) {
143 if let Some(p) = q.get(i) {
144 out.push(p.to_path_buf());
145 }
146 }
147 out
148 }
149
150 /// Make the GPU and decode queue match the current position.
151 fn sync(&mut self, navigated: bool) {
152 let now = Instant::now();
153 if navigated {
154 self.pool.bump_epoch();
155 self.input.cancel();
156 self.state.pending.clear();
157 }
158 let wanted = self.window_paths();
159 self.update_title();
160 let Some(cur) = wanted.first().cloned() else {
161 self.state.clear_shown();
162 self.uploads.clear();
163 if let Some(g) = &mut self.gfx {
164 g.renderer.retain(|_| false);
165 }
166 self.request_redraw();
167 return;
168 };
169
170 let keep = |p: &Path| {
171 wanted.iter().any(|w| w == p)
172 || self.state.shown.as_ref().is_some_and(|s| s.path == p)
173 || self.state.fade.as_ref().is_some_and(|f| f.path == p)
174 };
175 if let Some(g) = &mut self.gfx {
176 g.renderer.retain(keep);
177 }
178 self.uploads.retain(|(p, _)| wanted.contains(p));
179
180 let showing_cur = self.state.shown.as_ref().is_some_and(|s| s.path == cur);
181 if !showing_cur {
182 match self.cache.get(&cur) {
183 Some(img) => self.show(&cur, img, now),
184 None => {
185 self.state.loading_since.get_or_insert(now);
186 self.decode(&cur, Priority::Current);
187 }
188 }
189 }
190 for p in &wanted[1..] {
191 match self.cache.get(p) {
192 Some(img) => {
193 let ready = self.gfx.as_ref().is_some_and(|g| g.renderer.is_ready(p));
194 if !ready && !self.uploads.iter().any(|(u, _)| u == p) {
195 self.uploads.push_back((p.clone(), img));
196 }
197 }
198 None => self.decode(p, Priority::Preload),
199 }
200 }
201 self.request_redraw();
202 }
203
204 /// Upload `img` in full and put it on screen.
205 fn show(&mut self, path: &Path, img: Arc<DecodedImage>, now: Instant) {
206 let Some(g) = &mut self.gfx else {
207 // No GPU yet: keep it cached; `resumed` syncs again.
208 self.state.loading_since.get_or_insert(now);
209 return;
210 };
211 let img = fit_gpu(img, g.renderer.max_texture_dim());
212 g.renderer.upload(path, &img, usize::MAX);
213 self.uploads.retain(|(p, _)| p != path);
214 self.state.show(path, img.source_size, img.format, now);
215 self.request_redraw();
216 }
217
218 fn update_title(&self) {
219 if let Some(g) = &self.gfx {
220 let title = match self.state.queue.current() {
221 Some(p) => format!("{} — remage", ui::file_name(p)),
222 None => "remage".into(),
223 };
224 g.window.set_title(&title);
225 }
226 }
227
228 fn on_decoded(&mut self, path: PathBuf, result: Result<Arc<DecodedImage>, String>) {
229 lock(&self.arriving).remove(&path);
230 let now = Instant::now();
231 let is_current = self.state.queue.current() == Some(path.as_path());
232 match result {
233 Ok(img) => {
234 let wanted = self.window_paths();
235 let keep: Vec<&Path> = wanted.iter().map(PathBuf::as_path).collect();
236 self.cache.insert(path.clone(), img.clone(), &keep);
237 if is_current {
238 // Already on screen means this was a reload: replace the texture.
239 let reload = self.state.shown.as_ref().is_some_and(|s| s.path == path);
240 if reload && let Some(g) = &mut self.gfx {
241 g.renderer.retain(|p| p != path);
242 }
243 self.show(&path, img, now);
244 } else if wanted.contains(&path) && !self.uploads.iter().any(|(p, _)| *p == path) {
245 self.uploads.push_back((path, img));
246 }
247 }
248 Err(e) => {
249 log::warn!("{}: {e}", path.display());
250 self.state.queue.remove(&path);
251 self.state.skipped += 1;
252 if is_current {
253 self.state.loading_since = None;
254 let msg = format!("error: {}: {e}", ui::file_name(&path));
255 self.state.notify(msg, MsgKind::Error, now);
256 self.sync(true);
257 }
258 self.request_redraw();
259 }
260 }
261 }
262
263 fn reload(&mut self) {
264 if let Some(cur) = self.state.queue.current().map(Path::to_path_buf) {
265 self.cache.remove(&cur);
266 self.decode(&cur, Priority::Current);
267 }
268 }
269
270 fn apply(&mut self, action: Action, event_loop: &ActiveEventLoop) {
271 let fx = self.state.apply(action, Instant::now());
272 self.effects(fx, event_loop);
273 }
274
275 fn effects(&mut self, fx: Effects, event_loop: &ActiveEventLoop) {
276 if fx.quit {
277 event_loop.exit();
278 return;
279 }
280 if fx.fullscreen
281 && let Some(g) = &self.gfx
282 {
283 let on = g.window.fullscreen().is_some();
284 g.window
285 .set_fullscreen((!on).then_some(Fullscreen::Borderless(None)));
286 }
287 if fx.reload {
288 self.reload();
289 }
290 if fx.navigated {
291 self.sync(true);
292 }
293 if fx.redraw || fx.layout {
294 self.request_redraw();
295 }
296 }
297
298 fn on_key(&mut self, event: winit::event::KeyEvent, event_loop: &ActiveEventLoop) {
299 let now = Instant::now();
300 if event.state == ElementState::Released {
301 if let Some(dir) = self.held.remove(&event.physical_key) {
302 self.state.hold_pan(dir, false, now);
303 self.request_redraw();
304 }
305 return;
306 }
307 let action = self.input.key(&event.logical_key, self.modifiers);
308 let pending = self.input.pending();
309 if pending != self.state.pending {
310 self.state.pending = pending;
311 self.request_redraw();
312 }
313 let Some(action) = action else { return };
314 if event.repeat && !input::repeats(&action) {
315 return;
316 }
317 if let Action::Pan(dir) = action {
318 if self.state.shown.is_some() {
319 self.held.insert(event.physical_key, dir);
320 self.state.hold_pan(dir, true, now);
321 self.request_redraw();
322 }
323 return;
324 }
325 self.apply(action, event_loop);
326 }
327
328 fn redraw(&mut self) {
329 let now = Instant::now();
330 let ready = self.state.shown.is_some()
331 || self.state.queue.is_empty()
332 || now >= self.started + SHOW_TIMEOUT;
333 if !self.shown_window && !ready {
334 return; // about_to_wait wakes us at the timeout
335 }
336 self.state.advance(now);
337 self.state.tick(now);
338 let Some(g) = &mut self.gfx else { return };
339 if !self.shown_window {
340 g.window.set_visible(true);
341 }
342 let scene = ui::scene(&self.state, &self.theme, now, &mut |t| {
343 g.renderer.measure(t)
344 });
345 let result = g.renderer.render(&scene);
346 if let Some(path) = std::env::var_os("REMAGE_SNAPSHOT")
347 && !self.state.is_animating(now)
348 && self.state.loading_since.is_none()
349 {
350 match g.renderer.snapshot(&scene, Path::new(&path)) {
351 Ok(()) => log::info!("snapshot written to {}", Path::new(&path).display()),
352 Err(e) => log::error!("snapshot: {e:#}"),
353 }
354 }
355 drop(scene);
356 if let Err(e) = result {
357 log::error!("render: {e:#}");
358 self.state
359 .notify(format!("error: {e}"), MsgKind::Error, now);
360 }
361 if !self.shown_window {
362 self.shown_window = true;
363 log::debug!("first frame after {:.1?}", self.started.elapsed());
364 let proxy = self.proxy.clone();
365 std::thread::spawn(move || {
366 let _ = proxy.send_event(UserEvent::Fonts(text::all_fonts()));
367 });
368 }
369 if self.state.is_animating(Instant::now()) {
370 g.window.request_redraw();
371 }
372 }
373
374 /// Upload preloaded images a slice at a time. Returns whether work remains.
375 fn pump_uploads(&mut self) -> bool {
376 let Some(g) = &mut self.gfx else { return false };
377 if let Some((path, img)) = self.uploads.front() {
378 let img = fit_gpu(img.clone(), g.renderer.max_texture_dim());
379 if g.renderer.upload(path, &img, UPLOAD_BUDGET) {
380 self.uploads.pop_front();
381 }
382 }
383 !self.uploads.is_empty()
384 }
385}
386
387fn lock<T>(m: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
388 m.lock().unwrap_or_else(|e| e.into_inner())
389}
390
391/// Shrink an image that exceeds the GPU's texture limit (decoded before the limit was known).
392fn fit_gpu(img: Arc<DecodedImage>, max_dim: u32) -> Arc<DecodedImage> {
393 if img.size[0] <= max_dim && img.size[1] <= max_dim {
394 return img;
395 }
396 let owned = Arc::try_unwrap(img).unwrap_or_else(|a| DecodedImage {
397 pixels: a.pixels.clone(),
398 ..*a
399 });
400 match decode::downscale(owned, max_dim) {
401 Ok(small) => Arc::new(small),
402 Err(e) => {
403 log::error!("downscale: {e:#}");
404 Arc::new(DecodedImage {
405 size: [1, 1],
406 pixels: vec![0; 4],
407 format: "?",
408 source_size: [1, 1],
409 })
410 }
411 }
412}
413
414impl ApplicationHandler<UserEvent> for App {
415 fn resumed(&mut self, event_loop: &ActiveEventLoop) {
416 if self.gfx.is_some() {
417 return;
418 }
419 let (w, h) = self.opts.geometry.unwrap_or((1280, 800));
420 let mut attrs = Window::default_attributes()
421 .with_title("remage")
422 .with_inner_size(LogicalSize::new(w, h))
423 .with_decorations(false)
424 .with_visible(false);
425 if self.opts.fullscreen {
426 attrs = attrs.with_fullscreen(Some(Fullscreen::Borderless(None)));
427 }
428 let class = self.opts.class.clone();
429 attrs = winit::platform::wayland::WindowAttributesExtWayland::with_name(
430 attrs,
431 class.clone(),
432 class.clone(),
433 );
434 attrs =
435 winit::platform::x11::WindowAttributesExtX11::with_name(attrs, class.clone(), class);
436 let window = match event_loop.create_window(attrs) {
437 Ok(w) => Arc::new(w),
438 Err(e) => {
439 self.error = Some(anyhow::anyhow!("create window: {e}"));
440 event_loop.exit();
441 return;
442 }
443 };
444 let renderer = match Renderer::new(window.clone(), event_loop.owned_display_handle()) {
445 Ok(r) => r,
446 Err(e) => {
447 self.error = Some(e.context("initialise GPU"));
448 event_loop.exit();
449 return;
450 }
451 };
452 log::debug!("GPU ready after {:.1?}", self.started.elapsed());
453 self.max_dim
454 .store(renderer.max_texture_dim(), Ordering::Relaxed);
455 let size = window.inner_size();
456 self.state.set_window(
457 size.width as f32,
458 size.height as f32,
459 window.scale_factor() as f32,
460 );
461 self.gfx = Some(Gfx { renderer, window });
462 self.sync(false);
463 }
464
465 fn user_event(&mut self, _event_loop: &ActiveEventLoop, event: UserEvent) {
466 match event {
467 UserEvent::Decoded { path, result } => self.on_decoded(path, result),
468 UserEvent::Found(paths) => {
469 log::debug!("sniffing found {} more images", paths.len());
470 let was_empty = self.state.queue.is_empty();
471 self.state.queue.insert_sorted(paths);
472 self.sync(was_empty);
473 }
474 UserEvent::Fonts(db) => {
475 if let Some(g) = &mut self.gfx {
476 g.renderer.set_fonts(db);
477 g.window.request_redraw();
478 }
479 }
480 }
481 }
482
483 fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
484 let now = Instant::now();
485 match event {
486 WindowEvent::CloseRequested => event_loop.exit(),
487 WindowEvent::Resized(size) => {
488 let Some(g) = &mut self.gfx else { return };
489 g.renderer.resize(size.width, size.height);
490 let sf = g.window.scale_factor() as f32;
491 self.state
492 .set_window(size.width as f32, size.height as f32, sf);
493 self.request_redraw();
494 }
495 WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
496 let [w, h] = self.state.window();
497 self.state.set_window(w, h, scale_factor as f32);
498 self.request_redraw();
499 }
500 WindowEvent::RedrawRequested => self.redraw(),
501 WindowEvent::ModifiersChanged(m) => self.modifiers = m.state(),
502 WindowEvent::Focused(false) => {
503 self.held.clear();
504 self.state.stop_all_pans();
505 self.dragging = false;
506 }
507 WindowEvent::KeyboardInput {
508 event,
509 is_synthetic: false,
510 ..
511 } => {
512 self.on_key(event, event_loop);
513 }
514 WindowEvent::CursorMoved { position, .. } => {
515 let p = [position.x as f32, position.y as f32];
516 if self.dragging && self.state.shown.is_some() {
517 self.state
518 .view
519 .pan_pixels([p[0] - self.cursor[0], p[1] - self.cursor[1]]);
520 self.request_redraw();
521 }
522 self.cursor = p;
523 }
524 WindowEvent::MouseInput {
525 state,
526 button: MouseButton::Left,
527 ..
528 } => {
529 self.dragging = state == ElementState::Pressed;
530 }
531 WindowEvent::MouseWheel { delta, .. } => {
532 let steps = match delta {
533 MouseScrollDelta::LineDelta(_, y) => y,
534 MouseScrollDelta::PixelDelta(p) => p.y as f32 / PIXELS_PER_STEP,
535 };
536 if steps != 0.0 {
537 self.state.zoom_at(steps, self.cursor, now);
538 self.request_redraw();
539 }
540 }
541 _ => {}
542 }
543 }
544
545 fn new_events(&mut self, _event_loop: &ActiveEventLoop, cause: StartCause) {
546 if matches!(cause, StartCause::ResumeTimeReached { .. }) {
547 self.request_redraw();
548 }
549 }
550
551 fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
552 let now = Instant::now();
553 let busy = self.pump_uploads();
554 let (redraw, mut next) = self.state.tick(now);
555 if redraw {
556 self.request_redraw();
557 }
558 if !self.shown_window {
559 let at = self.started + SHOW_TIMEOUT;
560 next = Some(next.map_or(at, |n| n.min(at)));
561 }
562 event_loop.set_control_flow(match (busy, next) {
563 (true, _) => ControlFlow::Poll,
564 (false, Some(t)) => ControlFlow::WaitUntil(t),
565 (false, None) => ControlFlow::Wait,
566 });
567 }
568}