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()),
}
}
#[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: ¶graph::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_opt, height_opt) = buffer.size();
draw(
font_system.raw(),
&mut self.glyph_cache,
buffer,
Rectangle::new(
position,
Size::new(
width_opt.unwrap_or(pixels.width() as f32),
height_opt.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) {
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;
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 {
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 => {
}
}
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;
}
}
}