moon_cosmic

MoonBit port of cosmic-text (text shaping, layout, and editor primitives) built on moon_swash.

text
layout
bidi
shaping
swash
cosmic-text
moon add Milky2018/moon_cosmic@0.3.3
Download zip
Author
Version
0.3.3
License
Apache-2.0
Last updated
2 months ago
Downloads
32K
README

#Milky2018/moon_cosmic

MoonBit port of Rust cosmic-text (text shaping, layout, and editor primitives), built on top of Milky2018/moon_swash.

This repo includes ./cosmic-text-reference (the upstream Rust reference) and keeps behavior aligned via tests where feasible.

#Features

  • Font system (in-memory): load TTF/OTF/TTC bytes; resolve fonts by family + weight; per-codepoint fallback.
  • Shaping: Shaping::Advanced uses moon_swash/shape to produce clustered glyphs and advances; tabs snap to stops.
  • Wrapping & layout: Wrap::{None,Glyph,Word,WordOrGlyph}, Align::{Left,Right,Center,Justified,End}, hinting.
  • BiDi: paragraph iteration + embedding levels and visual reordering of glyph runs.
  • Raster cache: CacheKey + SwashCache backed by moon_swash/scale (variable wght axis support; fake-italic transform).
  • Editor primitives: Buffer, Editor, cursor/selection, hit-testing, and motion actions.

#Install

Add dependency:

moon add Milky2018/moon_cosmic

#Quick Start (Layout)

import Milky2018/moon_cosmic

fn main {
// Provide font bytes from your host app (file I/O is app-specific).
let font_bytes : Bytes = /* ... */

let font_system = FontSystem::new().load_font_data(font_bytes)
let metrics = Metrics::new(14.0F, 20.0F)

let attrs = Attrs::new().family(Family::Name("Inter"))
let attrs_list = AttrsList::new(attrs)

let buffer = Buffer::new(metrics)
.set_wrap(Wrap::WordOrGlyph)
.set_text("Hello world", attrs_list, Shaping::Advanced)
.set_size(Some(200.0F), None)
.layout_all_with_font_system(font_system)

for run in buffer.layout_runs() {
// `run.glyphs`: Array[LayoutGlyph]
// `run.line_w`: line width in pixels
ignore(run)
}
}

#Quick Start (Draw)

Buffer::draw renders via a callback (pixel-oriented for compatibility with the legacy renderer pattern).

import Milky2018/moon_cosmic

fn draw_demo(font_bytes : Bytes) -> Unit {
let font_system = FontSystem::new().load_font_data(font_bytes)
let cache = SwashCache::new()

let buffer = Buffer::new(Metrics::new(14.0F, 20.0F))
.set_text("Hi!", AttrsList::new(Attrs::new()), Shaping::Advanced)
.set_size(Some(200.0F), None)

let (_buffer, _cache) = buffer.draw(
font_system,
cache,
Color::rgb(0, 0, 0),
fn(_x, _y, _w, _h, _color) { () },
)
}

#Integration Notes

#Subpixel Cache Keys (Atlas Caching)

Downstream atlas caches should key by PhysicalGlyph.cache_key (includes x_bin/y_bin), not only (glyph_id, x, y).

#Font Collections (TTC / Multi-face)

FontSystem::load_font_data may load multiple faces from one file/collection. Each face gets its own font_id. Use FontSystem::get_font(font_id) / FontSystem::get_font_entry(font_id) to rasterize the correct face for a given LayoutGlyph.

#Dev Workflow

Common commands:

moon check # Lint/type-check moon test # Run tests moon fmt # Format moon info # Update .mbti moon info && moon fmt

#Test Font Blob

Some parity tests require real font metrics. This repo embeds Inter-Regular.ttf into src/test_fonts_test.mbt (base64).

To regenerate it (if the reference font changes):

python3 scripts/gen_test_font_mbt.py moon fmt moon test

#License

Apache-2.0. Upstream cosmic-text is MIT OR Apache-2.0; see ./cosmic-text-reference for details.

#
CacheKeyFlags

type CacheKeyFlags = UInt

Flags that change rendering.

#
Renderer

pub(open) trait Renderer {
rectangle(Self, x : Int, y : Int, w : UInt, h : UInt, color : Color) -> Unit
glyph(Self, physical_glyph : PhysicalGlyph, color : Color) -> Unit
}

Custom renderer for buffers and editors.

#
Action

pub(all) enum Action {
Motion(Motion)
Escape
Insert(Char)
Enter
Backspace
Delete
Indent
Unindent
Click(Int, Int)
DoubleClick(Int, Int)
TripleClick(Int, Int)
Drag(Int, Int)
Scroll(Float)
}

An action to perform on an editor.

#
Affinity

pub(all) enum Affinity {
Before
After
}

Whether to associate cursors placed at a boundary between runs with the run before or after it.
impl Eq for Affinity

#
Affinity::after

fn Affinity::after(self : Affinity) -> Bool

#
Affinity::before

fn Affinity::before(self : Affinity) -> Bool

#
Affinity::from_after

fn Affinity::from_after(after : Bool) -> Affinity

#
Affinity::from_before

fn Affinity::from_before(before : Bool) -> Affinity

#
Align

