fix: image loading

This commit is contained in:
2026-07-04 18:24:49 +02:00
parent c31166c087
commit 96b85f0159
4 changed files with 56 additions and 33 deletions
+22 -4
View File
@@ -336,6 +336,7 @@ impl GraphicsCommand {
} }
// Decode base64 payload // 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 !payload_part.is_empty() {
if let Ok(payload_str) = std::str::from_utf8(payload_part) { if let Ok(payload_str) = std::str::from_utf8(payload_part) {
if let Ok(decoded) = base64_decode(payload_str) { if let Ok(decoded) = base64_decode(payload_str) {
@@ -449,7 +450,7 @@ pub fn decode_gif(
looping: true, looping: true,
total_duration_ms, total_duration_ms,
state: AnimationState::Running, state: AnimationState::Running,
loops_remaining: None, loops_remaining: DEFAULT_ANIMATION_LOOPS,
}) })
} else { } else {
None None
@@ -628,7 +629,7 @@ pub fn decode_webm(
looping: true, looping: true,
total_duration_ms, total_duration_ms,
state: AnimationState::Running, state: AnimationState::Running,
loops_remaining: None, loops_remaining: DEFAULT_ANIMATION_LOOPS,
}) })
} else { } else {
None None
@@ -704,6 +705,8 @@ impl ImageData {
} }
} }
pub const DEFAULT_ANIMATION_LOOPS: Option<u32> = None; // None = infinite
/// Animation state for playback control. /// Animation state for playback control.
#[derive(Clone, Debug, PartialEq, Eq, Default)] #[derive(Clone, Debug, PartialEq, Eq, Default)]
pub enum AnimationState { pub enum AnimationState {
@@ -1162,7 +1165,7 @@ impl ImageStorage {
looping: true, looping: true,
total_duration_ms: 100, total_duration_ms: 100,
state: AnimationState::Loading, 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()); log::debug!("Animation {} running ({} frames)", id, anim.frames.len());
// Reset frame start when starting animation // Reset frame start when starting animation
anim.frame_start = None; anim.frame_start = None;
anim.looping = true;
AnimationState::Running AnimationState::Running
} }
_ => anim.state.clone(), _ => anim.state.clone(),
@@ -1755,20 +1759,30 @@ impl ImageStorage {
// Check loop count // Check loop count
if let Some(ref mut loops) = anim.loops_remaining { if let Some(ref mut loops) = anim.loops_remaining {
if *loops > 0 { if *loops > 0 {
log::debug!("Animation {} looping, {} loops remaining", id, *loops - 1);
*loops -= 1; *loops -= 1;
anim.current_frame = 0; anim.current_frame = 0;
} else { } else {
log::debug!("Animation {} stopped: no more loops", id);
// No more loops, stop // No more loops, stop
anim.state = AnimationState::Stopped; anim.state = AnimationState::Stopped;
continue; continue;
} }
} else { } else {
log::debug!("Animation {} looping indefinitely", id);
// Infinite looping // Infinite looping
anim.current_frame = 0; 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: stay on last frame
} else { } else {
anim.current_frame = next_frame; anim.current_frame = next_frame;
} }
@@ -1811,8 +1825,12 @@ impl ImageStorage {
/// when using the STANDARD_NO_PAD engine with lenient decoding. /// when using the STANDARD_NO_PAD engine with lenient decoding.
fn base64_decode(input: &str) -> Result<Vec<u8>, GraphicsError> { fn base64_decode(input: &str) -> Result<Vec<u8>, GraphicsError> {
// Use standard base64 with lenient decoding (ignores whitespace, handles missing padding) // 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 base64::engine::general_purpose::STANDARD
.decode(input.as_bytes()) .decode(&input)
.map_err(|_| GraphicsError::Base64DecodeFailed) .map_err(|_| GraphicsError::Base64DecodeFailed)
} }
+3
View File
@@ -103,6 +103,7 @@ impl ImageRenderer {
/// Upload an image to the GPU, creating or updating its texture. /// 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) { 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) // Get current frame data (handles animation frames automatically)
let data = image.current_frame_data(); 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) { pub fn sync_images(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, storage: &mut ImageStorage) {
// Update animations and get list of changed image IDs // Update animations and get list of changed image IDs
let changed_ids = storage.update_animations(); 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 // Re-upload frames that changed due to animation
for id in &changed_ids { for id in &changed_ids {
@@ -294,6 +296,7 @@ impl ImageRenderer {
// Image spans from visible_row to visible_row + placement.rows // Image spans from visible_row to visible_row + placement.rows
let image_bottom = visible_row + placement.rows as isize; let image_bottom = visible_row + placement.rows as isize;
if image_bottom < 0 || visible_row >= visible_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 continue; // Image is completely off-screen
} }
+6 -3
View File
@@ -2023,14 +2023,17 @@ impl App {
}); });
let has_selection = tab.panes.values().any(|p| p.selection.is_some()); 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 { // Check if any images have running animations
let image_animation_in_progress = tab.panes.values().any(|p| { let image_animation_in_progress = tab.panes.values().any(|p| {
p.terminal.image_storage.has_animations() p.terminal.image_storage.has_animations()
}); });
needs_another_frame = image_animation_in_progress;
return needs_another_frame; 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) // First pass: sync images and calculate dim factors (needs mutable access)
let mut dim_factors: Vec<(PaneId, f32)> = Vec::new(); let mut dim_factors: Vec<(PaneId, f32)> = Vec::new();
for (pane_id, _) in &geometries { for (pane_id, _) in &geometries {
+7 -8
View File
@@ -2747,10 +2747,9 @@ impl Terminal {
if !placement.suppress_cursor_move if !placement.suppress_cursor_move
&& !placement.virtual_placement && !placement.virtual_placement
{ {
// Move cursor down by (rows - 1) since we're already on the first row // Move cursor to the right and down by the image dimensions
// Then set cursor to the column after the image self.cursor_col += placement.cols;
let new_row = let new_row = self.cursor_row + placement.rows;
self.cursor_row + placement.rows.saturating_sub(1);
if new_row >= self.rows { if new_row >= self.rows {
// Need to scroll // Need to scroll
let scroll_amount = new_row - self.rows + 1; let scroll_amount = new_row - self.rows + 1;
@@ -2759,11 +2758,11 @@ impl Terminal {
} else { } else {
self.cursor_row = new_row; self.cursor_row = new_row;
} }
// Move cursor to after the image (or stay at column 0 of next line) // If cursor is now beyond the right edge, it will be handled by the normal
// Per protocol, cursor ends at the last row of the image // cursor movement logic (wrapping/scrolling) if applicable.
log::debug!( log::debug!(
"Cursor moved after image placement: row={} (moved {} rows)", "Cursor moved after image placement: col={}, row={} (moved {}x{} cells)",
self.cursor_row, placement.rows.saturating_sub(1) self.cursor_col, self.cursor_row, placement.cols, placement.rows
); );
} }
} }