1use std::{
2 any::Any,
3 fmt::{Debug, Formatter},
4 sync::Arc,
5};
6
7use crate::{
8 ActiveTheme, Icon, IconName, h_flex,
9 history::{History, HistoryItem},
10 scroll::{Scrollbar, ScrollbarShow},
11 v_flex,
12};
13
14use super::{
15 DockArea, Panel, PanelEvent, PanelInfo, PanelState, PanelView, StackPanel, TabPanel, TileMeta,
16};
17use gpui::{
18 AnyElement, App, AppContext, Bounds, Context, DismissEvent, Div, DragMoveEvent, Empty,
19 EntityId, EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement, MouseButton,
20 MouseDownEvent, MouseUpEvent, ParentElement, Pixels, Point, Render, ScrollHandle, Size,
21 StatefulInteractiveElement, Styled, WeakEntity, Window, actions, canvas, div,
22 prelude::FluentBuilder, px, size,
23};
24
25actions!(tiles, [Undo, Redo]);
26
27const MINIMUM_SIZE: Size<Pixels> = size(px(100.), px(100.));
28const DRAG_BAR_HEIGHT: Pixels = px(30.);
29const HANDLE_SIZE: Pixels = px(5.0);
30
31#[derive(Clone, PartialEq, Debug)]
32struct TileChange {
33 tile_id: EntityId,
34 old_bounds: Option<Bounds<Pixels>>,
35 new_bounds: Option<Bounds<Pixels>>,
36 old_order: Option<usize>,
37 new_order: Option<usize>,
38 version: usize,
39}
40
41impl HistoryItem for TileChange {
42 fn version(&self) -> usize {
43 self.version
44 }
45
46 fn set_version(&mut self, version: usize) {
47 self.version = version;
48 }
49}
50
51#[derive(Clone)]
52pub struct DragMoving(EntityId);
53impl Render for DragMoving {
54 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
55 Empty
56 }
57}
58
59#[derive(Clone, PartialEq)]
60enum ResizeSide {
61 Left,
62 Right,
63 Top,
64 Bottom,
65 BottomRight,
66}
67
68#[derive(Clone)]
69pub struct DragResizing(EntityId);
70
71impl Render for DragResizing {
72 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
73 Empty
74 }
75}
76
77#[derive(Clone)]
78struct ResizeDrag {
79 side: ResizeSide,
80 last_position: Point<Pixels>,
81 last_bounds: Bounds<Pixels>,
82}
83
84#[derive(Clone)]
86pub struct TileItem {
87 id: EntityId,
88 pub(crate) panel: Arc<dyn PanelView>,
89 bounds: Bounds<Pixels>,
90 z_index: usize,
91}
92
93impl Debug for TileItem {
94 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
95 f.debug_struct("TileItem")
96 .field("bounds", &self.bounds)
97 .field("z_index", &self.z_index)
98 .finish()
99 }
100}
101
102impl TileItem {
103 pub fn new(panel: Arc<dyn PanelView>, bounds: Bounds<Pixels>) -> Self {
104 Self {
105 id: panel.view().entity_id(),
106 panel,
107 bounds,
108 z_index: 0,
109 }
110 }
111
112 pub fn z_index(mut self, z_index: usize) -> Self {
113 self.z_index = z_index;
114 self
115 }
116}
117
118#[derive(Clone, Debug)]
119pub struct AnyDrag {
120 pub value: Arc<dyn Any>,
121}
122
123impl AnyDrag {
124 pub fn new(value: impl Any) -> Self {
125 Self {
126 value: Arc::new(value),
127 }
128 }
129}
130
131pub struct Tiles {
133 focus_handle: FocusHandle,
134 pub(crate) panels: Vec<TileItem>,
135 dragging_id: Option<EntityId>,
136 dragging_initial_mouse: Point<Pixels>,
137 dragging_initial_bounds: Bounds<Pixels>,
138 resizing_id: Option<EntityId>,
139 resizing_drag_data: Option<ResizeDrag>,
140 bounds: Bounds<Pixels>,
141 history: History<TileChange>,
142 scroll_handle: ScrollHandle,
143 scrollbar_show: Option<ScrollbarShow>,
144}
145
146impl Panel for Tiles {
147 fn panel_name(&self) -> &'static str {
148 "Tiles"
149 }
150
151 fn title(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
152 "Tiles".into_any_element()
153 }
154
155 fn dump(&self, cx: &App) -> PanelState {
156 let panels = self
157 .panels
158 .iter()
159 .map(|item: &TileItem| item.panel.dump(cx))
160 .collect();
161
162 let metas = self
163 .panels
164 .iter()
165 .map(|item: &TileItem| TileMeta {
166 bounds: item.bounds,
167 z_index: item.z_index,
168 })
169 .collect();
170
171 let mut state = PanelState::new(self);
172 state.panel_name = self.panel_name().to_string();
173 state.children = panels;
174 state.info = PanelInfo::Tiles { metas };
175 state
176 }
177}
178
179#[derive(Clone, Debug)]
180pub struct DragDrop(pub AnyDrag);
181
182impl EventEmitter<DragDrop> for Tiles {}
183
184impl Tiles {
185 pub fn new(_: &mut Window, cx: &mut Context<Self>) -> Self {
186 Self {
187 focus_handle: cx.focus_handle(),
188 panels: vec![],
189 dragging_id: None,
190 dragging_initial_mouse: Point::default(),
191 dragging_initial_bounds: Bounds::default(),
192 resizing_id: None,
193 scrollbar_show: None,
194 resizing_drag_data: None,
195 bounds: Bounds::default(),
196 history: History::new().group_interval(std::time::Duration::from_millis(100)),
197 scroll_handle: ScrollHandle::default(),
198 }
199 }
200
201 pub fn set_scrollbar_show(
203 &mut self,
204 scrollbar_show: Option<ScrollbarShow>,
205 cx: &mut Context<Self>,
206 ) {
207 self.scrollbar_show = scrollbar_show;
208 cx.notify();
209 }
210
211 pub fn panels(&self) -> &[TileItem] {
212 &self.panels
213 }
214
215 fn sorted_panels(&self) -> Vec<TileItem> {
216 let mut items: Vec<(usize, TileItem)> = self.panels.iter().cloned().enumerate().collect();
217 items.sort_by(|a, b| a.1.z_index.cmp(&b.1.z_index).then_with(|| a.0.cmp(&b.0)));
218 items.into_iter().map(|(_, item)| item).collect()
219 }
220
221 #[inline]
223 pub(crate) fn index_of(&self, id: &EntityId) -> Option<usize> {
224 self.panels.iter().position(|p| &p.id == id)
225 }
226
227 #[inline]
228 pub(crate) fn panel(&self, id: &EntityId) -> Option<&TileItem> {
229 self.panels.iter().find(|p| &p.id == id)
230 }
231
232 pub fn remove(&mut self, panel: Arc<dyn PanelView>, _: &mut Window, cx: &mut Context<Self>) {
234 if let Some(ix) = self.index_of(&panel.panel_id(cx)) {
235 self.panels.remove(ix);
236
237 cx.emit(PanelEvent::LayoutChanged);
238 }
239 }
240
241 fn calculate_magnetic_snap(
243 &self,
244 dragging_bounds: Bounds<Pixels>,
245 item_ix: usize,
246 snap_threshold: Pixels,
247 ) -> (Option<Pixels>, Option<Pixels>) {
248 let search_bounds = Bounds {
250 origin: Point {
251 x: dragging_bounds.left() - snap_threshold,
252 y: dragging_bounds.top() - snap_threshold,
253 },
254 size: Size {
255 width: dragging_bounds.size.width + snap_threshold * 2.0,
256 height: dragging_bounds.size.height + snap_threshold * 2.0,
257 },
258 };
259
260 let mut snap_x: Option<Pixels> = None;
261 let mut snap_y: Option<Pixels> = None;
262 let mut min_x_dist = snap_threshold;
263 let mut min_y_dist = snap_threshold;
264
265 let drag_left = dragging_bounds.left();
267 let drag_right = dragging_bounds.right();
268 let drag_top = dragging_bounds.top();
269 let drag_bottom = dragging_bounds.bottom();
270 let drag_width = dragging_bounds.size.width;
271 let drag_height = dragging_bounds.size.height;
272
273 let edge_snap_pos = px(0.);
275
276 let top_dist = drag_top.abs();
278 if top_dist < snap_threshold {
279 snap_y = Some(edge_snap_pos);
280 min_y_dist = top_dist;
281 }
282
283 let left_dist = drag_left.abs();
285 if left_dist < snap_threshold {
286 snap_x = Some(edge_snap_pos);
287 min_x_dist = left_dist;
288 }
289
290 if snap_x.is_some() && snap_y.is_some() {
292 return (snap_x, snap_y);
293 }
294
295 for (ix, other) in self.panels.iter().enumerate() {
296 if ix == item_ix {
297 continue;
298 }
299
300 let other_left = other.bounds.left();
302 let other_right = other.bounds.right();
303 let other_top = other.bounds.top();
304 let other_bottom = other.bounds.bottom();
305
306 if other_right < search_bounds.left()
308 || other_left > search_bounds.right()
309 || other_bottom < search_bounds.top()
310 || other_top > search_bounds.bottom()
311 {
312 continue;
313 }
314
315 if snap_x.is_none() {
317 let candidates = [
318 ((drag_left - other_left).abs(), other_left),
319 ((drag_left - other_right).abs(), other_right),
320 ((drag_right - other_left).abs(), other_left - drag_width),
321 ((drag_right - other_right).abs(), other_right - drag_width),
322 ];
323
324 for (dist, snap_pos) in candidates {
325 if dist < min_x_dist {
326 min_x_dist = dist;
327 snap_x = Some(snap_pos);
328 }
329 }
330 }
331
332 if snap_y.is_none() {
334 let candidates = [
335 ((drag_top - other_top).abs(), other_top),
336 ((drag_top - other_bottom).abs(), other_bottom),
337 ((drag_bottom - other_top).abs(), other_top - drag_height),
338 (
339 (drag_bottom - other_bottom).abs(),
340 other_bottom - drag_height,
341 ),
342 ];
343
344 for (dist, snap_pos) in candidates {
345 if dist < min_y_dist {
346 min_y_dist = dist;
347 snap_y = Some(snap_pos);
348 }
349 }
350 }
351
352 if snap_x.is_some() && snap_y.is_some() {
354 break;
355 }
356 }
357
358 (snap_x, snap_y)
359 }
360
361 fn apply_boundary_constraints(&self, mut origin: Point<Pixels>) -> Point<Pixels> {
363 if origin.y < px(0.) {
365 origin.y = px(0.);
366 }
367
368 let min_left = -self.dragging_initial_bounds.size.width + px(64.);
370 if origin.x < min_left {
371 origin.x = min_left;
372 }
373
374 origin
375 }
376
377 fn update_position(&mut self, mouse_position: Point<Pixels>, cx: &mut Context<Self>) {
378 let Some(dragging_id) = self.dragging_id else {
379 return;
380 };
381
382 let Some(item_ix) = self.panels.iter().position(|p| p.id == dragging_id) else {
383 return;
384 };
385
386 let previous_bounds = self.panels[item_ix].bounds;
387 let adjusted_position = mouse_position - self.bounds.origin;
388 let delta = adjusted_position - self.dragging_initial_mouse;
389 let mut new_origin = self.dragging_initial_bounds.origin + delta;
390
391 let snap_threshold = cx.theme().tile_grid_size;
393 let dragging_bounds = Bounds {
394 origin: new_origin,
395 size: self.dragging_initial_bounds.size,
396 };
397
398 let (snap_x, snap_y) =
399 self.calculate_magnetic_snap(dragging_bounds, item_ix, snap_threshold);
400
401 if let Some(x) = snap_x {
403 new_origin.x = x;
404 }
405 if let Some(y) = snap_y {
406 new_origin.y = y;
407 }
408
409 new_origin = self.apply_boundary_constraints(new_origin);
411
412 if new_origin != previous_bounds.origin {
414 self.panels[item_ix].bounds.origin = new_origin;
415 let item = &self.panels[item_ix];
416 let bounds = item.bounds;
417 let entity_id = item.panel.view().entity_id();
418
419 if !self.history.ignore {
420 self.history.push(TileChange {
421 tile_id: entity_id,
422 old_bounds: Some(previous_bounds),
423 new_bounds: Some(bounds),
424 old_order: None,
425 new_order: None,
426 version: 0,
427 });
428 }
429 cx.notify();
430 }
431 }
432
433 fn resize(
434 &mut self,
435 new_x: Option<Pixels>,
436 new_y: Option<Pixels>,
437 new_width: Option<Pixels>,
438 new_height: Option<Pixels>,
439 _: &mut Window,
440 cx: &mut Context<'_, Self>,
441 ) {
442 let Some(resizing_id) = self.resizing_id else {
443 return;
444 };
445 let Some(item) = self.panels.iter_mut().find(|item| item.id == resizing_id) else {
446 return;
447 };
448
449 let previous_bounds = item.bounds;
450 let final_x = if let Some(x) = new_x {
451 round_to_nearest_ten(x, cx)
452 } else {
453 previous_bounds.origin.x
454 };
455 let final_y = if let Some(y) = new_y {
456 round_to_nearest_ten(y, cx)
457 } else {
458 previous_bounds.origin.y
459 };
460 let final_width = if let Some(width) = new_width {
461 round_to_nearest_ten(width, cx)
462 } else {
463 previous_bounds.size.width
464 };
465
466 let final_height = if let Some(height) = new_height {
467 round_to_nearest_ten(height, cx)
468 } else {
469 previous_bounds.size.height
470 };
471
472 if final_width != item.bounds.size.width
474 || final_height != item.bounds.size.height
475 || final_x != item.bounds.origin.x
476 || final_y != item.bounds.origin.y
477 {
478 item.bounds.origin.x = final_x;
479 item.bounds.origin.y = final_y;
480 item.bounds.size.width = final_width;
481 item.bounds.size.height = final_height;
482
483 if !self.history.ignore {
485 self.history.push(TileChange {
486 tile_id: item.panel.view().entity_id(),
487 old_bounds: Some(previous_bounds),
488 new_bounds: Some(item.bounds),
489 old_order: None,
490 new_order: None,
491 version: 0,
492 });
493 }
494 }
495
496 cx.notify();
497 }
498
499 pub fn add_item(
500 &mut self,
501 item: TileItem,
502 dock_area: &WeakEntity<DockArea>,
503 window: &mut Window,
504 cx: &mut Context<Self>,
505 ) {
506 let Ok(tab_panel) = item.panel.view().downcast::<TabPanel>() else {
507 panic!("only allows to add TabPanel type")
508 };
509
510 tab_panel.update(cx, |tab_panel, _| {
511 tab_panel.set_in_tiles(true);
512 });
513
514 self.panels.push(item.clone());
515 window.defer(cx, {
516 let panel = item.panel.clone();
517 let dock_area = dock_area.clone();
518
519 move |window, cx| {
520 _ = dock_area.update(cx, |this, cx| {
522 if let Ok(tab_panel) = panel.view().downcast::<TabPanel>() {
523 this.subscribe_panel(&tab_panel, window, cx);
524 }
525 });
526 }
527 });
528
529 cx.emit(PanelEvent::LayoutChanged);
530 cx.notify();
531 }
532
533 #[inline]
534 fn reset_current_index(&mut self) {
535 self.dragging_id = None;
536 self.resizing_id = None;
537 }
538
539 fn bring_to_front(
541 &mut self,
542 target_id: Option<EntityId>,
543 cx: &mut Context<Self>,
544 ) -> Option<EntityId> {
545 let Some(old_id) = target_id else {
546 return None;
547 };
548
549 let old_ix = self.panels.iter().position(|item| item.id == old_id)?;
550 if old_ix < self.panels.len() {
551 let item = self.panels.remove(old_ix);
552 self.panels.push(item);
553 let new_ix = self.panels.len() - 1;
554 let new_id = self.panels[new_ix].id;
555 self.history.push(TileChange {
556 tile_id: new_id,
557 old_bounds: None,
558 new_bounds: None,
559 old_order: Some(old_ix),
560 new_order: Some(new_ix),
561 version: 0,
562 });
563 cx.notify();
564 return Some(new_id);
565 }
566 None
567 }
568
569 pub fn undo(&mut self, _: &mut Window, cx: &mut Context<Self>) {
571 self.history.ignore = true;
572
573 if let Some(changes) = self.history.undo() {
574 for change in changes {
575 if let Some(index) = self
576 .panels
577 .iter()
578 .position(|item| item.panel.view().entity_id() == change.tile_id)
579 {
580 if let Some(old_bounds) = change.old_bounds {
581 self.panels[index].bounds = old_bounds;
582 }
583 if let Some(old_order) = change.old_order {
584 let item = self.panels.remove(index);
585 self.panels.insert(old_order, item);
586 }
587 }
588 }
589 cx.emit(PanelEvent::LayoutChanged);
590 }
591
592 self.history.ignore = false;
593 cx.notify();
594 }
595
596 pub fn redo(&mut self, _: &mut Window, cx: &mut Context<Self>) {
598 self.history.ignore = true;
599
600 if let Some(changes) = self.history.redo() {
601 for change in changes {
602 if let Some(index) = self
603 .panels
604 .iter()
605 .position(|item| item.panel.view().entity_id() == change.tile_id)
606 {
607 if let Some(new_bounds) = change.new_bounds {
608 self.panels[index].bounds = new_bounds;
609 }
610 if let Some(new_order) = change.new_order {
611 let item = self.panels.remove(index);
612 self.panels.insert(new_order, item);
613 }
614 }
615 }
616 cx.emit(PanelEvent::LayoutChanged);
617 }
618
619 self.history.ignore = false;
620 cx.notify();
621 }
622
623 pub fn active_panel(&self, cx: &App) -> Option<Arc<dyn PanelView>> {
625 self.panels.last().and_then(|item| {
626 if let Ok(tab_panel) = item.panel.view().downcast::<TabPanel>() {
627 tab_panel.read(cx).active_panel(cx)
628 } else if let Ok(_) = item.panel.view().downcast::<StackPanel>() {
629 None
630 } else {
631 Some(item.panel.clone())
632 }
633 })
634 }
635
636 fn render_resize_handles(
638 &mut self,
639 _: &mut Window,
640 cx: &mut Context<Self>,
641 entity_id: EntityId,
642 item: &TileItem,
643 ) -> Vec<AnyElement> {
644 let item_id = item.id;
645 let item_bounds = item.bounds;
646 let handle_offset = -HANDLE_SIZE + px(1.);
647
648 let mut elements = Vec::new();
649
650 elements.push(
652 div()
653 .id("left-resize-handle")
654 .cursor_ew_resize()
655 .absolute()
656 .top_0()
657 .left(handle_offset)
658 .w(HANDLE_SIZE)
659 .h(item_bounds.size.height)
660 .on_mouse_down(
661 MouseButton::Left,
662 cx.listener({
663 move |this, event: &MouseDownEvent, window, cx| {
664 this.on_resize_handle_mouse_down(
665 ResizeSide::Left,
666 item_id,
667 item_bounds,
668 event,
669 window,
670 cx,
671 );
672 }
673 }),
674 )
675 .on_drag(DragResizing(entity_id), |drag, _, _, cx| {
676 cx.stop_propagation();
677 cx.new(|_| drag.clone())
678 })
679 .on_drag_move(cx.listener(
680 move |this, e: &DragMoveEvent<DragResizing>, window, cx| match e.drag(cx) {
681 DragResizing(id) => {
682 if *id != entity_id {
683 return;
684 }
685
686 let Some(ref drag_data) = this.resizing_drag_data else {
687 return;
688 };
689 if drag_data.side != ResizeSide::Left {
690 return;
691 }
692
693 let pos = e.event.position;
694 let delta = drag_data.last_position.x - pos.x;
695 let new_x = (drag_data.last_bounds.origin.x - delta).max(px(0.0));
696 let size_delta = drag_data.last_bounds.origin.x - new_x;
697 let new_width = (drag_data.last_bounds.size.width + size_delta)
698 .max(MINIMUM_SIZE.width);
699 this.resize(Some(new_x), None, Some(new_width), None, window, cx);
700 }
701 },
702 ))
703 .into_any_element(),
704 );
705
706 elements.push(
708 div()
709 .id("right-resize-handle")
710 .cursor_ew_resize()
711 .absolute()
712 .top_0()
713 .right(handle_offset)
714 .w(HANDLE_SIZE)
715 .h(item_bounds.size.height)
716 .on_mouse_down(
717 MouseButton::Left,
718 cx.listener({
719 move |this, event: &MouseDownEvent, window, cx| {
720 this.on_resize_handle_mouse_down(
721 ResizeSide::Right,
722 item_id,
723 item_bounds,
724 event,
725 window,
726 cx,
727 );
728 }
729 }),
730 )
731 .on_drag(DragResizing(entity_id), |drag, _, _, cx| {
732 cx.stop_propagation();
733 cx.new(|_| drag.clone())
734 })
735 .on_drag_move(cx.listener(
736 move |this, e: &DragMoveEvent<DragResizing>, window, cx| match e.drag(cx) {
737 DragResizing(id) => {
738 if *id != entity_id {
739 return;
740 }
741
742 let Some(ref drag_data) = this.resizing_drag_data else {
743 return;
744 };
745
746 if drag_data.side != ResizeSide::Right {
747 return;
748 }
749
750 let pos = e.event.position;
751 let delta = pos.x - drag_data.last_position.x;
752 let new_width =
753 (drag_data.last_bounds.size.width + delta).max(MINIMUM_SIZE.width);
754 this.resize(None, None, Some(new_width), None, window, cx);
755 }
756 },
757 ))
758 .into_any_element(),
759 );
760
761 elements.push(
763 div()
764 .id("top-resize-handle")
765 .cursor_ns_resize()
766 .absolute()
767 .left(px(0.0))
768 .top(handle_offset)
769 .w(item_bounds.size.width)
770 .h(HANDLE_SIZE)
771 .on_mouse_down(
772 MouseButton::Left,
773 cx.listener({
774 move |this, event: &MouseDownEvent, window, cx| {
775 this.on_resize_handle_mouse_down(
776 ResizeSide::Top,
777 item_id,
778 item_bounds,
779 event,
780 window,
781 cx,
782 );
783 }
784 }),
785 )
786 .on_drag(DragResizing(entity_id), |drag, _, _, cx| {
787 cx.stop_propagation();
788 cx.new(|_| drag.clone())
789 })
790 .on_drag_move(cx.listener(
791 move |this, e: &DragMoveEvent<DragResizing>, window, cx| match e.drag(cx) {
792 DragResizing(id) => {
793 if *id != entity_id {
794 return;
795 }
796
797 let Some(ref drag_data) = this.resizing_drag_data else {
798 return;
799 };
800 if drag_data.side != ResizeSide::Top {
801 return;
802 }
803
804 let pos = e.event.position;
805 let delta = drag_data.last_position.y - pos.y;
806 let new_y = (drag_data.last_bounds.origin.y - delta).max(px(0.));
807 let size_delta = drag_data.last_position.y - new_y;
808 let new_height = (drag_data.last_bounds.size.height + size_delta)
809 .max(MINIMUM_SIZE.width);
810 this.resize(None, Some(new_y), None, Some(new_height), window, cx);
811 }
812 },
813 ))
814 .into_any_element(),
815 );
816
817 elements.push(
819 div()
820 .id("bottom-resize-handle")
821 .cursor_ns_resize()
822 .absolute()
823 .left(px(0.0))
824 .bottom(handle_offset)
825 .w(item_bounds.size.width)
826 .h(HANDLE_SIZE)
827 .on_mouse_down(
828 MouseButton::Left,
829 cx.listener({
830 move |this, event: &MouseDownEvent, window, cx| {
831 this.on_resize_handle_mouse_down(
832 ResizeSide::Bottom,
833 item_id,
834 item_bounds,
835 event,
836 window,
837 cx,
838 );
839 }
840 }),
841 )
842 .on_drag(DragResizing(entity_id), |drag, _, _, cx| {
843 cx.stop_propagation();
844 cx.new(|_| drag.clone())
845 })
846 .on_drag_move(cx.listener(
847 move |this, e: &DragMoveEvent<DragResizing>, window, cx| match e.drag(cx) {
848 DragResizing(id) => {
849 if *id != entity_id {
850 return;
851 }
852
853 let Some(ref drag_data) = this.resizing_drag_data else {
854 return;
855 };
856
857 if drag_data.side != ResizeSide::Bottom {
858 return;
859 }
860
861 let pos = e.event.position;
862 let delta = pos.y - drag_data.last_position.y;
863 let new_height =
864 (drag_data.last_bounds.size.height + delta).max(MINIMUM_SIZE.width);
865 this.resize(None, None, None, Some(new_height), window, cx);
866 }
867 },
868 ))
869 .into_any_element(),
870 );
871
872 elements.push(
874 div()
875 .child(
876 Icon::new(IconName::ResizeCorner)
877 .size_3()
878 .absolute()
879 .right(px(1.))
880 .bottom(px(1.))
881 .text_color(cx.theme().muted_foreground.opacity(0.5)),
882 )
883 .child(
884 div()
885 .id("corner-resize-handle")
886 .cursor_nwse_resize()
887 .absolute()
888 .right(handle_offset)
889 .bottom(handle_offset)
890 .size_3()
891 .on_mouse_down(
892 MouseButton::Left,
893 cx.listener({
894 move |this, event: &MouseDownEvent, window, cx| {
895 this.on_resize_handle_mouse_down(
896 ResizeSide::BottomRight,
897 item_id,
898 item_bounds,
899 event,
900 window,
901 cx,
902 );
903 }
904 }),
905 )
906 .on_drag(DragResizing(entity_id), |drag, _, _, cx| {
907 cx.stop_propagation();
908 cx.new(|_| drag.clone())
909 })
910 .on_drag_move(cx.listener(
911 move |this, e: &DragMoveEvent<DragResizing>, window, cx| {
912 match e.drag(cx) {
913 DragResizing(id) => {
914 if *id != entity_id {
915 return;
916 }
917
918 let Some(ref drag_data) = this.resizing_drag_data else {
919 return;
920 };
921
922 if drag_data.side != ResizeSide::BottomRight {
923 return;
924 }
925
926 let pos = e.event.position;
927 let delta_x = pos.x - drag_data.last_position.x;
928 let delta_y = pos.y - drag_data.last_position.y;
929 let new_width = (drag_data.last_bounds.size.width
930 + delta_x)
931 .max(MINIMUM_SIZE.width);
932 let new_height = (drag_data.last_bounds.size.height
933 + delta_y)
934 .max(MINIMUM_SIZE.height);
935 this.resize(
936 None,
937 None,
938 Some(new_width),
939 Some(new_height),
940 window,
941 cx,
942 );
943 }
944 }
945 },
946 )),
947 )
948 .into_any_element(),
949 );
950
951 elements
952 }
953
954 fn on_resize_handle_mouse_down(
955 &mut self,
956 side: ResizeSide,
957 item_id: EntityId,
958 item_bounds: Bounds<Pixels>,
959 event: &MouseDownEvent,
960 _: &mut Window,
961 cx: &mut Context<'_, Self>,
962 ) {
963 let last_position = event.position;
964 self.resizing_id = Some(item_id);
965 self.resizing_drag_data = Some(ResizeDrag {
966 side,
967 last_position,
968 last_bounds: item_bounds,
969 });
970
971 if let Some(new_id) = self.bring_to_front(self.resizing_id, cx) {
972 self.resizing_id = Some(new_id);
973 }
974 cx.stop_propagation();
975 }
976
977 fn render_drag_bar(
979 &mut self,
980 _: &mut Window,
981 cx: &mut Context<Self>,
982 entity_id: EntityId,
983 item: &TileItem,
984 ) -> AnyElement {
985 let item_id = item.id;
986 let item_bounds = item.bounds;
987
988 h_flex()
989 .id("drag-bar")
990 .absolute()
991 .w_full()
992 .h(DRAG_BAR_HEIGHT)
993 .bg(cx.theme().transparent)
994 .on_mouse_down(
995 MouseButton::Left,
996 cx.listener(move |this, event: &MouseDownEvent, _, cx| {
997 let inner_pos = event.position - this.bounds.origin;
998 this.dragging_id = Some(item_id);
999 this.dragging_initial_mouse = inner_pos;
1000 this.dragging_initial_bounds = item_bounds;
1001
1002 if let Some(new_id) = this.bring_to_front(Some(item_id), cx) {
1003 this.dragging_id = Some(new_id);
1004 }
1005 }),
1006 )
1007 .on_drag(DragMoving(entity_id), |drag, _, _, cx| {
1008 cx.stop_propagation();
1009 cx.new(|_| drag.clone())
1010 })
1011 .on_drag_move(
1012 cx.listener(
1013 move |this, e: &DragMoveEvent<DragMoving>, _, cx| match e.drag(cx) {
1014 DragMoving(id) => {
1015 if *id != entity_id {
1016 return;
1017 }
1018 this.update_position(e.event.position, cx);
1019 }
1020 },
1021 ),
1022 )
1023 .into_any_element()
1024 }
1025
1026 fn render_panel(
1027 &mut self,
1028 item: &TileItem,
1029 window: &mut Window,
1030 cx: &mut Context<Self>,
1031 ) -> Div {
1032 let entity_id = cx.entity_id();
1033 let item_id = item.id;
1034 let panel_view = item.panel.view();
1035
1036 v_flex()
1037 .occlude()
1038 .bg(cx.theme().background)
1039 .border_1()
1040 .border_color(cx.theme().border)
1041 .absolute()
1042 .left(item.bounds.origin.x)
1043 .top(item.bounds.origin.y)
1044 .w(item.bounds.size.width + px(1.))
1046 .h(item.bounds.size.height + px(1.))
1047 .rounded(cx.theme().tile_radius)
1048 .child(h_flex().overflow_hidden().size_full().child(panel_view))
1049 .children(self.render_resize_handles(window, cx, entity_id, &item))
1050 .child(self.render_drag_bar(window, cx, entity_id, &item))
1051 .on_mouse_down(
1052 MouseButton::Left,
1053 cx.listener(move |this, _, _, _| {
1054 this.dragging_id = Some(item_id);
1055 }),
1056 )
1057 .on_mouse_up(
1059 MouseButton::Left,
1060 cx.listener(move |this, _, _, cx| {
1061 if this.dragging_id == Some(item_id) {
1062 this.dragging_id = None;
1063 this.bring_to_front(Some(item_id), cx);
1064 }
1065 }),
1066 )
1067 }
1068
1069 fn on_mouse_up(&mut self, _: &mut Window, cx: &mut Context<'_, Tiles>) {
1071 if self.dragging_id.is_some()
1073 || self.resizing_id.is_some()
1074 || self.resizing_drag_data.is_some()
1075 {
1076 let mut changes_to_push = vec![];
1077
1078 if let Some(dragging_id) = self.dragging_id {
1080 if let Some(idx) = self.panels.iter().position(|p| p.id == dragging_id) {
1081 let initial_bounds = self.dragging_initial_bounds;
1082 let current_bounds = self.panels[idx].bounds;
1083
1084 let aligned_origin = round_point_to_nearest_ten(current_bounds.origin, cx);
1086
1087 if initial_bounds.origin != aligned_origin
1088 || initial_bounds.size != current_bounds.size
1089 {
1090 self.panels[idx].bounds.origin = aligned_origin;
1091
1092 changes_to_push.push(TileChange {
1093 tile_id: self.panels[idx].panel.view().entity_id(),
1094 old_bounds: Some(initial_bounds),
1095 new_bounds: Some(self.panels[idx].bounds),
1096 old_order: None,
1097 new_order: None,
1098 version: 0,
1099 });
1100 }
1101 }
1102 }
1103
1104 if let Some(resizing_id) = self.resizing_id {
1106 if let Some(drag_data) = &self.resizing_drag_data {
1107 if let Some(item) = self.panel(&resizing_id) {
1108 let initial_bounds = drag_data.last_bounds;
1109 let current_bounds = item.bounds;
1110 if initial_bounds.size != current_bounds.size {
1111 changes_to_push.push(TileChange {
1112 tile_id: item.panel.view().entity_id(),
1113 old_bounds: Some(initial_bounds),
1114 new_bounds: Some(current_bounds),
1115 old_order: None,
1116 new_order: None,
1117 version: 0,
1118 });
1119 }
1120 }
1121 }
1122 }
1123
1124 if !changes_to_push.is_empty() {
1126 for change in changes_to_push {
1127 self.history.push(change);
1128 }
1129 }
1130
1131 self.reset_current_index();
1133 self.resizing_drag_data = None;
1134 cx.emit(PanelEvent::LayoutChanged);
1135 cx.notify();
1136 }
1137 }
1138}
1139
1140#[inline]
1141fn round_to_nearest_ten(value: Pixels, cx: &App) -> Pixels {
1142 (value / cx.theme().tile_grid_size).round() * cx.theme().tile_grid_size
1143}
1144
1145#[inline]
1146fn round_point_to_nearest_ten(point: Point<Pixels>, cx: &App) -> Point<Pixels> {
1147 Point::new(
1148 round_to_nearest_ten(point.x, cx),
1149 round_to_nearest_ten(point.y, cx),
1150 )
1151}
1152
1153impl Focusable for Tiles {
1154 fn focus_handle(&self, _cx: &App) -> FocusHandle {
1155 self.focus_handle.clone()
1156 }
1157}
1158impl EventEmitter<PanelEvent> for Tiles {}
1159impl EventEmitter<DismissEvent> for Tiles {}
1160impl Render for Tiles {
1161 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1162 let view = cx.entity().clone();
1163 let panels = self.sorted_panels();
1164 let scroll_bounds =
1165 self.panels
1166 .iter()
1167 .fold(Bounds::default(), |acc: Bounds<Pixels>, item| Bounds {
1168 origin: Point {
1169 x: acc.origin.x.min(item.bounds.origin.x),
1170 y: acc.origin.y.min(item.bounds.origin.y),
1171 },
1172 size: Size {
1173 width: acc.size.width.max(item.bounds.right()),
1174 height: acc.size.height.max(item.bounds.bottom()),
1175 },
1176 });
1177 let scroll_size = scroll_bounds.size - size(scroll_bounds.origin.x, scroll_bounds.origin.y);
1178
1179 div()
1180 .relative()
1181 .bg(cx.theme().tiles)
1182 .child(
1183 div()
1184 .id("tiles")
1185 .track_scroll(&self.scroll_handle)
1186 .size_full()
1187 .top(-px(1.))
1188 .left(-px(1.))
1189 .overflow_scroll()
1190 .children(
1191 panels
1192 .into_iter()
1193 .map(|item| self.render_panel(&item, window, cx)),
1194 )
1195 .child({
1196 canvas(
1197 move |bounds, _, cx| view.update(cx, |r, _| r.bounds = bounds),
1198 |_, _, _, _| {},
1199 )
1200 .absolute()
1201 .size_full()
1202 })
1203 .on_drop(cx.listener(move |_, item: &AnyDrag, _, cx| {
1204 cx.emit(DragDrop(item.clone()));
1205 })),
1206 )
1207 .on_mouse_up(
1208 MouseButton::Left,
1209 cx.listener(move |this, _event: &MouseUpEvent, window, cx| {
1210 this.on_mouse_up(window, cx);
1211 }),
1212 )
1213 .child(
1214 div()
1215 .absolute()
1216 .top_0()
1217 .left_0()
1218 .right_0()
1219 .bottom_0()
1220 .child(
1221 Scrollbar::new(&self.scroll_handle)
1222 .scroll_size(scroll_size)
1223 .when_some(self.scrollbar_show, |this, scrollbar_show| {
1224 this.scrollbar_show(scrollbar_show)
1225 }),
1226 ),
1227 )
1228 .size_full()
1229 }
1230}