pub(all) enum Align {
Left
Right
Center
Justified
End
}

Align or justify

#
Attrs

pub struct Attrs {
metadata : Int
family : Family
weight : Weight
stretch :
Stretch

style :
Style

metrics_opt : CacheMetrics?
}

impl Eq for Attrs
impl Hash for Attrs

#
Attrs::family

fn Attrs::family(self : Attrs, family : Family) -> Attrs

#
Attrs::family_value

fn Attrs::family_value(self : Attrs) -> Family

#
Attrs::metadata

fn Attrs::metadata(self : Attrs) -> Int

#
Attrs::metrics

fn Attrs::metrics(self : Attrs, metrics : Metrics) -> Attrs

Set Metrics, overriding values in Buffer for this span.

#
Attrs::metrics_opt

fn Attrs::metrics_opt(self : Attrs) -> Metrics?

Return optional Metrics override.

#
Attrs::new

fn Attrs::new() -> Attrs

#
Attrs::stretch

fn Attrs::stretch(self : Attrs, stretch :
Stretch
) -> Attrs

#
Attrs::stretch_value

fn Attrs::stretch_value(self : Attrs) ->
Stretch

#
Attrs::style

fn Attrs::style(self : Attrs, style :
Style
) -> Attrs

#
Attrs::style_value

fn Attrs::style_value(self : Attrs) ->
Style

#
Attrs::weight

fn Attrs::weight(self : Attrs, weight : Weight) -> Attrs

#
Attrs::weight_value

fn Attrs::weight_value(self : Attrs) -> Weight

#
Attrs::with_metadata

fn Attrs::with_metadata(metadata : Int) -> Attrs

#
AttrsList

pub struct AttrsList {
defaults : Attrs
spans : Array[AttrsSpan]
}

impl Eq for AttrsList
impl Hash for AttrsList

#
AttrsList::add_span

fn AttrsList::add_span(self : AttrsList, start : Int, end : Int, attrs : Attrs) -> AttrsList

#
AttrsList::defaults

fn AttrsList::defaults(self : AttrsList) -> Attrs

#
AttrsList::eq

fn AttrsList::eq(self : AttrsList, other : AttrsList) -> Bool

#
AttrsList::get_span

fn AttrsList::get_span(self : AttrsList, pos : Int) -> Attrs

#
AttrsList::new

fn AttrsList::new(defaults : Attrs) -> AttrsList

#
AttrsList::spans_iter

fn AttrsList::spans_iter(self : AttrsList) -> Array[AttrsSpan]

#
AttrsList::split_off

fn AttrsList::split_off(self : AttrsList, index : Int) -> (AttrsList, AttrsList)

Splits spans at index and returns (left, right).

#
AttrsSpan

pub struct AttrsSpan {
start : Int
end : Int
attrs : Attrs
}

impl Eq for AttrsSpan
impl Hash for AttrsSpan

#
BidiParagraphs

pub struct BidiParagraphs {
text : String
pos : Int
ascii_fast : Bool
}

Paragraph iterator for bidi-aware text processing.

Upstream cosmic-text uses unicode-bidi to find paragraph boundaries. This MoonBit port approximates the behavior:
  • Fast path for simple ASCII text: split on '\n' (does not special-case CRLF).
  • Otherwise: split on BidiClass::B (Paragraph_Separator) using swash properties.

#
BidiParagraphs::iter

fn BidiParagraphs::iter(self : BidiParagraphs) -> Iter[String]

#
BidiParagraphs::new

fn BidiParagraphs::new(text : String) -> BidiParagraphs

#
BidiParagraphs::next

fn BidiParagraphs::next(self : BidiParagraphs) -> (BidiParagraphs, String)?

Returns (next, paragraph_text) if any.

#
Buffer

pub struct Buffer {
lines : Array[BufferLine]
metrics : Metrics
width_opt : Float?
height_opt : Float?
scroll : Scroll
redraw : Bool
wrap : Wrap
monospace_width : Float?
tab_width : Int
hinting : Hinting
}

Minimal Buffer port scaffold from cosmic-text/src/buffer.rs.

This MVP focuses on line splitting, cache invalidation and redraw semantics.

#
Buffer::cursor_motion

fn Buffer::cursor_motion(self : Buffer, cursor : Cursor, cursor_x_opt : Int?, motion : Motion) -> (Cursor, Int?)?

Cursor motion based on current layout runs (MVP).

This uses visual lines from layout_runs() for vertical movement.

#
Buffer::draw

fn Buffer::draw(self : Buffer, font_system : FontSystem, swash_cache : SwashCache, text_color : Color, f : (Int, Int, UInt, UInt, Color) -> Unit) -> (Buffer, SwashCache)

Draw the buffer with a font system and swash cache.

This is a naming-aligned convenience wrapper (matches upstream's Buffer::draw intent).

#
Buffer::draw_with_font_system

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)

Draw the buffer using a SwashCache.

For compatibility with upstream's legacy renderer helper, the callback is called per pixel (w = 1, h = 1).

#
Buffer::hinting

fn Buffer::hinting(self : Buffer) -> Hinting

#
Buffer::hit

fn Buffer::hit(self : Buffer, x : Float, y : Float) -> Cursor?

