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_stack: Vec<(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_stack: Vec::new(),
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
877fn intersect_scissor(
878    a: (i32, i32, i32, i32),
879    b: (i32, i32, i32, i32),
880) -> (i32, i32, i32, i32) {
881    let ax2 = a.0.saturating_add(a.2);
882    let ay2 = a.1.saturating_add(a.3);
883    let bx2 = b.0.saturating_add(b.2);
884    let by2 = b.1.saturating_add(b.3);
885
886    let x1 = a.0.max(b.0);
887    let y1 = a.1.max(b.1);
888    let x2 = ax2.min(bx2);
889    let y2 = ay2.min(by2);
890
891    (x1, y1, (x2 - x1).max(0), (y2 - y1).max(0))
892}
893
894/// Render custom content to a [`Texture2D`]
895///
896/// Sets up a render target, points a camera at it, calls your closure, then
897/// restores the default camera and returns the resulting texture.
898/// The coordinate system inside the closure runs from `(0, 0)` at the top-left
899/// to `(width, height)` at the bottom-right.
900///
901/// Call this before the layout pass, then hand the texture to an element with `.image(tex)`.
902///
903/// # Example
904/// ```rust,ignore
905/// let tex = render_to_texture(200.0, 100.0, || {
906///     clear_background(BLANK);
907///     draw_circle(w / 2.0, h / 2.0, 40.0, RED);
908/// });
909/// ```
910pub fn render_to_texture(width: f32, height: f32, draw: impl FnOnce()) -> Texture2D {
911    let render_target = render_target_msaa(width as u32, height as u32);
912    render_target.texture.set_filter(FilterMode::Linear);
913    let mut cam = Camera2D::from_display_rect(Rect::new(0.0, 0.0, width, height));
914    cam.render_target = Some(render_target.clone());
915    set_camera(&cam);
916
917    draw();
918
919    set_default_camera();
920    render_target.texture
921}
922
923fn rounded_rectangle_texture(cr: &CornerRadii, bb: &BoundingBox, clip: &Option<(i32, i32, i32, i32)>) -> Texture2D {
924    let render_target = render_target_msaa(bb.width as u32, bb.height as u32);
925    render_target.texture.set_filter(FilterMode::Linear);
926    let mut cam = Camera2D::from_display_rect(Rect::new(0.0, 0.0, bb.width, bb.height));
927    cam.render_target = Some(render_target.clone());
928    set_camera(&cam);
929    unsafe {
930        get_internal_gl().quad_gl.scissor(None);
931    };
932
933    draw_good_rounded_rectangle(0.0, 0.0, bb.width, bb.height, cr, WHITE);
934
935    set_default_camera();
936    unsafe {
937        get_internal_gl().quad_gl.scissor(*clip);
938    }
939    render_target.texture
940}
941
942/// Render a TinyVG image to a RenderTarget, scaled to fit the given dimensions.
943/// Decodes from raw bytes, then delegates to `render_tinyvg_image`.
944#[cfg(feature = "tinyvg")]
945fn render_tinyvg_texture(
946    tvg_data: &[u8],
947    dest_width: f32,
948    dest_height: f32,
949    clip: &Option<(i32, i32, i32, i32)>,
950) -> Option<RenderTarget> {
951    use tinyvg::Decoder;
952    let decoder = Decoder::new(std::io::Cursor::new(tvg_data));
953    let image = match decoder.decode() {
954        Ok(img) => img,
955        Err(_) => return None,
956    };
957    render_tinyvg_image(&image, dest_width, dest_height, clip)
958}
959
960/// Render a decoded `tinyvg::format::Image` to a RenderTarget, scaled to fit the given dimensions.
961#[cfg(feature = "tinyvg")]
962fn render_tinyvg_image(
963    image: &tinyvg::format::Image,
964    dest_width: f32,
965    dest_height: f32,
966    clip: &Option<(i32, i32, i32, i32)>,
967) -> Option<RenderTarget> {
968    use tinyvg::format::{Command, Style, Segment, SegmentCommandKind, Point as TvgPoint, Color as TvgColor};
969    use kurbo::{BezPath, Point as KurboPoint, Vec2 as KurboVec2, ParamCurve, SvgArc, Arc as KurboArc, PathEl};
970    use lyon::tessellation::{FillTessellator, FillOptions, VertexBuffers, BuffersBuilder, FillVertex, FillRule};
971    use lyon::path::Path as LyonPath;
972    use lyon::math::point as lyon_point;
973    
974    fn tvg_to_kurbo(p: TvgPoint) -> KurboPoint {
975        KurboPoint::new(p.x, p.y)
976    }
977    
978    let tvg_width = image.header.width as f32;
979    let tvg_height = image.header.height as f32;
980    let scale_x = dest_width / tvg_width;
981    let scale_y = dest_height / tvg_height;
982    
983    let render_target = render_target_msaa(dest_width as u32, dest_height as u32);
984    render_target.texture.set_filter(FilterMode::Linear);
985    let mut cam = Camera2D::from_display_rect(Rect::new(0.0, 0.0, dest_width, dest_height));
986    cam.render_target = Some(render_target.clone());
987    set_camera(&cam);
988    unsafe {
989        get_internal_gl().quad_gl.scissor(None);
990    }
991    
992    let tvg_to_mq_color = |c: &TvgColor| -> Color {
993        let (r, g, b, a) = c.as_rgba();
994        Color::new(r as f32, g as f32, b as f32, a as f32)
995    };
996    
997    let style_to_color = |style: &Style, color_table: &[TvgColor]| -> Color {
998        match style {
999            Style::FlatColor { color_index } => {
1000                color_table.get(*color_index).map(|c| tvg_to_mq_color(c)).unwrap_or(WHITE)
1001            }
1002            Style::LinearGradient { color_index_0, .. } |
1003            Style::RadialGradient { color_index_0, .. } => {
1004                color_table.get(*color_index_0).map(|c| tvg_to_mq_color(c)).unwrap_or(WHITE)
1005            }
1006        }
1007    };
1008    
1009    let draw_filled_path_lyon = |bezpath: &BezPath, color: Color| {
1010        let mut builder = LyonPath::builder();
1011        let mut subpath_started = false;
1012        
1013        for el in bezpath.iter() {
1014            match el {
1015                PathEl::MoveTo(p) => {
1016                    if subpath_started {
1017                        builder.end(false);
1018                    }
1019                    builder.begin(lyon_point((p.x * scale_x as f64) as f32, (p.y * scale_y as f64) as f32));
1020                    subpath_started = true;
1021                }
1022                PathEl::LineTo(p) => {
1023                    builder.line_to(lyon_point((p.x * scale_x as f64) as f32, (p.y * scale_y as f64) as f32));
1024                }
1025                PathEl::QuadTo(c, p) => {
1026                    builder.quadratic_bezier_to(
1027                        lyon_point((c.x * scale_x as f64) as f32, (c.y * scale_y as f64) as f32),
1028                        lyon_point((p.x * scale_x as f64) as f32, (p.y * scale_y as f64) as f32),
1029                    );
1030                }
1031                PathEl::CurveTo(c1, c2, p) => {
1032                    builder.cubic_bezier_to(
1033                        lyon_point((c1.x * scale_x as f64) as f32, (c1.y * scale_y as f64) as f32),
1034                        lyon_point((c2.x * scale_x as f64) as f32, (c2.y * scale_y as f64) as f32),
1035                        lyon_point((p.x * scale_x as f64) as f32, (p.y * scale_y as f64) as f32),
1036                    );
1037                }
1038                PathEl::ClosePath => {
1039                    builder.end(true);
1040                    subpath_started = false;
1041                }
1042            }
1043        }
1044        
1045        if subpath_started {
1046            builder.end(true);
1047        }
1048        
1049        let lyon_path = builder.build();
1050        
1051        let mut geometry: VertexBuffers<[f32; 2], u16> = VertexBuffers::new();
1052        let mut tessellator = FillTessellator::new();
1053        
1054        let fill_options = FillOptions::default().with_fill_rule(FillRule::NonZero);
1055        
1056        let result = tessellator.tessellate_path(
1057            &lyon_path,
1058            &fill_options,
1059            &mut BuffersBuilder::new(&mut geometry, |vertex: FillVertex| {
1060                vertex.position().to_array()
1061            }),
1062        );
1063        
1064        if result.is_err() || geometry.indices.is_empty() {
1065            return;
1066        }
1067        
1068        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];
1069        
1070        let vertices: Vec<Vertex> = geometry.vertices.iter().map(|pos| {
1071            Vertex {
1072                position: Vec3::new(pos[0], pos[1], 0.0),
1073                uv: Vec2::ZERO,
1074                color: color_bytes,
1075                normal: Vec4::new(0.0, 0.0, 1.0, 0.0),
1076            }
1077        }).collect();
1078        
1079        let mesh = Mesh {
1080            vertices,
1081            indices: geometry.indices,
1082            texture: None,
1083        };
1084        draw_mesh(&mesh);
1085    };
1086    
1087    let draw_filled_polygon_tvg = |points: &[TvgPoint], color: Color| {
1088        if points.len() < 3 {
1089            return;
1090        }
1091        
1092        let mut builder = LyonPath::builder();
1093        builder.begin(lyon_point(points[0].x as f32 * scale_x, points[0].y as f32 * scale_y));
1094        for point in &points[1..] {
1095            builder.line_to(lyon_point(point.x as f32 * scale_x, point.y as f32 * scale_y));
1096        }
1097        builder.end(true);
1098        let lyon_path = builder.build();
1099        
1100        let mut geometry: VertexBuffers<[f32; 2], u16> = VertexBuffers::new();
1101        let mut tessellator = FillTessellator::new();
1102        
1103        let result = tessellator.tessellate_path(
1104            &lyon_path,
1105            &FillOptions::default(),
1106            &mut BuffersBuilder::new(&mut geometry, |vertex: FillVertex| {
1107                vertex.position().to_array()
1108            }),
1109        );
1110        
1111        if result.is_err() || geometry.indices.is_empty() {
1112            return;
1113        }
1114        
1115        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];
1116        
1117        let vertices: Vec<Vertex> = geometry.vertices.iter().map(|pos| {
1118            Vertex {
1119                position: Vec3::new(pos[0], pos[1], 0.0),
1120                uv: Vec2::ZERO,
1121                color: color_bytes,
1122                normal: Vec4::new(0.0, 0.0, 1.0, 0.0),
1123            }
1124        }).collect();
1125        
1126        let mesh = Mesh {
1127            vertices,
1128            indices: geometry.indices,
1129            texture: None,
1130        };
1131        draw_mesh(&mesh);
1132    };
1133    
1134    let build_bezpath = |segments: &[Segment]| -> BezPath {
1135        let mut bezier = BezPath::new();
1136        for segment in segments {
1137            let start = tvg_to_kurbo(segment.start);
1138            let mut pen = start;
1139            bezier.move_to(pen);
1140            
1141            for cmd in &segment.commands {
1142                match &cmd.kind {
1143                    SegmentCommandKind::Line { end } => {
1144                        let end_k = tvg_to_kurbo(*end);
1145                        bezier.line_to(end_k);
1146                        pen = end_k;
1147                    }
1148                    SegmentCommandKind::HorizontalLine { x } => {
1149                        let end = KurboPoint::new(*x, pen.y);
1150                        bezier.line_to(end);
1151                        pen = end;
1152                    }
1153                    SegmentCommandKind::VerticalLine { y } => {
1154                        let end = KurboPoint::new(pen.x, *y);
1155                        bezier.line_to(end);
1156                        pen = end;
1157                    }
1158                    SegmentCommandKind::CubicBezier { control_0, control_1, point_1 } => {
1159                        let c0 = tvg_to_kurbo(*control_0);
1160                        let c1 = tvg_to_kurbo(*control_1);
1161                        let p1 = tvg_to_kurbo(*point_1);
1162                        bezier.curve_to(c0, c1, p1);
1163                        pen = p1;
1164                    }
1165                    SegmentCommandKind::QuadraticBezier { control, point_1 } => {
1166                        let c = tvg_to_kurbo(*control);
1167                        let p1 = tvg_to_kurbo(*point_1);
1168                        bezier.quad_to(c, p1);
1169                        pen = p1;
1170                    }
1171                    SegmentCommandKind::ArcEllipse { large, sweep, radius_x, radius_y, rotation, target } => {
1172                        let target_k = tvg_to_kurbo(*target);
1173                        let svg_arc = SvgArc {
1174                            from: pen,
1175                            to: target_k,
1176                            radii: KurboVec2::new(*radius_x, *radius_y),
1177                            x_rotation: *rotation,
1178                            large_arc: *large,
1179                            sweep: *sweep,
1180                        };
1181                        if let Some(arc) = KurboArc::from_svg_arc(&svg_arc) {
1182                            for seg in arc.append_iter(0.2) {
1183                                bezier.push(seg);
1184                            }
1185                        }
1186                        pen = target_k;
1187                    }
1188                    SegmentCommandKind::ClosePath => {
1189                        bezier.close_path();
1190                        pen = start;
1191                    }
1192                }
1193            }
1194        }
1195        bezier
1196    };
1197    
1198    let line_scale = (scale_x + scale_y) / 2.0;
1199    
1200    for cmd in &image.commands {
1201        match cmd {
1202            Command::FillPath { fill_style, path, outline } => {
1203                let fill_color = style_to_color(fill_style, &image.color_table);
1204                let bezpath = build_bezpath(path);
1205                draw_filled_path_lyon(&bezpath, fill_color);
1206                
1207                if let Some(outline_style) = outline {
1208                    let line_color = style_to_color(&outline_style.line_style, &image.color_table);
1209                    let line_width = outline_style.line_width as f32 * line_scale;
1210                    for segment in path {
1211                        let start = segment.start;
1212                        let mut pen = start;
1213                        for cmd in &segment.commands {
1214                            match &cmd.kind {
1215                                SegmentCommandKind::Line { end } => {
1216                                    draw_line(
1217                                        pen.x as f32 * scale_x, pen.y as f32 * scale_y,
1218                                        end.x as f32 * scale_x, end.y as f32 * scale_y,
1219                                        line_width, line_color
1220                                    );
1221                                    pen = *end;
1222                                }
1223                                SegmentCommandKind::HorizontalLine { x } => {
1224                                    let end = TvgPoint { x: *x, y: pen.y };
1225                                    draw_line(
1226                                        pen.x as f32 * scale_x, pen.y as f32 * scale_y,
1227                                        end.x as f32 * scale_x, end.y as f32 * scale_y,
1228                                        line_width, line_color
1229                                    );
1230                                    pen = end;
1231                                }
1232                                SegmentCommandKind::VerticalLine { y } => {
1233                                    let end = TvgPoint { x: pen.x, y: *y };
1234                                    draw_line(
1235                                        pen.x as f32 * scale_x, pen.y as f32 * scale_y,
1236                                        end.x as f32 * scale_x, end.y as f32 * scale_y,
1237                                        line_width, line_color
1238                                    );
1239                                    pen = end;
1240                                }
1241                                SegmentCommandKind::ClosePath => {
1242                                    draw_line(
1243                                        pen.x as f32 * scale_x, pen.y as f32 * scale_y,
1244                                        start.x as f32 * scale_x, start.y as f32 * scale_y,
1245                                        line_width, line_color
1246                                    );
1247                                    pen = start;
1248                                }
1249                                SegmentCommandKind::CubicBezier { control_0, control_1, point_1 } => {
1250                                    let c0 = tvg_to_kurbo(*control_0);
1251                                    let c1 = tvg_to_kurbo(*control_1);
1252                                    let p1 = tvg_to_kurbo(*point_1);
1253                                    let p0 = tvg_to_kurbo(pen);
1254                                    let cubic = kurbo::CubicBez::new(p0, c0, c1, p1);
1255                                    let steps = 16usize;
1256                                    let mut prev = p0;
1257                                    for i in 1..=steps {
1258                                        let t = i as f64 / steps as f64;
1259                                        let next = cubic.eval(t);
1260                                        draw_line(
1261                                            prev.x as f32 * scale_x, prev.y as f32 * scale_y,
1262                                            next.x as f32 * scale_x, next.y as f32 * scale_y,
1263                                            line_width, line_color
1264                                        );
1265                                        prev = next;
1266                                    }
1267                                    pen = *point_1;
1268                                }
1269                                SegmentCommandKind::QuadraticBezier { control, point_1 } => {
1270                                    let c = tvg_to_kurbo(*control);
1271                                    let p1 = tvg_to_kurbo(*point_1);
1272                                    let p0 = tvg_to_kurbo(pen);
1273                                    let quad = kurbo::QuadBez::new(p0, c, p1);
1274                                    let steps = 12usize;
1275                                    let mut prev = p0;
1276                                    for i in 1..=steps {
1277                                        let t = i as f64 / steps as f64;
1278                                        let next = quad.eval(t);
1279                                        draw_line(
1280                                            prev.x as f32 * scale_x, prev.y as f32 * scale_y,
1281                                            next.x as f32 * scale_x, next.y as f32 * scale_y,
1282                                            line_width, line_color
1283                                        );
1284                                        prev = next;
1285                                    }
1286                                    pen = *point_1;
1287                                }
1288                                SegmentCommandKind::ArcEllipse { large, sweep, radius_x, radius_y, rotation, target } => {
1289                                    let target_k = tvg_to_kurbo(*target);
1290                                    let p0 = tvg_to_kurbo(pen);
1291                                    let svg_arc = SvgArc {
1292                                        from: p0,
1293                                        to: target_k,
1294                                        radii: KurboVec2::new(*radius_x, *radius_y),
1295                                        x_rotation: *rotation,
1296                                        large_arc: *large,
1297                                        sweep: *sweep,
1298                                    };
1299                                    if let Some(arc) = KurboArc::from_svg_arc(&svg_arc) {
1300                                        let mut prev = p0;
1301                                        for seg in arc.append_iter(0.2) {
1302                                            match seg {
1303                                                PathEl::LineTo(p) | PathEl::MoveTo(p) => {
1304                                                    draw_line(
1305                                                        prev.x as f32 * scale_x, prev.y as f32 * scale_y,
1306                                                        p.x as f32 * scale_x, p.y as f32 * scale_y,
1307                                                        line_width, line_color
1308                                                    );
1309                                                    prev = p;
1310                                                }
1311                                                PathEl::CurveTo(c0, c1, p) => {
1312                                                    // Flatten the curve
1313                                                    let cubic = kurbo::CubicBez::new(prev, c0, c1, p);
1314                                                    let steps = 8usize;
1315                                                    let mut prev_pt = prev;
1316                                                    for j in 1..=steps {
1317                                                        let t = j as f64 / steps as f64;
1318                                                        let next = cubic.eval(t);
1319                                                        draw_line(
1320                                                            prev_pt.x as f32 * scale_x, prev_pt.y as f32 * scale_y,
1321                                                            next.x as f32 * scale_x, next.y as f32 * scale_y,
1322                                                            line_width, line_color
1323                                                        );
1324                                                        prev_pt = next;
1325                                                    }
1326                                                    prev = p;
1327                                                }
1328                                                _ => {}
1329                                            }
1330                                        }
1331                                    }
1332                                    pen = *target;
1333                                }
1334                            }
1335                        }
1336                    }
1337                }
1338            }
1339            Command::FillRectangles { fill_style, rectangles, outline } => {
1340                let fill_color = style_to_color(fill_style, &image.color_table);
1341                for rect in rectangles {
1342                    draw_rectangle(
1343                        rect.x0 as f32 * scale_x,
1344                        rect.y0 as f32 * scale_y,
1345                        rect.width() as f32 * scale_x,
1346                        rect.height() as f32 * scale_y,
1347                        fill_color
1348                    );
1349                }
1350                
1351                if let Some(outline_style) = outline {
1352                    let line_color = style_to_color(&outline_style.line_style, &image.color_table);
1353                    let line_width = outline_style.line_width as f32 * line_scale;
1354                    for rect in rectangles {
1355                        draw_rectangle_lines(
1356                            rect.x0 as f32 * scale_x,
1357                            rect.y0 as f32 * scale_y,
1358                            rect.width() as f32 * scale_x,
1359                            rect.height() as f32 * scale_y,
1360                            line_width, line_color
1361                        );
1362                    }
1363                }
1364            }
1365            Command::FillPolygon { fill_style, polygon, outline } => {
1366                let fill_color = style_to_color(fill_style, &image.color_table);
1367                draw_filled_polygon_tvg(polygon, fill_color);
1368                
1369                if let Some(outline_style) = outline {
1370                    let line_color = style_to_color(&outline_style.line_style, &image.color_table);
1371                    let line_width = outline_style.line_width as f32 * line_scale;
1372                    for i in 0..polygon.len() {
1373                        let next = (i + 1) % polygon.len();
1374                        draw_line(
1375                            polygon[i].x as f32 * scale_x, polygon[i].y as f32 * scale_y,
1376                            polygon[next].x as f32 * scale_x, polygon[next].y as f32 * scale_y,
1377                            line_width, line_color
1378                        );
1379                    }
1380                }
1381            }
1382            Command::DrawLines { line_style, line_width, lines } => {
1383                let line_color = style_to_color(line_style, &image.color_table);
1384                for line in lines {
1385                    draw_line(
1386                        line.p0.x as f32 * scale_x, line.p0.y as f32 * scale_y,
1387                        line.p1.x as f32 * scale_x, line.p1.y as f32 * scale_y,
1388                        *line_width as f32 * line_scale, line_color
1389                    );
1390                }
1391            }
1392            Command::DrawLineLoop { line_style, line_width, close_path, points } => {
1393                let line_color = style_to_color(line_style, &image.color_table);
1394                for i in 0..points.len().saturating_sub(1) {
1395                    draw_line(
1396                        points[i].x as f32 * scale_x, points[i].y as f32 * scale_y,
1397                        points[i+1].x as f32 * scale_x, points[i+1].y as f32 * scale_y,
1398                        *line_width as f32 * line_scale, line_color
1399                    );
1400                }
1401                if *close_path && points.len() >= 2 {
1402                    let last = points.len() - 1;
1403                    draw_line(
1404                        points[last].x as f32 * scale_x, points[last].y as f32 * scale_y,
1405                        points[0].x as f32 * scale_x, points[0].y as f32 * scale_y,
1406                        *line_width as f32 * line_scale, line_color
1407                    );
1408                }
1409            }
1410            Command::DrawLinePath { line_style, line_width, path } => {
1411                let line_color = style_to_color(line_style, &image.color_table);
1412                let scaled_line_width = *line_width as f32 * line_scale;
1413                // Draw line path by tracing segments directly
1414                for segment in path {
1415                    let start = segment.start;
1416                    let mut pen = start;
1417                    for cmd in &segment.commands {
1418                        match &cmd.kind {
1419                            SegmentCommandKind::Line { end } => {
1420                                draw_line(
1421                                    pen.x as f32 * scale_x, pen.y as f32 * scale_y,
1422                                    end.x as f32 * scale_x, end.y as f32 * scale_y,
1423                                    scaled_line_width, line_color
1424                                );
1425                                pen = *end;
1426                            }
1427                            SegmentCommandKind::HorizontalLine { x } => {
1428                                let end = TvgPoint { x: *x, y: pen.y };
1429                                draw_line(
1430                                    pen.x as f32 * scale_x, pen.y as f32 * scale_y,
1431                                    end.x as f32 * scale_x, end.y as f32 * scale_y,
1432                                    scaled_line_width, line_color
1433                                );
1434                                pen = end;
1435                            }
1436                            SegmentCommandKind::VerticalLine { y } => {
1437                                let end = TvgPoint { x: pen.x, y: *y };
1438                                draw_line(
1439                                    pen.x as f32 * scale_x, pen.y as f32 * scale_y,
1440                                    end.x as f32 * scale_x, end.y as f32 * scale_y,
1441                                    scaled_line_width, line_color
1442                                );
1443                                pen = end;
1444                            }
1445                            SegmentCommandKind::ClosePath => {
1446                                draw_line(
1447                                    pen.x as f32 * scale_x, pen.y as f32 * scale_y,
1448                                    start.x as f32 * scale_x, start.y as f32 * scale_y,
1449                                    scaled_line_width, line_color
1450                                );
1451                                pen = start;
1452                            }
1453                            // For curves, we need to flatten them for line drawing
1454                            SegmentCommandKind::CubicBezier { control_0, control_1, point_1 } => {
1455                                let c0 = tvg_to_kurbo(*control_0);
1456                                let c1 = tvg_to_kurbo(*control_1);
1457                                let p1 = tvg_to_kurbo(*point_1);
1458                                let p0 = tvg_to_kurbo(pen);
1459                                let cubic = kurbo::CubicBez::new(p0, c0, c1, p1);
1460                                let steps = 16usize;
1461                                let mut prev = p0;
1462                                for i in 1..=steps {
1463                                    let t = i as f64 / steps as f64;
1464                                    let next = cubic.eval(t);
1465                                    draw_line(
1466                                        prev.x as f32 * scale_x, prev.y as f32 * scale_y,
1467                                        next.x as f32 * scale_x, next.y as f32 * scale_y,
1468                                        scaled_line_width, line_color
1469                                    );
1470                                    prev = next;
1471                                }
1472                                pen = *point_1;
1473                            }
1474                            SegmentCommandKind::QuadraticBezier { control, point_1 } => {
1475                                let c = tvg_to_kurbo(*control);
1476                                let p1 = tvg_to_kurbo(*point_1);
1477                                let p0 = tvg_to_kurbo(pen);
1478                                let quad = kurbo::QuadBez::new(p0, c, p1);
1479                                let steps = 12usize;
1480                                let mut prev = p0;
1481                                for i in 1..=steps {
1482                                    let t = i as f64 / steps as f64;
1483                                    let next = quad.eval(t);
1484                                    draw_line(
1485                                        prev.x as f32 * scale_x, prev.y as f32 * scale_y,
1486                                        next.x as f32 * scale_x, next.y as f32 * scale_y,
1487                                        scaled_line_width, line_color
1488                                    );
1489                                    prev = next;
1490                                }
1491                                pen = *point_1;
1492                            }
1493                            SegmentCommandKind::ArcEllipse { large, sweep, radius_x, radius_y, rotation, target } => {
1494                                let target_k = tvg_to_kurbo(*target);
1495                                let p0 = tvg_to_kurbo(pen);
1496                                let svg_arc = SvgArc {
1497                                    from: p0,
1498                                    to: target_k,
1499                                    radii: KurboVec2::new(*radius_x, *radius_y),
1500                                    x_rotation: *rotation,
1501                                    large_arc: *large,
1502                                    sweep: *sweep,
1503                                };
1504                                if let Some(arc) = KurboArc::from_svg_arc(&svg_arc) {
1505                                    let mut prev = p0;
1506                                    for seg in arc.append_iter(0.2) {
1507                                        match seg {
1508                                            PathEl::LineTo(p) | PathEl::MoveTo(p) => {
1509                                                draw_line(
1510                                                    prev.x as f32 * scale_x, prev.y as f32 * scale_y,
1511                                                    p.x as f32 * scale_x, p.y as f32 * scale_y,
1512                                                    scaled_line_width, line_color
1513                                                );
1514                                                prev = p;
1515                                            }
1516                                            PathEl::CurveTo(c0, c1, p) => {
1517                                                // Flatten the curve
1518                                                let cubic = kurbo::CubicBez::new(prev, c0, c1, p);
1519                                                let steps = 8usize;
1520                                                let mut prev_pt = prev;
1521                                                for j in 1..=steps {
1522                                                    let t = j as f64 / steps as f64;
1523                                                    let next = cubic.eval(t);
1524                                                    draw_line(
1525                                                        prev_pt.x as f32 * scale_x, prev_pt.y as f32 * scale_y,
1526                                                        next.x as f32 * scale_x, next.y as f32 * scale_y,
1527                                                        scaled_line_width, line_color
1528                                                    );
1529                                                    prev_pt = next;
1530                                                }
1531                                                prev = p;
1532                                            }
1533                                            _ => {}
1534                                        }
1535                                    }
1536                                }
1537                                pen = *target;
1538                            }
1539                        }
1540                    }
1541                }
1542            }
1543        }
1544    }
1545    
1546    set_default_camera();
1547    unsafe {
1548        get_internal_gl().quad_gl.scissor(*clip);
1549    }
1550    
1551    Some(render_target)
1552}
1553
1554fn resize(texture: &Texture2D, height: f32, width: f32, clip: &Option<(i32, i32, i32, i32)>) -> Texture2D {
1555    let render_target = render_target_msaa(width as u32, height as u32);
1556    render_target.texture.set_filter(FilterMode::Linear);
1557    let mut cam = Camera2D::from_display_rect(Rect::new(0.0, 0.0, width, height));
1558    cam.render_target = Some(render_target.clone());
1559    set_camera(&cam);
1560    unsafe {
1561        get_internal_gl().quad_gl.scissor(None);
1562    };
1563    draw_texture_ex(
1564        texture,
1565        0.0,
1566        0.0,
1567        WHITE,
1568        DrawTextureParams {
1569            dest_size: Some(Vec2::new(width, height)),
1570            flip_y: true,
1571            ..Default::default()
1572        },
1573    );
1574    set_default_camera();
1575    unsafe {
1576        get_internal_gl().quad_gl.scissor(*clip);
1577    }
1578    render_target.texture
1579}
1580
1581/// Draws all render commands to the screen using macroquad.
1582pub async fn render<CustomElementData: Clone + Default + std::fmt::Debug>(
1583    commands: Vec<RenderCommand<CustomElementData>>,
1584    handle_custom_command: impl Fn(&RenderCommand<CustomElementData>),
1585) {
1586    let mut state = RenderState::new();
1587    for command in commands {
1588        let current_clip = state.clip_stack.last().copied();
1589        match &command.config {
1590            RenderCommandConfig::Image(image) => {
1591                let bb = command.bounding_box;
1592                let cr = &image.corner_radii;
1593                let mut tint = ply_to_macroquad_color(&image.background_color);
1594                if tint == Color::new(0.0, 0.0, 0.0, 0.0) {
1595                    tint = Color::new(1.0, 1.0, 1.0, 1.0);
1596                }
1597
1598                match &image.data {
1599                    ImageSource::Texture(tex) => {
1600                        // Direct GPU texture — draw immediately, no TextureManager
1601                        let has_corner_radii = cr.top_left > 0.0 || cr.top_right > 0.0 || cr.bottom_left > 0.0 || cr.bottom_right > 0.0;
1602                        if !has_corner_radii {
1603                            draw_texture_ex(
1604                                tex,
1605                                bb.x,
1606                                bb.y,
1607                                tint,
1608                                DrawTextureParams {
1609                                    dest_size: Some(Vec2::new(bb.width, bb.height)),
1610                                    ..Default::default()
1611                                },
1612                            );
1613                        } else {
1614                            let mut manager = TEXTURE_MANAGER.lock().unwrap();
1615                            // Use texture raw pointer as a unique key for the corner-radii variant
1616                            let key = format!(
1617                                "tex-proc:{:?}:{}:{}:{}:{}:{}:{}:{:?}",
1618                                tex.raw_miniquad_id(),
1619                                bb.width, bb.height,
1620                                cr.top_left, cr.top_right, cr.bottom_left, cr.bottom_right,
1621                                current_clip
1622                            );
1623                            let texture = manager.get_or_create(key, || {
1624                                let mut resized_image: Image = resize(tex, bb.height, bb.width, &current_clip).get_texture_data();
1625                                let rounded_rect: Image = rounded_rectangle_texture(cr, &bb, &current_clip).get_texture_data();
1626                                for i in 0..resized_image.bytes.len()/4 {
1627                                    let this_alpha = resized_image.bytes[i * 4 + 3] as f32 / 255.0;
1628                                    let mask_alpha = rounded_rect.bytes[i * 4 + 3] as f32 / 255.0;
1629                                    resized_image.bytes[i * 4 + 3] = (this_alpha * mask_alpha * 255.0) as u8;
1630                                }
1631                                Texture2D::from_image(&resized_image)
1632                            });
1633                            draw_texture_ex(
1634                                texture,
1635                                bb.x,
1636                                bb.y,
1637                                tint,
1638                                DrawTextureParams {
1639                                    dest_size: Some(Vec2::new(bb.width, bb.height)),
1640                                    ..Default::default()
1641                                },
1642                            );
1643                        }
1644                    }
1645                    #[cfg(feature = "tinyvg")]
1646                    ImageSource::TinyVg(tvg_image) => {
1647                        // Procedural TinyVG — rasterize every frame (no caching, content may change)
1648                        let has_corner_radii = cr.top_left > 0.0 || cr.top_right > 0.0 || cr.bottom_left > 0.0 || cr.bottom_right > 0.0;
1649                        if let Some(tvg_rt) = render_tinyvg_image(tvg_image, bb.width, bb.height, &current_clip) {
1650                            let final_texture = if has_corner_radii {
1651                                let mut tvg_img: Image = tvg_rt.texture.get_texture_data();
1652                                let rounded_rect: Image = rounded_rectangle_texture(cr, &bb, &current_clip).get_texture_data();
1653                                for i in 0..tvg_img.bytes.len()/4 {
1654                                    let this_alpha = tvg_img.bytes[i * 4 + 3] as f32 / 255.0;
1655                                    let mask_alpha = rounded_rect.bytes[i * 4 + 3] as f32 / 255.0;
1656                                    tvg_img.bytes[i * 4 + 3] = (this_alpha * mask_alpha * 255.0) as u8;
1657                                }
1658                                Texture2D::from_image(&tvg_img)
1659                            } else {
1660                                tvg_rt.texture.clone()
1661                            };
1662                            draw_texture_ex(
1663                                &final_texture,
1664                                bb.x,
1665                                bb.y,
1666                                tint,
1667                                DrawTextureParams {
1668                                    dest_size: Some(Vec2::new(bb.width, bb.height)),
1669                                    flip_y: true,
1670                                    ..Default::default()
1671                                },
1672                            );
1673                        }
1674                    }
1675                    ImageSource::Asset(ga) => {
1676                        // Static asset — existing behavior
1677                        let mut manager = TEXTURE_MANAGER.lock().unwrap();
1678
1679                        #[cfg(feature = "tinyvg")]
1680                        let is_tvg = ga.get_name().to_lowercase().ends_with(".tvg");
1681                        #[cfg(not(feature = "tinyvg"))]
1682                        let is_tvg = false;
1683
1684                        #[cfg(feature = "tinyvg")]
1685                        if is_tvg {
1686                            let key = format!(
1687                                "tvg:{}:{}:{}:{}:{}:{}:{}:{:?}",
1688                                ga.get_name(),
1689                                bb.width, bb.height,
1690                                cr.top_left, cr.top_right, cr.bottom_left, cr.bottom_right,
1691                                current_clip
1692                            );
1693                            let has_corner_radii = cr.top_left > 0.0 || cr.top_right > 0.0 || cr.bottom_left > 0.0 || cr.bottom_right > 0.0;
1694                            let texture = if !has_corner_radii {
1695                                // No corner radii — cache the render target to keep its GL texture alive
1696                                if let Some(cached) = manager.get(&key) {
1697                                    cached
1698                                } else {
1699                                    match ga {
1700                                        GraphicAsset::Path(path) => {
1701                                            match load_file(resolve_asset_path(path)).await {
1702                                                Ok(tvg_bytes) => {
1703                                                    if let Some(tvg_rt) = render_tinyvg_texture(&tvg_bytes, bb.width, bb.height, &current_clip) {
1704                                                        manager.cache(key.clone(), tvg_rt)
1705                                                    } else {
1706                                                        warn!("Failed to load TinyVG image: {}", path);
1707                                                        manager.cache(key.clone(), Texture2D::from_rgba8(1, 1, &[0, 0, 0, 0]))
1708                                                    }
1709                                                }
1710                                                Err(error) => {
1711                                                    warn!("Failed to load TinyVG file: {}. Error: {}", path, error);
1712                                                    manager.cache(key.clone(), Texture2D::from_rgba8(1, 1, &[0, 0, 0, 0]))
1713                                                }
1714                                            }
1715                                        }
1716                                        GraphicAsset::Bytes { file_name, data: tvg_bytes } => {
1717                                            if let Some(tvg_rt) = render_tinyvg_texture(tvg_bytes, bb.width, bb.height, &current_clip) {
1718                                                manager.cache(key.clone(), tvg_rt)
1719                                            } else {
1720                                                warn!("Failed to load TinyVG image: {}", file_name);
1721                                                manager.cache(key.clone(), Texture2D::from_rgba8(1, 1, &[0, 0, 0, 0]))
1722                                            }
1723                                        }
1724                                    }
1725                                }
1726                            } else {
1727                                let zerocr_key = format!(
1728                                    "tvg:{}:{}:{}:{}:{}:{}:{}:{:?}",
1729                                    ga.get_name(),
1730                                    bb.width, bb.height,
1731                                    0.0, 0.0, 0.0, 0.0,
1732                                    current_clip
1733                                );
1734                                let base_texture = if let Some(cached) = manager.get(&zerocr_key) {
1735                                    cached
1736                                } else {
1737                                    match ga {
1738                                        GraphicAsset::Path(path) => {
1739                                            match load_file(resolve_asset_path(path)).await {
1740                                                Ok(tvg_bytes) => {
1741                                                    if let Some(tvg_rt) = render_tinyvg_texture(&tvg_bytes, bb.width, bb.height, &current_clip) {
1742                                                        manager.cache(zerocr_key.clone(), tvg_rt)
1743                                                    } else {
1744                                                        warn!("Failed to load TinyVG image: {}", path);
1745                                                        manager.cache(zerocr_key.clone(), Texture2D::from_rgba8(1, 1, &[0, 0, 0, 0]))
1746                                                    }
1747                                                }
1748                                                Err(error) => {
1749                                                    warn!("Failed to load TinyVG file: {}. Error: {}", path, error);
1750                                                    manager.cache(zerocr_key.clone(), Texture2D::from_rgba8(1, 1, &[0, 0, 0, 0]))
1751                                                }
1752                                            }
1753                                        }
1754                                        GraphicAsset::Bytes { file_name, data: tvg_bytes } => {
1755                                            if let Some(tvg_rt) = render_tinyvg_texture(tvg_bytes, bb.width, bb.height, &current_clip) {
1756                                                manager.cache(zerocr_key.clone(), tvg_rt)
1757                                            } else {
1758                                                warn!("Failed to load TinyVG image: {}", file_name);
1759                                                manager.cache(zerocr_key.clone(), Texture2D::from_rgba8(1, 1, &[0, 0, 0, 0]))
1760                                            }
1761                                        }
1762                                    }
1763                                }.clone();
1764                                manager.get_or_create(key, || {
1765                                    let mut tvg_image: Image = base_texture.get_texture_data();
1766                                    let rounded_rect: Image = rounded_rectangle_texture(cr, &bb, &current_clip).get_texture_data();
1767                                    for i in 0..tvg_image.bytes.len()/4 {
1768                                        let this_alpha = tvg_image.bytes[i * 4 + 3] as f32 / 255.0;
1769                                        let mask_alpha = rounded_rect.bytes[i * 4 + 3] as f32 / 255.0;
1770                                        tvg_image.bytes[i * 4 + 3] = (this_alpha * mask_alpha * 255.0) as u8;
1771                                    }
1772                                    Texture2D::from_image(&tvg_image)
1773                                })
1774                            };
1775                            draw_texture_ex(
1776                                texture,
1777                                bb.x,
1778                                bb.y,
1779                                tint,
1780                                DrawTextureParams {
1781                                    dest_size: Some(Vec2::new(bb.width, bb.height)),
1782                                    flip_y: true,
1783                                    ..Default::default()
1784                                },
1785                            );
1786                            continue;
1787                        }
1788
1789                        if !is_tvg && cr.top_left == 0.0 && cr.top_right == 0.0 && cr.bottom_left == 0.0 && cr.bottom_right == 0.0 {
1790                            let texture = match ga {
1791                                GraphicAsset::Path(path) => manager.get_or_load(path).await,
1792                                GraphicAsset::Bytes { file_name, data } => {
1793                                    manager.get_or_create(file_name.to_string(), || {
1794                                        Texture2D::from_file_with_format(data, None)
1795                                    })
1796                                }
1797                            };
1798                            draw_texture_ex(
1799                                texture,
1800                                bb.x,
1801                                bb.y,
1802                                tint,
1803                                DrawTextureParams {
1804                                    dest_size: Some(Vec2::new(bb.width, bb.height)),
1805                                    ..Default::default()
1806                                },
1807                            );
1808                        } else {
1809                            let source_texture = match ga {
1810                                GraphicAsset::Path(path) => manager.get_or_load(path).await.clone(),
1811                                GraphicAsset::Bytes { file_name, data } => {
1812                                    manager.get_or_create(file_name.to_string(), || {
1813                                        Texture2D::from_file_with_format(data, None)
1814                                    }).clone()
1815                                }
1816                            };
1817                            let key = format!(
1818                                "image:{}:{}:{}:{}:{}:{}:{}:{:?}",
1819                                ga.get_name(),
1820                                bb.width, bb.height,
1821                                cr.top_left, cr.top_right, cr.bottom_left, cr.bottom_right,
1822                                current_clip
1823                            );
1824                            let texture = manager.get_or_create(key, || {
1825                                let mut resized_image: Image = resize(&source_texture, bb.height, bb.width, &current_clip).get_texture_data();
1826                                let rounded_rect: Image = rounded_rectangle_texture(cr, &bb, &current_clip).get_texture_data();
1827                                for i in 0..resized_image.bytes.len()/4 {
1828                                    let this_alpha = resized_image.bytes[i * 4 + 3] as f32 / 255.0;
1829                                    let mask_alpha = rounded_rect.bytes[i * 4 + 3] as f32 / 255.0;
1830                                    resized_image.bytes[i * 4 + 3] = (this_alpha * mask_alpha * 255.0) as u8;
1831                                }
1832                                Texture2D::from_image(&resized_image)
1833                            });
1834                            draw_texture_ex(
1835                                texture,
1836                                bb.x,
1837                                bb.y,
1838                                tint,
1839                                DrawTextureParams {
1840                                    dest_size: Some(Vec2::new(bb.width, bb.height)),
1841                                    ..Default::default()
1842                                },
1843                            );
1844                        }
1845                    }
1846                }
1847            }
1848            RenderCommandConfig::Rectangle(config) => {
1849                let bb = command.bounding_box;
1850                let color = ply_to_macroquad_color(&config.color);
1851                let cr = &config.corner_radii;
1852
1853                // Activate effect material if present (Phase 1: single effect only)
1854                let has_effect = !command.effects.is_empty();
1855                if has_effect {
1856                    let effect = &command.effects[0];
1857                    let mut mat_mgr = MATERIAL_MANAGER.lock().unwrap();
1858                    let material = mat_mgr.get_or_create(effect);
1859                    apply_shader_uniforms(material, effect, &bb);
1860                    gl_use_material(material);
1861                }
1862
1863                if let Some(ref sr) = command.shape_rotation {
1864                    use crate::math::{classify_angle, AngleType};
1865                    let flip_x = sr.flip_x;
1866                    let flip_y = sr.flip_y;
1867                    match classify_angle(sr.rotation_radians) {
1868                        AngleType::Zero => {
1869                            // Flips only — remap corner radii
1870                            let cr = flip_corner_radii(cr, flip_x, flip_y);
1871                            draw_good_rounded_rectangle(bb.x, bb.y, bb.width, bb.height, &cr, color);
1872                        }
1873                        AngleType::Right90 => {
1874                            let cr = rotate_corner_radii_90(&flip_corner_radii(cr, flip_x, flip_y));
1875                            draw_good_rounded_rectangle(bb.x, bb.y, bb.width, bb.height, &cr, color);
1876                        }
1877                        AngleType::Straight180 => {
1878                            let cr = rotate_corner_radii_180(&flip_corner_radii(cr, flip_x, flip_y));
1879                            draw_good_rounded_rectangle(bb.x, bb.y, bb.width, bb.height, &cr, color);
1880                        }
1881                        AngleType::Right270 => {
1882                            let cr = rotate_corner_radii_270(&flip_corner_radii(cr, flip_x, flip_y));
1883                            draw_good_rounded_rectangle(bb.x, bb.y, bb.width, bb.height, &cr, color);
1884                        }
1885                        AngleType::Arbitrary(theta) => {
1886                            draw_good_rotated_rounded_rectangle(
1887                                bb.x, bb.y, bb.width, bb.height,
1888                                cr, color, theta, flip_x, flip_y,
1889                            );
1890                        }
1891                    }
1892                } else if cr.top_left == 0.0 && cr.top_right == 0.0 && cr.bottom_left == 0.0 && cr.bottom_right == 0.0 {
1893                    draw_rectangle(
1894                        bb.x,
1895                        bb.y,
1896                        bb.width,
1897                        bb.height,
1898                        color
1899                    );
1900                } else {
1901                    draw_good_rounded_rectangle(bb.x, bb.y, bb.width, bb.height, cr, color);
1902                }
1903
1904                // Deactivate effect material
1905                if has_effect {
1906                    gl_use_default_material();
1907                }
1908            }
1909            #[cfg(feature = "text-styling")]
1910            RenderCommandConfig::Text(config) => {
1911                let bb = command.bounding_box;
1912                let font_size = config.font_size as f32;
1913                // Ensure font is loaded
1914                if let Some(asset) = config.font_asset {
1915                    FontManager::ensure(asset).await;
1916                }
1917                // Hold the FM lock for the duration of text rendering — no clone needed
1918                let mut fm = FONT_MANAGER.lock().unwrap();
1919                let baseline_y = bb.y + fm.metrics(config.font_size, config.font_asset).baseline_offset;
1920                let font = if let Some(asset) = config.font_asset {
1921                    fm.get(asset)
1922                } else {
1923                    fm.get_default()
1924                };
1925                let default_color = ply_to_macroquad_color(&config.color);
1926
1927                // Activate effect material if present
1928                let has_effect = !command.effects.is_empty();
1929                if has_effect {
1930                    let effect = &command.effects[0];
1931                    let mut mat_mgr = MATERIAL_MANAGER.lock().unwrap();
1932                    let material = mat_mgr.get_or_create(effect);
1933                    apply_shader_uniforms(material, effect, &bb);
1934                    gl_use_material(material);
1935                }
1936
1937                let normal_render = || {
1938                    let x_scale = compute_letter_spacing_x_scale(
1939                        bb.width,
1940                        count_visible_chars(&config.text),
1941                        config.letter_spacing,
1942                    );
1943                    draw_text_ex(
1944                        &config.text,
1945                        bb.x,
1946                        baseline_y,
1947                        TextParams {
1948                            font_size: config.font_size as u16,
1949                            font,
1950                            font_scale: 1.0,
1951                            font_scale_aspect: x_scale,
1952                            rotation: 0.0,
1953                            color: default_color
1954                        }
1955                    );
1956                };
1957                
1958                let mut in_style_def = false;
1959                let mut escaped = false;
1960                let mut failed = false;
1961                
1962                let mut text_buffer = String::new();
1963                let mut style_buffer = String::new();
1964
1965                let line = config.text.to_string();
1966                let mut segments: Vec<StyledSegment> = Vec::new();
1967
1968                for c in line.chars() {
1969                    if escaped {
1970                        if in_style_def {
1971                            style_buffer.push(c);
1972                        } else {
1973                            text_buffer.push(c);
1974                        }
1975                        escaped = false;
1976                        continue;
1977                    }
1978
1979                    match c {
1980                        '\\' => {
1981                            escaped = true;
1982                        }
1983                        '{' => {
1984                            if in_style_def {
1985                                style_buffer.push(c); 
1986                            } else {
1987                                if !text_buffer.is_empty() {
1988                                    segments.push(StyledSegment {
1989                                        text: text_buffer.clone(),
1990                                        styles: state.style_stack.clone(),
1991                                    });
1992                                    text_buffer.clear();
1993                                }
1994                                in_style_def = true;
1995                            }
1996                        }
1997                        '|' => {
1998                            if in_style_def {
1999                                state.style_stack.push(style_buffer.clone());
2000                                style_buffer.clear();
2001                                in_style_def = false;
2002                            } else {
2003                                text_buffer.push(c);
2004                            }
2005                        }
2006                        '}' => {
2007                            if in_style_def {
2008                                style_buffer.push(c);
2009                            } else {
2010                                if !text_buffer.is_empty() {
2011                                    segments.push(StyledSegment {
2012                                        text: text_buffer.clone(),
2013                                        styles: state.style_stack.clone(),
2014                                    });
2015                                    text_buffer.clear();
2016                                }
2017                                
2018                                if state.style_stack.pop().is_none() {
2019                                    failed = true;
2020                                    break;
2021                                }
2022                            }
2023                        }
2024                        _ => {
2025                            if in_style_def {
2026                                style_buffer.push(c);
2027                            } else {
2028                                text_buffer.push(c);
2029                            }
2030                        }
2031                    }
2032                }
2033                if !(failed || in_style_def) {
2034                    if !text_buffer.is_empty() {
2035                        segments.push(StyledSegment {
2036                            text: text_buffer.clone(),
2037                            styles: state.style_stack.clone(),
2038                        });
2039                    }
2040                    
2041                    let time = get_time();
2042                    
2043                    let cursor_x = std::cell::Cell::new(bb.x);
2044                    let cursor_y = baseline_y;
2045                    let mut pending_renders = Vec::new();
2046                    
2047                    let x_scale = compute_letter_spacing_x_scale(
2048                        bb.width,
2049                        count_visible_chars(&config.text),
2050                        config.letter_spacing,
2051                    );
2052                    {
2053                        let mut tracker = ANIMATION_TRACKER.lock().unwrap();
2054                        let ts_default = crate::color::Color::rgba(
2055                            config.color.r,
2056                            config.color.g,
2057                            config.color.b,
2058                            config.color.a,
2059                        );
2060                        render_styled_text(
2061                            &segments,
2062                            time,
2063                            font_size,
2064                            ts_default,
2065                            &mut *tracker,
2066                            &mut state.total_char_index,
2067                            |text, tr, style_color| {
2068                                let text_string = text.to_string();
2069                                let text_width = measure_text(&text_string, font, config.font_size as u16, 1.0).width;
2070                                
2071                                let color = Color::new(style_color.r / 255.0, style_color.g / 255.0, style_color.b / 255.0, style_color.a / 255.0);
2072                                let x = cursor_x.get();
2073                                
2074                                pending_renders.push((x, text_string, tr, color));
2075                                
2076                                cursor_x.set(x + text_width*x_scale);
2077                            },
2078                            |text, tr, style_color| {
2079                                let text_string = text.to_string();
2080                                let color = Color::new(style_color.r / 255.0, style_color.g / 255.0, style_color.b / 255.0, style_color.a / 255.0);
2081                                let x = cursor_x.get();
2082                                
2083                                draw_text_ex(
2084                                    &text_string,
2085                                    x + tr.x*x_scale,
2086                                    cursor_y + tr.y,
2087                                    TextParams {
2088                                        font_size: config.font_size as u16,
2089                                        font,
2090                                        font_scale: tr.scale_y.max(0.01),
2091                                        font_scale_aspect: if tr.scale_y > 0.01 { tr.scale_x / tr.scale_y * x_scale } else { x_scale },
2092                                        rotation: tr.rotation.to_radians(),
2093                                        color
2094                                    }
2095                                );
2096                            }
2097                        );
2098                    }
2099                    for (x, text_string, tr, color) in pending_renders {
2100                        draw_text_ex(
2101                            &text_string,
2102                            x + tr.x*x_scale,
2103                            cursor_y + tr.y,
2104                            TextParams {
2105                                font_size: config.font_size as u16,
2106                                font,
2107                                font_scale: tr.scale_y.max(0.01),
2108                                font_scale_aspect: if tr.scale_y > 0.01 { tr.scale_x / tr.scale_y * x_scale } else { x_scale },
2109                                rotation: tr.rotation.to_radians(),
2110                                color
2111                            }
2112                        );
2113                    }
2114                } else {
2115                    if in_style_def {
2116                        warn!("Style definition didn't end! Here is what we tried to render: {}", config.text);
2117                    } else if failed {
2118                        warn!("Encountered }} without opened style! Make sure to escape curly braces with \\. Here is what we tried to render: {}", config.text);
2119                    }
2120                    normal_render();
2121                }
2122
2123                // Deactivate effect material
2124                if has_effect {
2125                    gl_use_default_material();
2126                }
2127            }
2128            #[cfg(not(feature = "text-styling"))]
2129            RenderCommandConfig::Text(config) => {
2130                let bb = command.bounding_box;
2131                let color = ply_to_macroquad_color(&config.color);
2132                // Ensure font is loaded
2133                if let Some(asset) = config.font_asset {
2134                    FontManager::ensure(asset).await;
2135                }
2136                // Hold the FM lock for the duration of text rendering — no clone needed
2137                let mut fm = FONT_MANAGER.lock().unwrap();
2138                let baseline_y = bb.y + fm.metrics(config.font_size, config.font_asset).baseline_offset;
2139                let font = if let Some(asset) = config.font_asset {
2140                    fm.get(asset)
2141                } else {
2142                    fm.get_default()
2143                };
2144
2145                // Activate effect material if present
2146                let has_effect = !command.effects.is_empty();
2147                if has_effect {
2148                    let effect = &command.effects[0];
2149                    let mut mat_mgr = MATERIAL_MANAGER.lock().unwrap();
2150                    let material = mat_mgr.get_or_create(effect);
2151                    apply_shader_uniforms(material, effect, &bb);
2152                    gl_use_material(material);
2153                }
2154
2155                let x_scale = compute_letter_spacing_x_scale(
2156                    bb.width,
2157                    config.text.chars().count(),
2158                    config.letter_spacing,
2159                );
2160                draw_text_ex(
2161                    &config.text,
2162                    bb.x,
2163                    baseline_y,
2164                    TextParams {
2165                        font_size: config.font_size as u16,
2166                        font,
2167                        font_scale: 1.0,
2168                        font_scale_aspect: x_scale,
2169                        rotation: 0.0,
2170                        color
2171                    }
2172                );
2173
2174                // Deactivate effect material
2175                if has_effect {
2176                    gl_use_default_material();
2177                }
2178            }
2179            RenderCommandConfig::Border(config) => {
2180                let bb = command.bounding_box;
2181                let bw = &config.width;
2182                let cr = &config.corner_radii;
2183                let color = ply_to_macroquad_color(&config.color);
2184                let s = match config.position {
2185                    BorderPosition::Outside => 1.,
2186                    BorderPosition::Middle => 0.5,
2187                    BorderPosition::Inside => 0.0,
2188                };
2189
2190                let get_sides = |corner: f32| {
2191                    (std::f32::consts::PI * corner / (2.0 * PIXELS_PER_POINT)).max(5.0) as usize
2192                };
2193                let v = |x: f32, y: f32| Vertex::new(x, y, 0., 0., 0., color);
2194
2195                let top = bw.top as f32;
2196                let left = bw.left as f32;
2197                let bottom = bw.bottom as f32;
2198                let right = bw.right as f32;
2199                let tl_r = cr.top_left;
2200                let tr_r = cr.top_right;
2201                let bl_r = cr.bottom_left;
2202                let br_r = cr.bottom_right;
2203
2204                let ox1 = bb.x - left * s;
2205                let ox2 = bb.x + bb.width + right * s;
2206                let oy1 = bb.y - top * s;
2207                let oy2 = bb.y + bb.height + bottom * s;
2208                let ix1 = bb.x + left * (1.0 - s);
2209                let ix2 = bb.x + bb.width - right * (1.0 - s);
2210                let iy1 = bb.y + top * (1.0 - s);
2211                let iy2 = bb.y + bb.height - bottom * (1.0 - s);
2212
2213                let o_tl_rx = tl_r + left * s;
2214                let o_tl_ry = tl_r + top * s;
2215                let o_tr_rx = tr_r + right * s;
2216                let o_tr_ry = tr_r + top * s;
2217                let o_bl_rx = bl_r + left * s;
2218                let o_bl_ry = bl_r + bottom * s;
2219                let o_br_rx = br_r + right * s;
2220                let o_br_ry = br_r + bottom * s;
2221                let i_tl_rx = (tl_r - left * (1.0 - s)).max(0.0);
2222                let i_tl_ry = (tl_r - top * (1.0 - s)).max(0.0);
2223                let i_tr_rx = (tr_r - right * (1.0 - s)).max(0.0);
2224                let i_tr_ry = (tr_r - top * (1.0 - s)).max(0.0);
2225                let i_bl_rx = (bl_r - left * (1.0 - s)).max(0.0);
2226                let i_bl_ry = (bl_r - bottom * (1.0 - s)).max(0.0);
2227                let i_br_rx = (br_r - right * (1.0 - s)).max(0.0);
2228                let i_br_ry = (br_r - bottom * (1.0 - s)).max(0.0);
2229
2230                let tl_sides = get_sides(o_tl_rx.max(o_tl_ry).max(i_tl_rx).max(i_tl_ry));
2231                let tr_sides = get_sides(o_tr_rx.max(o_tr_ry).max(i_tr_rx).max(i_tr_ry));
2232                let bl_sides = get_sides(o_bl_rx.max(o_bl_ry).max(i_bl_rx).max(i_bl_ry));
2233                let br_sides = get_sides(o_br_rx.max(o_br_ry).max(i_br_rx).max(i_br_ry));
2234                let side_count = tl_sides + tr_sides + bl_sides + br_sides;
2235
2236                let mut vertices = Vec::<Vertex>::with_capacity(16 + side_count * 4);
2237                let mut indices = Vec::<u16>::with_capacity(24 + side_count * 6);
2238
2239                // 4 quads
2240                vertices.extend([
2241                    // Top edge
2242                    v(ox1 + o_tl_rx, oy1),
2243                    v(ox2 - o_tr_rx, oy1),
2244                    v(ix1 + i_tl_rx, iy1),
2245                    v(ix2 - i_tr_rx, iy1),
2246                    // Bottom edge
2247                    v(ox1 + o_bl_rx, oy2),
2248                    v(ox2 - o_br_rx, oy2),
2249                    v(ix1 + i_bl_rx, iy2),
2250                    v(ix2 - i_br_rx, iy2),
2251                    // Left edge
2252                    v(ox1, oy1 + o_tl_ry),
2253                    v(ox1, oy2 - o_bl_ry),
2254                    v(ix1, iy1 + i_tl_ry),
2255                    v(ix1, iy2 - i_bl_ry),
2256                    // Right edge
2257                    v(ox2, oy1 + o_tr_ry),
2258                    v(ox2, oy2 - o_br_ry),
2259                    v(ix2, iy1 + i_tr_ry),
2260                    v(ix2, iy2 - i_br_ry),
2261                ]);
2262                for l in [0, 4, 8, 12] {
2263                    indices.extend([
2264                        l, l + 1, l + 2,
2265                        l + 1, l + 3, l + 2
2266                    ]);
2267                }
2268
2269                let corners = [
2270                    (
2271                        tl_sides,
2272                        PI,
2273                        ox1 + o_tl_rx,
2274                        oy1 + o_tl_ry,
2275                        ix1 + i_tl_rx,
2276                        iy1 + i_tl_ry,
2277                        o_tl_rx,
2278                        o_tl_ry,
2279                        i_tl_rx,
2280                        i_tl_ry,
2281                    ),
2282                    (
2283                        tr_sides,
2284                        PI * 1.5,
2285                        ox2 - o_tr_rx,
2286                        oy1 + o_tr_ry,
2287                        ix2 - i_tr_rx,
2288                        iy1 + i_tr_ry,
2289                        o_tr_rx,
2290                        o_tr_ry,
2291                        i_tr_rx,
2292                        i_tr_ry,
2293                    ),
2294                    (
2295                        bl_sides,
2296                        PI * 0.5,
2297                        ox1 + o_bl_rx,
2298                        oy2 - o_bl_ry,
2299                        ix1 + i_bl_rx,
2300                        iy2 - i_bl_ry,
2301                        o_bl_rx,
2302                        o_bl_ry,
2303                        i_bl_rx,
2304                        i_bl_ry,
2305                    ),
2306                    (
2307                        br_sides,
2308                        0.,
2309                        ox2 - o_br_rx,
2310                        oy2 - o_br_ry,
2311                        ix2 - i_br_rx,
2312                        iy2 - i_br_ry,
2313                        o_br_rx,
2314                        o_br_ry,
2315                        i_br_rx,
2316                        i_br_ry,
2317                    ),
2318                ];
2319
2320                for (sides, start, ocx, ocy, icx, icy, o_rx, o_ry, i_rx, i_ry) in corners {
2321                    let step = (PI / 2.) / (sides as f32);
2322
2323                    for i in 0..sides {
2324                        let i = i as f32;
2325                        let a1 = start + i * step;
2326                        let a2 = a1 + step;
2327                        let l = vertices.len() as u16;
2328
2329                        // quad
2330                        vertices.extend([
2331                            v(ocx + a1.cos() * o_rx, ocy + a1.sin() * o_ry),
2332                            v(ocx + a2.cos() * o_rx, ocy + a2.sin() * o_ry),
2333                            v(icx + a1.cos() * i_rx, icy + a1.sin() * i_ry),
2334                            v(icx + a2.cos() * i_rx, icy + a2.sin() * i_ry),
2335                        ]);
2336                        indices.extend([
2337                            l, l + 1, l + 2,
2338                            l + 1, l + 3, l + 2
2339                        ]);
2340                    }
2341                }
2342
2343                draw_mesh(&Mesh { vertices, indices, texture: None });
2344            }
2345            RenderCommandConfig::ScissorStart() => {
2346                let bb = command.bounding_box;
2347                // Layout coordinates are in logical pixels, but macroquad's
2348                // quad_gl.scissor() passes values to glScissor which operates
2349                // in physical (framebuffer) pixels.  Scale by DPI so the
2350                // scissor rectangle matches on high-DPI displays (e.g. WASM).
2351                let dpi = miniquad::window::dpi_scale();
2352                let next_clip = (
2353                    (bb.x * dpi) as i32,
2354                    (bb.y * dpi) as i32,
2355                    (bb.width * dpi) as i32,
2356                    (bb.height * dpi) as i32,
2357                );
2358
2359                let effective_clip = if let Some(parent_clip) = state.clip_stack.last().copied() {
2360                    intersect_scissor(parent_clip, next_clip)
2361                } else {
2362                    next_clip
2363                };
2364
2365                state.clip_stack.push(effective_clip);
2366                unsafe {
2367                    get_internal_gl().quad_gl.scissor(state.clip_stack.last().copied());
2368                }
2369            }
2370            RenderCommandConfig::ScissorEnd() => {
2371                state.clip_stack.pop();
2372                unsafe {
2373                    get_internal_gl().quad_gl.scissor(state.clip_stack.last().copied());
2374                }
2375            }
2376            RenderCommandConfig::Custom(_) => {
2377                handle_custom_command(&command);
2378            }
2379            RenderCommandConfig::GroupBegin { ref shader, ref visual_rotation } => {
2380                let bb = command.bounding_box;
2381                let rt = render_target_msaa(bb.width as u32, bb.height as u32);
2382                rt.texture.set_filter(FilterMode::Linear);
2383                let cam = Camera2D {
2384                    render_target: Some(rt.clone()),
2385                    ..Camera2D::from_display_rect(Rect::new(
2386                        bb.x, bb.y, bb.width, bb.height,
2387                    ))
2388                };
2389                set_camera(&cam);
2390                clear_background(Color::new(0.0, 0.0, 0.0, 0.0));
2391                state.rt_stack.push((rt, shader.clone(), *visual_rotation, bb));
2392            }
2393            RenderCommandConfig::GroupEnd => {
2394                if let Some((rt, shader_config, visual_rotation, bb)) = state.rt_stack.pop() {
2395                    // Restore previous camera
2396                    if let Some((prev_rt, _, _, prev_bb)) = state.rt_stack.last() {
2397                        let cam = Camera2D {
2398                            render_target: Some(prev_rt.clone()),
2399                            ..Camera2D::from_display_rect(Rect::new(
2400                                prev_bb.x, prev_bb.y, prev_bb.width, prev_bb.height,
2401                            ))
2402                        };
2403                        set_camera(&cam);
2404                    } else {
2405                        set_default_camera();
2406                    }
2407
2408                    // Apply the shader material if present
2409                    if let Some(ref config) = shader_config {
2410                        let mut mat_mgr = MATERIAL_MANAGER.lock().unwrap();
2411                        let material = mat_mgr.get_or_create(config);
2412                        apply_shader_uniforms(material, config, &bb);
2413                        gl_use_material(material);
2414                    }
2415
2416                    // Compute draw params — apply visual rotation if present
2417                    let (rotation, flip_x, flip_y, pivot) = match &visual_rotation {
2418                        Some(rot) => {
2419                            let pivot_screen = Vec2::new(
2420                                bb.x + rot.pivot_x * bb.width,
2421                                bb.y + rot.pivot_y * bb.height,
2422                            );
2423                            // flip_y is inverted because render targets are flipped in OpenGL
2424                            (rot.rotation_radians, rot.flip_x, !rot.flip_y, Some(pivot_screen))
2425                        }
2426                        None => (0.0, false, true, None),
2427                    };
2428
2429                    draw_texture_ex(
2430                        &rt.texture,
2431                        bb.x,
2432                        bb.y,
2433                        WHITE,
2434                        DrawTextureParams {
2435                            dest_size: Some(Vec2::new(bb.width, bb.height)),
2436                            rotation,
2437                            flip_x,
2438                            flip_y,
2439                            pivot,
2440                            ..Default::default()
2441                        },
2442                    );
2443
2444                    if shader_config.is_some() {
2445                        gl_use_default_material();
2446                    }
2447                }
2448            }
2449            RenderCommandConfig::None() => {}
2450        }
2451    }
2452    TEXTURE_MANAGER.lock().unwrap().clean();
2453    MATERIAL_MANAGER.lock().unwrap().clean();
2454    FONT_MANAGER.lock().unwrap().clean();
2455}
2456
2457pub fn create_measure_text_function(
2458) -> impl Fn(&str, &crate::TextConfig) -> crate::Dimensions + 'static {
2459    move |text: &str, config: &crate::TextConfig| {
2460        #[cfg(feature = "text-styling")]
2461        let cleaned_text = {
2462            // Remove macroquad_text_styling tags, handling escapes
2463            let mut result = String::new();
2464            let mut in_style_def = false;
2465            let mut escaped = false;
2466            for c in text.chars() {
2467                if escaped {
2468                    result.push(c);
2469                    escaped = false;
2470                    continue;
2471                }
2472                match c {
2473                    '\\' => {
2474                        escaped = true;
2475                    }
2476                    '{' => {
2477                        in_style_def = true;
2478                    }
2479                    '|' => {
2480                        if in_style_def {
2481                            in_style_def = false;
2482                        } else {
2483                            result.push(c);
2484                        }
2485                    }
2486                    '}' => {
2487                        // Nothing
2488                    }
2489                    _ => {
2490                        if !in_style_def {
2491                            result.push(c);
2492                        }
2493                    }
2494                }
2495            }
2496            if in_style_def {
2497                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);
2498            }
2499            result
2500        };
2501        #[cfg(not(feature = "text-styling"))]
2502        let cleaned_text = text.to_string();
2503        let mut fm = FONT_MANAGER.lock().unwrap();
2504        // Resolve font: use asset font if available, otherwise default
2505        let font = if let Some(asset) = config.font_asset {
2506            fm.get(asset)
2507        } else {
2508            fm.get_default()
2509        };
2510        let measured = macroquad::text::measure_text(
2511            &cleaned_text,
2512            font,
2513            config.font_size,
2514            1.0,
2515        );
2516        let metrics = fm.metrics(config.font_size as u16, config.font_asset);
2517        let added_space = (cleaned_text.chars().count().max(1) - 1) as f32 * config.letter_spacing as f32;
2518        crate::Dimensions::new(measured.width + added_space, metrics.height)
2519    }
2520}
2521
2522/// Count visible characters in text, skipping style tag markup.
2523/// This handles `{style_name|` openers, `}` closers, and `\` escapes.
2524#[cfg(feature = "text-styling")]
2525fn count_visible_chars(text: &str) -> usize {
2526    let mut count = 0;
2527    let mut in_style_def = false;
2528    let mut escaped = false;
2529    for c in text.chars() {
2530        if escaped { count += 1; escaped = false; continue; }
2531        match c {
2532            '\\' => { escaped = true; }
2533            '{' => { in_style_def = true; }
2534            '|' => { if in_style_def { in_style_def = false; } else { count += 1; } }
2535            '}' => { }
2536            _ => { if !in_style_def { count += 1; } }
2537        }
2538    }
2539    count
2540}
2541
2542/// Compute the horizontal scale factor needed to visually apply letter-spacing.
2543///
2544/// The bounding-box width already includes the total letter-spacing contribution
2545/// (`(visible_chars - 1) * letter_spacing`). By dividing out that contribution we
2546/// recover the raw text width, and the ratio `bb_width / raw_width` gives the
2547/// scale factor that macroquad should use to stretch each glyph.
2548fn compute_letter_spacing_x_scale(bb_width: f32, visible_char_count: usize, letter_spacing: u16) -> f32 {
2549    if letter_spacing == 0 || visible_char_count <= 1 {
2550        return 1.0;
2551    }
2552    let total_spacing = (visible_char_count as f32 - 1.0) * letter_spacing as f32;
2553    let raw_width = bb_width - total_spacing;
2554    if raw_width > 0.0 {
2555        bb_width / raw_width
2556    } else {
2557        1.0
2558    }
2559}