// Copyright (c) 2023-2024 Sophie Herold // Copyright (c) 2024 Fina Wilke // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . // // SPDX-License-Identifier: GPL-3.0-or-later use decoder::DecoderError; use super::*; impl imp::LpImage { // Set filename when (re)loading image async fn set_file_loaded(&self, file: &gio::File) { // Do not check if same file since it might be set via initialized with // `Self::init` before self.file.replace(Some(file.clone())); self.reload_file_info().await; self.setup_file_monitor().await; } /// Set filename after filename changed async fn set_file_changed(&self, file: &gio::File) { let obj = self.obj(); let prev_mime_type = obj.metadata().mime_type(); self.file.replace(Some(file.clone())); self.reload_file_info().await; // Reload if mime type changed since other decoding path might be responsible // now if obj.metadata().mime_type() != prev_mime_type { obj.load(file).await; return; } self.setup_file_monitor().await; } async fn setup_file_monitor(&self) { let obj = self.obj(); if let Some(file) = obj.file() { let monitor = file.monitor_file(gio::FileMonitorFlags::WATCH_MOVES, gio::Cancellable::NONE); if let Ok(m) = &monitor { m.connect_changed(glib::clone!( #[weak] obj, move |_, file_a, file_b, event| { obj.imp().file_changed(event, file_a, file_b); } )); } self.file_monitor.replace(monitor.ok()); } } /// Called when decoder sends update pub fn update(&self, update: DecoderUpdate) { let obj = self.obj(); match update { DecoderUpdate::Metadata(metadata) => { log::debug!("Received metadata"); self.metadata.borrow_mut().merge(*metadata); self.emmit_metadata_changed(); obj.reset_rotation(); } DecoderUpdate::Dimensions => { log::debug!("Received dimensions: {:?}", self.original_dimensions()); obj.notify_image_size_available(); self.configure_best_fit(); self.request_tiles(); self.configure_adjustments(); } DecoderUpdate::Redraw => { self.set_loaded(true); self.previous_frame_buffer.reset(); obj.queue_draw(); self.frame_buffer.rcu(|tiles| { let mut new_tiles = (**tiles).clone(); new_tiles.cleanup(self.zoom_target.get(), self.preload_area()); new_tiles }); if self.background_color.borrow().is_none() { glib::spawn_future_local(glib::clone!( #[weak] obj, async move { let imp = obj.imp(); let color = imp.background_color_guess().await; imp.set_background_color(color); if obj.is_mapped() { obj.queue_draw(); } } )); } } DecoderUpdate::Animated => { if self.still.get() { // Just load the first frame self.frame_buffer.next_frame(); } else { let callback_id = obj.add_tick_callback(glib::clone!( #[weak(rename_to = obj)] self, #[upgrade_or] glib::ControlFlow::Break, move |_, clock| obj.tick_callback(clock) )); self.tick_callback.replace(Some(callback_id)); } } DecoderUpdate::SpecificError(err) => { if self.obj().is_loaded() { log::warn!("Error occured while loading additional data: {err:?}"); } else { self.set_specific_error(err); } } DecoderUpdate::GenericError(err) => { if self.obj().is_loaded() { log::warn!("Error occured while loading additional data: {err:?}"); } else { self.set_error(Some(err)); } } } } /// Roughly called for every frame if image is visible /// /// We handle advancing to the next frame for animated GIFs etc here. fn tick_callback(&self, clock: &gdk::FrameClock) -> glib::ControlFlow { let obj = self.obj(); // Do not animate if not visible if !obj.is_mapped() { return glib::ControlFlow::Continue; } let elapsed = clock.frame_time() - self.last_animated_frame.get(); let duration = std::time::Duration::from_micros(elapsed as u64); // Check if it's time to show the next frame if self.frame_buffer.frame_timeout(duration) { // Just draw since frame_timeout updated to new frame obj.queue_draw(); self.last_animated_frame.set(clock.frame_time()); if let Some(decoder) = self.decoder.borrow().as_ref() { // Decode new frame and load it into buffer decoder.fill_frame_buffer(); } } glib::ControlFlow::Continue } fn preload_area(&self) -> graphene::Rect { let viewport = self.viewport(); viewport.inset_r(-viewport.width() / 3., -viewport.height() / 3.) } pub(super) fn request_tiles(&self) { if let Some(decoder) = self.decoder.borrow().as_ref() { if self.zoom_animation().state() != adw::AnimationState::Playing { // Force minimum tile size of 1000x1000 since with smaller // tiles the tiled rendering advantage disappears let x_inset = f32::min(-3., (self.viewport().width() - 1000.) / 2.); let y_inset = f32::min(-3., (self.viewport().height() - 1000.) / 2.); decoder.request(crate::decoder::TileRequest { viewport: self.viewport().inset_r(x_inset, y_inset), area: self.preload_area(), zoom: self.zoom_target.get(), }); } } } /// File changed on drive fn file_changed( &self, event: gio::FileMonitorEvent, file_a: &gio::File, file_b: Option<&gio::File>, ) { match event { gio::FileMonitorEvent::Renamed => { if let Some(file) = file_b.cloned() { log::debug!("Moved to {}", file.uri()); let obj = self.obj().to_owned(); // current file got replaced with a new one let file_replace = obj.file().map_or(false, |x| x.equal(&file)); if file_replace { log::debug!("Image got replaced {}", file.uri()); // TODO: error handling is missing glib::spawn_future_local(async move { obj.load(&file).await; }); } else { glib::spawn_future_local(async move { obj.imp().set_file_changed(&file).await; }); } } } gio::FileMonitorEvent::ChangesDoneHint => { let obj = self.obj().to_owned(); let file = file_a.clone(); log::debug!("Image was edited {}", file.uri()); // TODO: error handling is missing glib::spawn_future_local(async move { obj.load(&file).await; }); } gio::FileMonitorEvent::Deleted | gio::FileMonitorEvent::MovedOut | gio::FileMonitorEvent::Unmounted => { log::debug!("File no longer available: {event:?} {}", file_a.uri()); self.is_deleted.set(true); self.obj().notify_is_deleted(); } _ => {} } } fn set_error(&self, err: Option) { log::debug!("Decoding error: {err:?}"); // Keeping first error instead of replacing with new error if err.is_some() && self.error.borrow().is_some() { return; } self.error.replace(err.as_ref().map(|x| x.to_string())); self.obj().notify_error(); if err.is_some() { self.set_loaded(false); } } fn set_specific_error(&self, error: DecoderError) { let obj = self.obj(); if obj.specific_error() != error { self.specific_error.set(error); obj.notify_specific_error(); if error.is_err() { self.set_loaded(false); } } } fn set_loaded(&self, is_loaded: bool) { let obj = self.obj(); if obj.is_loaded() != is_loaded { self.is_loaded.set(is_loaded); obj.notify_is_loaded(); if is_loaded { self.set_error(None); self.set_specific_error(DecoderError::None); } } } } impl LpImage { pub fn init(&self, file: &gio::File) { self.imp().file.replace(Some(file.clone())); } pub async fn reload(&self) { if let Some(file) = self.file() { self.load(&file).await; } else { log::error!("Trying to reload image without file"); } } pub async fn load(&self, file: &gio::File) { let imp = self.imp(); log::debug!("Loading file {}", file.uri()); if imp.rotation_animation().state() == adw::AnimationState::Playing { log::debug!("Queueing image reload due to playing rotate animation."); self.imp().queued_reload.replace(Some(file.clone())); return; } imp.metadata.replace(Metadata::default()); self.imp().emmit_metadata_changed(); self.imp().set_file_loaded(file).await; let tiles = &self.imp().frame_buffer; // Delete all stored textures for reloads let previous_frame_buffer = tiles.reset(); // Store previos frames to show until new texture is loaded self.imp() .previous_frame_buffer .swap(previous_frame_buffer.load_full()); // Reset background color for reloads imp.set_background_color(None); let (decoder, decoder_update) = Decoder::new(file.clone(), self.metadata().mime_type(), tiles.clone()).await; let weak_obj = self.downgrade(); glib::spawn_future_local(async move { while let Ok(update) = decoder_update.recv().await { if let Some(obj) = weak_obj.upgrade() { obj.imp().update(update); } } log::debug!("Stopped listening to decoder since sender is gone"); }); imp.decoder.replace(Some(Arc::new(decoder))); } }