Draft text support in iced_tiny_skia
This commit is contained in:
parent
744f3028f4
commit
64fb722dfe
4 changed files with 523 additions and 112 deletions
|
|
@ -12,6 +12,8 @@ raw-window-handle = "0.5"
|
|||
softbuffer = "0.2"
|
||||
tiny-skia = "0.8"
|
||||
bytemuck = "1"
|
||||
rustc-hash = "1.1"
|
||||
ouroboros = "0.15"
|
||||
|
||||
[dependencies.iced_native]
|
||||
version = "0.9"
|
||||
|
|
@ -20,3 +22,16 @@ path = "../native"
|
|||
[dependencies.iced_graphics]
|
||||
version = "0.7"
|
||||
path = "../graphics"
|
||||
|
||||
[dependencies.cosmic-text]
|
||||
features = ["std", "swash"]
|
||||
git = "https://github.com/hecrj/cosmic-text"
|
||||
rev = "dc83efbf00a2efb4118403538e8a47bfd69c3e5e"
|
||||
|
||||
[dependencies.twox-hash]
|
||||
version = "1.6"
|
||||
default-features = false
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies.twox-hash]
|
||||
version = "1.6.1"
|
||||
features = ["std"]
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ use std::borrow::Cow;
|
|||
pub struct Backend {
|
||||
default_font: Font,
|
||||
default_text_size: f32,
|
||||
text_pipeline: crate::text::Pipeline,
|
||||
}
|
||||
|
||||
impl Backend {
|
||||
|
|
@ -16,6 +17,7 @@ impl Backend {
|
|||
Self {
|
||||
default_font: settings.default_font,
|
||||
default_text_size: settings.default_text_size,
|
||||
text_pipeline: crate::text::Pipeline::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -32,123 +34,161 @@ impl Backend {
|
|||
let scale_factor = viewport.scale_factor() as f32;
|
||||
|
||||
for primitive in primitives {
|
||||
draw_primitive(primitive, pixels, None, scale_factor, Vector::ZERO);
|
||||
self.draw_primitive(
|
||||
primitive,
|
||||
pixels,
|
||||
None,
|
||||
scale_factor,
|
||||
Vector::ZERO,
|
||||
);
|
||||
}
|
||||
|
||||
self.text_pipeline.end_frame();
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_primitive(
|
||||
primitive: &Primitive,
|
||||
pixels: &mut tiny_skia::PixmapMut<'_>,
|
||||
clip_mask: Option<&tiny_skia::ClipMask>,
|
||||
scale_factor: f32,
|
||||
translation: Vector,
|
||||
) {
|
||||
match primitive {
|
||||
Primitive::None => {}
|
||||
Primitive::Quad {
|
||||
bounds,
|
||||
background,
|
||||
border_radius: _, // TODO
|
||||
border_width,
|
||||
border_color,
|
||||
} => {
|
||||
let transform = tiny_skia::Transform::from_translate(
|
||||
translation.x,
|
||||
translation.y,
|
||||
)
|
||||
.post_scale(scale_factor, scale_factor);
|
||||
|
||||
let path = tiny_skia::PathBuilder::from_rect(
|
||||
tiny_skia::Rect::from_xywh(
|
||||
bounds.x,
|
||||
bounds.y,
|
||||
bounds.width,
|
||||
bounds.height,
|
||||
fn draw_primitive(
|
||||
&mut self,
|
||||
primitive: &Primitive,
|
||||
pixels: &mut tiny_skia::PixmapMut<'_>,
|
||||
clip_mask: Option<&tiny_skia::ClipMask>,
|
||||
scale_factor: f32,
|
||||
translation: Vector,
|
||||
) {
|
||||
match primitive {
|
||||
Primitive::None => {}
|
||||
Primitive::Quad {
|
||||
bounds,
|
||||
background,
|
||||
border_radius: _, // TODO
|
||||
border_width,
|
||||
border_color,
|
||||
} => {
|
||||
let transform = tiny_skia::Transform::from_translate(
|
||||
translation.x,
|
||||
translation.y,
|
||||
)
|
||||
.expect("Create quad rectangle"),
|
||||
);
|
||||
.post_scale(scale_factor, scale_factor);
|
||||
|
||||
pixels.fill_path(
|
||||
&path,
|
||||
&tiny_skia::Paint {
|
||||
shader: match background {
|
||||
Background::Color(color) => {
|
||||
tiny_skia::Shader::SolidColor(into_color(*color))
|
||||
}
|
||||
},
|
||||
anti_alias: true,
|
||||
..tiny_skia::Paint::default()
|
||||
},
|
||||
tiny_skia::FillRule::EvenOdd,
|
||||
transform,
|
||||
clip_mask,
|
||||
);
|
||||
let path = tiny_skia::PathBuilder::from_rect(
|
||||
tiny_skia::Rect::from_xywh(
|
||||
bounds.x,
|
||||
bounds.y,
|
||||
bounds.width,
|
||||
bounds.height,
|
||||
)
|
||||
.expect("Create quad rectangle"),
|
||||
);
|
||||
|
||||
if *border_width > 0.0 {
|
||||
pixels.stroke_path(
|
||||
pixels.fill_path(
|
||||
&path,
|
||||
&tiny_skia::Paint {
|
||||
shader: tiny_skia::Shader::SolidColor(into_color(
|
||||
*border_color,
|
||||
)),
|
||||
shader: match background {
|
||||
Background::Color(color) => {
|
||||
tiny_skia::Shader::SolidColor(into_color(
|
||||
*color,
|
||||
))
|
||||
}
|
||||
},
|
||||
anti_alias: true,
|
||||
..tiny_skia::Paint::default()
|
||||
},
|
||||
&tiny_skia::Stroke {
|
||||
width: *border_width,
|
||||
..tiny_skia::Stroke::default()
|
||||
},
|
||||
tiny_skia::FillRule::EvenOdd,
|
||||
transform,
|
||||
clip_mask,
|
||||
);
|
||||
|
||||
if *border_width > 0.0 {
|
||||
pixels.stroke_path(
|
||||
&path,
|
||||
&tiny_skia::Paint {
|
||||
shader: tiny_skia::Shader::SolidColor(into_color(
|
||||
*border_color,
|
||||
)),
|
||||
anti_alias: true,
|
||||
..tiny_skia::Paint::default()
|
||||
},
|
||||
&tiny_skia::Stroke {
|
||||
width: *border_width,
|
||||
..tiny_skia::Stroke::default()
|
||||
},
|
||||
transform,
|
||||
clip_mask,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Primitive::Text { .. } => {
|
||||
// TODO
|
||||
}
|
||||
Primitive::Image { .. } => {
|
||||
// TODO
|
||||
}
|
||||
Primitive::Svg { .. } => {
|
||||
// TODO
|
||||
}
|
||||
Primitive::Group { primitives } => {
|
||||
for primitive in primitives {
|
||||
draw_primitive(
|
||||
primitive,
|
||||
Primitive::Text {
|
||||
content,
|
||||
bounds,
|
||||
color,
|
||||
size,
|
||||
font,
|
||||
horizontal_alignment,
|
||||
vertical_alignment,
|
||||
} => {
|
||||
self.text_pipeline.draw(
|
||||
content,
|
||||
(*bounds + translation) * scale_factor,
|
||||
*color,
|
||||
*size * scale_factor,
|
||||
*font,
|
||||
*horizontal_alignment,
|
||||
*vertical_alignment,
|
||||
pixels,
|
||||
clip_mask,
|
||||
);
|
||||
}
|
||||
Primitive::Image { .. } => {
|
||||
// TODO
|
||||
}
|
||||
Primitive::Svg { .. } => {
|
||||
// TODO
|
||||
}
|
||||
Primitive::Group { primitives } => {
|
||||
for primitive in primitives {
|
||||
self.draw_primitive(
|
||||
primitive,
|
||||
pixels,
|
||||
clip_mask,
|
||||
scale_factor,
|
||||
translation,
|
||||
);
|
||||
}
|
||||
}
|
||||
Primitive::Translate {
|
||||
translation: offset,
|
||||
content,
|
||||
} => {
|
||||
self.draw_primitive(
|
||||
content,
|
||||
pixels,
|
||||
clip_mask,
|
||||
scale_factor,
|
||||
translation + *offset,
|
||||
);
|
||||
}
|
||||
Primitive::Clip { bounds, content } => {
|
||||
self.draw_primitive(
|
||||
content,
|
||||
pixels,
|
||||
Some(&rectangular_clip_mask(
|
||||
pixels,
|
||||
*bounds * scale_factor,
|
||||
)),
|
||||
scale_factor,
|
||||
translation,
|
||||
);
|
||||
}
|
||||
Primitive::Cached { cache } => {
|
||||
self.draw_primitive(
|
||||
cache,
|
||||
pixels,
|
||||
clip_mask,
|
||||
scale_factor,
|
||||
translation,
|
||||
);
|
||||
}
|
||||
Primitive::SolidMesh { .. } | Primitive::GradientMesh { .. } => {}
|
||||
}
|
||||
Primitive::Translate {
|
||||
translation: offset,
|
||||
content,
|
||||
} => {
|
||||
draw_primitive(
|
||||
content,
|
||||
pixels,
|
||||
clip_mask,
|
||||
scale_factor,
|
||||
translation + *offset,
|
||||
);
|
||||
}
|
||||
Primitive::Clip { bounds, content } => {
|
||||
draw_primitive(
|
||||
content,
|
||||
pixels,
|
||||
Some(&rectangular_clip_mask(pixels, *bounds * scale_factor)),
|
||||
scale_factor,
|
||||
translation,
|
||||
);
|
||||
}
|
||||
Primitive::Cached { cache } => {
|
||||
draw_primitive(cache, pixels, clip_mask, scale_factor, translation);
|
||||
}
|
||||
Primitive::SolidMesh { .. } | Primitive::GradientMesh { .. } => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -185,7 +225,7 @@ fn rectangular_clip_mask(
|
|||
|
||||
impl iced_graphics::Backend for Backend {
|
||||
fn trim_measurements(&mut self) {
|
||||
// TODO
|
||||
self.text_pipeline.trim_measurement_cache();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -204,30 +244,35 @@ impl backend::Text for Backend {
|
|||
|
||||
fn measure(
|
||||
&self,
|
||||
_contents: &str,
|
||||
_size: f32,
|
||||
_font: Font,
|
||||
_bounds: Size,
|
||||
contents: &str,
|
||||
size: f32,
|
||||
font: Font,
|
||||
bounds: Size,
|
||||
) -> (f32, f32) {
|
||||
// TODO
|
||||
(0.0, 0.0)
|
||||
self.text_pipeline.measure(contents, size, font, bounds)
|
||||
}
|
||||
|
||||
fn hit_test(
|
||||
&self,
|
||||
_contents: &str,
|
||||
_size: f32,
|
||||
_font: Font,
|
||||
_bounds: Size,
|
||||
_point: iced_native::Point,
|
||||
_nearest_only: bool,
|
||||
contents: &str,
|
||||
size: f32,
|
||||
font: Font,
|
||||
bounds: Size,
|
||||
point: iced_native::Point,
|
||||
nearest_only: bool,
|
||||
) -> Option<text::Hit> {
|
||||
// TODO
|
||||
None
|
||||
self.text_pipeline.hit_test(
|
||||
contents,
|
||||
size,
|
||||
font,
|
||||
bounds,
|
||||
point,
|
||||
nearest_only,
|
||||
)
|
||||
}
|
||||
|
||||
fn load_font(&mut self, _font: Cow<'static, [u8]>) {
|
||||
// TODO
|
||||
fn load_font(&mut self, font: Cow<'static, [u8]>) {
|
||||
self.text_pipeline.load_font(font);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ pub mod window;
|
|||
|
||||
mod backend;
|
||||
mod settings;
|
||||
mod text;
|
||||
|
||||
pub use backend::Backend;
|
||||
pub use settings::Settings;
|
||||
|
|
|
|||
350
tiny_skia/src/text.rs
Normal file
350
tiny_skia/src/text.rs
Normal file
|
|
@ -0,0 +1,350 @@
|
|||
pub use iced_native::text::Hit;
|
||||
|
||||
use iced_native::alignment;
|
||||
use iced_native::{Color, Font, Rectangle, Size};
|
||||
|
||||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
use std::borrow::Cow;
|
||||
use std::cell::RefCell;
|
||||
use std::collections::hash_map;
|
||||
use std::hash::{BuildHasher, Hash, Hasher};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Pipeline {
|
||||
system: Option<System>,
|
||||
}
|
||||
|
||||
#[ouroboros::self_referencing]
|
||||
struct System {
|
||||
fonts: cosmic_text::FontSystem,
|
||||
|
||||
#[borrows(fonts)]
|
||||
#[not_covariant]
|
||||
measurement_cache: RefCell<Cache<'this>>,
|
||||
|
||||
#[borrows(fonts)]
|
||||
#[not_covariant]
|
||||
render_cache: Cache<'this>,
|
||||
}
|
||||
|
||||
impl Pipeline {
|
||||
pub fn new() -> Self {
|
||||
Pipeline {
|
||||
system: Some(
|
||||
SystemBuilder {
|
||||
fonts: cosmic_text::FontSystem::new_with_fonts(
|
||||
[cosmic_text::fontdb::Source::Binary(Arc::new(
|
||||
include_bytes!("../../wgpu/fonts/Iced-Icons.ttf")
|
||||
.as_slice(),
|
||||
))]
|
||||
.into_iter(),
|
||||
),
|
||||
measurement_cache_builder: |_| RefCell::new(Cache::new()),
|
||||
render_cache_builder: |_| Cache::new(),
|
||||
}
|
||||
.build(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_font(&mut self, bytes: Cow<'static, [u8]>) {
|
||||
let heads = self.system.take().unwrap().into_heads();
|
||||
|
||||
let (locale, mut db) = heads.fonts.into_locale_and_db();
|
||||
|
||||
db.load_font_source(cosmic_text::fontdb::Source::Binary(Arc::new(
|
||||
bytes.into_owned(),
|
||||
)));
|
||||
|
||||
self.system = Some(
|
||||
SystemBuilder {
|
||||
fonts: cosmic_text::FontSystem::new_with_locale_and_db(
|
||||
locale, db,
|
||||
),
|
||||
measurement_cache_builder: |_| RefCell::new(Cache::new()),
|
||||
render_cache_builder: |_| Cache::new(),
|
||||
}
|
||||
.build(),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn draw(
|
||||
&mut self,
|
||||
content: &str,
|
||||
bounds: Rectangle,
|
||||
color: Color,
|
||||
size: f32,
|
||||
font: Font,
|
||||
_horizontal_alignment: alignment::Horizontal, // TODO
|
||||
_vertical_alignment: alignment::Vertical, // TODO
|
||||
pixels: &mut tiny_skia::PixmapMut<'_>,
|
||||
clip_mask: Option<&tiny_skia::ClipMask>,
|
||||
) {
|
||||
self.system.as_mut().unwrap().with_mut(|fields| {
|
||||
let key = Key {
|
||||
bounds: bounds.size(),
|
||||
content,
|
||||
font,
|
||||
size,
|
||||
};
|
||||
|
||||
let (_, buffer) = fields.render_cache.allocate(&fields.fonts, key);
|
||||
|
||||
let mut swash = cosmic_text::SwashCache::new(&fields.fonts);
|
||||
|
||||
for run in buffer.layout_runs() {
|
||||
for glyph in run.glyphs {
|
||||
// TODO: Outline support
|
||||
if let Some(image) = swash.get_image(glyph.cache_key) {
|
||||
let glyph_size = image.placement.width as usize
|
||||
* image.placement.height as usize;
|
||||
|
||||
if glyph_size == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
// TODO: Cache glyph rasterization
|
||||
let mut buffer = vec![0u32; glyph_size];
|
||||
|
||||
match image.content {
|
||||
cosmic_text::SwashContent::Mask => {
|
||||
let mut i = 0;
|
||||
|
||||
// TODO: Blend alpha
|
||||
let [r, g, b, _a] = color.into_rgba8();
|
||||
|
||||
for _y in 0..image.placement.height {
|
||||
for _x in 0..image.placement.width {
|
||||
buffer[i] =
|
||||
tiny_skia::ColorU8::from_rgba(
|
||||
b,
|
||||
g,
|
||||
r,
|
||||
image.data[i],
|
||||
)
|
||||
.premultiply()
|
||||
.get();
|
||||
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
cosmic_text::SwashContent::Color => {
|
||||
let mut i = 0;
|
||||
|
||||
for _y in 0..image.placement.height {
|
||||
for _x in 0..image.placement.width {
|
||||
// TODO: Blend alpha
|
||||
buffer[i] = (image.data[i + 3] as u32)
|
||||
<< 24
|
||||
| (image.data[i + 2] as u32) << 16
|
||||
| (image.data[i + 1] as u32) << 8
|
||||
| image.data[i] as u32;
|
||||
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
cosmic_text::SwashContent::SubpixelMask => {
|
||||
// TODO
|
||||
}
|
||||
}
|
||||
|
||||
let pixmap = tiny_skia::PixmapRef::from_bytes(
|
||||
bytemuck::cast_slice(&buffer),
|
||||
image.placement.width,
|
||||
image.placement.height,
|
||||
)
|
||||
.expect("Create glyph pixel map");
|
||||
|
||||
pixels.draw_pixmap(
|
||||
bounds.x as i32
|
||||
+ glyph.x_int
|
||||
+ image.placement.left,
|
||||
bounds.y as i32 - glyph.y_int - image.placement.top
|
||||
+ run.line_y as i32,
|
||||
pixmap,
|
||||
&tiny_skia::PixmapPaint::default(),
|
||||
tiny_skia::Transform::identity(),
|
||||
clip_mask,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn end_frame(&mut self) {
|
||||
self.system
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.with_render_cache_mut(|cache| cache.trim());
|
||||
}
|
||||
|
||||
pub fn measure(
|
||||
&self,
|
||||
content: &str,
|
||||
size: f32,
|
||||
font: Font,
|
||||
bounds: Size,
|
||||
) -> (f32, f32) {
|
||||
self.system.as_ref().unwrap().with(|fields| {
|
||||
let mut measurement_cache = fields.measurement_cache.borrow_mut();
|
||||
|
||||
let (_, paragraph) = measurement_cache.allocate(
|
||||
fields.fonts,
|
||||
Key {
|
||||
content,
|
||||
size,
|
||||
font,
|
||||
bounds,
|
||||
},
|
||||
);
|
||||
|
||||
let (total_lines, max_width) = paragraph
|
||||
.layout_runs()
|
||||
.enumerate()
|
||||
.fold((0, 0.0), |(_, max), (i, buffer)| {
|
||||
(i + 1, buffer.line_w.max(max))
|
||||
});
|
||||
|
||||
(max_width, size * 1.2 * total_lines as f32)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn hit_test(
|
||||
&self,
|
||||
content: &str,
|
||||
size: f32,
|
||||
font: iced_native::Font,
|
||||
bounds: iced_native::Size,
|
||||
point: iced_native::Point,
|
||||
_nearest_only: bool,
|
||||
) -> Option<Hit> {
|
||||
self.system.as_ref().unwrap().with(|fields| {
|
||||
let mut measurement_cache = fields.measurement_cache.borrow_mut();
|
||||
|
||||
let (_, paragraph) = measurement_cache.allocate(
|
||||
fields.fonts,
|
||||
Key {
|
||||
content,
|
||||
size,
|
||||
font,
|
||||
bounds,
|
||||
},
|
||||
);
|
||||
|
||||
let cursor = paragraph.hit(point.x, point.y)?;
|
||||
|
||||
Some(Hit::CharOffset(cursor.index))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn trim_measurement_cache(&mut self) {
|
||||
self.system
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.with_measurement_cache_mut(|cache| cache.borrow_mut().trim());
|
||||
}
|
||||
}
|
||||
|
||||
fn to_family(font: Font) -> cosmic_text::Family<'static> {
|
||||
match font {
|
||||
Font::Name(name) => cosmic_text::Family::Name(name),
|
||||
Font::SansSerif => cosmic_text::Family::SansSerif,
|
||||
Font::Serif => cosmic_text::Family::Serif,
|
||||
Font::Cursive => cosmic_text::Family::Cursive,
|
||||
Font::Fantasy => cosmic_text::Family::Fantasy,
|
||||
Font::Monospace => cosmic_text::Family::Monospace,
|
||||
}
|
||||
}
|
||||
|
||||
struct Cache<'a> {
|
||||
entries: FxHashMap<KeyHash, cosmic_text::Buffer<'a>>,
|
||||
recently_used: FxHashSet<KeyHash>,
|
||||
hasher: HashBuilder,
|
||||
trim_count: usize,
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
type HashBuilder = twox_hash::RandomXxHashBuilder64;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
type HashBuilder = std::hash::BuildHasherDefault<twox_hash::XxHash64>;
|
||||
|
||||
impl<'a> Cache<'a> {
|
||||
const TRIM_INTERVAL: usize = 300;
|
||||
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
entries: FxHashMap::default(),
|
||||
recently_used: FxHashSet::default(),
|
||||
hasher: HashBuilder::default(),
|
||||
trim_count: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn allocate(
|
||||
&mut self,
|
||||
fonts: &'a cosmic_text::FontSystem,
|
||||
key: Key<'_>,
|
||||
) -> (KeyHash, &mut cosmic_text::Buffer<'a>) {
|
||||
let hash = {
|
||||
let mut hasher = self.hasher.build_hasher();
|
||||
|
||||
key.content.hash(&mut hasher);
|
||||
key.size.to_bits().hash(&mut hasher);
|
||||
key.font.hash(&mut hasher);
|
||||
key.bounds.width.to_bits().hash(&mut hasher);
|
||||
key.bounds.height.to_bits().hash(&mut hasher);
|
||||
|
||||
hasher.finish()
|
||||
};
|
||||
|
||||
if let hash_map::Entry::Vacant(entry) = self.entries.entry(hash) {
|
||||
let metrics = cosmic_text::Metrics::new(key.size, key.size * 1.2);
|
||||
let mut buffer = cosmic_text::Buffer::new(fonts, metrics);
|
||||
|
||||
buffer.set_size(
|
||||
key.bounds.width,
|
||||
key.bounds.height.max(key.size * 1.2),
|
||||
);
|
||||
buffer.set_text(
|
||||
key.content,
|
||||
cosmic_text::Attrs::new()
|
||||
.family(to_family(key.font))
|
||||
.monospaced(matches!(key.font, Font::Monospace)),
|
||||
);
|
||||
|
||||
let _ = entry.insert(buffer);
|
||||
}
|
||||
|
||||
let _ = self.recently_used.insert(hash);
|
||||
|
||||
(hash, self.entries.get_mut(&hash).unwrap())
|
||||
}
|
||||
|
||||
fn trim(&mut self) {
|
||||
if self.trim_count >= Self::TRIM_INTERVAL {
|
||||
self.entries
|
||||
.retain(|key, _| self.recently_used.contains(key));
|
||||
|
||||
self.recently_used.clear();
|
||||
|
||||
self.trim_count = 0;
|
||||
} else {
|
||||
self.trim_count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct Key<'a> {
|
||||
content: &'a str,
|
||||
size: f32,
|
||||
font: Font,
|
||||
bounds: Size,
|
||||
}
|
||||
|
||||
type KeyHash = u64;
|
||||
Loading…
Add table
Add a link
Reference in a new issue