Hit-testing: map a physical x/y position to a cursor (MVP).

#
Buffer::layout_all

fn Buffer::layout_all(self : Buffer) -> Buffer

Layout all lines using the built-in shaping (no font system).

#
Buffer::layout_all_with_font_system

fn Buffer::layout_all_with_font_system(self : Buffer, font_system : FontSystem) -> Buffer

Layout all lines using a provided FontSystem (best-effort).

#
Buffer::layout_runs

fn Buffer::layout_runs(self : Buffer) -> Iter[LayoutRun]

#
Buffer::lines

fn Buffer::lines(self : Buffer) -> Array[BufferLine]

#
Buffer::metrics

fn Buffer::metrics(self : Buffer) -> Metrics

#
Buffer::new

fn Buffer::new(metrics : Metrics) -> Buffer

#
Buffer::new_empty

fn Buffer::new_empty(metrics : Metrics) -> Buffer

Create an empty Buffer with the provided Metrics.

Panics (fails) if metrics.line_height is zero (aligned with upstream).

#
Buffer::redraw

fn Buffer::redraw(self : Buffer) -> Bool

#
Buffer::scroll

fn Buffer::scroll(self : Buffer) -> Scroll

#
Buffer::set_hinting

fn Buffer::set_hinting(self : Buffer, hinting : Hinting) -> Buffer

#
Buffer::set_metrics

fn Buffer::set_metrics(self : Buffer, metrics : Metrics) -> Buffer

#
Buffer::set_monospace_width

fn Buffer::set_monospace_width(self : Buffer, monospace_width : Float?) -> Buffer

#
Buffer::set_redraw

fn Buffer::set_redraw(self : Buffer, redraw : Bool) -> Buffer

#
Buffer::set_rich_text

fn Buffer::set_rich_text(self : Buffer, spans : Array[(String, Attrs)], default_attrs : Attrs, shaping : Shaping, alignment : Align?) -> Buffer

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.

#
Buffer::set_scroll

fn Buffer::set_scroll(self : Buffer, scroll : Scroll) -> Buffer

#
Buffer::set_size

fn Buffer::set_size(self : Buffer, width_opt : Float?, height_opt : Float?) -> Buffer

#
Buffer::set_tab_width

fn Buffer::set_tab_width(self : Buffer, tab_width : Int) -> Buffer

#
Buffer::set_text

fn Buffer::set_text(self : Buffer, text : String, attrs_list : AttrsList, shaping : Shaping) -> Buffer

Set full text of buffer (MVP).

NOTE: attrs_list is copied per line (upstream spans are per line).

#
Buffer::set_wrap

fn Buffer::set_wrap(self : Buffer, wrap : Wrap) -> Buffer

#
Buffer::shape_all_with_font_system

fn Buffer::shape_all_with_font_system(self : Buffer, font_system : FontSystem) -> Buffer

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.

#
Buffer::size

fn Buffer::size(self : Buffer) -> (Float?, Float?)

#
Buffer::wrap

fn Buffer::wrap(self : Buffer) -> Wrap

#
BufferLine

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?
}

#
BufferLine::align

fn BufferLine::align(self : BufferLine) -> Align?

#
BufferLine::append

fn BufferLine::append(self : BufferLine, other : BufferLine) -> BufferLine

Append other to self (consumes both, returns updated line).

#
BufferLine::attrs_list

fn BufferLine::attrs_list(self : BufferLine) -> AttrsList

#
BufferLine::ending

fn BufferLine::ending(self : BufferLine) -> LineEnding

#
BufferLine::layout

fn BufferLine::layout(self : BufferLine, font_size : Float, width_opt : Float?, wrap : Wrap, match_mono_width : Float?, tab_width : Int, hinting : Hinting) -> BufferLine

#
BufferLine::layout_opt

fn BufferLine::layout_opt(self : BufferLine) -> Array[LayoutLine]?

#
BufferLine::layout_with_font_system

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

Layout using a provided FontSystem (best-effort).

#
BufferLine::new

fn BufferLine::new(text : String, ending : LineEnding, attrs_list : AttrsList, shaping : Shaping) -> BufferLine

#
BufferLine::reset

fn BufferLine::reset(self : BufferLine) -> BufferLine

#
BufferLine::reset_layout

fn BufferLine::reset_layout(self : BufferLine) -> BufferLine

#
BufferLine::reset_shaping

fn BufferLine::reset_shaping(self : BufferLine) -> BufferLine

#
BufferLine::set_align

fn BufferLine::set_align(self : BufferLine, align : Align?) -> (BufferLine, Bool)

#
BufferLine::set_ending

fn BufferLine::set_ending(self : BufferLine, ending : LineEnding) -> BufferLine

#
BufferLine::set_text

fn BufferLine::set_text(self : BufferLine, text : String, ending : LineEnding, attrs_list : AttrsList) -> (BufferLine, Bool)

#
BufferLine::shape

fn BufferLine::shape(self : BufferLine, tab_width : Int) -> BufferLine

#
BufferLine::shape_opt

fn BufferLine::shape_opt(self : BufferLine) -> ShapeLine?

#
BufferLine::shape_with_font_system

fn BufferLine::shape_with_font_system(self : BufferLine, font_system : FontSystem, tab_width : Int) -> BufferLine

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.

