use std::cell::Cell;
use std::rc::Rc;
use glyphon::{
Cache, Color as GlyphonColor, ColorMode, FontSystem, Resolution, SwashCache, TextArea,
TextAtlas, TextBounds, TextRenderer, Viewport,
};
use rustc_hash::FxHashMap;
use wgpu::{Device, MultisampleState, Queue, TextureFormat};
use crate::render_stats::{self, Pipeline};
use crate::widgets::font::FontWeight;
use crate::widgets::{FontFamily, LineHeight, TextAlign};
const MAX_CACHED: usize = 32;
const MAX_SIDE: u32 = 2048;
pub struct MaskSpec<'a> {
pub text: &'a str,
pub font_size: f32,
pub font_family: FontFamily,
pub font_weight: FontWeight,
pub line_height: LineHeight,
pub fit: Option<super::text_measurer::LineFit>,
pub align: TextAlign,
pub buffer: (f32, f32),
pub size: (u32, u32),
pub offset: (f32, f32),
pub density: f32,
}
#[derive(PartialEq, Eq, Hash)]
struct MaskKey {
text: String,
font_size_bits: u32,
weight: u16,
family: FontFamily,
line_height: (u8, u32),
align: TextAlign,
width: u32,
height: u32,
buffer: (u32, u32),
offset: (i32, i32),
fit: Option<super::text_measurer::LineFitKey>,
}
struct CachedMask {
#[allow(dead_code)] texture: wgpu::Texture,
view: Rc<wgpu::TextureView>,
last_used: Cell<u64>,
}
struct Shaper {
font_system: FontSystem,
swash_cache: SwashCache,
#[allow(dead_code)] cache: Cache,
atlas: TextAtlas,
text_renderer: TextRenderer,
viewport: Viewport,
}
impl Shaper {
fn new(device: &Device, queue: &Queue, format: TextureFormat) -> Self {
let mut font_system = FontSystem::new();
for data in crate::get_registered_fonts() {
font_system
.db_mut()
.load_font_source(glyphon::fontdb::Source::Binary(data));
}
let cache = Cache::new(device);
let mut atlas = TextAtlas::with_color_mode(device, queue, &cache, format, ColorMode::Web);
let text_renderer =
TextRenderer::new(&mut atlas, device, MultisampleState::default(), None);
let viewport = Viewport::new(device, &cache);
Self {
font_system,
swash_cache: SwashCache::new(),
cache,
atlas,
text_renderer,
viewport,
}
}
}
pub struct TextMaskRenderer {
shaper: Option<Shaper>,
format: TextureFormat,
masks: FxHashMap<MaskKey, Rc<CachedMask>>,
frame_gen: u64,
}
impl TextMaskRenderer {
pub fn new(format: TextureFormat) -> Self {
Self {
shaper: None,
format,
masks: FxHashMap::default(),
frame_gen: 0,
}
}
pub fn begin_frame(&mut self) {
self.frame_gen += 1;
if self.masks.len() > MAX_CACHED {
let current = self.frame_gen;
self.masks
.retain(|_, mask| mask.last_used.get() + 1 >= current);
}
}
pub fn mask(
&mut self,
device: &Device,
queue: &Queue,
spec: &MaskSpec<'_>,
) -> Option<Rc<wgpu::TextureView>> {
let (width, height) = spec.size;
if spec.text.is_empty() || width == 0 || height == 0 {
return None;
}
if width > MAX_SIDE || height > MAX_SIDE {
log::warn!(
"text backdrop blur skipped: mask would be {width}x{height}, over the {MAX_SIDE} limit"
);
return None;
}
let font_size = spec.font_size * spec.density;
let weight = if spec.font_weight == FontWeight::default() {
FontWeight::NORMAL
} else {
spec.font_weight
};
let key = MaskKey {
text: spec.text.to_owned(),
font_size_bits: font_size.to_bits(),
weight: weight.0,
family: spec.font_family,
line_height: spec.line_height.key(),
align: spec.align,
width,
height,
buffer: (spec.buffer.0 as u32, spec.buffer.1 as u32),
offset: (
(spec.offset.0 * 4.0).round() as i32,
(spec.offset.1 * 4.0).round() as i32,
),
fit: spec.fit.map(|fit| fit.key()),
};
if let Some(cached) = self.masks.get(&key) {
cached.last_used.set(self.frame_gen);
return Some(Rc::clone(&cached.view));
}
let format = self.format;
let shaper = self
.shaper
.get_or_insert_with(|| Shaper::new(device, queue, format));
let buffer = super::text::shape(
&mut shaper.font_system,
spec.text,
font_size,
spec.font_family,
spec.font_weight,
spec.line_height,
spec.align,
(Some(spec.buffer.0), Some(spec.buffer.1)),
spec.fit,
spec.density,
);
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("Text Mask"),
size: wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
});
let view = texture.create_view(&Default::default());
shaper.viewport.update(queue, Resolution { width, height });
let area = TextArea {
buffer: &buffer,
left: spec.offset.0,
top: spec.offset.1,
scale: 1.0,
bounds: TextBounds {
left: 0,
top: 0,
right: width as i32,
bottom: height as i32,
},
default_color: GlyphonColor::rgb(255, 255, 255),
custom_glyphs: &[],
};
if let Err(e) = shaper.text_renderer.prepare(
device,
queue,
&mut shaper.font_system,
&mut shaper.atlas,
&shaper.viewport,
vec![area],
&mut shaper.swash_cache,
) {
log::error!("text mask prepare failed: {e:?}");
return None;
}
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("Text Mask Encoder"),
});
{
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Text Mask Pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
store: wgpu::StoreOp::Store,
},
depth_slice: None,
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
match shaper
.text_renderer
.render(&shaper.atlas, &shaper.viewport, &mut pass)
{
Ok(()) => render_stats::record_draw_calls(Pipeline::Text, 1),
Err(e) => log::error!("text mask render failed: {e:?}"),
}
}
queue.submit(std::iter::once(encoder.finish()));
let view = Rc::new(view);
self.masks.insert(
key,
Rc::new(CachedMask {
texture,
view: Rc::clone(&view),
last_used: Cell::new(self.frame_gen),
}),
);
Some(view)
}
}