summaryrefslogblamecommitdiffstats
path: root/tiny_skia/src/text.rs
blob: 0fc3d1f71b26fd2f68222a00d76954bcb947ce61 (plain) (tree)
1
2
3
4
5
6
7
8
9
                           
                               


                                                                
                                                
                                  
                                       
                                     




                                       
 
                
                     
                            
                          




                          
                                           
                                              


         

                          
                                                            



                                        

                                                

     

                          


                                    

                                              
                                       
       





                                                        

                                                                                
             
                              





                                                             

                      
                           
          

     




                              

                                              
                                       












                                                                                
                                                      


                                        

                      
                           


          
                       



                          
                     
                            
                   

                                                    
                         
                                              
                                            
                                       
       
                                                 
 


                                                                                
                              
                                  

                    
                              
                        
                    

          
                                                                         
 














                                             

                      
                           
          

     




                                     

                                              
                                       








                                                                                






                                                             


                                        

                      
                           


          
                                  
                                    
                                
     

 







                                                

                                          
                                   
   
                                         
















                                                                       

                                                                      


                                                                    
                                                                 









                                                              





                                                      


                                                      

                                                                              
                           



                                                           







                                                     


                                                   
                                                

 










                                                               
                                     
                                            
 







                                         

                                                  





                                                                         
                                                                          







                                                           









                                                            
                                                       







                                                              










                                                            
                                                            







                                                              









                                                            
                                                            

         
                                               






                                                                 


                                                               




                                                                   


                                                               





                                 
use crate::core::alignment;
use crate::core::text::Shaping;
use crate::core::{
    Color, Font, Pixels, Point, Rectangle, Size, Transformation,
};
use crate::graphics::text::cache::{self, Cache};
use crate::graphics::text::editor;
use crate::graphics::text::font_system;
use crate::graphics::text::paragraph;

use rustc_hash::{FxHashMap, FxHashSet};
use std::borrow::Cow;
use std::cell::RefCell;
use std::collections::hash_map;

#[derive(Debug)]
pub struct Pipeline {
    glyph_cache: GlyphCache,
    cache: RefCell<Cache>,
}

impl Pipeline {
    pub fn new() -> Self {
        Pipeline {
            glyph_cache: GlyphCache::new(),
            cache: RefCell::new(Cache::new()),
        }
    }

    // TODO: Shared engine
    #[allow(dead_code)]
    pub fn load_font(&mut self, bytes: Cow<'static, [u8]>) {
        font_system()
            .write()
            .expect("Write font system")
            .load_font(bytes);

        self.cache = RefCell::new(Cache::new());
    }

    pub fn draw_paragraph(
        &mut self,
        paragraph: &paragraph::Weak,
        position: Point,
        color: Color,
        pixels: &mut tiny_skia::PixmapMut<'_>,
        clip_mask: Option<&tiny_skia::Mask>,
        transformation: Transformation,
    ) {
        use crate::core::text::Paragraph as _;

        let Some(paragraph) = paragraph.upgrade() else {
            return;
        };

        let mut font_system = font_system().write().expect("Write font system");

        draw(
            font_system.raw(),
            &mut self.glyph_cache,
            paragraph.buffer(),
            Rectangle::new(position, paragraph.min_bounds()),
            color,
            paragraph.horizontal_alignment(),
            paragraph.vertical_alignment(),
            pixels,
            clip_mask,
            transformation,
        );
    }

    pub fn draw_editor(
        &mut self,
        editor: &editor::Weak,
        position: Point,
        color: Color,
        pixels: &mut tiny_skia::PixmapMut<'_>,
        clip_mask: Option<&tiny_skia::Mask>,
        transformation: Transformation,
    ) {
        use crate::core::text::Editor as _;

        let Some(editor) = editor.upgrade() else {
            return;
        };

        let mut font_system = font_system().write().expect("Write font system");

        draw(
            font_system.raw(),
            &mut self.glyph_cache,
            editor.buffer(),
            Rectangle::new(position, editor.bounds()),
            color,
            alignment::Horizontal::Left,
            alignment::Vertical::Top,
            pixels,
            clip_mask,
            transformation,
        );
    }

