Font selection for text layers (#585)
* Add font dropdown * Add fonts * Font tool options * Fix tests * Replace http with https * Add variant selection * Do not embed default font * Use proxied font list API * Change default font to Merriweather * Remove outdated comment * Specify font once & load font into foreignobject * Fix tests * Rename variant to font_style * Change TextAreaInput to use FieldInput (WIP, breaks functionality) * Fix textarea functionality * Fix types * Add weight name mapping * Change labeling of "Italic" * Remove commented HTML node * Rename font "name" to "font_family" and "file" "font_file" * Fix errors * Fix fmt Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
parent
e01f0081a9
commit
a26679b96c
|
|
@ -64,8 +64,8 @@ impl Dispatcher {
|
|||
#[remain::unsorted]
|
||||
NoOp => {}
|
||||
Frontend(message) => {
|
||||
// Image data should be immediatly handled
|
||||
if let FrontendMessage::UpdateImageData { .. } = message {
|
||||
// Image and font loading should be immediately handled
|
||||
if let FrontendMessage::UpdateImageData { .. } | FrontendMessage::TriggerFontLoad { .. } = message {
|
||||
self.responses.push(message);
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
{"graphene_document":{"root":{"visible":true,"name":null,"data":{"Folder":{"next_assignment_id":919378319526168453,"layer_ids":[919378319526168452],"layers":[{"visible":true,"name":null,"data":{"Shape":{"path":[{"MoveTo":{"x":0.0,"y":0.0}},{"LineTo":{"x":1.0,"y":0.0}},{"LineTo":{"x":1.0,"y":1.0}},{"LineTo":{"x":0.0,"y":1.0}},"ClosePath"],"style":{"stroke":null,"fill":{"Solid":{"red":0.0,"green":0.0,"blue":0.0,"alpha":1.0}}},"render_index":1,"closed":true}},"transform":{"matrix2":[303.890625,0.0,-0.0,362.10546875],"translation":[-148.83984375,-235.8828125]},"blend_mode":"Normal","opacity":1.0}]}},"transform":{"matrix2":[1.0,0.0,0.0,1.0],"translation":[259.88359375,366.9]},"blend_mode":"Normal","opacity":1.0}},"saved_document_identifier":0,"name":"Untitled Document","layer_metadata":[[[919378319526168452],{"selected":true,"expanded":false}],[[],{"selected":false,"expanded":true}]],"layer_range_selection_reference":[919378319526168452],"movement_handler":{"pan":[-118.8,-45.60000000000001],"panning":false,"snap_tilt":false,"snap_tilt_released":false,"tilt":0.0,"tilting":false,"zoom":1.0,"zooming":false,"snap_zoom":false,"mouse_position":[0.0,0.0]},"artboard_message_handler":{"artboards_graphene_document":{"root":{"visible":true,"name":null,"data":{"Folder":{"next_assignment_id":0,"layer_ids":[],"layers":[]}},"transform":{"matrix2":[1.0,0.0,0.0,1.0],"translation":[259.88359375,366.9]},"blend_mode":"Normal","opacity":1.0}},"artboard_ids":[]},"properties_panel_message_handler":{"active_selection":[[919378319526168452],"Artwork"]},"overlays_visible":true,"snapping_enabled":true,"view_mode":"Normal","version":"0.0.5"}
|
||||
{"graphene_document":{"font_cache": {"data":{},"default":null}, "root":{"visible":true,"name":null,"data":{"Folder":{"next_assignment_id":919378319526168453,"layer_ids":[919378319526168452],"layers":[{"visible":true,"name":null,"data":{"Shape":{"path":[{"MoveTo":{"x":0.0,"y":0.0}},{"LineTo":{"x":1.0,"y":0.0}},{"LineTo":{"x":1.0,"y":1.0}},{"LineTo":{"x":0.0,"y":1.0}},"ClosePath"],"style":{"stroke":null,"fill":{"Solid":{"red":0.0,"green":0.0,"blue":0.0,"alpha":1.0}}},"render_index":1,"closed":true}},"transform":{"matrix2":[303.890625,0.0,-0.0,362.10546875],"translation":[-148.83984375,-235.8828125]},"blend_mode":"Normal","opacity":1.0}]}},"transform":{"matrix2":[1.0,0.0,0.0,1.0],"translation":[259.88359375,366.9]},"blend_mode":"Normal","opacity":1.0}},"saved_document_identifier":0,"name":"Untitled Document","layer_metadata":[[[919378319526168452],{"selected":true,"expanded":false}],[[],{"selected":false,"expanded":true}]],"layer_range_selection_reference":[919378319526168452],"movement_handler":{"pan":[-118.8,-45.60000000000001],"panning":false,"snap_tilt":false,"snap_tilt_released":false,"tilt":0.0,"tilting":false,"zoom":1.0,"zooming":false,"snap_zoom":false,"mouse_position":[0.0,0.0]},"artboard_message_handler":{"artboards_graphene_document":{"font_cache": {"data": {}}, "root":{"visible":true,"name":null,"data":{"Folder":{"next_assignment_id":0,"layer_ids":[],"layers":[]}},"transform":{"matrix2":[1.0,0.0,0.0,1.0],"translation":[259.88359375,366.9]},"blend_mode":"Normal","opacity":1.0}},"artboard_ids":[]},"properties_panel_message_handler":{"active_selection":[[919378319526168452],"Artwork"]},"overlays_visible":true,"snapping_enabled":true,"view_mode":"Normal","version":"0.0.6"}
|
||||
|
|
|
|||
|
|
@ -57,5 +57,5 @@ pub const FILE_EXPORT_SUFFIX: &str = ".svg";
|
|||
pub const COLOR_ACCENT: Color = Color::from_unsafe(0x00 as f32 / 255., 0xA8 as f32 / 255., 0xFF as f32 / 255.);
|
||||
|
||||
// Document
|
||||
pub const GRAPHITE_DOCUMENT_VERSION: &str = "0.0.5";
|
||||
pub const GRAPHITE_DOCUMENT_VERSION: &str = "0.0.6";
|
||||
pub const VIEWPORT_ZOOM_TO_FIT_PADDING_SCALE_FACTOR: f32 = 1.05;
|
||||
|
|
|
|||
|
|
@ -66,10 +66,18 @@ pub enum DocumentMessage {
|
|||
FolderChanged {
|
||||
affected_folder_path: Vec<LayerId>,
|
||||
},
|
||||
FontLoaded {
|
||||
font: String,
|
||||
data: Vec<u8>,
|
||||
is_default: bool,
|
||||
},
|
||||
GroupSelectedLayers,
|
||||
LayerChanged {
|
||||
affected_layer_path: Vec<LayerId>,
|
||||
},
|
||||
LoadFont {
|
||||
font: String,
|
||||
},
|
||||
MoveSelectedLayersTo {
|
||||
folder_path: Vec<LayerId>,
|
||||
insert_index: isize,
|
||||
|
|
|
|||
|
|
@ -162,7 +162,13 @@ impl DocumentMessageHandler {
|
|||
// TODO: Create VectorManipulatorShape when creating a kurbo shape as a stopgap, rather than on each new selection
|
||||
match &layer.ok()?.data {
|
||||
LayerDataType::Shape(shape) => Some(VectorShape::new(path_to_shape.to_vec(), viewport_transform, &shape.path, shape.closed, responses)),
|
||||
LayerDataType::Text(text) => Some(VectorShape::new(path_to_shape.to_vec(), viewport_transform, &text.to_bez_path_nonmut(), true, responses)),
|
||||
LayerDataType::Text(text) => Some(VectorShape::new(
|
||||
path_to_shape.to_vec(),
|
||||
viewport_transform,
|
||||
&text.to_bez_path_nonmut(&self.graphene_document.font_cache),
|
||||
true,
|
||||
responses,
|
||||
)),
|
||||
_ => None,
|
||||
}
|
||||
});
|
||||
|
|
@ -421,7 +427,7 @@ impl DocumentMessageHandler {
|
|||
.get_mut(&path)
|
||||
.ok_or_else(|| EditorError::Document(format!("Could not get layer metadata for {:?}", path)))?;
|
||||
let layer = self.graphene_document.layer(&path)?;
|
||||
let entry = layer_panel_entry(&data, self.graphene_document.multiply_transforms(&path)?, layer, path);
|
||||
let entry = layer_panel_entry(&data, self.graphene_document.multiply_transforms(&path)?, layer, path, &self.graphene_document.font_cache);
|
||||
Ok(entry)
|
||||
}
|
||||
|
||||
|
|
@ -442,7 +448,7 @@ impl DocumentMessageHandler {
|
|||
.ok()?;
|
||||
let layer = self.graphene_document.layer(path).ok()?;
|
||||
|
||||
Some(layer_panel_entry(layer_metadata, transform, layer, path.to_vec()))
|
||||
Some(layer_panel_entry(layer_metadata, transform, layer, path.to_vec(), &self.graphene_document.font_cache))
|
||||
}
|
||||
|
||||
/// When working with an insert index, deleting the layers may cause the insert index to point to a different location (if the layer being deleted was located before the insert index).
|
||||
|
|
@ -477,12 +483,12 @@ impl DocumentMessageHandler {
|
|||
/// Creates the blob URLs for the image data in the document
|
||||
pub fn load_image_data(&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>, responses: &mut VecDeque<Message>, image_data: &mut Vec<FrontendImageData>) {
|
||||
fn walk_layers(data: &LayerDataType, path: &mut Vec<LayerId>, image_data: &mut Vec<FrontendImageData>) {
|
||||
match data {
|
||||
LayerDataType::Folder(f) => {
|
||||
for (id, layer) in f.layer_ids.iter().zip(f.layers().iter()) {
|
||||
path.push(*id);
|
||||
walk_layers(&layer.data, path, responses, image_data);
|
||||
walk_layers(&layer.data, path, image_data);
|
||||
path.pop();
|
||||
}
|
||||
}
|
||||
|
|
@ -495,11 +501,17 @@ impl DocumentMessageHandler {
|
|||
}
|
||||
}
|
||||
|
||||
walk_layers(root, &mut path, responses, &mut image_data);
|
||||
walk_layers(root, &mut path, &mut image_data);
|
||||
if !image_data.is_empty() {
|
||||
responses.push_front(FrontendMessage::UpdateImageData { image_data }.into());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_default_font(&self, responses: &mut VecDeque<Message>) {
|
||||
if !self.graphene_document.font_cache.has_default() {
|
||||
responses.push_back(FrontendMessage::TriggerDefaultFontLoad.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PropertyHolder for DocumentMessageHandler {
|
||||
|
|
@ -897,6 +909,10 @@ impl MessageHandler<DocumentMessage, &InputPreprocessorMessageHandler> for Docum
|
|||
let affected_layer_path = affected_folder_path;
|
||||
responses.extend([LayerChanged { affected_layer_path }.into(), DocumentStructureChanged.into()]);
|
||||
}
|
||||
FontLoaded { font, data, is_default } => {
|
||||
self.graphene_document.font_cache.insert(font, data, is_default);
|
||||
responses.push_back(DocumentMessage::DirtyRenderDocument.into());
|
||||
}
|
||||
GroupSelectedLayers => {
|
||||
let mut new_folder_path = self.graphene_document.shallowest_common_folder(self.selected_layers()).unwrap_or(&[]).to_vec();
|
||||
|
||||
|
|
@ -932,6 +948,11 @@ impl MessageHandler<DocumentMessage, &InputPreprocessorMessageHandler> for Docum
|
|||
}
|
||||
responses.push_back(PropertiesPanelMessage::CheckSelectedWasUpdated { path: affected_layer_path }.into());
|
||||
}
|
||||
LoadFont { font } => {
|
||||
if !self.graphene_document.font_cache.loaded_font(&font) {
|
||||
responses.push_front(FrontendMessage::TriggerFontLoad { font }.into());
|
||||
}
|
||||
}
|
||||
MoveSelectedLayersTo {
|
||||
folder_path,
|
||||
insert_index,
|
||||
|
|
@ -1232,7 +1253,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessorMessageHandler> for Docum
|
|||
let text = self.graphene_document.layer(&path).unwrap().as_text().unwrap();
|
||||
responses.push_back(DocumentOperation::SetTextEditability { path, editable }.into());
|
||||
if editable {
|
||||
let color = if let Fill::Solid(solid_color) = text.style.fill() { *solid_color } else { Color::BLACK };
|
||||
let color = if let Fill::Solid(solid_color) = text.path_style.fill() { *solid_color } else { Color::BLACK };
|
||||
responses.push_back(
|
||||
FrontendMessage::DisplayEditableTextbox {
|
||||
text: text.text.clone(),
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
use graphene::document::FontCache;
|
||||
use graphene::layers::blend_mode::BlendMode;
|
||||
use graphene::layers::layer_info::{Layer, LayerData, LayerDataType};
|
||||
use graphene::layers::style::ViewMode;
|
||||
|
|
@ -20,14 +21,14 @@ impl LayerMetadata {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn layer_panel_entry(layer_metadata: &LayerMetadata, transform: DAffine2, layer: &Layer, path: Vec<LayerId>) -> LayerPanelEntry {
|
||||
pub fn layer_panel_entry(layer_metadata: &LayerMetadata, transform: DAffine2, layer: &Layer, path: Vec<LayerId>, font_cache: &FontCache) -> LayerPanelEntry {
|
||||
let name = layer.name.clone().unwrap_or_else(|| String::from(""));
|
||||
let arr = layer.data.bounding_box(transform).unwrap_or([DVec2::ZERO, DVec2::ZERO]);
|
||||
let arr = layer.data.bounding_box(transform, font_cache).unwrap_or([DVec2::ZERO, DVec2::ZERO]);
|
||||
let arr = arr.iter().map(|x| (*x).into()).collect::<Vec<(f64, f64)>>();
|
||||
|
||||
let mut thumbnail = String::new();
|
||||
let mut svg_defs = String::new();
|
||||
layer.data.clone().render(&mut thumbnail, &mut svg_defs, &mut vec![transform], ViewMode::Normal);
|
||||
layer.data.clone().render(&mut thumbnail, &mut svg_defs, &mut vec![transform], ViewMode::Normal, font_cache);
|
||||
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() {
|
||||
format!(
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ impl PortfolioMessageHandler {
|
|||
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*
|
||||
fn load_document(&mut self, new_document: DocumentMessageHandler, document_id: u64, replace_first_empty: bool, responses: &mut VecDeque<Message>) {
|
||||
// Special case when loading a document on an empty page
|
||||
if replace_first_empty && self.active_document().is_unmodified_default() {
|
||||
|
|
@ -79,6 +79,7 @@ impl PortfolioMessageHandler {
|
|||
);
|
||||
|
||||
new_document.load_image_data(responses, &new_document.graphene_document.root.data, Vec::new());
|
||||
new_document.load_default_font(responses);
|
||||
|
||||
self.documents.insert(document_id, new_document);
|
||||
|
||||
|
|
|
|||
|
|
@ -9,15 +9,40 @@ use serde::{Deserialize, Serialize};
|
|||
#[impl_message(Message, DocumentMessage, PropertiesPanel)]
|
||||
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum PropertiesPanelMessage {
|
||||
CheckSelectedWasDeleted { path: Vec<LayerId> },
|
||||
CheckSelectedWasUpdated { path: Vec<LayerId> },
|
||||
CheckSelectedWasDeleted {
|
||||
path: Vec<LayerId>,
|
||||
},
|
||||
CheckSelectedWasUpdated {
|
||||
path: Vec<LayerId>,
|
||||
},
|
||||
ClearSelection,
|
||||
ModifyFill { fill: Fill },
|
||||
ModifyName { name: String },
|
||||
ModifyStroke { stroke: Stroke },
|
||||
ModifyTransform { value: f64, transform_op: TransformOp },
|
||||
ModifyFill {
|
||||
fill: Fill,
|
||||
},
|
||||
ModifyFont {
|
||||
font_family: String,
|
||||
font_style: String,
|
||||
font_file: Option<String>,
|
||||
size: f64,
|
||||
},
|
||||
ModifyName {
|
||||
name: String,
|
||||
},
|
||||
ModifyStroke {
|
||||
stroke: Stroke,
|
||||
},
|
||||
ModifyText {
|
||||
new_text: String,
|
||||
},
|
||||
ModifyTransform {
|
||||
value: f64,
|
||||
transform_op: TransformOp,
|
||||
},
|
||||
ResendActiveProperties,
|
||||
SetActiveLayers { paths: Vec<Vec<LayerId>>, document: TargetDocument },
|
||||
SetActiveLayers {
|
||||
paths: Vec<Vec<LayerId>>,
|
||||
document: TargetDocument,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Copy, Debug, Serialize, Deserialize)]
|
||||
|
|
|
|||
|
|
@ -3,15 +3,16 @@ use super::utility_types::TargetDocument;
|
|||
use crate::document::properties_panel_message::TransformOp;
|
||||
use crate::layout::layout_message::LayoutTarget;
|
||||
use crate::layout::widgets::{
|
||||
ColorInput, IconLabel, LayoutRow, NumberInput, PopoverButton, RadioEntryData, RadioInput, Separator, SeparatorDirection, SeparatorType, TextInput, TextLabel, Widget, WidgetCallback, WidgetHolder,
|
||||
WidgetLayout,
|
||||
ColorInput, FontInput, IconLabel, LayoutRow, NumberInput, PopoverButton, RadioEntryData, RadioInput, Separator, SeparatorDirection, SeparatorType, TextAreaInput, TextInput, TextLabel, Widget,
|
||||
WidgetCallback, WidgetHolder, WidgetLayout,
|
||||
};
|
||||
use crate::message_prelude::*;
|
||||
|
||||
use graphene::color::Color;
|
||||
use graphene::document::Document as GrapheneDocument;
|
||||
use graphene::document::{Document as GrapheneDocument, FontCache};
|
||||
use graphene::layers::layer_info::{Layer, LayerDataType};
|
||||
use graphene::layers::style::{Fill, LineCap, LineJoin, Stroke};
|
||||
use graphene::layers::text_layer::TextLayer;
|
||||
use graphene::{LayerId, Operation};
|
||||
|
||||
use glam::{DAffine2, DVec2};
|
||||
|
|
@ -148,6 +149,23 @@ impl<'a> MessageHandler<PropertiesPanelMessage, PropertiesPanelMessageHandlerDat
|
|||
.into(),
|
||||
);
|
||||
}
|
||||
ModifyFont {
|
||||
font_family,
|
||||
font_style,
|
||||
font_file,
|
||||
size,
|
||||
} => {
|
||||
let (path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer");
|
||||
|
||||
responses.push_back(self.create_document_operation(Operation::ModifyFont {
|
||||
path,
|
||||
font_family,
|
||||
font_style,
|
||||
font_file,
|
||||
size,
|
||||
}));
|
||||
responses.push_back(ResendActiveProperties.into());
|
||||
}
|
||||
ModifyTransform { value, transform_op } => {
|
||||
let (path, target_document) = self.active_selection.as_ref().expect("Received update for properties panel with no active layer");
|
||||
let layer = get_document(*target_document).layer(path).unwrap();
|
||||
|
|
@ -162,8 +180,8 @@ impl<'a> MessageHandler<PropertiesPanelMessage, PropertiesPanelMessageHandlerDat
|
|||
};
|
||||
|
||||
let scale = match transform_op {
|
||||
Width => layer.bounding_transform().scale_x() / layer.transform.scale_x(),
|
||||
Height => layer.bounding_transform().scale_y() / layer.transform.scale_y(),
|
||||
Width => layer.bounding_transform(&get_document(*target_document).font_cache).scale_x() / layer.transform.scale_x(),
|
||||
Height => layer.bounding_transform(&get_document(*target_document).font_cache).scale_y() / layer.transform.scale_y(),
|
||||
_ => 1.,
|
||||
};
|
||||
|
||||
|
|
@ -184,6 +202,10 @@ impl<'a> MessageHandler<PropertiesPanelMessage, PropertiesPanelMessageHandlerDat
|
|||
let (path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer");
|
||||
responses.push_back(self.create_document_operation(Operation::SetLayerStroke { path, stroke }))
|
||||
}
|
||||
ModifyText { new_text } => {
|
||||
let (path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer");
|
||||
responses.push_back(Operation::SetTextContent { path, new_text }.into())
|
||||
}
|
||||
CheckSelectedWasUpdated { path } => {
|
||||
if self.matches_selected(&path) {
|
||||
responses.push_back(PropertiesPanelMessage::ResendActiveProperties.into())
|
||||
|
|
@ -212,8 +234,8 @@ impl<'a> MessageHandler<PropertiesPanelMessage, PropertiesPanelMessageHandlerDat
|
|||
let (path, target_document) = self.active_selection.clone().expect("Received update for properties panel with no active layer");
|
||||
let layer = get_document(target_document).layer(&path).unwrap();
|
||||
match target_document {
|
||||
TargetDocument::Artboard => register_artboard_layer_properties(layer, responses),
|
||||
TargetDocument::Artwork => register_artwork_layer_properties(layer, responses),
|
||||
TargetDocument::Artboard => register_artboard_layer_properties(layer, responses, &get_document(target_document).font_cache),
|
||||
TargetDocument::Artwork => register_artwork_layer_properties(layer, responses, &get_document(target_document).font_cache),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -224,7 +246,7 @@ impl<'a> MessageHandler<PropertiesPanelMessage, PropertiesPanelMessageHandlerDat
|
|||
}
|
||||
}
|
||||
|
||||
fn register_artboard_layer_properties(layer: &Layer, responses: &mut VecDeque<Message>) {
|
||||
fn register_artboard_layer_properties(layer: &Layer, responses: &mut VecDeque<Message>, font_cache: &FontCache) {
|
||||
let options_bar = vec![LayoutRow::Row {
|
||||
widgets: vec![
|
||||
WidgetHolder::new(Widget::IconLabel(IconLabel {
|
||||
|
|
@ -326,7 +348,7 @@ fn register_artboard_layer_properties(layer: &Layer, responses: &mut VecDeque<Me
|
|||
direction: SeparatorDirection::Horizontal,
|
||||
})),
|
||||
WidgetHolder::new(Widget::NumberInput(NumberInput {
|
||||
value: layer.bounding_transform().scale_x(),
|
||||
value: layer.bounding_transform(font_cache).scale_x(),
|
||||
label: "W".into(),
|
||||
unit: " px".into(),
|
||||
on_update: WidgetCallback::new(|number_input: &NumberInput| {
|
||||
|
|
@ -343,7 +365,7 @@ fn register_artboard_layer_properties(layer: &Layer, responses: &mut VecDeque<Me
|
|||
direction: SeparatorDirection::Horizontal,
|
||||
})),
|
||||
WidgetHolder::new(Widget::NumberInput(NumberInput {
|
||||
value: layer.bounding_transform().scale_y(),
|
||||
value: layer.bounding_transform(font_cache).scale_y(),
|
||||
label: "H".into(),
|
||||
unit: " px".into(),
|
||||
on_update: WidgetCallback::new(|number_input: &NumberInput| {
|
||||
|
|
@ -405,7 +427,7 @@ fn register_artboard_layer_properties(layer: &Layer, responses: &mut VecDeque<Me
|
|||
);
|
||||
}
|
||||
|
||||
fn register_artwork_layer_properties(layer: &Layer, responses: &mut VecDeque<Message>) {
|
||||
fn register_artwork_layer_properties(layer: &Layer, responses: &mut VecDeque<Message>, font_cache: &FontCache) {
|
||||
let options_bar = vec![LayoutRow::Row {
|
||||
widgets: vec![
|
||||
match &layer.data {
|
||||
|
|
@ -456,20 +478,21 @@ fn register_artwork_layer_properties(layer: &Layer, responses: &mut VecDeque<Mes
|
|||
let properties_body = match &layer.data {
|
||||
LayerDataType::Shape(shape) => {
|
||||
if let Some(fill_layout) = node_section_fill(shape.style.fill()) {
|
||||
vec![node_section_transform(layer), fill_layout, node_section_stroke(&shape.style.stroke().unwrap_or_default())]
|
||||
vec![node_section_transform(layer, font_cache), fill_layout, node_section_stroke(&shape.style.stroke().unwrap_or_default())]
|
||||
} else {
|
||||
vec![node_section_transform(layer), node_section_stroke(&shape.style.stroke().unwrap_or_default())]
|
||||
vec![node_section_transform(layer, font_cache), node_section_stroke(&shape.style.stroke().unwrap_or_default())]
|
||||
}
|
||||
}
|
||||
LayerDataType::Text(text) => {
|
||||
vec![
|
||||
node_section_transform(layer),
|
||||
node_section_fill(text.style.fill()).expect("Text should have fill"),
|
||||
node_section_stroke(&text.style.stroke().unwrap_or_default()),
|
||||
node_section_transform(layer, font_cache),
|
||||
node_section_font(text),
|
||||
node_section_fill(text.path_style.fill()).expect("Text should have fill"),
|
||||
node_section_stroke(&text.path_style.stroke().unwrap_or_default()),
|
||||
]
|
||||
}
|
||||
LayerDataType::Image(_) => {
|
||||
vec![node_section_transform(layer)]
|
||||
vec![node_section_transform(layer, font_cache)]
|
||||
}
|
||||
_ => {
|
||||
vec![]
|
||||
|
|
@ -492,7 +515,7 @@ fn register_artwork_layer_properties(layer: &Layer, responses: &mut VecDeque<Mes
|
|||
);
|
||||
}
|
||||
|
||||
fn node_section_transform(layer: &Layer) -> LayoutRow {
|
||||
fn node_section_transform(layer: &Layer, font_cache: &FontCache) -> LayoutRow {
|
||||
LayoutRow::Section {
|
||||
name: "Transform".into(),
|
||||
layout: vec![
|
||||
|
|
@ -616,7 +639,7 @@ fn node_section_transform(layer: &Layer) -> LayoutRow {
|
|||
direction: SeparatorDirection::Horizontal,
|
||||
})),
|
||||
WidgetHolder::new(Widget::NumberInput(NumberInput {
|
||||
value: layer.bounding_transform().scale_x(),
|
||||
value: layer.bounding_transform(font_cache).scale_x(),
|
||||
label: "W".into(),
|
||||
unit: " px".into(),
|
||||
on_update: WidgetCallback::new(|number_input: &NumberInput| {
|
||||
|
|
@ -633,7 +656,7 @@ fn node_section_transform(layer: &Layer) -> LayoutRow {
|
|||
direction: SeparatorDirection::Horizontal,
|
||||
})),
|
||||
WidgetHolder::new(Widget::NumberInput(NumberInput {
|
||||
value: layer.bounding_transform().scale_y(),
|
||||
value: layer.bounding_transform(font_cache).scale_y(),
|
||||
label: "H".into(),
|
||||
unit: " px".into(),
|
||||
on_update: WidgetCallback::new(|number_input: &NumberInput| {
|
||||
|
|
@ -651,6 +674,115 @@ fn node_section_transform(layer: &Layer) -> LayoutRow {
|
|||
}
|
||||
}
|
||||
|
||||
fn node_section_font(layer: &TextLayer) -> LayoutRow {
|
||||
let font_family = layer.font_family.clone();
|
||||
let font_style = layer.font_style.clone();
|
||||
let font_file = layer.font_file.clone();
|
||||
let size = layer.size;
|
||||
LayoutRow::Section {
|
||||
name: "Font".into(),
|
||||
layout: vec![
|
||||
LayoutRow::Row {
|
||||
widgets: vec![
|
||||
WidgetHolder::new(Widget::TextLabel(TextLabel {
|
||||
value: "Text".into(),
|
||||
..TextLabel::default()
|
||||
})),
|
||||
WidgetHolder::new(Widget::Separator(Separator {
|
||||
separator_type: SeparatorType::Unrelated,
|
||||
direction: SeparatorDirection::Horizontal,
|
||||
})),
|
||||
WidgetHolder::new(Widget::TextAreaInput(TextAreaInput {
|
||||
value: layer.text.clone(),
|
||||
on_update: WidgetCallback::new(|text_area: &TextAreaInput| PropertiesPanelMessage::ModifyText { new_text: text_area.value.clone() }.into()),
|
||||
})),
|
||||
],
|
||||
},
|
||||
LayoutRow::Row {
|
||||
widgets: vec![
|
||||
WidgetHolder::new(Widget::TextLabel(TextLabel {
|
||||
value: "Font".into(),
|
||||
..TextLabel::default()
|
||||
})),
|
||||
WidgetHolder::new(Widget::Separator(Separator {
|
||||
separator_type: SeparatorType::Unrelated,
|
||||
direction: SeparatorDirection::Horizontal,
|
||||
})),
|
||||
WidgetHolder::new(Widget::FontInput(FontInput {
|
||||
is_style_picker: false,
|
||||
font_family: layer.font_family.clone(),
|
||||
font_style: layer.font_style.clone(),
|
||||
font_file: String::new(),
|
||||
on_update: WidgetCallback::new(move |font_input: &FontInput| {
|
||||
PropertiesPanelMessage::ModifyFont {
|
||||
font_family: font_input.font_family.clone(),
|
||||
font_style: font_input.font_style.clone(),
|
||||
font_file: Some(font_input.font_file.clone()),
|
||||
size,
|
||||
}
|
||||
.into()
|
||||
}),
|
||||
})),
|
||||
],
|
||||
},
|
||||
LayoutRow::Row {
|
||||
widgets: vec![
|
||||
WidgetHolder::new(Widget::TextLabel(TextLabel {
|
||||
value: "Style".into(),
|
||||
..TextLabel::default()
|
||||
})),
|
||||
WidgetHolder::new(Widget::Separator(Separator {
|
||||
separator_type: SeparatorType::Unrelated,
|
||||
direction: SeparatorDirection::Horizontal,
|
||||
})),
|
||||
WidgetHolder::new(Widget::FontInput(FontInput {
|
||||
is_style_picker: true,
|
||||
font_family: layer.font_family.clone(),
|
||||
font_style: layer.font_style.clone(),
|
||||
font_file: String::new(),
|
||||
on_update: WidgetCallback::new(move |font_input: &FontInput| {
|
||||
PropertiesPanelMessage::ModifyFont {
|
||||
font_family: font_input.font_family.clone(),
|
||||
font_style: font_input.font_style.clone(),
|
||||
font_file: Some(font_input.font_file.clone()),
|
||||
size,
|
||||
}
|
||||
.into()
|
||||
}),
|
||||
})),
|
||||
],
|
||||
},
|
||||
LayoutRow::Row {
|
||||
widgets: vec![
|
||||
WidgetHolder::new(Widget::TextLabel(TextLabel {
|
||||
value: "Size".into(),
|
||||
..TextLabel::default()
|
||||
})),
|
||||
WidgetHolder::new(Widget::Separator(Separator {
|
||||
separator_type: SeparatorType::Unrelated,
|
||||
direction: SeparatorDirection::Horizontal,
|
||||
})),
|
||||
WidgetHolder::new(Widget::NumberInput(NumberInput {
|
||||
value: layer.size,
|
||||
min: Some(1.),
|
||||
unit: " px".into(),
|
||||
on_update: WidgetCallback::new(move |number_input: &NumberInput| {
|
||||
PropertiesPanelMessage::ModifyFont {
|
||||
font_family: font_family.clone(),
|
||||
font_style: font_style.clone(),
|
||||
font_file: font_file.clone(),
|
||||
size: number_input.value,
|
||||
}
|
||||
.into()
|
||||
}),
|
||||
..Default::default()
|
||||
})),
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
fn node_section_fill(fill: &Fill) -> Option<LayoutRow> {
|
||||
match fill {
|
||||
Fill::Solid(_) | Fill::None => Some(LayoutRow::Section {
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ impl MessageHandler<TransformLayerMessage, (&mut HashMap<Vec<LayerId>, LayerMeta
|
|||
selected.revert_operation();
|
||||
typing.clear();
|
||||
} else {
|
||||
*selected.pivot = selected.calculate_pivot();
|
||||
*selected.pivot = selected.calculate_pivot(&document.font_cache);
|
||||
}
|
||||
|
||||
*mouse_position = ipp.mouse.position;
|
||||
|
|
@ -128,7 +128,7 @@ impl MessageHandler<TransformLayerMessage, (&mut HashMap<Vec<LayerId>, LayerMeta
|
|||
self.transform_operation.apply_transform_operation(&mut selected, self.snap);
|
||||
}
|
||||
TransformOperation::Rotating(rotation) => {
|
||||
let selected_pivot = selected.calculate_pivot();
|
||||
let selected_pivot = selected.calculate_pivot(&document.font_cache);
|
||||
let angle = {
|
||||
let start_offset = self.mouse_position - selected_pivot;
|
||||
let end_offset = ipp.mouse.position - selected_pivot;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use crate::consts::{ROTATE_SNAP_ANGLE, SCALE_SNAP_INTERVAL};
|
||||
use crate::message_prelude::*;
|
||||
|
||||
use graphene::document::Document;
|
||||
use graphene::document::{Document, FontCache};
|
||||
use graphene::Operation as DocumentOperation;
|
||||
|
||||
use glam::{DAffine2, DVec2};
|
||||
|
|
@ -210,7 +210,7 @@ impl<'a> Selected<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn calculate_pivot(&mut self) -> DVec2 {
|
||||
pub fn calculate_pivot(&mut self, font_cache: &FontCache) -> DVec2 {
|
||||
let xy_summation = self
|
||||
.selected
|
||||
.iter()
|
||||
|
|
@ -221,7 +221,7 @@ impl<'a> Selected<'a> {
|
|||
.document
|
||||
.layer(path)
|
||||
.unwrap()
|
||||
.aabounding_box_for_transform(multiplied_transform)
|
||||
.aabounding_box_for_transform(multiplied_transform, font_cache)
|
||||
.unwrap_or([multiplied_transform.translation; 2]);
|
||||
|
||||
(bounds[0] + bounds[1]) / 2.
|
||||
|
|
|
|||
|
|
@ -24,8 +24,10 @@ pub enum FrontendMessage {
|
|||
DisplayRemoveEditableTextbox,
|
||||
|
||||
// Trigger prefix: cause a browser API to do something
|
||||
TriggerDefaultFontLoad,
|
||||
TriggerFileDownload { document: String, name: String },
|
||||
TriggerFileUpload,
|
||||
TriggerFontLoad { font: String },
|
||||
TriggerIndexedDbRemoveDocument { document_id: u64 },
|
||||
TriggerIndexedDbWriteDocument { document: String, details: FrontendDocumentDetails, version: String },
|
||||
TriggerTextCommit,
|
||||
|
|
|
|||
|
|
@ -91,12 +91,36 @@ impl MessageHandler<LayoutMessage, ()> for LayoutMessageHandler {
|
|||
let callback_message = (text_input.on_update.callback)(text_input);
|
||||
responses.push_back(callback_message);
|
||||
}
|
||||
Widget::TextAreaInput(text_area_input) => {
|
||||
let update_value = value.as_str().expect("TextAreaInput update was not of type: string");
|
||||
text_area_input.value = update_value.into();
|
||||
let callback_message = (text_area_input.on_update.callback)(text_area_input);
|
||||
responses.push_back(callback_message);
|
||||
}
|
||||
Widget::ColorInput(color_input) => {
|
||||
let update_value = value.as_str().map(String::from);
|
||||
color_input.value = update_value;
|
||||
let callback_message = (color_input.on_update.callback)(color_input);
|
||||
responses.push_back(callback_message);
|
||||
}
|
||||
Widget::FontInput(font_input) => {
|
||||
let update_value = value.as_object().expect("FontInput update was not of type: object");
|
||||
let font_family_value = update_value.get("fontFamily").expect("FontInput update does not have a fontFamily");
|
||||
let font_style_value = update_value.get("fontStyle").expect("FontInput update does not have a fontStyle");
|
||||
let font_file_value = update_value.get("fontFile").expect("FontInput update does not have a fontFile");
|
||||
|
||||
let font_family = font_family_value.as_str().expect("FontInput update fontFamily was not of type: string");
|
||||
let font_style = font_style_value.as_str().expect("FontInput update fontStyle was not of type: string");
|
||||
let font_file = font_file_value.as_str().expect("FontInput update fontFile was not of type: string");
|
||||
|
||||
font_input.font_family = font_family.into();
|
||||
font_input.font_style = font_style.into();
|
||||
font_input.font_file = font_file.into();
|
||||
|
||||
responses.push_back(DocumentMessage::LoadFont { font: font_file.into() }.into());
|
||||
let callback_message = (font_input.on_update.callback)(font_input);
|
||||
responses.push_back(callback_message);
|
||||
}
|
||||
Widget::TextLabel(_) => {}
|
||||
};
|
||||
self.send_layout(layout_target, responses);
|
||||
|
|
|
|||
|
|
@ -151,6 +151,7 @@ impl<T> Default for WidgetCallback<T> {
|
|||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum Widget {
|
||||
ColorInput(ColorInput),
|
||||
FontInput(FontInput),
|
||||
IconButton(IconButton),
|
||||
IconLabel(IconLabel),
|
||||
NumberInput(NumberInput),
|
||||
|
|
@ -158,6 +159,7 @@ pub enum Widget {
|
|||
PopoverButton(PopoverButton),
|
||||
RadioInput(RadioInput),
|
||||
Separator(Separator),
|
||||
TextAreaInput(TextAreaInput),
|
||||
TextInput(TextInput),
|
||||
TextLabel(TextLabel),
|
||||
}
|
||||
|
|
@ -200,6 +202,15 @@ pub struct TextInput {
|
|||
pub on_update: WidgetCallback<TextInput>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Derivative)]
|
||||
#[derivative(Debug, PartialEq, Default)]
|
||||
pub struct TextAreaInput {
|
||||
pub value: String,
|
||||
#[serde(skip)]
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
pub on_update: WidgetCallback<TextAreaInput>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Derivative)]
|
||||
#[derivative(Debug, PartialEq, Default)]
|
||||
pub struct ColorInput {
|
||||
|
|
@ -212,6 +223,22 @@ pub struct ColorInput {
|
|||
pub can_set_transparent: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Derivative)]
|
||||
#[derivative(Debug, PartialEq, Default)]
|
||||
pub struct FontInput {
|
||||
#[serde(rename = "isStyle")]
|
||||
pub is_style_picker: bool,
|
||||
#[serde(rename = "fontFamily")]
|
||||
pub font_family: String,
|
||||
#[serde(rename = "fontStyle")]
|
||||
pub font_style: String,
|
||||
#[serde(rename = "fontFile")]
|
||||
pub font_file: String,
|
||||
#[serde(skip)]
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
pub on_update: WidgetCallback<FontInput>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
|
||||
pub enum NumberInputIncrementBehavior {
|
||||
Add,
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ impl Default for GradientToolFsmState {
|
|||
|
||||
/// Computes the transform from gradient space to layer space (where gradient space is 0..1 in layer space)
|
||||
fn gradient_space_transform(path: &[LayerId], layer: &Layer, document: &DocumentMessageHandler) -> DAffine2 {
|
||||
let bounds = layer.aabounding_box_for_transform(DAffine2::IDENTITY).unwrap();
|
||||
let bounds = layer.aabounding_box_for_transform(DAffine2::IDENTITY, &document.graphene_document.font_cache).unwrap();
|
||||
let bound_transform = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]);
|
||||
|
||||
let multiplied = document.graphene_document.multiply_transforms(path).unwrap();
|
||||
|
|
|
|||
|
|
@ -407,7 +407,7 @@ impl Fsm for SelectToolFsmState {
|
|||
let selected = selected.iter().collect::<Vec<_>>();
|
||||
let mut selected = Selected::new(&mut bounds.original_transforms, &mut bounds.pivot, &selected, responses, &document.graphene_document);
|
||||
|
||||
*selected.pivot = selected.calculate_pivot();
|
||||
*selected.pivot = selected.calculate_pivot(&document.graphene_document.font_cache);
|
||||
}
|
||||
|
||||
data.layers_dragging = selected;
|
||||
|
|
|
|||
|
|
@ -3,12 +3,14 @@ use crate::document::DocumentMessageHandler;
|
|||
use crate::frontend::utility_types::MouseCursorIcon;
|
||||
use crate::input::keyboard::{Key, MouseMotion};
|
||||
use crate::input::InputPreprocessorMessageHandler;
|
||||
use crate::layout::widgets::{LayoutRow, NumberInput, PropertyHolder, Widget, WidgetCallback, WidgetHolder, WidgetLayout};
|
||||
use crate::layout::layout_message::LayoutTarget;
|
||||
use crate::layout::widgets::{FontInput, LayoutRow, NumberInput, PropertyHolder, Separator, SeparatorDirection, SeparatorType, Widget, WidgetCallback, WidgetHolder, WidgetLayout};
|
||||
use crate::message_prelude::*;
|
||||
use crate::misc::{HintData, HintGroup, HintInfo, KeysGroup};
|
||||
use crate::viewport_tools::tool::{DocumentToolData, Fsm, ToolActionHandlerData};
|
||||
|
||||
use glam::{DAffine2, DVec2};
|
||||
use graphene::document::FontCache;
|
||||
use graphene::intersection::Quad;
|
||||
use graphene::layers::style::{self, Fill, Stroke};
|
||||
use graphene::Operation;
|
||||
|
|
@ -24,11 +26,19 @@ pub struct TextTool {
|
|||
|
||||
pub struct TextOptions {
|
||||
font_size: u32,
|
||||
font_name: String,
|
||||
font_style: String,
|
||||
font_file: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for TextOptions {
|
||||
fn default() -> Self {
|
||||
Self { font_size: 14 }
|
||||
Self {
|
||||
font_size: 24,
|
||||
font_name: "Merriweather".into(),
|
||||
font_style: "Normal (400)".into(),
|
||||
font_file: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -58,21 +68,60 @@ pub enum TextMessage {
|
|||
#[remain::sorted]
|
||||
#[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)]
|
||||
pub enum TextOptionsUpdate {
|
||||
Font { family: String, style: String, file: String },
|
||||
FontSize(u32),
|
||||
}
|
||||
|
||||
impl PropertyHolder for TextTool {
|
||||
fn properties(&self) -> WidgetLayout {
|
||||
WidgetLayout::new(vec![LayoutRow::Row {
|
||||
widgets: vec![WidgetHolder::new(Widget::NumberInput(NumberInput {
|
||||
unit: " px".into(),
|
||||
label: "Font Size".into(),
|
||||
value: self.options.font_size as f64,
|
||||
is_integer: true,
|
||||
min: Some(1.),
|
||||
on_update: WidgetCallback::new(|number_input: &NumberInput| TextMessage::UpdateOptions(TextOptionsUpdate::FontSize(number_input.value as u32)).into()),
|
||||
..NumberInput::default()
|
||||
}))],
|
||||
widgets: vec![
|
||||
WidgetHolder::new(Widget::FontInput(FontInput {
|
||||
is_style_picker: false,
|
||||
font_family: self.options.font_name.clone(),
|
||||
font_style: self.options.font_style.clone(),
|
||||
on_update: WidgetCallback::new(|font_input: &FontInput| {
|
||||
TextMessage::UpdateOptions(TextOptionsUpdate::Font {
|
||||
family: font_input.font_family.clone(),
|
||||
style: font_input.font_style.clone(),
|
||||
file: font_input.font_file.clone(),
|
||||
})
|
||||
.into()
|
||||
}),
|
||||
..Default::default()
|
||||
})),
|
||||
WidgetHolder::new(Widget::Separator(Separator {
|
||||
direction: SeparatorDirection::Horizontal,
|
||||
separator_type: SeparatorType::Related,
|
||||
})),
|
||||
WidgetHolder::new(Widget::FontInput(FontInput {
|
||||
is_style_picker: true,
|
||||
font_family: self.options.font_name.clone(),
|
||||
font_style: self.options.font_style.clone(),
|
||||
on_update: WidgetCallback::new(|font_input: &FontInput| {
|
||||
TextMessage::UpdateOptions(TextOptionsUpdate::Font {
|
||||
family: font_input.font_family.clone(),
|
||||
style: font_input.font_style.clone(),
|
||||
file: font_input.font_file.clone(),
|
||||
})
|
||||
.into()
|
||||
}),
|
||||
..Default::default()
|
||||
})),
|
||||
WidgetHolder::new(Widget::Separator(Separator {
|
||||
direction: SeparatorDirection::Horizontal,
|
||||
separator_type: SeparatorType::Related,
|
||||
})),
|
||||
WidgetHolder::new(Widget::NumberInput(NumberInput {
|
||||
unit: " px".into(),
|
||||
label: "Size".into(),
|
||||
value: self.options.font_size as f64,
|
||||
is_integer: true,
|
||||
min: Some(1.),
|
||||
on_update: WidgetCallback::new(|number_input: &NumberInput| TextMessage::UpdateOptions(TextOptionsUpdate::FontSize(number_input.value as u32)).into()),
|
||||
..NumberInput::default()
|
||||
})),
|
||||
],
|
||||
}])
|
||||
}
|
||||
}
|
||||
|
|
@ -91,6 +140,13 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for TextTool {
|
|||
|
||||
if let ToolMessage::Text(TextMessage::UpdateOptions(action)) = action {
|
||||
match action {
|
||||
TextOptionsUpdate::Font { family, style, file } => {
|
||||
self.options.font_name = family;
|
||||
self.options.font_style = style;
|
||||
self.options.font_file = Some(file);
|
||||
|
||||
self.register_properties(responses, LayoutTarget::ToolOptions);
|
||||
}
|
||||
TextOptionsUpdate::FontSize(font_size) => self.options.font_size = font_size,
|
||||
}
|
||||
return;
|
||||
|
|
@ -155,25 +211,33 @@ fn resize_overlays(overlays: &mut Vec<Vec<LayerId>>, responses: &mut VecDeque<Me
|
|||
}
|
||||
}
|
||||
|
||||
fn update_overlays(document: &DocumentMessageHandler, data: &mut TextToolData, responses: &mut VecDeque<Message>) {
|
||||
fn update_overlays(document: &DocumentMessageHandler, data: &mut TextToolData, responses: &mut VecDeque<Message>, font_cache: &FontCache) {
|
||||
let visible_text_layers = document.selected_visible_text_layers().collect::<Vec<_>>();
|
||||
|
||||
resize_overlays(&mut data.overlays, responses, visible_text_layers.len());
|
||||
|
||||
for (layer_path, overlay_path) in visible_text_layers.into_iter().zip(&data.overlays) {
|
||||
let bounds = document
|
||||
.graphene_document
|
||||
.layer(layer_path)
|
||||
.unwrap()
|
||||
.aabounding_box_for_transform(document.graphene_document.multiply_transforms(layer_path).unwrap())
|
||||
.unwrap();
|
||||
let bounds = visible_text_layers
|
||||
.into_iter()
|
||||
.zip(&data.overlays)
|
||||
.filter_map(|(layer_path, overlay_path)| {
|
||||
document
|
||||
.graphene_document
|
||||
.layer(layer_path)
|
||||
.unwrap()
|
||||
.aabounding_box_for_transform(document.graphene_document.multiply_transforms(layer_path).unwrap(), font_cache)
|
||||
.map(|bounds| (bounds, overlay_path))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let new_len = bounds.len();
|
||||
|
||||
for (bounds, overlay_path) in bounds {
|
||||
let operation = Operation::SetLayerTransformInViewport {
|
||||
path: overlay_path.to_vec(),
|
||||
transform: transform_from_box(bounds[0], bounds[1]),
|
||||
};
|
||||
responses.push_back(DocumentMessage::Overlays(operation.into()).into());
|
||||
}
|
||||
resize_overlays(&mut data.overlays, responses, new_len);
|
||||
}
|
||||
|
||||
impl Fsm for TextToolFsmState {
|
||||
|
|
@ -196,7 +260,7 @@ impl Fsm for TextToolFsmState {
|
|||
if let ToolMessage::Text(event) = event {
|
||||
match (self, event) {
|
||||
(state, DocumentIsDirty) => {
|
||||
update_overlays(document, data, responses);
|
||||
update_overlays(document, data, responses, &document.graphene_document.font_cache);
|
||||
|
||||
state
|
||||
}
|
||||
|
|
@ -244,6 +308,9 @@ impl Fsm for TextToolFsmState {
|
|||
else if state == TextToolFsmState::Ready {
|
||||
let transform = DAffine2::from_translation(input.mouse.position).to_cols_array();
|
||||
let font_size = tool_options.font_size;
|
||||
let font_name = tool_options.font_name.clone();
|
||||
let font_style = tool_options.font_style.clone();
|
||||
let font_file = tool_options.font_file.clone();
|
||||
data.path = document.get_path_for_new_layer();
|
||||
|
||||
responses.push_back(
|
||||
|
|
@ -254,6 +321,9 @@ impl Fsm for TextToolFsmState {
|
|||
text: r#""#.to_string(),
|
||||
style: style::PathStyle::new(None, Fill::solid(tool_data.primary_color)),
|
||||
size: font_size as f64,
|
||||
font_name,
|
||||
font_style,
|
||||
font_file,
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
|
|
@ -329,7 +399,8 @@ impl Fsm for TextToolFsmState {
|
|||
}
|
||||
(Editing, UpdateBounds { new_text }) => {
|
||||
resize_overlays(&mut data.overlays, responses, 1);
|
||||
let mut path = document.graphene_document.layer(&data.path).unwrap().as_text().unwrap().bounding_box(&new_text).to_path(0.1);
|
||||
let text = document.graphene_document.layer(&data.path).unwrap().as_text().unwrap();
|
||||
let mut path = text.bounding_box(&new_text, text.load_face(&document.graphene_document.font_cache)).to_path(0.1);
|
||||
|
||||
fn glam_to_kurbo(transform: DAffine2) -> kurbo::Affine {
|
||||
kurbo::Affine::new(transform.to_cols_array())
|
||||
|
|
|
|||
|
|
@ -158,6 +158,10 @@ img {
|
|||
background-color: var(--color-6-lowergray);
|
||||
}
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-corner {
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
|
||||
.scrollable-x.scrollable-y {
|
||||
|
|
|
|||
|
|
@ -287,10 +287,14 @@ import {
|
|||
TriggerViewportResize,
|
||||
DisplayRemoveEditableTextbox,
|
||||
DisplayEditableTextbox,
|
||||
TriggerFontLoad,
|
||||
TriggerDefaultFontLoad,
|
||||
} from "@/dispatcher/js-messages";
|
||||
|
||||
import { textInputCleanup } from "@/lifetime/input";
|
||||
|
||||
import { loadDefaultFont, setLoadDefaultFontCallback } from "@/utilities/fonts";
|
||||
|
||||
import LayoutCol from "@/components/layout/LayoutCol.vue";
|
||||
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
||||
import IconButton from "@/components/widgets/buttons/IconButton.vue";
|
||||
|
|
@ -453,6 +457,14 @@ export default defineComponent({
|
|||
this.editor.dispatcher.subscribeJsMessage(TriggerTextCommit, () => {
|
||||
if (this.textInput) this.editor.instance.on_change_text(textInputCleanup(this.textInput.innerText));
|
||||
});
|
||||
this.editor.dispatcher.subscribeJsMessage(TriggerFontLoad, (triggerFontLoad) => {
|
||||
fetch(triggerFontLoad.font)
|
||||
.then((response) => response.arrayBuffer())
|
||||
.then((response) => {
|
||||
this.editor.instance.on_font_load(triggerFontLoad.font, new Uint8Array(response), false);
|
||||
});
|
||||
});
|
||||
this.editor.dispatcher.subscribeJsMessage(TriggerDefaultFontLoad, loadDefaultFont);
|
||||
this.editor.dispatcher.subscribeJsMessage(TriggerTextCopy, async (triggerTextCopy) => {
|
||||
// Clipboard API supported?
|
||||
if (!navigator.clipboard) return;
|
||||
|
|
@ -514,6 +526,7 @@ export default defineComponent({
|
|||
// TODO(mfish33): Replace with initialization system Issue:#524
|
||||
// Get initial Document Bar
|
||||
this.editor.instance.init_document_bar();
|
||||
setLoadDefaultFontCallback((font: string, data: Uint8Array) => this.editor.instance.on_font_load(font, data, true));
|
||||
},
|
||||
data() {
|
||||
const documentModeEntries: SectionsOfMenuListEntries = [
|
||||
|
|
|
|||
|
|
@ -15,7 +15,13 @@
|
|||
:incrementCallbackDecrease="() => updateLayout(component.widget_id, 'Decrement')"
|
||||
/>
|
||||
<TextInput v-if="component.kind === 'TextInput'" v-bind="component.props" @commitText="(value: string) => updateLayout(component.widget_id, value)" />
|
||||
<TextAreaInput v-if="component.kind === 'TextAreaInput'" v-bind="component.props" @commitText="(value: string) => updateLayout(component.widget_id, value)" />
|
||||
<ColorInput v-if="component.kind === 'ColorInput'" v-bind="component.props" @update:value="(value: string) => updateLayout(component.widget_id, value)" />
|
||||
<FontInput
|
||||
v-if="component.kind === 'FontInput'"
|
||||
v-bind="component.props"
|
||||
@changeFont="(value: { name: string, style: string, file: string }) => updateLayout(component.widget_id, value)"
|
||||
/>
|
||||
<IconButton v-if="component.kind === 'IconButton'" v-bind="component.props" :action="() => updateLayout(component.widget_id, null)" />
|
||||
<OptionalInput v-if="component.kind === 'OptionalInput'" v-bind="component.props" @update:checked="(value: boolean) => updateLayout(component.widget_id, value)" />
|
||||
<RadioInput v-if="component.kind === 'RadioInput'" v-bind="component.props" @update:selectedIndex="(value: number) => updateLayout(component.widget_id, value)" />
|
||||
|
|
@ -28,10 +34,24 @@
|
|||
|
||||
<style lang="scss">
|
||||
.widget-row {
|
||||
height: 32px;
|
||||
min-height: 32px;
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
> * {
|
||||
--widget-height: 24px;
|
||||
min-height: var(--widget-height);
|
||||
line-height: var(--widget-height);
|
||||
margin: calc((24px - var(--widget-height)) / 2 + 4px) 0;
|
||||
|
||||
&.icon-label.size-12 {
|
||||
--widget-height: 12px;
|
||||
}
|
||||
|
||||
&.icon-label.size-16 {
|
||||
--widget-height: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
|
@ -43,9 +63,11 @@ import { WidgetRow } from "@/dispatcher/js-messages";
|
|||
import IconButton from "@/components/widgets/buttons/IconButton.vue";
|
||||
import PopoverButton from "@/components/widgets/buttons/PopoverButton.vue";
|
||||
import ColorInput from "@/components/widgets/inputs/ColorInput.vue";
|
||||
import FontInput from "@/components/widgets/inputs/FontInput.vue";
|
||||
import NumberInput from "@/components/widgets/inputs/NumberInput.vue";
|
||||
import OptionalInput from "@/components/widgets/inputs/OptionalInput.vue";
|
||||
import RadioInput from "@/components/widgets/inputs/RadioInput.vue";
|
||||
import TextAreaInput from "@/components/widgets/inputs/TextAreaInput.vue";
|
||||
import TextInput from "@/components/widgets/inputs/TextInput.vue";
|
||||
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
|
||||
import TextLabel from "@/components/widgets/labels/TextLabel.vue";
|
||||
|
|
@ -73,6 +95,8 @@ export default defineComponent({
|
|||
TextLabel,
|
||||
IconLabel,
|
||||
ColorInput,
|
||||
FontInput,
|
||||
TextAreaInput,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
<template>
|
||||
<LayoutRow class="field-input" :class="{ disabled }">
|
||||
<input
|
||||
v-if="!textarea"
|
||||
:class="{ 'has-label': label }"
|
||||
:id="`field-input-${id}`"
|
||||
ref="input"
|
||||
|
|
@ -15,6 +16,22 @@
|
|||
@keydown.enter="() => $emit('textChanged')"
|
||||
@keydown.esc="() => $emit('cancelTextChange')"
|
||||
/>
|
||||
<textarea
|
||||
v-else
|
||||
:class="{ 'has-label': label }"
|
||||
:id="`field-input-${id}`"
|
||||
class="scrollable-y"
|
||||
data-scrollable-y
|
||||
ref="input"
|
||||
v-model="inputValue"
|
||||
:spellcheck="spellcheck"
|
||||
:disabled="disabled"
|
||||
@focus="() => $emit('textFocused')"
|
||||
@blur="() => $emit('textChanged')"
|
||||
@change="() => $emit('textChanged')"
|
||||
@keydown.ctrl.enter="() => $emit('textChanged')"
|
||||
@keydown.esc="() => $emit('cancelTextChange')"
|
||||
></textarea>
|
||||
<label v-if="label" :for="`field-input-${id}`">{{ label }}</label>
|
||||
<slot></slot>
|
||||
</LayoutRow>
|
||||
|
|
@ -23,7 +40,7 @@
|
|||
<style lang="scss">
|
||||
.field-input {
|
||||
min-width: 80px;
|
||||
height: 24px;
|
||||
height: auto;
|
||||
position: relative;
|
||||
border-radius: 2px;
|
||||
background: var(--color-1-nearblack);
|
||||
|
|
@ -43,7 +60,8 @@
|
|||
cursor: text;
|
||||
}
|
||||
|
||||
input {
|
||||
input,
|
||||
textarea {
|
||||
flex: 1 1 100%;
|
||||
width: 0;
|
||||
min-width: 30px;
|
||||
|
|
@ -55,6 +73,9 @@
|
|||
border: none;
|
||||
background: none;
|
||||
color: var(--color-e-nearwhite);
|
||||
}
|
||||
|
||||
input {
|
||||
text-align: center;
|
||||
|
||||
&:not(:focus).has-label {
|
||||
|
|
@ -72,11 +93,20 @@
|
|||
}
|
||||
}
|
||||
|
||||
textarea {
|
||||
min-height: calc(18px * 4);
|
||||
margin: 3px;
|
||||
padding: 0 5px;
|
||||
box-sizing: border-box;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
background: var(--color-2-mildblack);
|
||||
|
||||
label,
|
||||
input {
|
||||
input,
|
||||
textarea {
|
||||
color: var(--color-8-uppergray);
|
||||
}
|
||||
}
|
||||
|
|
@ -95,6 +125,7 @@ export default defineComponent({
|
|||
label: { type: String as PropType<string>, required: false },
|
||||
spellcheck: { type: Boolean as PropType<boolean>, default: false },
|
||||
disabled: { type: Boolean as PropType<boolean>, default: false },
|
||||
textarea: { type: Boolean as PropType<boolean>, default: false },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,165 @@
|
|||
<template>
|
||||
<LayoutRow class="dropdown-input">
|
||||
<LayoutRow class="dropdown-box" :class="{ disabled }" :style="{ minWidth: `${minWidth}px` }" @click="() => clickDropdownBox()" data-hover-menu-spawner>
|
||||
<span>{{ activeEntry.label }}</span>
|
||||
<IconLabel class="dropdown-arrow" :icon="'DropdownArrow'" />
|
||||
</LayoutRow>
|
||||
<MenuList
|
||||
v-model:activeEntry="activeEntry"
|
||||
@widthChanged="(newWidth: number) => onWidthChanged(newWidth)"
|
||||
:menuEntries="menuEntries"
|
||||
:direction="'Bottom'"
|
||||
:scrollableY="true"
|
||||
ref="menuList"
|
||||
/>
|
||||
</LayoutRow>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.dropdown-input {
|
||||
position: relative;
|
||||
|
||||
.dropdown-box {
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
background: var(--color-1-nearblack);
|
||||
height: 24px;
|
||||
border-radius: 2px;
|
||||
|
||||
.dropdown-icon {
|
||||
margin: 4px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
span {
|
||||
margin: 0;
|
||||
margin-left: 8px;
|
||||
flex: 1 1 100%;
|
||||
}
|
||||
|
||||
.dropdown-icon + span {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.dropdown-arrow {
|
||||
margin: 6px 2px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&.open {
|
||||
background: var(--color-6-lowergray);
|
||||
|
||||
span {
|
||||
color: var(--color-f-white);
|
||||
}
|
||||
|
||||
svg {
|
||||
fill: var(--color-f-white);
|
||||
}
|
||||
}
|
||||
|
||||
&.open {
|
||||
border-radius: 2px 2px 0 0;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
background: var(--color-2-mildblack);
|
||||
|
||||
span {
|
||||
color: var(--color-8-uppergray);
|
||||
}
|
||||
|
||||
svg {
|
||||
fill: var(--color-8-uppergray);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.menu-list .floating-menu-container .floating-menu-content {
|
||||
max-height: 400px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType } from "vue";
|
||||
|
||||
import { fontNames, getFontFile, getFontStyles } from "@/utilities/fonts";
|
||||
|
||||
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
||||
import MenuList, { MenuListEntry, SectionsOfMenuListEntries } from "@/components/widgets/floating-menus/MenuList.vue";
|
||||
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
|
||||
|
||||
export default defineComponent({
|
||||
emits: ["update:fontFamily", "update:fontStyle", "changeFont"],
|
||||
props: {
|
||||
fontFamily: { type: String as PropType<string>, required: true },
|
||||
fontStyle: { type: String as PropType<string>, required: true },
|
||||
disabled: { type: Boolean as PropType<boolean>, default: false },
|
||||
isStyle: { type: Boolean as PropType<boolean>, default: false },
|
||||
},
|
||||
data() {
|
||||
const { menuEntries, activeEntry } = this.updateEntries();
|
||||
return {
|
||||
menuEntries,
|
||||
activeEntry,
|
||||
minWidth: 0,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
clickDropdownBox() {
|
||||
if (!this.disabled) (this.$refs.menuList as typeof MenuList).setOpen();
|
||||
},
|
||||
selectFont(newName: string) {
|
||||
if (this.isStyle) this.$emit("update:fontStyle", newName);
|
||||
else this.$emit("update:fontFamily", newName);
|
||||
|
||||
{
|
||||
const fontFamily = this.isStyle ? this.fontFamily : newName;
|
||||
const fontStyle = this.isStyle ? newName : getFontStyles(newName)[0];
|
||||
const fontFile = getFontFile(fontFamily, fontStyle);
|
||||
this.$emit("changeFont", { fontFamily, fontStyle, fontFile });
|
||||
}
|
||||
},
|
||||
onWidthChanged(newWidth: number) {
|
||||
this.minWidth = newWidth;
|
||||
},
|
||||
updateEntries(): { menuEntries: SectionsOfMenuListEntries; activeEntry: MenuListEntry } {
|
||||
let selectedIndex = -1;
|
||||
const menuEntries: SectionsOfMenuListEntries = [
|
||||
(this.isStyle ? getFontStyles(this.fontFamily) : fontNames()).map((name, index) => {
|
||||
if (name === (this.isStyle ? this.fontStyle : this.fontFamily)) selectedIndex = index;
|
||||
|
||||
const result: MenuListEntry = {
|
||||
label: name,
|
||||
action: (): void => this.selectFont(name),
|
||||
};
|
||||
return result;
|
||||
}),
|
||||
];
|
||||
|
||||
const activeEntry = selectedIndex < 0 ? { label: "-" } : menuEntries.flat()[selectedIndex];
|
||||
|
||||
return { menuEntries, activeEntry };
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
fontFamily() {
|
||||
const { menuEntries, activeEntry } = this.updateEntries();
|
||||
this.menuEntries = menuEntries;
|
||||
this.activeEntry = activeEntry;
|
||||
},
|
||||
fontStyle() {
|
||||
const { menuEntries, activeEntry } = this.updateEntries();
|
||||
this.menuEntries = menuEntries;
|
||||
this.activeEntry = activeEntry;
|
||||
},
|
||||
},
|
||||
components: {
|
||||
IconLabel,
|
||||
MenuList,
|
||||
LayoutRow,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
<template>
|
||||
<FieldInput
|
||||
:textarea="true"
|
||||
class="text-area-input"
|
||||
:class="{ 'has-label': label }"
|
||||
v-model:value="inputValue"
|
||||
:label="label"
|
||||
:spellcheck="true"
|
||||
:disabled="disabled"
|
||||
@textFocused="() => onTextFocused()"
|
||||
@textChanged="() => onTextChanged()"
|
||||
@cancelTextChange="() => onCancelTextChange()"
|
||||
ref="fieldInput"
|
||||
></FieldInput>
|
||||
</template>
|
||||
|
||||
<style lang="scss"></style>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType } from "vue";
|
||||
|
||||
import FieldInput from "@/components/widgets/inputs/FieldInput.vue";
|
||||
|
||||
export default defineComponent({
|
||||
emits: ["update:value", "commitText"],
|
||||
props: {
|
||||
value: { type: String as PropType<string>, required: true },
|
||||
label: { type: String as PropType<string>, required: false },
|
||||
disabled: { type: Boolean as PropType<boolean>, default: false },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
editing: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
inputValue: {
|
||||
get() {
|
||||
return this.value;
|
||||
},
|
||||
set(value: string) {
|
||||
this.$emit("update:value", value);
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onTextFocused() {
|
||||
this.editing = true;
|
||||
},
|
||||
// Called only when `value` is changed from the <textarea> element via user input and committed, either
|
||||
// via the `change` event or when the <input> element is defocused (with the `blur` event binding)
|
||||
onTextChanged() {
|
||||
// The `inputElement.blur()` call in `onCancelTextChange()` causes itself to be run again, so this if statement skips a second run
|
||||
if (!this.editing) return;
|
||||
|
||||
this.onCancelTextChange();
|
||||
|
||||
// TODO: Find a less hacky way to do this
|
||||
const inputElement = (this.$refs.fieldInput as typeof FieldInput).$refs.input as HTMLTextAreaElement;
|
||||
this.$emit("commitText", inputElement.value);
|
||||
|
||||
// Required if value is not changed by the parent component upon update:value event
|
||||
inputElement.value = this.value;
|
||||
},
|
||||
onCancelTextChange() {
|
||||
this.editing = false;
|
||||
|
||||
const inputElement = (this.$refs.fieldInput as typeof FieldInput).$refs.input as HTMLTextAreaElement;
|
||||
inputElement.blur();
|
||||
},
|
||||
},
|
||||
components: { FieldInput },
|
||||
});
|
||||
</script>
|
||||
|
|
@ -388,6 +388,8 @@ export class IndexedDbDocumentDetails extends DocumentDetails {
|
|||
id!: string;
|
||||
}
|
||||
|
||||
export class TriggerDefaultFontLoad extends JsMessage {}
|
||||
|
||||
export class TriggerIndexedDbWriteDocument extends JsMessage {
|
||||
document!: string;
|
||||
|
||||
|
|
@ -403,6 +405,10 @@ export class TriggerIndexedDbRemoveDocument extends JsMessage {
|
|||
document_id!: string;
|
||||
}
|
||||
|
||||
export class TriggerFontLoad extends JsMessage {
|
||||
font!: string;
|
||||
}
|
||||
|
||||
export interface WidgetLayout {
|
||||
layout_target: unknown;
|
||||
layout: LayoutRow[];
|
||||
|
|
@ -427,7 +433,19 @@ export function isWidgetSection(layoutRow: WidgetRow | WidgetSection): layoutRow
|
|||
return Boolean((layoutRow as WidgetSection).layout);
|
||||
}
|
||||
|
||||
export type WidgetKind = "NumberInput" | "Separator" | "IconButton" | "PopoverButton" | "OptionalInput" | "RadioInput" | "TextInput" | "TextLabel" | "IconLabel" | "ColorInput";
|
||||
export type WidgetKind =
|
||||
| "NumberInput"
|
||||
| "Separator"
|
||||
| "IconButton"
|
||||
| "PopoverButton"
|
||||
| "OptionalInput"
|
||||
| "RadioInput"
|
||||
| "TextInput"
|
||||
| "TextAreaInput"
|
||||
| "TextLabel"
|
||||
| "IconLabel"
|
||||
| "ColorInput"
|
||||
| "FontInput";
|
||||
|
||||
export interface Widget {
|
||||
kind: WidgetKind;
|
||||
|
|
@ -522,9 +540,11 @@ export const messageConstructors: Record<string, MessageMaker> = {
|
|||
DisplayEditableTextbox,
|
||||
UpdateImageData,
|
||||
DisplayRemoveEditableTextbox,
|
||||
TriggerDefaultFontLoad,
|
||||
TriggerFileDownload,
|
||||
TriggerFileUpload,
|
||||
TriggerIndexedDbRemoveDocument,
|
||||
TriggerFontLoad,
|
||||
TriggerIndexedDbWriteDocument,
|
||||
TriggerTextCommit,
|
||||
TriggerTextCopy,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,81 @@
|
|||
const fontListAPI = "https://api.graphite.rs/font-list";
|
||||
|
||||
// Taken from https://developer.mozilla.org/en-US/docs/Web/CSS/font-weight#common_weight_name_mapping
|
||||
const weightNameMapping = new Map([
|
||||
[100, "Thin"],
|
||||
[200, "Extra Light"],
|
||||
[300, "Light"],
|
||||
[400, "Normal"],
|
||||
[500, "Medium"],
|
||||
[600, "Semi Bold"],
|
||||
[700, "Bold"],
|
||||
[800, "Extra Bold"],
|
||||
[900, "Black"],
|
||||
[950, "Extra Black"],
|
||||
]);
|
||||
|
||||
type fontCallbackType = (font: string, data: Uint8Array) => void;
|
||||
|
||||
let fontList = [] as { family: string; variants: string[]; files: Map<string, string> }[];
|
||||
let loadDefaultFontCallback = undefined as fontCallbackType | undefined;
|
||||
|
||||
fetch(fontListAPI)
|
||||
.then((response) => response.json())
|
||||
.then((json) => {
|
||||
const loadedFonts = json.items as { family: string; variants: string[]; files: { [name: string]: string } }[];
|
||||
fontList = loadedFonts.map((font) => {
|
||||
const { family } = font;
|
||||
const variants = font.variants.map(formatFontStyleName);
|
||||
const files = new Map(font.variants.map((x) => [formatFontStyleName(x), font.files[x]]));
|
||||
return { family, variants, files };
|
||||
});
|
||||
loadDefaultFont();
|
||||
});
|
||||
|
||||
function formatFontStyleName(fontStyle: string): string {
|
||||
const isItalic = fontStyle.endsWith("italic");
|
||||
const weight = fontStyle === "regular" || fontStyle === "italic" ? 400 : parseInt(fontStyle, 10);
|
||||
let weightName = "";
|
||||
|
||||
let bestWeight = Infinity;
|
||||
weightNameMapping.forEach((nameChecking, weightChecking) => {
|
||||
if (Math.abs(weightChecking - weight) < bestWeight) {
|
||||
bestWeight = Math.abs(weightChecking - weight);
|
||||
weightName = nameChecking;
|
||||
}
|
||||
});
|
||||
|
||||
return `${weightName}${isItalic ? " Italic" : ""} (${weight})`;
|
||||
}
|
||||
|
||||
export function loadDefaultFont(): void {
|
||||
const font = getFontFile("Merriweather", "Normal (400)");
|
||||
|
||||
if (font) {
|
||||
fetch(font)
|
||||
.then((response) => response.arrayBuffer())
|
||||
.then((response) => {
|
||||
if (loadDefaultFontCallback) loadDefaultFontCallback(font, new Uint8Array(response));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function setLoadDefaultFontCallback(callback: fontCallbackType): void {
|
||||
loadDefaultFontCallback = callback;
|
||||
loadDefaultFont();
|
||||
}
|
||||
|
||||
export function fontNames(): string[] {
|
||||
return fontList.map((value) => value.family);
|
||||
}
|
||||
|
||||
export function getFontStyles(fontFamily: string): string[] {
|
||||
const font = fontList.find((value) => value.family === fontFamily);
|
||||
return font ? font.variants : [];
|
||||
}
|
||||
|
||||
export function getFontFile(fontFamily: string, fontStyle: string): string | undefined {
|
||||
const font = fontList.find((value) => value.family === fontFamily);
|
||||
const fontFile = font && font.files.get(fontStyle);
|
||||
return fontFile && fontFile.replace("http://", "https://");
|
||||
}
|
||||
|
|
@ -325,6 +325,14 @@ impl JsEditorHandle {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// A font has been downloaded
|
||||
pub fn on_font_load(&self, font: String, data: Vec<u8>, is_default: bool) -> Result<(), JsValue> {
|
||||
let message = DocumentMessage::FontLoaded { font, data, is_default };
|
||||
self.dispatch(message);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A text box was changed
|
||||
pub fn update_bounds(&self, new_text: String) -> Result<(), JsValue> {
|
||||
let message = TextMessage::UpdateBounds { new_text };
|
||||
|
|
|
|||
|
|
@ -14,12 +14,50 @@ use kurbo::Affine;
|
|||
use serde::{Deserialize, Serialize};
|
||||
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).
|
||||
|
|
@ -28,6 +66,7 @@ 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 {
|
||||
|
|
@ -35,6 +74,7 @@ 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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -44,7 +84,7 @@ impl Document {
|
|||
pub fn render_root(&mut self, mode: ViewMode) -> String {
|
||||
let mut svg_defs = String::from("<defs>");
|
||||
|
||||
self.root.render(&mut vec![], mode, &mut svg_defs);
|
||||
self.root.render(&mut vec![], mode, &mut svg_defs, &self.font_cache);
|
||||
|
||||
svg_defs.push_str("</defs>");
|
||||
|
||||
|
|
@ -58,7 +98,7 @@ 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.layer(path).unwrap().intersects_quad(quad, path, intersections, &self.font_cache);
|
||||
}
|
||||
|
||||
/// Checks whether each layer under the root path intersects with the provided `quad` and returns the paths to all intersecting layers.
|
||||
|
|
@ -321,13 +361,13 @@ impl Document {
|
|||
pub fn viewport_bounding_box(&self, path: &[LayerId]) -> Result<Option<[DVec2; 2]>, DocumentError> {
|
||||
let layer = self.layer(path)?;
|
||||
let transform = self.multiply_transforms(path)?;
|
||||
Ok(layer.data.bounding_box(transform))
|
||||
Ok(layer.data.bounding_box(transform, &self.font_cache))
|
||||
}
|
||||
|
||||
pub fn bounding_box_and_transform(&self, path: &[LayerId]) -> 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).map(|bounds| (bounds, transform)))
|
||||
Ok(layer.data.bounding_box(layer.transform, &self.font_cache).map(|bounds| (bounds, transform)))
|
||||
}
|
||||
|
||||
pub fn visible_layers_bounding_box(&self) -> Option<[DVec2; 2]> {
|
||||
|
|
@ -491,11 +531,13 @@ impl Document {
|
|||
insert_index,
|
||||
transform,
|
||||
text,
|
||||
|
||||
style,
|
||||
size,
|
||||
font_name,
|
||||
font_style,
|
||||
font_file,
|
||||
} => {
|
||||
let layer = Layer::new(LayerDataType::Text(TextLayer::new(text, style, size)), transform);
|
||||
let layer = Layer::new(LayerDataType::Text(TextLayer::new(text, style, size, font_name, font_style, font_file, &self.font_cache)), transform);
|
||||
|
||||
self.set_layer(&path, layer, insert_index)?;
|
||||
|
||||
|
|
@ -520,7 +562,20 @@ impl Document {
|
|||
Some(vec![DocumentChanged])
|
||||
}
|
||||
Operation::SetTextContent { path, new_text } => {
|
||||
self.layer_mut(&path)?.as_text_mut()?.update_text(new_text);
|
||||
// Not using Document::layer_mut is necessary because we alson 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, &self.font_cache);
|
||||
|
||||
self.mark_as_dirty(&path)?;
|
||||
|
||||
Some([vec![DocumentChanged], update_thumbnails_upstream(&path)].concat())
|
||||
|
|
@ -679,6 +734,30 @@ impl Document {
|
|||
return Err(DocumentError::IndexOutOfBounds);
|
||||
}
|
||||
}
|
||||
Operation::ModifyFont {
|
||||
path,
|
||||
font_family,
|
||||
font_style,
|
||||
font_file,
|
||||
size,
|
||||
} => {
|
||||
// Not using Document::layer_mut is necessary because we alson 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_family = font_family;
|
||||
text.font_style = font_style;
|
||||
text.font_file = font_file;
|
||||
text.size = size;
|
||||
text.regenerate_path(text.load_face(&self.font_cache));
|
||||
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 }])
|
||||
|
|
@ -729,12 +808,20 @@ impl Document {
|
|||
self.set_transform_relative_to_viewport(&path, transform)?;
|
||||
self.mark_as_dirty(&path)?;
|
||||
|
||||
if let LayerDataType::Text(t) = &mut self.layer_mut(&path)?.data {
|
||||
let bezpath = t.to_bez_path();
|
||||
self.layer_mut(&path)?.data = layers::layer_info::LayerDataType::Shape(ShapeLayer::from_bez_path(bezpath, t.style.clone(), true));
|
||||
// Not using Document::layer_mut is necessary because we alson 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()))?;
|
||||
|
||||
if let LayerDataType::Text(t) = &mut layer_mut.data {
|
||||
let bezpath = t.to_bez_path(t.load_face(&self.font_cache));
|
||||
layer_mut.data = layers::layer_info::LayerDataType::Shape(ShapeLayer::from_bez_path(bezpath, t.path_style.clone(), true));
|
||||
}
|
||||
|
||||
if let LayerDataType::Shape(shape) = &mut self.layer_mut(&path)?.data {
|
||||
if let LayerDataType::Shape(shape) = &mut layer_mut.data {
|
||||
shape.path = bez_path;
|
||||
}
|
||||
Some([vec![DocumentChanged, LayerChanged { path: path.clone() }], update_thumbnails_upstream(&path)].concat())
|
||||
|
|
@ -795,7 +882,7 @@ impl Document {
|
|||
let layer = self.layer_mut(&path)?;
|
||||
match &mut layer.data {
|
||||
LayerDataType::Shape(s) => s.style = style,
|
||||
LayerDataType::Text(text) => text.style = style,
|
||||
LayerDataType::Text(text) => text.path_style = style,
|
||||
_ => return Err(DocumentError::NotAShape),
|
||||
}
|
||||
self.mark_as_dirty(&path)?;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
use super::layer_info::{Layer, LayerData, LayerDataType};
|
||||
use super::style::ViewMode;
|
||||
use crate::document::FontCache;
|
||||
use crate::intersection::Quad;
|
||||
use crate::{DocumentError, LayerId};
|
||||
|
||||
|
|
@ -21,24 +22,24 @@ pub struct FolderLayer {
|
|||
}
|
||||
|
||||
impl LayerData for FolderLayer {
|
||||
fn render(&mut self, svg: &mut String, svg_defs: &mut String, transforms: &mut Vec<glam::DAffine2>, view_mode: ViewMode) {
|
||||
fn render(&mut self, svg: &mut String, svg_defs: &mut String, transforms: &mut Vec<glam::DAffine2>, view_mode: ViewMode, font_cache: &FontCache) {
|
||||
for layer in &mut self.layers {
|
||||
let _ = writeln!(svg, "{}", layer.render(transforms, view_mode, svg_defs));
|
||||
let _ = writeln!(svg, "{}", layer.render(transforms, view_mode, svg_defs, font_cache));
|
||||
}
|
||||
}
|
||||
|
||||
fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>) {
|
||||
fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>, font_cache: &FontCache) {
|
||||
for (layer, layer_id) in self.layers().iter().zip(&self.layer_ids) {
|
||||
path.push(*layer_id);
|
||||
layer.intersects_quad(quad, path, intersections);
|
||||
layer.intersects_quad(quad, path, intersections, font_cache);
|
||||
path.pop();
|
||||
}
|
||||
}
|
||||
|
||||
fn bounding_box(&self, transform: glam::DAffine2) -> Option<[DVec2; 2]> {
|
||||
fn bounding_box(&self, transform: glam::DAffine2, font_cache: &FontCache) -> Option<[DVec2; 2]> {
|
||||
self.layers
|
||||
.iter()
|
||||
.filter_map(|layer| layer.data.bounding_box(transform * layer.transform))
|
||||
.filter_map(|layer| layer.data.bounding_box(transform * layer.transform, font_cache))
|
||||
.reduce(|a, b| [a[0].min(b[0]), a[1].max(b[1])])
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
use super::layer_info::LayerData;
|
||||
use super::style::ViewMode;
|
||||
use crate::document::FontCache;
|
||||
use crate::intersection::{intersect_quad_bez_path, Quad};
|
||||
use crate::LayerId;
|
||||
|
||||
|
|
@ -23,7 +24,7 @@ pub struct ImageLayer {
|
|||
}
|
||||
|
||||
impl LayerData for ImageLayer {
|
||||
fn render(&mut self, svg: &mut String, _svg_defs: &mut String, transforms: &mut Vec<DAffine2>, view_mode: ViewMode) {
|
||||
fn render(&mut self, svg: &mut String, _svg_defs: &mut String, transforms: &mut Vec<DAffine2>, view_mode: ViewMode, _font_cache: &FontCache) {
|
||||
let transform = self.transform(transforms, view_mode);
|
||||
let inverse = transform.inverse();
|
||||
|
||||
|
|
@ -55,7 +56,7 @@ impl LayerData for ImageLayer {
|
|||
let _ = svg.write_str("</g>");
|
||||
}
|
||||
|
||||
fn bounding_box(&self, transform: glam::DAffine2) -> Option<[DVec2; 2]> {
|
||||
fn bounding_box(&self, transform: glam::DAffine2, _font_cache: &FontCache) -> Option<[DVec2; 2]> {
|
||||
let mut path = self.bounds();
|
||||
|
||||
if transform.matrix2 == DMat2::ZERO {
|
||||
|
|
@ -67,7 +68,7 @@ impl LayerData for ImageLayer {
|
|||
Some([(x0, y0).into(), (x1, y1).into()])
|
||||
}
|
||||
|
||||
fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>) {
|
||||
fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>, _font_cache: &FontCache) {
|
||||
if intersect_quad_bez_path(quad, &self.bounds(), true) {
|
||||
intersections.push(path.clone());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ 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::DocumentError;
|
||||
use crate::LayerId;
|
||||
|
|
@ -54,12 +55,13 @@ pub trait LayerData {
|
|||
/// # use graphite_graphene::layers::shape_layer::ShapeLayer;
|
||||
/// # use graphite_graphene::layers::style::{Fill, PathStyle, ViewMode};
|
||||
/// # use graphite_graphene::layers::layer_info::LayerData;
|
||||
/// # use std::collections::HashMap;
|
||||
///
|
||||
/// let mut shape = ShapeLayer::rectangle(PathStyle::new(None, Fill::None));
|
||||
/// let mut svg = String::new();
|
||||
///
|
||||
/// // Render the shape without any transforms, in normal view mode
|
||||
/// shape.render(&mut svg, &mut String::new(), &mut vec![], ViewMode::Normal);
|
||||
/// shape.render(&mut svg, &mut String::new(), &mut vec![], ViewMode::Normal, &Default::default());
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// svg,
|
||||
|
|
@ -68,7 +70,7 @@ pub trait LayerData {
|
|||
/// </g>"
|
||||
/// );
|
||||
/// ```
|
||||
fn render(&mut self, svg: &mut String, svg_defs: &mut String, transforms: &mut Vec<glam::DAffine2>, view_mode: ViewMode);
|
||||
fn render(&mut self, svg: &mut String, svg_defs: &mut String, transforms: &mut Vec<glam::DAffine2>, view_mode: ViewMode, font_cache: &FontCache);
|
||||
|
||||
/// Determine the layers within this layer that intersect a given quad.
|
||||
/// # Example
|
||||
|
|
@ -78,6 +80,7 @@ pub trait LayerData {
|
|||
/// # use graphite_graphene::layers::layer_info::LayerData;
|
||||
/// # use graphite_graphene::intersection::Quad;
|
||||
/// # use glam::f64::{DAffine2, DVec2};
|
||||
/// # use std::collections::HashMap;
|
||||
///
|
||||
/// let mut shape = ShapeLayer::ellipse(PathStyle::new(None, Fill::None));
|
||||
/// let shape_id = 42;
|
||||
|
|
@ -86,11 +89,11 @@ pub trait LayerData {
|
|||
/// let quad = Quad::from_box([DVec2::ZERO, DVec2::ONE]);
|
||||
/// let mut intersections = vec![];
|
||||
///
|
||||
/// shape.intersects_quad(quad, &mut vec![shape_id], &mut intersections);
|
||||
/// shape.intersects_quad(quad, &mut vec![shape_id], &mut intersections, &Default::default());
|
||||
///
|
||||
/// assert_eq!(intersections, vec![vec![shape_id]]);
|
||||
/// ```
|
||||
fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>);
|
||||
fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>, font_cache: &FontCache);
|
||||
|
||||
// TODO: this doctest fails because 0 != 1e-32, maybe assert difference < epsilon?
|
||||
/// Calculate the bounding box for the layer's contents after applying a given transform.
|
||||
|
|
@ -100,29 +103,30 @@ pub trait LayerData {
|
|||
/// # use graphite_graphene::layers::style::{Fill, PathStyle};
|
||||
/// # use graphite_graphene::layers::layer_info::LayerData;
|
||||
/// # use glam::f64::{DAffine2, DVec2};
|
||||
/// # use std::collections::HashMap;
|
||||
/// let shape = ShapeLayer::ellipse(PathStyle::new(None, Fill::None));
|
||||
///
|
||||
/// // Calculate the bounding box without applying any transformations.
|
||||
/// // (The identity transform maps every vector to itself.)
|
||||
/// let transform = DAffine2::IDENTITY;
|
||||
/// let bounding_box = shape.bounding_box(transform);
|
||||
/// let bounding_box = shape.bounding_box(transform, &Default::default());
|
||||
///
|
||||
/// assert_eq!(bounding_box, Some([DVec2::ZERO, DVec2::ONE]));
|
||||
/// ```
|
||||
fn bounding_box(&self, transform: glam::DAffine2) -> Option<[DVec2; 2]>;
|
||||
fn bounding_box(&self, transform: glam::DAffine2, font_cache: &FontCache) -> Option<[DVec2; 2]>;
|
||||
}
|
||||
|
||||
impl LayerData for LayerDataType {
|
||||
fn render(&mut self, svg: &mut String, svg_defs: &mut String, transforms: &mut Vec<glam::DAffine2>, view_mode: ViewMode) {
|
||||
self.inner_mut().render(svg, svg_defs, transforms, view_mode)
|
||||
fn render(&mut self, svg: &mut String, svg_defs: &mut String, transforms: &mut Vec<glam::DAffine2>, view_mode: ViewMode, font_cache: &FontCache) {
|
||||
self.inner_mut().render(svg, svg_defs, transforms, view_mode, font_cache)
|
||||
}
|
||||
|
||||
fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>) {
|
||||
self.inner().intersects_quad(quad, path, intersections)
|
||||
fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>, font_cache: &FontCache) {
|
||||
self.inner().intersects_quad(quad, path, intersections, font_cache)
|
||||
}
|
||||
|
||||
fn bounding_box(&self, transform: glam::DAffine2) -> Option<[DVec2; 2]> {
|
||||
self.inner().bounding_box(transform)
|
||||
fn bounding_box(&self, transform: glam::DAffine2, font_cache: &FontCache) -> Option<[DVec2; 2]> {
|
||||
self.inner().bounding_box(transform, font_cache)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -219,7 +223,7 @@ impl Layer {
|
|||
LayerIter { stack: vec![self] }
|
||||
}
|
||||
|
||||
pub fn render(&mut self, transforms: &mut Vec<DAffine2>, view_mode: ViewMode, svg_defs: &mut String) -> &str {
|
||||
pub fn render(&mut self, transforms: &mut Vec<DAffine2>, view_mode: ViewMode, svg_defs: &mut String, font_cache: &FontCache) -> &str {
|
||||
if !self.visible {
|
||||
return "";
|
||||
}
|
||||
|
|
@ -228,7 +232,7 @@ impl Layer {
|
|||
transforms.push(self.transform);
|
||||
self.thumbnail_cache.clear();
|
||||
self.svg_defs_cache.clear();
|
||||
self.data.render(&mut self.thumbnail_cache, &mut self.svg_defs_cache, transforms, view_mode);
|
||||
self.data.render(&mut self.thumbnail_cache, &mut self.svg_defs_cache, transforms, view_mode, font_cache);
|
||||
|
||||
self.cache.clear();
|
||||
let _ = writeln!(self.cache, r#"<g transform="matrix("#);
|
||||
|
|
@ -250,13 +254,13 @@ impl Layer {
|
|||
self.cache.as_str()
|
||||
}
|
||||
|
||||
pub fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>) {
|
||||
pub fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>, font_cache: &FontCache) {
|
||||
if !self.visible {
|
||||
return;
|
||||
}
|
||||
|
||||
let transformed_quad = self.transform.inverse() * quad;
|
||||
self.data.intersects_quad(transformed_quad, path, intersections)
|
||||
self.data.intersects_quad(transformed_quad, path, intersections, font_cache)
|
||||
}
|
||||
|
||||
/// Compute the bounding box of the layer after applying a transform to it.
|
||||
|
|
@ -268,31 +272,32 @@ impl Layer {
|
|||
/// # use graphite_graphene::layers::style::PathStyle;
|
||||
/// # use glam::DVec2;
|
||||
/// # use glam::f64::DAffine2;
|
||||
/// # use std::collections::HashMap;
|
||||
/// // Create a rectangle with the default dimensions, from `(0|0)` to `(1|1)`
|
||||
/// let layer: Layer = ShapeLayer::rectangle(PathStyle::default()).into();
|
||||
///
|
||||
/// // Apply the Identity transform, which leaves the points unchanged
|
||||
/// assert_eq!(
|
||||
/// layer.aabounding_box_for_transform(DAffine2::IDENTITY),
|
||||
/// layer.aabounding_box_for_transform(DAffine2::IDENTITY, &Default::default()),
|
||||
/// Some([DVec2::ZERO, DVec2::ONE]),
|
||||
/// );
|
||||
///
|
||||
/// // Apply a transform that scales every point by a factor of two
|
||||
/// let transform = DAffine2::from_scale(DVec2::ONE * 2.);
|
||||
/// assert_eq!(
|
||||
/// layer.aabounding_box_for_transform(transform),
|
||||
/// layer.aabounding_box_for_transform(transform, &Default::default()),
|
||||
/// Some([DVec2::ZERO, DVec2::ONE * 2.]),
|
||||
/// );
|
||||
pub fn aabounding_box_for_transform(&self, transform: DAffine2) -> Option<[DVec2; 2]> {
|
||||
self.data.bounding_box(transform)
|
||||
pub fn aabounding_box_for_transform(&self, transform: DAffine2, font_cache: &FontCache) -> Option<[DVec2; 2]> {
|
||||
self.data.bounding_box(transform, font_cache)
|
||||
}
|
||||
|
||||
pub fn aabounding_box(&self) -> Option<[DVec2; 2]> {
|
||||
self.aabounding_box_for_transform(self.transform)
|
||||
pub fn aabounding_box(&self, font_cache: &FontCache) -> Option<[DVec2; 2]> {
|
||||
self.aabounding_box_for_transform(self.transform, font_cache)
|
||||
}
|
||||
|
||||
pub fn bounding_transform(&self) -> DAffine2 {
|
||||
let scale = match self.aabounding_box_for_transform(DAffine2::IDENTITY) {
|
||||
pub fn bounding_transform(&self, font_cache: &FontCache) -> DAffine2 {
|
||||
let scale = match self.aabounding_box_for_transform(DAffine2::IDENTITY, font_cache) {
|
||||
Some([a, b]) => {
|
||||
let dimensions = b - a;
|
||||
DAffine2::from_scale(dimensions)
|
||||
|
|
@ -360,7 +365,7 @@ impl Layer {
|
|||
pub fn style(&self) -> Result<&PathStyle, DocumentError> {
|
||||
match &self.data {
|
||||
LayerDataType::Shape(s) => Ok(&s.style),
|
||||
LayerDataType::Text(t) => Ok(&t.style),
|
||||
LayerDataType::Text(t) => Ok(&t.path_style),
|
||||
_ => Err(DocumentError::NotAShape),
|
||||
}
|
||||
}
|
||||
|
|
@ -368,7 +373,7 @@ 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.style),
|
||||
LayerDataType::Text(t) => Ok(&mut t.path_style),
|
||||
_ => Err(DocumentError::NotAShape),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
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::LayerId;
|
||||
|
||||
|
|
@ -30,7 +31,7 @@ pub struct ShapeLayer {
|
|||
}
|
||||
|
||||
impl LayerData for ShapeLayer {
|
||||
fn render(&mut self, svg: &mut String, svg_defs: &mut String, transforms: &mut Vec<DAffine2>, view_mode: ViewMode) {
|
||||
fn render(&mut self, svg: &mut String, svg_defs: &mut String, transforms: &mut Vec<DAffine2>, view_mode: ViewMode, _font_cache: &FontCache) {
|
||||
let mut path = self.path.clone();
|
||||
|
||||
let kurbo::Rect { x0, y0, x1, y1 } = path.bounding_box();
|
||||
|
|
@ -61,7 +62,7 @@ impl LayerData for ShapeLayer {
|
|||
let _ = svg.write_str("</g>");
|
||||
}
|
||||
|
||||
fn bounding_box(&self, transform: glam::DAffine2) -> Option<[DVec2; 2]> {
|
||||
fn bounding_box(&self, transform: glam::DAffine2, _font_cache: &FontCache) -> Option<[DVec2; 2]> {
|
||||
use kurbo::Shape;
|
||||
|
||||
let mut path = self.path.clone();
|
||||
|
|
@ -74,7 +75,7 @@ impl LayerData for ShapeLayer {
|
|||
Some([(x0, y0).into(), (x1, y1).into()])
|
||||
}
|
||||
|
||||
fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>) {
|
||||
fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>, _font_cache: &FontCache) {
|
||||
if intersect_quad_bez_path(quad, &self.path, self.style.fill().is_some()) {
|
||||
intersections.push(path.clone());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,93 +0,0 @@
|
|||
Copyright 2010, 2012, 2014 Adobe Systems Incorporated (http://www.adobe.com/), with Reserved Font Name ‘Source’.
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
Binary file not shown.
|
|
@ -1,10 +1,12 @@
|
|||
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;
|
||||
|
||||
use glam::{DAffine2, DMat2, DVec2};
|
||||
use kurbo::{Affine, BezPath, Rect, Shape};
|
||||
use rustybuzz::Face;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Write;
|
||||
|
||||
|
|
@ -17,16 +19,18 @@ fn glam_to_kurbo(transform: DAffine2) -> Affine {
|
|||
/// 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).
|
||||
/// Currently, the only supported font is `SourceSansPro-Regular`.
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
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 style: PathStyle,
|
||||
pub path_style: PathStyle,
|
||||
/// 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>,
|
||||
#[serde(skip)]
|
||||
pub editable: bool,
|
||||
#[serde(skip)]
|
||||
|
|
@ -34,7 +38,7 @@ pub struct TextLayer {
|
|||
}
|
||||
|
||||
impl LayerData for TextLayer {
|
||||
fn render(&mut self, svg: &mut String, svg_defs: &mut String, transforms: &mut Vec<DAffine2>, view_mode: ViewMode) {
|
||||
fn render(&mut self, svg: &mut String, svg_defs: &mut String, transforms: &mut Vec<DAffine2>, view_mode: ViewMode, font_cache: &FontCache) {
|
||||
let transform = self.transform(transforms, view_mode);
|
||||
let inverse = transform.inverse();
|
||||
|
||||
|
|
@ -50,18 +54,26 @@ 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 _ = write!(svg, r#"<style>@font-face {{font-family: local-font;src: url({});}}")</style>"#, url);
|
||||
}
|
||||
|
||||
let _ = write!(
|
||||
svg,
|
||||
r#"<foreignObject transform="matrix({})"></foreignObject>"#,
|
||||
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 mut path = self.to_bez_path();
|
||||
let buzz_face = self.load_face(font_cache);
|
||||
|
||||
let mut path = self.to_bez_path(buzz_face);
|
||||
|
||||
let kurbo::Rect { x0, y0, x1, y1 } = path.bounding_box();
|
||||
let bounds = [(x0, y0).into(), (x1, y1).into()];
|
||||
|
|
@ -75,14 +87,16 @@ impl LayerData for TextLayer {
|
|||
svg,
|
||||
r#"<path d="{}" {} />"#,
|
||||
path.to_svg(),
|
||||
self.style.render(view_mode, svg_defs, transform, bounds, transformed_bounds)
|
||||
self.path_style.render(view_mode, svg_defs, transform, bounds, transformed_bounds)
|
||||
);
|
||||
}
|
||||
let _ = svg.write_str("</g>");
|
||||
}
|
||||
|
||||
fn bounding_box(&self, transform: glam::DAffine2) -> Option<[DVec2; 2]> {
|
||||
let mut path = self.bounding_box(&self.text).to_path(0.1);
|
||||
fn bounding_box(&self, transform: glam::DAffine2, font_cache: &FontCache) -> Option<[DVec2; 2]> {
|
||||
let buzz_face = Some(self.load_face(font_cache)?);
|
||||
|
||||
let mut path = self.bounding_box(&self.text, buzz_face).to_path(0.1);
|
||||
|
||||
if transform.matrix2 == DMat2::ZERO {
|
||||
return None;
|
||||
|
|
@ -93,14 +107,20 @@ impl LayerData for TextLayer {
|
|||
Some([(x0, y0).into(), (x1, y1).into()])
|
||||
}
|
||||
|
||||
fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>) {
|
||||
if intersect_quad_bez_path(quad, &self.bounding_box(&self.text).to_path(0.), true) {
|
||||
fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>, font_cache: &FontCache) {
|
||||
let buzz_face = self.load_face(font_cache);
|
||||
|
||||
if intersect_quad_bez_path(quad, &self.bounding_box(&self.text, buzz_face).to_path(0.), true) {
|
||||
intersections.push(path.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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"))
|
||||
}
|
||||
|
||||
pub fn transform(&self, transforms: &[DAffine2], mode: ViewMode) -> DAffine2 {
|
||||
let start = match mode {
|
||||
ViewMode::Outline => 0,
|
||||
|
|
@ -109,61 +129,61 @@ 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) -> Self {
|
||||
pub fn new(text: String, style: PathStyle, size: f64, font_family: String, font_style: String, font_file: Option<String>, font_cache: &FontCache) -> Self {
|
||||
let mut new = Self {
|
||||
text,
|
||||
style,
|
||||
path_style: style,
|
||||
size,
|
||||
line_width: None,
|
||||
font_family,
|
||||
font_style,
|
||||
font_file,
|
||||
editable: false,
|
||||
cached_path: None,
|
||||
};
|
||||
|
||||
new.regenerate_path();
|
||||
new.regenerate_path(new.load_face(font_cache));
|
||||
|
||||
new
|
||||
}
|
||||
|
||||
/// Converts to a [BezPath], populating the cache if necessary.
|
||||
#[inline]
|
||||
pub fn to_bez_path(&mut self) -> BezPath {
|
||||
pub fn to_bez_path(&mut self, buzz_face: Option<Face>) -> BezPath {
|
||||
if self.cached_path.is_none() {
|
||||
self.regenerate_path();
|
||||
self.regenerate_path(buzz_face);
|
||||
}
|
||||
self.cached_path.clone().unwrap()
|
||||
}
|
||||
|
||||
/// Converts to a [BezPath], without populating the cache.
|
||||
#[inline]
|
||||
pub fn to_bez_path_nonmut(&self) -> BezPath {
|
||||
self.cached_path.clone().unwrap_or_else(|| self.generate_path())
|
||||
}
|
||||
pub fn to_bez_path_nonmut(&self, font_cache: &FontCache) -> BezPath {
|
||||
let buzz_face = self.load_face(font_cache);
|
||||
|
||||
/// Get the font face for `SourceSansPro-Regular`.
|
||||
/// For now, the font is hardcoded in the wasm binary.
|
||||
#[inline]
|
||||
fn font_face() -> rustybuzz::Face<'static> {
|
||||
rustybuzz::Face::from_slice(include_bytes!("SourceSansPro/SourceSansPro-Regular.ttf"), 0).unwrap()
|
||||
self.cached_path.clone().unwrap_or_else(|| self.generate_path(buzz_face))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn generate_path(&self) -> BezPath {
|
||||
to_kurbo::to_kurbo(&self.text, Self::font_face(), self.size, self.line_width)
|
||||
fn generate_path(&self, buzz_face: Option<Face>) -> BezPath {
|
||||
to_kurbo::to_kurbo(&self.text, buzz_face, self.size, self.line_width)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn bounding_box(&self, text: &str) -> Rect {
|
||||
let far = to_kurbo::bounding_box(text, Self::font_face(), self.size, self.line_width);
|
||||
pub fn bounding_box(&self, text: &str, buzz_face: Option<Face>) -> Rect {
|
||||
let far = to_kurbo::bounding_box(text, buzz_face, self.size, self.line_width);
|
||||
Rect::new(0., 0., far.x, far.y)
|
||||
}
|
||||
|
||||
/// Populate the cache.
|
||||
pub fn regenerate_path(&mut self) {
|
||||
self.cached_path = Some(self.generate_path());
|
||||
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) {
|
||||
pub fn update_text(&mut self, text: String, font_cache: &FontCache) {
|
||||
let buzz_face = self.load_face(font_cache);
|
||||
|
||||
self.text = text;
|
||||
self.regenerate_path();
|
||||
self.regenerate_path(buzz_face);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,7 +67,13 @@ fn wrap_word(line_width: Option<f64>, glyph_buffer: &GlyphBuffer, scale: f64, x_
|
|||
false
|
||||
}
|
||||
|
||||
pub fn to_kurbo(str: &str, buzz_face: rustybuzz::Face, font_size: f64, line_width: Option<f64>) -> BezPath {
|
||||
pub fn to_kurbo(str: &str, buzz_face: Option<rustybuzz::Face>, font_size: f64, line_width: Option<f64>) -> BezPath {
|
||||
let buzz_face = match buzz_face {
|
||||
Some(face) => face,
|
||||
// Show blank layer if font has not loaded
|
||||
None => return BezPath::default(),
|
||||
};
|
||||
|
||||
let (scale, line_height, mut buffer) = font_properties(&buzz_face, font_size);
|
||||
|
||||
let mut builder = Builder {
|
||||
|
|
@ -106,7 +112,13 @@ pub fn to_kurbo(str: &str, buzz_face: rustybuzz::Face, font_size: f64, line_widt
|
|||
builder.path
|
||||
}
|
||||
|
||||
pub fn bounding_box(str: &str, buzz_face: rustybuzz::Face, font_size: f64, line_width: Option<f64>) -> DVec2 {
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -53,6 +53,9 @@ pub enum Operation {
|
|||
text: String,
|
||||
style: style::PathStyle,
|
||||
size: f64,
|
||||
font_name: String,
|
||||
font_style: String,
|
||||
font_file: Option<String>,
|
||||
},
|
||||
AddImage {
|
||||
path: Vec<LayerId>,
|
||||
|
|
@ -119,6 +122,13 @@ pub enum Operation {
|
|||
DuplicateLayer {
|
||||
path: Vec<LayerId>,
|
||||
},
|
||||
ModifyFont {
|
||||
path: Vec<LayerId>,
|
||||
font_family: String,
|
||||
font_style: String,
|
||||
font_file: Option<String>,
|
||||
size: f64,
|
||||
},
|
||||
RenameLayer {
|
||||
layer_path: Vec<LayerId>,
|
||||
new_name: String,
|
||||
|
|
|
|||
Loading…
Reference in New Issue