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
39 changes: 26 additions & 13 deletions src/input/state/core/selection_actions/translation/bounds.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,16 +77,23 @@ impl InputState {
}
}

fn clamp_axis_delta(position: i32, size: i32, screen: i32, delta: i32) -> i32 {
if screen <= 0 || size <= 0 {
fn clamp_axis_delta(position: i32, size: i32, start: i32, end_limit: i32, delta: i32) -> i32 {
if end_limit <= start || size <= 0 {
return delta;
}

let extent = end_limit.saturating_sub(start);
let end = position.saturating_add(size);
let (min_delta, max_delta) = if size <= screen {
(0i32.saturating_sub(position), screen.saturating_sub(end))
let (min_delta, max_delta) = if size <= extent {
(
start.saturating_sub(position),
end_limit.saturating_sub(end),
)
} else {
(screen.saturating_sub(end), 0i32.saturating_sub(position))
(
end_limit.saturating_sub(end),
start.saturating_sub(position),
)
};

delta.clamp(min_delta, max_delta)
Expand All @@ -100,18 +107,24 @@ impl InputState {
) -> Option<(i32, i32)> {
let bounds = self.movable_selection_bounds(measurer)?;
let (screen_width, screen_height) = self.view.screen_size();
let screen_width = screen_width.min(i32::MAX as u32) as i32;
let screen_height = screen_height.min(i32::MAX as u32) as i32;
// Shape bounds are canvas coordinates, so the limit has to be the
// visible canvas area: board pan and zoom both move the screen rect
// away from the canvas origin. Without a known screen size there is
// nothing to clamp against.
let visible = self.view.visible_canvas_rect(self.board_view_offset());
let (start_x, start_y) = (visible.x, visible.y);
let end_x = visible.x.saturating_add(visible.width);
let end_y = visible.y.saturating_add(visible.height);

let clamped_dx = if dx == 0 {
0
let clamped_dx = if dx == 0 || screen_width == 0 {
dx
} else {
Self::clamp_axis_delta(bounds.x, bounds.width, screen_width, dx)
Self::clamp_axis_delta(bounds.x, bounds.width, start_x, end_x, dx)
};
let clamped_dy = if dy == 0 {
0
let clamped_dy = if dy == 0 || screen_height == 0 {
dy
} else {
Self::clamp_axis_delta(bounds.y, bounds.height, screen_height, dy)
Self::clamp_axis_delta(bounds.y, bounds.height, start_y, end_y, dy)
};

Some((clamped_dx, clamped_dy))
Expand Down
2 changes: 1 addition & 1 deletion src/input/state/core/utility/interaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use crate::util::Rect;
use std::time::Instant;

impl InputState {
fn board_view_offset(&self) -> (f64, f64) {
pub(crate) fn board_view_offset(&self) -> (f64, f64) {
if self.board_is_transparent() || !self.boards.pan_enabled() {
(0.0, 0.0)
} else {
Expand Down
87 changes: 87 additions & 0 deletions src/input/state/tests/transform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -555,3 +555,90 @@ fn stretching_a_flat_curved_arrow_downward_grows_its_arc() {
other => panic!("expected arrow, got {other:?}"),
}
}

#[test]
fn moving_a_selection_on_a_panned_board_follows_the_pointer() {
let measurer = crate::draw::TextMeasurer::default();
let mut state = create_test_input_state();
state.update_screen_dimensions(1920, 1080);
assert!(state.boards.switch_to_id(crate::input::BOARD_ID_BLACKBOARD));
// Screen (0, 0) is canvas (-3306, 93) on this board.
state.boards.active_frame_mut().set_view_offset(-3306, 93);
let shape_id = state.boards.active_frame_mut().add_shape(Shape::Rect {
x: -3200,
y: 193,
w: 480,
h: 100,
fill: false,
color: state.style.current_color,
thick: state.style.current_thickness,
});
state.set_selection(vec![shape_id]);

let before = state
.boards
.active_frame()
.shape(shape_id)
.unwrap()
.bounding_box()
.expect("rect should have bounds");

assert!(state.apply_translation_to_selection_with(&measurer, 5, 0));

let after = state
.boards
.active_frame()
.shape(shape_id)
.unwrap()
.bounding_box()
.expect("rect should have bounds");
assert_eq!(
after.x - before.x,
5,
"a 5px drag must move the selection by 5 canvas px, not teleport it to the canvas origin"
);
}

#[test]
fn moving_a_selection_at_zoom_keeps_it_inside_the_visible_canvas() {
let measurer = crate::draw::TextMeasurer::default();
let mut state = create_test_input_state();
state.update_screen_dimensions(1920, 1080);
assert!(state.boards.switch_to_id(crate::input::BOARD_ID_BLACKBOARD));
state.view.set_zoom_status(true, false, 4.0, (0.0, 0.0));
// At 4x the visible canvas is 480x270. The 400px-wide selection carries
// ~2px of line-width bounds padding, so it may slide 78px before its right
// edge reaches the visible canvas edge.
let shape_id = state.boards.active_frame_mut().add_shape(Shape::Rect {
x: 0,
y: 0,
w: 400,
h: 100,
fill: false,
color: state.style.current_color,
thick: state.style.current_thickness,
});
state.set_selection(vec![shape_id]);

let before = state
.boards
.active_frame()
.shape(shape_id)
.unwrap()
.bounding_box()
.expect("rect should have bounds");
state.apply_translation_to_selection_with(&measurer, 300, 0);
let after = state
.boards
.active_frame()
.shape(shape_id)
.unwrap()
.bounding_box()
.expect("rect should have bounds");

assert_eq!(
after.x - before.x,
78,
"the selection must stop at the visible canvas edge (480 canvas px at 4x), not at the screen width"
);
}