use oxiui_core::{
geometry::Rect,
paint::{DrawList, ImageData, ImageFilter},
Color, UiError,
};
use oxiui_text::{GlyphAtlas, GlyphKey, TextPipeline, TextStyle};
pub struct TextBridge {
pipeline: TextPipeline,
atlas: GlyphAtlas,
}
impl TextBridge {
pub fn new(pipeline: TextPipeline, atlas_capacity: usize) -> Self {
Self {
pipeline,
atlas: GlyphAtlas::new(atlas_capacity),
}
}
pub fn expand_draw_text(
&mut self,
out: &mut DrawList,
rect: Rect,
text: &str,
style: &TextStyle,
color: Color,
) -> Result<(), UiError> {
let shaped = self
.pipeline
.shape(text, style)
.map_err(|e| UiError::Render(e.to_string()))?;
let pen_x0 = rect.left();
let pen_y0 = rect.top();
for line in &shaped.lines {
for glyph_pos in line {
let glyph_id = glyph_id_from_byte_offset(text, glyph_pos.byte_offset);
let key = GlyphKey::new(glyph_id, style.font_size, glyph_pos.x.fract(), 0.0);
let entry_result =
self.atlas
.get_or_rasterize(&mut self.pipeline, key, text, style);
let entry = match entry_result {
Ok(e) => e,
Err(_) => continue, };
let bm = &entry.bitmap;
if bm.width == 0 || bm.height == 0 || bm.pixels.is_empty() {
continue;
}
let rgba = greyscale_to_rgba(&bm.pixels, color);
let dest = Rect::new(
pen_x0 + glyph_pos.x + entry.bearing.0 as f32,
pen_y0 + glyph_pos.y + entry.bearing.1 as f32,
bm.width as f32,
bm.height as f32,
);
if dest.width() > 0.0 && dest.height() > 0.0 {
let image = ImageData::new(rgba, bm.width, bm.height);
out.push_image(image, dest, ImageFilter::Bilinear);
}
}
}
Ok(())
}
pub fn atlas(&self) -> &GlyphAtlas {
&self.atlas
}
pub fn atlas_utilization(&self) -> f32 {
self.atlas.utilization()
}
}
fn glyph_id_from_byte_offset(text: &str, byte_offset: usize) -> u16 {
if byte_offset >= text.len() {
return 0;
}
let ch = text[byte_offset..].chars().next().unwrap_or('\0');
(ch as u32 & 0xFFFF) as u16
}
fn greyscale_to_rgba(grey: &[u8], color: Color) -> Vec<u8> {
let mut out = Vec::with_capacity(grey.len() * 4);
let (r, g, b, a) = (color.0, color.1, color.2, color.3);
for &v in grey {
let alpha = ((a as u32 * v as u32) / 255) as u8;
out.push(r);
out.push(g);
out.push(b);
out.push(alpha);
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn greyscale_to_rgba_full_coverage() {
let rgba = greyscale_to_rgba(&[255, 128], Color(255, 0, 0, 255));
assert_eq!(rgba.len(), 8);
assert_eq!(rgba[0], 255);
assert_eq!(rgba[1], 0);
assert_eq!(rgba[2], 0);
assert_eq!(rgba[3], 255);
assert_eq!(rgba[7], 128);
}
#[test]
fn greyscale_to_rgba_zero_coverage_is_transparent() {
let rgba = greyscale_to_rgba(&[0], Color(255, 255, 255, 255));
assert_eq!(rgba[3], 0);
}
#[test]
fn greyscale_to_rgba_zero_tint_alpha_is_transparent() {
let rgba = greyscale_to_rgba(&[255], Color(255, 255, 255, 0));
assert_eq!(rgba[3], 0);
}
#[test]
fn greyscale_to_rgba_output_length_is_4x_input() {
let input: Vec<u8> = (0..10).collect();
let rgba = greyscale_to_rgba(&input, Color(0, 0, 0, 255));
assert_eq!(rgba.len(), 40);
}
#[test]
fn glyph_id_ascii_a_is_97() {
let id = glyph_id_from_byte_offset("A", 0);
assert_eq!(id, 'A' as u16);
}
#[test]
fn glyph_id_out_of_range_returns_zero() {
let id = glyph_id_from_byte_offset("A", 100);
assert_eq!(id, 0);
}
#[test]
fn glyph_id_empty_text_returns_zero() {
let id = glyph_id_from_byte_offset("", 0);
assert_eq!(id, 0);
}
#[test]
fn text_bridge_from_invalid_font_fails() {
let result = TextPipeline::from_bytes(&[]);
assert!(result.is_err());
}
#[test]
fn text_bridge_atlas_utilization_starts_at_zero() {
let atlas = GlyphAtlas::new(100);
let u = atlas.utilization();
assert!((u - 0.0).abs() < f32::EPSILON);
}
}