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