    pub fn draw_cached(
        &mut self,
        content: &str,
        bounds: Rectangle,
        color: Color,
        size: Pixels,
        line_height: Pixels,
        font: Font,
        horizontal_alignment: alignment::Horizontal,
        vertical_alignment: alignment::Vertical,
        shaping: Shaping,
        pixels: &mut tiny_skia::PixmapMut<'_>,
        clip_mask: Option<&tiny_skia::Mask>,
        transformation: Transformation,
    ) {
        let line_height = f32::from(line_height);

        let mut font_system = font_system().write().expect("Write font system");
        let font_system = font_system.raw();

        let key = cache::Key {
            bounds: bounds.size(),
            content,
            font,
            size: size.into(),
            line_height,
            shaping,
        };

        let (_, entry) = self.cache.get_mut().allocate(font_system, key);

        let width = entry.min_bounds.width;
        let height = entry.min_bounds.height;

        draw(
            font_system,
            &mut self.glyph_cache,
            &entry.buffer,
            Rectangle {
                width,
                height,
                ..bounds
            },
            color,
            horizontal_alignment,
            vertical_alignment,
            pixels,
            clip_mask,
            transformation,
        );
    }

    pub fn draw_raw(
        &mut self,
        buffer: &cosmic_text::Buffer,
        position: Point,
        color: Color,
        pixels: &mut tiny_skia::PixmapMut<'_>,
        clip_mask: Option<&tiny_skia::Mask>,
        transformation: Transformation,
    ) {
        let mut font_system = font_system().write().expect("Write font system");

        let (width, height) = buffer.size();

        draw(
            font_system.raw(),
            &mut self.glyph_cache,
            buffer,
            Rectangle::new(
                position,
                Size::new(
                    width.unwrap_or(pixels.width() as f32),
                    height.unwrap_or(pixels.height() as f32),
                ),
            ),
            color,
            alignment::Horizontal::Left,
            alignment::Vertical::Top,
            pixels,
            clip_mask,
            transformation,
        );
    }

    pub fn trim_cache(&mut self) {
        self.cache.get_mut().trim();
        self.glyph_cache.trim();
    }
}

fn draw(
    font_system: &mut cosmic_text::FontSystem,
    glyph_cache: &mut GlyphCache,
    buffer: &cosmic_text::Buffer,
    bounds: Rectangle,
    color: Color,
    horizontal_alignment: alignment::Horizontal,
    vertical_alignment: alignment::Vertical,
    pixels: &mut tiny_skia::PixmapMut<'_>,
    clip_mask: Option<&tiny_skia::Mask>,
    transformation: Transformation,
) {
    let bounds = bounds * transformation;

    let x = match horizontal_alignment {
        alignment::Horizontal::Left => bounds.x,
        alignment::Horizontal::Center => bounds.x - bounds.width / 2.0,
        alignment::Horizontal::Right => bounds.x - bounds.width,
    };

    let y = match vertical_alignment {
        alignment::Vertical::Top => bounds.y,
        alignment::Vertical::Center => bounds.y - bounds.height / 2.0,
        alignment::Vertical::Bottom => bounds.y - bounds.height,
    };

    let mut swash = cosmic_text::SwashCache::new();

    for run in buffer.layout_runs() {
        for glyph in run.glyphs {
            let physical_glyph =
                glyph.physical((x, y), transformation.scale_factor());

            if let Some((buffer, placement)) = glyph_cache.allocate(
                physical_glyph.cache_key,
                glyph.color_opt.map(from_color).unwrap_or(color),
                font_system,
                &mut swash,
            ) {
                let pixmap = tiny_skia::PixmapRef::from_bytes(
                    buffer,
                    placement.width,
                    placement.height,
                )
                .expect("Create glyph pixel map");

                let opacity = color.a
                    * glyph
                        .color_opt
                        .map(|c| c.a() as f32 / 255.0)
                        .unwrap_or(1.0);

                pixels.draw_pixmap(
                    physical_glyph.x + placement.left,
                    physical_glyph.y - placement.top
                        + (run.line_y * transformation.scale_factor()).round()
                            as i32,
                    pixmap,
                    &tiny_skia::PixmapPaint {
                        opacity,
                        ..tiny_skia::PixmapPaint::default()
                    },
                    tiny_skia::Transform::identity(),
                    clip_mask,
                );
            }
        }
    }
}

