///|
pub fn SunRasterRenderer::text_maturity_diagnostic(
self : SunRasterRenderer,
text? : String = "MoUI Sun text diagnostics",
font? : @core.FontSpec = @core.FontSpec::new(size=14.0),
max_width? : Double = 96.0,
) -> SunTextMaturityDiagnostic {
let system = self.text_system()
let layout = system.layout_paragraph(
@core.TextLayoutInput::new(text~, font~, max_width~),
)
let selection = sun_text_diagnostic_selection_range(layout)
let selection_rects = layout.selection_rects(selection)
let hit = sun_text_diagnostic_hit_test(layout, max_width~)
let metadata = layout.metadata
let registered_font_count = self.text_system.font_count()
let paragraph_layout_ready = metadata.paragraph_layout_available &&
layout.line_count() > 0 &&
layout.size.height > 0.0
let line_metrics_ready = metadata.line_metrics_available &&
layout.lines.length() == layout.line_count()
let selection_rects_ready = metadata.selection_rects_available &&
selection_rects.length() > 0
let hit_test_ready = metadata.hit_test_available && hit.is_inside
let color_emoji_ready = false
let unicode_17_grapheme_boundary_ready = true
let ready_count = sun_text_ready_count([
registered_font_count > 0,
paragraph_layout_ready,
line_metrics_ready,
selection_rects_ready,
hit_test_ready,
metadata.bidi_visual_order_ready,
unicode_17_grapheme_boundary_ready,
])
let gap_count = sun_text_gap_count([
metadata.bidi_visual_order_ready,
metadata.native_paragraph_ready,
color_emoji_ready,
])
{
text_system_id: system.id(),
registered_font_count,
paragraph_line_count: layout.line_count(),
selection_rect_count: selection_rects.length(),
hit_test_inside: hit.is_inside,
paragraph_layout_ready,
line_metrics_ready,
selection_rects_ready,
hit_test_ready,
bidi_visual_order_ready: metadata.bidi_visual_order_ready,
native_paragraph_ready: metadata.native_paragraph_ready,
color_emoji_ready,
unicode_17_grapheme_boundary_ready,
ready_count,
gap_count,
}
}
///|
pub fn SunRasterRenderer::emoji_font_fallback_diagnostic(
self : SunRasterRenderer,
text? : String = "\u{1F600}",
font? : @core.FontSpec = @core.FontSpec::new(
families=@core.FontFamilyStack::new([Emoji]),
size=32.0,
),
) -> SunEmojiFontFallbackDiagnostic {
let system = self.text_system()
let faces = self.resolved_faces(font)
let plan = @sun_text.plan_font_fallback(faces, text)
let spans = plan.get_spans()
let mut fallback_span_count = 0
for span in spans {
if !span.is_missing() && span.get_face_index() > 0 {
fallback_span_count += 1
}
}
let missing_char_count = plan.get_missing_char_count()
let covered_char_count = (plan.get_char_count() - missing_char_count).max(0)
let emoji_hint = sun_text_contains_emoji_hint(text)
let registered_font_fallback_ready = faces.length() > 1 &&
fallback_span_count > 0 &&
missing_char_count == 0
let missing_glyph_recovery_ready = emoji_hint &&
registered_font_fallback_ready &&
covered_char_count == plan.get_char_count()
let glyph_format = "coverage-mask"
let font_source = "sun-registered-font-chain"
let shaper = "font-fallback-plan"
let fallback_character = @render_common.representative_fallback_codepoint(
text,
skip_joiners=true,
)
let resolved_fallback_face_index = @sun_text.select_font_face_index_for_char(
faces, fallback_character,
)
let resolved_font_name = sun_resolved_face_label(resolved_fallback_face_index)
let fallback_language_tags = sun_font_fallback_language_tags(text)
let fallback_script_tag = sun_fallback_script_tag(fallback_language_tags)
let fallback_request_language_count = fallback_language_tags.length()
let glyph_count = covered_char_count
let cluster_count = @core.TextGraphemeBoundaries::new(text~).cluster_count()
let measured = system.measure_text(@core.TextLayoutInput::new(text~, font~))
let emoji_fallback_attempted = emoji_hint && faces.length() > 0
let fallback_request_ready = fallback_character > 0 &&
faces.length() > 0 &&
sun_first_font_family_label(font).length() > 0 &&
fallback_request_language_count == fallback_language_tags.length()
{
text,
text_system_id: system.id(),
requested_font_family: sun_first_font_family_label(font),
font_source,
shaper,
resolved_fallback_face_index,
resolved_font_name,
fallback_language_tags,
fallback_script_tag,
fallback_language_tag_count: fallback_language_tags.length(),
fallback_request_language_count,
registered_font_count: self.text_system.font_count(),
resolved_face_count: faces.length(),
span_count: plan.span_count(),
fallback_span_count,
missing_char_count,
covered_char_count,
emoji_hint,
registered_font_fallback_ready,
missing_glyph_recovery_ready,
deterministic_color_emoji_ready: false,
glyph_format,
glyph_key: sun_emoji_glyph_key(
font_source,
system.id(),
shaper,
resolved_fallback_face_index,
fallback_script_tag,
fallback_language_tags,
fallback_request_language_count,
sun_first_font_family_label(font),
self.text_system.font_count(),
faces.length(),
fallback_character,
glyph_format,
),
glyph_count,
cluster_count,
glyph_width: sun_positive_pixel_extent(measured.size.width),
glyph_height: sun_positive_pixel_extent(measured.size.height),
resolved_missing_glyph_count: missing_char_count,
fallback_character,
emoji_fallback_attempted,
fallback_request_ready,
}
}
///|
pub fn SunRasterRenderer::text_visual_order_diagnostic(
self : SunRasterRenderer,
text : String,
font? : @core.FontSpec = @core.FontSpec::new(size=20.0, weight=500),
max_width? : Double = 1000000000.0,
) -> SunTextVisualOrderDiagnostic? {
if text == "" {
return None
}
let layout = self
.text_system()
.layout_paragraph(@core.TextLayoutInput::new(text~, font~, max_width~))
let visual = layout.visual_order
Some({
logical_text: visual.logical_text,
visual_text: visual.visual_text,
logical_cluster_count: visual.logical_cluster_count,
visual_cluster_count: visual.visual_cluster_count,
segment_count: visual.segment_count,
glyph_count: visual.glyph_count,
width: layout.size.width,
visual_order_differs: visual.visual_order_differs,
bidi_visual_order_ready: visual.bidi_visual_order_ready,
})
}
///|
pub fn SunRasterRenderer::text_selection_geometry_diagnostic(
self : SunRasterRenderer,
text? : String = "MoUI Sun text diagnostics",
selection? : @core.TextRange? = None,
font? : @core.FontSpec = @core.FontSpec::new(size=14.0),
max_width? : Double = 96.0,
) -> SunTextSelectionGeometryDiagnostic {
let system = self.text_system()
let layout = system.layout_paragraph(
@core.TextLayoutInput::new(text~, font~, max_width~),
)
let range = match selection {
Some(selection) => selection.normalized()
None => sun_text_diagnostic_selection_range(layout)
}
let range = @core.normalize_grapheme_range(
@core.TextGraphemeBoundaries::new(text~),
range,
)
let selection_rects = layout.selection_rects(range)
let hit = sun_text_diagnostic_hit_test(layout, max_width~)
let bidi_ready = layout.metadata.bidi_visual_order_ready
let visual_order = layout.visual_order
{
text,
selection_range: range,
selection_rect_count: selection_rects.length(),
selected_line_count: sun_selected_line_count(layout, range),
line_count: layout.line_count(),
hit_test_offset: hit.offset,
hit_test_line_index: hit.line_index,
hit_test_inside: hit.is_inside,
bidi_visual_order_ready: bidi_ready,
visual_order_differs: visual_order.visual_order_differs,
visual_segment_count: visual_order.segment_count,
bidi_visual_order_observed: bidi_ready &&
visual_order.bidi_visual_order_ready &&
visual_order.visual_order_differs &&
visual_order.logical_cluster_count == visual_order.visual_cluster_count &&
visual_order.segment_count >= 3 &&
visual_order.glyph_count >= 1,
}
}
///|
fn sun_backend_text_maturity_summary() -> String {
let renderer = create_with_present_target(
@core.SurfaceMetrics::from_logical(
size=@core.Size::new(width=1.0, height=1.0),
),
SunPresentTarget::new(description="sun-backend-info-text-maturity", present=_ => {
true
}),
) catch {
err => return "Sun text maturity audit pending: \{err.message()}"
}
renderer.text_maturity_diagnostic(text="A A", max_width=12.0).summary()
}
///|
fn sun_backend_info_description(surface_summary : String) -> String {
"Sun CPU raster over moui_sun graphics/text, " +
surface_summary +
", " +
sun_backend_text_maturity_summary() +
", cached-layer diagnostics, async image lifecycle, structured unsupported-command diagnostics, and platform pixel presenters."
}
///|
fn sun_text_diagnostic_selection_range(
layout : @core.TextParagraphLayoutResult,
) -> @core.TextRange {
if layout.lines.length() > 1 {
@core.TextRange::new(
start=layout.lines[0].text_range.start,
end=layout.lines[1].text_range.end,
)
} else {
@core.TextRange::new(start=0, end=layout.text.to_array().length())
}
}
///|
fn sun_text_diagnostic_hit_test(
layout : @core.TextParagraphLayoutResult,
max_width~ : Double,
) -> @core.TextParagraphHitTestResult {
if layout.lines.is_empty() {
layout.hit_test(@core.Point::new(x=0.0, y=0.0))
} else {
let line = if layout.lines.length() > 1 {
layout.lines[1]
} else {
layout.lines[0]
}
layout.hit_test(
@core.Point::new(
x=(line.origin.x + line.size.width * 0.5).min(max_width),
y=line.origin.y + line.size.height * 0.5,
),
)
}
}
///|
fn sun_selected_line_count(
layout : @core.TextParagraphLayoutResult,
range : @core.TextRange,
) -> Int {
let normalized = range.normalized()
let mut count = 0
for line in layout.lines {
let line_range = line.text_range.normalized()
let overlaps = normalized.start < line_range.end &&
normalized.end > line_range.start
if overlaps ||
(
normalized.is_collapsed() &&
normalized.start >= line_range.start &&
normalized.start <= line_range.end
) {
count += 1
}
}
count
}
///|
fn sun_first_font_family_label(font : @core.FontSpec) -> String {
if font.families.families.length() == 0 {
return "system-ui"
}
sun_font_family_label(font.families.families[0])
}
///|
fn sun_font_family_label(family : @core.FontFamily) -> String {
match family {
Name(name) => name
_ => family.css_name()
}
}
///|
fn sun_text_contains_emoji_hint(text : String) -> Bool {
for ch in text {
if @render_common.codepoint_is_emoji_hint(ch.to_int()) {
return true
}
}
false
}
///|
fn sun_emoji_glyph_key(
font_source : String,
text_system_id : String,
shaper : String,
resolved_fallback_face_index : Int,
fallback_script_tag : String,
fallback_language_tags : Array[String],
fallback_request_language_count : Int,
requested_font_family : String,
registered_font_count : Int,
resolved_face_count : Int,
fallback_character : Int,
glyph_format : String,
) -> String {
let script_key = if fallback_script_tag == "" {
"script=und"
} else {
"script=\{fallback_script_tag}"
}
let language_key = if fallback_language_tags.is_empty() {
"lang-tags=none"
} else {
"lang-tags=\{fallback_language_tags.join("+")}"
}
"\{font_source}|\{text_system_id}|\{shaper}|resolved-face=\{resolved_fallback_face_index}|\{script_key}|langs=\{fallback_request_language_count}|\{language_key}|family=\{requested_font_family}|registered=\{registered_font_count}|resolved=\{resolved_face_count}|emoji-u+\{fallback_character}|format=\{glyph_format}"
}
///|
fn sun_resolved_face_label(face_index : Int) -> String {
if face_index < 0 {
"missing"
} else {
"sun-registered-face-\{face_index}"
}
}
///|
fn sun_positive_pixel_extent(value : Double) -> Int {
value.ceil().to_int().max(1)
}
///|
fn sun_font_fallback_language_tags(text : String) -> Array[String] {
let tags : Array[String] = []
let representative = @render_common.representative_fallback_codepoint(
text,
skip_joiners=true,
)
match sun_script_language_tag_for_codepoint(representative) {
Some(tag) => sun_push_language_tag(tags, tag)
None => ()
}
if representative > 0 {
sun_push_language_tag(tags, "en")
}
tags
}
///|
fn sun_fallback_script_tag(tags : Array[String]) -> String {
if tags.is_empty() {
""
} else {
tags[0]
}
}
///|
fn sun_script_language_tag_for_codepoint(codepoint : Int) -> String? {
if codepoint <= 0 {
None
} else if @render_common.codepoint_is_emoji_hint(codepoint) {
Some("und-Zsye")
} else if @render_common.codepoint_is_kana(codepoint) {
Some("ja")
} else if @render_common.codepoint_is_cjk_ideograph(codepoint) {
Some("zh-Hans")
} else if @render_common.codepoint_is_hangul_cluster_codepoint(codepoint) ||
@render_common.codepoint_is_hangul_compatibility_jamo(codepoint) {
Some("ko")
} else if sun_codepoint_is_hebrew_script(codepoint) {
Some("he")
} else if sun_codepoint_is_arabic_script(codepoint) {
Some("ar")
} else if sun_codepoint_is_devanagari_script(codepoint) {
Some("hi")
} else if sun_codepoint_is_thai_script(codepoint) {
Some("th")
} else if sun_codepoint_is_lao_script(codepoint) {
Some("lo")
} else if sun_codepoint_is_sinhala_script(codepoint) {
Some("si")
} else if sun_codepoint_is_khmer_script(codepoint) {
Some("km")
} else if sun_codepoint_is_myanmar_script(codepoint) {
Some("my")
} else if sun_codepoint_is_latin_script(codepoint) {
Some("en")
} else {
None
}
}
///|
fn sun_push_language_tag(tags : Array[String], tag : String) -> Unit {
if tag != "" && !tags.contains(tag) {
tags.push(tag)
}
}
///|
fn sun_codepoint_is_hebrew_script(codepoint : Int) -> Bool {
codepoint >= 0x0590 && codepoint <= 0x05FF
}
///|
fn sun_codepoint_is_arabic_script(codepoint : Int) -> Bool {
(codepoint >= 0x0600 && codepoint <= 0x08FF) ||
(codepoint >= 0xFB50 && codepoint <= 0xFDFF) ||
(codepoint >= 0xFE70 && codepoint <= 0xFEFF)
}
///|
fn sun_codepoint_is_devanagari_script(codepoint : Int) -> Bool {
codepoint >= 0x0900 && codepoint <= 0x097F
}
///|
fn sun_codepoint_is_thai_script(codepoint : Int) -> Bool {
codepoint >= 0x0E00 && codepoint <= 0x0E7F
}
///|
fn sun_codepoint_is_lao_script(codepoint : Int) -> Bool {
codepoint >= 0x0E80 && codepoint <= 0x0EFF
}
///|
fn sun_codepoint_is_sinhala_script(codepoint : Int) -> Bool {
codepoint >= 0x0D80 && codepoint <= 0x0DFF
}
///|
fn sun_codepoint_is_khmer_script(codepoint : Int) -> Bool {
codepoint >= 0x1780 && codepoint <= 0x17FF
}
///|
fn sun_codepoint_is_myanmar_script(codepoint : Int) -> Bool {
codepoint >= 0x1000 && codepoint <= 0x109F
}
///|
fn sun_codepoint_is_latin_script(codepoint : Int) -> Bool {
(codepoint >= 0x0041 && codepoint <= 0x005A) ||
(codepoint >= 0x0061 && codepoint <= 0x007A) ||
(codepoint >= 0x00C0 && codepoint <= 0x024F) ||
(codepoint >= 0x1E00 && codepoint <= 0x1EFF)
}
///|
fn sun_text_ready_count(values : Array[Bool]) -> Int {
let mut count = 0
for value in values {
if value {
count += 1
}
}
count
}
///|
fn sun_text_gap_count(values : Array[Bool]) -> Int {
let mut count = 0
for value in values {
if !value {
count += 1
}
}
count
}