1use std::path::PathBuf;
2use std::sync::Arc;
3use std::time::Instant;
4
5use image::GenericImageView;
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 image::ImageReader::open(&last).ok()?.decode().ok()
74}
75
76struct PreviewApp {
77 target_path: PathBuf,
78 duration: std::time::Duration,
79 effect: Effect,
80 window: Option<Arc<Window>>,
81 renderer: Option<Renderer>,
82 surface: Option<wgpu::Surface<'static>>,
83 surface_format: Option<wgpu::TextureFormat>,
84 bg_bind: Option<wgpu::BindGroup>,
85 new_bind: Option<wgpu::BindGroup>,
86 _bg_tex: Option<wgpu::Texture>,
87 _new_tex: Option<wgpu::Texture>,
88 start: Option<Instant>,
89 img_size: (u32, u32),
90 old_img_size: (u32, u32),
91 animated: Option<crate::animated::AnimatedImage>,
92 play_tex: Option<wgpu::Texture>,
93 play_bind: Option<wgpu::BindGroup>,
94 shown_frame: usize,
95 video: Option<crate::video::VideoPlayback>,
96 video_tex: Option<wgpu::Texture>,
97 video_bind: Option<wgpu::BindGroup>,
98 video_size: (u32, u32),
99 per_output_uniforms: Option<crate::renderer::PerOutputUniforms>,
100}
101
102impl PreviewApp {
103 fn new(target_path: PathBuf, duration: std::time::Duration, effect: Effect) -> Self {
104 Self {
105 target_path,
106 duration,
107 effect,
108 window: None,
109 renderer: None,
110 surface: None,
111 surface_format: None,
112 bg_bind: None,
113 new_bind: None,
114 _bg_tex: None,
115 _new_tex: None,
116 start: None,
117 img_size: (1, 1),
118 old_img_size: (1, 1),
119 animated: None,
120 play_tex: None,
121 play_bind: None,
122 shown_frame: usize::MAX,
123 video: None,
124 video_tex: None,
125 video_bind: None,
126 video_size: (1, 1),
127 per_output_uniforms: None,
128 }
129 }
130
131 fn configure_surface(
132 &self,
133 renderer: &Renderer,
134 surface: &wgpu::Surface<'static>,
135 format: wgpu::TextureFormat,
136 width: u32,
137 height: u32,
138 ) {
139 surface.configure(
140 &renderer.device,
141 &wgpu::SurfaceConfiguration {
142 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
143 format,
144 width: width.max(1),
145 height: height.max(1),
146 present_mode: wgpu::PresentMode::AutoVsync,
147 alpha_mode: wgpu::CompositeAlphaMode::Auto,
148 view_formats: vec![],
149 desired_maximum_frame_latency: 2,
150 },
151 );
152 }
153}
154
155impl ApplicationHandler for PreviewApp {
156 fn resumed(&mut self, event_loop: &ActiveEventLoop) {
157 if self.window.is_some() {
158 return;
159 }
160
161 let window = match event_loop.create_window(
162 Window::default_attributes()
163 .with_title("wallr preview")
164 .with_inner_size(LogicalSize::new(1280.0, 720.0)),
165 ) {
166 Ok(w) => Arc::new(w),
167 Err(e) => {
168 eprintln!("failed to create preview window: {e}");
169 event_loop.exit();
170 return;
171 }
172 };
173
174 let renderer = match pollster::block_on(Renderer::new()) {
175 Ok(r) => r,
176 Err(e) => {
177 eprintln!("renderer init failed: {e}");
178 event_loop.exit();
179 return;
180 }
181 };
182
183 let surface = match renderer.instance.create_surface(window.clone()) {
184 Ok(s) => s,
185 Err(e) => {
186 eprintln!("surface creation failed: {e}");
187 event_loop.exit();
188 return;
189 }
190 };
191
192 let caps = surface.get_capabilities(&renderer.adapter);
193 let format = caps
194 .formats
195 .iter()
196 .find(|f| f.is_srgb())
197 .copied()
198 .unwrap_or(caps.formats[0]);
199
200 let size = window.inner_size();
201 self.configure_surface(&renderer, &surface, format, size.width, size.height);
202
203 if crate::video::VideoDecoder::is_video_file(&self.target_path) {
206 let playback = crate::video::VideoPlayback::new();
207 match playback.start(
208 &self.target_path,
209 crate::video::HwAccel::from_config("auto"),
210 ) {
211 Ok(meta) => {
212 let first = playback.wait_first_frame(std::time::Duration::from_millis(2000));
213 let (w, h) = (meta.width, meta.height);
214 let (tex, bind) = renderer.create_texture(w, h);
215 if let Some(frame) = first {
216 renderer.update_texture(&tex, &frame.data, frame.width, frame.height);
217 }
218 self.video = Some(playback);
219 self.video_tex = Some(tex);
220 self.video_bind = Some(bind);
221 self.video_size = (w, h);
222 self.window = Some(window);
223 self.per_output_uniforms = Some(renderer.create_per_output_uniforms());
224 self.renderer = Some(renderer);
225 self.surface = Some(surface);
226 self.surface_format = Some(format);
227 self.start = Some(Instant::now());
228 return;
229 }
230 Err(e) => {
231 eprintln!("failed to start video preview: {e}");
232 event_loop.exit();
233 return;
234 }
235 }
236 }
237
238 let img = match image::ImageReader::open(&self.target_path) {
239 Ok(reader) => match reader.decode() {
240 Ok(img) => img,
241 Err(e) => {
242 eprintln!("failed to decode image: {e}");
243 event_loop.exit();
244 return;
245 }
246 },
247 Err(e) => {
248 eprintln!("failed to open image: {e}");
249 event_loop.exit();
250 return;
251 }
252 };
253
254 let (new_tex, new_bind) = match renderer.load_texture(&img) {
255 Ok(t) => t,
256 Err(e) => {
257 eprintln!("failed to upload image: {e}");
258 event_loop.exit();
259 return;
260 }
261 };
262
263 let (w, h) = img.dimensions();
264
265 let (bg_tex, bg_bind, old_size) =
271 match load_last_wallpaper(&self.target_path).and_then(|bg| {
272 renderer
273 .load_texture(&bg)
274 .ok()
275 .map(|(tex, bind)| (tex, bind, bg.dimensions()))
276 }) {
277 Some((tex, bind, size)) => (tex, bind, size),
278 None => {
279 let black = image::DynamicImage::ImageRgba8(image::ImageBuffer::from_pixel(
280 w.max(1),
281 h.max(1),
282 image::Rgba([0, 0, 0, 255]),
283 ));
284 match renderer.load_texture(&black) {
285 Ok((tex, bind)) => (tex, bind, (w, h)),
286 Err(e) => {
287 eprintln!("failed to upload background: {e}");
288 event_loop.exit();
289 return;
290 }
291 }
292 }
293 };
294
295 self.animated = crate::animated::AnimatedImage::decode(&self.target_path)
299 .ok()
300 .flatten();
301 if let Some(anim) = self.animated.as_mut() {
302 let (w, h) = (anim.width, anim.height);
303 let (tex, bind) = renderer.create_texture(w, h);
304 let first = anim.first_frame();
305 if !first.is_empty() {
306 renderer.update_texture(&tex, first, w, h);
307 }
308 self.play_tex = Some(tex);
309 self.play_bind = Some(bind);
310 }
311
312 self.window = Some(window);
313 self.per_output_uniforms = Some(renderer.create_per_output_uniforms());
314 self.renderer = Some(renderer);
315 self.surface = Some(surface);
316 self.surface_format = Some(format);
317 self.bg_bind = Some(bg_bind);
318 self.new_bind = Some(new_bind);
319 self._bg_tex = Some(bg_tex);
320 self._new_tex = Some(new_tex);
321 self.img_size = img.dimensions();
322 self.old_img_size = old_size;
323 self.start = Some(Instant::now());
324 }
325
326 fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
327 match event {
328 WindowEvent::CloseRequested => event_loop.exit(),
329 WindowEvent::Resized(size) => {
330 if let (Some(renderer), Some(surface), Some(format)) =
331 (&self.renderer, &self.surface, self.surface_format)
332 {
333 self.configure_surface(renderer, surface, format, size.width, size.height);
334 }
335 }
336 WindowEvent::RedrawRequested => self.render_frame(event_loop),
337 _ => {}
338 }
339 }
340
341 fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
342 if let Some(window) = &self.window {
343 window.request_redraw();
344 }
345 }
346}
347
348impl PreviewApp {
349 fn render_frame(&mut self, event_loop: &ActiveEventLoop) {
350 if self.video.is_some() {
352 self.render_video_frame(event_loop);
353 return;
354 }
355
356 let Some(start) = self.start else { return };
357
358 let elapsed = start.elapsed().as_millis() as f32;
359 let total = self.duration.as_millis() as f32;
360 let progress = if self.animated.is_some() {
361 (elapsed / total).min(1.0)
364 } else {
365 let hold = total * 0.25;
370 let cycle = elapsed % (total + hold);
371 (cycle / total).min(1.0)
372 };
373
374 let size = self
375 .window
376 .as_ref()
377 .map(|w| w.inner_size())
378 .unwrap_or(winit::dpi::PhysicalSize::new(1, 1));
379
380 if self.animated.is_some() && progress >= 1.0 {
381 self.render_live_frame(event_loop, size, elapsed, total);
382 return;
383 }
384
385 let Some(renderer) = &self.renderer else {
386 return;
387 };
388 let Some(surface) = &self.surface else { return };
389 let Some(format) = self.surface_format else {
390 return;
391 };
392 let Some(bg) = &self.bg_bind else { return };
393 let Some(new_bind) = &self.new_bind else {
394 return;
395 };
396
397 let (img_w, img_h) = self.img_size;
398 let (old_w, old_h) = self.old_img_size;
399
400 let uniforms = compute_effect_uniforms(&self.effect, progress);
401
402 match renderer.render_frame(
403 crate::renderer::FrameRequest {
404 surface,
405 format,
406 bg_bind: bg,
407 new_bind,
408 effect: &uniforms,
409 width: size.width,
410 height: size.height,
411 img_width: img_w,
412 img_height: img_h,
413 old_img_width: old_w,
414 old_img_height: old_h,
415 scaling_mode: 0,
416 },
417 self.per_output_uniforms.as_ref().unwrap(),
418 ) {
419 Ok(crate::renderer::FrameStatus::TimedOut) => {
420 }
424 Err(e) => {
425 eprintln!("render error: {e}");
426 event_loop.exit();
427 }
428 _ => {}
429 }
430 }
431
432 fn render_video_frame(&mut self, event_loop: &ActiveEventLoop) {
435 let Some(renderer) = &self.renderer else {
436 return;
437 };
438 let Some(surface) = &self.surface else { return };
439 let Some(format) = self.surface_format else {
440 return;
441 };
442 let Some(playback) = &self.video else { return };
443 let Some(tex) = &self.video_tex else { return };
444 let Some(bind) = &self.video_bind else { return };
445
446 let size = self
447 .window
448 .as_ref()
449 .map(|w| w.inner_size())
450 .unwrap_or(winit::dpi::PhysicalSize::new(1, 1));
451
452 let (w, h) = self.video_size;
453 if let Some(frame) = playback.next_frame()
454 && frame.width == w
455 && frame.height == h
456 {
457 renderer.update_texture(tex, &frame.data, frame.width, frame.height);
458 }
459
460 let uniforms = compute_effect_uniforms(&self.effect, 1.0);
461 match renderer.render_frame(
462 crate::renderer::FrameRequest {
463 surface,
464 format,
465 bg_bind: bind,
466 new_bind: bind,
467 effect: &uniforms,
468 width: size.width.max(1),
469 height: size.height.max(1),
470 img_width: w,
471 img_height: h,
472 old_img_width: w,
473 old_img_height: h,
474 scaling_mode: 0,
475 },
476 self.per_output_uniforms.as_ref().unwrap(),
477 ) {
478 Ok(crate::renderer::FrameStatus::TimedOut) => {}
479 Err(e) => {
480 eprintln!("render error: {e}");
481 event_loop.exit();
482 }
483 _ => {}
484 }
485 }
486
487 fn render_live_frame(
492 &mut self,
493 event_loop: &ActiveEventLoop,
494 size: winit::dpi::PhysicalSize<u32>,
495 elapsed: f32,
496 total: f32,
497 ) {
498 let Some(renderer) = &self.renderer else {
499 return;
500 };
501 let Some(surface) = &self.surface else { return };
502 let Some(format) = self.surface_format else {
503 return;
504 };
505 let Some(anim) = self.animated.as_mut() else {
506 return;
507 };
508 let Some(tex) = &self.play_tex else { return };
509 let Some(bind) = &self.play_bind else { return };
510
511 let (anim_w, anim_h) = (anim.width, anim.height);
512 let live_ms = (elapsed - total).max(0.0) as u64;
513 let index = anim.frame_index_at(std::time::Duration::from_millis(live_ms));
514 if index != self.shown_frame {
515 let frame = anim.frame_at(index);
516 if !frame.is_empty() {
517 renderer.update_texture(tex, frame, anim_w, anim_h);
518 self.shown_frame = index;
519 }
520 }
521
522 let uniforms = compute_effect_uniforms(&self.effect, 1.0);
523 match renderer.render_frame(
524 crate::renderer::FrameRequest {
525 surface,
526 format,
527 bg_bind: bind,
528 new_bind: bind,
529 effect: &uniforms,
530 width: size.width,
531 height: size.height,
532 img_width: anim.width,
533 img_height: anim.height,
534 old_img_width: anim.width,
535 old_img_height: anim.height,
536 scaling_mode: 0,
537 },
538 self.per_output_uniforms.as_ref().unwrap(),
539 ) {
540 Ok(crate::renderer::FrameStatus::TimedOut) => {}
541 Err(e) => {
542 eprintln!("render error: {e}");
543 event_loop.exit();
544 }
545 _ => {}
546 }
547 }
548}