#
BufferLine::split_off

fn BufferLine::split_off(self : BufferLine, index : Int) -> (BufferLine, BufferLine)

Split off new line at index. Returns (left, right).

#
BufferLine::text

fn BufferLine::text(self : BufferLine) -> String

#
CacheKey

pub struct CacheKey {
font_id : Int
glyph_id : Int
font_size_bits : UInt
x_bin : SubpixelBin
y_bin : SubpixelBin
font_weight : Int
flags : UInt
}

Key for building a glyph cache.

NOTE: we keep this as a plain struct (instead of bitfields) to match the conceptual model of cosmic-text.
impl Eq for CacheKey
impl Hash for CacheKey
impl Show for CacheKey

#
CacheKey::has_disable_hinting

fn CacheKey::has_disable_hinting(self : CacheKey) -> Bool

#
CacheKey::has_fake_italic

fn CacheKey::has_fake_italic(self : CacheKey) -> Bool

#
CacheKey::has_pixel_font

fn CacheKey::has_pixel_font(self : CacheKey) -> Bool

#
CacheKey::new

fn CacheKey::new(font_id : Int, glyph_id : Int, font_size : Float, pos : (Float, Float), font_weight : Int, flags : UInt) -> (CacheKey, Int, Int)

#
CacheKey::x_bin

fn CacheKey::x_bin(self : CacheKey) -> SubpixelBin

#
CacheKey::y_bin

fn CacheKey::y_bin(self : CacheKey) -> SubpixelBin

#
CacheMetrics

pub struct CacheMetrics {
font_size_bits : UInt
line_height_bits : UInt
}

Metrics, but implementing Eq and Hash using u32 representation of f32.

This mirrors upstream cosmic-text CacheMetrics, enabling per-span metrics overrides (font size / line height) while keeping Attrs hashable.
impl Eq for CacheMetrics

#
CacheMetrics::font_size

fn CacheMetrics::font_size(self : CacheMetrics) -> Float

#
CacheMetrics::from_metrics

fn CacheMetrics::from_metrics(metrics : Metrics) -> CacheMetrics

#
CacheMetrics::line_height

fn CacheMetrics::line_height(self : CacheMetrics) -> Float

#
CacheMetrics::to_metrics

fn CacheMetrics::to_metrics(self : CacheMetrics) -> Metrics

#
Cached

pub(all) enum Cached[T] {
Empty
Unused(T)
Used(T)
}

Ported from cosmic-text/src/cached.rs (cosmic-text is dual-licensed MIT OR Apache-2.0).

MoonBit version is implemented in a functional style (returns updated Cached), but keeps the same state machine semantics as the upstream &mut self API.

#
Cached::get

fn[T] Cached::get(self : Cached[T]) -> T?

#
Cached::is_unused

fn[T] Cached::is_unused(self : Cached[T]) -> Bool

#
Cached::is_used

fn[T] Cached::is_used(self : Cached[T]) -> Bool

#
Cached::set_unused

fn[T] Cached::set_unused(self : Cached[T]) -> Cached[T]

#
Cached::set_used

fn[T] Cached::set_used(_self : Cached[T], val : T) -> Cached[T]

#
Cached::take_unused

fn[T] Cached::take_unused(self : Cached[T]) -> (Cached[T], T?)

#
Cached::take_used

fn[T] Cached::take_used(self : Cached[T]) -> (Cached[T], T?)

#
Change

pub(all) struct Change {
items : Array[ChangeItem]
}

A set of change items grouped into one logical change.

#
Change::default

fn Change::default() -> Change

#
Change::reverse

fn Change::reverse(self : Change) -> Change

#
ChangeItem

pub(all) struct ChangeItem {
start : Cursor
end : Cursor
text : String
insert : Bool
}

A unique change to an editor.

#
ChangeItem::reverse

fn ChangeItem::reverse(self : ChangeItem) -> ChangeItem

#
Color

pub struct Color {
value : UInt
}

Text color (ARGB packed, aligned with upstream).
impl Eq for Color

#
Color::a

fn Color::a(self : Color) -> Byte

#
Color::as_rgba

fn Color::as_rgba(self : Color) -> (Byte, Byte, Byte, Byte)

#
Color::b

fn Color::b(self : Color) -> Byte

#
Color::g

fn Color::g(self : Color) -> Byte

#
Color::r

fn Color::r(self : Color) -> Byte

#
Color::rgb

fn Color::rgb(r : Byte, g : Byte, b : Byte) -> Color

#
Color::rgba

fn Color::rgba(r : Byte, g : Byte, b : Byte, a : Byte) -> Color

#
Cursor

pub struct Cursor {
line : Int
index : Int
affinity : Affinity
}

Current cursor location
impl Eq for Cursor

#
Cursor::new

fn Cursor::new(line : Int, index : Int) -> Cursor

#
Cursor::new_with_affinity

fn Cursor::new_with_affinity(line : Int, index : Int, affinity : Affinity) -> Cursor

#
Editor

pub struct Editor {
buffer : Buffer
cursor : Cursor
cursor_x_opt : Int?
selection : Selection
cursor_moved : Bool
auto_indent : Bool
change_opt : Change?
}

