Image and text bug fixes (#685)

* Image and text bugfixes

* Mark only the required layer types as dirty

* Fix doctest

* Disable selection if empty

* Cleanup naming

* Simplify cache deleting on export

* Minor css style change

* Nit

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
0HyperCube
2022-06-30 02:18:01 +01:00
committed by Keavon Chambers
co-authored by Keavon Chambers
parent 2e3e079982
commit 020f700c92
22 changed files with 243 additions and 128 deletions
Generated
+7
View File
@@ -34,6 +34,12 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "base64"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
[[package]] [[package]]
name = "bezier-rs" name = "bezier-rs"
version = "0.0.0" version = "0.0.0"
@@ -154,6 +160,7 @@ dependencies = [
name = "graphite-graphene" name = "graphite-graphene"
version = "0.0.0" version = "0.0.0"
dependencies = [ dependencies = [
"base64",
"glam", "glam",
"kurbo", "kurbo",
"log", "log",
@@ -79,6 +79,7 @@ impl MessageHandler<DialogMessage, &PortfolioMessageHandler> for DialogMessageHa
file_name: portfolio.active_document().name.clone(), file_name: portfolio.active_document().name.clone(),
scale_factor: 1., scale_factor: 1.,
artboards, artboards,
has_selection: portfolio.active_document().selected_layers().next().is_some(),
..Default::default() ..Default::default()
}; };
self.export_dialog.register_properties(responses, LayoutTarget::DialogDetails); self.export_dialog.register_properties(responses, LayoutTarget::DialogDetails);
+9 -4
View File
@@ -15,6 +15,7 @@ pub struct Export {
pub scale_factor: f64, pub scale_factor: f64,
pub bounds: ExportBounds, pub bounds: ExportBounds,
pub artboards: HashMap<LayerId, String>, pub artboards: HashMap<LayerId, String>,
pub has_selection: bool,
} }
impl PropertyHolder for Export { impl PropertyHolder for Export {
@@ -60,15 +61,19 @@ impl PropertyHolder for Export {
})), })),
]; ];
let artboards = self.artboards.iter().map(|(&val, name)| (ExportBounds::Artboard(val), name.to_string())); let artboards = self.artboards.iter().map(|(&val, name)| (ExportBounds::Artboard(val), name.to_string(), false));
let mut export_area_options = vec![(ExportBounds::AllArtwork, "All Artwork".to_string())]; let mut export_area_options = vec![
(ExportBounds::AllArtwork, "All Artwork".to_string(), false),
(ExportBounds::Selection, "Selection".to_string(), !self.has_selection),
];
export_area_options.extend(artboards); export_area_options.extend(artboards);
let index = export_area_options.iter().position(|(val, _)| val == &self.bounds).unwrap(); let index = export_area_options.iter().position(|(val, _, _)| val == &self.bounds).unwrap();
let entries = vec![export_area_options let entries = vec![export_area_options
.into_iter() .into_iter()
.map(|(val, name)| DropdownEntryData { .map(|(val, name, disabled)| DropdownEntryData {
label: name, label: name,
on_update: WidgetCallback::new(move |_| ExportDialogUpdate::ExportBounds(val).into()), on_update: WidgetCallback::new(move |_| ExportDialogUpdate::ExportBounds(val).into()),
disabled,
..Default::default() ..Default::default()
}) })
.collect()]; .collect()];
@@ -2,7 +2,7 @@ use crate::message_prelude::*;
use graphene::color::Color; use graphene::color::Color;
use graphene::document::Document as GrapheneDocument; use graphene::document::Document as GrapheneDocument;
use graphene::layers::style::{self, Fill, ViewMode}; use graphene::layers::style::{self, Fill, RenderData, ViewMode};
use graphene::layers::text_layer::FontCache; use graphene::layers::text_layer::FontCache;
use graphene::DocumentResponse; use graphene::DocumentResponse;
use graphene::Operation as DocumentOperation; use graphene::Operation as DocumentOperation;
@@ -85,9 +85,10 @@ impl MessageHandler<ArtboardMessage, &FontCache> for ArtboardMessageHandler {
.into(), .into(),
) )
} else { } else {
let render_data = RenderData::new(ViewMode::Normal, font_cache, None, false);
responses.push_back( responses.push_back(
FrontendMessage::UpdateDocumentArtboards { FrontendMessage::UpdateDocumentArtboards {
svg: self.artboards_graphene_document.render_root(ViewMode::Normal, font_cache, None), svg: self.artboards_graphene_document.render_root(render_data),
} }
.into(), .into(),
); );
+28 -18
View File
@@ -1,5 +1,5 @@
use super::clipboards::Clipboard; use super::clipboards::Clipboard;
use super::layer_panel::{layer_panel_entry, LayerDataTypeDiscriminant, LayerMetadata, LayerPanelEntry, RawBuffer}; use super::layer_panel::{layer_panel_entry, LayerMetadata, LayerPanelEntry, RawBuffer};
use super::properties_panel_message_handler::PropertiesPanelMessageHandlerData; use super::properties_panel_message_handler::PropertiesPanelMessageHandlerData;
use super::utility_types::{AlignAggregate, AlignAxis, DocumentSave, FlipAxis}; use super::utility_types::{AlignAggregate, AlignAxis, DocumentSave, FlipAxis};
use super::utility_types::{DocumentMode, TargetDocument}; use super::utility_types::{DocumentMode, TargetDocument};
@@ -21,16 +21,15 @@ use graphene::color::Color;
use graphene::document::Document as GrapheneDocument; use graphene::document::Document as GrapheneDocument;
use graphene::layers::blend_mode::BlendMode; use graphene::layers::blend_mode::BlendMode;
use graphene::layers::folder_layer::FolderLayer; use graphene::layers::folder_layer::FolderLayer;
use graphene::layers::layer_info::LayerDataType; use graphene::layers::layer_info::{LayerDataType, LayerDataTypeDiscriminant};
use graphene::layers::style::{Fill, ViewMode}; use graphene::layers::style::{Fill, RenderData, ViewMode};
use graphene::layers::text_layer::FontCache; use graphene::layers::text_layer::{Font, FontCache};
use graphene::{DocumentError, DocumentResponse, LayerId, Operation as DocumentOperation}; use graphene::{DocumentError, DocumentResponse, LayerId, Operation as DocumentOperation};
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
use log::warn; use log::warn;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::collections::{HashMap, HashSet, VecDeque};
use std::collections::VecDeque;
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub struct DocumentMessageHandler { pub struct DocumentMessageHandler {
@@ -491,18 +490,20 @@ impl DocumentMessageHandler {
path path
} }
/// Creates the blob URLs for the image data in the document /// Loads layer resources such as creating the blob URLs for the images and loading all of the fonts in the document
pub fn load_image_data(&self, responses: &mut VecDeque<Message>, root: &LayerDataType, mut path: Vec<LayerId>) { pub fn load_layer_resources(&self, responses: &mut VecDeque<Message>, root: &LayerDataType, mut path: Vec<LayerId>) {
let mut image_data = Vec::new(); fn walk_layers(data: &LayerDataType, path: &mut Vec<LayerId>, image_data: &mut Vec<FrontendImageData>, fonts: &mut HashSet<Font>) {
fn walk_layers(data: &LayerDataType, path: &mut Vec<LayerId>, image_data: &mut Vec<FrontendImageData>) {
match data { match data {
LayerDataType::Folder(f) => { LayerDataType::Folder(f) => {
for (id, layer) in f.layer_ids.iter().zip(f.layers().iter()) { for (id, layer) in f.layer_ids.iter().zip(f.layers().iter()) {
path.push(*id); path.push(*id);
walk_layers(&layer.data, path, image_data); walk_layers(&layer.data, path, image_data, fonts);
path.pop(); path.pop();
} }
} }
LayerDataType::Text(txt) => {
fonts.insert(txt.font.clone());
}
LayerDataType::Image(img) => image_data.push(FrontendImageData { LayerDataType::Image(img) => image_data.push(FrontendImageData {
path: path.clone(), path: path.clone(),
image_data: img.image_data.clone(), image_data: img.image_data.clone(),
@@ -512,10 +513,15 @@ impl DocumentMessageHandler {
} }
} }
walk_layers(root, &mut path, &mut image_data); let mut image_data = Vec::new();
let mut fonts = HashSet::new();
walk_layers(root, &mut path, &mut image_data, &mut fonts);
if !image_data.is_empty() { if !image_data.is_empty() {
responses.push_front(FrontendMessage::UpdateImageData { image_data }.into()); responses.push_front(FrontendMessage::UpdateImageData { image_data }.into());
} }
for font in fonts {
responses.push_front(FrontendMessage::TriggerFontLoad { font, is_default: false }.into());
}
} }
pub fn update_document_widgets(&self, responses: &mut VecDeque<Message>) { pub fn update_document_widgets(&self, responses: &mut VecDeque<Message>) {
@@ -1040,12 +1046,14 @@ impl MessageHandler<DocumentMessage, (&InputPreprocessorMessageHandler, &FontCac
let old_transform = self.graphene_document.root.transform; let old_transform = self.graphene_document.root.transform;
// Reset the root's transform (required to avoid any rotation by the user) // Reset the root's transform (required to avoid any rotation by the user)
self.graphene_document.root.transform = DAffine2::IDENTITY; self.graphene_document.root.transform = DAffine2::IDENTITY;
self.graphene_document.root.cache_dirty = true; GrapheneDocument::mark_children_as_dirty(&mut self.graphene_document.root);
// Calculates the bounding box of the region to be exported // Calculates the bounding box of the region to be exported
use crate::frontend::utility_types::ExportBounds;
let bbox = match bounds { let bbox = match bounds {
crate::frontend::utility_types::ExportBounds::AllArtwork => self.all_layer_bounds(font_cache), ExportBounds::AllArtwork => self.all_layer_bounds(font_cache),
crate::frontend::utility_types::ExportBounds::Artboard(id) => self ExportBounds::Selection => self.selected_visible_layers_bounding_box(font_cache),
ExportBounds::Artboard(id) => self
.artboard_message_handler .artboard_message_handler
.artboards_graphene_document .artboards_graphene_document
.layer(&[id]) .layer(&[id])
@@ -1061,14 +1069,15 @@ impl MessageHandler<DocumentMessage, (&InputPreprocessorMessageHandler, &FontCac
false => file_name + file_suffix, false => file_name + file_suffix,
}; };
let rendered = self.graphene_document.render_root(self.view_mode, font_cache, None); let render_data = RenderData::new(self.view_mode, font_cache, None, true);
let rendered = self.graphene_document.render_root(render_data);
let document = format!( let document = format!(
r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="{} {} {} {}" width="{}px" height="{}">{}{}</svg>"#, r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="{} {} {} {}" width="{}px" height="{}">{}{}</svg>"#,
bbox[0].x, bbox[0].y, size.x, size.y, size.x, size.y, "\n", rendered bbox[0].x, bbox[0].y, size.x, size.y, size.x, size.y, "\n", rendered
); );
self.graphene_document.root.transform = old_transform; self.graphene_document.root.transform = old_transform;
self.graphene_document.root.cache_dirty = true; GrapheneDocument::mark_children_as_dirty(&mut self.graphene_document.root);
if file_type == FileType::Svg { if file_type == FileType::Svg {
responses.push_back(FrontendMessage::TriggerFileDownload { document, name }.into()); responses.push_back(FrontendMessage::TriggerFileDownload { document, name }.into());
@@ -1218,9 +1227,10 @@ impl MessageHandler<DocumentMessage, (&InputPreprocessorMessageHandler, &FontCac
} }
RenameLayer { layer_path, new_name } => responses.push_back(DocumentOperation::RenameLayer { layer_path, new_name }.into()), RenameLayer { layer_path, new_name } => responses.push_back(DocumentOperation::RenameLayer { layer_path, new_name }.into()),
RenderDocument => { RenderDocument => {
let render_data = RenderData::new(self.view_mode, font_cache, Some(ipp.document_bounds()), false);
responses.push_back( responses.push_back(
FrontendMessage::UpdateDocumentArtwork { FrontendMessage::UpdateDocumentArtwork {
svg: self.graphene_document.render_root(self.view_mode, font_cache, Some(ipp.document_bounds())), svg: self.graphene_document.render_root(render_data),
} }
.into(), .into(),
); );
+4 -38
View File
@@ -1,12 +1,11 @@
use graphene::layers::layer_info::{Layer, LayerData, LayerDataType}; use graphene::layers::layer_info::{Layer, LayerData, LayerDataTypeDiscriminant};
use graphene::layers::style::ViewMode; use graphene::layers::style::{RenderData, ViewMode};
use graphene::layers::text_layer::FontCache; use graphene::layers::text_layer::FontCache;
use graphene::LayerId; use graphene::LayerId;
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
use serde::ser::SerializeStruct; use serde::ser::SerializeStruct;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Copy)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Copy)]
pub struct LayerMetadata { pub struct LayerMetadata {
@@ -27,7 +26,8 @@ pub fn layer_panel_entry(layer_metadata: &LayerMetadata, transform: DAffine2, la
let mut thumbnail = String::new(); let mut thumbnail = String::new();
let mut svg_defs = String::new(); let mut svg_defs = String::new();
layer.data.clone().render(&mut thumbnail, &mut svg_defs, &mut vec![transform], ViewMode::Normal, font_cache, None); let render_data = RenderData::new(ViewMode::Normal, font_cache, None, false);
layer.data.clone().render(&mut thumbnail, &mut svg_defs, &mut vec![transform], render_data);
let transform = transform.to_cols_array().iter().map(ToString::to_string).collect::<Vec<_>>().join(","); let transform = transform.to_cols_array().iter().map(ToString::to_string).collect::<Vec<_>>().join(",");
let thumbnail = if let [(x_min, y_min), (x_max, y_max)] = arr.as_slice() { let thumbnail = if let [(x_min, y_min), (x_max, y_max)] = arr.as_slice() {
format!( format!(
@@ -90,37 +90,3 @@ pub struct LayerPanelEntry {
pub path: Vec<LayerId>, pub path: Vec<LayerId>,
pub thumbnail: String, pub thumbnail: String,
} }
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum LayerDataTypeDiscriminant {
Folder,
Shape,
Text,
Image,
}
impl fmt::Display for LayerDataTypeDiscriminant {
fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
let name = match self {
LayerDataTypeDiscriminant::Folder => "Folder",
LayerDataTypeDiscriminant::Shape => "Shape",
LayerDataTypeDiscriminant::Text => "Text",
LayerDataTypeDiscriminant::Image => "Image",
};
formatter.write_str(name)
}
}
impl From<&LayerDataType> for LayerDataTypeDiscriminant {
fn from(data: &LayerDataType) -> Self {
use LayerDataType::*;
match data {
Folder(_) => LayerDataTypeDiscriminant::Folder,
Shape(_) => LayerDataTypeDiscriminant::Shape,
Text(_) => LayerDataTypeDiscriminant::Text,
Image(_) => LayerDataTypeDiscriminant::Image,
}
}
}
@@ -2,7 +2,7 @@ use crate::input::InputPreprocessorMessageHandler;
use crate::message_prelude::*; use crate::message_prelude::*;
use graphene::document::Document as GrapheneDocument; use graphene::document::Document as GrapheneDocument;
use graphene::layers::style::ViewMode; use graphene::layers::style::{RenderData, ViewMode};
use graphene::layers::text_layer::FontCache; use graphene::layers::text_layer::FontCache;
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
@@ -32,7 +32,8 @@ impl MessageHandler<OverlaysMessage, (bool, &FontCache, &InputPreprocessorMessag
responses.push_back( responses.push_back(
FrontendMessage::UpdateDocumentOverlays { FrontendMessage::UpdateDocumentOverlays {
svg: if overlays_visible { svg: if overlays_visible {
self.overlays_graphene_document.render_root(ViewMode::Normal, font_cache, Some(ipp.document_bounds())) let render_data = RenderData::new(ViewMode::Normal, font_cache, Some(ipp.document_bounds()), false);
self.overlays_graphene_document.render_root(render_data)
} else { } else {
String::from("") String::from("")
}, },
@@ -7,6 +7,7 @@ use crate::layout::layout_message::LayoutTarget;
use crate::layout::widgets::PropertyHolder; use crate::layout::widgets::PropertyHolder;
use crate::{dialog, message_prelude::*}; use crate::{dialog, message_prelude::*};
use graphene::layers::layer_info::LayerDataTypeDiscriminant;
use graphene::layers::text_layer::{Font, FontCache}; use graphene::layers::text_layer::{Font, FontCache};
use graphene::Operation as DocumentOperation; use graphene::Operation as DocumentOperation;
@@ -47,11 +48,10 @@ impl PortfolioMessageHandler {
// Uses binary search to find the index of the element where number is bigger than i // Uses binary search to find the index of the element where number is bigger than i
let new_doc_title_num = doc_title_numbers.binary_search(&0).map_or_else(|e| e, |v| v) + 1; let new_doc_title_num = doc_title_numbers.binary_search(&0).map_or_else(|e| e, |v| v) + 1;
let name = match new_doc_title_num { match new_doc_title_num {
1 => DEFAULT_DOCUMENT_NAME.to_string(), 1 => DEFAULT_DOCUMENT_NAME.to_string(),
_ => format!("{} {}", DEFAULT_DOCUMENT_NAME, new_doc_title_num), _ => format!("{} {}", DEFAULT_DOCUMENT_NAME, new_doc_title_num),
}; }
name
} }
// TODO Fix how this doesn't preserve tab order upon loading new document from *File > Load* // TODO Fix how this doesn't preserve tab order upon loading new document from *File > Load*
@@ -81,7 +81,7 @@ impl PortfolioMessageHandler {
); );
new_document.update_layer_tree_options_bar_widgets(responses, &self.font_cache); new_document.update_layer_tree_options_bar_widgets(responses, &self.font_cache);
new_document.load_image_data(responses, &new_document.graphene_document.root.data, Vec::new()); new_document.load_layer_resources(responses, &new_document.graphene_document.root.data, Vec::new());
self.documents.insert(document_id, new_document); self.documents.insert(document_id, new_document);
@@ -287,7 +287,8 @@ impl MessageHandler<PortfolioMessage, &InputPreprocessorMessageHandler> for Port
is_default, is_default,
} => { } => {
self.font_cache.insert(Font::new(font_family, font_style), preview_url, data, is_default); self.font_cache.insert(Font::new(font_family, font_style), preview_url, data, is_default);
responses.push_back(DocumentMessage::DirtyRenderDocument.into()); self.active_document_mut().graphene_document.mark_all_layers_of_type_as_dirty(LayerDataTypeDiscriminant::Text);
responses.push_back(DocumentMessage::RenderDocument.into());
} }
LoadFont { font, is_default } => { LoadFont { font, is_default } => {
if !self.font_cache.loaded_font(&font) { if !self.font_cache.loaded_font(&font) {
@@ -387,7 +388,7 @@ impl MessageHandler<PortfolioMessage, &InputPreprocessorMessageHandler> for Port
} }
.into(), .into(),
); );
self.active_document().load_image_data(responses, &entry.layer.data, destination_path.clone()); self.active_document().load_layer_resources(responses, &entry.layer.data, destination_path.clone());
responses.push_front( responses.push_front(
DocumentOperation::InsertLayer { DocumentOperation::InsertLayer {
layer: entry.layer.clone(), layer: entry.layer.clone(),
@@ -428,7 +429,7 @@ impl MessageHandler<PortfolioMessage, &InputPreprocessorMessageHandler> for Port
} }
.into(), .into(),
); );
self.active_document().load_image_data(responses, &entry.layer.data, destination_path.clone()); self.active_document().load_layer_resources(responses, &entry.layer.data, destination_path.clone());
responses.push_front( responses.push_front(
DocumentOperation::InsertLayer { DocumentOperation::InsertLayer {
layer: entry.layer.clone(), layer: entry.layer.clone(),
@@ -1,4 +1,3 @@
use super::layer_panel::LayerDataTypeDiscriminant;
use super::utility_types::TargetDocument; use super::utility_types::TargetDocument;
use crate::document::properties_panel_message::TransformOp; use crate::document::properties_panel_message::TransformOp;
use crate::layout::layout_message::LayoutTarget; use crate::layout::layout_message::LayoutTarget;
@@ -10,7 +9,7 @@ use crate::message_prelude::*;
use graphene::color::Color; use graphene::color::Color;
use graphene::document::Document as GrapheneDocument; use graphene::document::Document as GrapheneDocument;
use graphene::layers::layer_info::{Layer, LayerDataType}; use graphene::layers::layer_info::{Layer, LayerDataType, LayerDataTypeDiscriminant};
use graphene::layers::style::{Fill, Gradient, GradientType, LineCap, LineJoin, Stroke}; use graphene::layers::style::{Fill, Gradient, GradientType, LineCap, LineJoin, Stroke};
use graphene::layers::text_layer::{FontCache, TextLayer}; use graphene::layers::text_layer::{FontCache, TextLayer};
use graphene::{LayerId, Operation}; use graphene::{LayerId, Operation};
+1
View File
@@ -61,6 +61,7 @@ impl FileType {
#[derive(Clone, Copy, Debug, Eq, Deserialize, PartialEq, Serialize)] #[derive(Clone, Copy, Debug, Eq, Deserialize, PartialEq, Serialize)]
pub enum ExportBounds { pub enum ExportBounds {
AllArtwork, AllArtwork,
Selection,
Artboard(LayerId), Artboard(LayerId),
} }
+1
View File
@@ -522,6 +522,7 @@ pub struct DropdownEntryData {
pub shortcut: Vec<String>, pub shortcut: Vec<String>,
#[serde(rename = "shortcutRequiresLock")] #[serde(rename = "shortcutRequiresLock")]
pub shortcut_requires_lock: bool, pub shortcut_requires_lock: bool,
pub disabled: bool,
pub children: Vec<Vec<DropdownEntryData>>, pub children: Vec<Vec<DropdownEntryData>>,
#[serde(skip)] #[serde(skip)]
@@ -20,11 +20,11 @@
v-for="(entry, entryIndex) in virtualScrollingEntryHeight ? section.slice(virtualScrollingStartIndex, virtualScrollingEndIndex) : section" v-for="(entry, entryIndex) in virtualScrollingEntryHeight ? section.slice(virtualScrollingStartIndex, virtualScrollingEndIndex) : section"
:key="entryIndex + (virtualScrollingEntryHeight ? virtualScrollingStartIndex : 0)" :key="entryIndex + (virtualScrollingEntryHeight ? virtualScrollingStartIndex : 0)"
class="row" class="row"
:class="{ open: isEntryOpen(entry), active: entry.label === highlighted?.label }" :class="{ open: isEntryOpen(entry), active: entry.label === highlighted?.label, disabled: entry.disabled }"
:style="{ height: virtualScrollingEntryHeight || '20px' }" :style="{ height: virtualScrollingEntryHeight || '20px' }"
@click="() => onEntryClick(entry)" @click="() => !entry.disabled && onEntryClick(entry)"
@pointerenter="() => onEntryPointerEnter(entry)" @pointerenter="() => !entry.disabled && onEntryPointerEnter(entry)"
@pointerleave="() => onEntryPointerLeave(entry)" @pointerleave="() => !entry.disabled && onEntryPointerLeave(entry)"
> >
<IconLabel v-if="entry.icon && drawIcon" :icon="entry.icon" class="entry-icon" /> <IconLabel v-if="entry.icon && drawIcon" :icon="entry.icon" class="entry-icon" />
<div v-else-if="drawIcon" class="no-icon"></div> <div v-else-if="drawIcon" class="no-icon"></div>
@@ -141,6 +141,20 @@
color: var(--color-f-white); color: var(--color-f-white);
} }
} }
&.disabled {
&:hover {
background: none;
}
span {
color: var(--color-8-uppergray);
}
svg {
fill: var(--color-8-uppergray);
}
}
} }
} }
} }
@@ -168,6 +182,7 @@ interface MenuListEntryData<Value = string> {
font?: URL; font?: URL;
shortcut?: string[]; shortcut?: string[];
shortcutRequiresLock?: boolean; shortcutRequiresLock?: boolean;
disabled?: boolean;
action?: () => void; action?: () => void;
children?: SectionsOfMenuListEntries; children?: SectionsOfMenuListEntries;
} }
@@ -256,7 +271,7 @@ const MenuList = defineComponent({
if (this.interactive) this.highlighted = this.activeEntry; if (this.interactive) this.highlighted = this.activeEntry;
const menuOpen = this.isOpen; const menuOpen = this.isOpen;
const flatEntries = this.entries.flat(); const flatEntries = this.entries.flat().filter((entry) => !entry.disabled);
const openChild = flatEntries.findIndex((entry) => entry.children?.length && entry.ref?.isOpen); const openChild = flatEntries.findIndex((entry) => entry.children?.length && entry.ref?.isOpen);
const openSubmenu = (highlighted: MenuListEntry<string>): void => { const openSubmenu = (highlighted: MenuListEntry<string>): void => {
+1 -1
View File
@@ -40,7 +40,7 @@ export function createPortfolioState(editor: Editor) {
if (!context) return; if (!context) return;
// Fill the canvas with white if jpeg (does not support transparency and defaults to black) // Fill the canvas with white if jpeg (does not support transparency and defaults to black)
if (triggerRasterDownload.mime.endsWith("jpg")) { if (triggerRasterDownload.mime.endsWith("jpeg")) {
context.fillStyle = "white"; context.fillStyle = "white";
context.fillRect(0, 0, triggerRasterDownload.size.x, triggerRasterDownload.size.y); context.fillRect(0, 0, triggerRasterDownload.size.x, triggerRasterDownload.size.y);
} }
+1
View File
@@ -17,6 +17,7 @@ kurbo = { git = "https://github.com/linebender/kurbo.git", features = [
"serde", "serde",
] } ] }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
base64 = "0.13"
glam = { version = "0.17", features = ["serde"] } glam = { version = "0.17", features = ["serde"] }
# Font rendering # Font rendering
+26 -5
View File
@@ -3,9 +3,9 @@ use crate::intersection::Quad;
use crate::layers; use crate::layers;
use crate::layers::folder_layer::FolderLayer; use crate::layers::folder_layer::FolderLayer;
use crate::layers::image_layer::ImageLayer; use crate::layers::image_layer::ImageLayer;
use crate::layers::layer_info::{Layer, LayerData, LayerDataType}; use crate::layers::layer_info::{Layer, LayerData, LayerDataType, LayerDataTypeDiscriminant};
use crate::layers::shape_layer::ShapeLayer; use crate::layers::shape_layer::ShapeLayer;
use crate::layers::style::ViewMode; use crate::layers::style::RenderData;
use crate::layers::text_layer::{Font, FontCache, TextLayer}; use crate::layers::text_layer::{Font, FontCache, TextLayer};
use crate::{DocumentError, DocumentResponse, Operation}; use crate::{DocumentError, DocumentResponse, Operation};
@@ -42,10 +42,10 @@ impl Default for Document {
impl Document { impl Document {
/// Wrapper around render, that returns the whole document as a Response. /// Wrapper around render, that returns the whole document as a Response.
pub fn render_root(&mut self, mode: ViewMode, font_cache: &FontCache, culling_bounds: Option<[DVec2; 2]>) -> String { pub fn render_root(&mut self, render_data: RenderData) -> String {
let mut svg_defs = String::from("<defs>"); let mut svg_defs = String::from("<defs>");
self.root.render(&mut vec![], mode, &mut svg_defs, font_cache, culling_bounds); self.root.render(&mut vec![], &mut svg_defs, render_data);
svg_defs.push_str("</defs>"); svg_defs.push_str("</defs>");
@@ -375,6 +375,27 @@ impl Document {
Ok(()) 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;
}
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> { pub fn transforms(&self, path: &[LayerId]) -> Result<Vec<DAffine2>, DocumentError> {
let mut root = &self.root; let mut root = &self.root;
let mut transforms = vec![self.root.transform]; let mut transforms = vec![self.root.transform];
@@ -697,7 +718,7 @@ impl Document {
text.font = Font::new(font_family, font_style); text.font = Font::new(font_family, font_style);
text.size = size; text.size = size;
text.regenerate_path(text.load_face(font_cache)); text.cached_path = Some(text.generate_path(text.load_face(font_cache)));
self.mark_as_dirty(&path)?; self.mark_as_dirty(&path)?;
Some([vec![DocumentChanged, LayerChanged { path: path.clone() }], update_thumbnails_upstream(&path)].concat()) Some([vec![DocumentChanged, LayerChanged { path: path.clone() }], update_thumbnails_upstream(&path)].concat())
} }
+3 -3
View File
@@ -1,5 +1,5 @@
use super::layer_info::{Layer, LayerData, LayerDataType}; use super::layer_info::{Layer, LayerData, LayerDataType};
use super::style::ViewMode; use super::style::RenderData;
use crate::intersection::Quad; use crate::intersection::Quad;
use crate::layers::text_layer::FontCache; use crate::layers::text_layer::FontCache;
use crate::{DocumentError, LayerId}; use crate::{DocumentError, LayerId};
@@ -22,9 +22,9 @@ pub struct FolderLayer {
} }
impl LayerData for FolderLayer { impl LayerData for FolderLayer {
fn render(&mut self, svg: &mut String, svg_defs: &mut String, transforms: &mut Vec<glam::DAffine2>, view_mode: ViewMode, font_cache: &FontCache, culling_bounds: Option<[DVec2; 2]>) { fn render(&mut self, svg: &mut String, svg_defs: &mut String, transforms: &mut Vec<glam::DAffine2>, render_data: RenderData) {
for layer in &mut self.layers { for layer in &mut self.layers {
let _ = writeln!(svg, "{}", layer.render(transforms, view_mode, svg_defs, font_cache, culling_bounds)); let _ = writeln!(svg, "{}", layer.render(transforms, svg_defs, render_data));
} }
} }
+14 -8
View File
@@ -1,5 +1,5 @@
use super::layer_info::LayerData; use super::layer_info::LayerData;
use super::style::ViewMode; use super::style::{RenderData, ViewMode};
use crate::intersection::{intersect_quad_bez_path, Quad}; use crate::intersection::{intersect_quad_bez_path, Quad};
use crate::layers::text_layer::FontCache; use crate::layers::text_layer::FontCache;
use crate::LayerId; use crate::LayerId;
@@ -9,6 +9,8 @@ use kurbo::{Affine, BezPath, Shape as KurboShape};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::fmt::Write; use std::fmt::Write;
mod base64_serde;
fn glam_to_kurbo(transform: DAffine2) -> Affine { fn glam_to_kurbo(transform: DAffine2) -> Affine {
Affine::new(transform.to_cols_array()) Affine::new(transform.to_cols_array())
} }
@@ -16,6 +18,7 @@ fn glam_to_kurbo(transform: DAffine2) -> Affine {
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub struct ImageLayer { pub struct ImageLayer {
pub mime: String, pub mime: String,
#[serde(serialize_with = "base64_serde::as_base64", deserialize_with = "base64_serde::from_base64")]
pub image_data: Vec<u8>, pub image_data: Vec<u8>,
#[serde(skip)] #[serde(skip)]
pub blob_url: Option<String>, pub blob_url: Option<String>,
@@ -24,8 +27,8 @@ pub struct ImageLayer {
} }
impl LayerData for ImageLayer { impl LayerData for ImageLayer {
fn render(&mut self, svg: &mut String, _svg_defs: &mut String, transforms: &mut Vec<DAffine2>, view_mode: ViewMode, _font_cache: &FontCache, _culling_bounds: Option<[DVec2; 2]>) { fn render(&mut self, svg: &mut String, _svg_defs: &mut String, transforms: &mut Vec<DAffine2>, render_data: RenderData) {
let transform = self.transform(transforms, view_mode); let transform = self.transform(transforms, render_data.view_mode);
let inverse = transform.inverse(); let inverse = transform.inverse();
if !inverse.is_finite() { if !inverse.is_finite() {
@@ -47,12 +50,15 @@ impl LayerData for ImageLayer {
.collect::<String>(); .collect::<String>();
let _ = write!( let _ = write!(
svg, svg,
r#"<image width="{}" height="{}" transform="matrix({})" xlink:href="{}" />"#, r#"<image width="{}" height="{}" transform="matrix({})" href=""#,
self.dimensions.x, self.dimensions.x, self.dimensions.y, svg_transform,
self.dimensions.y,
svg_transform,
self.blob_url.as_ref().unwrap_or(&String::new())
); );
if render_data.embed_images {
let _ = write!(svg, "data:{};base64,{}", self.mime, base64::encode(&self.image_data));
} else {
let _ = write!(svg, "{}", self.blob_url.as_ref().unwrap_or(&String::new()));
}
let _ = svg.write_str(r#""/>"#);
let _ = svg.write_str("</g>"); let _ = svg.write_str("</g>");
} }
@@ -0,0 +1,21 @@
//! Basic wrapper for [`serde`] for [`base64`] encoding
use serde::{Deserialize, Deserializer, Serializer};
pub fn as_base64<T, S>(key: &T, serializer: S) -> Result<S::Ok, S::Error>
where
T: AsRef<[u8]>,
S: Serializer,
{
serializer.serialize_str(&base64::encode(key.as_ref()))
}
pub fn from_base64<'a, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
where
D: Deserializer<'a>,
{
use serde::de::Error;
String::deserialize(deserializer)
.and_then(|string| base64::decode(&string).map_err(|err| Error::custom(err.to_string())))
.map_err(serde::de::Error::custom)
}
+50 -10
View File
@@ -2,13 +2,14 @@ use super::blend_mode::BlendMode;
use super::folder_layer::FolderLayer; use super::folder_layer::FolderLayer;
use super::image_layer::ImageLayer; use super::image_layer::ImageLayer;
use super::shape_layer::ShapeLayer; use super::shape_layer::ShapeLayer;
use super::style::{PathStyle, ViewMode}; use super::style::{PathStyle, RenderData};
use super::text_layer::TextLayer; use super::text_layer::TextLayer;
use crate::intersection::Quad; use crate::intersection::Quad;
use crate::layers::text_layer::FontCache; use crate::layers::text_layer::FontCache;
use crate::DocumentError; use crate::DocumentError;
use crate::LayerId; use crate::LayerId;
use core::fmt;
use glam::{DAffine2, DMat2, DVec2}; use glam::{DAffine2, DMat2, DVec2};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::fmt::Write; use std::fmt::Write;
@@ -46,6 +47,40 @@ impl LayerDataType {
} }
} }
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum LayerDataTypeDiscriminant {
Folder,
Shape,
Text,
Image,
}
impl fmt::Display for LayerDataTypeDiscriminant {
fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
let name = match self {
LayerDataTypeDiscriminant::Folder => "Folder",
LayerDataTypeDiscriminant::Shape => "Shape",
LayerDataTypeDiscriminant::Text => "Text",
LayerDataTypeDiscriminant::Image => "Image",
};
formatter.write_str(name)
}
}
impl From<&LayerDataType> for LayerDataTypeDiscriminant {
fn from(data: &LayerDataType) -> Self {
use LayerDataType::*;
match data {
Folder(_) => LayerDataTypeDiscriminant::Folder,
Shape(_) => LayerDataTypeDiscriminant::Shape,
Text(_) => LayerDataTypeDiscriminant::Text,
Image(_) => LayerDataTypeDiscriminant::Image,
}
}
}
/// Defines shared behavior for every layer type. /// Defines shared behavior for every layer type.
pub trait LayerData { pub trait LayerData {
/// Render the layer as an SVG tag to a given string. /// Render the layer as an SVG tag to a given string.
@@ -53,7 +88,7 @@ pub trait LayerData {
/// # Example /// # Example
/// ``` /// ```
/// # use graphite_graphene::layers::shape_layer::ShapeLayer; /// # use graphite_graphene::layers::shape_layer::ShapeLayer;
/// # use graphite_graphene::layers::style::{Fill, PathStyle, ViewMode}; /// # use graphite_graphene::layers::style::{Fill, PathStyle, ViewMode, RenderData};
/// # use graphite_graphene::layers::layer_info::LayerData; /// # use graphite_graphene::layers::layer_info::LayerData;
/// # use std::collections::HashMap; /// # use std::collections::HashMap;
/// ///
@@ -61,7 +96,9 @@ pub trait LayerData {
/// let mut svg = String::new(); /// let mut svg = String::new();
/// ///
/// // Render the shape without any transforms, in normal view mode /// // Render the shape without any transforms, in normal view mode
/// shape.render(&mut svg, &mut String::new(), &mut vec![], ViewMode::Normal, &Default::default(), None); /// # let font_cache = Default::default();
/// let render_data = RenderData::new(ViewMode::Normal, &font_cache, None, false);
/// shape.render(&mut svg, &mut String::new(), &mut vec![], render_data);
/// ///
/// assert_eq!( /// assert_eq!(
/// svg, /// svg,
@@ -70,7 +107,7 @@ pub trait LayerData {
/// </g>" /// </g>"
/// ); /// );
/// ``` /// ```
fn render(&mut self, svg: &mut String, svg_defs: &mut String, transforms: &mut Vec<glam::DAffine2>, view_mode: ViewMode, font_cache: &FontCache, culling_bounds: Option<[DVec2; 2]>); fn render(&mut self, svg: &mut String, svg_defs: &mut String, transforms: &mut Vec<glam::DAffine2>, render_data: RenderData);
/// Determine the layers within this layer that intersect a given quad. /// Determine the layers within this layer that intersect a given quad.
/// # Example /// # Example
@@ -117,8 +154,8 @@ pub trait LayerData {
} }
impl LayerData for LayerDataType { impl LayerData for LayerDataType {
fn render(&mut self, svg: &mut String, svg_defs: &mut String, transforms: &mut Vec<glam::DAffine2>, view_mode: ViewMode, font_cache: &FontCache, viewport_bounds: Option<[DVec2; 2]>) { fn render(&mut self, svg: &mut String, svg_defs: &mut String, transforms: &mut Vec<glam::DAffine2>, render_data: RenderData) {
self.inner_mut().render(svg, svg_defs, transforms, view_mode, font_cache, viewport_bounds) self.inner_mut().render(svg, svg_defs, transforms, render_data)
} }
fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>, font_cache: &FontCache) { fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>, font_cache: &FontCache) {
@@ -223,14 +260,17 @@ impl Layer {
LayerIter { stack: vec![self] } LayerIter { stack: vec![self] }
} }
pub fn render(&mut self, transforms: &mut Vec<DAffine2>, view_mode: ViewMode, svg_defs: &mut String, font_cache: &FontCache, culling_bounds: Option<[DVec2; 2]>) -> &str { pub fn render(&mut self, transforms: &mut Vec<DAffine2>, svg_defs: &mut String, render_data: RenderData) -> &str {
if !self.visible { if !self.visible {
return ""; return "";
} }
transforms.push(self.transform); transforms.push(self.transform);
if let Some(viewport_bounds) = culling_bounds { if let Some(viewport_bounds) = render_data.culling_bounds {
if let Some(bounding_box) = self.data.bounding_box(transforms.iter().cloned().reduce(|a, b| a * b).unwrap_or(DAffine2::IDENTITY), font_cache) { if let Some(bounding_box) = self
.data
.bounding_box(transforms.iter().cloned().reduce(|a, b| a * b).unwrap_or(DAffine2::IDENTITY), render_data.font_cache)
{
let is_overlapping = let is_overlapping =
viewport_bounds[0].x < bounding_box[1].x && bounding_box[0].x < viewport_bounds[1].x && viewport_bounds[0].y < bounding_box[1].y && bounding_box[0].y < viewport_bounds[1].y; viewport_bounds[0].x < bounding_box[1].x && bounding_box[0].x < viewport_bounds[1].x && viewport_bounds[0].y < bounding_box[1].y && bounding_box[0].y < viewport_bounds[1].y;
if !is_overlapping { if !is_overlapping {
@@ -245,7 +285,7 @@ impl Layer {
if self.cache_dirty { if self.cache_dirty {
self.thumbnail_cache.clear(); self.thumbnail_cache.clear();
self.svg_defs_cache.clear(); self.svg_defs_cache.clear();
self.data.render(&mut self.thumbnail_cache, &mut self.svg_defs_cache, transforms, view_mode, font_cache, culling_bounds); self.data.render(&mut self.thumbnail_cache, &mut self.svg_defs_cache, transforms, render_data);
self.cache.clear(); self.cache.clear();
let _ = writeln!(self.cache, r#"<g transform="matrix("#); let _ = writeln!(self.cache, r#"<g transform="matrix("#);
+4 -4
View File
@@ -1,5 +1,5 @@
use super::layer_info::LayerData; use super::layer_info::LayerData;
use super::style::{self, PathStyle, ViewMode}; use super::style::{self, PathStyle, RenderData, ViewMode};
use crate::intersection::{intersect_quad_bez_path, Quad}; use crate::intersection::{intersect_quad_bez_path, Quad};
use crate::layers::text_layer::FontCache; use crate::layers::text_layer::FontCache;
use crate::LayerId; use crate::LayerId;
@@ -32,13 +32,13 @@ pub struct ShapeLayer {
} }
impl LayerData for ShapeLayer { impl LayerData for ShapeLayer {
fn render(&mut self, svg: &mut String, svg_defs: &mut String, transforms: &mut Vec<DAffine2>, view_mode: ViewMode, _font_cache: &FontCache, _culling_bounds: Option<[DVec2; 2]>) { fn render(&mut self, svg: &mut String, svg_defs: &mut String, transforms: &mut Vec<DAffine2>, render_data: RenderData) {
let mut path = self.path.clone(); let mut path = self.path.clone();
let kurbo::Rect { x0, y0, x1, y1 } = path.bounding_box(); let kurbo::Rect { x0, y0, x1, y1 } = path.bounding_box();
let layer_bounds = [(x0, y0).into(), (x1, y1).into()]; let layer_bounds = [(x0, y0).into(), (x1, y1).into()];
let transform = self.transform(transforms, view_mode); let transform = self.transform(transforms, render_data.view_mode);
let inverse = transform.inverse(); let inverse = transform.inverse();
if !inverse.is_finite() { if !inverse.is_finite() {
let _ = write!(svg, "<!-- SVG shape has an invalid transform -->"); let _ = write!(svg, "<!-- SVG shape has an invalid transform -->");
@@ -58,7 +58,7 @@ impl LayerData for ShapeLayer {
svg, svg,
r#"<path d="{}" {} />"#, r#"<path d="{}" {} />"#,
path.to_svg(), path.to_svg(),
self.style.render(view_mode, svg_defs, transform, layer_bounds, transformed_bounds) self.style.render(render_data.view_mode, svg_defs, transform, layer_bounds, transformed_bounds)
); );
let _ = svg.write_str("</g>"); let _ = svg.write_str("</g>");
} }
+21
View File
@@ -1,5 +1,6 @@
//! Contains stylistic options for SVG elements. //! Contains stylistic options for SVG elements.
use super::text_layer::FontCache;
use crate::color::Color; use crate::color::Color;
use crate::consts::{LAYER_OUTLINE_STROKE_COLOR, LAYER_OUTLINE_STROKE_WEIGHT}; use crate::consts::{LAYER_OUTLINE_STROKE_COLOR, LAYER_OUTLINE_STROKE_WEIGHT};
@@ -36,6 +37,26 @@ impl Default for ViewMode {
} }
} }
/// Contains metadata for rendering the document as an svg
#[derive(Debug, Clone, Copy)]
pub struct RenderData<'a> {
pub view_mode: ViewMode,
pub font_cache: &'a FontCache,
pub culling_bounds: Option<[DVec2; 2]>,
pub embed_images: bool,
}
impl<'a> RenderData<'a> {
pub fn new(view_mode: ViewMode, font_cache: &'a FontCache, culling_bounds: Option<[DVec2; 2]>, embed_images: bool) -> Self {
Self {
view_mode,
font_cache,
culling_bounds,
embed_images,
}
}
}
#[derive(PartialEq, Eq, Clone, Copy, Debug, Hash, Serialize, Deserialize)] #[derive(PartialEq, Eq, Clone, Copy, Debug, Hash, Serialize, Deserialize)]
pub enum GradientType { pub enum GradientType {
Linear, Linear,
+16 -19
View File
@@ -1,5 +1,5 @@
use super::layer_info::LayerData; use super::layer_info::LayerData;
use super::style::{PathStyle, ViewMode}; use super::style::{PathStyle, RenderData, ViewMode};
use crate::intersection::{intersect_quad_bez_path, Quad}; use crate::intersection::{intersect_quad_bez_path, Quad};
use crate::LayerId; use crate::LayerId;
pub use font_cache::{Font, FontCache}; pub use font_cache::{Font, FontCache};
@@ -33,12 +33,12 @@ pub struct TextLayer {
#[serde(skip)] #[serde(skip)]
pub editable: bool, pub editable: bool,
#[serde(skip)] #[serde(skip)]
cached_path: Option<BezPath>, pub cached_path: Option<BezPath>,
} }
impl LayerData for TextLayer { impl LayerData for TextLayer {
fn render(&mut self, svg: &mut String, svg_defs: &mut String, transforms: &mut Vec<DAffine2>, view_mode: ViewMode, font_cache: &FontCache, _culling_bounds: Option<[DVec2; 2]>) { fn render(&mut self, svg: &mut String, svg_defs: &mut String, transforms: &mut Vec<DAffine2>, render_data: RenderData) {
let transform = self.transform(transforms, view_mode); let transform = self.transform(transforms, render_data.view_mode);
let inverse = transform.inverse(); let inverse = transform.inverse();
if !inverse.is_finite() { if !inverse.is_finite() {
@@ -53,8 +53,8 @@ impl LayerData for TextLayer {
let _ = svg.write_str(r#")">"#); let _ = svg.write_str(r#")">"#);
if self.editable { if self.editable {
let font = font_cache.resolve_font(&self.font); let font = render_data.font_cache.resolve_font(&self.font);
if let Some(url) = font.and_then(|font| font_cache.get_preview_url(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#"<style>@font-face {{font-family: local-font;src: url({});}}")</style>"#, url);
} }
@@ -70,7 +70,7 @@ impl LayerData for TextLayer {
font.map(|_| r#" style="font-family: local-font;""#).unwrap_or_default() font.map(|_| r#" style="font-family: local-font;""#).unwrap_or_default()
); );
} else { } else {
let buzz_face = self.load_face(font_cache); let buzz_face = self.load_face(render_data.font_cache);
let mut path = self.to_bez_path(buzz_face); let mut path = self.to_bez_path(buzz_face);
@@ -86,7 +86,7 @@ impl LayerData for TextLayer {
svg, svg,
r#"<path d="{}" {} />"#, r#"<path d="{}" {} />"#,
path.to_svg(), path.to_svg(),
self.path_style.render(view_mode, svg_defs, transform, bounds, transformed_bounds) self.path_style.render(render_data.view_mode, svg_defs, transform, bounds, transformed_bounds)
); );
} }
let _ = svg.write_str("</g>"); let _ = svg.write_str("</g>");
@@ -139,7 +139,7 @@ impl TextLayer {
cached_path: None, cached_path: None,
}; };
new.regenerate_path(new.load_face(font_cache)); new.cached_path = Some(new.generate_path(new.load_face(font_cache)));
new new
} }
@@ -147,8 +147,10 @@ impl TextLayer {
/// Converts to a [BezPath], populating the cache if necessary. /// Converts to a [BezPath], populating the cache if necessary.
#[inline] #[inline]
pub fn to_bez_path(&mut self, buzz_face: Option<Face>) -> BezPath { pub fn to_bez_path(&mut self, buzz_face: Option<Face>) -> BezPath {
if self.cached_path.is_none() { if self.cached_path.as_ref().filter(|x| !x.is_empty()).is_none() {
self.regenerate_path(buzz_face); let path = self.generate_path(buzz_face);
self.cached_path = Some(path.clone());
return path;
} }
self.cached_path.clone().unwrap() self.cached_path.clone().unwrap()
} }
@@ -158,11 +160,11 @@ impl TextLayer {
pub fn to_bez_path_nonmut(&self, font_cache: &FontCache) -> BezPath { pub fn to_bez_path_nonmut(&self, font_cache: &FontCache) -> BezPath {
let buzz_face = self.load_face(font_cache); let buzz_face = self.load_face(font_cache);
self.cached_path.clone().unwrap_or_else(|| self.generate_path(buzz_face)) self.cached_path.clone().filter(|x| !x.is_empty()).unwrap_or_else(|| self.generate_path(buzz_face))
} }
#[inline] #[inline]
fn generate_path(&self, buzz_face: Option<Face>) -> BezPath { pub fn generate_path(&self, buzz_face: Option<Face>) -> BezPath {
to_kurbo::to_kurbo(&self.text, buzz_face, self.size, self.line_width) to_kurbo::to_kurbo(&self.text, buzz_face, self.size, self.line_width)
} }
@@ -172,15 +174,10 @@ impl TextLayer {
Rect::new(0., 0., far.x, far.y) Rect::new(0., 0., far.x, far.y)
} }
/// Populate the cache.
pub fn regenerate_path(&mut self, buzz_face: Option<Face>) {
self.cached_path = Some(self.generate_path(buzz_face));
}
pub fn update_text(&mut self, text: String, font_cache: &FontCache) { pub fn update_text(&mut self, text: String, font_cache: &FontCache) {
let buzz_face = self.load_face(font_cache); let buzz_face = self.load_face(font_cache);
self.text = text; self.text = text;
self.regenerate_path(buzz_face); self.cached_path = Some(self.generate_path(buzz_face));
} }
} }