sugarloaf 0.3.9

Sugarloaf is Rio rendering engine, designed to be multiplatform. It is based on WebGPU, Rust library for Desktops and WebAssembly for Web (JavaScript). This project is created and maintained for Rio terminal purposes but feel free to use it.
Documentation
// Copyright (c) 2023-present, Raphael Amorim.
//
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.
//
// Compositor with vertex capture for text run caching

use crate::layout::{SpanStyleDecoration, UnderlineShape};
use crate::renderer::batch::{BatchManager, DrawCmd, QuadInstance, RunUnderline};
pub use crate::renderer::batch::{Rect, Vertex};
use crate::renderer::image_cache::glyph::GlyphCacheSession;
use crate::renderer::text::*;

pub struct Compositor {
    pub batches: BatchManager,
}

impl Compositor {
    pub fn new() -> Self {
        Self {
            batches: BatchManager::new(),
        }
    }

    /// Creates an underline decoration based on the style and rect
    pub fn create_underline_from_decoration(
        &self,
        style: &TextRunStyle,
    ) -> Option<RunUnderline> {
        match style.decoration {
            Some(SpanStyleDecoration::Underline(info)) => {
                // Use font metrics for thickness when available, otherwise fall back to shape-based defaults
                let underline_thickness = if style.underline_thickness > 0.0 {
                    style.underline_thickness
                } else {
                    // Fallback thickness based on font size
                    // Use approximately 8% of font size for all underline types
                    let base_thickness = style.font_size * 0.08;
                    base_thickness.max(1.0)
                };

                // Use real font metrics for proper underline positioning
                let underline_offset =
                    self.calculate_underline_offset(style, underline_thickness);

                Some(RunUnderline {
                    enabled: true,
                    offset: underline_offset,
                    size: underline_thickness,
                    color: style.decoration_color.unwrap_or(style.color),
                    is_doubled: info.is_doubled,
                    shape: info.shape,
                })
            }
            Some(SpanStyleDecoration::Strikethrough) => {
                // Strikethrough should be positioned through the middle of the text
                // Use font-provided strikeout_offset if available, otherwise x_height/2
                let strikethrough_offset = if style.strikeout_offset != 0.0 {
                    // Font provides strikeout_offset as distance from baseline
                    -style.strikeout_offset
                } else if style.x_height > 0.0 {
                    // x_height is the height of lowercase letters, strike through middle
                    -(style.x_height / 2.0)
                } else {
                    // Fallback: 25% of ascent above baseline
                    -(style.ascent * 0.25)
                };

                // Use font metrics for thickness when available
                let strikethrough_thickness = if style.underline_thickness > 0.0 {
                    style.underline_thickness
                } else {
                    1.5
                };

                Some(RunUnderline {
                    enabled: true,
                    offset: strikethrough_offset,
                    size: strikethrough_thickness,
                    color: style.decoration_color.unwrap_or(style.color),
                    is_doubled: false,
                    shape: UnderlineShape::Regular,
                })
            }
            _ => None,
        }
    }

    /// Calculate underline offset using font metrics, with fallback
    fn calculate_underline_offset(
        &self,
        style: &TextRunStyle,
        underline_thickness: f32,
    ) -> f32 {
        // Use font's built-in underline position when available
        if style.underline_offset != 0.0 {
            // Font provides underline_offset as distance from baseline to underline top
            // Negative values mean below baseline, which is what we want
            // But Rio's renderer expects positive offset for below baseline
            -style.underline_offset
        } else {
            // Fallback: place underline 1 thickness below baseline
            underline_thickness
        }
    }

    #[inline]
    pub fn finish(
        &mut self,
        instances: &mut Vec<QuadInstance>,
        vertices: &mut Vec<Vertex>,
        cmds: &mut Vec<DrawCmd>,
    ) {
        self.batches.build_display_list(instances, vertices, cmds);
        self.batches.reset();
    }

    /// Standard draw_run method (for compatibility)
    #[inline]
    pub fn draw_run(
        &mut self,
        session: &mut GlyphCacheSession,
        rect: impl Into<Rect>,
        depth: f32,
        style: &TextRunStyle,
        glyphs: &[Glyph],
        order: u8,
    ) {
        self.draw_run_internal(session, rect, depth, style, glyphs, order);
    }

