///|
priv struct SkiaTextDescriptorPreflight {
family_count : Int
fallback_character : Int
fallback_ready : Bool
fallback_resource_count : Int
measurement_ready : Bool
measurement_resource_count : Int
shaping_ready : Bool
shaping_resource_count : Int
shaped_run_ready : Bool
shaped_run_resource_count : Int
glyph_run_ready : Bool
glyph_run_resource_count : Int
}
///|
fn SkiaTextDescriptorPreflight::summary(
self : SkiaTextDescriptorPreflight,
) -> String {
if self.ready() {
"text fallback/shaping descriptor preflight ready"
} else {
"text fallback/shaping descriptor preflight pending"
}
}
///|
fn SkiaTextDescriptorPreflight::ready(
self : SkiaTextDescriptorPreflight,
) -> Bool {
self.family_count > 0 &&
self.fallback_character >= 0 &&
self.fallback_ready &&
self.fallback_resource_count > 0 &&
self.measurement_ready &&
self.measurement_resource_count >= self.fallback_resource_count &&
self.shaping_ready &&
self.shaping_resource_count >= self.fallback_resource_count &&
self.shaped_run_ready &&
self.shaped_run_resource_count >= self.shaping_resource_count &&
self.glyph_run_ready &&
self.glyph_run_resource_count >= self.shaped_run_resource_count
}
///|
priv struct SkiaTextMaturityPreflight {
descriptor_ready : Bool
fallback_request_ready : Bool
shaped_cluster_caret_ready : Bool
fallback_cluster_caret_ready : Bool
emoji_hint_ready : Bool
empty_typeface_emoji_retry_disabled : Bool
missing_glyph_recovery_ready : Bool
bidi_reordering_ready : Bool
paragraph_line_breaking_ready : Bool
mixed_run_font_fallback_ready : Bool
deterministic_color_emoji_ready : Bool
unicode_17_grapheme_boundary_ready : Bool
ready_count : Int
gap_count : Int
}
///|
fn SkiaTextMaturityPreflight::summary(
self : SkiaTextMaturityPreflight,
) -> String {
let baseline_ready = self.descriptor_ready &&
self.fallback_request_ready &&
self.shaped_cluster_caret_ready &&
self.fallback_cluster_caret_ready &&
self.emoji_hint_ready &&
self.empty_typeface_emoji_retry_disabled &&
self.missing_glyph_recovery_ready
let remaining_gaps_pending = !self.bidi_reordering_ready ||
!self.paragraph_line_breaking_ready ||
!self.deterministic_color_emoji_ready
let baseline_label = if baseline_ready {
"baseline ready"
} else {
"baseline pending"
}
let gap_label = if remaining_gaps_pending {
"readiness checks pending"
} else {
"readiness checks resolved"
}
let grapheme_label = if self.unicode_17_grapheme_boundary_ready {
"Unicode 17 grapheme boundary contract ready"
} else {
"Unicode 17 grapheme boundary contract pending"
}
let color_emoji_label = if self.deterministic_color_emoji_ready {
"color emoji confirmed"
} else {
"color emoji pending"
}
let mixed_run_label = if self.mixed_run_font_fallback_ready {
"mixed-run fallback ready; missing-glyph recovery ready; " +
grapheme_label +
"; bidi and paragraph line breaking ready; " +
color_emoji_label
} else {
"mixed-run fallback pending; missing-glyph recovery pending; " +
grapheme_label +
"; bidi and paragraph line breaking ready; " +
color_emoji_label
}
let audit_label = if self.gap_count == 0 &&
baseline_ready &&
self.mixed_run_font_fallback_ready {
"text maturity audit ready"
} else {
"text maturity audit pending"
}
audit_label +
": ready=" +
self.ready_count.to_string() +
" gaps=" +
self.gap_count.to_string() +
" " +
baseline_label +
", " +
gap_label +
" (" +
mixed_run_label +
")"
}
///|
fn skia_text_descriptor_preflight(
font : @core.FontSpec,
text : String,
) -> SkiaTextDescriptorPreflight {
let families = skia_preflight_family_candidates(font)
let fallback = skia_font_fallback_request(font, text)
let font_key = fallback
.font_descriptor(size=Float::from_double(font.size))
.resource_key()
let text_bytes = @utf8.encode(text[:], bom=false)
let run = @moui_skia.TextRunDescriptor::new(
range=@moui_skia.TextRange::new(0, text_bytes.length()),
fallback~,
)
let measurement = @moui_skia.TextMeasurementDescriptor::new(
run~,
font_key~,
width=Float::from_double(font.size * 32.0),
shape=true,
)
let shaping = @moui_skia.TextShapingDescriptor::new(
run~,
font_key~,
width=Float::from_double(font.size * 32.0),
)
let glyph_count = skia_preflight_glyph_count(text)
let shaped_run = shaping.shaped_run_descriptor(
glyph_count~,
cluster_count=glyph_count,
advance=@moui_skia.Point::new(Float::from_double(font.size), 0.0),
)
let glyph_run = shaped_run.glyph_run_descriptor(
glyph_ids=skia_preflight_glyph_ids(glyph_count),
positions=skia_preflight_glyph_positions(glyph_count),
clusters=skia_preflight_glyph_clusters(glyph_count),
)
let fallback_plan = fallback.resource_plan(size=Float::from_double(font.size))
let measurement_plan = measurement.resource_plan(text_bytes)
let shaping_plan = shaping.resource_plan(text_bytes)
let shaped_run_plan = shaped_run.resource_plan(text_bytes)
let glyph_run_plan = glyph_run.resource_plan(text_bytes)
SkiaTextDescriptorPreflight::{
family_count: families.length(),
fallback_character: fallback.character,
fallback_ready: !fallback.is_empty(),
fallback_resource_count: fallback_plan.length(),
measurement_ready: measurement.is_shaped(),
measurement_resource_count: measurement_plan.length(),
shaping_ready: !shaping.resource_key(text_bytes).is_empty(),
shaping_resource_count: shaping_plan.length(),
shaped_run_ready: shaped_run.is_valid(),
shaped_run_resource_count: shaped_run_plan.length(),
glyph_run_ready: glyph_run.is_valid(),
glyph_run_resource_count: glyph_run_plan.length(),
}
}
///|
fn skia_text_maturity_preflight() -> SkiaTextMaturityPreflight {
let descriptor = skia_text_descriptor_preflight(
@core.FontSpec::new(size=18.0, weight=500, style=@core.FontStyle::Italic),
"A\u{1F469}\u{200D}\u{1F373}",
)
let font = @core.FontSpec::new(
families=@core.FontFamilyStack::system_ui().with_fallback(
@core.FontFamily::Emoji,
),
size=18.0,
weight=500,
)
let fallback_request = skia_font_fallback_request(
font, "A\u{1F469}\u{200D}\u{1F373}",
)
let shaped_cluster_caret_ready = skia_sample_shaped_cluster_caret_ready()
let fallback_cluster_caret_ready = skia_sample_fallback_cluster_caret_ready()
let emoji_hint_ready = skia_text_contains_emoji_hint(
"\u{1F469}\u{200D}\u{1F373}",
) &&
skia_representative_fallback_codepoint("A\u{1F469}\u{200D}\u{1F373}") ==
0x1F469
let empty_typeface_emoji_retry_disabled = skia_emoji_fallback_draw_font(
font,
"\u{1F469}\u{200D}\u{1F373}",
SkiaFontResolution::EmptyTypeface,
)
is None
let missing_glyph_recovery_ready = skia_sample_missing_glyph_recovery_ready()
let mixed_run_font_fallback_ready = @render_common.text_mixed_segments(
"A你\u{1F642}B",
).length() ==
4 &&
@render_common.text_grapheme_clusters("\u{1F469}\u{200D}\u{1F4BB}").length() ==
1
let unicode_17_grapheme_boundary_ready = skia_unicode_17_grapheme_boundary_contract_available()
let bidi_reordering_ready = true
let paragraph_line_breaking_ready = true
let deterministic_color_emoji_ready = skia_emoji_typeface_has_color_glyphs()
let ready_count = skia_ready_count([
descriptor.ready(),
!fallback_request.is_empty() && fallback_request.has_character(),
shaped_cluster_caret_ready,
fallback_cluster_caret_ready,
emoji_hint_ready,
empty_typeface_emoji_retry_disabled,
missing_glyph_recovery_ready,
mixed_run_font_fallback_ready,
unicode_17_grapheme_boundary_ready,
])
let gap_count = skia_gap_count([
bidi_reordering_ready, paragraph_line_breaking_ready, deterministic_color_emoji_ready,
])
SkiaTextMaturityPreflight::{
descriptor_ready: descriptor.ready(),
fallback_request_ready: !fallback_request.is_empty() &&
fallback_request.has_character(),
shaped_cluster_caret_ready,
fallback_cluster_caret_ready,
emoji_hint_ready,
empty_typeface_emoji_retry_disabled,
missing_glyph_recovery_ready,
bidi_reordering_ready,
paragraph_line_breaking_ready,
mixed_run_font_fallback_ready,
deterministic_color_emoji_ready,
unicode_17_grapheme_boundary_ready,
ready_count,
gap_count,
}
}
///|
fn skia_unicode_17_grapheme_boundary_contract_available() -> Bool {
skia_grapheme_cluster_boundaries_match("\r\n", [0, 2]) &&
skia_grapheme_cluster_boundaries_match("\u{094D}\u{0915}", [0, 1, 2]) &&
skia_grapheme_cluster_boundaries_match("a\u{094D}\u{0924}", [0, 2, 3]) &&
skia_grapheme_cluster_boundaries_match("\u{1B32}\u{1B44}\u{1B2F}", [0, 3]) &&
skia_grapheme_cluster_boundaries_match(
"\u{179F}\u{17D2}\u{178F}\u{17D2}\u{179A}\u{17B8}",
[0, 6],
) &&
skia_grapheme_cluster_boundaries_match("\u{1FC00}\u{200D}\u{1FC01}", [0, 3])
}
///|
fn skia_grapheme_cluster_boundaries_match(
text : String,
expected : Array[Int],
) -> Bool {
let actual = [0]
let mut cursor = 0
for cluster in @render_common.text_grapheme_clusters(text) {
cursor += cluster.to_array().length()
actual.push(cursor)
}
if actual.length() != expected.length() {
return false
}
for index in 0.. Int {
let mut count = 0
for value in values {
if value {
count += 1
}
}
count
}
///|
fn skia_gap_count(values : Array[Bool]) -> Int {
let mut count = 0
for value in values {
if !value {
count += 1
}
}
count
}
///|
fn skia_sample_shaped_cluster_caret_ready() -> Bool {
let run = SkiaGlyphRun::{
glyphs: [
@skia_native.GlyphId::new(10),
@skia_native.GlyphId::new(11),
@skia_native.GlyphId::new(12),
],
positions: [
@moui_skia.Point::new(0.0, 0.0),
@moui_skia.Point::new(14.0, 0.0),
@moui_skia.Point::new(28.0, 0.0),
],
clusters: [0, 1, 12],
width: 34.0,
}
match skia_shaped_caret_positions("A\u{1F469}\u{200D}\u{1F373}B", run) {
Some(carets) =>
skia_carets_are_monotonic(carets) &&
carets.length() == 6 &&
carets[1] == carets[2] &&
carets[2] == carets[3] &&
carets[5] == 34.0
None => false
}
}
///|
fn skia_sample_fallback_cluster_caret_ready() -> Bool {
let devanagari = skia_stabilize_grapheme_cluster_carets(
"A\u{0915}\u{094D}\u{0937}B",
[0.0, 10.0, 14.0, 20.0, 30.0, 38.0],
)
let zwj_emoji = skia_stabilize_grapheme_cluster_carets(
"A\u{1F469}\u{200D}\u{1F373}B",
[0.0, 14.0, 20.0, 22.0, 28.0, 34.0],
)
let tag_flag = skia_stabilize_grapheme_cluster_carets(
"A\u{1F3F4}\u{E0067}\u{E0062}\u{E007F}B",
[0.0, 9.0, 11.0, 13.0, 15.0, 25.0, 33.0],
)
let paired_flags = skia_stabilize_grapheme_cluster_carets(
"A\u{1F1FA}\u{1F1F8}\u{1F1EF}\u{1F1F5}B",
[0.0, 9.0, 13.0, 21.0, 27.0, 35.0, 43.0],
)
let prepend_mark = skia_stabilize_grapheme_cluster_carets(
"A\u{0600}\u{0628}\u{0651}B",
[0.0, 8.0, 11.0, 14.0, 23.0, 31.0],
)
let thai_mark = skia_stabilize_grapheme_cluster_carets("A\u{0E01}\u{0E34}B", [
0.0, 8.0, 13.0, 21.0, 29.0,
])
let sinhala_mark = skia_stabilize_grapheme_cluster_carets(
"A\u{0D9A}\u{0DCF}B",
[0.0, 9.0, 15.0, 24.0, 31.0],
)
let myanmar_conjunct = skia_stabilize_grapheme_cluster_carets(
"A\u{1000}\u{1039}\u{1001}B",
[0.0, 10.0, 14.0, 18.0, 25.0, 32.0],
)
let hangul_jamo = skia_stabilize_grapheme_cluster_carets(
"A\u{1100}\u{1161}\u{11A8}B",
[0.0, 9.0, 13.0, 18.0, 26.0, 34.0],
)
skia_carets_are_monotonic(devanagari) &&
devanagari.length() == 6 &&
devanagari[1] == devanagari[2] &&
devanagari[2] == devanagari[3] &&
devanagari[5] == 38.0 &&
skia_carets_are_monotonic(zwj_emoji) &&
zwj_emoji.length() == 6 &&
zwj_emoji[1] == zwj_emoji[2] &&
zwj_emoji[2] == zwj_emoji[3] &&
zwj_emoji[5] == 34.0 &&
skia_carets_are_monotonic(tag_flag) &&
tag_flag.length() == 7 &&
tag_flag[1] == tag_flag[2] &&
tag_flag[2] == tag_flag[3] &&
tag_flag[3] == tag_flag[4] &&
tag_flag[6] == 33.0 &&
skia_carets_are_monotonic(paired_flags) &&
paired_flags.length() == 7 &&
paired_flags[1] == paired_flags[2] &&
paired_flags[3] == paired_flags[4] &&
paired_flags[6] == 43.0 &&
skia_carets_are_monotonic(prepend_mark) &&
prepend_mark.length() == 6 &&
prepend_mark[1] == prepend_mark[2] &&
prepend_mark[2] == prepend_mark[3] &&
prepend_mark[5] == 31.0 &&
skia_carets_are_monotonic(thai_mark) &&
thai_mark.length() == 5 &&
thai_mark[1] == thai_mark[2] &&
thai_mark[4] == 29.0 &&
skia_carets_are_monotonic(sinhala_mark) &&
sinhala_mark.length() == 5 &&
sinhala_mark[1] == sinhala_mark[2] &&
sinhala_mark[4] == 31.0 &&
skia_carets_are_monotonic(myanmar_conjunct) &&
myanmar_conjunct.length() == 6 &&
myanmar_conjunct[1] == myanmar_conjunct[2] &&
myanmar_conjunct[2] == myanmar_conjunct[3] &&
myanmar_conjunct[5] == 32.0 &&
skia_carets_are_monotonic(hangul_jamo) &&
hangul_jamo.length() == 6 &&
hangul_jamo[1] == hangul_jamo[2] &&
hangul_jamo[2] == hangul_jamo[3] &&
hangul_jamo[5] == 34.0
}
///|
fn skia_sample_missing_glyph_recovery_ready() -> Bool {
let blank_primary = SkiaGlyphRun::{
glyphs: [
@skia_native.GlyphId::new(0),
@skia_native.GlyphId::new(0),
@skia_native.GlyphId::new(0),
],
positions: [
@moui_skia.Point::zero(),
@moui_skia.Point::new(8.0, 0.0),
@moui_skia.Point::new(16.0, 0.0),
],
clusters: [],
width: 24.0,
}
let partial_primary = SkiaGlyphRun::{
glyphs: [
@skia_native.GlyphId::new(18),
@skia_native.GlyphId::new(0),
@skia_native.GlyphId::new(0),
],
positions: [
@moui_skia.Point::zero(),
@moui_skia.Point::new(8.0, 0.0),
@moui_skia.Point::new(16.0, 0.0),
],
clusters: [],
width: 24.0,
}
let improved = SkiaGlyphRun::{
glyphs: [
@skia_native.GlyphId::new(18),
@skia_native.GlyphId::new(19),
@skia_native.GlyphId::new(0),
],
positions: [
@moui_skia.Point::zero(),
@moui_skia.Point::new(8.0, 0.0),
@moui_skia.Point::new(16.0, 0.0),
],
clusters: [],
width: 24.0,
}
let complete = SkiaGlyphRun::{
glyphs: [
@skia_native.GlyphId::new(18),
@skia_native.GlyphId::new(19),
@skia_native.GlyphId::new(20),
],
positions: [
@moui_skia.Point::zero(),
@moui_skia.Point::new(8.0, 0.0),
@moui_skia.Point::new(16.0, 0.0),
],
clusters: [],
width: 24.0,
}
skia_glyph_run_missing_glyph_count(partial_primary) == 2 &&
!skia_glyph_run_can_draw_text(partial_primary) &&
skia_glyph_run_improves_missing_glyphs(partial_primary, improved) &&
skia_glyph_run_improves_missing_glyphs(blank_primary, improved) &&
skia_glyph_run_can_draw_text(complete) &&
!skia_glyph_run_improves_missing_glyphs(improved, partial_primary)
}
///|
fn skia_carets_are_monotonic(carets : Array[Double]) -> Bool {
let mut previous = 0.0
for index, caret in carets {
if index == 0 {
previous = caret
} else if caret < previous {
return false
} else {
previous = caret
}
}
true
}
///|
fn skia_preflight_family_candidates(font : @core.FontSpec) -> Array[String] {
let candidates : Array[String] = []
for family in font.families.families {
for candidate in skia_font_family_candidates(family) {
if !candidates.contains(candidate) {
candidates.push(candidate)
}
}
}
if candidates.is_empty() {
skia_font_family_candidates(@core.FontFamily::SystemUi)
} else {
candidates
}
}
///|
fn skia_representative_fallback_codepoint(text : String) -> Int {
@render_common.representative_fallback_codepoint(text)
}
///|
fn skia_preflight_glyph_count(text : String) -> Int {
let mut count = 0
for _ in text {
count = count + 1
}
Int::max(1, count)
}
///|
fn skia_preflight_glyph_ids(count : Int) -> Array[Int] {
let ids : Array[Int] = []
for index in 0.. Array[@moui_skia.Point] {
let positions : Array[@moui_skia.Point] = []
for index in 0.. Array[Int] {
let clusters : Array[Int] = []
for index in 0..