Refactor font loading from per-document to the portfolio (#659)

* Cleanup default font loading

* Refactor fonts

* Fix menulist mouse navigation

* Format

* Formatting

* Move default font into consts.rs

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
0HyperCube
2022-05-26 16:27:33 -07:00
committed by Keavon Chambers
co-authored by Keavon Chambers
parent 7a9c3c958d
commit 469e40d4e9
60 changed files with 826 additions and 842 deletions
+3 -3
View File
@@ -9,7 +9,7 @@ use std::cell::RefCell;
use std::fmt::{self, Debug, Formatter};
use std::mem::swap;
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq)]
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)]
pub enum BooleanOperation {
Union,
Difference,
@@ -507,10 +507,10 @@ pub fn composite_boolean_operation(mut select: BooleanOperation, shapes: &mut Ve
}
BooleanOperation::SubtractFront => {
let mut result = vec![shapes[0].borrow().clone()];
for shape_idx in 1..shapes.len() {
for shape_idx in shapes.iter().skip(1) {
let mut temp = Vec::new();
for mut partial in result {
match boolean_operation(select, &mut partial, &mut shapes[shape_idx].borrow_mut()) {
match boolean_operation(select, &mut partial, &mut shape_idx.borrow_mut()) {
Ok(mut partial_result) => temp.append(&mut partial_result),
Err(BooleanOperationError::NothingDone) => temp.push(partial),
Err(err) => return Err(err),
+25 -71
View File
@@ -6,7 +6,7 @@ use crate::layers::image_layer::ImageLayer;
use crate::layers::layer_info::{Layer, LayerData, LayerDataType};
use crate::layers::shape_layer::ShapeLayer;
use crate::layers::style::ViewMode;
use crate::layers::text_layer::TextLayer;
use crate::layers::text_layer::{Font, FontCache, TextLayer};
use crate::{DocumentError, DocumentResponse, Operation};
use glam::{DAffine2, DVec2};
@@ -15,50 +15,12 @@ use serde::{Deserialize, Serialize};
use std::cell::RefCell;
use std::cmp::max;
use std::collections::hash_map::DefaultHasher;
use std::collections::HashMap;
use std::hash::{Hash, Hasher};
/// A number that identifies a layer.
/// This does not technically need to be unique globally, only within a folder.
pub type LayerId = u64;
/// A cache of all loaded fonts along with a string of the name of the default font (sent from js)
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct FontCache {
data: HashMap<String, Vec<u8>>,
default_font: Option<String>,
}
impl FontCache {
/// Returns the font family name if the font is cached, otherwise returns the default font family name if that is cached
pub fn resolve_font<'a>(&'a self, font: Option<&'a String>) -> Option<&'a String> {
font.filter(|font| self.loaded_font(font))
.map_or(self.default_font.as_ref().filter(|font| self.loaded_font(font)), Some)
}
/// Try to get the bytes for a font
pub fn get<'a>(&'a self, font: Option<&String>) -> Option<&'a Vec<u8>> {
self.resolve_font(font).and_then(|font| self.data.get(font))
}
/// Check if the font is already loaded
pub fn loaded_font(&self, font: &str) -> bool {
self.data.contains_key(font)
}
/// Insert a new font into the cache
pub fn insert(&mut self, font: String, data: Vec<u8>, is_default: bool) {
if is_default {
self.default_font = Some(font.clone());
}
self.data.insert(font, data);
}
/// Checks if the font cache has a default font
pub fn has_default(&self) -> bool {
self.default_font.is_some()
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Document {
/// The root layer, usually a [FolderLayer](layers::folder_layer::FolderLayer) that contains all other [Layers](layers::layer_info::Layer).
@@ -67,7 +29,6 @@ pub struct Document {
/// This identifier is not a hash and is not guaranteed to be equal for equivalent documents.
#[serde(skip)]
pub state_identifier: DefaultHasher,
pub font_cache: FontCache,
}
impl Default for Document {
@@ -75,17 +36,16 @@ impl Default for Document {
Self {
root: Layer::new(LayerDataType::Folder(FolderLayer::default()), DAffine2::IDENTITY.to_cols_array()),
state_identifier: DefaultHasher::new(),
font_cache: FontCache::default(),
}
}
}
impl Document {
/// Wrapper around render, that returns the whole document as a Response.
pub fn render_root(&mut self, mode: ViewMode) -> String {
pub fn render_root(&mut self, mode: ViewMode, font_cache: &FontCache) -> String {
let mut svg_defs = String::from("<defs>");
self.root.render(&mut vec![], mode, &mut svg_defs, &self.font_cache);
self.root.render(&mut vec![], mode, &mut svg_defs, font_cache);
svg_defs.push_str("</defs>");
@@ -98,14 +58,14 @@ impl Document {
}
/// Checks whether each layer under `path` intersects with the provided `quad` and adds all intersection layers as paths to `intersections`.
pub fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>) {
self.layer(path).unwrap().intersects_quad(quad, path, intersections, &self.font_cache);
pub fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>, font_cache: &FontCache) {
self.layer(path).unwrap().intersects_quad(quad, path, intersections, font_cache);
}
/// Checks whether each layer under the root path intersects with the provided `quad` and returns the paths to all intersecting layers.
pub fn intersects_quad_root(&self, quad: Quad) -> Vec<Vec<LayerId>> {
pub fn intersects_quad_root(&self, quad: Quad, font_cache: &FontCache) -> Vec<Vec<LayerId>> {
let mut intersections = Vec::new();
self.intersects_quad(quad, &mut vec![], &mut intersections);
self.intersects_quad(quad, &mut vec![], &mut intersections, font_cache);
intersections
}
@@ -359,26 +319,26 @@ impl Document {
Ok(())
}
pub fn viewport_bounding_box(&self, path: &[LayerId]) -> Result<Option<[DVec2; 2]>, DocumentError> {
pub fn viewport_bounding_box(&self, path: &[LayerId], font_cache: &FontCache) -> Result<Option<[DVec2; 2]>, DocumentError> {
let layer = self.layer(path)?;
let transform = self.multiply_transforms(path)?;
Ok(layer.data.bounding_box(transform, &self.font_cache))
Ok(layer.data.bounding_box(transform, font_cache))
}
pub fn bounding_box_and_transform(&self, path: &[LayerId]) -> Result<Option<([DVec2; 2], DAffine2)>, DocumentError> {
pub fn bounding_box_and_transform(&self, path: &[LayerId], font_cache: &FontCache) -> Result<Option<([DVec2; 2], DAffine2)>, DocumentError> {
let layer = self.layer(path)?;
let transform = self.multiply_transforms(&path[..path.len() - 1])?;
Ok(layer.data.bounding_box(layer.transform, &self.font_cache).map(|bounds| (bounds, transform)))
Ok(layer.data.bounding_box(layer.transform, font_cache).map(|bounds| (bounds, transform)))
}
pub fn visible_layers_bounding_box(&self) -> Option<[DVec2; 2]> {
pub fn visible_layers_bounding_box(&self, font_cache: &FontCache) -> Option<[DVec2; 2]> {
let mut paths = vec![];
self.visible_layers(&mut vec![], &mut paths).ok()?;
self.combined_viewport_bounding_box(paths.iter().map(|x| x.as_slice()))
self.combined_viewport_bounding_box(paths.iter().map(|x| x.as_slice()), font_cache)
}
pub fn combined_viewport_bounding_box<'a>(&self, paths: impl Iterator<Item = &'a [LayerId]>) -> Option<[DVec2; 2]> {
let boxes = paths.filter_map(|path| self.viewport_bounding_box(path).ok()?);
pub fn combined_viewport_bounding_box<'a>(&self, paths: impl Iterator<Item = &'a [LayerId]>, font_cache: &FontCache) -> Option<[DVec2; 2]> {
let boxes = paths.filter_map(|path| self.viewport_bounding_box(path, font_cache).ok()?);
boxes.reduce(|a, b| [a[0].min(b[0]), a[1].max(b[1])])
}
@@ -473,7 +433,7 @@ impl Document {
/// Mutate the document by applying the `operation` to it. If the operation necessitates a
/// reaction from the frontend, responses may be returned.
pub fn handle_operation(&mut self, operation: Operation) -> Result<Option<Vec<DocumentResponse>>, DocumentError> {
pub fn handle_operation(&mut self, operation: Operation, font_cache: &FontCache) -> Result<Option<Vec<DocumentResponse>>, DocumentError> {
use DocumentResponse::*;
operation.pseudo_hash().hash(&mut self.state_identifier);
@@ -536,9 +496,11 @@ impl Document {
size,
font_name,
font_style,
font_file,
} => {
let layer = Layer::new(LayerDataType::Text(TextLayer::new(text, style, size, font_name, font_style, font_file, &self.font_cache)), transform);
let font = Font::new(font_name, font_style);
let layer_text = TextLayer::new(text, style, size, font, font_cache);
let layer_data = LayerDataType::Text(layer_text);
let layer = Layer::new(layer_data, transform);
self.set_layer(&path, layer, insert_index)?;
@@ -575,7 +537,7 @@ impl Document {
.layer_mut(id)
.ok_or_else(|| DocumentError::LayerNotFound(path.clone()))?
.as_text_mut()?
.update_text(new_text, &self.font_cache);
.update_text(new_text, font_cache);
self.mark_as_dirty(&path)?;
@@ -723,13 +685,7 @@ impl Document {
return Err(DocumentError::IndexOutOfBounds);
}
}
Operation::ModifyFont {
path,
font_family,
font_style,
font_file,
size,
} => {
Operation::ModifyFont { path, font_family, font_style, size } => {
// Not using Document::layer_mut is necessary because we also need to borrow the font cache
let mut current_folder = &mut self.root;
let (folder_path, id) = split_path(&path)?;
@@ -739,11 +695,9 @@ impl Document {
let layer_mut = current_folder.as_folder_mut()?.layer_mut(id).ok_or_else(|| DocumentError::LayerNotFound(folder_path.into()))?;
let text = layer_mut.as_text_mut()?;
text.font_family = font_family;
text.font_style = font_style;
text.font_file = font_file;
text.font = Font::new(font_family, font_style);
text.size = size;
text.regenerate_path(text.load_face(&self.font_cache));
text.regenerate_path(text.load_face(font_cache));
self.mark_as_dirty(&path)?;
Some([vec![DocumentChanged, LayerChanged { path: path.clone() }], update_thumbnails_upstream(&path)].concat())
}
@@ -806,7 +760,7 @@ impl Document {
let layer_mut = current_folder.as_folder_mut()?.layer_mut(id).ok_or_else(|| DocumentError::LayerNotFound(folder_path.into()))?;
if let LayerDataType::Text(t) = &mut layer_mut.data {
let bezpath = t.to_bez_path(t.load_face(&self.font_cache));
let bezpath = t.to_bez_path(t.load_face(font_cache));
layer_mut.data = layers::layer_info::LayerDataType::Shape(ShapeLayer::from_bez_path(bezpath, t.path_style.clone(), true));
}
+1 -1
View File
@@ -2,7 +2,7 @@ use super::LayerId;
use crate::boolean_ops::BooleanOperationError;
/// A set of different errors that can occur when using Graphene.
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DocumentError {
LayerNotFound(Vec<LayerId>),
InvalidPath,
+1 -1
View File
@@ -3,7 +3,7 @@ use std::fmt;
/// Describes how overlapping SVG elements should be blended together.
/// See the [MDN Docs](https://developer.mozilla.org/en-US/docs/Web/CSS/blend-mode#examples) for examples.
#[derive(PartialEq, Copy, Clone, Debug, Serialize, Deserialize)]
#[derive(PartialEq, Eq, Copy, Clone, Debug, Serialize, Deserialize)]
pub enum BlendMode {
// Basic group
Normal,
+1 -1
View File
@@ -1,7 +1,7 @@
use super::layer_info::{Layer, LayerData, LayerDataType};
use super::style::ViewMode;
use crate::document::FontCache;
use crate::intersection::Quad;
use crate::layers::text_layer::FontCache;
use crate::{DocumentError, LayerId};
use glam::DVec2;
+1 -1
View File
@@ -1,7 +1,7 @@
use super::layer_info::LayerData;
use super::style::ViewMode;
use crate::document::FontCache;
use crate::intersection::{intersect_quad_bez_path, Quad};
use crate::layers::text_layer::FontCache;
use crate::LayerId;
use glam::{DAffine2, DMat2, DVec2};
+1 -1
View File
@@ -4,8 +4,8 @@ use super::image_layer::ImageLayer;
use super::shape_layer::ShapeLayer;
use super::style::{PathStyle, ViewMode};
use super::text_layer::TextLayer;
use crate::document::FontCache;
use crate::intersection::Quad;
use crate::layers::text_layer::FontCache;
use crate::DocumentError;
use crate::LayerId;
+1 -1
View File
@@ -1,7 +1,7 @@
use super::layer_info::LayerData;
use super::style::{self, PathStyle, ViewMode};
use crate::document::FontCache;
use crate::intersection::{intersect_quad_bez_path, Quad};
use crate::layers::text_layer::FontCache;
use crate::LayerId;
use glam::{DAffine2, DMat2, DVec2};
+4 -4
View File
@@ -20,7 +20,7 @@ fn format_opacity(name: &str, opacity: f32) -> String {
}
/// Represents different ways of rendering an object
#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
pub enum ViewMode {
/// Render with normal coloration at the current viewport resolution
Normal,
@@ -36,7 +36,7 @@ impl Default for ViewMode {
}
}
#[derive(PartialEq, Clone, Copy, Debug, Hash, Serialize, Deserialize)]
#[derive(PartialEq, Eq, Clone, Copy, Debug, Hash, Serialize, Deserialize)]
pub enum GradientType {
Linear,
Radial,
@@ -174,7 +174,7 @@ impl Fill {
/// The stroke (outline) style of an SVG element.
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum LineCap {
Butt,
Round,
@@ -192,7 +192,7 @@ impl Display for LineCap {
}
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum LineJoin {
Miter,
Bevel,
@@ -1,8 +1,8 @@
use super::layer_info::LayerData;
use super::style::{PathStyle, ViewMode};
use crate::document::FontCache;
use crate::intersection::{intersect_quad_bez_path, Quad};
use crate::LayerId;
pub use font_cache::{Font, FontCache};
use glam::{DAffine2, DMat2, DVec2};
use kurbo::{Affine, BezPath, Rect, Shape};
@@ -10,6 +10,7 @@ use rustybuzz::Face;
use serde::{Deserialize, Serialize};
use std::fmt::Write;
mod font_cache;
mod to_kurbo;
fn glam_to_kurbo(transform: DAffine2) -> Affine {
@@ -28,9 +29,7 @@ pub struct TextLayer {
/// Font size in pixels.
pub size: f64,
pub line_width: Option<f64>,
pub font_family: String,
pub font_style: String,
pub font_file: Option<String>,
pub font: Font,
#[serde(skip)]
pub editable: bool,
#[serde(skip)]
@@ -54,8 +53,8 @@ impl LayerData for TextLayer {
let _ = svg.write_str(r#")">"#);
if self.editable {
let font = font_cache.resolve_font(self.font_file.as_ref());
if let Some(url) = font {
let font = font_cache.resolve_font(&self.font);
if let Some(url) = font.and_then(|font| font_cache.get_preview_url(font)) {
let _ = write!(svg, r#"<style>@font-face {{font-family: local-font;src: url({});}}")</style>"#, url);
}
@@ -118,7 +117,7 @@ impl LayerData for TextLayer {
impl TextLayer {
pub fn load_face<'a>(&self, font_cache: &'a FontCache) -> Option<Face<'a>> {
font_cache.get(self.font_file.as_ref()).map(|data| rustybuzz::Face::from_slice(data, 0).expect("Loading font failed"))
font_cache.get(&self.font).map(|data| rustybuzz::Face::from_slice(data, 0).expect("Loading font failed"))
}
pub fn transform(&self, transforms: &[DAffine2], mode: ViewMode) -> DAffine2 {
@@ -129,15 +128,13 @@ impl TextLayer {
transforms.iter().skip(start).cloned().reduce(|a, b| a * b).unwrap_or(DAffine2::IDENTITY)
}
pub fn new(text: String, style: PathStyle, size: f64, font_family: String, font_style: String, font_file: Option<String>, font_cache: &FontCache) -> Self {
pub fn new(text: String, style: PathStyle, size: f64, font: Font, font_cache: &FontCache) -> Self {
let mut new = Self {
text,
path_style: style,
size,
line_width: None,
font_family,
font_style,
font_file,
font,
editable: false,
cached_path: None,
};
@@ -0,0 +1,64 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
/// A font type (storing font family and font style and an optional preview URL)
#[derive(Debug, Clone, Serialize, Deserialize, Hash, PartialEq, Eq)]
pub struct Font {
pub font_family: String,
pub font_style: String,
}
impl Font {
pub fn new(font_family: String, font_style: String) -> Self {
Self { font_family, font_style }
}
}
/// A cache of all loaded font data and preview urls along with the default font (send from `init_app` in `editor_api.rs`)
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct FontCache {
/// Actual font file data used for rendering a font with ttf_parser and rustybuzz
font_file_data: HashMap<Font, Vec<u8>>,
/// Web font preview URLs used for showing fonts when live editing
preview_urls: HashMap<Font, String>,
/// The default font (used as a fallback)
default_font: Option<Font>,
}
impl FontCache {
/// Returns the font family name if the font is cached, otherwise returns the default font family name if that is cached
pub fn resolve_font<'a>(&'a self, font: &'a Font) -> Option<&'a Font> {
if self.loaded_font(font) {
Some(font)
} else {
self.default_font.as_ref().filter(|font| self.loaded_font(font))
}
}
/// Try to get the bytes for a font
pub fn get<'a>(&'a self, font: &Font) -> Option<&'a Vec<u8>> {
self.resolve_font(font).and_then(|font| self.font_file_data.get(font))
}
/// Check if the font is already loaded
pub fn loaded_font(&self, font: &Font) -> bool {
self.font_file_data.contains_key(font)
}
/// Insert a new font into the cache
pub fn insert(&mut self, font: Font, perview_url: String, data: Vec<u8>, is_default: bool) {
if is_default {
self.default_font = Some(font.clone());
}
self.font_file_data.insert(font.clone(), data);
self.preview_urls.insert(font, perview_url);
}
/// Checks if the font cache has a default font
pub fn has_default(&self) -> bool {
self.default_font.is_some()
}
/// Gets the preview URL for showing in text field when live editing
pub fn get_preview_url(&self, font: &Font) -> Option<&String> {
self.preview_urls.get(font)
}
}
-2
View File
@@ -55,7 +55,6 @@ pub enum Operation {
size: f64,
font_name: String,
font_style: String,
font_file: Option<String>,
},
AddImage {
path: Vec<LayerId>,
@@ -126,7 +125,6 @@ pub enum Operation {
path: Vec<LayerId>,
font_family: String,
font_style: String,
font_file: Option<String>,
size: f64,
},
RenameLayer {