Krolyxon
Commit e3295f9

Branches

Tags

Refs →

remagesrcapp.rs

1741 lines · 63.4 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::{Arc, Mutex};
6use std::time::{Duration, Instant, SystemTime};
7
8use serde_json::{Value, json};
9
10use winit::application::ApplicationHandler;
11use winit::dpi::LogicalSize;
12use winit::event::{ElementState, MouseButton, MouseScrollDelta, StartCause, WindowEvent};
13use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoopProxy};
14use winit::keyboard::{ModifiersState, PhysicalKey};
15use winit::window::{Fullscreen, Window, WindowId};
16
17use crate::action::{Action, ModeArg, Switch, Zoom};
18use crate::cache::ImageCache;
19use crate::config::watch::ConfigWatch;
20use crate::config::{Config, LoadError, Loaded, Output, Targets, suggest};
21use crate::decode::{self, DecodedImage};
22use crate::exec;
23use crate::input::keymap::{KeyTables, Keymap};
24use crate::input::keys::{Key, Scroll};
25use crate::input::{self};
26use crate::ipc::{self, Query, Request, Topic};
27use crate::render::{Renderer, text};
28use crate::scan;
29use crate::state::view::{Dir, FitMode};
30use crate::state::{AppState, Effects, Mode, MsgKind, Settings};
31use crate::thumbs::{self, CacheOptions, Thumb, ThumbError};
32use crate::ui;
33use crate::worker::{JobKey, Pool, Priority};
34
35/// GPU upload budget per event-loop turn, so preloads never cause a hitch.
36const UPLOAD_BUDGET: usize = 32 << 20;
37/// Show the window once the first image is ready, or after this long regardless.
38const SHOW_TIMEOUT: Duration = Duration::from_millis(300);
39/// Wheel pixels per zoom step for touchpads.
40const PIXELS_PER_STEP: f32 = 60.0;
41/// An ambiguous key sequence (`g` bound, and `gg` too) resolves after this long, like vim's
42/// `timeoutlen`.
43const SEQUENCE_TIMEOUT: Duration = Duration::from_millis(1000);
44/// A press and release closer than this is a click, not a drag.
45const CLICK_SLOP: f32 = 4.0;
46/// Config errors stay on screen until fixed (or dismissed with `<Esc>`).
47const CONFIG_ERROR_TIME: Duration = Duration::from_secs(3600);
48/// In the grid, the selected image decodes once the cursor has rested this long, so `<CR>`
49/// opens it at once without decoding everything the cursor passes over.
50const GRID_REST: Duration = Duration::from_millis(250);
51/// A scalable image is re-rendered for the zoom once the view has rested this long.
52const REFINE_DELAY: Duration = Duration::from_millis(150);
53/// Two clicks on the same thumbnail this close together open it.
54const DOUBLE_CLICK: Duration = Duration::from_millis(400);
55
56pub enum UserEvent {
57 Decoded {
58 path: PathBuf,
59 /// The file's mtime when decoding started, for `refresh`.
60 mtime: Option<SystemTime>,
61 result: Result<Arc<DecodedImage>, String>,
62 },
63 /// Unknown-extension files that turned out to be images.
64 Found(Vec<PathBuf>),
65 /// Embedded plus system fonts, loaded off-thread after the first frame.
66 Fonts(glyphon::fontdb::Database),
67 /// The config was reloaded (file change, SIGUSR1 or `reload-config`).
68 Config(Box<Result<Loaded, LoadError>>),
69 /// A re-sort finished: the queue in its new order.
70 Sorted(Vec<PathBuf>),
71 /// A thumbnail for cells of `px` pixels.
72 Thumb {
73 path: PathBuf,
74 px: u32,
75 mtime: Option<SystemTime>,
76 result: Result<Thumb, ThumbError>,
77 },
78 /// A sharper version of an image (SVG at the current zoom, a developed RAW).
79 Refined {
80 path: PathBuf,
81 result: Result<Arc<DecodedImage>, String>,
82 },
83 /// Output or the exit of a user command.
84 Command { id: u64, event: exec::Event },
85 /// `refresh` re-checked these files: their mtime, or `None` if they are gone.
86 Refreshed(Vec<(PathBuf, Option<SystemTime>)>),
87 /// A request on the IPC socket.
88 Ipc(ipc::Incoming),
89}
90
91/// Command-line settings that beat the config, across reloads too.
92#[derive(Clone, Debug, Default)]
93pub struct Overrides {
94 pub fit: Option<FitMode>,
95 pub zoom: Option<f32>,
96 pub bar: Option<bool>,
97}
98
99impl Overrides {
100 pub fn apply(&self, s: &mut Settings) {
101 if let Some(fit) = self.fit {
102 s.fit = fit;
103 }
104 if self.zoom.is_some() {
105 s.zoom = self.zoom;
106 }
107 }
108}
109
110pub struct Options {
111 pub fullscreen: bool,
112 pub geometry: Option<(u32, u32)>,
113 pub class: String,
114 pub overrides: Overrides,
115 /// Never write the shared thumbnail cache (`-p`).
116 pub private: bool,
117 /// The IPC socket, passed to commands as `$REMAGE_SOCKET`.
118 pub socket: Option<PathBuf>,
119}
120
121/// Where a chain of actions came from, which decides what some of them act on.
122#[derive(Clone, Debug, Default)]
123struct Ctx {
124 /// The cursor, for mouse-triggered zoom.
125 anchor: Option<[f32; 2]>,
126 /// A command's targets, which its `refresh` re-checks (otherwise the whole queue).
127 targets: Option<Arc<[PathBuf]>>,
128}
129
130/// A user command from spawn to exit.
131struct Job {
132 name: String,
133 output: Output,
134 targets: Arc<[PathBuf]>,
135 /// The command's `after` actions, then whatever followed `run` in its binding: they wait
136 /// for a successful exit.
137 then: Vec<Action>,
138 /// stdout lines run as actions so far.
139 lines: usize,
140}
141
142/// An IPC connection streaming events.
143struct Subscriber {
144 topics: Vec<Topic>,
145 tx: crossbeam_channel::Sender<String>,
146}
147
148/// What subscribers last heard about.
149#[derive(Clone, Debug, PartialEq)]
150struct Published {
151 image: (Option<PathBuf>, usize, usize),
152 marks: u64,
153 mode: Mode,
154}
155
156/// Thumbnail jobs for the current cell size.
157#[derive(Default)]
158struct ThumbJobs {
159 /// Cell size the atlas and requests are for (0 = none yet).
160 cell: u32,
161 requested: HashMap<PathBuf, Priority>,
162 /// Paths near the view; the atlas never evicts these.
163 wanted: HashSet<PathBuf>,
164 /// Finished thumbnails waiting for the atlas.
165 arrivals: VecDeque<(PathBuf, Thumb)>,
166}
167
168struct Gfx {
169 renderer: Renderer,
170 // Dropped after the renderer, whose surface borrows the window.
171 window: Arc<Window>,
172}
173
174pub struct App {
175 opts: Options,
176 state: AppState,
177 config: Config,
178 keymap: Keymap,
179 watch: ConfigWatch,
180 /// A config error is on the status line.
181 config_error: bool,
182 /// `reload-config` ran, so the next reload is confirmed on screen.
183 reload_requested: bool,
184 /// When the pending key sequence became ambiguous.
185 ambiguous_since: Option<Instant>,
186 /// Actions a held key repeats.
187 repeat: Option<(PhysicalKey, Vec<Action>)>,
188 pool: Pool,
189 proxy: EventLoopProxy<UserEvent>,
190 cache: ImageCache,
191 /// Decoded images whose result event is still in the loop's queue.
192 arriving: Arc<Mutex<HashSet<PathBuf>>>,
193 gfx: Option<Gfx>,
194 uploads: VecDeque<(PathBuf, Arc<DecodedImage>)>,
195 thumbs: ThumbJobs,
196 thumb_cache: CacheOptions,
197 /// When the grid cursor last moved, until the selected image is requested.
198 grid_rest: Option<Instant>,
199 /// The last click in the grid, for double-clicks.
200 last_click: Option<(Instant, usize)>,
201 modifiers: ModifiersState,
202 cursor: [f32; 2],
203 dragging: bool,
204 /// Where the left button went down, while it is a click candidate.
205 press: Option<[f32; 2]>,
206 /// Wheel motion not yet turned into whole `<ScrollUp>`/`<ScrollDown>` presses.
207 scroll: [f32; 2],
208 held: HashMap<PhysicalKey, Dir>,
209 started: Instant,
210 shown_window: bool,
211 /// File mtimes when they were last decoded or thumbnailed, so `refresh` spots changes.
212 stamps: HashMap<PathBuf, SystemTime>,
213 jobs: HashMap<u64, Job>,
214 next_job: u64,
215 /// A command waiting for `y` at its confirm prompt.
216 confirm: Option<(Job, exec::Prepared)>,
217 subscribers: Vec<Subscriber>,
218 published: Option<Published>,
219 /// A refinement waiting for the view to rest: (path, target size, since).
220 refine_wait: Option<(PathBuf, u32, Instant)>,
221 /// The image being refined.
222 refining: Option<PathBuf>,
223 /// Refinements that failed; not retried.
224 refine_failed: HashSet<PathBuf>,
225 /// `quit-no-output` ran: `-o` prints nothing.
226 no_output: bool,
227 pub error: Option<anyhow::Error>,
228}
229
230impl App {
231 pub fn new(
232 opts: Options,
233 state: AppState,
234 proxy: EventLoopProxy<UserEvent>,
235 loaded: Loaded,
236 watch: ConfigWatch,
237 ) -> Self {
238 let cache = ImageCache::new(loaded.config.image.cache_mb << 20);
239 let thumb_cache = CacheOptions::new(loaded.config.grid.thumbnail_cache, opts.private);
240 let mut app = Self {
241 opts,
242 state,
243 keymap: keymap(&loaded.keys),
244 config: loaded.config,
245 watch,
246 config_error: false,
247 reload_requested: false,
248 ambiguous_since: None,
249 repeat: None,
250 pool: Pool::new(),
251 proxy,
252 cache,
253 arriving: Arc::default(),
254 gfx: None,
255 uploads: VecDeque::new(),
256 thumbs: ThumbJobs::default(),
257 thumb_cache,
258 grid_rest: None,
259 last_click: None,
260 modifiers: ModifiersState::empty(),
261 cursor: [0.0, 0.0],
262 dragging: false,
263 press: None,
264 scroll: [0.0, 0.0],
265 held: HashMap::new(),
266 started: Instant::now(),
267 shown_window: false,
268 stamps: HashMap::new(),
269 jobs: HashMap::new(),
270 next_job: 1,
271 confirm: None,
272 subscribers: Vec::new(),
273 published: None,
274 refine_wait: None,
275 refining: None,
276 refine_failed: HashSet::new(),
277 no_output: false,
278 error: None,
279 };
280 // Decode the first image while the window and GPU come up.
281 app.sync(true);
282 app
283 }
284
285 fn request_redraw(&self) {
286 if let Some(g) = &self.gfx {
287 g.window.request_redraw();
288 }
289 }
290
291 fn decode(&self, path: &Path, prio: Priority) {
292 if lock(&self.arriving).contains(path) {
293 return;
294 }
295 let proxy = self.proxy.clone();
296 let opts = self.decode_options();
297 let arriving = self.arriving.clone();
298 let path = path.to_path_buf();
299 self.pool
300 .submit(JobKey::Decode(path.clone()), prio, move || {
301 let t = Instant::now();
302 let mtime = mtime(&path);
303 let result = decode::decode(&path, &opts)
304 .map(Arc::new)
305 .map_err(|e| e.to_string());
306 log::debug!("decoded {} in {:.1?}", path.display(), t.elapsed());
307 lock(&arriving).insert(path.clone());
308 let _ = proxy.send_event(UserEvent::Decoded {
309 path,
310 mtime,
311 result,
312 });
313 });
314 }
315
316 fn decode_options(&self) -> decode::Options {
317 let [w, h] = self.state.window();
318 let long = w.max(h) as u32;
319 decode::Options {
320 // Before the window exists, a typical screen.
321 target: if long > 1 { long } else { 2048 },
322 auto_orient: self.config.image.auto_orient,
323 color: self.config.image.color_management,
324 }
325 }
326
327 /// Whether the shown image should be re-rendered sharper for the current zoom. Waits for
328 /// the view to rest; returns when to check again.
329 fn check_refine(&mut self, now: Instant) -> Option<Instant> {
330 if self.state.mode != Mode::Image || self.state.view.is_animating(now) {
331 return None;
332 }
333 let path = self
334 .state
335 .shown
336 .as_ref()
337 .filter(|s| Some(s.path.as_path()) == self.state.queue.current())?
338 .path
339 .clone();
340 if self.refining.is_some() || self.refine_failed.contains(&path) {
341 return None;
342 }
343 let img = self.cache.get(&path)?;
344 let refine = img.refine.clone()?;
345 let source = img.source_size[0].max(img.source_size[1]) as f32;
346 let want = (source * self.state.view.scale()).round() as u32;
347 let Some(target) = refine.wants(img.size[0].max(img.size[1]), want) else {
348 self.refine_wait = None;
349 return None;
350 };
351 match &self.refine_wait {
352 Some((p, t, since)) if *p == path && *t == target => {
353 let at = *since + REFINE_DELAY;
354 if now < at {
355 return Some(at);
356 }
357 }
358 _ => {
359 self.refine_wait = Some((path, target, now));
360 return Some(now + REFINE_DELAY);
361 }
362 }
363 self.refine_wait = None;
364 self.refining = Some(path.clone());
365 log::debug!("refining {} to {target} px", path.display());
366 let proxy = self.proxy.clone();
367 let opts = self.decode_options();
368 self.pool
369 .submit(JobKey::Refine(path.clone()), Priority::Current, move || {
370 let t = Instant::now();
371 let result = refine
372 .run(&path, target, &opts)
373 .map(Arc::new)
374 .map_err(|e| format!("{e:#}"));
375 log::debug!("refined {} in {:.1?}", path.display(), t.elapsed());
376 let _ = proxy.send_event(UserEvent::Refined { path, result });
377 });
378 None
379 }
380
381 fn on_refined(&mut self, path: PathBuf, result: Result<Arc<DecodedImage>, String>) {
382 if self.refining.as_ref() == Some(&path) {
383 self.refining = None;
384 }
385 match result {
386 Ok(img) => {
387 let wanted = self.window_paths();
388 let keep: Vec<&Path> = wanted.iter().map(PathBuf::as_path).collect();
389 self.cache.insert(path.clone(), img.clone(), &keep);
390 let shown = self.state.shown.as_ref().is_some_and(|s| s.path == path);
391 if shown && let Some(g) = &mut self.gfx {
392 // Same source size, so the view stays exactly where it is.
393 g.renderer.upload(&path, &img, usize::MAX);
394 g.window.request_redraw();
395 }
396 }
397 Err(e) => {
398 log::warn!("{}: {e}", path.display());
399 self.refine_failed.insert(path);
400 }
401 }
402 }
403
404 /// The current file and its preload neighbours, current first.
405 fn window_paths(&self) -> Vec<PathBuf> {
406 let q = &self.state.queue;
407 let Some(cur) = q.current() else {
408 return Vec::new();
409 };
410 let mut out = vec![cur.to_path_buf()];
411 for i in q.neighbours(self.state.settings.preload, self.state.settings.wrap) {
412 if let Some(p) = q.get(i) {
413 out.push(p.to_path_buf());
414 }
415 }
416 out
417 }
418
419 /// Make the GPU and decode queue match the current position.
420 fn sync(&mut self, navigated: bool) {
421 let now = Instant::now();
422 if navigated {
423 self.pool.bump_epoch();
424 // A pending refinement was for the image we left (and its job may be cancelled).
425 self.refine_wait = None;
426 self.refining = None;
427 }
428 let wanted = self.window_paths();
429 self.update_title();
430 let Some(cur) = wanted.first().cloned() else {
431 self.state.clear_shown();
432 self.uploads.clear();
433 if let Some(g) = &mut self.gfx {
434 g.renderer.retain(|_| false);
435 }
436 self.request_redraw();
437 return;
438 };
439 if self.state.in_grid() {
440 if navigated || !self.cache_has(&cur) {
441 self.grid_rest = Some(now);
442 }
443 self.sync_thumbs();
444 self.request_redraw();
445 return;
446 }
447
448 let keep = |p: &Path| {
449 wanted.iter().any(|w| w == p)
450 || self.state.shown.as_ref().is_some_and(|s| s.path == p)
451 || self.state.fade.as_ref().is_some_and(|f| f.path == p)
452 };
453 if let Some(g) = &mut self.gfx {
454 g.renderer.retain(keep);
455 }
456 self.uploads.retain(|(p, _)| wanted.contains(p));
457
458 let showing_cur = self.state.shown.as_ref().is_some_and(|s| s.path == cur);
459 if !showing_cur {
460 match self.cache.get(&cur) {
461 Some(img) => self.show(&cur, img, now),
462 None => {
463 self.state.loading_since.get_or_insert(now);
464 self.decode(&cur, Priority::Current);
465 }
466 }
467 }
468 for p in &wanted[1..] {
469 match self.cache.get(p) {
470 Some(img) => {
471 let ready = self.gfx.as_ref().is_some_and(|g| g.renderer.is_ready(p));
472 if !ready && !self.uploads.iter().any(|(u, _)| u == p) {
473 self.uploads.push_back((p.clone(), img));
474 }
475 }
476 None => self.decode(p, Priority::Preload),
477 }
478 }
479 self.request_redraw();
480 }
481
482 fn cache_has(&mut self, path: &Path) -> bool {
483 self.cache.get(path).is_some()
484 }
485
486 /// Request thumbnails around the grid view: visible ones first, then a page above and
487 /// two below. Requests that scrolled out of range are cancelled.
488 fn sync_thumbs(&mut self) {
489 if !self.state.in_grid() {
490 return;
491 }
492 let Some(g) = &mut self.gfx else { return };
493 let px = self.state.cell_px();
494 if px != self.thumbs.cell {
495 for p in self.thumbs.requested.drain().map(|(p, _)| p) {
496 self.pool.cancel(&JobKey::Thumb(p));
497 }
498 self.thumbs.arrivals.clear();
499 self.state.thumbs.clear();
500 g.renderer.reset_thumbs(px);
501 self.thumbs.cell = px;
502 }
503 let l = self.state.grid_layout();
504 let visible = l.visible(self.state.grid.target(&l));
505 let span = l.page_rows() * l.cols;
506 let near = visible.start.saturating_sub(span)..(visible.end + 2 * span).min(l.count);
507 let q = &self.state.queue;
508 let mut wanted = HashSet::with_capacity(near.len());
509 let mut visible_paths = HashSet::with_capacity(visible.len());
510 let mut submit = Vec::new();
511 for i in near {
512 let Some(p) = q.get(i) else { continue };
513 wanted.insert(p.to_path_buf());
514 let prio = if visible.contains(&i) {
515 visible_paths.insert(p.to_path_buf());
516 Priority::ThumbVisible
517 } else {
518 Priority::Thumb
519 };
520 if !self.state.thumbs.contains_key(p) && self.thumbs.requested.get(p) != Some(&prio) {
521 submit.push((p.to_path_buf(), prio));
522 }
523 }
524 // Visible first, in order.
525 submit.sort_by_key(|(_, prio)| *prio);
526 for (p, prio) in submit {
527 self.request_thumb(p.clone(), prio);
528 self.thumbs.requested.insert(p, prio);
529 }
530 let mut demote = Vec::new();
531 self.thumbs.requested.retain(|p, prio| {
532 if !wanted.contains(p) {
533 self.pool.cancel(&JobKey::Thumb(p.clone()));
534 return false;
535 }
536 if *prio == Priority::ThumbVisible && !visible_paths.contains(p) {
537 *prio = Priority::Thumb;
538 demote.push(p.clone());
539 }
540 true
541 });
542 for p in demote {
543 self.request_thumb(p, Priority::Thumb);
544 }
545 self.thumbs.wanted = wanted;
546 }
547
548 fn request_thumb(&self, path: PathBuf, prio: Priority) {
549 let proxy = self.proxy.clone();
550 let cache = self.thumb_cache.clone();
551 let px = self.thumbs.cell;
552 self.pool
553 .submit(JobKey::Thumb(path.clone()), prio, move || {
554 let t = Instant::now();
555 let mtime = mtime(&path);
556 let result = thumbs::load(&path, px, &cache);
557 log::trace!("thumbnail {} in {:.1?}", path.display(), t.elapsed());
558 let _ = proxy.send_event(UserEvent::Thumb {
559 path,
560 px,
561 mtime,
562 result,
563 });
564 });
565 }
566
567 fn on_thumb(
568 &mut self,
569 path: PathBuf,
570 px: u32,
571 mtime: Option<SystemTime>,
572 result: Result<Thumb, ThumbError>,
573 ) {
574 self.thumbs.requested.remove(&path);
575 if px != self.thumbs.cell {
576 return;
577 }
578 match result {
579 Ok(t) => {
580 if let Some(m) = mtime {
581 self.stamps.insert(path.clone(), m);
582 }
583 self.thumbs.arrivals.push_back((path, t));
584 }
585 Err(e) => {
586 // Undecodable: it leaves the queue, as in image mode.
587 log::warn!("{}: {e}", path.display());
588 let was_current = self.state.queue.current() == Some(path.as_path());
589 if self.state.queue.remove(&path) {
590 self.state.skipped += 1;
591 }
592 self.sync(was_current);
593 self.request_redraw();
594 }
595 }
596 }
597
598 /// Every thumbnail on screen is in the atlas.
599 fn grid_settled(&self) -> bool {
600 let l = self.state.grid_layout();
601 let scroll = self.state.grid.scroll(&l, Instant::now());
602 l.visible(scroll).all(|i| {
603 self.state
604 .queue
605 .get(i)
606 .is_some_and(|p| self.state.thumbs.contains_key(p))
607 })
608 }
609
610 /// Upload `img` in full and put it on screen.
611 fn show(&mut self, path: &Path, img: Arc<DecodedImage>, now: Instant) {
612 let Some(g) = &mut self.gfx else {
613 // No GPU yet: keep it cached; `resumed` syncs again.
614 self.state.loading_since.get_or_insert(now);
615 return;
616 };
617 g.renderer.upload(path, &img, usize::MAX);
618 self.uploads.retain(|(p, _)| p != path);
619 self.state.show(path, img.source_size, img.format, now);
620 self.state.set_frames(img.delays(), img.animated, now);
621 self.request_redraw();
622 }
623
624 fn update_title(&self) {
625 if let Some(g) = &self.gfx {
626 let q = &self.state.queue;
627 let title = match q.current() {
628 Some(p) => self
629 .config
630 .window
631 .title
632 .replace("{name}", &ui::file_name(p))
633 .replace("{index}", &(q.index() + 1).to_string())
634 .replace("{count}", &q.len().to_string()),
635 None => "remage".into(),
636 };
637 g.window.set_title(&title);
638 }
639 }
640
641 /// Show a config error until the config is fixed.
642 pub fn config_failed(&mut self, message: &str) {
643 self.state.notify_for(
644 format!("config error: {}", short_error(message)),
645 MsgKind::Error,
646 Instant::now(),
647 CONFIG_ERROR_TIME,
648 );
649 self.config_error = true;
650 self.request_redraw();
651 }
652
653 /// Adopt a reloaded config. Runtime toggles (bar, filter, alpha background) only change
654 /// when their config value did, so a reload doesn't undo what the user just toggled.
655 fn apply_config(&mut self, loaded: Loaded) {
656 let now = Instant::now();
657 for w in &loaded.warnings {
658 log::warn!("{w}");
659 }
660 let old = std::mem::replace(&mut self.config, loaded.config);
661 let new = &self.config;
662 decode::set_decoders(&new.decoders);
663 let mut settings = Settings::from_config(new);
664 self.opts.overrides.apply(&mut settings);
665 let preload_changed = settings.preload != self.state.settings.preload;
666 if old.grid.size != new.grid.size {
667 self.state.grid.size = new.grid.size;
668 }
669 if old.grid.thumbnail_cache != new.grid.thumbnail_cache {
670 self.thumb_cache = CacheOptions::new(new.grid.thumbnail_cache, self.opts.private);
671 }
672 self.state.set_settings(settings);
673 self.state.set_theme(loaded.theme, now);
674 self.keymap = keymap(&loaded.keys);
675 self.ambiguous_since = None;
676 self.state.pending.clear();
677 self.cache.set_budget(new.image.cache_mb << 20);
678 if old.image.filter != new.image.filter {
679 self.state.nearest = new.image.filter.nearest();
680 }
681 if old.image.alpha_background != new.image.alpha_background {
682 self.state.alpha_bg = new.image.alpha_background;
683 }
684 if old.ui.statusbar != new.ui.statusbar && self.opts.overrides.bar.is_none() {
685 let s = if new.ui.statusbar {
686 Switch::On
687 } else {
688 Switch::Off
689 };
690 self.state.apply(Action::Bar(s), now);
691 }
692 if let Some(g) = &mut self.gfx {
693 if old.window.decorations != new.window.decorations {
694 g.window.set_decorations(new.window.decorations);
695 }
696 g.renderer.set_font_families(font_families(new));
697 }
698 self.update_title();
699 // Silent on routine changes (a new wallpaper palette); confirm fixes and manual reloads.
700 if std::mem::take(&mut self.config_error) || std::mem::take(&mut self.reload_requested) {
701 self.state.notify("config reloaded", MsgKind::Info, now);
702 }
703 if preload_changed {
704 self.sync(false);
705 }
706 self.sync_thumbs();
707 self.request_redraw();
708 }
709
710 fn on_decoded(
711 &mut self,
712 path: PathBuf,
713 mtime: Option<SystemTime>,
714 result: Result<Arc<DecodedImage>, String>,
715 ) {
716 lock(&self.arriving).remove(&path);
717 let now = Instant::now();
718 let is_current = self.state.queue.current() == Some(path.as_path());
719 let in_grid = self.state.in_grid();
720 match result {
721 Ok(img) => {
722 if let Some(m) = mtime {
723 self.stamps.insert(path.clone(), m);
724 }
725 let wanted = self.window_paths();
726 let keep: Vec<&Path> = wanted.iter().map(PathBuf::as_path).collect();
727 self.cache.insert(path.clone(), img.clone(), &keep);
728 if in_grid {
729 // Cached for when the grid opens it.
730 } else if is_current {
731 // Already on screen means this was a reload: replace the texture.
732 let reload = self.state.shown.as_ref().is_some_and(|s| s.path == path);
733 if reload && let Some(g) = &mut self.gfx {
734 g.renderer.retain(|p| p != path);
735 }
736 self.show(&path, img, now);
737 } else if wanted.contains(&path) && !self.uploads.iter().any(|(p, _)| *p == path) {
738 self.uploads.push_back((path, img));
739 }
740 }
741 Err(e) => {
742 log::warn!("{}: {e}", path.display());
743 self.state.queue.remove(&path);
744 self.state.skipped += 1;
745 if is_current {
746 self.state.loading_since = None;
747 let msg = format!("error: {}: {e}", ui::file_name(&path));
748 self.state.notify(msg, MsgKind::Error, now);
749 self.sync(true);
750 }
751 self.request_redraw();
752 }
753 }
754 }
755
756 fn reload(&mut self) {
757 if let Some(cur) = self.state.queue.current().map(Path::to_path_buf) {
758 self.cache.remove(&cur);
759 self.decode(&cur, Priority::Current);
760 }
761 }
762
763 /// Run actions in order. A `run` suspends the rest of the chain until its command exits
764 /// successfully. Errors are shown on the status line; the first is also returned for IPC.
765 fn run(&mut self, actions: Vec<Action>, ctx: &Ctx, el: &ActiveEventLoop) -> Result<(), String> {
766 let mut rest = actions.into_iter();
767 while let Some(action) = rest.next() {
768 match &action {
769 Action::Run(name) => return self.start_command(name, rest.collect()),
770 Action::Cancel => {
771 self.keymap.cancel();
772 self.state.pending.clear();
773 self.config_error = false;
774 self.answer(false);
775 }
776 Action::QuitNoOutput => self.no_output = true,
777 _ => {}
778 }
779 let fx = self.state.apply_at(action, ctx.anchor, Instant::now());
780 self.effects(fx, ctx, el);
781 if fx.quit {
782 break;
783 }
784 }
785 Ok(())
786 }
787
788 /// Start `[commands.<name>]`, or ask first when it has a `confirm`.
789 fn start_command(&mut self, name: &str, then: Vec<Action>) -> Result<(), String> {
790 let now = Instant::now();
791 let fail = |state: &mut AppState, msg: String| {
792 state.notify(format!("error: {msg}"), MsgKind::Error, now);
793 Err(msg)
794 };
795 let Some(cmd) = self.config.commands.get(name).cloned() else {
796 let hint = suggest(name, self.config.commands.keys().map(String::as_str))
797 .map(|s| format!(", did you mean `{s}`?"))
798 .unwrap_or_default();
799 self.request_redraw();
800 return fail(&mut self.state, format!("no command named `{name}`{hint}"));
801 };
802 let q = &self.state.queue;
803 let targets: Arc<[PathBuf]> =
804 exec::targets(cmd.targets, q.current(), &q.marked(), q.files()).into();
805 if targets.is_empty() && exec::needs_files(&cmd) {
806 let why = if cmd.targets == Targets::Marked {
807 "no marked files"
808 } else {
809 "no images"
810 };
811 self.request_redraw();
812 return fail(&mut self.state, format!("{name}: {why}"));
813 }
814 let size = self
815 .state
816 .shown
817 .as_ref()
818 .filter(|s| Some(s.path.as_path()) == q.current())
819 .map(|s| s.size);
820 let ctx = exec::Context {
821 current: q.current(),
822 index: q.index(),
823 targets: &targets,
824 size,
825 mode: self.state.mode.name(),
826 socket: self.opts.socket.as_deref(),
827 };
828 let prepared = exec::prepare(&cmd, &ctx);
829 let question = cmd.confirm.as_deref().map(|c| exec::expand_text(c, &ctx));
830 let mut chain = cmd.after.0.unwrap_or_default();
831 chain.extend(then);
832 let job = Job {
833 name: name.to_string(),
834 output: cmd.output,
835 targets,
836 then: chain,
837 lines: 0,
838 };
839 match question {
840 Some(q) => {
841 self.answer(false);
842 self.state.prompt = Some(q);
843 self.confirm = Some((job, prepared));
844 self.request_redraw();
845 }
846 None => self.spawn_job(job, prepared),
847 }
848 Ok(())
849 }
850
851 /// Settle a confirm prompt: `y` runs the command, anything else drops it.
852 fn answer(&mut self, yes: bool) {
853 let Some((job, prepared)) = self.confirm.take() else {
854 return;
855 };
856 self.state.prompt = None;
857 if yes {
858 self.spawn_job(job, prepared);
859 }
860 self.request_redraw();
861 }
862
863 fn spawn_job(&mut self, job: Job, prepared: exec::Prepared) {
864 let id = self.next_job;
865 self.next_job += 1;
866 log::info!(
867 "run {}: {} {:?}",
868 job.name,
869 prepared.program.to_string_lossy(),
870 prepared.args
871 );
872 let proxy = self.proxy.clone();
873 exec::spawn(&job.name, prepared, job.output, move |event| {
874 let _ = proxy.send_event(UserEvent::Command { id, event });
875 });
876 self.state.running.push((id, job.name.clone()));
877 self.jobs.insert(id, job);
878 self.request_redraw();
879 }
880
881 fn on_command(&mut self, id: u64, event: exec::Event, el: &ActiveEventLoop) {
882 let now = Instant::now();
883 match event {
884 exec::Event::Line(line) => {
885 let Some(job) = self.jobs.get_mut(&id) else {
886 return;
887 };
888 job.lines += 1;
889 let name = job.name.clone();
890 let ctx = Ctx {
891 anchor: None,
892 targets: Some(job.targets.clone()),
893 };
894 match line.parse::<Action>() {
895 Ok(a) => {
896 let _ = self.run(vec![a], &ctx, el);
897 }
898 Err(e) => {
899 log::warn!("{name}: {line}: {e}");
900 self.state
901 .notify(format!("error: {name}: {e}"), MsgKind::Error, now);
902 self.request_redraw();
903 }
904 }
905 }
906 exec::Event::Done(outcome) => {
907 let Some(job) = self.jobs.remove(&id) else {
908 return;
909 };
910 self.state.running.retain(|(j, _)| *j != id);
911 self.request_redraw();
912 if !outcome.success() {
913 let msg = format!("error: {}", outcome.error());
914 log::warn!("{}: {msg}", job.name);
915 self.state.notify(msg, MsgKind::Error, now);
916 return;
917 }
918 log::debug!("{}: done", job.name);
919 match (job.output, outcome.last_line) {
920 (Output::Status, Some(line)) => self.state.notify(line, MsgKind::Info, now),
921 // Actions speak for themselves.
922 (Output::Actions, _) if job.lines > 0 => {}
923 _ => self.state.notify("done", MsgKind::Info, now),
924 }
925 let ctx = Ctx {
926 anchor: None,
927 targets: Some(job.targets),
928 };
929 let _ = self.run(job.then, &ctx, el);
930 }
931 }
932 }
933
934 /// Re-check `targets` (default: the whole queue, plus the current file) on disk, off-thread.
935 fn refresh(&mut self, targets: Option<Arc<[PathBuf]>>) {
936 let mut files: Vec<PathBuf> = match targets {
937 Some(t) => t.to_vec(),
938 None => self.state.queue.files().to_vec(),
939 };
940 if let Some(cur) = self.state.queue.current()
941 && !files.iter().any(|f| f == cur)
942 {
943 files.push(cur.to_path_buf());
944 }
945 let proxy = self.proxy.clone();
946 std::thread::spawn(move || {
947 let found = files
948 .into_iter()
949 .map(|p| {
950 let m = std::fs::metadata(&p)
951 .ok()
952 .map(|m| m.modified().unwrap_or(SystemTime::UNIX_EPOCH));
953 (p, m)
954 })
955 .collect();
956 let _ = proxy.send_event(UserEvent::Refreshed(found));
957 });
958 }
959
960 /// Drop files that vanished and reload ones that changed since they were loaded.
961 fn on_refreshed(&mut self, found: Vec<(PathBuf, Option<SystemTime>)>) {
962 let cur = self.state.queue.current().map(Path::to_path_buf);
963 let showing = |s: &AppState, p: &Path| {
964 s.mode == Mode::Image && s.shown.as_ref().is_some_and(|s| s.path == p)
965 };
966 let mut navigated = false;
967 let mut stale = Vec::new();
968 let mut gone = 0;
969 for (path, mtime) in found {
970 match mtime {
971 None => {
972 if self.state.queue.remove(&path) {
973 gone += 1;
974 navigated |= cur.as_ref() == Some(&path);
975 }
976 self.cache.remove(&path);
977 self.stamps.remove(&path);
978 self.state.thumbs.remove(&path);
979 }
980 Some(t) if self.stamps.get(&path).is_some_and(|s| *s != t) => stale.push(path),
981 Some(_) => {}
982 }
983 }
984 let mut reload_current = false;
985 for path in &stale {
986 self.cache.remove(path);
987 self.stamps.remove(path);
988 self.state.thumbs.remove(path);
989 self.uploads.retain(|(p, _)| p != path);
990 if cur.as_ref() == Some(path) && showing(&self.state, path) {
991 // Keep it on screen until the new version replaces it.
992 reload_current = true;
993 } else if let Some(g) = &mut self.gfx {
994 g.renderer.retain(|p| p != path);
995 }
996 }
997 if gone + stale.len() > 0 {
998 log::debug!("refresh: {gone} gone, {} changed", stale.len());
999 }
1000 if reload_current && let Some(cur) = &cur {
1001 self.decode(cur, Priority::Current);
1002 }
1003 if gone + stale.len() > 0 {
1004 self.sync(navigated);
1005 self.request_redraw();
1006 }
1007 }
1008
1009 fn on_ipc(&mut self, inc: ipc::Incoming, el: &ActiveEventLoop) {
1010 let reply = match inc.request {
1011 Request::Actions(actions) => match self.run(actions, &Ctx::default(), el) {
1012 Ok(()) => ipc::ok(Value::Null),
1013 Err(e) => ipc::error(&e),
1014 },
1015 Request::Query(q) => ipc::ok(self.query(q)),
1016 Request::Subscribe(topics) => match inc.events {
1017 Some(tx) => {
1018 if self.published.is_none() {
1019 self.published = Some(self.snapshot());
1020 }
1021 self.subscribers.push(Subscriber { topics, tx });
1022 ipc::ok(Value::Null)
1023 }
1024 None => ipc::error("no event channel"),
1025 },
1026 };
1027 let _ = inc.reply.send(reply);
1028 }
1029
1030 fn query(&self, q: Query) -> Value {
1031 let s = &self.state;
1032 let paths = |ps: &mut dyn Iterator<Item = &Path>| -> Value {
1033 ps.map(ipc::path_json).collect::<Vec<_>>().into()
1034 };
1035 match q {
1036 Query::Current => s.queue.current().map_or(Value::Null, ipc::path_json),
1037 Query::Marked => paths(&mut s.queue.marked().into_iter()),
1038 Query::Queue => paths(&mut s.queue.files().iter().map(PathBuf::as_path)),
1039 Query::State => {
1040 let shown = s
1041 .shown
1042 .as_ref()
1043 .filter(|sh| Some(sh.path.as_path()) == s.queue.current());
1044 let fullscreen = self
1045 .gfx
1046 .as_ref()
1047 .is_some_and(|g| g.window.fullscreen().is_some());
1048 json!({
1049 "mode": s.mode.name(),
1050 "current": s.queue.current().map(ipc::path_json),
1051 "index": (!s.queue.is_empty()).then(|| s.queue.index() + 1),
1052 "count": s.queue.len(),
1053 "marked": s.queue.marked_count(),
1054 "width": shown.map(|sh| sh.size[0]),
1055 "height": shown.map(|sh| sh.size[1]),
1056 "format": shown.map(|sh| sh.format),
1057 "zoom": shown.map(|_| (s.view.scale() * 1000.0).round() / 10.0),
1058 "fit": s.settings.fit,
1059 "bar": s.bar,
1060 "fullscreen": fullscreen,
1061 "skipped": s.skipped,
1062 "running": s.running.iter().map(|(_, n)| n.as_str()).collect::<Vec<_>>(),
1063 "pid": std::process::id(),
1064 })
1065 }
1066 }
1067 }
1068
1069 fn snapshot(&self) -> Published {
1070 let q = &self.state.queue;
1071 Published {
1072 image: (q.current().map(Path::to_path_buf), q.index(), q.len()),
1073 marks: q.marks_version(),
1074 mode: self.state.mode,
1075 }
1076 }
1077
1078 /// Tell subscribers what changed since they last heard.
1079 fn publish(&mut self) {
1080 if self.subscribers.is_empty() {
1081 self.published = None;
1082 return;
1083 }
1084 let now = self.snapshot();
1085 let Some(before) = self.published.replace(now.clone()) else {
1086 return;
1087 };
1088 let q = &self.state.queue;
1089 let mut events = Vec::new();
1090 if before.image != now.image {
1091 let data = json!({
1092 "path": q.current().map(ipc::path_json),
1093 "index": (!q.is_empty()).then(|| q.index() + 1),
1094 "count": q.len(),
1095 });
1096 events.push((Topic::Image, data));
1097 }
1098 if before.marks != now.marks {
1099 let files: Vec<Value> = q.marked().into_iter().map(ipc::path_json).collect();
1100 events.push((
1101 Topic::Marks,
1102 json!({ "count": files.len(), "files": files }),
1103 ));
1104 }
1105 if before.mode != now.mode {
1106 events.push((Topic::Mode, json!(now.mode.name())));
1107 }
1108 for (topic, data) in events {
1109 let line = ipc::event(topic, data);
1110 self.subscribers
1111 .retain(|s| !s.topics.contains(&topic) || s.tx.send(line.clone()).is_ok());
1112 }
1113 }
1114
1115 /// Marked files for `-o`, unless the user quit with `quit-no-output`.
1116 pub fn marked_output(&self) -> Option<Vec<PathBuf>> {
1117 (!self.no_output).then(|| {
1118 self.state
1119 .queue
1120 .marked()
1121 .into_iter()
1122 .map(Path::to_path_buf)
1123 .collect()
1124 })
1125 }
1126
1127 fn effects(&mut self, fx: Effects, ctx: &Ctx, event_loop: &ActiveEventLoop) {
1128 if fx.quit {
1129 event_loop.exit();
1130 return;
1131 }
1132 if let Some(s) = fx.fullscreen
1133 && let Some(g) = &self.gfx
1134 {
1135 let on = g.window.fullscreen().is_some();
1136 let want = match s {
1137 Switch::Toggle => !on,
1138 Switch::On => true,
1139 Switch::Off => false,
1140 };
1141 if want != on {
1142 g.window
1143 .set_fullscreen(want.then_some(Fullscreen::Borderless(None)));
1144 }
1145 }
1146 if fx.reload {
1147 self.reload();
1148 }
1149 if fx.reload_config {
1150 self.reload_requested = true;
1151 self.watch.reload();
1152 }
1153 if fx.refresh {
1154 self.refresh(ctx.targets.clone());
1155 }
1156 if let Some(key) = fx.sort {
1157 let mut files = self.state.queue.files().to_vec();
1158 let proxy = self.proxy.clone();
1159 std::thread::spawn(move || {
1160 scan::sort_paths(&mut files, key, false);
1161 let _ = proxy.send_event(UserEvent::Sorted(files));
1162 });
1163 }
1164 if fx.mode {
1165 // Opening an image from the grid loads it like navigating to it.
1166 self.sync(self.state.mode == Mode::Image || fx.navigated);
1167 } else if fx.navigated {
1168 self.sync(true);
1169 } else if fx.reordered {
1170 self.sync(false);
1171 }
1172 // Scrolling, a new size or the bar can bring new thumbnails into view.
1173 self.sync_thumbs();
1174 if fx.redraw || fx.layout {
1175 self.request_redraw();
1176 }
1177 }
1178
1179 /// Mirror the keymap's pending keys on the status line and arm the ambiguity timeout.
1180 fn update_pending(&mut self, now: Instant) {
1181 let pending = self.keymap.pending_display();
1182 if pending != self.state.pending {
1183 self.state.pending = pending;
1184 self.request_redraw();
1185 }
1186 self.ambiguous_since = self
1187 .keymap
1188 .is_ambiguous(self.state.mode)
1189 .then(|| self.ambiguous_since.unwrap_or(now));
1190 }
1191
1192 fn on_key(&mut self, event: winit::event::KeyEvent, event_loop: &ActiveEventLoop) {
1193 let now = Instant::now();
1194 if event.state == ElementState::Released {
1195 if let Some(dir) = self.held.remove(&event.physical_key) {
1196 self.state.hold_pan(dir, false, now);
1197 self.request_redraw();
1198 }
1199 if self
1200 .repeat
1201 .as_ref()
1202 .is_some_and(|(k, _)| *k == event.physical_key)
1203 {
1204 self.repeat = None;
1205 }
1206 return;
1207 }
1208 if self.confirm.is_some() {
1209 // The prompt takes the next key: `y` runs the command, anything else cancels.
1210 if !event.repeat
1211 && let Some(key) = input::key(&event.logical_key, self.modifiers)
1212 {
1213 self.answer(key == Key::plain('y') || key == Key::plain('Y'));
1214 }
1215 return;
1216 }
1217 if event.repeat {
1218 // Repeats bypass the trie: only a completed, repeatable binding repeats.
1219 if let Some((k, actions)) = &self.repeat
1220 && *k == event.physical_key
1221 {
1222 let actions = actions.clone();
1223 let _ = self.run(actions, &Ctx::default(), event_loop);
1224 }
1225 return;
1226 }
1227 let Some(key) = input::key(&event.logical_key, self.modifiers) else {
1228 return;
1229 };
1230 let runs = self.keymap.feed(self.state.mode, key);
1231 self.update_pending(now);
1232 self.repeat = None;
1233 for run in runs {
1234 let actions = run.counted();
1235 if let [Action::Pan(dir)] = actions[..]
1236 && self.state.mode == Mode::Image
1237 {
1238 if self.state.shown.is_some() {
1239 self.held.insert(event.physical_key, dir);
1240 self.state.hold_pan(dir, true, now);
1241 self.request_redraw();
1242 }
1243 continue;
1244 }
1245 if !actions.is_empty() && actions.iter().all(Action::repeats) {
1246 self.repeat = Some((event.physical_key, actions.clone()));
1247 }
1248 let _ = self.run(actions, &Ctx::default(), event_loop);
1249 }
1250 }
1251
1252 /// A mouse button or wheel notch: bound actions run at the cursor, bypassing sequences.
1253 fn on_mouse_key(&mut self, key: Key, event_loop: &ActiveEventLoop) {
1254 self.keymap.cancel();
1255 self.update_pending(Instant::now());
1256 if let Some(actions) = self.keymap.lookup(self.state.mode, &[key]) {
1257 let actions = actions.to_vec();
1258 let ctx = Ctx {
1259 anchor: Some(self.cursor),
1260 ..Ctx::default()
1261 };
1262 let _ = self.run(actions, &ctx, event_loop);
1263 }
1264 }
1265
1266 /// `pixels` is set for touchpads, whose deltas are pixels divided by [`PIXELS_PER_STEP`].
1267 fn on_wheel(&mut self, delta: [f32; 2], pixels: bool, event_loop: &ActiveEventLoop) {
1268 let now = Instant::now();
1269 let [dx, dy] = delta;
1270 if dy != 0.0 {
1271 let dir = if dy > 0.0 { Scroll::Up } else { Scroll::Down };
1272 let key = input::scroll(dir, self.modifiers);
1273 // Wheel zoom and grid scrolling stay continuous for touchpads rather than stepping
1274 // per notch.
1275 let in_grid = self.state.in_grid();
1276 match self.keymap.lookup(self.state.mode, &[key]) {
1277 Some([Action::Scroll(d @ (Dir::Up | Dir::Down), n)]) if in_grid => {
1278 let sign = if *d == Dir::Up { -1.0 } else { 1.0 };
1279 let amount = dy.abs() * *n as f32 * sign;
1280 let moved = if pixels {
1281 self.state.grid_drag(-amount * PIXELS_PER_STEP, now)
1282 } else {
1283 self.state.grid_scroll(amount, now)
1284 };
1285 self.after_grid_scroll(moved);
1286 return;
1287 }
1288 Some([Action::Zoom(Zoom::In(n))]) => {
1289 let steps = dy.abs() * *n as f32;
1290 self.state.zoom_at(steps, self.cursor, now);
1291 self.request_redraw();
1292 return;
1293 }
1294 Some([Action::Zoom(Zoom::Out(n))]) => {
1295 let steps = -dy.abs() * *n as f32;
1296 self.state.zoom_at(steps, self.cursor, now);
1297 self.request_redraw();
1298 return;
1299 }
1300 _ => {}
1301 }
1302 }
1303 for (axis, d, pos, neg) in [
1304 (0, dx, Scroll::Left, Scroll::Right),
1305 (1, dy, Scroll::Up, Scroll::Down),
1306 ] {
1307 if d == 0.0 {
1308 continue;
1309 }
1310 // Reversing direction drops the leftover fraction.
1311 if self.scroll[axis] * d < 0.0 {
1312 self.scroll[axis] = 0.0;
1313 }
1314 self.scroll[axis] += d;
1315 while self.scroll[axis].abs() >= 1.0 {
1316 let up = self.scroll[axis] > 0.0;
1317 self.scroll[axis] -= self.scroll[axis].signum();
1318 let key = input::scroll(if up { pos } else { neg }, self.modifiers);
1319 self.on_mouse_key(key, event_loop);
1320 }
1321 }
1322 }
1323
1324 /// The grid scrolled; `moved` says whether it pulled the cursor along.
1325 fn after_grid_scroll(&mut self, moved: bool) {
1326 if moved {
1327 self.sync(true);
1328 } else {
1329 self.sync_thumbs();
1330 }
1331 self.request_redraw();
1332 }
1333
1334 /// A click (press and release in place) in the grid selects; a second one opens.
1335 fn grid_click(&mut self, event_loop: &ActiveEventLoop) {
1336 let now = Instant::now();
1337 let Some(i) = self.state.grid_hit(self.cursor, now) else {
1338 return;
1339 };
1340 let double = self
1341 .last_click
1342 .is_some_and(|(t, j)| j == i && now < t + DOUBLE_CLICK);
1343 self.last_click = (!double).then_some((now, i));
1344 let mut actions = vec![Action::Goto(i + 1)];
1345 if double {
1346 actions.push(Action::Mode(ModeArg::Image));
1347 }
1348 let _ = self.run(actions, &Ctx::default(), event_loop);
1349 }
1350
1351 fn redraw(&mut self) {
1352 let now = Instant::now();
1353 let ready = self.state.shown.is_some()
1354 || self.state.queue.is_empty()
1355 || (self.state.in_grid() && self.grid_settled())
1356 || now >= self.started + SHOW_TIMEOUT;
1357 if !self.shown_window && !ready {
1358 return; // about_to_wait wakes us at the timeout
1359 }
1360 self.state.advance(now);
1361 self.state.tick(now);
1362 let settled = if self.state.in_grid() {
1363 self.thumbs.arrivals.is_empty() && self.grid_settled()
1364 } else {
1365 self.state.loading_since.is_none()
1366 };
1367 // An animation moved on (or a page turned): put that frame in the texture.
1368 let frame = self.state.frame();
1369 if let Some(path) = self
1370 .state
1371 .shown
1372 .as_ref()
1373 .filter(|_| self.state.mode == Mode::Image)
1374 .map(|s| s.path.clone())
1375 && let Some(g) = &self.gfx
1376 && g.renderer.frame(&path).is_some_and(|f| f != frame)
1377 && let Some(img) = self.cache.get(&path)
1378 && let Some(g) = &mut self.gfx
1379 {
1380 g.renderer.show_frame(&path, &img, frame);
1381 }
1382 let Some(g) = &mut self.gfx else { return };
1383 if !self.shown_window {
1384 g.window.set_visible(true);
1385 }
1386 let theme = self.state.theme_at(now);
1387 let scene = ui::scene(&self.state, &theme, now, &mut |t| g.renderer.measure(t));
1388 let result = g.renderer.render(&scene);
1389 if let Some(path) = std::env::var_os("REMAGE_SNAPSHOT")
1390 && !self.state.is_animating(now)
1391 && settled
1392 {
1393 match g.renderer.snapshot(&scene, Path::new(&path)) {
1394 Ok(()) => log::info!("snapshot written to {}", Path::new(&path).display()),
1395 Err(e) => log::error!("snapshot: {e:#}"),
1396 }
1397 }
1398 drop(scene);
1399 if let Err(e) = result {
1400 log::error!("render: {e:#}");
1401 self.state
1402 .notify(format!("error: {e}"), MsgKind::Error, now);
1403 }
1404 if !self.shown_window {
1405 self.shown_window = true;
1406 log::debug!("first frame after {:.1?}", self.started.elapsed());
1407 let proxy = self.proxy.clone();
1408 std::thread::spawn(move || {
1409 let _ = proxy.send_event(UserEvent::Fonts(text::all_fonts()));
1410 });
1411 }
1412 if self.state.is_animating(Instant::now()) {
1413 g.window.request_redraw();
1414 }
1415 }
1416
1417 /// Upload thumbnails, then preloaded images a slice at a time. Returns whether work remains.
1418 fn pump_uploads(&mut self) -> bool {
1419 let Some(g) = &mut self.gfx else { return false };
1420 let now = Instant::now();
1421 let mut bytes = 0;
1422 while bytes < UPLOAD_BUDGET
1423 && let Some((path, t)) = self.thumbs.arrivals.pop_front()
1424 {
1425 bytes += t.pixels.len();
1426 let wanted = &self.thumbs.wanted;
1427 for gone in g.renderer.insert_thumb(&path, &t, |p| wanted.contains(p)) {
1428 self.state.thumbs.remove(&gone);
1429 }
1430 if g.renderer.has_thumb(&path) {
1431 self.state.thumb_ready(path, t.size, now);
1432 }
1433 g.window.request_redraw();
1434 }
1435 if !self.thumbs.arrivals.is_empty() {
1436 return true;
1437 }
1438 if let Some((path, img)) = self.uploads.front()
1439 && g.renderer.upload(path, img, UPLOAD_BUDGET)
1440 {
1441 self.uploads.pop_front();
1442 }
1443 !self.uploads.is_empty()
1444 }
1445}
1446
1447/// A config error trimmed for the status line: `config.toml:2:1: unknown field `fitt`; did you
1448/// mean `fit`?` without the directory or the list of every valid field.
1449fn short_error(message: &str) -> String {
1450 let mut s = match message.split_once(": ") {
1451 Some((loc, rest)) if loc.contains('/') => {
1452 format!("{}: {rest}", loc.rsplit('/').next().unwrap_or(loc))
1453 }
1454 _ => message.to_string(),
1455 };
1456 if let Some(start) = s.find(", expected one of") {
1457 let end = s[start..]
1458 .find("; did you mean")
1459 .map_or(s.len(), |i| start + i);
1460 s.replace_range(start..end, "");
1461 }
1462 s
1463}
1464
1465fn keymap(layers: &[KeyTables]) -> Keymap {
1466 let refs: Vec<&KeyTables> = layers.iter().collect();
1467 Keymap::new(&refs)
1468}
1469
1470fn font_families(c: &Config) -> [String; 3] {
1471 [
1472 c.ui.font_ui.clone(),
1473 c.ui.font_mono.clone(),
1474 c.ui.font_display.clone(),
1475 ]
1476}
1477
1478fn mtime(path: &Path) -> Option<SystemTime> {
1479 std::fs::metadata(path).and_then(|m| m.modified()).ok()
1480}
1481
1482fn lock<T>(m: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
1483 m.lock().unwrap_or_else(|e| e.into_inner())
1484}
1485
1486impl ApplicationHandler<UserEvent> for App {
1487 fn resumed(&mut self, event_loop: &ActiveEventLoop) {
1488 if self.gfx.is_some() {
1489 return;
1490 }
1491 let (w, h) = self.opts.geometry.unwrap_or((1280, 800));
1492 let mut attrs = Window::default_attributes()
1493 .with_title("remage")
1494 .with_inner_size(LogicalSize::new(w, h))
1495 .with_decorations(self.config.window.decorations)
1496 .with_visible(false);
1497 if self.opts.fullscreen {
1498 attrs = attrs.with_fullscreen(Some(Fullscreen::Borderless(None)));
1499 }
1500 let class = self.opts.class.clone();
1501 attrs = winit::platform::wayland::WindowAttributesExtWayland::with_name(
1502 attrs,
1503 class.clone(),
1504 class.clone(),
1505 );
1506 attrs =
1507 winit::platform::x11::WindowAttributesExtX11::with_name(attrs, class.clone(), class);
1508 let window = match event_loop.create_window(attrs) {
1509 Ok(w) => Arc::new(w),
1510 Err(e) => {
1511 self.error = Some(anyhow::anyhow!("create window: {e}"));
1512 event_loop.exit();
1513 return;
1514 }
1515 };
1516 let renderer = match Renderer::new(window.clone(), event_loop.owned_display_handle()) {
1517 Ok(mut r) => {
1518 r.set_font_families(font_families(&self.config));
1519 r
1520 }
1521 Err(e) => {
1522 self.error = Some(e.context("initialise GPU"));
1523 event_loop.exit();
1524 return;
1525 }
1526 };
1527 log::debug!("GPU ready after {:.1?}", self.started.elapsed());
1528 let size = window.inner_size();
1529 self.state.set_window(
1530 size.width as f32,
1531 size.height as f32,
1532 window.scale_factor() as f32,
1533 );
1534 self.gfx = Some(Gfx { renderer, window });
1535 self.sync(false);
1536 }
1537
1538 fn user_event(&mut self, event_loop: &ActiveEventLoop, event: UserEvent) {
1539 match event {
1540 UserEvent::Decoded {
1541 path,
1542 mtime,
1543 result,
1544 } => self.on_decoded(path, mtime, result),
1545 UserEvent::Found(paths) => {
1546 log::debug!("sniffing found {} more images", paths.len());
1547 let was_empty = self.state.queue.is_empty();
1548 self.state.queue.insert_sorted(paths);
1549 self.sync(was_empty);
1550 }
1551 UserEvent::Fonts(db) => {
1552 if let Some(g) = &mut self.gfx {
1553 g.renderer.set_fonts(db);
1554 g.window.request_redraw();
1555 }
1556 }
1557 UserEvent::Config(result) => match *result {
1558 Ok(loaded) => {
1559 log::info!("config reloaded");
1560 self.apply_config(loaded);
1561 }
1562 Err(e) => {
1563 log::error!("{e}");
1564 self.config_failed(&e.message);
1565 }
1566 },
1567 UserEvent::Sorted(files) => {
1568 self.state.queue.reorder(files);
1569 self.sync(false);
1570 }
1571 UserEvent::Thumb {
1572 path,
1573 px,
1574 mtime,
1575 result,
1576 } => self.on_thumb(path, px, mtime, result),
1577 UserEvent::Refined { path, result } => self.on_refined(path, result),
1578 UserEvent::Command { id, event } => self.on_command(id, event, event_loop),
1579 UserEvent::Refreshed(found) => self.on_refreshed(found),
1580 UserEvent::Ipc(inc) => self.on_ipc(inc, event_loop),
1581 }
1582 }
1583
1584 fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
1585 match event {
1586 WindowEvent::CloseRequested => event_loop.exit(),
1587 WindowEvent::Resized(size) => {
1588 let Some(g) = &mut self.gfx else { return };
1589 g.renderer.resize(size.width, size.height);
1590 let sf = g.window.scale_factor() as f32;
1591 self.state
1592 .set_window(size.width as f32, size.height as f32, sf);
1593 self.sync_thumbs();
1594 self.request_redraw();
1595 }
1596 WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
1597 let [w, h] = self.state.window();
1598 self.state.set_window(w, h, scale_factor as f32);
1599 self.sync_thumbs();
1600 self.request_redraw();
1601 }
1602 WindowEvent::RedrawRequested => self.redraw(),
1603 WindowEvent::ModifiersChanged(m) => self.modifiers = m.state(),
1604 WindowEvent::Focused(false) => {
1605 self.held.clear();
1606 self.state.stop_all_pans();
1607 self.dragging = false;
1608 }
1609 WindowEvent::KeyboardInput {
1610 event,
1611 is_synthetic: false,
1612 ..
1613 } => {
1614 self.on_key(event, event_loop);
1615 }
1616 WindowEvent::CursorMoved { position, .. } => {
1617 let p = [position.x as f32, position.y as f32];
1618 let delta = [p[0] - self.cursor[0], p[1] - self.cursor[1]];
1619 self.cursor = p;
1620 if self.dragging && self.state.in_grid() {
1621 let moved = self.state.grid_drag(delta[1], Instant::now());
1622 self.after_grid_scroll(moved);
1623 } else if self.dragging && self.state.shown.is_some() {
1624 self.state.view.pan_pixels(delta);
1625 self.request_redraw();
1626 }
1627 }
1628 WindowEvent::MouseInput { state, button, .. } => {
1629 let pressed = state == ElementState::Pressed;
1630 if button == MouseButton::Left {
1631 // Drag pans; a press and release in place is a `<LeftMouse>` click.
1632 self.dragging = pressed;
1633 if pressed {
1634 self.press = Some(self.cursor);
1635 return;
1636 }
1637 let Some(p) = self.press.take() else { return };
1638 let moved = (self.cursor[0] - p[0]).hypot(self.cursor[1] - p[1]);
1639 if moved > CLICK_SLOP * self.state.scale_factor() {
1640 return;
1641 }
1642 if self.state.in_grid() {
1643 self.grid_click(event_loop);
1644 }
1645 } else if !pressed {
1646 return;
1647 }
1648 if let Some(key) = input::button(button, self.modifiers) {
1649 self.on_mouse_key(key, event_loop);
1650 }
1651 }
1652 WindowEvent::MouseWheel { delta, .. } => {
1653 let (d, pixels) = match delta {
1654 MouseScrollDelta::LineDelta(x, y) => ([x, y], false),
1655 MouseScrollDelta::PixelDelta(p) => (
1656 [p.x as f32 / PIXELS_PER_STEP, p.y as f32 / PIXELS_PER_STEP],
1657 true,
1658 ),
1659 };
1660 self.on_wheel(d, pixels, event_loop);
1661 }
1662 _ => {}
1663 }
1664 }
1665
1666 fn new_events(&mut self, _event_loop: &ActiveEventLoop, cause: StartCause) {
1667 if matches!(cause, StartCause::ResumeTimeReached { .. }) {
1668 self.request_redraw();
1669 }
1670 }
1671
1672 fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
1673 let now = Instant::now();
1674 let busy = self.pump_uploads();
1675 if let Some(t) = self.ambiguous_since
1676 && now >= t + SEQUENCE_TIMEOUT
1677 {
1678 let runs = self.keymap.timeout(self.state.mode);
1679 self.update_pending(now);
1680 for run in runs {
1681 let _ = self.run(run.counted(), &Ctx::default(), event_loop);
1682 }
1683 }
1684 if let Some(t) = self.grid_rest
1685 && now >= t + GRID_REST
1686 {
1687 self.grid_rest = None;
1688 if self.state.in_grid()
1689 && let Some(cur) = self.state.queue.current().map(Path::to_path_buf)
1690 && !self.cache_has(&cur)
1691 {
1692 self.decode(&cur, Priority::Preload);
1693 }
1694 }
1695 self.publish();
1696 let (redraw, mut next) = self.state.tick(now);
1697 if let Some(t) = self.grid_rest {
1698 let at = t + GRID_REST;
1699 next = Some(next.map_or(at, |n| n.min(at)));
1700 }
1701 if let Some(t) = self.ambiguous_since {
1702 let at = t + SEQUENCE_TIMEOUT;
1703 next = Some(next.map_or(at, |n| n.min(at)));
1704 }
1705 if let Some(at) = self.check_refine(now) {
1706 next = Some(next.map_or(at, |n| n.min(at)));
1707 }
1708 if redraw {
1709 self.request_redraw();
1710 }
1711 if !self.shown_window {
1712 let at = self.started + SHOW_TIMEOUT;
1713 next = Some(next.map_or(at, |n| n.min(at)));
1714 }
1715 event_loop.set_control_flow(match (busy, next) {
1716 (true, _) => ControlFlow::Poll,
1717 (false, Some(t)) => ControlFlow::WaitUntil(t),
1718 (false, None) => ControlFlow::Wait,
1719 });
1720 }
1721}
1722
1723#[cfg(test)]
1724mod tests {
1725 use super::*;
1726
1727 #[test]
1728 fn config_errors_are_shortened_for_the_bar() {
1729 assert_eq!(
1730 short_error(
1731 "/home/u/.config/remage/config.toml:2:1: unknown field `fitt`, expected one of \
1732 `fit`, `zoom_step`; did you mean `fit`?"
1733 ),
1734 "config.toml:2:1: unknown field `fitt`; did you mean `fit`?"
1735 );
1736 assert_eq!(
1737 short_error("a.toml:1:1: unknown field `x`, expected `y`"),
1738 "a.toml:1:1: unknown field `x`, expected `y`"
1739 );
1740 }
1741}