    /// Internal rendering implementation
    #[inline]
    fn draw_run_internal(
        &mut self,
        session: &mut GlyphCacheSession,
        rect: impl Into<Rect>,
        depth: f32,
        style: &TextRunStyle,
        glyphs: &[Glyph],
        order: u8,
    ) {
        let rect = rect.into();
        let underline = self.create_underline_from_decoration(style);

        let subpx_bias = (0.125, 0.);
        let color = style.color;

        if let Some(builtin_character) = style.drawable_char {
            if let Some(bg_color) = style.background_color {
                let bg_rect =
                    Rect::new(rect.x, style.topline, rect.width, style.line_height);
                self.batches.rect(&bg_rect, depth, &bg_color, 0);
            }

            if let Some(cursor) = style.cursor {
                // Calculate cursor dimensions based on font metrics, not line height
                let font_height = style.ascent + style.descent;
                let cursor_top = style.baseline - style.ascent;

                match cursor.kind {
                    crate::CursorKind::Block => {
                        let cursor_rect =
                            Rect::new(rect.x, cursor_top, rect.width, font_height);
                        self.batches.rect(
                            &cursor_rect,
                            depth,
                            &cursor.color,
                            cursor.order,
                        );
                    }
                    crate::CursorKind::HollowBlock => {
                        let outer_rect =
                            Rect::new(rect.x, cursor_top, rect.width, font_height);
                        self.batches.rect(
                            &outer_rect,
                            depth,
                            &cursor.color,
                            cursor.order,
                        );

                        if let Some(bg_color) = style.background_color {
                            let inner_rect = Rect::new(
                                rect.x + 1.0,
                                cursor_top + 1.0,
                                rect.width - 2.0,
                                font_height - 2.0,
                            );
                            self.batches.rect(
                                &inner_rect,
                                depth,
                                &bg_color,
                                cursor.order,
                            );
                        }
                    }
                    crate::CursorKind::Caret => {
                        let caret_rect = Rect::new(rect.x, cursor_top, 2.0, font_height);
                        self.batches.rect(
                            &caret_rect,
                            depth,
                            &cursor.color,
                            cursor.order,
                        );
                    }
                    crate::CursorKind::Underline => {
                        let caret_rect =
                            Rect::new(rect.x, style.baseline + 1.0, rect.width, 2.0);
                        self.batches.rect(
                            &caret_rect,
                            depth,
                            &cursor.color,
                            cursor.order,
                        );
                    }
                }
            }

            if let Some(underline) = underline {
                self.batches.draw_underline(
                    &underline,
                    rect.x,
                    rect.width,
                    style.baseline,
                    depth,
                    style.line_height,
                );
            }

            self.batches.draw_drawable_character(
                rect.x,
                style.topline,
                rect.width,
                builtin_character,
                color,
                depth,
                style.line_height,
                0,
            );
        } else {
            // Handle regular glyphs
            for glyph in glyphs {
                let entry = session.get(glyph.id);
                if let Some(entry) = entry {
                    if let Some(img) = session.get_image(entry.image) {
                        let gx = (glyph.x + subpx_bias.0).floor() + entry.left as f32;
                        let gy = (glyph.y + subpx_bias.1).floor() - entry.top as f32;

                        // Scale PUA glyphs to fit constraint width
                        let glyph_rect =
                            if let Some((cell_w, cells)) = style.scale_constraint {
                                let target_w = cell_w * cells as f32;
                                let target_h = style.line_height;
                                let orig_w = entry.width as f32;
                                let orig_h = entry.height as f32;

                                // Fit proportionally within the target area
                                let scale =
                                    (target_w / orig_w).min(target_h / orig_h).min(1.0);
                                let sw = orig_w * scale;
                                let sh = orig_h * scale;

                                // Center horizontally within constraint width
                                let cx = glyph.x + (target_w - sw) / 2.0;
                                // Align vertically from the baseline
                                let cy = gy + (orig_h - sh) / 2.0;

                                Rect::new(cx, cy, sw, sh)
                            } else {
                                Rect::new(gx, gy, entry.width as f32, entry.height as f32)
                            };
                        let coords = [img.min.0, img.min.1, img.max.0, img.max.1];

                        if entry.is_bitmap {
                            let bitmap_color = [1.0, 1.0, 1.0, 1.0];
                            // Get atlas index for this image (0-based), add 1 for layer (0 = no texture)
                            let atlas_layer = session
                                .get_atlas_index(entry.image)
                                .map(|idx| (idx + 1) as i32)
                                .unwrap_or(1);
                            self.batches.add_image_rect(
                                &glyph_rect,
                                depth,
                                &bitmap_color,
                                &coords,
                                atlas_layer,
                            );
                        } else {
                            self.batches.add_mask_rect_with_order(
                                &glyph_rect,
                                depth,
                                &color,
                                &coords,
                                true,
                                order,
                            );
                        }
                    }
                }
            }

            if let Some(bg_color) = style.background_color {
                let bg_rect =
                    Rect::new(rect.x, style.topline, rect.width, style.line_height);
                self.batches.rect(&bg_rect, depth, &bg_color, 0);
            }

            if let Some(cursor) = style.cursor {
                // Calculate cursor dimensions based on font metrics, not line height
                let font_height = style.ascent + style.descent;
                let cursor_top = style.baseline - style.ascent;

                match cursor.kind {
                    crate::CursorKind::Block => {
                        let cursor_rect =
                            Rect::new(rect.x, cursor_top, rect.width, font_height);
                        self.batches.rect(
                            &cursor_rect,
                            depth,
                            &cursor.color,
                            cursor.order,
                        );
                    }
                    crate::CursorKind::HollowBlock => {
                        let outer_rect =
                            Rect::new(rect.x, cursor_top, rect.width, font_height);
                        self.batches.rect(
                            &outer_rect,
                            depth,
                            &cursor.color,
                            cursor.order,
                        );

                        if let Some(bg_color) = style.background_color {
                            let inner_rect = Rect::new(
                                rect.x + 1.0,
                                cursor_top + 1.0,
                                rect.width - 2.0,
                                font_height - 2.0,
                            );
                            self.batches.rect(
                                &inner_rect,
                                depth,
                                &bg_color,
                                cursor.order,
                            );
                        }
                    }
                    crate::CursorKind::Caret => {
                        let caret_rect = Rect::new(rect.x, cursor_top, 2.0, font_height);
                        self.batches.rect(
                            &caret_rect,
                            depth,
                            &cursor.color,
                            cursor.order,
                        );
                    }
                    crate::CursorKind::Underline => {
                        let caret_rect =
                            Rect::new(rect.x, style.baseline + 1.0, rect.width, 2.0);
                        self.batches.rect(
                            &caret_rect,
                            depth,
                            &cursor.color,
                            cursor.order,
                        );
                    }
                }
            }

            if let Some(underline) = underline {
                self.batches.draw_underline(
                    &underline,
                    rect.x,
                    rect.width,
                    style.baseline,
                    depth,
                    style.line_height,
                );
            }
        }
    }
}

impl Default for Compositor {
    fn default() -> Self {
        Self::new()
    }
}