// 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.
///|
/// Minimal shaping port scaffold from `cosmic-text/src/shape.rs`.
///
/// This MVP produces 1 glyph per UTF-16 code unit and does not perform script runs,
/// harfbuzz shaping, BiDi, or font fallback.
pub(all) enum Shaping {
Basic
Advanced
}
///|
pub struct ShapeGlyph {
start : Int
end : Int
x_advance : Float
y_advance : Float
x_offset : Float
y_offset : Float
font_id : Int
font_weight : Int
glyph_id : Int
metadata : Int
/// 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,
font_id,
font_weight: 400,
glyph_id,
metadata,
level: 0,
metrics_opt: None,
}
}
///|
pub struct ShapeLine {
rtl : Bool
glyphs : Array[ShapeGlyph]
}
///|
pub fn ShapeLine::new(rtl : Bool, glyphs : Array[ShapeGlyph]) -> ShapeLine {
ShapeLine::{ rtl, glyphs }
}
///|
pub fn ShapeLine::empty() -> ShapeLine {
ShapeLine::{ 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 bidi_is_strong_rtl(b : @moon_swash.BidiClass) -> Bool {
match b {
R | AL | AN | RLE | RLO | RLI => true
_ => false
}
}
///|
fn bidi_is_strong_ltr(b : @moon_swash.BidiClass) -> Bool {
match b {
L | LRE | LRO | LRI => true
_ => false
}
}
///|
fn detect_base_rtl(text : String) -> Bool {
// Heuristic: use the first strong directional character.
for ch in text {
let props = @moon_swash.CharInfo::from_char(ch).properties()
let bidi = props.bidi_class()
if bidi_is_strong_rtl(bidi) {
return true
}
if bidi_is_strong_ltr(bidi) {
return false
}
}
false
}
///|
fn compute_bidi_levels(text : String, rtl : Bool) -> Array[Int] {
let base = if rtl { 1 } else { 0 }
let levels : Array[Int] = Array::makei(text.length(), _ => base)
for p in text.iter2() {
let idx = p.0
let ch = p.1
let props = @moon_swash.CharInfo::from_char(ch).properties()
let bidi = props.bidi_class()
let lev = if bidi_is_strong_rtl(bidi) {
1
} else if bidi_is_strong_ltr(bidi) {
if rtl {
2
} else {
0
}
} else {
base
}
let end = idx + ch.utf16_len()
let mut i = idx
while i < end && i < levels.length() {
levels.set(i, lev)
i = i + 1
}
}
levels
}
///|
fn script_eq(a : @moon_swash.Script, b : @moon_swash.Script) -> Bool {
a.to_opentype() == b.to_opentype()
}
///|
/// Build a shaped line from text and attributes list.
///
/// MVP notes:
/// - 1 code unit -> 1 glyph
/// - `glyph_id` is the code unit value
/// - `x_advance` is measured in "cells": 1.0 for normal chars, `tab_width` for tab.
pub fn ShapeLine::build(
_self : ShapeLine,
text : String,
attrs_list : AttrsList,
shaping : Shaping,
tab_width : Int,
) -> ShapeLine {
let rtl = shaping is Advanced && detect_base_rtl(text)
let levels = compute_bidi_levels(text, rtl)
let glyphs : Array[ShapeGlyph] = []
let len = text.length()
for i in 0.. 1 glyph),
/// but uses `moon_swash` charmap + glyph metrics so advances are font-derived.
pub fn ShapeLine::build_with_font_system(
_self : ShapeLine,
font_system : FontSystem,
text : String,
attrs_list : AttrsList,
shaping : Shaping,
tab_width : Int,
) -> 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
}
let default_font_opt = font_system.get_font(default_font_id)
let default_font = match default_font_opt {
None =>
return ShapeLine::build(
ShapeLine::empty(),
text,
attrs_list,
shaping,
tab_width,
)
Some(f) => f
}
// 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 rtl = detect_base_rtl(text)
let levels = compute_bidi_levels(text, rtl)
let direction = if rtl {
@shape.Direction::RightToLeft
} else {
@shape.Direction::LeftToRight
}
let cx = @shape.ShapeContext::new()
let empty_features : Array[@moon_swash.Setting[UInt16]] = []
let empty_variations : Array[@moon_swash.Setting[Double]] = []
let default_cmap = @moon_swash.Charmap::from_font(default_font)
let runs : Array[(Int, @moon_swash.Script, Array[@moon_swash.Token])] = []
let mut off = 0U
let mut cur_script = @moon_swash.Script::Latin
let mut cur_font_id = default_font_id
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 attrs = attrs_list.get_span(i)
// Tabs are shaped as spaces (upstream behavior).
let ch = if ch0 == '\t' { ' ' } else { ch0 }
let cp = ch.to_int().reinterpret_as_uint()
let font_id = match font_system.resolve_for_codepoint(attrs, cp) {
None => default_font_id
Some(id) => id
}
let info = @moon_swash.CharInfo::from_char(ch)
let tok = @moon_swash.Token::new(
ch,
off,
len_u,
info,
attrs.metadata().reinterpret_as_uint(),
)
let effective = match script0 {
Common | Inherited | Unknown =>
if cur_tokens.length() == 0 {
@moon_swash.Script::Latin
} else {
cur_script
}
_ => script0
}
if cur_tokens.length() == 0 {
cur_script = effective
cur_font_id = font_id
} else if !script_eq(effective, cur_script) || font_id != cur_font_id {
runs.push((cur_font_id, cur_script, cur_tokens))
cur_tokens = []
cur_script = effective
cur_font_id = font_id
}
cur_tokens.push(tok)
off = off + len_u
}
let glyphs : Array[ShapeGlyph] = []
if cur_tokens.length() != 0 {
runs.push((cur_font_id, cur_script, cur_tokens))
}
let cluster = @moon_swash.CharCluster::new()
for run in runs {
let font_id = run.0
let script = run.1
let run_tokens = run.2
let font_opt = font_system.get_font(font_id)
let font = match font_opt {
None => continue
Some(f) => f
}
let cmap = @moon_swash.Charmap::from_font(font)
let shaper = cx
.builder(font)
.script(script)
.direction(direction)
.language(None)
.size(1.0)
.features(empty_features.iter())
.variations(empty_variations.iter())
.build()
let parser = @moon_swash.Parser::new(script, run_tokens.iter())
while parser.next(cluster) {
cluster.map(fn(ch) { cmap.map(ch.to_int().reinterpret_as_uint()) })
|> ignore
shaper.add_cluster(cluster)
}
shaper.shape_with(fn(gc) {
let start = gc.source().start().reinterpret_as_int()
let end = gc.source().end().reinterpret_as_int()
let attrs = attrs_list.get_span(start)
let meta = gc.data().reinterpret_as_int()
let lev = if levels.get(start) is Some(v) {
v
} else if rtl {
1
} else {
0
}
for g in gc.glyphs() {
glyphs.push(ShapeGlyph::{
start,
end,
x_advance: Float::from_double(g.advance()),
y_advance: 0.0F,
x_offset: Float::from_double(g.x()),
y_offset: Float::from_double(g.y()),
font_id,
font_weight: attrs.weight_value().value(),
glyph_id: g.id().to_int(),
metadata: meta,
level: lev,
metrics_opt: attrs.metrics_opt(),
})
}
})
}
// 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 by snapping to the next multiple of (space_advance * tab_width).
let gm = default_font.glyph_metrics([])
let upem = gm.units_per_em().to_int().to_double()
let space_gid = default_cmap.map(32U)
let space_adv = if upem == 0.0 {
0.0
} else {
gm.advance_width(space_gid) / upem
}
let step = space_adv * tab_width.to_double()
if step != 0.0 {
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 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()
}
}
}
ShapeLine::{ rtl, glyphs }
}