diff options
| author | 2023-02-26 23:40:17 +0100 | |
|---|---|---|
| committer | 2023-02-26 23:40:17 +0100 | |
| commit | 64fb722dfe8769d4a92edb0133f1863383ecfd86 (patch) | |
| tree | 1cc23b5a3d1ce49b4838ce50d906ef32524165e3 /tiny_skia | |
| parent | 744f3028f484c44899fed56d9190387569828a95 (diff) | |
| download | iced-64fb722dfe8769d4a92edb0133f1863383ecfd86.tar.gz iced-64fb722dfe8769d4a92edb0133f1863383ecfd86.tar.bz2 iced-64fb722dfe8769d4a92edb0133f1863383ecfd86.zip | |
Draft text support in `iced_tiny_skia`
Diffstat (limited to '')
| -rw-r--r-- | tiny_skia/Cargo.toml | 15 | ||||
| -rw-r--r-- | tiny_skia/src/backend.rs | 269 | ||||
| -rw-r--r-- | tiny_skia/src/lib.rs | 1 | ||||
| -rw-r--r-- | tiny_skia/src/text.rs | 350 | 
4 files changed, 523 insertions, 112 deletions
| diff --git a/tiny_skia/Cargo.toml b/tiny_skia/Cargo.toml index 70b1f5e7..55f7f5b7 100644 --- a/tiny_skia/Cargo.toml +++ b/tiny_skia/Cargo.toml @@ -12,6 +12,8 @@ raw-window-handle = "0.5"  softbuffer = "0.2"  tiny-skia = "0.8"  bytemuck = "1" +rustc-hash = "1.1" +ouroboros = "0.15"  [dependencies.iced_native]  version = "0.9" @@ -20,3 +22,16 @@ path = "../native"  [dependencies.iced_graphics]  version = "0.7"  path = "../graphics" + +[dependencies.cosmic-text] +features = ["std", "swash"] +git = "https://github.com/hecrj/cosmic-text" +rev = "dc83efbf00a2efb4118403538e8a47bfd69c3e5e" + +[dependencies.twox-hash] +version = "1.6" +default-features = false + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies.twox-hash] +version = "1.6.1" +features = ["std"] diff --git a/tiny_skia/src/backend.rs b/tiny_skia/src/backend.rs index 9eea1a32..5e743479 100644 --- a/tiny_skia/src/backend.rs +++ b/tiny_skia/src/backend.rs @@ -9,6 +9,7 @@ use std::borrow::Cow;  pub struct Backend {      default_font: Font,      default_text_size: f32, +    text_pipeline: crate::text::Pipeline,  }  impl Backend { @@ -16,6 +17,7 @@ impl Backend {          Self {              default_font: settings.default_font,              default_text_size: settings.default_text_size, +            text_pipeline: crate::text::Pipeline::new(),          }      } @@ -32,123 +34,161 @@ impl Backend {          let scale_factor = viewport.scale_factor() as f32;          for primitive in primitives { -            draw_primitive(primitive, pixels, None, scale_factor, Vector::ZERO); +            self.draw_primitive( +                primitive, +                pixels, +                None, +                scale_factor, +                Vector::ZERO, +            );          } + +        self.text_pipeline.end_frame();      } -} -fn draw_primitive( -    primitive: &Primitive, -    pixels: &mut tiny_skia::PixmapMut<'_>, -    clip_mask: Option<&tiny_skia::ClipMask>, -    scale_factor: f32, -    translation: Vector, -) { -    match primitive { -        Primitive::None => {} -        Primitive::Quad { -            bounds, -            background, -            border_radius: _, // TODO -            border_width, -            border_color, -        } => { -            let transform = tiny_skia::Transform::from_translate( -                translation.x, -                translation.y, -            ) -            .post_scale(scale_factor, scale_factor); - -            let path = tiny_skia::PathBuilder::from_rect( -                tiny_skia::Rect::from_xywh( -                    bounds.x, -                    bounds.y, -                    bounds.width, -                    bounds.height, +    fn draw_primitive( +        &mut self, +        primitive: &Primitive, +        pixels: &mut tiny_skia::PixmapMut<'_>, +        clip_mask: Option<&tiny_skia::ClipMask>, +        scale_factor: f32, +        translation: Vector, +    ) { +        match primitive { +            Primitive::None => {} +            Primitive::Quad { +                bounds, +                background, +                border_radius: _, // TODO +                border_width, +                border_color, +            } => { +                let transform = tiny_skia::Transform::from_translate( +                    translation.x, +                    translation.y,                  ) -                .expect("Create quad rectangle"), -            ); +                .post_scale(scale_factor, scale_factor); -            pixels.fill_path( -                &path, -                &tiny_skia::Paint { -                    shader: match background { -                        Background::Color(color) => { -                            tiny_skia::Shader::SolidColor(into_color(*color)) -                        } -                    }, -                    anti_alias: true, -                    ..tiny_skia::Paint::default() -                }, -                tiny_skia::FillRule::EvenOdd, -                transform, -                clip_mask, -            ); +                let path = tiny_skia::PathBuilder::from_rect( +                    tiny_skia::Rect::from_xywh( +                        bounds.x, +                        bounds.y, +                        bounds.width, +                        bounds.height, +                    ) +                    .expect("Create quad rectangle"), +                ); -            if *border_width > 0.0 { -                pixels.stroke_path( +                pixels.fill_path(                      &path,                      &tiny_skia::Paint { -                        shader: tiny_skia::Shader::SolidColor(into_color( -                            *border_color, -                        )), +                        shader: match background { +                            Background::Color(color) => { +                                tiny_skia::Shader::SolidColor(into_color( +                                    *color, +                                )) +                            } +                        },                          anti_alias: true,                          ..tiny_skia::Paint::default()                      }, -                    &tiny_skia::Stroke { -                        width: *border_width, -                        ..tiny_skia::Stroke::default() -                    }, +                    tiny_skia::FillRule::EvenOdd,                      transform,                      clip_mask,                  ); + +                if *border_width > 0.0 { +                    pixels.stroke_path( +                        &path, +                        &tiny_skia::Paint { +                            shader: tiny_skia::Shader::SolidColor(into_color( +                                *border_color, +                            )), +                            anti_alias: true, +                            ..tiny_skia::Paint::default() +                        }, +                        &tiny_skia::Stroke { +                            width: *border_width, +                            ..tiny_skia::Stroke::default() +                        }, +                        transform, +                        clip_mask, +                    ); +                }              } -        } -        Primitive::Text { .. } => { -            // TODO -        } -        Primitive::Image { .. } => { -            // TODO -        } -        Primitive::Svg { .. } => { -            // TODO -        } -        Primitive::Group { primitives } => { -            for primitive in primitives { -                draw_primitive( -                    primitive, +            Primitive::Text { +                content, +                bounds, +                color, +                size, +                font, +                horizontal_alignment, +                vertical_alignment, +            } => { +                self.text_pipeline.draw( +                    content, +                    (*bounds + translation) * scale_factor, +                    *color, +                    *size * scale_factor, +                    *font, +                    *horizontal_alignment, +                    *vertical_alignment, +                    pixels, +                    clip_mask, +                ); +            } +            Primitive::Image { .. } => { +                // TODO +            } +            Primitive::Svg { .. } => { +                // TODO +            } +            Primitive::Group { primitives } => { +                for primitive in primitives { +                    self.draw_primitive( +                        primitive, +                        pixels, +                        clip_mask, +                        scale_factor, +                        translation, +                    ); +                } +            } +            Primitive::Translate { +                translation: offset, +                content, +            } => { +                self.draw_primitive( +                    content,                      pixels,                      clip_mask,                      scale_factor, +                    translation + *offset, +                ); +            } +            Primitive::Clip { bounds, content } => { +                self.draw_primitive( +                    content, +                    pixels, +                    Some(&rectangular_clip_mask( +                        pixels, +                        *bounds * scale_factor, +                    )), +                    scale_factor,                      translation,                  );              } +            Primitive::Cached { cache } => { +                self.draw_primitive( +                    cache, +                    pixels, +                    clip_mask, +                    scale_factor, +                    translation, +                ); +            } +            Primitive::SolidMesh { .. } | Primitive::GradientMesh { .. } => {}          } -        Primitive::Translate { -            translation: offset, -            content, -        } => { -            draw_primitive( -                content, -                pixels, -                clip_mask, -                scale_factor, -                translation + *offset, -            ); -        } -        Primitive::Clip { bounds, content } => { -            draw_primitive( -                content, -                pixels, -                Some(&rectangular_clip_mask(pixels, *bounds * scale_factor)), -                scale_factor, -                translation, -            ); -        } -        Primitive::Cached { cache } => { -            draw_primitive(cache, pixels, clip_mask, scale_factor, translation); -        } -        Primitive::SolidMesh { .. } | Primitive::GradientMesh { .. } => {}      }  } @@ -185,7 +225,7 @@ fn rectangular_clip_mask(  impl iced_graphics::Backend for Backend {      fn trim_measurements(&mut self) { -        // TODO +        self.text_pipeline.trim_measurement_cache();      }  } @@ -204,30 +244,35 @@ impl backend::Text for Backend {      fn measure(          &self, -        _contents: &str, -        _size: f32, -        _font: Font, -        _bounds: Size, +        contents: &str, +        size: f32, +        font: Font, +        bounds: Size,      ) -> (f32, f32) { -        // TODO -        (0.0, 0.0) +        self.text_pipeline.measure(contents, size, font, bounds)      }      fn hit_test(          &self, -        _contents: &str, -        _size: f32, -        _font: Font, -        _bounds: Size, -        _point: iced_native::Point, -        _nearest_only: bool, +        contents: &str, +        size: f32, +        font: Font, +        bounds: Size, +        point: iced_native::Point, +        nearest_only: bool,      ) -> Option<text::Hit> { -        // TODO -        None +        self.text_pipeline.hit_test( +            contents, +            size, +            font, +            bounds, +            point, +            nearest_only, +        )      } -    fn load_font(&mut self, _font: Cow<'static, [u8]>) { -        // TODO +    fn load_font(&mut self, font: Cow<'static, [u8]>) { +        self.text_pipeline.load_font(font);      }  } diff --git a/tiny_skia/src/lib.rs b/tiny_skia/src/lib.rs index fce44e9e..420a1ffb 100644 --- a/tiny_skia/src/lib.rs +++ b/tiny_skia/src/lib.rs @@ -2,6 +2,7 @@ pub mod window;  mod backend;  mod settings; +mod text;  pub use backend::Backend;  pub use settings::Settings; diff --git a/tiny_skia/src/text.rs b/tiny_skia/src/text.rs new file mode 100644 index 00000000..5bd6eff1 --- /dev/null +++ b/tiny_skia/src/text.rs @@ -0,0 +1,350 @@ +pub use iced_native::text::Hit; + +use iced_native::alignment; +use iced_native::{Color, Font, Rectangle, Size}; + +use rustc_hash::{FxHashMap, FxHashSet}; +use std::borrow::Cow; +use std::cell::RefCell; +use std::collections::hash_map; +use std::hash::{BuildHasher, Hash, Hasher}; +use std::sync::Arc; + +#[allow(missing_debug_implementations)] +pub struct Pipeline { +    system: Option<System>, +} + +#[ouroboros::self_referencing] +struct System { +    fonts: cosmic_text::FontSystem, + +    #[borrows(fonts)] +    #[not_covariant] +    measurement_cache: RefCell<Cache<'this>>, + +    #[borrows(fonts)] +    #[not_covariant] +    render_cache: Cache<'this>, +} + +impl Pipeline { +    pub fn new() -> Self { +        Pipeline { +            system: Some( +                SystemBuilder { +                    fonts: cosmic_text::FontSystem::new_with_fonts( +                        [cosmic_text::fontdb::Source::Binary(Arc::new( +                            include_bytes!("../../wgpu/fonts/Iced-Icons.ttf") +                                .as_slice(), +                        ))] +                        .into_iter(), +                    ), +                    measurement_cache_builder: |_| RefCell::new(Cache::new()), +                    render_cache_builder: |_| Cache::new(), +                } +                .build(), +            ), +        } +    } + +    pub fn load_font(&mut self, bytes: Cow<'static, [u8]>) { +        let heads = self.system.take().unwrap().into_heads(); + +        let (locale, mut db) = heads.fonts.into_locale_and_db(); + +        db.load_font_source(cosmic_text::fontdb::Source::Binary(Arc::new( +            bytes.into_owned(), +        ))); + +        self.system = Some( +            SystemBuilder { +                fonts: cosmic_text::FontSystem::new_with_locale_and_db( +                    locale, db, +                ), +                measurement_cache_builder: |_| RefCell::new(Cache::new()), +                render_cache_builder: |_| Cache::new(), +            } +            .build(), +        ); +    } + +    pub fn draw( +        &mut self, +        content: &str, +        bounds: Rectangle, +        color: Color, +        size: f32, +        font: Font, +        _horizontal_alignment: alignment::Horizontal, // TODO +        _vertical_alignment: alignment::Vertical,     // TODO +        pixels: &mut tiny_skia::PixmapMut<'_>, +        clip_mask: Option<&tiny_skia::ClipMask>, +    ) { +        self.system.as_mut().unwrap().with_mut(|fields| { +            let key = Key { +                bounds: bounds.size(), +                content, +                font, +                size, +            }; + +            let (_, buffer) = fields.render_cache.allocate(&fields.fonts, key); + +            let mut swash = cosmic_text::SwashCache::new(&fields.fonts); + +            for run in buffer.layout_runs() { +                for glyph in run.glyphs { +                    // TODO: Outline support +                    if let Some(image) = swash.get_image(glyph.cache_key) { +                        let glyph_size = image.placement.width as usize +                            * image.placement.height as usize; + +                        if glyph_size == 0 { +                            continue; +                        } + +                        // TODO: Cache glyph rasterization +                        let mut buffer = vec![0u32; glyph_size]; + +                        match image.content { +                            cosmic_text::SwashContent::Mask => { +                                let mut i = 0; + +                                // TODO: Blend alpha +                                let [r, g, b, _a] = color.into_rgba8(); + +                                for _y in 0..image.placement.height { +                                    for _x in 0..image.placement.width { +                                        buffer[i] = +                                            tiny_skia::ColorU8::from_rgba( +                                                b, +                                                g, +                                                r, +                                                image.data[i], +                                            ) +                                            .premultiply() +                                            .get(); + +                                        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] = (image.data[i + 3] as u32) +                                            << 24 +                                            | (image.data[i + 2] as u32) << 16 +                                            | (image.data[i + 1] as u32) << 8 +                                            | image.data[i] as u32; + +                                        i += 1; +                                    } +                                } +                            } +                            cosmic_text::SwashContent::SubpixelMask => { +                                // TODO +                            } +                        } + +                        let pixmap = tiny_skia::PixmapRef::from_bytes( +                            bytemuck::cast_slice(&buffer), +                            image.placement.width, +                            image.placement.height, +                        ) +                        .expect("Create glyph pixel map"); + +                        pixels.draw_pixmap( +                            bounds.x as i32 +                                + glyph.x_int +                                + image.placement.left, +                            bounds.y as i32 - glyph.y_int - image.placement.top +                                + run.line_y as i32, +                            pixmap, +                            &tiny_skia::PixmapPaint::default(), +                            tiny_skia::Transform::identity(), +                            clip_mask, +                        ); +                    } +                } +            } +        }); +    } + +    pub fn end_frame(&mut self) { +        self.system +            .as_mut() +            .unwrap() +            .with_render_cache_mut(|cache| cache.trim()); +    } + +    pub fn measure( +        &self, +        content: &str, +        size: f32, +        font: Font, +        bounds: Size, +    ) -> (f32, f32) { +        self.system.as_ref().unwrap().with(|fields| { +            let mut measurement_cache = fields.measurement_cache.borrow_mut(); + +            let (_, paragraph) = measurement_cache.allocate( +                fields.fonts, +                Key { +                    content, +                    size, +                    font, +                    bounds, +                }, +            ); + +            let (total_lines, max_width) = paragraph +                .layout_runs() +                .enumerate() +                .fold((0, 0.0), |(_, max), (i, buffer)| { +                    (i + 1, buffer.line_w.max(max)) +                }); + +            (max_width, size * 1.2 * total_lines as f32) +        }) +    } + +    pub fn hit_test( +        &self, +        content: &str, +        size: f32, +        font: iced_native::Font, +        bounds: iced_native::Size, +        point: iced_native::Point, +        _nearest_only: bool, +    ) -> Option<Hit> { +        self.system.as_ref().unwrap().with(|fields| { +            let mut measurement_cache = fields.measurement_cache.borrow_mut(); + +            let (_, paragraph) = measurement_cache.allocate( +                fields.fonts, +                Key { +                    content, +                    size, +                    font, +                    bounds, +                }, +            ); + +            let cursor = paragraph.hit(point.x, point.y)?; + +            Some(Hit::CharOffset(cursor.index)) +        }) +    } + +    pub fn trim_measurement_cache(&mut self) { +        self.system +            .as_mut() +            .unwrap() +            .with_measurement_cache_mut(|cache| cache.borrow_mut().trim()); +    } +} + +fn to_family(font: Font) -> cosmic_text::Family<'static> { +    match font { +        Font::Name(name) => cosmic_text::Family::Name(name), +        Font::SansSerif => cosmic_text::Family::SansSerif, +        Font::Serif => cosmic_text::Family::Serif, +        Font::Cursive => cosmic_text::Family::Cursive, +        Font::Fantasy => cosmic_text::Family::Fantasy, +        Font::Monospace => cosmic_text::Family::Monospace, +    } +} + +struct Cache<'a> { +    entries: FxHashMap<KeyHash, cosmic_text::Buffer<'a>>, +    recently_used: FxHashSet<KeyHash>, +    hasher: HashBuilder, +    trim_count: usize, +} + +#[cfg(not(target_arch = "wasm32"))] +type HashBuilder = twox_hash::RandomXxHashBuilder64; + +#[cfg(target_arch = "wasm32")] +type HashBuilder = std::hash::BuildHasherDefault<twox_hash::XxHash64>; + +impl<'a> Cache<'a> { +    const TRIM_INTERVAL: usize = 300; + +    fn new() -> Self { +        Self { +            entries: FxHashMap::default(), +            recently_used: FxHashSet::default(), +            hasher: HashBuilder::default(), +            trim_count: 0, +        } +    } + +    fn allocate( +        &mut self, +        fonts: &'a cosmic_text::FontSystem, +        key: Key<'_>, +    ) -> (KeyHash, &mut cosmic_text::Buffer<'a>) { +        let hash = { +            let mut hasher = self.hasher.build_hasher(); + +            key.content.hash(&mut hasher); +            key.size.to_bits().hash(&mut hasher); +            key.font.hash(&mut hasher); +            key.bounds.width.to_bits().hash(&mut hasher); +            key.bounds.height.to_bits().hash(&mut hasher); + +            hasher.finish() +        }; + +        if let hash_map::Entry::Vacant(entry) = self.entries.entry(hash) { +            let metrics = cosmic_text::Metrics::new(key.size, key.size * 1.2); +            let mut buffer = cosmic_text::Buffer::new(fonts, metrics); + +            buffer.set_size( +                key.bounds.width, +                key.bounds.height.max(key.size * 1.2), +            ); +            buffer.set_text( +                key.content, +                cosmic_text::Attrs::new() +                    .family(to_family(key.font)) +                    .monospaced(matches!(key.font, Font::Monospace)), +            ); + +            let _ = entry.insert(buffer); +        } + +        let _ = self.recently_used.insert(hash); + +        (hash, self.entries.get_mut(&hash).unwrap()) +    } + +    fn trim(&mut self) { +        if self.trim_count >= Self::TRIM_INTERVAL { +            self.entries +                .retain(|key, _| self.recently_used.contains(key)); + +            self.recently_used.clear(); + +            self.trim_count = 0; +        } else { +            self.trim_count += 1; +        } +    } +} + +#[derive(Debug, Clone, Copy)] +struct Key<'a> { +    content: &'a str, +    size: f32, +    font: Font, +    bounds: Size, +} + +type KeyHash = u64; | 