fn from_color(color: cosmic_text::Color) -> Color {
    let [r, g, b, a] = color.as_rgba();

    Color::from_rgba8(r, g, b, a as f32 / 255.0)
}

#[derive(Debug, Clone, Default)]
struct GlyphCache {
    entries: FxHashMap<
        (cosmic_text::CacheKey, [u8; 3]),
        (Vec<u32>, cosmic_text::Placement),
    >,
    recently_used: FxHashSet<(cosmic_text::CacheKey, [u8; 3])>,
    trim_count: usize,
}

impl GlyphCache {
    const TRIM_INTERVAL: usize = 300;
    const CAPACITY_LIMIT: usize = 16 * 1024;

    fn new() -> Self {
        GlyphCache::default()
    }

    fn allocate(
        &mut self,
        cache_key: cosmic_text::CacheKey,
        color: Color,
        font_system: &mut cosmic_text::FontSystem,
        swash: &mut cosmic_text::SwashCache,
    ) -> Option<(&[u8], cosmic_text::Placement)> {
        let [r, g, b, _a] = color.into_rgba8();
        let key = (cache_key, [r, g, b]);

        if let hash_map::Entry::Vacant(entry) = self.entries.entry(key) {
            // TODO: Outline support
            let image = swash.get_image_uncached(font_system, cache_key)?;

            let glyph_size = image.placement.width as usize
                * image.placement.height as usize;

            if glyph_size == 0 {
                return None;
            }

            let mut buffer = vec![0u32; glyph_size];

            match image.content {
                cosmic_text::SwashContent::Mask => {
                    let mut i = 0;

                    // TODO: Blend alpha

                    for _y in 0..image.placement.height {
                        for _x in 0..image.placement.width {
                            buffer[i] = bytemuck::cast(
                                tiny_skia::ColorU8::from_rgba(
                                    b,
                                    g,
                                    r,
                                    image.data[i],
                                )
                                .premultiply(),
                            );

                            i += 1;
                        }
                    }
                }
                cosmic_text::SwashContent::Color => {
                    let mut i = 0;

                    for _y in 0..image.placement.height {
                        for _x in 0..image.placement.width {
                            // TODO: Blend alpha
                            buffer[i >> 2] = bytemuck::cast(
                                tiny_skia::ColorU8::from_rgba(
                                    image.data[i + 2],
                                    image.data[i + 1],
                                    image.data[i],
                                    image.data[i + 3],
                                )
                                .premultiply(),
                            );

                            i += 4;
                        }
                    }
                }
                cosmic_text::SwashContent::SubpixelMask => {
                    // TODO
                }
            }

            let _ = entry.insert((buffer, image.placement));
        }

        let _ = self.recently_used.insert(key);

        self.entries.get(&key).map(|(buffer, placement)| {
            (bytemuck::cast_slice(buffer.as_slice()), *placement)
        })
    }

    pub fn trim(&mut self) {
        if self.trim_count > Self::TRIM_INTERVAL
            || self.recently_used.len() >= Self::CAPACITY_LIMIT
        {
            self.entries
                .retain(|key, _| self.recently_used.contains(key));

            self.recently_used.clear();

            self.entries.shrink_to(Self::CAPACITY_LIMIT);
            self.recently_used.shrink_to(Self::CAPACITY_LIMIT);

            self.trim_count = 0;
        } else {
            self.trim_count += 1;
        }
    }
}