Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions src/draw/shape/transform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,43 @@ use crate::util;
use super::Shape;

impl Shape {
/// Limits movement so every translated integer coordinate stays representable.
pub(crate) fn constrain_translation(&self, dx: i32, dy: i32) -> (i32, i32) {
let constrain = |position: i32, delta: i32| {
let position = i64::from(position);
let clamped = i64::from(delta).clamp(
i64::from(i32::MIN) - position,
i64::from(i32::MAX) - position,
);
i32::try_from(clamped)
.expect("anchor-constrained movement lies between zero and the requested i32 delta")
};

match self {
Shape::Line { x1, y1, x2, y2, .. } | Shape::Arrow { x1, y1, x2, y2, .. } => (
constrain(*x2, constrain(*x1, dx)),
constrain(*y2, constrain(*y1, dy)),
),
Shape::Rect { x, y, .. }
| Shape::BlurRect { x, y, .. }
| Shape::Image { x, y, .. }
| Shape::Text { x, y, .. }
| Shape::StepMarker { x, y, .. }
| Shape::StickyNote { x, y, .. } => (constrain(*x, dx), constrain(*y, dy)),
Shape::Ellipse { cx, cy, .. } | Shape::Spotlight { cx, cy, .. } => {
(constrain(*cx, dx), constrain(*cy, dy))
}
Shape::Polygon { points, .. } | Shape::EraserStroke { points, .. } => {
points.iter().fold((dx, dy), |(dx, dy), &(x, y)| {
(constrain(x, dx), constrain(y, dy))
})
}
Shape::Freehand { .. }
| Shape::FreehandPressure { .. }
| Shape::MarkerStroke { .. } => (dx, dy),
}
}

pub(crate) fn translate(&mut self, dx: i32, dy: i32) {
match self {
Shape::Freehand { points, .. } => {
Expand Down
4 changes: 2 additions & 2 deletions src/input/state/core/history.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,12 @@ impl InputState {
// A live slider drag holds snapshots from before it began; it has to
// land as its own entry before history moves past it.
self.finish_properties_slider_drag_with(measurer);
let selection_chrome = self.selection_chrome_bounds_with(measurer);
let selection_chrome = self.selection_chrome_damage_with(measurer);
let Some(action) = step(self.boards.active_frame_mut()) else {
return false;
};

self.mark_selection_dirty_region(selection_chrome);
self.mark_selection_chrome_damage(selection_chrome);
self.apply_action_side_effects_with(measurer, &action);
// A recognition chip advertises this undo; once history moves it no
// longer describes the canvas.
Expand Down
78 changes: 39 additions & 39 deletions src/input/state/core/selection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@ use crate::util::Rect;
use std::collections::HashSet;
use std::time::Instant;

/// What to repaint for a selection's chrome.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum SelectionChromeDamage {
/// The chrome's canvas area, if anything selected is painted.
Region(Option<Rect>),
/// The painted bounds do not fit a [`Rect`], for example because they
/// end past `i32::MAX`, yet every member still paints a halo.
Everything,
}

#[derive(Debug, Clone, Default)]
enum SelectionState {
#[default]
Expand Down Expand Up @@ -201,65 +211,55 @@ impl InputState {
change: impl FnOnce(&mut SelectionInteraction),
) {
let previous_ids = self.selected_shape_ids().to_vec();
let previous_chrome = self.selection_chrome_bounds_with(measurer);
let previous_chrome = self.selection_chrome_damage_with(measurer);

change(&mut self.selection_interaction);
self.close_properties_panel();

if self.selected_shape_ids() != previous_ids.as_slice() {
self.mark_selection_dirty_region(previous_chrome);
self.mark_selection_chrome_damage(previous_chrome);
self.mark_selection_chrome_dirty_with(measurer);
self.needs_redraw = true;
}
}

/// Canvas bounds of everything painted for the selection: the halos, the
/// Canvas area of everything painted for the selection: the halos, the
/// dashed handle frame, and the resize handle of a lone text shape, which
/// sits outside the shape.
pub(crate) fn selection_chrome_bounds_with(&self, measurer: &TextMeasurer) -> Option<Rect> {
let bounds = self.selection_bounds_with(measurer)?;
match self.selected_text_resize_handle_with(measurer) {
pub(crate) fn selection_chrome_damage_with(
&self,
measurer: &TextMeasurer,
) -> SelectionChromeDamage {
let Some(bounds) = self.selection_bounds_with(measurer) else {
return if self.selection_is_painted_with(measurer) {
SelectionChromeDamage::Everything
} else {
SelectionChromeDamage::Region(None)
};
};

let chrome = match self.selected_text_resize_handle_with(measurer) {
Some((_, handle)) => bounds.union(handle),
None => Some(bounds),
};
SelectionChromeDamage::Region(chrome)
}

pub(crate) fn mark_selection_chrome_damage(&mut self, damage: SelectionChromeDamage) {
match damage {
SelectionChromeDamage::Region(chrome) => self.mark_selection_dirty_region(chrome),
SelectionChromeDamage::Everything => {
self.dirty_tracker.mark_full();
self.mark_selection_dirty_region(None);
}
}
}

/// Repaints the current selection chrome. Also used when an interaction
/// ends and the handles it hid reappear without a membership change.
pub(crate) fn mark_selection_chrome_dirty_with(&mut self, measurer: &TextMeasurer) {
let chrome = self.selection_chrome_bounds_with(measurer);
self.mark_selection_dirty_region(chrome);
}

pub(crate) fn selection_bounding_box_with(
&self,
measurer: &TextMeasurer,
ids: &[ShapeId],
) -> Option<Rect> {
let frame = self.boards.active_frame();
let mut min_x = i32::MAX;
let mut min_y = i32::MAX;
let mut max_x = i32::MIN;
let mut max_y = i32::MIN;
let mut found = false;

for id in ids {
if let Some(shape) = frame.shape(*id)
&& let Some(bounds) = shape.bounding_box_with(measurer)
{
min_x = min_x.min(bounds.x);
min_y = min_y.min(bounds.y);
max_x = max_x.max(bounds.x + bounds.width);
max_y = max_y.max(bounds.y + bounds.height);
found = true;
}
}

if found {
Rect::from_min_max(min_x, min_y, max_x, max_y)
} else {
None
}
let chrome = self.selection_chrome_damage_with(measurer);
self.mark_selection_chrome_damage(chrome);
}

pub(crate) fn selection_screen_bounding_box_with(
Expand Down
4 changes: 2 additions & 2 deletions src/input/state/core/selection_actions/delete.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ impl InputState {

// Measured before the shapes go: the chrome of a deleted text shape
// reaches past the shape to its resize handle.
let selection_chrome = self.selection_chrome_bounds_with(measurer);
let selection_chrome = self.selection_chrome_damage_with(measurer);
let effects = crate::input::state::core::editing::CanvasEdit::delete(
self.boards.active_frame_mut(),
id_set,
Expand All @@ -50,7 +50,7 @@ impl InputState {
return false;
}

self.mark_selection_dirty_region(selection_chrome);
self.mark_selection_chrome_damage(selection_chrome);
self.clear_selection_with(measurer);
true
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ const MIN_RESIZE_SIZE: f64 = 10.0;

impl InputState {
pub(super) fn point_near(&self, x: i32, y: i32, cx: i32, cy: i32, radius: i32) -> bool {
(x - cx).abs() <= radius && (y - cy).abs() <= radius
let near = |a: i32, b: i32| (i64::from(a) - i64::from(b)).abs() <= i64::from(radius);
near(x, cx) && near(y, cy)
}

pub(super) fn selection_handle_probes(
Expand Down
22 changes: 17 additions & 5 deletions src/input/state/core/selection_actions/text/handles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,24 @@ const TEXT_RESIZE_HANDLE_SIZE: i32 = 10;
const TEXT_RESIZE_HANDLE_OFFSET: i32 = 6;

impl InputState {
/// Returns no handle when it would end past `i32::MAX`, which text
/// placed at the coordinate limit can reach.
fn text_resize_handle_rect(bounds: Rect) -> Option<Rect> {
let size = TEXT_RESIZE_HANDLE_SIZE;
let half = size / 2;
let center_x = bounds.x + bounds.width + TEXT_RESIZE_HANDLE_OFFSET;
let center_y = bounds.y + bounds.height + TEXT_RESIZE_HANDLE_OFFSET;
Rect::new(center_x - half, center_y - half, size, size)
let size = i64::from(TEXT_RESIZE_HANDLE_SIZE);
let inset = i64::from(TEXT_RESIZE_HANDLE_OFFSET) - size / 2;
let x = i64::from(bounds.x) + i64::from(bounds.width) + inset;
let y = i64::from(bounds.y) + i64::from(bounds.height) + inset;
let limit = i64::from(i32::MAX);
if x + size > limit || y + size > limit {
return None;
}

Rect::new(
i32::try_from(x).ok()?,
i32::try_from(y).ok()?,
TEXT_RESIZE_HANDLE_SIZE,
TEXT_RESIZE_HANDLE_SIZE,
)
}

pub(crate) fn selected_text_resize_handle_with(
Expand Down
Loading
Loading