// 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 `Buffer` port scaffold from `cosmic-text/src/buffer.rs`.
pub struct Buffer {
lines : Array[BufferLine]
metrics : Metrics
width_opt : Float?
height_opt : Float?
scroll : Scroll
redraw : Bool
wrap : Wrap
ellipsize : Ellipsize
monospace_width : Float?
tab_width : Int
hinting : Hinting
dirty_relayout : Bool
dirty_tab_shape : Bool
dirty_text_set : Bool
dirty_scroll : Bool
}
///|
pub fn Buffer::new(metrics : Metrics) -> Buffer {
Buffer::new_empty(metrics)
}
///|
/// Create an empty Buffer with the provided Metrics.
///
/// Panics (fails) if `metrics.line_height` is zero (aligned with upstream).
pub fn Buffer::new_empty(metrics : Metrics) -> Buffer {
if metrics.line_height == 0.0F {
abort("line height cannot be 0")
}
Buffer::{
lines: [],
metrics,
width_opt: None,
height_opt: None,
scroll: Scroll::default(),
redraw: false,
wrap: Wrap::WordOrGlyph,
ellipsize: Ellipsize::None,
monospace_width: None,
tab_width: 8,
hinting: Hinting::Disabled,
dirty_relayout: false,
dirty_tab_shape: false,
dirty_text_set: false,
dirty_scroll: false,
}
}
///|
pub fn Buffer::metrics(self : Buffer) -> Metrics {
self.metrics
}
///|
pub fn Buffer::lines(self : Buffer) -> Array[BufferLine] {
self.lines
}
///|
pub fn Buffer::redraw(self : Buffer) -> Bool {
self.redraw
}
///|
pub fn Buffer::set_redraw(self : Buffer, redraw : Bool) -> Buffer {
if self.redraw == redraw {
self
} else {
Buffer::{ ..self, redraw, }
}
}
///|
pub fn Buffer::scroll(self : Buffer) -> Scroll {
self.scroll
}
///|
pub fn Buffer::set_scroll(self : Buffer, scroll : Scroll) -> Buffer {
if self.scroll.line == scroll.line &&
self.scroll.vertical == scroll.vertical &&
self.scroll.horizontal == scroll.horizontal {
self
} else {
Buffer::{ ..self, scroll, redraw: true, dirty_scroll: true }
}
}
///|
pub fn Buffer::wrap(self : Buffer) -> Wrap {
self.wrap
}
///|
pub fn Buffer::ellipsize(self : Buffer) -> Ellipsize {
self.ellipsize
}
///|
pub fn Buffer::hinting(self : Buffer) -> Hinting {
self.hinting
}
///|
fn wrap_eq(a : Wrap, b : Wrap) -> Bool {
match (a, b) {
(None, None) => true
(Glyph, Glyph) => true
(Word, Word) => true
(WordOrGlyph, WordOrGlyph) => true
_ => false
}
}
///|
fn ellipsize_height_limit_eq(
a : EllipsizeHeightLimit,
b : EllipsizeHeightLimit,
) -> Bool {
match (a, b) {
(Lines(a0), Lines(b0)) => a0 == b0
(Height(a0), Height(b0)) => a0 == b0
_ => false
}
}
///|
fn ellipsize_eq(a : Ellipsize, b : Ellipsize) -> Bool {
match (a, b) {
(None, None) => true
(Start(a0), Start(b0)) => ellipsize_height_limit_eq(a0, b0)
(Middle(a0), Middle(b0)) => ellipsize_height_limit_eq(a0, b0)
(End(a0), End(b0)) => ellipsize_height_limit_eq(a0, b0)
_ => false
}
}
///|
fn hinting_eq(a : Hinting, b : Hinting) -> Bool {
match (a, b) {
(Disabled, Disabled) => true
(Enabled, Enabled) => true
_ => false
}
}
///|
fn float_opt_eq(a : Float?, b : Float?) -> Bool {
match (a, b) {
(None, None) => true
(Some(x), Some(y)) => x == y
_ => false
}
}
///|
fn clamp_nonneg_opt(v : Float?) -> Float? {
match v {
None => None
Some(x) => if x < 0.0F { Some(0.0F) } else { Some(x) }
}
}
///|
fn layout_total_height(
metrics : Metrics,
layout_opt : Array[LayoutLine]?,
) -> Float {
match layout_opt {
None => metrics.line_height
Some(layout) => {
let mut h = 0.0F
for layout_line in layout {
let line_height = if layout_line.line_height_opt is Some(v) {
v
} else {
metrics.line_height
}
h = h + line_height
}
h
}
}
}
///|
fn ensure_line_layout_cached(
lines : Array[BufferLine],
line_i : Int,
font_system : FontSystem,
font_size : Float,
width_opt : Float?,
wrap : Wrap,
ellipsize : Ellipsize,
monospace_width : Float?,
tab_width : Int,
hinting : Hinting,
) -> Array[LayoutLine]? {
if line_i < 0 || line_i >= lines.length() {
return None
}
let line = lines[line_i].layout_with_font_system(
font_system, font_size, width_opt, wrap, ellipsize, monospace_width, tab_width,
hinting,
)
lines.set(line_i, line)
line.layout_opt()
}
///|
fn line_has_tab(s : String) -> Bool {
for i in 0.. Array[BufferLine] {
let out : Array[BufferLine] = []
for line in lines {
out.push(line.reset_layout())
}
out
}
///|
fn reset_shaping_lines_for_tab_width(
lines : Array[BufferLine],
) -> Array[BufferLine] {
let out : Array[BufferLine] = []
for line in lines {
if line.shape_opt() is Some(_) && line_has_tab(line.text()) {
out.push(line.reset_shaping())
} else {
out.push(line)
}
}
out
}
///|
fn buffer_has_dirty(buffer : Buffer) -> Bool {
buffer.dirty_relayout ||
buffer.dirty_tab_shape ||
buffer.dirty_text_set ||
buffer.dirty_scroll
}
///|
fn buffer_resolve_dirty(buffer : Buffer) -> Buffer? {
if !buffer_has_dirty(buffer) {
return None
}
let lines = buffer.lines
if !buffer.dirty_text_set {
let lines = if buffer.dirty_tab_shape {
reset_shaping_lines_for_tab_width(lines)
} else {
lines
}
let lines = if buffer.dirty_relayout {
reset_layout_lines(lines)
} else {
lines
}
return Some(Buffer::{
..buffer,
lines,
redraw: true,
dirty_relayout: false,
dirty_tab_shape: false,
dirty_text_set: false,
dirty_scroll: false,
})
}
Some(Buffer::{
..buffer,
lines,
redraw: true,
dirty_relayout: false,
dirty_tab_shape: false,
dirty_text_set: false,
dirty_scroll: false,
})
}
///|
pub fn Buffer::set_wrap(self : Buffer, wrap : Wrap) -> Buffer {
if wrap_eq(self.wrap, wrap) {
self
} else {
Buffer::{ ..self, wrap, redraw: true, dirty_relayout: true }
}
}
///|
pub fn Buffer::set_ellipsize(self : Buffer, ellipsize : Ellipsize) -> Buffer {
if ellipsize_eq(self.ellipsize, ellipsize) {
self
} else {
Buffer::{ ..self, ellipsize, redraw: true, dirty_relayout: true }
}
}
///|
pub fn Buffer::set_tab_width(self : Buffer, tab_width : Int) -> Buffer {
if tab_width <= 0 || self.tab_width == tab_width {
self
} else {
Buffer::{
..self,
tab_width,
redraw: true,
dirty_tab_shape: true,
dirty_relayout: true,
}
}
}
///|
pub fn Buffer::set_hinting(self : Buffer, hinting : Hinting) -> Buffer {
if hinting_eq(self.hinting, hinting) {
self
} else {
Buffer::{ ..self, hinting, redraw: true, dirty_relayout: true }
}
}
///|
pub fn Buffer::set_monospace_width(
self : Buffer,
monospace_width : Float?,
) -> Buffer {
if float_opt_eq(self.monospace_width, monospace_width) {
self
} else {
Buffer::{ ..self, monospace_width, redraw: true, dirty_relayout: true }
}
}
///|
pub fn Buffer::set_metrics(self : Buffer, metrics : Metrics) -> Buffer {
self.set_metrics_and_size(metrics, self.width_opt, self.height_opt)
}
///|
pub fn Buffer::set_metrics_and_size(
self : Buffer,
metrics : Metrics,
width_opt : Float?,
height_opt : Float?,
) -> Buffer {
if metrics.font_size == 0.0F {
abort("font size cannot be 0")
}
if metrics.line_height == 0.0F {
abort("line height cannot be 0")
}
let clamped_width_opt = clamp_nonneg_opt(width_opt)
let clamped_height_opt = clamp_nonneg_opt(height_opt)
if self.metrics.font_size == metrics.font_size &&
self.metrics.line_height == metrics.line_height &&
float_opt_eq(self.width_opt, clamped_width_opt) &&
float_opt_eq(self.height_opt, clamped_height_opt) {
return self
}
Buffer::{
..self,
metrics,
width_opt: clamped_width_opt,
height_opt: clamped_height_opt,
redraw: true,
dirty_relayout: true,
}
}
///|
pub fn Buffer::set_size(
self : Buffer,
width_opt : Float?,
height_opt : Float?,
) -> Buffer {
self.set_metrics_and_size(self.metrics, width_opt, height_opt)
}
///|
pub fn Buffer::size(self : Buffer) -> (Float?, Float?) {
(self.width_opt, self.height_opt)
}
///|
fn slice_string(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()
}
///|
/// Set full text of buffer.
///
/// NOTE: `attrs_list` is copied per line (upstream spans are per line).
pub fn Buffer::set_text(
self : Buffer,
text : String,
attrs_list : AttrsList,
shaping : Shaping,
) -> Buffer {
let mut iter = LineIter::new(text)
let new_lines = self.lines
let mut line_count = 0
while true {
let (next_iter, item_opt) = iter.next()
iter = next_iter
if item_opt is Some(item) {
let start = item.0
let end = item.1
let ending = item.2
let line_text = slice_string(text, start, end)
if line_count < new_lines.length() {
let line = new_lines[line_count]
let (next_line0, _) = line.set_text(line_text, ending, attrs_list)
let (next_line, _) = next_line0.set_align(None)
new_lines.set(line_count, next_line)
} else {
// `attrs_list` is currently shared across split lines.
new_lines.push(BufferLine::new(line_text, ending, attrs_list, shaping))
}
line_count = line_count + 1
} else {
break
}
}
// Upstream behavior: ensure there is a trailing line with `LineEnding::None`.
let need_trailing = if line_count == 0 {
true
} else {
match new_lines[line_count - 1].ending() {
None => false
_ => true
}
}
if need_trailing {
if line_count < new_lines.length() {
let line = new_lines[line_count]
let (next_line0, _) = line.set_text("", LineEnding::None, attrs_list)
let (next_line, _) = next_line0.set_align(None)
new_lines.set(line_count, next_line)
} else {
new_lines.push(BufferLine::new("", LineEnding::None, attrs_list, shaping))
}
line_count = line_count + 1
}
new_lines.truncate(line_count)
Buffer::{
..self,
lines: new_lines,
scroll: Scroll::default(),
redraw: true,
dirty_relayout: false,
dirty_tab_shape: false,
dirty_text_set: true,
dirty_scroll: false,
}
}
///|
/// Set rich text of buffer using styled spans (pairs of text and attrs).
///
/// This mirrors `cosmic-text`'s `Buffer::set_rich_text` behavior:
/// - Concatenates spans into one string.
/// - Splits into paragraphs using `BidiParagraphs`.
/// - Builds per-line `AttrsList` spans relative to each line.
/// - Uses `LineEnding::default()` for all lines.
pub fn Buffer::set_rich_text(
self : Buffer,
spans : Array[(String, Attrs)],
default_attrs : Attrs,
shaping : Shaping,
alignment : Align?,
) -> Buffer {
let string_sb = StringBuilder::new(size_hint=256)
let span_data : Array[(Attrs, Int, Int)] = []
let mut end = 0
for sp in spans {
let text = sp.0
let attrs = sp.1
let start = end
end = end + text.length()
string_sb.write_string(text)
span_data.push((attrs, start, end))
}
let string = string_sb.to_string()
// Empty text: keep one empty line.
if string.length() == 0 {
let line = BufferLine::new(
"",
LineEnding::default(),
AttrsList::new(default_attrs),
shaping,
)
let (line2, _) = line.set_align(alignment)
return Buffer::{
..self,
lines: [line2],
scroll: Scroll::default(),
redraw: true,
dirty_relayout: false,
dirty_tab_shape: false,
dirty_text_set: true,
dirty_scroll: false,
}
}
let lines : Array[BufferLine] = []
let mut paras = BidiParagraphs::new(string)
while true {
let start = paras.pos
match paras.next() {
None => break
Some((next, para)) => {
paras = next
let end0 = start + para.length()
let mut attrs_list = AttrsList::new(default_attrs)
for si in 0.. start { s_start } else { start }
let isect_end = if s_end < end0 { s_end } else { end0 }
if isect_start < isect_end && attrs != default_attrs {
attrs_list = attrs_list.add_span(
isect_start - start,
isect_end - start,
attrs,
)
}
}
let line = BufferLine::new(
para,
LineEnding::default(),
attrs_list,
shaping,
)
let (line2, _) = line.set_align(alignment)
lines.push(line2)
}
}
}
Buffer::{
..self,
lines,
scroll: Scroll::default(),
redraw: true,
dirty_relayout: false,
dirty_tab_shape: false,
dirty_text_set: true,
dirty_scroll: false,
}
}
///|
/// Shape all lines using a provided FontSystem (best-effort).
///
/// This is a coarse-grained helper for clients that do not want to manage
/// per-line shaping manually yet.
pub fn Buffer::shape_all_with_font_system(
self : Buffer,
font_system : FontSystem,
) -> Buffer {
let resolved = match buffer_resolve_dirty(self) {
None => self
Some(v) => v
}
let lines : Array[BufferLine] = []
for line in resolved.lines {
lines.push(line.shape_with_font_system(font_system, resolved.tab_width))
}
Buffer::{ ..resolved, lines, }
}
///|
/// Layout all lines using a provided FontSystem (best-effort).
pub fn Buffer::layout_all_with_font_system(
self : Buffer,
font_system : FontSystem,
) -> Buffer {
let resolved = match buffer_resolve_dirty(self) {
None => self
Some(v) => v
}
let lines : Array[BufferLine] = []
for line in resolved.lines {
lines.push(
line.layout_with_font_system(
font_system,
resolved.metrics.font_size,
resolved.width_opt,
resolved.wrap,
resolved.ellipsize,
resolved.monospace_width,
resolved.tab_width,
resolved.hinting,
),
)
}
Buffer::{ ..resolved, lines, }
}
///|
/// Layout all lines using the built-in shaping (no font system).
pub fn Buffer::layout_all(self : Buffer) -> Buffer {
let resolved = match buffer_resolve_dirty(self) {
None => self
Some(v) => v
}
let lines : Array[BufferLine] = []
for line in resolved.lines {
lines.push(
line.layout(
resolved.metrics.font_size,
resolved.width_opt,
resolved.wrap,
resolved.ellipsize,
resolved.monospace_width,
resolved.tab_width,
resolved.hinting,
),
)
}
Buffer::{ ..resolved, lines, }
}
///|
pub fn Buffer::line_shape(
self : Buffer,
font_system : FontSystem,
line_i : Int,
) -> ShapeLine? {
if line_i < 0 || line_i >= self.lines.length() {
return None
}
self.lines[line_i]
.shape_with_font_system(font_system, self.tab_width)
.shape_opt()
}
///|
pub fn Buffer::line_layout(
self : Buffer,
font_system : FontSystem,
line_i : Int,
) -> Array[LayoutLine]? {
if line_i < 0 || line_i >= self.lines.length() {
return None
}
self.lines[line_i]
.layout_with_font_system(
font_system,
self.metrics.font_size,
self.width_opt,
self.wrap,
self.ellipsize,
self.monospace_width,
self.tab_width,
self.hinting,
)
.layout_opt()
}
///|
pub fn Buffer::shape_until_scroll(
self : Buffer,
font_system : FontSystem,
prune : Bool,
) -> Buffer {
let resolved = match buffer_resolve_dirty(self) {
None => return self
Some(v) => v
}
let metrics = resolved.metrics
let old_scroll = resolved.scroll
let mut scroll = resolved.scroll
let lines = resolved.lines
while true {
// Keep vertical offset non-negative by moving to previous lines.
while scroll.vertical < 0.0F {
if scroll.line > 0 {
let line_i = scroll.line - 1
let layout_opt = ensure_line_layout_cached(
lines,
line_i,
font_system,
resolved.metrics.font_size,
resolved.width_opt,
resolved.wrap,
resolved.ellipsize,
resolved.monospace_width,
resolved.tab_width,
resolved.hinting,
)
let layout_height = layout_total_height(metrics, layout_opt)
scroll = Scroll::{
..scroll,
line: line_i,
vertical: scroll.vertical + layout_height,
}
} else {
scroll = Scroll::{ ..scroll, vertical: 0.0F }
break
}
}
let scroll_start = scroll.vertical
let height_limit = if self.height_opt is Some(h) { h } else { 1.0e30F }
let scroll_end = scroll_start + height_limit
let mut total_height = 0.0F
let mut line_i = 0
while line_i < lines.length() {
if line_i < scroll.line {
if prune {
lines.set(line_i, lines[line_i].reset_shaping())
}
line_i = line_i + 1
continue
}
if total_height > scroll_end {
if prune {
lines.set(line_i, lines[line_i].reset_shaping())
line_i = line_i + 1
continue
}
break
}
let layout_opt = ensure_line_layout_cached(
lines,
line_i,
font_system,
resolved.metrics.font_size,
resolved.width_opt,
resolved.wrap,
resolved.ellipsize,
resolved.monospace_width,
resolved.tab_width,
resolved.hinting,
)
let layout_height = layout_total_height(metrics, layout_opt)
total_height = total_height + layout_height
if line_i == scroll.line && layout_height <= scroll.vertical {
scroll = Scroll::{
..scroll,
line: scroll.line + 1,
vertical: scroll.vertical - layout_height,
}
}
line_i = line_i + 1
}
if total_height < scroll_end && scroll.line > 0 {
scroll = Scroll::{
..scroll,
vertical: scroll.vertical - (scroll_end - total_height),
}
} else {
break
}
}
let redraw = resolved.redraw ||
old_scroll.line != scroll.line ||
old_scroll.vertical != scroll.vertical ||
old_scroll.horizontal != scroll.horizontal
Buffer::{ ..resolved, lines, scroll, redraw }
}
///|
pub fn Buffer::layout_cursor(
self : Buffer,
font_system : FontSystem,
cursor : Cursor,
) -> LayoutCursor? {
let layout = self.line_layout(font_system, cursor.line)
if layout is None {
return None
}
let lines = if layout is Some(v) { v } else { [] }
for p in lines.iter2() {
let layout_i = p.0
let layout_line = p.1
for gp in layout_line.glyphs.iter2() {
let glyph_i = gp.0
let glyph = gp.1
let cursor_end = Cursor::new_with_affinity(cursor.line, glyph.end, Before)
let cursor_start = Cursor::new_with_affinity(
cursor.line,
glyph.start,
After,
)
let glyph_ltr = glyph.level % 2 == 0
let cursor_left = if glyph_ltr { cursor_start } else { cursor_end }
let cursor_right = if glyph_ltr { cursor_end } else { cursor_start }
if cursor == cursor_left {
return Some(LayoutCursor::new(cursor.line, layout_i, glyph_i))
}
if cursor == cursor_right {
return Some(LayoutCursor::new(cursor.line, layout_i, glyph_i + 1))
}
}
}
Some(LayoutCursor::new(cursor.line, 0, 0))
}
///|
pub fn Buffer::shape_until_cursor(
self : Buffer,
font_system : FontSystem,
cursor : Cursor,
prune : Bool,
) -> Buffer {
let base = self.shape_until_scroll(font_system, prune)
if cursor.line < 0 || cursor.line >= base.lines.length() {
return base
}
let metrics = base.metrics
let old_scroll = base.scroll
let layout_cursor = match base.layout_cursor(font_system, cursor) {
Some(v) => v
None => return base
}
let layout = match base.line_layout(font_system, layout_cursor.line) {
Some(v) => v
None => return base
}
if layout_cursor.layout < 0 || layout_cursor.layout >= layout.length() {
return base
}
let mut layout_y = 0.0F
for i in 0.. layout_cursor.line ||
(scroll.line == layout_cursor.line && scroll.vertical > layout_y) {
scroll = Scroll::{ ..scroll, line: layout_cursor.line, vertical: layout_y }
} else if base.height_opt is Some(height) {
let mut line_i = layout_cursor.line
if line_i <= scroll.line {
if total_height > height + scroll.vertical {
scroll = Scroll::{ ..scroll, vertical: total_height - height }
}
} else {
while line_i > scroll.line {
line_i = line_i - 1
match base.line_layout(font_system, line_i) {
Some(line_layout) =>
for layout_line in line_layout {
let line_height = if layout_line.line_height_opt is Some(v) {
v
} else {
metrics.line_height
}
total_height = total_height + line_height
}
None => total_height = total_height + metrics.line_height
}
if total_height > height + scroll.vertical {
scroll = Scroll::{
..scroll,
line: line_i,
vertical: total_height - height,
}
}
}
}
}
let mut buffer = if scroll.line != base.scroll.line ||
scroll.vertical != base.scroll.vertical ||
scroll.horizontal != base.scroll.horizontal {
Buffer::{ ..base, scroll, redraw: true, dirty_scroll: true }
} else {
base
}
buffer = buffer.shape_until_scroll(font_system, prune)
match buffer.layout_cursor(font_system, cursor) {
Some(layout_cursor2) =>
match buffer.line_layout(font_system, layout_cursor2.line) {
Some(layout_lines) =>
match layout_lines.get(layout_cursor2.layout) {
Some(layout_line) => {
let glyph_opt = match
layout_line.glyphs.get(layout_cursor2.glyph) {
Some(v) => Some(v)
None =>
if layout_line.glyphs.length() > 0 {
Some(layout_line.glyphs[layout_line.glyphs.length() - 1])
} else {
None
}
}
match glyph_opt {
Some(glyph) => {
let x_a = glyph.x
let x_b = glyph.x + glyph.w
let x_min = if x_a < x_b { x_a } else { x_b }
let x_max = if x_a > x_b { x_a } else { x_b }
if x_min < buffer.scroll.horizontal {
buffer = Buffer::{
..buffer,
scroll: Scroll::{ ..buffer.scroll, horizontal: x_min },
redraw: true,
}
}
if buffer.width_opt is Some(width) &&
x_max > buffer.scroll.horizontal + width {
buffer = Buffer::{
..buffer,
scroll: Scroll::{
..buffer.scroll,
horizontal: x_max - width,
},
redraw: true,
}
}
}
None => ()
}
}
None => ()
}
None => ()
}
None => ()
}
if old_scroll.line != buffer.scroll.line ||
old_scroll.vertical != buffer.scroll.vertical ||
old_scroll.horizontal != buffer.scroll.horizontal {
Buffer::{ ..buffer, redraw: true }
} else {
buffer
}
}
///|
/// A line of visible text for rendering.
pub struct LayoutRun {
line_i : Int
line_y : Float
line_top : Float
line_height : Float
text : String
rtl : Bool
glyphs : Array[LayoutGlyph]
decorations : Array[DecorationSpan]
line_w : Float
}
///|
fn affinity_rank(a : Affinity) -> Int {
match a {
Before => 0
After => 1
}
}
///|
fn cursor_compare(a : Cursor, b : Cursor) -> Int {
if a.line != b.line {
if a.line < b.line {
-1
} else {
1
}
} else if a.index != b.index {
if a.index < b.index {
-1
} else {
1
}
} else {
let ar = affinity_rank(a.affinity)
let br = affinity_rank(b.affinity)
if ar < br {
-1
} else if ar > br {
1
} else {
0
}
}
}
///|
fn cursor_in_range(cursor : Cursor, start : Cursor, end : Cursor) -> Bool {
cursor_compare(cursor, start) >= 0 && cursor_compare(cursor, end) <= 0
}
///|
fn run_cursor_from_glyph_left(run : LayoutRun, glyph : LayoutGlyph) -> Cursor {
if run.rtl {
Cursor::new_with_affinity(run.line_i, glyph.end, Before)
} else {
Cursor::new_with_affinity(run.line_i, glyph.start, After)
}
}
///|
fn run_cursor_from_glyph_right(run : LayoutRun, glyph : LayoutGlyph) -> Cursor {
if run.rtl {
Cursor::new_with_affinity(run.line_i, glyph.start, After)
} else {
Cursor::new_with_affinity(run.line_i, glyph.end, Before)
}
}
///|
/// Return highlighted x-span `(x_left, width)` intersecting this run.
pub fn LayoutRun::highlight(
self : LayoutRun,
cursor_start : Cursor,
cursor_end : Cursor,
) -> (Float, Float)? {
let mut x_start : Float? = None
let mut x_end : Float? = None
let rtl_factor = if self.rtl { 1.0F } else { 0.0F }
let ltr_factor = 1.0F - rtl_factor
for glyph in self.glyphs {
let cursor_left = run_cursor_from_glyph_left(self, glyph)
if cursor_in_range(cursor_left, cursor_start, cursor_end) {
let x = glyph.x + glyph.w * rtl_factor
if x_start is None {
x_start = Some(x)
}
x_end = Some(x)
}
let cursor_right = run_cursor_from_glyph_right(self, glyph)
if cursor_in_range(cursor_right, cursor_start, cursor_end) {
let x = glyph.x + glyph.w * ltr_factor
if x_start is None {
x_start = Some(x)
}
x_end = Some(x)
}
}
match (x_start, x_end) {
(Some(a), Some(b)) => {
let min_x = if a < b { a } else { b }
let max_x = if a > b { a } else { b }
Some((min_x, max_x - min_x))
}
_ => None
}
}
///|
pub fn Buffer::layout_runs(self : Buffer) -> Iter[LayoutRun] {
let runs : Array[LayoutRun] = []
let n = self.lines.length()
if n == 0 {
return runs.iter()
}
let start_line = if self.scroll.line < 0 {
0
} else if self.scroll.line >= n {
n
} else {
self.scroll.line
}
let mut line_top_acc = 0.0F
for i in start_line.. shape.rtl
None => return runs.iter()
}
let layout_lines = match line.layout_opt() {
Some(layout) => layout
None => return runs.iter()
}
for l in layout_lines {
let line_height = if l.line_height_opt is Some(h) {
h
} else {
self.metrics.line_height
}
let line_top = line_top_acc - self.scroll.vertical
let glyph_height = l.max_ascent + l.max_descent
let centering_offset = (line_height - glyph_height) / 2.0F
let line_y = line_top + centering_offset + l.max_ascent
if self.height_opt is Some(h) && line_y - l.max_ascent > h {
return runs.iter()
}
line_top_acc = line_top_acc + line_height
if line_y + l.max_descent < 0.0F {
continue
}
runs.push(LayoutRun::{
line_i: i,
line_y,
line_top,
line_height,
text: line.text(),
rtl,
glyphs: l.glyphs,
decorations: l.decorations,
line_w: l.w,
})
}
}
runs.iter()
}
///|
/// Hit-testing: map a physical x/y position to a cursor.
pub fn Buffer::hit(self : Buffer, x : Float, y : Float) -> Cursor? {
let buffer = self.layout_all()
let mut last : Cursor? = None
for run in buffer.layout_runs() {
let line_cursor = if run.glyphs.length() == 0 {
Cursor::new_with_affinity(run.line_i, 0, After)
} else {
Cursor::new_with_affinity(
run.line_i,
run.glyphs[run.glyphs.length() - 1].end,
Before,
)
}
last = Some(line_cursor)
if y >= run.line_top && y < run.line_top + run.line_height {
if run.glyphs.length() == 0 {
return Some(Cursor::new_with_affinity(run.line_i, 0, After))
}
let glyphs = run.glyphs
let mut min_start = glyphs[0].start
let mut max_end = glyphs[0].end
for g in glyphs {
if g.start < min_start {
min_start = g.start
}
if g.end > max_end {
max_end = g.end
}
}
// Clamp to glyph span.
let last_g = glyphs[glyphs.length() - 1]
if run.rtl {
if x <= glyphs[0].x {
return Some(Cursor::new_with_affinity(run.line_i, max_end, Before))
}
if x >= last_g.x + last_g.w {
return Some(Cursor::new_with_affinity(run.line_i, min_start, After))
}
} else {
if x <= glyphs[0].x {
return Some(Cursor::new_with_affinity(run.line_i, min_start, After))
}
if x >= last_g.x + last_g.w {
return Some(Cursor::new_with_affinity(run.line_i, max_end, Before))
}
}
for g in glyphs {
let mid = g.x + g.w / 2.0F
if x < mid {
if run.rtl {
return Some(Cursor::new_with_affinity(run.line_i, g.end, Before))
}
return Some(Cursor::new_with_affinity(run.line_i, g.start, After))
}
if x < g.x + g.w {
if run.rtl {
return Some(Cursor::new_with_affinity(run.line_i, g.start, After))
}
return Some(Cursor::new_with_affinity(run.line_i, g.end, Before))
}
}
return Some(line_cursor)
}
}
last
}
///|
/// Cursor motion based on current layout runs.
///
/// This uses visual lines from `layout_runs()` for vertical movement.
pub fn Buffer::cursor_motion(
self : Buffer,
cursor : Cursor,
cursor_x_opt : Int?,
motion : Motion,
) -> (Cursor, Int?)? {
// Layout to get visual line runs.
let laid_out = self.layout_all()
let buffer = Buffer::{
..laid_out,
scroll: Scroll::default(),
height_opt: None,
}
let runs : Array[LayoutRun] = []
for r in buffer.layout_runs() {
runs.push(r)
}
if runs.length() == 0 {
return None
}
fn x_for_index(run : LayoutRun, index : Int) -> Float {
let glyphs = run.glyphs
if glyphs.length() == 0 {
return 0.0F
}
// Order-independent mapping (glyphs may be in visual order, not start-index order).
let mut min_start = glyphs[0].start
let mut min_start_x0 = glyphs[0].x
let mut min_start_x1 = glyphs[0].x + glyphs[0].w
let mut max_end = glyphs[0].end
let mut max_end_x0 = glyphs[0].x
let mut max_end_x1 = glyphs[0].x + glyphs[0].w
for g in glyphs {
if g.start < min_start {
min_start = g.start
min_start_x0 = g.x
min_start_x1 = g.x + g.w
}
if g.end > max_end {
max_end = g.end
max_end_x0 = g.x
max_end_x1 = g.x + g.w
}
if index == g.start {
return if run.rtl { g.x + g.w } else { g.x }
}
if index == g.end {
return if run.rtl { g.x } else { g.x + g.w }
}
if index > g.start && index < g.end {
let span = g.end - g.start
let off = index - g.start
let t = Float::from_double(off.to_double() / span.to_double())
return if run.rtl { g.x + g.w - g.w * t } else { g.x + g.w * t }
}
}
if index <= min_start {
if run.rtl {
min_start_x1
} else {
min_start_x0
}
} else if index >= max_end {
if run.rtl {
max_end_x0
} else {
max_end_x1
}
} else {
0.0F
}
}
fn index_for_x(run : LayoutRun, x : Float) -> Int {
if run.glyphs.length() == 0 {
return 0
}
let glyphs = run.glyphs
// Clamp to the visual extents.
let mut min_x = glyphs[0].x
let mut min_x_start = glyphs[0].start
let mut max_x = glyphs[0].x + glyphs[0].w
let mut max_x_end = glyphs[0].end
for g in glyphs {
if g.x < min_x {
min_x = g.x
min_x_start = g.start
}
if g.x + g.w > max_x {
max_x = g.x + g.w
max_x_end = g.end
}
}
if x <= min_x {
return if run.rtl { max_x_end } else { min_x_start }
}
if x >= max_x {
return if run.rtl { min_x_start } else { max_x_end }
}
for g in glyphs {
let mid = g.x + g.w / 2.0F
if x < mid {
return if run.rtl { g.end } else { g.start }
}
if x < g.x + g.w {
return if run.rtl { g.start } else { g.end }
}
}
if run.rtl {
min_x_start
} else {
max_x_end
}
}
fn int_from_float(v : Float) -> Int {
v.to_double().to_int()
}
fn find_run_idx(cursor : Cursor) -> Int? {
let mut cur_run_idx : Int? = None
for i in 0.. max_end {
max_end = g.end
}
}
if cursor.index >= min_start {
if cursor.index < max_end {
cur_run_idx = Some(i)
break
}
if cursor.index == max_end {
// Prefer the next run if this is a wrap boundary.
if i + 1 >= runs.length() || runs[i + 1].line_i != cursor.line {
cur_run_idx = Some(i)
break
}
}
}
}
}
// If we can't map index -> run (e.g. stale cursor), fall back to first run on the line.
if cur_run_idx is None {
for i in 0.. {
let line_run_idxs : Array[Int] = []
for i in 0..= line_run_idxs.length() {
layout = line_run_idxs.length() - 1
}
let run = runs[line_run_idxs[layout]]
if run.glyphs.length() == 0 {
return Some(
(Cursor::new_with_affinity(run.line_i, 0, After), cursor_x_opt),
)
}
let mut glyph_i = layout_cursor.glyph
if glyph_i < 0 {
glyph_i = 0
}
if glyph_i < run.glyphs.length() {
let glyph = run.glyphs[glyph_i]
return Some(
(
Cursor::new_with_affinity(run.line_i, glyph.start, After),
cursor_x_opt,
),
)
}
let mut max_end = run.glyphs[0].end
for g in run.glyphs {
if g.end > max_end {
max_end = g.end
}
}
return Some(
(Cursor::new_with_affinity(run.line_i, max_end, Before), cursor_x_opt),
)
}
_ => ()
}
// Vertical movement uses visual runs.
let delta_opt : Int? = match motion {
Up => Some(-1)
Down => Some(1)
Vertical(px) => {
let lh = int_from_float(buffer.metrics.line_height)
if lh == 0 {
None
} else {
Some(px / lh)
}
}
PageUp =>
match buffer.height_opt {
None => None
Some(h) => {
let lh = int_from_float(buffer.metrics.line_height)
if lh == 0 {
None
} else {
Some(-int_from_float(h) / lh)
}
}
}
PageDown =>
match buffer.height_opt {
None => None
Some(h) => {
let lh = int_from_float(buffer.metrics.line_height)
if lh == 0 {
None
} else {
Some(int_from_float(h) / lh)
}
}
}
_ => None
}
if delta_opt is Some(delta0) {
if delta0 == 0 {
return Some((cursor, cursor_x_opt))
}
let cur_run_idx_opt = find_run_idx(cursor)
if cur_run_idx_opt is None {
return None
}
let cur_idx = if cur_run_idx_opt is Some(v) { v } else { 0 }
let cur_run = runs[cur_idx]
let cur_x = x_for_index(cur_run, cursor.index)
let desired_x = match cursor_x_opt {
None => cur_x
Some(xi) => Float::from_double(xi.to_double())
}
let target = cur_idx + delta0
if target < 0 || target >= runs.length() {
return None
}
let run = runs[target]
let new_index = index_for_x(run, desired_x)
return Some(
(
Cursor::new_with_affinity(run.line_i, new_index, cursor.affinity),
Some(int_from_float(desired_x)),
),
)
}
// Fallback: text-index motion (same as Editor's cursor_motion).
let mut c = cursor
fn move_prev_grapheme(buffer : Buffer, c0 : Cursor) -> Cursor {
let mut c = c0
if c.index > 0 {
let text = buffer.lines()[c.line].text()
let ranges = grapheme_indices_uax29(text)
let mut found = 0
for r in ranges {
if c.index > r.0 && c.index <= r.1 {
found = r.0
}
}
c = Cursor::new_with_affinity(c.line, found, Affinity::After)
} else if c.line > 0 {
let prev = c.line - 1
let prev_len = buffer.lines()[prev].text().length()
c = Cursor::new_with_affinity(prev, prev_len, Affinity::After)
}
c
}
fn move_next_grapheme(buffer : Buffer, c0 : Cursor) -> Cursor {
let mut c = c0
let len = buffer.lines()[c.line].text().length()
if c.index < len {
let text = buffer.lines()[c.line].text()
let ranges = grapheme_indices_uax29(text)
let mut found = len
for r in ranges {
if c.index >= r.0 && c.index < r.1 {
found = r.1
break
}
}
c = Cursor::new_with_affinity(c.line, found, Affinity::Before)
} else if c.line + 1 < buffer.lines().length() {
c = Cursor::new_with_affinity(c.line + 1, 0, Affinity::Before)
}
c
}
fn move_prev_word(buffer : Buffer, c0 : Cursor) -> Cursor {
let mut c = c0
let text = buffer.lines()[c.line].text()
if c.index > 0 {
let ranges = word_indices_uax29(text)
let mut found = 0
let mut i = ranges.length() - 1
while i >= 0 {
let (ws, _we) = ranges[i]
if ws < c.index {
found = ws
break
}
if i == 0 {
break
}
i = i - 1
}
c = Cursor::new_with_affinity(c.line, found, c.affinity)
} else if c.line > 0 {
let prev = c.line - 1
c = Cursor::new_with_affinity(
prev,
buffer.lines()[prev].text().length(),
c.affinity,
)
}
c
}
fn move_next_word(buffer : Buffer, c0 : Cursor) -> Cursor {
let mut c = c0
let text = buffer.lines()[c.line].text()
if c.index < text.length() {
let ranges = word_indices_uax29(text)
let mut found = text.length()
for pair in ranges {
let we = pair.1
if we > c.index {
found = we
break
}
}
c = Cursor::new_with_affinity(c.line, found, c.affinity)
} else if c.line + 1 < buffer.lines().length() {
c = Cursor::new_with_affinity(c.line + 1, 0, c.affinity)
}
c
}
match motion {
Previous => c = move_prev_grapheme(buffer, c)
Next => c = move_next_grapheme(buffer, c)
Left => {
let rtl = match buffer.lines()[c.line].shape_opt() {
None => false
Some(s) => s.rtl
}
c = if rtl {
move_next_grapheme(buffer, c)
} else {
move_prev_grapheme(buffer, c)
}
}
Right => {
let rtl = match buffer.lines()[c.line].shape_opt() {
None => false
Some(s) => s.rtl
}
c = if rtl {
move_prev_grapheme(buffer, c)
} else {
move_next_grapheme(buffer, c)
}
}
PreviousWord => c = move_prev_word(buffer, c)
NextWord => c = move_next_word(buffer, c)
LeftWord => {
let rtl = match buffer.lines()[c.line].shape_opt() {
None => false
Some(s) => s.rtl
}
c = if rtl {
move_next_word(buffer, c)
} else {
move_prev_word(buffer, c)
}
}
RightWord => {
let rtl = match buffer.lines()[c.line].shape_opt() {
None => false
Some(s) => s.rtl
}
c = if rtl {
move_prev_word(buffer, c)
} else {
move_next_word(buffer, c)
}
}
Home =>
match find_run_idx(c) {
None => c = Cursor::new_with_affinity(c.line, 0, After)
Some(idx) => {
let run = runs[idx]
if run.glyphs.length() == 0 {
c = Cursor::new_with_affinity(c.line, 0, After)
} else {
let mut min_start = run.glyphs[0].start
for g in run.glyphs {
if g.start < min_start {
min_start = g.start
}
}
c = Cursor::new_with_affinity(c.line, min_start, After)
}
}
}
End =>
match find_run_idx(c) {
None =>
c = Cursor::new_with_affinity(
c.line,
buffer.lines()[c.line].text().length(),
Before,
)
Some(idx) => {
let run = runs[idx]
if run.glyphs.length() == 0 {
c = Cursor::new_with_affinity(c.line, 0, Before)
} else {
let mut max_end = run.glyphs[0].end
for g in run.glyphs {
if g.end > max_end {
max_end = g.end
}
}
c = Cursor::new_with_affinity(c.line, max_end, Before)
}
}
}
SoftHome => {
let text = buffer.lines()[c.line].text()
// Upstream `cosmic-text` behavior: always jump to the first non-whitespace
// character on the line, or 0 if the line is all whitespace.
let mut target = 0
for p in text.iter2() {
let idx = p.0
let ch = p.1
if !ch.is_whitespace() {
target = idx
break
}
}
c = Cursor::new_with_affinity(c.line, target, c.affinity)
}
BufferStart => c = Cursor::new_with_affinity(0, 0, c.affinity)
BufferEnd => {
let last = buffer.lines().length() - 1
c = Cursor::new_with_affinity(
last,
buffer.lines()[last].text().length(),
c.affinity,
)
}
ParagraphStart => c = Cursor::new_with_affinity(c.line, 0, c.affinity)
ParagraphEnd =>
c = Cursor::new_with_affinity(
c.line,
buffer.lines()[c.line].text().length(),
c.affinity,
)
GotoLine(line_i) => {
if line_i < 0 || line_i >= buffer.lines().length() {
return None
}
match find_run_idx(c) {
None => c = Cursor::new_with_affinity(line_i, 0, c.affinity)
Some(run_idx) => {
// Find current line-local visual layout and glyph indices.
let mut layout = 0
for i in 0.. 0 {
let mut found = false
for gi in 0.. max_end {
max_end = g.end
}
}
if c.index == max_end {
glyph = run.glyphs.length()
}
}
}
let lc = LayoutCursor::new(line_i, layout, glyph)
match
buffer.cursor_motion(c, cursor_x_opt, Motion::LayoutCursor(lc)) {
None => return None
Some((nc, nx)) => return Some((nc, nx))
}
}
}
}
_ => ()
}
Some((c, None))
}
///|
/// Draw the buffer using a `SwashCache`.
///
/// For compatibility with upstream's legacy renderer helper, the callback is
/// called per pixel (`w = 1`, `h = 1`).
pub fn Buffer::draw_with_font_system(
self : Buffer,
font_system : FontSystem,
swash_cache : SwashCache,
text_color : Color,
f : (Int, Int, UInt, UInt, Color) -> Unit,
) -> (Buffer, SwashCache) {
let buffer = self.layout_all_with_font_system(font_system)
let renderer = LegacyRenderer::new(font_system, swash_cache, f)
for run in buffer.layout_runs() {
for glyph in run.glyphs {
let physical = glyph.physical((0.0F, run.line_y), 1.0F)
let color = if glyph.color_opt is Some(c) { c } else { text_color }
renderer.glyph(physical, color)
}
render_decoration(renderer, run, text_color)
}
(buffer, renderer.cache)
}
///|
/// Draw the buffer with a font system and swash cache.
///
/// This is a naming-aligned convenience wrapper (matches upstream's `Buffer::draw` intent).
pub fn Buffer::draw(
self : Buffer,
font_system : FontSystem,
swash_cache : SwashCache,
text_color : Color,
f : (Int, Int, UInt, UInt, Color) -> Unit,
) -> (Buffer, SwashCache) {
self.draw_with_font_system(font_system, swash_cache, text_color, f)
}