Krolyxon
Commit 92928dd

Branches

Tags

Refs →

remagesrcapp.rs

904 lines · 32.2 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, Switch, Zoom};
17use crate::cache::ImageCache;
18use crate::config::watch::ConfigWatch;
19use crate::config::{Config, LoadError, Loaded};
20use crate::decode::{self, DecodedImage};
21use crate::input::keymap::{KeyTables, Keymap};
22use crate::input::keys::{Key, Scroll};
23use crate::input::{self};
24use crate::render::{Renderer, text};
25use crate::scan;
26use crate::state::view::{Dir, FitMode};
27use crate::state::{AppState, Effects, MsgKind, Settings};
28use crate::ui;
29use crate::worker::{JobKey, Pool, Priority};
30
31/// GPU upload budget per event-loop turn, so preloads never cause a hitch.
32const UPLOAD_BUDGET: usize = 32 << 20;
33/// Show the window once the first image is ready, or after this long regardless.
34const SHOW_TIMEOUT: Duration = Duration::from_millis(300);
35/// Wheel pixels per zoom step for touchpads.
36const PIXELS_PER_STEP: f32 = 60.0;
37/// An ambiguous key sequence (`g` bound, and `gg` too) resolves after this long, like vim's
38/// `timeoutlen`.
39const SEQUENCE_TIMEOUT: Duration = Duration::from_millis(1000);
40/// A press and release closer than this is a click, not a drag.
41const CLICK_SLOP: f32 = 4.0;
42/// Config errors stay on screen until fixed (or dismissed with `<Esc>`).
43const CONFIG_ERROR_TIME: Duration = Duration::from_secs(3600);
44
45pub enum UserEvent {
46 Decoded {
47 path: PathBuf,
48 result: Result<Arc<DecodedImage>, String>,
49 },
50 /// Unknown-extension files that turned out to be images.
51 Found(Vec<PathBuf>),
52 /// Embedded plus system fonts, loaded off-thread after the first frame.
53 Fonts(glyphon::fontdb::Database),
54 /// The config was reloaded (file change, SIGUSR1 or `reload-config`).
55 Config(Box<Result<Loaded, LoadError>>),
56 /// A re-sort finished: the queue in its new order.
57 Sorted(Vec<PathBuf>),
58}
59
60/// Command-line settings that beat the config, across reloads too.
61#[derive(Clone, Debug, Default)]
62pub struct Overrides {
63 pub fit: Option<FitMode>,
64 pub zoom: Option<f32>,
65 pub bar: Option<bool>,
66}
67
68impl Overrides {
69 pub fn apply(&self, s: &mut Settings) {
70 if let Some(fit) = self.fit {
71 s.fit = fit;
72 }
73 if self.zoom.is_some() {
74 s.zoom = self.zoom;
75 }
76 }
77}
78
79pub struct Options {
80 pub fullscreen: bool,
81 pub geometry: Option<(u32, u32)>,
82 pub class: String,
83 pub overrides: Overrides,
84}
85
86struct Gfx {
87 renderer: Renderer,
88 // Dropped after the renderer, whose surface borrows the window.
89 window: Arc<Window>,
90}
91
92pub struct App {
93 opts: Options,
94 state: AppState,
95 config: Config,
96 keymap: Keymap,
97 watch: ConfigWatch,
98 /// A config error is on the status line.
99 config_error: bool,
100 /// `reload-config` ran, so the next reload is confirmed on screen.
101 reload_requested: bool,
102 /// When the pending key sequence became ambiguous.
103 ambiguous_since: Option<Instant>,
104 /// Actions a held key repeats.
105 repeat: Option<(PhysicalKey, Vec<Action>)>,
106 pool: Pool,
107 proxy: EventLoopProxy<UserEvent>,
108 cache: ImageCache,
109 max_dim: Arc<AtomicU32>,
110 /// Decoded images whose result event is still in the loop's queue.
111 arriving: Arc<Mutex<HashSet<PathBuf>>>,
112 gfx: Option<Gfx>,
113 uploads: VecDeque<(PathBuf, Arc<DecodedImage>)>,
114 modifiers: ModifiersState,
115 cursor: [f32; 2],
116 dragging: bool,
117 /// Where the left button went down, while it is a click candidate.
118 press: Option<[f32; 2]>,
119 /// Wheel motion not yet turned into whole `<ScrollUp>`/`<ScrollDown>` presses.
120 scroll: [f32; 2],
121 held: HashMap<PhysicalKey, Dir>,
122 started: Instant,
123 shown_window: bool,
124 pub error: Option<anyhow::Error>,
125}
126
127impl App {
128 pub fn new(
129 opts: Options,
130 state: AppState,
131 proxy: EventLoopProxy<UserEvent>,
132 loaded: Loaded,
133 watch: ConfigWatch,
134 ) -> Self {
135 let cache = ImageCache::new(loaded.config.image.cache_mb << 20);
136 let mut app = Self {
137 opts,
138 state,
139 keymap: keymap(&loaded.keys),
140 config: loaded.config,
141 watch,
142 config_error: false,
143 reload_requested: false,
144 ambiguous_since: None,
145 repeat: None,
146 pool: Pool::new(),
147 proxy,
148 cache,
149 // Until the GPU reports its limit; uploads re-check.
150 max_dim: Arc::new(AtomicU32::new(16384)),
151 arriving: Arc::default(),
152 gfx: None,
153 uploads: VecDeque::new(),
154 modifiers: ModifiersState::empty(),
155 cursor: [0.0, 0.0],
156 dragging: false,
157 press: None,
158 scroll: [0.0, 0.0],
159 held: HashMap::new(),
160 started: Instant::now(),
161 shown_window: false,
162 error: None,
163 };
164 // Decode the first image while the window and GPU come up.
165 app.sync(true);
166 app
167 }
168
169 fn request_redraw(&self) {
170 if let Some(g) = &self.gfx {
171 g.window.request_redraw();
172 }
173 }
174
175 fn decode(&self, path: &Path, prio: Priority) {
176 if lock(&self.arriving).contains(path) {
177 return;
178 }
179 let proxy = self.proxy.clone();
180 let max_dim = self.max_dim.clone();
181 let arriving = self.arriving.clone();
182 let path = path.to_path_buf();
183 self.pool
184 .submit(JobKey::Decode(path.clone()), prio, move || {
185 let t = Instant::now();
186 let result = decode::decode(&path, max_dim.load(Ordering::Relaxed))
187 .map(Arc::new)
188 .map_err(|e| e.to_string());
189 log::debug!("decoded {} in {:.1?}", path.display(), t.elapsed());
190 lock(&arriving).insert(path.clone());
191 let _ = proxy.send_event(UserEvent::Decoded { path, result });
192 });
193 }
194
195 /// The current file and its preload neighbours, current first.
196 fn window_paths(&self) -> Vec<PathBuf> {
197 let q = &self.state.queue;
198 let Some(cur) = q.current() else {
199 return Vec::new();
200 };
201 let mut out = vec![cur.to_path_buf()];
202 for i in q.neighbours(self.state.settings.preload, self.state.settings.wrap) {
203 if let Some(p) = q.get(i) {
204 out.push(p.to_path_buf());
205 }
206 }
207 out
208 }
209
210 /// Make the GPU and decode queue match the current position.
211 fn sync(&mut self, navigated: bool) {
212 let now = Instant::now();
213 if navigated {
214 self.pool.bump_epoch();
215 }
216 let wanted = self.window_paths();
217 self.update_title();
218 let Some(cur) = wanted.first().cloned() else {
219 self.state.clear_shown();
220 self.uploads.clear();
221 if let Some(g) = &mut self.gfx {
222 g.renderer.retain(|_| false);
223 }
224 self.request_redraw();
225 return;
226 };
227
228 let keep = |p: &Path| {
229 wanted.iter().any(|w| w == p)
230 || self.state.shown.as_ref().is_some_and(|s| s.path == p)
231 || self.state.fade.as_ref().is_some_and(|f| f.path == p)
232 };
233 if let Some(g) = &mut self.gfx {
234 g.renderer.retain(keep);
235 }
236 self.uploads.retain(|(p, _)| wanted.contains(p));
237
238 let showing_cur = self.state.shown.as_ref().is_some_and(|s| s.path == cur);
239 if !showing_cur {
240 match self.cache.get(&cur) {
241 Some(img) => self.show(&cur, img, now),
242 None => {
243 self.state.loading_since.get_or_insert(now);
244 self.decode(&cur, Priority::Current);
245 }
246 }
247 }
248 for p in &wanted[1..] {
249 match self.cache.get(p) {
250 Some(img) => {
251 let ready = self.gfx.as_ref().is_some_and(|g| g.renderer.is_ready(p));
252 if !ready && !self.uploads.iter().any(|(u, _)| u == p) {
253 self.uploads.push_back((p.clone(), img));
254 }
255 }
256 None => self.decode(p, Priority::Preload),
257 }
258 }
259 self.request_redraw();
260 }
261
262 /// Upload `img` in full and put it on screen.
263 fn show(&mut self, path: &Path, img: Arc<DecodedImage>, now: Instant) {
264 let Some(g) = &mut self.gfx else {
265 // No GPU yet: keep it cached; `resumed` syncs again.
266 self.state.loading_since.get_or_insert(now);
267 return;
268 };
269 let img = fit_gpu(img, g.renderer.max_texture_dim());
270 g.renderer.upload(path, &img, usize::MAX);
271 self.uploads.retain(|(p, _)| p != path);
272 self.state.show(path, img.source_size, img.format, now);
273 self.request_redraw();
274 }
275
276 fn update_title(&self) {
277 if let Some(g) = &self.gfx {
278 let q = &self.state.queue;
279 let title = match q.current() {
280 Some(p) => self
281 .config
282 .window
283 .title
284 .replace("{name}", &ui::file_name(p))
285 .replace("{index}", &(q.index() + 1).to_string())
286 .replace("{count}", &q.len().to_string()),
287 None => "remage".into(),
288 };
289 g.window.set_title(&title);
290 }
291 }
292
293 /// Show a config error until the config is fixed.
294 pub fn config_failed(&mut self, message: &str) {
295 self.state.notify_for(
296 format!("config error: {}", short_error(message)),
297 MsgKind::Error,
298 Instant::now(),
299 CONFIG_ERROR_TIME,
300 );
301 self.config_error = true;
302 self.request_redraw();
303 }
304
305 /// Adopt a reloaded config. Runtime toggles (bar, filter, alpha background) only change
306 /// when their config value did, so a reload doesn't undo what the user just toggled.
307 fn apply_config(&mut self, loaded: Loaded) {
308 let now = Instant::now();
309 for w in &loaded.warnings {
310 log::warn!("{w}");
311 }
312 let old = std::mem::replace(&mut self.config, loaded.config);
313 let new = &self.config;
314 let mut settings = Settings::from_config(new);
315 self.opts.overrides.apply(&mut settings);
316 let preload_changed = settings.preload != self.state.settings.preload;
317 self.state.set_settings(settings);
318 self.state.set_theme(loaded.theme, now);
319 self.keymap = keymap(&loaded.keys);
320 self.ambiguous_since = None;
321 self.state.pending.clear();
322 self.cache.set_budget(new.image.cache_mb << 20);
323 if old.image.filter != new.image.filter {
324 self.state.nearest = new.image.filter.nearest();
325 }
326 if old.image.alpha_background != new.image.alpha_background {
327 self.state.alpha_bg = new.image.alpha_background;
328 }
329 if old.ui.statusbar != new.ui.statusbar && self.opts.overrides.bar.is_none() {
330 let s = if new.ui.statusbar {
331 Switch::On
332 } else {
333 Switch::Off
334 };
335 self.state.apply(Action::Bar(s), now);
336 }
337 if let Some(g) = &mut self.gfx {
338 if old.window.decorations != new.window.decorations {
339 g.window.set_decorations(new.window.decorations);
340 }
341 g.renderer.set_font_families(font_families(new));
342 }
343 self.update_title();
344 // Silent on routine changes (a new wallpaper palette); confirm fixes and manual reloads.
345 if std::mem::take(&mut self.config_error) || std::mem::take(&mut self.reload_requested) {
346 self.state.notify("config reloaded", MsgKind::Info, now);
347 }
348 if preload_changed {
349 self.sync(false);
350 }
351 self.request_redraw();
352 }
353
354 fn on_decoded(&mut self, path: PathBuf, result: Result<Arc<DecodedImage>, String>) {
355 lock(&self.arriving).remove(&path);
356 let now = Instant::now();
357 let is_current = self.state.queue.current() == Some(path.as_path());
358 match result {
359 Ok(img) => {
360 let wanted = self.window_paths();
361 let keep: Vec<&Path> = wanted.iter().map(PathBuf::as_path).collect();
362 self.cache.insert(path.clone(), img.clone(), &keep);
363 if is_current {
364 // Already on screen means this was a reload: replace the texture.
365 let reload = self.state.shown.as_ref().is_some_and(|s| s.path == path);
366 if reload && let Some(g) = &mut self.gfx {
367 g.renderer.retain(|p| p != path);
368 }
369 self.show(&path, img, now);
370 } else if wanted.contains(&path) && !self.uploads.iter().any(|(p, _)| *p == path) {
371 self.uploads.push_back((path, img));
372 }
373 }
374 Err(e) => {
375 log::warn!("{}: {e}", path.display());
376 self.state.queue.remove(&path);
377 self.state.skipped += 1;
378 if is_current {
379 self.state.loading_since = None;
380 let msg = format!("error: {}: {e}", ui::file_name(&path));
381 self.state.notify(msg, MsgKind::Error, now);
382 self.sync(true);
383 }
384 self.request_redraw();
385 }
386 }
387 }
388
389 fn reload(&mut self) {
390 if let Some(cur) = self.state.queue.current().map(Path::to_path_buf) {
391 self.cache.remove(&cur);
392 self.decode(&cur, Priority::Current);
393 }
394 }
395
396 /// Run actions in order; `anchor` is the cursor for mouse-triggered ones.
397 fn run(&mut self, actions: Vec<Action>, anchor: Option<[f32; 2]>, el: &ActiveEventLoop) {
398 for action in actions {
399 if action == Action::Cancel {
400 self.keymap.cancel();
401 self.state.pending.clear();
402 self.config_error = false;
403 }
404 let fx = self.state.apply_at(action, anchor, Instant::now());
405 self.effects(fx, el);
406 if fx.quit {
407 return;
408 }
409 }
410 }
411
412 fn effects(&mut self, fx: Effects, event_loop: &ActiveEventLoop) {
413 if fx.quit {
414 event_loop.exit();
415 return;
416 }
417 if let Some(s) = fx.fullscreen
418 && let Some(g) = &self.gfx
419 {
420 let on = g.window.fullscreen().is_some();
421 let want = match s {
422 Switch::Toggle => !on,
423 Switch::On => true,
424 Switch::Off => false,
425 };
426 if want != on {
427 g.window
428 .set_fullscreen(want.then_some(Fullscreen::Borderless(None)));
429 }
430 }
431 if fx.reload {
432 self.reload();
433 }
434 if fx.reload_config {
435 self.reload_requested = true;
436 self.watch.reload();
437 }
438 if let Some(key) = fx.sort {
439 let mut files = self.state.queue.files().to_vec();
440 let proxy = self.proxy.clone();
441 std::thread::spawn(move || {
442 scan::sort_paths(&mut files, key, false);
443 let _ = proxy.send_event(UserEvent::Sorted(files));
444 });
445 }
446 if fx.navigated {
447 self.sync(true);
448 } else if fx.reordered {
449 self.sync(false);
450 }
451 if fx.redraw || fx.layout {
452 self.request_redraw();
453 }
454 }
455
456 /// Mirror the keymap's pending keys on the status line and arm the ambiguity timeout.
457 fn update_pending(&mut self, now: Instant) {
458 let pending = self.keymap.pending_display();
459 if pending != self.state.pending {
460 self.state.pending = pending;
461 self.request_redraw();
462 }
463 self.ambiguous_since = self
464 .keymap
465 .is_ambiguous(self.state.mode)
466 .then(|| self.ambiguous_since.unwrap_or(now));
467 }
468
469 fn on_key(&mut self, event: winit::event::KeyEvent, event_loop: &ActiveEventLoop) {
470 let now = Instant::now();
471 if event.state == ElementState::Released {
472 if let Some(dir) = self.held.remove(&event.physical_key) {
473 self.state.hold_pan(dir, false, now);
474 self.request_redraw();
475 }
476 if self
477 .repeat
478 .as_ref()
479 .is_some_and(|(k, _)| *k == event.physical_key)
480 {
481 self.repeat = None;
482 }
483 return;
484 }
485 if event.repeat {
486 // Repeats bypass the trie: only a completed, repeatable binding repeats.
487 if let Some((k, actions)) = &self.repeat
488 && *k == event.physical_key
489 {
490 let actions = actions.clone();
491 self.run(actions, None, event_loop);
492 }
493 return;
494 }
495 let Some(key) = input::key(&event.logical_key, self.modifiers) else {
496 return;
497 };
498 let runs = self.keymap.feed(self.state.mode, key);
499 self.update_pending(now);
500 self.repeat = None;
501 for run in runs {
502 let actions = run.counted();
503 if let [Action::Pan(dir)] = actions[..] {
504 if self.state.shown.is_some() {
505 self.held.insert(event.physical_key, dir);
506 self.state.hold_pan(dir, true, now);
507 self.request_redraw();
508 }
509 continue;
510 }
511 if !actions.is_empty() && actions.iter().all(Action::repeats) {
512 self.repeat = Some((event.physical_key, actions.clone()));
513 }
514 self.run(actions, None, event_loop);
515 }
516 }
517
518 /// A mouse button or wheel notch: bound actions run at the cursor, bypassing sequences.
519 fn on_mouse_key(&mut self, key: Key, event_loop: &ActiveEventLoop) {
520 self.keymap.cancel();
521 self.update_pending(Instant::now());
522 if let Some(actions) = self.keymap.lookup(self.state.mode, &[key]) {
523 let actions = actions.to_vec();
524 self.run(actions, Some(self.cursor), event_loop);
525 }
526 }
527
528 fn on_wheel(&mut self, delta: [f32; 2], event_loop: &ActiveEventLoop) {
529 let now = Instant::now();
530 let [dx, dy] = delta;
531 if dy != 0.0 {
532 let dir = if dy > 0.0 { Scroll::Up } else { Scroll::Down };
533 let key = input::scroll(dir, self.modifiers);
534 // Wheel zoom stays continuous for touchpads rather than stepping per notch.
535 match self.keymap.lookup(self.state.mode, &[key]) {
536 Some([Action::Zoom(Zoom::In(n))]) => {
537 let steps = dy.abs() * *n as f32;
538 self.state.zoom_at(steps, self.cursor, now);
539 self.request_redraw();
540 return;
541 }
542 Some([Action::Zoom(Zoom::Out(n))]) => {
543 let steps = -dy.abs() * *n as f32;
544 self.state.zoom_at(steps, self.cursor, now);
545 self.request_redraw();
546 return;
547 }
548 _ => {}
549 }
550 }
551 for (axis, d, pos, neg) in [
552 (0, dx, Scroll::Left, Scroll::Right),
553 (1, dy, Scroll::Up, Scroll::Down),
554 ] {
555 if d == 0.0 {
556 continue;
557 }
558 // Reversing direction drops the leftover fraction.
559 if self.scroll[axis] * d < 0.0 {
560 self.scroll[axis] = 0.0;
561 }
562 self.scroll[axis] += d;
563 while self.scroll[axis].abs() >= 1.0 {
564 let up = self.scroll[axis] > 0.0;
565 self.scroll[axis] -= self.scroll[axis].signum();
566 let key = input::scroll(if up { pos } else { neg }, self.modifiers);
567 self.on_mouse_key(key, event_loop);
568 }
569 }
570 }
571
572 fn redraw(&mut self) {
573 let now = Instant::now();
574 let ready = self.state.shown.is_some()
575 || self.state.queue.is_empty()
576 || now >= self.started + SHOW_TIMEOUT;
577 if !self.shown_window && !ready {
578 return; // about_to_wait wakes us at the timeout
579 }
580 self.state.advance(now);
581 self.state.tick(now);
582 let Some(g) = &mut self.gfx else { return };
583 if !self.shown_window {
584 g.window.set_visible(true);
585 }
586 let theme = self.state.theme_at(now);
587 let scene = ui::scene(&self.state, &theme, now, &mut |t| g.renderer.measure(t));
588 let result = g.renderer.render(&scene);
589 if let Some(path) = std::env::var_os("REMAGE_SNAPSHOT")
590 && !self.state.is_animating(now)
591 && self.state.loading_since.is_none()
592 {
593 match g.renderer.snapshot(&scene, Path::new(&path)) {
594 Ok(()) => log::info!("snapshot written to {}", Path::new(&path).display()),
595 Err(e) => log::error!("snapshot: {e:#}"),
596 }
597 }
598 drop(scene);
599 if let Err(e) = result {
600 log::error!("render: {e:#}");
601 self.state
602 .notify(format!("error: {e}"), MsgKind::Error, now);
603 }
604 if !self.shown_window {
605 self.shown_window = true;
606 log::debug!("first frame after {:.1?}", self.started.elapsed());
607 let proxy = self.proxy.clone();
608 std::thread::spawn(move || {
609 let _ = proxy.send_event(UserEvent::Fonts(text::all_fonts()));
610 });
611 }
612 if self.state.is_animating(Instant::now()) {
613 g.window.request_redraw();
614 }
615 }
616
617 /// Upload preloaded images a slice at a time. Returns whether work remains.
618 fn pump_uploads(&mut self) -> bool {
619 let Some(g) = &mut self.gfx else { return false };
620 if let Some((path, img)) = self.uploads.front() {
621 let img = fit_gpu(img.clone(), g.renderer.max_texture_dim());
622 if g.renderer.upload(path, &img, UPLOAD_BUDGET) {
623 self.uploads.pop_front();
624 }
625 }
626 !self.uploads.is_empty()
627 }
628}
629
630/// A config error trimmed for the status line: `config.toml:2:1: unknown field `fitt`; did you
631/// mean `fit`?` without the directory or the list of every valid field.
632fn short_error(message: &str) -> String {
633 let mut s = match message.split_once(": ") {
634 Some((loc, rest)) if loc.contains('/') => {
635 format!("{}: {rest}", loc.rsplit('/').next().unwrap_or(loc))
636 }
637 _ => message.to_string(),
638 };
639 if let Some(start) = s.find(", expected one of") {
640 let end = s[start..]
641 .find("; did you mean")
642 .map_or(s.len(), |i| start + i);
643 s.replace_range(start..end, "");
644 }
645 s
646}
647
648fn keymap(layers: &[KeyTables]) -> Keymap {
649 let refs: Vec<&KeyTables> = layers.iter().collect();
650 Keymap::new(&refs)
651}
652
653fn font_families(c: &Config) -> [String; 3] {
654 [
655 c.ui.font_ui.clone(),
656 c.ui.font_mono.clone(),
657 c.ui.font_display.clone(),
658 ]
659}
660
661fn lock<T>(m: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
662 m.lock().unwrap_or_else(|e| e.into_inner())
663}
664
665/// Shrink an image that exceeds the GPU's texture limit (decoded before the limit was known).
666fn fit_gpu(img: Arc<DecodedImage>, max_dim: u32) -> Arc<DecodedImage> {
667 if img.size[0] <= max_dim && img.size[1] <= max_dim {
668 return img;
669 }
670 let owned = Arc::try_unwrap(img).unwrap_or_else(|a| DecodedImage {
671 pixels: a.pixels.clone(),
672 ..*a
673 });
674 match decode::downscale(owned, max_dim) {
675 Ok(small) => Arc::new(small),
676 Err(e) => {
677 log::error!("downscale: {e:#}");
678 Arc::new(DecodedImage {
679 size: [1, 1],
680 pixels: vec![0; 4],
681 format: "?",
682 source_size: [1, 1],
683 })
684 }
685 }
686}
687
688impl ApplicationHandler<UserEvent> for App {
689 fn resumed(&mut self, event_loop: &ActiveEventLoop) {
690 if self.gfx.is_some() {
691 return;
692 }
693 let (w, h) = self.opts.geometry.unwrap_or((1280, 800));
694 let mut attrs = Window::default_attributes()
695 .with_title("remage")
696 .with_inner_size(LogicalSize::new(w, h))
697 .with_decorations(self.config.window.decorations)
698 .with_visible(false);
699 if self.opts.fullscreen {
700 attrs = attrs.with_fullscreen(Some(Fullscreen::Borderless(None)));
701 }
702 let class = self.opts.class.clone();
703 attrs = winit::platform::wayland::WindowAttributesExtWayland::with_name(
704 attrs,
705 class.clone(),
706 class.clone(),
707 );
708 attrs =
709 winit::platform::x11::WindowAttributesExtX11::with_name(attrs, class.clone(), class);
710 let window = match event_loop.create_window(attrs) {
711 Ok(w) => Arc::new(w),
712 Err(e) => {
713 self.error = Some(anyhow::anyhow!("create window: {e}"));
714 event_loop.exit();
715 return;
716 }
717 };
718 let renderer = match Renderer::new(window.clone(), event_loop.owned_display_handle()) {
719 Ok(mut r) => {
720 r.set_font_families(font_families(&self.config));
721 r
722 }
723 Err(e) => {
724 self.error = Some(e.context("initialise GPU"));
725 event_loop.exit();
726 return;
727 }
728 };
729 log::debug!("GPU ready after {:.1?}", self.started.elapsed());
730 self.max_dim
731 .store(renderer.max_texture_dim(), Ordering::Relaxed);
732 let size = window.inner_size();
733 self.state.set_window(
734 size.width as f32,
735 size.height as f32,
736 window.scale_factor() as f32,
737 );
738 self.gfx = Some(Gfx { renderer, window });
739 self.sync(false);
740 }
741
742 fn user_event(&mut self, _event_loop: &ActiveEventLoop, event: UserEvent) {
743 match event {
744 UserEvent::Decoded { path, result } => self.on_decoded(path, result),
745 UserEvent::Found(paths) => {
746 log::debug!("sniffing found {} more images", paths.len());
747 let was_empty = self.state.queue.is_empty();
748 self.state.queue.insert_sorted(paths);
749 self.sync(was_empty);
750 }
751 UserEvent::Fonts(db) => {
752 if let Some(g) = &mut self.gfx {
753 g.renderer.set_fonts(db);
754 g.window.request_redraw();
755 }
756 }
757 UserEvent::Config(result) => match *result {
758 Ok(loaded) => {
759 log::info!("config reloaded");
760 self.apply_config(loaded);
761 }
762 Err(e) => {
763 log::error!("{e}");
764 self.config_failed(&e.message);
765 }
766 },
767 UserEvent::Sorted(files) => {
768 self.state.queue.reorder(files);
769 self.sync(false);
770 }
771 }
772 }
773
774 fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
775 match event {
776 WindowEvent::CloseRequested => event_loop.exit(),
777 WindowEvent::Resized(size) => {
778 let Some(g) = &mut self.gfx else { return };
779 g.renderer.resize(size.width, size.height);
780 let sf = g.window.scale_factor() as f32;
781 self.state
782 .set_window(size.width as f32, size.height as f32, sf);
783 self.request_redraw();
784 }
785 WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
786 let [w, h] = self.state.window();
787 self.state.set_window(w, h, scale_factor as f32);
788 self.request_redraw();
789 }
790 WindowEvent::RedrawRequested => self.redraw(),
791 WindowEvent::ModifiersChanged(m) => self.modifiers = m.state(),
792 WindowEvent::Focused(false) => {
793 self.held.clear();
794 self.state.stop_all_pans();
795 self.dragging = false;
796 }
797 WindowEvent::KeyboardInput {
798 event,
799 is_synthetic: false,
800 ..
801 } => {
802 self.on_key(event, event_loop);
803 }
804 WindowEvent::CursorMoved { position, .. } => {
805 let p = [position.x as f32, position.y as f32];
806 if self.dragging && self.state.shown.is_some() {
807 self.state
808 .view
809 .pan_pixels([p[0] - self.cursor[0], p[1] - self.cursor[1]]);
810 self.request_redraw();
811 }
812 self.cursor = p;
813 }
814 WindowEvent::MouseInput { state, button, .. } => {
815 let pressed = state == ElementState::Pressed;
816 if button == MouseButton::Left {
817 // Drag pans; a press and release in place is a `<LeftMouse>` click.
818 self.dragging = pressed;
819 if pressed {
820 self.press = Some(self.cursor);
821 return;
822 }
823 let Some(p) = self.press.take() else { return };
824 let moved = (self.cursor[0] - p[0]).hypot(self.cursor[1] - p[1]);
825 if moved > CLICK_SLOP * self.state.scale_factor() {
826 return;
827 }
828 } else if !pressed {
829 return;
830 }
831 if let Some(key) = input::button(button, self.modifiers) {
832 self.on_mouse_key(key, event_loop);
833 }
834 }
835 WindowEvent::MouseWheel { delta, .. } => {
836 let d = match delta {
837 MouseScrollDelta::LineDelta(x, y) => [x, y],
838 MouseScrollDelta::PixelDelta(p) => {
839 [p.x as f32 / PIXELS_PER_STEP, p.y as f32 / PIXELS_PER_STEP]
840 }
841 };
842 self.on_wheel(d, event_loop);
843 }
844 _ => {}
845 }
846 }
847
848 fn new_events(&mut self, _event_loop: &ActiveEventLoop, cause: StartCause) {
849 if matches!(cause, StartCause::ResumeTimeReached { .. }) {
850 self.request_redraw();
851 }
852 }
853
854 fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
855 let now = Instant::now();
856 let busy = self.pump_uploads();
857 if let Some(t) = self.ambiguous_since
858 && now >= t + SEQUENCE_TIMEOUT
859 {
860 let runs = self.keymap.timeout(self.state.mode);
861 self.update_pending(now);
862 for run in runs {
863 self.run(run.counted(), None, event_loop);
864 }
865 }
866 let (redraw, mut next) = self.state.tick(now);
867 if let Some(t) = self.ambiguous_since {
868 let at = t + SEQUENCE_TIMEOUT;
869 next = Some(next.map_or(at, |n| n.min(at)));
870 }
871 if redraw {
872 self.request_redraw();
873 }
874 if !self.shown_window {
875 let at = self.started + SHOW_TIMEOUT;
876 next = Some(next.map_or(at, |n| n.min(at)));
877 }
878 event_loop.set_control_flow(match (busy, next) {
879 (true, _) => ControlFlow::Poll,
880 (false, Some(t)) => ControlFlow::WaitUntil(t),
881 (false, None) => ControlFlow::Wait,
882 });
883 }
884}
885
886#[cfg(test)]
887mod tests {
888 use super::*;
889
890 #[test]
891 fn config_errors_are_shortened_for_the_bar() {
892 assert_eq!(
893 short_error(
894 "/home/u/.config/remage/config.toml:2:1: unknown field `fitt`, expected one of \
895 `fit`, `zoom_step`; did you mean `fit`?"
896 ),
897 "config.toml:2:1: unknown field `fitt`; did you mean `fit`?"
898 );
899 assert_eq!(
900 short_error("a.toml:1:1: unknown field `x`, expected `y`"),
901 "a.toml:1:1: unknown field `x`, expected `y`"
902 );
903 }
904}