## Objective Fix missing characters in GPUI's web renderer for emoji-capable symbols that default to text presentation, such as repeated bare spider webs (`🕸🕸🕸`). The default Canvas policy previously rejected these symbols before checking whether bundled fonts could render them. ## Solution Separate emoji eligibility from presentation. Emoji-capable graphemes can now use Canvas when their glyph is missing, while existing bundled text glyphs remain native and explicit text/emoji presentation remains intact. CJK fallback remains opt-in. Add regression coverage for repeated spider webs and mixed bare, VS15, and VS16 sequences, including every glyph's source index and presentation, missing-glyph reports, copy preservation, and raster-cache reuse. ## Testing - `cargo test -p gpui_web --lib`: all 19 tests passed. - `cargo fmt -p gpui_web -- --check`, example Rust formatting, and `git diff --check`: passed. - `trunk build` in `crates/gpui_web/examples/hello_web`: passed. - Headless Chrome on macOS: browser regression failed against the old implementation and passed with the fix. Inspected the resulting screenshot; all mixed-presentation spider webs render. - Firefox, Safari, and other operating systems have not been exercised. To run the browser workflow, serve the gallery with `trunk serve` from `crates/gpui_web/examples/hello_web`, then run `node crates/gpui_web/examples/hello_web/test_canvas_fallback.mjs http://127.0.0.1:8080` from the repository root. Release Notes: - [GPUI] Fixed missing text-default emoji, including spider-web symbols, in GPUI's web renderer.
118 lines
3.7 KiB
Rust
118 lines
3.7 KiB
Rust
use gpui::{FallbackFontClass, FontRun, MissingGlyph, MissingGlyphSink, font, px};
|
|
use std::{
|
|
borrow::Cow,
|
|
sync::{Arc, Mutex},
|
|
};
|
|
use wasm_bindgen::prelude::*;
|
|
|
|
fn main() {}
|
|
|
|
#[derive(Default)]
|
|
struct Reports(Mutex<Vec<MissingGlyph>>);
|
|
|
|
impl MissingGlyphSink for Reports {
|
|
fn report(&self, missing: Vec<MissingGlyph>) {
|
|
self.0.lock().expect("report lock").extend(missing);
|
|
}
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
pub fn test_missing_glyph_notifications() -> Result<(), JsValue> {
|
|
let platform = gpui_platform::current_platform(false);
|
|
let text_system = platform.text_system();
|
|
text_system
|
|
.add_fonts(vec![Cow::Borrowed(include_bytes!(
|
|
"../../../../assets/fonts/lilex/Lilex-Regular.ttf"
|
|
))])
|
|
.map_err(|error| JsValue::from_str(&error.to_string()))?;
|
|
let font_id = text_system
|
|
.font_id(&font("Lilex"))
|
|
.map_err(|error| JsValue::from_str(&error.to_string()))?;
|
|
let reports = Arc::new(Reports::default());
|
|
text_system.set_missing_glyph_sink(Some(reports.clone()));
|
|
let text = "A界😀👨👩👧👦か\u{3099}";
|
|
let runs = [FontRun {
|
|
len: text.len(),
|
|
font_id,
|
|
}];
|
|
let layout = text_system.layout_line(text, px(24.), &runs);
|
|
assert!(
|
|
layout
|
|
.runs
|
|
.iter()
|
|
.any(|run| run.glyphs.iter().any(|glyph| glyph.is_emoji))
|
|
);
|
|
let missing = reports.0.lock().expect("report lock").clone();
|
|
assert_eq!(
|
|
missing
|
|
.iter()
|
|
.map(|report| report.grapheme())
|
|
.collect::<Vec<_>>(),
|
|
["界", "か\u{3099}"],
|
|
);
|
|
assert!(
|
|
missing
|
|
.iter()
|
|
.all(|report| report.font_class() == FallbackFontClass::Monospace)
|
|
);
|
|
|
|
for graphemes in [
|
|
vec!["🕸", "🕸", "🕸"],
|
|
vec!["🕸", "🕸\u{fe0f}", "🕸\u{fe0e}", "🕸"],
|
|
] {
|
|
reports.0.lock().expect("report lock").clear();
|
|
let text = graphemes.concat();
|
|
let layout = text_system.layout_line(
|
|
&text,
|
|
px(24.),
|
|
&[FontRun {
|
|
len: text.len(),
|
|
font_id,
|
|
}],
|
|
);
|
|
let glyphs = layout
|
|
.runs
|
|
.iter()
|
|
.flat_map(|run| run.glyphs.iter().map(move |glyph| (run.font_id, glyph)))
|
|
.collect::<Vec<_>>();
|
|
assert_eq!(glyphs.len(), graphemes.len());
|
|
let mut source_index = 0;
|
|
for ((resolved_font, glyph), grapheme) in glyphs.iter().zip(&graphemes) {
|
|
assert_ne!(
|
|
*resolved_font, font_id,
|
|
"Missing Lilex glyph must fall back"
|
|
);
|
|
assert_eq!(glyph.index, source_index, "Preserve each source grapheme");
|
|
assert_eq!(glyph.is_emoji, grapheme.ends_with('\u{fe0f}'));
|
|
source_index += grapheme.len();
|
|
}
|
|
assert_eq!(source_index, text.len());
|
|
assert!(
|
|
reports.0.lock().expect("report lock").is_empty(),
|
|
"Every spider-web glyph must resolve, not just the first cached raster"
|
|
);
|
|
}
|
|
|
|
reports.0.lock().expect("report lock").clear();
|
|
let bundled = "Hello ©";
|
|
let layout = text_system.layout_line(
|
|
bundled,
|
|
px(24.),
|
|
&[FontRun {
|
|
len: bundled.len(),
|
|
font_id,
|
|
}],
|
|
);
|
|
for run in &layout.runs {
|
|
assert_eq!(run.font_id, font_id, "Retain existing bundled text glyphs");
|
|
assert!(run.glyphs.iter().all(|glyph| !glyph.is_emoji));
|
|
}
|
|
assert!(reports.0.lock().expect("report lock").is_empty());
|
|
|
|
reports.0.lock().expect("report lock").clear();
|
|
text_system.set_missing_glyph_sink(None);
|
|
text_system.layout_line(text, px(24.), &runs);
|
|
assert!(reports.0.lock().expect("report lock").is_empty());
|
|
Ok(())
|
|
}
|