1use std::path::PathBuf;
2use std::sync::Arc;
3use std::time::Instant;
4
5use image::ImageDecoder;
6use tracing::info;
7use winit::application::ApplicationHandler;
8use winit::dpi::LogicalSize;
9use winit::event::WindowEvent;
10use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
11use winit::window::{Window, WindowId};
12
13use crate::animation::{Effect, compute_effect_uniforms};
14use crate::renderer::Renderer;
15
16#[derive(Debug, thiserror::Error)]
17pub enum PreviewError {
18 #[error("failed to open preview window: {0}")]
19 Window(String),
20}
21
22pub struct PreviewWindow {
23 pub target_path: PathBuf,
24 pub duration: std::time::Duration,
25 pub effect: Effect,
26}
27
28impl PreviewWindow {
29 pub fn new(target_path: PathBuf) -> Self {
30 Self {
31 target_path,
32 duration: std::time::Duration::from_millis(2000),
33 effect: Effect::Grow(crate::animation::GrowParams::default()),
34 }
35 }
36
37 pub async fn run(&self) -> Result<(), PreviewError> {
38 info!("Initializing preview mode for: {:?}", self.target_path);
39
40 let event_loop = EventLoop::new().map_err(|e| PreviewError::Window(e.to_string()))?;
41 event_loop.set_control_flow(ControlFlow::Poll);
42
43 let mut app = PreviewApp::new(self.target_path.clone(), self.duration, self.effect.clone());
44 event_loop
45 .run_app(&mut app)
46 .map_err(|e| PreviewError::Window(e.to_string()))?;
47 Ok(())
48 }
49}
50
51fn last_wallpaper_state() -> Option<PathBuf> {
53 let state_path = dirs::cache_dir()
54 .unwrap_or_else(|| std::path::PathBuf::from("/tmp"))
55 .join("wallr/last_wallpaper");
56 std::fs::read_to_string(state_path)
57 .ok()
58 .map(|s| PathBuf::from(s.trim()))
59}
60
61fn load_last_wallpaper(target: &std::path::Path) -> Option<image::DynamicImage> {
65 let last = last_wallpaper_state().filter(|p| p.exists())?;
66 let same = last.canonicalize().unwrap_or_else(|_| last.clone())
67 == target
68 .canonicalize()
69 .unwrap_or_else(|_| target.to_path_buf());
70 if same {
71 return None;
72 }
73 let decoder = image::ImageReader::open(&last).ok()?.into_decoder().ok()?;
74 let (width, height) = decoder.dimensions();
75 Renderer::validate_static_decode(width, height, decoder.total_bytes()).ok()?;
76 image::DynamicImage::from_decoder(decoder).ok()
77}
78
79struct PreviewApp {
80 target_path: PathBuf,
81 duration: std::time::Duration,
82 effect: Effect,
83 window: Option<Arc<Window>>,
84 renderer: Option<Renderer>,
85 surface: Option<wgpu::Surface<'static>>,
86 surface_format: Option<wgpu::TextureFormat>,
87 bg_bind: Option<wgpu::BindGroup>,
88 new_bind: Option<wgpu::BindGroup>,
89 _bg_tex: Option<wgpu::Texture>,
90 _new_tex: Option<wgpu::Texture>,
91 start: Option<Instant>,
92 img_size: (u32, u32),
93 old_img_size: (u32, u32),
94 animated: Option<crate::animated::AnimatedImage>,
95 play_tex: Option<wgpu::Texture>,
96 play_bind: Option<wgpu::BindGroup>,
97 shown_frame: usize,
98 video: Option<crate::video::VideoPlayback>,
99 video_texture: Option<crate::renderer::VideoTexture>,
100 video_size: (u32, u32),
101 per_output_uniforms: Option<crate::renderer::PerOutputUniforms>,
102}
103
104impl PreviewApp {
105 fn new(target_path: PathBuf, duration: std::time::Duration, effect: Effect) -> Self {
106 Self {
107 target_path,
108 duration,
109 effect,
110 window: None,
111 renderer: None,
112 surface: None,
113 surface_format: None,
114 bg_bind: None,
115 new_bind: None,
116 _bg_tex: None,
117 _new_tex: None,
118 start: None,
119 img_size: (1, 1),
120 old_img_size: (1, 1),
121 animated: None,
122 play_tex: None,
123 play_bind: None,
124 shown_frame: usize::MAX,
125 video: None,
126 video_texture: None,
127 video_size: (1, 1),
128 per_output_uniforms: None,
129 }
130 }
131
132 fn configure_surface(
133 &self,
134 renderer: &Renderer,
135 surface: &wgpu::Surface<'static>,
136 format: wgpu::TextureFormat,
137 width: u32,
138 height: u32,
139 ) {
140 surface.configure(
141 &renderer.device,
142 &wgpu::SurfaceConfiguration {
143 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
144 format,
145 width: width.max(1),
146 height: height.max(1),
147 present_mode: wgpu::PresentMode::AutoVsync,
148 alpha_mode: wgpu::CompositeAlphaMode::Auto,
149 view_formats: vec![],
150 desired_maximum_frame_latency: 2,
151 },
152 );
153 }
154}
155
156impl ApplicationHandler for PreviewApp {
157 fn resumed(&mut self, event_loop: &ActiveEventLoop) {
158 if self.window.is_some() {
159 return;
160 }
161
162 let window = match event_loop.create_window(
163 Window::default_attributes()
164 .with_title("wallr preview")
165 .with_inner_size(LogicalSize::new(1280.0, 720.0)),
166 ) {
167 Ok(w) => Arc::new(w),
168 Err(e) => {
169 eprintln!("failed to create preview window: {e}");
170 event_loop.exit();
171 return;
172 }
173 };
174
175 let renderer = match pollster::block_on(Renderer::new()) {
176 Ok(r) => r,
177 Err(e) => {
178 eprintln!("renderer init failed: {e}");
179 event_loop.exit();
180 return;
181 }
182 };
183
184 let surface = match renderer.instance.create_surface(window.clone()) {
185 Ok(s) => s,
186 Err(e) => {
187 eprintln!("surface creation failed: {e}");
188 event_loop.exit();
189 return;
190 }
191 };
192
193 let caps = surface.get_capabilities(&renderer.adapter);
194 let format = caps
195 .formats
196 .iter()
197 .find(|f| f.is_srgb())
198 .copied()
199 .unwrap_or(caps.formats[0]);
200
201 let size = window.inner_size();
202 self.configure_surface(&renderer, &surface, format, size.width, size.height);
203
204 if crate::video::VideoDecoder::is_video_file(&self.target_path) {
207 let playback = crate::video::VideoPlayback::new();
208 match crate::video::VideoPlayback::prepare(
209 &self.target_path,
210 crate::video::HwAccel::from_config("auto"),
211 crate::config::VideoConfig::default().preload_frames,
212 std::time::Duration::from_millis(2000),
213 |metadata| {
214 renderer
215 .validate_video_texture(metadata.width, metadata.height)
216 .map_err(crate::video::VideoError::GpuResourceCreation)
217 },
218 ) {
219 Ok(mut prepared) => {
220 let meta = prepared.metadata().clone();
221 let first = prepared.take_first_frame();
222 let (w, h) = (meta.width, meta.height);
223 let texture = match renderer.create_video_texture(w, h) {
224 Ok(texture) => texture,
225 Err(e) => {
226 eprintln!("failed to create video texture: {e}");
227 event_loop.exit();
228 return;
229 }
230 };
231 if let Some(frame) = first {
232 if let Err(e) = renderer.update_video_texture(&texture, &frame.data) {
233 eprintln!("failed to upload first video frame: {e}");
234 event_loop.exit();
235 return;
236 }
237 }
238 playback.commit(prepared, 0);
239 self.video = Some(playback);
240 self.video_texture = Some(texture);
241 self.video_size = (w, h);
242 self.window = Some(window);
243 self.per_output_uniforms = Some(renderer.create_per_output_uniforms());
244 self.renderer = Some(renderer);
245 self.surface = Some(surface);
246 self.surface_format = Some(format);
247 self.start = Some(Instant::now());
248 return;
249 }
250 Err(e) => {
251 eprintln!("failed to start video preview: {e}");
252 event_loop.exit();
253 return;
254 }
255 }
256 }
257
258 let img = match image::ImageReader::open(&self.target_path) {
259 Ok(reader) => match reader.into_decoder() {
260 Ok(decoder) => {
261 let (width, height) = decoder.dimensions();
262 if let Err(e) =
263 Renderer::validate_static_decode(width, height, decoder.total_bytes())
264 {
265 eprintln!("failed to decode image: {e}");
266 event_loop.exit();
267 return;
268 }
269 match image::DynamicImage::from_decoder(decoder) {
270 Ok(img) => img,
271 Err(e) => {
272 eprintln!("failed to decode image: {e}");
273 event_loop.exit();
274 return;
275 }
276 }
277 }
278 Err(e) => {
279 eprintln!("failed to decode image: {e}");
280 event_loop.exit();
281 return;
282 }
283 },
284 Err(e) => {
285 eprintln!("failed to open image: {e}");
286 event_loop.exit();
287 return;
288 }
289 };
290
291 let (new_tex, new_bind, w, h) =
292 match renderer.load_texture(&img, size.width, size.height, 0) {
293 Ok(t) => t,
294 Err(e) => {
295 eprintln!("failed to upload image: {e}");
296 event_loop.exit();
297 return;
298 }
299 };
300
301 let (bg_tex, bg_bind, old_size) =
307 match load_last_wallpaper(&self.target_path).and_then(|bg| {
308 renderer
309 .load_texture(&bg, size.width, size.height, 0)
310 .ok()
311 .map(|(tex, bind, width, height)| (tex, bind, (width, height)))
312 }) {
313 Some((tex, bind, size)) => (tex, bind, size),
314 None => {
315 let black = image::DynamicImage::ImageRgba8(image::ImageBuffer::from_pixel(
316 w.max(1),
317 h.max(1),
318 image::Rgba([0, 0, 0, 255]),
319 ));
320 match renderer.load_texture(&black, size.width, size.height, 0) {
321 Ok((tex, bind, width, height)) => (tex, bind, (width, height)),
322 Err(e) => {
323 eprintln!("failed to upload background: {e}");
324 event_loop.exit();
325 return;
326 }
327 }
328 }
329 };
330
331 self.animated = crate::animated::AnimatedImage::decode(&self.target_path)
335 .ok()
336 .flatten();
337 if let Some(anim) = self.animated.as_mut() {
338 let (w, h) = (anim.width, anim.height);
339 let (tex, bind) = match renderer.create_texture(w, h) {
340 Ok(texture) => texture,
341 Err(e) => {
342 eprintln!("failed to create GIF texture: {e}");
343 event_loop.exit();
344 return;
345 }
346 };
347 let first = anim.first_frame();
348 if !first.is_empty() {
349 renderer.update_texture(&tex, first, w, h);
350 }
351 self.play_tex = Some(tex);
352 self.play_bind = Some(bind);
353 }
354
355 self.window = Some(window);
356 self.per_output_uniforms = Some(renderer.create_per_output_uniforms());
357 self.renderer = Some(renderer);
358 self.surface = Some(surface);
359 self.surface_format = Some(format);
360 self.bg_bind = Some(bg_bind);
361 self.new_bind = Some(new_bind);
362 self._bg_tex = Some(bg_tex);
363 self._new_tex = Some(new_tex);
364 self.img_size = (w, h);
365 self.old_img_size = old_size;
366 self.start = Some(Instant::now());
367 }
368
369 fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
370 match event {
371 WindowEvent::CloseRequested => event_loop.exit(),
372 WindowEvent::Resized(size) => {
373 if let (Some(renderer), Some(surface), Some(format)) =
374 (&self.renderer, &self.surface, self.surface_format)
375 {
376 self.configure_surface(renderer, surface, format, size.width, size.height);
377 }
378 }
379 WindowEvent::RedrawRequested => self.render_frame(event_loop),
380 _ => {}
381 }
382 }
383
384 fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
385 if let Some(window) = &self.window {
386 window.request_redraw();
387 }
388 }
389}
390
391impl PreviewApp {
392 fn render_frame(&mut self, event_loop: &ActiveEventLoop) {
393 if self.video.is_some() {
395 self.render_video_frame(event_loop);
396 return;
397 }
398
399 let Some(start) = self.start else { return };
400
401 let elapsed = start.elapsed().as_millis() as f32;
402 let total = self.duration.as_millis() as f32;
403 let progress = if self.animated.is_some() {
404 (elapsed / total).min(1.0)
407 } else {
408 let hold = total * 0.25;
413 let cycle = elapsed % (total + hold);
414 (cycle / total).min(1.0)
415 };
416
417 let size = self
418 .window
419 .as_ref()
420 .map(|w| w.inner_size())
421 .unwrap_or(winit::dpi::PhysicalSize::new(1, 1));
422
423 if self.animated.is_some() && progress >= 1.0 {
424 self.render_live_frame(event_loop, size, elapsed, total);
425 return;
426 }
427
428 let Some(renderer) = &self.renderer else {
429 return;
430 };
431 let Some(surface) = &self.surface else { return };
432 let Some(format) = self.surface_format else {
433 return;
434 };
435 let Some(bg) = &self.bg_bind else { return };
436 let Some(new_bind) = &self.new_bind else {
437 return;
438 };
439
440 let (img_w, img_h) = self.img_size;
441 let (old_w, old_h) = self.old_img_size;
442
443 let uniforms = compute_effect_uniforms(&self.effect, progress);
444
445 match renderer.render_frame(
446 crate::renderer::FrameRequest {
447 surface,
448 format,
449 bg_bind: bg,
450 new_bind,
451 effect: &uniforms,
452 width: size.width,
453 height: size.height,
454 img_width: img_w,
455 img_height: img_h,
456 old_img_width: old_w,
457 old_img_height: old_h,
458 scaling_mode: 0,
459 },
460 self.per_output_uniforms.as_ref().unwrap(),
461 ) {
462 Ok(crate::renderer::FrameStatus::TimedOut) => {
463 }
467 Err(e) => {
468 eprintln!("render error: {e}");
469 event_loop.exit();
470 }
471 _ => {}
472 }
473 }
474
475 fn render_video_frame(&mut self, event_loop: &ActiveEventLoop) {
478 let Some(renderer) = &self.renderer else {
479 return;
480 };
481 let Some(surface) = &self.surface else { return };
482 let Some(format) = self.surface_format else {
483 return;
484 };
485 let Some(playback) = &self.video else { return };
486 let Some(texture) = &self.video_texture else {
487 return;
488 };
489
490 let size = self
491 .window
492 .as_ref()
493 .map(|w| w.inner_size())
494 .unwrap_or(winit::dpi::PhysicalSize::new(1, 1));
495
496 let (w, h) = self.video_size;
497 if let Some(frame) = playback.next_frame()
498 && frame.width == w
499 && frame.height == h
500 {
501 if let Err(e) = renderer.update_video_texture(texture, &frame.data) {
502 eprintln!("video upload error: {e}");
503 event_loop.exit();
504 return;
505 }
506 }
507
508 let uniforms = compute_effect_uniforms(&self.effect, 1.0);
509 match renderer.render_frame(
510 crate::renderer::FrameRequest {
511 surface,
512 format,
513 bg_bind: texture.bind_group(),
514 new_bind: texture.bind_group(),
515 effect: &uniforms,
516 width: size.width.max(1),
517 height: size.height.max(1),
518 img_width: w,
519 img_height: h,
520 old_img_width: w,
521 old_img_height: h,
522 scaling_mode: 0,
523 },
524 self.per_output_uniforms.as_ref().unwrap(),
525 ) {
526 Ok(crate::renderer::FrameStatus::TimedOut) => {}
527 Err(e) => {
528 eprintln!("render error: {e}");
529 event_loop.exit();
530 }
531 _ => {}
532 }
533 }
534
535 fn render_live_frame(
540 &mut self,
541 event_loop: &ActiveEventLoop,
542 size: winit::dpi::PhysicalSize<u32>,
543 elapsed: f32,
544 total: f32,
545 ) {
546 let Some(renderer) = &self.renderer else {
547 return;
548 };
549 let Some(surface) = &self.surface else { return };
550 let Some(format) = self.surface_format else {
551 return;
552 };
553 let Some(anim) = self.animated.as_mut() else {
554 return;
555 };
556 let Some(tex) = &self.play_tex else { return };
557 let Some(bind) = &self.play_bind else { return };
558
559 let (anim_w, anim_h) = (anim.width, anim.height);
560 let live_ms = (elapsed - total).max(0.0) as u64;
561 let index = anim.frame_index_at(std::time::Duration::from_millis(live_ms));
562 if index != self.shown_frame {
563 let frame = anim.frame_at(index);
564 if !frame.is_empty() {
565 renderer.update_texture(tex, frame, anim_w, anim_h);
566 self.shown_frame = index;
567 }
568 }
569
570 let uniforms = compute_effect_uniforms(&self.effect, 1.0);
571 match renderer.render_frame(
572 crate::renderer::FrameRequest {
573 surface,
574 format,
575 bg_bind: bind,
576 new_bind: bind,
577 effect: &uniforms,
578 width: size.width,
579 height: size.height,
580 img_width: anim.width,
581 img_height: anim.height,
582 old_img_width: anim.width,
583 old_img_height: anim.height,
584 scaling_mode: 0,
585 },
586 self.per_output_uniforms.as_ref().unwrap(),
587 ) {
588 Ok(crate::renderer::FrameStatus::TimedOut) => {}
589 Err(e) => {
590 eprintln!("render error: {e}");
591 event_loop.exit();
592 }
593 _ => {}
594 }
595 }
596}