diff --git a/docs/changelog.md b/docs/changelog.md index 2b1aabb4a..2de50fc34 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -32,6 +32,11 @@ As features stabilize some brief notes about them will accumulate here. * Don't hide mouse cursor when pressing only modifier keys. #3570 * [PaneSelect](config/lua/keyassignment/PaneSelect.md) will now un-zoom to show all panes, then re-zoom after performing its action. #3573 +* Images, especially animated images, are now decoded in the background. When + used as a background layer for the terminal, we now use the normal + terminal background color as a placeholder until the first frame has been + decoded. In other circumstances, you may observe a brief black frame while + waiting for the image to decode. #### New diff --git a/wezterm-gui/src/glyphcache.rs b/wezterm-gui/src/glyphcache.rs index 8650ca65d..d7cbfab10 100644 --- a/wezterm-gui/src/glyphcache.rs +++ b/wezterm-gui/src/glyphcache.rs @@ -31,6 +31,12 @@ use wezterm_font::units::*; use wezterm_font::{FontConfiguration, GlyphInfo, LoadedFont, LoadedFontId}; use wezterm_term::Underline; +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum LoadState { + Loading, + Loaded, +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct CellMetricKey { pub pixel_width: u16, @@ -383,6 +389,7 @@ struct FrameState { source: FrameSource, current_frame: DecodedFrame, frames: Vec, + load_state: LoadState, } impl FrameState { @@ -405,6 +412,7 @@ impl FrameState { height: BLACK_SIZE, duration: Duration::from_millis(0), }, + load_state: LoadState::Loading, } } @@ -414,6 +422,7 @@ impl FrameState { Ok(frame) => { self.frames.push(frame.clone()); self.current_frame = frame; + self.load_state = LoadState::Loaded; true } Err(TryRecvError::Empty) => false, @@ -480,6 +489,14 @@ impl DecodedImage { } } + fn load_state(&self) -> LoadState { + let frames = self.frames.borrow(); + match frames.as_ref() { + Some(state) => state.load_state, + None => LoadState::Loading, + } + } + fn start_frame_decoder(lease: BlobLease, image_data: &Arc) -> Self { match FrameDecoder::start(lease.clone()) { Ok(rx) => Self { @@ -854,7 +871,7 @@ impl GlyphCache { decoded: &DecodedImage, padding: Option, min_frame_duration: Duration, - ) -> anyhow::Result<(Sprite, Option)> { + ) -> anyhow::Result<(Sprite, Option, LoadState)> { let mut handle = DecodedImageHandle { h: decoded.image.data(), current_frame: *decoded.current_frame.borrow(), @@ -862,12 +879,12 @@ impl GlyphCache { match &*handle.h { ImageDataType::Rgba8 { hash, .. } => { if let Some(sprite) = frame_cache.get(hash) { - return Ok((sprite.clone(), None)); + return Ok((sprite.clone(), None, decoded.load_state())); } let sprite = atlas.allocate_with_padding(&handle, padding)?; frame_cache.insert(*hash, sprite.clone()); - return Ok((sprite, None)); + return Ok((sprite, None, decoded.load_state())); } ImageDataType::AnimRgba8 { hashes, @@ -915,7 +932,7 @@ impl GlyphCache { let hash = hashes[*decoded_current_frame]; if let Some(sprite) = frame_cache.get(&hash) { - return Ok((sprite.clone(), next)); + return Ok((sprite.clone(), next, decoded.load_state())); } let sprite = atlas.allocate_with_padding(&handle, padding)?; @@ -928,6 +945,7 @@ impl GlyphCache { *decoded_frame_start + durations[*decoded_current_frame].max(min_frame_duration), ), + decoded.load_state(), )); } ImageDataType::EncodedLease(_) | ImageDataType::EncodedFile(_) => { @@ -967,7 +985,7 @@ impl GlyphCache { let hash = frames.frame_hash(); if let Some(sprite) = frame_cache.get(&hash) { - return Ok((sprite.clone(), next)); + return Ok((sprite.clone(), next, frames.load_state)); } let frame = Image::from_raw( @@ -982,6 +1000,7 @@ impl GlyphCache { Ok(( sprite, Some(*decoded_frame_start + frames.frame_duration().max(min_frame_duration)), + frames.load_state, )) } } @@ -991,7 +1010,7 @@ impl GlyphCache { &mut self, image_data: &Arc, padding: Option, - ) -> anyhow::Result<(Sprite, Option)> { + ) -> anyhow::Result<(Sprite, Option, LoadState)> { let hash = image_data.hash(); if let Some(decoded) = self.image_cache.get(&hash) { diff --git a/wezterm-gui/src/termwindow/background.rs b/wezterm-gui/src/termwindow/background.rs index 21d35cd1b..d3ca334a6 100644 --- a/wezterm-gui/src/termwindow/background.rs +++ b/wezterm-gui/src/termwindow/background.rs @@ -1,4 +1,5 @@ use crate::color::LinearRgba; +use crate::glyphcache::LoadState; use crate::quad::{QuadAllocator, QuadTrait}; use crate::termwindow::RenderState; use crate::utilsprites::RenderMetrics; @@ -397,15 +398,17 @@ impl crate::TermWindow { &self, bg_color: LinearRgba, top: StableRowIndex, - ) -> anyhow::Result<()> { + ) -> anyhow::Result { let gl_state = self.render_state.as_ref().unwrap(); let mut layer_idx = -127; + let mut loaded_any = false; for layer in self.window_background.iter() { if self.render_background(gl_state, bg_color, layer, layer_idx, top)? { + loaded_any = true; layer_idx = layer_idx.saturating_add(1); } } - Ok(()) + Ok(loaded_any) } fn render_background( @@ -422,12 +425,16 @@ impl crate::TermWindow { let color = bg_color.mul_alpha(layer.def.opacity); - let (sprite, next_due) = gl_state + let (sprite, next_due, load_state) = gl_state .glyph_cache .borrow_mut() .cached_image(&layer.source, None)?; self.update_next_frame_time(next_due); + if load_state == LoadState::Loading { + return Ok(false); + } + let pixel_width = self.dimensions.pixel_width as f32; let pixel_height = self.dimensions.pixel_height as f32; let pixel_aspect = pixel_width / pixel_height; diff --git a/wezterm-gui/src/termwindow/render/mod.rs b/wezterm-gui/src/termwindow/render/mod.rs index 6f0ab9e4e..8cf551b66 100644 --- a/wezterm-gui/src/termwindow/render/mod.rs +++ b/wezterm-gui/src/termwindow/render/mod.rs @@ -432,7 +432,7 @@ impl crate::TermWindow { padding.next_power_of_two() }; - let (sprite, next_due) = gl_state + let (sprite, next_due, _load_state) = gl_state .glyph_cache .borrow_mut() .cached_image(image.image_data(), Some(padding))?; diff --git a/wezterm-gui/src/termwindow/render/paint.rs b/wezterm-gui/src/termwindow/render/paint.rs index 55b292aee..01d79a97c 100644 --- a/wezterm-gui/src/termwindow/render/paint.rs +++ b/wezterm-gui/src/termwindow/render/paint.rs @@ -171,6 +171,8 @@ impl crate::TermWindow { log::trace!("quad map elapsed {:?}", start.elapsed()); metrics::histogram!("quad.map", start.elapsed()); + let mut paint_terminal_background = false; + // Render the full window background match (self.window_background.is_empty(), self.allow_images) { (false, true) => { @@ -185,8 +187,16 @@ impl crate::TermWindow { }) .unwrap_or(0); - self.render_backgrounds(bg_color, top) + let loaded_any = self + .render_backgrounds(bg_color, top) .context("render_backgrounds")?; + + if !loaded_any { + // Either there was a problem loading the background(s) + // or they haven't finished loading yet. + // Use the regular terminal background until that changes. + paint_terminal_background = true; + } } _ if window_is_transparent => { // Avoid doubling up the background color: the panes @@ -194,32 +204,36 @@ impl crate::TermWindow { // should be no gaps that need filling in } _ => { - // Regular window background color - let background = if panes.len() == 1 { - // If we're the only pane, use the pane's palette - // to draw the padding background - panes[0].pane.palette().background - } else { - self.palette().background - } - .to_linear() - .mul_alpha(self.config.window_background_opacity); - - self.filled_rectangle( - &mut layers, - 0, - euclid::rect( - 0., - 0., - self.dimensions.pixel_width as f32, - self.dimensions.pixel_height as f32, - ), - background, - ) - .context("filled_rectangle for window background")?; + paint_terminal_background = true; } } + if paint_terminal_background { + // Regular window background color + let background = if panes.len() == 1 { + // If we're the only pane, use the pane's palette + // to draw the padding background + panes[0].pane.palette().background + } else { + self.palette().background + } + .to_linear() + .mul_alpha(self.config.window_background_opacity); + + self.filled_rectangle( + &mut layers, + 0, + euclid::rect( + 0., + 0., + self.dimensions.pixel_width as f32, + self.dimensions.pixel_height as f32, + ), + background, + ) + .context("filled_rectangle for window background")?; + } + for pos in panes { if pos.is_active { self.update_text_cursor(&pos);