Add support for custom icons/glyphs (#102)
* add support for svg icons * remove SVG helper struct * forgot to remove default features * rework api for custom glyphs * remove unused file * expose custom glyph structs * remove `InlineBox` * use slice for TextArea::custom_glyphs * offset custom glyphs by text area position * remove svg feature * remove unused file * add scale field to CustomGlyphInput * update custom-glyphs example to winit 0.30 * fix the mess merge conflicts made * add final newline * make custom-glyphs a default feature * remove custom-glyphs feature * remove unnecessary pub(crate) * rename CustomGlyphDesc to CustomGlyph * rename CustomGlyphID to CustomGlyphId * improve custom glyph API and refactor text renderer * rename CustomGlyphInput and CustomGlyphOutput, add some docs
This commit is contained in:
parent
ce6ede951c
commit
b2129f1765
9 changed files with 1173 additions and 222 deletions
113
src/custom_glyph.rs
Normal file
113
src/custom_glyph.rs
Normal file
|
@ -0,0 +1,113 @@
|
|||
use crate::Color;
|
||||
use cosmic_text::SubpixelBin;
|
||||
|
||||
pub type CustomGlyphId = u16;
|
||||
|
||||
/// A custom glyph to render
|
||||
#[derive(Default, Debug, Clone, Copy, PartialEq)]
|
||||
pub struct CustomGlyph {
|
||||
/// The unique identifier for this glyph
|
||||
pub id: CustomGlyphId,
|
||||
/// The position of the left edge of the glyph
|
||||
pub left: f32,
|
||||
/// The position of the top edge of the glyph
|
||||
pub top: f32,
|
||||
/// The width of the glyph
|
||||
pub width: f32,
|
||||
/// The height of the glyph
|
||||
pub height: f32,
|
||||
/// The color of this glyph (only relevant if the glyph is rendered with the
|
||||
/// type [`ContentType::Mask`])
|
||||
///
|
||||
/// Set to `None` to use [`TextArea::default_color`].
|
||||
pub color: Option<Color>,
|
||||
/// If `true`, then this glyph will be snapped to the nearest whole physical
|
||||
/// pixel and the resulting `SubpixelBin`'s in `RasterizationRequest` will always
|
||||
/// be `Zero` (useful for images and other large glyphs).
|
||||
pub snap_to_physical_pixel: bool,
|
||||
/// Additional metadata about the glyph
|
||||
pub metadata: usize,
|
||||
}
|
||||
|
||||
/// A request to rasterize a custom glyph
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct RasterizationRequest {
|
||||
/// The unique identifier of the glyph
|
||||
pub id: CustomGlyphId,
|
||||
/// The width of the glyph in physical pixels
|
||||
pub width: u16,
|
||||
/// The height of the glyph in physical pixels
|
||||
pub height: u16,
|
||||
/// Binning of fractional X offset
|
||||
///
|
||||
/// If `CustomGlyph::snap_to_physical_pixel` was set to `true`, then this
|
||||
/// will always be `Zero`.
|
||||
pub x_bin: SubpixelBin,
|
||||
/// Binning of fractional Y offset
|
||||
///
|
||||
/// If `CustomGlyph::snap_to_physical_pixel` was set to `true`, then this
|
||||
/// will always be `Zero`.
|
||||
pub y_bin: SubpixelBin,
|
||||
/// The scaling factor applied to the text area (Note that `width` and
|
||||
/// `height` are already scaled by this factor.)
|
||||
pub scale: f32,
|
||||
}
|
||||
|
||||
/// A rasterized custom glyph
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RasterizedCustomGlyph {
|
||||
/// The raw image data
|
||||
pub data: Vec<u8>,
|
||||
/// The type of image data contained in `data`
|
||||
pub content_type: ContentType,
|
||||
}
|
||||
|
||||
impl RasterizedCustomGlyph {
|
||||
pub(crate) fn validate(&self, input: &RasterizationRequest, expected_type: Option<ContentType>) {
|
||||
if let Some(expected_type) = expected_type {
|
||||
assert_eq!(self.content_type, expected_type, "Custom glyph rasterizer must always produce the same content type for a given input. Expected {:?}, got {:?}. Input: {:?}", expected_type, self.content_type, input);
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
self.data.len(),
|
||||
input.width as usize * input.height as usize * self.content_type.bytes_per_pixel(),
|
||||
"Invalid custom glyph rasterizer output. Expected data of length {}, got length {}. Input: {:?}",
|
||||
input.width as usize * input.height as usize * self.content_type.bytes_per_pixel(),
|
||||
self.data.len(),
|
||||
input,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct CustomGlyphCacheKey {
|
||||
/// Font ID
|
||||
pub glyph_id: CustomGlyphId,
|
||||
/// Glyph width
|
||||
pub width: u16,
|
||||
/// Glyph height
|
||||
pub height: u16,
|
||||
/// Binning of fractional X offset
|
||||
pub x_bin: SubpixelBin,
|
||||
/// Binning of fractional Y offset
|
||||
pub y_bin: SubpixelBin,
|
||||
}
|
||||
|
||||
/// The type of image data contained in a rasterized glyph
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub enum ContentType {
|
||||
/// Each pixel contains 32 bits of rgba data
|
||||
Color,
|
||||
/// Each pixel contains a single 8 bit channel
|
||||
Mask,
|
||||
}
|
||||
|
||||
impl ContentType {
|
||||
/// The number of bytes per pixel for this content type
|
||||
pub fn bytes_per_pixel(&self) -> usize {
|
||||
match self {
|
||||
Self::Color => 4,
|
||||
Self::Mask => 1,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,19 +5,21 @@
|
|||
//! [etagere]: https://github.com/nical/etagere
|
||||
|
||||
mod cache;
|
||||
mod custom_glyph;
|
||||
mod error;
|
||||
mod text_atlas;
|
||||
mod text_render;
|
||||
mod viewport;
|
||||
|
||||
pub use cache::Cache;
|
||||
pub use custom_glyph::{
|
||||
ContentType, CustomGlyph, CustomGlyphId, RasterizationRequest, RasterizedCustomGlyph,
|
||||
};
|
||||
pub use error::{PrepareError, RenderError};
|
||||
pub use text_atlas::{ColorMode, TextAtlas};
|
||||
pub use text_render::TextRenderer;
|
||||
pub use viewport::Viewport;
|
||||
|
||||
use text_render::ContentType;
|
||||
|
||||
// Re-export all top-level types from `cosmic-text` for convenience.
|
||||
#[doc(no_inline)]
|
||||
pub use cosmic_text::{
|
||||
|
@ -117,4 +119,7 @@ pub struct TextArea<'a> {
|
|||
pub bounds: TextBounds,
|
||||
// The default color of the text area.
|
||||
pub default_color: Color,
|
||||
|
||||
/// Additional custom glyphs to render
|
||||
pub custom_glyphs: &'a [CustomGlyph],
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use crate::{
|
||||
text_render::ContentType, Cache, CacheKey, FontSystem, GlyphDetails, GpuCacheStatus, SwashCache,
|
||||
text_render::GlyphonCacheKey, Cache, ContentType, RasterizationRequest, RasterizedCustomGlyph,
|
||||
FontSystem, GlyphDetails, GpuCacheStatus, SwashCache,
|
||||
};
|
||||
use etagere::{size2, Allocation, BucketedAtlasAllocator};
|
||||
use lru::LruCache;
|
||||
|
@ -20,8 +21,8 @@ pub(crate) struct InnerAtlas {
|
|||
pub texture_view: TextureView,
|
||||
pub packer: BucketedAtlasAllocator,
|
||||
pub size: u32,
|
||||
pub glyph_cache: LruCache<CacheKey, GlyphDetails, Hasher>,
|
||||
pub glyphs_in_use: HashSet<CacheKey, Hasher>,
|
||||
pub glyph_cache: LruCache<GlyphonCacheKey, GlyphDetails, Hasher>,
|
||||
pub glyphs_in_use: HashSet<GlyphonCacheKey, Hasher>,
|
||||
pub max_texture_dimension_2d: u32,
|
||||
}
|
||||
|
||||
|
@ -106,12 +107,12 @@ impl InnerAtlas {
|
|||
self.kind.num_channels()
|
||||
}
|
||||
|
||||
pub(crate) fn promote(&mut self, glyph: CacheKey) {
|
||||
pub(crate) fn promote(&mut self, glyph: GlyphonCacheKey) {
|
||||
self.glyph_cache.promote(&glyph);
|
||||
self.glyphs_in_use.insert(glyph);
|
||||
}
|
||||
|
||||
pub(crate) fn put(&mut self, glyph: CacheKey, details: GlyphDetails) {
|
||||
pub(crate) fn put(&mut self, glyph: GlyphonCacheKey, details: GlyphDetails) {
|
||||
self.glyph_cache.put(glyph, details);
|
||||
self.glyphs_in_use.insert(glyph);
|
||||
}
|
||||
|
@ -122,6 +123,8 @@ impl InnerAtlas {
|
|||
queue: &wgpu::Queue,
|
||||
font_system: &mut FontSystem,
|
||||
cache: &mut SwashCache,
|
||||
scale_factor: f32,
|
||||
mut rasterize_custom_glyph: impl FnMut(RasterizationRequest) -> Option<RasterizedCustomGlyph>,
|
||||
) -> bool {
|
||||
if self.size >= self.max_texture_dimension_2d {
|
||||
return false;
|
||||
|
@ -157,10 +160,38 @@ impl InnerAtlas {
|
|||
GpuCacheStatus::SkipRasterization => continue,
|
||||
};
|
||||
|
||||
let image = cache.get_image_uncached(font_system, cache_key).unwrap();
|
||||
let (image_data, width, height) = match cache_key {
|
||||
GlyphonCacheKey::Text(cache_key) => {
|
||||
let image = cache.get_image_uncached(font_system, cache_key).unwrap();
|
||||
let width = image.placement.width as usize;
|
||||
let height = image.placement.height as usize;
|
||||
|
||||
let width = image.placement.width as usize;
|
||||
let height = image.placement.height as usize;
|
||||
(image.data, width, height)
|
||||
}
|
||||
GlyphonCacheKey::Custom(cache_key) => {
|
||||
let input = RasterizationRequest {
|
||||
id: cache_key.glyph_id,
|
||||
width: cache_key.width,
|
||||
height: cache_key.height,
|
||||
x_bin: cache_key.x_bin,
|
||||
y_bin: cache_key.y_bin,
|
||||
scale: scale_factor,
|
||||
};
|
||||
|
||||
let Some(rasterized_glyph) = (rasterize_custom_glyph)(input) else {
|
||||
panic!("Custom glyph rasterizer returned `None` when it previously returned `Some` for the same input {:?}", &input);
|
||||
};
|
||||
|
||||
// Sanity checks on the rasterizer output
|
||||
rasterized_glyph.validate(&input, Some(self.kind.as_content_type()));
|
||||
|
||||
(
|
||||
rasterized_glyph.data,
|
||||
cache_key.width as usize,
|
||||
cache_key.height as usize,
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
queue.write_texture(
|
||||
ImageCopyTexture {
|
||||
|
@ -173,7 +204,7 @@ impl InnerAtlas {
|
|||
},
|
||||
aspect: TextureAspect::All,
|
||||
},
|
||||
&image.data,
|
||||
&image_data,
|
||||
ImageDataLayout {
|
||||
offset: 0,
|
||||
bytes_per_row: Some(width as u32 * self.kind.num_channels() as u32),
|
||||
|
@ -224,6 +255,13 @@ impl Kind {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn as_content_type(&self) -> ContentType {
|
||||
match self {
|
||||
Self::Mask => ContentType::Mask,
|
||||
Self::Color { .. } => ContentType::Color,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The color mode of an [`Atlas`].
|
||||
|
@ -313,10 +351,26 @@ impl TextAtlas {
|
|||
font_system: &mut FontSystem,
|
||||
cache: &mut SwashCache,
|
||||
content_type: ContentType,
|
||||
scale_factor: f32,
|
||||
rasterize_custom_glyph: impl FnMut(RasterizationRequest) -> Option<RasterizedCustomGlyph>,
|
||||
) -> bool {
|
||||
let did_grow = match content_type {
|
||||
ContentType::Mask => self.mask_atlas.grow(device, queue, font_system, cache),
|
||||
ContentType::Color => self.color_atlas.grow(device, queue, font_system, cache),
|
||||
ContentType::Mask => self.mask_atlas.grow(
|
||||
device,
|
||||
queue,
|
||||
font_system,
|
||||
cache,
|
||||
scale_factor,
|
||||
rasterize_custom_glyph,
|
||||
),
|
||||
ContentType::Color => self.color_atlas.grow(
|
||||
device,
|
||||
queue,
|
||||
font_system,
|
||||
cache,
|
||||
scale_factor,
|
||||
rasterize_custom_glyph,
|
||||
),
|
||||
};
|
||||
|
||||
if did_grow {
|
||||
|
@ -326,7 +380,7 @@ impl TextAtlas {
|
|||
did_grow
|
||||
}
|
||||
|
||||
pub(crate) fn glyph(&self, glyph: &CacheKey) -> Option<&GlyphDetails> {
|
||||
pub(crate) fn glyph(&self, glyph: &GlyphonCacheKey) -> Option<&GlyphDetails> {
|
||||
self.mask_atlas
|
||||
.glyph_cache
|
||||
.peek(glyph)
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
use crate::{
|
||||
ColorMode, FontSystem, GlyphDetails, GlyphToRender, GpuCacheStatus, PrepareError, RenderError,
|
||||
SwashCache, SwashContent, TextArea, TextAtlas, Viewport,
|
||||
custom_glyph::CustomGlyphCacheKey, ColorMode, ContentType, RasterizationRequest, RasterizedCustomGlyph,
|
||||
FontSystem, GlyphDetails, GlyphToRender, GpuCacheStatus, PrepareError, RenderError, SwashCache,
|
||||
SwashContent, TextArea, TextAtlas, Viewport,
|
||||
};
|
||||
use cosmic_text::{Color, SubpixelBin};
|
||||
use std::{slice, sync::Arc};
|
||||
use wgpu::{
|
||||
Buffer, BufferDescriptor, BufferUsages, DepthStencilState, Device, Extent3d, ImageCopyTexture,
|
||||
|
@ -43,8 +45,82 @@ impl TextRenderer {
|
|||
}
|
||||
}
|
||||
|
||||
/// Prepares all of the provided text areas for rendering.
|
||||
pub fn prepare<'a>(
|
||||
&mut self,
|
||||
device: &Device,
|
||||
queue: &Queue,
|
||||
font_system: &mut FontSystem,
|
||||
atlas: &mut TextAtlas,
|
||||
viewport: &Viewport,
|
||||
text_areas: impl IntoIterator<Item = TextArea<'a>>,
|
||||
cache: &mut SwashCache,
|
||||
) -> Result<(), PrepareError> {
|
||||
self.prepare_with_depth_and_custom(
|
||||
device,
|
||||
queue,
|
||||
font_system,
|
||||
atlas,
|
||||
viewport,
|
||||
text_areas,
|
||||
cache,
|
||||
zero_depth,
|
||||
|_| None,
|
||||
)
|
||||
}
|
||||
|
||||
/// Prepares all of the provided text areas for rendering.
|
||||
pub fn prepare_with_depth<'a>(
|
||||
&mut self,
|
||||
device: &Device,
|
||||
queue: &Queue,
|
||||
font_system: &mut FontSystem,
|
||||
atlas: &mut TextAtlas,
|
||||
viewport: &Viewport,
|
||||
text_areas: impl IntoIterator<Item = TextArea<'a>>,
|
||||
cache: &mut SwashCache,
|
||||
metadata_to_depth: impl FnMut(usize) -> f32,
|
||||
) -> Result<(), PrepareError> {
|
||||
self.prepare_with_depth_and_custom(
|
||||
device,
|
||||
queue,
|
||||
font_system,
|
||||
atlas,
|
||||
viewport,
|
||||
text_areas,
|
||||
cache,
|
||||
metadata_to_depth,
|
||||
|_| None,
|
||||
)
|
||||
}
|
||||
|
||||
/// Prepares all of the provided text areas for rendering.
|
||||
pub fn prepare_with_custom<'a>(
|
||||
&mut self,
|
||||
device: &Device,
|
||||
queue: &Queue,
|
||||
font_system: &mut FontSystem,
|
||||
atlas: &mut TextAtlas,
|
||||
viewport: &Viewport,
|
||||
text_areas: impl IntoIterator<Item = TextArea<'a>>,
|
||||
cache: &mut SwashCache,
|
||||
rasterize_custom_glyph: impl FnMut(RasterizationRequest) -> Option<RasterizedCustomGlyph>,
|
||||
) -> Result<(), PrepareError> {
|
||||
self.prepare_with_depth_and_custom(
|
||||
device,
|
||||
queue,
|
||||
font_system,
|
||||
atlas,
|
||||
viewport,
|
||||
text_areas,
|
||||
cache,
|
||||
zero_depth,
|
||||
rasterize_custom_glyph,
|
||||
)
|
||||
}
|
||||
|
||||
/// Prepares all of the provided text areas for rendering.
|
||||
pub fn prepare_with_depth_and_custom<'a>(
|
||||
&mut self,
|
||||
device: &Device,
|
||||
queue: &Queue,
|
||||
|
@ -54,6 +130,7 @@ impl TextRenderer {
|
|||
text_areas: impl IntoIterator<Item = TextArea<'a>>,
|
||||
cache: &mut SwashCache,
|
||||
mut metadata_to_depth: impl FnMut(usize) -> f32,
|
||||
mut rasterize_custom_glyph: impl FnMut(RasterizationRequest) -> Option<RasterizedCustomGlyph>,
|
||||
) -> Result<(), PrepareError> {
|
||||
self.glyph_vertices.clear();
|
||||
|
||||
|
@ -65,6 +142,88 @@ impl TextRenderer {
|
|||
let bounds_max_x = text_area.bounds.right.min(resolution.width as i32);
|
||||
let bounds_max_y = text_area.bounds.bottom.min(resolution.height as i32);
|
||||
|
||||
for glyph in text_area.custom_glyphs.iter() {
|
||||
let x = text_area.left + (glyph.left * text_area.scale);
|
||||
let y = text_area.top + (glyph.top * text_area.scale);
|
||||
let width = (glyph.width * text_area.scale).round() as u16;
|
||||
let height = (glyph.height * text_area.scale).round() as u16;
|
||||
|
||||
let (x, y, x_bin, y_bin) = if glyph.snap_to_physical_pixel {
|
||||
(
|
||||
x.round() as i32,
|
||||
y.round() as i32,
|
||||
SubpixelBin::Zero,
|
||||
SubpixelBin::Zero,
|
||||
)
|
||||
} else {
|
||||
let (x, x_bin) = SubpixelBin::new(x);
|
||||
let (y, y_bin) = SubpixelBin::new(y);
|
||||
(x, y, x_bin, y_bin)
|
||||
};
|
||||
|
||||
let cache_key = GlyphonCacheKey::Custom(CustomGlyphCacheKey {
|
||||
glyph_id: glyph.id,
|
||||
width,
|
||||
height,
|
||||
x_bin,
|
||||
y_bin,
|
||||
});
|
||||
|
||||
let color = glyph.color.unwrap_or(text_area.default_color);
|
||||
|
||||
if let Some(glyph_to_render) = prepare_glyph(
|
||||
x,
|
||||
y,
|
||||
0.0,
|
||||
color,
|
||||
glyph.metadata,
|
||||
cache_key,
|
||||
atlas,
|
||||
device,
|
||||
queue,
|
||||
cache,
|
||||
font_system,
|
||||
text_area.scale,
|
||||
bounds_min_x,
|
||||
bounds_min_y,
|
||||
bounds_max_x,
|
||||
bounds_max_y,
|
||||
|_cache, _font_system, rasterize_custom_glyph| -> Option<GetGlyphImageResult> {
|
||||
if width == 0 || height == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let input = RasterizationRequest {
|
||||
id: glyph.id,
|
||||
width,
|
||||
height,
|
||||
x_bin,
|
||||
y_bin,
|
||||
scale: text_area.scale,
|
||||
};
|
||||
|
||||
let Some(output) = (rasterize_custom_glyph)(input) else {
|
||||
return None;
|
||||
};
|
||||
|
||||
output.validate(&input, None);
|
||||
|
||||
Some(GetGlyphImageResult {
|
||||
content_type: output.content_type,
|
||||
top: 0,
|
||||
left: 0,
|
||||
width,
|
||||
height,
|
||||
data: output.data,
|
||||
})
|
||||
},
|
||||
&mut metadata_to_depth,
|
||||
&mut rasterize_custom_glyph,
|
||||
)? {
|
||||
self.glyph_vertices.push(glyph_to_render);
|
||||
}
|
||||
}
|
||||
|
||||
let is_run_visible = |run: &cosmic_text::LayoutRun| {
|
||||
let start_y = (text_area.top + run.line_top) as i32;
|
||||
let end_y = (text_area.top + run.line_top + run.line_height) as i32;
|
||||
|
@ -83,189 +242,61 @@ impl TextRenderer {
|
|||
let physical_glyph =
|
||||
glyph.physical((text_area.left, text_area.top), text_area.scale);
|
||||
|
||||
if atlas
|
||||
.mask_atlas
|
||||
.glyph_cache
|
||||
.contains(&physical_glyph.cache_key)
|
||||
{
|
||||
atlas.mask_atlas.promote(physical_glyph.cache_key);
|
||||
} else if atlas
|
||||
.color_atlas
|
||||
.glyph_cache
|
||||
.contains(&physical_glyph.cache_key)
|
||||
{
|
||||
atlas.color_atlas.promote(physical_glyph.cache_key);
|
||||
} else {
|
||||
let Some(image) =
|
||||
cache.get_image_uncached(font_system, physical_glyph.cache_key)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let content_type = match image.content {
|
||||
SwashContent::Color => ContentType::Color,
|
||||
SwashContent::Mask => ContentType::Mask,
|
||||
SwashContent::SubpixelMask => {
|
||||
// Not implemented yet, but don't panic if this happens.
|
||||
ContentType::Mask
|
||||
}
|
||||
};
|
||||
|
||||
let width = image.placement.width as usize;
|
||||
let height = image.placement.height as usize;
|
||||
|
||||
let should_rasterize = width > 0 && height > 0;
|
||||
|
||||
let (gpu_cache, atlas_id, inner) = if should_rasterize {
|
||||
let mut inner = atlas.inner_for_content_mut(content_type);
|
||||
|
||||
// Find a position in the packer
|
||||
let allocation = loop {
|
||||
match inner.try_allocate(width, height) {
|
||||
Some(a) => break a,
|
||||
None => {
|
||||
if !atlas.grow(
|
||||
device,
|
||||
queue,
|
||||
font_system,
|
||||
cache,
|
||||
content_type,
|
||||
) {
|
||||
return Err(PrepareError::AtlasFull);
|
||||
}
|
||||
|
||||
inner = atlas.inner_for_content_mut(content_type);
|
||||
}
|
||||
}
|
||||
};
|
||||
let atlas_min = allocation.rectangle.min;
|
||||
|
||||
queue.write_texture(
|
||||
ImageCopyTexture {
|
||||
texture: &inner.texture,
|
||||
mip_level: 0,
|
||||
origin: Origin3d {
|
||||
x: atlas_min.x as u32,
|
||||
y: atlas_min.y as u32,
|
||||
z: 0,
|
||||
},
|
||||
aspect: TextureAspect::All,
|
||||
},
|
||||
&image.data,
|
||||
ImageDataLayout {
|
||||
offset: 0,
|
||||
bytes_per_row: Some(width as u32 * inner.num_channels() as u32),
|
||||
rows_per_image: None,
|
||||
},
|
||||
Extent3d {
|
||||
width: width as u32,
|
||||
height: height as u32,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
);
|
||||
|
||||
(
|
||||
GpuCacheStatus::InAtlas {
|
||||
x: atlas_min.x as u16,
|
||||
y: atlas_min.y as u16,
|
||||
content_type,
|
||||
},
|
||||
Some(allocation.id),
|
||||
inner,
|
||||
)
|
||||
} else {
|
||||
let inner = &mut atlas.color_atlas;
|
||||
(GpuCacheStatus::SkipRasterization, None, inner)
|
||||
};
|
||||
|
||||
inner.put(
|
||||
physical_glyph.cache_key,
|
||||
GlyphDetails {
|
||||
width: width as u16,
|
||||
height: height as u16,
|
||||
gpu_cache,
|
||||
atlas_id,
|
||||
top: image.placement.top as i16,
|
||||
left: image.placement.left as i16,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
let details = atlas.glyph(&physical_glyph.cache_key).unwrap();
|
||||
|
||||
let mut x = physical_glyph.x + details.left as i32;
|
||||
let mut y = (run.line_y * text_area.scale).round() as i32 + physical_glyph.y
|
||||
- details.top as i32;
|
||||
|
||||
let (mut atlas_x, mut atlas_y, content_type) = match details.gpu_cache {
|
||||
GpuCacheStatus::InAtlas { x, y, content_type } => (x, y, content_type),
|
||||
GpuCacheStatus::SkipRasterization => continue,
|
||||
};
|
||||
|
||||
let mut width = details.width as i32;
|
||||
let mut height = details.height as i32;
|
||||
|
||||
// Starts beyond right edge or ends beyond left edge
|
||||
let max_x = x + width;
|
||||
if x > bounds_max_x || max_x < bounds_min_x {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Starts beyond bottom edge or ends beyond top edge
|
||||
let max_y = y + height;
|
||||
if y > bounds_max_y || max_y < bounds_min_y {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Clip left ege
|
||||
if x < bounds_min_x {
|
||||
let right_shift = bounds_min_x - x;
|
||||
|
||||
x = bounds_min_x;
|
||||
width = max_x - bounds_min_x;
|
||||
atlas_x += right_shift as u16;
|
||||
}
|
||||
|
||||
// Clip right edge
|
||||
if x + width > bounds_max_x {
|
||||
width = bounds_max_x - x;
|
||||
}
|
||||
|
||||
// Clip top edge
|
||||
if y < bounds_min_y {
|
||||
let bottom_shift = bounds_min_y - y;
|
||||
|
||||
y = bounds_min_y;
|
||||
height = max_y - bounds_min_y;
|
||||
atlas_y += bottom_shift as u16;
|
||||
}
|
||||
|
||||
// Clip bottom edge
|
||||
if y + height > bounds_max_y {
|
||||
height = bounds_max_y - y;
|
||||
}
|
||||
|
||||
let color = match glyph.color_opt {
|
||||
Some(some) => some,
|
||||
None => text_area.default_color,
|
||||
};
|
||||
|
||||
let depth = metadata_to_depth(glyph.metadata);
|
||||
if let Some(glyph_to_render) = prepare_glyph(
|
||||
physical_glyph.x,
|
||||
physical_glyph.y,
|
||||
run.line_y,
|
||||
color,
|
||||
glyph.metadata,
|
||||
GlyphonCacheKey::Text(physical_glyph.cache_key),
|
||||
atlas,
|
||||
device,
|
||||
queue,
|
||||
cache,
|
||||
font_system,
|
||||
text_area.scale,
|
||||
bounds_min_x,
|
||||
bounds_min_y,
|
||||
bounds_max_x,
|
||||
bounds_max_y,
|
||||
|cache,
|
||||
font_system,
|
||||
_rasterize_custom_glyph|
|
||||
-> Option<GetGlyphImageResult> {
|
||||
let Some(image) =
|
||||
cache.get_image_uncached(font_system, physical_glyph.cache_key)
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
|
||||
self.glyph_vertices.push(GlyphToRender {
|
||||
pos: [x, y],
|
||||
dim: [width as u16, height as u16],
|
||||
uv: [atlas_x, atlas_y],
|
||||
color: color.0,
|
||||
content_type_with_srgb: [
|
||||
content_type as u16,
|
||||
match atlas.color_mode {
|
||||
ColorMode::Accurate => TextColorConversion::ConvertToLinear,
|
||||
ColorMode::Web => TextColorConversion::None,
|
||||
} as u16,
|
||||
],
|
||||
depth,
|
||||
});
|
||||
let content_type = match image.content {
|
||||
SwashContent::Color => ContentType::Color,
|
||||
SwashContent::Mask => ContentType::Mask,
|
||||
SwashContent::SubpixelMask => {
|
||||
// Not implemented yet, but don't panic if this happens.
|
||||
ContentType::Mask
|
||||
}
|
||||
};
|
||||
|
||||
Some(GetGlyphImageResult {
|
||||
content_type,
|
||||
top: image.placement.top as i16,
|
||||
left: image.placement.left as i16,
|
||||
width: image.placement.width as u16,
|
||||
height: image.placement.height as u16,
|
||||
data: image.data,
|
||||
})
|
||||
},
|
||||
&mut metadata_to_depth,
|
||||
&mut rasterize_custom_glyph,
|
||||
)? {
|
||||
self.glyph_vertices.push(glyph_to_render);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -302,28 +333,6 @@ impl TextRenderer {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn prepare<'a>(
|
||||
&mut self,
|
||||
device: &Device,
|
||||
queue: &Queue,
|
||||
font_system: &mut FontSystem,
|
||||
atlas: &mut TextAtlas,
|
||||
viewport: &Viewport,
|
||||
text_areas: impl IntoIterator<Item = TextArea<'a>>,
|
||||
cache: &mut SwashCache,
|
||||
) -> Result<(), PrepareError> {
|
||||
self.prepare_with_depth(
|
||||
device,
|
||||
queue,
|
||||
font_system,
|
||||
atlas,
|
||||
viewport,
|
||||
text_areas,
|
||||
cache,
|
||||
zero_depth,
|
||||
)
|
||||
}
|
||||
|
||||
/// Renders all layouts that were previously provided to `prepare`.
|
||||
pub fn render<'pass>(
|
||||
&'pass self,
|
||||
|
@ -345,13 +354,6 @@ impl TextRenderer {
|
|||
}
|
||||
}
|
||||
|
||||
#[repr(u16)]
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub enum ContentType {
|
||||
Color = 0,
|
||||
Mask = 1,
|
||||
}
|
||||
|
||||
#[repr(u16)]
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
enum TextColorConversion {
|
||||
|
@ -359,6 +361,12 @@ enum TextColorConversion {
|
|||
ConvertToLinear = 1,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub(crate) enum GlyphonCacheKey {
|
||||
Text(cosmic_text::CacheKey),
|
||||
Custom(CustomGlyphCacheKey),
|
||||
}
|
||||
|
||||
fn next_copy_buffer_size(size: u64) -> u64 {
|
||||
let align_mask = COPY_BUFFER_ALIGNMENT - 1;
|
||||
((size.next_power_of_two() + align_mask) & !align_mask).max(COPY_BUFFER_ALIGNMENT)
|
||||
|
@ -385,3 +393,199 @@ fn create_oversized_buffer(
|
|||
fn zero_depth(_: usize) -> f32 {
|
||||
0f32
|
||||
}
|
||||
|
||||
struct GetGlyphImageResult {
|
||||
content_type: ContentType,
|
||||
top: i16,
|
||||
left: i16,
|
||||
width: u16,
|
||||
height: u16,
|
||||
data: Vec<u8>,
|
||||
}
|
||||
|
||||
fn prepare_glyph<R>(
|
||||
x: i32,
|
||||
y: i32,
|
||||
line_y: f32,
|
||||
color: Color,
|
||||
metadata: usize,
|
||||
cache_key: GlyphonCacheKey,
|
||||
atlas: &mut TextAtlas,
|
||||
device: &Device,
|
||||
queue: &Queue,
|
||||
cache: &mut SwashCache,
|
||||
font_system: &mut FontSystem,
|
||||
scale_factor: f32,
|
||||
bounds_min_x: i32,
|
||||
bounds_min_y: i32,
|
||||
bounds_max_x: i32,
|
||||
bounds_max_y: i32,
|
||||
get_glyph_image: impl FnOnce(
|
||||
&mut SwashCache,
|
||||
&mut FontSystem,
|
||||
&mut R,
|
||||
) -> Option<GetGlyphImageResult>,
|
||||
mut metadata_to_depth: impl FnMut(usize) -> f32,
|
||||
mut rasterize_custom_glyph: R,
|
||||
) -> Result<Option<GlyphToRender>, PrepareError>
|
||||
where
|
||||
R: FnMut(RasterizationRequest) -> Option<RasterizedCustomGlyph>,
|
||||
{
|
||||
if atlas.mask_atlas.glyph_cache.contains(&cache_key) {
|
||||
atlas.mask_atlas.promote(cache_key);
|
||||
} else if atlas.color_atlas.glyph_cache.contains(&cache_key) {
|
||||
atlas.color_atlas.promote(cache_key);
|
||||
} else {
|
||||
let Some(image) = (get_glyph_image)(cache, font_system, &mut rasterize_custom_glyph) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let should_rasterize = image.width > 0 && image.height > 0;
|
||||
|
||||
let (gpu_cache, atlas_id, inner) = if should_rasterize {
|
||||
let mut inner = atlas.inner_for_content_mut(image.content_type);
|
||||
|
||||
// Find a position in the packer
|
||||
let allocation = loop {
|
||||
match inner.try_allocate(image.width as usize, image.height as usize) {
|
||||
Some(a) => break a,
|
||||
None => {
|
||||
if !atlas.grow(
|
||||
device,
|
||||
queue,
|
||||
font_system,
|
||||
cache,
|
||||
image.content_type,
|
||||
scale_factor,
|
||||
&mut rasterize_custom_glyph,
|
||||
) {
|
||||
return Err(PrepareError::AtlasFull);
|
||||
}
|
||||
|
||||
inner = atlas.inner_for_content_mut(image.content_type);
|
||||
}
|
||||
}
|
||||
};
|
||||
let atlas_min = allocation.rectangle.min;
|
||||
|
||||
queue.write_texture(
|
||||
ImageCopyTexture {
|
||||
texture: &inner.texture,
|
||||
mip_level: 0,
|
||||
origin: Origin3d {
|
||||
x: atlas_min.x as u32,
|
||||
y: atlas_min.y as u32,
|
||||
z: 0,
|
||||
},
|
||||
aspect: TextureAspect::All,
|
||||
},
|
||||
&image.data,
|
||||
ImageDataLayout {
|
||||
offset: 0,
|
||||
bytes_per_row: Some(image.width as u32 * inner.num_channels() as u32),
|
||||
rows_per_image: None,
|
||||
},
|
||||
Extent3d {
|
||||
width: image.width as u32,
|
||||
height: image.height as u32,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
);
|
||||
|
||||
(
|
||||
GpuCacheStatus::InAtlas {
|
||||
x: atlas_min.x as u16,
|
||||
y: atlas_min.y as u16,
|
||||
content_type: image.content_type,
|
||||
},
|
||||
Some(allocation.id),
|
||||
inner,
|
||||
)
|
||||
} else {
|
||||
let inner = &mut atlas.color_atlas;
|
||||
(GpuCacheStatus::SkipRasterization, None, inner)
|
||||
};
|
||||
|
||||
inner.put(
|
||||
cache_key,
|
||||
GlyphDetails {
|
||||
width: image.width,
|
||||
height: image.height,
|
||||
gpu_cache,
|
||||
atlas_id,
|
||||
top: image.top,
|
||||
left: image.left,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
let details = atlas.glyph(&cache_key).unwrap();
|
||||
|
||||
let mut x = x + details.left as i32;
|
||||
let mut y = (line_y * scale_factor).round() as i32 + y - details.top as i32;
|
||||
|
||||
let (mut atlas_x, mut atlas_y, content_type) = match details.gpu_cache {
|
||||
GpuCacheStatus::InAtlas { x, y, content_type } => (x, y, content_type),
|
||||
GpuCacheStatus::SkipRasterization => return Ok(None),
|
||||
};
|
||||
|
||||
let mut width = details.width as i32;
|
||||
let mut height = details.height as i32;
|
||||
|
||||
// Starts beyond right edge or ends beyond left edge
|
||||
let max_x = x + width;
|
||||
if x > bounds_max_x || max_x < bounds_min_x {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// Starts beyond bottom edge or ends beyond top edge
|
||||
let max_y = y + height;
|
||||
if y > bounds_max_y || max_y < bounds_min_y {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// Clip left ege
|
||||
if x < bounds_min_x {
|
||||
let right_shift = bounds_min_x - x;
|
||||
|
||||
x = bounds_min_x;
|
||||
width = max_x - bounds_min_x;
|
||||
atlas_x += right_shift as u16;
|
||||
}
|
||||
|
||||
// Clip right edge
|
||||
if x + width > bounds_max_x {
|
||||
width = bounds_max_x - x;
|
||||
}
|
||||
|
||||
// Clip top edge
|
||||
if y < bounds_min_y {
|
||||
let bottom_shift = bounds_min_y - y;
|
||||
|
||||
y = bounds_min_y;
|
||||
height = max_y - bounds_min_y;
|
||||
atlas_y += bottom_shift as u16;
|
||||
}
|
||||
|
||||
// Clip bottom edge
|
||||
if y + height > bounds_max_y {
|
||||
height = bounds_max_y - y;
|
||||
}
|
||||
|
||||
let depth = metadata_to_depth(metadata);
|
||||
|
||||
Ok(Some(GlyphToRender {
|
||||
pos: [x, y],
|
||||
dim: [width as u16, height as u16],
|
||||
uv: [atlas_x, atlas_y],
|
||||
color: color.0,
|
||||
content_type_with_srgb: [
|
||||
content_type as u16,
|
||||
match atlas.color_mode {
|
||||
ColorMode::Accurate => TextColorConversion::ConvertToLinear,
|
||||
ColorMode::Web => TextColorConversion::None,
|
||||
} as u16,
|
||||
],
|
||||
depth,
|
||||
}))
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue