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}
96
97impl PreviewApp {
98 fn new(target_path: PathBuf, duration: std::time::Duration, effect: Effect) -> Self {
99 Self {
100 target_path,
101 duration,
102 effect,
103 window: None,
104 renderer: None,
105 surface: None,
106 surface_format: None,
107 bg_bind: None,
108 new_bind: None,
109 _bg_tex: None,
110 _new_tex: None,
111 start: None,
112 img_size: (1, 1),
113 old_img_size: (1, 1),
114 animated: None,
115 play_tex: None,
116 play_bind: None,
117 shown_frame: usize::MAX,
118 }
119 }
120
121 fn configure_surface(
122 &self,
123 renderer: &Renderer,
124 surface: &wgpu::Surface<'static>,
125 format: wgpu::TextureFormat,
126 width: u32,
127 height: u32,
128 ) {
129 surface.configure(
130 &renderer.device,
131 &wgpu::SurfaceConfiguration {
132 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
133 format,
134 width: width.max(1),
135 height: height.max(1),
136 present_mode: wgpu::PresentMode::AutoVsync,
137 alpha_mode: wgpu::CompositeAlphaMode::Auto,
138 view_formats: vec![],
139 desired_maximum_frame_latency: 2,
140 },
141 );
142 }
143}
144
145impl ApplicationHandler for PreviewApp {
146 fn resumed(&mut self, event_loop: &ActiveEventLoop) {
147 if self.window.is_some() {
148 return;
149 }
150
151 let window = match event_loop.create_window(
152 Window::default_attributes()
153 .with_title("wallr preview")
154 .with_inner_size(LogicalSize::new(1280.0, 720.0)),
155 ) {
156 Ok(w) => Arc::new(w),
157 Err(e) => {
158 eprintln!("failed to create preview window: {e}");
159 event_loop.exit();
160 return;
161 }
162 };
163
164 let renderer = match pollster::block_on(Renderer::new()) {
165 Ok(r) => r,
166 Err(e) => {
167 eprintln!("renderer init failed: {e}");
168 event_loop.exit();
169 return;
170 }
171 };
172
173 let surface = match renderer.instance.create_surface(window.clone()) {
174 Ok(s) => s,
175 Err(e) => {
176 eprintln!("surface creation failed: {e}");
177 event_loop.exit();
178 return;
179 }
180 };
181
182 let caps = surface.get_capabilities(&renderer.adapter);
183 let format = caps
184 .formats
185 .iter()
186 .find(|f| f.is_srgb())
187 .copied()
188 .unwrap_or(caps.formats[0]);
189
190 let size = window.inner_size();
191 self.configure_surface(&renderer, &surface, format, size.width, size.height);
192
193 let img = match image::ImageReader::open(&self.target_path) {
194 Ok(reader) => match reader.decode() {
195 Ok(img) => img,
196 Err(e) => {
197 eprintln!("failed to decode image: {e}");
198 event_loop.exit();
199 return;
200 }
201 },
202 Err(e) => {
203 eprintln!("failed to open image: {e}");
204 event_loop.exit();
205 return;
206 }
207 };
208
209 let (new_tex, new_bind) = match renderer.load_texture(&img) {
210 Ok(t) => t,
211 Err(e) => {
212 eprintln!("failed to upload image: {e}");
213 event_loop.exit();
214 return;
215 }
216 };
217
218 let (w, h) = img.dimensions();
219
220 let (bg_tex, bg_bind, old_size) =
226 match load_last_wallpaper(&self.target_path).and_then(|bg| {
227 renderer
228 .load_texture(&bg)
229 .ok()
230 .map(|(tex, bind)| (tex, bind, bg.dimensions()))
231 }) {
232 Some((tex, bind, size)) => (tex, bind, size),
233 None => {
234 let black = image::DynamicImage::ImageRgba8(image::ImageBuffer::from_pixel(
235 w.max(1),
236 h.max(1),
237 image::Rgba([0, 0, 0, 255]),
238 ));
239 match renderer.load_texture(&black) {
240 Ok((tex, bind)) => (tex, bind, (w, h)),
241 Err(e) => {
242 eprintln!("failed to upload background: {e}");
243 event_loop.exit();
244 return;
245 }
246 }
247 }
248 };
249
250 self.animated = crate::animated::AnimatedImage::decode(&self.target_path)
254 .ok()
255 .flatten();
256 if let Some(anim) = &self.animated {
257 let (tex, bind) = renderer.create_texture(anim.width, anim.height);
258 renderer.update_texture(&tex, anim.first_frame(), anim.width, anim.height);
259 self.play_tex = Some(tex);
260 self.play_bind = Some(bind);
261 }
262
263 self.window = Some(window);
264 self.renderer = Some(renderer);
265 self.surface = Some(surface);
266 self.surface_format = Some(format);
267 self.bg_bind = Some(bg_bind);
268 self.new_bind = Some(new_bind);
269 self._bg_tex = Some(bg_tex);
270 self._new_tex = Some(new_tex);
271 self.img_size = img.dimensions();
272 self.old_img_size = old_size;
273 self.start = Some(Instant::now());
274 }
275
276 fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
277 match event {
278 WindowEvent::CloseRequested => event_loop.exit(),
279 WindowEvent::Resized(size) => {
280 if let (Some(renderer), Some(surface), Some(format)) =
281 (&self.renderer, &self.surface, self.surface_format)
282 {
283 self.configure_surface(renderer, surface, format, size.width, size.height);
284 }
285 }
286 WindowEvent::RedrawRequested => self.render_frame(event_loop),
287 _ => {}
288 }
289 }
290
291 fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
292 if let Some(window) = &self.window {
293 window.request_redraw();
294 }
295 }
296}
297
298impl PreviewApp {
299 fn render_frame(&mut self, event_loop: &ActiveEventLoop) {
300 let Some(start) = self.start else { return };
301
302 let elapsed = start.elapsed().as_millis() as f32;
303 let total = self.duration.as_millis() as f32;
304 let progress = if self.animated.is_some() {
305 (elapsed / total).min(1.0)
308 } else {
309 let hold = total * 0.25;
314 let cycle = elapsed % (total + hold);
315 (cycle / total).min(1.0)
316 };
317
318 let size = self
319 .window
320 .as_ref()
321 .map(|w| w.inner_size())
322 .unwrap_or(winit::dpi::PhysicalSize::new(1, 1));
323
324 if self.animated.is_some() && progress >= 1.0 {
325 self.render_live_frame(event_loop, size, elapsed, total);
326 return;
327 }
328
329 let Some(renderer) = &self.renderer else {
330 return;
331 };
332 let Some(surface) = &self.surface else { return };
333 let Some(format) = self.surface_format else {
334 return;
335 };
336 let Some(bg) = &self.bg_bind else { return };
337 let Some(new_bind) = &self.new_bind else {
338 return;
339 };
340
341 let (img_w, img_h) = self.img_size;
342 let (old_w, old_h) = self.old_img_size;
343
344 let uniforms = compute_effect_uniforms(&self.effect, progress);
345
346 match renderer.render_frame(crate::renderer::FrameRequest {
347 surface,
348 format,
349 bg_bind: bg,
350 new_bind,
351 effect: &uniforms,
352 width: size.width,
353 height: size.height,
354 img_width: img_w,
355 img_height: img_h,
356 old_img_width: old_w,
357 old_img_height: old_h,
358 }) {
359 Ok(crate::renderer::FrameStatus::TimedOut) => {
360 }
364 Err(e) => {
365 eprintln!("render error: {e}");
366 event_loop.exit();
367 }
368 _ => {}
369 }
370 }
371
372 fn render_live_frame(
377 &mut self,
378 event_loop: &ActiveEventLoop,
379 size: winit::dpi::PhysicalSize<u32>,
380 elapsed: f32,
381 total: f32,
382 ) {
383 let Some(renderer) = &self.renderer else {
384 return;
385 };
386 let Some(surface) = &self.surface else { return };
387 let Some(format) = self.surface_format else {
388 return;
389 };
390 let Some(anim) = &self.animated else { return };
391 let Some(tex) = &self.play_tex else { return };
392 let Some(bind) = &self.play_bind else { return };
393
394 let live_ms = (elapsed - total).max(0.0) as u64;
395 let index = anim.frame_index_at(std::time::Duration::from_millis(live_ms));
396 if index != self.shown_frame {
397 renderer.update_texture(tex, anim.frame_at(index), anim.width, anim.height);
398 self.shown_frame = index;
399 }
400
401 let uniforms = compute_effect_uniforms(&self.effect, 1.0);
402 match renderer.render_frame(crate::renderer::FrameRequest {
403 surface,
404 format,
405 bg_bind: bind,
406 new_bind: bind,
407 effect: &uniforms,
408 width: size.width,
409 height: size.height,
410 img_width: anim.width,
411 img_height: anim.height,
412 old_img_width: anim.width,
413 old_img_height: anim.height,
414 }) {
415 Ok(crate::renderer::FrameStatus::TimedOut) => {}
416 Err(e) => {
417 eprintln!("render error: {e}");
418 event_loop.exit();
419 }
420 _ => {}
421 }
422 }
423}