Cache stable active-cell layout measurements (#38242)

## What changed

- Reuse desired and rendered heights for active transcript cells that report a stable height.
- Invalidate the cached layout when the cell identity, revision, width, render mode, or syntax theme changes, and when the active cell is removed.
- Continue rebuilding display lines on every frame so animated content stays live, while dynamic-height cells bypass the persistent cache.

## Testing

- Add rendering tests for height reuse, bottom-anchored overflow, cache invalidation, animated display lines, and dynamic-height cells.

GitOrigin-RevId: 414cc4269bfb6b68105837c82e31787272d335a7
This commit is contained in:
Charlie Marsh authored and copyberry committed 2026-08-12 19:43:32 +00:00
1 parent 74004b5397
commit 3d7bb2dd2e
5 files changed
+345 -7

No files matched your search

+80 -4
View File
@@ -1,6 +1,9 @@
//! Render composition for the main chat widget surface.
use super::transcript::ActiveCellLayoutCache;
use super::transcript::ActiveCellLayoutCacheKey;
use super::*;
use std::cell::Cell;
impl ChatWidget {
pub(crate) fn as_renderable(&self) -> RenderableItem<'_> {
@@ -10,6 +13,18 @@ impl ChatWidget {
child: cell.as_ref(),
top: 1,
right: active_cell_right_reserve,
// Externally backed transcript cells can also change viewport height without an
// active-cell revision. Spinner cells remain safe because their indicator width
// is stable and their display lines are still rebuilt on every frame.
persistent_layout: cell.has_stable_transcript_height().then_some(
PersistentActiveCellLayout {
cache: &self.transcript.active_cell_layout,
cell_identity: cell.as_ref() as *const dyn HistoryCell as *const ()
as usize,
revision: self.transcript.active_cell_revision,
render_mode: self.history_render_mode(),
},
),
})),
None => RenderableItem::Owned(Box::new(())),
};
@@ -19,6 +34,7 @@ impl ChatWidget {
child: cell,
top: 1,
right: active_cell_right_reserve,
persistent_layout: None,
}))
}
_ => RenderableItem::Owned(Box::new(())),
@@ -33,6 +49,7 @@ impl ChatWidget {
child: cell,
top: 1,
right: active_cell_right_reserve,
persistent_layout: None,
})),
);
}
@@ -43,6 +60,7 @@ impl ChatWidget {
child: cell,
top: 1,
right: active_cell_right_reserve,
persistent_layout: None,
})),
);
}
@@ -66,6 +84,14 @@ struct TranscriptAreaRenderable<'a> {
child: &'a dyn HistoryCell,
top: u16,
right: u16,
persistent_layout: Option<PersistentActiveCellLayout<'a>>,
}
struct PersistentActiveCellLayout<'a> {
cache: &'a Cell<Option<ActiveCellLayoutCache>>,
cell_identity: usize,
revision: u64,
render_mode: HistoryRenderMode,
}
impl Renderable for TranscriptAreaRenderable<'_> {
@@ -76,9 +102,19 @@ impl Renderable for TranscriptAreaRenderable<'_> {
let y = if area.height == 0 {
0
} else {
let overflow = paragraph
.line_count(area.width)
.saturating_sub(usize::from(area.height));
let rendered_height = if let Some((cache, mut layout)) = self.layout(area.width) {
if let Some(height) = layout.rendered_height {
height
} else {
let height = paragraph.line_count(area.width);
layout.rendered_height = Some(height);
cache.set(Some(layout));
height
}
} else {
paragraph.line_count(area.width)
};
let overflow = rendered_height.saturating_sub(usize::from(area.height));
u16::try_from(overflow).unwrap_or(u16::MAX)
};
Clear.render(area, buf);
@@ -87,11 +123,47 @@ impl Renderable for TranscriptAreaRenderable<'_> {
fn desired_height(&self, width: u16) -> u16 {
let child_width = width.saturating_sub(self.right).max(1);
HistoryCell::desired_height(self.child, child_width) + self.top
let desired_height = if let Some((cache, mut layout)) = self.layout(child_width) {
if let Some(height) = layout.desired_height {
height
} else {
let height = HistoryCell::desired_height(self.child, child_width);
layout.desired_height = Some(height);
cache.set(Some(layout));
height
}
} else {
HistoryCell::desired_height(self.child, child_width)
};
desired_height + self.top
}
}
impl TranscriptAreaRenderable<'_> {
fn layout(
&self,
width: u16,
) -> Option<(&Cell<Option<ActiveCellLayoutCache>>, ActiveCellLayoutCache)> {
let persistent = self.persistent_layout.as_ref()?;
let key = ActiveCellLayoutCacheKey {
cell_identity: persistent.cell_identity,
revision: persistent.revision,
width,
render_mode: persistent.render_mode,
syntax_theme_revision: crate::render::highlight::syntax_theme_revision(),
};
let layout = persistent
.cache
.get()
.filter(|layout| layout.key == key)
.unwrap_or(ActiveCellLayoutCache {
key,
desired_height: None,
rendered_height: None,
});
Some((persistent.cache, layout))
}
fn child_area(&self, area: Rect) -> Rect {
let y = area.y.saturating_add(self.top);
let height = area.height.saturating_sub(self.top);
@@ -104,6 +176,10 @@ impl TranscriptAreaRenderable<'_> {
}
}
#[cfg(test)]
#[path = "rendering_tests.rs"]
mod tests;
impl Renderable for ChatWidget {
fn render(&self, area: Rect, buf: &mut Buffer) {
self.as_renderable().render(area, buf);