#
Editor::action

fn Editor::action(self : Editor, action : Action) -> Editor

Perform an action on the editor (MVP: cursor motion is text-index based, no hit-testing yet).

#
Editor::apply_change

fn Editor::apply_change(self : Editor, change : Change) -> (Editor, Bool)

#
Editor::auto_indent

fn Editor::auto_indent(self : Editor) -> Bool

#
Editor::buffer

fn Editor::buffer(self : Editor) -> Buffer

#
Editor::copy_selection

fn Editor::copy_selection(self : Editor) -> String?

#
Editor::cursor

fn Editor::cursor(self : Editor) -> Cursor

#
Editor::cursor_position

fn Editor::cursor_position(self : Editor) -> (Int, Int)?

Get X and Y position of the top-left corner of the cursor (MVP).

#
Editor::delete_range

fn Editor::delete_range(self : Editor, start : Cursor, end : Cursor) -> Editor

Delete text starting at start cursor and ending at end cursor.

#
Editor::delete_selection

fn Editor::delete_selection(self : Editor) -> (Editor, Bool)

#
Editor::finish_change

fn Editor::finish_change(self : Editor) -> (Editor, Change?)

#
Editor::insert_at

fn Editor::insert_at(self : Editor, cursor : Cursor, data : String, attrs_list_opt : AttrsList?) -> (Editor, Cursor)

Insert data at the specified cursor, returning the updated editor and the new cursor.

NOTE: attrs_list_opt is applied to inserted text; when None, we use the previous character's attrs as defaults.

#
Editor::insert_string

fn Editor::insert_string(self : Editor, data : String, attrs_list_opt : AttrsList?) -> Editor

Insert string at current cursor, replacing selection if present.

#
Editor::new

fn Editor::new(buffer : Buffer) -> Editor

#
Editor::render

fn[R : Renderer] Editor::render(self : Editor, renderer : R, text_color : Color, cursor_color : Color, selection_color : Color, selected_text_color : Color) -> Unit

Render editor contents with selection and cursor highlights.

#
Editor::selection

fn Editor::selection(self : Editor) -> Selection

#
Editor::selection_bounds

fn Editor::selection_bounds(self : Editor) -> (Cursor, Cursor)?

Get the bounds of the current selection (line/word selection is expanded).

#
Editor::set_auto_indent

fn Editor::set_auto_indent(self : Editor, auto_indent : Bool) -> Editor

#
Editor::set_cursor

fn Editor::set_cursor(self : Editor, cursor : Cursor) -> Editor

#
Editor::set_selection

fn Editor::set_selection(self : Editor, selection : Selection) -> Editor

#
Editor::set_tab_width

fn Editor::set_tab_width(self : Editor, tab_width : Int) -> Editor

Set tab width in spaces. A value of 0 is ignored (matches upstream).

#
Editor::start_change

fn Editor::start_change(self : Editor) -> Editor

#
Editor::tab_width

fn Editor::tab_width(self : Editor) -> Int

#
Family

pub(all) enum Family {
Name(String)
Serif
SansSerif
Cursive
Fantasy
Monospace
}

Font family selector (mirrors fontdb::Family in cosmic-text).
impl Eq for Family
impl Hash for Family
impl Show for Family

#
FontCachedCodepointSupportInfo

type FontCachedCodepointSupportInfo

#
FontEntry

pub struct FontEntry {
id : Int
family_name : String
postscript_name : String
font :
FontRef

charmap_proxy :
CharmapProxy

attributes :
Attributes

is_monospace : Bool
not_emoji : Bool
}

Minimal font system wrapper for the MoonBit cosmic-text port.

Upstream cosmic-text uses fontdb for font discovery. In this MoonBit port we start with an in-memory font list backed by moon_swash.

#
FontEntry::attributes

#
FontEntry::charmap_proxy

#
FontEntry::family_name

fn FontEntry::family_name(self : FontEntry) -> String

#
FontEntry::id

fn FontEntry::id(self : FontEntry) -> Int

#
FontEntry::is_monospace

fn FontEntry::is_monospace(self : FontEntry) -> Bool

#
FontEntry::not_emoji

fn FontEntry::not_emoji(self : FontEntry) -> Bool

#
FontEntry::postscript_name

fn FontEntry::postscript_name(self : FontEntry) -> String

#
FontMatchAttrs

pub struct FontMatchAttrs {
family : Family
weight : Weight
stretch :
Stretch

style :
Style

}

#
FontSystem

pub struct FontSystem {
locale : String
fonts : Array[FontEntry]
font_matches_cache :
HashMap
[FontMatchAttrs, Array[Int]]
codepoint_support_cache :
HashMap
[Int, FontCachedCodepointSupportInfo]
shape_run_cache : ShapeRunCache
// private fields
}

#
FontSystem::count_supported_codepoints

fn FontSystem::count_supported_codepoints(self : FontSystem, font_id : Int, codepoints : Array[UInt]) -> Int

#
FontSystem::font_matches

fn FontSystem::font_matches(self : FontSystem, attrs : Attrs) -> Array[Int]

Get ordered font match candidates for attrs (cached).

#
FontSystem::font_matches_for_script

