// Copyright 2025 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
///|
/// Shaping primitives ported from `cosmic-text/src/shape.rs`.
pub(all) enum Shaping {
Basic
Advanced
}
///|
const SHAPE_WEIGHT_AXIS_TAG : UInt = 0x77676874U // "wght"
///|
pub struct ShapeGlyph {
start : Int
end : Int
x_advance : Float
y_advance : Float
x_offset : Float
y_offset : Float
/// Normalized ascent in em units.
ascent : Float
/// Normalized descent in em units.
descent : Float
font_id : Int
font_weight : Int
glyph_id : Int
color_opt : Color?
metadata : Int
cache_key_flags : CacheKeyFlags
text_decoration : TextDecoration
underline_metrics : DecorationMetrics
strikethrough_metrics : DecorationMetrics
/// BiDi embedding level (LTR if divisible by 2).
level : Int
/// Optional Metrics override (font size / line height) for this glyph's span.
metrics_opt : Metrics?
}
///|
pub fn ShapeGlyph::new(
start : Int,
end : Int,
x_advance : Float,
y_advance : Float,
x_offset : Float,
y_offset : Float,
font_id : Int,
glyph_id : Int,
metadata : Int,
) -> ShapeGlyph {
ShapeGlyph::{
start,
end,
x_advance,
y_advance,
x_offset,
y_offset,
ascent: 0.0F,
descent: 0.0F,
font_id,
font_weight: 400,
glyph_id,
color_opt: None,
metadata,
cache_key_flags: 0U,
text_decoration: TextDecoration::new(),
underline_metrics: DecorationMetrics::new(-0.125F, 1.0F / 14.0F),
strikethrough_metrics: DecorationMetrics::new(0.3F, 1.0F / 14.0F),
level: 0,
metrics_opt: None,
}
}
///|
pub fn ShapeGlyph::with_ascent_descent(
self : ShapeGlyph,
ascent : Float,
descent : Float,
) -> ShapeGlyph {
ShapeGlyph::{ ..self, ascent, descent }
}
///|
pub struct ShapeLine {
text : String
rtl : Bool
glyphs : Array[ShapeGlyph]
}
///|
pub fn ShapeLine::new(rtl : Bool, glyphs : Array[ShapeGlyph]) -> ShapeLine {
ShapeLine::{ text: "", rtl, glyphs }
}
///|
pub fn ShapeLine::empty() -> ShapeLine {
ShapeLine::{ text: "", rtl: false, glyphs: [] }
}
///|
fn is_tab(code : Int) -> Bool {
code == 9
}
///|
fn utf16_len_of_char(ch : Char) -> Int {
// UTF-16: codepoints outside BMP take 2 code units.
if ch.to_int() <= 0xFFFF {
1
} else {
2
}
}
///|
fn script_eq(a : @moon_swash.Script, b : @moon_swash.Script) -> Bool {
a.to_opentype() == b.to_opentype()
}
///|
fn shape_script_needs_fallback_stage(script : @moon_swash.Script) -> Bool {
match script {
@moon_swash.Script::Common
| @moon_swash.Script::Inherited
| @moon_swash.Script::Latin
| @moon_swash.Script::Unknown => false
_ => true
}
}
///|
fn shape_script_in_array(
scripts : Array[@moon_swash.Script],
script : @moon_swash.Script,
) -> Bool {
for s in scripts {
if script_eq(s, script) {
return true
}
}
false
}
///|
fn collect_run_fallback_scripts(
run_tokens : Array[@moon_swash.Token],
) -> Array[@moon_swash.Script] {
let scripts : Array[@moon_swash.Script] = []
for tok in run_tokens {
let script = @moon_swash.CharInfo::from_char(tok.ch()).properties().script()
if shape_script_needs_fallback_stage(script) &&
!shape_script_in_array(scripts, script) {
scripts.push(script)
}
}
scripts
}
///|
fn shape_primary_script_seed(
script : @moon_swash.Script,
) -> (@moon_swash.Script, Bool) {
match script {
Common | Inherited | Unknown => (@moon_swash.Script::Latin, true)
_ => (script, false)
}
}
///|
fn shape_levels_require_new_run(prev_level : Int, new_level : Int) -> Bool {
prev_level != new_level
}
///|
fn attrs_for_position(
attrs_list : AttrsList,
pos : Int,
text_len : Int,
) -> Attrs {
if text_len <= 0 {
attrs_list.defaults()
} else {
let i = if pos < 0 {
0
} else if pos >= text_len {
text_len - 1
} else {
pos
}
attrs_list.get_span(i)
}
}
///|
fn clamp_text_index(v : Int, text_len : Int) -> Int {
if v < 0 {
0
} else if v > text_len {
text_len
} else {
v
}
}
///|
fn slice_text_utf16(text : String, start : Int, end : Int) -> String {
let text_len = text.length()
let s = clamp_text_index(start, text_len)
let e = clamp_text_index(end, text_len)
let to = if e < s { s } else { e }
let sb = StringBuilder::new(size_hint=(to - s) * 2)
sb.write_view(text[:].view(start_offset=s, end_offset=to))
sb.to_string()
}
///|
fn attrs_list_for_utf16_range(
attrs_list : AttrsList,
start : Int,
end : Int,
text_len : Int,
) -> AttrsList {
let s = clamp_text_index(start, text_len)
let e0 = clamp_text_index(end, text_len)
let e = if e0 < s { s } else { e0 }
let (_, right) = attrs_list.split_off(s)
let (mid, _) = right.split_off(e - s)
mid
}
///|
fn reverse_glyphs(glyphs : Array[ShapeGlyph]) -> Unit {
if glyphs.length() < 2 {
return
}
let mut i = 0
let mut j = glyphs.length() - 1
while i < j {
let a = glyphs[i]
let b = glyphs[j]
glyphs.set(i, b)
glyphs.set(j, a)
i = i + 1
j = j - 1
}
}
///|
fn absf(v : Float) -> Float {
if v < 0.0F {
0.0F - v
} else {
v
}
}
///|
fn is_space_like_char(ch : Char) -> Bool {
if ch == '\t' {
return true
}
let cat = @moon_swash.CharInfo::from_char(ch).category()
cat is SpaceSeparator || cat is LineSeparator || cat is ParagraphSeparator
}
///|
fn glyph_is_blank(text : String, glyph : ShapeGlyph) -> Bool {
let start = clamp_text_index(glyph.start, text.length())
let end0 = clamp_text_index(glyph.end, text.length())
let end = if end0 < start { start } else { end0 }
if end <= start {
return false
}
let run = slice_text_utf16(text, start, end)
let mut has_char = false
for ch in run {
has_char = true
if !is_space_like_char(ch) {
return false
}
}
has_char
}
///|
fn normalize_rtl_mark_clusters(
glyphs : Array[ShapeGlyph],
run_rtl : Bool,
) -> Unit {
if !run_rtl || glyphs.length() < 2 {
return
}
let mut i = 0
while i < glyphs.length() {
let start = glyphs[i].start
let end = glyphs[i].end
let mut j = i + 1
while j < glyphs.length() &&
glyphs[j].start == start &&
glyphs[j].end == end {
j = j + 1
}
if j - i > 1 {
let mut base_adv = 0.0F
let bases : Array[ShapeGlyph] = []
let marks : Array[ShapeGlyph] = []
for k in i.. base_adv {
base_adv = g.x_advance
}
}
}
if base_adv != 0.0F && marks.length() > 0 {
// swash mark offsets in RTL clusters are relative to cluster end;
// normalize to cluster start semantics used by reference shaping.
for m in 0.. marks[a].x_offset {
let ga = marks[a]
let gb = marks[b]
marks.set(a, gb)
marks.set(b, ga)
}
b = b + 1
}
a = a + 1
}
let mut p = i
for g in bases {
glyphs.set(p, g)
p = p + 1
}
for g in marks {
glyphs.set(p, g)
p = p + 1
}
}
}
i = j
}
}
///|
priv struct RunGlyphChunk {
blank : Bool
glyphs : Array[ShapeGlyph]
}
///|
fn reorder_run_glyphs_for_bidi(
text : String,
run_glyphs : Array[ShapeGlyph],
line_rtl : Bool,
run_rtl : Bool,
) -> Array[ShapeGlyph] {
if run_glyphs.length() < 2 {
return run_glyphs
}
let chunks : Array[RunGlyphChunk] = []
for glyph in run_glyphs {
let blank = glyph_is_blank(text, glyph)
if !blank && chunks.length() > 0 && !chunks[chunks.length() - 1].blank {
chunks[chunks.length() - 1].glyphs.push(glyph)
} else {
chunks.push(RunGlyphChunk::{ blank, glyphs: [glyph] })
}
}
// Swash emits RTL clusters in logical order while reference HarfBuzz stream
// is visual. Adjust per-word glyph direction first, then apply word-order flip.
let need_reverse_word_glyphs = if run_rtl { !line_rtl } else { line_rtl }
if need_reverse_word_glyphs {
for i in 0.. Bool {
for m in missing {
if m == start {
return true
}
}
false
}
///|
fn remove_missing_range(missing : Array[Int], start : Int, end : Int) -> Unit {
let keep : Array[Int] = []
for m in missing {
if m < start || m >= end {
keep.push(m)
}
}
missing.clear()
for m in keep {
missing.push(m)
}
}
///|
fn replace_cluster_glyphs(
run_glyphs : Array[ShapeGlyph],
start : Int,
end : Int,
replacement : Array[ShapeGlyph],
) -> Array[ShapeGlyph] {
let out : Array[ShapeGlyph] = []
let mut inserted = false
for g in run_glyphs {
let in_range = g.start >= start && g.end <= end
if in_range && !inserted {
for rg in replacement {
out.push(rg)
}
inserted = true
}
if !in_range {
out.push(g)
}
}
if !inserted {
for rg in replacement {
out.push(rg)
}
}
out
}
///|
fn merge_fallback_glyphs(
run_glyphs : Array[ShapeGlyph],
missing : Array[Int],
fallback_glyphs : Array[ShapeGlyph],
fallback_missing : Array[Int],
) -> Array[ShapeGlyph] {
let mut merged = run_glyphs
let mut fb_i = 0
while fb_i < fallback_glyphs.length() {
let start = fallback_glyphs[fb_i].start
let end = fallback_glyphs[fb_i].end
if !missing_contains(missing, start) ||
missing_contains(fallback_missing, start) {
fb_i = fb_i + 1
continue
}
let replacement : Array[ShapeGlyph] = []
while fb_i < fallback_glyphs.length() {
let g = fallback_glyphs[fb_i]
if g.start >= start && g.end <= end {
replacement.push(g)
fb_i = fb_i + 1
} else {
break
}
}
remove_missing_range(missing, start, end)
merged = replace_cluster_glyphs(merged, start, end, replacement)
}
merged
}
///|
fn shape_clamp_double(v : Double, lo : Double, hi : Double) -> Double {
if v < lo {
lo
} else if v > hi {
hi
} else {
v
}
}
///|
fn shape_variation_settings_for_font(
font : @moon_swash.FontRef,
attrs : Attrs,
) -> Array[@moon_swash.VariationSetting] {
let settings : Array[@moon_swash.VariationSetting] = []
match font.variations().find_by_tag(SHAPE_WEIGHT_AXIS_TAG) {
None => settings
Some(axis) => {
let w = attrs.weight_value().value().to_double()
let value = shape_clamp_double(w, axis.min_value(), axis.max_value())
settings.push(@moon_swash.Setting::Setting(SHAPE_WEIGHT_AXIS_TAG, value))
settings
}
}
}
///|
fn hb_tag_from_swash_tag(tag : UInt) -> @common.Tag {
@common.Tag::from_bytes(
((tag >> 24) & 0xFFU).to_byte(),
((tag >> 16) & 0xFFU).to_byte(),
((tag >> 8) & 0xFFU).to_byte(),
(tag & 0xFFU).to_byte(),
)
}
///|
fn hb_script_from_swash_script(script : @moon_swash.Script) -> @common.Script {
@common.Script::from_iso15924_tag(hb_tag_from_swash_tag(script.to_opentype()))
}
///|
fn hb_direction_from_run_rtl(run_rtl : Bool) -> @common.Direction {
if run_rtl {
@common.direction_rtl
} else {
@common.direction_ltr
}
}
///|
fn shape_harfbuzz_variations(
font : @moon_swash.FontRef,
attrs : Attrs,
) -> Array[@var.AxisCoord] {
let axis_coords : Array[@var.AxisCoord] = []
let settings = shape_variation_settings_for_font(font, attrs)
for setting in settings {
axis_coords.push(
@var.AxisCoord::new(hb_tag_from_swash_tag(setting.tag), setting.value),
)
}
axis_coords
}
///|
fn shape_run_with_harfbuzz(
font_system : FontSystem,
entry : FontEntry,
attrs_list : AttrsList,
levels : Array[Int],
text_len : Int,
line_rtl : Bool,
run_rtl : Bool,
script : @moon_swash.Script,
run_tokens : Array[@moon_swash.Token],
run_attrs : Attrs,
font_id : Int,
font_ascent : Float,
font_descent : Float,
underline_metrics : DecorationMetrics,
strikethrough_metrics : DecorationMetrics,
) -> (Array[ShapeGlyph], Array[Int])? {
if run_tokens.length() == 0 {
return Some(([], []))
}
let mut source_start = run_tokens[0].offset().reinterpret_as_int()
let mut source_end = source_start + run_tokens[0].len().reinterpret_as_int()
for t in run_tokens {
let s = t.offset().reinterpret_as_int()
let e = s + t.len().reinterpret_as_int()
if s < source_start {
source_start = s
}
if e > source_end {
source_end = e
}
}
if source_end < source_start {
source_end = source_start
}
let upem = entry.font.metrics([]).units_per_em.to_int()
if upem <= 0 {
return None
}
let hb_font = font_system.hb_font_for_entry(entry)
hb_font.set_scale(upem, upem)
let axis_coords = shape_harfbuzz_variations(entry.font, run_attrs)
match hb_font.set_variations(axis_coords) {
Err(_) => ()
Ok(_) => ()
}
let hb_buffer = font_system.shape_hb_buffer()
hb_buffer.clear()
hb_buffer.set_direction(hb_direction_from_run_rtl(run_rtl))
hb_buffer.set_script(hb_script_from_swash_script(script))
hb_buffer.set_language(@common.Language::from_string("und"))
for t in run_tokens {
let ch = if t.ch() == '\t' { ' ' } else { t.ch() }
hb_buffer.add_codepoint(
ch.to_uint(),
cluster=t.offset().reinterpret_as_int() - source_start,
)
}
match @hb_shape.shape(hb_font, hb_buffer, shapers=["ot"]) {
Err(_) => return None
Ok(_) => ()
}
let infos = hb_buffer.infos()
let positions = hb_buffer.positions()
if infos.length() != positions.length() {
return None
}
let upem_d = upem.to_double()
let out : Array[ShapeGlyph] = []
let missing : Array[Int] = []
for i in 0.. source_end {
source_end
} else {
start0
}
let attrs = attrs_for_position(attrs_list, start, text_len)
let lev = if levels.get(start) is Some(v) {
v
} else if line_rtl {
1
} else {
0
}
if info.codepoint == 0U {
missing.push(start)
}
let letter_spacing = attrs_letter_spacing(attrs)
out.push(ShapeGlyph::{
start,
end: source_end,
x_advance: Float::from_double(pos.x_advance.to_double() / upem_d) +
letter_spacing,
y_advance: Float::from_double(pos.y_advance.to_double() / upem_d),
x_offset: Float::from_double(pos.x_offset.to_double() / upem_d),
y_offset: Float::from_double(pos.y_offset.to_double() / upem_d),
ascent: font_ascent,
descent: font_descent,
font_id,
font_weight: attrs.weight_value().value(),
glyph_id: info.codepoint.reinterpret_as_int(),
color_opt: attrs.color_opt_value(),
metadata: attrs.metadata(),
cache_key_flags: attrs.cache_key_flags_value(),
text_decoration: attrs.text_decoration_value(),
underline_metrics,
strikethrough_metrics,
level: lev,
metrics_opt: attrs.metrics_opt(),
})
}
let use_rtl_end_adjust = if out.length() > 1 {
out[0].start > out[out.length() - 1].start
} else {
run_rtl
}
if use_rtl_end_adjust {
for i in 1.. 1 {
let mut i = out.length() - 1
while i > 0 {
let next_start = out[i].start
let next_end = out[i].end
let g = out[i - 1]
out.set(i - 1, ShapeGlyph::{
..g,
end: if g.start == next_start {
next_end
} else {
next_start
},
})
i = i - 1
}
}
Some((out, missing))
}
///|
fn attrs_letter_spacing(attrs : Attrs) -> Float {
match attrs.letter_spacing_opt() {
None => 0.0F
Some(v) => v
}
}
///|
fn shape_run_missing_tokens(
run_tokens : Array[@moon_swash.Token],
) -> Array[Int] {
let missing : Array[Int] = []
for t in run_tokens {
let s = t.offset().reinterpret_as_int()
if !missing_contains(missing, s) {
missing.push(s)
}
}
missing
}
///|
fn shape_run_with_font(
font_system : FontSystem,
attrs_list : AttrsList,
levels : Array[Int],
text_len : Int,
line_rtl : Bool,
run_rtl : Bool,
script : @moon_swash.Script,
run_tokens : Array[@moon_swash.Token],
font_id : Int,
) -> (Array[ShapeGlyph], Array[Int]) {
let entry = match font_system.get_font_entry(font_id) {
None => return ([], shape_run_missing_tokens(run_tokens))
Some(v) => v
}
let (font_ascent, font_descent, underline_metrics, strikethrough_metrics) = {
let metrics = entry.font.metrics([])
let upem = metrics.units_per_em.to_int()
if upem <= 0 {
(
0.0F,
0.0F,
DecorationMetrics::new(-0.125F, 1.0F / 14.0F),
DecorationMetrics::new(0.3F, 1.0F / 14.0F),
)
} else {
let upem_d = upem.to_double()
let scale = 1.0 / upem_d
let stroke = if metrics.stroke_size == 0.0 {
1.0 / 14.0
} else {
metrics.stroke_size / upem_d
}
(
Float::from_double(metrics.ascent * scale),
Float::from_double(metrics.descent * scale),
DecorationMetrics::new(
Float::from_double(metrics.underline_offset / upem_d),
Float::from_double(stroke),
),
DecorationMetrics::new(
Float::from_double(metrics.strikeout_offset / upem_d),
Float::from_double(stroke),
),
)
}
}
let run_attrs = if run_tokens.length() == 0 {
attrs_list.defaults()
} else {
attrs_for_position(
attrs_list,
run_tokens[0].offset().reinterpret_as_int(),
text_len,
)
}
match
shape_run_with_harfbuzz(
font_system, entry, attrs_list, levels, text_len, line_rtl, run_rtl, script,
run_tokens, run_attrs, font_id, font_ascent, font_descent, underline_metrics,
strikethrough_metrics,
) {
None => ([], shape_run_missing_tokens(run_tokens))
Some(ret) => ret
}
}
///|
/// Build a shaped line from text and attributes list.
pub fn ShapeLine::build(
_self : ShapeLine,
text : String,
attrs_list : AttrsList,
shaping : Shaping,
tab_width : Int,
) -> ShapeLine {
let bidi_info = if shaping is Advanced {
BidiInfo::new(text)
} else {
BidiInfo::new_with_para_level(text, Some(0))
}
let rtl = bidi_info.rtl()
let levels = bidi_info.adjusted_levels()
let glyphs : Array[ShapeGlyph] = []
let len = text.length()
for i in 0.. ShapeLine {
if shaping is Basic {
return ShapeLine::build(
ShapeLine::empty(),
text,
attrs_list,
shaping,
tab_width,
)
}
let default_font_id_opt = font_system.resolve(attrs_list.defaults())
let default_font_id = match default_font_id_opt {
None =>
return ShapeLine::build(
ShapeLine::empty(),
text,
attrs_list,
shaping,
tab_width,
)
Some(id) => id
}
if font_system.get_font(default_font_id) is None {
return ShapeLine::build(
ShapeLine::empty(),
text,
attrs_list,
shaping,
tab_width,
)
}
// Use swash's shaping engine for clusters/ligatures/marks. We intentionally use
// size=1.0 so the returned advances/offsets are in "em" units (like upstream),
// which are later multiplied by `font_size` during layout.
let bidi_info = BidiInfo::new(text)
let rtl = bidi_info.rtl()
let levels = bidi_info.adjusted_levels()
let runs : Array[
(
@moon_swash.Script,
Array[@moon_swash.Script],
Bool,
Array[@moon_swash.Token],
),
] = []
let mut off = 0U
let mut cur_primary_script = @moon_swash.Script::Latin
let mut cur_primary_pending = true
let mut cur_level = if rtl { 1 } else { 0 }
let mut cur_rtl = rtl
let mut cur_scripts : Array[@moon_swash.Script] = []
let mut cur_tokens : Array[@moon_swash.Token] = []
for ch0 in text {
let script0 = @moon_swash.CharInfo::from_char(ch0).properties().script()
let len_u = utf16_len_of_char(ch0).reinterpret_as_uint()
let i = off.reinterpret_as_int()
let level_i = if levels.get(i) is Some(v) { v } else if rtl { 1 } else { 0 }
let run_rtl = level_i % 2 != 0
// Tabs are shaped as spaces (upstream behavior).
let ch = if ch0 == '\t' { ' ' } else { ch0 }
let info = @moon_swash.CharInfo::from_char(ch)
let tok = @moon_swash.Token::Token(ch, off, len_u, info, off)
let primary = shape_primary_script_seed(script0)
if cur_tokens.length() == 0 {
cur_primary_script = primary.0
cur_primary_pending = primary.1
cur_level = level_i
cur_rtl = run_rtl
cur_scripts = []
} else if shape_levels_require_new_run(cur_level, level_i) {
runs.push((cur_primary_script, cur_scripts, cur_rtl, cur_tokens))
cur_tokens = []
cur_primary_script = primary.0
cur_primary_pending = primary.1
cur_level = level_i
cur_rtl = run_rtl
cur_scripts = []
} else if cur_primary_pending &&
!(script0 is Common || script0 is Inherited || script0 is Unknown) {
cur_primary_script = script0
cur_primary_pending = false
}
if shape_script_needs_fallback_stage(script0) &&
!shape_script_in_array(cur_scripts, script0) {
cur_scripts.push(script0)
}
cur_tokens.push(tok)
off = off + len_u
}
let glyphs : Array[ShapeGlyph] = []
if cur_tokens.length() != 0 {
runs.push((cur_primary_script, cur_scripts, cur_rtl, cur_tokens))
}
for run in runs {
let script = run.0
let run_scripts = if run.1.length() == 0 {
collect_run_fallback_scripts(run.3)
} else {
run.1
}
let run_rtl = run.2
let run_tokens = run.3
let run_start = if run_tokens.length() == 0 {
0
} else {
run_tokens[0].offset().reinterpret_as_int()
}
let run_end = if run_tokens.length() == 0 {
run_start
} else {
let last = run_tokens[run_tokens.length() - 1]
(last.offset() + last.len()).reinterpret_as_int()
}
let cache_key = ShapeRunKey::new(
slice_text_utf16(text, run_start, run_end),
attrs_list_for_utf16_range(attrs_list, run_start, run_end, text.length()),
)
if font_system.shape_run_cache.get(cache_key) is Some(cache_glyphs) {
for g in cache_glyphs {
glyphs.push(ShapeGlyph::{
..g,
start: g.start + run_start,
end: g.end + run_start,
})
}
continue
}
let run_attrs = attrs_for_position(attrs_list, run_start, text.length())
let run_codepoints : Array[UInt] = []
for t in run_tokens {
run_codepoints.push(t.ch().to_int().reinterpret_as_uint())
}
let fallback_iter = font_fallback_iter_init(
font_system, run_attrs, run_scripts, run_codepoints,
)
let run_font_id = match font_fallback_next(font_system, fallback_iter) {
None => default_font_id
Some(id) => id
}
let (default_glyphs, default_missing) = shape_run_with_font(
font_system,
attrs_list,
levels,
text.length(),
rtl,
run_rtl,
script,
run_tokens,
run_font_id,
)
let mut run_glyphs = default_glyphs
let missing = default_missing
if missing.length() > 0 {
while true {
let alt_font_id = match font_fallback_next(font_system, fallback_iter) {
None => break
Some(id) => if id == run_font_id { continue } else { id }
}
let (fb_glyphs, fb_missing) = shape_run_with_font(
font_system,
attrs_list,
levels,
text.length(),
rtl,
run_rtl,
script,
run_tokens,
alt_font_id,
)
run_glyphs = merge_fallback_glyphs(
run_glyphs, missing, fb_glyphs, fb_missing,
)
if missing.length() == 0 {
break
}
}
}
match
font_fallback_check_missing(
font_system,
fallback_iter,
slice_text_utf16(text, run_start, run_end),
) {
None => ()
Some(info) => font_fallback_emit_missing_warning(font_system, info)
}
run_glyphs = reorder_run_glyphs_for_bidi(text, run_glyphs, rtl, run_rtl)
let cached_run_glyphs : Array[ShapeGlyph] = []
for g in run_glyphs {
glyphs.push(g)
cached_run_glyphs.push(ShapeGlyph::{
..g,
start: g.start - run_start,
end: g.end - run_start,
})
}
font_system.shape_run_cache.insert(cache_key, cached_run_glyphs)
}
// If shaping produces no glyphs, fall back to the simple shaper to keep layout stable.
if glyphs.length() == 0 {
return ShapeLine::build(
ShapeLine::empty(),
text,
attrs_list,
shaping,
tab_width,
)
}
// Apply tab stops using each tab glyph's own advance as the tab unit.
let mut x = 0.0
for i in 0..= 0 &&
g0.start < text.length() &&
is_tab(text.code_unit_at(g0.start).to_int())
if is_tab_glyph {
let step = g0.x_advance.to_double() * tab_width.to_double()
if step != 0.0 {
let next = ((x / step).floor() + 1.0) * step
let adv = next - x
let g1 = ShapeGlyph::{ ..g0, x_advance: Float::from_double(adv) }
glyphs.set(i, g1)
x = next
} else {
x = x + g0.x_advance.to_double()
}
} else {
x = x + g0.x_advance.to_double()
}
}
ShapeLine::{ text, rtl, glyphs }
}
///|
pub fn ShapeGlyph::width(self : ShapeGlyph, font_size : Float) -> Float {
let glyph_font_size = match self.metrics_opt {
None => font_size
Some(m) => m.font_size
}
glyph_font_size * self.x_advance
}
///|
pub fn ShapeLine::width(self : ShapeLine, font_size : Float) -> Float {
let mut width = 0.0F
for glyph in self.glyphs {
width = width + glyph.width(font_size)
}
width
}
///|
fn shape_line_source_text(line : ShapeLine) -> String {
if line.text.length() > 0 {
return line.text
}
let mut max_end = 0
for g in line.glyphs {
if g.end > max_end {
max_end = g.end
}
}
let sb = StringBuilder::new(size_hint=max_end)
for _ in 0.. LayoutLine {
if line.glyphs.length() == 0 {
return line
}
let glyphs : Array[LayoutGlyph] = []
let mut x = roundf(line.glyphs[0].x)
let start_x = x
for g in line.glyphs {
let w = roundf(g.w)
glyphs.push(LayoutGlyph::{ ..g, x, w })
x = x + w
}
LayoutLine::{ ..line, w: x - start_x, glyphs }
}
///|
fn shape_max_layout_width(lines : Array[LayoutLine]) -> Float? {
if lines.length() == 0 {
return None
}
let mut max_w = lines[0].w
for line in lines {
if line.w > max_w {
max_w = line.w
}
}
Some(max_w)
}
///|
fn shape_postprocess_layout(
text : String,
lines0 : Array[LayoutLine],
rtl : Bool,
align_opt : Align?,
width_opt : Float?,
hinting : Hinting,
) -> Array[LayoutLine] {
let mut lines = lines0
let default_align = if rtl { Align::Right } else { Align::Left }
let align = if align_opt is Some(a) { a } else { default_align }
let line_width_opt = if width_opt is Some(width) {
Some(width)
} else {
shape_max_layout_width(lines)
}
if line_width_opt is Some(width) {
lines = apply_align_with_rtl(text, lines, width, align, rtl)
}
if hinting is Hinting::Enabled {
let hinted : Array[LayoutLine] = []
for line in lines {
hinted.push(shape_hint_layout_line(line))
}
lines = hinted
}
lines
}
///|
pub fn ShapeLine::layout(
self : ShapeLine,
font_size : Float,
width_opt : Float?,
wrap : Wrap,
ellipsize : Ellipsize,
align : Align?,
match_mono_width : Float?,
hinting : Hinting,
) -> Array[LayoutLine] {
let cell_w = if match_mono_width is Some(w) { w } else { font_size }
let text = shape_line_source_text(self)
let lines0 = layout_from_shape(text, self, font_size, cell_w, width_opt, wrap)
let lines = apply_ellipsize(lines0, ellipsize, wrap, width_opt, font_size)
shape_postprocess_layout(text, lines, self.rtl, align, width_opt, hinting)
}
///|
pub fn ShapeLine::layout_to_buffer(
self : ShapeLine,
font_size : Float,
width_opt : Float?,
wrap : Wrap,
ellipsize : Ellipsize,
align : Align?,
layout_lines : Array[LayoutLine],
match_mono_width : Float?,
hinting : Hinting,
) -> Unit {
layout_lines.clear()
for
line in self.layout(
font_size, width_opt, wrap, ellipsize, align, match_mono_width, hinting,
) {
layout_lines.push(line)
}
}