T06: hidden cells blank, inactive window hollows the cursor, glyphs painted on one baseline with CoreText's upward offsets flipped
CI / platforms (windows-latest) (push) Failing after 8s
CI / macos (push) Failing after 8m50s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s

This commit is contained in:
ZacharyZhang-NY committed 2026-10-14 13:11:51 -04:00
1 parent ae1d8ce746
commit 3c8f7460de
4 files changed
+89 -46

No files matched your search

+1 -1
View File
@@ -73,11 +73,11 @@ pub fn open(launch: Launch, cx: &mut App) -> anyhow::Result<WindowHandle<Workspa
..Default::default()
};
let (pty, events) = spawn(&launch, (80, 24), (8, 16), config(SettingsStore::get(cx)))?;
let terminal = cx.new(|cx| TerminalView::new(pty, events, cx));
let window = cx.open_window(options, |window, cx| {
window
.observe_window_appearance(|_, cx| themes::apply(cx))
.detach();
let terminal = cx.new(|cx| TerminalView::new(pty, events, window, cx));
window.focus(&terminal.focus_handle(cx), cx);
cx.new(|cx| Workspace {
focus: cx.focus_handle(),
+18 -8
View File
@@ -91,15 +91,17 @@ pub fn frame<T: EventListener>(term: &Term<T>, ink: &Ink) -> Frame {
let spacer = cell
.flags
.intersects(Flags::WIDE_CHAR_SPACER | Flags::LEADING_WIDE_CHAR_SPACER);
let hidden = cell.flags.contains(Flags::HIDDEN);
let cluster = cell
.zerowidth()
.filter(|_| !hidden)
.filter(|marks| !marks.is_empty())
.map(|marks| {
std::iter::once(cell.c)
.chain(marks.iter().copied())
.collect::<String>()
});
let shown = if spacer || cluster.is_some() {
let shown = if spacer || hidden || cluster.is_some() {
' '
} else {
cell.c
@@ -111,7 +113,7 @@ pub fn frame<T: EventListener>(term: &Term<T>, ink: &Ink) -> Frame {
column: point.column.0,
shape: content.cursor.shape,
wide: cell.flags.contains(Flags::WIDE_CHAR),
text: cluster.clone().unwrap_or_else(|| cell.c.to_string()),
text: cluster.clone().unwrap_or_else(|| shown.to_string()),
style: look,
});
}
@@ -140,10 +142,13 @@ fn style(cell: &Cell, ink: &Ink, set: &Colors) -> Style {
if flags.contains(Flags::DIM) {
fg = fg.opacity(DIM);
}
if flags.contains(Flags::HIDDEN) {
let hidden = flags.contains(Flags::HIDDEN);
if hidden {
fg = bg;
}
let underline = if flags.contains(Flags::DOUBLE_UNDERLINE) {
let underline = if hidden {
Underline::None
} else if flags.contains(Flags::DOUBLE_UNDERLINE) {
Underline::Double
} else if flags.contains(Flags::UNDERCURL) {
Underline::Curly
@@ -165,7 +170,7 @@ fn style(cell: &Cell, ink: &Ink, set: &Colors) -> Style {
underline_color: cell
.underline_color()
.map_or(fg, |color| ink.resolve(color, set)),
strike: flags.contains(Flags::STRIKEOUT),
strike: flags.contains(Flags::STRIKEOUT) && !hidden,
}
}
@@ -252,8 +257,13 @@ mod tests {
}
#[test]
fn hidden_text_takes_its_ground() {
let (frame, ink) = shown(b"\x1b[8mh");
assert_eq!(frame.rows[0].runs[0].1.fg, ink.bg);
fn hidden_cells_show_nothing_but_their_ground() {
let (frame, _) = shown("\x1b[8;4;9;41mSe\u{301}\x1b[0m\x1b[2D".as_bytes());
assert_eq!(frame.rows[0].text.trim_end(), "");
assert!(frame.rows[0].clusters.is_empty());
let (_, style) = frame.rows[0].runs[0];
assert_eq!((style.underline, style.strike), (Underline::None, false));
assert!(style.bg.is_some(), "the ground stays");
assert_eq!(frame.cursor.expect("a cursor").text, " ");
}
}
+66 -35
View File
@@ -2,9 +2,9 @@ use std::rc::Rc;
use alacritty_terminal::vte::ansi::CursorShape;
use gpui::{
App, Bounds, Entity, Font, FontId, FontStyle, FontWeight, Hsla, IntoElement, Pixels, Point,
Size, StrikethroughStyle, Styled, TextAlign, TextRun, UnderlineStyle, Window, canvas, fill,
outline, point, px, size,
Bounds, Entity, Font, FontId, FontStyle, FontWeight, Hsla, IntoElement, Pixels, Point, Size,
StrikethroughStyle, Styled, TextRun, UnderlineStyle, Window, canvas, fill, outline, point, px,
size,
};
use super::{
@@ -48,7 +48,7 @@ pub fn grid(
}
});
},
move |bounds, _, window, cx| {
move |bounds, _, window, _| {
log::trace!(
"terminal: paint {:?}, row 0 {:?}",
bounds,
@@ -56,10 +56,10 @@ pub fn grid(
);
for (index, row) in frame.rows.iter().enumerate() {
let top = bounds.origin + point(px(0.), cell.height * index as f32);
paint_row(row, top, &look, window, cx);
paint_row(row, top, &look, window);
}
if let Some((cursor, shape)) = &cursor {
paint_cursor(cursor, *shape, bounds.origin, &look, window, cx);
paint_cursor(cursor, *shape, bounds.origin, &look, window);
}
},
)
@@ -97,13 +97,7 @@ fn spans(row: &super::frame::Row) -> Vec<(usize, usize, Style)> {
.collect()
}
fn paint_row(
row: &super::frame::Row,
top: Point<Pixels>,
look: &Look,
window: &mut Window,
cx: &mut App,
) {
fn paint_row(row: &super::frame::Row, top: Point<Pixels>, look: &Look, window: &mut Window) {
let cell = look.cell;
let at = |column: usize| top + point(cell.width * column as f32, px(0.));
let spans = spans(row);
@@ -123,17 +117,9 @@ fn paint_row(
runs(text, style, style.fg, look, window)
})
.collect();
let shaped = window.text_system().shape_line(
row.text.clone().into(),
look.size,
&runs,
Some(cell.width),
);
if let Err(error) = shaped.paint(top, cell.height, TextAlign::Left, None, window, cx) {
log::error!("terminal: a row failed to paint: {error:#}");
}
draw(&row.text, &runs, top, Some(cell.width), look, window);
for (column, cluster, style) in &row.clusters {
text(cluster, style.fg, style, at(*column), look, window, cx);
text(cluster, style.fg, style, at(*column), look, window);
}
for (column, columns, style) in spans {
let width = cell.width * columns as f32;
@@ -169,7 +155,7 @@ fn private(ch: char) -> bool {
matches!(ch, '\u{E000}'..='\u{F8FF}' | '\u{F0000}'..='\u{FFFFD}' | '\u{100000}'..='\u{10FFFD}')
}
/// Paints `content` alone at `origin`, a cell's worth.
/// Paints `content` alone at `origin`, a cell's top-left, its marks where shaping puts them.
fn text(
content: &str,
color: Hsla,
@@ -177,20 +163,67 @@ fn text(
origin: Point<Pixels>,
look: &Look,
window: &mut Window,
cx: &mut App,
) {
let runs = runs(content, style, color, look, window);
let shaped = window.text_system().shape_line(
content.to_string().into(),
look.size,
&runs,
Some(look.cell.width),
);
if let Err(error) = shaped.paint(origin, look.cell.height, TextAlign::Left, None, window, cx) {
log::error!("terminal: `{content}` failed to paint: {error:#}");
draw(content, &runs, origin, None, look, window);
}
/// Shapes `content` and paints each glyph on the cell's baseline below `top`; `force` sets every advance.
fn draw(
content: &str,
runs: &[TextRun],
top: Point<Pixels>,
force: Option<Pixels>,
look: &Look,
window: &mut Window,
) {
let shaped =
window
.text_system()
.shape_line(content.to_string().into(), look.size, runs, force);
let baseline = top.y + look.baseline;
for run in &shaped.runs {
for glyph in &run.glyphs {
let color = color_at(runs, glyph.index);
let at = point(
top.x + glyph.position.x,
baseline + upward(glyph.position.y),
);
let painted = if glyph.is_emoji {
window.paint_emoji(at, run.font_id, glyph.id, look.size)
} else {
window.paint_glyph(at, run.font_id, glyph.id, look.size, color)
};
if let Err(error) = painted {
log::error!("terminal: a glyph of `{content}` failed to paint: {error:#}");
}
}
}
}
/// The color of the run holding byte `index`.
fn color_at(runs: &[TextRun], index: usize) -> Hsla {
let mut end = 0;
runs.iter()
.find(|run| {
end += run.len;
index < end
})
.expect("a glyph indexes its own text")
.color
}
/// CoreText gives glyph offsets growing upward; the screen grows downward.
#[cfg(target_os = "macos")]
fn upward(y: Pixels) -> Pixels {
-y
}
#[cfg(not(target_os = "macos"))]
fn upward(y: Pixels) -> Pixels {
y
}
/// Underlines and strikethrough under `width` from `origin`, a cell's top-left.
fn decorate(origin: Point<Pixels>, width: Pixels, style: &Style, look: &Look, window: &mut Window) {
let rule = look.rule;
@@ -252,7 +285,6 @@ fn paint_cursor(
origin: Point<Pixels>,
look: &Look,
window: &mut Window,
cx: &mut App,
) {
let cell = look.cell;
let corner = origin
@@ -272,7 +304,6 @@ fn paint_cursor(
corner,
look,
window,
cx,
);
}
CursorShape::HollowBlock => {
+4 -2
View File
@@ -47,6 +47,7 @@ pub struct TerminalView {
shown: bool,
_events: Task<()>,
_blink: Task<()>,
_activation: Subscription,
_settings: Subscription,
}
@@ -60,7 +61,7 @@ impl Focusable for TerminalView {
impl TerminalView {
/// Draws `pty`; the first layout fits its grid to the box.
pub fn new(pty: Pty, events: Events, cx: &mut Context<Self>) -> Self {
pub fn new(pty: Pty, events: Events, window: &mut Window, cx: &mut Context<Self>) -> Self {
let size = {
let term = pty.term.lock();
(term.columns(), term.screen_lines())
@@ -74,6 +75,7 @@ impl TerminalView {
shown: true,
_events: Self::pump(events, cx),
_blink: Self::blink(cx),
_activation: cx.observe_window_activation(window, |_, _, cx| cx.notify()),
_settings: cx.observe_global::<SettingsStore>(|view, cx| {
view.pty
.term
@@ -228,7 +230,7 @@ impl Render for TerminalView {
let shown = frame(&term, &ink);
let blinks = term.cursor_style().blinking;
drop(term);
let focused = self.focus.is_focused(window);
let focused = self.focus.is_focused(window) && window.is_window_active();
self.blinking = focused && blinks;
if !self.blinking {
self.shown = true;