mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-22 01:18:11 +08:00
Migrate text layers to nodes (#1155)
* Initial work towards text to node * Add the text generate node * Implement live edit * Fix merge error * Cleanup text tool * Implement text * Fix transforms * Fix broken image frame * Double click to edit text * Fix rendering text on load * Moving whilst editing * Better text properties * Prevent changing vector when there is a Text node * Push node api * Use node fn macro * Stable ids * Image module as a seperate file * Explain check for "Input Frame" node * Code review --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
committed by
Keavon Chambers
co-authored by
Keavon Chambers
parent
271f9d5158
commit
ef93f8442a
@@ -5,7 +5,6 @@ use crate::layers::layer_info::{Layer, LayerData, LayerDataType, LayerDataTypeDi
|
||||
use crate::layers::nodegraph_layer::{CachedOutputData, NodeGraphFrameLayer};
|
||||
use crate::layers::shape_layer::ShapeLayer;
|
||||
use crate::layers::style::RenderData;
|
||||
use crate::layers::text_layer::{Font, TextLayer};
|
||||
use crate::{DocumentError, DocumentResponse, Operation};
|
||||
|
||||
use glam::{DAffine2, DVec2};
|
||||
@@ -430,30 +429,6 @@ impl Document {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Marks all decendants of the specified [Layer] of a specific [LayerDataType] as dirty
|
||||
fn mark_layers_of_type_as_dirty(root: &mut Layer, data_type: LayerDataTypeDiscriminant) -> bool {
|
||||
if let LayerDataType::Folder(folder) = &mut root.data {
|
||||
let mut dirty = false;
|
||||
for layer in folder.layers_mut() {
|
||||
dirty = Self::mark_layers_of_type_as_dirty(layer, data_type) || dirty;
|
||||
}
|
||||
root.cache_dirty = dirty;
|
||||
}
|
||||
if LayerDataTypeDiscriminant::from(&root.data) == data_type {
|
||||
root.cache_dirty = true;
|
||||
if let LayerDataType::Text(text) = &mut root.data {
|
||||
text.cached_path = None;
|
||||
}
|
||||
}
|
||||
|
||||
root.cache_dirty
|
||||
}
|
||||
|
||||
/// Marks all layers in the [Document] of a specific [LayerDataType] as dirty
|
||||
pub fn mark_all_layers_of_type_as_dirty(&mut self, data_type: LayerDataTypeDiscriminant) -> bool {
|
||||
Self::mark_layers_of_type_as_dirty(&mut self.root, data_type)
|
||||
}
|
||||
|
||||
pub fn transforms(&self, path: &[LayerId]) -> Result<Vec<DAffine2>, DocumentError> {
|
||||
let mut root = &self.root;
|
||||
let mut transforms = vec![self.root.transform];
|
||||
@@ -539,25 +514,6 @@ impl Document {
|
||||
|
||||
Some([vec![DocumentChanged, CreatedLayer { path: path.clone() }], update_thumbnails_upstream(&path)].concat())
|
||||
}
|
||||
Operation::AddText {
|
||||
path,
|
||||
insert_index,
|
||||
transform,
|
||||
text,
|
||||
style,
|
||||
size,
|
||||
font_name,
|
||||
font_style,
|
||||
} => {
|
||||
let font = Font::new(font_name, font_style);
|
||||
let layer_text = TextLayer::new(text, style, size, font, render_data);
|
||||
let layer_data = LayerDataType::Text(layer_text);
|
||||
let layer = Layer::new(layer_data, transform);
|
||||
|
||||
self.set_layer(&path, layer, insert_index)?;
|
||||
|
||||
Some([vec![DocumentChanged, CreatedLayer { path: path.clone() }], update_thumbnails_upstream(&path)].concat())
|
||||
}
|
||||
Operation::AddNodeGraphFrame {
|
||||
path,
|
||||
insert_index,
|
||||
@@ -576,30 +532,6 @@ impl Document {
|
||||
}
|
||||
Some(vec![LayerChanged { path: layer_path.clone() }])
|
||||
}
|
||||
Operation::SetTextEditability { path, editable } => {
|
||||
self.layer_mut(&path)?.as_text_mut()?.editable = editable;
|
||||
self.mark_as_dirty(&path)?;
|
||||
Some(vec![DocumentChanged])
|
||||
}
|
||||
Operation::SetTextContent { path, new_text } => {
|
||||
// Not using Document::layer_mut is necessary because we also need to borrow the font cache
|
||||
let mut current_folder = &mut self.root;
|
||||
|
||||
let (layer_path, id) = split_path(&path)?;
|
||||
for id in layer_path {
|
||||
current_folder = current_folder.as_folder_mut()?.layer_mut(*id).ok_or_else(|| DocumentError::LayerNotFound(layer_path.into()))?;
|
||||
}
|
||||
current_folder
|
||||
.as_folder_mut()?
|
||||
.layer_mut(id)
|
||||
.ok_or_else(|| DocumentError::LayerNotFound(path.clone()))?
|
||||
.as_text_mut()?
|
||||
.update_text(new_text, render_data);
|
||||
|
||||
self.mark_as_dirty(&path)?;
|
||||
|
||||
Some([vec![DocumentChanged], update_thumbnails_upstream(&path)].concat())
|
||||
}
|
||||
Operation::AddNgon {
|
||||
path,
|
||||
insert_index,
|
||||
@@ -736,22 +668,6 @@ impl Document {
|
||||
return Err(DocumentError::IndexOutOfBounds);
|
||||
}
|
||||
}
|
||||
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)?;
|
||||
for id in folder_path {
|
||||
current_folder = current_folder.as_folder_mut()?.layer_mut(*id).ok_or_else(|| DocumentError::LayerNotFound(folder_path.into()))?;
|
||||
}
|
||||
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 = Font::new(font_family, font_style);
|
||||
text.size = size;
|
||||
text.cached_path = Some(text.generate_path(text.load_face(render_data)));
|
||||
self.mark_as_dirty(&path)?;
|
||||
Some([vec![DocumentChanged, LayerChanged { path: path.clone() }], update_thumbnails_upstream(&path)].concat())
|
||||
}
|
||||
Operation::RenameLayer { layer_path: path, new_name: name } => {
|
||||
self.layer_mut(&path)?.name = Some(name);
|
||||
Some(vec![LayerChanged { path }])
|
||||
@@ -791,7 +707,9 @@ impl Document {
|
||||
let layer = self.layer_mut(&path).expect("Clearing node graph image for invalid layer");
|
||||
match &mut layer.data {
|
||||
LayerDataType::NodeGraphFrame(node_graph) => {
|
||||
node_graph.cached_output_data = CachedOutputData::None;
|
||||
if matches!(node_graph.cached_output_data, CachedOutputData::BlobURL(_)) {
|
||||
node_graph.cached_output_data = CachedOutputData::None;
|
||||
}
|
||||
}
|
||||
e => panic!("Incorrectly trying to clear the blob URL for layer of type {}", LayerDataTypeDiscriminant::from(&*e)),
|
||||
}
|
||||
@@ -984,7 +902,6 @@ impl Document {
|
||||
let layer = self.layer_mut(&path)?;
|
||||
match &mut layer.data {
|
||||
LayerDataType::Shape(s) => s.style = style,
|
||||
LayerDataType::Text(text) => text.path_style = style,
|
||||
_ => return Err(DocumentError::NotShape),
|
||||
}
|
||||
self.mark_as_dirty(&path)?;
|
||||
|
||||
@@ -3,7 +3,6 @@ use super::folder_layer::FolderLayer;
|
||||
use super::nodegraph_layer::NodeGraphFrameLayer;
|
||||
use super::shape_layer::ShapeLayer;
|
||||
use super::style::{PathStyle, RenderData};
|
||||
use super::text_layer::TextLayer;
|
||||
use crate::intersection::Quad;
|
||||
use crate::DocumentError;
|
||||
use crate::LayerId;
|
||||
@@ -23,8 +22,6 @@ pub enum LayerDataType {
|
||||
Folder(FolderLayer),
|
||||
/// A layer that wraps a [ShapeLayer] struct.
|
||||
Shape(ShapeLayer),
|
||||
/// A layer that wraps a [TextLayer] struct.
|
||||
Text(TextLayer),
|
||||
/// A layer that wraps an [NodeGraphFrameLayer] struct.
|
||||
NodeGraphFrame(NodeGraphFrameLayer),
|
||||
}
|
||||
@@ -34,7 +31,6 @@ impl LayerDataType {
|
||||
match self {
|
||||
LayerDataType::Shape(s) => s,
|
||||
LayerDataType::Folder(f) => f,
|
||||
LayerDataType::Text(t) => t,
|
||||
LayerDataType::NodeGraphFrame(n) => n,
|
||||
}
|
||||
}
|
||||
@@ -43,7 +39,6 @@ impl LayerDataType {
|
||||
match self {
|
||||
LayerDataType::Shape(s) => s,
|
||||
LayerDataType::Folder(f) => f,
|
||||
LayerDataType::Text(t) => t,
|
||||
LayerDataType::NodeGraphFrame(n) => n,
|
||||
}
|
||||
}
|
||||
@@ -75,7 +70,6 @@ impl From<&LayerDataType> for LayerDataTypeDiscriminant {
|
||||
match data {
|
||||
Folder(_) => LayerDataTypeDiscriminant::Folder,
|
||||
Shape(_) => LayerDataTypeDiscriminant::Shape,
|
||||
Text(_) => LayerDataTypeDiscriminant::Text,
|
||||
NodeGraphFrame(_) => LayerDataTypeDiscriminant::NodeGraphFrame,
|
||||
}
|
||||
}
|
||||
@@ -459,24 +453,6 @@ impl Layer {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a mutable reference to the Text element wrapped by the layer.
|
||||
/// This operation will fail if the [Layer type](Layer::data) is not `LayerDataType::Text`.
|
||||
pub fn as_text_mut(&mut self) -> Result<&mut TextLayer, DocumentError> {
|
||||
match &mut self.data {
|
||||
LayerDataType::Text(t) => Ok(t),
|
||||
_ => Err(DocumentError::NotText),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a reference to the Text element wrapped by the layer.
|
||||
/// This operation will fail if the [Layer type](Layer::data) is not `LayerDataType::Text`.
|
||||
pub fn as_text(&self) -> Result<&TextLayer, DocumentError> {
|
||||
match &self.data {
|
||||
LayerDataType::Text(t) => Ok(t),
|
||||
_ => Err(DocumentError::NotText),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a mutable reference to the NodeNetwork
|
||||
/// This operation will fail if the [Layer type](Layer::data) is not `LayerDataType::NodeGraphFrame`.
|
||||
pub fn as_node_graph_mut(&mut self) -> Result<&mut graph_craft::document::NodeNetwork, DocumentError> {
|
||||
@@ -505,7 +481,6 @@ impl Layer {
|
||||
pub fn style(&self) -> Result<&PathStyle, DocumentError> {
|
||||
match &self.data {
|
||||
LayerDataType::Shape(s) => Ok(&s.style),
|
||||
LayerDataType::Text(t) => Ok(&t.path_style),
|
||||
LayerDataType::NodeGraphFrame(t) => t.as_vector_data().map(|vector| &vector.style).ok_or(DocumentError::NotShape),
|
||||
_ => Err(DocumentError::NotShape),
|
||||
}
|
||||
@@ -514,7 +489,6 @@ impl Layer {
|
||||
pub fn style_mut(&mut self) -> Result<&mut PathStyle, DocumentError> {
|
||||
match &mut self.data {
|
||||
LayerDataType::Shape(s) => Ok(&mut s.style),
|
||||
LayerDataType::Text(t) => Ok(&mut t.path_style),
|
||||
_ => Err(DocumentError::NotShape),
|
||||
}
|
||||
}
|
||||
@@ -551,12 +525,6 @@ impl From<ShapeLayer> for Layer {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TextLayer> for Layer {
|
||||
fn from(from: TextLayer) -> Layer {
|
||||
Layer::new(LayerDataType::Text(from), DAffine2::IDENTITY.to_cols_array())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for &'a Layer {
|
||||
type Item = &'a Layer;
|
||||
type IntoIter = LayerIter<'a>;
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
//! There are currently these different types of layers:
|
||||
//! * [Folder layers](folder_layer::FolderLayer), which encapsulate sub-layers
|
||||
//! * [Shape layers](shape_layer::ShapeLayer), which contain generic SVG [`<path>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/path)s
|
||||
//! * [Text layers](text_layer::TextLayer), which contain a description of laid out text
|
||||
//! * [Node Graph layers](nodegraph_layer::NodegraphLayer), which contain a node graph frame
|
||||
//!
|
||||
//! Refer to the module-level documentation for detailed information on each layer.
|
||||
@@ -23,10 +22,9 @@ pub mod folder_layer;
|
||||
pub mod layer_info;
|
||||
/// Contains the [NodegraphLayer](nodegraph_layer::NodegraphLayer) type that contains a node graph.
|
||||
pub mod nodegraph_layer;
|
||||
// TODO: Remove shape layers after rewriting the overlay system
|
||||
/// Contains the [ShapeLayer](shape_layer::ShapeLayer) type, a generic SVG element defined using Bezier paths.
|
||||
pub mod shape_layer;
|
||||
/// Contains the [TextLayer](text_layer::TextLayer) type.
|
||||
pub mod text_layer;
|
||||
|
||||
mod render_data;
|
||||
pub use render_data::RenderData;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::style::ViewMode;
|
||||
use super::text_layer::FontCache;
|
||||
use graphene_std::text::FontCache;
|
||||
|
||||
use glam::DVec2;
|
||||
|
||||
|
||||
@@ -1,179 +0,0 @@
|
||||
use super::layer_info::LayerData;
|
||||
use super::style::{PathStyle, RenderData, ViewMode};
|
||||
use crate::intersection::{intersect_quad_bez_path, Quad};
|
||||
use crate::LayerId;
|
||||
pub use font_cache::{Font, FontCache};
|
||||
|
||||
use graphene_std::vector::subpath::Subpath;
|
||||
|
||||
use glam::{DAffine2, DMat2, DVec2};
|
||||
use rustybuzz::Face;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Write;
|
||||
|
||||
mod font_cache;
|
||||
mod to_path;
|
||||
|
||||
/// A line, or multiple lines, of text drawn in the document.
|
||||
/// Like [ShapeLayers](super::shape_layer::ShapeLayer), [TextLayer] are rendered as
|
||||
/// [`<path>`s](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/path).
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, specta::Type)]
|
||||
pub struct TextLayer {
|
||||
/// The string of text, encompassing one or multiple lines.
|
||||
pub text: String,
|
||||
/// Fill color and stroke used to render the text.
|
||||
pub path_style: PathStyle,
|
||||
/// Font size in pixels.
|
||||
pub size: f64,
|
||||
pub line_width: Option<f64>,
|
||||
pub font: Font,
|
||||
#[serde(skip)]
|
||||
pub editable: bool,
|
||||
#[serde(skip)]
|
||||
pub cached_path: Option<Subpath>,
|
||||
}
|
||||
|
||||
impl LayerData for TextLayer {
|
||||
fn render(&mut self, svg: &mut String, svg_defs: &mut String, transforms: &mut Vec<DAffine2>, render_data: &RenderData) -> bool {
|
||||
let transform = self.transform(transforms, render_data.view_mode);
|
||||
let inverse = transform.inverse();
|
||||
|
||||
if !inverse.is_finite() {
|
||||
let _ = write!(svg, "<!-- SVG shape has an invalid transform -->");
|
||||
return false;
|
||||
}
|
||||
|
||||
let _ = writeln!(svg, r#"<g transform="matrix("#);
|
||||
inverse.to_cols_array().iter().enumerate().for_each(|(i, entry)| {
|
||||
let _ = svg.write_str(&(entry.to_string() + if i == 5 { "" } else { "," }));
|
||||
});
|
||||
let _ = svg.write_str(r#")">"#);
|
||||
|
||||
if self.editable {
|
||||
let font = render_data.font_cache.resolve_font(&self.font);
|
||||
if let Some(url) = font.and_then(|font| render_data.font_cache.get_preview_url(font)) {
|
||||
let _ = write!(svg, r#"<style>@font-face {{font-family: local-font;src: url({});}}")</style>"#, url);
|
||||
}
|
||||
|
||||
let _ = write!(
|
||||
svg,
|
||||
r#"<foreignObject transform="matrix({})"{}></foreignObject>"#,
|
||||
transform
|
||||
.to_cols_array()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, entry)| { entry.to_string() + if i == 5 { "" } else { "," } })
|
||||
.collect::<String>(),
|
||||
font.map(|_| r#" style="font-family: local-font;""#).unwrap_or_default()
|
||||
);
|
||||
} else {
|
||||
let buzz_face = self.load_face(render_data);
|
||||
|
||||
let mut path = self.to_subpath(buzz_face);
|
||||
|
||||
let bounds = path.bounding_box().unwrap_or_default();
|
||||
|
||||
path.apply_affine(transform);
|
||||
|
||||
let transformed_bounds = path.bounding_box().unwrap_or_default();
|
||||
|
||||
let _ = write!(
|
||||
svg,
|
||||
r#"<path d="{}" {} />"#,
|
||||
path.to_svg(),
|
||||
self.path_style.render(render_data.view_mode, svg_defs, transform, bounds, transformed_bounds)
|
||||
);
|
||||
}
|
||||
let _ = svg.write_str("</g>");
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
fn bounding_box(&self, transform: glam::DAffine2, render_data: &RenderData) -> Option<[DVec2; 2]> {
|
||||
let buzz_face = Some(self.load_face(render_data)?);
|
||||
|
||||
if transform.matrix2 == DMat2::ZERO {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some((transform * self.bounding_box(&self.text, buzz_face)).bounding_box())
|
||||
}
|
||||
|
||||
fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>, render_data: &RenderData) {
|
||||
let buzz_face = self.load_face(render_data);
|
||||
|
||||
if intersect_quad_bez_path(quad, &self.bounding_box(&self.text, buzz_face).path(), true) {
|
||||
intersections.push(path.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TextLayer {
|
||||
pub fn load_face<'a>(&self, render_data: &'a RenderData) -> Option<Face<'a>> {
|
||||
render_data.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 {
|
||||
let start = match mode {
|
||||
ViewMode::Outline => 0,
|
||||
_ => (transforms.len() as i32 - 1).max(0) as usize,
|
||||
};
|
||||
transforms.iter().skip(start).cloned().reduce(|a, b| a * b).unwrap_or(DAffine2::IDENTITY)
|
||||
}
|
||||
|
||||
pub fn new(text: String, style: PathStyle, size: f64, font: Font, render_data: &RenderData) -> Self {
|
||||
let mut new = Self {
|
||||
text,
|
||||
path_style: style,
|
||||
size,
|
||||
line_width: None,
|
||||
font,
|
||||
editable: false,
|
||||
cached_path: None,
|
||||
};
|
||||
|
||||
new.cached_path = Some(new.generate_path(new.load_face(render_data)));
|
||||
|
||||
new
|
||||
}
|
||||
|
||||
/// Converts to a [Subpath], populating the cache if necessary.
|
||||
#[inline]
|
||||
pub fn to_subpath(&mut self, buzz_face: Option<Face>) -> Subpath {
|
||||
if self.cached_path.as_ref().filter(|subpath| !subpath.manipulator_groups().is_empty()).is_none() {
|
||||
let path = self.generate_path(buzz_face);
|
||||
self.cached_path = Some(path.clone());
|
||||
return path;
|
||||
}
|
||||
self.cached_path.clone().unwrap()
|
||||
}
|
||||
|
||||
/// Converts to a [Subpath], without populating the cache.
|
||||
#[inline]
|
||||
pub fn to_subpath_nonmut(&self, render_data: &RenderData) -> Subpath {
|
||||
let buzz_face = self.load_face(render_data);
|
||||
|
||||
self.cached_path
|
||||
.clone()
|
||||
.filter(|subpath| !subpath.manipulator_groups().is_empty())
|
||||
.unwrap_or_else(|| self.generate_path(buzz_face))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn generate_path(&self, buzz_face: Option<Face>) -> Subpath {
|
||||
to_path::to_path(&self.text, buzz_face, self.size, self.line_width)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn bounding_box(&self, text: &str, buzz_face: Option<Face>) -> Quad {
|
||||
let far = to_path::bounding_box(text, buzz_face, self.size, self.line_width);
|
||||
Quad::from_box([DVec2::ZERO, far])
|
||||
}
|
||||
|
||||
pub fn update_text(&mut self, text: String, render_data: &RenderData) {
|
||||
let buzz_face = self.load_face(render_data);
|
||||
|
||||
self.text = text;
|
||||
self.cached_path = Some(self.generate_path(buzz_face));
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
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, specta::Type)]
|
||||
pub struct Font {
|
||||
#[serde(rename = "fontFamily")]
|
||||
pub font_family: String,
|
||||
#[serde(rename = "fontStyle")]
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -1,167 +0,0 @@
|
||||
use graphene_std::vector::consts::ManipulatorType;
|
||||
use graphene_std::vector::manipulator_group::ManipulatorGroup;
|
||||
use graphene_std::vector::manipulator_point::ManipulatorPoint;
|
||||
use graphene_std::vector::subpath::Subpath;
|
||||
|
||||
use glam::DVec2;
|
||||
use rustybuzz::ttf_parser::{GlyphId, OutlineBuilder};
|
||||
use rustybuzz::{GlyphBuffer, UnicodeBuffer};
|
||||
|
||||
struct Builder {
|
||||
path: Subpath,
|
||||
pos: DVec2,
|
||||
offset: DVec2,
|
||||
ascender: f64,
|
||||
scale: f64,
|
||||
}
|
||||
|
||||
impl Builder {
|
||||
fn point(&self, x: f32, y: f32) -> DVec2 {
|
||||
self.pos + self.offset + DVec2::new(x as f64, self.ascender - y as f64) * self.scale
|
||||
}
|
||||
}
|
||||
|
||||
impl OutlineBuilder for Builder {
|
||||
fn move_to(&mut self, x: f32, y: f32) {
|
||||
let anchor = self.point(x, y);
|
||||
if self.path.manipulator_groups().last().filter(|el| el.points.iter().any(Option::is_some)).is_some() {
|
||||
self.path.manipulator_groups_mut().push_end(ManipulatorGroup::closed());
|
||||
}
|
||||
self.path.manipulator_groups_mut().push_end(ManipulatorGroup::new_with_anchor(anchor));
|
||||
}
|
||||
|
||||
fn line_to(&mut self, x: f32, y: f32) {
|
||||
let anchor = self.point(x, y);
|
||||
self.path.manipulator_groups_mut().push_end(ManipulatorGroup::new_with_anchor(anchor));
|
||||
}
|
||||
|
||||
fn quad_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32) {
|
||||
let [handle, anchor] = [self.point(x1, y1), self.point(x2, y2)];
|
||||
self.path.manipulator_groups_mut().last_mut().unwrap().points[ManipulatorType::OutHandle] = Some(ManipulatorPoint::new(handle, ManipulatorType::OutHandle));
|
||||
self.path.manipulator_groups_mut().push_end(ManipulatorGroup::new_with_anchor(anchor));
|
||||
}
|
||||
|
||||
fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x3: f32, y3: f32) {
|
||||
let [handle1, handle2, anchor] = [self.point(x1, y1), self.point(x2, y2), self.point(x3, y3)];
|
||||
self.path.manipulator_groups_mut().last_mut().unwrap().points[ManipulatorType::OutHandle] = Some(ManipulatorPoint::new(handle1, ManipulatorType::OutHandle));
|
||||
self.path.manipulator_groups_mut().push_end(ManipulatorGroup::new_with_anchor(anchor));
|
||||
self.path.manipulator_groups_mut().last_mut().unwrap().points[ManipulatorType::InHandle] = Some(ManipulatorPoint::new(handle2, ManipulatorType::InHandle));
|
||||
}
|
||||
|
||||
fn close(&mut self) {
|
||||
self.path.manipulator_groups_mut().push_end(ManipulatorGroup::closed());
|
||||
}
|
||||
}
|
||||
|
||||
fn font_properties(buzz_face: &rustybuzz::Face, font_size: f64) -> (f64, f64, UnicodeBuffer) {
|
||||
let scale = (buzz_face.units_per_em() as f64).recip() * font_size;
|
||||
let line_height = font_size;
|
||||
let buffer = UnicodeBuffer::new();
|
||||
(scale, line_height, buffer)
|
||||
}
|
||||
|
||||
fn push_str(buffer: &mut UnicodeBuffer, word: &str, trailing_space: bool) {
|
||||
buffer.push_str(word);
|
||||
|
||||
if trailing_space {
|
||||
buffer.push_str(" ");
|
||||
}
|
||||
}
|
||||
|
||||
fn wrap_word(line_width: Option<f64>, glyph_buffer: &GlyphBuffer, scale: f64, x_pos: f64) -> bool {
|
||||
if let Some(line_width) = line_width {
|
||||
let word_length: i32 = glyph_buffer.glyph_positions().iter().map(|pos| pos.x_advance).sum();
|
||||
let scaled_word_length = word_length as f64 * scale;
|
||||
|
||||
if scaled_word_length + x_pos > line_width {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn to_path(str: &str, buzz_face: Option<rustybuzz::Face>, font_size: f64, line_width: Option<f64>) -> Subpath {
|
||||
let buzz_face = match buzz_face {
|
||||
Some(face) => face,
|
||||
// Show blank layer if font has not loaded
|
||||
None => return Subpath::default(),
|
||||
};
|
||||
|
||||
let (scale, line_height, mut buffer) = font_properties(&buzz_face, font_size);
|
||||
|
||||
let mut builder = Builder {
|
||||
path: Subpath::new(),
|
||||
pos: DVec2::ZERO,
|
||||
offset: DVec2::ZERO,
|
||||
ascender: (buzz_face.ascender() as f64 / buzz_face.height() as f64) * font_size / scale,
|
||||
scale,
|
||||
};
|
||||
|
||||
for line in str.split('\n') {
|
||||
let length = line.split(' ').count();
|
||||
for (index, word) in line.split(' ').enumerate() {
|
||||
push_str(&mut buffer, word, index != length - 1);
|
||||
let glyph_buffer = rustybuzz::shape(&buzz_face, &[], buffer);
|
||||
|
||||
if wrap_word(line_width, &glyph_buffer, scale, builder.pos.x) {
|
||||
builder.pos = DVec2::new(0., builder.pos.y + line_height);
|
||||
}
|
||||
|
||||
for (glyph_position, glyph_info) in glyph_buffer.glyph_positions().iter().zip(glyph_buffer.glyph_infos()) {
|
||||
if let Some(line_width) = line_width {
|
||||
if builder.pos.x + (glyph_position.x_advance as f64 * builder.scale) >= line_width {
|
||||
builder.pos = DVec2::new(0., builder.pos.y + line_height);
|
||||
}
|
||||
}
|
||||
builder.offset = DVec2::new(glyph_position.x_offset as f64, glyph_position.y_offset as f64) * builder.scale;
|
||||
buzz_face.outline_glyph(GlyphId(glyph_info.glyph_id as u16), &mut builder);
|
||||
builder.pos += DVec2::new(glyph_position.x_advance as f64, glyph_position.y_advance as f64) * builder.scale;
|
||||
}
|
||||
|
||||
buffer = glyph_buffer.clear();
|
||||
}
|
||||
builder.pos = DVec2::new(0., builder.pos.y + line_height);
|
||||
}
|
||||
builder.path
|
||||
}
|
||||
|
||||
pub fn bounding_box(str: &str, buzz_face: Option<rustybuzz::Face>, font_size: f64, line_width: Option<f64>) -> DVec2 {
|
||||
let buzz_face = match buzz_face {
|
||||
Some(face) => face,
|
||||
// Show blank layer if font has not loaded
|
||||
None => return DVec2::ZERO,
|
||||
};
|
||||
|
||||
let (scale, line_height, mut buffer) = font_properties(&buzz_face, font_size);
|
||||
|
||||
let mut pos = DVec2::ZERO;
|
||||
let mut bounds = DVec2::ZERO;
|
||||
|
||||
for line in str.split('\n') {
|
||||
let length = line.split(' ').count();
|
||||
for (index, word) in line.split(' ').enumerate() {
|
||||
push_str(&mut buffer, word, index != length - 1);
|
||||
|
||||
let glyph_buffer = rustybuzz::shape(&buzz_face, &[], buffer);
|
||||
|
||||
if wrap_word(line_width, &glyph_buffer, scale, pos.x) {
|
||||
pos = DVec2::new(0., pos.y + line_height);
|
||||
}
|
||||
|
||||
for glyph_position in glyph_buffer.glyph_positions() {
|
||||
if let Some(line_width) = line_width {
|
||||
if pos.x + (glyph_position.x_advance as f64 * scale) >= line_width {
|
||||
pos = DVec2::new(0., pos.y + line_height);
|
||||
}
|
||||
}
|
||||
pos += DVec2::new(glyph_position.x_advance as f64, glyph_position.y_advance as f64) * scale;
|
||||
}
|
||||
bounds = bounds.max(pos + DVec2::new(0., line_height));
|
||||
|
||||
buffer = glyph_buffer.clear();
|
||||
}
|
||||
pos = DVec2::new(0., pos.y + line_height);
|
||||
}
|
||||
|
||||
bounds
|
||||
}
|
||||
@@ -36,16 +36,6 @@ pub enum Operation {
|
||||
transform: [f64; 6],
|
||||
style: style::PathStyle,
|
||||
},
|
||||
AddText {
|
||||
path: Vec<LayerId>,
|
||||
insert_index: isize,
|
||||
transform: [f64; 6],
|
||||
style: style::PathStyle,
|
||||
text: String,
|
||||
size: f64,
|
||||
font_name: String,
|
||||
font_style: String,
|
||||
},
|
||||
AddNodeGraphFrame {
|
||||
path: Vec<LayerId>,
|
||||
insert_index: isize,
|
||||
@@ -68,14 +58,6 @@ pub enum Operation {
|
||||
layer_path: Vec<LayerId>,
|
||||
pivot: (f64, f64),
|
||||
},
|
||||
SetTextEditability {
|
||||
path: Vec<LayerId>,
|
||||
editable: bool,
|
||||
},
|
||||
SetTextContent {
|
||||
path: Vec<LayerId>,
|
||||
new_text: String,
|
||||
},
|
||||
AddPolyline {
|
||||
path: Vec<LayerId>,
|
||||
insert_index: isize,
|
||||
@@ -128,12 +110,6 @@ pub enum Operation {
|
||||
DuplicateLayer {
|
||||
path: Vec<LayerId>,
|
||||
},
|
||||
ModifyFont {
|
||||
path: Vec<LayerId>,
|
||||
font_family: String,
|
||||
size: f64,
|
||||
font_style: String,
|
||||
},
|
||||
MoveSelectedManipulatorPoints {
|
||||
layer_path: Vec<LayerId>,
|
||||
delta: (f64, f64),
|
||||
|
||||
Reference in New Issue
Block a user