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