// 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.
///|
/// Port scaffold from `cosmic-text/src/buffer_line.rs`.
///
/// This implementation is intentionally minimal and test-driven:
/// - It focuses on cache invalidation and text/span mutations (append/split_off),
/// keeping API naming/semantics aligned with upstream.
fn string_concat(a : String, b : String) -> String {
let sb = StringBuilder::new(size_hint=(a.length() + b.length()) * 2)
sb.write_string(a)
sb.write_string(b)
sb.to_string()
}
///|
fn substring(s : String, start : Int, end : Int) -> String {
let sb = StringBuilder::new(size_hint=(end - start) * 2)
sb.write_view(s[:].view(start_offset=start, end_offset=end))
sb.to_string()
}
///|
fn empty_layout_line() -> LayoutLine {
LayoutLine::{
start: 0,
end: 0,
w: 0.0F,
max_ascent: 0.0F,
max_descent: 0.0F,
line_height_opt: None,
glyphs: [],
}
}
///|
fn max_layout_width(layout : Array[LayoutLine]) -> Float? {
if layout.length() == 0 {
return None
}
let mut max_w = layout[0].w
for l in layout {
if l.w > max_w {
max_w = l.w
}
}
Some(max_w)
}
///|
fn justify_line(text : String, line : LayoutLine, width : Float) -> LayoutLine {
let extra = width - line.w
if extra <= 0.0F {
return 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
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 }
}
///|
fn justify_lines_except_last(
text : String,
layout : Array[LayoutLine],
width : Float,
) -> Array[LayoutLine] {
let out : Array[LayoutLine] = []
let n = layout.length()
for i in 0.. LayoutLine {
if l.glyphs.length() == 0 {
return l
}
let glyphs : Array[LayoutGlyph] = []
let mut x = roundf(l.glyphs[0].x)
let start_x = x
for g in l.glyphs {
let w = roundf(g.w)
glyphs.push(LayoutGlyph::{ ..g, x, w })
x = x + w
}
LayoutLine::{ ..l, w: x - start_x, glyphs }
}
///|
fn apply_layout_postprocess(
text : String,
layout0 : Array[LayoutLine],
rtl : Bool,
align_opt : Align?,
width_opt : Float?,
hinting : Hinting,
) -> Array[LayoutLine] {
let mut layout = if layout0.length() == 0 {
[empty_layout_line()]
} else {
layout0
}
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 {
max_layout_width(layout)
}
if line_width_opt is Some(width) {
match align {
Justified => layout = justify_lines_except_last(text, layout, width)
_ => layout = apply_align_with_rtl(text, layout, width, align, rtl)
}
}
if hinting is Hinting::Enabled {
let out : Array[LayoutLine] = []
for l in layout {
out.push(hint_line(l))
}
layout = out
}
layout
}
///|
fn align_eq(a : Align, b : Align) -> Bool {
match (a, b) {
(Left, Left) => true
(Right, Right) => true
(Center, Center) => true
(Justified, Justified) => true
(End, End) => true
_ => false
}
}
///|
fn align_opt_eq(a : Align?, b : Align?) -> Bool {
match (a, b) {
(None, None) => true
(Some(a1), Some(b1)) => align_eq(a1, b1)
_ => false
}
}
///|
pub struct BufferLine {
text : String
ending : LineEnding
attrs_list : AttrsList
align : Align?
shape_opt : Cached[ShapeLine]
layout_opt : Cached[Array[LayoutLine]]
shaping : Shaping
metadata : Int?
}
///|
pub fn BufferLine::new(
text : String,
ending : LineEnding,
attrs_list : AttrsList,
shaping : Shaping,
) -> BufferLine {
BufferLine::{
text,
ending,
attrs_list,
align: None,
shape_opt: Empty,
layout_opt: Empty,
shaping,
metadata: None,
}
}
///|
pub fn BufferLine::text(self : BufferLine) -> String {
self.text
}
///|
pub fn BufferLine::ending(self : BufferLine) -> LineEnding {
self.ending
}
///|
pub fn BufferLine::set_ending(
self : BufferLine,
ending : LineEnding,
) -> BufferLine {
if ending.as_str() == self.ending.as_str() {
self
} else {
BufferLine::{ ..self, ending, }.reset_shaping()
}
}
///|
pub fn BufferLine::attrs_list(self : BufferLine) -> AttrsList {
self.attrs_list
}
///|
pub fn BufferLine::align(self : BufferLine) -> Align? {
self.align
}
///|
pub fn BufferLine::reset(self : BufferLine) -> BufferLine {
BufferLine::{ ..self, metadata: None }.reset_shaping()
}
///|
pub fn BufferLine::reset_shaping(self : BufferLine) -> BufferLine {
let shape_opt = self.shape_opt.set_unused()
BufferLine::{ ..self, shape_opt, }.reset_layout()
}
///|
pub fn BufferLine::reset_layout(self : BufferLine) -> BufferLine {
let layout_opt = self.layout_opt.set_unused()
BufferLine::{ ..self, layout_opt, }
}
///|
pub fn BufferLine::shape_opt(self : BufferLine) -> ShapeLine? {
self.shape_opt.get()
}
///|
pub fn BufferLine::shape(self : BufferLine, tab_width : Int) -> BufferLine {
if self.shape_opt.is_unused() {
let (shape_opt, reused_opt) = self.shape_opt.take_unused()
let reused = if reused_opt is Some(v) { v } else { ShapeLine::empty() }
let built = reused.build(
self.text,
self.attrs_list,
self.shaping,
tab_width,
)
let shape_opt = shape_opt.set_used(built)
BufferLine::{ ..self, shape_opt, }.reset_layout()
} else {
self
}
}
///|
/// Shape using a provided FontSystem (best-effort).
///
/// This is an incremental step towards upstream `cosmic-text`:
/// we keep the same cache behavior, but build glyph advances from the font.
pub fn BufferLine::shape_with_font_system(
self : BufferLine,
font_system : FontSystem,
tab_width : Int,
) -> BufferLine {
if self.shape_opt.is_unused() {
let (shape_opt, reused_opt) = self.shape_opt.take_unused()
let reused = if reused_opt is Some(v) { v } else { ShapeLine::empty() }
let built = reused.build_with_font_system(
font_system,
self.text,
self.attrs_list,
self.shaping,
tab_width,
)
let shape_opt = shape_opt.set_used(built)
BufferLine::{ ..self, shape_opt, }.reset_layout()
} else {
self
}
}
///|
pub fn BufferLine::layout_opt(self : BufferLine) -> Array[LayoutLine]? {
self.layout_opt.get()
}
///|
pub fn BufferLine::layout(
self : BufferLine,
font_size : Float,
width_opt : Float?,
wrap : Wrap,
match_mono_width : Float?,
tab_width : Int,
hinting : Hinting,
) -> BufferLine {
if self.layout_opt.is_unused() {
let line = self.shape(tab_width)
let shape_opt = line.shape_opt()
if shape_opt is Some(shape) {
let cell_w = if match_mono_width is Some(w) { w } else { font_size }
let layout = layout_from_shape(
line.text,
shape,
font_size,
cell_w,
width_opt,
wrap,
)
let layout = apply_layout_postprocess(
line.text,
layout,
shape.rtl,
line.align,
width_opt,
hinting,
)
let (layout_opt, _) = line.layout_opt.take_unused()
let layout_opt = layout_opt.set_used(layout)
BufferLine::{ ..line, layout_opt, }
} else {
line
}
} else {
self
}
}
///|
/// Layout using a provided FontSystem (best-effort).
pub fn BufferLine::layout_with_font_system(
self : BufferLine,
font_system : FontSystem,
font_size : Float,
width_opt : Float?,
wrap : Wrap,
match_mono_width : Float?,
tab_width : Int,
hinting : Hinting,
) -> BufferLine {
if self.layout_opt.is_unused() {
let line = self.shape_with_font_system(font_system, tab_width)
let shape_opt = line.shape_opt()
if shape_opt is Some(shape) {
let cell_w = if match_mono_width is Some(w) { w } else { font_size }
let layout = layout_from_shape(
line.text,
shape,
font_size,
cell_w,
width_opt,
wrap,
)
let layout = apply_layout_postprocess(
line.text,
layout,
shape.rtl,
line.align,
width_opt,
hinting,
)
let (layout_opt, _) = line.layout_opt.take_unused()
let layout_opt = layout_opt.set_used(layout)
BufferLine::{ ..line, layout_opt, }
} else {
line
}
} else {
// Match existing API: if layout is cached, no work.
// `hinting` is currently unused (MVP), but kept for API parity.
ignore(hinting)
self
}
}
///|
pub fn BufferLine::set_text(
self : BufferLine,
text : String,
ending : LineEnding,
attrs_list : AttrsList,
) -> (BufferLine, Bool) {
if text != self.text ||
ending.as_str() != self.ending.as_str() ||
!attrs_list.eq(self.attrs_list) {
let updated = BufferLine::{ ..self, text, ending, attrs_list }.reset()
(updated, true)
} else {
(self, false)
}
}
///|
pub fn BufferLine::set_align(
self : BufferLine,
align : Align?,
) -> (BufferLine, Bool) {
if align_opt_eq(align, self.align) {
(self, false)
} else {
(BufferLine::{ ..self, align, }.reset_layout(), true)
}
}
///|
/// Append `other` to `self` (consumes both, returns updated line).
pub fn BufferLine::append(self : BufferLine, other : BufferLine) -> BufferLine {
let len = self.text.length()
let other_text = other.text
let other_len = other_text.length()
let mut attrs_list = self.attrs_list
if other.attrs_list.defaults() != attrs_list.defaults() {
attrs_list = attrs_list.add_span(
len,
len + other_len,
other.attrs_list.defaults(),
)
}
for span in other.attrs_list.spans_iter() {
attrs_list = attrs_list.add_span(
span.start + len,
span.end + len,
span.attrs,
)
}
BufferLine::{
..self,
text: string_concat(self.text, other_text),
ending: other.ending,
attrs_list,
}.reset()
}
///|
/// Split off new line at index. Returns (left, right).
pub fn BufferLine::split_off(
self : BufferLine,
index : Int,
) -> (BufferLine, BufferLine) {
let index = if index < 0 { 0 } else { index }
let end = self.text.length()
let idx = if index > end { end } else { index }
let left_text = substring(self.text, 0, idx)
let right_text = substring(self.text, idx, end)
let (left_attrs, right_attrs) = self.attrs_list.split_off(idx)
let left = BufferLine::{
..self,
text: left_text,
ending: None,
attrs_list: left_attrs,
}.reset()
let mut right = BufferLine::new(
right_text,
self.ending,
right_attrs,
self.shaping,
)
right = BufferLine::{ ..right, align: self.align }
(left, right)
}