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