685 lines
24 KiB
Rust
685 lines
24 KiB
Rust
#[test]
|
|
fn canvas_paints_before_property_panel_overlays() {
|
|
let source = std::fs::read_to_string(format!(
|
|
"{}/src/widget_host/paint.rs",
|
|
env!("CARGO_MANIFEST_DIR")
|
|
))
|
|
.expect("paint source is readable");
|
|
|
|
let canvas = source
|
|
.find("CanvasViewport::from_editor")
|
|
.expect("canvas paint block marker exists");
|
|
let property = source
|
|
.find("PropertyPanel::for_selection")
|
|
.expect("property panel paint block marker exists");
|
|
|
|
assert!(
|
|
canvas < property,
|
|
"PropertyPanel must paint after CanvasViewport so popovers extending into the canvas are not covered"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn image_fill_popover_paints_above_status_bar() {
|
|
let source = std::fs::read_to_string(format!(
|
|
"{}/src/widget_host/paint.rs",
|
|
env!("CARGO_MANIFEST_DIR")
|
|
))
|
|
.expect("paint source is readable");
|
|
|
|
let status = source
|
|
.find("StatusBar::for_editor")
|
|
.expect("status paint block marker exists");
|
|
let overlays = source
|
|
.find("PropertyPanel overlays")
|
|
.expect("property overlay paint block marker exists");
|
|
|
|
assert!(
|
|
status < overlays,
|
|
"image-fill popover must paint after StatusBar so the zoom pill cannot cover adjustment rows"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn canvaskit_clip_ops_use_intersect_clips_without_canvas2d_paths() {
|
|
let source = std::fs::read_to_string(format!(
|
|
"{}/src/op_ck_bridge.js",
|
|
env!("CARGO_MANIFEST_DIR")
|
|
))
|
|
.expect("CanvasKit bridge source is readable");
|
|
|
|
let clip_rect = source
|
|
.find("clipRect(x, y, w, h)")
|
|
.expect("CanvasKit clipRect bridge marker exists");
|
|
let rect_op = source[clip_rect..]
|
|
.find("canvas.clipRect(CK.LTRBRect")
|
|
.expect("clipRect uses CanvasKit clipRect")
|
|
+ clip_rect;
|
|
let intersect = source[rect_op..]
|
|
.find("CK.ClipOp.Intersect")
|
|
.expect("clipRect uses intersect clipping")
|
|
+ rect_op;
|
|
let clip_round = source
|
|
.find("clipRoundRect(x, y, w, h, rad)")
|
|
.expect("CanvasKit clipRoundRect bridge marker exists");
|
|
let round_op = source[clip_round..]
|
|
.find("canvas.clipRRect(CK.RRectXY")
|
|
.expect("clipRoundRect uses CanvasKit clipRRect")
|
|
+ clip_round;
|
|
|
|
assert!(
|
|
clip_rect < rect_op
|
|
&& rect_op < intersect
|
|
&& intersect < clip_round
|
|
&& clip_round < round_op,
|
|
"CanvasKit clips must use explicit intersect clip ops, not stale Canvas2D paths"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn canvaskit_frame_resets_save_stack_before_flush() {
|
|
let source = std::fs::read_to_string(format!(
|
|
"{}/src/op_ck_bridge.js",
|
|
env!("CARGO_MANIFEST_DIR")
|
|
))
|
|
.expect("CanvasKit bridge source is readable");
|
|
|
|
let begin = source
|
|
.find("beginFrame()")
|
|
.expect("CanvasKit beginFrame marker exists");
|
|
let begin_restore = source[begin..]
|
|
.find("while (canvas.getSaveCount() > 1) canvas.restore();")
|
|
.expect("beginFrame must clear leaked save/clip/matrix state")
|
|
+ begin;
|
|
let save = source[begin_restore..]
|
|
.find("canvas.save();")
|
|
.expect("beginFrame saves a clean frame baseline")
|
|
+ begin_restore;
|
|
let end = source
|
|
.find("endFrame()")
|
|
.expect("CanvasKit endFrame marker exists");
|
|
let end_restore = source[end..]
|
|
.find("while (canvas.getSaveCount() > 1) canvas.restore();")
|
|
.expect("endFrame restores back to the baseline")
|
|
+ end;
|
|
let flush = source[end_restore..]
|
|
.find("surface.flush();")
|
|
.expect("endFrame flushes after restoring canvas state")
|
|
+ end_restore;
|
|
|
|
assert!(
|
|
begin < begin_restore
|
|
&& begin_restore < save
|
|
&& save < end
|
|
&& end < end_restore
|
|
&& end_restore < flush,
|
|
"CanvasKit frames must reset leaked state before painting and restore before flushing"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn canvaskit_text_defaults_to_browser_system_font_fallback() {
|
|
let source = std::fs::read_to_string(format!(
|
|
"{}/src/op_ck_bridge.js",
|
|
env!("CARGO_MANIFEST_DIR")
|
|
))
|
|
.expect("CanvasKit bridge source is readable");
|
|
|
|
for marker in [
|
|
"const hasCjk",
|
|
"const isEmojiCp",
|
|
"const segments = (t)",
|
|
"const shouldUseBrowserTextFallback = (_t, _emojiRun) => Boolean(browserTextCtx);",
|
|
"drawBrowserText(seg.text, cx, y, sz, weight, italic, r, g, b, a, seg.emoji)",
|
|
"browserTextMeasure(seg.text, sz, weight, italic)",
|
|
"CK.Typeface.GetDefault()",
|
|
] {
|
|
assert!(
|
|
source.contains(marker),
|
|
"CanvasKit text must preserve `{marker}` for browser/system font rendering"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn canvaskit_browser_text_fallback_does_not_require_canvas_paint() {
|
|
let source = std::fs::read_to_string(format!(
|
|
"{}/src/op_ck_bridge.js",
|
|
env!("CARGO_MANIFEST_DIR")
|
|
))
|
|
.expect("CanvasKit bridge source is readable");
|
|
|
|
for marker in [
|
|
"const allSegmentsUseBrowserTextFallback = (segs) =>",
|
|
"const segs = segments(t);",
|
|
"if (allSegmentsUseBrowserTextFallback(segs))",
|
|
] {
|
|
assert!(
|
|
source.contains(marker),
|
|
"CanvasKit drawText must preserve `{marker}` so browser/system text fallback cannot throw from optional PaintStyle setup"
|
|
);
|
|
}
|
|
|
|
// The browser/system-font fallback fast path (which returns before any
|
|
// CanvasKit Paint is allocated) lives in the shared `drawScriptRun`
|
|
// helper that `drawText` — and the uncovered segments of a family-aware
|
|
// run — delegate to.
|
|
let script_run = source
|
|
.find("const drawScriptRun = (t, x, y, sz, weight, italic, r, g, b, a) =>")
|
|
.expect("drawScriptRun helper exists");
|
|
let fallback_fast_path = source[script_run..]
|
|
.find("if (allSegmentsUseBrowserTextFallback(segs))")
|
|
.expect("browser text fast path exists")
|
|
+ script_run;
|
|
let fill_paint = source[script_run..]
|
|
.find("const p = fillPaint(r, g, b, a);")
|
|
.expect("CanvasKit paint fallback exists")
|
|
+ script_run;
|
|
assert!(
|
|
fallback_fast_path < fill_paint,
|
|
"drawScriptRun must take the browser/system font path before allocating CanvasKit Paint"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn canvaskit_text_measurement_uses_draw_text_style() {
|
|
let bridge = std::fs::read_to_string(format!(
|
|
"{}/src/op_ck_bridge.js",
|
|
env!("CARGO_MANIFEST_DIR")
|
|
))
|
|
.expect("CanvasKit bridge source is readable");
|
|
let backend =
|
|
std::fs::read_to_string(format!("{}/src/canvaskit.rs", env!("CARGO_MANIFEST_DIR")))
|
|
.expect("CanvasKit backend source is readable");
|
|
|
|
for marker in [
|
|
"measureTextStyled(t, sz, weight, italic)",
|
|
"browserTextMeasure(seg.text, sz, weight, italic)",
|
|
] {
|
|
assert!(
|
|
bridge.contains(marker),
|
|
"CanvasKit bridge must preserve `{marker}` so measured text matches drawn text"
|
|
);
|
|
}
|
|
|
|
for marker in [
|
|
"fn measure_text_styled(",
|
|
"weight: u16,",
|
|
"italic: bool,",
|
|
".measure_text_styled(text, font_size, weight as i32, italic)",
|
|
] {
|
|
assert!(
|
|
backend.contains(marker),
|
|
"CanvasKit backend must preserve `{marker}` so web layout measures with draw-time text style"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn canvaskit_paint_style_is_guarded_before_set_style() {
|
|
let source = std::fs::read_to_string(format!(
|
|
"{}/src/op_ck_bridge.js",
|
|
env!("CARGO_MANIFEST_DIR")
|
|
))
|
|
.expect("CanvasKit bridge source is readable");
|
|
|
|
for marker in [
|
|
"const setPaintStyle = (paint, style) =>",
|
|
"setPaintStyle(p, CK.PaintStyle.Fill)",
|
|
"setPaintStyle(p, CK.PaintStyle.Stroke)",
|
|
"setPaintStyle(p, CK.PaintStyle.StrokeAndFill)",
|
|
] {
|
|
assert!(
|
|
source.contains(marker),
|
|
"CanvasKit bridge must preserve `{marker}` so optional PaintStyle enums cannot throw during repaint"
|
|
);
|
|
}
|
|
|
|
assert!(
|
|
!source.contains("p.setStyle(CK.PaintStyle.Fill)")
|
|
&& !source.contains("p.setStyle(CK.PaintStyle.Stroke);")
|
|
&& !source.contains("p.setStyle(CK.PaintStyle.StrokeAndFill)"),
|
|
"CanvasKit bridge must guard PaintStyle before setStyle to avoid leaving the web host stuck in a borrowed repaint"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn canvaskit_bridge_registers_browser_system_fonts() {
|
|
let source = std::fs::read_to_string(format!(
|
|
"{}/src/op_ck_bridge.js",
|
|
env!("CARGO_MANIFEST_DIR")
|
|
))
|
|
.expect("CanvasKit bridge source is readable");
|
|
|
|
for marker in [
|
|
"const systemTypefaces",
|
|
"registerSystemFont(family, bytes)",
|
|
"CK.Typeface.MakeFreeTypeFaceFromData(copyBytes(bytes))",
|
|
"systemTypefaceFor(t, emojiRun)",
|
|
"const hasSystemFallbackText",
|
|
"hasSystemFallbackText(t)",
|
|
"Kohinoor Devanagari",
|
|
"Noto Sans Thai",
|
|
] {
|
|
assert!(
|
|
source.contains(marker),
|
|
"CanvasKit bridge must preserve `{marker}` so web text can use browser system fonts"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn canvaskit_bridge_uses_browser_text_fallback_when_local_font_api_is_unavailable() {
|
|
let source = std::fs::read_to_string(format!(
|
|
"{}/src/op_ck_bridge.js",
|
|
env!("CARGO_MANIFEST_DIR")
|
|
))
|
|
.expect("CanvasKit bridge source is readable");
|
|
|
|
for marker in [
|
|
"const browserTextCanvas",
|
|
"const shouldUseBrowserTextFallback",
|
|
"const drawBrowserText",
|
|
"CK.MakeImageFromCanvasImageSource",
|
|
"browserTextMeasure(seg.text, sz, weight, italic)",
|
|
] {
|
|
assert!(
|
|
source.contains(marker),
|
|
"CanvasKit bridge must preserve `{marker}` so non-CJK multilingual text can use browser system fonts without bundled font files"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn canvaskit_browser_text_fallback_covers_locale_scripts_and_emoji() {
|
|
let source = std::fs::read_to_string(format!(
|
|
"{}/src/op_ck_bridge.js",
|
|
env!("CARGO_MANIFEST_DIR")
|
|
))
|
|
.expect("CanvasKit bridge source is readable");
|
|
|
|
for marker in [
|
|
"Apple Color Emoji",
|
|
"Segoe UI Emoji",
|
|
"PingFang SC",
|
|
"Apple SD Gothic Neo",
|
|
"Hiragino Kaku Gothic ProN",
|
|
"Yu Gothic",
|
|
"Kohinoor Devanagari",
|
|
"Devanagari Sangam MN",
|
|
"Nirmala UI",
|
|
"Mangal",
|
|
"Thonburi",
|
|
"Leelawadee UI",
|
|
] {
|
|
assert!(
|
|
source.contains(marker),
|
|
"CanvasKit browser fallback stack must preserve `{marker}` for locale and emoji text"
|
|
);
|
|
}
|
|
|
|
let fallback_line = source
|
|
.lines()
|
|
.find(|line| line.contains("const shouldUseBrowserTextFallback"))
|
|
.expect("browser text fallback predicate exists");
|
|
assert!(
|
|
fallback_line.contains("Boolean(browserTextCtx)"),
|
|
"browser text fallback should route text through browser system fonts by default"
|
|
);
|
|
assert!(
|
|
!fallback_line.contains("!hasCjk(t)"),
|
|
"browser text fallback must not exclude CJK/Hangul locale names"
|
|
);
|
|
assert!(
|
|
!fallback_line.contains("!emojiRun"),
|
|
"browser text fallback must not exclude emoji runs from browser system fonts"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn canvaskit_bridge_guards_optional_paint_styles() {
|
|
let source = std::fs::read_to_string(format!(
|
|
"{}/src/op_ck_bridge.js",
|
|
env!("CARGO_MANIFEST_DIR")
|
|
))
|
|
.expect("CanvasKit bridge source is readable");
|
|
|
|
assert!(
|
|
source.contains("const isPaintStyle = (style)"),
|
|
"CanvasKit bridge must validate optional PaintStyle enum values before setStyle"
|
|
);
|
|
assert!(
|
|
source.contains("isPaintStyle(CK.PaintStyle.StrokeAndFill)"),
|
|
"CanvasKit bridge must not pass optional StrokeAndFill directly to Paint.setStyle"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn canvaskit_svg_path_nodes_fit_to_destination_rect() {
|
|
let bridge = std::fs::read_to_string(format!(
|
|
"{}/src/op_ck_bridge.js",
|
|
env!("CARGO_MANIFEST_DIR")
|
|
))
|
|
.expect("CanvasKit bridge source is readable");
|
|
for marker in [
|
|
"const fitPathToRect = (path, x, y, w, h)",
|
|
"const bounds = path.getBounds();",
|
|
"const sx = Math.abs(nativeW) > 0.01 ? w / nativeW : 1;",
|
|
"const sy = Math.abs(nativeH) > 0.01 ? h / nativeH : 1;",
|
|
"fillSvgPathInRect(d, x, y, w, h, evenOdd, r, g, b, a)",
|
|
"strokeSvgPathInRect(d, x, y, w, h, r, g, b, a, sw)",
|
|
] {
|
|
assert!(
|
|
bridge.contains(marker),
|
|
"CanvasKit bridge must preserve `{marker}` so arbitrary .op path nodes fit their own bounds"
|
|
);
|
|
}
|
|
|
|
let backend =
|
|
std::fs::read_to_string(format!("{}/src/canvaskit.rs", env!("CARGO_MANIFEST_DIR")))
|
|
.expect("CanvasKit backend source is readable");
|
|
for marker in [
|
|
"js_name = fillSvgPathInRect",
|
|
"js_name = strokeSvgPathInRect",
|
|
"fn fill_svg_path_in_rect(&mut self, d: &str, rect: Rect, color: Color)",
|
|
"fn stroke_svg_path_in_rect(&mut self, d: &str, rect: Rect, color: Color, width: f32)",
|
|
] {
|
|
assert!(
|
|
backend.contains(marker),
|
|
"CanvasKit backend must override `{marker}` instead of using the icon-size fallback"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn canvaskit_browser_text_raster_key_drops_color_and_alpha() {
|
|
let source = std::fs::read_to_string(format!(
|
|
"{}/src/op_ck_bridge.js",
|
|
env!("CARGO_MANIFEST_DIR")
|
|
))
|
|
.expect("CanvasKit bridge source is readable");
|
|
|
|
// TEXT segments are keyed on (text, size, weight, italic, supersample)
|
|
// only — colour is applied at draw time, so N colours reuse ONE raster.
|
|
assert!(
|
|
source.contains(": [t, sz, weight, italic ? 1 : 0, ss].join('\\n');"),
|
|
"text raster cache key must contain no colour/alpha component"
|
|
);
|
|
assert!(
|
|
source.contains(": 'rgba(255, 255, 255, 1)';"),
|
|
"text glyph run must rasterize as a white full-alpha coverage mask"
|
|
);
|
|
// The unconditional RGBA-keyed raster cache (the thrash source) is gone —
|
|
// the ONLY colour-keyed path left is the emoji branch (prefixed 'e').
|
|
assert!(
|
|
!source.contains("const key = [t, sz, weight, italic ? 1 : 0, r, g, b, a, ss]"),
|
|
"the always-RGBA-keyed raster cache (the thrash source) must be gone"
|
|
);
|
|
// Emoji / colour-glyph segments keep the exact legacy baked-colour path
|
|
// (they ignore fillStyle, so tinting a white mask would flatten them).
|
|
assert!(
|
|
source.contains("const browserTextImage = (t, sz, weight, italic, emoji, r, g, b, a) =>"),
|
|
"browserTextImage must take an `emoji` selector + colour for the legacy emoji path"
|
|
);
|
|
assert!(
|
|
source.contains("['e', t, sz, weight, italic ? 1 : 0, r, g, b, a, ss].join('\\n')"),
|
|
"emoji segments must keep a colour-keyed raster (legacy baked-colour path)"
|
|
);
|
|
assert!(
|
|
source.contains("`rgba(${Math.round(r * 255)}, ${Math.round(g * 255)}, ${Math.round(b * 255)}, ${a})`"),
|
|
"emoji branch must bake the requested colour into fillStyle (colour glyphs ignore a white mask + tint)"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn canvaskit_text_rasters_refresh_at_effective_scale() {
|
|
let source = std::fs::read_to_string(format!(
|
|
"{}/src/op_ck_bridge.js",
|
|
env!("CARGO_MANIFEST_DIR")
|
|
))
|
|
.expect("CanvasKit bridge source is readable");
|
|
|
|
let scale_fn = source
|
|
.find("const effectiveTextScale = () =>")
|
|
.expect("effective text scale helper exists");
|
|
let image_fn = source
|
|
.find("const browserTextImage = (t, sz, weight, italic, emoji, r, g, b, a) =>")
|
|
.expect("browserTextImage exists");
|
|
let image_end = source[image_fn..]
|
|
.find("const TINT_FILTER_CACHE_CAP = 64;")
|
|
.expect("browserTextImage ends before the tint-filter cache")
|
|
+ image_fn;
|
|
let scale_body = &source[scale_fn..image_fn];
|
|
let image_body = &source[image_fn..image_end];
|
|
for marker in [
|
|
"const m = canvas.getTotalMatrix();",
|
|
"const xScale = Math.hypot(m[0], m[3]);",
|
|
"const yScale = Math.hypot(m[1], m[4]);",
|
|
"return Math.max(1, xScale, yScale);",
|
|
] {
|
|
assert!(
|
|
scale_body.contains(marker),
|
|
"effective text scale must preserve `{marker}` for rotated/non-uniform transforms"
|
|
);
|
|
}
|
|
assert!(
|
|
image_body.contains("const ss = effectiveTextScale();"),
|
|
"browserTextImage must supersample at the current CanvasKit transform scale"
|
|
);
|
|
for forbidden in ["clearBrowserTextCache()", "browserTextCache.clear()"] {
|
|
assert!(
|
|
!scale_body.contains(forbidden) && !image_body.contains(forbidden),
|
|
"effective-scale changes must partition the LRU by `ss`, not call `{forbidden}` per draw"
|
|
);
|
|
}
|
|
|
|
for marker in [
|
|
"['e', t, sz, weight, italic ? 1 : 0, r, g, b, a, ss].join('\\n')",
|
|
": [t, sz, weight, italic ? 1 : 0, ss].join('\\n');",
|
|
] {
|
|
assert!(
|
|
image_body.contains(marker),
|
|
"text raster key must preserve `{marker}`"
|
|
);
|
|
}
|
|
assert!(
|
|
!image_body.contains("[t, sz, weight, italic ? 1 : 0, r, g, b, a, ss]"),
|
|
"ordinary text raster keys must remain colour-independent"
|
|
);
|
|
|
|
let clear_fn = source
|
|
.find("const clearBrowserTextCache = () =>")
|
|
.expect("browser text cache cleanup helper exists");
|
|
let text_dpr = source[clear_fn..]
|
|
.find("let textDpr = 1;")
|
|
.expect("cache cleanup helper ends before textDpr state")
|
|
+ clear_fn;
|
|
let clear_body = &source[clear_fn..text_dpr];
|
|
let delete = clear_body
|
|
.find("entry.image.delete()")
|
|
.expect("cache cleanup deletes CanvasKit image handles");
|
|
let clear = clear_body[delete..]
|
|
.find("browserTextCache.clear();")
|
|
.expect("cache cleanup clears the map")
|
|
+ delete;
|
|
let set_dpr = source.find("setDpr(v)").expect("setDpr bridge exists");
|
|
let set_dpr_end = source[set_dpr..]
|
|
.find("\n },")
|
|
.expect("setDpr method has a local end boundary")
|
|
+ set_dpr;
|
|
let set_dpr_body = &source[set_dpr..set_dpr_end];
|
|
let clear_call = set_dpr_body
|
|
.find("clearBrowserTextCache();")
|
|
.expect("DPR changes clear stale text rasters");
|
|
assert!(delete < clear && clear_call < set_dpr_body.len());
|
|
assert_eq!(
|
|
source.matches("clearBrowserTextCache();").count(),
|
|
1,
|
|
"setDpr must be the only call site that clears all browser text rasters"
|
|
);
|
|
assert_eq!(
|
|
source.matches("browserTextCache.clear();").count(),
|
|
1,
|
|
"only the handle-safe cleanup helper may clear the browser text raster map"
|
|
);
|
|
|
|
let eviction = image_body
|
|
.find("if (browserTextCache.size > 512)")
|
|
.expect("browser text raster cache remains bounded");
|
|
assert!(
|
|
image_body[eviction..].contains("old.image.delete()"),
|
|
"LRU eviction must keep deleting the evicted CanvasKit image handle"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn canvaskit_browser_text_cache_is_lru() {
|
|
let source = std::fs::read_to_string(format!(
|
|
"{}/src/op_ck_bridge.js",
|
|
env!("CARGO_MANIFEST_DIR")
|
|
))
|
|
.expect("CanvasKit bridge source is readable");
|
|
|
|
// A cache hit re-inserts the entry (delete + set) so eviction drops the
|
|
// least-recently-used run, not the oldest-inserted (FIFO).
|
|
let image_fn = source
|
|
.find("const browserTextImage = (t, sz, weight, italic, emoji, r, g, b, a) =>")
|
|
.expect("browserTextImage marker exists");
|
|
let hit = source[image_fn..]
|
|
.find("const hit = browserTextCache.get(key);")
|
|
.expect("browserTextImage looks up the cache before rasterizing")
|
|
+ image_fn;
|
|
let reinsert_delete = source[hit..]
|
|
.find("browserTextCache.delete(key);")
|
|
.expect("LRU hit re-inserts by deleting first")
|
|
+ hit;
|
|
let reinsert_set = source[reinsert_delete..]
|
|
.find("browserTextCache.set(key, hit);")
|
|
.expect("LRU hit re-inserts to move the entry to most-recent")
|
|
+ reinsert_delete;
|
|
// Eviction still deletes the wasm image handle of the dropped entry.
|
|
let evict = source[reinsert_set..]
|
|
.find("if (browserTextCache.size > 512)")
|
|
.expect("512-cap eviction still bounds the raster cache")
|
|
+ reinsert_set;
|
|
let evict_delete = source[evict..]
|
|
.find("old.image.delete()")
|
|
.expect("evicted raster image handle is deleted")
|
|
+ evict;
|
|
assert!(
|
|
hit < reinsert_delete
|
|
&& reinsert_delete < reinsert_set
|
|
&& reinsert_set < evict
|
|
&& evict < evict_delete,
|
|
"browser text raster cache must be LRU (re-insert on hit) and delete evicted image handles"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn canvaskit_text_tint_color_filter_cache_is_bounded_and_freed() {
|
|
let source = std::fs::read_to_string(format!(
|
|
"{}/src/op_ck_bridge.js",
|
|
env!("CARGO_MANIFEST_DIR")
|
|
))
|
|
.expect("CanvasKit bridge source is readable");
|
|
|
|
// The tinting filter is a WHITE-mask SrcIn blend, keyed on rgb only (alpha
|
|
// rides the paint), cached in a bounded LRU map that deletes evicted handles.
|
|
for marker in [
|
|
"const TINT_FILTER_CACHE_CAP = 64;",
|
|
"const tintFilterCache = new Map();",
|
|
"const tintColorFilter = (r, g, b) =>",
|
|
"CK.ColorFilter.MakeBlend(col(r, g, b, 1), CK.BlendMode.SrcIn)",
|
|
"const key = r + '\\n' + g + '\\n' + b;",
|
|
] {
|
|
assert!(
|
|
source.contains(marker),
|
|
"tint filter cache must preserve `{marker}` (rgb-keyed SrcIn blend, bounded LRU)"
|
|
);
|
|
}
|
|
|
|
// LRU re-insert on hit + delete on eviction, mirroring svgPathCache hygiene.
|
|
let cache_fn = source
|
|
.find("const tintColorFilter = (r, g, b) =>")
|
|
.expect("tintColorFilter marker exists");
|
|
let hit = source[cache_fn..]
|
|
.find("const hit = tintFilterCache.get(key);")
|
|
.expect("tintColorFilter looks up the cache")
|
|
+ cache_fn;
|
|
let reinsert = source[hit..]
|
|
.find("tintFilterCache.set(key, hit);")
|
|
.expect("tint filter LRU re-inserts on hit")
|
|
+ hit;
|
|
let evict = source[reinsert..]
|
|
.find("if (tintFilterCache.size > TINT_FILTER_CACHE_CAP)")
|
|
.expect("tint filter cache is bounded")
|
|
+ reinsert;
|
|
let evict_delete = source[evict..]
|
|
.find("old.delete()")
|
|
.expect("evicted ColorFilter wasm handle is deleted")
|
|
+ evict;
|
|
assert!(
|
|
hit < reinsert && reinsert < evict && evict < evict_delete,
|
|
"tint ColorFilter cache must be LRU and delete evicted wasm handles"
|
|
);
|
|
|
|
// Draw site: dedicated tint paint (not the shared fill paint) applies the
|
|
// filter + opacity, then draws the raster with that paint.
|
|
for marker in [
|
|
"const cachedTintPaint = new CK.Paint();",
|
|
"paint.setColorFilter(tintColorFilter(r, g, b) || null);",
|
|
"paint.setAlphaf(a < 0 ? 0 : a > 1 ? 1 : a);",
|
|
"canvas.drawImage(entry.image, 0, 0, paint);",
|
|
"canvas.drawImage(entry.image, x - 2, y - entry.baseline, paint);",
|
|
] {
|
|
assert!(
|
|
source.contains(marker),
|
|
"drawBrowserText must preserve `{marker}` (tint-at-draw via dedicated paint)"
|
|
);
|
|
}
|
|
// The tint paint must not alias the shared fill/stroke paints.
|
|
assert!(
|
|
!source.contains("const cachedTintPaint = cachedFillPaint"),
|
|
"tint paint must be its own object, not an alias of the shared fill paint"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn canvaskit_svg_path_nodes_preserve_gradient_and_inner_shadow_paths() {
|
|
let bridge = std::fs::read_to_string(format!(
|
|
"{}/src/op_ck_bridge.js",
|
|
env!("CARGO_MANIFEST_DIR")
|
|
))
|
|
.expect("CanvasKit bridge source is readable");
|
|
for marker in [
|
|
"fillSvgPathInRectLinearGradient(",
|
|
"fillSvgPathInRectRadialGradient(",
|
|
"fillInnerShadowSvgPath(",
|
|
"CK.Shader.MakeLinearGradient",
|
|
"CK.Shader.MakeRadialGradient",
|
|
"CK.BlendMode.DstOut",
|
|
] {
|
|
assert!(
|
|
bridge.contains(marker),
|
|
"CanvasKit bridge must preserve `{marker}` so SVG path effects match desktop rendering"
|
|
);
|
|
}
|
|
|
|
let backend =
|
|
std::fs::read_to_string(format!("{}/src/canvaskit.rs", env!("CARGO_MANIFEST_DIR")))
|
|
.expect("CanvasKit backend source is readable");
|
|
for marker in [
|
|
"js_name = fillSvgPathInRectLinearGradient",
|
|
"js_name = fillSvgPathInRectRadialGradient",
|
|
"js_name = fillInnerShadowSvgPath",
|
|
"fn fill_svg_path_in_rect_linear_gradient(",
|
|
"fn fill_svg_path_in_rect_radial_gradient(",
|
|
"fn fill_inner_shadow_svg_path(",
|
|
] {
|
|
assert!(
|
|
backend.contains(marker),
|
|
"CanvasKit backend must override `{marker}` instead of using default fallback rendering"
|
|
);
|
|
}
|
|
}
|