Skip to main content

ply_engine/
renderer.rs

1use std::f32::consts::PI;
2
3use macroquad::prelude::*;
4use macroquad::miniquad::{BlendState, BlendFactor, BlendValue, Equation};
5use crate::{math::BoundingBox, render_commands::{CornerRadii, RenderCommand, RenderCommandConfig}, shaders::{ShaderConfig, ShaderUniformValue}, elements::BorderPosition};
6
7#[cfg(feature = "text-styling")]
8use crate::text_styling::{render_styled_text, StyledSegment};
9#[cfg(feature = "text-styling")]
10use rustc_hash::FxHashMap;
11
12const PIXELS_PER_POINT: f32 = 2.0;
13
14/// On Android, the APK asset root is the `assets/` directory,
15/// so paths like `"assets/fonts/x.ttf"` need the prefix stripped.
16fn resolve_asset_path(path: &str) -> &str {
17    #[cfg(target_os = "android")]
18    if let Some(stripped) = path.strip_prefix("assets/") {
19        return stripped;
20    }
21    path
22}
23
24#[cfg(feature = "text-styling")]
25static ANIMATION_TRACKER: std::sync::LazyLock<std::sync::Mutex<FxHashMap<String, (usize, f64)>>> = std::sync::LazyLock::new(|| std::sync::Mutex::new(FxHashMap::default()));
26
27/// Represents an asset that can be loaded as a texture. This can be either a file path or embedded bytes.
28#[derive(Debug)]
29pub enum GraphicAsset {
30    Path(&'static str), // For external assets
31    Bytes{file_name: &'static str, data: &'static [u8]}, // For embedded assets
32}
33impl GraphicAsset {
34    pub fn get_name(&self) -> &str {
35        match self {
36            GraphicAsset::Path(path) => path,
37            GraphicAsset::Bytes { file_name, .. } => file_name,
38        }
39    }
40}
41
42/// Represents the source of image data for an element. Accepts static assets,
43/// runtime GPU textures, or procedural TinyVG scene graphs.
44#[derive(Debug, Clone)]
45pub enum ImageSource {
46    /// Static asset: file path or embedded bytes (existing behavior).
47    Asset(&'static GraphicAsset),
48    /// Pre-existing GPU texture handle (lightweight, Copy).
49    Texture(Texture2D),
50    /// Procedural TinyVG scene graph, rasterized at the element's layout size each frame.
51    #[cfg(feature = "tinyvg")]
52    TinyVg(tinyvg::format::Image),
53}
54
55impl ImageSource {
56    /// Returns a human-readable name for debug/logging purposes.
57    pub fn get_name(&self) -> &str {
58        match self {
59            ImageSource::Asset(ga) => ga.get_name(),
60            ImageSource::Texture(_) => "[Texture2D]",
61            #[cfg(feature = "tinyvg")]
62            ImageSource::TinyVg(_) => "[TinyVG procedural]",
63        }
64    }
65}
66
67impl From<&'static GraphicAsset> for ImageSource {
68    fn from(asset: &'static GraphicAsset) -> Self {
69        ImageSource::Asset(asset)
70    }
71}
72
73impl From<Texture2D> for ImageSource {
74    fn from(tex: Texture2D) -> Self {
75        ImageSource::Texture(tex)
76    }
77}
78
79#[cfg(feature = "tinyvg")]
80impl From<tinyvg::format::Image> for ImageSource {
81    fn from(img: tinyvg::format::Image) -> Self {
82        ImageSource::TinyVg(img)
83    }
84}
85
86/// Represents a font asset that can be loaded. This can be either a file path or embedded bytes.
87#[derive(Debug)]
88pub enum FontAsset {
89    /// A file path to a `.ttf` font file (e.g. `"assets/fonts/lexend.ttf"`).
90    Path(&'static str),
91    /// Embedded font bytes, typically via `include_bytes!`.
92    Bytes {
93        file_name: &'static str,
94        data: &'static [u8],
95    },
96}
97
98impl FontAsset {
99    /// Returns a unique key string for this asset (the path or file name).
100    pub fn key(&self) -> &'static str {
101        match self {
102            FontAsset::Path(path) => path,
103            FontAsset::Bytes { file_name, .. } => file_name,
104        }
105    }
106}
107
108/// Global FontManager. Manages font loading, caching, and eviction.
109pub static FONT_MANAGER: std::sync::LazyLock<std::sync::Mutex<FontManager>> =
110    std::sync::LazyLock::new(|| std::sync::Mutex::new(FontManager::new()));
111
112/// Manages fonts, loading and caching them as needed.
113pub struct FontManager {
114    fonts: rustc_hash::FxHashMap<&'static str, FontData>,
115    default_font: Option<DefaultFont>,
116    pub max_frames_not_used: usize,
117    font_metrics: rustc_hash::FxHashMap<(u16, usize), FontMetrics>,
118}
119struct DefaultFont {
120    key: &'static str,
121    font: Font,
122}
123#[derive(Clone, Copy)]
124struct FontMetrics {
125    pub height: f32,
126    pub baseline_offset: f32,
127}
128struct FontData {
129    pub frames_not_used: usize,
130    pub font: Font,
131}
132impl FontManager {
133    pub fn new() -> Self {
134        Self {
135            fonts: rustc_hash::FxHashMap::default(),
136            default_font: None,
137            max_frames_not_used: 60,
138            font_metrics: rustc_hash::FxHashMap::default(),
139        }
140    }
141
142    /// Get a cached font by its asset key.
143    pub fn get(&mut self, asset: &'static FontAsset) -> Option<&Font> {
144        let key = asset.key();
145        if let Some(data) = self.fonts.get_mut(key) {
146            return Some(&data.font);
147        }
148        // Fall back to the default font if the key matches
149        self.default_font.as_ref()
150            .filter(|d| d.key == key)
151            .map(|d| &d.font)
152    }
153
154    /// Get the default font (set via [`load_default`](FontManager::load_default)).
155    /// Returns `None` if no default font has been set.
156    pub fn get_default(&self) -> Option<&Font> {
157        self.default_font.as_ref().map(|d| &d.font)
158    }
159
160    fn metrics(&mut self, font_size: u16, font_asset: Option<&'static FontAsset>) -> FontMetrics {
161        let font_ptr = font_asset.map_or(0usize, |a| a as *const _ as usize);
162        let key = (font_size, font_ptr);
163        if let Some(&m) = self.font_metrics.get(&key) {
164            return m;
165        }
166        let (font, found) = if let Some(a) = font_asset {
167            let k = a.key();
168            if let Some(data) = self.fonts.get(k) {
169                (Some(&data.font), true)
170            } else if self.default_font.as_ref().map(|d| d.key) == Some(k) {
171                (self.default_font.as_ref().map(|d| &d.font), true)
172            } else {
173                (self.default_font.as_ref().map(|d| &d.font), false)
174            }
175        } else {
176            (self.default_font.as_ref().map(|d| &d.font), true)
177        };
178        let ref_dims = macroquad::text::measure_text("Xig", font, font_size, 1.0);
179        let m = FontMetrics {
180            height: ref_dims.height,
181            baseline_offset: ref_dims.offset_y,
182        };
183        if found {
184            self.font_metrics.insert(key, m);
185        }
186        m
187    }
188
189    /// Load the default font. Stored outside the cache and never evicted.
190    pub async fn load_default(asset: &'static FontAsset) {
191        let font = match asset {
192            FontAsset::Bytes { data, .. } => {
193                macroquad::text::load_ttf_font_from_bytes(data)
194                    .expect("Failed to load font from bytes")
195            }
196            FontAsset::Path(path) => {
197                let resolved = resolve_asset_path(path);
198                macroquad::text::load_ttf_font(resolved).await
199                    .unwrap_or_else(|e| panic!("Failed to load font '{}': {:?}", path, e))
200            }
201        };
202        let mut fm = FONT_MANAGER.lock().unwrap();
203        fm.default_font = Some(DefaultFont { key: asset.key(), font });
204    }
205
206    /// Ensure a font is loaded (no-op if already cached).
207    pub async fn ensure(asset: &'static FontAsset) {
208        // Check if already loaded (quick lock)
209        {
210            let mut fm = FONT_MANAGER.lock().unwrap();
211            // Already the default font?
212            if fm.default_font.as_ref().map(|d| d.key) == Some(asset.key()) {
213                return;
214            }
215            // Already in cache?
216            if let Some(data) = fm.fonts.get_mut(asset.key()) {
217                data.frames_not_used = 0;
218                return;
219            }
220        }
221
222        // Load outside the lock
223        let font = match asset {
224            FontAsset::Bytes { data, .. } => {
225                macroquad::text::load_ttf_font_from_bytes(data)
226                    .expect("Failed to load font from bytes")
227            }
228            FontAsset::Path(path) => {
229                let resolved = resolve_asset_path(path);
230                macroquad::text::load_ttf_font(resolved).await
231                    .unwrap_or_else(|e| panic!("Failed to load font '{}': {:?}", path, e))
232            }
233        };
234
235        // Insert with lock
236        let mut fm = FONT_MANAGER.lock().unwrap();
237        let key = asset.key();
238        fm.fonts.entry(key).or_insert(FontData { frames_not_used: 0, font });
239    }
240
241    pub fn clean(&mut self) {
242        self.fonts.retain(|_, data| data.frames_not_used <= self.max_frames_not_used);
243        for (_, data) in self.fonts.iter_mut() {
244            data.frames_not_used += 1;
245        }
246    }
247
248    pub fn is_loaded(asset: &'static FontAsset) -> bool {
249        let fm = FONT_MANAGER.lock().unwrap();
250        let key = asset.key();
251        if fm.default_font.as_ref().map(|d| d.key) == Some(key) {
252            return true;
253        }
254        fm.fonts.contains_key(key)
255    }
256
257    /// Returns the number of currently loaded fonts.
258    pub fn size(&self) -> usize {
259        self.fonts.len()
260    }
261}
262
263/// Global TextureManager. Can also be used outside the renderer to manage your own macroquad textures.
264pub static TEXTURE_MANAGER: std::sync::LazyLock<std::sync::Mutex<TextureManager>> = std::sync::LazyLock::new(|| std::sync::Mutex::new(TextureManager::new()));
265
266/// Manages textures, loading and unloading them as needed. No manual management needed.
267/// 
268/// You can adjust `max_frames_not_used` to control how many frames a texture can go unused before being unloaded.
269pub struct TextureManager {
270    textures: rustc_hash::FxHashMap<String, CacheEntry>,
271    pub max_frames_not_used: usize,
272}
273struct CacheEntry {
274    frames_not_used: usize,
275    owner: TextureOwner,
276}
277enum TextureOwner {
278    Standalone(Texture2D),
279    RenderTarget(RenderTarget),
280}
281
282impl TextureOwner {
283    pub fn texture(&self) -> &Texture2D {
284        match self {
285            TextureOwner::Standalone(tex) => tex,
286            TextureOwner::RenderTarget(rt) => &rt.texture,
287        }
288    }
289}
290
291impl From<Texture2D> for TextureOwner {
292    fn from(tex: Texture2D) -> Self {
293        TextureOwner::Standalone(tex)
294    }
295}
296
297impl From<RenderTarget> for TextureOwner {
298    fn from(rt: RenderTarget) -> Self {
299        TextureOwner::RenderTarget(rt)
300    }
301}
302
303impl TextureManager {
304    pub fn new() -> Self {
305        Self {
306            textures: rustc_hash::FxHashMap::default(),
307            max_frames_not_used: 1,
308        }
309    }
310
311    /// Get a cached texture by its key.
312    pub fn get(&mut self, path: &str) -> Option<&Texture2D> {
313        if let Some(entry) = self.textures.get_mut(path) {
314            entry.frames_not_used = 0;
315            Some(entry.owner.texture())
316        } else {
317            None
318        }
319    }
320
321    /// Get the cached texture by its key, or load from a file path and cache it.
322    pub async fn get_or_load(&mut self, path: &'static str) -> &Texture2D {
323        if !self.textures.contains_key(path) {
324            let texture = load_texture(resolve_asset_path(path)).await.unwrap();
325            self.textures.insert(path.to_owned(), CacheEntry { frames_not_used: 0, owner: texture.into() });
326        }
327        let entry = self.textures.get_mut(path).unwrap();
328        entry.frames_not_used = 0;
329        entry.owner.texture()
330    }
331
332    /// Get the cached texture by its key, or create it using the provided function and cache it.
333    pub fn get_or_create<F>(&mut self, key: String, create_fn: F) -> &Texture2D
334    where F: FnOnce() -> Texture2D
335    {
336        if !self.textures.contains_key(&key) {
337            let texture = create_fn();
338            self.textures.insert(key.clone(), CacheEntry { frames_not_used: 0, owner: texture.into() });
339        }
340        let entry = self.textures.get_mut(&key).unwrap();
341        entry.frames_not_used = 0;
342        entry.owner.texture()
343    }
344
345    pub async fn get_or_create_async<F, Fut>(&mut self, key: String, create_fn: F) -> &Texture2D
346    where F: FnOnce() -> Fut,
347          Fut: std::future::Future<Output = Texture2D>
348    {
349        if !self.textures.contains_key(&key) {
350            let texture = create_fn().await;
351            self.textures.insert(key.clone(), CacheEntry { frames_not_used: 0, owner: texture.into() });
352        }
353        let entry = self.textures.get_mut(&key).unwrap();
354        entry.frames_not_used = 0;
355        entry.owner.texture()
356    }
357
358    /// Cache a value with the given key. Accepts `Texture2D` or `RenderTarget`.
359    #[allow(private_bounds)]
360    pub fn cache(&mut self, key: String, value: impl Into<TextureOwner>) -> &Texture2D {
361        self.textures.insert(key.clone(), CacheEntry { frames_not_used: 0, owner: value.into() });
362        self.textures.get(&key).unwrap().owner.texture()
363    }
364
365    pub fn clean(&mut self) {
366        self.textures.retain(|_, entry| entry.frames_not_used <= self.max_frames_not_used);
367        for (_, entry) in self.textures.iter_mut() {
368            entry.frames_not_used += 1;
369        }
370    }
371
372    pub fn size(&self) -> usize {
373        self.textures.len()
374    }
375}
376
377/// Default passthrough vertex shader used for all shader effects.
378const DEFAULT_VERTEX_SHADER: &str = "#version 100
379attribute vec3 position;
380attribute vec2 texcoord;
381attribute vec4 color0;
382varying lowp vec2 uv;
383varying lowp vec4 color;
384uniform mat4 Model;
385uniform mat4 Projection;
386void main() {
387    gl_Position = Projection * Model * vec4(position, 1);
388    color = color0 / 255.0;
389    uv = texcoord;
390}
391";
392
393/// Default fragment shader as fallback.
394pub const DEFAULT_FRAGMENT_SHADER: &str = "#version 100
395precision lowp float;
396varying vec2 uv;
397varying vec4 color;
398uniform sampler2D Texture;
399void main() {
400    gl_FragColor = color;
401}
402";
403
404/// Global MaterialManager for caching compiled shader materials.
405pub static MATERIAL_MANAGER: std::sync::LazyLock<std::sync::Mutex<MaterialManager>> =
406    std::sync::LazyLock::new(|| std::sync::Mutex::new(MaterialManager::new()));
407
408/// Manages compiled GPU materials (shaders), caching them by fragment source.
409///
410/// Equivalent to `TextureManager` but for materials. The renderer creates and uses
411/// this to avoid recompiling shaders every frame.
412///
413/// Also holds a runtime shader storage (`name → source`) for [`ShaderAsset::Stored`]
414/// shaders. Update stored sources with [`set_source`](Self::set_source); the old
415/// compiled material is evicted automatically when the source changes.
416pub struct MaterialManager {
417    materials: rustc_hash::FxHashMap<std::borrow::Cow<'static, str>, MaterialData>,
418    /// Runtime shader storage: name → fragment source.
419    shader_storage: rustc_hash::FxHashMap<String, String>,
420    /// How many frames a material can go unused before being evicted.
421    pub max_frames_not_used: usize,
422}
423
424struct MaterialData {
425    pub frames_not_used: usize,
426    pub material: Material,
427}
428
429impl MaterialManager {
430    pub fn new() -> Self {
431        Self {
432            materials: rustc_hash::FxHashMap::default(),
433            shader_storage: rustc_hash::FxHashMap::default(),
434            max_frames_not_used: 60, // Keep materials longer than textures
435        }
436    }
437
438    /// Get or create a material for the given shader config.
439    /// The material is cached by fragment source string.
440    pub fn get_or_create(&mut self, config: &ShaderConfig) -> &Material {
441        let key: &str = &config.fragment;
442        if !self.materials.contains_key(key) {
443            // Derive uniform declarations from the config
444            let mut uniform_decls: Vec<UniformDesc> = vec![
445                // Auto-uniforms
446                UniformDesc::new("u_resolution", UniformType::Float2),
447                UniformDesc::new("u_position", UniformType::Float2),
448            ];
449            for u in &config.uniforms {
450                let utype = match &u.value {
451                    ShaderUniformValue::Float(_) => UniformType::Float1,
452                    ShaderUniformValue::Vec2(_) => UniformType::Float2,
453                    ShaderUniformValue::Vec3(_) => UniformType::Float3,
454                    ShaderUniformValue::Vec4(_) => UniformType::Float4,
455                    ShaderUniformValue::Int(_) => UniformType::Int1,
456                    ShaderUniformValue::Mat4(_) => UniformType::Mat4,
457                };
458                uniform_decls.push(UniformDesc::new(&u.name, utype));
459            }
460
461            let blend_pipeline_params = PipelineParams {
462                color_blend: Some(BlendState::new(
463                    Equation::Add,
464                    BlendFactor::Value(BlendValue::SourceAlpha),
465                    BlendFactor::OneMinusValue(BlendValue::SourceAlpha),
466                )),
467                alpha_blend: Some(BlendState::new(
468                    Equation::Add,
469                    BlendFactor::Value(BlendValue::SourceAlpha),
470                    BlendFactor::OneMinusValue(BlendValue::SourceAlpha),
471                )),
472                ..Default::default()
473            };
474
475            let material = load_material(
476                ShaderSource::Glsl {
477                    vertex: DEFAULT_VERTEX_SHADER,
478                    fragment: &config.fragment,
479                },
480                MaterialParams {
481                    pipeline_params: blend_pipeline_params,
482                    uniforms: uniform_decls,
483                    ..Default::default()
484                },
485            )
486            .unwrap_or_else(|e| {
487                eprintln!("Failed to compile shader material: {:?}", e);
488                // Fall back to default material 
489                load_material(
490                    ShaderSource::Glsl {
491                        vertex: DEFAULT_VERTEX_SHADER,
492                        fragment: DEFAULT_FRAGMENT_SHADER,
493                    },
494                    MaterialParams::default(),
495                )
496                .unwrap()
497            });
498
499            self.materials.insert(config.fragment.clone(), MaterialData {
500                frames_not_used: 0,
501                material,
502            });
503        }
504
505        let entry = self.materials.get_mut(key).unwrap();
506        entry.frames_not_used = 0;
507        &entry.material
508    }
509
510    /// Evict materials that haven't been used recently.
511    pub fn clean(&mut self) {
512        self.materials.retain(|_, data| data.frames_not_used <= self.max_frames_not_used);
513        for (_, data) in self.materials.iter_mut() {
514            data.frames_not_used += 1;
515        }
516    }
517
518    /// Store or update a named shader source.
519    ///
520    /// If the source changed compared to the previously stored value, the old
521    /// compiled material is evicted from the cache so the next render pass
522    /// recompiles automatically. No-ops if the source is unchanged.
523    pub fn set_source(&mut self, name: &str, fragment: &str) {
524        if let Some(old_source) = self.shader_storage.get(name) {
525            if old_source == fragment {
526                return; // Unchanged — nothing to do
527            }
528            // Evict stale material keyed by the old source
529            self.materials.remove(old_source.as_str());
530        }
531        self.shader_storage.insert(name.to_string(), fragment.to_string());
532    }
533
534    /// Look up a stored shader source by name.
535    pub fn get_source(&self, name: &str) -> Option<&str> {
536        self.shader_storage.get(name).map(String::as_str)
537    }
538}
539
540/// Update a named shader source in the global shader storage.
541///
542/// When the source changes, the previously compiled material is evicted
543/// and will be recompiled on the next render pass. No-ops if unchanged.
544///
545/// Use with [`ShaderAsset::Stored`](crate::shaders::ShaderAsset::Stored) to reference
546/// the stored source by name.
547///
548/// # Example
549/// ```rust,ignore
550/// set_shader_source("live_shader", &editor.text);
551///
552/// const LIVE: ShaderAsset = ShaderAsset::Stored("live_shader");
553/// ui.element()
554///     .effect(&LIVE, |s| s.uniform("u_time", get_time() as f32))
555///     .build();
556/// ```
557pub fn set_shader_source(name: &str, fragment: &str) {
558    MATERIAL_MANAGER.lock().unwrap().set_source(name, fragment);
559}
560
561/// Apply shader uniforms to a material, including auto-uniforms.
562fn apply_shader_uniforms(material: &Material, config: &ShaderConfig, bb: &BoundingBox) {
563    // Auto-uniforms
564    material.set_uniform("u_resolution", (bb.width, bb.height));
565    material.set_uniform("u_position", (bb.x, bb.y));
566
567    // User-defined uniforms
568    for u in &config.uniforms {
569        match &u.value {
570            ShaderUniformValue::Float(v) => material.set_uniform(&u.name, *v),
571            ShaderUniformValue::Vec2(v) => material.set_uniform(&u.name, *v),
572            ShaderUniformValue::Vec3(v) => material.set_uniform(&u.name, *v),
573            ShaderUniformValue::Vec4(v) => material.set_uniform(&u.name, *v),
574            ShaderUniformValue::Int(v) => material.set_uniform(&u.name, *v),
575            ShaderUniformValue::Mat4(v) => material.set_uniform(&u.name, *v),
576        }
577    }
578}
579
580fn ply_to_macroquad_color(ply_color: &crate::color::Color) -> Color {
581    Color {
582        r: ply_color.r / 255.0,
583        g: ply_color.g / 255.0,
584        b: ply_color.b / 255.0,
585        a: ply_color.a / 255.0,
586    }
587}
588
589/// Draws a rounded rectangle as a single triangle-fan mesh.
590/// This avoids the visual artifacts of multi-shape rendering and handles alpha correctly.
591fn draw_good_rounded_rectangle(x: f32, y: f32, w: f32, h: f32, cr: &CornerRadii, color: Color) {
592    use std::f32::consts::{FRAC_PI_2, PI};
593
594    if cr.top_left == 0.0 && cr.top_right == 0.0 && cr.bottom_left == 0.0 && cr.bottom_right == 0.0 {
595        draw_rectangle(x, y, w, h, color);
596        return;
597    }
598
599    // Generate outline vertices for the rounded rectangle
600    // Pre-allocate: each corner produces ~(FRAC_PI_2 * radius / PIXELS_PER_POINT).max(6) + 1 vertices
601    let est_verts = [cr.top_left, cr.top_right, cr.bottom_left, cr.bottom_right]
602        .iter()
603        .map(|&r| if r <= 0.0 { 1 } else { ((FRAC_PI_2 * r) / PIXELS_PER_POINT).max(6.0) as usize + 1 })
604        .sum::<usize>();
605    let mut outline: Vec<Vec2> = Vec::with_capacity(est_verts);
606
607    let add_arc = |outline: &mut Vec<Vec2>, cx: f32, cy: f32, radius: f32, start_angle: f32, end_angle: f32| {
608        if radius <= 0.0 {
609            outline.push(Vec2::new(cx, cy));
610            return;
611        }
612        let sides = ((FRAC_PI_2 * radius) / PIXELS_PER_POINT).max(6.0) as usize;
613        // Use incremental rotation to avoid per-point cos/sin
614        let step = (end_angle - start_angle) / sides as f32;
615        let step_cos = step.cos();
616        let step_sin = step.sin();
617        let mut dx = start_angle.cos() * radius;
618        let mut dy = start_angle.sin() * radius;
619        for _ in 0..=sides {
620            outline.push(Vec2::new(cx + dx, cy + dy));
621            let new_dx = dx * step_cos - dy * step_sin;
622            let new_dy = dx * step_sin + dy * step_cos;
623            dx = new_dx;
624            dy = new_dy;
625        }
626    };
627
628    // Top-left corner: arc from π to 3π/2
629    add_arc(&mut outline, x + cr.top_left, y + cr.top_left, cr.top_left,
630            PI, 3.0 * FRAC_PI_2);
631    // Top-right corner: arc from 3π/2 to 2π
632    add_arc(&mut outline, x + w - cr.top_right, y + cr.top_right, cr.top_right,
633            3.0 * FRAC_PI_2, 2.0 * PI);
634    // Bottom-right corner: arc from 0 to π/2
635    add_arc(&mut outline, x + w - cr.bottom_right, y + h - cr.bottom_right, cr.bottom_right,
636            0.0, FRAC_PI_2);
637    // Bottom-left corner: arc from π/2 to π
638    add_arc(&mut outline, x + cr.bottom_left, y + h - cr.bottom_left, cr.bottom_left,
639            FRAC_PI_2, PI);
640
641    let n = outline.len();
642    if n < 3 { return; }
643
644    let color_bytes = [
645        (color.r * 255.0) as u8,
646        (color.g * 255.0) as u8,
647        (color.b * 255.0) as u8,
648        (color.a * 255.0) as u8,
649    ];
650
651    let cx = x + w / 2.0;
652    let cy = y + h / 2.0;
653
654    let mut vertices = Vec::with_capacity(n + 1);
655    // Center vertex (index 0)
656    vertices.push(Vertex {
657        position: Vec3::new(cx, cy, 0.0),
658        uv: Vec2::new(0.5, 0.5),
659        color: color_bytes,
660        normal: Vec4::new(0.0, 0.0, 1.0, 0.0),
661    });
662    // Outline vertices (indices 1..=n)
663    for p in &outline {
664        vertices.push(Vertex {
665            position: Vec3::new(p.x, p.y, 0.0),
666            uv: Vec2::new((p.x - x) / w, (p.y - y) / h),
667            color: color_bytes,
668            normal: Vec4::new(0.0, 0.0, 1.0, 0.0),
669        });
670    }
671
672    let mut indices = Vec::with_capacity(n * 3);
673    for i in 0..n {
674        indices.push(0u16); // center
675        indices.push((i + 1) as u16);
676        indices.push(((i + 1) % n + 1) as u16);
677    }
678
679    let mesh = Mesh {
680        vertices,
681        indices,
682        texture: None,
683    };
684    draw_mesh(&mesh);
685}
686
687/// Draws a rounded rectangle rotated by `rotation_radians` around its center.
688/// All outline vertices are rotated before building the triangle fan mesh.
689/// `(x, y, w, h)` is the *original* (unrotated) bounding box — the centre of
690/// rotation is `(x + w/2, y + h/2)`.
691fn draw_good_rotated_rounded_rectangle(
692    x: f32,
693    y: f32,
694    w: f32,
695    h: f32,
696    cr: &CornerRadii,
697    color: Color,
698    rotation_radians: f32,
699    flip_x: bool,
700    flip_y: bool,
701) {
702    use std::f32::consts::{FRAC_PI_2, PI};
703
704    let cx = x + w / 2.0;
705    let cy = y + h / 2.0;
706
707    let cos_r = rotation_radians.cos();
708    let sin_r = rotation_radians.sin();
709
710    // Rotate a point around (cx, cy)
711    let rotate_point = |px: f32, py: f32| -> Vec2 {
712        // Apply flips relative to centre first
713        let mut dx = px - cx;
714        let mut dy = py - cy;
715        if flip_x { dx = -dx; }
716        if flip_y { dy = -dy; }
717        let rx = dx * cos_r - dy * sin_r;
718        let ry = dx * sin_r + dy * cos_r;
719        Vec2::new(cx + rx, cy + ry)
720    };
721
722    // Build outline in local (unrotated) space, then rotate
723    // Pre-allocate based on expected corner vertex count
724    let est_verts = if cr.top_left == 0.0 && cr.top_right == 0.0 && cr.bottom_left == 0.0 && cr.bottom_right == 0.0 {
725        4
726    } else {
727        [cr.top_left, cr.top_right, cr.bottom_left, cr.bottom_right]
728            .iter()
729            .map(|&r| if r <= 0.0 { 1 } else { ((FRAC_PI_2 * r) / PIXELS_PER_POINT).max(6.0) as usize + 1 })
730            .sum::<usize>()
731    };
732    let mut outline: Vec<Vec2> = Vec::with_capacity(est_verts);
733
734    let add_arc = |outline: &mut Vec<Vec2>, arc_cx: f32, arc_cy: f32, radius: f32, start_angle: f32, end_angle: f32| {
735        if radius <= 0.0 {
736            outline.push(rotate_point(arc_cx, arc_cy));
737            return;
738        }
739        let sides = ((FRAC_PI_2 * radius) / PIXELS_PER_POINT).max(6.0) as usize;
740        // Use incremental rotation to avoid per-point cos/sin
741        let step = (end_angle - start_angle) / sides as f32;
742        let step_cos = step.cos();
743        let step_sin = step.sin();
744        let mut dx = start_angle.cos() * radius;
745        let mut dy = start_angle.sin() * radius;
746        for _ in 0..=sides {
747            outline.push(rotate_point(arc_cx + dx, arc_cy + dy));
748            let new_dx = dx * step_cos - dy * step_sin;
749            let new_dy = dx * step_sin + dy * step_cos;
750            dx = new_dx;
751            dy = new_dy;
752        }
753    };
754
755    if cr.top_left == 0.0 && cr.top_right == 0.0 && cr.bottom_left == 0.0 && cr.bottom_right == 0.0 {
756        // Sharp rectangle — just rotate 4 corners
757        outline.push(rotate_point(x, y));
758        outline.push(rotate_point(x + w, y));
759        outline.push(rotate_point(x + w, y + h));
760        outline.push(rotate_point(x, y + h));
761    } else {
762        add_arc(&mut outline, x + cr.top_left, y + cr.top_left, cr.top_left,
763                PI, 3.0 * FRAC_PI_2);
764        add_arc(&mut outline, x + w - cr.top_right, y + cr.top_right, cr.top_right,
765                3.0 * FRAC_PI_2, 2.0 * PI);
766        add_arc(&mut outline, x + w - cr.bottom_right, y + h - cr.bottom_right, cr.bottom_right,
767                0.0, FRAC_PI_2);
768        add_arc(&mut outline, x + cr.bottom_left, y + h - cr.bottom_left, cr.bottom_left,
769                FRAC_PI_2, PI);
770    }
771
772    let n = outline.len();
773    if n < 3 { return; }
774
775    let color_bytes = [
776        (color.r * 255.0) as u8,
777        (color.g * 255.0) as u8,
778        (color.b * 255.0) as u8,
779        (color.a * 255.0) as u8,
780    ];
781
782    let center_rot = Vec2::new(cx, cy);
783
784    let mut vertices = Vec::with_capacity(n + 1);
785    vertices.push(Vertex {
786        position: Vec3::new(center_rot.x, center_rot.y, 0.0),
787        uv: Vec2::new(0.5, 0.5),
788        color: color_bytes,
789        normal: Vec4::new(0.0, 0.0, 1.0, 0.0),
790    });
791    for p in &outline {
792        vertices.push(Vertex {
793            position: Vec3::new(p.x, p.y, 0.0),
794            uv: Vec2::new((p.x - x) / w, (p.y - y) / h),
795            color: color_bytes,
796            normal: Vec4::new(0.0, 0.0, 1.0, 0.0),
797        });
798    }
799
800    let mut indices = Vec::with_capacity(n * 3);
801    for i in 0..n {
802        indices.push(0u16);
803        indices.push((i + 1) as u16);
804        indices.push(((i + 1) % n + 1) as u16);
805    }
806
807    draw_mesh(&Mesh { vertices, indices, texture: None });
808}
809
810/// Remap corner radii for a 90° clockwise rotation.
811fn rotate_corner_radii_90(cr: &CornerRadii) -> CornerRadii {
812    CornerRadii {
813        top_left: cr.bottom_left,
814        top_right: cr.top_left,
815        bottom_right: cr.top_right,
816        bottom_left: cr.bottom_right,
817    }
818}
819
820/// Remap corner radii for a 180° rotation.
821fn rotate_corner_radii_180(cr: &CornerRadii) -> CornerRadii {
822    CornerRadii {
823        top_left: cr.bottom_right,
824        top_right: cr.bottom_left,
825        bottom_right: cr.top_left,
826        bottom_left: cr.top_right,
827    }
828}
829
830/// Remap corner radii for a 270° clockwise rotation.
831fn rotate_corner_radii_270(cr: &CornerRadii) -> CornerRadii {
832    CornerRadii {
833        top_left: cr.top_right,
834        top_right: cr.bottom_right,
835        bottom_right: cr.bottom_left,
836        bottom_left: cr.top_left,
837    }
838}
839
840/// Apply flip_x and flip_y to corner radii (before rotation).
841fn flip_corner_radii(cr: &CornerRadii, flip_x: bool, flip_y: bool) -> CornerRadii {
842    let mut result = cr.clone();
843    if flip_x {
844        std::mem::swap(&mut result.top_left, &mut result.top_right);
845        std::mem::swap(&mut result.bottom_left, &mut result.bottom_right);
846    }
847    if flip_y {
848        std::mem::swap(&mut result.top_left, &mut result.bottom_left);
849        std::mem::swap(&mut result.top_right, &mut result.bottom_right);
850    }
851    result
852}
853
854struct RenderState {
855    clip: Option<(i32, i32, i32, i32)>,
856    /// Render target stack for group effects (shaders and/or visual rotation).
857    rt_stack: Vec<(RenderTarget, Option<crate::shaders::ShaderConfig>, Option<crate::engine::VisualRotationConfig>, BoundingBox)>,
858    #[cfg(feature = "text-styling")]
859    style_stack: Vec<String>,
860    #[cfg(feature = "text-styling")]
861    total_char_index: usize,
862}
863
864impl RenderState {
865    fn new() -> Self {
866        Self {
867            clip: None,
868            rt_stack: Vec::new(),
869            #[cfg(feature = "text-styling")]
870            style_stack: Vec::new(),
871            #[cfg(feature = "text-styling")]
872            total_char_index: 0,
873        }
874    }
875}
876
877/// Render custom content to a [`Texture2D`]
878///
879/// Sets up a render target, points a camera at it, calls your closure, then
880/// restores the default camera and returns the resulting texture.
881/// The coordinate system inside the closure runs from `(0, 0)` at the top-left
882/// to `(width, height)` at the bottom-right.
883///
884/// Call this before the layout pass, then hand the texture to an element with `.image(tex)`.
885///
886/// # Example
887/// ```rust,ignore
888/// let tex = render_to_texture(200.0, 100.0, || {
889///     clear_background(BLANK);
890///     draw_circle(w / 2.0, h / 2.0, 40.0, RED);
891/// });
892/// ```
893pub fn render_to_texture(width: f32, height: f32, draw: impl FnOnce()) -> Texture2D {
894    let render_target = render_target_msaa(width as u32, height as u32);
895    render_target.texture.set_filter(FilterMode::Linear);
896    let mut cam = Camera2D::from_display_rect(Rect::new(0.0, 0.0, width, height));
897    cam.render_target = Some(render_target.clone());
898    set_camera(&cam);
899
900    draw();
901
902    set_default_camera();
903    render_target.texture
904}
905
906fn rounded_rectangle_texture(cr: &CornerRadii, bb: &BoundingBox, clip: &Option<(i32, i32, i32, i32)>) -> Texture2D {
907    let render_target = render_target_msaa(bb.width as u32, bb.height as u32);
908    render_target.texture.set_filter(FilterMode::Linear);
909    let mut cam = Camera2D::from_display_rect(Rect::new(0.0, 0.0, bb.width, bb.height));
910    cam.render_target = Some(render_target.clone());
911    set_camera(&cam);
912    unsafe {
913        get_internal_gl().quad_gl.scissor(None);
914    };
915
916    draw_good_rounded_rectangle(0.0, 0.0, bb.width, bb.height, cr, WHITE);
917
918    set_default_camera();
919    unsafe {
920        get_internal_gl().quad_gl.scissor(*clip);
921    }
922    render_target.texture
923}
924
925/// Render a TinyVG image to a RenderTarget, scaled to fit the given dimensions.
926/// Decodes from raw bytes, then delegates to `render_tinyvg_image`.
927#[cfg(feature = "tinyvg")]
928fn render_tinyvg_texture(
929    tvg_data: &[u8],
930    dest_width: f32,
931    dest_height: f32,
932    clip: &Option<(i32, i32, i32, i32)>,
933) -> Option<RenderTarget> {
934    use tinyvg::Decoder;
935    let decoder = Decoder::new(std::io::Cursor::new(tvg_data));
936    let image = match decoder.decode() {
937        Ok(img) => img,
938        Err(_) => return None,
939    };
940    render_tinyvg_image(&image, dest_width, dest_height, clip)
941}
942
943/// Render a decoded `tinyvg::format::Image` to a RenderTarget, scaled to fit the given dimensions.
944#[cfg(feature = "tinyvg")]
945fn render_tinyvg_image(
946    image: &tinyvg::format::Image,
947    dest_width: f32,
948    dest_height: f32,
949    clip: &Option<(i32, i32, i32, i32)>,
950) -> Option<RenderTarget> {
951    use tinyvg::format::{Command, Style, Segment, SegmentCommandKind, Point as TvgPoint, Color as TvgColor};
952    use kurbo::{BezPath, Point as KurboPoint, Vec2 as KurboVec2, ParamCurve, SvgArc, Arc as KurboArc, PathEl};
953    use lyon::tessellation::{FillTessellator, FillOptions, VertexBuffers, BuffersBuilder, FillVertex, FillRule};
954    use lyon::path::Path as LyonPath;
955    use lyon::math::point as lyon_point;
956    
957    fn tvg_to_kurbo(p: TvgPoint) -> KurboPoint {
958        KurboPoint::new(p.x, p.y)
959    }
960    
961    let tvg_width = image.header.width as f32;
962    let tvg_height = image.header.height as f32;
963    let scale_x = dest_width / tvg_width;
964    let scale_y = dest_height / tvg_height;
965    
966    let render_target = render_target_msaa(dest_width as u32, dest_height as u32);
967    render_target.texture.set_filter(FilterMode::Linear);
968    let mut cam = Camera2D::from_display_rect(Rect::new(0.0, 0.0, dest_width, dest_height));
969    cam.render_target = Some(render_target.clone());
970    set_camera(&cam);
971    unsafe {
972        get_internal_gl().quad_gl.scissor(None);
973    }
974    
975    let tvg_to_mq_color = |c: &TvgColor| -> Color {
976        let (r, g, b, a) = c.as_rgba();
977        Color::new(r as f32, g as f32, b as f32, a as f32)
978    };
979    
980    let style_to_color = |style: &Style, color_table: &[TvgColor]| -> Color {
981        match style {
982            Style::FlatColor { color_index } => {
983                color_table.get(*color_index).map(|c| tvg_to_mq_color(c)).unwrap_or(WHITE)
984            }
985            Style::LinearGradient { color_index_0, .. } |
986            Style::RadialGradient { color_index_0, .. } => {
987                color_table.get(*color_index_0).map(|c| tvg_to_mq_color(c)).unwrap_or(WHITE)
988            }
989        }
990    };
991    
992    let draw_filled_path_lyon = |bezpath: &BezPath, color: Color| {
993        let mut builder = LyonPath::builder();
994        let mut subpath_started = false;
995        
996        for el in bezpath.iter() {
997            match el {
998                PathEl::MoveTo(p) => {
999                    if subpath_started {
1000                        builder.end(false);
1001                    }
1002                    builder.begin(lyon_point((p.x * scale_x as f64) as f32, (p.y * scale_y as f64) as f32));
1003                    subpath_started = true;
1004                }
1005                PathEl::LineTo(p) => {
1006                    builder.line_to(lyon_point((p.x * scale_x as f64) as f32, (p.y * scale_y as f64) as f32));
1007                }
1008                PathEl::QuadTo(c, p) => {
1009                    builder.quadratic_bezier_to(
1010                        lyon_point((c.x * scale_x as f64) as f32, (c.y * scale_y as f64) as f32),
1011                        lyon_point((p.x * scale_x as f64) as f32, (p.y * scale_y as f64) as f32),
1012                    );
1013                }
1014                PathEl::CurveTo(c1, c2, p) => {
1015                    builder.cubic_bezier_to(
1016                        lyon_point((c1.x * scale_x as f64) as f32, (c1.y * scale_y as f64) as f32),
1017                        lyon_point((c2.x * scale_x as f64) as f32, (c2.y * scale_y as f64) as f32),
1018                        lyon_point((p.x * scale_x as f64) as f32, (p.y * scale_y as f64) as f32),
1019                    );
1020                }
1021                PathEl::ClosePath => {
1022                    builder.end(true);
1023                    subpath_started = false;
1024                }
1025            }
1026        }
1027        
1028        if subpath_started {
1029            builder.end(true);
1030        }
1031        
1032        let lyon_path = builder.build();
1033        
1034        let mut geometry: VertexBuffers<[f32; 2], u16> = VertexBuffers::new();
1035        let mut tessellator = FillTessellator::new();
1036        
1037        let fill_options = FillOptions::default().with_fill_rule(FillRule::NonZero);
1038        
1039        let result = tessellator.tessellate_path(
1040            &lyon_path,
1041            &fill_options,
1042            &mut BuffersBuilder::new(&mut geometry, |vertex: FillVertex| {
1043                vertex.position().to_array()
1044            }),
1045        );
1046        
1047        if result.is_err() || geometry.indices.is_empty() {
1048            return;
1049        }
1050        
1051        let color_bytes = [(color.r * 255.0) as u8, (color.g * 255.0) as u8, (color.b * 255.0) as u8, (color.a * 255.0) as u8];
1052        
1053        let vertices: Vec<Vertex> = geometry.vertices.iter().map(|pos| {
1054            Vertex {
1055                position: Vec3::new(pos[0], pos[1], 0.0),
1056                uv: Vec2::ZERO,
1057                color: color_bytes,
1058                normal: Vec4::new(0.0, 0.0, 1.0, 0.0),
1059            }
1060        }).collect();
1061        
1062        let mesh = Mesh {
1063            vertices,
1064            indices: geometry.indices,
1065            texture: None,
1066        };
1067        draw_mesh(&mesh);
1068    };
1069    
1070    let draw_filled_polygon_tvg = |points: &[TvgPoint], color: Color| {
1071        if points.len() < 3 {
1072            return;
1073        }
1074        
1075        let mut builder = LyonPath::builder();
1076        builder.begin(lyon_point(points[0].x as f32 * scale_x, points[0].y as f32 * scale_y));
1077        for point in &points[1..] {
1078            builder.line_to(lyon_point(point.x as f32 * scale_x, point.y as f32 * scale_y));
1079        }
1080        builder.end(true);
1081        let lyon_path = builder.build();
1082        
1083        let mut geometry: VertexBuffers<[f32; 2], u16> = VertexBuffers::new();
1084        let mut tessellator = FillTessellator::new();
1085        
1086        let result = tessellator.tessellate_path(
1087            &lyon_path,
1088            &FillOptions::default(),
1089            &mut BuffersBuilder::new(&mut geometry, |vertex: FillVertex| {
1090                vertex.position().to_array()
1091            }),
1092        );
1093        
1094        if result.is_err() || geometry.indices.is_empty() {
1095            return;
1096        }
1097        
1098        let color_bytes = [(color.r * 255.0) as u8, (color.g * 255.0) as u8, (color.b * 255.0) as u8, (color.a * 255.0) as u8];
1099        
1100        let vertices: Vec<Vertex> = geometry.vertices.iter().map(|pos| {
1101            Vertex {
1102                position: Vec3::new(pos[0], pos[1], 0.0),
1103                uv: Vec2::ZERO,
1104                color: color_bytes,
1105                normal: Vec4::new(0.0, 0.0, 1.0, 0.0),
1106            }
1107        }).collect();
1108        
1109        let mesh = Mesh {
1110            vertices,
1111            indices: geometry.indices,
1112            texture: None,
1113        };
1114        draw_mesh(&mesh);
1115    };
1116    
1117    let build_bezpath = |segments: &[Segment]| -> BezPath {
1118        let mut bezier = BezPath::new();
1119        for segment in segments {
1120            let start = tvg_to_kurbo(segment.start);
1121            let mut pen = start;
1122            bezier.move_to(pen);
1123            
1124            for cmd in &segment.commands {
1125                match &cmd.kind {
1126                    SegmentCommandKind::Line { end } => {
1127                        let end_k = tvg_to_kurbo(*end);
1128                        bezier.line_to(end_k);
1129                        pen = end_k;
1130                    }
1131                    SegmentCommandKind::HorizontalLine { x } => {
1132                        let end = KurboPoint::new(*x, pen.y);
1133                        bezier.line_to(end);
1134                        pen = end;
1135                    }
1136                    SegmentCommandKind::VerticalLine { y } => {
1137                        let end = KurboPoint::new(pen.x, *y);
1138                        bezier.line_to(end);
1139                        pen = end;
1140                    }
1141                    SegmentCommandKind::CubicBezier { control_0, control_1, point_1 } => {
1142                        let c0 = tvg_to_kurbo(*control_0);
1143                        let c1 = tvg_to_kurbo(*control_1);
1144                        let p1 = tvg_to_kurbo(*point_1);
1145                        bezier.curve_to(c0, c1, p1);
1146                        pen = p1;
1147                    }
1148                    SegmentCommandKind::QuadraticBezier { control, point_1 } => {
1149                        let c = tvg_to_kurbo(*control);
1150                        let p1 = tvg_to_kurbo(*point_1);
1151                        bezier.quad_to(c, p1);
1152                        pen = p1;
1153                    }
1154                    SegmentCommandKind::ArcEllipse { large, sweep, radius_x, radius_y, rotation, target } => {
1155                        let target_k = tvg_to_kurbo(*target);
1156                        let svg_arc = SvgArc {
1157                            from: pen,
1158                            to: target_k,
1159                            radii: KurboVec2::new(*radius_x, *radius_y),
1160                            x_rotation: *rotation,
1161                            large_arc: *large,
1162                            sweep: *sweep,
1163                        };
1164                        if let Some(arc) = KurboArc::from_svg_arc(&svg_arc) {
1165                            for seg in arc.append_iter(0.2) {
1166                                bezier.push(seg);
1167                            }
1168                        }
1169                        pen = target_k;
1170                    }
1171                    SegmentCommandKind::ClosePath => {
1172                        bezier.close_path();
1173                        pen = start;
1174                    }
1175                }
1176            }
1177        }
1178        bezier
1179    };
1180    
1181    let line_scale = (scale_x + scale_y) / 2.0;
1182    
1183    for cmd in &image.commands {
1184        match cmd {
1185            Command::FillPath { fill_style, path, outline } => {
1186                let fill_color = style_to_color(fill_style, &image.color_table);
1187                let bezpath = build_bezpath(path);
1188                draw_filled_path_lyon(&bezpath, fill_color);
1189                
1190                if let Some(outline_style) = outline {
1191                    let line_color = style_to_color(&outline_style.line_style, &image.color_table);
1192                    let line_width = outline_style.line_width as f32 * line_scale;
1193                    for segment in path {
1194                        let start = segment.start;
1195                        let mut pen = start;
1196                        for cmd in &segment.commands {
1197                            match &cmd.kind {
1198                                SegmentCommandKind::Line { end } => {
1199                                    draw_line(
1200                                        pen.x as f32 * scale_x, pen.y as f32 * scale_y,
1201                                        end.x as f32 * scale_x, end.y as f32 * scale_y,
1202                                        line_width, line_color
1203                                    );
1204                                    pen = *end;
1205                                }
1206                                SegmentCommandKind::HorizontalLine { x } => {
1207                                    let end = TvgPoint { x: *x, y: pen.y };
1208                                    draw_line(
1209                                        pen.x as f32 * scale_x, pen.y as f32 * scale_y,
1210                                        end.x as f32 * scale_x, end.y as f32 * scale_y,
1211                                        line_width, line_color
1212                                    );
1213                                    pen = end;
1214                                }
1215                                SegmentCommandKind::VerticalLine { y } => {
1216                                    let end = TvgPoint { x: pen.x, y: *y };
1217                                    draw_line(
1218                                        pen.x as f32 * scale_x, pen.y as f32 * scale_y,
1219                                        end.x as f32 * scale_x, end.y as f32 * scale_y,
1220                                        line_width, line_color
1221                                    );
1222                                    pen = end;
1223                                }
1224                                SegmentCommandKind::ClosePath => {
1225                                    draw_line(
1226                                        pen.x as f32 * scale_x, pen.y as f32 * scale_y,
1227                                        start.x as f32 * scale_x, start.y as f32 * scale_y,
1228                                        line_width, line_color
1229                                    );
1230                                    pen = start;
1231                                }
1232                                SegmentCommandKind::CubicBezier { control_0, control_1, point_1 } => {
1233                                    let c0 = tvg_to_kurbo(*control_0);
1234                                    let c1 = tvg_to_kurbo(*control_1);
1235                                    let p1 = tvg_to_kurbo(*point_1);
1236                                    let p0 = tvg_to_kurbo(pen);
1237                                    let cubic = kurbo::CubicBez::new(p0, c0, c1, p1);
1238                                    let steps = 16usize;
1239                                    let mut prev = p0;
1240                                    for i in 1..=steps {
1241                                        let t = i as f64 / steps as f64;
1242                                        let next = cubic.eval(t);
1243                                        draw_line(
1244                                            prev.x as f32 * scale_x, prev.y as f32 * scale_y,
1245                                            next.x as f32 * scale_x, next.y as f32 * scale_y,
1246                                            line_width, line_color
1247                                        );
1248                                        prev = next;
1249                                    }
1250                                    pen = *point_1;
1251                                }
1252                                SegmentCommandKind::QuadraticBezier { control, point_1 } => {
1253                                    let c = tvg_to_kurbo(*control);
1254                                    let p1 = tvg_to_kurbo(*point_1);
1255                                    let p0 = tvg_to_kurbo(pen);
1256                                    let quad = kurbo::QuadBez::new(p0, c, p1);
1257                                    let steps = 12usize;
1258                                    let mut prev = p0;
1259                                    for i in 1..=steps {
1260                                        let t = i as f64 / steps as f64;
1261                                        let next = quad.eval(t);
1262                                        draw_line(
1263                                            prev.x as f32 * scale_x, prev.y as f32 * scale_y,
1264                                            next.x as f32 * scale_x, next.y as f32 * scale_y,
1265                                            line_width, line_color
1266                                        );
1267                                        prev = next;
1268                                    }
1269                                    pen = *point_1;
1270                                }
1271                                SegmentCommandKind::ArcEllipse { large, sweep, radius_x, radius_y, rotation, target } => {
1272                                    let target_k = tvg_to_kurbo(*target);
1273                                    let p0 = tvg_to_kurbo(pen);
1274                                    let svg_arc = SvgArc {
1275                                        from: p0,
1276                                        to: target_k,
1277                                        radii: KurboVec2::new(*radius_x, *radius_y),
1278                                        x_rotation: *rotation,
1279                                        large_arc: *large,
1280                                        sweep: *sweep,
1281                                    };
1282                                    if let Some(arc) = KurboArc::from_svg_arc(&svg_arc) {
1283                                        let mut prev = p0;
1284                                        for seg in arc.append_iter(0.2) {
1285                                            match seg {
1286                                                PathEl::LineTo(p) | PathEl::MoveTo(p) => {
1287                                                    draw_line(
1288                                                        prev.x as f32 * scale_x, prev.y as f32 * scale_y,
1289                                                        p.x as f32 * scale_x, p.y as f32 * scale_y,
1290                                                        line_width, line_color
1291                                                    );
1292                                                    prev = p;
1293                                                }
1294                                                PathEl::CurveTo(c0, c1, p) => {
1295                                                    // Flatten the curve
1296                                                    let cubic = kurbo::CubicBez::new(prev, c0, c1, p);
1297                                                    let steps = 8usize;
1298                                                    let mut prev_pt = prev;
1299                                                    for j in 1..=steps {
1300                                                        let t = j as f64 / steps as f64;
1301                                                        let next = cubic.eval(t);
1302                                                        draw_line(
1303                                                            prev_pt.x as f32 * scale_x, prev_pt.y as f32 * scale_y,
1304                                                            next.x as f32 * scale_x, next.y as f32 * scale_y,
1305                                                            line_width, line_color
1306                                                        );
1307                                                        prev_pt = next;
1308                                                    }
1309                                                    prev = p;
1310                                                }
1311                                                _ => {}
1312                                            }
1313                                        }
1314                                    }
1315                                    pen = *target;
1316                                }
1317                            }
1318                        }
1319                    }
1320                }
1321            }
1322            Command::FillRectangles { fill_style, rectangles, outline } => {
1323                let fill_color = style_to_color(fill_style, &image.color_table);
1324                for rect in rectangles {
1325                    draw_rectangle(
1326                        rect.x0 as f32 * scale_x,
1327                        rect.y0 as f32 * scale_y,
1328                        rect.width() as f32 * scale_x,
1329                        rect.height() as f32 * scale_y,
1330                        fill_color
1331                    );
1332                }
1333                
1334                if let Some(outline_style) = outline {
1335                    let line_color = style_to_color(&outline_style.line_style, &image.color_table);
1336                    let line_width = outline_style.line_width as f32 * line_scale;
1337                    for rect in rectangles {
1338                        draw_rectangle_lines(
1339                            rect.x0 as f32 * scale_x,
1340                            rect.y0 as f32 * scale_y,
1341                            rect.width() as f32 * scale_x,
1342                            rect.height() as f32 * scale_y,
1343                            line_width, line_color
1344                        );
1345                    }
1346                }
1347            }
1348            Command::FillPolygon { fill_style, polygon, outline } => {
1349                let fill_color = style_to_color(fill_style, &image.color_table);
1350                draw_filled_polygon_tvg(polygon, fill_color);
1351                
1352                if let Some(outline_style) = outline {
1353                    let line_color = style_to_color(&outline_style.line_style, &image.color_table);
1354                    let line_width = outline_style.line_width as f32 * line_scale;
1355                    for i in 0..polygon.len() {
1356                        let next = (i + 1) % polygon.len();
1357                        draw_line(
1358                            polygon[i].x as f32 * scale_x, polygon[i].y as f32 * scale_y,
1359                            polygon[next].x as f32 * scale_x, polygon[next].y as f32 * scale_y,
1360                            line_width, line_color
1361                        );
1362                    }
1363                }
1364            }
1365            Command::DrawLines { line_style, line_width, lines } => {
1366                let line_color = style_to_color(line_style, &image.color_table);
1367                for line in lines {
1368                    draw_line(
1369                        line.p0.x as f32 * scale_x, line.p0.y as f32 * scale_y,
1370                        line.p1.x as f32 * scale_x, line.p1.y as f32 * scale_y,
1371                        *line_width as f32 * line_scale, line_color
1372                    );
1373                }
1374            }
1375            Command::DrawLineLoop { line_style, line_width, close_path, points } => {
1376                let line_color = style_to_color(line_style, &image.color_table);
1377                for i in 0..points.len().saturating_sub(1) {
1378                    draw_line(
1379                        points[i].x as f32 * scale_x, points[i].y as f32 * scale_y,
1380                        points[i+1].x as f32 * scale_x, points[i+1].y as f32 * scale_y,
1381                        *line_width as f32 * line_scale, line_color
1382                    );
1383                }
1384                if *close_path && points.len() >= 2 {
1385                    let last = points.len() - 1;
1386                    draw_line(
1387                        points[last].x as f32 * scale_x, points[last].y as f32 * scale_y,
1388                        points[0].x as f32 * scale_x, points[0].y as f32 * scale_y,
1389                        *line_width as f32 * line_scale, line_color
1390                    );
1391                }
1392            }
1393            Command::DrawLinePath { line_style, line_width, path } => {
1394                let line_color = style_to_color(line_style, &image.color_table);
1395                let scaled_line_width = *line_width as f32 * line_scale;
1396                // Draw line path by tracing segments directly
1397                for segment in path {
1398                    let start = segment.start;
1399                    let mut pen = start;
1400                    for cmd in &segment.commands {
1401                        match &cmd.kind {
1402                            SegmentCommandKind::Line { end } => {
1403                                draw_line(
1404                                    pen.x as f32 * scale_x, pen.y as f32 * scale_y,
1405                                    end.x as f32 * scale_x, end.y as f32 * scale_y,
1406                                    scaled_line_width, line_color
1407                                );
1408                                pen = *end;
1409                            }
1410                            SegmentCommandKind::HorizontalLine { x } => {
1411                                let end = TvgPoint { x: *x, y: pen.y };
1412                                draw_line(
1413                                    pen.x as f32 * scale_x, pen.y as f32 * scale_y,
1414                                    end.x as f32 * scale_x, end.y as f32 * scale_y,
1415                                    scaled_line_width, line_color
1416                                );
1417                                pen = end;
1418                            }
1419                            SegmentCommandKind::VerticalLine { y } => {
1420                                let end = TvgPoint { x: pen.x, y: *y };
1421                                draw_line(
1422                                    pen.x as f32 * scale_x, pen.y as f32 * scale_y,
1423                                    end.x as f32 * scale_x, end.y as f32 * scale_y,
1424                                    scaled_line_width, line_color
1425                                );
1426                                pen = end;
1427                            }
1428                            SegmentCommandKind::ClosePath => {
1429                                draw_line(
1430                                    pen.x as f32 * scale_x, pen.y as f32 * scale_y,
1431                                    start.x as f32 * scale_x, start.y as f32 * scale_y,
1432                                    scaled_line_width, line_color
1433                                );
1434                                pen = start;
1435                            }
1436                            // For curves, we need to flatten them for line drawing
1437                            SegmentCommandKind::CubicBezier { control_0, control_1, point_1 } => {
1438                                let c0 = tvg_to_kurbo(*control_0);
1439                                let c1 = tvg_to_kurbo(*control_1);
1440                                let p1 = tvg_to_kurbo(*point_1);
1441                                let p0 = tvg_to_kurbo(pen);
1442                                let cubic = kurbo::CubicBez::new(p0, c0, c1, p1);
1443                                let steps = 16usize;
1444                                let mut prev = p0;
1445                                for i in 1..=steps {
1446                                    let t = i as f64 / steps as f64;
1447                                    let next = cubic.eval(t);
1448                                    draw_line(
1449                                        prev.x as f32 * scale_x, prev.y as f32 * scale_y,
1450                                        next.x as f32 * scale_x, next.y as f32 * scale_y,
1451                                        scaled_line_width, line_color
1452                                    );
1453                                    prev = next;
1454                                }
1455                                pen = *point_1;
1456                            }
1457                            SegmentCommandKind::QuadraticBezier { control, point_1 } => {
1458                                let c = tvg_to_kurbo(*control);
1459                                let p1 = tvg_to_kurbo(*point_1);
1460                                let p0 = tvg_to_kurbo(pen);
1461                                let quad = kurbo::QuadBez::new(p0, c, p1);
1462                                let steps = 12usize;
1463                                let mut prev = p0;
1464                                for i in 1..=steps {
1465                                    let t = i as f64 / steps as f64;
1466                                    let next = quad.eval(t);
1467                                    draw_line(
1468                                        prev.x as f32 * scale_x, prev.y as f32 * scale_y,
1469                                        next.x as f32 * scale_x, next.y as f32 * scale_y,
1470                                        scaled_line_width, line_color
1471                                    );
1472                                    prev = next;
1473                                }
1474                                pen = *point_1;
1475                            }
1476                            SegmentCommandKind::ArcEllipse { large, sweep, radius_x, radius_y, rotation, target } => {
1477                                let target_k = tvg_to_kurbo(*target);
1478                                let p0 = tvg_to_kurbo(pen);
1479                                let svg_arc = SvgArc {
1480                                    from: p0,
1481                                    to: target_k,
1482                                    radii: KurboVec2::new(*radius_x, *radius_y),
1483                                    x_rotation: *rotation,
1484                                    large_arc: *large,
1485                                    sweep: *sweep,
1486                                };
1487                                if let Some(arc) = KurboArc::from_svg_arc(&svg_arc) {
1488                                    let mut prev = p0;
1489                                    for seg in arc.append_iter(0.2) {
1490                                        match seg {
1491                                            PathEl::LineTo(p) | PathEl::MoveTo(p) => {
1492                                                draw_line(
1493                                                    prev.x as f32 * scale_x, prev.y as f32 * scale_y,
1494                                                    p.x as f32 * scale_x, p.y as f32 * scale_y,
1495                                                    scaled_line_width, line_color
1496                                                );
1497                                                prev = p;
1498                                            }
1499                                            PathEl::CurveTo(c0, c1, p) => {
1500                                                // Flatten the curve
1501                                                let cubic = kurbo::CubicBez::new(prev, c0, c1, p);
1502                                                let steps = 8usize;
1503                                                let mut prev_pt = prev;
1504                                                for j in 1..=steps {
1505                                                    let t = j as f64 / steps as f64;
1506                                                    let next = cubic.eval(t);
1507                                                    draw_line(
1508                                                        prev_pt.x as f32 * scale_x, prev_pt.y as f32 * scale_y,
1509                                                        next.x as f32 * scale_x, next.y as f32 * scale_y,
1510                                                        scaled_line_width, line_color
1511                                                    );
1512                                                    prev_pt = next;
1513                                                }
1514                                                prev = p;
1515                                            }
1516                                            _ => {}
1517                                        }
1518                                    }
1519                                }
1520                                pen = *target;
1521                            }
1522                        }
1523                    }
1524                }
1525            }
1526        }
1527    }
1528    
1529    set_default_camera();
1530    unsafe {
1531        get_internal_gl().quad_gl.scissor(*clip);
1532    }
1533    
1534    Some(render_target)
1535}
1536
1537fn resize(texture: &Texture2D, height: f32, width: f32, clip: &Option<(i32, i32, i32, i32)>) -> Texture2D {
1538    let render_target = render_target_msaa(width as u32, height as u32);
1539    render_target.texture.set_filter(FilterMode::Linear);
1540    let mut cam = Camera2D::from_display_rect(Rect::new(0.0, 0.0, width, height));
1541    cam.render_target = Some(render_target.clone());
1542    set_camera(&cam);
1543    unsafe {
1544        get_internal_gl().quad_gl.scissor(None);
1545    };
1546    draw_texture_ex(
1547        texture,
1548        0.0,
1549        0.0,
1550        WHITE,
1551        DrawTextureParams {
1552            dest_size: Some(Vec2::new(width, height)),
1553            flip_y: true,
1554            ..Default::default()
1555        },
1556    );
1557    set_default_camera();
1558    unsafe {
1559        get_internal_gl().quad_gl.scissor(*clip);
1560    }
1561    render_target.texture
1562}
1563
1564/// Draws all render commands to the screen using macroquad.
1565pub async fn render<CustomElementData: Clone + Default + std::fmt::Debug>(
1566    commands: Vec<RenderCommand<CustomElementData>>,
1567    handle_custom_command: impl Fn(&RenderCommand<CustomElementData>),
1568) {
1569    let mut state = RenderState::new();
1570    for command in commands {
1571        match &command.config {
1572            RenderCommandConfig::Image(image) => {
1573                let bb = command.bounding_box;
1574                let cr = &image.corner_radii;
1575                let mut tint = ply_to_macroquad_color(&image.background_color);
1576                if tint == Color::new(0.0, 0.0, 0.0, 0.0) {
1577                    tint = Color::new(1.0, 1.0, 1.0, 1.0);
1578                }
1579
1580                match &image.data {
1581                    ImageSource::Texture(tex) => {
1582                        // Direct GPU texture — draw immediately, no TextureManager
1583                        let has_corner_radii = cr.top_left > 0.0 || cr.top_right > 0.0 || cr.bottom_left > 0.0 || cr.bottom_right > 0.0;
1584                        if !has_corner_radii {
1585                            draw_texture_ex(
1586                                tex,
1587                                bb.x,
1588                                bb.y,
1589                                tint,
1590                                DrawTextureParams {
1591                                    dest_size: Some(Vec2::new(bb.width, bb.height)),
1592                                    ..Default::default()
1593                                },
1594                            );
1595                        } else {
1596                            let mut manager = TEXTURE_MANAGER.lock().unwrap();
1597                            // Use texture raw pointer as a unique key for the corner-radii variant
1598                            let key = format!(
1599                                "tex-proc:{:?}:{}:{}:{}:{}:{}:{}:{:?}",
1600                                tex.raw_miniquad_id(),
1601                                bb.width, bb.height,
1602                                cr.top_left, cr.top_right, cr.bottom_left, cr.bottom_right,
1603                                state.clip
1604                            );
1605                            let texture = manager.get_or_create(key, || {
1606                                let mut resized_image: Image = resize(tex, bb.height, bb.width, &state.clip).get_texture_data();
1607                                let rounded_rect: Image = rounded_rectangle_texture(cr, &bb, &state.clip).get_texture_data();
1608                                for i in 0..resized_image.bytes.len()/4 {
1609                                    let this_alpha = resized_image.bytes[i * 4 + 3] as f32 / 255.0;
1610                                    let mask_alpha = rounded_rect.bytes[i * 4 + 3] as f32 / 255.0;
1611                                    resized_image.bytes[i * 4 + 3] = (this_alpha * mask_alpha * 255.0) as u8;
1612                                }
1613                                Texture2D::from_image(&resized_image)
1614                            });
1615                            draw_texture_ex(
1616                                texture,
1617                                bb.x,
1618                                bb.y,
1619                                tint,
1620                                DrawTextureParams {
1621                                    dest_size: Some(Vec2::new(bb.width, bb.height)),
1622                                    ..Default::default()
1623                                },
1624                            );
1625                        }
1626                    }
1627                    #[cfg(feature = "tinyvg")]
1628                    ImageSource::TinyVg(tvg_image) => {
1629                        // Procedural TinyVG — rasterize every frame (no caching, content may change)
1630                        let has_corner_radii = cr.top_left > 0.0 || cr.top_right > 0.0 || cr.bottom_left > 0.0 || cr.bottom_right > 0.0;
1631                        if let Some(tvg_rt) = render_tinyvg_image(tvg_image, bb.width, bb.height, &state.clip) {
1632                            let final_texture = if has_corner_radii {
1633                                let mut tvg_img: Image = tvg_rt.texture.get_texture_data();
1634                                let rounded_rect: Image = rounded_rectangle_texture(cr, &bb, &state.clip).get_texture_data();
1635                                for i in 0..tvg_img.bytes.len()/4 {
1636                                    let this_alpha = tvg_img.bytes[i * 4 + 3] as f32 / 255.0;
1637                                    let mask_alpha = rounded_rect.bytes[i * 4 + 3] as f32 / 255.0;
1638                                    tvg_img.bytes[i * 4 + 3] = (this_alpha * mask_alpha * 255.0) as u8;
1639                                }
1640                                Texture2D::from_image(&tvg_img)
1641                            } else {
1642                                tvg_rt.texture.clone()
1643                            };
1644                            draw_texture_ex(
1645                                &final_texture,
1646                                bb.x,
1647                                bb.y,
1648                                tint,
1649                                DrawTextureParams {
1650                                    dest_size: Some(Vec2::new(bb.width, bb.height)),
1651                                    flip_y: true,
1652                                    ..Default::default()
1653                                },
1654                            );
1655                        }
1656                    }
1657                    ImageSource::Asset(ga) => {
1658                        // Static asset — existing behavior
1659                        let mut manager = TEXTURE_MANAGER.lock().unwrap();
1660
1661                        #[cfg(feature = "tinyvg")]
1662                        let is_tvg = ga.get_name().to_lowercase().ends_with(".tvg");
1663                        #[cfg(not(feature = "tinyvg"))]
1664                        let is_tvg = false;
1665
1666                        #[cfg(feature = "tinyvg")]
1667                        if is_tvg {
1668                            let key = format!(
1669                                "tvg:{}:{}:{}:{}:{}:{}:{}:{:?}",
1670                                ga.get_name(),
1671                                bb.width, bb.height,
1672                                cr.top_left, cr.top_right, cr.bottom_left, cr.bottom_right,
1673                                state.clip
1674                            );
1675                            let has_corner_radii = cr.top_left > 0.0 || cr.top_right > 0.0 || cr.bottom_left > 0.0 || cr.bottom_right > 0.0;
1676                            let texture = if !has_corner_radii {
1677                                // No corner radii — cache the render target to keep its GL texture alive
1678                                if let Some(cached) = manager.get(&key) {
1679                                    cached
1680                                } else {
1681                                    match ga {
1682                                        GraphicAsset::Path(path) => {
1683                                            match load_file(resolve_asset_path(path)).await {
1684                                                Ok(tvg_bytes) => {
1685                                                    if let Some(tvg_rt) = render_tinyvg_texture(&tvg_bytes, bb.width, bb.height, &state.clip) {
1686                                                        manager.cache(key.clone(), tvg_rt)
1687                                                    } else {
1688                                                        warn!("Failed to load TinyVG image: {}", path);
1689                                                        manager.cache(key.clone(), Texture2D::from_rgba8(1, 1, &[0, 0, 0, 0]))
1690                                                    }
1691                                                }
1692                                                Err(error) => {
1693                                                    warn!("Failed to load TinyVG file: {}. Error: {}", path, error);
1694                                                    manager.cache(key.clone(), Texture2D::from_rgba8(1, 1, &[0, 0, 0, 0]))
1695                                                }
1696                                            }
1697                                        }
1698                                        GraphicAsset::Bytes { file_name, data: tvg_bytes } => {
1699                                            if let Some(tvg_rt) = render_tinyvg_texture(tvg_bytes, bb.width, bb.height, &state.clip) {
1700                                                manager.cache(key.clone(), tvg_rt)
1701                                            } else {
1702                                                warn!("Failed to load TinyVG image: {}", file_name);
1703                                                manager.cache(key.clone(), Texture2D::from_rgba8(1, 1, &[0, 0, 0, 0]))
1704                                            }
1705                                        }
1706                                    }
1707                                }
1708                            } else {
1709                                let zerocr_key = format!(
1710                                    "tvg:{}:{}:{}:{}:{}:{}:{}:{:?}",
1711                                    ga.get_name(),
1712                                    bb.width, bb.height,
1713                                    0.0, 0.0, 0.0, 0.0,
1714                                    state.clip
1715                                );
1716                                let base_texture = if let Some(cached) = manager.get(&zerocr_key) {
1717                                    cached
1718                                } else {
1719                                    match ga {
1720                                        GraphicAsset::Path(path) => {
1721                                            match load_file(resolve_asset_path(path)).await {
1722                                                Ok(tvg_bytes) => {
1723                                                    if let Some(tvg_rt) = render_tinyvg_texture(&tvg_bytes, bb.width, bb.height, &state.clip) {
1724                                                        manager.cache(zerocr_key.clone(), tvg_rt)
1725                                                    } else {
1726                                                        warn!("Failed to load TinyVG image: {}", path);
1727                                                        manager.cache(zerocr_key.clone(), Texture2D::from_rgba8(1, 1, &[0, 0, 0, 0]))
1728                                                    }
1729                                                }
1730                                                Err(error) => {
1731                                                    warn!("Failed to load TinyVG file: {}. Error: {}", path, error);
1732                                                    manager.cache(zerocr_key.clone(), Texture2D::from_rgba8(1, 1, &[0, 0, 0, 0]))
1733                                                }
1734                                            }
1735                                        }
1736                                        GraphicAsset::Bytes { file_name, data: tvg_bytes } => {
1737                                            if let Some(tvg_rt) = render_tinyvg_texture(tvg_bytes, bb.width, bb.height, &state.clip) {
1738                                                manager.cache(zerocr_key.clone(), tvg_rt)
1739                                            } else {
1740                                                warn!("Failed to load TinyVG image: {}", file_name);
1741                                                manager.cache(zerocr_key.clone(), Texture2D::from_rgba8(1, 1, &[0, 0, 0, 0]))
1742                                            }
1743                                        }
1744                                    }
1745                                }.clone();
1746                                manager.get_or_create(key, || {
1747                                    let mut tvg_image: Image = base_texture.get_texture_data();
1748                                    let rounded_rect: Image = rounded_rectangle_texture(cr, &bb, &state.clip).get_texture_data();
1749                                    for i in 0..tvg_image.bytes.len()/4 {
1750                                        let this_alpha = tvg_image.bytes[i * 4 + 3] as f32 / 255.0;
1751                                        let mask_alpha = rounded_rect.bytes[i * 4 + 3] as f32 / 255.0;
1752                                        tvg_image.bytes[i * 4 + 3] = (this_alpha * mask_alpha * 255.0) as u8;
1753                                    }
1754                                    Texture2D::from_image(&tvg_image)
1755                                })
1756                            };
1757                            draw_texture_ex(
1758                                texture,
1759                                bb.x,
1760                                bb.y,
1761                                tint,
1762                                DrawTextureParams {
1763                                    dest_size: Some(Vec2::new(bb.width, bb.height)),
1764                                    flip_y: true,
1765                                    ..Default::default()
1766                                },
1767                            );
1768                            continue;
1769                        }
1770
1771                        if !is_tvg && cr.top_left == 0.0 && cr.top_right == 0.0 && cr.bottom_left == 0.0 && cr.bottom_right == 0.0 {
1772                            let texture = match ga {
1773                                GraphicAsset::Path(path) => manager.get_or_load(path).await,
1774                                GraphicAsset::Bytes { file_name, data } => {
1775                                    manager.get_or_create(file_name.to_string(), || {
1776                                        Texture2D::from_file_with_format(data, None)
1777                                    })
1778                                }
1779                            };
1780                            draw_texture_ex(
1781                                texture,
1782                                bb.x,
1783                                bb.y,
1784                                tint,
1785                                DrawTextureParams {
1786                                    dest_size: Some(Vec2::new(bb.width, bb.height)),
1787                                    ..Default::default()
1788                                },
1789                            );
1790                        } else {
1791                            let source_texture = match ga {
1792                                GraphicAsset::Path(path) => manager.get_or_load(path).await.clone(),
1793                                GraphicAsset::Bytes { file_name, data } => {
1794                                    manager.get_or_create(file_name.to_string(), || {
1795                                        Texture2D::from_file_with_format(data, None)
1796                                    }).clone()
1797                                }
1798                            };
1799                            let key = format!(
1800                                "image:{}:{}:{}:{}:{}:{}:{}:{:?}",
1801                                ga.get_name(),
1802                                bb.width, bb.height,
1803                                cr.top_left, cr.top_right, cr.bottom_left, cr.bottom_right,
1804                                state.clip
1805                            );
1806                            let texture = manager.get_or_create(key, || {
1807                                let mut resized_image: Image = resize(&source_texture, bb.height, bb.width, &state.clip).get_texture_data();
1808                                let rounded_rect: Image = rounded_rectangle_texture(cr, &bb, &state.clip).get_texture_data();
1809                                for i in 0..resized_image.bytes.len()/4 {
1810                                    let this_alpha = resized_image.bytes[i * 4 + 3] as f32 / 255.0;
1811                                    let mask_alpha = rounded_rect.bytes[i * 4 + 3] as f32 / 255.0;
1812                                    resized_image.bytes[i * 4 + 3] = (this_alpha * mask_alpha * 255.0) as u8;
1813                                }
1814                                Texture2D::from_image(&resized_image)
1815                            });
1816                            draw_texture_ex(
1817                                texture,
1818                                bb.x,
1819                                bb.y,
1820                                tint,
1821                                DrawTextureParams {
1822                                    dest_size: Some(Vec2::new(bb.width, bb.height)),
1823                                    ..Default::default()
1824                                },
1825                            );
1826                        }
1827                    }
1828                }
1829            }
1830            RenderCommandConfig::Rectangle(config) => {
1831                let bb = command.bounding_box;
1832                let color = ply_to_macroquad_color(&config.color);
1833                let cr = &config.corner_radii;
1834
1835                // Activate effect material if present (Phase 1: single effect only)
1836                let has_effect = !command.effects.is_empty();
1837                if has_effect {
1838                    let effect = &command.effects[0];
1839                    let mut mat_mgr = MATERIAL_MANAGER.lock().unwrap();
1840                    let material = mat_mgr.get_or_create(effect);
1841                    apply_shader_uniforms(material, effect, &bb);
1842                    gl_use_material(material);
1843                }
1844
1845                if let Some(ref sr) = command.shape_rotation {
1846                    use crate::math::{classify_angle, AngleType};
1847                    let flip_x = sr.flip_x;
1848                    let flip_y = sr.flip_y;
1849                    match classify_angle(sr.rotation_radians) {
1850                        AngleType::Zero => {
1851                            // Flips only — remap corner radii
1852                            let cr = flip_corner_radii(cr, flip_x, flip_y);
1853                            draw_good_rounded_rectangle(bb.x, bb.y, bb.width, bb.height, &cr, color);
1854                        }
1855                        AngleType::Right90 => {
1856                            let cr = rotate_corner_radii_90(&flip_corner_radii(cr, flip_x, flip_y));
1857                            draw_good_rounded_rectangle(bb.x, bb.y, bb.width, bb.height, &cr, color);
1858                        }
1859                        AngleType::Straight180 => {
1860                            let cr = rotate_corner_radii_180(&flip_corner_radii(cr, flip_x, flip_y));
1861                            draw_good_rounded_rectangle(bb.x, bb.y, bb.width, bb.height, &cr, color);
1862                        }
1863                        AngleType::Right270 => {
1864                            let cr = rotate_corner_radii_270(&flip_corner_radii(cr, flip_x, flip_y));
1865                            draw_good_rounded_rectangle(bb.x, bb.y, bb.width, bb.height, &cr, color);
1866                        }
1867                        AngleType::Arbitrary(theta) => {
1868                            draw_good_rotated_rounded_rectangle(
1869                                bb.x, bb.y, bb.width, bb.height,
1870                                cr, color, theta, flip_x, flip_y,
1871                            );
1872                        }
1873                    }
1874                } else if cr.top_left == 0.0 && cr.top_right == 0.0 && cr.bottom_left == 0.0 && cr.bottom_right == 0.0 {
1875                    draw_rectangle(
1876                        bb.x,
1877                        bb.y,
1878                        bb.width,
1879                        bb.height,
1880                        color
1881                    );
1882                } else {
1883                    draw_good_rounded_rectangle(bb.x, bb.y, bb.width, bb.height, cr, color);
1884                }
1885
1886                // Deactivate effect material
1887                if has_effect {
1888                    gl_use_default_material();
1889                }
1890            }
1891            #[cfg(feature = "text-styling")]
1892            RenderCommandConfig::Text(config) => {
1893                let bb = command.bounding_box;
1894                let font_size = config.font_size as f32;
1895                // Ensure font is loaded
1896                if let Some(asset) = config.font_asset {
1897                    FontManager::ensure(asset).await;
1898                }
1899                // Hold the FM lock for the duration of text rendering — no clone needed
1900                let mut fm = FONT_MANAGER.lock().unwrap();
1901                let baseline_y = bb.y + fm.metrics(config.font_size, config.font_asset).baseline_offset;
1902                let font = if let Some(asset) = config.font_asset {
1903                    fm.get(asset)
1904                } else {
1905                    fm.get_default()
1906                };
1907                let default_color = ply_to_macroquad_color(&config.color);
1908
1909                // Activate effect material if present
1910                let has_effect = !command.effects.is_empty();
1911                if has_effect {
1912                    let effect = &command.effects[0];
1913                    let mut mat_mgr = MATERIAL_MANAGER.lock().unwrap();
1914                    let material = mat_mgr.get_or_create(effect);
1915                    apply_shader_uniforms(material, effect, &bb);
1916                    gl_use_material(material);
1917                }
1918
1919                let normal_render = || {
1920                    let x_scale = compute_letter_spacing_x_scale(
1921                        bb.width,
1922                        count_visible_chars(&config.text),
1923                        config.letter_spacing,
1924                    );
1925                    draw_text_ex(
1926                        &config.text,
1927                        bb.x,
1928                        baseline_y,
1929                        TextParams {
1930                            font_size: config.font_size as u16,
1931                            font,
1932                            font_scale: 1.0,
1933                            font_scale_aspect: x_scale,
1934                            rotation: 0.0,
1935                            color: default_color
1936                        }
1937                    );
1938                };
1939                
1940                let mut in_style_def = false;
1941                let mut escaped = false;
1942                let mut failed = false;
1943                
1944                let mut text_buffer = String::new();
1945                let mut style_buffer = String::new();
1946
1947                let line = config.text.to_string();
1948                let mut segments: Vec<StyledSegment> = Vec::new();
1949
1950                for c in line.chars() {
1951                    if escaped {
1952                        if in_style_def {
1953                            style_buffer.push(c);
1954                        } else {
1955                            text_buffer.push(c);
1956                        }
1957                        escaped = false;
1958                        continue;
1959                    }
1960
1961                    match c {
1962                        '\\' => {
1963                            escaped = true;
1964                        }
1965                        '{' => {
1966                            if in_style_def {
1967                                style_buffer.push(c); 
1968                            } else {
1969                                if !text_buffer.is_empty() {
1970                                    segments.push(StyledSegment {
1971                                        text: text_buffer.clone(),
1972                                        styles: state.style_stack.clone(),
1973                                    });
1974                                    text_buffer.clear();
1975                                }
1976                                in_style_def = true;
1977                            }
1978                        }
1979                        '|' => {
1980                            if in_style_def {
1981                                state.style_stack.push(style_buffer.clone());
1982                                style_buffer.clear();
1983                                in_style_def = false;
1984                            } else {
1985                                text_buffer.push(c);
1986                            }
1987                        }
1988                        '}' => {
1989                            if in_style_def {
1990                                style_buffer.push(c);
1991                            } else {
1992                                if !text_buffer.is_empty() {
1993                                    segments.push(StyledSegment {
1994                                        text: text_buffer.clone(),
1995                                        styles: state.style_stack.clone(),
1996                                    });
1997                                    text_buffer.clear();
1998                                }
1999                                
2000                                if state.style_stack.pop().is_none() {
2001                                    failed = true;
2002                                    break;
2003                                }
2004                            }
2005                        }
2006                        _ => {
2007                            if in_style_def {
2008                                style_buffer.push(c);
2009                            } else {
2010                                text_buffer.push(c);
2011                            }
2012                        }
2013                    }
2014                }
2015                if !(failed || in_style_def) {
2016                    if !text_buffer.is_empty() {
2017                        segments.push(StyledSegment {
2018                            text: text_buffer.clone(),
2019                            styles: state.style_stack.clone(),
2020                        });
2021                    }
2022                    
2023                    let time = get_time();
2024                    
2025                    let cursor_x = std::cell::Cell::new(bb.x);
2026                    let cursor_y = baseline_y;
2027                    let mut pending_renders = Vec::new();
2028                    
2029                    let x_scale = compute_letter_spacing_x_scale(
2030                        bb.width,
2031                        count_visible_chars(&config.text),
2032                        config.letter_spacing,
2033                    );
2034                    {
2035                        let mut tracker = ANIMATION_TRACKER.lock().unwrap();
2036                        let ts_default = crate::color::Color::rgba(
2037                            config.color.r,
2038                            config.color.g,
2039                            config.color.b,
2040                            config.color.a,
2041                        );
2042                        render_styled_text(
2043                            &segments,
2044                            time,
2045                            font_size,
2046                            ts_default,
2047                            &mut *tracker,
2048                            &mut state.total_char_index,
2049                            |text, tr, style_color| {
2050                                let text_string = text.to_string();
2051                                let text_width = measure_text(&text_string, font, config.font_size as u16, 1.0).width;
2052                                
2053                                let color = Color::new(style_color.r / 255.0, style_color.g / 255.0, style_color.b / 255.0, style_color.a / 255.0);
2054                                let x = cursor_x.get();
2055                                
2056                                pending_renders.push((x, text_string, tr, color));
2057                                
2058                                cursor_x.set(x + text_width*x_scale);
2059                            },
2060                            |text, tr, style_color| {
2061                                let text_string = text.to_string();
2062                                let color = Color::new(style_color.r / 255.0, style_color.g / 255.0, style_color.b / 255.0, style_color.a / 255.0);
2063                                let x = cursor_x.get();
2064                                
2065                                draw_text_ex(
2066                                    &text_string,
2067                                    x + tr.x*x_scale,
2068                                    cursor_y + tr.y,
2069                                    TextParams {
2070                                        font_size: config.font_size as u16,
2071                                        font,
2072                                        font_scale: tr.scale_y.max(0.01),
2073                                        font_scale_aspect: if tr.scale_y > 0.01 { tr.scale_x / tr.scale_y * x_scale } else { x_scale },
2074                                        rotation: tr.rotation.to_radians(),
2075                                        color
2076                                    }
2077                                );
2078                            }
2079                        );
2080                    }
2081                    for (x, text_string, tr, color) in pending_renders {
2082                        draw_text_ex(
2083                            &text_string,
2084                            x + tr.x*x_scale,
2085                            cursor_y + tr.y,
2086                            TextParams {
2087                                font_size: config.font_size as u16,
2088                                font,
2089                                font_scale: tr.scale_y.max(0.01),
2090                                font_scale_aspect: if tr.scale_y > 0.01 { tr.scale_x / tr.scale_y * x_scale } else { x_scale },
2091                                rotation: tr.rotation.to_radians(),
2092                                color
2093                            }
2094                        );
2095                    }
2096                } else {
2097                    if in_style_def {
2098                        warn!("Style definition didn't end! Here is what we tried to render: {}", config.text);
2099                    } else if failed {
2100                        warn!("Encountered }} without opened style! Make sure to escape curly braces with \\. Here is what we tried to render: {}", config.text);
2101                    }
2102                    normal_render();
2103                }
2104
2105                // Deactivate effect material
2106                if has_effect {
2107                    gl_use_default_material();
2108                }
2109            }
2110            #[cfg(not(feature = "text-styling"))]
2111            RenderCommandConfig::Text(config) => {
2112                let bb = command.bounding_box;
2113                let color = ply_to_macroquad_color(&config.color);
2114                // Ensure font is loaded
2115                if let Some(asset) = config.font_asset {
2116                    FontManager::ensure(asset).await;
2117                }
2118                // Hold the FM lock for the duration of text rendering — no clone needed
2119                let mut fm = FONT_MANAGER.lock().unwrap();
2120                let baseline_y = bb.y + fm.metrics(config.font_size, config.font_asset).baseline_offset;
2121                let font = if let Some(asset) = config.font_asset {
2122                    fm.get(asset)
2123                } else {
2124                    fm.get_default()
2125                };
2126
2127                // Activate effect material if present
2128                let has_effect = !command.effects.is_empty();
2129                if has_effect {
2130                    let effect = &command.effects[0];
2131                    let mut mat_mgr = MATERIAL_MANAGER.lock().unwrap();
2132                    let material = mat_mgr.get_or_create(effect);
2133                    apply_shader_uniforms(material, effect, &bb);
2134                    gl_use_material(material);
2135                }
2136
2137                let x_scale = compute_letter_spacing_x_scale(
2138                    bb.width,
2139                    config.text.chars().count(),
2140                    config.letter_spacing,
2141                );
2142                draw_text_ex(
2143                    &config.text,
2144                    bb.x,
2145                    baseline_y,
2146                    TextParams {
2147                        font_size: config.font_size as u16,
2148                        font,
2149                        font_scale: 1.0,
2150                        font_scale_aspect: x_scale,
2151                        rotation: 0.0,
2152                        color
2153                    }
2154                );
2155
2156                // Deactivate effect material
2157                if has_effect {
2158                    gl_use_default_material();
2159                }
2160            }
2161            RenderCommandConfig::Border(config) => {
2162                let bb = command.bounding_box;
2163                let bw = &config.width;
2164                let cr = &config.corner_radii;
2165                let color = ply_to_macroquad_color(&config.color);
2166                let s = match config.position {
2167                    BorderPosition::Outside => 1.,
2168                    BorderPosition::Middle => 0.5,
2169                    BorderPosition::Inside => 0.0,
2170                };
2171
2172                let get_sides = |corner: f32| {
2173                    (std::f32::consts::PI * corner / (2.0 * PIXELS_PER_POINT)).max(5.0) as usize
2174                };
2175                let v = |x: f32, y: f32| Vertex::new(x, y, 0., 0., 0., color);
2176
2177                let top = bw.top as f32;
2178                let left = bw.left as f32;
2179                let bottom = bw.bottom as f32;
2180                let right = bw.right as f32;
2181                let tl_r = cr.top_left;
2182                let tr_r = cr.top_right;
2183                let bl_r = cr.bottom_left;
2184                let br_r = cr.bottom_right;
2185
2186                let ox1 = bb.x - left * s;
2187                let ox2 = bb.x + bb.width + right * s;
2188                let oy1 = bb.y - top * s;
2189                let oy2 = bb.y + bb.height + bottom * s;
2190                let ix1 = bb.x + left * (1.0 - s);
2191                let ix2 = bb.x + bb.width - right * (1.0 - s);
2192                let iy1 = bb.y + top * (1.0 - s);
2193                let iy2 = bb.y + bb.height - bottom * (1.0 - s);
2194
2195                let o_tl_rx = tl_r + left * s;
2196                let o_tl_ry = tl_r + top * s;
2197                let o_tr_rx = tr_r + right * s;
2198                let o_tr_ry = tr_r + top * s;
2199                let o_bl_rx = bl_r + left * s;
2200                let o_bl_ry = bl_r + bottom * s;
2201                let o_br_rx = br_r + right * s;
2202                let o_br_ry = br_r + bottom * s;
2203                let i_tl_rx = (tl_r - left * (1.0 - s)).max(0.0);
2204                let i_tl_ry = (tl_r - top * (1.0 - s)).max(0.0);
2205                let i_tr_rx = (tr_r - right * (1.0 - s)).max(0.0);
2206                let i_tr_ry = (tr_r - top * (1.0 - s)).max(0.0);
2207                let i_bl_rx = (bl_r - left * (1.0 - s)).max(0.0);
2208                let i_bl_ry = (bl_r - bottom * (1.0 - s)).max(0.0);
2209                let i_br_rx = (br_r - right * (1.0 - s)).max(0.0);
2210                let i_br_ry = (br_r - bottom * (1.0 - s)).max(0.0);
2211
2212                let tl_sides = get_sides(o_tl_rx.max(o_tl_ry).max(i_tl_rx).max(i_tl_ry));
2213                let tr_sides = get_sides(o_tr_rx.max(o_tr_ry).max(i_tr_rx).max(i_tr_ry));
2214                let bl_sides = get_sides(o_bl_rx.max(o_bl_ry).max(i_bl_rx).max(i_bl_ry));
2215                let br_sides = get_sides(o_br_rx.max(o_br_ry).max(i_br_rx).max(i_br_ry));
2216                let side_count = tl_sides + tr_sides + bl_sides + br_sides;
2217
2218                let mut vertices = Vec::<Vertex>::with_capacity(16 + side_count * 4);
2219                let mut indices = Vec::<u16>::with_capacity(24 + side_count * 6);
2220
2221                // 4 quads
2222                vertices.extend([
2223                    // Top edge
2224                    v(ox1 + o_tl_rx, oy1),
2225                    v(ox2 - o_tr_rx, oy1),
2226                    v(ix1 + i_tl_rx, iy1),
2227                    v(ix2 - i_tr_rx, iy1),
2228                    // Bottom edge
2229                    v(ox1 + o_bl_rx, oy2),
2230                    v(ox2 - o_br_rx, oy2),
2231                    v(ix1 + i_bl_rx, iy2),
2232                    v(ix2 - i_br_rx, iy2),
2233                    // Left edge
2234                    v(ox1, oy1 + o_tl_ry),
2235                    v(ox1, oy2 - o_bl_ry),
2236                    v(ix1, iy1 + i_tl_ry),
2237                    v(ix1, iy2 - i_bl_ry),
2238                    // Right edge
2239                    v(ox2, oy1 + o_tr_ry),
2240                    v(ox2, oy2 - o_br_ry),
2241                    v(ix2, iy1 + i_tr_ry),
2242                    v(ix2, iy2 - i_br_ry),
2243                ]);
2244                for l in [0, 4, 8, 12] {
2245                    indices.extend([
2246                        l, l + 1, l + 2,
2247                        l + 1, l + 3, l + 2
2248                    ]);
2249                }
2250
2251                let corners = [
2252                    (
2253                        tl_sides,
2254                        PI,
2255                        ox1 + o_tl_rx,
2256                        oy1 + o_tl_ry,
2257                        ix1 + i_tl_rx,
2258                        iy1 + i_tl_ry,
2259                        o_tl_rx,
2260                        o_tl_ry,
2261                        i_tl_rx,
2262                        i_tl_ry,
2263                    ),
2264                    (
2265                        tr_sides,
2266                        PI * 1.5,
2267                        ox2 - o_tr_rx,
2268                        oy1 + o_tr_ry,
2269                        ix2 - i_tr_rx,
2270                        iy1 + i_tr_ry,
2271                        o_tr_rx,
2272                        o_tr_ry,
2273                        i_tr_rx,
2274                        i_tr_ry,
2275                    ),
2276                    (
2277                        bl_sides,
2278                        PI * 0.5,
2279                        ox1 + o_bl_rx,
2280                        oy2 - o_bl_ry,
2281                        ix1 + i_bl_rx,
2282                        iy2 - i_bl_ry,
2283                        o_bl_rx,
2284                        o_bl_ry,
2285                        i_bl_rx,
2286                        i_bl_ry,
2287                    ),
2288                    (
2289                        br_sides,
2290                        0.,
2291                        ox2 - o_br_rx,
2292                        oy2 - o_br_ry,
2293                        ix2 - i_br_rx,
2294                        iy2 - i_br_ry,
2295                        o_br_rx,
2296                        o_br_ry,
2297                        i_br_rx,
2298                        i_br_ry,
2299                    ),
2300                ];
2301
2302                for (sides, start, ocx, ocy, icx, icy, o_rx, o_ry, i_rx, i_ry) in corners {
2303                    let step = (PI / 2.) / (sides as f32);
2304
2305                    for i in 0..sides {
2306                        let i = i as f32;
2307                        let a1 = start + i * step;
2308                        let a2 = a1 + step;
2309                        let l = vertices.len() as u16;
2310
2311                        // quad
2312                        vertices.extend([
2313                            v(ocx + a1.cos() * o_rx, ocy + a1.sin() * o_ry),
2314                            v(ocx + a2.cos() * o_rx, ocy + a2.sin() * o_ry),
2315                            v(icx + a1.cos() * i_rx, icy + a1.sin() * i_ry),
2316                            v(icx + a2.cos() * i_rx, icy + a2.sin() * i_ry),
2317                        ]);
2318                        indices.extend([
2319                            l, l + 1, l + 2,
2320                            l + 1, l + 3, l + 2
2321                        ]);
2322                    }
2323                }
2324
2325                draw_mesh(&Mesh { vertices, indices, texture: None });
2326            }
2327            RenderCommandConfig::ScissorStart() => {
2328                let bb = command.bounding_box;
2329                // Layout coordinates are in logical pixels, but macroquad's
2330                // quad_gl.scissor() passes values to glScissor which operates
2331                // in physical (framebuffer) pixels.  Scale by DPI so the
2332                // scissor rectangle matches on high-DPI displays (e.g. WASM).
2333                let dpi = miniquad::window::dpi_scale();
2334                state.clip = Some((
2335                    (bb.x * dpi) as i32,
2336                    (bb.y * dpi) as i32,
2337                    (bb.width * dpi) as i32,
2338                    (bb.height * dpi) as i32,
2339                ));
2340                unsafe {
2341                    get_internal_gl().quad_gl.scissor(state.clip);
2342                }
2343            }
2344            RenderCommandConfig::ScissorEnd() => {
2345                state.clip = None;
2346                unsafe {
2347                    get_internal_gl().quad_gl.scissor(None);
2348                }
2349            }
2350            RenderCommandConfig::Custom(_) => {
2351                handle_custom_command(&command);
2352            }
2353            RenderCommandConfig::GroupBegin { ref shader, ref visual_rotation } => {
2354                let bb = command.bounding_box;
2355                let rt = render_target_msaa(bb.width as u32, bb.height as u32);
2356                rt.texture.set_filter(FilterMode::Linear);
2357                let cam = Camera2D {
2358                    render_target: Some(rt.clone()),
2359                    ..Camera2D::from_display_rect(Rect::new(
2360                        bb.x, bb.y, bb.width, bb.height,
2361                    ))
2362                };
2363                set_camera(&cam);
2364                clear_background(Color::new(0.0, 0.0, 0.0, 0.0));
2365                state.rt_stack.push((rt, shader.clone(), *visual_rotation, bb));
2366            }
2367            RenderCommandConfig::GroupEnd => {
2368                if let Some((rt, shader_config, visual_rotation, bb)) = state.rt_stack.pop() {
2369                    // Restore previous camera
2370                    if let Some((prev_rt, _, _, prev_bb)) = state.rt_stack.last() {
2371                        let cam = Camera2D {
2372                            render_target: Some(prev_rt.clone()),
2373                            ..Camera2D::from_display_rect(Rect::new(
2374                                prev_bb.x, prev_bb.y, prev_bb.width, prev_bb.height,
2375                            ))
2376                        };
2377                        set_camera(&cam);
2378                    } else {
2379                        set_default_camera();
2380                    }
2381
2382                    // Apply the shader material if present
2383                    if let Some(ref config) = shader_config {
2384                        let mut mat_mgr = MATERIAL_MANAGER.lock().unwrap();
2385                        let material = mat_mgr.get_or_create(config);
2386                        apply_shader_uniforms(material, config, &bb);
2387                        gl_use_material(material);
2388                    }
2389
2390                    // Compute draw params — apply visual rotation if present
2391                    let (rotation, flip_x, flip_y, pivot) = match &visual_rotation {
2392                        Some(rot) => {
2393                            let pivot_screen = Vec2::new(
2394                                bb.x + rot.pivot_x * bb.width,
2395                                bb.y + rot.pivot_y * bb.height,
2396                            );
2397                            // flip_y is inverted because render targets are flipped in OpenGL
2398                            (rot.rotation_radians, rot.flip_x, !rot.flip_y, Some(pivot_screen))
2399                        }
2400                        None => (0.0, false, true, None),
2401                    };
2402
2403                    draw_texture_ex(
2404                        &rt.texture,
2405                        bb.x,
2406                        bb.y,
2407                        WHITE,
2408                        DrawTextureParams {
2409                            dest_size: Some(Vec2::new(bb.width, bb.height)),
2410                            rotation,
2411                            flip_x,
2412                            flip_y,
2413                            pivot,
2414                            ..Default::default()
2415                        },
2416                    );
2417
2418                    if shader_config.is_some() {
2419                        gl_use_default_material();
2420                    }
2421                }
2422            }
2423            RenderCommandConfig::None() => {}
2424        }
2425    }
2426    TEXTURE_MANAGER.lock().unwrap().clean();
2427    MATERIAL_MANAGER.lock().unwrap().clean();
2428    FONT_MANAGER.lock().unwrap().clean();
2429}
2430
2431pub fn create_measure_text_function(
2432) -> impl Fn(&str, &crate::TextConfig) -> crate::Dimensions + 'static {
2433    move |text: &str, config: &crate::TextConfig| {
2434        #[cfg(feature = "text-styling")]
2435        let cleaned_text = {
2436            // Remove macroquad_text_styling tags, handling escapes
2437            let mut result = String::new();
2438            let mut in_style_def = false;
2439            let mut escaped = false;
2440            for c in text.chars() {
2441                if escaped {
2442                    result.push(c);
2443                    escaped = false;
2444                    continue;
2445                }
2446                match c {
2447                    '\\' => {
2448                        escaped = true;
2449                    }
2450                    '{' => {
2451                        in_style_def = true;
2452                    }
2453                    '|' => {
2454                        if in_style_def {
2455                            in_style_def = false;
2456                        } else {
2457                            result.push(c);
2458                        }
2459                    }
2460                    '}' => {
2461                        // Nothing
2462                    }
2463                    _ => {
2464                        if !in_style_def {
2465                            result.push(c);
2466                        }
2467                    }
2468                }
2469            }
2470            if in_style_def {
2471                warn!("Ended inside a style definition while cleaning text for measurement! Make sure to escape curly braces with \\. Here is what we tried to measure: {}", text);
2472            }
2473            result
2474        };
2475        #[cfg(not(feature = "text-styling"))]
2476        let cleaned_text = text.to_string();
2477        let mut fm = FONT_MANAGER.lock().unwrap();
2478        // Resolve font: use asset font if available, otherwise default
2479        let font = if let Some(asset) = config.font_asset {
2480            fm.get(asset)
2481        } else {
2482            fm.get_default()
2483        };
2484        let measured = macroquad::text::measure_text(
2485            &cleaned_text,
2486            font,
2487            config.font_size,
2488            1.0,
2489        );
2490        let metrics = fm.metrics(config.font_size as u16, config.font_asset);
2491        let added_space = (cleaned_text.chars().count().max(1) - 1) as f32 * config.letter_spacing as f32;
2492        crate::Dimensions::new(measured.width + added_space, metrics.height)
2493    }
2494}
2495
2496/// Count visible characters in text, skipping style tag markup.
2497/// This handles `{style_name|` openers, `}` closers, and `\` escapes.
2498#[cfg(feature = "text-styling")]
2499fn count_visible_chars(text: &str) -> usize {
2500    let mut count = 0;
2501    let mut in_style_def = false;
2502    let mut escaped = false;
2503    for c in text.chars() {
2504        if escaped { count += 1; escaped = false; continue; }
2505        match c {
2506            '\\' => { escaped = true; }
2507            '{' => { in_style_def = true; }
2508            '|' => { if in_style_def { in_style_def = false; } else { count += 1; } }
2509            '}' => { }
2510            _ => { if !in_style_def { count += 1; } }
2511        }
2512    }
2513    count
2514}
2515
2516/// Compute the horizontal scale factor needed to visually apply letter-spacing.
2517///
2518/// The bounding-box width already includes the total letter-spacing contribution
2519/// (`(visible_chars - 1) * letter_spacing`). By dividing out that contribution we
2520/// recover the raw text width, and the ratio `bb_width / raw_width` gives the
2521/// scale factor that macroquad should use to stretch each glyph.
2522fn compute_letter_spacing_x_scale(bb_width: f32, visible_char_count: usize, letter_spacing: u16) -> f32 {
2523    if letter_spacing == 0 || visible_char_count <= 1 {
2524        return 1.0;
2525    }
2526    let total_spacing = (visible_char_count as f32 - 1.0) * letter_spacing as f32;
2527    let raw_width = bb_width - total_spacing;
2528    if raw_width > 0.0 {
2529        bb_width / raw_width
2530    } else {
2531        1.0
2532    }
2533}