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}
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_tex: None,
124 video_bind: None,
125 video_size: (1, 1),
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 ) {
209 Ok(meta) => {
210 let first = playback.wait_first_frame(std::time::Duration::from_millis(2000));
211 let (w, h) = (meta.width, meta.height);
212 let (tex, bind) = renderer.create_texture(w, h);
213 if let Some(frame) = first {
214 renderer.update_texture(&tex, &frame.data, frame.width, frame.height);
215 }
216 self.video = Some(playback);
217 self.video_tex = Some(tex);
218 self.video_bind = Some(bind);
219 self.video_size = (w, h);
220 self.window = Some(window);
221 self.renderer = Some(renderer);
222 self.surface = Some(surface);
223 self.surface_format = Some(format);
224 self.start = Some(Instant::now());
225 return;
226 }
227 Err(e) => {
228 eprintln!("failed to start video preview: {e}");
229 event_loop.exit();
230 return;
231 }
232 }
233 }
234
235 let img = match image::ImageReader::open(&self.target_path) {
236 Ok(reader) => match reader.decode() {
237 Ok(img) => img,
238 Err(e) => {
239 eprintln!("failed to decode image: {e}");
240 event_loop.exit();
241 return;
242 }
243 },
244 Err(e) => {
245 eprintln!("failed to open image: {e}");
246 event_loop.exit();
247 return;
248 }
249 };
250
251 let (new_tex, new_bind) = match renderer.load_texture(&img) {
252 Ok(t) => t,
253 Err(e) => {
254 eprintln!("failed to upload image: {e}");
255 event_loop.exit();
256 return;
257 }
258 };
259
260 let (w, h) = img.dimensions();
261
262 let (bg_tex, bg_bind, old_size) =
268 match load_last_wallpaper(&self.target_path).and_then(|bg| {
269 renderer
270 .load_texture(&bg)
271 .ok()
272 .map(|(tex, bind)| (tex, bind, bg.dimensions()))
273 }) {
274 Some((tex, bind, size)) => (tex, bind, size),
275 None => {
276 let black = image::DynamicImage::ImageRgba8(image::ImageBuffer::from_pixel(
277 w.max(1),
278 h.max(1),
279 image::Rgba([0, 0, 0, 255]),
280 ));
281 match renderer.load_texture(&black) {
282 Ok((tex, bind)) => (tex, bind, (w, h)),
283 Err(e) => {
284 eprintln!("failed to upload background: {e}");
285 event_loop.exit();
286 return;
287 }
288 }
289 }
290 };
291
292 self.animated = crate::animated::AnimatedImage::decode(&self.target_path)
296 .ok()
297 .flatten();
298 if let Some(anim) = self.animated.as_mut() {
299 let (w, h) = (anim.width, anim.height);
300 let (tex, bind) = renderer.create_texture(w, h);
301 let first = anim.first_frame();
302 if !first.is_empty() {
303 renderer.update_texture(&tex, first, w, h);
304 }
305 self.play_tex = Some(tex);
306 self.play_bind = Some(bind);
307 }
308
309 self.window = Some(window);
310 self.renderer = Some(renderer);
311 self.surface = Some(surface);
312 self.surface_format = Some(format);
313 self.bg_bind = Some(bg_bind);
314 self.new_bind = Some(new_bind);
315 self._bg_tex = Some(bg_tex);
316 self._new_tex = Some(new_tex);
317 self.img_size = img.dimensions();
318 self.old_img_size = old_size;
319 self.start = Some(Instant::now());
320 }
321
322 fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
323 match event {
324 WindowEvent::CloseRequested => event_loop.exit(),
325 WindowEvent::Resized(size) => {
326 if let (Some(renderer), Some(surface), Some(format)) =
327 (&self.renderer, &self.surface, self.surface_format)
328 {
329 self.configure_surface(renderer, surface, format, size.width, size.height);
330 }
331 }
332 WindowEvent::RedrawRequested => self.render_frame(event_loop),
333 _ => {}
334 }
335 }
336
337 fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
338 if let Some(window) = &self.window {
339 window.request_redraw();
340 }
341 }
342}
343
344impl PreviewApp {
345 fn render_frame(&mut self, event_loop: &ActiveEventLoop) {
346 if self.video.is_some() {
348 self.render_video_frame(event_loop);
349 return;
350 }
351
352 let Some(start) = self.start else { return };
353
354 let elapsed = start.elapsed().as_millis() as f32;
355 let total = self.duration.as_millis() as f32;
356 let progress = if self.animated.is_some() {
357 (elapsed / total).min(1.0)
360 } else {
361 let hold = total * 0.25;
366 let cycle = elapsed % (total + hold);
367 (cycle / total).min(1.0)
368 };
369
370 let size = self
371 .window
372 .as_ref()
373 .map(|w| w.inner_size())
374 .unwrap_or(winit::dpi::PhysicalSize::new(1, 1));
375
376 if self.animated.is_some() && progress >= 1.0 {
377 self.render_live_frame(event_loop, size, elapsed, total);
378 return;
379 }
380
381 let Some(renderer) = &self.renderer else {
382 return;
383 };
384 let Some(surface) = &self.surface else { return };
385 let Some(format) = self.surface_format else {
386 return;
387 };
388 let Some(bg) = &self.bg_bind else { return };
389 let Some(new_bind) = &self.new_bind else {
390 return;
391 };
392
393 let (img_w, img_h) = self.img_size;
394 let (old_w, old_h) = self.old_img_size;
395
396 let uniforms = compute_effect_uniforms(&self.effect, progress);
397
398 match renderer.render_frame(crate::renderer::FrameRequest {
399 surface,
400 format,
401 bg_bind: bg,
402 new_bind,
403 effect: &uniforms,
404 width: size.width,
405 height: size.height,
406 img_width: img_w,
407 img_height: img_h,
408 old_img_width: old_w,
409 old_img_height: old_h,
410 scaling_mode: 0,
411 }) {
412 Ok(crate::renderer::FrameStatus::TimedOut) => {
413 }
417 Err(e) => {
418 eprintln!("render error: {e}");
419 event_loop.exit();
420 }
421 _ => {}
422 }
423 }
424
425 fn render_video_frame(&mut self, event_loop: &ActiveEventLoop) {
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(playback) = &self.video else { return };
436 let Some(tex) = &self.video_tex else { return };
437 let Some(bind) = &self.video_bind else { return };
438
439 let size = self
440 .window
441 .as_ref()
442 .map(|w| w.inner_size())
443 .unwrap_or(winit::dpi::PhysicalSize::new(1, 1));
444
445 let (w, h) = self.video_size;
446 if let Some(frame) = playback.next_frame()
447 && frame.width == w
448 && frame.height == h
449 {
450 renderer.update_texture(tex, &frame.data, frame.width, frame.height);
451 }
452
453 let uniforms = compute_effect_uniforms(&self.effect, 1.0);
454 match renderer.render_frame(crate::renderer::FrameRequest {
455 surface,
456 format,
457 bg_bind: bind,
458 new_bind: bind,
459 effect: &uniforms,
460 width: size.width.max(1),
461 height: size.height.max(1),
462 img_width: w,
463 img_height: h,
464 old_img_width: w,
465 old_img_height: h,
466 scaling_mode: 0,
467 }) {
468 Ok(crate::renderer::FrameStatus::TimedOut) => {}
469 Err(e) => {
470 eprintln!("render error: {e}");
471 event_loop.exit();
472 }
473 _ => {}
474 }
475 }
476
477 fn render_live_frame(
482 &mut self,
483 event_loop: &ActiveEventLoop,
484 size: winit::dpi::PhysicalSize<u32>,
485 elapsed: f32,
486 total: f32,
487 ) {
488 let Some(renderer) = &self.renderer else {
489 return;
490 };
491 let Some(surface) = &self.surface else { return };
492 let Some(format) = self.surface_format else {
493 return;
494 };
495 let Some(anim) = self.animated.as_mut() else {
496 return;
497 };
498 let Some(tex) = &self.play_tex else { return };
499 let Some(bind) = &self.play_bind else { return };
500
501 let (anim_w, anim_h) = (anim.width, anim.height);
502 let live_ms = (elapsed - total).max(0.0) as u64;
503 let index = anim.frame_index_at(std::time::Duration::from_millis(live_ms));
504 if index != self.shown_frame {
505 let frame = anim.frame_at(index);
506 if !frame.is_empty() {
507 renderer.update_texture(tex, frame, anim_w, anim_h);
508 self.shown_frame = index;
509 }
510 }
511
512 let uniforms = compute_effect_uniforms(&self.effect, 1.0);
513 match renderer.render_frame(crate::renderer::FrameRequest {
514 surface,
515 format,
516 bg_bind: bind,
517 new_bind: bind,
518 effect: &uniforms,
519 width: size.width,
520 height: size.height,
521 img_width: anim.width,
522 img_height: anim.height,
523 old_img_width: anim.width,
524 old_img_height: anim.height,
525 scaling_mode: 0,
526 }) {
527 Ok(crate::renderer::FrameStatus::TimedOut) => {}
528 Err(e) => {
529 eprintln!("render error: {e}");
530 event_loop.exit();
531 }
532 _ => {}
533 }
534 }
535}