fn FontSystem::font_matches_for_script(self : FontSystem, attrs : Attrs, script :
Script
) -> Array[Int]

#
FontSystem::font_matches_for_scripts

fn FontSystem::font_matches_for_scripts(self : FontSystem, attrs : Attrs, scripts : Array[
Script
]) -> Array[Int]

#
FontSystem::fonts

fn FontSystem::fonts(self : FontSystem) -> Array[FontEntry]

#
FontSystem::get_font

fn FontSystem::get_font(self : FontSystem, font_id : Int) ->
FontRef
?

#
FontSystem::get_font_entry

fn FontSystem::get_font_entry(self : FontSystem, font_id : Int) -> FontEntry?

Get the full FontEntry for a font_id (useful for downstream rasterization with TTC collections).

#
FontSystem::load_font_data

fn FontSystem::load_font_data(self : FontSystem, data : Bytes) -> FontSystem

Load font data (TTF/OTF/TTC bytes) into the system.

If the bytes do not parse as a font/collection, this is a no-op.

#
FontSystem::locale

fn FontSystem::locale(self : FontSystem) -> String

#
FontSystem::new

fn FontSystem::new() -> FontSystem

#
FontSystem::new_with_locale

fn FontSystem::new_with_locale(locale : String) -> FontSystem

#
FontSystem::resolve

fn FontSystem::resolve(self : FontSystem, attrs : Attrs) -> Int?

Resolve a font ID for the given attrs (best-effort).

#
FontSystem::resolve_for_codepoint

fn FontSystem::resolve_for_codepoint(self : FontSystem, attrs : Attrs, codepoint : UInt) -> Int?

Resolve a font ID for a specific codepoint (best-effort).

This prefers the requested family (when present) and then minimizes weight difference, falling back to any loaded font that supports the codepoint.

#
FontSystem::select_family

fn FontSystem::select_family(self : FontSystem, family : Family) -> Int?

Find a font ID that matches the family selector (best-effort).

#
Hinting

pub(all) enum Hinting {
Disabled
Enabled
}

Metrics hinting strategy

#
Justify

pub(all) enum Justify {
Left
Right
Center
Justified
}

#
LayoutCursor

pub struct LayoutCursor {
line : Int
layout : Int
glyph : Int
}

The position of a cursor within a Buffer.
impl Eq for LayoutCursor

#
LayoutCursor::new

fn LayoutCursor::new(line : Int, layout : Int, glyph : Int) -> LayoutCursor

#
LayoutGlyph

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
level : Int
line_height_opt : Float?
x_offset : Float
y_offset : Float
color_opt : Color?
metadata : Int
cache_key_flags : UInt
}

A laid out glyph (MVP subset)

#
LayoutGlyph::new

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

Convenience constructor (fills optional fields with defaults).

#
LayoutGlyph::physical

fn LayoutGlyph::physical(self : LayoutGlyph, offset : (Float, Float), scale : Float) -> PhysicalGlyph

#
LayoutLine

pub struct LayoutLine {
start : Int
end : Int
w : Float
max_ascent : Float
max_descent : Float
line_height_opt : Float?
glyphs : Array[LayoutGlyph]
}

A line of laid out glyphs (MVP subset)

#
LayoutRun

pub struct LayoutRun {
line_i : Int
line_y : Float
line_top : Float
line_height : Float
text : String
rtl : Bool
glyphs : Array[LayoutGlyph]
line_w : Float
}

A line of visible text for rendering (MVP subset).

#
LegacyRenderer

pub struct LegacyRenderer {
font_system : FontSystem
cache : SwashCache
callback : (Int, Int, UInt, UInt, Color) -> Unit
}

Helper to migrate from the legacy callback-based renderer.

This matches upstream LegacyRenderer: rectangles are forwarded to the callback, and glyphs are rasterized through SwashCache::with_pixels.

#
LegacyRenderer::new

fn LegacyRenderer::new(font_system : FontSystem, cache : SwashCache, callback : (Int, Int, UInt, UInt, Color) -> Unit) -> LegacyRenderer

#
LineBreak

pub(all) enum LineBreak {
NoWrap
AnyCharacter
WordBoundary
}

Compatibility layer for Bevy-style Text2d concepts.

Upstream cosmic-text uses Wrap + Align. Bevy exposes similar concepts as line-break and justification; we provide a small mapping layer here.

#
LineEnding

pub(all) enum LineEnding {
Lf
CrLf
Cr
LfCr
None
}

Ported from cosmic-text/src/line_ending.rs (cosmic-text is dual-licensed MIT OR Apache-2.0).

#
LineEnding::as_str

fn LineEnding::as_str(self : LineEnding) -> String

#
LineEnding::default

fn LineEnding::default() -> LineEnding

Default line ending (matches upstream: LF).

#
LineIter

pub struct LineIter {
string : String
start : Int
end : Int
}

#
LineIter::new

fn LineIter::new(string : String) -> LineIter

#
LineIter::next

fn LineIter::next(self : LineIter) -> (LineIter, (Int, Int, LineEnding)?)

Returns (start, end, ending) where [start,end) is the line content range.

#
Metrics

pub struct Metrics {
font_size : Float
line_height : Float
}

Ported from cosmic-text/src/buffer.rs (cosmic-text is dual-licensed MIT OR Apache-2.0).

