Extract platform-dependant FontSystem
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
This commit is contained in:
co-authored by
Nathan Sobo
parent
e0e4cff815
commit
9178e91cc0
@@ -1,5 +1,5 @@
|
||||
use super::{BoolExt as _, Dispatcher, Window};
|
||||
use crate::{executor, platform, FontCache};
|
||||
use super::{BoolExt as _, Dispatcher, FontSystem, Window};
|
||||
use crate::{executor, platform};
|
||||
use anyhow::Result;
|
||||
use cocoa::base::id;
|
||||
use objc::{class, msg_send, sel, sel_impl};
|
||||
@@ -7,12 +7,14 @@ use std::{rc::Rc, sync::Arc};
|
||||
|
||||
pub struct App {
|
||||
dispatcher: Arc<Dispatcher>,
|
||||
fonts: Arc<FontSystem>,
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
dispatcher: Arc::new(Dispatcher),
|
||||
fonts: Arc::new(FontSystem::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,8 +35,11 @@ impl platform::App for App {
|
||||
&self,
|
||||
options: platform::WindowOptions,
|
||||
executor: Rc<executor::Foreground>,
|
||||
font_cache: Arc<FontCache>,
|
||||
) -> Result<Box<dyn platform::Window>> {
|
||||
Ok(Box::new(Window::open(options, executor, font_cache)?))
|
||||
Ok(Box::new(Window::open(options, executor, self.fonts())?))
|
||||
}
|
||||
|
||||
fn fonts(&self) -> Arc<dyn platform::FontSystem> {
|
||||
self.fonts.clone()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,338 @@
|
||||
use crate::{
|
||||
fonts::{FontId, GlyphId},
|
||||
geometry::{
|
||||
rect::{RectF, RectI},
|
||||
transform2d::Transform2F,
|
||||
vector::vec2f,
|
||||
},
|
||||
platform,
|
||||
text_layout::{Glyph, Line, Run},
|
||||
};
|
||||
use cocoa::appkit::{CGFloat, CGPoint};
|
||||
use core_foundation::{
|
||||
attributed_string::CFMutableAttributedString,
|
||||
base::{CFRange, TCFType},
|
||||
number::CFNumber,
|
||||
string::CFString,
|
||||
};
|
||||
use core_graphics::{
|
||||
base::CGGlyph, color_space::CGColorSpace, context::CGContext, geometry::CGAffineTransform,
|
||||
};
|
||||
use core_text::{
|
||||
font_descriptor::kCTFontSizeAttribute, line::CTLine, string_attributes::kCTFontAttributeName,
|
||||
};
|
||||
use font_kit::{
|
||||
canvas::RasterizationOptions, hinting::HintingOptions, metrics::Metrics,
|
||||
properties::Properties, source::SystemSource,
|
||||
};
|
||||
use parking_lot::RwLock;
|
||||
use std::{char, convert::TryFrom};
|
||||
|
||||
#[allow(non_upper_case_globals)]
|
||||
const kCGImageAlphaOnly: u32 = 7;
|
||||
|
||||
pub struct FontSystem(RwLock<FontSystemState>);
|
||||
|
||||
struct FontSystemState {
|
||||
source: SystemSource,
|
||||
fonts: Vec<font_kit::font::Font>,
|
||||
}
|
||||
|
||||
impl FontSystem {
|
||||
pub fn new() -> Self {
|
||||
Self(RwLock::new(FontSystemState {
|
||||
source: SystemSource::new(),
|
||||
fonts: Vec::new(),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl platform::FontSystem for FontSystem {
|
||||
fn load_family(&self, name: &str) -> anyhow::Result<Vec<FontId>> {
|
||||
self.0.write().load_family(name)
|
||||
}
|
||||
|
||||
fn select_font(&self, font_ids: &[FontId], properties: &Properties) -> anyhow::Result<FontId> {
|
||||
self.0.read().select_font(font_ids, properties)
|
||||
}
|
||||
|
||||
fn font_metrics(&self, font_id: FontId) -> Metrics {
|
||||
self.0.read().font_metrics(font_id)
|
||||
}
|
||||
|
||||
fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> anyhow::Result<RectF> {
|
||||
self.0.read().typographic_bounds(font_id, glyph_id)
|
||||
}
|
||||
|
||||
fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId> {
|
||||
self.0.read().glyph_for_char(font_id, ch)
|
||||
}
|
||||
|
||||
fn rasterize_glyph(
|
||||
&self,
|
||||
font_id: FontId,
|
||||
font_size: f32,
|
||||
glyph_id: GlyphId,
|
||||
scale_factor: f32,
|
||||
) -> Option<(RectI, Vec<u8>)> {
|
||||
self.0
|
||||
.read()
|
||||
.rasterize_glyph(font_id, font_size, glyph_id, scale_factor)
|
||||
}
|
||||
|
||||
fn layout_str(
|
||||
&self,
|
||||
text: &str,
|
||||
font_size: f32,
|
||||
runs: &[(std::ops::Range<usize>, FontId)],
|
||||
) -> Line {
|
||||
self.0.read().layout_str(text, font_size, runs)
|
||||
}
|
||||
}
|
||||
|
||||
impl FontSystemState {
|
||||
fn load_family(&mut self, name: &str) -> anyhow::Result<Vec<FontId>> {
|
||||
let mut font_ids = Vec::new();
|
||||
for font in self.source.select_family_by_name(name)?.fonts() {
|
||||
let font = font.load()?;
|
||||
font_ids.push(FontId(self.fonts.len()));
|
||||
self.fonts.push(font);
|
||||
}
|
||||
Ok(font_ids)
|
||||
}
|
||||
|
||||
fn select_font(&self, font_ids: &[FontId], properties: &Properties) -> anyhow::Result<FontId> {
|
||||
let candidates = font_ids
|
||||
.iter()
|
||||
.map(|font_id| self.fonts[font_id.0].properties())
|
||||
.collect::<Vec<_>>();
|
||||
let idx = font_kit::matching::find_best_match(&candidates, properties)?;
|
||||
Ok(font_ids[idx])
|
||||
}
|
||||
|
||||
fn font_metrics(&self, font_id: FontId) -> Metrics {
|
||||
self.fonts[font_id.0].metrics()
|
||||
}
|
||||
|
||||
fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> anyhow::Result<RectF> {
|
||||
Ok(self.fonts[font_id.0].typographic_bounds(glyph_id)?)
|
||||
}
|
||||
|
||||
fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId> {
|
||||
self.fonts[font_id.0].glyph_for_char(ch)
|
||||
}
|
||||
|
||||
fn rasterize_glyph(
|
||||
&self,
|
||||
font_id: FontId,
|
||||
font_size: f32,
|
||||
glyph_id: GlyphId,
|
||||
scale_factor: f32,
|
||||
) -> Option<(RectI, Vec<u8>)> {
|
||||
let font = &self.fonts[font_id.0];
|
||||
let scale = Transform2F::from_scale(scale_factor);
|
||||
let bounds = font
|
||||
.raster_bounds(
|
||||
glyph_id,
|
||||
font_size,
|
||||
scale,
|
||||
HintingOptions::None,
|
||||
RasterizationOptions::GrayscaleAa,
|
||||
)
|
||||
.ok()?;
|
||||
|
||||
if bounds.width() == 0 || bounds.height() == 0 {
|
||||
None
|
||||
} else {
|
||||
let mut pixels = vec![0; bounds.width() as usize * bounds.height() as usize];
|
||||
let ctx = CGContext::create_bitmap_context(
|
||||
Some(pixels.as_mut_ptr() as *mut _),
|
||||
bounds.width() as usize,
|
||||
bounds.height() as usize,
|
||||
8,
|
||||
bounds.width() as usize,
|
||||
&CGColorSpace::create_device_gray(),
|
||||
kCGImageAlphaOnly,
|
||||
);
|
||||
|
||||
// Move the origin to bottom left and account for scaling, this
|
||||
// makes drawing text consistent with the font-kit's raster_bounds.
|
||||
ctx.translate(0.0, bounds.height() as CGFloat);
|
||||
let transform = scale.translate(-bounds.origin().to_f32());
|
||||
ctx.set_text_matrix(&CGAffineTransform {
|
||||
a: transform.matrix.m11() as CGFloat,
|
||||
b: -transform.matrix.m21() as CGFloat,
|
||||
c: -transform.matrix.m12() as CGFloat,
|
||||
d: transform.matrix.m22() as CGFloat,
|
||||
tx: transform.vector.x() as CGFloat,
|
||||
ty: -transform.vector.y() as CGFloat,
|
||||
});
|
||||
|
||||
ctx.set_font(&font.native_font().copy_to_CGFont());
|
||||
ctx.set_font_size(font_size as CGFloat);
|
||||
ctx.show_glyphs_at_positions(&[glyph_id as CGGlyph], &[CGPoint::new(0.0, 0.0)]);
|
||||
|
||||
Some((bounds, pixels))
|
||||
}
|
||||
}
|
||||
|
||||
fn layout_str(
|
||||
&self,
|
||||
text: &str,
|
||||
font_size: f32,
|
||||
runs: &[(std::ops::Range<usize>, FontId)],
|
||||
) -> Line {
|
||||
let font_id_attr_name = CFString::from_static_string("zed_font_id");
|
||||
|
||||
let mut string = CFMutableAttributedString::new();
|
||||
string.replace_str(&CFString::new(text), CFRange::init(0, 0));
|
||||
|
||||
let mut utf16_lens = text.chars().map(|c| c.len_utf16());
|
||||
let mut prev_char_ix = 0;
|
||||
let mut prev_utf16_ix = 0;
|
||||
|
||||
for (range, font_id) in runs {
|
||||
let utf16_start = prev_utf16_ix
|
||||
+ utf16_lens
|
||||
.by_ref()
|
||||
.take(range.start - prev_char_ix)
|
||||
.sum::<usize>();
|
||||
let utf16_end = utf16_start
|
||||
+ utf16_lens
|
||||
.by_ref()
|
||||
.take(range.end - range.start)
|
||||
.sum::<usize>();
|
||||
prev_char_ix = range.end;
|
||||
prev_utf16_ix = utf16_end;
|
||||
|
||||
let cf_range = CFRange::init(utf16_start as isize, (utf16_end - utf16_start) as isize);
|
||||
let font = &self.fonts[font_id.0];
|
||||
unsafe {
|
||||
string.set_attribute(
|
||||
cf_range,
|
||||
kCTFontAttributeName,
|
||||
&font.native_font().clone_with_font_size(font_size as f64),
|
||||
);
|
||||
string.set_attribute(
|
||||
cf_range,
|
||||
font_id_attr_name.as_concrete_TypeRef(),
|
||||
&CFNumber::from(font_id.0 as i64),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let line = CTLine::new_with_attributed_string(string.as_concrete_TypeRef());
|
||||
|
||||
let width = line.get_typographic_bounds().width as f32;
|
||||
|
||||
let mut utf16_chars = text.encode_utf16();
|
||||
let mut char_ix = 0;
|
||||
let mut prev_utf16_ix = 0;
|
||||
|
||||
let mut runs = Vec::new();
|
||||
for run in line.glyph_runs().into_iter() {
|
||||
let font_id = FontId(
|
||||
run.attributes()
|
||||
.unwrap()
|
||||
.get(&font_id_attr_name)
|
||||
.downcast::<CFNumber>()
|
||||
.unwrap()
|
||||
.to_i64()
|
||||
.unwrap() as usize,
|
||||
);
|
||||
|
||||
let mut glyphs = Vec::new();
|
||||
for ((glyph_id, position), utf16_ix) in run
|
||||
.glyphs()
|
||||
.iter()
|
||||
.zip(run.positions().iter())
|
||||
.zip(run.string_indices().iter())
|
||||
{
|
||||
let utf16_ix = usize::try_from(*utf16_ix).unwrap();
|
||||
char_ix +=
|
||||
char::decode_utf16(utf16_chars.by_ref().take(utf16_ix - prev_utf16_ix)).count();
|
||||
prev_utf16_ix = utf16_ix;
|
||||
|
||||
glyphs.push(Glyph {
|
||||
id: *glyph_id as GlyphId,
|
||||
position: vec2f(position.x as f32, position.y as f32),
|
||||
index: char_ix,
|
||||
});
|
||||
}
|
||||
|
||||
runs.push(Run { font_id, glyphs })
|
||||
}
|
||||
|
||||
Line {
|
||||
width,
|
||||
runs,
|
||||
font_size,
|
||||
len: char_ix + 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use font_kit::properties::{Style, Weight};
|
||||
use platform::FontSystem as _;
|
||||
|
||||
#[test]
|
||||
fn test_layout_str() -> anyhow::Result<()> {
|
||||
let fonts = FontSystem::new();
|
||||
let menlo = fonts.load_family("Menlo")?;
|
||||
let menlo_regular = fonts.select_font(&menlo, &Properties::new())?;
|
||||
let menlo_italic = fonts.select_font(&menlo, &Properties::new().style(Style::Italic))?;
|
||||
let menlo_bold = fonts.select_font(&menlo, &Properties::new().weight(Weight::BOLD))?;
|
||||
|
||||
let line = fonts.layout_str(
|
||||
"hello world",
|
||||
16.0,
|
||||
&[
|
||||
(0..2, menlo_bold),
|
||||
(2..6, menlo_italic),
|
||||
(6..11, menlo_regular),
|
||||
],
|
||||
);
|
||||
assert_eq!(line.runs.len(), 3);
|
||||
assert_eq!(line.runs[0].font_id, menlo_bold);
|
||||
assert_eq!(line.runs[0].glyphs.len(), 2);
|
||||
assert_eq!(line.runs[1].font_id, menlo_italic);
|
||||
assert_eq!(line.runs[1].glyphs.len(), 4);
|
||||
assert_eq!(line.runs[2].font_id, menlo_regular);
|
||||
assert_eq!(line.runs[2].glyphs.len(), 5);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_char_indices() -> anyhow::Result<()> {
|
||||
let fonts = FontSystem::new();
|
||||
let zapfino = fonts.load_family("Zapfino")?;
|
||||
let zapfino_regular = fonts.select_font(&zapfino, &Properties::new())?;
|
||||
let menlo = fonts.load_family("Menlo")?;
|
||||
let menlo_regular = fonts.select_font(&menlo, &Properties::new())?;
|
||||
|
||||
let text = "This is, m𐍈re 𐍈r less, Zapfino!𐍈";
|
||||
let line = fonts.layout_str(
|
||||
text,
|
||||
16.0,
|
||||
&[
|
||||
(0..9, zapfino_regular),
|
||||
(9..22, menlo_regular),
|
||||
(22..text.encode_utf16().count(), zapfino_regular),
|
||||
],
|
||||
);
|
||||
assert_eq!(
|
||||
line.runs
|
||||
.iter()
|
||||
.flat_map(|r| r.glyphs.iter())
|
||||
.map(|g| g.index)
|
||||
.collect::<Vec<_>>(),
|
||||
vec![
|
||||
0, 2, 4, 5, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 30, 31
|
||||
]
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
mod app;
|
||||
mod dispatcher;
|
||||
mod event;
|
||||
mod fonts;
|
||||
mod geometry;
|
||||
mod renderer;
|
||||
mod runner;
|
||||
@@ -11,6 +12,7 @@ use crate::platform;
|
||||
pub use app::App;
|
||||
use cocoa::base::{BOOL, NO, YES};
|
||||
pub use dispatcher::Dispatcher;
|
||||
pub use fonts::FontSystem;
|
||||
pub use runner::Runner;
|
||||
use window::Window;
|
||||
|
||||
|
||||
@@ -2,8 +2,9 @@ use super::{sprite_cache::SpriteCache, window::RenderContext};
|
||||
use crate::{
|
||||
color::ColorU,
|
||||
geometry::vector::{vec2i, Vector2I},
|
||||
platform,
|
||||
scene::Layer,
|
||||
FontCache, Scene,
|
||||
Scene,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use metal::{MTLResourceOptions, NSRange};
|
||||
@@ -27,7 +28,7 @@ impl Renderer {
|
||||
pub fn new(
|
||||
device: metal::Device,
|
||||
pixel_format: metal::MTLPixelFormat,
|
||||
font_cache: Arc<FontCache>,
|
||||
fonts: Arc<dyn platform::FontSystem>,
|
||||
) -> Result<Self> {
|
||||
let library = device
|
||||
.new_library_with_data(SHADERS_METALLIB)
|
||||
@@ -53,7 +54,7 @@ impl Renderer {
|
||||
|
||||
let atlas_size: Vector2I = vec2i(1024, 768);
|
||||
Ok(Self {
|
||||
sprite_cache: SpriteCache::new(device.clone(), atlas_size, font_cache),
|
||||
sprite_cache: SpriteCache::new(device.clone(), atlas_size, fonts),
|
||||
quad_pipeline_state: build_pipeline_state(
|
||||
&device,
|
||||
&library,
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use crate::{
|
||||
fonts::{FontId, GlyphId},
|
||||
geometry::{
|
||||
rect::RectI,
|
||||
vector::{vec2i, Vector2I},
|
||||
},
|
||||
FontCache,
|
||||
platform,
|
||||
};
|
||||
use etagere::BucketedAtlasAllocator;
|
||||
use metal::{MTLPixelFormat, TextureDescriptor};
|
||||
use ordered_float::OrderedFloat;
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
#[derive(Hash, Eq, PartialEq)]
|
||||
struct GlyphDescriptor {
|
||||
@@ -30,18 +29,22 @@ pub struct GlyphSprite {
|
||||
pub struct SpriteCache {
|
||||
device: metal::Device,
|
||||
atlas_size: Vector2I,
|
||||
font_cache: Arc<FontCache>,
|
||||
fonts: Arc<dyn platform::FontSystem>,
|
||||
atlasses: Vec<Atlas>,
|
||||
glyphs: HashMap<GlyphDescriptor, Option<GlyphSprite>>,
|
||||
}
|
||||
|
||||
impl SpriteCache {
|
||||
pub fn new(device: metal::Device, size: Vector2I, font_cache: Arc<FontCache>) -> Self {
|
||||
pub fn new(
|
||||
device: metal::Device,
|
||||
size: Vector2I,
|
||||
fonts: Arc<dyn platform::FontSystem>,
|
||||
) -> Self {
|
||||
let atlasses = vec![Atlas::new(&device, size)];
|
||||
Self {
|
||||
device,
|
||||
atlas_size: size,
|
||||
font_cache,
|
||||
fonts,
|
||||
atlasses,
|
||||
glyphs: Default::default(),
|
||||
}
|
||||
@@ -58,7 +61,7 @@ impl SpriteCache {
|
||||
glyph_id: GlyphId,
|
||||
scale_factor: f32,
|
||||
) -> Option<GlyphSprite> {
|
||||
let font_cache = &self.font_cache;
|
||||
let fonts = &self.fonts;
|
||||
let atlasses = &mut self.atlasses;
|
||||
let atlas_size = self.atlas_size;
|
||||
let device = &self.device;
|
||||
@@ -70,7 +73,7 @@ impl SpriteCache {
|
||||
})
|
||||
.or_insert_with(|| {
|
||||
let (glyph_bounds, mask) =
|
||||
font_cache.render_glyph(font_id, font_size, glyph_id, scale_factor)?;
|
||||
fonts.rasterize_glyph(font_id, font_size, glyph_id, scale_factor)?;
|
||||
assert!(glyph_bounds.width() < atlas_size.x());
|
||||
assert!(glyph_bounds.height() < atlas_size.y());
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::{
|
||||
executor,
|
||||
geometry::vector::Vector2F,
|
||||
platform::{self, Event, WindowContext},
|
||||
FontCache, Scene,
|
||||
Scene,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use cocoa::{
|
||||
@@ -141,7 +141,7 @@ impl Window {
|
||||
pub fn open(
|
||||
options: platform::WindowOptions,
|
||||
executor: Rc<executor::Foreground>,
|
||||
font_cache: Arc<FontCache>,
|
||||
fonts: Arc<dyn platform::FontSystem>,
|
||||
) -> Result<Self> {
|
||||
const PIXEL_FORMAT: metal::MTLPixelFormat = metal::MTLPixelFormat::BGRA8Unorm;
|
||||
|
||||
@@ -194,7 +194,7 @@ impl Window {
|
||||
synthetic_drag_counter: 0,
|
||||
executor,
|
||||
scene_to_render: Default::default(),
|
||||
renderer: Renderer::new(device.clone(), PIXEL_FORMAT, font_cache)?,
|
||||
renderer: Renderer::new(device.clone(), PIXEL_FORMAT, fonts)?,
|
||||
command_queue: device.new_command_queue(),
|
||||
device,
|
||||
layer,
|
||||
|
||||
@@ -8,13 +8,19 @@ pub mod current {
|
||||
|
||||
use crate::{
|
||||
executor,
|
||||
geometry::{rect::RectF, vector::Vector2F},
|
||||
FontCache, Scene,
|
||||
fonts::{FontId, GlyphId},
|
||||
geometry::{
|
||||
rect::{RectF, RectI},
|
||||
vector::Vector2F,
|
||||
},
|
||||
text_layout::Line,
|
||||
Scene,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use async_task::Runnable;
|
||||
pub use event::Event;
|
||||
use std::{path::PathBuf, rc::Rc, sync::Arc};
|
||||
use font_kit::{metrics::Metrics as FontMetrics, properties::Properties as FontProperties};
|
||||
use std::{ops::Range, path::PathBuf, rc::Rc, sync::Arc};
|
||||
|
||||
pub trait Runner {
|
||||
fn on_finish_launching<F: 'static + FnOnce()>(self, callback: F) -> Self where;
|
||||
@@ -32,8 +38,8 @@ pub trait App {
|
||||
&self,
|
||||
options: WindowOptions,
|
||||
executor: Rc<executor::Foreground>,
|
||||
font_cache: Arc<FontCache>,
|
||||
) -> Result<Box<dyn Window>>;
|
||||
fn fonts(&self) -> Arc<dyn FontSystem>;
|
||||
}
|
||||
|
||||
pub trait Dispatcher: Send + Sync {
|
||||
@@ -56,3 +62,23 @@ pub struct WindowOptions<'a> {
|
||||
pub bounds: RectF,
|
||||
pub title: Option<&'a str>,
|
||||
}
|
||||
|
||||
pub trait FontSystem: Send + Sync {
|
||||
fn load_family(&self, name: &str) -> anyhow::Result<Vec<FontId>>;
|
||||
fn select_font(
|
||||
&self,
|
||||
font_ids: &[FontId],
|
||||
properties: &FontProperties,
|
||||
) -> anyhow::Result<FontId>;
|
||||
fn font_metrics(&self, font_id: FontId) -> FontMetrics;
|
||||
fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> anyhow::Result<RectF>;
|
||||
fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId>;
|
||||
fn rasterize_glyph(
|
||||
&self,
|
||||
font_id: FontId,
|
||||
font_size: f32,
|
||||
glyph_id: GlyphId,
|
||||
scale_factor: f32,
|
||||
) -> Option<(RectI, Vec<u8>)>;
|
||||
fn layout_str(&self, text: &str, font_size: f32, runs: &[(Range<usize>, FontId)]) -> Line;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user