mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-21 11:18:12 +08:00
Add String[] as a graphic type for typography (#4141)
* feat: Render List<String> as raw paths in SVG and Vell mode * chore: code review * chore: change the hardcoded layout bounds to parley's * chore: code review * feat: Split text node to text_layer and text_to_vector node * fix: CI fail because of difference in nature of Mac and github action * chore: fix * chore: replace FontStack as it got removed in parley 0.9 * chore: fmt * chore: migrate the rendering as of new resource architechture * chore: add text_layer node to text tool for testing * code review * Make boolean ops support the Text type * chore: Move fallback_font_resource authority from editor to text node * Fix 'Text Layer' node missing font dropdown * Change node doc comments from Vec<T> to T[] * Add migrations from the old Text node to Text -> Text to Vector * Consolidate * Rename the text attributes and reorder tilt to come before max_width/height * Detect legacy Text nodes in the split migration by their trailing separate_glyphs input * Code review * Frame Text layer thumbnails by laying out their text for bounds * Give Text layers click targets and selection outlines via collect_metadata * Route Text tool through Text to Vector with fill, fixing editing-preview placement --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
co-authored by
Keavon Chambers
parent
13abf9fa8c
commit
5f100946f2
@@ -336,6 +336,7 @@ impl TableItemLayout for Graphic {
|
||||
Self::RasterGPU(list) => list.identifier(),
|
||||
Self::Color(list) => list.identifier(),
|
||||
Self::Gradient(list) => list.identifier(),
|
||||
Self::Text(list) => list.identifier(),
|
||||
}
|
||||
}
|
||||
// Don't put a breadcrumb for Graphic
|
||||
@@ -350,6 +351,7 @@ impl TableItemLayout for Graphic {
|
||||
Self::RasterGPU(list) => list.layout_with_breadcrumb(data),
|
||||
Self::Color(list) => list.layout_with_breadcrumb(data),
|
||||
Self::Gradient(list) => list.layout_with_breadcrumb(data),
|
||||
Self::Text(list) => list.layout_with_breadcrumb(data),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,15 +259,17 @@ impl<'a> ModifyInputsContext<'a> {
|
||||
Some(NodeInput::value(TaggedValue::Resource(font_resource_id), false)),
|
||||
Some(NodeInput::value(TaggedValue::F64(typesetting.font_size), false)),
|
||||
Some(NodeInput::value(TaggedValue::F64(typesetting.line_height_ratio), false)),
|
||||
Some(NodeInput::value(TaggedValue::F64(typesetting.character_spacing), false)),
|
||||
Some(NodeInput::value(TaggedValue::F64(typesetting.letter_spacing), false)),
|
||||
Some(NodeInput::value(TaggedValue::F64(typesetting.letter_tilt), false)),
|
||||
Some(NodeInput::value(TaggedValue::Bool(typesetting.max_width.is_some()), false)),
|
||||
Some(NodeInput::value(TaggedValue::F64(typesetting.max_width.unwrap_or(100.)), false)),
|
||||
Some(NodeInput::value(TaggedValue::Bool(typesetting.max_height.is_some()), false)),
|
||||
Some(NodeInput::value(TaggedValue::F64(typesetting.max_height.unwrap_or(100.)), false)),
|
||||
Some(NodeInput::value(TaggedValue::F64(typesetting.tilt), false)),
|
||||
Some(NodeInput::value(TaggedValue::TextAlign(typesetting.align), false)),
|
||||
Some(NodeInput::value(TaggedValue::Bool(false), false)),
|
||||
]);
|
||||
let text_to_vector = resolve_proto_node_type(graphene_std::text::text_to_vector::IDENTIFIER)
|
||||
.expect("Text to Vector node does not exist")
|
||||
.default_node_template();
|
||||
let transform = resolve_proto_node_type(graphene_std::transform_nodes::transform::IDENTIFIER)
|
||||
.expect("Transform node does not exist")
|
||||
.default_node_template();
|
||||
@@ -275,12 +277,17 @@ impl<'a> ModifyInputsContext<'a> {
|
||||
.expect("Fill node does not exist")
|
||||
.default_node_template();
|
||||
|
||||
// Build the chain `Text -> Text to Vector -> Transform -> Fill -> layer`
|
||||
let text_id = NodeId::new();
|
||||
self.network_interface.insert_node(text_id, text, &[]);
|
||||
self.network_interface.move_node_to_chain_start(&text_id, layer, &[], self.import);
|
||||
|
||||
self.responses.add(DocumentMessage::Resource(ResourceMessage::AddFont { resource_id: font_resource_id, font }));
|
||||
|
||||
let text_to_vector_id = NodeId::new();
|
||||
self.network_interface.insert_node(text_to_vector_id, text_to_vector, &[]);
|
||||
self.network_interface.move_node_to_chain_start(&text_to_vector_id, layer, &[], self.import);
|
||||
|
||||
let transform_id = NodeId::new();
|
||||
self.network_interface.insert_node(transform_id, transform, &[]);
|
||||
self.network_interface.move_node_to_chain_start(&transform_id, layer, &[], self.import);
|
||||
|
||||
@@ -8,7 +8,7 @@ use crate::messages::portfolio::document::utility_types::network_interface::{
|
||||
DocumentNodeMetadata, DocumentNodePersistentMetadata, InputMetadata, NodeNetworkInterface, NodeNetworkMetadata, NodeNetworkPersistentMetadata, NodeTemplate, NodeTypePersistentMetadata,
|
||||
Vec2InputSettings, WidgetOverride,
|
||||
};
|
||||
use crate::messages::prelude::{FontsMessageHandler, Message, ResourceMessageHandler};
|
||||
use crate::messages::prelude::{FontsMessage, FontsMessageHandler, Message, ResourceMessageHandler, Responses};
|
||||
use crate::node_graph_executor::NodeGraphExecutor;
|
||||
use glam::DVec2;
|
||||
use graph_craft::ProtoNodeIdentifier;
|
||||
@@ -2047,6 +2047,10 @@ fn static_input_properties() -> InputProperties {
|
||||
map.insert(
|
||||
"text_font".to_string(),
|
||||
Box::new(|node_id, index, context| {
|
||||
// Lazily load the font catalog (like the Text tool) so the dropdown has entries
|
||||
if context.fonts.font_catalog.is_empty() {
|
||||
context.responses.add(FontsMessage::LoadCatalog);
|
||||
}
|
||||
let (font, style) = node_properties::font_inputs(ParameterWidgetsInfo::new(node_id, index, true, context));
|
||||
let mut result = vec![LayoutGroup::row(font)];
|
||||
if let Some(style) = style {
|
||||
|
||||
@@ -292,9 +292,21 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
||||
return;
|
||||
};
|
||||
|
||||
let node_template = document_node_type.default_node_template();
|
||||
let mut node_template = document_node_type.default_node_template();
|
||||
self.context_menu = None;
|
||||
|
||||
// A freshly added Text node carries no font, so give it the default font (registered like the Text tool does)
|
||||
if node_type == DefinitionIdentifier::ProtoNode(graphene_std::text::text::IDENTIFIER) {
|
||||
let font_resource_id = graph_craft::application_io::resource::ResourceId::new();
|
||||
if let Some(font_input) = node_template.document_node.inputs.get_mut(graphene_std::text::text::FontInput::INDEX) {
|
||||
*font_input = NodeInput::value(TaggedValue::Resource(font_resource_id), false);
|
||||
}
|
||||
responses.add(DocumentMessage::Resource(ResourceMessage::AddFont {
|
||||
resource_id: font_resource_id,
|
||||
font: graphene_std::text::Font::default(),
|
||||
}));
|
||||
}
|
||||
|
||||
if add_transaction {
|
||||
responses.add(DocumentMessage::AddTransaction);
|
||||
}
|
||||
|
||||
@@ -856,8 +856,13 @@ pub fn font_inputs(parameter_widgets_info: ParameterWidgetsInfo) -> (Vec<WidgetI
|
||||
return (vec![], None);
|
||||
};
|
||||
|
||||
if let Some(TaggedValue::Resource(resource_id)) = input.as_non_exposed_value() {
|
||||
let font = fonts.id_font(resources, *resource_id).unwrap_or_default();
|
||||
// A freshly added node carries the empty-resource `TypeDefault` placeholder until a font is chosen
|
||||
let font = match input.as_non_exposed_value() {
|
||||
Some(TaggedValue::Resource(resource_id)) => fonts.id_font(resources, *resource_id).unwrap_or_default(),
|
||||
Some(TaggedValue::TypeDefault(_)) => Font::default(),
|
||||
_ => return (first_widgets, second_widgets),
|
||||
};
|
||||
{
|
||||
first_widgets.extend_from_slice(&[
|
||||
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||
DropdownInput::new(vec![
|
||||
|
||||
@@ -228,10 +228,10 @@ pub fn text_width(text: &str, font_size: f64) -> f64 {
|
||||
let typesetting = TypesettingConfig {
|
||||
font_size,
|
||||
line_height_ratio: 1.2,
|
||||
character_spacing: 0.,
|
||||
letter_spacing: 0.,
|
||||
letter_tilt: 0.,
|
||||
max_width: None,
|
||||
max_height: None,
|
||||
tilt: 0.,
|
||||
align: TextAlign::AlignLeft,
|
||||
};
|
||||
|
||||
|
||||
@@ -1109,10 +1109,10 @@ impl OverlayContextInternal {
|
||||
let typesetting = TypesettingConfig {
|
||||
font_size: FONT_SIZE,
|
||||
line_height_ratio: 1.2,
|
||||
character_spacing: 0.,
|
||||
letter_spacing: 0.,
|
||||
letter_tilt: 0.,
|
||||
max_width: None,
|
||||
max_height: None,
|
||||
tilt: 0.,
|
||||
align: TextAlign::AlignLeft,
|
||||
};
|
||||
|
||||
|
||||
@@ -644,7 +644,7 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[
|
||||
// text
|
||||
// ================================
|
||||
NodeReplacement {
|
||||
node: graphene_std::text::text::IDENTIFIER,
|
||||
node: ProtoNodeIdentifier::new("graphene_std::text::TextNode"),
|
||||
aliases: &["graphene_core::text::text::TextNode", "graphene_core::text::TextGeneratorNode", "graphene_core::text::TextNode"],
|
||||
},
|
||||
NodeReplacement {
|
||||
@@ -978,6 +978,16 @@ pub fn document_migration_string_preprocessing(document_serialized_content: Stri
|
||||
.fold(document_serialized_content, |document_serialized_content, (old, new)| document_serialized_content.replace(old, new))
|
||||
}
|
||||
|
||||
/// Rebuilds the old 13-input "Text" node template from the current `text` template plus the trailing `separate_glyphs` input it dropped,
|
||||
/// so the staged input-count migrations can still upgrade old text nodes before the split.
|
||||
fn legacy_text_node_template() -> Option<NodeTemplate> {
|
||||
let mut template = resolve_document_node_type(&DefinitionIdentifier::ProtoNode(graphene_std::text::text::IDENTIFIER))?.default_node_template();
|
||||
template.document_node.implementation = DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_std::text::TextNode"));
|
||||
template.document_node.inputs.push(NodeInput::value(TaggedValue::Bool(false), false));
|
||||
template.persistent_node_metadata.input_metadata.push(Default::default());
|
||||
Some(template)
|
||||
}
|
||||
|
||||
fn replace_optional_f64_null(input: &str) -> String {
|
||||
let mut result = String::new();
|
||||
let mut last_end = 0;
|
||||
@@ -1250,6 +1260,19 @@ pub fn document_migration_upgrades(document: &mut DocumentMessageHandler, reset_
|
||||
}
|
||||
}
|
||||
|
||||
// Record which old text nodes are chain-positioned now, before `migrate_node`'s staged input-count migrations run, since those set
|
||||
// the upstream chain to absolute; the split below re-chains exactly the nodes that were originally part of a layer chain.
|
||||
let text_nodes_in_chain: std::collections::HashSet<NodeId> = document
|
||||
.network_interface
|
||||
.document_network()
|
||||
.recursive_nodes()
|
||||
.filter_map(|(node_id, _, path)| {
|
||||
(document.network_interface.reference(node_id, &path) == Some(DefinitionIdentifier::ProtoNode(ProtoNodeIdentifier::new("graphene_std::text::TextNode")))
|
||||
&& document.network_interface.is_chain(node_id, &path))
|
||||
.then_some(*node_id)
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Apply upgrades to each unmodified node.
|
||||
let nodes = document
|
||||
.network_interface
|
||||
@@ -1260,6 +1283,91 @@ pub fn document_migration_upgrades(document: &mut DocumentMessageHandler, reset_
|
||||
for (node_id, node, network_path) in &nodes {
|
||||
migrate_node(node_id, node, network_path, document, reset_node_definitions_on_open);
|
||||
}
|
||||
|
||||
// The old geometry-producing "Text" node was split into the current "Text" (`String[]`) -> "Text to Vector" pair, which reuses the same
|
||||
// proto identifier. Runs after `migrate_node` normalizes old text nodes to the legacy 13-input layout, distinguished from the current
|
||||
// 12-input node by the trailing `separate_glyphs` input (index 12): forward inputs 0..=11 onto the new node and move it onto `text_to_vector`.
|
||||
let old_text_nodes: Vec<(NodeId, Vec<NodeId>)> = document
|
||||
.network_interface
|
||||
.document_network()
|
||||
.recursive_nodes()
|
||||
.filter_map(|(node_id, node, path)| {
|
||||
// `separate_glyphs` is a `Bool` value or a wire feeding one; only a different value type there means a newer input, not the old node
|
||||
let has_legacy_separate_glyphs = node.inputs.len() == 13 && node.inputs.get(12).is_some_and(|input| matches!(input.as_value(), None | Some(TaggedValue::Bool(_))));
|
||||
(has_legacy_separate_glyphs && document.network_interface.reference(node_id, &path) == Some(DefinitionIdentifier::ProtoNode(ProtoNodeIdentifier::new("graphene_std::text::TextNode"))))
|
||||
.then_some((*node_id, path))
|
||||
})
|
||||
.collect();
|
||||
for (node_id, network_path) in &old_text_nodes {
|
||||
// Pre-load `outward_wires` so the splice below resolves the original downstream wiring from cache rather than a mutated state.
|
||||
let _ = document.network_interface.outward_wires(network_path);
|
||||
|
||||
// Convert the old node in place to the current `text` node (12 inputs), capturing its old inputs.
|
||||
let Some(text_definition) = resolve_document_node_type(&DefinitionIdentifier::ProtoNode(graphene_std::text::text::IDENTIFIER)) else {
|
||||
continue;
|
||||
};
|
||||
let mut text_template = text_definition.default_node_template();
|
||||
document.network_interface.replace_implementation(node_id, network_path, &mut text_template);
|
||||
let Some(old_inputs) = document.network_interface.replace_inputs(node_id, network_path, &mut text_template) else {
|
||||
continue;
|
||||
};
|
||||
// The current `text` node reorders the legacy inputs (Letter Tilt moved up to sit right after Letter Spacing), so map each new
|
||||
// input index to the legacy 13-input index it sources from. Legacy order:
|
||||
// [primary, text, font, size, line_height, letter_spacing, has_max_width, max_width, has_max_height, max_height, letter_tilt, align, separate_glyphs].
|
||||
const LEGACY_INPUT_FOR_NEW: [usize; 12] = [0, 1, 2, 3, 4, 5, 10, 6, 7, 8, 9, 11];
|
||||
for (new_index, &legacy_index) in LEGACY_INPUT_FOR_NEW.iter().enumerate() {
|
||||
if let Some(input) = old_inputs.get(legacy_index) {
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, new_index), input.clone(), network_path);
|
||||
}
|
||||
}
|
||||
let separate_glyphs = old_inputs.get(12).cloned();
|
||||
|
||||
// Collect the inputs reading the old text node's output before any rewiring so the new node can be spliced onto those wires.
|
||||
let downstream_consumers: Vec<InputConnector> = document
|
||||
.network_interface
|
||||
.outward_wires(network_path)
|
||||
.and_then(|wires| wires.get(&OutputConnector::node(*node_id, 0)))
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
|
||||
let text_was_in_chain = text_nodes_in_chain.contains(node_id);
|
||||
|
||||
// Insert the `text_to_vector` node that converts the `text` `String[]` output back into vector geometry.
|
||||
let Some(text_to_vector_definition) = resolve_document_node_type(&DefinitionIdentifier::ProtoNode(graphene_std::text::text_to_vector::IDENTIFIER)) else {
|
||||
continue;
|
||||
};
|
||||
let text_to_vector_id = NodeId::new();
|
||||
document
|
||||
.network_interface
|
||||
.insert_node(text_to_vector_id, text_to_vector_definition.default_node_template(), network_path);
|
||||
|
||||
// Splice `text_to_vector` onto the wire(s) leaving `text` (`insert_node_between` is the pure wire-splice the editor uses for
|
||||
// dropping a node on a wire), then carry the old `separate_glyphs` value onto its second input.
|
||||
if let Some((first_consumer, remaining_consumers)) = downstream_consumers.split_first() {
|
||||
document.network_interface.insert_node_between(&text_to_vector_id, first_consumer, 0, network_path);
|
||||
for consumer in remaining_consumers {
|
||||
document.network_interface.set_input(consumer, NodeInput::node(text_to_vector_id, 0), network_path);
|
||||
}
|
||||
} else {
|
||||
document
|
||||
.network_interface
|
||||
.set_input(&InputConnector::node(text_to_vector_id, 0), NodeInput::node(*node_id, 0), network_path);
|
||||
}
|
||||
if let Some(separate_glyphs) = separate_glyphs {
|
||||
document.network_interface.set_input(&InputConnector::node(text_to_vector_id, 1), separate_glyphs, network_path);
|
||||
}
|
||||
|
||||
// If `text` was in a layer chain, re-chain `text_to_vector` and its upstream so both lay out by distance from the layer (the splice
|
||||
// broke the chain, like `move_node_to_chain_start`). Otherwise `text` is absolute, so place `text_to_vector` beside it instead of
|
||||
// leaving it at the origin.
|
||||
if text_was_in_chain {
|
||||
document.network_interface.force_set_upstream_to_chain(&text_to_vector_id, network_path);
|
||||
} else if let Some(text_position) = document.network_interface.position(node_id, network_path) {
|
||||
document
|
||||
.network_interface
|
||||
.shift_absolute_node_position(&text_to_vector_id, text_position + IVec2::new(7, 0), network_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId], document: &mut DocumentMessageHandler, reset_node_definitions_on_open: bool) -> Option<()> {
|
||||
@@ -1484,8 +1592,8 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
|
||||
}
|
||||
|
||||
// Upgrade Text node to include line height and character spacing, which were previously hardcoded to 1, from https://github.com/GraphiteEditor/Graphite/pull/2016
|
||||
if reference == DefinitionIdentifier::ProtoNode(graphene_std::text::text::IDENTIFIER) && inputs_count == 8 {
|
||||
let mut template: NodeTemplate = resolve_document_node_type(&reference)?.default_node_template();
|
||||
if reference == DefinitionIdentifier::ProtoNode(ProtoNodeIdentifier::new("graphene_std::text::TextNode")) && inputs_count == 8 {
|
||||
let mut template: NodeTemplate = legacy_text_node_template()?;
|
||||
document.network_interface.replace_implementation(node_id, network_path, &mut template);
|
||||
let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut template)?;
|
||||
|
||||
@@ -1507,7 +1615,7 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
|
||||
if inputs_count == 6 {
|
||||
old_inputs[5].clone()
|
||||
} else {
|
||||
NodeInput::value(TaggedValue::F64(TypesettingConfig::default().character_spacing), false)
|
||||
NodeInput::value(TaggedValue::F64(TypesettingConfig::default().letter_spacing), false)
|
||||
},
|
||||
network_path,
|
||||
);
|
||||
@@ -1534,7 +1642,7 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
|
||||
if inputs_count >= 9 {
|
||||
old_inputs[8].clone()
|
||||
} else {
|
||||
NodeInput::value(TaggedValue::F64(TypesettingConfig::default().tilt), false)
|
||||
NodeInput::value(TaggedValue::F64(TypesettingConfig::default().letter_tilt), false)
|
||||
},
|
||||
network_path,
|
||||
);
|
||||
@@ -1561,8 +1669,8 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
|
||||
|
||||
// Insert bool parameters for `has_max_width` and `has_max_height`:
|
||||
// https://github.com/GraphiteEditor/Graphite/pull/3643
|
||||
if reference == DefinitionIdentifier::ProtoNode(graphene_std::text::text::IDENTIFIER) && inputs_count == 11 {
|
||||
let mut template: NodeTemplate = resolve_document_node_type(&reference)?.default_node_template();
|
||||
if reference == DefinitionIdentifier::ProtoNode(ProtoNodeIdentifier::new("graphene_std::text::TextNode")) && inputs_count == 11 {
|
||||
let mut template: NodeTemplate = legacy_text_node_template()?;
|
||||
document.network_interface.replace_implementation(node_id, network_path, &mut template);
|
||||
let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut template)?;
|
||||
|
||||
@@ -1714,7 +1822,7 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
|
||||
|
||||
// Convert text nodes from the old `editor-api` scope + `Font` input to a single font `Resource` input.
|
||||
// The chosen typeface is recorded as a `DataSource::Font` in the document's resource registry and loaded on open.
|
||||
if reference == DefinitionIdentifier::ProtoNode(graphene_std::text::text::IDENTIFIER) && inputs_count == 13 && matches!(node.inputs.first(), Some(NodeInput::Scope(_))) {
|
||||
if reference == DefinitionIdentifier::ProtoNode(ProtoNodeIdentifier::new("graphene_std::text::TextNode")) && inputs_count == 13 && matches!(node.inputs.first(), Some(NodeInput::Scope(_))) {
|
||||
document
|
||||
.network_interface
|
||||
.set_input(&InputConnector::node(*node_id, 0), NodeInput::value(TaggedValue::None, false), network_path);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use graph_craft::application_io::resource::Resource;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
const FALLBACK_FONT_BYTES: &[u8] = include_bytes!("source-sans-pro-regular.ttf");
|
||||
pub static FALLBACK_FONT_RESOURCE: LazyLock<Resource> = LazyLock::new(|| Resource::new(FALLBACK_FONT_BYTES));
|
||||
// Re-export the fallback font resource from text-nodes, which is the authoritative location.
|
||||
// This avoids duplicating the font bytes in the editor binary.
|
||||
// This file can be removed after deciding where to place the authority of fallback_resource.
|
||||
pub use graphene_std::text_nodes::FALLBACK_FONT_RESOURCE;
|
||||
|
||||
Binary file not shown.
@@ -468,57 +468,54 @@ pub fn get_text<'a>(
|
||||
network_interface: &'a NodeNetworkInterface,
|
||||
fonts: &FontsMessageHandler,
|
||||
resources: &ResourceMessageHandler,
|
||||
) -> Option<(&'a String, Font, TypesettingConfig, bool)> {
|
||||
) -> Option<(&'a String, Font, TypesettingConfig)> {
|
||||
let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs(&DefinitionIdentifier::ProtoNode(graphene_std::text::text::IDENTIFIER))?;
|
||||
|
||||
let Some(TaggedValue::String(text)) = &inputs[graphene_std::text::text::TextInput::INDEX].as_value() else {
|
||||
let Some(TaggedValue::String(text)) = inputs.get(graphene_std::text::text::TextInput::INDEX)?.as_value() else {
|
||||
return None;
|
||||
};
|
||||
let font = match &inputs[graphene_std::text::text::FontInput::INDEX].as_value() {
|
||||
let font = match inputs.get(graphene_std::text::text::FontInput::INDEX)?.as_value() {
|
||||
Some(TaggedValue::Resource(resource_id)) => fonts.id_font(resources, *resource_id).unwrap_or_default(),
|
||||
_ => Font::default(),
|
||||
};
|
||||
let Some(&TaggedValue::F64(font_size)) = inputs[graphene_std::text::text::SizeInput::INDEX].as_value() else {
|
||||
let Some(&TaggedValue::F64(font_size)) = inputs.get(graphene_std::text::text::SizeInput::INDEX)?.as_value() else {
|
||||
return None;
|
||||
};
|
||||
let Some(&TaggedValue::F64(line_height_ratio)) = inputs[graphene_std::text::text::LineHeightInput::INDEX].as_value() else {
|
||||
let Some(&TaggedValue::F64(line_height_ratio)) = inputs.get(graphene_std::text::text::LineHeightInput::INDEX)?.as_value() else {
|
||||
return None;
|
||||
};
|
||||
let Some(&TaggedValue::F64(character_spacing)) = inputs[graphene_std::text::text::CharacterSpacingInput::INDEX].as_value() else {
|
||||
let Some(&TaggedValue::F64(letter_spacing)) = inputs.get(graphene_std::text::text::LetterSpacingInput::INDEX)?.as_value() else {
|
||||
return None;
|
||||
};
|
||||
let Some(&TaggedValue::Bool(has_max_width)) = inputs[graphene_std::text::text::HasMaxWidthInput::INDEX].as_value() else {
|
||||
let Some(&TaggedValue::Bool(has_max_width)) = inputs.get(graphene_std::text::text::HasMaxWidthInput::INDEX)?.as_value() else {
|
||||
return None;
|
||||
};
|
||||
let Some(&TaggedValue::F64(max_width)) = inputs[graphene_std::text::text::MaxWidthInput::INDEX].as_value() else {
|
||||
let Some(&TaggedValue::F64(max_width)) = inputs.get(graphene_std::text::text::MaxWidthInput::INDEX)?.as_value() else {
|
||||
return None;
|
||||
};
|
||||
let Some(&TaggedValue::Bool(has_max_height)) = inputs[graphene_std::text::text::HasMaxHeightInput::INDEX].as_value() else {
|
||||
let Some(&TaggedValue::Bool(has_max_height)) = inputs.get(graphene_std::text::text::HasMaxHeightInput::INDEX)?.as_value() else {
|
||||
return None;
|
||||
};
|
||||
let Some(&TaggedValue::F64(max_height)) = inputs[graphene_std::text::text::MaxHeightInput::INDEX].as_value() else {
|
||||
let Some(&TaggedValue::F64(max_height)) = inputs.get(graphene_std::text::text::MaxHeightInput::INDEX)?.as_value() else {
|
||||
return None;
|
||||
};
|
||||
let Some(&TaggedValue::F64(tilt)) = inputs[graphene_std::text::text::TiltInput::INDEX].as_value() else {
|
||||
let Some(&TaggedValue::F64(letter_tilt)) = inputs.get(graphene_std::text::text::LetterTiltInput::INDEX)?.as_value() else {
|
||||
return None;
|
||||
};
|
||||
let Some(&TaggedValue::TextAlign(align)) = inputs[graphene_std::text::text::AlignInput::INDEX].as_value() else {
|
||||
return None;
|
||||
};
|
||||
let Some(&TaggedValue::Bool(per_glyph_items)) = inputs[graphene_std::text::text::SeparateGlyphsInput::INDEX].as_value() else {
|
||||
let Some(&TaggedValue::TextAlign(align)) = inputs.get(graphene_std::text::text::AlignInput::INDEX)?.as_value() else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let typesetting = TypesettingConfig {
|
||||
font_size,
|
||||
line_height_ratio,
|
||||
letter_spacing,
|
||||
letter_tilt,
|
||||
max_width: has_max_width.then_some(max_width),
|
||||
max_height: has_max_height.then_some(max_height),
|
||||
character_spacing,
|
||||
tilt,
|
||||
align,
|
||||
};
|
||||
Some((text, font, typesetting, per_glyph_items))
|
||||
Some((text, font, typesetting))
|
||||
}
|
||||
|
||||
pub fn get_stroke_width(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<f64> {
|
||||
|
||||
@@ -69,7 +69,7 @@ pub fn text_bounding_box(layer: LayerNodeIdentifier, document: &DocumentMessageH
|
||||
}
|
||||
|
||||
// Fallback: recompute from text content (e.g. layer hasn't rendered yet)
|
||||
let Some((text, font, typesetting, _)) = get_text(layer, &document.network_interface, fonts, &document.resources) else {
|
||||
let Some((text, font, typesetting)) = get_text(layer, &document.network_interface, fonts, &document.resources) else {
|
||||
return Quad::from_box([DVec2::ZERO, DVec2::ZERO]);
|
||||
};
|
||||
let font = fonts.get_resource_or_queue_load(&font, responses);
|
||||
|
||||
@@ -35,11 +35,11 @@ pub struct TextTool {
|
||||
}
|
||||
|
||||
pub struct TextOptions {
|
||||
font_size: f64,
|
||||
character_spacing: f64,
|
||||
font: Font,
|
||||
font_size: f64,
|
||||
letter_spacing: f64,
|
||||
letter_tilt: f64,
|
||||
fill: ToolColorOptions,
|
||||
tilt: f64,
|
||||
align: TextAlign,
|
||||
/// Set of layers we last synced from, used to detect real selection changes vs. internal node toggles.
|
||||
last_synced_selection: Vec<LayerNodeIdentifier>,
|
||||
@@ -48,11 +48,11 @@ pub struct TextOptions {
|
||||
impl Default for TextOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
font_size: 24.,
|
||||
character_spacing: 0.,
|
||||
font: Font::new(graphene_std::consts::DEFAULT_FONT_FAMILY.into(), graphene_std::consts::DEFAULT_FONT_STYLE.into()),
|
||||
font_size: 24.,
|
||||
letter_spacing: 0.,
|
||||
letter_tilt: 0.,
|
||||
fill: ToolColorOptions::new_enabled(),
|
||||
tilt: 0.,
|
||||
align: TextAlign::default(),
|
||||
last_synced_selection: Vec::new(),
|
||||
}
|
||||
@@ -298,7 +298,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Text
|
||||
ToolMessage::Text(TextToolMessage::UpdateOptions { options }) => options,
|
||||
ToolMessage::Text(TextToolMessage::SelectionChanged) => {
|
||||
if let Some(layer) = can_edit_selected(context.document)
|
||||
&& let Some((_, font, typesetting, _)) = graph_modification_utils::get_text(layer, &context.document.network_interface, context.fonts, &context.document.resources)
|
||||
&& let Some((_, font, typesetting)) = graph_modification_utils::get_text(layer, &context.document.network_interface, context.fonts, &context.document.resources)
|
||||
{
|
||||
self.options.align = typesetting.align;
|
||||
self.options.font_size = typesetting.font_size;
|
||||
@@ -516,7 +516,7 @@ impl TextToolData {
|
||||
fn load_layer_text_node(&mut self, document: &DocumentMessageHandler, fonts: &FontsMessageHandler) -> Option<()> {
|
||||
let transform = document.metadata().transform_to_viewport(self.layer);
|
||||
let color = graph_modification_utils::get_fill_color(self.layer, &document.network_interface).unwrap_or(Color::BLACK);
|
||||
let (text, font, typesetting, _) = graph_modification_utils::get_text(self.layer, &document.network_interface, fonts, &document.resources)?;
|
||||
let (text, font, typesetting) = graph_modification_utils::get_text(self.layer, &document.network_interface, fonts, &document.resources)?;
|
||||
self.editing_text = Some(EditingText {
|
||||
text: text.clone(),
|
||||
font,
|
||||
@@ -573,7 +573,7 @@ impl TextToolData {
|
||||
});
|
||||
responses.add(GraphOperationMessage::FillSet {
|
||||
layer: self.layer,
|
||||
fill: if let Some(color) = editing_text.color { Fill::Solid(color) } else { Fill::None },
|
||||
fill: editing_text.color.map_or(Fill::None, Fill::Solid),
|
||||
});
|
||||
let transform = editing_text.transform;
|
||||
self.editing_text = Some(editing_text);
|
||||
@@ -654,14 +654,21 @@ impl Fsm for TextToolFsmState {
|
||||
let ToolMessage::Text(event) = event else { return self };
|
||||
match (self, event) {
|
||||
(TextToolFsmState::Editing, TextToolMessage::Overlays { context: mut overlay_context }) => {
|
||||
let transform = document.metadata().transform_to_viewport(tool_data.layer).to_cols_array();
|
||||
// While editing, the text is blanked, so the layer's rendered transform metadata is absent; read the Transform node so the overlay tracks placement
|
||||
let transform = document
|
||||
.metadata()
|
||||
.transform_to_viewport_with_first_transform_node_if_group(tool_data.layer, &document.network_interface)
|
||||
.to_cols_array();
|
||||
responses.add(FrontendMessage::DisplayEditableTextboxTransform { transform });
|
||||
if let Some(editing_text) = tool_data.editing_text.as_mut() {
|
||||
let font_resource = fonts.get_resource_or_queue_load(&editing_text.font, responses);
|
||||
let far = graphene_std::text::bounding_box(&tool_data.new_text, &font_resource, editing_text.typesetting, false);
|
||||
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;
|
||||
let transformed_quad = document
|
||||
.metadata()
|
||||
.transform_to_viewport_with_first_transform_node_if_group(tool_data.layer, &document.network_interface)
|
||||
* quad;
|
||||
overlay_context.quad(transformed_quad, None, Some(fill_color));
|
||||
}
|
||||
}
|
||||
@@ -701,7 +708,7 @@ impl Fsm for TextToolFsmState {
|
||||
bounding_box_manager.render_quad(&mut overlay_context);
|
||||
// Draw red overlay if text is clipped
|
||||
let transformed_quad = layer_transform * bounds;
|
||||
if let Some((text, font, typesetting, _)) = graph_modification_utils::get_text(layer.unwrap(), &document.network_interface, fonts, &document.resources) {
|
||||
if let Some((text, font, typesetting)) = graph_modification_utils::get_text(layer.unwrap(), &document.network_interface, fonts, &document.resources) {
|
||||
let font_resource = fonts.get_resource_or_queue_load(&font, responses);
|
||||
if lines_clipping(text.as_str(), &font_resource, typesetting) {
|
||||
overlay_context.line(transformed_quad.0[2], transformed_quad.0[3], Some(COLOR_OVERLAY_RED), Some(3.));
|
||||
@@ -964,17 +971,23 @@ impl Fsm for TextToolFsmState {
|
||||
return TextToolFsmState::Editing;
|
||||
}
|
||||
|
||||
// Otherwise create some new text
|
||||
let constraint_size = has_dragged.then_some((start - end).abs());
|
||||
// Otherwise create some new text. The drag bounds are in viewport space; map them into document space for the text's
|
||||
// transform and wrapping size, then compose with document-to-viewport so the editing overlay (a screen-space CSS matrix) carries the zoom.
|
||||
let document_to_viewport = document.metadata().document_to_viewport;
|
||||
let viewport_to_document = document_to_viewport.inverse();
|
||||
let document_start = viewport_to_document.transform_point2(start);
|
||||
let document_end = viewport_to_document.transform_point2(end);
|
||||
|
||||
let constraint_size = has_dragged.then_some((document_start - document_end).abs());
|
||||
let editing_text = EditingText {
|
||||
text: String::new(),
|
||||
transform: DAffine2::from_translation(start),
|
||||
transform: document_to_viewport * DAffine2::from_translation(document_start),
|
||||
typesetting: TypesettingConfig {
|
||||
font_size: tool_options.font_size,
|
||||
letter_spacing: tool_options.letter_spacing,
|
||||
letter_tilt: tool_options.letter_tilt,
|
||||
max_width: constraint_size.map(|size| size.x),
|
||||
character_spacing: tool_options.character_spacing,
|
||||
max_height: constraint_size.map(|size| size.y),
|
||||
tilt: tool_options.tilt,
|
||||
align: tool_options.align,
|
||||
..TypesettingConfig::default()
|
||||
},
|
||||
|
||||
@@ -9,7 +9,7 @@ use graph_craft::document::{NodeId, NodeNetwork};
|
||||
use graph_craft::graphene_compiler::Compiler;
|
||||
use graph_craft::proto::GraphErrors;
|
||||
use graphene_std::application_io::{ApplicationIo, ExportFormat, ImageTexture, NodeGraphUpdateMessage, NodeGraphUpdateSender, RenderConfig};
|
||||
use graphene_std::bounds::{BoundingBox, RenderBoundingBox};
|
||||
use graphene_std::bounds::RenderBoundingBox;
|
||||
use graphene_std::list::List;
|
||||
use graphene_std::memo::IORecord;
|
||||
use graphene_std::ops::Convert;
|
||||
@@ -414,10 +414,10 @@ impl NodeRuntime {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Graphic list: thumbnail
|
||||
// Graphic list: thumbnail (text-aware bounds, since the `BoundingBox` trait can't lay out `Graphic::Text` content)
|
||||
if let Some(io) = introspected_data.downcast_ref::<IORecord<Context, List<Graphic>>>() {
|
||||
if update_thumbnails {
|
||||
let bounds = io.output.thumbnail_bounding_box(DAffine2::IDENTITY, true);
|
||||
let bounds = graphene_std::renderer::graphic_list_bounding_box(&io.output, DAffine2::IDENTITY);
|
||||
Self::render_thumbnail(&mut self.thumbnail_renders, parent_network_node_id, &io.output, bounds, responses)
|
||||
}
|
||||
}
|
||||
@@ -434,6 +434,13 @@ impl NodeRuntime {
|
||||
// Insert the vector modify
|
||||
self.vector_modify.insert(parent_network_node_id, io.output.element(0).cloned().unwrap_or_default());
|
||||
}
|
||||
// String list: thumbnail (bounds need text layout, which the `BoundingBox` trait can't do for a bare `String`)
|
||||
else if let Some(io) = introspected_data.downcast_ref::<IORecord<Context, List<String>>>() {
|
||||
if update_thumbnails {
|
||||
let bounds = graphene_std::renderer::text_list_bounding_box(&io.output, DAffine2::IDENTITY);
|
||||
Self::render_thumbnail(&mut self.thumbnail_renders, parent_network_node_id, &io.output, bounds, responses)
|
||||
}
|
||||
}
|
||||
// Other
|
||||
else {
|
||||
log::warn!("Failed to downcast monitor node output {parent_network_node_id:?}");
|
||||
|
||||
Reference in New Issue
Block a user