// 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 layout scaffolding ported from `cosmic-text/src/layout.rs`.
///
/// NOTE: This is an MVP focused on wrap/layout tests; shaping/font fallback will be added later.
///|
/// Metrics hinting strategy
pub(all) enum Hinting {
Disabled
Enabled
}
///|
/// Wrapping mode
pub(all) enum Wrap {
None
Glyph
Word
WordOrGlyph
}
///|
/// Align or justify
pub(all) enum Align {
Left
Right
Center
Justified
End
}
///|
/// A laid out glyph (MVP subset)
pub struct LayoutGlyph {
start : Int
end : Int
font_size : Float
font_id : Int
font_weight : Int
glyph_id : Int
x : Float
y : Float
w : Float
/// BiDi embedding level (LTR if divisible by 2).
level : Int
line_height_opt : Float?
x_offset : Float
y_offset : Float
color_opt : Color?
metadata : Int
cache_key_flags : CacheKeyFlags
}
///|
/// Convenience constructor (fills optional fields with defaults).
pub fn LayoutGlyph::new(
start : Int,
end : Int,
font_size : Float,
font_id : Int,
font_weight : Int,
glyph_id : Int,
x : Float,
y : Float,
w : Float,
metadata : Int,
) -> LayoutGlyph {
LayoutGlyph::{
start,
end,
font_size,
font_id,
font_weight,
glyph_id,
x,
y,
w,
level: 0,
line_height_opt: None,
x_offset: 0.0F,
y_offset: 0.0F,
color_opt: None,
metadata,
cache_key_flags: 0U,
}
}
///|
/// A line of laid out glyphs (MVP subset)
pub struct LayoutLine {
start : Int
end : Int
w : Float
max_ascent : Float
max_descent : Float
line_height_opt : Float?
glyphs : Array[LayoutGlyph]
}
///|
/// A laid out glyph in physical/pixel coordinates, ready for rasterization.
pub struct PhysicalGlyph {
cache_key : CacheKey
x : Int
y : Int
}
///|
pub fn LayoutGlyph::physical(
self : LayoutGlyph,
offset : (Float, Float),
scale : Float,
) -> PhysicalGlyph {
let x_off = self.font_size * self.x_offset
let y_off = self.font_size * self.y_offset
let pos_x = (self.x + x_off) * scale + offset.0
// Hinting in Y axis (match upstream idea): truncate to integral.
let pos_y = truncf((self.y - y_off) * scale + offset.1)
let (cache_key, x, y) = CacheKey::new(
self.font_id,
self.glyph_id,
self.font_size * scale,
(pos_x, pos_y),
self.font_weight,
self.cache_key_flags,
)
PhysicalGlyph::{ cache_key, x, y }
}
///|
fn float_from_int(v : Int) -> Float {
Float::from_double(v.to_double())
}
///|
fn glyph_layout_width(
glyph : ShapeGlyph,
default_font_size : Float,
cell_w : Float,
) -> Float {
let glyph_font_size = match glyph.metrics_opt {
Option::None => default_font_size
Option::Some(m) => m.font_size
}
let mut w = glyph.x_advance * glyph_font_size
if cell_w != default_font_size && default_font_size != 0.0F {
let match_em_width = cell_w / default_font_size
if match_em_width != 0.0F {
// Keep behavior close to upstream `layout_to_buffer`: snap per-glyph advance.
w = roundf(w / match_em_width) * match_em_width
}
}
w
}
///|
fn max_int(a : Int, b : Int) -> Int {
if a > b {
a
} else {
b
}
}
///|
///|
fn prefix_range_w(prefix : Array[Float], start : Int, end : Int) -> Float {
let s = if start < 0 { 0 } else { start }
let e = if end < s { s } else { end }
let a = if prefix.get(s) is Some(v) { v } else { 0.0F }
let b = if prefix.get(e) is Some(v) { v } else { a }
b - a
}
///|
priv struct GlyphWrapSegment {
glyph_start : Int
glyph_end : Int
blank : Bool
}
///|
fn layout_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 layout_glyph_is_blank(text : String, glyph : ShapeGlyph) -> Bool {
let start = if glyph.start < 0 {
0
} else if glyph.start > text.length() {
text.length()
} else {
glyph.start
}
let end0 = if glyph.end < 0 {
0
} else if glyph.end > text.length() {
text.length()
} else {
glyph.end
}
let end = if end0 < start { start } else { end0 }
if end <= start {
return false
}
let run = try! text[start:end]
let mut has_char = false
for ch in run {
has_char = true
if !layout_is_space_like_char(ch) {
return false
}
}
has_char
}
///|
fn build_glyph_wrap_segments(
text : String,
shape : ShapeLine,
) -> Array[GlyphWrapSegment] {
let segs : Array[GlyphWrapSegment] = []
let n = shape.glyphs.length()
let mut i = 0
while i < n {
let blank = layout_glyph_is_blank(text, shape.glyphs[i])
if blank {
segs.push(GlyphWrapSegment::{ glyph_start: i, glyph_end: i + 1, blank })
i = i + 1
continue
}
let start = i
i = i + 1
while i < n && !layout_glyph_is_blank(text, shape.glyphs[i]) {
i = i + 1
}
segs.push(GlyphWrapSegment::{ glyph_start: start, glyph_end: i, blank })
}
segs
}
///|
fn build_layout_line_from_shape(
shape : ShapeLine,
glyph_start : Int,
glyph_end : Int,
default_font_size : Float,
cell_w : Float,
) -> LayoutLine {
let glyphs : Array[LayoutGlyph] = []
// Compute the source text span for this visual line from glyph coverage.
let mut text_start = shape.glyphs[glyph_start].start
let mut text_end = shape.glyphs[glyph_start].end
for i in glyph_start.. text_end {
text_end = g.end
}
}
let mut x = 0.0F
let mut max_line_height : Float? = Option::None
let mut max_ascent = 0.0F
let mut max_descent = 0.0F
for i in glyph_start.. default_font_size
Option::Some(m) => m.font_size
}
let w = glyph_layout_width(g, default_font_size, cell_w)
let glyph_line_height_opt = match g.metrics_opt {
Option::None => Option::None
Option::Some(m) => Option::Some(m.line_height)
}
match (max_line_height, glyph_line_height_opt) {
(Option::None, Option::Some(h)) => max_line_height = Option::Some(h)
(Option::Some(a), Option::Some(b)) =>
if b > a {
max_line_height = Option::Some(b)
}
_ => ()
}
let glyph_ascent = glyph_font_size * g.ascent
if glyph_ascent > max_ascent {
max_ascent = glyph_ascent
}
let glyph_descent = glyph_font_size * g.descent
if glyph_descent > max_descent {
max_descent = glyph_descent
}
glyphs.push(LayoutGlyph::{
start: g.start,
end: g.end,
font_size: glyph_font_size,
font_id: g.font_id,
font_weight: g.font_weight,
glyph_id: g.glyph_id,
x,
y: 0.0F,
w,
level: g.level,
line_height_opt: glyph_line_height_opt,
x_offset: g.x_offset,
y_offset: g.y_offset,
color_opt: None,
metadata: g.metadata,
cache_key_flags: 0U,
})
x = x + w
}
LayoutLine::{
start: text_start,
end: text_end,
w: x,
max_ascent,
max_descent,
line_height_opt: max_line_height,
glyphs,
}
}
///|
fn push_layout_line_from_shape(
lines : Array[LayoutLine],
shape : ShapeLine,
glyph_start : Int,
glyph_end : Int,
font_size : Float,
cell_w : Float,
) -> Unit {
if glyph_end <= glyph_start {
return
}
lines.push(
build_layout_line_from_shape(
shape, glyph_start, glyph_end, font_size, cell_w,
),
)
}
///|
fn push_line(
lines : Array[LayoutLine],
text_start : Int,
text_end : Int,
glyph_w : Float,
) -> Unit {
let len = text_end - text_start
let w = glyph_w * float_from_int(len)
// MVP: 1 glyph per code unit, with x computed from glyph width.
let glyphs : Array[LayoutGlyph] = []
for i in 0.. Unit {
let max_glyphs_f = (max_w / glyph_w).to_double()
let max_glyphs = max_int(1, max_glyphs_f.to_int())
let mut i = text_start
while i < text_end {
let next = if i + max_glyphs > text_end { text_end } else { i + max_glyphs }
push_line(lines, i, next, glyph_w)
i = next
}
}
///|
/// ASCII-only layout for wrap tests.
///
/// Semantics:
/// - Treat each code unit as one glyph.
/// - `glyph_w` is a monospace advance in pixels.
pub fn layout_ascii(
text : String,
glyph_w : Float,
width_opt : Float?,
wrap : Wrap,
) -> Array[LayoutLine] {
let len = text.length()
let lines : Array[LayoutLine] = []
if len == 0 {
return lines
}
match wrap {
None => {
push_line(lines, 0, len, glyph_w)
return lines
}
Glyph => {
let max_w = if width_opt is Some(w) { w } else { 0.0F }
push_line_glyph_chunks(lines, 0, len, glyph_w, max_w)
return lines
}
_ => ()
}
if width_opt is None {
push_line(lines, 0, len, glyph_w)
return lines
}
let max_w = if width_opt is Some(w) { w } else { 0.0F }
let fallback_to_glyph = match wrap {
WordOrGlyph => true
_ => false
}
// Word/WordOrGlyph: linebreak-based segments with explicit blank tokens.
let segs = wrap_word_segments(text)
let mut i = 0
while i < segs.length() {
// Skip leading blank segments.
while i < segs.length() && segs[i].2 {
i = i + 1
}
if i >= segs.length() {
break
}
let line_start_i = i
let start_pos = segs[line_start_i].0
let mut cur_end_pos = start_pos
let mut last_non_blank_end_pos = start_pos
let mut emitted = false
while i < segs.length() {
let seg = segs[i]
let blank = seg.2
let word_w = glyph_w * float_from_int(seg.1 - seg.0)
let cur_w = glyph_w * float_from_int(cur_end_pos - start_pos)
let would_w = glyph_w * float_from_int(seg.1 - start_pos)
let fits = would_w <= max_w || (blank && cur_w <= max_w)
if fits {
cur_end_pos = seg.1
if !blank {
last_non_blank_end_pos = seg.1
}
i = i + 1
continue
}
// Doesn't fit.
if cur_end_pos == start_pos {
// Segment doesn't fit on an empty line.
if fallback_to_glyph && !blank && word_w > max_w {
push_line_glyph_chunks(lines, seg.0, seg.1, glyph_w, max_w)
i = i + 1
emitted = true
break
} else {
// Place it anyway (may overflow), then start new line.
push_line(lines, seg.0, seg.1, glyph_w)
i = i + 1
emitted = true
break
}
}
// Emit current line without trailing blanks (blank tokens are explicit).
if last_non_blank_end_pos > start_pos {
push_line(lines, start_pos, last_non_blank_end_pos, glyph_w)
}
emitted = true
// Start a new line at the current segment (don't consume it).
break
}
// End-of-input: emit remaining line (trim trailing blanks).
if i >= segs.length() && !emitted && last_non_blank_end_pos > start_pos {
push_line(lines, start_pos, last_non_blank_end_pos, glyph_w)
}
}
lines
}
///|
fn finalize_layout_lines(
lines : Array[LayoutLine],
rtl : Bool,
width_opt : Float?,
) -> Array[LayoutLine] {
if !rtl || lines.length() == 0 {
return lines
}
let line_width = match width_opt {
Some(w) => w
None => {
let mut max_w = 0.0F
for line in lines {
if line.w > max_w {
max_w = line.w
}
}
max_w
}
}
let out : Array[LayoutLine] = []
for line in lines {
let glyphs : Array[LayoutGlyph] = []
for glyph in line.glyphs {
glyphs.push(LayoutGlyph::{ ..glyph, x: line_width - (glyph.x + glyph.w) })
}
out.push(LayoutLine::{ ..line, glyphs, })
}
out
}
///|
priv struct WrapWordLayout {
glyph_start : Int
glyph_end : Int
blank : Bool
w : Float
}
///|
priv struct WrapSpanLayout {
level : Int
words : Array[WrapWordLayout]
}
///|
priv struct VisualRangeLayout {
span_index : Int
start_word : Int
start_glyph : Int
end_word : Int
end_glyph : Int
}
///|
fn build_wrap_spans_from_shape(
text : String,
shape : ShapeLine,
font_size : Float,
cell_w : Float,
) -> Array[WrapSpanLayout] {
let spans : Array[WrapSpanLayout] = []
let n = shape.glyphs.length()
let mut span_start = 0
while span_start < n {
let level = shape.glyphs[span_start].level
let mut span_end = span_start + 1
while span_end < n && shape.glyphs[span_end].level == level {
span_end = span_end + 1
}
let words : Array[WrapWordLayout] = []
let mut i = span_start
while i < span_end {
let blank = layout_glyph_is_blank(text, shape.glyphs[i])
if blank {
let w = glyph_layout_width(shape.glyphs[i], font_size, cell_w)
words.push(WrapWordLayout::{
glyph_start: i,
glyph_end: i + 1,
blank: true,
w,
})
i = i + 1
} else {
let word_start = i
let mut w = 0.0F
while i < span_end && !layout_glyph_is_blank(text, shape.glyphs[i]) {
w = w + glyph_layout_width(shape.glyphs[i], font_size, cell_w)
i = i + 1
}
words.push(WrapWordLayout::{
glyph_start: word_start,
glyph_end: i,
blank: false,
w,
})
}
}
spans.push(WrapSpanLayout::{ level, words })
span_start = span_end
}
spans
}
///|
fn add_visual_range(
ranges : Array[VisualRangeLayout],
cur_w : Float,
span_index : Int,
start_word : Int,
start_glyph : Int,
end_word : Int,
end_glyph : Int,
width : Float,
) -> Float {
if start_word == end_word && start_glyph == end_glyph {
return cur_w
}
ranges.push(VisualRangeLayout::{
span_index,
start_word,
start_glyph,
end_word,
end_glyph,
})
cur_w + width
}
///|
fn reverse_runs(runs : Array[(Int, Int)], start : Int, end : Int) -> Unit {
if end <= start {
return
}
let mut i = start
let mut j = end - 1
while i < j {
let t = runs[i]
runs[i] = runs[j]
runs[j] = t
i = i + 1
j = j - 1
}
}
///|
fn reorder_visual_range_runs(
spans : Array[WrapSpanLayout],
ranges : Array[VisualRangeLayout],
) -> Array[(Int, Int)] {
let out : Array[(Int, Int)] = []
if ranges.length() == 0 {
return out
}
let levels : Array[Int] = []
for r in ranges {
levels.push(spans[r.span_index].level)
}
let mut start = 0
let mut run_level = levels[0]
let mut min_level = run_level
let mut max_level = run_level
for i in 1.. max_level {
max_level = run_level
}
}
}
out.push((start, levels.length()))
let mut min_odd = min_level
if min_odd % 2 == 0 {
min_odd = min_odd + 1
}
let mut level = max_level
while level >= min_odd {
let mut seq_start = 0
while seq_start < out.length() {
let run_start = out[seq_start].0
if levels[run_start] < level {
seq_start = seq_start + 1
continue
}
let mut seq_end = seq_start + 1
while seq_end < out.length() {
let next_run_start = out[seq_end].0
if levels[next_run_start] < level {
break
}
seq_end = seq_end + 1
}
reverse_runs(out, seq_start, seq_end)
seq_start = seq_end
}
level = level - 1
}
out
}
///|
fn build_layout_lines_from_visual_ranges(
shape : ShapeLine,
font_size : Float,
cell_w : Float,
visual_ranges : Array[Array[VisualRangeLayout]],
visual_widths : Array[Float],
spans : Array[WrapSpanLayout],
) -> Array[LayoutLine] {
let lines : Array[LayoutLine] = []
for li in 0.. (Float, Int?, Int?, Float?, Float, Float) {
let mut x2 = x
let mut ts = text_start
let mut te = text_end
let mut max_h = max_line_height
let mut max_a = max_ascent
let mut max_d = max_descent
let vr = ranges[ridx]
let span = spans[vr.span_index]
let end_add = if vr.end_glyph == 0 { 0 } else { 1 }
for wi in vr.start_word..<(vr.end_word + end_add) {
let word = span.words[wi]
let word_len = word.glyph_end - word.glyph_start
let local_start = if wi == vr.start_word { vr.start_glyph } else { 0 }
let local_end = if wi == vr.end_word { vr.end_glyph } else { word_len }
for gi in local_start.. font_size
Some(m) => m.font_size
}
let w = glyph_layout_width(g, font_size, cell_w)
let glyph_line_height_opt : Float? = match g.metrics_opt {
None => None
Some(m) => Some(m.line_height)
}
match (max_h, glyph_line_height_opt) {
(None, Some(h)) => max_h = Some(h)
(Some(a), Some(b)) => if b > a { max_h = Some(b) }
_ => ()
}
let glyph_ascent = glyph_font_size * g.ascent
if glyph_ascent > max_a {
max_a = glyph_ascent
}
let glyph_descent = glyph_font_size * g.descent
if glyph_descent > max_d {
max_d = glyph_descent
}
match ts {
None => ts = Some(g.start)
Some(v) => if g.start < v { ts = Some(g.start) }
}
match te {
None => te = Some(g.end)
Some(v) => if g.end > v { te = Some(g.end) }
}
glyphs.push(LayoutGlyph::{
start: g.start,
end: g.end,
font_size: glyph_font_size,
font_id: g.font_id,
font_weight: g.font_weight,
glyph_id: g.glyph_id,
x: x2,
y: 0.0F,
w,
level: g.level,
line_height_opt: glyph_line_height_opt,
x_offset: g.x_offset,
y_offset: g.y_offset,
color_opt: None,
metadata: g.metadata,
cache_key_flags: 0U,
})
x2 = x2 + w
}
}
(x2, ts, te, max_h, max_a, max_d)
}
if shape.rtl {
let mut ri = runs.length()
while ri > 0 {
let run = runs[ri - 1]
for ridx in run.0.. Array[LayoutLine] {
let lines : Array[LayoutLine] = []
if text.length() == 0 {
lines.push(LayoutLine::{
start: 0,
end: 0,
w: 0.0F,
max_ascent: 0.0F,
max_descent: 0.0F,
line_height_opt: None,
glyphs: [],
})
return finalize_layout_lines(lines, shape.rtl, width_opt)
}
let shape_for_wrap = shape
let glyph_len = shape_for_wrap.glyphs.length()
if glyph_len == 0 {
return finalize_layout_lines(lines, shape_for_wrap.rtl, width_opt)
}
match wrap {
None => {
let mut line = build_layout_line_from_shape(
shape_for_wrap, 0, glyph_len, font_size, cell_w,
)
let spans = build_wrap_spans_from_shape(
text, shape_for_wrap, font_size, cell_w,
)
let mut total_w = 0.0F
for span in spans {
for word in span.words {
total_w = total_w + word.w
}
}
line = LayoutLine::{ ..line, w: total_w }
lines.push(line)
return finalize_layout_lines(lines, shape_for_wrap.rtl, width_opt)
}
_ => ()
}
match (width_opt, wrap) {
// No width constraint:
// - Glyph wrap: no wrapping required, keep full glyph span (includes spaces).
// - Word/WordOrGlyph: no wrapping required, keep full glyph span (matches upstream behavior).
(None, Glyph) => {
push_layout_line_from_shape(
lines, shape_for_wrap, 0, glyph_len, font_size, cell_w,
)
return finalize_layout_lines(lines, shape_for_wrap.rtl, width_opt)
}
(None, Word) | (None, WordOrGlyph) => {
push_layout_line_from_shape(
lines, shape_for_wrap, 0, glyph_len, font_size, cell_w,
)
return finalize_layout_lines(lines, shape_for_wrap.rtl, width_opt)
}
_ => ()
}
let max_w = if width_opt is Some(w) { w } else { 0.0F }
match wrap {
Glyph | Word | WordOrGlyph => {
let spans = build_wrap_spans_from_shape(
text, shape_for_wrap, font_size, cell_w,
)
let visual_ranges : Array[Array[VisualRangeLayout]] = []
let visual_widths : Array[Float] = []
let mut current_ranges : Array[VisualRangeLayout] = []
let mut current_w = 0.0F
for span_i in 0.. 0 {
let i = wi - 1
let word = span.words[i]
let word_w = word.w
let fits = current_w + (word_range_w + word_w) <= max_w ||
(word.blank && current_w + word_range_w <= max_w)
if fits {
if word.blank {
width_before_last_blank = word_range_w
}
word_range_w = word_range_w + word_w
} else if wrap is Glyph || (wrap is WordOrGlyph && word_w > max_w) {
if word_range_w > 0.0F && wrap is WordOrGlyph && word_w > max_w {
current_w = add_visual_range(
current_ranges,
current_w,
span_i,
i + 1,
0,
fitting_word,
fitting_glyph,
word_range_w,
)
if current_ranges.length() > 0 {
visual_ranges.push(current_ranges)
visual_widths.push(current_w)
current_ranges = []
current_w = 0.0F
}
word_range_w = 0.0F
fitting_word = i
fitting_glyph = 0
}
let word_len = word.glyph_end - word.glyph_start
let mut gi = word_len
while gi > 0 {
let gidx = gi - 1
let abs_i = word.glyph_start + gidx
let glyph_w = glyph_layout_width(
shape_for_wrap.glyphs[abs_i],
font_size,
cell_w,
)
if current_w + (word_range_w + glyph_w) <= max_w {
word_range_w = word_range_w + glyph_w
} else {
current_w = add_visual_range(
current_ranges,
current_w,
span_i,
i,
gidx + 1,
fitting_word,
fitting_glyph,
word_range_w,
)
if current_ranges.length() > 0 {
visual_ranges.push(current_ranges)
visual_widths.push(current_w)
current_ranges = []
current_w = 0.0F
}
word_range_w = glyph_w
fitting_word = i
fitting_glyph = gidx + 1
}
gi = gidx
}
} else {
if word_range_w > 0.0F {
let trailing_blank = i + 1 < span.words.length() &&
span.words[i + 1].blank
if trailing_blank {
current_w = add_visual_range(
current_ranges,
current_w,
span_i,
i + 2,
0,
fitting_word,
fitting_glyph,
width_before_last_blank,
)
} else {
current_w = add_visual_range(
current_ranges,
current_w,
span_i,
i + 1,
0,
fitting_word,
fitting_glyph,
word_range_w,
)
}
if current_ranges.length() > 0 {
visual_ranges.push(current_ranges)
visual_widths.push(current_w)
current_ranges = []
current_w = 0.0F
}
}
if word.blank {
word_range_w = 0.0F
fitting_word = i
fitting_glyph = 0
} else {
word_range_w = word_w
fitting_word = i + 1
fitting_glyph = 0
}
}
wi = i
}
current_w = add_visual_range(
current_ranges, current_w, span_i, 0, 0, fitting_word, fitting_glyph,
word_range_w,
)
} else {
let mut fitting_word = 0
let mut fitting_glyph = 0
for i in 0.. max_w) {
if word_range_w > 0.0F && wrap is WordOrGlyph && word_w > max_w {
current_w = add_visual_range(
current_ranges, current_w, span_i, fitting_word, fitting_glyph,
i, 0, word_range_w,
)
if current_ranges.length() > 0 {
visual_ranges.push(current_ranges)
visual_widths.push(current_w)
current_ranges = []
current_w = 0.0F
}
word_range_w = 0.0F
fitting_word = i
fitting_glyph = 0
}
let word_len = word.glyph_end - word.glyph_start
for gidx in 0.. 0 {
visual_ranges.push(current_ranges)
visual_widths.push(current_w)
current_ranges = []
current_w = 0.0F
}
word_range_w = glyph_w
fitting_word = i
fitting_glyph = gidx
}
}
} else {
if word_range_w > 0.0F {
let trailing_blank = i > 0 && span.words[i - 1].blank
if trailing_blank {
current_w = add_visual_range(
current_ranges,
current_w,
span_i,
fitting_word,
fitting_glyph,
i - 1,
0,
width_before_last_blank,
)
} else {
current_w = add_visual_range(
current_ranges, current_w, span_i, fitting_word, fitting_glyph,
i, 0, word_range_w,
)
}
if current_ranges.length() > 0 {
visual_ranges.push(current_ranges)
visual_widths.push(current_w)
current_ranges = []
current_w = 0.0F
}
}
if word.blank {
word_range_w = 0.0F
fitting_word = i + 1
fitting_glyph = 0
} else {
word_range_w = word_w
fitting_word = i
fitting_glyph = 0
}
}
}
current_w = add_visual_range(
current_ranges,
current_w,
span_i,
fitting_word,
fitting_glyph,
span.words.length(),
0,
word_range_w,
)
}
}
if current_ranges.length() > 0 {
visual_ranges.push(current_ranges)
visual_widths.push(current_w)
}
let new_lines = build_layout_lines_from_visual_ranges(
shape_for_wrap, font_size, cell_w, visual_ranges, visual_widths, spans,
)
for line in new_lines {
lines.push(line)
}
return finalize_layout_lines(lines, shape_for_wrap.rtl, width_opt)
}
_ => ()
}
let fallback_to_glyph = match wrap {
WordOrGlyph => true
_ => false
}
// Prefix sums of pixel advances for quick range width queries.
let prefix : Array[Float] = []
let mut acc = 0.0F
prefix.push(acc)
for i in 0..= segs.length() {
break
}
let line_start_i = i
let mut line_start_g = segs[line_start_i].glyph_start
let mut cur_end_g = line_start_g
let mut before_last_blank_end_g = line_start_g
let mut kept_overflow_after_blank = false
let mut seen_nonblank = false
let mut seen_incongruent_nonblank = false
let mut emitted = false
// Track last "fitting" glyph end and the end position before the most recent
// blank segment, mirroring upstream `width_before_last_blank` behavior.
while i < segs.length() {
let seg = segs[i]
let blank = seg.blank
let would_end_g = seg.glyph_end
let seg_rtl = shape_for_wrap.glyphs[seg.glyph_start].level % 2 != 0
let would_w = prefix_range_w(prefix, line_start_g, would_end_g)
let cur_w = prefix_range_w(prefix, line_start_g, cur_end_g)
let fits = would_w <= max_w || (blank && cur_w <= max_w)
if fits {
if blank {
before_last_blank_end_g = cur_end_g
} else if seg_rtl != shape_for_wrap.rtl {
seen_incongruent_nonblank = true
}
if !blank {
seen_nonblank = true
}
cur_end_g = would_end_g
i = i + 1
continue
}
// Doesn't fit.
if i == line_start_i {
// Segment doesn't fit on an empty line.
let word_start_g = seg.glyph_start
let word_end_g = seg.glyph_end
if fallback_to_glyph &&
prefix_range_w(prefix, word_start_g, word_end_g) > max_w {
// Match reference behavior: commit wrapped chunks except the final one.
// Keep the final chunk as the current in-progress line so trailing blanks
// can still attach before the next wrap decision.
if seg_rtl != shape_for_wrap.rtl {
let mut chunk_end = word_end_g
let mut chunk_w = 0.0F
let mut gi = word_end_g
while gi > word_start_g {
let gidx = gi - 1
let glyph_w = glyph_layout_width(
shape_for_wrap.glyphs[gidx],
font_size,
cell_w,
)
if chunk_w + glyph_w > max_w && gidx + 1 < chunk_end {
push_layout_line_from_shape(
lines,
shape_for_wrap,
gidx + 1,
chunk_end,
font_size,
cell_w,
)
chunk_end = gidx + 1
chunk_w = 0.0F
}
chunk_w = chunk_w + glyph_w
gi = gidx
}
line_start_g = word_start_g
cur_end_g = chunk_end
} else {
let mut chunk_start = word_start_g
let mut chunk_w = 0.0F
for gi in word_start_g.. max_w && gi > chunk_start {
push_layout_line_from_shape(
lines, shape_for_wrap, chunk_start, gi, font_size, cell_w,
)
chunk_start = gi
chunk_w = 0.0F
}
chunk_w = chunk_w + glyph_w
}
line_start_g = chunk_start
cur_end_g = word_end_g
}
before_last_blank_end_g = cur_end_g
seen_nonblank = true
i = i + 1
continue
} else {
// Word wrap parity: keep the overflowing word as the in-progress line,
// and let the next segment decide whether trailing blank should be dropped.
cur_end_g = word_end_g
if !blank {
seen_nonblank = true
}
i = i + 1
continue
}
}
// Reference parity on word-wrap: if a non-blank segment overflows right
// after a blank separator, keep it on the same line and allow overflow.
// This matches span-transition behavior in upstream layout.
if (wrap is Word || wrap is WordOrGlyph) &&
!blank &&
i > line_start_i &&
segs[i - 1].blank &&
!kept_overflow_after_blank &&
!seen_incongruent_nonblank &&
!(wrap is WordOrGlyph &&
prefix_range_w(prefix, seg.glyph_start, seg.glyph_end) > max_w) &&
seg_rtl != shape_for_wrap.rtl {
cur_end_g = would_end_g
kept_overflow_after_blank = true
seen_nonblank = true
seen_incongruent_nonblank = true
i = i + 1
continue
}
// Emit current line, possibly trimming trailing blanks when wrapping.
let emit_end = if i > line_start_i && segs[i - 1].blank {
if wrap is Word {
before_last_blank_end_g
} else if blank {
cur_end_g
} else if wrap is WordOrGlyph &&
prefix_range_w(prefix, seg.glyph_start, seg.glyph_end) > max_w {
cur_end_g
} else {
before_last_blank_end_g
}
} else {
cur_end_g
}
if emit_end > line_start_g {
push_layout_line_from_shape(
lines, shape_for_wrap, line_start_g, emit_end, font_size, cell_w,
)
}
// When a blank segment triggers wrap, consume it so the next line does not
// start with that overflowing blank (matches upstream word-wrap behavior).
if blank && (wrap is Word || seen_nonblank) {
i = i + 1
}
emitted = true
// Start a new line at the current segment (do not consume it).
break
}
// End-of-input: keep trailing blanks that fit in the last visual line.
if i >= segs.length() && !emitted && cur_end_g > line_start_g {
push_layout_line_from_shape(
lines, shape_for_wrap, line_start_g, cur_end_g, font_size, cell_w,
)
}
}
finalize_layout_lines(lines, shape_for_wrap.rtl, width_opt)
}
///|
fn apply_align_to_line(
text : String,
line : LayoutLine,
width : Float,
align : Align,
rtl : Bool,
) -> LayoutLine {
let extra = width - line.w
if extra <= 0.0F {
return line
}
match align {
Left => line
Center => {
let dx = extra / 2.0F
let glyphs : Array[LayoutGlyph] = []
for g in line.glyphs {
glyphs.push(LayoutGlyph::{ ..g, x: g.x + dx })
}
LayoutLine::{ ..line, glyphs, }
}
Right => {
let dx = extra
let glyphs : Array[LayoutGlyph] = []
for g in line.glyphs {
glyphs.push(LayoutGlyph::{ ..g, x: g.x + dx })
}
LayoutLine::{ ..line, glyphs, }
}
End =>
if rtl {
line
} else {
let dx = extra
let glyphs : Array[LayoutGlyph] = []
for g in line.glyphs {
glyphs.push(LayoutGlyph::{ ..g, x: g.x + dx })
}
LayoutLine::{ ..line, glyphs, }
}
Justified => {
// MVP: distribute extra width across ASCII spaces in this line.
let mut space_count = 0
for i in line.start..= 0 && i < text.length() && text.code_unit_at(i) == 32 {
space_count = space_count + 1
}
}
if space_count == 0 {
return line
}
let add = extra / Float::from_double(space_count.to_double())
let glyphs : Array[LayoutGlyph] = []
let mut dx = 0.0F
for g in line.glyphs {
let mut w = g.w
// Space glyph is identified by [start,end) covering a single code unit space.
if g.end - g.start == 1 &&
g.start >= 0 &&
g.start < text.length() &&
text.code_unit_at(g.start) == 32 {
w = w + add
glyphs.push(LayoutGlyph::{ ..g, x: g.x + dx, w })
dx = dx + add
} else {
glyphs.push(LayoutGlyph::{ ..g, x: g.x + dx, w })
}
}
LayoutLine::{ ..line, w: line.w + extra, glyphs }
}
}
}
///|
/// Apply Align to precomputed layout lines (requires a finite width).
pub fn apply_align_with_rtl(
text : String,
lines : Array[LayoutLine],
width : Float,
align : Align,
rtl : Bool,
) -> Array[LayoutLine] {
let out : Array[LayoutLine] = []
for line in lines {
out.push(apply_align_to_line(text, line, width, align, rtl))
}
out
}
///|
/// Apply Align to precomputed layout lines assuming LTR paragraph direction.
pub fn apply_align(
text : String,
lines : Array[LayoutLine],
width : Float,
align : Align,
) -> Array[LayoutLine] {
apply_align_with_rtl(text, lines, width, align, false)
}