use crate::error::{ChartError, Result};
use glyphon::{
Attrs, Buffer, Cache, Color, Family, FontSystem, Metrics, Resolution, Shaping, SwashCache,
TextArea, TextAtlas, TextBounds, TextRenderer as GlyphonRenderer, Viewport as GlyphonViewport,
};
use wgpu::{Device, Queue, TextureFormat};
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum TextAnchor {
Start,
Middle,
End,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum TextBaseline {
Top,
Middle,
Bottom,
}
#[derive(Debug, Clone)]
pub struct TextItem {
pub text: String,
pub x: f32,
pub y: f32,
pub font_size: f32,
pub color: crate::theme::Color,
pub anchor: TextAnchor,
pub baseline: TextBaseline,
}
pub struct TextRenderer {
font_system: FontSystem,
swash_cache: SwashCache,
viewport: GlyphonViewport,
atlas: TextAtlas,
renderer: GlyphonRenderer,
viewport_width: u32,
viewport_height: u32,
text_buffers: Vec<Buffer>,
}
impl TextRenderer {
pub fn new(
device: &Device,
queue: &Queue,
format: TextureFormat,
width: u32,
height: u32,
) -> Result<Self> {
let font_system = FontSystem::new();
let swash_cache = SwashCache::new();
let cache = Cache::new(device);
let viewport = GlyphonViewport::new(device, &cache);
let mut atlas = TextAtlas::new(device, queue, &cache, format);
let renderer =
GlyphonRenderer::new(&mut atlas, device, wgpu::MultisampleState::default(), None);
Ok(Self {
font_system,
swash_cache,
viewport,
atlas,
renderer,
viewport_width: width,
viewport_height: height,
text_buffers: Vec::new(),
})
}
pub fn resize(&mut self, _device: &Device, queue: &Queue, width: u32, height: u32) {
self.viewport_width = width;
self.viewport_height = height;
self.viewport.update(
queue,
Resolution {
width: self.viewport_width,
height: self.viewport_height,
},
);
}
pub fn prepare(
&mut self,
device: &Device,
queue: &Queue,
text_items: &[TextItem],
) -> Result<()> {
self.text_buffers.clear();
for item in text_items {
let mut buffer = Buffer::new(
&mut self.font_system,
Metrics::new(item.font_size, item.font_size * 1.2),
);
buffer.set_size(
&mut self.font_system,
Some(self.viewport_width as f32),
Some(self.viewport_height as f32),
);
let attrs = Attrs::new().family(Family::SansSerif);
buffer.set_text(&mut self.font_system, &item.text, &attrs, Shaping::Advanced);
buffer.shape_until_scroll(&mut self.font_system, false);
self.text_buffers.push(buffer);
}
let mut text_areas = Vec::new();
for (i, item) in text_items.iter().enumerate() {
let buffer = &self.text_buffers[i];
let (text_width, text_height) = self.measure_buffer(buffer);
let left = match item.anchor {
TextAnchor::Start => item.x,
TextAnchor::Middle => item.x - text_width / 2.0,
TextAnchor::End => item.x - text_width,
};
let top = match item.baseline {
TextBaseline::Top => item.y,
TextBaseline::Middle => item.y - text_height / 2.0,
TextBaseline::Bottom => item.y - text_height,
};
let text_area = TextArea {
buffer,
left,
top,
scale: 1.0,
bounds: TextBounds {
left: left as i32,
top: top as i32,
right: (left + self.viewport_width as f32) as i32,
bottom: (top + self.viewport_height as f32) as i32,
},
default_color: Color::rgba(
(item.color.r * 255.0) as u8,
(item.color.g * 255.0) as u8,
(item.color.b * 255.0) as u8,
(item.color.a * 255.0) as u8,
),
custom_glyphs: &[],
};
text_areas.push(text_area);
}
self.viewport.update(
queue,
Resolution {
width: self.viewport_width,
height: self.viewport_height,
},
);
self.renderer
.prepare(
device,
queue,
&mut self.font_system,
&mut self.atlas,
&self.viewport,
text_areas,
&mut self.swash_cache,
)
.map_err(|e| ChartError::TextRendering(format!("Failed to prepare text: {:?}", e)))?;
Ok(())
}
pub fn render<'a>(&'a self, render_pass: &mut wgpu::RenderPass<'a>) -> Result<()> {
self.renderer
.render(&self.atlas, &self.viewport, render_pass)
.map_err(|e| ChartError::TextRendering(format!("Failed to render text: {:?}", e)))?;
Ok(())
}
fn measure_buffer(&self, buffer: &Buffer) -> (f32, f32) {
let mut width = 0.0f32;
let mut height = 0.0f32;
for run in buffer.layout_runs() {
width = width.max(run.line_w);
height += run.line_height;
}
(width, height)
}
pub fn clear_cache(&mut self) {
self.atlas.trim();
}
}