#
Metrics::new

fn Metrics::new(font_size : Float, line_height : Float) -> Metrics

#
Metrics::relative

fn Metrics::relative(font_size : Float, line_height_scale : Float) -> Metrics

Create metrics with given font size and calculate line height using relative scale.

#
Metrics::scale

fn Metrics::scale(self : Metrics, scale : Float) -> Metrics

Scale font size and line height.

#
Motion

pub(all) enum Motion {
LayoutCursor(LayoutCursor)
Previous
Next
Left
Right
Up
Down
Home
SoftHome
End
ParagraphStart
ParagraphEnd
PageUp
PageDown
Vertical(Int)
PreviousWord
NextWord
LeftWord
RightWord
BufferStart
BufferEnd
GotoLine(Int)
}

A motion to perform on a Cursor
impl Eq for Motion

#
PhysicalGlyph

pub struct PhysicalGlyph {
cache_key : CacheKey
x : Int
y : Int
}

A laid out glyph in physical/pixel coordinates, ready for rasterization.

#
Scroll

pub struct Scroll {
line : Int
vertical : Float
horizontal : Float
}

Scroll position in Buffer

#
Scroll::default

fn Scroll::default() -> Scroll

#
Scroll::new

fn Scroll::new(line : Int, vertical : Float, horizontal : Float) -> Scroll

#
Selection

pub(all) enum Selection {
None
Normal(Cursor)
Line(Cursor)
Word(Cursor)
}

Selection mode.
impl Eq for Selection

#
ShapeGlyph

pub struct ShapeGlyph {
start : Int
end : Int
x_advance : Float
y_advance : Float
x_offset : Float
y_offset : Float
ascent : Float
descent : Float
font_id : Int
font_weight : Int
glyph_id : Int
metadata : Int
level : Int
metrics_opt : Metrics?
}

#
ShapeGlyph::new

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::with_ascent_descent

fn ShapeGlyph::with_ascent_descent(self : ShapeGlyph, ascent : Float, descent : Float) -> ShapeGlyph

#
ShapeLine

pub struct ShapeLine {
rtl : Bool
glyphs : Array[ShapeGlyph]
}

#
ShapeLine::build

fn ShapeLine::build(_self : ShapeLine, text : String, attrs_list : AttrsList, shaping : Shaping, tab_width : Int) -> ShapeLine

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.

#
ShapeLine::build_with_font_system

fn ShapeLine::build_with_font_system(_self : ShapeLine, font_system : FontSystem, text : String, attrs_list : AttrsList, shaping : Shaping, tab_width : Int) -> ShapeLine

Build a shaped line using a FontSystem (best-effort).

Current behavior is still "Basic shaping" (1 UTF-16 code unit -> 1 glyph), but uses moon_swash charmap + glyph metrics so advances are font-derived.

#
ShapeLine::empty

fn ShapeLine::empty() -> ShapeLine

#
ShapeLine::new

fn ShapeLine::new(rtl : Bool, glyphs : Array[ShapeGlyph]) -> ShapeLine

#
ShapeRunCache

pub struct ShapeRunCache {
age : UInt64
cache :
HashMap
[ShapeRunKey, (UInt64, Array[ShapeGlyph])]
}

A helper structure for caching shape runs.

#
ShapeRunCache::clear

fn ShapeRunCache::clear(self : ShapeRunCache) -> Unit

Clear all cached runs.

#
ShapeRunCache::get

Get a cache item, updating age if found.

#
ShapeRunCache::insert

fn ShapeRunCache::insert(self : ShapeRunCache, key : ShapeRunKey, glyphs : Array[ShapeGlyph]) -> Unit

Insert a cache item with current age.

#
ShapeRunCache::new

#
ShapeRunCache::peek

Peek a cache item without updating its age.

#
ShapeRunCache::trim

fn ShapeRunCache::trim(self : ShapeRunCache, keep_ages : UInt64) -> Unit

Remove anything in the cache with an age older than keep_ages.

#
ShapeRunKey

pub struct ShapeRunKey {
text : String
default_attrs : Attrs
attrs_spans : Array[(Int, Int, Attrs)]
}

Key for caching shape runs.
impl Eq for ShapeRunKey
impl Hash for ShapeRunKey

#
ShapeRunKey::new

fn ShapeRunKey::new(text : String, attrs_list : AttrsList) -> ShapeRunKey

#
Shaping

pub(all) enum Shaping {
Basic
Advanced
}

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.

#
SubpixelBin

pub enum SubpixelBin {
Zero
One
Two
Three
}

Binning of subpixel position for cache optimization.
impl Eq for SubpixelBin
impl Hash for SubpixelBin
impl Show for SubpixelBin

#
SubpixelBin::as_float

fn SubpixelBin::as_float(self : SubpixelBin) -> Float

#
SubpixelBin::new

fn SubpixelBin::new(pos : Float) -> (Int, SubpixelBin)

Returns (integral pixel position, bin for fractional part).

Matches upstream cosmic-text binning behavior, including negative positions.

#
SwashCache

#
SwashCache::get_image

fn SwashCache::get_image(self : SwashCache, font_system : FontSystem, cache_key : CacheKey) ->
Image
?

