///|
/// High-level text state for GUI/editor code: a keyed font registry plus
/// layout and caret helpers built on the stable FontFace/TextLayout facades.
struct TextSystem {
loader : MemoryFontLoader
face_cache : FontFaceCache
mut font_keys : Array[String]
} derive(Debug)
///|
pub extend TextSystem with Debug::{to_repr}
///|
pub extend TextSystem with FontLoader::{load_font_data}
///|
/// Caret geometry in layout-local coordinates for one text insertion point.
struct TextCaret {
index : Int
line : Int
x : Double
y : Double
height : Double
baseline_y : Double
} derive(Eq, Debug)
///|
pub extend TextCaret with Debug::{to_repr}
///|
pub extend TextCaret with Eq::{not_equal, equal}
///|
/// Highlight geometry for one visual line of a text selection.
struct TextSelectionRect {
start_index : Int
end_index : Int
line : Int
x : Double
y : Double
width : Double
height : Double
baseline_y : Double
} derive(Eq, Debug)
///|
pub extend TextSelectionRect with Debug::{to_repr}
///|
pub extend TextSelectionRect with Eq::{not_equal, equal}
///|
/// Result of registering checked font data in a TextSystem.
struct TextSystemFontRegistration {
key : String
face : FontFace
font_data : FontData
did_replace : Bool
did_replace_cached_face : Bool
font_count : Int
} derive(Debug)
///|
pub extend TextSystemFontRegistration with Debug::{to_repr}
///|
/// Prepared text layout for GUI/editor code that needs several geometry
/// queries from the same registered font, text, and layout config.
struct TextSystemLayout {
key : String
text : String
config : LayoutConfig
face : FontFace
layout : TextLayout
} derive(Debug)
///|
pub extend TextSystemLayout with Debug::{to_repr}
///|
priv struct TextLineSourceSpan {
start_index : Int
end_index : Int
}
///|
/// Create an empty text system with an unbounded parsed-face cache.
pub fn TextSystem::new() -> TextSystem {
{
loader: MemoryFontLoader::new(),
face_cache: FontFaceCache::new(),
font_keys: [],
}
}
///|
/// Create an empty text system with an LRU parsed-face cache capacity.
pub fn TextSystem::with_font_cache_capacity(capacity : Int) -> TextSystem {
{
loader: MemoryFontLoader::new(),
face_cache: FontFaceCache::with_capacity(capacity),
font_keys: [],
}
}
///|
/// Register checked font bytes under a caller-provided key.
pub fn TextSystem::register_font_bytes(
self : TextSystem,
key : String,
data : Bytes,
font_index? : Int = 0,
) -> Result[FontFace, FontParseError] {
match self.register_font_bytes_result(key, data, font_index~) {
Ok(result) => Ok(result.get_face())
Err(error) => Err(error)
}
}
///|
/// Register checked font bytes and return registration telemetry.
pub fn TextSystem::register_font_bytes_result(
self : TextSystem,
key : String,
data : Bytes,
font_index? : Int = 0,
) -> Result[TextSystemFontRegistration, FontParseError] {
self.register_font_data_result(key, FontData::new(data, font_index~))
}
///|
/// Register checked font data under a caller-provided key.
pub fn TextSystem::register_font_data(
self : TextSystem,
key : String,
font_data : FontData,
) -> Result[FontFace, FontParseError] {
match self.register_font_data_result(key, font_data) {
Ok(result) => Ok(result.get_face())
Err(error) => Err(error)
}
}
///|
/// Register checked font data and return registration telemetry.
pub fn TextSystem::register_font_data_result(
self : TextSystem,
key : String,
font_data : FontData,
) -> Result[TextSystemFontRegistration, FontParseError] {
match FontFace::from_data(font_data) {
Ok(face) => {
let did_replace = self.loader.contains(key)
let did_replace_cached_face = self.face_cache.contains(key)
ignore(self.loader.insert(key, font_data))
if !did_replace {
self.font_keys.push(key)
}
Ok({
key,
face: self.face_cache.insert(key, face),
font_data,
did_replace,
did_replace_cached_face,
font_count: self.font_count(),
})
}
Err(error) => Err(error)
}
}
///|
/// Read checked font bytes from a file and register them under a
/// caller-provided key, caching the loaded data in this TextSystem.
pub fn TextSystem::register_font_file(
self : TextSystem,
key : String,
path : String,
font_index? : Int = 0,
) -> Result[FontFace, FontLoadError] {
match self.register_font_file_result(key, path, font_index~) {
Ok(result) => Ok(result.get_face())
Err(error) => Err(error)
}
}
///|
/// Read checked font bytes from a file, register them as in-memory font data,
/// and return registration telemetry.
pub fn TextSystem::register_font_file_result(
self : TextSystem,
key : String,
path : String,
font_index? : Int = 0,
) -> Result[TextSystemFontRegistration, FontLoadError] {
let loaded : Bytes? = Some(read_file(path)) catch { _ => None }
match loaded {
Some(data) =>
match
self.register_font_data_result(key, FontData::new(data, font_index~)) {
Ok(result) => Ok(result)
Err(error) => Err(FontParseFailed(key, error))
}
None => Err(FontFileReadFailed(key, path))
}
}
///|
/// Return the font registry key that was registered.
pub fn TextSystemFontRegistration::get_key(
self : TextSystemFontRegistration,
) -> String {
self.key
}
///|
/// Return the checked face that was registered.
pub fn TextSystemFontRegistration::get_face(
self : TextSystemFontRegistration,
) -> FontFace {
self.face
}
///|
/// Return the checked font data that was registered.
pub fn TextSystemFontRegistration::get_font_data(
self : TextSystemFontRegistration,
) -> FontData {
self.font_data
}
///|
/// Return whether registration replaced existing font data for the key.
pub fn TextSystemFontRegistration::did_replace(
self : TextSystemFontRegistration,
) -> Bool {
self.did_replace
}
///|
/// Return whether registration replaced an already-cached parsed face.
pub fn TextSystemFontRegistration::did_replace_cached_face(
self : TextSystemFontRegistration,
) -> Bool {
self.did_replace_cached_face
}
///|
/// Return the registered font-data entry count after this registration.
pub fn TextSystemFontRegistration::get_font_count(
self : TextSystemFontRegistration,
) -> Int {
self.font_count
}
///|
/// Return the number of registered font-data entries.
pub fn TextSystem::font_count(self : TextSystem) -> Int {
self.loader.len()
}
///|
/// Return registered font keys in registration order.
pub fn TextSystem::get_font_keys(self : TextSystem) -> Array[String] {
copy_text_system_font_keys(self.font_keys)
}
///|
/// Return a registered font key by registration-order index.
pub fn TextSystem::get_font_key(self : TextSystem, index : Int) -> String? {
guard index >= 0 && index < self.font_keys.length() else { return None }
Some(self.font_keys[index])
}
///|
/// Return whether font data has been registered for a key.
pub fn TextSystem::contains_font(self : TextSystem, key : String) -> Bool {
self.loader.contains(key)
}
///|
/// Return registered font data for a key.
pub fn TextSystem::get_font_data(
self : TextSystem,
key : String,
) -> Result[FontData, FontLoadError] {
FontLoader::load_font_data(self.loader, key)
}
///|
pub impl FontLoader for TextSystem with fn load_font_data(self, key) {
self.get_font_data(key)
}
///|
/// Drop one registered font-data entry and any parsed face cached for the key.
///
/// Returns whether font data was registered before removal.
pub fn TextSystem::remove_font(self : TextSystem, key : String) -> Bool {
let had_font = self.loader.contains(key)
self.loader.remove(key)
self.face_cache.remove(key)
if had_font {
self.remove_font_key(key)
}
had_font
}
///|
/// Drop registered font data and parsed face cache entries.
pub fn TextSystem::clear_fonts(self : TextSystem) -> Unit {
self.loader.clear()
self.face_cache.clear()
self.font_keys = []
}
///|
fn copy_text_system_font_keys(keys : Array[String]) -> Array[String] {
let copied = []
for key in keys {
copied.push(key)
}
copied
}
///|
fn TextSystem::remove_font_key(self : TextSystem, key : String) -> Unit {
let mut index = 0
while index < self.font_keys.length() {
if self.font_keys[index] == key {
ignore(self.font_keys.remove(index))
} else {
index += 1
}
}
}
///|
/// Return the number of parsed font faces currently cached.
pub fn TextSystem::font_face_cache_len(self : TextSystem) -> Int {
self.face_cache.len()
}
///|
/// Return the parsed face cache capacity, or 0 for unbounded caches.
pub fn TextSystem::font_face_cache_capacity(self : TextSystem) -> Int {
self.face_cache.capacity()
}
///|
/// Return whether the parsed face cache has an entry capacity.
pub fn TextSystem::is_font_face_cache_capacity_bounded(
self : TextSystem,
) -> Bool {
self.face_cache.is_capacity_bounded()
}
///|
/// Return whether a parsed face is cached for a registered font key.
pub fn TextSystem::contains_cached_font_face(
self : TextSystem,
key : String,
) -> Bool {
self.face_cache.contains(key)
}
///|
/// Clear parsed face cache entries while preserving registered font data.
pub fn TextSystem::clear_font_face_cache(self : TextSystem) -> Unit {
self.face_cache.clear()
}
///|
/// Return a checked face for a registered font key.
pub fn TextSystem::get_font_face(
self : TextSystem,
key : String,
) -> Result[FontFace, FontLoadError] {
match self.get_font_face_result(key) {
Ok(result) => Ok(result.get_face())
Err(error) => Err(error)
}
}
///|
/// Return a checked face for a registered font key with cache telemetry.
pub fn TextSystem::get_font_face_result(
self : TextSystem,
key : String,
) -> Result[FontFaceCacheParseResult, FontLoadError] {
self.face_cache.get_or_load_result(key, self.loader)
}
///|
/// Layout text using a registered font key.
pub fn TextSystem::layout_text(
self : TextSystem,
key : String,
text : String,
config : LayoutConfig,
) -> Result[TextLayout, FontLoadError] {
match self.prepare_layout(key, text, config) {
Ok(prepared) => Ok(prepared.get_layout())
Err(error) => Err(error)
}
}
///|
/// Prepare a reusable layout snapshot for caret, hit-test, and selection
/// geometry queries.
pub fn TextSystem::prepare_layout(
self : TextSystem,
key : String,
text : String,
config : LayoutConfig,
) -> Result[TextSystemLayout, FontLoadError] {
match self.get_font_face(key) {
Err(error) => Err(error)
Ok(face) =>
Ok({
key,
text,
config,
face,
layout: TextLayout::layout(face, text, config),
})
}
}
///|
/// Return the registered font key used by this prepared layout.
pub fn TextSystemLayout::get_key(self : TextSystemLayout) -> String {
self.key
}
///|
/// Return the source text used by this prepared layout.
pub fn TextSystemLayout::get_text(self : TextSystemLayout) -> String {
self.text
}
///|
/// Return the layout configuration used by this prepared layout.
pub fn TextSystemLayout::get_config(self : TextSystemLayout) -> LayoutConfig {
self.config
}
///|
/// Return the checked font face captured by this prepared layout.
pub fn TextSystemLayout::get_face(self : TextSystemLayout) -> FontFace {
self.face
}
///|
/// Return the computed text layout.
pub fn TextSystemLayout::get_layout(self : TextSystemLayout) -> TextLayout {
self.layout
}
///|
/// Compute caret geometry for a text insertion index from this prepared layout.
pub fn TextSystemLayout::caret_for_index(
self : TextSystemLayout,
index : Int,
) -> TextCaret {
text_caret_from_layout(self.face, self.layout, self.text, index, self.config)
}
///|
/// Compute nearest caret geometry at a layout-local position from this prepared
/// layout.
pub fn TextSystemLayout::caret_at_position(
self : TextSystemLayout,
x : Double,
y : Double,
) -> TextCaret {
text_caret_at_position_from_layout(
self.face,
self.layout,
self.text,
x,
y,
self.config,
)
}
///|
/// Compute selection highlight rectangles from this prepared layout.
pub fn TextSystemLayout::selection_rects(
self : TextSystemLayout,
start_index : Int,
end_index : Int,
) -> Array[TextSelectionRect] {
text_selection_rects_from_layout(
self.layout,
self.text,
start_index,
end_index,
self.config,
)
}
///|
/// Compute caret geometry for a text insertion index using a registered font.
pub fn TextSystem::caret_for_text(
self : TextSystem,
key : String,
text : String,
index : Int,
config : LayoutConfig,
) -> Result[TextCaret, FontLoadError] {
match self.get_font_face(key) {
Err(error) => Err(error)
Ok(face) => {
let layout = TextLayout::layout(face, text, config)
Ok(text_caret_from_layout(face, layout, text, index, config))
}
}
}
///|
/// Compute caret geometry for the nearest insertion point at a layout-local
/// position using a registered font.
pub fn TextSystem::caret_at_text_position(
self : TextSystem,
key : String,
text : String,
x : Double,
y : Double,
config : LayoutConfig,
) -> Result[TextCaret, FontLoadError] {
match self.get_font_face(key) {
Err(error) => Err(error)
Ok(face) => {
let layout = TextLayout::layout(face, text, config)
Ok(text_caret_at_position_from_layout(face, layout, text, x, y, config))
}
}
}
///|
/// Compute one highlight rectangle per visual line for a text selection range.
pub fn TextSystem::selection_rects_for_text(
self : TextSystem,
key : String,
text : String,
start_index : Int,
end_index : Int,
config : LayoutConfig,
) -> Result[Array[TextSelectionRect], FontLoadError] {
match self.get_font_face(key) {
Err(error) => Err(error)
Ok(face) => {
let layout = TextLayout::layout(face, text, config)
Ok(
text_selection_rects_from_layout(
layout, text, start_index, end_index, config,
),
)
}
}
}
///|
/// Return the clamped text insertion index.
pub fn TextCaret::get_index(self : TextCaret) -> Int {
self.index
}
///|
/// Return the zero-based visual line containing this caret.
pub fn TextCaret::get_line(self : TextCaret) -> Int {
self.line
}
///|
/// Return caret x in layout-local coordinates.
pub fn TextCaret::get_x(self : TextCaret) -> Double {
self.x
}
///|
/// Return caret top y in layout-local coordinates.
pub fn TextCaret::get_y(self : TextCaret) -> Double {
self.y
}
///|
/// Return caret rectangle height.
pub fn TextCaret::get_height(self : TextCaret) -> Double {
self.height
}
///|
/// Return the baseline y for the caret line.
pub fn TextCaret::get_baseline_y(self : TextCaret) -> Double {
self.baseline_y
}
///|
/// Return the clamped source start index covered by this selection rect.
pub fn TextSelectionRect::get_start_index(self : TextSelectionRect) -> Int {
self.start_index
}
///|
/// Return the clamped source end index covered by this selection rect.
pub fn TextSelectionRect::get_end_index(self : TextSelectionRect) -> Int {
self.end_index
}
///|
/// Return the zero-based visual line containing this selection rect.
pub fn TextSelectionRect::get_line(self : TextSelectionRect) -> Int {
self.line
}
///|
/// Return selection rect x in layout-local coordinates.
pub fn TextSelectionRect::get_x(self : TextSelectionRect) -> Double {
self.x
}
///|
/// Return selection rect top y in layout-local coordinates.
pub fn TextSelectionRect::get_y(self : TextSelectionRect) -> Double {
self.y
}
///|
/// Return selection rect width.
pub fn TextSelectionRect::get_width(self : TextSelectionRect) -> Double {
self.width
}
///|
/// Return selection rect height.
pub fn TextSelectionRect::get_height(self : TextSelectionRect) -> Double {
self.height
}
///|
/// Return the baseline y for the selection rect line.
pub fn TextSelectionRect::get_baseline_y(self : TextSelectionRect) -> Double {
self.baseline_y
}
///|
fn text_caret_from_layout(
face : FontFace,
layout : TextLayout,
text : String,
index : Int,
config : LayoutConfig,
) -> TextCaret {
let caret_index = clamp_text_caret_index(text, index)
let runs = layout.get_runs()
guard runs.length() > 0 else {
return empty_text_caret(face, caret_index, config)
}
let spans = text_line_source_spans(text, runs)
let line = text_caret_line_for_index(spans, caret_index)
let run = runs[line]
let span = spans[line]
let visible_before = visible_text_count_before(
text,
span.start_index,
caret_index.min(span.end_index),
)
let baseline_y = run.get_baseline_y()
{
index: caret_index,
line,
x: text_caret_x_for_run(run, visible_before),
y: baseline_y - layout.get_baseline(),
height: config.line_height,
baseline_y,
}
}
///|
fn text_caret_at_position_from_layout(
face : FontFace,
layout : TextLayout,
text : String,
x : Double,
y : Double,
config : LayoutConfig,
) -> TextCaret {
let runs = layout.get_runs()
guard runs.length() > 0 else { return empty_text_caret(face, 0, config) }
let hit_x = normalize_text_hit_coordinate(x)
let hit_y = normalize_text_hit_coordinate(y)
let line = text_caret_line_for_y(layout, runs, hit_y, config)
let run = runs[line]
let spans = text_line_source_spans(text, runs)
let span = spans[line]
let visible_before = text_caret_visible_count_for_x(run, hit_x)
let index = text_index_after_visible_count(
text,
span.start_index,
span.end_index,
visible_before,
)
let baseline_y = run.get_baseline_y()
{
index,
line,
x: text_caret_x_for_run(run, visible_before),
y: baseline_y - layout.get_baseline(),
height: config.line_height,
baseline_y,
}
}
///|
fn normalize_text_hit_coordinate(value : Double) -> Double {
guard value == value else { return 0.0 }
guard value - value == value - value else {
if value < 0.0 {
return 0.0 - text_hit_coordinate_limit
} else {
return text_hit_coordinate_limit
}
}
value
}
///|
let text_hit_coordinate_limit = 1.0e100
///|
fn empty_text_caret(
face : FontFace,
index : Int,
config : LayoutConfig,
) -> TextCaret {
let baseline_y = scaled_ascent(face, config)
{ index, line: 0, x: 0.0, y: 0.0, height: config.line_height, baseline_y, }
}
///|
fn clamp_text_caret_index(text : String, index : Int) -> Int {
normalize_text_caret_index(text, index.max(0).min(text.length()))
}
///|
fn normalize_text_caret_index(text : String, index : Int) -> Int {
guard index > 0 && index < text.length() else { return index }
match text.get_char(index) {
None => next_text_char_index(text, index)
Some(_) =>
match (text.get_char(index - 1), text.get_char(index)) {
(Some('\r'), Some('\n')) => next_text_char_index(text, index)
_ => index
}
}
}
///|
fn clamp_text_range(
text : String,
start_index : Int,
end_index : Int,
) -> (Int, Int) {
let start = clamp_text_caret_index(text, start_index)
let end = clamp_text_caret_index(text, end_index)
if start <= end {
(start, end)
} else {
(end, start)
}
}
///|
fn text_selection_rects_from_layout(
layout : TextLayout,
text : String,
start_index : Int,
end_index : Int,
config : LayoutConfig,
) -> Array[TextSelectionRect] {
let (range_start, range_end) = clamp_text_range(text, start_index, end_index)
guard range_start < range_end else { return [] }
let runs = layout.get_runs()
guard runs.length() > 0 else { return [] }
let spans = text_line_source_spans(text, runs)
let rects = []
for i in 0.. x {
let baseline_y = run.get_baseline_y()
rects.push({
start_index: rect_start,
end_index: rect_end,
line: i,
x,
y: baseline_y - layout.get_baseline(),
width: end_x - x,
height: config.line_height,
baseline_y,
})
}
}
}
rects
}
///|
fn scaled_ascent(face : FontFace, config : LayoutConfig) -> Double {
let metrics = face.get_metrics()
guard metrics.units_per_em > 0 else { return config.font_size }
metrics.ascent.to_double() *
config.font_size /
metrics.units_per_em.to_double()
}
///|
fn text_line_source_spans(
text : String,
runs : Array[GlyphRun],
) -> Array[TextLineSourceSpan] {
let spans = []
let mut cursor = 0
for run in runs {
let start_index = cursor
cursor = consume_visible_text(text, cursor, run.glyph_count())
cursor = consume_zero_width_text_before_line_boundary(text, cursor)
spans.push({ start_index, end_index: cursor, })
cursor = consume_line_break_at(text, cursor)
}
spans
}
///|
fn consume_visible_text(text : String, index : Int, count : Int) -> Int {
let mut cursor = index
let mut remaining = count
while cursor < text.length() && remaining > 0 {
match text.get_char(cursor) {
Some(ch) =>
if is_text_line_break(ch) {
return cursor
} else {
if is_visible_layout_char(ch) {
remaining = remaining - 1
}
cursor = next_text_char_index(text, cursor)
}
None => cursor = next_text_char_index(text, cursor)
}
}
cursor
}
///|
fn consume_line_break_at(text : String, index : Int) -> Int {
match text.get_char(index) {
Some('\r') => {
let next_index = next_text_char_index(text, index)
match text.get_char(next_index) {
Some('\n') => next_text_char_index(text, next_index)
_ => next_index
}
}
Some('\n') => next_text_char_index(text, index)
_ => index
}
}
///|
fn consume_zero_width_text_before_line_boundary(
text : String,
index : Int,
) -> Int {
let mut cursor = index
while cursor < text.length() {
match text.get_char(cursor) {
Some(ch) =>
if is_text_line_break(ch) {
return cursor
} else if is_visible_layout_char(ch) {
return cursor
} else {
cursor = next_text_char_index(text, cursor)
}
None => cursor = next_text_char_index(text, cursor)
}
}
cursor
}
///|
fn text_caret_line_for_index(
spans : Array[TextLineSourceSpan],
index : Int,
) -> Int {
guard spans.length() > 0 else { return 0 }
for i in 0.. Int {
guard runs.length() > 0 else { return 0 }
for i in 0.. Int {
let mut count = 0
let mut cursor = start_index
while cursor < end_index && cursor < text.length() {
match text.get_char(cursor) {
Some(ch) =>
if is_text_line_break(ch) {
return count
} else {
if is_visible_layout_char(ch) {
count = count + 1
}
cursor = next_text_char_index(text, cursor)
}
None => cursor = next_text_char_index(text, cursor)
}
}
count
}
///|
fn text_caret_visible_count_for_x(run : GlyphRun, x : Double) -> Int {
let glyphs = run.get_glyphs()
guard glyphs.length() > 0 else { return 0 }
for i in 0.. Int {
guard visible_count > 0 else { return start_index }
let mut remaining = visible_count
let mut cursor = start_index
while cursor < end_index && cursor < text.length() {
match text.get_char(cursor) {
Some(ch) =>
if is_text_line_break(ch) {
return cursor
} else {
if is_visible_layout_char(ch) {
remaining = remaining - 1
if remaining <= 0 {
return next_text_char_index(text, cursor)
}
}
cursor = next_text_char_index(text, cursor)
}
None => cursor = next_text_char_index(text, cursor)
}
}
end_index
}
///|
fn next_text_char_index(text : String, index : Int) -> Int {
let mut cursor = (index + 1).max(0)
while cursor < text.length() {
match text.get_char(cursor) {
Some(_) => return cursor
None => cursor = cursor + 1
}
}
text.length()
}
///|
fn text_caret_x_for_run(run : GlyphRun, visible_before : Int) -> Double {
let glyphs = run.get_glyphs()
guard glyphs.length() > 0 else { return 0.0 }
let glyph_index = visible_before.max(0).min(glyphs.length())
if glyph_index < glyphs.length() {
glyphs[glyph_index].origin_x
} else {
let last = glyphs[glyphs.length() - 1]
last.origin_x + last.get_advance_width()
}
}
///|
fn is_text_line_break(ch : Char) -> Bool {
ch is '\n' || ch is '\r'
}
///|
fn is_visible_layout_char(ch : Char) -> Bool {
!(ch is '\t') && !is_text_line_break(ch)
}