Implement font::load command in iced_native

This commit is contained in:
Héctor Ramón Jiménez 2023-02-04 11:12:15 +01:00
parent b29de28d1f
commit 238154af4a
No known key found for this signature in database
GPG key ID: 140CC052C94F138E
14 changed files with 384 additions and 241 deletions

View file

@ -1,5 +1,6 @@
use iced::alignment::{self, Alignment};
use iced::event::{self, Event};
use iced::font::{self, Font};
use iced::keyboard;
use iced::subscription;
use iced::theme::{self, Theme};
@ -9,7 +10,7 @@ use iced::widget::{
};
use iced::window;
use iced::{Application, Element};
use iced::{Color, Command, Font, Length, Settings, Subscription};
use iced::{Color, Command, Length, Settings, Subscription};
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
@ -44,6 +45,7 @@ struct State {
#[derive(Debug, Clone)]
enum Message {
Loaded(Result<SavedState, LoadError>),
FontLoaded(Result<(), font::Error>),
Saved(Result<(), SaveError>),
InputChanged(String),
CreateTask,
@ -61,7 +63,11 @@ impl Application for Todos {
fn new(_flags: ()) -> (Todos, Command<Message>) {
(
Todos::Loading,
Command::batch(vec![
font::load(include_bytes!("../fonts/icons.ttf").as_slice())
.map(Message::FontLoaded),
Command::perform(SavedState::load(), Message::Loaded),
]),
)
}
@ -384,7 +390,7 @@ fn view_controls(tasks: &[Task], current_filter: Filter) -> Element<Message> {
let tasks_left = tasks.iter().filter(|task| !task.completed).count();
let filter_button = |label, filter, current_filter| {
let label = text(label).size(16);
let label = text(label);
let button = button(label).style(if filter == current_filter {
theme::Button::Primary
@ -401,8 +407,7 @@ fn view_controls(tasks: &[Task], current_filter: Filter) -> Element<Message> {
tasks_left,
if tasks_left == 1 { "task" } else { "tasks" }
))
.width(Length::Fill)
.size(16),
.width(Length::Fill),
row![
filter_button("All", Filter::All, current_filter),
filter_button("Active", Filter::Active, current_filter),

View file

@ -4,6 +4,8 @@ use iced_native::svg;
use iced_native::text;
use iced_native::{Font, Point, Size};
use std::borrow::Cow;
/// The graphics backend of a [`Renderer`].
///
/// [`Renderer`]: crate::Renderer
@ -64,6 +66,9 @@ pub trait Text {
point: Point,
nearest_only: bool,
) -> Option<text::Hit>;
/// Loads a [`Font`] from its bytes.
fn load_font(&mut self, font: Cow<'static, [u8]>);
}
/// A graphics backend that supports image rendering.

View file

@ -10,6 +10,7 @@ use iced_native::{Background, Color, Element, Font, Point, Rectangle, Size};
pub use iced_native::renderer::Style;
use std::borrow::Cow;
use std::marker::PhantomData;
/// A backend-agnostic renderer that supports all the built-in widgets.
@ -167,6 +168,10 @@ where
)
}
fn load_font(&mut self, bytes: Cow<'static, [u8]>) {
self.backend.load_font(bytes);
}
fn fill_text(&mut self, text: Text<'_, Self::Font>) {
self.primitives.push(Primitive::Text {
content: text.content.to_string(),

View file

@ -1,10 +1,12 @@
use crate::clipboard;
use crate::font;
use crate::system;
use crate::widget;
use crate::window;
use iced_futures::MaybeSend;
use std::borrow::Cow;
use std::fmt;
/// An action that a [`Command`] can perform.
@ -27,6 +29,15 @@ pub enum Action<T> {
/// Run a widget action.
Widget(widget::Action<T>),
/// Load a font from its bytes.
LoadFont {
/// The bytes of the font to load.
bytes: Cow<'static, [u8]>,
/// The message to produce when the font has been loaded.
tagger: Box<dyn Fn(Result<(), font::Error>) -> T>,
},
}
impl<T> Action<T> {
@ -49,6 +60,10 @@ impl<T> Action<T> {
Self::Window(window) => Action::Window(window.map(f)),
Self::System(system) => Action::System(system.map(f)),
Self::Widget(widget) => Action::Widget(widget.map(f)),
Self::LoadFont { bytes, tagger } => Action::LoadFont {
bytes,
tagger: Box::new(move |result| f(tagger(result))),
},
}
}
}
@ -63,6 +78,7 @@ impl<T> fmt::Debug for Action<T> {
Self::Window(action) => write!(f, "Action::Window({action:?})"),
Self::System(action) => write!(f, "Action::System({action:?})"),
Self::Widget(_action) => write!(f, "Action::Widget"),
Self::LoadFont { .. } => write!(f, "Action::LoadFont"),
}
}
}

19
native/src/font.rs Normal file
View file

@ -0,0 +1,19 @@
//! Load and use fonts.
pub use iced_core::Font;
use crate::command::{self, Command};
use std::borrow::Cow;
/// An error while loading a font.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Error {}
/// Load a font from its bytes.
pub fn load(
bytes: impl Into<Cow<'static, [u8]>>,
) -> Command<Result<(), Error>> {
Command::single(command::Action::LoadFont {
bytes: bytes.into(),
tagger: Box::new(std::convert::identity),
})
}

View file

@ -47,6 +47,7 @@
pub mod clipboard;
pub mod command;
pub mod event;
pub mod font;
pub mod image;
pub mod keyboard;
pub mod layout;
@ -80,8 +81,8 @@ mod debug;
pub use iced_core::alignment;
pub use iced_core::time;
pub use iced_core::{
color, Alignment, Background, Color, ContentFit, Font, Length, Padding,
Pixels, Point, Rectangle, Size, Vector,
color, Alignment, Background, Color, ContentFit, Length, Padding, Pixels,
Point, Rectangle, Size, Vector,
};
pub use iced_futures::{executor, futures};
pub use iced_style::application;
@ -95,6 +96,7 @@ pub use command::Command;
pub use debug::Debug;
pub use element::Element;
pub use event::Event;
pub use font::Font;
pub use hasher::Hasher;
pub use layout::Layout;
pub use overlay::Overlay;

View file

@ -1,4 +1,5 @@
//! Build interactive programs using The Elm Architecture.
use crate::text;
use crate::{Command, Element, Renderer};
mod state;
@ -8,7 +9,7 @@ pub use state::State;
/// The core of a user interface application following The Elm Architecture.
pub trait Program: Sized {
/// The graphics backend to use to draw the [`Program`].
type Renderer: Renderer;
type Renderer: Renderer + text::Renderer;
/// The type of __messages__ your [`Program`] will produce.
type Message: std::fmt::Debug + Send;

View file

@ -2,6 +2,8 @@ use crate::renderer::{self, Renderer};
use crate::text::{self, Text};
use crate::{Background, Font, Point, Rectangle, Size, Theme, Vector};
use std::borrow::Cow;
/// A renderer that does nothing.
///
/// It can be useful if you are writing tests!
@ -52,6 +54,8 @@ impl text::Renderer for Null {
16.0
}
fn load_font(&mut self, _font: Cow<'static, [u8]>) {}
fn measure(
&self,
_content: &str,

View file

@ -2,6 +2,8 @@
use crate::alignment;
use crate::{Color, Point, Rectangle, Size, Vector};
use std::borrow::Cow;
/// A paragraph.
#[derive(Debug, Clone, Copy)]
pub struct Text<'a, Font> {
@ -72,7 +74,7 @@ pub trait Renderer: crate::Renderer {
/// [`ICON_FONT`]: Self::ICON_FONT
const ARROW_DOWN_ICON: char;
/// Returns the default [`Font`].
/// Returns the default [`Self::Font`].
fn default_font(&self) -> Self::Font;
/// Returns the default size of [`Text`].
@ -112,6 +114,9 @@ pub trait Renderer: crate::Renderer {
nearest_only: bool,
) -> Option<Hit>;
/// Loads a [`Self::Font`] from its bytes.
fn load_font(&mut self, font: Cow<'static, [u8]>);
/// Draws the given [`Text`].
fn fill_text(&mut self, text: Text<'_, Self::Font>);
}

View file

@ -196,6 +196,7 @@ use iced_glow as renderer;
pub use iced_native::theme;
pub use runtime::event;
pub use runtime::font;
pub use runtime::subscription;
pub use application::Application;
@ -203,6 +204,7 @@ pub use element::Element;
pub use error::Error;
pub use event::Event;
pub use executor::Executor;
pub use font::Font;
pub use renderer::Renderer;
pub use result::Result;
pub use sandbox::Sandbox;
@ -213,8 +215,8 @@ pub use theme::Theme;
pub use runtime::alignment;
pub use runtime::futures;
pub use runtime::{
color, Alignment, Background, Color, Command, ContentFit, Font, Length,
Padding, Point, Rectangle, Size, Vector,
color, Alignment, Background, Color, Command, ContentFit, Length, Padding,
Point, Rectangle, Size, Vector,
};
#[cfg(feature = "system")]

View file

@ -36,6 +36,7 @@ bitflags = "1.2"
once_cell = "1.0"
rustc-hash = "1.1"
twox-hash = "1.6"
ouroboros = "0.15"
[dependencies.bytemuck]
version = "1.9"

View file

@ -14,6 +14,8 @@ use tracing::info_span;
#[cfg(any(feature = "image", feature = "svg"))]
use crate::image;
use std::borrow::Cow;
/// A [`wgpu`] graphics backend for [`iced`].
///
/// [`wgpu`]: https://github.com/gfx-rs/wgpu-rs
@ -234,6 +236,10 @@ impl backend::Text for Backend {
nearest_only,
)
}
fn load_font(&mut self, font: Cow<'static, [u8]>) {
self.text_pipeline.load_font(font);
}
}
#[cfg(feature = "image")]

View file

@ -5,108 +5,37 @@ 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::hash::{BuildHasher, Hash, Hasher};
use std::sync::Arc;
use twox_hash::RandomXxHashBuilder64;
#[allow(missing_debug_implementations)]
pub struct Pipeline {
system: Option<System>,
renderers: Vec<glyphon::TextRenderer>,
atlas: glyphon::TextAtlas,
cache: glyphon::SwashCache<'static>,
measurement_cache: RefCell<Cache>,
render_cache: Cache,
layer: usize,
}
struct Cache {
entries: FxHashMap<KeyHash, glyphon::Buffer<'static>>,
recently_used: FxHashSet<KeyHash>,
hasher: RandomXxHashBuilder64,
#[ouroboros::self_referencing]
struct System {
fonts: glyphon::FontSystem,
#[borrows(fonts)]
#[not_covariant]
cache: glyphon::SwashCache<'this>,
#[borrows(fonts)]
#[not_covariant]
measurement_cache: RefCell<Cache<'this>>,
#[borrows(fonts)]
#[not_covariant]
render_cache: Cache<'this>,
}
impl Cache {
fn new() -> Self {
Self {
entries: FxHashMap::default(),
recently_used: FxHashSet::default(),
hasher: RandomXxHashBuilder64::default(),
}
}
fn get(&self, key: &KeyHash) -> Option<&glyphon::Buffer<'static>> {
self.entries.get(key)
}
fn allocate(
&mut self,
key: Key<'_>,
) -> (KeyHash, &mut glyphon::Buffer<'static>) {
let hash = {
let mut hasher = self.hasher.build_hasher();
key.content.hash(&mut hasher);
(key.size as i32).hash(&mut hasher);
key.font.hash(&mut hasher);
(key.bounds.width as i32).hash(&mut hasher);
(key.bounds.height as i32).hash(&mut hasher);
key.color.into_rgba8().hash(&mut hasher);
hasher.finish()
};
if !self.entries.contains_key(&hash) {
let metrics =
glyphon::Metrics::new(key.size as i32, (key.size * 1.2) as i32);
let mut buffer = glyphon::Buffer::new(&FONT_SYSTEM, metrics);
buffer.set_size(key.bounds.width as i32, key.bounds.height as i32);
buffer.set_text(
key.content,
glyphon::Attrs::new().family(to_family(key.font)).color({
let [r, g, b, a] = key.color.into_linear();
glyphon::Color::rgba(
(r * 255.0) as u8,
(g * 255.0) as u8,
(b * 255.0) as u8,
(a * 255.0) as u8,
)
}),
);
let _ = self.entries.insert(hash, buffer);
}
let _ = self.recently_used.insert(hash);
(hash, self.entries.get_mut(&hash).unwrap())
}
fn trim(&mut self) {
self.entries
.retain(|key, _| self.recently_used.contains(key));
self.recently_used.clear();
}
}
#[derive(Debug, Clone, Copy)]
struct Key<'a> {
content: &'a str,
size: f32,
font: Font,
bounds: Size,
color: Color,
}
type KeyHash = u64;
// TODO: Share with `iced_graphics`
static FONT_SYSTEM: once_cell::sync::Lazy<glyphon::FontSystem> =
once_cell::sync::Lazy::new(glyphon::FontSystem::new);
impl Pipeline {
pub fn new(
device: &wgpu::Device,
@ -114,15 +43,41 @@ impl Pipeline {
format: wgpu::TextureFormat,
) -> Self {
Pipeline {
system: Some(
SystemBuilder {
fonts: glyphon::FontSystem::new(),
cache_builder: |fonts| glyphon::SwashCache::new(fonts),
measurement_cache_builder: |_| RefCell::new(Cache::new()),
render_cache_builder: |_| Cache::new(),
}
.build(),
),
renderers: Vec::new(),
atlas: glyphon::TextAtlas::new(device, queue, format),
cache: glyphon::SwashCache::new(&FONT_SYSTEM),
measurement_cache: RefCell::new(Cache::new()),
render_cache: Cache::new(),
layer: 0,
}
}
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(glyphon::fontdb::Source::Binary(Arc::new(
bytes.to_owned(),
)));
self.system = Some(
SystemBuilder {
fonts: glyphon::FontSystem::new_with_locale_and_db(locale, db),
cache_builder: |fonts| glyphon::SwashCache::new(fonts),
measurement_cache_builder: |_| RefCell::new(Cache::new()),
render_cache_builder: |_| Cache::new(),
}
.build(),
);
}
pub fn prepare(
&mut self,
device: &wgpu::Device,
@ -132,6 +87,7 @@ impl Pipeline {
scale_factor: f32,
target_size: Size<u32>,
) {
self.system.as_mut().unwrap().with_mut(|fields| {
if self.renderers.len() <= self.layer {
self.renderers
.push(glyphon::TextRenderer::new(device, queue));
@ -142,7 +98,9 @@ impl Pipeline {
let keys: Vec<_> = sections
.iter()
.map(|section| {
let (key, _) = self.render_cache.allocate(Key {
let (key, _) = fields.render_cache.allocate(
fields.fonts,
Key {
content: section.content,
size: section.size * scale_factor,
font: section.font,
@ -151,7 +109,8 @@ impl Pipeline {
height: section.bounds.height * scale_factor,
},
color: section.color,
});
},
);
key
})
@ -168,8 +127,10 @@ impl Pipeline {
.iter()
.zip(keys.iter())
.map(|(section, key)| {
let buffer =
self.render_cache.get(key).expect("Get cached buffer");
let buffer = fields
.render_cache
.get(key)
.expect("Get cached buffer");
let x = section.bounds.x * scale_factor;
let y = section.bounds.y * scale_factor;
@ -216,9 +177,10 @@ impl Pipeline {
},
&text_areas,
glyphon::Color::rgb(0, 0, 0),
&mut self.cache,
fields.cache,
)
.expect("Prepare text sections");
});
}
pub fn render(
@ -251,7 +213,10 @@ impl Pipeline {
pub fn end_frame(&mut self) {
self.renderers.truncate(self.layer);
self.render_cache.trim();
self.system
.as_mut()
.unwrap()
.with_render_cache_mut(|cache| cache.trim());
self.layer = 0;
}
@ -263,15 +228,19 @@ impl Pipeline {
font: Font,
bounds: Size,
) -> (f32, f32) {
let mut measurement_cache = self.measurement_cache.borrow_mut();
self.system.as_ref().unwrap().with(|fields| {
let mut measurement_cache = fields.measurement_cache.borrow_mut();
let (_, paragraph) = measurement_cache.allocate(Key {
let (_, paragraph) = measurement_cache.allocate(
fields.fonts,
Key {
content,
size: size,
font,
bounds,
color: Color::BLACK,
});
},
);
let (total_lines, max_width) = paragraph
.layout_runs()
@ -281,6 +250,7 @@ impl Pipeline {
});
(max_width, size * 1.2 * total_lines as f32)
})
}
pub fn hit_test(
@ -292,23 +262,31 @@ impl Pipeline {
point: iced_native::Point,
_nearest_only: bool,
) -> Option<Hit> {
let mut measurement_cache = self.measurement_cache.borrow_mut();
self.system.as_ref().unwrap().with(|fields| {
let mut measurement_cache = fields.measurement_cache.borrow_mut();
let (_, paragraph) = measurement_cache.allocate(Key {
let (_, paragraph) = measurement_cache.allocate(
fields.fonts,
Key {
content,
size: size,
font,
bounds,
color: Color::BLACK,
});
},
);
let cursor = paragraph.hit(point.x as i32, point.y as i32)?;
Some(Hit::CharOffset(cursor.index))
})
}
pub fn trim_measurement_cache(&mut self) {
self.measurement_cache.borrow_mut().trim();
self.system
.as_mut()
.unwrap()
.with_measurement_cache_mut(|cache| cache.borrow_mut().trim());
}
}
@ -322,3 +300,87 @@ fn to_family(font: Font) -> glyphon::Family<'static> {
Font::Monospace => glyphon::Family::Monospace,
}
}
struct Cache<'a> {
entries: FxHashMap<KeyHash, glyphon::Buffer<'a>>,
recently_used: FxHashSet<KeyHash>,
hasher: RandomXxHashBuilder64,
}
impl<'a> Cache<'a> {
fn new() -> Self {
Self {
entries: FxHashMap::default(),
recently_used: FxHashSet::default(),
hasher: RandomXxHashBuilder64::default(),
}
}
fn get(&self, key: &KeyHash) -> Option<&glyphon::Buffer<'a>> {
self.entries.get(key)
}
fn allocate(
&mut self,
fonts: &'a glyphon::FontSystem,
key: Key<'_>,
) -> (KeyHash, &mut glyphon::Buffer<'a>) {
let hash = {
let mut hasher = self.hasher.build_hasher();
key.content.hash(&mut hasher);
(key.size as i32).hash(&mut hasher);
key.font.hash(&mut hasher);
(key.bounds.width as i32).hash(&mut hasher);
(key.bounds.height as i32).hash(&mut hasher);
key.color.into_rgba8().hash(&mut hasher);
hasher.finish()
};
if !self.entries.contains_key(&hash) {
let metrics =
glyphon::Metrics::new(key.size as i32, (key.size * 1.2) as i32);
let mut buffer = glyphon::Buffer::new(&fonts, metrics);
buffer.set_size(key.bounds.width as i32, key.bounds.height as i32);
buffer.set_text(
key.content,
glyphon::Attrs::new().family(to_family(key.font)).color({
let [r, g, b, a] = key.color.into_linear();
glyphon::Color::rgba(
(r * 255.0) as u8,
(g * 255.0) as u8,
(b * 255.0) as u8,
(a * 255.0) as u8,
)
}),
);
let _ = self.entries.insert(hash, buffer);
}
let _ = self.recently_used.insert(hash);
(hash, self.entries.get_mut(&hash).unwrap())
}
fn trim(&mut self) {
self.entries
.retain(|key, _| self.recently_used.contains(key));
self.recently_used.clear();
}
}
#[derive(Debug, Clone, Copy)]
struct Key<'a> {
content: &'a str,
size: f32,
font: Font,
bounds: Size,
color: Color,
}
type KeyHash = u64;

View file

@ -851,6 +851,16 @@ pub fn run_command<A, E>(
current_cache = user_interface.into_cache();
*cache = current_cache;
}
command::Action::LoadFont { bytes, tagger } => {
use crate::text::Renderer;
// TODO: Error handling (?)
renderer.load_font(bytes);
proxy
.send_event(tagger(Ok(())))
.expect("Send message to event loop");
}
}
}
}