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:
0HyperCube
2022-04-21 09:50:44 +01:00
committed by Keavon Chambers
parent 916a575446
commit 239aa03453
36 changed files with 1029 additions and 253 deletions

View File

@@ -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,

View File

@@ -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(),

View File

@@ -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!(

View File

@@ -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);

View File

@@ -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)]

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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.