#
SwashCache::get_image_uncached

fn SwashCache::get_image_uncached(self : SwashCache, font_system : FontSystem, cache_key : CacheKey) ->
Image
?

#
SwashCache::get_outline_commands

fn SwashCache::get_outline_commands(self : SwashCache, font_system : FontSystem, cache_key : CacheKey) -> (Array[
Vector
], Array[
Verb
])?

Create outline commands (path data) from a cache key, caching results.

This matches upstream SwashCache::get_outline_commands.

#
SwashCache::get_outline_commands_uncached

fn SwashCache::get_outline_commands_uncached(self : SwashCache, font_system : FontSystem, cache_key : CacheKey) -> (Array[
Vector
], Array[
Verb
])?

Create outline commands (path data) from a cache key, without caching results.

This matches upstream SwashCache::get_outline_commands_uncached.

#
SwashCache::new

fn SwashCache::new() -> SwashCache

#
SwashCache::with_pixels

fn SwashCache::with_pixels(self : SwashCache, font_system : FontSystem, cache_key : CacheKey, base : Color, f : (Int, Int, Color) -> Unit) -> Unit

Enumerate pixels in a cached glyph image.

#
Weight

pub struct Weight {
value : Int
}

Font weight (1..=1000). This is a simplified stand-in for fontdb::Weight.
impl Eq for Weight
impl Hash for Weight
impl Show for Weight

#
Weight::bold

fn Weight::bold() -> Weight

#
Weight::new

fn Weight::new(value : Int) -> Weight

#
Weight::normal

fn Weight::normal() -> Weight

#
Weight::value

fn Weight::value(self : Weight) -> Int

#
Wrap

pub(all) enum Wrap {
None
Glyph
Word
WordOrGlyph
}

Wrapping mode

#
CACHE_KEY_FLAG_DISABLE_HINTING

let CACHE_KEY_FLAG_DISABLE_HINTING : UInt

#
CACHE_KEY_FLAG_FAKE_ITALIC

let CACHE_KEY_FLAG_FAKE_ITALIC : UInt

#
CACHE_KEY_FLAG_PIXEL_FONT

let CACHE_KEY_FLAG_PIXEL_FONT : UInt

#
apply_align

fn apply_align(text : String, lines : Array[LayoutLine], width : Float, align : Align) -> Array[LayoutLine]

Apply Align to precomputed layout lines assuming LTR paragraph direction.

#
apply_align_with_rtl

fn apply_align_with_rtl(text : String, lines : Array[LayoutLine], width : Float, align : Align, rtl : Bool) -> Array[LayoutLine]

Apply Align to precomputed layout lines (requires a finite width).

#
cosmic_text_version

fn cosmic_text_version() -> String

Port scaffold for cosmic-text (MIT OR Apache-2.0).

The port will be implemented incrementally, keeping Bevy/cosmic-text data-flow parity.

#
floorf

fn floorf(x : Float) -> Float

#
grapheme_indices_uax29

fn grapheme_indices_uax29(s : String) -> Array[(Int, Int)]

Grapheme cluster ranges [start,end) in UTF-16 code units (UAX#29-ish).

This is sufficient for making Backspace/Delete operate on user-perceived characters.

#
justify_to_align

fn justify_to_align(j : Justify) -> Align

#
layout_ascii

fn layout_ascii(text : String, glyph_w : Float, width_opt : Float?, wrap : Wrap) -> Array[LayoutLine]

ASCII-only layout for wrap tests.

Semantics:
  • Treat each code unit as one glyph.
  • glyph_w is a monospace advance in pixels.

#
layout_from_shape

fn layout_from_shape(text : String, shape : ShapeLine, font_size : Float, cell_w : Float, width_opt : Float?, wrap : Wrap) -> Array[LayoutLine]

Layout from ShapeLine (MVP).

cell_w is the pixel width per "advance cell" (ShapeGlyph.x_advance is in cells).

#
line_break_to_wrap

fn line_break_to_wrap(lb : LineBreak) -> Wrap

#
roundf

fn roundf(x : Float) -> Float

#
truncf

fn truncf(x : Float) -> Float

#
word_indices_uax29

fn word_indices_uax29(s : String) -> Array[(Int, Int)]

Word boundary ranges based on swash Unicode analysis (UAX#29-ish).

Returns segments separated by Boundary::Word/Line/Mandatory.

#
word_indices_whitespace

fn word_indices_whitespace(s : String) -> Array[(Int, Int)]

Returns word ranges [start,end) (UTF-16 code unit indices), splitting on whitespace.

#
wrap_word_segments

fn wrap_word_segments(s : String) -> Array[(Int, Int, Bool)]

Word segments used for wrapping (cosmic-text-style).

Upstream uses unicode_linebreak::linebreaks to split into segments, then splits trailing whitespace into individual blank "words".

This helper approximates that behavior using swash boundary analysis:
  • Use moon_swash.analyze to identify UAX#14 linebreak opportunities: split on Boundary::Line or Boundary::Mandatory (ignore Boundary::Word).
  • For each segment, split trailing whitespace characters into individual blank segments (mirrors upstream's char::is_whitespace() scan).

Returns (start, end, blank) ranges in UTF-16 code units.