Skip to main content

ply_engine/
renderer.rs

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