fix(tui/welcome): stabilize intro logo sizing

This commit is contained in:
James Peter
2025-12-31 18:21:02 +10:00
parent 1ff5fe179d
commit 117863e28a
3 changed files with 108 additions and 32 deletions
+13
View File
@@ -4796,6 +4796,17 @@ impl ChatWidget<'_> {
self.render_request_cache_dirty.set(true);
}
fn update_welcome_height_hint(&self, height: u16) {
for cell in self.history_cells.iter() {
if let Some(welcome) = cell
.as_any()
.downcast_ref::<crate::history_cell::AnimatedWelcomeCell>()
{
welcome.set_available_height(height);
}
}
}
fn is_frozen_cell(cell: &dyn HistoryCell) -> bool {
cell.as_any().downcast_ref::<FrozenHistoryCell>().is_some()
}
@@ -37334,6 +37345,8 @@ impl WidgetRef for &ChatWidget<'_> {
height: history_area.height,
};
self.update_welcome_height_hint(content_area.height);
// Reset the full history region to the baseline theme background once per frame.
// Individual cells only repaint when their visuals differ (e.g., assistant tint),
// which keeps overdraw minimal while ensuring stale characters disappear.
+48 -16
View File
@@ -32,7 +32,7 @@ pub fn intro_art_size_for_width(width: u16) -> IntroArtSize {
}
}
pub fn intro_art_size_for_area(width: u16, height: u16) -> IntroArtSize {
pub(crate) fn intro_art_size_for_area(width: u16, height: u16) -> IntroArtSize {
if width >= LARGE_MIN_WIDTH && height >= LARGE_MIN_HEIGHT {
IntroArtSize::Large
} else if width >= MEDIUM_MIN_WIDTH && height >= MEDIUM_MIN_HEIGHT {
@@ -53,23 +53,14 @@ pub fn intro_art_height(size: IntroArtSize) -> u16 {
}
}
pub fn render_intro_animation_with_size(
area: Rect,
buf: &mut Buffer,
t: f32,
size: IntroArtSize,
version: &str,
) {
render_intro_animation_with_size_and_alpha(area, buf, t, 1.0, size, version);
}
pub fn render_intro_animation_with_size_and_alpha(
pub(crate) fn render_intro_animation_with_size_and_alpha_offset(
area: Rect,
buf: &mut Buffer,
t: f32,
alpha: f32,
size: IntroArtSize,
version: &str,
row_offset: u16,
) {
if area.width == 0 || area.height == 0 {
return;
@@ -83,7 +74,24 @@ pub fn render_intro_animation_with_size_and_alpha(
let scan_p = smoothstep(0.55, 0.85, t);
let frame = (t * 60.0) as u32;
let lines = welcome_lines(size, version);
let mut lines = welcome_lines(size, version);
let full_width = lines.iter().map(|line| line.chars().count()).max().unwrap_or(0);
let start = row_offset as usize;
if start >= lines.len() {
return;
}
let end = (start + area.height as usize).min(lines.len());
if start > 0 || end < lines.len() {
lines = lines[start..end].iter().cloned().collect();
if full_width > 0 {
for line in &mut lines {
let len = line.chars().count();
if len < full_width {
line.push_str(&" ".repeat(full_width - len));
}
}
}
}
let (char_mask, anim_mask, shadow_mask, w, h) =
lines_masks(&lines, |ch| ANIMATED_CHARS.contains(&ch));
if w == 0 || h == 0 {
@@ -616,7 +624,15 @@ mod tests {
let rect = Rect::new(0, 0, width, height);
let mut buf = Buffer::empty(rect);
render_intro_animation_with_size(rect, &mut buf, 1.0, IntroArtSize::Large, &version);
render_intro_animation_with_size_and_alpha_offset(
rect,
&mut buf,
1.0,
1.0,
IntroArtSize::Large,
&version,
0,
);
let rendered = buffer_to_strings(&buf, rect);
assert_eq!(trim_lines(rendered), trim_lines(expected));
@@ -631,7 +647,15 @@ mod tests {
let rect = Rect::new(0, 0, width, height);
let mut buf = Buffer::empty(rect);
render_intro_animation_with_size(rect, &mut buf, 1.0, IntroArtSize::Medium, &version);
render_intro_animation_with_size_and_alpha_offset(
rect,
&mut buf,
1.0,
1.0,
IntroArtSize::Medium,
&version,
0,
);
let rendered = buffer_to_strings(&buf, rect);
assert_eq!(trim_lines(rendered), trim_lines(expected));
@@ -645,7 +669,15 @@ mod tests {
let rect = Rect::new(0, 0, width, 1);
let mut buf = Buffer::empty(rect);
render_intro_animation_with_size(rect, &mut buf, 1.0, IntroArtSize::Small, &version);
render_intro_animation_with_size_and_alpha_offset(
rect,
&mut buf,
1.0,
1.0,
IntroArtSize::Small,
&version,
0,
);
let rendered = buffer_to_strings(&buf, rect);
assert_eq!(trim_lines(rendered), trim_lines(expected));
+47 -16
View File
@@ -7,7 +7,7 @@ pub(crate) struct AnimatedWelcomeCell {
completed: Cell<bool>,
fade_start: RefCell<Option<Instant>>,
faded_out: Cell<bool>,
locked_height: Cell<Option<u16>>,
available_height: Cell<Option<u16>>,
variant: Cell<Option<crate::glitch_animation::IntroArtSize>>,
version_label: String,
hidden: Cell<bool>,
@@ -20,13 +20,17 @@ impl AnimatedWelcomeCell {
completed: Cell::new(false),
fade_start: RefCell::new(None),
faded_out: Cell::new(false),
locked_height: Cell::new(None),
available_height: Cell::new(None),
variant: Cell::new(None),
version_label: format!("v{}", code_version::version()),
hidden: Cell::new(false),
}
}
pub(crate) fn set_available_height(&self, height: u16) {
self.available_height.set(Some(height));
}
fn fade_start(&self) -> Option<Instant> {
*self.fade_start.borrow()
}
@@ -71,13 +75,16 @@ impl HistoryCell for AnimatedWelcomeCell {
}
fn desired_height(&self, width: u16) -> u16 {
// Lock height to the largest art that fits the current width so we can
// grow back when vertical space returns (instead of permanently
// shrinking after a tight layout). Height is recomputed on every
// measurement so width changes immediately refresh the locked value.
let variant_for_width = crate::glitch_animation::intro_art_size_for_width(width);
let h = crate::glitch_animation::intro_art_height(variant_for_width);
self.locked_height.set(Some(h));
let height_hint = self.available_height.get();
let variant = height_hint
.map(|height| {
crate::glitch_animation::intro_art_size_for_area(
width,
height.saturating_sub(3),
)
})
.unwrap_or_else(|| crate::glitch_animation::intro_art_size_for_width(width));
let h = crate::glitch_animation::intro_art_height(variant);
h.saturating_add(3)
}
@@ -86,6 +93,10 @@ impl HistoryCell for AnimatedWelcomeCell {
}
fn custom_render(&self, area: Rect, buf: &mut Buffer) {
self.custom_render_with_skip(area, buf, 0);
}
fn custom_render_with_skip(&self, area: Rect, buf: &mut Buffer, skip_rows: u16) {
if self.hidden.get() {
return;
}
@@ -102,7 +113,11 @@ impl HistoryCell for AnimatedWelcomeCell {
}
}
let current_variant = crate::glitch_animation::intro_art_size_for_area(area.width, area.height);
let height_hint = self.available_height.get().unwrap_or(area.height);
let current_variant = crate::glitch_animation::intro_art_size_for_area(
area.width,
height_hint.saturating_sub(3),
);
let previous_variant = self.variant.get();
let variant_changed = previous_variant.map_or(false, |v| v != current_variant);
@@ -117,17 +132,30 @@ impl HistoryCell for AnimatedWelcomeCell {
let variant_for_render = current_variant;
let art_height = crate::glitch_animation::intro_art_height(current_variant).min(area.height);
let art_height = crate::glitch_animation::intro_art_height(current_variant);
let full_height = art_height.saturating_add(3);
// Prefer the logo low in the cell so spare lines sit above it. Keep a
// small bottom gap (up to 2 rows) when there's room; otherwise center.
let slack = area.height.saturating_sub(art_height);
let slack = full_height.saturating_sub(art_height);
let bottom_pad = slack.min(2);
let top_pad = slack.saturating_sub(bottom_pad);
let art_top = top_pad;
let art_bottom = art_top.saturating_add(art_height);
let vis_top = skip_rows;
let vis_bottom = skip_rows.saturating_add(area.height);
let intersection_top = art_top.max(vis_top);
let intersection_bottom = art_bottom.min(vis_bottom);
if intersection_bottom <= intersection_top {
return;
}
let row_offset = intersection_top.saturating_sub(art_top);
let y_offset = intersection_top.saturating_sub(vis_top);
let visible_height = intersection_bottom.saturating_sub(intersection_top);
let positioned_area = Rect {
x: area.x,
y: area.y.saturating_add(top_pad),
y: area.y.saturating_add(y_offset),
width: area.width,
height: art_height,
height: visible_height,
};
let fade_duration = Duration::from_millis(800);
@@ -137,13 +165,14 @@ impl HistoryCell for AnimatedWelcomeCell {
if fade_elapsed < fade_duration && !self.faded_out.get() {
let fade_progress = fade_elapsed.as_secs_f32() / fade_duration.as_secs_f32();
let alpha = 1.0 - fade_progress;
crate::glitch_animation::render_intro_animation_with_size_and_alpha(
crate::glitch_animation::render_intro_animation_with_size_and_alpha_offset(
positioned_area,
buf,
1.0,
alpha,
current_variant,
&self.version_label,
row_offset,
);
} else {
self.faded_out.set(true);
@@ -163,12 +192,14 @@ impl HistoryCell for AnimatedWelcomeCell {
1.0
};
crate::glitch_animation::render_intro_animation_with_size(
crate::glitch_animation::render_intro_animation_with_size_and_alpha_offset(
positioned_area,
buf,
progress,
1.0,
variant_for_render,
&self.version_label,
row_offset,
);
}