Replace rustybuzz with HarfRust (#417)
* Use HarfRust for shaping * Replace ttf-parser with skrifa entirely * Fix clippy lints * Add shape plan cache * Bump harfrust and skrifa * Fix no_std build * Simplify the shape plan cache * Please the paperclip * Cache font ID with plan * Tune shape plan cache for "BiDi Processing" bench
This commit is contained in:
parent
3c1f6c9e8a
commit
2610c869f6
7 changed files with 221 additions and 121 deletions
12
Cargo.toml
12
Cargo.toml
|
|
@ -7,25 +7,25 @@ edition = "2021"
|
|||
license = "MIT OR Apache-2.0"
|
||||
documentation = "https://docs.rs/cosmic-text/latest/cosmic_text/"
|
||||
repository = "https://github.com/pop-os/cosmic-text"
|
||||
rust-version = "1.75"
|
||||
rust-version = "1.80"
|
||||
|
||||
[dependencies]
|
||||
bitflags = "2.4.1"
|
||||
core_maths = { version = "0.1.1", optional = true }
|
||||
cosmic_undo_2 = { version = "0.2.0", optional = true }
|
||||
fontdb = { version = "0.23", default-features = false }
|
||||
harfrust = { version = "0.2.0", default-features = false }
|
||||
hashbrown = { version = "0.14.1", optional = true, default-features = false }
|
||||
libm = { version = "0.2.8", optional = true }
|
||||
log = "0.4.20"
|
||||
modit = { version = "0.1.4", optional = true }
|
||||
rangemap = "1.4.0"
|
||||
rustc-hash = { version = "1.1.0", default-features = false }
|
||||
rustybuzz = { version = "0.14", default-features = false, features = ["libm"] }
|
||||
self_cell = "1.0.1"
|
||||
skrifa = { version = "0.36.0", default-features = false }
|
||||
smol_str = { version = "0.2.2", default-features = false }
|
||||
syntect = { version = "5.1.0", optional = true }
|
||||
sys-locale = { version = "0.3.1", optional = true }
|
||||
ttf-parser = { version = "0.21", default-features = false }
|
||||
unicode-linebreak = "0.1.5"
|
||||
unicode-script = "0.5.5"
|
||||
unicode-segmentation = "1.10.1"
|
||||
|
|
@ -49,16 +49,16 @@ optional = true
|
|||
default = ["std", "swash", "fontconfig"]
|
||||
fontconfig = ["fontdb/fontconfig", "std"]
|
||||
monospace_fallback = []
|
||||
no_std = ["rustybuzz/libm", "hashbrown", "dep:libm", "core_maths"]
|
||||
no_std = ["hashbrown", "dep:libm", "skrifa/libm", "core_maths"]
|
||||
peniko = ["dep:peniko"]
|
||||
shape-run-cache = []
|
||||
std = [
|
||||
"fontdb/memmap",
|
||||
"fontdb/std",
|
||||
"rustybuzz/std",
|
||||
"harfrust/std",
|
||||
"skrifa/std",
|
||||
"swash?/std",
|
||||
"sys-locale",
|
||||
"ttf-parser/std",
|
||||
"unicode-bidi/std",
|
||||
]
|
||||
vi = ["modit", "syntect", "cosmic_undo_2"]
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
Pure Rust multi-line text handling.
|
||||
|
||||
COSMIC Text provides advanced text shaping, layout, and rendering wrapped up
|
||||
into a simple abstraction. Shaping is provided by rustybuzz, and supports a
|
||||
into a simple abstraction. Shaping is provided by HarfRust, and supports a
|
||||
wide variety of advanced shaping operations. Rendering is provided by swash,
|
||||
which supports ligatures and color emoji. Layout is implemented custom, in safe
|
||||
Rust, and supports bidirectional text. Font fallback is also a custom
|
||||
|
|
@ -37,7 +37,7 @@ The following features must be supported before this is "ready":
|
|||
- [x] Text styles (bold, italic, etc.)
|
||||
- [x] Per-buffer
|
||||
- [x] Per-span
|
||||
- [x] Font shaping (using rustybuzz)
|
||||
- [x] Font shaping (using HarfRust)
|
||||
- [x] Cache results
|
||||
- [x] RTL
|
||||
- [x] Bidirectional rendering
|
||||
|
|
|
|||
|
|
@ -44,6 +44,28 @@ fn bench_bidi_processing(c: &mut Criterion) {
|
|||
});
|
||||
}
|
||||
|
||||
fn bench_lang_mixed(c: &mut Criterion) {
|
||||
let mut fs = ct::FontSystem::new();
|
||||
let mut buffer = ct::Buffer::new(&mut fs, ct::Metrics::new(14.0, 20.0));
|
||||
buffer.set_size(&mut fs, Some(500.0), None);
|
||||
|
||||
let bidi_text = include_str!("../sample/hello.txt");
|
||||
|
||||
c.benchmark_group("bench_lang_mixed")
|
||||
.sample_size(10)
|
||||
.bench_function("ShapeLine/Mixed-Language Text", |b| {
|
||||
b.iter(|| {
|
||||
buffer.set_text(
|
||||
&mut fs,
|
||||
black_box(&bidi_text),
|
||||
&ct::Attrs::new(),
|
||||
ct::Shaping::Advanced,
|
||||
);
|
||||
buffer.shape_until_scroll(&mut fs, false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn bench_layout_heavy(c: &mut Criterion) {
|
||||
let mut fs = ct::FontSystem::new();
|
||||
let mut buffer = ct::Buffer::new(&mut fs, ct::Metrics::new(14.0, 20.0));
|
||||
|
|
@ -117,6 +139,7 @@ criterion_group!(
|
|||
benches,
|
||||
bench_ascii_fast_path,
|
||||
bench_bidi_processing,
|
||||
bench_lang_mixed,
|
||||
bench_layout_heavy,
|
||||
bench_combined_stress,
|
||||
bench_bidi_paragraphs_ascii,
|
||||
|
|
|
|||
193
src/font/mod.rs
193
src/font/mod.rs
|
|
@ -1,7 +1,10 @@
|
|||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
// re-export ttf_parser
|
||||
pub use ttf_parser;
|
||||
use harfrust::Shaper;
|
||||
use skrifa::raw::{ReadError, TableProvider as _};
|
||||
use skrifa::{metrics::Metrics, prelude::*};
|
||||
// re-export skrifa
|
||||
pub use skrifa;
|
||||
// re-export peniko::Font;
|
||||
#[cfg(feature = "peniko")]
|
||||
pub use peniko::Font as PenikoFont;
|
||||
|
|
@ -12,7 +15,6 @@ use alloc::sync::Arc;
|
|||
#[cfg(not(feature = "std"))]
|
||||
use alloc::vec::Vec;
|
||||
|
||||
use rustybuzz::Face as RustybuzzFace;
|
||||
use self_cell::self_cell;
|
||||
|
||||
pub mod fallback;
|
||||
|
|
@ -21,12 +23,19 @@ pub use fallback::{Fallback, PlatformFallback};
|
|||
pub use self::system::*;
|
||||
mod system;
|
||||
|
||||
struct OwnedFaceData {
|
||||
data: Arc<dyn AsRef<[u8]> + Send + Sync>,
|
||||
shaper_data: harfrust::ShaperData,
|
||||
shaper_instance: harfrust::ShaperInstance,
|
||||
metrics: Metrics,
|
||||
}
|
||||
|
||||
self_cell!(
|
||||
struct OwnedFace {
|
||||
owner: Arc<dyn AsRef<[u8]> + Send + Sync>,
|
||||
owner: OwnedFaceData,
|
||||
|
||||
#[covariant]
|
||||
dependent: RustybuzzFace,
|
||||
dependent: Shaper,
|
||||
}
|
||||
);
|
||||
|
||||
|
|
@ -40,7 +49,7 @@ struct FontMonospaceFallback {
|
|||
pub struct Font {
|
||||
#[cfg(feature = "swash")]
|
||||
swash: (u32, swash::CacheKey),
|
||||
rustybuzz: OwnedFace,
|
||||
harfrust: OwnedFace,
|
||||
#[cfg(not(feature = "peniko"))]
|
||||
data: Arc<dyn AsRef<[u8]> + Send + Sync>,
|
||||
#[cfg(feature = "peniko")]
|
||||
|
|
@ -89,8 +98,16 @@ impl Font {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn rustybuzz(&self) -> &RustybuzzFace<'_> {
|
||||
self.rustybuzz.borrow_dependent()
|
||||
pub fn shaper(&self) -> &harfrust::Shaper<'_> {
|
||||
self.harfrust.borrow_dependent()
|
||||
}
|
||||
|
||||
pub(crate) fn shaper_instance(&self) -> &harfrust::ShaperInstance {
|
||||
&self.harfrust.borrow_owner().shaper_instance
|
||||
}
|
||||
|
||||
pub fn metrics(&self) -> &Metrics {
|
||||
&self.harfrust.borrow_owner().metrics
|
||||
}
|
||||
|
||||
#[cfg(feature = "peniko")]
|
||||
|
|
@ -113,59 +130,6 @@ impl Font {
|
|||
pub fn new(db: &fontdb::Database, id: fontdb::ID, weight: fontdb::Weight) -> Option<Self> {
|
||||
let info = db.face(id)?;
|
||||
|
||||
let monospace_fallback = if cfg!(feature = "monospace_fallback") {
|
||||
db.with_face_data(id, |font_data, face_index| {
|
||||
let face = ttf_parser::Face::parse(font_data, face_index).ok()?;
|
||||
let monospace_em_width = info
|
||||
.monospaced
|
||||
.then(|| {
|
||||
let hor_advance = face.glyph_hor_advance(face.glyph_index(' ')?)?;
|
||||
let upem = face.units_per_em();
|
||||
Some(f32::from(hor_advance) / f32::from(upem))
|
||||
})
|
||||
.flatten();
|
||||
|
||||
if info.monospaced && monospace_em_width.is_none() {
|
||||
None?;
|
||||
}
|
||||
|
||||
let scripts = face
|
||||
.tables()
|
||||
.gpos
|
||||
.into_iter()
|
||||
.chain(face.tables().gsub)
|
||||
.flat_map(|table| table.scripts)
|
||||
.map(|script| script.tag.to_bytes())
|
||||
.collect();
|
||||
|
||||
let mut unicode_codepoints = Vec::new();
|
||||
|
||||
face.tables()
|
||||
.cmap?
|
||||
.subtables
|
||||
.into_iter()
|
||||
.filter(ttf_parser::cmap::Subtable::is_unicode)
|
||||
.for_each(|subtable| {
|
||||
unicode_codepoints.reserve(1024);
|
||||
subtable.codepoints(|code_point| {
|
||||
if subtable.glyph_index(code_point).is_some() {
|
||||
unicode_codepoints.push(code_point);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
unicode_codepoints.shrink_to_fit();
|
||||
|
||||
Some(FontMonospaceFallback {
|
||||
monospace_em_width,
|
||||
scripts,
|
||||
unicode_codepoints,
|
||||
})
|
||||
})?
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let data = match &info.source {
|
||||
fontdb::Source::Binary(data) => Arc::clone(data),
|
||||
#[cfg(feature = "std")]
|
||||
|
|
@ -177,6 +141,77 @@ impl Font {
|
|||
fontdb::Source::SharedFile(_path, data) => Arc::clone(data),
|
||||
};
|
||||
|
||||
// It's a bit unfortunate but we need to parse the data into a `FontRef`
|
||||
// twice--once to construct the HarfRust `ShaperInstance` and
|
||||
// `ShaperData`, and once to create the persistent `FontRef` tied to the
|
||||
// lifetime of the face data.
|
||||
let font_ref = FontRef::from_index((*data).as_ref(), info.index).ok()?;
|
||||
let location = font_ref
|
||||
.axes()
|
||||
.location([(Tag::new(b"wght"), weight.0 as f32)]);
|
||||
let metrics = font_ref.metrics(Size::unscaled(), &location);
|
||||
|
||||
let monospace_fallback = if cfg!(feature = "monospace_fallback") {
|
||||
(|| {
|
||||
let glyph_metrics = font_ref.glyph_metrics(Size::unscaled(), &location);
|
||||
let charmap = font_ref.charmap();
|
||||
let monospace_em_width = info
|
||||
.monospaced
|
||||
.then(|| {
|
||||
let hor_advance = glyph_metrics.advance_width(charmap.map(' ')?)?;
|
||||
let upem = metrics.units_per_em;
|
||||
Some(hor_advance / f32::from(upem))
|
||||
})
|
||||
.flatten();
|
||||
|
||||
if info.monospaced && monospace_em_width.is_none() {
|
||||
None?;
|
||||
}
|
||||
|
||||
let scripts = font_ref
|
||||
.gpos()
|
||||
.ok()?
|
||||
.script_list()
|
||||
.ok()?
|
||||
.script_records()
|
||||
.iter()
|
||||
.chain(
|
||||
font_ref
|
||||
.gsub()
|
||||
.ok()?
|
||||
.script_list()
|
||||
.ok()?
|
||||
.script_records()
|
||||
.iter(),
|
||||
)
|
||||
.map(|script| script.script_tag().into_bytes())
|
||||
.collect();
|
||||
|
||||
let mut unicode_codepoints = Vec::new();
|
||||
|
||||
for (code_point, _) in charmap.mappings() {
|
||||
unicode_codepoints.push(code_point);
|
||||
}
|
||||
|
||||
unicode_codepoints.shrink_to_fit();
|
||||
|
||||
Some(FontMonospaceFallback {
|
||||
monospace_em_width,
|
||||
scripts,
|
||||
unicode_codepoints,
|
||||
})
|
||||
})()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let (shaper_instance, shaper_data) = {
|
||||
(
|
||||
harfrust::ShaperInstance::from_coords(&font_ref, location.coords().iter().copied()),
|
||||
harfrust::ShaperData::new(&font_ref),
|
||||
)
|
||||
};
|
||||
|
||||
Some(Self {
|
||||
id: info.id,
|
||||
monospace_fallback,
|
||||
|
|
@ -185,21 +220,27 @@ impl Font {
|
|||
let swash = swash::FontRef::from_index((*data).as_ref(), info.index as usize)?;
|
||||
(swash.offset, swash.key)
|
||||
},
|
||||
rustybuzz: OwnedFace::try_new(Arc::clone(&data), |data| {
|
||||
RustybuzzFace::from_slice((**data).as_ref(), info.index)
|
||||
.ok_or(())
|
||||
.map(|mut face| {
|
||||
if let Some(axis) = face
|
||||
.variation_axes()
|
||||
.into_iter()
|
||||
.find(|axis| axis.tag == ttf_parser::Tag::from_bytes(b"wght"))
|
||||
{
|
||||
let wght = f32::from(weight.0).clamp(axis.min_value, axis.max_value);
|
||||
let _ = face.set_variation(ttf_parser::Tag::from_bytes(b"wght"), wght);
|
||||
}
|
||||
face
|
||||
})
|
||||
})
|
||||
harfrust: OwnedFace::try_new(
|
||||
OwnedFaceData {
|
||||
data: Arc::clone(&data),
|
||||
shaper_data,
|
||||
shaper_instance,
|
||||
metrics,
|
||||
},
|
||||
|OwnedFaceData {
|
||||
data,
|
||||
shaper_data,
|
||||
shaper_instance,
|
||||
..
|
||||
}| {
|
||||
let font_ref = FontRef::from_index((**data).as_ref(), info.index)?;
|
||||
let shaper = shaper_data
|
||||
.shaper(&font_ref)
|
||||
.instance(Some(shaper_instance))
|
||||
.build();
|
||||
Ok::<_, ReadError>(shaper)
|
||||
},
|
||||
)
|
||||
.ok()?,
|
||||
#[cfg(not(feature = "peniko"))]
|
||||
data,
|
||||
|
|
|
|||
|
|
@ -7,10 +7,11 @@ use alloc::vec::Vec;
|
|||
use core::fmt;
|
||||
use core::ops::{Deref, DerefMut};
|
||||
use fontdb::Query;
|
||||
use skrifa::raw::{ReadError, TableProvider as _};
|
||||
|
||||
// re-export fontdb and rustybuzz
|
||||
// re-export fontdb and harfrust
|
||||
pub use fontdb;
|
||||
pub use rustybuzz;
|
||||
pub use harfrust;
|
||||
|
||||
use super::fallback::{Fallback, Fallbacks, MonospaceFallbackInfo, PlatformFallback};
|
||||
|
||||
|
|
@ -182,19 +183,20 @@ impl FontSystem {
|
|||
if cfg!(feature = "monospace_fallback") {
|
||||
for &id in &monospace_font_ids {
|
||||
db.with_face_data(id, |font_data, face_index| {
|
||||
let _ = ttf_parser::Face::parse(font_data, face_index).map(|face| {
|
||||
face.tables()
|
||||
.gpos
|
||||
.into_iter()
|
||||
.chain(face.tables().gsub)
|
||||
.flat_map(|table| table.scripts)
|
||||
.inspect(|script| {
|
||||
per_script_monospace_font_ids
|
||||
.entry(script.tag.to_bytes())
|
||||
.or_default()
|
||||
.insert(id);
|
||||
})
|
||||
});
|
||||
let face = skrifa::FontRef::from_index(font_data, face_index)?;
|
||||
for script in face
|
||||
.gpos()?
|
||||
.script_list()?
|
||||
.script_records()
|
||||
.iter()
|
||||
.chain(face.gsub()?.script_list()?.script_records().iter())
|
||||
{
|
||||
per_script_monospace_font_ids
|
||||
.entry(script.script_tag().into_bytes())
|
||||
.or_default()
|
||||
.insert(id);
|
||||
}
|
||||
Ok::<_, ReadError>(())
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
//!
|
||||
//! This library provides advanced text handling in a generic way. It provides abstractions for
|
||||
//! shaping, font discovery, font fallback, layout, rasterization, and editing. Shaping utilizes
|
||||
//! rustybuzz, font discovery utilizes fontdb, and the rasterization is optional and utilizes
|
||||
//! harfrust, font discovery utilizes fontdb, and the rasterization is optional and utilizes
|
||||
//! swash. The other features are developed internal to this library.
|
||||
//!
|
||||
//! It is recommended that you start by creating a [`FontSystem`], after which you can create a
|
||||
|
|
|
|||
76
src/shape.rs
76
src/shape.rs
|
|
@ -10,6 +10,7 @@ use crate::{
|
|||
#[cfg(not(feature = "std"))]
|
||||
use alloc::vec::Vec;
|
||||
|
||||
use alloc::collections::VecDeque;
|
||||
use core::cmp::{max, min};
|
||||
use core::fmt;
|
||||
use core::mem;
|
||||
|
|
@ -78,11 +79,17 @@ impl Shaping {
|
|||
}
|
||||
}
|
||||
|
||||
const NUM_SHAPE_PLANS: usize = 6;
|
||||
|
||||
/// A set of buffers containing allocations for shaped text.
|
||||
#[derive(Default)]
|
||||
pub struct ShapeBuffer {
|
||||
/// Cache for harfrust shape plans. Stores up to [`NUM_SHAPE_PLANS`] plans at once. Inserting a new one past that
|
||||
/// will remove the one that was least recently added (not least recently used).
|
||||
shape_plan_cache: VecDeque<(fontdb::ID, harfrust::ShapePlan)>,
|
||||
|
||||
/// Buffer for holding unicode text.
|
||||
rustybuzz_buffer: Option<rustybuzz::UnicodeBuffer>,
|
||||
harfrust_buffer: Option<harfrust::UnicodeBuffer>,
|
||||
|
||||
/// Temporary buffers for scripts.
|
||||
scripts: Vec<Script>,
|
||||
|
|
@ -119,15 +126,15 @@ fn shape_fallback(
|
|||
) -> Vec<usize> {
|
||||
let run = &line[start_run..end_run];
|
||||
|
||||
let font_scale = font.rustybuzz().units_per_em() as f32;
|
||||
let ascent = f32::from(font.rustybuzz().ascender()) / font_scale;
|
||||
let descent = -f32::from(font.rustybuzz().descender()) / font_scale;
|
||||
let font_scale = font.metrics().units_per_em as f32;
|
||||
let ascent = font.metrics().ascent / font_scale;
|
||||
let descent = -font.metrics().descent / font_scale;
|
||||
|
||||
let mut buffer = scratch.rustybuzz_buffer.take().unwrap_or_default();
|
||||
let mut buffer = scratch.harfrust_buffer.take().unwrap_or_default();
|
||||
buffer.set_direction(if span_rtl {
|
||||
rustybuzz::Direction::RightToLeft
|
||||
harfrust::Direction::RightToLeft
|
||||
} else {
|
||||
rustybuzz::Direction::LeftToRight
|
||||
harfrust::Direction::LeftToRight
|
||||
});
|
||||
if run.contains('\t') {
|
||||
// Push string to buffer, replacing tabs with spaces
|
||||
|
|
@ -140,29 +147,56 @@ fn shape_fallback(
|
|||
}
|
||||
buffer.guess_segment_properties();
|
||||
|
||||
let rtl = matches!(buffer.direction(), rustybuzz::Direction::RightToLeft);
|
||||
let rtl = matches!(buffer.direction(), harfrust::Direction::RightToLeft);
|
||||
assert_eq!(rtl, span_rtl);
|
||||
|
||||
let attrs = attrs_list.get_span(start_run);
|
||||
let mut rb_font_features = Vec::new();
|
||||
|
||||
// Convert attrs::Feature to rustybuzz::Feature
|
||||
for feature in attrs.font_features.features {
|
||||
rb_font_features.push(rustybuzz::Feature::new(
|
||||
rustybuzz::ttf_parser::Tag::from_bytes(feature.tag.as_bytes()),
|
||||
// Convert attrs::Feature to harfrust::Feature
|
||||
for feature in &attrs.font_features.features {
|
||||
rb_font_features.push(harfrust::Feature::new(
|
||||
harfrust::Tag::new(feature.tag.as_bytes()),
|
||||
feature.value,
|
||||
0..usize::MAX,
|
||||
));
|
||||
}
|
||||
|
||||
let shape_plan = rustybuzz::ShapePlan::new(
|
||||
font.rustybuzz(),
|
||||
buffer.direction(),
|
||||
Some(buffer.script()),
|
||||
buffer.language().as_ref(),
|
||||
&rb_font_features,
|
||||
);
|
||||
let glyph_buffer = rustybuzz::shape_with_plan(font.rustybuzz(), &shape_plan, buffer);
|
||||
let language = buffer.language();
|
||||
let key = harfrust::ShapePlanKey::new(Some(buffer.script()), buffer.direction())
|
||||
.features(&rb_font_features)
|
||||
.instance(Some(font.shaper_instance()))
|
||||
.language(language.as_ref());
|
||||
|
||||
let shape_plan = match scratch
|
||||
.shape_plan_cache
|
||||
.iter()
|
||||
.find(|(id, plan)| *id == font.id() && key.matches(plan))
|
||||
{
|
||||
Some((_font_id, plan)) => plan,
|
||||
None => {
|
||||
let plan = harfrust::ShapePlan::new(
|
||||
font.shaper(),
|
||||
buffer.direction(),
|
||||
Some(buffer.script()),
|
||||
buffer.language().as_ref(),
|
||||
&rb_font_features,
|
||||
);
|
||||
if scratch.shape_plan_cache.len() >= NUM_SHAPE_PLANS {
|
||||
scratch.shape_plan_cache.pop_front();
|
||||
}
|
||||
scratch.shape_plan_cache.push_back((font.id(), plan));
|
||||
&scratch
|
||||
.shape_plan_cache
|
||||
.back()
|
||||
.expect("we just pushed the shape plan")
|
||||
.1
|
||||
}
|
||||
};
|
||||
|
||||
let glyph_buffer = font
|
||||
.shaper()
|
||||
.shape_with_plan(shape_plan, buffer, &rb_font_features);
|
||||
let glyph_infos = glyph_buffer.glyph_infos();
|
||||
let glyph_positions = glyph_buffer.glyph_positions();
|
||||
|
||||
|
|
@ -230,7 +264,7 @@ fn shape_fallback(
|
|||
}
|
||||
|
||||
// Restore the buffer to save an allocation.
|
||||
scratch.rustybuzz_buffer = Some(glyph_buffer.clear());
|
||||
scratch.harfrust_buffer = Some(glyph_buffer.clear());
|
||||
|
||||
missing
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue