1use gpui::{Bounds, EntityId, Pixels, Point, Size, px, size};
10
11pub const MINIMUM_SIZE: Size<Pixels> = size(px(100.), px(100.));
15
16pub const DRAG_BAR_HEIGHT: Pixels = px(30.);
20
21pub const HANDLE_SIZE: Pixels = px(5.0);
24
25#[derive(Clone, PartialEq, Debug)]
30pub struct TileChange {
31 tile_id: EntityId,
32 old_bounds: Option<Bounds<Pixels>>,
33 new_bounds: Option<Bounds<Pixels>>,
34}
35
36impl TileChange {
37 pub fn bounds_change(
39 tile_id: EntityId,
40 old_bounds: Bounds<Pixels>,
41 new_bounds: Bounds<Pixels>,
42 ) -> Self {
43 Self {
44 tile_id,
45 old_bounds: Some(old_bounds),
46 new_bounds: Some(new_bounds),
47 }
48 }
49
50 pub fn tile_id(&self) -> EntityId {
51 self.tile_id
52 }
53
54 pub fn old_bounds(&self) -> Option<Bounds<Pixels>> {
55 self.old_bounds
56 }
57
58 pub fn new_bounds(&self) -> Option<Bounds<Pixels>> {
59 self.new_bounds
60 }
61}
62
63#[derive(Clone, Copy, PartialEq, Eq, Debug)]
65pub enum ResizeSide {
66 Left,
67 Right,
68 Top,
69 Bottom,
70 BottomRight,
71}
72
73#[derive(Clone, Copy, Debug)]
82pub struct ResizeDrag {
83 side: ResizeSide,
84 start_position: Point<Pixels>,
85 last_bounds: Bounds<Pixels>,
86}
87
88impl ResizeDrag {
89 pub fn new(
90 side: ResizeSide,
91 start_position: Point<Pixels>,
92 last_bounds: Bounds<Pixels>,
93 ) -> Self {
94 Self {
95 side,
96 start_position,
97 last_bounds,
98 }
99 }
100
101 pub fn side(&self) -> ResizeSide {
102 self.side
103 }
104
105 pub fn start_position(&self) -> Point<Pixels> {
106 self.start_position
107 }
108
109 pub fn last_bounds(&self) -> Bounds<Pixels> {
110 self.last_bounds
111 }
112
113 pub fn with_last_bounds(mut self, last_bounds: Bounds<Pixels>) -> Self {
114 self.last_bounds = last_bounds;
115 self
116 }
117}
118
119pub fn snap_edge(edge: Pixels, candidates: &[Pixels], threshold: Pixels) -> Option<Pixels> {
122 let mut best: Option<Pixels> = None;
123 let mut best_dist = threshold;
124 for &candidate in candidates {
125 let dist = (edge - candidate).abs();
126 if dist < best_dist {
127 best_dist = dist;
128 best = Some(candidate);
129 }
130 }
131 best
132}
133
134pub fn compute_resized_bounds(
145 previous: Bounds<Pixels>,
146 new_x: Option<Pixels>,
147 new_y: Option<Pixels>,
148 new_width: Option<Pixels>,
149 new_height: Option<Pixels>,
150 other_bounds: &[Bounds<Pixels>],
151 grid_size: Pixels,
152) -> Bounds<Pixels> {
153 let mut x_edges = Vec::with_capacity(other_bounds.len() * 2);
155 let mut y_edges = Vec::with_capacity(other_bounds.len() * 2);
156 for bounds in other_bounds {
157 x_edges.push(bounds.left());
158 x_edges.push(bounds.right());
159 y_edges.push(bounds.top());
160 y_edges.push(bounds.bottom());
161 }
162
163 let prev_right = previous.origin.x + previous.size.width;
164 let prev_bottom = previous.origin.y + previous.size.height;
165
166 let (final_x, final_width) = if let Some(x) = new_x {
168 let raw_left = x.max(px(0.));
170 let mut candidates = x_edges.clone();
171 candidates.push(px(0.));
172 let snapped_left = snap_edge(raw_left, &candidates, grid_size)
173 .unwrap_or_else(|| round_to_grid(raw_left, grid_size));
174 let width = (prev_right - snapped_left).max(MINIMUM_SIZE.width);
175 (snapped_left, width)
176 } else if let Some(width) = new_width {
177 let raw_right = previous.origin.x + width;
179 let snapped_right = snap_edge(raw_right, &x_edges, grid_size)
180 .unwrap_or_else(|| round_to_grid(raw_right, grid_size));
181 let width = (snapped_right - previous.origin.x).max(MINIMUM_SIZE.width);
182 (previous.origin.x, width)
183 } else {
184 (previous.origin.x, previous.size.width)
185 };
186
187 let (final_y, final_height) = if let Some(y) = new_y {
189 let raw_top = y.max(px(0.));
191 let mut candidates = y_edges.clone();
192 candidates.push(px(0.));
193 let snapped_top = snap_edge(raw_top, &candidates, grid_size)
194 .unwrap_or_else(|| round_to_grid(raw_top, grid_size));
195 let height = (prev_bottom - snapped_top).max(MINIMUM_SIZE.height);
196 (snapped_top, height)
197 } else if let Some(height) = new_height {
198 let raw_bottom = previous.origin.y + height;
200 let snapped_bottom = snap_edge(raw_bottom, &y_edges, grid_size)
201 .unwrap_or_else(|| round_to_grid(raw_bottom, grid_size));
202 let height = (snapped_bottom - previous.origin.y).max(MINIMUM_SIZE.height);
203 (previous.origin.y, height)
204 } else {
205 (previous.origin.y, previous.size.height)
206 };
207
208 Bounds {
209 origin: Point {
210 x: final_x,
211 y: final_y,
212 },
213 size: Size {
214 width: final_width,
215 height: final_height,
216 },
217 }
218}
219
220pub fn round_to_grid(value: Pixels, grid_size: Pixels) -> Pixels {
230 (value / grid_size).round() * grid_size
231}
232
233pub fn magnetic_snap(
241 moving: Bounds<Pixels>,
242 others: &[Bounds<Pixels>],
243 threshold: Pixels,
244) -> Point<Pixels> {
245 let search_bounds = Bounds {
247 origin: Point {
248 x: moving.left() - threshold,
249 y: moving.top() - threshold,
250 },
251 size: Size {
252 width: moving.size.width + threshold * 2.0,
253 height: moving.size.height + threshold * 2.0,
254 },
255 };
256
257 let mut snap_x: Option<Pixels> = None;
258 let mut snap_y: Option<Pixels> = None;
259 let mut min_x_dist = threshold;
260 let mut min_y_dist = threshold;
261
262 let drag_left = moving.left();
264 let drag_right = moving.right();
265 let drag_top = moving.top();
266 let drag_bottom = moving.bottom();
267 let drag_width = moving.size.width;
268 let drag_height = moving.size.height;
269
270 let edge_snap_pos = px(0.);
272
273 let top_dist = drag_top.abs();
275 if top_dist < threshold {
276 snap_y = Some(edge_snap_pos);
277 min_y_dist = top_dist;
278 }
279
280 let left_dist = drag_left.abs();
282 if left_dist < threshold {
283 snap_x = Some(edge_snap_pos);
284 min_x_dist = left_dist;
285 }
286
287 if snap_x.is_none() || snap_y.is_none() {
289 for other in others {
290 if snap_x.is_some() && snap_y.is_some() {
291 break;
292 }
293
294 let other_left = other.left();
296 let other_right = other.right();
297 let other_top = other.top();
298 let other_bottom = other.bottom();
299
300 if other_right < search_bounds.left()
302 || other_left > search_bounds.right()
303 || other_bottom < search_bounds.top()
304 || other_top > search_bounds.bottom()
305 {
306 continue;
307 }
308
309 if snap_x.is_none() {
311 let candidates = [
312 ((drag_left - other_left).abs(), other_left),
313 ((drag_left - other_right).abs(), other_right),
314 ((drag_right - other_left).abs(), other_left - drag_width),
315 ((drag_right - other_right).abs(), other_right - drag_width),
316 ];
317
318 for (dist, snap_pos) in candidates {
319 if dist < min_x_dist {
320 min_x_dist = dist;
321 snap_x = Some(snap_pos);
322 }
323 }
324 }
325
326 if snap_y.is_none() {
328 let candidates = [
329 ((drag_top - other_top).abs(), other_top),
330 ((drag_top - other_bottom).abs(), other_bottom),
331 ((drag_bottom - other_top).abs(), other_top - drag_height),
332 (
333 (drag_bottom - other_bottom).abs(),
334 other_bottom - drag_height,
335 ),
336 ];
337
338 for (dist, snap_pos) in candidates {
339 if dist < min_y_dist {
340 min_y_dist = dist;
341 snap_y = Some(snap_pos);
342 }
343 }
344 }
345 }
346 }
347
348 Point {
349 x: snap_x.unwrap_or(moving.origin.x),
350 y: snap_y.unwrap_or(moving.origin.y),
351 }
352}
353
354pub fn apply_boundary_constraints(origin: Point<Pixels>, dragging_width: Pixels) -> Point<Pixels> {
366 let mut origin = origin;
367
368 if origin.y < px(0.) {
370 origin.y = px(0.);
371 }
372
373 let min_left = -dragging_width + px(64.);
375 if origin.x < min_left {
376 origin.x = min_left;
377 }
378
379 origin
380}
381
382pub fn content_size(tiles: &[Bounds<Pixels>]) -> Size<Pixels> {
391 let mut left = px(0.);
392 let mut top = px(0.);
393 let mut right = px(0.);
394 let mut bottom = px(0.);
395 for bounds in tiles {
396 left = left.min(bounds.left());
397 top = top.min(bounds.top());
398 right = right.max(bounds.right());
399 bottom = bottom.max(bounds.bottom());
400 }
401 size(right - left, bottom - top)
402}
403
404#[cfg(test)]
405mod tests {
406 use super::*;
407
408 fn b(x: f32, y: f32, w: f32, h: f32) -> Bounds<Pixels> {
409 Bounds {
410 origin: Point { x: px(x), y: px(y) },
411 size: Size {
412 width: px(w),
413 height: px(h),
414 },
415 }
416 }
417
418 #[test]
419 fn test_snap_edge_within_threshold() {
420 assert_eq!(
422 snap_edge(px(102.), &[px(100.), px(300.)], px(8.)),
423 Some(px(100.))
424 );
425 }
426
427 #[test]
428 fn test_snap_edge_outside_threshold() {
429 assert_eq!(snap_edge(px(120.), &[px(100.), px(300.)], px(8.)), None);
431 }
432
433 #[test]
434 fn test_snap_edge_picks_nearest() {
435 assert_eq!(
437 snap_edge(px(303.), &[px(308.), px(300.)], px(8.)),
438 Some(px(300.))
439 );
440 }
441
442 #[test]
443 fn test_snap_edge_empty_candidates() {
444 assert_eq!(snap_edge(px(50.), &[], px(8.)), None);
445 }
446
447 #[test]
448 fn test_resize_right_edge_snaps_to_neighbor_left() {
449 let prev = b(0., 0., 196., 100.);
452 let neighbor = b(200., 0., 100., 100.);
453 let out =
454 compute_resized_bounds(prev, None, None, Some(px(197.)), None, &[neighbor], px(8.));
455 assert_eq!(out.origin.x, px(0.));
456 assert_eq!(out.size.width, px(200.));
457 }
458
459 #[test]
460 fn test_resize_bottom_edge_snaps_to_neighbor_top() {
461 let prev = b(0., 0., 100., 196.);
462 let neighbor = b(0., 200., 100., 100.);
463 let out =
464 compute_resized_bounds(prev, None, None, None, Some(px(197.)), &[neighbor], px(8.));
465 assert_eq!(out.origin.y, px(0.));
466 assert_eq!(out.size.height, px(200.));
467 }
468
469 #[test]
470 fn test_resize_left_edge_snaps_and_pins_right() {
471 let prev = b(200., 0., 100., 100.);
474 let neighbor = b(0., 0., 100., 100.);
475 let out = compute_resized_bounds(
476 prev,
477 Some(px(103.)),
478 None,
479 Some(px(197.)),
480 None,
481 &[neighbor],
482 px(8.),
483 );
484 assert_eq!(out.origin.x, px(100.));
485 assert_eq!(out.size.width, px(200.));
486 }
487
488 #[test]
489 fn test_resize_corner_snaps_both_edges() {
490 let prev = b(0., 0., 196., 196.);
492 let right_neighbor = b(100., 0., 200., 100.); let bottom_neighbor = b(0., 100., 100., 150.); let out = compute_resized_bounds(
495 prev,
496 None,
497 None,
498 Some(px(298.)),
499 Some(px(248.)),
500 &[right_neighbor, bottom_neighbor],
501 px(8.),
502 );
503 assert_eq!(out.size.width, px(300.));
504 assert_eq!(out.size.height, px(250.));
505 }
506
507 #[test]
508 fn test_resize_grid_rounds_when_no_neighbor_close() {
509 let prev = b(0., 0., 100., 100.);
511 let out = compute_resized_bounds(prev, None, None, Some(px(153.)), None, &[], px(8.));
512 assert_eq!(out.size.width, px(152.));
513 }
514
515 #[test]
516 fn test_resize_respects_minimum_size() {
517 let prev = b(0., 0., 100., 100.);
518 let out = compute_resized_bounds(prev, None, None, Some(px(10.)), None, &[], px(8.));
519 assert_eq!(out.size.width, MINIMUM_SIZE.width);
520 }
521
522 #[test]
523 fn content_size_spans_from_the_origin_to_the_far_edge() {
524 assert_eq!(
525 content_size(&[b(20., 20., 380., 280.), b(420., 20., 380., 280.)]),
526 size(px(800.), px(300.)),
527 "the extent runs from the canvas origin, not from the first tile"
528 );
529 assert_eq!(
530 content_size(&[]),
531 size(px(0.), px(0.)),
532 "an empty canvas scrolls nowhere"
533 );
534 assert_eq!(
535 content_size(&[b(-40., -10., 100., 100.)]),
536 size(px(100.), px(100.)),
537 "a tile dragged past the origin still reports the distance across it"
538 );
539 }
540
541 #[test]
542 fn test_resize_no_change_returns_previous_geometry() {
543 let prev = b(0., 0., 100., 100.);
544 let out = compute_resized_bounds(prev, None, None, None, None, &[], px(8.));
545 assert_eq!(out.origin.x, px(0.));
546 assert_eq!(out.origin.y, px(0.));
547 assert_eq!(out.size.width, px(100.));
548 assert_eq!(out.size.height, px(100.));
549 }
550}