mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-10-01 02:08:12 +08:00
Add max width/height to text layers and draggable text boxes to the Text tool (#2118)
* Make progress in text tool * Add line_width to gcore and gstd * minor fix * Dragging sets line_width correctly * Get draw overlay to work * Typo fix * Make progress in text tool * Add line_width to gcore and gstd * minor fix * Dragging sets line_width correctly * Get draw overlay to work * Typo fix * Improve text bounding box * Add toggle for editing line width * Take absolute value of drag * Fix optional properties * Code review * Attempt to add box height and abort with keys * Attempt to add key modifiers and snap manager * Use resize for improved dragging * Refactor typesetting configuration into a struct * Fix missing px unit in frontend * Remove lines on rendered text * Fix backwards compatibility * Refactor lenient slection as an associate function in tool data * Add dashed quad to text nodes * Use correct names for max height and width * Additional renames and reorder * ReResolve conflict * Code review and improvements --------- Co-authored-by: hypercube <0hypercube@gmail.com> Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
co-authored by
hypercube
Keavon Chambers
parent
f225756655
commit
66357540bb
@@ -4,7 +4,7 @@ use crate::messages::prelude::*;
|
||||
use bezier_rs::Subpath;
|
||||
use graph_craft::document::{value::TaggedValue, NodeId, NodeInput};
|
||||
use graphene_core::raster::{BlendMode, ImageFrame};
|
||||
use graphene_core::text::Font;
|
||||
use graphene_core::text::{Font, TypesettingConfig};
|
||||
use graphene_core::vector::style::Gradient;
|
||||
use graphene_core::vector::PointId;
|
||||
use graphene_core::Color;
|
||||
@@ -127,7 +127,7 @@ pub fn get_text_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkIn
|
||||
}
|
||||
|
||||
/// Gets properties from the Text node
|
||||
pub fn get_text(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<(&String, &Font, f64, f64, f64)> {
|
||||
pub fn get_text(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<(&String, &Font, TypesettingConfig)> {
|
||||
let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs("Text")?;
|
||||
|
||||
let Some(TaggedValue::String(text)) = &inputs[1].as_value() else { return None };
|
||||
@@ -135,8 +135,17 @@ pub fn get_text(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInter
|
||||
let Some(&TaggedValue::F64(font_size)) = inputs[3].as_value() else { return None };
|
||||
let Some(&TaggedValue::F64(line_height_ratio)) = inputs[4].as_value() else { return None };
|
||||
let Some(&TaggedValue::F64(character_spacing)) = inputs[5].as_value() else { return None };
|
||||
let Some(&TaggedValue::OptionalF64(max_width)) = inputs[6].as_value() else { return None };
|
||||
let Some(&TaggedValue::OptionalF64(max_height)) = inputs[7].as_value() else { return None };
|
||||
|
||||
Some((text, font, font_size, line_height_ratio, character_spacing))
|
||||
let typesetting = TypesettingConfig {
|
||||
font_size,
|
||||
line_height_ratio,
|
||||
max_width,
|
||||
character_spacing,
|
||||
max_height,
|
||||
};
|
||||
Some((text, font, typesetting))
|
||||
}
|
||||
|
||||
pub fn get_stroke_width(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<f64> {
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
use crate::messages::input_mapper::utility_types::input_mouse::ViewportPosition;
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use crate::messages::prelude::*;
|
||||
use crate::messages::tool::common_functionality::snapping::SnapManager;
|
||||
use crate::messages::tool::common_functionality::snapping::{SnapCandidatePoint, SnapConstraint, SnapData, SnapManager, SnapTypeConfiguration};
|
||||
use crate::messages::{input_mapper::utility_types::input_keyboard::Key, portfolio::document::graph_operation::utility_types::TransformIn};
|
||||
use glam::{DAffine2, DVec2, Vec2Swizzles};
|
||||
|
||||
use super::snapping::{SnapCandidatePoint, SnapConstraint, SnapData, SnapTypeConfiguration};
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Resize {
|
||||
drag_start: ViewportPosition,
|
||||
/// Stored as a document position so the start doesn't move if the canvas is panned.
|
||||
drag_start: DVec2,
|
||||
pub layer: Option<LayerNodeIdentifier>,
|
||||
pub snap_manager: SnapManager,
|
||||
}
|
||||
@@ -29,6 +27,8 @@ impl Resize {
|
||||
root_transform.transform_point2(self.drag_start)
|
||||
}
|
||||
|
||||
/// Compute the drag start and end based on the current mouse position. If the layer doesn't exist, returns [`None`].
|
||||
/// If you want to draw even without a layer, use [`Resize::calculate_points_ignore_layer`].
|
||||
pub fn calculate_points(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, center: Key, lock_ratio: Key) -> Option<[DVec2; 2]> {
|
||||
let layer = self.layer?;
|
||||
|
||||
@@ -41,7 +41,12 @@ impl Resize {
|
||||
self.layer.take();
|
||||
return None;
|
||||
}
|
||||
Some(self.calculate_points_ignore_layer(document, input, center, lock_ratio))
|
||||
}
|
||||
|
||||
/// Compute the drag start and end based on the current mouse position. Ignores the state of the layer.
|
||||
/// If you want to only draw whilst a layer exists, use [`Resize::calculate_points`].
|
||||
pub fn calculate_points_ignore_layer(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, center: Key, lock_ratio: Key) -> [DVec2; 2] {
|
||||
let start = self.viewport_drag_start(document);
|
||||
let mouse = input.mouse.position;
|
||||
let document_to_viewport = document.navigation_handler.calculate_offset_transform(input.viewport_bounds.center(), &document.document_ptz);
|
||||
@@ -88,7 +93,7 @@ impl Resize {
|
||||
self.snap_manager.update_indicator(snapped);
|
||||
}
|
||||
|
||||
Some(points_viewport)
|
||||
points_viewport
|
||||
}
|
||||
|
||||
pub fn calculate_transform(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, center: Key, lock_ratio: Key, skip_rerender: bool) -> Option<Message> {
|
||||
|
||||
@@ -112,7 +112,7 @@ struct ArtboardToolData {
|
||||
drag_current: DVec2,
|
||||
auto_panning: AutoPanning,
|
||||
snap_candidates: Vec<SnapCandidatePoint>,
|
||||
dragging_current_artboad_location: IVec2,
|
||||
dragging_current_artboard_location: IVec2,
|
||||
}
|
||||
|
||||
impl ArtboardToolData {
|
||||
@@ -140,7 +140,7 @@ impl ArtboardToolData {
|
||||
fn start_resizing(&mut self, _selected_edges: (bool, bool, bool, bool), _document: &DocumentMessageHandler, _input: &InputPreprocessorMessageHandler) {
|
||||
if let Some(bounds) = &mut self.bounding_box_manager {
|
||||
bounds.center_of_transformation = bounds.transform.transform_point2((bounds.bounds[0] + bounds.bounds[1]) / 2.);
|
||||
self.dragging_current_artboad_location = bounds.bounds[0].round().as_ivec2();
|
||||
self.dragging_current_artboard_location = bounds.bounds[0].round().as_ivec2();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,9 +200,9 @@ impl ArtboardToolData {
|
||||
dimensions: size.round().as_ivec2(),
|
||||
});
|
||||
|
||||
let translation = position.round().as_ivec2() - self.dragging_current_artboad_location;
|
||||
self.dragging_current_artboad_location = position.round().as_ivec2();
|
||||
for child in self.selected_artboard.unwrap().children(&document.metadata()) {
|
||||
let translation = position.round().as_ivec2() - self.dragging_current_artboard_location;
|
||||
self.dragging_current_artboard_location = position.round().as_ivec2();
|
||||
for child in self.selected_artboard.unwrap().children(document.metadata()) {
|
||||
let local_translation = document.metadata().downstream_transform_to_document(child).inverse().transform_vector2(-translation.as_dvec2());
|
||||
responses.add(GraphOperationMessage::TransformChange {
|
||||
layer: child,
|
||||
@@ -221,12 +221,9 @@ impl Fsm for ArtboardToolFsmState {
|
||||
fn transition(self, event: ToolMessage, tool_data: &mut Self::ToolData, tool_action_data: &mut ToolActionHandlerData, _tool_options: &(), responses: &mut VecDeque<Message>) -> Self {
|
||||
let ToolActionHandlerData { document, input, .. } = tool_action_data;
|
||||
|
||||
let ToolMessage::Artboard(event) = event else {
|
||||
return self;
|
||||
};
|
||||
|
||||
let hovered = ArtboardToolData::hovered_artboard(document, input).is_some();
|
||||
|
||||
let ToolMessage::Artboard(event) = event else { return self };
|
||||
match (self, event) {
|
||||
(state, ArtboardToolMessage::Overlays(mut overlay_context)) => {
|
||||
if state != ArtboardToolFsmState::Drawing {
|
||||
|
||||
@@ -326,9 +326,7 @@ impl Fsm for BrushToolFsmState {
|
||||
responses.add(BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::NoDisplayLegacyWarning));
|
||||
}
|
||||
|
||||
let ToolMessage::Brush(event) = event else {
|
||||
return self;
|
||||
};
|
||||
let ToolMessage::Brush(event) = event else { return self };
|
||||
match (self, event) {
|
||||
(BrushToolFsmState::Ready, BrushToolMessage::DragStart) => {
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
|
||||
@@ -188,9 +188,7 @@ impl Fsm for EllipseToolFsmState {
|
||||
|
||||
let shape_data = &mut tool_data.data;
|
||||
|
||||
let ToolMessage::Ellipse(event) = event else {
|
||||
return self;
|
||||
};
|
||||
let ToolMessage::Ellipse(event) = event else { return self };
|
||||
match (self, event) {
|
||||
(_, EllipseToolMessage::Overlays(mut overlay_context)) => {
|
||||
shape_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context);
|
||||
|
||||
@@ -82,9 +82,7 @@ impl Fsm for EyedropperToolFsmState {
|
||||
fn transition(self, event: ToolMessage, _tool_data: &mut Self::ToolData, tool_action_data: &mut ToolActionHandlerData, _tool_options: &(), responses: &mut VecDeque<Message>) -> Self {
|
||||
let ToolActionHandlerData { global_tool_data, input, .. } = tool_action_data;
|
||||
|
||||
let ToolMessage::Eyedropper(event) = event else {
|
||||
return self;
|
||||
};
|
||||
let ToolMessage::Eyedropper(event) = event else { return self };
|
||||
match (self, event) {
|
||||
// Ready -> Sampling
|
||||
(EyedropperToolFsmState::Ready, mouse_down) if matches!(mouse_down, EyedropperToolMessage::SamplePrimaryColorBegin | EyedropperToolMessage::SampleSecondaryColorBegin) => {
|
||||
|
||||
@@ -81,10 +81,7 @@ impl Fsm for FillToolFsmState {
|
||||
document, global_tool_data, input, ..
|
||||
} = handler_data;
|
||||
|
||||
let ToolMessage::Fill(event) = event else {
|
||||
return self;
|
||||
};
|
||||
|
||||
let ToolMessage::Fill(event) = event else { return self };
|
||||
match (self, event) {
|
||||
(FillToolFsmState::Ready, color_event) => {
|
||||
let Some(layer_identifier) = document.click(input) else {
|
||||
|
||||
@@ -194,9 +194,7 @@ impl Fsm for FreehandToolFsmState {
|
||||
..
|
||||
} = tool_action_data;
|
||||
|
||||
let ToolMessage::Freehand(event) = event else {
|
||||
return self;
|
||||
};
|
||||
let ToolMessage::Freehand(event) = event else { return self };
|
||||
match (self, event) {
|
||||
(_, FreehandToolMessage::Overlays(mut overlay_context)) => {
|
||||
path_endpoint_overlays(document, shape_editor, &mut overlay_context);
|
||||
|
||||
@@ -241,10 +241,7 @@ impl Fsm for GradientToolFsmState {
|
||||
document, global_tool_data, input, ..
|
||||
} = tool_action_data;
|
||||
|
||||
let ToolMessage::Gradient(event) = event else {
|
||||
return self;
|
||||
};
|
||||
|
||||
let ToolMessage::Gradient(event) = event else { return self };
|
||||
match (self, event) {
|
||||
(_, GradientToolMessage::Overlays(mut overlay_context)) => {
|
||||
let selected = tool_data.selected_gradient.as_ref();
|
||||
|
||||
@@ -91,9 +91,7 @@ impl Fsm for ImaginateToolFsmState {
|
||||
) -> Self {
|
||||
let shape_data = &mut tool_data.data;
|
||||
|
||||
let ToolMessage::Imaginate(event) = event else {
|
||||
return self;
|
||||
};
|
||||
let ToolMessage::Imaginate(event) = event else { return self };
|
||||
match (self, event) {
|
||||
(ImaginateToolFsmState::Ready, ImaginateToolMessage::DragStart) => {
|
||||
shape_data.start(document, input);
|
||||
|
||||
@@ -163,9 +163,7 @@ impl Fsm for LineToolFsmState {
|
||||
document, global_tool_data, input, ..
|
||||
} = tool_action_data;
|
||||
|
||||
let ToolMessage::Line(event) = event else {
|
||||
return self;
|
||||
};
|
||||
let ToolMessage::Line(event) = event else { return self };
|
||||
match (self, event) {
|
||||
(_, LineToolMessage::Overlays(mut overlay_context)) => {
|
||||
tool_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context);
|
||||
|
||||
@@ -95,10 +95,7 @@ impl Fsm for NavigateToolFsmState {
|
||||
_tool_options: &Self::ToolOptions,
|
||||
responses: &mut VecDeque<Message>,
|
||||
) -> Self {
|
||||
let ToolMessage::Navigate(navigate) = message else {
|
||||
return self;
|
||||
};
|
||||
|
||||
let ToolMessage::Navigate(navigate) = message else { return self };
|
||||
match navigate {
|
||||
NavigateToolMessage::PointerUp { zoom_in } => {
|
||||
if self == NavigateToolFsmState::ZoomOrClickZooming {
|
||||
|
||||
@@ -588,10 +588,7 @@ impl Fsm for PathToolFsmState {
|
||||
|
||||
fn transition(self, event: ToolMessage, tool_data: &mut Self::ToolData, tool_action_data: &mut ToolActionHandlerData, _tool_options: &(), responses: &mut VecDeque<Message>) -> Self {
|
||||
let ToolActionHandlerData { document, input, shape_editor, .. } = tool_action_data;
|
||||
let ToolMessage::Path(event) = event else {
|
||||
return self;
|
||||
};
|
||||
|
||||
let ToolMessage::Path(event) = event else { return self };
|
||||
match (self, event) {
|
||||
(_, PathToolMessage::SelectionChanged) => {
|
||||
// Set the newly targeted layers to visible
|
||||
|
||||
@@ -550,9 +550,7 @@ impl Fsm for PenToolFsmState {
|
||||
transform = DAffine2::IDENTITY;
|
||||
}
|
||||
|
||||
let ToolMessage::Pen(event) = event else {
|
||||
return self;
|
||||
};
|
||||
let ToolMessage::Pen(event) = event else { return self };
|
||||
match (self, event) {
|
||||
(_, PenToolMessage::SelectionChanged) => {
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
|
||||
@@ -234,9 +234,7 @@ impl Fsm for PolygonToolFsmState {
|
||||
|
||||
let polygon_data = &mut tool_data.data;
|
||||
|
||||
let ToolMessage::Polygon(event) = event else {
|
||||
return self;
|
||||
};
|
||||
let ToolMessage::Polygon(event) = event else { return self };
|
||||
match (self, event) {
|
||||
(_, PolygonToolMessage::Overlays(mut overlay_context)) => {
|
||||
polygon_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context);
|
||||
|
||||
@@ -102,7 +102,6 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for Rectang
|
||||
self.fsm_state.process_event(message, &mut self.tool_data, tool_data, &self.options, responses, true);
|
||||
return;
|
||||
};
|
||||
|
||||
match action {
|
||||
RectangleOptionsUpdate::FillColor(color) => {
|
||||
self.options.fill.custom_color = color;
|
||||
@@ -193,10 +192,7 @@ impl Fsm for RectangleToolFsmState {
|
||||
) -> Self {
|
||||
let shape_data = &mut tool_data.data;
|
||||
|
||||
let ToolMessage::Rectangle(event) = event else {
|
||||
return self;
|
||||
};
|
||||
|
||||
let ToolMessage::Rectangle(event) = event else { return self };
|
||||
match (self, event) {
|
||||
(_, RectangleToolMessage::Overlays(mut overlay_context)) => {
|
||||
shape_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context);
|
||||
|
||||
@@ -9,7 +9,7 @@ use crate::messages::portfolio::document::utility_types::document_metadata::Laye
|
||||
use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, FlipAxis};
|
||||
use crate::messages::portfolio::document::utility_types::network_interface::{FlowType, NodeNetworkInterface, NodeTemplate};
|
||||
use crate::messages::portfolio::document::utility_types::transformation::Selected;
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils::is_layer_fed_by_node_of_name;
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils::{get_text, is_layer_fed_by_node_of_name};
|
||||
use crate::messages::tool::common_functionality::pivot::Pivot;
|
||||
use crate::messages::tool::common_functionality::snapping::{self, SnapCandidatePoint, SnapData, SnapManager};
|
||||
use crate::messages::tool::common_functionality::transformation_cage::*;
|
||||
@@ -17,6 +17,7 @@ use crate::messages::tool::common_functionality::{auto_panning::AutoPanning, mea
|
||||
|
||||
use graph_craft::document::NodeId;
|
||||
use graphene_core::renderer::Quad;
|
||||
use graphene_core::text::load_face;
|
||||
use graphene_std::renderer::Rect;
|
||||
use graphene_std::vector::misc::BooleanOperation;
|
||||
|
||||
@@ -402,11 +403,9 @@ impl Fsm for SelectToolFsmState {
|
||||
type ToolOptions = ();
|
||||
|
||||
fn transition(self, event: ToolMessage, tool_data: &mut Self::ToolData, tool_action_data: &mut ToolActionHandlerData, _tool_options: &(), responses: &mut VecDeque<Message>) -> Self {
|
||||
let ToolActionHandlerData { document, input, .. } = tool_action_data;
|
||||
let ToolActionHandlerData { document, input, font_cache, .. } = tool_action_data;
|
||||
|
||||
let ToolMessage::Select(event) = event else {
|
||||
return self;
|
||||
};
|
||||
let ToolMessage::Select(event) = event else { return self };
|
||||
match (self, event) {
|
||||
(_, SelectToolMessage::Overlays(mut overlay_context)) => {
|
||||
tool_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context);
|
||||
@@ -424,6 +423,17 @@ impl Fsm for SelectToolFsmState {
|
||||
.filter(|layer| !document.network_interface.is_artboard(&layer.to_node(), &[]))
|
||||
{
|
||||
overlay_context.outline(document.metadata().layer_outline(layer), document.metadata().transform_to_viewport(layer));
|
||||
|
||||
if is_layer_fed_by_node_of_name(layer, &document.network_interface, "Text") {
|
||||
let (text, font, typesetting) = get_text(layer, &document.network_interface).expect("Text layer should have text when interacting with the Text tool in `interact()`");
|
||||
|
||||
let buzz_face = font_cache.get(font).map(|data| load_face(data));
|
||||
let far = graphene_core::text::bounding_box(text, buzz_face, typesetting);
|
||||
let quad = Quad::from_box([DVec2::ZERO, far]);
|
||||
let transformed_quad = document.metadata().transform_to_viewport(layer) * quad;
|
||||
|
||||
overlay_context.dashed_quad(transformed_quad, None, Some(7.), Some(5.));
|
||||
}
|
||||
}
|
||||
|
||||
// Update bounds
|
||||
|
||||
@@ -194,9 +194,7 @@ impl Fsm for SplineToolFsmState {
|
||||
document, global_tool_data, input, ..
|
||||
} = tool_action_data;
|
||||
|
||||
let ToolMessage::Spline(event) = event else {
|
||||
return self;
|
||||
};
|
||||
let ToolMessage::Spline(event) = event else { return self };
|
||||
match (self, event) {
|
||||
(_, SplineToolMessage::CanvasTransformed) => self,
|
||||
(SplineToolFsmState::Ready, SplineToolMessage::DragStart) => {
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
#![allow(clippy::too_many_arguments)]
|
||||
|
||||
use super::tool_prelude::*;
|
||||
use crate::consts::DRAG_THRESHOLD;
|
||||
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::portfolio::document::utility_types::network_interface::InputConnector;
|
||||
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 crate::messages::tool::common_functionality::snapping::SnapData;
|
||||
use crate::messages::tool::common_functionality::{auto_panning::AutoPanning, resize::Resize};
|
||||
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use graph_craft::document::{NodeId, NodeInput};
|
||||
use graphene_core::renderer::Quad;
|
||||
use graphene_core::text::{load_face, Font, FontCache};
|
||||
use graphene_core::text::{load_face, Font, FontCache, TypesettingConfig};
|
||||
use graphene_core::vector::style::Fill;
|
||||
use graphene_core::Color;
|
||||
|
||||
@@ -54,8 +57,12 @@ pub enum TextToolMessage {
|
||||
|
||||
// Tool-specific messages
|
||||
CommitText,
|
||||
DragStart,
|
||||
DragStop,
|
||||
EditSelected,
|
||||
Interact,
|
||||
PointerMove { center: Key, lock_ratio: Key },
|
||||
PointerOutsideViewport { center: Key, lock_ratio: Key },
|
||||
TextChange { new_text: String, is_right_click: bool },
|
||||
UpdateBounds { new_text: String },
|
||||
UpdateOptions(TextOptionsUpdate),
|
||||
@@ -122,7 +129,7 @@ fn create_text_widgets(tool: &TextTool) -> Vec<WidgetHolder> {
|
||||
.on_update(|number_input: &NumberInput| TextToolMessage::UpdateOptions(TextOptionsUpdate::LineHeightRatio(number_input.value.unwrap())).into())
|
||||
.widget_holder();
|
||||
let character_spacing = NumberInput::new(Some(tool.options.character_spacing))
|
||||
.label("Character Spacing")
|
||||
.label("Char. Spacing")
|
||||
.int()
|
||||
.min(0.)
|
||||
.max((1_u64 << f64::MANTISSA_DIGITS) as f64)
|
||||
@@ -193,13 +200,19 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for TextToo
|
||||
fn actions(&self) -> ActionList {
|
||||
match self.fsm_state {
|
||||
TextToolFsmState::Ready => actions!(TextToolMessageDiscriminant;
|
||||
Interact,
|
||||
DragStart,
|
||||
PointerMove,
|
||||
),
|
||||
TextToolFsmState::Editing => actions!(TextToolMessageDiscriminant;
|
||||
Interact,
|
||||
DragStart,
|
||||
Abort,
|
||||
CommitText,
|
||||
),
|
||||
TextToolFsmState::Placing | TextToolFsmState::Dragging => actions!(TextToolMessageDiscriminant;
|
||||
DragStop,
|
||||
Abort,
|
||||
PointerMove,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -218,17 +231,22 @@ impl ToolTransition for TextTool {
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
enum TextToolFsmState {
|
||||
/// The tool is ready to place or edit text.
|
||||
#[default]
|
||||
Ready,
|
||||
/// The user is typing in the interactive viewport text area.
|
||||
Editing,
|
||||
/// The user is clicking to add a new text layer, but hasn't dragged or released the left mouse button yet.
|
||||
Placing,
|
||||
/// The user is dragging to create a new text area.
|
||||
Dragging,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct EditingText {
|
||||
text: String,
|
||||
font: Font,
|
||||
font_size: f64,
|
||||
line_height_ratio: f64,
|
||||
character_spacing: f64,
|
||||
typesetting: TypesettingConfig,
|
||||
color: Option<Color>,
|
||||
transform: DAffine2,
|
||||
}
|
||||
@@ -238,6 +256,10 @@ struct TextToolData {
|
||||
layer: LayerNodeIdentifier,
|
||||
editing_text: Option<EditingText>,
|
||||
new_text: String,
|
||||
resize: Resize,
|
||||
auto_panning: AutoPanning,
|
||||
// Since the overlays must be drawn without knowledge of the inputs
|
||||
cached_resize_bounds: [DVec2; 2],
|
||||
}
|
||||
|
||||
impl TextToolData {
|
||||
@@ -259,11 +281,13 @@ impl TextToolData {
|
||||
if let Some(editing_text) = self.editing_text.as_ref().filter(|_| editable) {
|
||||
responses.add(FrontendMessage::DisplayEditableTextbox {
|
||||
text: editing_text.text.clone(),
|
||||
line_width: None,
|
||||
font_size: editing_text.font_size,
|
||||
line_height_ratio: editing_text.typesetting.line_height_ratio,
|
||||
font_size: editing_text.typesetting.font_size,
|
||||
color: editing_text.color.unwrap_or(Color::BLACK),
|
||||
url: font_cache.get_preview_url(&editing_text.font).cloned().unwrap_or_default(),
|
||||
transform: editing_text.transform.to_cols_array(),
|
||||
max_width: editing_text.typesetting.max_width,
|
||||
max_height: editing_text.typesetting.max_height,
|
||||
});
|
||||
} else {
|
||||
// Check if DisplayRemoveEditableTextbox is already in the responses queue
|
||||
@@ -279,13 +303,11 @@ impl TextToolData {
|
||||
fn load_layer_text_node(&mut self, document: &DocumentMessageHandler) -> 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, font_size, line_height_ratio, character_spacing) = graph_modification_utils::get_text(self.layer, &document.network_interface)?;
|
||||
let (text, font, typesetting) = graph_modification_utils::get_text(self.layer, &document.network_interface)?;
|
||||
self.editing_text = Some(EditingText {
|
||||
text: text.clone(),
|
||||
font: font.clone(),
|
||||
font_size,
|
||||
line_height_ratio,
|
||||
character_spacing,
|
||||
typesetting,
|
||||
color: Some(color),
|
||||
transform,
|
||||
});
|
||||
@@ -318,76 +340,59 @@ impl TextToolData {
|
||||
};
|
||||
}
|
||||
|
||||
fn interact(
|
||||
&mut self,
|
||||
state: TextToolFsmState,
|
||||
input: &InputPreprocessorMessageHandler,
|
||||
document: &DocumentMessageHandler,
|
||||
font_cache: &FontCache,
|
||||
responses: &mut VecDeque<Message>,
|
||||
) -> TextToolFsmState {
|
||||
// Check if the user has selected an existing text layer
|
||||
if let Some(clicked_text_layer_path) = document
|
||||
fn new_text(&mut self, document: &DocumentMessageHandler, editing_text: EditingText, font_cache: &FontCache, responses: &mut VecDeque<Message>) {
|
||||
// Create new text
|
||||
self.new_text = String::new();
|
||||
responses.add(DocumentMessage::AddTransaction);
|
||||
|
||||
self.layer = LayerNodeIdentifier::new_unchecked(NodeId::new());
|
||||
|
||||
responses.add(GraphOperationMessage::NewTextLayer {
|
||||
id: self.layer.to_node(),
|
||||
text: String::new(),
|
||||
font: editing_text.font.clone(),
|
||||
typesetting: editing_text.typesetting,
|
||||
parent: document.new_layer_parent(true),
|
||||
insert_index: 0,
|
||||
});
|
||||
responses.add(Message::StartBuffer);
|
||||
responses.add(GraphOperationMessage::FillSet {
|
||||
layer: self.layer,
|
||||
fill: if editing_text.color.is_some() { Fill::Solid(editing_text.color.unwrap()) } else { Fill::None },
|
||||
});
|
||||
responses.add(GraphOperationMessage::TransformSet {
|
||||
layer: self.layer,
|
||||
transform: editing_text.transform,
|
||||
transform_in: TransformIn::Viewport,
|
||||
skip_rerender: true,
|
||||
});
|
||||
self.editing_text = Some(editing_text);
|
||||
|
||||
self.set_editing(true, font_cache, responses);
|
||||
|
||||
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![self.layer.to_node()] });
|
||||
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
}
|
||||
|
||||
fn check_click(document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, font_cache: &FontCache) -> Option<LayerNodeIdentifier> {
|
||||
document
|
||||
.metadata()
|
||||
.all_layers()
|
||||
.filter(|&layer| is_layer_fed_by_node_of_name(layer, &document.network_interface, "Text"))
|
||||
.find(|&layer| {
|
||||
let (text, font, font_size, line_height_ratio, character_spacing) =
|
||||
let (text, font, typesetting) =
|
||||
graph_modification_utils::get_text(layer, &document.network_interface).expect("Text layer should have text when interacting with the Text tool in `interact()`");
|
||||
|
||||
let buzz_face = font_cache.get(font).map(|data| load_face(data));
|
||||
let far = graphene_core::text::bounding_box(text, buzz_face, font_size, line_height_ratio, character_spacing, None);
|
||||
let far = graphene_core::text::bounding_box(text, buzz_face, typesetting);
|
||||
let quad = Quad::from_box([DVec2::ZERO, far]);
|
||||
let transformed_quad = document.metadata().transform_to_viewport(layer) * quad;
|
||||
|
||||
let mouse = DVec2::new(input.mouse.position.x, input.mouse.position.y);
|
||||
|
||||
transformed_quad.contains(mouse)
|
||||
}) {
|
||||
self.start_editing_layer(clicked_text_layer_path, state, document, font_cache, responses);
|
||||
|
||||
TextToolFsmState::Editing
|
||||
}
|
||||
// Create new text
|
||||
else if let Some(editing_text) = self.editing_text.as_ref().filter(|_| state == TextToolFsmState::Ready) {
|
||||
responses.add(DocumentMessage::AddTransaction);
|
||||
|
||||
self.layer = LayerNodeIdentifier::new_unchecked(NodeId::new());
|
||||
|
||||
responses.add(GraphOperationMessage::NewTextLayer {
|
||||
id: self.layer.to_node(),
|
||||
text: String::new(),
|
||||
font: editing_text.font.clone(),
|
||||
size: editing_text.font_size,
|
||||
line_height_ratio: editing_text.line_height_ratio,
|
||||
character_spacing: editing_text.character_spacing,
|
||||
parent: document.new_layer_bounding_artboard(input),
|
||||
insert_index: 0,
|
||||
});
|
||||
responses.add(Message::StartBuffer);
|
||||
responses.add(GraphOperationMessage::FillSet {
|
||||
layer: self.layer,
|
||||
fill: if editing_text.color.is_some() { Fill::Solid(editing_text.color.unwrap()) } else { Fill::None },
|
||||
});
|
||||
responses.add(GraphOperationMessage::TransformSet {
|
||||
layer: self.layer,
|
||||
transform: editing_text.transform,
|
||||
transform_in: TransformIn::Viewport,
|
||||
skip_rerender: true,
|
||||
});
|
||||
|
||||
self.set_editing(true, font_cache, responses);
|
||||
|
||||
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![self.layer.to_node()] });
|
||||
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
TextToolFsmState::Editing
|
||||
} else {
|
||||
// Removing old text as editable
|
||||
self.set_editing(false, font_cache, responses);
|
||||
|
||||
TextToolFsmState::Ready
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -420,9 +425,12 @@ impl Fsm for TextToolFsmState {
|
||||
font_cache,
|
||||
..
|
||||
} = transition_data;
|
||||
let ToolMessage::Text(event) = event else {
|
||||
return self;
|
||||
};
|
||||
let fill_color = graphene_std::Color::from_rgb_str(crate::consts::COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap())
|
||||
.unwrap()
|
||||
.with_alpha(0.05)
|
||||
.rgba_hex();
|
||||
|
||||
let ToolMessage::Text(event) = event else { return self };
|
||||
match (self, event) {
|
||||
(TextToolFsmState::Editing, TextToolMessage::Overlays(mut overlay_context)) => {
|
||||
responses.add(FrontendMessage::DisplayEditableTextboxTransform {
|
||||
@@ -430,51 +438,47 @@ impl Fsm for TextToolFsmState {
|
||||
});
|
||||
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,
|
||||
editing_text.line_height_ratio,
|
||||
editing_text.character_spacing,
|
||||
None,
|
||||
);
|
||||
let far = graphene_core::text::bounding_box(&tool_data.new_text, buzz_face, editing_text.typesetting);
|
||||
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, None);
|
||||
overlay_context.quad(transformed_quad, Some(&("#".to_string() + &fill_color)));
|
||||
}
|
||||
}
|
||||
|
||||
TextToolFsmState::Editing
|
||||
}
|
||||
(_, TextToolMessage::Overlays(mut overlay_context)) => {
|
||||
for layer in document.network_interface.selected_nodes(&[]).unwrap().selected_layers(document.metadata()) {
|
||||
let Some((text, font, font_size, line_height_ratio, character_spacing)) = graph_modification_utils::get_text(layer, &document.network_interface) 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, line_height_ratio, character_spacing, None);
|
||||
let quad = Quad::from_box([DVec2::ZERO, far]);
|
||||
let multiplied = document.metadata().transform_to_viewport(layer) * quad;
|
||||
overlay_context.quad(multiplied, None);
|
||||
if matches!(self, Self::Placing | Self::Dragging) {
|
||||
// Get the updated selection box bounds
|
||||
let quad = Quad::from_box(tool_data.cached_resize_bounds);
|
||||
|
||||
// Draw a bounding box on the layers to be selected
|
||||
for layer in document.intersect_quad_no_artboards(quad, input) {
|
||||
overlay_context.quad(
|
||||
Quad::from_box(document.metadata().bounding_box_viewport(layer).unwrap_or([DVec2::ZERO; 2])),
|
||||
Some(&("#".to_string() + &fill_color)),
|
||||
);
|
||||
}
|
||||
|
||||
overlay_context.quad(quad, Some(&("#".to_string() + &fill_color)));
|
||||
} else {
|
||||
for layer in document.network_interface.selected_nodes(&[]).unwrap().selected_layers(document.metadata()) {
|
||||
let Some((text, font, typesetting)) = graph_modification_utils::get_text(layer, &document.network_interface) else {
|
||||
continue;
|
||||
};
|
||||
let buzz_face = font_cache.get(font).map(|data| load_face(data));
|
||||
|
||||
let far = graphene_core::text::bounding_box(text, buzz_face, typesetting);
|
||||
let quad = Quad::from_box([DVec2::ZERO, far]);
|
||||
let multiplied = document.metadata().transform_to_viewport(layer) * quad;
|
||||
overlay_context.quad(multiplied, None);
|
||||
}
|
||||
}
|
||||
tool_data.resize.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context);
|
||||
|
||||
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,
|
||||
line_height_ratio: tool_options.line_height_ratio,
|
||||
character_spacing: tool_options.character_spacing,
|
||||
font: Font::new(tool_options.font_name.clone(), tool_options.font_style.clone()),
|
||||
color: tool_options.fill.active_color(),
|
||||
});
|
||||
tool_data.new_text = String::new();
|
||||
|
||||
tool_data.interact(state, input, document, font_cache, responses)
|
||||
}
|
||||
(state, TextToolMessage::EditSelected) => {
|
||||
if let Some(layer) = can_edit_selected(document) {
|
||||
tool_data.start_editing_layer(layer, state, document, font_cache, responses);
|
||||
@@ -484,12 +488,87 @@ impl Fsm for TextToolFsmState {
|
||||
state
|
||||
}
|
||||
(state, TextToolMessage::Abort) => {
|
||||
input.mouse.finish_transaction(tool_data.resize.viewport_drag_start(document), responses);
|
||||
tool_data.resize.cleanup(responses);
|
||||
|
||||
if state == TextToolFsmState::Editing {
|
||||
tool_data.set_editing(false, font_cache, responses);
|
||||
}
|
||||
|
||||
TextToolFsmState::Ready
|
||||
}
|
||||
(TextToolFsmState::Ready, TextToolMessage::DragStart) => {
|
||||
tool_data.resize.start(document, input);
|
||||
tool_data.cached_resize_bounds = [tool_data.resize.viewport_drag_start(document); 2];
|
||||
|
||||
TextToolFsmState::Placing
|
||||
}
|
||||
(Self::Placing | TextToolFsmState::Dragging, TextToolMessage::PointerMove { center, lock_ratio }) => {
|
||||
tool_data.cached_resize_bounds = tool_data.resize.calculate_points_ignore_layer(document, input, center, lock_ratio);
|
||||
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
|
||||
// Auto-panning
|
||||
let messages = [
|
||||
TextToolMessage::PointerOutsideViewport { center, lock_ratio }.into(),
|
||||
TextToolMessage::PointerMove { center, lock_ratio }.into(),
|
||||
];
|
||||
tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses);
|
||||
|
||||
TextToolFsmState::Dragging
|
||||
}
|
||||
(_, TextToolMessage::PointerMove { .. }) => {
|
||||
tool_data.resize.snap_manager.preview_draw(&SnapData::new(document, input), input.mouse.position);
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
|
||||
self
|
||||
}
|
||||
(TextToolFsmState::Placing | TextToolFsmState::Dragging, TextToolMessage::PointerOutsideViewport { .. }) => {
|
||||
// Auto-panning setup
|
||||
let _ = tool_data.auto_panning.shift_viewport(input, responses);
|
||||
|
||||
TextToolFsmState::Dragging
|
||||
}
|
||||
(state, TextToolMessage::PointerOutsideViewport { center, lock_ratio }) => {
|
||||
// Auto-panning stop
|
||||
let messages = [
|
||||
TextToolMessage::PointerOutsideViewport { center, lock_ratio }.into(),
|
||||
TextToolMessage::PointerMove { center, lock_ratio }.into(),
|
||||
];
|
||||
tool_data.auto_panning.stop(&messages, responses);
|
||||
|
||||
state
|
||||
}
|
||||
(TextToolFsmState::Placing | TextToolFsmState::Dragging, TextToolMessage::DragStop) => {
|
||||
let [start, end] = tool_data.cached_resize_bounds;
|
||||
let has_dragged = (start - end).length_squared() > DRAG_THRESHOLD * DRAG_THRESHOLD;
|
||||
|
||||
// Check if the user has clicked (no dragging) on some existing text
|
||||
if !has_dragged {
|
||||
if let Some(clicked_text_layer_path) = TextToolData::check_click(document, input, font_cache) {
|
||||
tool_data.start_editing_layer(clicked_text_layer_path, self, document, font_cache, responses);
|
||||
return TextToolFsmState::Editing;
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise create some new text
|
||||
let constraint_size = has_dragged.then_some((start - end).abs());
|
||||
let editing_text = EditingText {
|
||||
text: String::new(),
|
||||
transform: DAffine2::from_translation(start),
|
||||
typesetting: TypesettingConfig {
|
||||
font_size: tool_options.font_size,
|
||||
line_height_ratio: tool_options.line_height_ratio,
|
||||
max_width: constraint_size.map(|size| size.x),
|
||||
character_spacing: tool_options.character_spacing,
|
||||
max_height: constraint_size.map(|size| size.y),
|
||||
},
|
||||
font: Font::new(tool_options.font_name.clone(), tool_options.font_style.clone()),
|
||||
color: tool_options.fill.active_color(),
|
||||
};
|
||||
tool_data.new_text(document, editing_text, font_cache, responses);
|
||||
TextToolFsmState::Editing
|
||||
}
|
||||
(TextToolFsmState::Editing, TextToolMessage::CommitText) => {
|
||||
if tool_data.new_text.is_empty() {
|
||||
return tool_data.delete_empty_layer(font_cache, responses);
|
||||
@@ -540,18 +619,31 @@ impl Fsm for TextToolFsmState {
|
||||
let hint_data = match self {
|
||||
TextToolFsmState::Ready => HintData(vec![
|
||||
HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, "Place Text")]),
|
||||
HintGroup(vec![
|
||||
HintInfo::mouse(MouseMotion::LmbDrag, "Place Text Box"),
|
||||
HintInfo::keys([Key::Shift], "Constrain Square").prepend_plus(),
|
||||
HintInfo::keys([Key::Alt], "From Center").prepend_plus(),
|
||||
]),
|
||||
HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, "Edit Text")]),
|
||||
]),
|
||||
TextToolFsmState::Editing => HintData(vec![HintGroup(vec![
|
||||
HintInfo::keys([Key::Control, Key::Enter], "").add_mac_keys([Key::Command, Key::Enter]),
|
||||
HintInfo::keys([Key::Escape], "Commit Changes").prepend_slash(),
|
||||
])]),
|
||||
TextToolFsmState::Placing | TextToolFsmState::Dragging => HintData(vec![
|
||||
HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]),
|
||||
HintGroup(vec![HintInfo::keys([Key::Shift], "Constrain Square"), HintInfo::keys([Key::Alt], "From Center")]),
|
||||
]),
|
||||
};
|
||||
|
||||
responses.add(FrontendMessage::UpdateInputHints { hint_data });
|
||||
}
|
||||
|
||||
fn update_cursor(&self, responses: &mut VecDeque<Message>) {
|
||||
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Text });
|
||||
let cursor = match self {
|
||||
TextToolFsmState::Dragging => MouseCursorIcon::Crosshair,
|
||||
_ => MouseCursorIcon::Text,
|
||||
};
|
||||
responses.add(FrontendMessage::UpdateMouseCursor { cursor });
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user