fix: image loading
This commit is contained in:
+37
-19
@@ -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<u32> = 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<Vec<u8>, 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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
+9
-6
@@ -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 {
|
||||
|
||||
+7
-8
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user