mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-07-08 00:05:00 +00:00
Text spans
This commit is contained in:
parent
938a688fa0
commit
c2d0787bad
33 changed files with 1295 additions and 486 deletions
91
Cargo.lock
generated
91
Cargo.lock
generated
|
@ -1105,6 +1105,26 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cosmic-text"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75acbfb314aeb4f5210d379af45ed1ec2c98c7f1790bf57b8a4c562ac0c51b71"
|
||||
dependencies = [
|
||||
"fontdb 0.15.0",
|
||||
"libm",
|
||||
"log",
|
||||
"rangemap",
|
||||
"rustc-hash",
|
||||
"rustybuzz 0.11.0",
|
||||
"self_cell",
|
||||
"sys-locale 0.3.1",
|
||||
"unicode-bidi",
|
||||
"unicode-linebreak",
|
||||
"unicode-script",
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.12"
|
||||
|
@ -1681,6 +1701,19 @@ dependencies = [
|
|||
"roxmltree",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fontdb"
|
||||
version = "0.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "020e203f177c0fb250fb19455a252e838d2bbbce1f80f25ecc42402aafa8cd38"
|
||||
dependencies = [
|
||||
"log",
|
||||
"memmap2 0.8.0",
|
||||
"slotmap",
|
||||
"tinyvec",
|
||||
"ttf-parser 0.19.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fontdb"
|
||||
version = "0.16.2"
|
||||
|
@ -1689,7 +1722,7 @@ checksum = "b0299020c3ef3f60f526a4f64ab4a3d4ce116b1acbf24cdd22da0068e5d81dc3"
|
|||
dependencies = [
|
||||
"fontconfig-parser",
|
||||
"log",
|
||||
"memmap2",
|
||||
"memmap2 0.9.4",
|
||||
"slotmap",
|
||||
"tinyvec",
|
||||
"ttf-parser 0.20.0",
|
||||
|
@ -2331,6 +2364,7 @@ dependencies = [
|
|||
"base64 0.21.7",
|
||||
"bezier-rs",
|
||||
"bytemuck",
|
||||
"cosmic-text",
|
||||
"dyn-any",
|
||||
"glam",
|
||||
"image",
|
||||
|
@ -2342,11 +2376,11 @@ dependencies = [
|
|||
"num-traits",
|
||||
"rand 0.8.5",
|
||||
"rand_chacha 0.3.1",
|
||||
"rustybuzz 0.10.0",
|
||||
"serde",
|
||||
"specta",
|
||||
"spirv-std",
|
||||
"tokio",
|
||||
"unicode-segmentation",
|
||||
"usvg",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
|
@ -3404,6 +3438,15 @@ version = "2.7.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149"
|
||||
|
||||
[[package]]
|
||||
name = "memmap2"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43a5a03cefb0d953ec0be133036f14e109412fa594edc2f77227249db66cc3ed"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memmap2"
|
||||
version = "0.9.4"
|
||||
|
@ -4610,6 +4653,12 @@ version = "0.1.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c8a99fddc9f0ba0a85884b8d14e3592853e787d581ca1816c91349b10e4eeab"
|
||||
|
||||
[[package]]
|
||||
name = "rangemap"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "795915a3930a5d6bafd9053d37602fea3e61be2e5d4d788983a8ba9654c1c6f2"
|
||||
|
||||
[[package]]
|
||||
name = "raw-window-handle"
|
||||
version = "0.5.2"
|
||||
|
@ -4952,14 +5001,15 @@ checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4"
|
|||
|
||||
[[package]]
|
||||
name = "rustybuzz"
|
||||
version = "0.10.0"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "71cd15fef9112a1f94ac64b58d1e4628192631ad6af4dc69997f995459c874e7"
|
||||
checksum = "2ee8fe2a8461a0854a37101fe7a1b13998d0cfa987e43248e81d2a5f4570f6fa"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"bytemuck",
|
||||
"libm",
|
||||
"smallvec",
|
||||
"ttf-parser 0.19.2",
|
||||
"ttf-parser 0.20.0",
|
||||
"unicode-bidi-mirroring",
|
||||
"unicode-ccc",
|
||||
"unicode-properties",
|
||||
|
@ -5051,7 +5101,7 @@ checksum = "82b2eaf3a5b264a521b988b2e73042e742df700c4f962cde845d1541adb46550"
|
|||
dependencies = [
|
||||
"ab_glyph",
|
||||
"log",
|
||||
"memmap2",
|
||||
"memmap2 0.9.4",
|
||||
"smithay-client-toolkit",
|
||||
"tiny-skia",
|
||||
]
|
||||
|
@ -5099,6 +5149,12 @@ dependencies = [
|
|||
"thin-slice",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "self_cell"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "58bf37232d3bb9a2c4e641ca2a11d83b5062066f88df7fed36c28772046d65ba"
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.22"
|
||||
|
@ -5430,7 +5486,7 @@ dependencies = [
|
|||
"cursor-icon",
|
||||
"libc",
|
||||
"log",
|
||||
"memmap2",
|
||||
"memmap2 0.9.4",
|
||||
"rustix 0.38.31",
|
||||
"thiserror",
|
||||
"wayland-backend",
|
||||
|
@ -5686,6 +5742,15 @@ dependencies = [
|
|||
"windows-sys 0.45.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sys-locale"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e801cf239ecd6ccd71f03d270d67dd53d13e90aab208bf4b8fe4ad957ea949b0"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-configuration"
|
||||
version = "0.5.1"
|
||||
|
@ -5856,7 +5921,7 @@ dependencies = [
|
|||
"serialize-to-javascript",
|
||||
"shared_child",
|
||||
"state",
|
||||
"sys-locale",
|
||||
"sys-locale 0.2.4",
|
||||
"tar",
|
||||
"tauri-macros",
|
||||
"tauri-runtime",
|
||||
|
@ -6516,6 +6581,12 @@ version = "1.0.12"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-linebreak"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-normalization"
|
||||
version = "0.1.23"
|
||||
|
@ -6588,7 +6659,7 @@ dependencies = [
|
|||
"base64 0.21.7",
|
||||
"data-url",
|
||||
"flate2",
|
||||
"fontdb",
|
||||
"fontdb 0.16.2",
|
||||
"imagesize",
|
||||
"kurbo 0.9.5",
|
||||
"log",
|
||||
|
@ -7689,7 +7760,7 @@ dependencies = [
|
|||
"js-sys",
|
||||
"libc",
|
||||
"log",
|
||||
"memmap2",
|
||||
"memmap2 0.9.4",
|
||||
"ndk 0.8.0",
|
||||
"ndk-sys 0.5.0+25.2.9519653",
|
||||
"objc2",
|
||||
|
|
|
@ -72,7 +72,7 @@ glam = { version = "0.25", default-features = false, features = ["serde"] }
|
|||
node-macro = { path = "node-graph/node-macro" }
|
||||
base64 = { version = "0.21" }
|
||||
image = { version = "0.24", default-features = false, features = ["png"] }
|
||||
rustybuzz = { version = "0.10.0" }
|
||||
cosmic-text = { version = "0.10", default-features = false, features = ["std"] }
|
||||
num-derive = { version = "0.4" }
|
||||
num-traits = { version = "0.2.15", default-features = false, features = [
|
||||
"i128",
|
||||
|
|
|
@ -6,7 +6,6 @@ use crate::messages::prelude::*;
|
|||
use crate::messages::tool::utility_types::HintData;
|
||||
|
||||
use graph_craft::document::NodeId;
|
||||
use graphene_core::raster::color::Color;
|
||||
use graphene_core::text::Font;
|
||||
|
||||
#[impl_message(Message, Frontend)]
|
||||
|
@ -24,15 +23,6 @@ pub enum FrontendMessage {
|
|||
},
|
||||
DisplayEditableTextbox {
|
||||
text: String,
|
||||
#[serde(rename = "lineWidth")]
|
||||
line_width: Option<f64>,
|
||||
#[serde(rename = "fontSize")]
|
||||
font_size: f64,
|
||||
color: Color,
|
||||
url: String,
|
||||
transform: [f64; 6],
|
||||
},
|
||||
DisplayEditableTextboxTransform {
|
||||
transform: [f64; 6],
|
||||
},
|
||||
DisplayRemoveEditableTextbox,
|
||||
|
@ -94,7 +84,6 @@ pub enum FrontendMessage {
|
|||
TriggerSavePreferences {
|
||||
preferences: PreferencesMessageHandler,
|
||||
},
|
||||
TriggerTextCommit,
|
||||
TriggerTextCopy {
|
||||
#[serde(rename = "copyText")]
|
||||
copy_text: String,
|
||||
|
|
|
@ -134,6 +134,8 @@ pub fn default_mapping() -> Mapping {
|
|||
entry!(KeyDown(Escape); action_dispatch=EyedropperToolMessage::Abort),
|
||||
//
|
||||
// TextToolMessage
|
||||
entry!(KeyDown(Lmb); action_dispatch=TextToolMessage::Select),
|
||||
entry!(PointerMove; action_dispatch=TextToolMessage::Drag),
|
||||
entry!(KeyUp(Lmb); action_dispatch=TextToolMessage::Interact),
|
||||
entry!(KeyDown(Escape); action_dispatch=TextToolMessage::Abort),
|
||||
entry!(KeyDown(Enter); modifiers=[Accel], action_dispatch=TextToolMessage::CommitText),
|
||||
|
|
|
@ -8,7 +8,6 @@ use graph_craft::document::DocumentNode;
|
|||
use graph_craft::document::NodeId;
|
||||
use graphene_core::raster::BlendMode;
|
||||
use graphene_core::raster::ImageFrame;
|
||||
use graphene_core::text::Font;
|
||||
use graphene_core::uuid::ManipulatorGroupId;
|
||||
use graphene_core::vector::brush_stroke::BrushStroke;
|
||||
use graphene_core::vector::style::{Fill, Stroke};
|
||||
|
@ -89,9 +88,7 @@ pub enum GraphOperationMessage {
|
|||
},
|
||||
NewTextLayer {
|
||||
id: NodeId,
|
||||
text: String,
|
||||
font: Font,
|
||||
size: f64,
|
||||
text: graphene_core::text::RichText,
|
||||
parent: LayerNodeIdentifier,
|
||||
insert_index: isize,
|
||||
},
|
||||
|
|
|
@ -7,7 +7,6 @@ use crate::messages::prelude::*;
|
|||
use bezier_rs::{ManipulatorGroup, Subpath};
|
||||
use graph_craft::document::{generate_uuid, NodeId, NodeInput, NodeNetwork};
|
||||
use graphene_core::renderer::Quad;
|
||||
use graphene_core::text::Font;
|
||||
use graphene_core::uuid::ManipulatorGroupId;
|
||||
use graphene_core::vector::style::{Fill, Gradient, GradientType, LineCap, LineJoin, Stroke};
|
||||
use graphene_core::Color;
|
||||
|
@ -180,17 +179,10 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageData<'_>> for Gr
|
|||
}
|
||||
load_network_structure(document_network, document_metadata, selected_nodes, collapsed);
|
||||
}
|
||||
GraphOperationMessage::NewTextLayer {
|
||||
id,
|
||||
text,
|
||||
font,
|
||||
size,
|
||||
parent,
|
||||
insert_index,
|
||||
} => {
|
||||
GraphOperationMessage::NewTextLayer { id, text, parent, insert_index } => {
|
||||
let mut modify_inputs = ModifyInputsContext::new(document_network, document_metadata, node_graph, responses);
|
||||
if let Some(layer) = modify_inputs.create_layer_with_insert_index(id, insert_index, parent) {
|
||||
modify_inputs.insert_text(text, font, size, layer);
|
||||
modify_inputs.insert_text(text, layer);
|
||||
}
|
||||
load_network_structure(document_network, document_metadata, selected_nodes, collapsed);
|
||||
}
|
||||
|
@ -318,10 +310,11 @@ fn import_usvg_node(modify_inputs: &mut ModifyInputsContext, node: &usvg::Node,
|
|||
usvg::Node::Image(_image) => {
|
||||
warn!("Skip image")
|
||||
}
|
||||
usvg::Node::Text(text) => {
|
||||
let font = Font::new(crate::consts::DEFAULT_FONT_FAMILY.to_string(), crate::consts::DEFAULT_FONT_STYLE.to_string());
|
||||
modify_inputs.insert_text(text.chunks.iter().map(|chunk| chunk.text.clone()).collect(), font, 24., layer);
|
||||
modify_inputs.fill_set(Fill::Solid(Color::BLACK));
|
||||
usvg::Node::Text(_text) => {
|
||||
warn!("Skip text");
|
||||
// let font = Font::new(crate::consts::DEFAULT_FONT_FAMILY.to_string(), crate::consts::DEFAULT_FONT_STYLE.to_string());
|
||||
// modify_inputs.insert_text(text.chunks.iter().map(|chunk| chunk.text.clone()).collect(), layer);
|
||||
// modify_inputs.fill_set(Fill::Solid(Color::BLACK));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ use bezier_rs::Subpath;
|
|||
use graph_craft::document::value::TaggedValue;
|
||||
use graph_craft::document::{generate_uuid, DocumentNode, NodeId, NodeInput, NodeNetwork, NodeOutput};
|
||||
use graphene_core::raster::{BlendMode, ImageFrame};
|
||||
use graphene_core::text::Font;
|
||||
use graphene_core::text::RichText;
|
||||
use graphene_core::uuid::ManipulatorGroupId;
|
||||
use graphene_core::vector::brush_stroke::BrushStroke;
|
||||
use graphene_core::vector::style::{Fill, FillType, Stroke};
|
||||
|
@ -226,13 +226,13 @@ impl<'a> ModifyInputsContext<'a> {
|
|||
self.responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
}
|
||||
|
||||
pub fn insert_text(&mut self, text: String, font: Font, size: f64, layer: NodeId) {
|
||||
pub fn insert_text(&mut self, rich_text: RichText, layer: NodeId) {
|
||||
let text = resolve_document_node_type("Text").expect("Text node does not exist").to_document_node(
|
||||
[
|
||||
NodeInput::Network(graph_craft::concrete!(graphene_std::wasm_application_io::WasmEditorApi)),
|
||||
NodeInput::value(TaggedValue::String(text), false),
|
||||
NodeInput::value(TaggedValue::Font(font), false),
|
||||
NodeInput::value(TaggedValue::F64(size), false),
|
||||
NodeInput::value(TaggedValue::RichText(rich_text), false),
|
||||
NodeInput::value(TaggedValue::F64(f64::MAX), false),
|
||||
NodeInput::value(TaggedValue::VectorData(graphene_core::vector::VectorData::empty()), false),
|
||||
],
|
||||
Default::default(),
|
||||
);
|
||||
|
|
|
@ -2482,9 +2482,9 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
|||
name: "Text Generator".to_string(),
|
||||
inputs: vec![
|
||||
NodeInput::Network(concrete!(application_io::EditorApi<graphene_std::wasm_application_io::WasmApplicationIo>)),
|
||||
NodeInput::Network(concrete!(String)),
|
||||
NodeInput::Network(concrete!(graphene_core::text::Font)),
|
||||
NodeInput::Network(concrete!(graphene_core::text::RichText)),
|
||||
NodeInput::Network(concrete!(f64)),
|
||||
NodeInput::Network(concrete!(graphene_core::vector::VectorData)),
|
||||
],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::text::TextGeneratorNode<_, _, _>")),
|
||||
..Default::default()
|
||||
|
@ -2505,9 +2505,16 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
|||
}),
|
||||
inputs: vec![
|
||||
DocumentInputType::none(),
|
||||
DocumentInputType::value("Text", TaggedValue::String("Lorem ipsum".to_string()), false),
|
||||
DocumentInputType::value("Font", TaggedValue::Font(Font::new(DEFAULT_FONT_FAMILY.into(), DEFAULT_FONT_STYLE.into())), false),
|
||||
DocumentInputType::value("Size", TaggedValue::F64(24.), false),
|
||||
DocumentInputType::value(
|
||||
"Rich Text",
|
||||
TaggedValue::RichText(graphene_core::text::RichText::new(
|
||||
"Rich Text",
|
||||
[graphene_core::text::TextSpan::new(Font::new(DEFAULT_FONT_FAMILY.into(), DEFAULT_FONT_STYLE.into()), 24.)],
|
||||
)),
|
||||
false,
|
||||
),
|
||||
DocumentInputType::value("Line Length", TaggedValue::F64(f64::MAX), false),
|
||||
DocumentInputType::value("Path", TaggedValue::VectorData(graphene_core::vector::VectorData::empty()), false),
|
||||
],
|
||||
outputs: vec![DocumentOutputType::new("Vector", FrontendGraphDataType::Subpath)],
|
||||
properties: node_properties::node_section_font,
|
||||
|
|
|
@ -12,7 +12,6 @@ use graphene_core::memo::IORecord;
|
|||
use graphene_core::raster::{
|
||||
BlendMode, CellularDistanceFunction, CellularReturnType, Color, DomainWarpType, FractalType, ImageFrame, LuminanceCalculation, NoiseType, RedGreenBlue, RelativeAbsolute, SelectiveColorChoice,
|
||||
};
|
||||
use graphene_core::text::Font;
|
||||
use graphene_core::vector::style::{FillType, GradientType, LineCap, LineJoin};
|
||||
|
||||
use glam::{DVec2, IVec2, UVec2};
|
||||
|
@ -109,7 +108,27 @@ fn text_area_widget(document_node: &DocumentNode, node_id: NodeId, index: usize,
|
|||
.on_update(update_value(|x: &TextAreaInput| TaggedValue::String(x.value.clone()), node_id, index))
|
||||
.on_commit(commit_value)
|
||||
.widget_holder(),
|
||||
])
|
||||
]);
|
||||
} else if let NodeInput::Value {
|
||||
tagged_value: TaggedValue::RichText(x),
|
||||
exposed: false,
|
||||
} = &document_node.inputs[index]
|
||||
{
|
||||
let text = x.clone();
|
||||
widgets.extend_from_slice(&[
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
TextAreaInput::new(x.text.clone())
|
||||
.on_update(update_value(
|
||||
move |x: &TextAreaInput| {
|
||||
let mut text = text.clone();
|
||||
text.text = x.value.clone();
|
||||
TaggedValue::RichText(text)
|
||||
},
|
||||
node_id,
|
||||
index,
|
||||
))
|
||||
.widget_holder(),
|
||||
]);
|
||||
}
|
||||
widgets
|
||||
}
|
||||
|
@ -284,40 +303,6 @@ fn vec_dvec2_input(document_node: &DocumentNode, node_id: NodeId, index: usize,
|
|||
widgets
|
||||
}
|
||||
|
||||
fn font_inputs(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, blank_assist: bool) -> (Vec<WidgetHolder>, Option<Vec<WidgetHolder>>) {
|
||||
let mut first_widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, blank_assist);
|
||||
let mut second_widgets = None;
|
||||
|
||||
let from_font_input = |font: &FontInput| TaggedValue::Font(Font::new(font.font_family.clone(), font.font_style.clone()));
|
||||
|
||||
if let NodeInput::Value {
|
||||
tagged_value: TaggedValue::Font(font),
|
||||
exposed: false,
|
||||
} = &document_node.inputs[index]
|
||||
{
|
||||
first_widgets.extend_from_slice(&[
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
FontInput::new(font.font_family.clone(), font.font_style.clone())
|
||||
.on_update(update_value(from_font_input, node_id, index))
|
||||
.on_commit(commit_value)
|
||||
.widget_holder(),
|
||||
]);
|
||||
|
||||
let mut second_row = vec![TextLabel::new("").widget_holder()];
|
||||
add_blank_assist(&mut second_row);
|
||||
second_row.extend_from_slice(&[
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
FontInput::new(font.font_family.clone(), font.font_style.clone())
|
||||
.is_style_picker(true)
|
||||
.on_update(update_value(from_font_input, node_id, index))
|
||||
.on_commit(commit_value)
|
||||
.widget_holder(),
|
||||
]);
|
||||
second_widgets = Some(second_row);
|
||||
}
|
||||
(first_widgets, second_widgets)
|
||||
}
|
||||
|
||||
fn vector_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, blank_assist: bool) -> Vec<WidgetHolder> {
|
||||
let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::Vector, blank_assist);
|
||||
|
||||
|
@ -1594,16 +1579,11 @@ pub fn transform_properties(document_node: &DocumentNode, node_id: NodeId, _cont
|
|||
}
|
||||
|
||||
pub fn node_section_font(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
|
||||
let text = text_area_widget(document_node, node_id, 1, "Text", true);
|
||||
let (font, style) = font_inputs(document_node, node_id, 2, "Font", true);
|
||||
let size = number_widget(document_node, node_id, 3, "Size", NumberInput::default().unit(" px").min(1.), true);
|
||||
let text = text_area_widget(document_node, node_id, 1, "Rich Text", true);
|
||||
let line_width = number_widget(document_node, node_id, 2, "Line Width", NumberInput::default().min(0.), true);
|
||||
let path = start_widgets(document_node, node_id, 3, "Path", FrontendGraphDataType::Vector, false);
|
||||
|
||||
let mut result = vec![LayoutGroup::Row { widgets: text }, LayoutGroup::Row { widgets: font }];
|
||||
if let Some(style) = style {
|
||||
result.push(LayoutGroup::Row { widgets: style });
|
||||
}
|
||||
result.push(LayoutGroup::Row { widgets: size });
|
||||
result
|
||||
vec![LayoutGroup::Row { widgets: text }, LayoutGroup::Row { widgets: line_width }, LayoutGroup::Row { widgets: path }]
|
||||
}
|
||||
|
||||
pub fn imaginate_properties(document_node: &DocumentNode, node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
|
||||
|
|
|
@ -264,8 +264,9 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
|
|||
self.persistent_data.font_cache.insert(font, preview_url, data, is_default);
|
||||
self.executor.update_font_cache(self.persistent_data.font_cache.clone());
|
||||
|
||||
if self.active_document_mut().is_some() {
|
||||
if self.active_document_id().is_some() {
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
responses.add(TextToolMessage::RefreshFonts);
|
||||
}
|
||||
}
|
||||
PortfolioMessage::ImaginateCheckServerStatus => {
|
||||
|
|
|
@ -5,9 +5,9 @@ use crate::messages::prelude::*;
|
|||
use bezier_rs::{ManipulatorGroup, Subpath};
|
||||
use graph_craft::document::{value::TaggedValue, DocumentNode, NodeId, NodeInput, NodeNetwork};
|
||||
use graphene_core::raster::{BlendMode, ImageFrame};
|
||||
use graphene_core::text::Font;
|
||||
use graphene_core::uuid::ManipulatorGroupId;
|
||||
use graphene_core::vector::style::{FillType, Gradient};
|
||||
use graphene_core::vector::VectorData;
|
||||
use graphene_core::Color;
|
||||
|
||||
use glam::DVec2;
|
||||
|
@ -165,33 +165,31 @@ pub fn get_text_id(layer: LayerNodeIdentifier, document_network: &NodeNetwork) -
|
|||
}
|
||||
|
||||
/// Gets properties from the Text node
|
||||
pub fn get_text(layer: LayerNodeIdentifier, document_network: &NodeNetwork) -> Option<(&String, &Font, f64)> {
|
||||
pub fn get_text(layer: LayerNodeIdentifier, document_network: &NodeNetwork) -> Option<(&graphene_core::text::RichText, f64, &VectorData)> {
|
||||
let inputs = NodeGraphLayer::new(layer, document_network).find_node_inputs("Text")?;
|
||||
let NodeInput::Value {
|
||||
tagged_value: TaggedValue::String(text),
|
||||
tagged_value: TaggedValue::RichText(text),
|
||||
..
|
||||
} = &inputs[1]
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let NodeInput::Value {
|
||||
tagged_value: TaggedValue::Font(font),
|
||||
tagged_value: TaggedValue::F64(line_width),
|
||||
..
|
||||
} = &inputs[2]
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let NodeInput::Value {
|
||||
tagged_value: TaggedValue::F64(font_size),
|
||||
tagged_value: TaggedValue::VectorData(path),
|
||||
..
|
||||
} = inputs[3]
|
||||
} = &inputs[3]
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
|
||||
Some((text, font, font_size))
|
||||
Some((text, *line_width, path))
|
||||
}
|
||||
|
||||
pub fn get_stroke_width(layer: LayerNodeIdentifier, network: &NodeNetwork) -> Option<f64> {
|
||||
|
|
|
@ -1,19 +1,23 @@
|
|||
#![allow(clippy::too_many_arguments)]
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::tool_prelude::*;
|
||||
use crate::application::generate_uuid;
|
||||
use crate::consts::{DEFAULT_FONT_FAMILY, DEFAULT_FONT_STYLE};
|
||||
use crate::consts::{DEFAULT_FONT_FAMILY, DEFAULT_FONT_STYLE, SELECTION_TOLERANCE};
|
||||
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
|
||||
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType};
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils::{self, is_layer_fed_by_node_of_name};
|
||||
|
||||
use glam::Vec2;
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use graph_craft::document::NodeId;
|
||||
use graphene_core::renderer::Quad;
|
||||
use graphene_core::text::{load_face, Font, FontCache};
|
||||
use graphene_core::text::cosmic_text::Edit;
|
||||
use graphene_core::text::{Font, FontCache, RichText, TextSpan};
|
||||
use graphene_core::vector::style::Fill;
|
||||
use graphene_core::vector::VectorData;
|
||||
use graphene_core::Color;
|
||||
|
||||
#[derive(Default)]
|
||||
|
@ -27,6 +31,14 @@ pub struct TextOptions {
|
|||
font_size: u32,
|
||||
font_name: String,
|
||||
font_style: String,
|
||||
bold: bool,
|
||||
bold_size: f32,
|
||||
italic: bool,
|
||||
italic_size: f32,
|
||||
letter_spacing: f32,
|
||||
word_spacing: f32,
|
||||
line_spacing: f32,
|
||||
kerning: Vec2,
|
||||
fill: ToolColorOptions,
|
||||
}
|
||||
|
||||
|
@ -36,11 +48,36 @@ impl Default for TextOptions {
|
|||
font_size: 24,
|
||||
font_name: DEFAULT_FONT_FAMILY.into(),
|
||||
font_style: DEFAULT_FONT_STYLE.into(),
|
||||
bold: false,
|
||||
bold_size: 10.,
|
||||
italic: false,
|
||||
italic_size: 15.,
|
||||
letter_spacing: 0.,
|
||||
word_spacing: 0.,
|
||||
line_spacing: 1.,
|
||||
kerning: Vec2::ZERO,
|
||||
fill: ToolColorOptions::new_primary(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TextOptions {
|
||||
pub fn to_span(&self) -> TextSpan {
|
||||
TextSpan {
|
||||
font: Arc::new(Font::new(self.font_name.clone(), self.font_style.clone())),
|
||||
font_size: self.font_size as f32,
|
||||
bold: self.bold.then_some(self.bold_size),
|
||||
italic: self.italic.then_some(self.italic_size),
|
||||
letter_spacing: self.letter_spacing,
|
||||
word_spacing: self.word_spacing,
|
||||
line_spacing: self.line_spacing,
|
||||
color: self.fill.active_color().unwrap_or(Color::BLACK),
|
||||
kerning: self.kerning,
|
||||
offset: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[impl_message(Message, ToolMessage, Text)]
|
||||
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
|
||||
pub enum TextToolMessage {
|
||||
|
@ -51,10 +88,13 @@ pub enum TextToolMessage {
|
|||
|
||||
// Tool-specific messages
|
||||
CommitText,
|
||||
Drag,
|
||||
EditSelected,
|
||||
Interact,
|
||||
TextChange { new_text: String },
|
||||
UpdateBounds { new_text: String },
|
||||
RefreshFonts,
|
||||
Select,
|
||||
TextInput { input_type: String, data: Option<String> },
|
||||
TextNavigate { key: String, shift: bool, ctrl: bool },
|
||||
UpdateOptions(TextOptionsUpdate),
|
||||
}
|
||||
|
||||
|
@ -64,6 +104,15 @@ pub enum TextOptionsUpdate {
|
|||
FillColorType(ToolColorType),
|
||||
Font { family: String, style: String },
|
||||
FontSize(u32),
|
||||
Bold(bool),
|
||||
Italic(bool),
|
||||
BoldSize(f32),
|
||||
ItalicSize(f32),
|
||||
LetterSpacing(f32),
|
||||
WordSpacing(f32),
|
||||
LineSpacing(f32),
|
||||
KerningX(f32),
|
||||
KerningY(f32),
|
||||
WorkingColors(Option<Color>, Option<Color>),
|
||||
}
|
||||
|
||||
|
@ -79,8 +128,31 @@ impl ToolMetadata for TextTool {
|
|||
}
|
||||
}
|
||||
|
||||
fn get_cursor_index(buffer: &graphene_core::text::cosmic_text::Buffer, cursor: graphene_core::text::cosmic_text::Cursor) -> usize {
|
||||
let mut index = 0;
|
||||
for line in buffer.lines.iter().take(cursor.line) {
|
||||
index += line.text().len() + 1;
|
||||
}
|
||||
index + cursor.index
|
||||
}
|
||||
|
||||
fn create_text_widgets(tool: &TextTool) -> Vec<WidgetHolder> {
|
||||
let font = FontInput::new(&tool.options.font_name, &tool.options.font_style)
|
||||
let span = tool.tool_data.editing_text.as_ref().and_then(|editing| {
|
||||
let mut offset = 0;
|
||||
let cursor = get_cursor_index(editing.editor.buffer(), editing.editor.cursor());
|
||||
editing
|
||||
.text
|
||||
.spans
|
||||
.iter()
|
||||
.filter(|span| {
|
||||
offset += span.offset;
|
||||
offset <= cursor
|
||||
})
|
||||
.last()
|
||||
});
|
||||
let family = span.map_or(&tool.options.font_name, |span| &span.font.font_family);
|
||||
let style = span.map_or(&tool.options.font_style, |span| &span.font.font_style);
|
||||
let font = FontInput::new(family, style)
|
||||
.is_style_picker(false)
|
||||
.on_update(|font_input: &FontInput| {
|
||||
TextToolMessage::UpdateOptions(TextOptionsUpdate::Font {
|
||||
|
@ -90,7 +162,7 @@ fn create_text_widgets(tool: &TextTool) -> Vec<WidgetHolder> {
|
|||
.into()
|
||||
})
|
||||
.widget_holder();
|
||||
let style = FontInput::new(&tool.options.font_name, &tool.options.font_style)
|
||||
let style = FontInput::new(family, style)
|
||||
.is_style_picker(true)
|
||||
.on_update(|font_input: &FontInput| {
|
||||
TextToolMessage::UpdateOptions(TextOptionsUpdate::Font {
|
||||
|
@ -100,7 +172,13 @@ fn create_text_widgets(tool: &TextTool) -> Vec<WidgetHolder> {
|
|||
.into()
|
||||
})
|
||||
.widget_holder();
|
||||
let size = NumberInput::new(Some(tool.options.font_size as f64))
|
||||
let bold = CheckboxInput::new(span.map_or(tool.options.bold, |span| span.bold.is_some()))
|
||||
.icon("Bold")
|
||||
.on_update(|input: &CheckboxInput| TextToolMessage::UpdateOptions(TextOptionsUpdate::Bold(input.checked)).into());
|
||||
let italic = CheckboxInput::new(span.map_or(tool.options.italic, |span| span.italic.is_some()))
|
||||
.icon("Italic")
|
||||
.on_update(|input: &CheckboxInput| TextToolMessage::UpdateOptions(TextOptionsUpdate::Italic(input.checked)).into());
|
||||
let size = NumberInput::new(Some(span.map_or(tool.options.font_size as f64, |span| span.font_size as f64)))
|
||||
.unit(" px")
|
||||
.label("Size")
|
||||
.int()
|
||||
|
@ -108,15 +186,93 @@ fn create_text_widgets(tool: &TextTool) -> Vec<WidgetHolder> {
|
|||
.max((1_u64 << std::f64::MANTISSA_DIGITS) as f64)
|
||||
.on_update(|number_input: &NumberInput| TextToolMessage::UpdateOptions(TextOptionsUpdate::FontSize(number_input.value.unwrap() as u32)).into())
|
||||
.widget_holder();
|
||||
|
||||
vec![
|
||||
font,
|
||||
Separator::new(SeparatorType::Related).widget_holder(),
|
||||
style,
|
||||
Separator::new(SeparatorType::Related).widget_holder(),
|
||||
bold.widget_holder(),
|
||||
italic.widget_holder(),
|
||||
bold_italic_options(tool, span).widget_holder(),
|
||||
Separator::new(SeparatorType::Related).widget_holder(),
|
||||
size,
|
||||
spacing_options(tool, span).widget_holder(),
|
||||
]
|
||||
}
|
||||
|
||||
fn bold_italic_options(tool: &TextTool, span: Option<&TextSpan>) -> PopoverButton {
|
||||
PopoverButton::new("Bold and Italic", "Bold and italic customization settings").options_widget(vec![
|
||||
LayoutGroup::Row {
|
||||
widgets: vec![
|
||||
TextLabel::new("Boldness").table_align(true).widget_holder(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
NumberInput::new(Some(span.map(|span| span.bold).flatten().unwrap_or(tool.options.bold_size) as f64))
|
||||
.min(0.)
|
||||
.on_update(|number_input: &NumberInput| TextToolMessage::UpdateOptions(TextOptionsUpdate::BoldSize(number_input.value.unwrap() as f32)).into())
|
||||
.widget_holder(),
|
||||
],
|
||||
},
|
||||
LayoutGroup::Row {
|
||||
widgets: vec![
|
||||
TextLabel::new("Italic slant").table_align(true).widget_holder(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
NumberInput::new(Some(span.map(|span| span.italic).flatten().unwrap_or(tool.options.italic_size) as f64))
|
||||
.min(0.)
|
||||
.on_update(|number_input: &NumberInput| TextToolMessage::UpdateOptions(TextOptionsUpdate::ItalicSize(number_input.value.unwrap() as f32)).into())
|
||||
.widget_holder(),
|
||||
],
|
||||
},
|
||||
])
|
||||
}
|
||||
|
||||
fn spacing_options(tool: &TextTool, span: Option<&TextSpan>) -> PopoverButton {
|
||||
PopoverButton::new("Text Spacing", "Text spacing customization settings").options_widget(vec![
|
||||
LayoutGroup::Row {
|
||||
widgets: vec![
|
||||
TextLabel::new("Letter spacing").table_align(true).widget_holder(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
NumberInput::new(Some(span.map_or(tool.options.letter_spacing, |span| span.letter_spacing) as f64))
|
||||
.on_update(|number_input: &NumberInput| TextToolMessage::UpdateOptions(TextOptionsUpdate::LetterSpacing(number_input.value.unwrap() as f32)).into())
|
||||
.widget_holder(),
|
||||
],
|
||||
},
|
||||
LayoutGroup::Row {
|
||||
widgets: vec![
|
||||
TextLabel::new("Word spacing").table_align(true).widget_holder(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
NumberInput::new(Some(span.map_or(tool.options.word_spacing, |span| span.word_spacing) as f64))
|
||||
.on_update(|number_input: &NumberInput| TextToolMessage::UpdateOptions(TextOptionsUpdate::WordSpacing(number_input.value.unwrap() as f32)).into())
|
||||
.widget_holder(),
|
||||
],
|
||||
},
|
||||
LayoutGroup::Row {
|
||||
widgets: vec![
|
||||
TextLabel::new("Line spacing").table_align(true).widget_holder(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
NumberInput::new(Some(span.map_or(tool.options.line_spacing, |span| span.line_spacing) as f64))
|
||||
.on_update(|number_input: &NumberInput| TextToolMessage::UpdateOptions(TextOptionsUpdate::LineSpacing(number_input.value.unwrap() as f32)).into())
|
||||
.widget_holder(),
|
||||
],
|
||||
},
|
||||
LayoutGroup::Row {
|
||||
widgets: vec![
|
||||
TextLabel::new("Kerning").table_align(true).widget_holder(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
NumberInput::new(Some(span.map_or(tool.options.kerning, |span| span.kerning).x as f64))
|
||||
.on_update(|number_input: &NumberInput| TextToolMessage::UpdateOptions(TextOptionsUpdate::KerningX(number_input.value.unwrap() as f32)).into())
|
||||
.label("X")
|
||||
.widget_holder(),
|
||||
Separator::new(SeparatorType::Related).widget_holder(),
|
||||
NumberInput::new(Some(span.map_or(tool.options.kerning, |span| span.kerning).y as f64))
|
||||
.on_update(|number_input: &NumberInput| TextToolMessage::UpdateOptions(TextOptionsUpdate::KerningY(number_input.value.unwrap() as f32)).into())
|
||||
.label("Y")
|
||||
.widget_holder(),
|
||||
],
|
||||
},
|
||||
])
|
||||
}
|
||||
|
||||
impl LayoutHolder for TextTool {
|
||||
fn layout(&self) -> Layout {
|
||||
let mut widgets = create_text_widgets(self);
|
||||
|
@ -135,6 +291,53 @@ impl LayoutHolder for TextTool {
|
|||
}
|
||||
}
|
||||
|
||||
fn update_span(tool_data: &mut TextToolData, font_cache: &FontCache, modification: impl Fn(&mut TextSpan)) {
|
||||
let Some(editing_text) = &mut tool_data.editing_text else { return };
|
||||
let editor = &mut editing_text.editor;
|
||||
let selection = editor.select_opt().filter(|&select| select != editor.cursor());
|
||||
let (start_index, end_index) = if let Some(selection) = selection {
|
||||
let cursor = get_cursor_index(editor.buffer(), editor.cursor());
|
||||
let selection = get_cursor_index(editor.buffer(), selection);
|
||||
let (start, end) = (cursor.min(selection), cursor.max(selection));
|
||||
|
||||
let mut text_index = editing_text.text.spans.first().map_or(0, |span| span.offset);
|
||||
let mut span_index = 0;
|
||||
while editing_text.text.spans.get(span_index + 1).map_or(false, |next| text_index + next.offset < start) {
|
||||
span_index += 1;
|
||||
text_index += editing_text.text.spans[span_index].offset;
|
||||
}
|
||||
if !editing_text.text.spans.get(span_index + 1).map_or(false, |next| next.offset == start - text_index) {
|
||||
if let Some(next) = editing_text.text.spans.get_mut(span_index + 1) {
|
||||
next.offset -= start - text_index;
|
||||
}
|
||||
editing_text.text.spans.insert(span_index + 1, editing_text.text.spans[span_index].clone().offset(start - text_index));
|
||||
}
|
||||
let start_span = span_index + 1;
|
||||
|
||||
while editing_text.text.spans.get(span_index + 1).map_or(false, |next| text_index + next.offset < end) {
|
||||
span_index += 1;
|
||||
text_index += editing_text.text.spans[span_index].offset;
|
||||
}
|
||||
if !editing_text.text.spans.get(span_index + 1).map_or(false, |next| next.offset == end - text_index) {
|
||||
if let Some(next) = editing_text.text.spans.get_mut(span_index + 1) {
|
||||
next.offset -= end - text_index;
|
||||
}
|
||||
editing_text.text.spans.insert(span_index + 1, editing_text.text.spans[span_index].clone().offset(end - text_index));
|
||||
}
|
||||
|
||||
let end_span = span_index + 1;
|
||||
(start_span, end_span)
|
||||
} else {
|
||||
(0, editing_text.text.spans.len())
|
||||
};
|
||||
for span in &mut editing_text.text.spans[start_index..end_index] {
|
||||
modification(span);
|
||||
}
|
||||
if let Some(mut font_system) = font_cache.get_system() {
|
||||
graphene_core::text::create_buffer(editor.buffer_mut(), &mut font_system, &editing_text.text, font_cache, editing_text.line_length);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for TextTool {
|
||||
fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, tool_data: &mut ToolActionHandlerData<'a>) {
|
||||
let ToolMessage::Text(TextToolMessage::UpdateOptions(action)) = message else {
|
||||
|
@ -143,23 +346,64 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for TextToo
|
|||
};
|
||||
match action {
|
||||
TextOptionsUpdate::Font { family, style } => {
|
||||
self.options.font_name = family;
|
||||
self.options.font_style = style;
|
||||
|
||||
self.send_layout(responses, LayoutTarget::ToolOptions);
|
||||
self.options.font_name = family.clone();
|
||||
self.options.font_style = style.clone();
|
||||
let font = Arc::new(Font::new(family, style));
|
||||
update_span(&mut self.tool_data, tool_data.font_cache, |span| span.font = font.clone());
|
||||
}
|
||||
TextOptionsUpdate::FontSize(font_size) => {
|
||||
self.options.font_size = font_size;
|
||||
update_span(&mut self.tool_data, tool_data.font_cache, |span| span.font_size = font_size as f32);
|
||||
}
|
||||
TextOptionsUpdate::FontSize(font_size) => self.options.font_size = font_size,
|
||||
TextOptionsUpdate::FillColor(color) => {
|
||||
self.options.fill.custom_color = color;
|
||||
self.options.fill.color_type = ToolColorType::Custom;
|
||||
update_span(&mut self.tool_data, tool_data.font_cache, |span| span.color = color.unwrap_or(Color::BLACK));
|
||||
}
|
||||
TextOptionsUpdate::FillColorType(color_type) => self.options.fill.color_type = color_type,
|
||||
TextOptionsUpdate::WorkingColors(primary, secondary) => {
|
||||
self.options.fill.primary_working_color = primary;
|
||||
self.options.fill.secondary_working_color = secondary;
|
||||
}
|
||||
TextOptionsUpdate::Bold(value) => {
|
||||
self.options.bold = value;
|
||||
update_span(&mut self.tool_data, tool_data.font_cache, |span| span.bold = self.options.bold.then_some(self.options.bold_size));
|
||||
}
|
||||
TextOptionsUpdate::Italic(value) => {
|
||||
self.options.italic = value;
|
||||
update_span(&mut self.tool_data, tool_data.font_cache, |span| span.italic = self.options.italic.then_some(self.options.italic_size));
|
||||
}
|
||||
TextOptionsUpdate::BoldSize(value) => {
|
||||
self.options.bold_size = value;
|
||||
update_span(&mut self.tool_data, tool_data.font_cache, |span| span.bold = self.options.bold.then_some(self.options.bold_size));
|
||||
}
|
||||
TextOptionsUpdate::ItalicSize(value) => {
|
||||
self.options.italic_size = value;
|
||||
update_span(&mut self.tool_data, tool_data.font_cache, |span| span.italic = self.options.italic.then_some(self.options.italic_size));
|
||||
}
|
||||
TextOptionsUpdate::LetterSpacing(value) => {
|
||||
self.options.letter_spacing = value;
|
||||
update_span(&mut self.tool_data, tool_data.font_cache, |span| span.letter_spacing = value);
|
||||
}
|
||||
TextOptionsUpdate::WordSpacing(value) => {
|
||||
self.options.word_spacing = value;
|
||||
update_span(&mut self.tool_data, tool_data.font_cache, |span| span.word_spacing = value);
|
||||
}
|
||||
TextOptionsUpdate::LineSpacing(value) => {
|
||||
self.options.line_spacing = value;
|
||||
update_span(&mut self.tool_data, tool_data.font_cache, |span| span.line_spacing = value);
|
||||
}
|
||||
TextOptionsUpdate::KerningX(value) => {
|
||||
self.options.kerning.x = value;
|
||||
update_span(&mut self.tool_data, tool_data.font_cache, |span| span.kerning.x = value);
|
||||
}
|
||||
TextOptionsUpdate::KerningY(value) => {
|
||||
self.options.kerning.y = value;
|
||||
update_span(&mut self.tool_data, tool_data.font_cache, |span| span.kerning.y = value);
|
||||
}
|
||||
}
|
||||
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
self.send_layout(responses, LayoutTarget::ToolOptions);
|
||||
}
|
||||
|
||||
|
@ -172,6 +416,11 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for TextToo
|
|||
Interact,
|
||||
Abort,
|
||||
CommitText,
|
||||
Select
|
||||
),
|
||||
TextToolFsmState::Selecting | TextToolFsmState::Wrap => actions!(TextToolMessageDiscriminant;
|
||||
Interact,
|
||||
Drag,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
@ -194,17 +443,21 @@ enum TextToolFsmState {
|
|||
#[default]
|
||||
Ready,
|
||||
Editing,
|
||||
Selecting,
|
||||
Wrap,
|
||||
}
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Debug)]
|
||||
pub struct EditingText {
|
||||
text: String,
|
||||
font: Font,
|
||||
font_size: f64,
|
||||
text: RichText,
|
||||
line_length: f64,
|
||||
path: VectorData,
|
||||
color: Option<Color>,
|
||||
transform: DAffine2,
|
||||
editor: graphene_core::text::cosmic_text::Editor,
|
||||
composition: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
#[derive(Debug, Default)]
|
||||
struct TextToolData {
|
||||
layer: LayerNodeIdentifier,
|
||||
editing_text: Option<EditingText>,
|
||||
|
@ -213,51 +466,67 @@ struct TextToolData {
|
|||
|
||||
impl TextToolData {
|
||||
/// Set the editing state of the currently modifying layer
|
||||
fn set_editing(&self, editable: bool, font_cache: &FontCache, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
||||
if let Some(node_id) = graph_modification_utils::get_fill_id(self.layer, &document.network) {
|
||||
fn set_editing(&mut self, editable: bool, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
||||
let fill_node = graph_modification_utils::get_fill_id(self.layer, &document.network);
|
||||
if let Some(node_id) = fill_node {
|
||||
responses.add(NodeGraphMessage::SetVisibility { node_id, visible: !editable });
|
||||
}
|
||||
|
||||
if let Some(editing_text) = self.editing_text.as_ref().filter(|_| editable) {
|
||||
if let Some(editing_text) = self.editing_text.as_ref().filter(|_| !editable) {
|
||||
responses.add(GraphOperationMessage::FillSet {
|
||||
layer: self.layer,
|
||||
fill: editing_text.color.map_or(Fill::None, |color| Fill::Solid(color)),
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(editing_text) = self.editing_text.as_mut().filter(|_| editable) {
|
||||
responses.add(FrontendMessage::DisplayEditableTextbox {
|
||||
text: editing_text.text.clone(),
|
||||
line_width: None,
|
||||
font_size: editing_text.font_size,
|
||||
color: editing_text.color.unwrap_or(Color::BLACK),
|
||||
url: font_cache.get_preview_url(&editing_text.font).cloned().unwrap_or_default(),
|
||||
text: editing_text.text.text.clone(),
|
||||
transform: editing_text.transform.to_cols_array(),
|
||||
});
|
||||
use graphene_core::text::cosmic_text::{Affinity, Cursor};
|
||||
if let Some(last) = editing_text.editor.buffer().lines.last() {
|
||||
let last_index = last.text().len();
|
||||
let line = editing_text.editor.buffer().lines.len() - 1;
|
||||
editing_text.editor.set_select_opt(Some(Cursor::new_with_affinity(0, 0, Affinity::Before)));
|
||||
editing_text.editor.set_cursor(Cursor::new_with_affinity(line, last_index, Affinity::After));
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
}
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
} else {
|
||||
responses.add(FrontendMessage::DisplayRemoveEditableTextbox);
|
||||
}
|
||||
responses.add(ToolMessage::RefreshToolOptions);
|
||||
}
|
||||
|
||||
fn load_layer_text_node(&mut self, document: &DocumentMessageHandler) -> Option<()> {
|
||||
fn load_layer_text_node(&mut self, document: &DocumentMessageHandler, font_cache: &FontCache) -> Option<()> {
|
||||
let transform = document.metadata().transform_to_viewport(self.layer);
|
||||
let color = graph_modification_utils::get_fill_color(self.layer, &document.network).unwrap_or(Color::BLACK);
|
||||
let (text, font, font_size) = graph_modification_utils::get_text(self.layer, &document.network)?;
|
||||
let color = Some(graph_modification_utils::get_fill_color(self.layer, &document.network).unwrap_or(Color::BLACK));
|
||||
let (text, line_length, path) = graph_modification_utils::get_text(self.layer, &document.network)?;
|
||||
let editor = graphene_core::text::create_cosmic_editor(&text, font_cache, line_length)?;
|
||||
self.editing_text = Some(EditingText {
|
||||
text: text.clone(),
|
||||
font: font.clone(),
|
||||
font_size,
|
||||
color: Some(color),
|
||||
line_length,
|
||||
path: path.clone(),
|
||||
transform,
|
||||
color,
|
||||
editor,
|
||||
composition: None,
|
||||
});
|
||||
self.new_text = text.clone();
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn start_editing_layer(&mut self, layer: LayerNodeIdentifier, tool_state: TextToolFsmState, document: &DocumentMessageHandler, font_cache: &FontCache, responses: &mut VecDeque<Message>) {
|
||||
if tool_state == TextToolFsmState::Editing {
|
||||
self.set_editing(false, font_cache, document, responses);
|
||||
if tool_state != TextToolFsmState::Ready {
|
||||
self.set_editing(false, document, responses);
|
||||
}
|
||||
|
||||
self.layer = layer;
|
||||
self.load_layer_text_node(document);
|
||||
self.load_layer_text_node(document, font_cache);
|
||||
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
|
||||
self.set_editing(true, font_cache, document, responses);
|
||||
self.set_editing(true, document, responses);
|
||||
|
||||
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![self.layer.to_node()] });
|
||||
}
|
||||
|
@ -280,15 +549,13 @@ impl TextToolData {
|
|||
|
||||
responses.add(GraphOperationMessage::NewTextLayer {
|
||||
id: self.layer.to_node(),
|
||||
text: String::new(),
|
||||
font: editing_text.font.clone(),
|
||||
size: editing_text.font_size,
|
||||
text: editing_text.text.clone(),
|
||||
parent: document.new_layer_parent(),
|
||||
insert_index: -1,
|
||||
});
|
||||
responses.add(GraphOperationMessage::FillSet {
|
||||
responses.add(GraphOperationMessage::TransformSetPivot {
|
||||
layer: self.layer,
|
||||
fill: if editing_text.color.is_some() { Fill::Solid(editing_text.color.unwrap()) } else { Fill::None },
|
||||
pivot: DVec2::ZERO,
|
||||
});
|
||||
responses.add(GraphOperationMessage::TransformSet {
|
||||
layer: self.layer,
|
||||
|
@ -297,37 +564,18 @@ impl TextToolData {
|
|||
skip_rerender: true,
|
||||
});
|
||||
|
||||
self.set_editing(true, font_cache, document, responses);
|
||||
self.set_editing(true, document, responses);
|
||||
|
||||
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![self.layer.to_node()] });
|
||||
|
||||
TextToolFsmState::Editing
|
||||
} else {
|
||||
// Removing old text as editable
|
||||
self.set_editing(false, font_cache, document, responses);
|
||||
self.set_editing(false, document, responses);
|
||||
|
||||
TextToolFsmState::Ready
|
||||
}
|
||||
}
|
||||
|
||||
fn get_bounds(&self, text: &str, font_cache: &FontCache) -> Option<[DVec2; 2]> {
|
||||
let editing_text = self.editing_text.as_ref()?;
|
||||
let buzz_face = font_cache.get(&editing_text.font).map(|data| load_face(data));
|
||||
let subpaths = graphene_core::text::to_path(text, buzz_face, editing_text.font_size, None);
|
||||
let bounds = subpaths.iter().filter_map(|subpath| subpath.bounding_box());
|
||||
let combined_bounds = bounds.reduce(|a, b| [a[0].min(b[0]), a[1].max(b[1])]).unwrap_or_default();
|
||||
Some(combined_bounds)
|
||||
}
|
||||
|
||||
fn fix_text_bounds(&self, new_text: &str, _document: &DocumentMessageHandler, font_cache: &FontCache, responses: &mut VecDeque<Message>) -> Option<()> {
|
||||
responses.add(GraphOperationMessage::UpdateBounds {
|
||||
layer: self.layer,
|
||||
old_bounds: self.get_bounds(&self.editing_text.as_ref()?.text, font_cache)?,
|
||||
new_bounds: self.get_bounds(new_text, font_cache)?,
|
||||
});
|
||||
|
||||
Some(())
|
||||
}
|
||||
}
|
||||
|
||||
fn can_edit_selected(document: &DocumentMessageHandler) -> Option<LayerNodeIdentifier> {
|
||||
|
@ -362,47 +610,109 @@ impl Fsm for TextToolFsmState {
|
|||
return self;
|
||||
};
|
||||
match (self, event) {
|
||||
(TextToolFsmState::Editing, TextToolMessage::Overlays(mut overlay_context)) => {
|
||||
responses.add(FrontendMessage::DisplayEditableTextboxTransform {
|
||||
transform: document.metadata().transform_to_viewport(tool_data.layer).to_cols_array(),
|
||||
});
|
||||
if let Some(editing_text) = tool_data.editing_text.as_ref() {
|
||||
let buzz_face = font_cache.get(&editing_text.font).map(|data| load_face(data));
|
||||
let far = graphene_core::text::bounding_box(&tool_data.new_text, buzz_face, editing_text.font_size, None);
|
||||
if far.x != 0. && far.y != 0. {
|
||||
let quad = Quad::from_box([DVec2::ZERO, far]);
|
||||
let transformed_quad = document.metadata().transform_to_viewport(tool_data.layer) * quad;
|
||||
overlay_context.quad(transformed_quad);
|
||||
}
|
||||
}
|
||||
(TextToolFsmState::Editing | TextToolFsmState::Selecting | TextToolFsmState::Wrap, TextToolMessage::Overlays(mut overlay_context)) => {
|
||||
let transform = document.metadata().transform_to_viewport(tool_data.layer);
|
||||
|
||||
TextToolFsmState::Editing
|
||||
}
|
||||
(_, TextToolMessage::Overlays(mut overlay_context)) => {
|
||||
for layer in document.selected_nodes.selected_layers(document.metadata()) {
|
||||
let Some((text, font, font_size)) = graph_modification_utils::get_text(layer, &document.network) else {
|
||||
continue;
|
||||
};
|
||||
let buzz_face = font_cache.get(font).map(|data| load_face(data));
|
||||
let far = graphene_core::text::bounding_box(text, buzz_face, font_size, None);
|
||||
let quad = Quad::from_box([DVec2::ZERO, far]);
|
||||
let multiplied = document.metadata().transform_to_viewport(layer) * quad;
|
||||
overlay_context.quad(multiplied);
|
||||
let Some(editing_text) = tool_data.editing_text.as_ref() else { return self };
|
||||
if editing_text.composition.is_none() {
|
||||
responses.add(FrontendMessage::DisplayEditableTextbox {
|
||||
text: editing_text.editor.copy_selection().unwrap_or_default(),
|
||||
transform: (transform * DAffine2::from_translation(graphene_core::text::cursor_rectangle(&editing_text.editor, &editing_text.text).map_or(DVec2::ZERO, |cursor| cursor[0])))
|
||||
.to_cols_array(),
|
||||
});
|
||||
}
|
||||
let Some(mut font_system) = font_cache.get_system() else { return self };
|
||||
let subpaths = graphene_core::text::buffer_to_path(editing_text.editor.buffer(), &mut font_system, &editing_text.text.spans, &editing_text.path);
|
||||
overlay_context.outline(subpaths.iter(), transform);
|
||||
let handle = graphene_core::text::find_line_wrap_handle(editing_text.editor.buffer(), &editing_text.text.spans);
|
||||
overlay_context.manipulator_anchor(transform.transform_point2(handle), self == TextToolFsmState::Wrap, None);
|
||||
let subpaths = graphene_core::text::selection_shape(&editing_text.editor, &editing_text.text);
|
||||
overlay_context.outline(subpaths.iter(), transform);
|
||||
let subpaths = graphene_core::text::cursor_shape(&editing_text.editor, &editing_text.text);
|
||||
overlay_context.outline(subpaths.iter(), transform);
|
||||
|
||||
self
|
||||
}
|
||||
(state, TextToolMessage::Interact) => {
|
||||
tool_data.editing_text = Some(EditingText {
|
||||
text: String::new(),
|
||||
transform: DAffine2::from_translation(input.mouse.position),
|
||||
font_size: tool_options.font_size as f64,
|
||||
font: Font::new(tool_options.font_name.clone(), tool_options.font_style.clone()),
|
||||
(_, TextToolMessage::RefreshFonts) => {
|
||||
if let Some(editing) = &mut tool_data.editing_text {
|
||||
if let Some(mut font_system) = font_cache.get_system() {
|
||||
graphene_core::text::create_buffer(editing.editor.buffer_mut(), &mut font_system, &editing.text, &font_cache, editing.line_length);
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
(_, TextToolMessage::Select) => {
|
||||
let Some(editing_text) = tool_data.editing_text.as_mut() else { return self };
|
||||
let handle = graphene_core::text::find_line_wrap_handle(editing_text.editor.buffer(), &editing_text.text.spans);
|
||||
let to_viewport = document.metadata().transform_to_viewport(tool_data.layer);
|
||||
if to_viewport.transform_point2(handle).distance_squared(input.mouse.position) < SELECTION_TOLERANCE * SELECTION_TOLERANCE {
|
||||
return TextToolFsmState::Wrap;
|
||||
}
|
||||
|
||||
let pos = to_viewport.inverse().transform_point2(input.mouse.position);
|
||||
let Some(cursor) = graphene_core::text::compute_cursor_position(editing_text.editor.buffer(), &editing_text.text, pos)
|
||||
.filter(|_| graphene_core::text::has_hit_text_bounds(editing_text.editor.buffer(), &editing_text.text.spans, pos.as_vec2()))
|
||||
else {
|
||||
return self;
|
||||
};
|
||||
editing_text.editor.set_cursor(cursor);
|
||||
responses.add(ToolMessage::RefreshToolOptions);
|
||||
editing_text.editor.set_select_opt(None);
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
|
||||
return TextToolFsmState::Selecting;
|
||||
}
|
||||
(TextToolFsmState::Selecting, TextToolMessage::Drag) => {
|
||||
let Some(editing_text) = tool_data.editing_text.as_mut() else { return self };
|
||||
let pos = document.metadata().transform_to_viewport(tool_data.layer).inverse().transform_point2(input.mouse.position);
|
||||
if editing_text.editor.select_opt().is_none() {
|
||||
editing_text.editor.set_select_opt(Some(editing_text.editor.cursor()));
|
||||
}
|
||||
if let Some(cursor) = graphene_core::text::compute_cursor_position(editing_text.editor.buffer(), &editing_text.text, pos) {
|
||||
editing_text.editor.set_cursor(cursor);
|
||||
responses.add(ToolMessage::RefreshToolOptions);
|
||||
}
|
||||
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
self
|
||||
}
|
||||
(TextToolFsmState::Wrap, TextToolMessage::Drag) => {
|
||||
let Some(editing_text) = tool_data.editing_text.as_mut() else { return self };
|
||||
let pos = document.metadata().transform_to_viewport(tool_data.layer).inverse().transform_point2(input.mouse.position);
|
||||
editing_text.line_length = pos.x.max(0.);
|
||||
if let Some(mut font_system) = font_cache.get_system() {
|
||||
graphene_core::text::create_buffer(editing_text.editor.buffer_mut(), &mut font_system, &editing_text.text, font_cache, editing_text.line_length);
|
||||
}
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
self
|
||||
}
|
||||
(TextToolFsmState::Selecting | TextToolFsmState::Wrap, TextToolMessage::Interact) => TextToolFsmState::Editing,
|
||||
(TextToolFsmState::Ready, TextToolMessage::Interact) => {
|
||||
let font = Arc::new(Font::new(tool_options.font_name.clone(), tool_options.font_style.clone()));
|
||||
let size = tool_options.font_size as f32;
|
||||
let text = {
|
||||
let normal = TextSpan::new(font.clone(), size);
|
||||
let italic = TextSpan::new(font.clone(), size).offset(5).italic(Some(5.));
|
||||
let bold = TextSpan::new(font.clone(), size).offset(7).bold(Some(2.));
|
||||
let letter = TextSpan::new(font.clone(), size).offset(5).letter_spacing(5.);
|
||||
let word = TextSpan::new(font, size).offset(15).word_spacing(10.);
|
||||
RichText::new("text italic bold letter spacing word spacing", [normal, italic, bold, letter, word])
|
||||
};
|
||||
let line_length = f64::MAX;
|
||||
let editor = graphene_core::text::create_cosmic_editor(&text, font_cache, line_length);
|
||||
tool_data.editing_text = editor.map(|editor| EditingText {
|
||||
text,
|
||||
editor,
|
||||
color: tool_options.fill.active_color(),
|
||||
transform: DAffine2::from_translation(input.mouse.position),
|
||||
path: VectorData::empty(),
|
||||
line_length,
|
||||
composition: None,
|
||||
});
|
||||
tool_data.new_text = String::new();
|
||||
|
||||
tool_data.interact(state, input.mouse.position, document, font_cache, responses)
|
||||
tool_data.interact(self, input.mouse.position, document, font_cache, responses)
|
||||
}
|
||||
(state, TextToolMessage::EditSelected) => {
|
||||
if let Some(layer) = can_edit_selected(document) {
|
||||
|
@ -413,33 +723,146 @@ impl Fsm for TextToolFsmState {
|
|||
state
|
||||
}
|
||||
(state, TextToolMessage::Abort) => {
|
||||
if state == TextToolFsmState::Editing {
|
||||
tool_data.set_editing(false, font_cache, document, responses);
|
||||
if state != TextToolFsmState::Ready {
|
||||
tool_data.set_editing(false, document, responses);
|
||||
}
|
||||
|
||||
TextToolFsmState::Ready
|
||||
}
|
||||
(TextToolFsmState::Editing, TextToolMessage::CommitText) => {
|
||||
responses.add(FrontendMessage::TriggerTextCommit);
|
||||
|
||||
TextToolFsmState::Editing
|
||||
}
|
||||
(TextToolFsmState::Editing, TextToolMessage::TextChange { new_text }) => {
|
||||
tool_data.fix_text_bounds(&new_text, document, font_cache, responses);
|
||||
(TextToolFsmState::Editing | TextToolFsmState::Selecting | TextToolFsmState::Wrap, TextToolMessage::CommitText) | (TextToolFsmState::Editing, TextToolMessage::Interact) => {
|
||||
tool_data.set_editing(false, document, responses);
|
||||
let Some(editing_text) = tool_data.editing_text.take() else {
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
return TextToolFsmState::Ready;
|
||||
};
|
||||
responses.add(NodeGraphMessage::SetQualifiedInputValue {
|
||||
node_path: vec![graph_modification_utils::get_text_id(tool_data.layer, &document.network).unwrap()],
|
||||
input_index: 1,
|
||||
value: TaggedValue::String(new_text),
|
||||
value: TaggedValue::RichText(editing_text.text),
|
||||
});
|
||||
|
||||
tool_data.set_editing(false, font_cache, document, responses);
|
||||
responses.add(NodeGraphMessage::SetQualifiedInputValue {
|
||||
node_path: vec![graph_modification_utils::get_text_id(tool_data.layer, &document.network).unwrap()],
|
||||
input_index: 2,
|
||||
value: TaggedValue::F64(editing_text.line_length),
|
||||
});
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
|
||||
TextToolFsmState::Ready
|
||||
}
|
||||
(TextToolFsmState::Editing, TextToolMessage::UpdateBounds { new_text }) => {
|
||||
tool_data.new_text = new_text;
|
||||
(_, TextToolMessage::TextInput { input_type, data }) => {
|
||||
let Some(editing_text) = &mut tool_data.editing_text else { return self };
|
||||
let Some(mut font_system) = font_cache.get_system() else { return self };
|
||||
use graphene_core::text::cosmic_text::{Action, AttrsList, BufferLine, Cursor, Shaping};
|
||||
match input_type.as_str() {
|
||||
"insertText" | "insertFromPaste" => {
|
||||
if let Some(data) = data {
|
||||
editing_text.editor.insert_string(&data, None)
|
||||
}
|
||||
}
|
||||
"insertLineBreak" => editing_text.editor.action(&mut font_system, Action::Enter),
|
||||
"deleteContentForward" => editing_text.editor.action(&mut font_system, Action::Delete),
|
||||
"deleteContentBackward" => editing_text.editor.action(&mut font_system, Action::Backspace),
|
||||
"insertCompositionText" => {
|
||||
editing_text.editor.delete_selection();
|
||||
let cursor = editing_text.editor.cursor();
|
||||
let line = &mut editing_text.editor.buffer_mut().lines[cursor.line];
|
||||
|
||||
let after = line.split_off(cursor.index);
|
||||
let after_len = after.text().len();
|
||||
|
||||
line.split_off(line.text().len() - editing_text.composition.unwrap_or(0));
|
||||
editing_text.composition = data.as_ref().filter(|data| !data.is_empty()).map(|data| data.len());
|
||||
|
||||
if let Some(data) = data {
|
||||
line.append(BufferLine::new(data, AttrsList::new(line.attrs_list().get_span(cursor.index.saturating_sub(1))), Shaping::Advanced));
|
||||
}
|
||||
line.append(after);
|
||||
let index = editing_text.editor.buffer().lines[cursor.line].text().len() - after_len;
|
||||
editing_text.editor.set_cursor(Cursor { index, ..cursor });
|
||||
}
|
||||
"compositionend" => editing_text.composition = None,
|
||||
input_type => warn!("Unhandled input type {input_type}"),
|
||||
}
|
||||
editing_text.text.text.clear();
|
||||
let mut used = vec![false; editing_text.text.spans.len()];
|
||||
let mut last_total_offset = 0;
|
||||
for (index, line) in editing_text.editor.buffer().lines.iter().enumerate() {
|
||||
if index != 0 {
|
||||
editing_text.text.text.push('\n');
|
||||
}
|
||||
let line_start = editing_text.text.text.len();
|
||||
let spans = line.attrs_list().spans();
|
||||
for (val, attrs) in spans {
|
||||
if used[attrs.metadata] {
|
||||
continue;
|
||||
}
|
||||
let offset = (line_start + val.start) - last_total_offset;
|
||||
editing_text.text.spans[attrs.metadata].offset = offset;
|
||||
last_total_offset += offset;
|
||||
used[attrs.metadata] = true;
|
||||
}
|
||||
editing_text.text.text.push_str(line.text());
|
||||
|
||||
if !used[line.attrs_list().defaults().metadata] {
|
||||
let len = editing_text.text.text.len();
|
||||
let offset = len - last_total_offset;
|
||||
editing_text.text.spans[line.attrs_list().defaults().metadata].offset = offset;
|
||||
last_total_offset += offset;
|
||||
used[line.attrs_list().defaults().metadata] = true;
|
||||
}
|
||||
}
|
||||
|
||||
used[0] = true;
|
||||
let mut used = used.iter();
|
||||
editing_text.text.spans.retain(|_| used.next() == Some(&true));
|
||||
editing_text.text.spans[0].offset = 0;
|
||||
graphene_core::text::create_buffer(editing_text.editor.buffer_mut(), &mut font_system, &editing_text.text, font_cache, editing_text.line_length);
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
TextToolFsmState::Editing
|
||||
self
|
||||
}
|
||||
(_, TextToolMessage::TextNavigate { key, shift, ctrl }) => {
|
||||
let Some(editing_text) = &mut tool_data.editing_text else { return self };
|
||||
let Some(mut font_system) = font_cache.get_system() else { return self };
|
||||
|
||||
use graphene_core::text::cosmic_text::Action;
|
||||
let action = match key.as_str() {
|
||||
"ArrowLeft" if ctrl => Action::LeftWord,
|
||||
"ArrowLeft" => Action::Left,
|
||||
"ArrowRight" if ctrl => Action::RightWord,
|
||||
"ArrowRight" => Action::Right,
|
||||
"ArrowUp" => Action::Up,
|
||||
"ArrowDown" => Action::Down,
|
||||
"Home" if ctrl => Action::BufferStart,
|
||||
"Home" => Action::Home,
|
||||
"End" if ctrl => Action::BufferEnd,
|
||||
"End" => Action::End,
|
||||
"a" if ctrl => {
|
||||
use graphene_core::text::cosmic_text::{Affinity, Cursor};
|
||||
if let Some(last) = editing_text.editor.buffer().lines.last() {
|
||||
let last_index = last.text().len();
|
||||
let line = editing_text.editor.buffer().lines.len() - 1;
|
||||
editing_text.editor.set_select_opt(Some(Cursor::new_with_affinity(0, 0, Affinity::Before)));
|
||||
editing_text.editor.set_cursor(Cursor::new_with_affinity(line, last_index, Affinity::After));
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
_ => return self,
|
||||
};
|
||||
|
||||
if shift {
|
||||
editing_text
|
||||
.editor
|
||||
.set_select_opt(Some(editing_text.editor.select_opt().unwrap_or_else(|| editing_text.editor.cursor())));
|
||||
} else {
|
||||
editing_text.editor.set_select_opt(None);
|
||||
}
|
||||
editing_text.editor.action(&mut font_system, action);
|
||||
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
self
|
||||
}
|
||||
(_, TextToolMessage::WorkingColorChanged) => {
|
||||
responses.add(TextToolMessage::UpdateOptions(TextOptionsUpdate::WorkingColors(
|
||||
|
@ -458,7 +881,7 @@ impl Fsm for TextToolFsmState {
|
|||
HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, "Place Text")]),
|
||||
HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, "Edit Text")]),
|
||||
]),
|
||||
TextToolFsmState::Editing => HintData(vec![
|
||||
TextToolFsmState::Editing | TextToolFsmState::Selecting | TextToolFsmState::Wrap => HintData(vec![
|
||||
HintGroup(vec![HintInfo::keys([Key::Escape], "Discard Changes")]),
|
||||
HintGroup(vec![HintInfo::keys([Key::Control, Key::Enter], "Commit Changes").add_mac_keys([Key::Command, Key::Enter])]),
|
||||
]),
|
||||
|
|
3
frontend/assets/icon-16px-solid/bold.svg
Normal file
3
frontend/assets/icon-16px-solid/bold.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m 7.2857146,1.9999998 q 2.4033617,0 3.6302524,0.672269 1.226891,0.6722689 1.226891,2.3697484 0,1.0252098 -0.487422,1.7310921 -0.470588,0.7058824 -1.361344,0.8571429 v 0.084034 q 0.605042,0.1176454 1.092437,0.4201682 0.487369,0.302521 0.773109,0.8739497 0.28574,0.5714287 0.28574,1.5126049 0,1.630253 -1.176471,2.554622 Q 10.092438,14 8.0420174,14 H 3.5546217 V 1.9999998 Z m 0.2857399,4.7563027 q 1.1092438,0 1.529412,-0.3529398 0.4369747,-0.3529399 0.4369747,-1.0252102 0,-0.6890759 -0.5042019,-0.9915973 Q 8.5294378,4.0840342 7.4370005,4.0840342 H 6.0924637 V 6.7563025 Z M 6.0924637,8.7731096 V 11.89916 h 1.6638657 q 1.1428574,0 1.5966388,-0.436974 0.4538083,-0.453782 0.4538083,-1.193278 0,-0.6722687 -0.4705883,-1.0756302 Q 8.88238,8.7731096 7.6723225,8.7731096 Z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 852 B |
3
frontend/assets/icon-16px-solid/italic.svg
Normal file
3
frontend/assets/icon-16px-solid/italic.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m 4.6302435,14 0.168076,-0.823529 1.428574,-0.336136 2.05042,-9.6806707 -1.277314,-0.3193285 0.168076,-0.8403359 H 11.369757 L 11.201681,2.8403358 9.7563025,3.1596643 7.7058835,12.840335 9.0000005,13.176471 8.8319245,14 Z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 304 B |
|
@ -2,16 +2,13 @@
|
|||
import { getContext, onMount, tick } from "svelte";
|
||||
|
||||
import type { DocumentState } from "@graphite/state-providers/document";
|
||||
import { textInputCleanup } from "@graphite/utility-functions/keyboard-entry";
|
||||
import { extractPixelData, rasterizeSVGCanvas } from "@graphite/utility-functions/rasterization";
|
||||
import type { Editor } from "@graphite/wasm-communication/editor";
|
||||
import {
|
||||
type MouseCursorIcon,
|
||||
type XY,
|
||||
DisplayEditableTextbox,
|
||||
DisplayEditableTextboxTransform,
|
||||
DisplayRemoveEditableTextbox,
|
||||
TriggerTextCommit,
|
||||
TriggerViewportResize,
|
||||
UpdateDocumentArtwork,
|
||||
UpdateDocumentRulers,
|
||||
|
@ -37,7 +34,7 @@
|
|||
const document = getContext<DocumentState>("document");
|
||||
|
||||
// Interactive text editing
|
||||
let textInput: undefined | HTMLDivElement = undefined;
|
||||
let textInput: undefined | HTMLInputElement = undefined;
|
||||
let showTextInput: boolean;
|
||||
let textInputMatrix: number[];
|
||||
|
||||
|
@ -286,54 +283,39 @@
|
|||
canvasCursor = cursorString;
|
||||
}
|
||||
|
||||
// Text entry
|
||||
export function triggerTextCommit() {
|
||||
if (!textInput) return;
|
||||
const textCleaned = textInputCleanup(textInput.innerText);
|
||||
editor.instance.onChangeText(textCleaned);
|
||||
}
|
||||
|
||||
export async function displayEditableTextbox(displayEditableTextbox: DisplayEditableTextbox) {
|
||||
showTextInput = true;
|
||||
if (!showTextInput) {
|
||||
showTextInput = true;
|
||||
await tick();
|
||||
|
||||
await tick();
|
||||
if (!textInput) {
|
||||
return;
|
||||
}
|
||||
|
||||
textInput.style.transformOrigin = "0 0";
|
||||
textInput.style.width = "20px";
|
||||
textInput.style.height = "auto";
|
||||
textInput.style.color = "transparent";
|
||||
textInput.style.background = "transparent";
|
||||
textInput.style.opacity = "0";
|
||||
|
||||
textInput.onkeydown = (e) => editor.instance.onTextNavigate(e.key, e.shiftKey, e.ctrlKey);
|
||||
textInput.ondrag = (e) => e.preventDefault();
|
||||
|
||||
textInput.onbeforeinput = (e) => {
|
||||
e.preventDefault();
|
||||
editor.instance.onTextInput(e.inputType, e.data || undefined);
|
||||
};
|
||||
addEventListener("compositionend", (e) => editor.instance.onTextInput("compositionend", e.data || undefined));
|
||||
}
|
||||
if (!textInput) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (displayEditableTextbox.text === "") textInput.textContent = "";
|
||||
else textInput.textContent = `${displayEditableTextbox.text}\n`;
|
||||
|
||||
textInput.contentEditable = "true";
|
||||
textInput.style.transformOrigin = "0 0";
|
||||
textInput.style.width = displayEditableTextbox.lineWidth ? `${displayEditableTextbox.lineWidth}px` : "max-content";
|
||||
textInput.style.height = "auto";
|
||||
textInput.style.fontSize = `${displayEditableTextbox.fontSize}px`;
|
||||
textInput.style.color = displayEditableTextbox.color.toHexOptionalAlpha() || "transparent";
|
||||
|
||||
textInput.oninput = () => {
|
||||
if (!textInput) return;
|
||||
editor.instance.updateBounds(textInputCleanup(textInput.innerText));
|
||||
};
|
||||
textInput.value = displayEditableTextbox.text;
|
||||
textInputMatrix = displayEditableTextbox.transform;
|
||||
const newFont = new FontFace("text-font", `url(${displayEditableTextbox.url})`);
|
||||
window.document.fonts.add(newFont);
|
||||
textInput.style.fontFamily = "text-font";
|
||||
|
||||
// Necessary to select contenteditable: https://stackoverflow.com/questions/6139107/programmatically-select-text-in-a-contenteditable-html-element/6150060#6150060
|
||||
|
||||
const range = window.document.createRange();
|
||||
range.selectNodeContents(textInput);
|
||||
|
||||
const selection = window.getSelection();
|
||||
if (selection) {
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
}
|
||||
|
||||
textInput.focus();
|
||||
textInput.click();
|
||||
textInput.setSelectionRange(0, textInput.value.length);
|
||||
|
||||
// Sends the text input element used for interactively editing with the text tool in a custom event
|
||||
window.dispatchEvent(new CustomEvent("modifyinputfield", { detail: textInput }));
|
||||
|
@ -399,19 +381,11 @@
|
|||
});
|
||||
|
||||
// Text entry
|
||||
editor.subscriptions.subscribeJsMessage(TriggerTextCommit, async () => {
|
||||
await tick();
|
||||
|
||||
triggerTextCommit();
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(DisplayEditableTextbox, async (data) => {
|
||||
await tick();
|
||||
|
||||
displayEditableTextbox(data);
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(DisplayEditableTextboxTransform, async (data) => {
|
||||
textInputMatrix = data.transform;
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(DisplayRemoveEditableTextbox, async () => {
|
||||
await tick();
|
||||
|
||||
|
@ -491,7 +465,7 @@
|
|||
</svg>
|
||||
<div class="text-input" style:width={canvasWidthCSS} style:height={canvasHeightCSS} style:pointer-events={showTextInput ? "auto" : ""}>
|
||||
{#if showTextInput}
|
||||
<div bind:this={textInput} style:transform="matrix({textInputMatrix})" />
|
||||
<input bind:this={textInput} style:transform="matrix({textInputMatrix})" />
|
||||
{/if}
|
||||
</div>
|
||||
<canvas class="overlays" width={canvasWidthRoundedToEven} height={canvasHeightRoundedToEven} style:width={canvasWidthCSS} style:height={canvasHeightCSS} data-overlays-canvas>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
import { ICONS } from "@graphite/utility-functions/icons";
|
||||
import type { IconName } from "@graphite/utility-functions/icons";
|
||||
|
||||
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
|
||||
|
@ -39,6 +40,7 @@
|
|||
type="checkbox"
|
||||
id={`checkbox-input-${id}`}
|
||||
{checked}
|
||||
style:height={`${ICONS[icon].size}px`}
|
||||
on:change={(_) => dispatch("checked", inputElement?.checked || false)}
|
||||
{disabled}
|
||||
tabindex={disabled ? -1 : 0}
|
||||
|
@ -67,7 +69,6 @@
|
|||
// Unchecked
|
||||
label {
|
||||
display: flex;
|
||||
height: 16px;
|
||||
// Provides rounded corners for the :focus outline
|
||||
border-radius: 2px;
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import { type DialogState } from "@graphite/state-providers/dialog";
|
|||
import { type DocumentState } from "@graphite/state-providers/document";
|
||||
import { type FullscreenState } from "@graphite/state-providers/fullscreen";
|
||||
import { type PortfolioState } from "@graphite/state-providers/portfolio";
|
||||
import { makeKeyboardModifiersBitfield, textInputCleanup, getLocalizedScanCode } from "@graphite/utility-functions/keyboard-entry";
|
||||
import { makeKeyboardModifiersBitfield, getLocalizedScanCode } from "@graphite/utility-functions/keyboard-entry";
|
||||
import { platformIsMac } from "@graphite/utility-functions/platform";
|
||||
import { extractPixelData } from "@graphite/utility-functions/rasterization";
|
||||
import { stripIndents } from "@graphite/utility-functions/strip-indents";
|
||||
|
@ -161,7 +161,6 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
|
|||
const { target } = e;
|
||||
const isTargetingCanvas = target instanceof Element && target.closest("[data-viewport]");
|
||||
const inDialog = target instanceof Element && target.closest("[data-dialog] [data-floating-menu-content]");
|
||||
const inTextInput = target === textToolInteractiveInputElement;
|
||||
|
||||
if (get(dialog).visible && !inDialog) {
|
||||
dialog.dismissDialog();
|
||||
|
@ -169,10 +168,7 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
|
|||
e.stopPropagation();
|
||||
}
|
||||
|
||||
if (!inTextInput) {
|
||||
if (textToolInteractiveInputElement) editor.instance.onChangeText(textInputCleanup(textToolInteractiveInputElement.innerText));
|
||||
else viewportPointerInteractionOngoing = isTargetingCanvas instanceof Element;
|
||||
}
|
||||
viewportPointerInteractionOngoing = isTargetingCanvas instanceof Element;
|
||||
|
||||
if (viewportPointerInteractionOngoing) {
|
||||
const modifiers = makeKeyboardModifiersBitfield(e);
|
||||
|
|
|
@ -94,6 +94,7 @@ import AlignRight from "@graphite-frontend/assets/icon-16px-solid/align-right.sv
|
|||
import AlignTop from "@graphite-frontend/assets/icon-16px-solid/align-top.svg";
|
||||
import AlignVerticalCenter from "@graphite-frontend/assets/icon-16px-solid/align-vertical-center.svg";
|
||||
import Artboard from "@graphite-frontend/assets/icon-16px-solid/artboard.svg";
|
||||
import Bold from "@graphite-frontend/assets/icon-16px-solid/bold.svg";
|
||||
import BooleanDifference from "@graphite-frontend/assets/icon-16px-solid/boolean-difference.svg";
|
||||
import BooleanIntersect from "@graphite-frontend/assets/icon-16px-solid/boolean-intersect.svg";
|
||||
import BooleanSubtractBack from "@graphite-frontend/assets/icon-16px-solid/boolean-subtract-back.svg";
|
||||
|
@ -119,6 +120,7 @@ import GraphViewOpen from "@graphite-frontend/assets/icon-16px-solid/graph-view-
|
|||
import GraphiteLogo from "@graphite-frontend/assets/icon-16px-solid/graphite-logo.svg";
|
||||
import IconsGrid from "@graphite-frontend/assets/icon-16px-solid/icons-grid.svg";
|
||||
import Image from "@graphite-frontend/assets/icon-16px-solid/image.svg";
|
||||
import Italic from "@graphite-frontend/assets/icon-16px-solid/italic.svg";
|
||||
import Layer from "@graphite-frontend/assets/icon-16px-solid/layer.svg";
|
||||
import License from "@graphite-frontend/assets/icon-16px-solid/license.svg";
|
||||
import NewLayer from "@graphite-frontend/assets/icon-16px-solid/new-layer.svg";
|
||||
|
@ -167,6 +169,7 @@ const SOLID_16PX = {
|
|||
AlignTop: { svg: AlignTop, size: 16 },
|
||||
AlignVerticalCenter: { svg: AlignVerticalCenter, size: 16 },
|
||||
Artboard: { svg: Artboard, size: 16 },
|
||||
Bold: { svg: Bold, size: 16 },
|
||||
BooleanDifference: { svg: BooleanDifference, size: 16 },
|
||||
BooleanIntersect: { svg: BooleanIntersect, size: 16 },
|
||||
BooleanSubtractBack: { svg: BooleanSubtractBack, size: 16 },
|
||||
|
@ -192,6 +195,7 @@ const SOLID_16PX = {
|
|||
GraphViewOpen: { svg: GraphViewOpen, size: 16 },
|
||||
IconsGrid: { svg: IconsGrid, size: 16 },
|
||||
Image: { svg: Image, size: 16 },
|
||||
Italic: { svg: Italic, size: 16 },
|
||||
Layer: { svg: Layer, size: 16 },
|
||||
License: { svg: License, size: 16 },
|
||||
NewLayer: { svg: NewLayer, size: 16 },
|
||||
|
|
|
@ -11,12 +11,6 @@ export function makeKeyboardModifiersBitfield(e: WheelEvent | PointerEvent | Mou
|
|||
);
|
||||
}
|
||||
|
||||
// Necessary because innerText puts an extra newline character at the end when the text is more than one line.
|
||||
export function textInputCleanup(text: string): string {
|
||||
if (text[text.length - 1] === "\n") return text.slice(0, -1);
|
||||
return text;
|
||||
}
|
||||
|
||||
// This function tries to find what scan code the user pressed, even if using a non-US keyboard.
|
||||
// Directly using `KeyboardEvent.code` scan code only works on a US QWERTY layout, because alternate layouts like
|
||||
// QWERTZ (German) or AZERTY (French) will end up reporting the wrong keys.
|
||||
|
|
|
@ -580,19 +580,6 @@ export class UpdateDocumentLayerStructureJs extends JsMessage {
|
|||
export class DisplayEditableTextbox extends JsMessage {
|
||||
readonly text!: string;
|
||||
|
||||
readonly lineWidth!: undefined | number;
|
||||
|
||||
readonly fontSize!: number;
|
||||
|
||||
@Type(() => Color)
|
||||
readonly color!: Color;
|
||||
|
||||
readonly url!: string;
|
||||
|
||||
readonly transform!: number[];
|
||||
}
|
||||
|
||||
export class DisplayEditableTextboxTransform extends JsMessage {
|
||||
readonly transform!: number[];
|
||||
}
|
||||
|
||||
|
@ -656,8 +643,6 @@ export class TriggerVisitLink extends JsMessage {
|
|||
url!: string;
|
||||
}
|
||||
|
||||
export class TriggerTextCommit extends JsMessage {}
|
||||
|
||||
export class TriggerTextCopy extends JsMessage {
|
||||
readonly copyText!: string;
|
||||
}
|
||||
|
@ -1285,7 +1270,6 @@ export const messageMakers: Record<string, MessageMaker> = {
|
|||
DisplayDialogDismiss,
|
||||
DisplayDialogPanic,
|
||||
DisplayEditableTextbox,
|
||||
DisplayEditableTextboxTransform,
|
||||
DisplayRemoveEditableTextbox,
|
||||
TriggerAboutGraphiteLocalizedCommitDate,
|
||||
TriggerCopyToClipboardBlobUrl,
|
||||
|
@ -1305,7 +1289,6 @@ export const messageMakers: Record<string, MessageMaker> = {
|
|||
TriggerRefreshBoundsOfViewports,
|
||||
TriggerRevokeBlobUrl,
|
||||
TriggerSavePreferences,
|
||||
TriggerTextCommit,
|
||||
TriggerTextCopy,
|
||||
TriggerViewportResize,
|
||||
TriggerVisitLink,
|
||||
|
|
|
@ -517,15 +517,6 @@ impl JsEditorHandle {
|
|||
self.dispatch(message);
|
||||
}
|
||||
|
||||
/// A text box was committed
|
||||
#[wasm_bindgen(js_name = onChangeText)]
|
||||
pub fn on_change_text(&self, new_text: String) -> Result<(), JsValue> {
|
||||
let message = TextToolMessage::TextChange { new_text };
|
||||
self.dispatch(message);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A font has been downloaded
|
||||
#[wasm_bindgen(js_name = onFontLoad)]
|
||||
pub fn on_font_load(&self, font_family: String, font_style: String, preview_url: String, data: Vec<u8>, is_default: bool) -> Result<(), JsValue> {
|
||||
|
@ -542,11 +533,18 @@ impl JsEditorHandle {
|
|||
}
|
||||
|
||||
/// A text box was changed
|
||||
#[wasm_bindgen(js_name = updateBounds)]
|
||||
pub fn update_bounds(&self, new_text: String) -> Result<(), JsValue> {
|
||||
let message = TextToolMessage::UpdateBounds { new_text };
|
||||
#[wasm_bindgen(js_name = onTextInput)]
|
||||
pub fn on_text_input(&self, input_type: String, data: Option<String>) -> Result<(), JsValue> {
|
||||
let message = TextToolMessage::TextInput { input_type, data };
|
||||
self.dispatch(message);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A text box was changed
|
||||
#[wasm_bindgen(js_name = onTextNavigate)]
|
||||
pub fn on_text_navigate(&self, key: String, shift: bool, ctrl: bool) -> Result<(), JsValue> {
|
||||
let message = TextToolMessage::TextNavigate { key, shift, ctrl };
|
||||
self.dispatch(message);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
|
@ -18,4 +18,4 @@ glam = { version = "0.25", features = ["serde"] }
|
|||
|
||||
dyn-any = { version = "0.3.0", path = "../dyn-any", optional = true }
|
||||
serde = { workspace = true, optional = true }
|
||||
log = { workspace = true, optional = true }
|
||||
log = { workspace = true}
|
||||
|
|
|
@ -111,7 +111,6 @@ impl Debug for Bezier {
|
|||
debug_struct_ref.field("end", &self.end).finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "dyn-any")]
|
||||
unsafe impl dyn_any::StaticType for Bezier {
|
||||
type Static = Bezier;
|
||||
|
|
|
@ -272,9 +272,9 @@ impl Bezier {
|
|||
_ => *self,
|
||||
};
|
||||
|
||||
let should_flip_direction = (self.start - intersection).normalize().abs_diff_eq(normal_start, MAX_ABSOLUTE_DIFFERENCE);
|
||||
let should_flip_direction = (self.start - intersection).normalize_or_zero().abs_diff_eq(normal_start, MAX_ABSOLUTE_DIFFERENCE);
|
||||
intermediate.apply_transformation(|point| {
|
||||
let mut direction_unit_vector = (intersection - point).normalize();
|
||||
let mut direction_unit_vector = (intersection - point).normalize_or_zero();
|
||||
if should_flip_direction {
|
||||
direction_unit_vector *= -1.;
|
||||
}
|
||||
|
|
|
@ -35,7 +35,7 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
|
|||
/// Both the input global `t` value and the output `t` value are in euclidean space, meaning there is a constant rate of change along the arc length.
|
||||
pub fn global_euclidean_to_local_euclidean(&self, global_t: f64, lengths: &[f64], total_length: f64) -> (usize, f64) {
|
||||
let mut accumulator = 0.;
|
||||
for (index, length) in lengths.iter().enumerate() {
|
||||
for (index, &length) in lengths.iter().enumerate() {
|
||||
let length_ratio = length / total_length;
|
||||
if (index == 0 || accumulator <= global_t) && global_t <= accumulator + length_ratio {
|
||||
return (index, ((global_t - accumulator) / length_ratio).clamp(0., 1.));
|
||||
|
|
|
@ -322,8 +322,8 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
|
|||
let in_tangent = in_segment.tangent(TValue::Parametric(1.));
|
||||
let out_tangent = out_segment.tangent(TValue::Parametric(0.));
|
||||
|
||||
let normalized_in_tangent = in_tangent.normalize();
|
||||
let normalized_out_tangent = out_tangent.normalize();
|
||||
let normalized_in_tangent = in_tangent.try_normalize()?;
|
||||
let normalized_out_tangent = out_tangent.try_normalize()?;
|
||||
|
||||
// The tangents must not be parallel for the miter join
|
||||
if !normalized_in_tangent.abs_diff_eq(normalized_out_tangent, MAX_ABSOLUTE_DIFFERENCE) && !normalized_in_tangent.abs_diff_eq(-normalized_out_tangent, MAX_ABSOLUTE_DIFFERENCE) {
|
||||
|
@ -333,8 +333,8 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
|
|||
let intersection_to_end = out_segment.start() - intersection;
|
||||
|
||||
// Draw the miter join if the intersection occurs in the correct direction with respect to the path
|
||||
if start_to_intersection.normalize().abs_diff_eq(in_tangent, MAX_ABSOLUTE_DIFFERENCE)
|
||||
&& intersection_to_end.normalize().abs_diff_eq(out_tangent, MAX_ABSOLUTE_DIFFERENCE)
|
||||
if start_to_intersection.try_normalize()?.abs_diff_eq(in_tangent, MAX_ABSOLUTE_DIFFERENCE)
|
||||
&& intersection_to_end.try_normalize()?.abs_diff_eq(out_tangent, MAX_ABSOLUTE_DIFFERENCE)
|
||||
&& miter_limit >= 1. / (start_to_intersection.angle_between(-intersection_to_end).abs() / 2.).sin()
|
||||
{
|
||||
return Some(ManipulatorGroup {
|
||||
|
|
|
@ -16,7 +16,8 @@ std = [
|
|||
"glam/std",
|
||||
"specta",
|
||||
"num-traits/std",
|
||||
"rustybuzz",
|
||||
"cosmic-text",
|
||||
"unicode-segmentation",
|
||||
"image",
|
||||
]
|
||||
default = ["serde", "kurbo", "log", "std", "rand_chacha", "wasm"]
|
||||
|
@ -52,7 +53,8 @@ image = { workspace = true, optional = true, default-features = false, features
|
|||
"png",
|
||||
] }
|
||||
specta = { workspace = true, optional = true }
|
||||
rustybuzz = { workspace = true, optional = true }
|
||||
cosmic-text = { workspace = true, optional = true }
|
||||
unicode-segmentation = { version = "1.10", optional = true }
|
||||
num-derive = { workspace = true }
|
||||
num-traits = { workspace = true, default-features = false, features = ["i128"] }
|
||||
wasm-bindgen = { workspace = true, optional = true }
|
||||
|
|
|
@ -1,21 +1,116 @@
|
|||
mod font_cache;
|
||||
mod to_path;
|
||||
|
||||
use crate::application_io::EditorApi;
|
||||
use core::future::Future;
|
||||
|
||||
use crate::vector::VectorData;
|
||||
use crate::Color;
|
||||
use crate::{application_io::EditorApi, transform::Footprint};
|
||||
use alloc::sync::Arc;
|
||||
use dyn_any::{DynAny, StaticType};
|
||||
pub use font_cache::*;
|
||||
use glam::Vec2;
|
||||
use node_macro::node_fn;
|
||||
pub use to_path::*;
|
||||
|
||||
use crate::Node;
|
||||
|
||||
pub struct TextGeneratorNode<Text, FontName, Size> {
|
||||
text: Text,
|
||||
font_name: FontName,
|
||||
font_size: Size,
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct TextSpan {
|
||||
pub offset: usize,
|
||||
pub bold: Option<f32>,
|
||||
pub italic: Option<f32>,
|
||||
pub font: Arc<Font>,
|
||||
pub font_size: f32,
|
||||
pub letter_spacing: f32,
|
||||
pub word_spacing: f32,
|
||||
pub line_spacing: f32,
|
||||
pub kerning: Vec2,
|
||||
pub color: Color,
|
||||
}
|
||||
|
||||
impl core::hash::Hash for TextSpan {
|
||||
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
|
||||
self.offset.hash(state);
|
||||
self.bold.map(|x| x.to_bits()).hash(state);
|
||||
self.italic.map(|x| x.to_bits()).hash(state);
|
||||
self.font.hash(state);
|
||||
self.font_size.to_bits().hash(state);
|
||||
self.letter_spacing.to_bits().hash(state);
|
||||
self.word_spacing.to_bits().hash(state);
|
||||
self.line_spacing.to_bits().hash(state);
|
||||
self.kerning.x.to_bits().hash(state);
|
||||
self.kerning.y.to_bits().hash(state);
|
||||
self.color.hash(state);
|
||||
}
|
||||
}
|
||||
impl TextSpan {
|
||||
pub fn new(font: impl Into<Arc<Font>>, font_size: f32) -> Self {
|
||||
Self {
|
||||
offset: 0,
|
||||
bold: None,
|
||||
italic: None,
|
||||
font: font.into(),
|
||||
font_size,
|
||||
letter_spacing: 0.,
|
||||
word_spacing: 0.,
|
||||
line_spacing: 1.,
|
||||
kerning: Vec2::ZERO,
|
||||
color: Color::BLACK,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn offset(mut self, offset: usize) -> Self {
|
||||
self.offset = offset;
|
||||
self
|
||||
}
|
||||
pub fn italic(mut self, italic: Option<f32>) -> Self {
|
||||
self.italic = italic;
|
||||
self
|
||||
}
|
||||
pub fn bold(mut self, bold: Option<f32>) -> Self {
|
||||
self.bold = bold;
|
||||
self
|
||||
}
|
||||
pub fn letter_spacing(mut self, letter_spacing: f32) -> Self {
|
||||
self.letter_spacing = letter_spacing;
|
||||
self
|
||||
}
|
||||
pub fn word_spacing(mut self, word_spacing: f32) -> Self {
|
||||
self.word_spacing = word_spacing;
|
||||
self
|
||||
}
|
||||
}
|
||||
#[derive(Clone, Debug, Default, Hash, PartialEq, DynAny)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct RichText {
|
||||
pub text: String,
|
||||
pub spans: Vec<TextSpan>,
|
||||
}
|
||||
|
||||
impl RichText {
|
||||
pub fn new(text: impl Into<String>, spans: impl Into<Vec<TextSpan>>) -> Self {
|
||||
Self {
|
||||
text: text.into(),
|
||||
spans: spans.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TextGeneratorNode<RichTextNode, LineLengthNode, PathNode> {
|
||||
text: RichTextNode,
|
||||
line_length: LineLengthNode,
|
||||
path: PathNode,
|
||||
}
|
||||
|
||||
#[node_fn(TextGeneratorNode)]
|
||||
fn generate_text<'a: 'input, T>(editor: EditorApi<'a, T>, text: String, font_name: Font, font_size: f64) -> crate::vector::VectorData {
|
||||
let buzz_face = editor.font_cache.get(&font_name).map(|data| load_face(data));
|
||||
crate::vector::VectorData::from_subpaths(to_path(&text, buzz_face, font_size, None))
|
||||
async fn generate_text<'a: 'input, T, FV: Future<Output = VectorData>>(
|
||||
editor: EditorApi<'a, T>,
|
||||
text: RichText,
|
||||
line_length: f64,
|
||||
path: impl Node<Footprint, Output = FV>,
|
||||
) -> crate::vector::VectorData {
|
||||
let path = self.path.eval(editor.render_config.viewport).await;
|
||||
crate::vector::VectorData::from_subpaths(rich_text_to_path(&text, line_length, &path, editor.font_cache))
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
use dyn_any::{DynAny, StaticType};
|
||||
|
||||
use alloc::sync::Arc;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Mutex;
|
||||
|
||||
/// A font type (storing font family and font style and an optional preview URL)
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Hash, PartialEq, Eq, DynAny, specta::Type)]
|
||||
|
@ -17,33 +19,27 @@ impl Font {
|
|||
}
|
||||
|
||||
/// A cache of all loaded font data and preview urls along with the default font (send from `init_app` in `editor_api.rs`)
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default, PartialEq)]
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct FontCache {
|
||||
/// Actual font file data used for rendering a font with ttf_parser and rustybuzz
|
||||
font_file_data: HashMap<Font, Vec<u8>>,
|
||||
/// Actual font file data used for rendering a font
|
||||
pub font_attrs: HashMap<Font, cosmic_text::AttrsOwned>,
|
||||
/// Web font preview URLs used for showing fonts when live editing
|
||||
preview_urls: HashMap<Font, String>,
|
||||
/// The default font (used as a fallback)
|
||||
default_font: Option<Font>,
|
||||
pub default_font: Option<Font>,
|
||||
system: Option<Arc<Mutex<cosmic_text::FontSystem>>>,
|
||||
}
|
||||
impl FontCache {
|
||||
/// Returns the font family name if the font is cached, otherwise returns the default font family name if that is cached
|
||||
pub fn resolve_font<'a>(&'a self, font: &'a Font) -> Option<&'a Font> {
|
||||
if self.loaded_font(font) {
|
||||
Some(font)
|
||||
} else {
|
||||
self.default_font.as_ref().filter(|font| self.loaded_font(font))
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to get the bytes for a font
|
||||
pub fn get<'a>(&'a self, font: &Font) -> Option<&'a Vec<u8>> {
|
||||
self.resolve_font(font).and_then(|font| self.font_file_data.get(font))
|
||||
#[must_use]
|
||||
fn font_system() -> Arc<Mutex<cosmic_text::FontSystem>> {
|
||||
// TODO: better locale
|
||||
Arc::new(Mutex::new(cosmic_text::FontSystem::new_with_locale_and_db("en".to_string(), cosmic_text::fontdb::Database::new())))
|
||||
}
|
||||
|
||||
/// Check if the font is already loaded
|
||||
#[must_use]
|
||||
pub fn loaded_font(&self, font: &Font) -> bool {
|
||||
self.font_file_data.contains_key(font)
|
||||
self.font_attrs.contains_key(font)
|
||||
}
|
||||
|
||||
/// Insert a new font into the cache
|
||||
|
@ -51,19 +47,41 @@ impl FontCache {
|
|||
if is_default {
|
||||
self.default_font = Some(font.clone());
|
||||
}
|
||||
self.font_file_data.insert(font.clone(), data);
|
||||
let mut font_system = self.system.get_or_insert_with(Self::font_system).lock().expect("acquire font system");
|
||||
let data = Arc::new(data);
|
||||
let db = font_system.db_mut();
|
||||
let id = db.load_font_source(cosmic_text::fontdb::Source::Binary(data.clone()))[0];
|
||||
if let Some(face) = db.face(id) {
|
||||
info!("Face {face:#?}");
|
||||
let attrs = cosmic_text::AttrsOwned::new(
|
||||
cosmic_text::Attrs::new()
|
||||
.family(cosmic_text::Family::Name(&face.families[0].0))
|
||||
.stretch(face.stretch)
|
||||
.style(face.style)
|
||||
.weight(face.weight),
|
||||
);
|
||||
self.font_attrs.insert(font.clone(), attrs);
|
||||
}
|
||||
|
||||
self.preview_urls.insert(font, perview_url);
|
||||
}
|
||||
|
||||
/// Checks if the font cache has a default font
|
||||
#[must_use]
|
||||
pub fn has_default(&self) -> bool {
|
||||
self.default_font.is_some()
|
||||
}
|
||||
|
||||
/// Gets the preview URL for showing in text field when live editing
|
||||
#[must_use]
|
||||
pub fn get_preview_url(&self, font: &Font) -> Option<&String> {
|
||||
self.preview_urls.get(font)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_system(&self) -> Option<std::sync::MutexGuard<cosmic_text::FontSystem>> {
|
||||
self.system.as_ref().map(|system| system.lock().expect("acquire font system"))
|
||||
}
|
||||
}
|
||||
|
||||
impl core::hash::Hash for FontCache {
|
||||
|
@ -73,7 +91,13 @@ impl core::hash::Hash for FontCache {
|
|||
font.hash(state);
|
||||
url.hash(state)
|
||||
});
|
||||
self.font_file_data.len().hash(state);
|
||||
self.font_file_data.keys().for_each(|font| font.hash(state));
|
||||
self.font_attrs.len().hash(state);
|
||||
self.font_attrs.keys().for_each(|font| font.hash(state));
|
||||
}
|
||||
}
|
||||
|
||||
impl core::cmp::PartialEq for FontCache {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.font_attrs == other.font_attrs
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,47 +1,54 @@
|
|||
use crate::uuid::ManipulatorGroupId;
|
||||
|
||||
use crate::text::to_path::cosmic_text::Edit;
|
||||
use crate::{uuid::ManipulatorGroupId, vector::VectorData};
|
||||
use bezier_rs::{ManipulatorGroup, Subpath};
|
||||
use glam::{DAffine2, DVec2, Vec2};
|
||||
pub extern crate cosmic_text;
|
||||
use super::{FontCache, RichText, TextSpan};
|
||||
use core::cmp;
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
use glam::DVec2;
|
||||
use rustybuzz::ttf_parser::{GlyphId, OutlineBuilder};
|
||||
use rustybuzz::{GlyphBuffer, UnicodeBuffer};
|
||||
|
||||
/// Builds subpaths out of a glyph outline
|
||||
struct Builder {
|
||||
current_subpath: Subpath<ManipulatorGroupId>,
|
||||
other_subpaths: Vec<Subpath<ManipulatorGroupId>>,
|
||||
pos: DVec2,
|
||||
offset: DVec2,
|
||||
transform: DAffine2,
|
||||
ascender: f64,
|
||||
scale: f64,
|
||||
font_size: f64,
|
||||
bold: Option<f64>,
|
||||
italic: Option<f64>,
|
||||
id: ManipulatorGroupId,
|
||||
}
|
||||
|
||||
impl Builder {
|
||||
fn point(&self, x: f32, y: f32) -> DVec2 {
|
||||
self.pos + self.offset + DVec2::new(x as f64, self.ascender - y as f64) * self.scale
|
||||
fn convert_point(&self, x: f32, y: f32) -> DVec2 {
|
||||
self.transform
|
||||
.transform_point2(DVec2::new(x as f64 + y as f64 * self.font_size * self.italic.unwrap_or(0.) / self.ascender, 0. - y as f64) * self.scale)
|
||||
}
|
||||
}
|
||||
|
||||
impl OutlineBuilder for Builder {
|
||||
impl cosmic_text::rustybuzz::ttf_parser::OutlineBuilder for Builder {
|
||||
fn move_to(&mut self, x: f32, y: f32) {
|
||||
if !self.current_subpath.is_empty() {
|
||||
self.other_subpaths.push(core::mem::replace(&mut self.current_subpath, Subpath::new(Vec::new(), false)));
|
||||
}
|
||||
self.current_subpath.push_manipulator_group(ManipulatorGroup::new_anchor_with_id(self.point(x, y), self.id.next_id()));
|
||||
self.current_subpath
|
||||
.push_manipulator_group(ManipulatorGroup::new_anchor_with_id(self.convert_point(x, y), self.id.next_id()));
|
||||
}
|
||||
|
||||
fn line_to(&mut self, x: f32, y: f32) {
|
||||
self.current_subpath.push_manipulator_group(ManipulatorGroup::new_anchor_with_id(self.point(x, y), self.id.next_id()));
|
||||
self.current_subpath
|
||||
.push_manipulator_group(ManipulatorGroup::new_anchor_with_id(self.convert_point(x, y), self.id.next_id()));
|
||||
}
|
||||
|
||||
fn quad_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32) {
|
||||
let [handle, anchor] = [self.point(x1, y1), self.point(x2, y2)];
|
||||
let [handle, anchor] = [self.convert_point(x1, y1), self.convert_point(x2, y2)];
|
||||
self.current_subpath.last_manipulator_group_mut().unwrap().out_handle = Some(handle);
|
||||
self.current_subpath.push_manipulator_group(ManipulatorGroup::new_anchor_with_id(anchor, self.id.next_id()));
|
||||
}
|
||||
|
||||
fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x3: f32, y3: f32) {
|
||||
let [handle1, handle2, anchor] = [self.point(x1, y1), self.point(x2, y2), self.point(x3, y3)];
|
||||
let [handle1, handle2, anchor] = [self.convert_point(x1, y1), self.convert_point(x2, y2), self.convert_point(x3, y3)];
|
||||
self.current_subpath.last_manipulator_group_mut().unwrap().out_handle = Some(handle1);
|
||||
self.current_subpath
|
||||
.push_manipulator_group(ManipulatorGroup::new_with_id(anchor, Some(handle2), None, self.id.next_id()));
|
||||
|
@ -49,129 +56,389 @@ impl OutlineBuilder for Builder {
|
|||
|
||||
fn close(&mut self) {
|
||||
self.current_subpath.set_closed(true);
|
||||
if let Some(bold) = self.bold {
|
||||
self.current_subpath = self.current_subpath.offset(-bold * self.scale * self.font_size, bezier_rs::Join::Miter(None));
|
||||
}
|
||||
self.other_subpaths.push(core::mem::replace(&mut self.current_subpath, Subpath::new(Vec::new(), false)));
|
||||
}
|
||||
}
|
||||
|
||||
fn font_properties(buzz_face: &rustybuzz::Face, font_size: f64) -> (f64, f64, UnicodeBuffer) {
|
||||
let scale = (buzz_face.units_per_em() as f64).recip() * font_size;
|
||||
let line_height = font_size;
|
||||
let buffer = UnicodeBuffer::new();
|
||||
(scale, line_height, buffer)
|
||||
#[must_use]
|
||||
pub fn rich_text_to_path(text: &RichText, line_length: f64, path: &VectorData, font_cache: &FontCache) -> Vec<Subpath<ManipulatorGroupId>> {
|
||||
let Some(mut font_system) = font_cache.get_system() else { return Vec::new() };
|
||||
let mut buffer = construct_buffer(text, &mut font_system);
|
||||
|
||||
create_buffer(&mut buffer, &mut font_system, text, font_cache, line_length);
|
||||
|
||||
buffer_to_path(&buffer, &mut font_system, &text.spans, path)
|
||||
}
|
||||
|
||||
fn push_str(buffer: &mut UnicodeBuffer, word: &str, trailing_space: bool) {
|
||||
buffer.push_str(word);
|
||||
|
||||
if trailing_space {
|
||||
buffer.push_str(" ");
|
||||
}
|
||||
}
|
||||
|
||||
fn wrap_word(line_width: Option<f64>, glyph_buffer: &GlyphBuffer, scale: f64, x_pos: f64) -> bool {
|
||||
if let Some(line_width) = line_width {
|
||||
let word_length: i32 = glyph_buffer.glyph_positions().iter().map(|pos| pos.x_advance).sum();
|
||||
let scaled_word_length = word_length as f64 * scale;
|
||||
|
||||
if scaled_word_length + x_pos > line_width {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn to_path(str: &str, buzz_face: Option<rustybuzz::Face>, font_size: f64, line_width: Option<f64>) -> Vec<Subpath<ManipulatorGroupId>> {
|
||||
let buzz_face = match buzz_face {
|
||||
Some(face) => face,
|
||||
// Show blank layer if font has not loaded
|
||||
None => return vec![],
|
||||
};
|
||||
|
||||
let (scale, line_height, mut buffer) = font_properties(&buzz_face, font_size);
|
||||
|
||||
#[must_use]
|
||||
pub fn buffer_to_path(buffer: &cosmic_text::Buffer, font_system: &mut cosmic_text::FontSystem, spans: &[TextSpan], path: &VectorData) -> Vec<Subpath<ManipulatorGroupId>> {
|
||||
let mut builder = Builder {
|
||||
current_subpath: Subpath::new(Vec::new(), false),
|
||||
other_subpaths: Vec::new(),
|
||||
pos: DVec2::ZERO,
|
||||
offset: DVec2::ZERO,
|
||||
ascender: (buzz_face.ascender() as f64 / buzz_face.height() as f64) * font_size / scale,
|
||||
scale,
|
||||
transform: DAffine2::IDENTITY,
|
||||
ascender: 0.,
|
||||
scale: 1.,
|
||||
font_size: 1.,
|
||||
bold: None,
|
||||
italic: None,
|
||||
id: ManipulatorGroupId::ZERO,
|
||||
};
|
||||
let subpath = path
|
||||
.stroke_bezier_paths()
|
||||
.map(|mut subpath| {
|
||||
subpath.apply_transform(path.transform);
|
||||
|
||||
for line in str.split('\n') {
|
||||
let length = line.split(' ').count();
|
||||
for (index, word) in line.split(' ').enumerate() {
|
||||
push_str(&mut buffer, word, index != length - 1);
|
||||
let glyph_buffer = rustybuzz::shape(&buzz_face, &[], buffer);
|
||||
(subpath.iter().map(|bezier| bezier.length(None)).collect::<Vec<f64>>(), subpath)
|
||||
})
|
||||
.find(|(_, subpath)| !subpath.is_empty());
|
||||
|
||||
if wrap_word(line_width, &glyph_buffer, scale, builder.pos.x) {
|
||||
builder.pos = DVec2::new(0., builder.pos.y + line_height);
|
||||
}
|
||||
// Inspect the output runs
|
||||
let mut offset;
|
||||
for run in buffer.layout_runs() {
|
||||
offset = DVec2::ZERO;
|
||||
for glyph_position in run.glyphs.iter() {
|
||||
let Some(font) = font_system.get_font(glyph_position.font_id) else { continue };
|
||||
let buzz_face = font.rustybuzz();
|
||||
builder.scale = glyph_position.font_size as f64 / buzz_face.units_per_em() as f64;
|
||||
builder.font_size = glyph_position.font_size as f64;
|
||||
builder.ascender = (buzz_face.ascender() as f64 / buzz_face.height() as f64) * glyph_position.font_size as f64 / builder.scale;
|
||||
let span = &spans[glyph_position.metadata];
|
||||
builder.bold = span.bold.map(|x| x as f64);
|
||||
builder.italic = span.italic.map(|x| x as f64);
|
||||
|
||||
for (glyph_position, glyph_info) in glyph_buffer.glyph_positions().iter().zip(glyph_buffer.glyph_infos()) {
|
||||
if let Some(line_width) = line_width {
|
||||
if builder.pos.x + (glyph_position.x_advance as f64 * builder.scale) >= line_width {
|
||||
builder.pos = DVec2::new(0., builder.pos.y + line_height);
|
||||
}
|
||||
let glyph_offset = DVec2::new(glyph_position.x_offset as f64, glyph_position.y_offset as f64) + span.kerning.as_dvec2() + offset;
|
||||
if let Some((lengths, subpath)) = &subpath {
|
||||
let total_length: f64 = lengths.iter().sum();
|
||||
let eval_euclidean = |dist: f64| {
|
||||
let (segment_index, segment_t_euclidean) = subpath.global_euclidean_to_local_euclidean(dist / total_length, lengths.as_slice(), total_length);
|
||||
let segment = subpath.get_segment(segment_index).unwrap();
|
||||
let segment_t_parametric = segment.euclidean_to_parametric_with_total_length(segment_t_euclidean, 0., lengths[segment_index]);
|
||||
segment.evaluate(bezier_rs::TValue::Parametric(segment_t_parametric))
|
||||
};
|
||||
|
||||
// Text on path based on https://svgwg.org/svg2-draft/text.html#TextpathLayoutRules
|
||||
let left_x = glyph_position.x as f64 + glyph_offset.x;
|
||||
let right_x = left_x + glyph_position.w as f64;
|
||||
let centre_x = (left_x + right_x) / 2.;
|
||||
if right_x >= total_length {
|
||||
break;
|
||||
}
|
||||
builder.offset = DVec2::new(glyph_position.x_offset as f64, glyph_position.y_offset as f64) * builder.scale;
|
||||
buzz_face.outline_glyph(GlyphId(glyph_info.glyph_id as u16), &mut builder);
|
||||
if !builder.current_subpath.is_empty() {
|
||||
builder.other_subpaths.push(core::mem::replace(&mut builder.current_subpath, Subpath::new(Vec::new(), false)));
|
||||
}
|
||||
|
||||
builder.pos += DVec2::new(glyph_position.x_advance as f64, glyph_position.y_advance as f64) * builder.scale;
|
||||
let left = eval_euclidean(left_x);
|
||||
let right = eval_euclidean(right_x);
|
||||
let centre = eval_euclidean(centre_x);
|
||||
let angle = DVec2::X.angle_between(right - left);
|
||||
let angle = if angle.is_finite() { angle } else { 0. };
|
||||
builder.transform = DAffine2::from_translation(centre) * DAffine2::from_angle(angle) * DAffine2::from_translation(DVec2::X * (left_x - centre_x));
|
||||
} else {
|
||||
let pos = DVec2::new(glyph_position.x as f64, glyph_position.y as f64 + run.line_y as f64);
|
||||
builder.transform = DAffine2::from_translation(pos + glyph_offset);
|
||||
}
|
||||
buzz_face.outline_glyph(cosmic_text::rustybuzz::ttf_parser::GlyphId(glyph_position.glyph_id), &mut builder);
|
||||
|
||||
buffer = glyph_buffer.clear();
|
||||
if &run.text[glyph_position.start..glyph_position.end] == " " {
|
||||
offset.x += span.word_spacing as f64;
|
||||
}
|
||||
offset.x += span.letter_spacing as f64;
|
||||
}
|
||||
builder.pos = DVec2::new(0., builder.pos.y + line_height);
|
||||
}
|
||||
|
||||
builder.other_subpaths
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
#[must_use]
|
||||
pub fn has_hit_text_bounds(buffer: &cosmic_text::Buffer, spans: &[TextSpan], pos: Vec2) -> bool {
|
||||
let mut offset;
|
||||
let mut min = Vec2::MAX;
|
||||
let mut max = Vec2::MIN;
|
||||
let line_height = buffer.metrics().line_height;
|
||||
for run in buffer.layout_runs() {
|
||||
offset = Vec2::ZERO;
|
||||
for glyph_position in run.glyphs.iter() {
|
||||
let span = &spans[glyph_position.metadata];
|
||||
|
||||
let (scale, line_height, mut buffer) = font_properties(&buzz_face, font_size);
|
||||
let glyph_offset = Vec2::new(glyph_position.x_offset, glyph_position.y_offset) + span.kerning + offset;
|
||||
let glyph_pos = Vec2::new(glyph_position.x, glyph_position.y + run.line_y) + glyph_offset;
|
||||
|
||||
let mut pos = DVec2::ZERO;
|
||||
let mut bounds = DVec2::ZERO;
|
||||
let space = &run.text[glyph_position.start..glyph_position.end] == " ";
|
||||
let spacing = span.letter_spacing + if space { span.word_spacing } else { 0. };
|
||||
min = min.min(Vec2::new(glyph_pos.x, glyph_pos.y - line_height));
|
||||
max = max.max(Vec2::new(glyph_pos.x + spacing + glyph_position.w, glyph_pos.y));
|
||||
|
||||
for line in str.split('\n') {
|
||||
let length = line.split(' ').count();
|
||||
for (index, word) in line.split(' ').enumerate() {
|
||||
push_str(&mut buffer, word, index != length - 1);
|
||||
|
||||
let glyph_buffer = rustybuzz::shape(&buzz_face, &[], buffer);
|
||||
|
||||
if wrap_word(line_width, &glyph_buffer, scale, pos.x) {
|
||||
pos = DVec2::new(0., pos.y + line_height);
|
||||
}
|
||||
|
||||
for glyph_position in glyph_buffer.glyph_positions() {
|
||||
if let Some(line_width) = line_width {
|
||||
if pos.x + (glyph_position.x_advance as f64 * scale) >= line_width {
|
||||
pos = DVec2::new(0., pos.y + line_height);
|
||||
}
|
||||
}
|
||||
pos += DVec2::new(glyph_position.x_advance as f64, glyph_position.y_advance as f64) * scale;
|
||||
}
|
||||
bounds = bounds.max(pos + DVec2::new(0., line_height));
|
||||
|
||||
buffer = glyph_buffer.clear();
|
||||
offset.x += spacing;
|
||||
}
|
||||
}
|
||||
min.x <= pos.x && min.y <= pos.y && pos.x <= max.x && pos.y <= max.y
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn find_line_wrap_handle(buffer: &cosmic_text::Buffer, spans: &[TextSpan]) -> DVec2 {
|
||||
if buffer.size().0 != f64::MAX as f32 {
|
||||
return DVec2::new(buffer.size().0 as f64, buffer.metrics().line_height as f64);
|
||||
}
|
||||
let mut offset = 0.;
|
||||
let mut line = 0;
|
||||
let mut max = 0_f32;
|
||||
for run in buffer.layout_runs() {
|
||||
if run.line_i != line {
|
||||
offset = 0.;
|
||||
}
|
||||
line = run.line_i;
|
||||
for glyph_position in run.glyphs.iter() {
|
||||
let span = &spans[glyph_position.metadata];
|
||||
|
||||
max = max.max(glyph_position.x + glyph_position.w + offset + glyph_position.x_offset);
|
||||
|
||||
if &run.text[glyph_position.start..glyph_position.end] == " " {
|
||||
offset += span.word_spacing;
|
||||
}
|
||||
offset += span.letter_spacing;
|
||||
}
|
||||
pos = DVec2::new(0., pos.y + line_height);
|
||||
}
|
||||
|
||||
bounds
|
||||
DVec2::new(max as f64, buffer.metrics().line_height as f64)
|
||||
}
|
||||
|
||||
pub fn load_face(data: &[u8]) -> rustybuzz::Face {
|
||||
rustybuzz::Face::from_slice(data, 0).expect("Loading font failed")
|
||||
fn get_cursor_in_run(cursor: &cosmic_text::Cursor, run: &cosmic_text::LayoutRun) -> Option<(usize, f32)> {
|
||||
if cursor.line != run.line_i {
|
||||
return None;
|
||||
}
|
||||
for (glyph_i, glyph) in run.glyphs.iter().enumerate() {
|
||||
if cursor.index == glyph.start {
|
||||
return Some((glyph_i, 0.0));
|
||||
} else if cursor.index > glyph.start && cursor.index < glyph.end {
|
||||
// Guess x offset based on characters
|
||||
let mut before = 0;
|
||||
let mut total = 0;
|
||||
|
||||
let cluster = &run.text[glyph.start..glyph.end];
|
||||
for (i, _) in cluster.grapheme_indices(true) {
|
||||
if glyph.start + i < cursor.index {
|
||||
before += 1;
|
||||
}
|
||||
total += 1;
|
||||
}
|
||||
|
||||
let offset_x = glyph.w * (before as f32) / (total as f32);
|
||||
return Some((glyph_i, offset_x));
|
||||
}
|
||||
}
|
||||
match run.glyphs.last() {
|
||||
Some(glyph) => {
|
||||
if cursor.index == glyph.end {
|
||||
return Some((run.glyphs.len(), 0.0));
|
||||
}
|
||||
}
|
||||
None => {
|
||||
return Some((0, 0.0));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn selection_shape(editor: &cosmic_text::Editor, text: &RichText) -> Vec<Subpath<ManipulatorGroupId>> {
|
||||
let Some(select) = editor.select_opt() else { return Vec::new() };
|
||||
let (start, end) = match select.line.cmp(&editor.cursor().line) {
|
||||
cmp::Ordering::Greater => (editor.cursor(), select),
|
||||
cmp::Ordering::Less => (select, editor.cursor()),
|
||||
cmp::Ordering::Equal => {
|
||||
if select.index < editor.cursor().index {
|
||||
(select, editor.cursor())
|
||||
} else {
|
||||
(editor.cursor(), select)
|
||||
}
|
||||
}
|
||||
};
|
||||
let mut result = Vec::new();
|
||||
let line_height = editor.buffer().metrics().line_height as f64;
|
||||
let mut offset_spacing;
|
||||
for run in editor.buffer().layout_runs() {
|
||||
offset_spacing = DVec2::ZERO;
|
||||
|
||||
let run: cosmic_text::LayoutRun = run;
|
||||
if run.line_i >= start.line && run.line_i <= end.line {
|
||||
let mut range_opt: Option<(f32, f32)> = None;
|
||||
for glyph in run.glyphs {
|
||||
let spans = &text.spans[glyph.metadata];
|
||||
|
||||
// Guess x offset based on characters
|
||||
let cluster = &run.text[glyph.start..glyph.end];
|
||||
let total = cluster.grapheme_indices(true).count();
|
||||
let mut c_x = glyph.x;
|
||||
let c_w = glyph.w / total as f32;
|
||||
let spacing = if &run.text[glyph.start..glyph.end] == " " { spans.word_spacing as f64 } else { 0. } + spans.letter_spacing as f64;
|
||||
for (i, c) in cluster.grapheme_indices(true) {
|
||||
let c_start = glyph.start + i;
|
||||
let c_end = glyph.start + i + c.len();
|
||||
if (start.line != run.line_i || c_end > start.index) && (end.line != run.line_i || c_start < end.index) {
|
||||
range_opt = match range_opt.take() {
|
||||
Some((min, max)) => Some((min.min(c_x + offset_spacing.x as f32), max.max(c_x + c_w + offset_spacing.x as f32 + spacing as f32))),
|
||||
None => Some((c_x + offset_spacing.x as f32, (c_x + c_w + offset_spacing.x as f32 + spacing as f32))),
|
||||
};
|
||||
} else if let Some((min, max)) = range_opt.take() {
|
||||
result.push(Subpath::new_rect(
|
||||
DVec2::new(min as f64, run.line_top as f64),
|
||||
DVec2::new(max as f64, run.line_top as f64 + line_height),
|
||||
));
|
||||
}
|
||||
c_x += c_w;
|
||||
}
|
||||
offset_spacing.x += spacing;
|
||||
}
|
||||
|
||||
if let Some((mut min, mut max)) = range_opt.take() {
|
||||
if end.line > run.line_i {
|
||||
// Draw to end of line
|
||||
if run.rtl {
|
||||
min = 0.;
|
||||
} else {
|
||||
max = if editor.buffer().size().0 == f32::MAX { max } else { editor.buffer().size().0 };
|
||||
}
|
||||
}
|
||||
result.push(Subpath::new_rect(
|
||||
DVec2::new(min as f64, run.line_top as f64),
|
||||
DVec2::new(max as f64, run.line_top as f64 + line_height),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
pub fn cursor_rectangle(editor: &cosmic_text::Editor, text: &RichText) -> Option<[DVec2; 2]> {
|
||||
let line_height = editor.buffer().metrics().line_height as f64;
|
||||
for run in editor.buffer().layout_runs() {
|
||||
let Some((cursor_glyph, cursor_glyph_offset)) = get_cursor_in_run(&editor.cursor(), &run) else {
|
||||
continue;
|
||||
};
|
||||
let letter_spacing: f32 = run.glyphs.iter().take(cursor_glyph).map(|glyph| text.spans[glyph.metadata].letter_spacing).sum();
|
||||
let spaces = run.glyphs.iter().take(cursor_glyph).filter(|glyph| &run.text[glyph.start..glyph.end] == " ");
|
||||
let word_spacing: f32 = spaces.map(|glyph| text.spans[glyph.metadata].word_spacing).sum();
|
||||
let x = match run.glyphs.get(cursor_glyph) {
|
||||
Some(glyph) => {
|
||||
// Start of detected glyph
|
||||
if glyph.level.is_rtl() {
|
||||
glyph.x + glyph.w - cursor_glyph_offset
|
||||
} else {
|
||||
glyph.x + cursor_glyph_offset
|
||||
}
|
||||
}
|
||||
None => match run.glyphs.last() {
|
||||
Some(glyph) => {
|
||||
// End of last glyph
|
||||
if glyph.level.is_rtl() {
|
||||
glyph.x
|
||||
} else {
|
||||
glyph.x + glyph.w
|
||||
}
|
||||
} // Start of empty line
|
||||
None => 0.,
|
||||
},
|
||||
};
|
||||
return Some([
|
||||
DVec2::new(x as f64 + letter_spacing as f64 + word_spacing as f64, run.line_top as f64),
|
||||
DVec2::new(x as f64 + 1. + letter_spacing as f64 + word_spacing as f64, run.line_top as f64 + line_height),
|
||||
]);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn cursor_shape(editor: &cosmic_text::Editor, text: &RichText) -> Vec<Subpath<ManipulatorGroupId>> {
|
||||
if let Some(cursor) = cursor_rectangle(editor, text) {
|
||||
vec![Subpath::new_rect(cursor[0], cursor[1])]
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn compute_cursor_position(buffer: &cosmic_text::Buffer, text: &RichText, mut pos: DVec2) -> Option<cosmic_text::Cursor> {
|
||||
// Adjust for letter and word spacing
|
||||
let cosmic_text::Metrics { font_size, line_height } = buffer.metrics();
|
||||
let length = buffer.layout_runs().size_hint().0;
|
||||
for (index, run) in buffer.layout_runs().enumerate() {
|
||||
let first = index == 0;
|
||||
let last = index + 1 == length;
|
||||
let line_y = run.line_y;
|
||||
if !((pos.y as f32 >= line_y - font_size || first) && ((pos.y as f32) < line_y - font_size + line_height || last)) {
|
||||
continue;
|
||||
}
|
||||
for glyph in run.glyphs {
|
||||
let span = &text.spans[glyph.metadata];
|
||||
|
||||
let space = &run.text[glyph.start..glyph.end] == " ";
|
||||
let spacing = span.letter_spacing + if space { span.word_spacing } else { 0. };
|
||||
|
||||
if glyph.x <= pos.x as f32 && glyph.x + glyph.w + spacing >= pos.x as f32 {
|
||||
pos.x = glyph.x as f64 + ((pos.x - glyph.x as f64) / (glyph.w + spacing) as f64).clamp(0., 1.) * glyph.w as f64;
|
||||
} else if glyph.x + glyph.w + spacing <= pos.x as f32 {
|
||||
pos.x -= spacing as f64;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compute on the underlying text with the updated position
|
||||
buffer.hit(pos.x as f32, pos.y as f32)
|
||||
}
|
||||
|
||||
pub fn create_buffer(buffer: &mut cosmic_text::Buffer, font_system: &mut cosmic_text::FontSystem, text: &RichText, font_cache: &FontCache, line_length: f64) {
|
||||
// Set a size for the text buffer, in pixels
|
||||
buffer.set_size(font_system, line_length as f32, f32::MAX);
|
||||
buffer.set_metrics(font_system, create_metrics(text));
|
||||
|
||||
// Add our rich text spans
|
||||
let mut start = 0;
|
||||
let mut next_spans = text.spans.iter().skip(1);
|
||||
let spans = text.spans.iter().enumerate().filter_map(|(metadata, span)| {
|
||||
start += span.offset;
|
||||
start = start.min(text.text.len());
|
||||
let text = next_spans.next().map_or(&text.text[start..], |next| &text.text[start..(start + next.offset).min(text.text.len())]);
|
||||
|
||||
create_cosmic_attrs(font_cache, span, metadata).map(|attrs| (text, attrs))
|
||||
});
|
||||
buffer.set_rich_text(font_system, spans, cosmic_text::Shaping::Advanced);
|
||||
|
||||
// Trailing new line
|
||||
if text.text.as_bytes().last().is_some_and(|&c| c == b'\n') {
|
||||
let span_index = text.spans.len() - 1;
|
||||
if let Some(attrs) = create_cosmic_attrs(font_cache, &text.spans[span_index], span_index) {
|
||||
buffer.lines.push(cosmic_text::BufferLine::new("", cosmic_text::AttrsList::new(attrs), cosmic_text::Shaping::Advanced));
|
||||
}
|
||||
}
|
||||
|
||||
// Perform shaping as desired
|
||||
buffer.shape_until_scroll(font_system);
|
||||
}
|
||||
#[must_use]
|
||||
fn create_cosmic_attrs<'a>(font_cache: &'a FontCache, span: &TextSpan, metadata: usize) -> Option<cosmic_text::Attrs<'a>> {
|
||||
font_cache
|
||||
.font_attrs
|
||||
.get(&*span.font)
|
||||
.or_else(|| font_cache.default_font.as_ref().and_then(|font| font_cache.font_attrs.get(font)))
|
||||
.map(|attrs| attrs.as_attrs().metadata(metadata))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
fn construct_buffer(text: &RichText, font_system: &mut cosmic_text::FontSystem) -> cosmic_text::Buffer {
|
||||
let metrics = create_metrics(text);
|
||||
|
||||
// A Buffer provides shaping and layout for a UTF-8 string, create one per text widget
|
||||
cosmic_text::Buffer::new(font_system, metrics)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
fn create_metrics(text: &RichText) -> cosmic_text::Metrics {
|
||||
let max_font_size = text.spans.iter().map(|span| span.font_size).max_by(|a, b| a.partial_cmp(b).unwrap()).unwrap_or(24.);
|
||||
let max_line = text.spans.iter().map(|span| span.font_size * span.line_spacing).max_by(|a, b| a.partial_cmp(b).unwrap()).unwrap_or(24.);
|
||||
cosmic_text::Metrics::new(max_font_size, max_line)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn create_cosmic_editor(text: &RichText, font_cache: &FontCache, line_length: f64) -> Option<cosmic_text::Editor> {
|
||||
let mut font_system = font_cache.get_system()?;
|
||||
let mut buffer = construct_buffer(text, &mut font_system);
|
||||
|
||||
create_buffer(&mut buffer, &mut font_system, text, font_cache, line_length);
|
||||
Some(cosmic_text::Editor::new(buffer))
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ use crate::proto::{ConstructionArgs, ProtoNetwork, ProtoNode, ProtoNodeInput};
|
|||
|
||||
use dyn_any::{DynAny, StaticType};
|
||||
pub use graphene_core::uuid::generate_uuid;
|
||||
use graphene_core::vector::VectorData;
|
||||
use graphene_core::{GraphicGroup, ProtoNodeIdentifier, Type};
|
||||
|
||||
use glam::IVec2;
|
||||
|
@ -1160,42 +1161,40 @@ impl NodeNetwork {
|
|||
self.nodes.extend(extraction_nodes);
|
||||
}
|
||||
|
||||
/// Due to the adaptive resolution system, nodes that take a `GraphicGroup` as input must call the upstream node with the `Footprint` parameter.
|
||||
/// Due to the adaptive resolution system, nodes that take a `GraphicGroup` or `VectorData` as input must call the upstream node with the `Footprint` parameter.
|
||||
///
|
||||
/// However, in the case of the default input, we must insert a node that takes an input of `Footprint` and returns `GraphicGroup::Empty`, in order to satisfy the type system.
|
||||
/// However, in the case of the default input, we must insert a node that takes an input of `Footprint` and returns `GraphicGroup::Empty` or `VectorData::empty()`, in order to satisfy the type system.
|
||||
/// This is because the standard value node takes in `()`.
|
||||
pub fn resolve_empty_stacks(&mut self) {
|
||||
const EMPTY_STACK: &str = "Empty Stack";
|
||||
|
||||
let new_id = generate_uuid();
|
||||
let mut used = false;
|
||||
for target_tagged_value in [TaggedValue::GraphicGroup(GraphicGroup::EMPTY), TaggedValue::VectorData(VectorData::empty())] {
|
||||
let new_id = generate_uuid();
|
||||
let mut used = false;
|
||||
|
||||
// We filter out the newly inserted empty stack in case `resolve_empty_stacks` runs multiple times.
|
||||
for node in self.nodes.values_mut().filter(|node| node.name != EMPTY_STACK) {
|
||||
for input in &mut node.inputs {
|
||||
if let NodeInput::Value {
|
||||
tagged_value: TaggedValue::GraphicGroup(graphic_group),
|
||||
..
|
||||
} = input
|
||||
{
|
||||
if *graphic_group == GraphicGroup::EMPTY {
|
||||
*input = NodeInput::node(NodeId(new_id), 0);
|
||||
used = true;
|
||||
// We filter out the newly inserted empty stack in case `resolve_empty_stacks` runs multiple times.
|
||||
for node in self.nodes.values_mut().filter(|node| node.name != EMPTY_STACK) {
|
||||
for input in &mut node.inputs {
|
||||
if let NodeInput::Value { tagged_value, .. } = input {
|
||||
if *tagged_value == target_tagged_value {
|
||||
*input = NodeInput::node(NodeId(new_id), 0);
|
||||
used = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Only insert the node if necessary.
|
||||
if used {
|
||||
let new_node = DocumentNode {
|
||||
name: EMPTY_STACK.to_string(),
|
||||
implementation: DocumentNodeImplementation::proto("graphene_core::transform::CullNode<_>"),
|
||||
manual_composition: Some(concrete!(graphene_core::transform::Footprint)),
|
||||
inputs: vec![NodeInput::value(TaggedValue::GraphicGroup(graphene_core::GraphicGroup::EMPTY), false)],
|
||||
..Default::default()
|
||||
};
|
||||
self.nodes.insert(NodeId(new_id), new_node);
|
||||
// Only insert the node if necessary.
|
||||
if used {
|
||||
let new_node = DocumentNode {
|
||||
name: EMPTY_STACK.to_string(),
|
||||
implementation: DocumentNodeImplementation::proto("graphene_core::transform::CullNode<_>"),
|
||||
manual_composition: Some(concrete!(graphene_core::transform::Footprint)),
|
||||
inputs: vec![NodeInput::value(target_tagged_value, false)],
|
||||
..Default::default()
|
||||
};
|
||||
self.nodes.insert(NodeId(new_id), new_node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -73,6 +73,7 @@ pub enum TaggedValue {
|
|||
Footprint(graphene_core::transform::Footprint),
|
||||
RenderOutput(RenderOutput),
|
||||
Palette(Vec<Color>),
|
||||
RichText(graphene_core::text::RichText),
|
||||
}
|
||||
|
||||
#[allow(clippy::derived_hash_with_manual_eq)]
|
||||
|
@ -150,6 +151,7 @@ impl Hash for TaggedValue {
|
|||
Self::Footprint(x) => x.hash(state),
|
||||
Self::RenderOutput(x) => x.hash(state),
|
||||
Self::Palette(x) => x.hash(state),
|
||||
Self::RichText(x) => x.hash(state),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -214,6 +216,7 @@ impl<'a> TaggedValue {
|
|||
TaggedValue::Footprint(x) => Box::new(x),
|
||||
TaggedValue::RenderOutput(x) => Box::new(x),
|
||||
TaggedValue::Palette(x) => Box::new(x),
|
||||
TaggedValue::RichText(x) => Box::new(x),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -228,6 +231,7 @@ impl<'a> TaggedValue {
|
|||
TaggedValue::Bool(x) => x.to_string(),
|
||||
TaggedValue::BlendMode(x) => "BlendMode::".to_string() + &x.to_string(),
|
||||
TaggedValue::Color(x) => format!("Color {x:?}"),
|
||||
TaggedValue::RichText(x) => format!("\"rich{}\"", x.text),
|
||||
_ => panic!("Cannot convert to primitive string"),
|
||||
}
|
||||
}
|
||||
|
@ -290,6 +294,7 @@ impl<'a> TaggedValue {
|
|||
TaggedValue::Footprint(_) => concrete!(graphene_core::transform::Footprint),
|
||||
TaggedValue::RenderOutput(_) => concrete!(RenderOutput),
|
||||
TaggedValue::Palette(_) => concrete!(Vec<Color>),
|
||||
TaggedValue::RichText(_) => concrete!(graphene_core::text::RichText),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -357,6 +362,7 @@ impl<'a> TaggedValue {
|
|||
}
|
||||
x if x == TypeId::of::<graphene_core::transform::Footprint>() => Ok(TaggedValue::Footprint(*downcast(input).unwrap())),
|
||||
x if x == TypeId::of::<Vec<Color>>() => Ok(TaggedValue::Palette(*downcast(input).unwrap())),
|
||||
x if x == TypeId::of::<graphene_core::text::RichText>() => Ok(TaggedValue::RichText(*downcast(input).unwrap())),
|
||||
_ => Err(format!("Cannot convert {:?} to TaggedValue", DynAny::type_name(input.as_ref()))),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -767,7 +767,7 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
|
|||
input: Vec<graphene_core::vector::bezier_rs::Subpath<graphene_core::uuid::ManipulatorGroupId>>,
|
||||
params: [Vec<graphene_core::uuid::ManipulatorGroupId>]
|
||||
),
|
||||
register_node!(graphene_core::text::TextGeneratorNode<_, _, _>, input: WasmEditorApi, params: [String, graphene_core::text::Font, f64]),
|
||||
async_node!(graphene_core::text::TextGeneratorNode<_, _, _>, input: WasmEditorApi, output: VectorData, fn_params: [() => graphene_core::text::RichText, () => f64, Footprint => graphene_core::vector::VectorData]),
|
||||
register_node!(graphene_std::brush::VectorPointsNode, input: VectorData, params: []),
|
||||
register_node!(graphene_core::ExtractImageFrame, input: WasmEditorApi, params: []),
|
||||
async_node!(graphene_core::ConstructLayerNode<_, _>, input: Footprint, output: GraphicGroup, fn_params: [Footprint => graphene_core::GraphicElement, Footprint => GraphicGroup]),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue