Improve Text tool resize/drag behavior (#2428)

* Have red be below quads

* Code review pt 1

* Skip rendering of pivot

* Code review pt 2

* Code review pt 3

* Cancel resize and its hints

* Remove the redundant placing message

* Dragging state for text tool fsm

* Cleanup

* Fix line tool undo and abort problems

* Code review

* 3px textbox overflow bottom

* Some more cleanup

* Fix reversed match arms that had been converted to if-else

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
mTvare 2025-03-19 12:19:49 +05:30 committed by GitHub
parent 43275b7a1e
commit b98711dbdb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 234 additions and 259 deletions

View file

@ -88,10 +88,9 @@ pub struct MouseState {
impl MouseState {
pub fn finish_transaction(&self, drag_start: DVec2, responses: &mut VecDeque<Message>) {
match drag_start.distance(self.position) <= DRAG_THRESHOLD {
true => responses.add(DocumentMessage::AbortTransaction),
false => responses.add(DocumentMessage::EndTransaction),
}
let drag_too_small = drag_start.distance(self.position) <= DRAG_THRESHOLD;
let response = if drag_too_small { DocumentMessage::AbortTransaction } else { DocumentMessage::EndTransaction };
responses.add(response);
}
}

View file

@ -174,10 +174,7 @@ impl MessageHandler<NavigationMessage, NavigationMessageData<'_>> for Navigation
responses.add(DocumentMessage::PTZUpdate);
}
NavigationMessage::CanvasPanMouseWheel { use_y_as_x } => {
let delta = match use_y_as_x {
false => -ipp.mouse.scroll_delta.as_dvec2(),
true => (-ipp.mouse.scroll_delta.y, 0.).into(),
} * VIEWPORT_SCROLL_RATE;
let delta = if use_y_as_x { (-ipp.mouse.scroll_delta.y, 0.).into() } else { -ipp.mouse.scroll_delta.as_dvec2() } * VIEWPORT_SCROLL_RATE;
responses.add(NavigationMessage::CanvasPan { delta });
responses.add(NodeGraphMessage::SetGridAlignedEdges);
}

View file

@ -36,7 +36,7 @@ fn grid_overlay_rectangular(document: &DocumentMessageHandler, overlay_context:
} else {
DVec2::new(secondary_pos, primary_end)
};
overlay_context.line(document_to_viewport.transform_point2(start), document_to_viewport.transform_point2(end), Some(&grid_color));
overlay_context.line(document_to_viewport.transform_point2(start), document_to_viewport.transform_point2(end), Some(&grid_color), None);
}
}
}
@ -105,7 +105,7 @@ fn grid_overlay_isometric(document: &DocumentMessageHandler, overlay_context: &m
let x_pos = (((min_x - origin.x) / spacing).ceil() + line_index as f64) * spacing + origin.x;
let start = DVec2::new(x_pos, min_y);
let end = DVec2::new(x_pos, max_y);
overlay_context.line(document_to_viewport.transform_point2(start), document_to_viewport.transform_point2(end), Some(&grid_color));
overlay_context.line(document_to_viewport.transform_point2(start), document_to_viewport.transform_point2(end), Some(&grid_color), None);
}
for (tan, multiply) in [(tan_a, -1.), (tan_b, 1.)] {
@ -119,7 +119,7 @@ fn grid_overlay_isometric(document: &DocumentMessageHandler, overlay_context: &m
let y_pos = (((inverse_project(&min_y) - origin.y) / spacing).ceil() + line_index as f64) * spacing + origin.y;
let start = DVec2::new(min_x, project(&DVec2::new(min_x, y_pos)));
let end = DVec2::new(max_x, project(&DVec2::new(max_x, y_pos)));
overlay_context.line(document_to_viewport.transform_point2(start), document_to_viewport.transform_point2(end), Some(&grid_color));
overlay_context.line(document_to_viewport.transform_point2(start), document_to_viewport.transform_point2(end), Some(&grid_color), None);
}
}
}
@ -166,6 +166,7 @@ fn grid_overlay_isometric_dot(document: &DocumentMessageHandler, overlay_context
document_to_viewport.transform_point2(start),
document_to_viewport.transform_point2(end),
Some(&grid_color),
None,
Some(1.),
Some((spacing_x / cos_a) * document_to_viewport.matrix2.x_axis.length() - 1.),
None,

View file

@ -59,17 +59,17 @@ fn overlay_bezier_handles(bezier: Bezier, segment_id: SegmentId, transform: DAff
match bezier.handles {
BezierHandles::Quadratic { handle } if not_under_anchor(handle, bezier.start) && not_under_anchor(handle, bezier.end) => {
overlay_context.line(handle, bezier.start, None);
overlay_context.line(handle, bezier.end, None);
overlay_context.line(handle, bezier.start, None, None);
overlay_context.line(handle, bezier.end, None, None);
overlay_context.manipulator_handle(handle, is_selected(ManipulatorPointId::PrimaryHandle(segment_id)), None);
}
BezierHandles::Cubic { handle_start, handle_end } => {
if not_under_anchor(handle_start, bezier.start) {
overlay_context.line(handle_start, bezier.start, None);
overlay_context.line(handle_start, bezier.start, None, None);
overlay_context.manipulator_handle(handle_start, is_selected(ManipulatorPointId::PrimaryHandle(segment_id)), None);
}
if not_under_anchor(handle_end, bezier.end) {
overlay_context.line(handle_end, bezier.end, None);
overlay_context.line(handle_end, bezier.end, None, None);
overlay_context.manipulator_handle(handle_end, is_selected(ManipulatorPointId::EndHandle(segment_id)), None);
}
}
@ -93,17 +93,17 @@ pub fn overlay_bezier_handle_specific_point(
BezierHandles::Quadratic { handle } => {
if not_under_anchor(handle, bezier.start) && not_under_anchor(handle, bezier.end) {
let end = if start == point_to_render { bezier.start } else { bezier.end };
overlay_context.line(handle, end, None);
overlay_context.line(handle, end, None, None);
overlay_context.manipulator_handle(handle, is_selected(ManipulatorPointId::PrimaryHandle(segment_id)), None);
}
}
BezierHandles::Cubic { handle_start, handle_end } => {
if not_under_anchor(handle_start, bezier.start) && (point_to_render == start) {
overlay_context.line(handle_start, bezier.start, None);
overlay_context.line(handle_start, bezier.start, None, None);
overlay_context.manipulator_handle(handle_start, is_selected(ManipulatorPointId::PrimaryHandle(segment_id)), None);
}
if not_under_anchor(handle_end, bezier.end) && (point_to_render == end) {
overlay_context.line(handle_end, bezier.end, None);
overlay_context.line(handle_end, bezier.end, None, None);
overlay_context.manipulator_handle(handle_end, is_selected(ManipulatorPointId::EndHandle(segment_id)), None);
}
}

View file

@ -127,11 +127,12 @@ impl OverlayContext {
self.end_dpi_aware_transform();
}
pub fn line(&mut self, start: DVec2, end: DVec2, color: Option<&str>) {
self.dashed_line(start, end, color, None, None, None)
pub fn line(&mut self, start: DVec2, end: DVec2, color: Option<&str>, thickness: Option<f64>) {
self.dashed_line(start, end, color, thickness, None, None, None)
}
pub fn dashed_line(&mut self, start: DVec2, end: DVec2, color: Option<&str>, dash_width: Option<f64>, dash_gap_width: Option<f64>, dash_offset: Option<f64>) {
#[allow(clippy::too_many_arguments)]
pub fn dashed_line(&mut self, start: DVec2, end: DVec2, color: Option<&str>, thickness: Option<f64>, dash_width: Option<f64>, dash_gap_width: Option<f64>, dash_offset: Option<f64>) {
self.start_dpi_aware_transform();
// Set the dash pattern
@ -159,8 +160,10 @@ impl OverlayContext {
self.render_context.begin_path();
self.render_context.move_to(start.x, start.y);
self.render_context.line_to(end.x, end.y);
self.render_context.set_line_width(thickness.unwrap_or(1.));
self.render_context.set_stroke_style_str(color.unwrap_or(COLOR_OVERLAY_BLUE));
self.render_context.stroke();
self.render_context.set_line_width(1.);
// Reset the dash pattern back to solid
if dash_width.is_some() {
@ -309,8 +312,8 @@ impl OverlayContext {
let end_point1 = pivot + radius * DVec2::from_angle(angle + offset_angle);
let end_point2 = pivot + radius * DVec2::from_angle(offset_angle);
self.line(pivot, end_point1, Some(color_line));
self.line(pivot, end_point2, Some(color_line));
self.line(pivot, end_point1, Some(color_line), None);
self.line(pivot, end_point2, Some(color_line), None);
self.draw_arc(pivot, arc_radius, offset_angle, (angle) % TAU + offset_angle);
}
@ -323,7 +326,7 @@ impl OverlayContext {
.to_rgba_hex_srgb();
fill_color.insert(0, '#');
let fill_color = Some(fill_color.as_str());
self.line(start + DVec2::X * radius * sign, start + DVec2::X * (radius * scale), None);
self.line(start + DVec2::X * radius * sign, start + DVec2::X * (radius * scale), None, None);
self.circle(start, radius, fill_color, None);
self.circle(start, radius * scale.abs(), fill_color, None);
self.text(

View file

@ -89,10 +89,7 @@ impl MessageHandler<PreferencesMessage, ()> for PreferencesMessageHandler {
PreferencesMessage::ModifyLayout { zoom_with_scroll } => {
self.zoom_with_scroll = zoom_with_scroll;
let variant = match zoom_with_scroll {
false => MappingVariant::Default,
true => MappingVariant::ZoomWithScroll,
};
let variant = if zoom_with_scroll { MappingVariant::ZoomWithScroll } else { MappingVariant::Default };
responses.add(KeyMappingMessage::ModifyMapping(variant));
}
PreferencesMessage::SelectionMode { selection_mode } => {

View file

@ -8,7 +8,7 @@ fn draw_dashed_line(line_start: DVec2, line_end: DVec2, transform: DAffine2, ove
let min_viewport = transform.transform_point2(line_start);
let max_viewport = transform.transform_point2(line_end);
overlay_context.dashed_line(min_viewport, max_viewport, None, Some(2.), Some(2.), Some(0.5));
overlay_context.dashed_line(min_viewport, max_viewport, None, None, Some(2.), Some(2.), Some(0.5));
}
/// Draws a solid line with a length annotation between two points transformed by the given affine transformations.
fn draw_line_with_length(line_start: DVec2, line_end: DVec2, transform: DAffine2, document_to_viewport: DAffine2, overlay_context: &mut OverlayContext, label_alignment: LabelAlignment) {
@ -16,7 +16,7 @@ fn draw_line_with_length(line_start: DVec2, line_end: DVec2, transform: DAffine2
let min_viewport = transform.transform_point2(line_start);
let max_viewport = transform.transform_point2(line_end);
overlay_context.line(min_viewport, max_viewport, None);
overlay_context.line(min_viewport, max_viewport, None, None);
// Remove trailing zeros from the formatted string
let length = format!("{:.2}", transform_to_document.transform_vector2(line_end - line_start).length())

View file

@ -81,10 +81,10 @@ impl Pivot {
}
}
pub fn update_pivot(&mut self, document: &DocumentMessageHandler, overlay_context: &mut OverlayContext, angle: f64) {
pub fn update_pivot(&mut self, document: &DocumentMessageHandler, overlay_context: &mut OverlayContext, draw_data: Option<(f64,)>) {
self.recalculate_pivot(document);
if let Some(pivot) = self.pivot {
overlay_context.pivot(pivot, angle);
if let (Some(pivot), Some(data)) = (self.pivot, draw_data) {
overlay_context.pivot(pivot, data.0);
}
}

View file

@ -593,10 +593,7 @@ impl ShapeState {
if points_colinear_status.any(|point| first_is_colinear != point) {
return ManipulatorAngle::Mixed;
}
match first_is_colinear {
false => ManipulatorAngle::Free,
true => ManipulatorAngle::Colinear,
}
if first_is_colinear { ManipulatorAngle::Colinear } else { ManipulatorAngle::Free }
}
pub fn convert_manipulator_handles_to_colinear(&self, vector_data: &VectorData, point_id: PointId, responses: &mut VecDeque<Message>, layer: LayerNodeIdentifier) {

View file

@ -414,12 +414,13 @@ impl SnapManager {
let start = DVec2::new(first.max().x, y);
let end = DVec2::new(second.min().x, y);
let signed_size = if bottom { y_size } else { -y_size };
overlay_context.line(transform.transform_point2(start), transform.transform_point2(start + DVec2::Y * signed_size), None);
overlay_context.line(transform.transform_point2(end), transform.transform_point2(end + DVec2::Y * signed_size), None);
overlay_context.line(transform.transform_point2(start), transform.transform_point2(start + DVec2::Y * signed_size), None, None);
overlay_context.line(transform.transform_point2(end), transform.transform_point2(end + DVec2::Y * signed_size), None, None);
overlay_context.line(
transform.transform_point2(start + DVec2::Y * signed_size / 2.),
transform.transform_point2(end + DVec2::Y * signed_size / 2.),
None,
None,
);
}
}
@ -432,12 +433,13 @@ impl SnapManager {
let start = DVec2::new(x, first.max().y);
let end = DVec2::new(x, second.min().y);
let signed_size = if right { x_size } else { -x_size };
overlay_context.line(transform.transform_point2(start), transform.transform_point2(start + DVec2::X * signed_size), None);
overlay_context.line(transform.transform_point2(end), transform.transform_point2(end + DVec2::X * signed_size), None);
overlay_context.line(transform.transform_point2(start), transform.transform_point2(start + DVec2::X * signed_size), None, None);
overlay_context.line(transform.transform_point2(end), transform.transform_point2(end + DVec2::X * signed_size), None, None);
overlay_context.line(
transform.transform_point2(start + DVec2::X * signed_size / 2.),
transform.transform_point2(end + DVec2::X * signed_size / 2.),
None,
None,
);
}
}
@ -460,7 +462,7 @@ impl SnapManager {
let align = [ind.alignment_target_x, ind.alignment_target_y].map(|target| target.map(|target| to_viewport.transform_point2(target)));
let any_align = align.iter().flatten().next().is_some();
for &target in align.iter().flatten() {
overlay_context.line(viewport, target, None);
overlay_context.line(viewport, target, None, None);
}
for &target in align.iter().flatten() {
overlay_context.manipulator_handle(target, false, None);

View file

@ -374,7 +374,7 @@ pub struct BoundingBoxManager {
pub bounds: [DVec2; 2],
/// The transform to viewport space for the bounds co-ordinates when the bounds were last updated.
pub transform: DAffine2,
/// Was the transform previously singular?
/// Whether the transform is actually singular but adjusted to not be so.
pub transform_tampered: bool,
/// The transform to viewport space for the bounds co-ordinates when the transformation was started.
pub original_bound_transform: DAffine2,
@ -566,16 +566,24 @@ impl BoundingBoxManager {
}
}
pub fn render_quad(&self, overlay_context: &mut OverlayContext) {
let quad = self.transform * Quad::from_box(self.bounds);
// Draw the bounding box rectangle
overlay_context.quad(quad, None);
}
/// Update the position of the bounding box and transform handles
pub fn render_overlays(&mut self, overlay_context: &mut OverlayContext) {
pub fn render_overlays(&mut self, overlay_context: &mut OverlayContext, render_quad: bool) {
let quad = self.transform * Quad::from_box(self.bounds);
let category = self.overlay_display_category();
let horizontal_edges = [quad.top_right().midpoint(quad.bottom_right()), quad.bottom_left().midpoint(quad.top_left())];
let vertical_edges = [quad.top_left().midpoint(quad.top_right()), quad.bottom_right().midpoint(quad.bottom_left())];
// Draw the bounding box rectangle
overlay_context.quad(quad, None);
if render_quad {
self.render_quad(overlay_context);
}
let mut draw_handle = |point: DVec2, angle: f64| {
let quad = DAffine2::from_angle_translation(angle, point) * Quad::from_box([DVec2::splat(-RESIZE_HANDLE_SIZE / 2.), DVec2::splat(RESIZE_HANDLE_SIZE / 2.)]);

View file

@ -60,7 +60,7 @@ pub fn text_bounding_box(layer: LayerNodeIdentifier, document: &DocumentMessageH
let (text, font, typesetting) = get_text(layer, &document.network_interface).expect("Text layer should have text when interacting with the Text tool");
let buzz_face = font_cache.get(font).map(|data| load_face(data));
let far = graphene_core::text::bounding_box(text, buzz_face.as_ref(), typesetting);
let far = graphene_core::text::bounding_box(text, buzz_face.as_ref(), typesetting, false);
Quad::from_box([DVec2::ZERO, far])
}

View file

@ -231,7 +231,7 @@ impl Fsm for ArtboardToolFsmState {
bounding_box_manager.bounds = bounds;
bounding_box_manager.transform = document.metadata().document_to_viewport;
bounding_box_manager.render_overlays(&mut overlay_context);
bounding_box_manager.render_overlays(&mut overlay_context, true);
} else {
tool_data.bounding_box_manager.take();
}

View file

@ -255,7 +255,7 @@ impl Fsm for GradientToolFsmState {
let Gradient { start, end, stops, .. } = gradient;
let (start, end) = (transform.transform_point2(start), transform.transform_point2(end));
overlay_context.line(start, end, None);
overlay_context.line(start, end, None, None);
overlay_context.manipulator_handle(start, dragging == Some(GradientDragTarget::Start), None);
overlay_context.manipulator_handle(end, dragging == Some(GradientDragTarget::End), None);

View file

@ -189,7 +189,7 @@ impl Fsm for LineToolFsmState {
let [viewport_start, viewport_end] = [start, end].map(|point| document.metadata().transform_to_viewport(layer).transform_point2(point));
if (start.x - end.x).abs() > f64::EPSILON * 1000. && (start.y - end.y).abs() > f64::EPSILON * 1000. {
overlay_context.line(viewport_start, viewport_end, None);
overlay_context.line(viewport_start, viewport_end, None, None);
overlay_context.square(viewport_start, Some(6.), None, None);
overlay_context.square(viewport_end, Some(6.), None, None);
}
@ -205,6 +205,8 @@ impl Fsm for LineToolFsmState {
let snapped = tool_data.snap_manager.free_snap(&SnapData::new(document, input), &point, SnapTypeConfiguration::default());
tool_data.drag_start = snapped.snapped_point_document;
responses.add(DocumentMessage::StartTransaction);
for (layer, [document_start, document_end]) in tool_data.selected_layers_with_position.iter() {
let transform = document.metadata().transform_to_viewport(*layer);
let viewport_x = transform.transform_vector2(DVec2::X).normalize_or_zero() * BOUNDS_SELECT_THRESHOLD;
@ -226,8 +228,6 @@ impl Fsm for LineToolFsmState {
}
}
responses.add(DocumentMessage::StartTransaction);
let node_type = resolve_document_node_type("Line").expect("Line node does not exist");
let node = node_type.node_template_input_override([
None,
@ -320,9 +320,7 @@ impl Fsm for LineToolFsmState {
(LineToolFsmState::Drawing, LineToolMessage::Abort) => {
tool_data.snap_manager.cleanup(responses);
tool_data.editing_layer.take();
if tool_data.dragging_endpoint.is_none() {
responses.add(DocumentMessage::AbortTransaction);
}
responses.add(DocumentMessage::AbortTransaction);
LineToolFsmState::Ready
}
(_, LineToolMessage::WorkingColorChanged) => {
@ -377,12 +375,12 @@ fn generate_line(tool_data: &mut LineToolData, snap_data: SnapData, lock_angle:
tool_data.angle = angle;
let angle_vec = DVec2::from_angle(angle);
if lock_angle {
let angle_vec = DVec2::new(angle.cos(), angle.sin());
line_length = (document_points[1] - document_points[0]).dot(angle_vec);
}
document_points[1] = document_points[0] + line_length * DVec2::new(angle.cos(), angle.sin());
document_points[1] = document_points[0] + line_length * angle_vec;
let constrained = snap_angle || lock_angle;
let snap = &mut tool_data.snap_manager;

View file

@ -970,12 +970,12 @@ impl Fsm for PathToolFsmState {
match axis {
Axis::Y => {
overlay_context.line(origin - DVec2::Y * viewport_diagonal, origin + DVec2::Y * viewport_diagonal, Some(COLOR_OVERLAY_BLUE));
overlay_context.line(origin - DVec2::X * viewport_diagonal, origin + DVec2::X * viewport_diagonal, Some(other));
overlay_context.line(origin - DVec2::Y * viewport_diagonal, origin + DVec2::Y * viewport_diagonal, Some(COLOR_OVERLAY_BLUE), None);
overlay_context.line(origin - DVec2::X * viewport_diagonal, origin + DVec2::X * viewport_diagonal, Some(other), None);
}
Axis::X | Axis::Both => {
overlay_context.line(origin - DVec2::X * viewport_diagonal, origin + DVec2::X * viewport_diagonal, Some(COLOR_OVERLAY_BLUE));
overlay_context.line(origin - DVec2::Y * viewport_diagonal, origin + DVec2::Y * viewport_diagonal, Some(other));
overlay_context.line(origin - DVec2::X * viewport_diagonal, origin + DVec2::X * viewport_diagonal, Some(COLOR_OVERLAY_BLUE), None);
overlay_context.line(origin - DVec2::Y * viewport_diagonal, origin + DVec2::Y * viewport_diagonal, Some(other), None);
}
}
}
@ -986,8 +986,8 @@ impl Fsm for PathToolFsmState {
if let Some(closest_segment) = &tool_data.segment {
overlay_context.manipulator_anchor(closest_segment.closest_point_to_viewport(), false, Some(COLOR_OVERLAY_BLUE));
if let (Some(handle1), Some(handle2)) = closest_segment.handle_positions(document.metadata()) {
overlay_context.line(closest_segment.closest_point_to_viewport(), handle1, Some(COLOR_OVERLAY_BLUE));
overlay_context.line(closest_segment.closest_point_to_viewport(), handle2, Some(COLOR_OVERLAY_BLUE));
overlay_context.line(closest_segment.closest_point_to_viewport(), handle1, Some(COLOR_OVERLAY_BLUE), None);
overlay_context.line(closest_segment.closest_point_to_viewport(), handle2, Some(COLOR_OVERLAY_BLUE), None);
overlay_context.manipulator_handle(handle1, false, Some(COLOR_OVERLAY_BLUE));
overlay_context.manipulator_handle(handle2, false, Some(COLOR_OVERLAY_BLUE));
}

View file

@ -1272,7 +1272,7 @@ impl Fsm for PenToolFsmState {
}
// Draw the line between the currently-being-placed anchor and its currently-being-dragged-out outgoing handle (opposite the one currently being dragged out)
overlay_context.line(next_anchor, next_handle_start, None);
overlay_context.line(next_anchor, next_handle_start, None, None);
match tool_options.pen_overlay_mode {
PenOverlayMode::AllHandles => {
@ -1289,19 +1289,19 @@ impl Fsm for PenToolFsmState {
if let (Some(anchor_start), Some(handle_start), Some(handle_end)) = (anchor_start, handle_start, handle_end) {
// Draw the line between the most recently placed anchor and its outgoing handle (which is currently influencing the currently-being-placed segment)
overlay_context.line(anchor_start, handle_start, None);
overlay_context.line(anchor_start, handle_start, None, None);
// Draw the line between the currently-being-placed anchor and its incoming handle (opposite the one currently being dragged out)
overlay_context.line(next_anchor, handle_end, None);
overlay_context.line(next_anchor, handle_end, None, None);
if self == PenToolFsmState::PlacingAnchor && anchor_start != handle_start && tool_data.modifiers.lock_angle {
// Draw the line between the currently-being-placed anchor and last-placed point (lock angle bent overlays)
overlay_context.dashed_line(anchor_start, next_anchor, None, Some(4.), Some(4.), Some(0.5));
overlay_context.dashed_line(anchor_start, next_anchor, None, None, Some(4.), Some(4.), Some(0.5));
}
// Draw the line between the currently-being-placed anchor and last-placed point (snap angle bent overlays)
if self == PenToolFsmState::PlacingAnchor && anchor_start != handle_start && tool_data.modifiers.snap_angle {
overlay_context.dashed_line(anchor_start, next_anchor, None, Some(4.), Some(4.), Some(0.5));
overlay_context.dashed_line(anchor_start, next_anchor, None, None, Some(4.), Some(4.), Some(0.5));
}
if self == PenToolFsmState::DraggingHandle(tool_data.handle_mode) && valid(next_anchor, handle_end) {

View file

@ -594,7 +594,7 @@ impl Fsm for SelectToolFsmState {
bounding_box_manager.bounds = bounds;
bounding_box_manager.transform = transform;
bounding_box_manager.transform_tampered = transform_tampered;
bounding_box_manager.render_overlays(&mut overlay_context);
bounding_box_manager.render_overlays(&mut overlay_context, true);
} else {
tool_data.bounding_box_manager.take();
}
@ -656,7 +656,7 @@ impl Fsm for SelectToolFsmState {
});
// Update pivot
tool_data.pivot.update_pivot(document, &mut overlay_context, angle);
tool_data.pivot.update_pivot(document, &mut overlay_context, Some((angle,)));
// Update compass rose
tool_data.compass_rose.refresh_position(document);
@ -696,7 +696,7 @@ impl Fsm for SelectToolFsmState {
&format!("#{}", color_string)
};
let line_center = tool_data.line_center;
overlay_context.line(line_center - direction * viewport_diagonal, line_center + direction * viewport_diagonal, Some(color));
overlay_context.line(line_center - direction * viewport_diagonal, line_center + direction * viewport_diagonal, Some(color), None);
}
}
}
@ -721,8 +721,8 @@ impl Fsm for SelectToolFsmState {
let edge = DVec2::from_angle(snapped_angle) * viewport_diagonal;
let perp = edge.perp();
overlay_context.line(origin - edge * viewport_diagonal, origin + edge * viewport_diagonal, Some(COLOR_OVERLAY_BLUE));
overlay_context.line(origin - perp * viewport_diagonal, origin + perp * viewport_diagonal, Some(other));
overlay_context.line(origin - edge * viewport_diagonal, origin + edge * viewport_diagonal, Some(COLOR_OVERLAY_BLUE), None);
overlay_context.line(origin - perp * viewport_diagonal, origin + perp * viewport_diagonal, Some(other), None);
}
// Check if the tool is in selection mode
@ -1269,18 +1269,6 @@ impl Fsm for SelectToolFsmState {
state
}
(SelectToolFsmState::Dragging { .. }, SelectToolMessage::Enter) => {
let response = match input.mouse.position.distance(tool_data.drag_start) < 10. * f64::EPSILON {
true => DocumentMessage::AbortTransaction,
false => DocumentMessage::EndTransaction,
};
tool_data.axis_align = false;
tool_data.snap_manager.cleanup(responses);
responses.add_front(response);
let selection = tool_data.nested_selection_behavior;
SelectToolFsmState::Ready { selection }
}
(SelectToolFsmState::Dragging { has_dragged, .. }, SelectToolMessage::DragStop { remove_from_selection }) => {
// Deselect layer if not snap dragging
responses.add(DocumentMessage::EndTransaction);
@ -1337,48 +1325,25 @@ impl Fsm for SelectToolFsmState {
let selection = tool_data.nested_selection_behavior;
SelectToolFsmState::Ready { selection }
}
(SelectToolFsmState::ResizingBounds | SelectToolFsmState::SkewingBounds { .. }, SelectToolMessage::DragStop { .. } | SelectToolMessage::Enter) => {
let response = match input.mouse.position.distance(tool_data.drag_start) < 10. * f64::EPSILON {
true => DocumentMessage::AbortTransaction,
false => DocumentMessage::EndTransaction,
};
(
SelectToolFsmState::ResizingBounds | SelectToolFsmState::SkewingBounds { .. } | SelectToolFsmState::RotatingBounds | SelectToolFsmState::Dragging { .. },
SelectToolMessage::DragStop { .. } | SelectToolMessage::Enter,
) => {
let drag_too_small = input.mouse.position.distance(tool_data.drag_start) < 10. * f64::EPSILON;
let response = if drag_too_small { DocumentMessage::AbortTransaction } else { DocumentMessage::EndTransaction };
responses.add(response);
tool_data.axis_align = false;
tool_data.snap_manager.cleanup(responses);
if let Some(bounds) = &mut tool_data.bounding_box_manager {
bounds.original_transforms.clear();
if !matches!(self, SelectToolFsmState::DraggingPivot) {
if let Some(bounds) = &mut tool_data.bounding_box_manager {
bounds.original_transforms.clear();
}
}
let selection = tool_data.nested_selection_behavior;
SelectToolFsmState::Ready { selection }
}
(SelectToolFsmState::RotatingBounds, SelectToolMessage::DragStop { .. } | SelectToolMessage::Enter) => {
let response = match input.mouse.position.distance(tool_data.drag_start) < 10. * f64::EPSILON {
true => DocumentMessage::AbortTransaction,
false => DocumentMessage::EndTransaction,
};
responses.add(response);
if let Some(bounds) = &mut tool_data.bounding_box_manager {
bounds.original_transforms.clear();
}
let selection = tool_data.nested_selection_behavior;
SelectToolFsmState::Ready { selection }
}
(SelectToolFsmState::DraggingPivot, SelectToolMessage::DragStop { .. } | SelectToolMessage::Enter) => {
let response = match input.mouse.position.distance(tool_data.drag_start) < 10. * f64::EPSILON {
true => DocumentMessage::AbortTransaction,
false => DocumentMessage::EndTransaction,
};
responses.add(response);
tool_data.snap_manager.cleanup(responses);
let selection = tool_data.nested_selection_behavior;
SelectToolFsmState::Ready { selection }
}
(SelectToolFsmState::Drawing { selection_shape, .. }, SelectToolMessage::DragStop { remove_from_selection }) => {
let quad = tool_data.selection_quad();

View file

@ -203,6 +203,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for TextToo
match self.fsm_state {
TextToolFsmState::Ready => actions!(TextToolMessageDiscriminant;
DragStart,
PointerOutsideViewport,
PointerMove,
),
TextToolFsmState::Editing => actions!(TextToolMessageDiscriminant;
@ -213,11 +214,13 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for TextToo
DragStop,
Abort,
PointerMove,
PointerOutsideViewport,
),
TextToolFsmState::ResizingBounds => actions!(TextToolMessageDiscriminant;
DragStop,
Abort,
PointerMove,
PointerOutsideViewport,
),
}
}
@ -242,9 +245,9 @@ enum TextToolFsmState {
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.
Placing,
/// The user is dragging an existing text layer to move it.
Dragging,
/// The user is dragging to resize the text area.
ResizingBounds,
@ -271,6 +274,8 @@ struct TextToolData {
layer: LayerNodeIdentifier,
editing_text: Option<EditingText>,
new_text: String,
drag_start: DVec2,
drag_current: DVec2,
resize: Resize,
auto_panning: AutoPanning,
// Since the overlays must be drawn without knowledge of the inputs
@ -464,7 +469,7 @@ impl Fsm for TextToolFsmState {
});
if let Some(editing_text) = tool_data.editing_text.as_mut() {
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.as_ref(), editing_text.typesetting);
let far = graphene_core::text::bounding_box(&tool_data.new_text, buzz_face.as_ref(), 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;
@ -475,7 +480,7 @@ impl Fsm for TextToolFsmState {
TextToolFsmState::Editing
}
(_, TextToolMessage::Overlays(mut overlay_context)) => {
if matches!(self, Self::Placing | Self::Dragging) {
if matches!(self, Self::Placing) {
// Get the updated selection box bounds
let quad = Quad::from_box(tool_data.cached_resize_bounds);
@ -506,21 +511,18 @@ impl Fsm for TextToolFsmState {
bounding_box_manager.bounds = [bounds.0[0], bounds.0[2]];
bounding_box_manager.transform = layer_transform;
bounding_box_manager.render_overlays(&mut overlay_context);
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) {
let buzz_face = font_cache.get(font).map(|data| load_face(data));
if lines_clipping(text.as_str(), buzz_face, typesetting) {
overlay_context.line(transformed_quad.0[2], transformed_quad.0[3], Some(COLOR_OVERLAY_RED));
overlay_context.line(transformed_quad.0[2], transformed_quad.0[3], Some(COLOR_OVERLAY_RED), Some(3.));
}
}
// The angle is choosen to be parallel to the X axis in the bounds transform.
let angle = bounding_box_manager.transform.transform_vector2(DVec2::X).to_angle();
// Update pivot
tool_data.pivot.update_pivot(document, &mut overlay_context, angle);
bounding_box_manager.render_overlays(&mut overlay_context, false);
tool_data.pivot.update_pivot(document, &mut overlay_context, None);
} else {
tool_data.bounding_box_manager.take();
}
@ -540,6 +542,8 @@ impl Fsm for TextToolFsmState {
(TextToolFsmState::Ready, TextToolMessage::DragStart) => {
tool_data.resize.start(document, input);
tool_data.cached_resize_bounds = [tool_data.resize.viewport_drag_start(document); 2];
tool_data.drag_start = input.mouse.position;
tool_data.drag_current = input.mouse.position;
let dragging_bounds = tool_data.bounding_box_manager.as_mut().and_then(|bounding_box| {
let edges = bounding_box.check_selected_edges(input.mouse.position);
@ -557,7 +561,7 @@ impl Fsm for TextToolFsmState {
let mut all_selected = selected.selected_visible_and_unlocked_layers(&document.network_interface);
let selected = all_selected.find(|layer| is_layer_fed_by_node_of_name(*layer, &document.network_interface, "Text"));
if let Some(_selected_edges) = dragging_bounds {
if dragging_bounds.is_some() {
responses.add(DocumentMessage::StartTransaction);
// Set the original transform
@ -568,12 +572,25 @@ impl Fsm for TextToolFsmState {
if let Some(bounds) = &mut tool_data.bounding_box_manager {
bounds.original_bound_transform = bounds.transform;
bounds.center_of_transformation = bounds.transform.transform_point2((bounds.bounds[0] + bounds.bounds[1]) / 2.);
}
tool_data.get_snap_candidates(document, font_cache);
return TextToolFsmState::ResizingBounds;
} else if let Some(clicked_layer) = TextToolData::check_click(document, input, font_cache) {
responses.add(DocumentMessage::StartTransaction);
if selected != Some(clicked_layer) {
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![clicked_layer.to_node()] });
}
let original_transform = document.metadata().transform_to_document(clicked_layer);
tool_data.layer_dragging = Some(ResizingLayer {
id: clicked_layer,
original_transform,
});
tool_data.get_snap_candidates(document, font_cache);
return TextToolFsmState::Dragging;
}
TextToolFsmState::Placing
}
@ -596,7 +613,7 @@ impl Fsm for TextToolFsmState {
TextToolFsmState::Ready
}
(Self::Placing | TextToolFsmState::Dragging, TextToolMessage::PointerMove { center, lock_ratio }) => {
(TextToolFsmState::Placing, TextToolMessage::PointerMove { center, lock_ratio }) => {
tool_data.cached_resize_bounds = tool_data.resize.calculate_points_ignore_layer(document, input, center, lock_ratio, false);
responses.add(OverlaysMessage::Draw);
@ -608,18 +625,42 @@ impl Fsm for TextToolFsmState {
];
tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses);
TextToolFsmState::Placing
}
(TextToolFsmState::Dragging, TextToolMessage::PointerMove { center, lock_ratio }) => {
if let Some(dragging_layer) = &tool_data.layer_dragging {
let delta = input.mouse.position - tool_data.drag_current;
tool_data.drag_current = input.mouse.position;
responses.add(GraphOperationMessage::TransformChange {
layer: dragging_layer.id,
transform: DAffine2::from_translation(delta),
transform_in: TransformIn::Viewport,
skip_rerender: false,
});
responses.add(NodeGraphMessage::RunDocumentGraph);
// 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
}
(TextToolFsmState::ResizingBounds, TextToolMessage::PointerMove { center, lock_ratio }) => {
if let Some(bounds) = &mut tool_data.bounding_box_manager {
if let Some(movement) = &mut bounds.selected_edges {
let (center_bool, lock_ratio_bool) = (input.keyboard.key(center), input.keyboard.key(lock_ratio));
let center_position = center_bool.then_some(bounds.center_of_transformation);
let (centered, constrain) = (input.keyboard.key(center), input.keyboard.key(lock_ratio));
let center_position = centered.then_some(bounds.center_of_transformation);
let Some(dragging_layer) = tool_data.layer_dragging else { return TextToolFsmState::Ready };
let Some(node_id) = graph_modification_utils::get_text_id(dragging_layer.id, &document.network_interface) else {
warn!("Cannot get text node id");
tool_data.layer_dragging = None;
tool_data.layer_dragging.take();
return TextToolFsmState::Ready;
};
@ -630,7 +671,7 @@ impl Fsm for TextToolFsmState {
snap_data: SnapData::ignore(document, input, &selected),
});
let (position, size) = movement.new_size(input.mouse.position, bounds.original_bound_transform, center_position, lock_ratio_bool, snap);
let (position, size) = movement.new_size(input.mouse.position, bounds.original_bound_transform, center_position, constrain, snap);
// Normalize so the size is always positive
let (position, size) = (position.min(position + size), size.abs());
@ -677,13 +718,13 @@ impl Fsm for TextToolFsmState {
self
}
(TextToolFsmState::Placing | TextToolFsmState::Dragging, TextToolMessage::PointerOutsideViewport { .. }) => {
(TextToolFsmState::Placing, TextToolMessage::PointerOutsideViewport { .. }) => {
// Auto-panning setup
let _ = tool_data.auto_panning.shift_viewport(input, responses);
TextToolFsmState::Dragging
TextToolFsmState::Placing
}
(TextToolFsmState::ResizingBounds, TextToolMessage::PointerOutsideViewport { .. }) => {
(TextToolFsmState::ResizingBounds | TextToolFsmState::Dragging, TextToolMessage::PointerOutsideViewport { .. }) => {
// AutoPanning
if let Some(shift) = tool_data.auto_panning.shift_viewport(input, responses) {
if let Some(bounds) = &mut tool_data.bounding_box_manager {
@ -705,10 +746,8 @@ impl Fsm for TextToolFsmState {
state
}
(TextToolFsmState::ResizingBounds, TextToolMessage::DragStop) => {
let response = match input.mouse.position.distance(tool_data.resize.viewport_drag_start(document)) < 10. * f64::EPSILON {
true => DocumentMessage::AbortTransaction,
false => DocumentMessage::EndTransaction,
};
let drag_too_small = input.mouse.position.distance(tool_data.resize.viewport_drag_start(document)) < 10. * f64::EPSILON;
let response = if drag_too_small { DocumentMessage::AbortTransaction } else { DocumentMessage::EndTransaction };
responses.add(response);
tool_data.resize.snap_manager.cleanup(responses);
@ -719,7 +758,7 @@ impl Fsm for TextToolFsmState {
TextToolFsmState::Ready
}
(TextToolFsmState::Placing | TextToolFsmState::Dragging, TextToolMessage::DragStop) => {
(TextToolFsmState::Placing, TextToolMessage::DragStop) => {
let [start, end] = tool_data.cached_resize_bounds;
let has_dragged = (start - end).length_squared() > DRAG_THRESHOLD * DRAG_THRESHOLD;
@ -749,6 +788,27 @@ impl Fsm for TextToolFsmState {
tool_data.new_text(document, editing_text, font_cache, responses);
TextToolFsmState::Editing
}
(TextToolFsmState::Dragging, TextToolMessage::DragStop) => {
let drag_too_small = input.mouse.position.distance(tool_data.drag_start) < 10. * f64::EPSILON;
let response = if drag_too_small { DocumentMessage::AbortTransaction } else { DocumentMessage::EndTransaction };
responses.add(response);
tool_data.resize.snap_manager.cleanup(responses);
if let Some(bounds) = &mut tool_data.bounding_box_manager {
bounds.original_transforms.clear();
}
if drag_too_small {
if let Some(layer_info) = &tool_data.layer_dragging {
tool_data.start_editing_layer(layer_info.id, self, document, font_cache, responses);
return TextToolFsmState::Editing;
}
}
tool_data.layer_dragging.take();
TextToolFsmState::Ready
}
(TextToolFsmState::Editing, TextToolMessage::TextChange { new_text, is_left_or_right_click }) => {
tool_data.new_text = new_text;
@ -789,16 +849,21 @@ impl Fsm for TextToolFsmState {
}
responses.add(FrontendMessage::TriggerTextCommit);
TextToolFsmState::Editing
}
(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);
if matches!(state, TextToolFsmState::ResizingBounds | TextToolFsmState::Dragging) {
responses.add(DocumentMessage::AbortTransaction);
if let Some(bounds) = &mut tool_data.bounding_box_manager {
bounds.original_transforms.clear();
}
if matches!(state, TextToolFsmState::Dragging) {
tool_data.layer_dragging.take();
}
} else {
input.mouse.finish_transaction(tool_data.resize.viewport_drag_start(document), responses);
}
tool_data.resize.cleanup(responses);
TextToolFsmState::Ready
}
@ -821,12 +886,13 @@ impl Fsm for TextToolFsmState {
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![
TextToolFsmState::Placing => 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")]),
]),
TextToolFsmState::Dragging => HintData(vec![HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()])]),
TextToolFsmState::ResizingBounds => HintData(vec![
HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, "Resize Text Box")]),
HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]),
HintGroup(vec![HintInfo::keys([Key::Shift], "Lock Aspect Ratio"), HintInfo::keys([Key::Alt], "From Center")]),
]),
};
@ -836,7 +902,7 @@ impl Fsm for TextToolFsmState {
fn update_cursor(&self, responses: &mut VecDeque<Message>) {
let cursor = match self {
TextToolFsmState::Dragging => MouseCursorIcon::Crosshair,
TextToolFsmState::Placing => MouseCursorIcon::Crosshair,
_ => MouseCursorIcon::Text,
};
responses.add(FrontendMessage::UpdateMouseCursor { cursor });

View file

@ -242,7 +242,7 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
if matches!(axis_constraint, Axis::Both | Axis::X) && translation.x != 0. {
let end = if self.local { (quad[1] - quad[0]).rotate(e1) + quad[0] } else { quad[1] };
overlay_context.line(quad[0], end, None);
overlay_context.line(quad[0], end, None, None);
let x_transform = DAffine2::from_translation((quad[0] + end) / 2.);
overlay_context.text(&format_rounded(translation.x, 3), COLOR_OVERLAY_BLUE, None, x_transform, 4., [Pivot::Middle, Pivot::End]);
@ -250,7 +250,7 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
if matches!(axis_constraint, Axis::Both | Axis::Y) && translation.y != 0. {
let end = if self.local { (quad[3] - quad[0]).rotate(e1) + quad[0] } else { quad[3] };
overlay_context.line(quad[0], end, None);
overlay_context.line(quad[0], end, None, None);
let x_parameter = viewport_translate.x.clamp(-1., 1.);
let y_transform = DAffine2::from_translation((quad[0] + end) / 2. + x_parameter * DVec2::X * 0.);
let pivot_selection = if x_parameter >= -1e-3 { Pivot::Start } else { Pivot::End };
@ -259,8 +259,8 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
}
}
if matches!(axis_constraint, Axis::Both) && translation.x != 0. && translation.y != 0. {
overlay_context.dashed_line(quad[1], quad[2], None, Some(2.), Some(2.), Some(0.5));
overlay_context.dashed_line(quad[3], quad[2], None, Some(2.), Some(2.), Some(0.5));
overlay_context.dashed_line(quad[1], quad[2], None, None, Some(2.), Some(2.), Some(0.5));
overlay_context.dashed_line(quad[3], quad[2], None, None, Some(2.), Some(2.), Some(0.5));
}
}
TransformOperation::Scaling(scale) => {
@ -274,9 +274,9 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
let end_point = pivot + local_edge * scale.max(1.);
if scale > 0. {
overlay_context.dashed_line(pivot, boundary_point, None, Some(4.), Some(4.), Some(0.5));
overlay_context.dashed_line(pivot, boundary_point, None, None, Some(4.), Some(4.), Some(0.5));
}
overlay_context.line(boundary_point, end_point, None);
overlay_context.line(boundary_point, end_point, None, None);
let transform = DAffine2::from_translation(boundary_point.midpoint(pivot) + local_edge.perp().normalize_or(DVec2::X) * local_edge.element_product().signum() * 24.);
overlay_context.text(&text, COLOR_OVERLAY_BLUE, None, transform, 16., [Pivot::Middle, Pivot::Middle]);

View file

@ -56,11 +56,7 @@ impl<PointId: crate::Identifier> Iterator for SubpathIter<'_, PointId> {
return None;
}
let closed = if self.is_always_closed { true } else { self.subpath.closed };
let len = self.subpath.len() - 1
+ match closed {
true => 1,
false => 0,
};
let len = self.subpath.len() - 1 + if closed { 1 } else { 0 };
if self.index >= len {
return None;
}

View file

@ -299,19 +299,13 @@ fn absolute_value<U: num_traits::float::Float>(_: impl Ctx, #[implementations(f6
/// The minimum function (min) picks the smaller of two numbers.
#[node_macro::node(category("Math: Numeric"))]
fn min<T: core::cmp::PartialOrd>(_: impl Ctx, #[implementations(f64, &f64, f32, &f32, u32, &u32, &str)] value: T, #[implementations(f64, &f64, f32, &f32, u32, &u32, &str)] other_value: T) -> T {
match value < other_value {
true => value,
false => other_value,
}
if value < other_value { value } else { other_value }
}
/// The maximum function (max) picks the larger of two numbers.
#[node_macro::node(category("Math: Numeric"))]
fn max<T: core::cmp::PartialOrd>(_: impl Ctx, #[implementations(f64, &f64, f32, &f32, u32, &u32, &str)] value: T, #[implementations(f64, &f64, f32, &f32, u32, &u32, &str)] other_value: T) -> T {
match value > other_value {
true => value,
false => other_value,
}
if value > other_value { value } else { other_value }
}
/// The clamp function (clamp) restricts a number to a specified range between a minimum and maximum value. The minimum and maximum values are automatically swapped if they are reversed.

View file

@ -7,7 +7,7 @@ use rustybuzz::{GlyphBuffer, UnicodeBuffer};
struct Builder {
current_subpath: Subpath<PointId>,
other_subpaths: Vec<Subpath<PointId>>,
pos: DVec2,
text_cursor: DVec2,
offset: DVec2,
ascender: f64,
scale: f64,
@ -16,7 +16,7 @@ struct Builder {
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
self.text_cursor + self.offset + DVec2::new(x as f64, self.ascender - y as f64) * self.scale
}
}
@ -99,11 +99,7 @@ impl Default for TypesettingConfig {
}
pub fn to_path(str: &str, buzz_face: Option<rustybuzz::Face>, typesetting: TypesettingConfig) -> Vec<Subpath<PointId>> {
let buzz_face = match buzz_face {
Some(face) => face,
// Show blank layer if font has not loaded
None => return vec![],
};
let Some(buzz_face) = buzz_face else { return vec![] };
let space_glyph = buzz_face.glyph_index(' ');
let (scale, line_height, mut buffer) = font_properties(&buzz_face, typesetting.font_size, typesetting.line_height_ratio);
@ -111,7 +107,7 @@ pub fn to_path(str: &str, buzz_face: Option<rustybuzz::Face>, typesetting: Types
let mut builder = Builder {
current_subpath: Subpath::new(Vec::new(), false),
other_subpaths: Vec::new(),
pos: DVec2::ZERO,
text_cursor: DVec2::ZERO,
offset: DVec2::ZERO,
ascender: (buzz_face.ascender() as f64 / buzz_face.height() as f64) * typesetting.font_size / scale,
scale,
@ -124,19 +120,19 @@ pub fn to_path(str: &str, buzz_face: Option<rustybuzz::Face>, typesetting: Types
let glyph_buffer = rustybuzz::shape(&buzz_face, &[], buffer);
// Don't wrap the first word
if index != 0 && wrap_word(typesetting.max_width, &glyph_buffer, scale, typesetting.character_spacing, builder.pos.x, space_glyph) {
builder.pos = DVec2::new(0., builder.pos.y + line_height);
if index != 0 && wrap_word(typesetting.max_width, &glyph_buffer, scale, typesetting.character_spacing, builder.text_cursor.x, space_glyph) {
builder.text_cursor = DVec2::new(0., builder.text_cursor.y + line_height);
}
for (glyph_position, glyph_info) in glyph_buffer.glyph_positions().iter().zip(glyph_buffer.glyph_infos()) {
let glyph_id = GlyphId(glyph_info.glyph_id as u16);
if let Some(max_width) = typesetting.max_width {
if space_glyph != Some(glyph_id) && builder.pos.x + (glyph_position.x_advance as f64 * builder.scale * typesetting.character_spacing) >= max_width {
builder.pos = DVec2::new(0., builder.pos.y + line_height);
if space_glyph != Some(glyph_id) && builder.text_cursor.x + (glyph_position.x_advance as f64 * builder.scale * typesetting.character_spacing) >= max_width {
builder.text_cursor = DVec2::new(0., builder.text_cursor.y + line_height);
}
}
// Clip when the height is exceeded
if typesetting.max_height.is_some_and(|max_height| builder.pos.y > max_height - line_height) {
if typesetting.max_height.is_some_and(|max_height| builder.text_cursor.y > max_height - line_height) {
return builder.other_subpaths;
}
@ -146,30 +142,31 @@ pub fn to_path(str: &str, buzz_face: Option<rustybuzz::Face>, typesetting: Types
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 * typesetting.character_spacing, glyph_position.y_advance as f64) * builder.scale;
builder.text_cursor += DVec2::new(glyph_position.x_advance as f64 * typesetting.character_spacing, glyph_position.y_advance as f64) * builder.scale;
}
buffer = glyph_buffer.clear();
}
builder.pos = DVec2::new(0., builder.pos.y + line_height);
builder.text_cursor = DVec2::new(0., builder.text_cursor.y + line_height);
}
builder.other_subpaths
}
pub fn bounding_box(str: &str, buzz_face: Option<&rustybuzz::Face>, typesetting: TypesettingConfig) -> DVec2 {
let buzz_face = match buzz_face {
Some(face) => face,
// Show blank layer if font has not loaded
None => return DVec2::ZERO,
};
pub fn bounding_box(str: &str, buzz_face: Option<&rustybuzz::Face>, typesetting: TypesettingConfig, for_clipping_test: bool) -> DVec2 {
// Show blank layer if font has not loaded
let Some(buzz_face) = buzz_face else { return DVec2::ZERO };
let space_glyph = buzz_face.glyph_index(' ');
let (scale, line_height, mut buffer) = font_properties(buzz_face, typesetting.font_size, typesetting.line_height_ratio);
let mut pos = DVec2::ZERO;
let mut bounds = DVec2::ZERO;
let [mut text_cursor, mut bounds] = [DVec2::ZERO; 2];
if !for_clipping_test {
if let (Some(max_height), Some(max_width)) = (typesetting.max_height, typesetting.max_width) {
return DVec2::new(max_width, max_height);
}
}
for line in str.split('\n') {
for (index, word) in SplitWordsIncludingSpaces::new(line).enumerate() {
@ -178,32 +175,34 @@ pub fn bounding_box(str: &str, buzz_face: Option<&rustybuzz::Face>, typesetting:
let glyph_buffer = rustybuzz::shape(buzz_face, &[], buffer);
// Don't wrap the first word
if index != 0 && wrap_word(typesetting.max_width, &glyph_buffer, scale, typesetting.character_spacing, pos.x, space_glyph) {
pos = DVec2::new(0., pos.y + line_height);
if index != 0 && wrap_word(typesetting.max_width, &glyph_buffer, scale, typesetting.character_spacing, text_cursor.x, space_glyph) {
text_cursor = DVec2::new(0., text_cursor.y + line_height);
}
for (glyph_position, glyph_info) in glyph_buffer.glyph_positions().iter().zip(glyph_buffer.glyph_infos()) {
let glyph_id = GlyphId(glyph_info.glyph_id as u16);
if let Some(max_width) = typesetting.max_width {
if space_glyph != Some(glyph_id) && pos.x + (glyph_position.x_advance as f64 * scale * typesetting.character_spacing) >= max_width {
pos = DVec2::new(0., pos.y + line_height);
if space_glyph != Some(glyph_id) && text_cursor.x + (glyph_position.x_advance as f64 * scale * typesetting.character_spacing) >= max_width {
text_cursor = DVec2::new(0., text_cursor.y + line_height);
}
}
pos += DVec2::new(glyph_position.x_advance as f64 * typesetting.character_spacing, glyph_position.y_advance as f64) * scale;
bounds = bounds.max(pos + DVec2::new(0., line_height));
text_cursor += DVec2::new(glyph_position.x_advance as f64 * typesetting.character_spacing, glyph_position.y_advance as f64) * scale;
bounds = bounds.max(text_cursor + DVec2::new(0., line_height));
}
buffer = glyph_buffer.clear();
}
pos = DVec2::new(0., pos.y + line_height);
bounds = bounds.max(pos);
text_cursor = DVec2::new(0., text_cursor.y + line_height);
bounds = bounds.max(text_cursor);
}
if let Some(max_width) = typesetting.max_width {
bounds.x = max_width;
}
if let Some(max_height) = typesetting.max_height {
bounds.y = max_height;
if !for_clipping_test {
if let Some(max_width) = typesetting.max_width {
bounds.x = max_width;
}
if let Some(max_height) = typesetting.max_height {
bounds.y = max_height;
}
}
bounds
@ -214,56 +213,9 @@ pub fn load_face(data: &[u8]) -> rustybuzz::Face {
}
pub fn lines_clipping(str: &str, buzz_face: Option<rustybuzz::Face>, typesetting: TypesettingConfig) -> bool {
let buzz_face = match buzz_face {
Some(face) => face,
// False if font hasn't loaded
None => return false,
};
if typesetting.max_height.is_none() {
return false;
}
let space_glyph = buzz_face.glyph_index(' ');
let (scale, line_height, mut buffer) = font_properties(&buzz_face, typesetting.font_size, typesetting.line_height_ratio);
let mut pos = DVec2::ZERO;
let mut bounds = DVec2::ZERO;
for line in str.split('\n') {
for (index, word) in SplitWordsIncludingSpaces::new(line).enumerate() {
push_str(&mut buffer, word);
let glyph_buffer = rustybuzz::shape(&buzz_face, &[], buffer);
// Don't wrap the first word
if index != 0 && wrap_word(typesetting.max_width, &glyph_buffer, scale, typesetting.character_spacing, pos.x, space_glyph) {
pos = DVec2::new(0., pos.y + line_height);
}
for (glyph_position, glyph_info) in glyph_buffer.glyph_positions().iter().zip(glyph_buffer.glyph_infos()) {
let glyph_id = GlyphId(glyph_info.glyph_id as u16);
if let Some(max_width) = typesetting.max_width {
if space_glyph != Some(glyph_id) && pos.x + (glyph_position.x_advance as f64 * scale * typesetting.character_spacing) >= max_width {
pos = DVec2::new(0., pos.y + line_height);
}
}
pos += DVec2::new(glyph_position.x_advance as f64 * typesetting.character_spacing, glyph_position.y_advance as f64) * scale;
bounds = bounds.max(pos + DVec2::new(0., line_height));
}
buffer = glyph_buffer.clear();
}
pos = DVec2::new(0., pos.y + line_height);
bounds = bounds.max(pos);
}
if typesetting.max_height.unwrap() < bounds.y {
return true;
}
false
let Some(max_height) = typesetting.max_height else { return false };
let bounds = bounding_box(str, buzz_face.as_ref(), typesetting, true);
max_height < bounds.y
}
struct SplitWordsIncludingSpaces<'a> {