// 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.
///|
pub struct TextEntity {
id : Int
depth : Int
font_smoothing : FontSmoothing
}
///|
pub struct TextSpanRange {
start : Int
length : Int
}
///|
pub struct CosmicBuffer {
text : String
spans : Array[TextSpanRange]
font_size : Float
line_height : Float
justify : Justify
linebreak : LineBreak
max_width : Float?
max_height : Float?
}
///|
pub fn CosmicBuffer::default() -> CosmicBuffer {
CosmicBuffer::{
text: "",
spans: [],
font_size: 20.0F,
line_height: 20.0F,
justify: Justify::Left,
linebreak: LineBreak::WordBoundary,
max_width: None,
max_height: None,
}
}
///|
pub struct ComputedTextBlock {
buffer : CosmicBuffer
needs_rerender : Bool
entities : Array[TextEntity]
}
///|
pub fn ComputedTextBlock::default() -> ComputedTextBlock {
ComputedTextBlock::{
buffer: CosmicBuffer::default(),
needs_rerender: true,
entities: [],
}
}
///|
pub fn ComputedTextBlock::needs_rerender(self : ComputedTextBlock) -> Bool {
self.needs_rerender
}
///|
pub fn ComputedTextBlock::buffer(self : ComputedTextBlock) -> CosmicBuffer {
self.buffer
}
///|
pub fn ComputedTextBlock::entities(
self : ComputedTextBlock,
) -> Array[TextEntity] {
self.entities
}
///|
pub struct TextPipeline {}
///|
pub fn TextPipeline::default() -> TextPipeline {
TextPipeline::{ }
}
///|
fn build_text_entities(instance : TextInstance) -> Array[TextEntity] {
let entities : Array[TextEntity] = []
entities.push(TextEntity::{
id: instance.id,
depth: 0,
font_smoothing: instance.font.font_smoothing,
})
for span in instance.spans {
entities.push(TextEntity::{
id: span.id,
depth: 1,
font_smoothing: span.font.font_smoothing,
})
}
entities
}
///|
fn build_text_buffer(instance : TextInstance) -> CosmicBuffer {
let mut text = instance.text.content
let spans : Array[TextSpanRange] = []
let mut cursor = text.length()
spans.push(TextSpanRange::{ start: 0, length: cursor })
for span in instance.spans {
let content = span.span.content
let length = content.length()
spans.push(TextSpanRange::{ start: cursor, length })
text = text + content
cursor = cursor + length
}
let line_height = instance.line_height.eval(instance.font.font_size)
CosmicBuffer::{
text,
spans,
font_size: instance.font.font_size,
line_height,
justify: instance.layout.justify,
linebreak: instance.layout.linebreak,
max_width: instance.bounds.width,
max_height: instance.bounds.height,
}
}
///|
fn text_span_color(instance : TextInstance, span_index : Int) -> TextColor {
if span_index == 0 {
instance.color
} else {
let index = span_index - 1
if index >= 0 && index < instance.spans.length() {
instance.spans[index].color
} else {
instance.color
}
}
}
///|
let font_atlas_set_state : Ref[FontAtlasSet] = Ref::new(FontAtlasSet::default())
///|
/// Global font system state for `moon_cosmic` (engine-side shaping/wrap/BiDi).
priv struct CosmicFontSystemState {
font_system : @cosmic.FontSystem
// Map asset font handle id -> cosmic font id (best-effort: first face in the data).
handle_to_font_id : Array[(Int, Int)]
// Map cosmic font id -> asset font handle id (for rasterization/atlas selection).
font_id_to_handle_id : Array[Int]
// Cache family names for asset font handles (best-effort).
handle_to_family : Array[(Int, String)]
}
///|
let cosmic_font_system_state : Ref[CosmicFontSystemState] = Ref::new(CosmicFontSystemState::{
font_system: @cosmic.FontSystem::new(),
handle_to_font_id: [],
font_id_to_handle_id: [],
handle_to_family: [],
})
///|
fn find_int_pair_value(pairs : Array[(Int, Int)], key : Int) -> Int? {
let n = pairs.length()
for i in 0.. String? {
let n = pairs.length()
for i in 0.. Int? {
let handle_id = font.id()
let state0 = cosmic_font_system_state.val
match find_int_pair_value(state0.handle_to_font_id, handle_id) {
Some(id) => return Some(id)
None => ()
}
let bytes_opt = @asset.asset_font_read_bytes(font)
if bytes_opt is None {
return None
}
let bytes = if bytes_opt is Some(v) { v } else { return None }
let old_len = state0.font_system.fonts().length()
let font_system = state0.font_system.load_font_data(bytes)
let new_len = font_system.fonts().length()
if new_len <= old_len {
return None
}
let handle_to_font_id = state0.handle_to_font_id
handle_to_font_id.push((handle_id, old_len))
let font_id_to_handle_id = state0.font_id_to_handle_id
// `FontSystem::new()` may preload system fonts, so `font_id` is not
// guaranteed to start from 0 for user-loaded fonts. Keep an index-aligned
// mapping (font_id -> asset handle id) and use -1 as "unknown".
while font_id_to_handle_id.length() < old_len {
font_id_to_handle_id.push(-1)
}
while font_id_to_handle_id.length() < new_len {
font_id_to_handle_id.push(-1)
}
for i in old_len..= 0 && old_len < fonts.length() {
let family = fonts[old_len].family_name()
handle_to_family.push((handle_id, family))
}
}
cosmic_font_system_state.val = CosmicFontSystemState::{
font_system,
handle_to_font_id,
font_id_to_handle_id,
handle_to_family,
}
Some(old_len)
}
///|
fn cosmic_font_family_name(font : @asset.Handle[@asset.Font]) -> String? {
let handle_id = font.id()
let state0 = cosmic_font_system_state.val
match find_str_pair_value(state0.handle_to_family, handle_id) {
Some(v) => Some(v)
None => {
// Best-effort: ensure the font is loaded, then query again.
cosmic_get_or_load_font_id(font) |> ignore
find_str_pair_value(
cosmic_font_system_state.val.handle_to_family,
handle_id,
)
}
}
}
///|
fn cosmic_font_handle_id_for_font_id(font_id : Int) -> Int? {
let ids = cosmic_font_system_state.val.font_id_to_handle_id
if font_id < 0 || font_id >= ids.length() {
return None
}
let handle_id = ids[font_id]
if handle_id <= 0 {
None
} else {
Some(handle_id)
}
}
///|
fn justify_is_left(justify : Justify) -> Bool {
match justify {
Justify::Left => true
_ => false
}
}
///|
fn cosmic_physical_xy(
glyph : @cosmic.LayoutGlyph,
font_smoothing : FontSmoothing,
) -> (@cosmic.CacheKey, Int, Int, Int, Int) {
fn bin_index(bin : @cosmic.SubpixelBin) -> Int {
match bin {
@cosmic.SubpixelBin::Zero => 0
@cosmic.SubpixelBin::One => 1
@cosmic.SubpixelBin::Two => 2
@cosmic.SubpixelBin::Three => 3
}
}
// Bevy parity:
// - FontSmoothing::None rounds subpixel layout fields first, then builds
// the physical glyph from that rounded layout glyph.
// - FontSmoothing::AntiAliased uses the original layout glyph directly.
match font_smoothing {
FontSmoothing::None => {
// `moon_cosmic.LayoutGlyph` is externally read-only, so we cannot clone-and-edit
// like Bevy's Rust code. Rebuild the same rounded physical parameters directly.
let rounded_x = @cosmic.roundf(glyph.x)
let rounded_y = @cosmic.roundf(glyph.y)
let rounded_x_offset = @cosmic.roundf(glyph.x_offset)
let rounded_y_offset = @cosmic.roundf(glyph.y_offset)
let x_off = glyph.font_size * rounded_x_offset
let y_off = glyph.font_size * rounded_y_offset
let pos_x = rounded_x + x_off
let pos_y = @cosmic.truncf(rounded_y - y_off)
let (cache_key, phys_x, phys_y) = @cosmic.CacheKey::new(
glyph.font_id,
glyph.glyph_id,
glyph.font_size,
(pos_x, pos_y),
glyph.font_weight,
glyph.cache_key_flags,
)
(
cache_key,
phys_x,
phys_y,
bin_index(cache_key.x_bin()),
bin_index(cache_key.y_bin()),
)
}
FontSmoothing::AntiAliased => {
let physical = glyph.physical((0.0F, 0.0F), 1.0F)
let cache_key = physical.cache_key
(
cache_key,
physical.x,
physical.y,
bin_index(cache_key.x_bin()),
bin_index(cache_key.y_bin()),
)
}
}
}
///|
fn text_update_layout(instance : TextInstance) -> TextInstance {
if !instance.computed.needs_rerender {
return instance
}
let buffer = build_text_buffer(instance)
let entities = build_text_entities(instance)
// Ensure all fonts used by this text block are loaded into the cosmic font system.
//
// Important: When font bytes are not yet available (async host load),
// `cosmic_get_or_load_font_id` returns None and the layout phase may produce
// zero glyphs. We must keep `needs_rerender=true` in that case so the text
// retries once assets become available.
let mut missing_glyph = false
if cosmic_get_or_load_font_id(instance.font.font) is None {
missing_glyph = true
}
for span in instance.spans {
if cosmic_get_or_load_font_id(span.font.font) is None {
missing_glyph = true
}
}
let font_system = cosmic_font_system_state.val.font_system
// Build rich text spans for cosmic buffer.
let spans : Array[(String, @cosmic.Attrs)] = []
let base_metrics = @cosmic.Metrics::new(buffer.font_size, buffer.line_height)
let base_family = cosmic_font_family_name(instance.font.font)
let mut base_attrs = @cosmic.Attrs::with_metadata(0).metrics(base_metrics)
if base_family is Some(name) && name.length() > 0 {
base_attrs = base_attrs.family(@cosmic.Family::Name(name))
}
spans.push((instance.text.content, base_attrs))
for i in 0.. 0 {
attrs = attrs.family(@cosmic.Family::Name(name))
}
spans.push((sp.span.content, attrs))
}
let wrap = match buffer.linebreak {
LineBreak::WordBoundary => @cosmic.Wrap::Word
LineBreak::AnyCharacter => @cosmic.Wrap::Glyph
LineBreak::WordOrCharacter => @cosmic.Wrap::WordOrGlyph
LineBreak::NoWrap => @cosmic.Wrap::None
}
let align = match buffer.justify {
Justify::Left => @cosmic.Align::Left
Justify::Center => @cosmic.Align::Center
Justify::Right => @cosmic.Align::Right
Justify::Justified => @cosmic.Align::Justified
}
let mut cosmic_buffer = @cosmic.Buffer::new_empty(base_metrics)
cosmic_buffer = cosmic_buffer.set_wrap(wrap)
cosmic_buffer = cosmic_buffer.set_size(buffer.max_width, buffer.max_height)
cosmic_buffer = cosmic_buffer.set_rich_text(
spans,
base_attrs,
@cosmic.Shaping::Advanced,
Some(align),
)
// Layout pass (best-effort).
cosmic_buffer = cosmic_buffer.layout_all_with_font_system(font_system)
// Workaround for alignment not working for unbounded text (bevy parity).
if buffer.max_width is None && !justify_is_left(buffer.justify) {
let mut w = 0.0F
for run in cosmic_buffer.layout_runs() {
if run.line_w > w {
w = run.line_w
}
}
cosmic_buffer = cosmic_buffer.set_size(Some(w), buffer.max_height)
cosmic_buffer = cosmic_buffer.layout_all_with_font_system(font_system)
}
// First pass: compute the "box size" used for layout and optional y-axis flip.
// Mirrors Bevy `buffer_dimensions` (max line width, sum of line heights, ceil).
let mut box_w_raw = 0.0F
let mut box_h_raw = 0.0F
for run in cosmic_buffer.layout_runs() {
if run.line_w > box_w_raw {
box_w_raw = run.line_w
}
box_h_raw = box_h_raw + run.line_height
}
let box_w = Float::from_double(box_w_raw.to_double().ceil())
let box_h = Float::from_double(box_h_raw.to_double().ceil())
// Second pass: produce glyphs + run geometry.
let glyphs : Array[PositionedGlyph] = []
let run_geometry : Array[RunGeometry] = []
for run in cosmic_buffer.layout_runs() {
let run_glyph_start = glyphs.length()
let mut run_top_correction = 0.0F
let mut maybe_run_geom : RunGeometry? = None
let mut end_x = 0.0F
for layout_glyph0 in run.glyphs {
let span_index = layout_glyph0.metadata
if span_index < 0 || span_index >= entities.length() {
continue
}
let font_smoothing = entities[span_index].font_smoothing
let layout_glyph = layout_glyph0
if maybe_run_geom is Some(rg) && rg.span_index != span_index {
run_geometry.push(rg)
maybe_run_geom = None
}
if maybe_run_geom is None {
let font_opt = font_system.get_font(layout_glyph.font_id)
let font_ref = match font_opt {
None => continue
Some(f) => f
}
let m = font_ref.metrics([])
let upem = m.units_per_em.to_int()
let scalar = if upem <= 0 {
0.0F
} else {
layout_glyph.font_size / Float::from_double(upem.to_double())
}
let stroke = Float::from_double(
(m.stroke_size * scalar.to_double()).round(),
)
let stroke_size = if stroke < 1.0F { 1.0F } else { stroke }
let start = if end_x > layout_glyph.x { end_x } else { layout_glyph.x }
maybe_run_geom = Some(RunGeometry::{
span_index,
bounds: @math.Rect::new(
@math.Vec2::new(start, run.line_top),
@math.Vec2::new(start, run.line_top + run.line_height),
),
strikethrough_y: @cosmic.roundf(
run.line_y -
Float::from_double(m.strikeout_offset * scalar.to_double()),
),
strikethrough_thickness: stroke_size,
underline_y: @cosmic.roundf(
run.line_y -
Float::from_double(m.underline_offset * scalar.to_double()),
),
underline_thickness: stroke_size,
})
}
end_x = layout_glyph.x + layout_glyph.w
if maybe_run_geom is Some(rg0) {
let bounds = @math.Rect::new(
rg0.bounds.min,
@math.Vec2::new(end_x, rg0.bounds.max.y),
)
maybe_run_geom = Some(RunGeometry::{ ..rg0, bounds, })
}
let (physical_key, phys_x_i, phys_y_i, subpixel_x, subpixel_y) = cosmic_physical_xy(
layout_glyph, font_smoothing,
)
let font_handle_id_opt = cosmic_font_handle_id_for_font_id(
layout_glyph.font_id,
)
if font_handle_id_opt is None {
missing_glyph = true
continue
}
let font_handle_id = if font_handle_id_opt is Some(v) {
v
} else {
continue
}
let font_handle = @asset.Handle::new(font_handle_id)
let raster_opt = font_rasterize_cache_key(
font_system, physical_key, font_smoothing,
)
if raster_opt is None {
missing_glyph = true
continue
}
let raster = if raster_opt is Some(v) { v } else { continue }
let cache_key = GlyphCacheKey::new(
physical_key.glyph_id,
subpixel_x,
subpixel_y,
physical_key.font_weight,
physical_key.flags,
)
let font_key = FontAtlasKey::new(
font_handle,
Float::reinterpret_from_uint(physical_key.font_size_bits),
font_smoothing,
)
let atlases = font_atlas_set_state.val.get_or_insert(font_key)
let atlas_info_opt = add_glyph_to_atlas(
atlases, cache_key, raster, font_smoothing,
)
if atlas_info_opt is None {
missing_glyph = true
continue
}
let atlas_info = if atlas_info_opt is Some(v) { v } else { continue }
let layout_opt = @asset.asset_get_texture_atlas_layout(
atlas_info.texture_atlas,
)
if layout_opt is None {
missing_glyph = true
continue
}
let atlas_layout = if layout_opt is Some(v) { v } else { continue }
let rect_opt = atlas_layout.textures.get(atlas_info.location.glyph_index)
if rect_opt is None {
missing_glyph = true
continue
}
let rect = if rect_opt is Some(v) { v } else { continue }
let glyph_size_u = rect.size()
let glyph_size_x = Float::from_double(glyph_size_u.x.to_double())
let glyph_size_y = Float::from_double(glyph_size_u.y.to_double())
let left = Float::from_double(atlas_info.location.offset.x.to_double())
let top = Float::from_double(atlas_info.location.offset.y.to_double())
let phys_x = Float::from_double(phys_x_i.to_double())
let phys_y = Float::from_double(phys_y_i.to_double())
let correction = if top > phys_y { top - phys_y } else { 0.0F }
if correction > run_top_correction {
run_top_correction = correction
}
let x = glyph_size_x / 2.0F + left + phys_x
let y_raw = @cosmic.roundf(run.line_y) +
phys_y -
top +
glyph_size_y / 2.0F
// Bevy parity: `BottomToTop` flips Y using the computed box height.
let y = match instance.y_axis {
YAxisOrientation::TopToBottom => y_raw
YAxisOrientation::BottomToTop => box_h - y_raw
}
glyphs.push(PositionedGlyph::{
position: @math.Vec2::new(x, y),
size: @math.Vec2::new(glyph_size_x, glyph_size_y),
atlas_info,
span_index,
line_index: run.line_i,
byte_index: layout_glyph0.start,
byte_length: layout_glyph0.end - layout_glyph0.start,
})
}
if instance.y_axis == YAxisOrientation::TopToBottom &&
run_top_correction > 0.0F {
let run_glyph_end = glyphs.length()
for i in run_glyph_start.. Unit {
let count = text_state.val.texts.length()
for i in 0.. Unit {
@render2d.render2d_clear_overlay_sprites(world)
text_debug_clear_frame()
let texts = text_state.val.texts
for instance in texts {
let layout_info = instance.layout_info
fn rotate_vec2(v : @math.Vec2, rot : Float) -> @math.Vec2 {
let cosv = @math.cos(rot)
let sinv = @math.sin(rot)
@math.Vec2::new(v.x * cosv - v.y * sinv, v.x * sinv + v.y * cosv)
}
fn absf(v : Float) -> Float {
if v < 0.0F {
0.0F - v
} else {
v
}
}
fn to_y_up(y_axis : YAxisOrientation, v : @math.Vec2) -> @math.Vec2 {
match y_axis {
YAxisOrientation::TopToBottom => @math.Vec2::new(v.x, 0.0F - v.y)
YAxisOrientation::BottomToTop => v
}
}
fn round_to_pixel(v : Float, pixel_scale : Float) -> Float {
let sf = if pixel_scale <= 0.0F { 1.0F } else { pixel_scale }
Float::from_double(
(v.to_double() * sf.to_double()).round() / sf.to_double(),
)
}
fn glyph_world_bounds(
center : @math.Vec2,
glyph_size : @math.Vec2,
scale : @math.Vec2,
rotation : Float,
) -> @math.Rect {
let half = @math.Vec2::new(
glyph_size.x * absf(scale.x) / 2.0F,
glyph_size.y * absf(scale.y) / 2.0F,
)
let p0 = rotate_vec2(
@math.Vec2::new(0.0F - half.x, 0.0F - half.y),
rotation,
)
let p1 = rotate_vec2(@math.Vec2::new(half.x, 0.0F - half.y), rotation)
let p2 = rotate_vec2(@math.Vec2::new(half.x, half.y), rotation)
let p3 = rotate_vec2(@math.Vec2::new(0.0F - half.x, half.y), rotation)
let x0 = center.x + p0.x
let y0 = center.y + p0.y
let x1 = center.x + p1.x
let y1 = center.y + p1.y
let x2 = center.x + p2.x
let y2 = center.y + p2.y
let x3 = center.x + p3.x
let y3 = center.y + p3.y
let mut min_x = x0
let mut max_x = x0
let mut min_y = y0
let mut max_y = y0
if x1 < min_x {
min_x = x1
}
if x2 < min_x {
min_x = x2
}
if x3 < min_x {
min_x = x3
}
if x1 > max_x {
max_x = x1
}
if x2 > max_x {
max_x = x2
}
if x3 > max_x {
max_x = x3
}
if y1 < min_y {
min_y = y1
}
if y2 < min_y {
min_y = y2
}
if y3 < min_y {
min_y = y3
}
if y1 > max_y {
max_y = y1
}
if y2 > max_y {
max_y = y2
}
if y3 > max_y {
max_y = y3
}
@math.Rect::new(
@math.Vec2::new(min_x, min_y),
@math.Vec2::new(max_x, max_y),
)
}
let rot = instance.transform.rotation_z()
let scale = @math.Vec2::new(
instance.transform.scale.x,
instance.transform.scale.y,
)
let base = @math.Vec2::new(
instance.transform.translation.x,
instance.transform.translation.y,
)
// Bevy parity:
// - Text2d: use `Anchor` + `TextLayoutInfo.size` to compute the local origin.
// - UI text: treat `GlobalTransform`/`Transform` as the center of the UI node, then translate
// by (-0.5 * node_size) to convert to top-left origin.
let layout_size = layout_info.size
let mut origin_offset_layout = @math.Vec2::new(0.0F, 0.0F)
if instance.ui_node_size is Some(node_size) {
origin_offset_layout = @math.Vec2::new(
0.0F - node_size.x / 2.0F,
0.0F - node_size.y / 2.0F,
)
} else {
let a = instance.anchor.as_vec()
// text_anchor = -(anchor + 0.5)
let text_anchor = @math.Vec2::new(
0.0F - (a.x + 0.5F),
0.0F - (a.y + 0.5F),
)
origin_offset_layout = @math.Vec2::new(
layout_size.x * text_anchor.x,
layout_size.y * text_anchor.y,
)
}
// Convert layout coordinates (+Y down for UI) into our world-style local (+Y up) coordinates.
let origin_offset = to_y_up(instance.y_axis, origin_offset_layout)
let origin_scaled = @math.Vec2::new(
origin_offset.x * scale.x,
origin_offset.y * scale.y,
)
let origin_rotated = rotate_vec2(origin_scaled, rot)
let mut text_origin = @math.Vec2::new(
base.x + origin_rotated.x,
base.y + origin_rotated.y,
)
// Bevy parity for UI text extraction: snap the node's top-left origin to
// physical pixels (DPR aware) before adding per-glyph offsets.
// before adding per-glyph offsets.
if instance.ui_node_size is Some(_) {
let pixel_scale = @window.window_scale_factor()
text_origin = @math.Vec2::new(
round_to_pixel(text_origin.x, pixel_scale),
round_to_pixel(text_origin.y, pixel_scale),
)
}
for glyph in layout_info.glyphs {
let atlas_info = glyph.atlas_info
let layout_opt = @asset.asset_get_texture_atlas_layout(
atlas_info.texture_atlas,
)
if layout_opt is None {
continue
}
let layout = if layout_opt is Some(value) { value } else { continue }
let index = atlas_info.location.glyph_index
if index < 0 || index >= layout.textures.length() {
continue
}
let rect = layout.textures[index]
let atlas_size = layout.size
if atlas_size.x <= 0 || atlas_size.y <= 0 {
continue
}
let atlas_width = Float::from_double(atlas_size.x.to_double())
let atlas_height = Float::from_double(atlas_size.y.to_double())
let uv_min = @math.Vec2::new(
Float::from_double(rect.min.x.to_double()) / atlas_width,
Float::from_double(rect.min.y.to_double()) / atlas_height,
)
let uv_max = @math.Vec2::new(
Float::from_double(rect.max.x.to_double()) / atlas_width,
Float::from_double(rect.max.y.to_double()) / atlas_height,
)
let glyph_pos = to_y_up(instance.y_axis, glyph.position)
let local_scaled = @math.Vec2::new(
glyph_pos.x * scale.x,
glyph_pos.y * scale.y,
)
let offset = rotate_vec2(local_scaled, rot)
let translation = @math.Vec2::new(
text_origin.x + offset.x,
text_origin.y + offset.y,
)
let transform = @math.Transform::from_xy_rotation_scale(
translation,
rot,
scale,
z=instance.transform.translation.z,
)
let glyph_bounds = glyph_world_bounds(translation, glyph.size, scale, rot)
let color = text_span_color(instance, glyph.span_index).color
if instance.shadow is Some(shadow) {
let shadow_translation = @math.Vec2::new(
translation.x + shadow.offset.x,
translation.y + shadow.offset.y,
)
let shadow_transform = @math.Transform::from_xy_rotation_scale(
shadow_translation,
instance.transform.rotation_z(),
scale,
z=instance.transform.translation.z,
)
@render2d.render2d_push_sprite_uv_scissor(
world,
atlas_info.texture,
shadow_transform,
uv_min,
uv_max,
shadow.color,
instance.layers,
instance.ui_scissor,
)
}
@render2d.render2d_push_sprite_uv_scissor(
world,
atlas_info.texture,
transform,
uv_min,
uv_max,
color,
instance.layers,
instance.ui_scissor,
)
text_debug_push_glyph(
instance.id,
glyph_bounds,
instance.ui_scissor,
atlas_info.texture.id(),
uv_min,
uv_max,
)
}
}
}
///|
pub fn[W : @render2d.HasRes_RenderState + @core.ChangeTickWorld] text_plugin(
app : @app.App[W],
) -> @app.App[W] {
app.add_system(text_update_system).add_system(text_render_system)
}