From 96b85f015937fd12babc006349920e08dc317194 Mon Sep 17 00:00:00 2001 From: zach Date: Sat, 4 Jul 2026 18:24:49 +0200 Subject: [PATCH] fix: image loading --- src/graphics.rs | 56 ++++++++++++++++++++++++++++--------------- src/image_renderer.rs | 3 +++ src/main.rs | 15 +++++++----- src/terminal.rs | 15 ++++++------ 4 files changed, 56 insertions(+), 33 deletions(-) diff --git a/src/graphics.rs b/src/graphics.rs index ae5578d..4933109 100644 --- a/src/graphics.rs +++ b/src/graphics.rs @@ -336,6 +336,7 @@ impl GraphicsCommand { } // Decode base64 payload + log::debug!("Parsing payload: len={}, content={:?}", payload_part.len(), std::str::from_utf8(payload_part).ok()); if !payload_part.is_empty() { if let Ok(payload_str) = std::str::from_utf8(payload_part) { if let Ok(decoded) = base64_decode(payload_str) { @@ -449,7 +450,7 @@ pub fn decode_gif( looping: true, total_duration_ms, state: AnimationState::Running, - loops_remaining: None, + loops_remaining: DEFAULT_ANIMATION_LOOPS, }) } else { None @@ -628,7 +629,7 @@ pub fn decode_webm( looping: true, total_duration_ms, state: AnimationState::Running, - loops_remaining: None, + loops_remaining: DEFAULT_ANIMATION_LOOPS, }) } else { None @@ -704,6 +705,8 @@ impl ImageData { } } +pub const DEFAULT_ANIMATION_LOOPS: Option = None; // None = infinite + /// Animation state for playback control. #[derive(Clone, Debug, PartialEq, Eq, Default)] pub enum AnimationState { @@ -1162,7 +1165,7 @@ impl ImageStorage { looping: true, total_duration_ms: 100, state: AnimationState::Loading, - loops_remaining: None, + loops_remaining: DEFAULT_ANIMATION_LOOPS, }); } @@ -1359,6 +1362,7 @@ impl ImageStorage { log::debug!("Animation {} running ({} frames)", id, anim.frames.len()); // Reset frame start when starting animation anim.frame_start = None; + anim.looping = true; AnimationState::Running } _ => anim.state.clone(), @@ -1754,21 +1758,31 @@ impl ImageStorage { if anim.looping { // Check loop count if let Some(ref mut loops) = anim.loops_remaining { - if *loops > 0 { - *loops -= 1; - anim.current_frame = 0; - } else { - // No more loops, stop - anim.state = AnimationState::Stopped; - continue; - } - } else { - // Infinite looping - anim.current_frame = 0; - } - } - // else: stay on last frame - } else { + if *loops > 0 { + log::debug!("Animation {} looping, {} loops remaining", id, *loops - 1); + *loops -= 1; + anim.current_frame = 0; + } else { + log::debug!("Animation {} stopped: no more loops", id); + // No more loops, stop + anim.state = AnimationState::Stopped; + continue; + } + + } else { + log::debug!("Animation {} looping indefinitely", id); + // Infinite looping + anim.current_frame = 0; + } + + } + log::debug!("Animation {} reached end, looping={}", id, anim.looping); + if !anim.looping { + log::debug!("Animation {} stopping (looping=false)", id); + } + // else: stay on last frame + } else { + anim.current_frame = next_frame; } @@ -1811,8 +1825,12 @@ impl ImageStorage { /// when using the STANDARD_NO_PAD engine with lenient decoding. fn base64_decode(input: &str) -> Result, GraphicsError> { // Use standard base64 with lenient decoding (ignores whitespace, handles missing padding) + let mut input = input.to_string(); + while input.len() % 4 != 0 { + input.push('='); + } base64::engine::general_purpose::STANDARD - .decode(input.as_bytes()) + .decode(&input) .map_err(|_| GraphicsError::Base64DecodeFailed) } diff --git a/src/image_renderer.rs b/src/image_renderer.rs index af38a81..df75ebe 100644 --- a/src/image_renderer.rs +++ b/src/image_renderer.rs @@ -103,6 +103,7 @@ impl ImageRenderer { /// Upload an image to the GPU, creating or updating its texture. pub fn upload_image(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, image: &ImageData) { + log::debug!("upload_image: id={}, width={}, height={}, data_len={}", image.id, image.width, image.height, image.data.len()); // Get current frame data (handles animation frames automatically) let data = image.current_frame_data(); @@ -231,6 +232,7 @@ impl ImageRenderer { pub fn sync_images(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, storage: &mut ImageStorage) { // Update animations and get list of changed image IDs let changed_ids = storage.update_animations(); + log::debug!("Sync images: changed_ids={:?}, dirty={}", changed_ids, storage.dirty); // Re-upload frames that changed due to animation for id in &changed_ids { @@ -294,6 +296,7 @@ impl ImageRenderer { // Image spans from visible_row to visible_row + placement.rows let image_bottom = visible_row + placement.rows as isize; if image_bottom < 0 || visible_row >= visible_rows as isize { + log::debug!("Image {} culled: visible_row={}, image_bottom={}, visible_rows={}", placement.image_id, visible_row, image_bottom, visible_rows); continue; // Image is completely off-screen } diff --git a/src/main.rs b/src/main.rs index 17d3294..9514d21 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2023,14 +2023,17 @@ impl App { }); let has_selection = tab.panes.values().any(|p| p.selection.is_some()); - if !has_dirty_content && !self.needs_redraw && self.edge_glows.is_empty() && !fade_in_progress && !has_selection && !has_pending_redraw { - let image_animation_in_progress = tab.panes.values().any(|p| { - p.terminal.image_storage.has_animations() - }); - needs_another_frame = image_animation_in_progress; - return needs_another_frame; + // Check if any images have running animations + let image_animation_in_progress = tab.panes.values().any(|p| { + p.terminal.image_storage.has_animations() + }); + + if !has_dirty_content && !self.needs_redraw && self.edge_glows.is_empty() && !fade_in_progress && !has_selection && !has_pending_redraw && !image_animation_in_progress { + return false; } + needs_another_frame = image_animation_in_progress; + // First pass: sync images and calculate dim factors (needs mutable access) let mut dim_factors: Vec<(PaneId, f32)> = Vec::new(); for (pane_id, _) in &geometries { diff --git a/src/terminal.rs b/src/terminal.rs index 3220c4c..88503d3 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -2747,10 +2747,9 @@ impl Terminal { if !placement.suppress_cursor_move && !placement.virtual_placement { - // Move cursor down by (rows - 1) since we're already on the first row - // Then set cursor to the column after the image - let new_row = - self.cursor_row + placement.rows.saturating_sub(1); + // Move cursor to the right and down by the image dimensions + self.cursor_col += placement.cols; + let new_row = self.cursor_row + placement.rows; if new_row >= self.rows { // Need to scroll let scroll_amount = new_row - self.rows + 1; @@ -2759,11 +2758,11 @@ impl Terminal { } else { self.cursor_row = new_row; } - // Move cursor to after the image (or stay at column 0 of next line) - // Per protocol, cursor ends at the last row of the image + // If cursor is now beyond the right edge, it will be handled by the normal + // cursor movement logic (wrapping/scrolling) if applicable. log::debug!( - "Cursor moved after image placement: row={} (moved {} rows)", - self.cursor_row, placement.rows.saturating_sub(1) + "Cursor moved after image placement: col={}, row={} (moved {}x{} cells)", + self.cursor_col, self.cursor_row, placement.cols, placement.rows ); } }