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 for (ix, other) in self.panels.iter().enumerate() {
276 if ix == item_ix {
277 continue;
278 }
279
280 let other_left = other.bounds.left();
282 let other_right = other.bounds.right();
283 let other_top = other.bounds.top();
284 let other_bottom = other.bounds.bottom();
285
286 if other_right < search_bounds.left()
288 || other_left > search_bounds.right()
289 || other_bottom < search_bounds.top()
290 || other_top > search_bounds.bottom()
291 {
292 continue;
293 }
294
295 if snap_x.is_none() {
297 let candidates = [
298 ((drag_left - other_left).abs(), other_left),
299 ((drag_left - other_right).abs(), other_right),
300 ((drag_right - other_left).abs(), other_left - drag_width),
301 ((drag_right - other_right).abs(), other_right - drag_width),
302 ];
303
304 for (dist, snap_pos) in candidates {
305 if dist < min_x_dist {
306 min_x_dist = dist;
307 snap_x = Some(snap_pos);
308 }
309 }
310 }
311
312 if snap_y.is_none() {
314 let candidates = [
315 ((drag_top - other_top).abs(), other_top),
316 ((drag_top - other_bottom).abs(), other_bottom),
317 ((drag_bottom - other_top).abs(), other_top - drag_height),
318 (
319 (drag_bottom - other_bottom).abs(),
320 other_bottom - drag_height,
321 ),
322 ];
323
324 for (dist, snap_pos) in candidates {
325 if dist < min_y_dist {
326 min_y_dist = dist;
327 snap_y = Some(snap_pos);
328 }
329 }
330 }
331
332 if snap_x.is_some() && snap_y.is_some() {
334 break;
335 }
336 }
337
338 (snap_x, snap_y)
339 }
340
341 fn apply_boundary_constraints(&self, mut origin: Point<Pixels>) -> Point<Pixels> {
343 if origin.y < px(0.) {
345 origin.y = px(0.);
346 }
347
348 let min_left = -self.dragging_initial_bounds.size.width + px(64.);
350 if origin.x < min_left {
351 origin.x = min_left;
352 }
353
354 origin
355 }
356
357 fn update_position(&mut self, mouse_position: Point<Pixels>, cx: &mut Context<Self>) {
358 let Some(dragging_id) = self.dragging_id else {
359 return;
360 };
361
362 let Some(item_ix) = self.panels.iter().position(|p| p.id == dragging_id) else {
363 return;
364 };
365
366 let previous_bounds = self.panels[item_ix].bounds;
367 let adjusted_position = mouse_position - self.bounds.origin;
368 let delta = adjusted_position - self.dragging_initial_mouse;
369 let mut new_origin = self.dragging_initial_bounds.origin + delta;
370
371 let snap_threshold = cx.theme().tile_grid_size;
373 let dragging_bounds = Bounds {
374 origin: new_origin,
375 size: self.dragging_initial_bounds.size,
376 };
377
378 let (snap_x, snap_y) =
379 self.calculate_magnetic_snap(dragging_bounds, item_ix, snap_threshold);
380
381 if let Some(x) = snap_x {
383 new_origin.x = x;
384 }
385 if let Some(y) = snap_y {
386 new_origin.y = y;
387 }
388
389 new_origin = self.apply_boundary_constraints(new_origin);
391
392 if new_origin != previous_bounds.origin {
394 self.panels[item_ix].bounds.origin = new_origin;
395 let item = &self.panels[item_ix];
396 let bounds = item.bounds;
397 let entity_id = item.panel.view().entity_id();
398
399 if !self.history.ignore {
400 self.history.push(TileChange {
401 tile_id: entity_id,
402 old_bounds: Some(previous_bounds),
403 new_bounds: Some(bounds),
404 old_order: None,
405 new_order: None,
406 version: 0,
407 });
408 }
409 cx.notify();
410 }
411 }
412
413 fn resize(
414 &mut self,
415 new_x: Option<Pixels>,
416 new_y: Option<Pixels>,
417 new_width: Option<Pixels>,
418 new_height: Option<Pixels>,
419 _: &mut Window,
420 cx: &mut Context<'_, Self>,
421 ) {
422 let Some(resizing_id) = self.resizing_id else {
423 return;
424 };
425 let Some(item) = self.panels.iter_mut().find(|item| item.id == resizing_id) else {
426 return;
427 };
428
429 let previous_bounds = item.bounds;
430 let final_x = if let Some(x) = new_x {
431 round_to_nearest_ten(x, cx)
432 } else {
433 previous_bounds.origin.x
434 };
435 let final_y = if let Some(y) = new_y {
436 round_to_nearest_ten(y, cx)
437 } else {
438 previous_bounds.origin.y
439 };
440 let final_width = if let Some(width) = new_width {
441 round_to_nearest_ten(width, cx)
442 } else {
443 previous_bounds.size.width
444 };
445
446 let final_height = if let Some(height) = new_height {
447 round_to_nearest_ten(height, cx)
448 } else {
449 previous_bounds.size.height
450 };
451
452 if final_width != item.bounds.size.width
454 || final_height != item.bounds.size.height
455 || final_x != item.bounds.origin.x
456 || final_y != item.bounds.origin.y
457 {
458 item.bounds.origin.x = final_x;
459 item.bounds.origin.y = final_y;
460 item.bounds.size.width = final_width;
461 item.bounds.size.height = final_height;
462
463 if !self.history.ignore {
465 self.history.push(TileChange {
466 tile_id: item.panel.view().entity_id(),
467 old_bounds: Some(previous_bounds),
468 new_bounds: Some(item.bounds),
469 old_order: None,
470 new_order: None,
471 version: 0,
472 });
473 }
474 }
475
476 cx.notify();
477 }
478
479 pub fn add_item(
480 &mut self,
481 item: TileItem,
482 dock_area: &WeakEntity<DockArea>,
483 window: &mut Window,
484 cx: &mut Context<Self>,
485 ) {
486 let Ok(tab_panel) = item.panel.view().downcast::<TabPanel>() else {
487 panic!("only allows to add TabPanel type")
488 };
489
490 tab_panel.update(cx, |tab_panel, _| {
491 tab_panel.set_in_tiles(true);
492 });
493
494 self.panels.push(item.clone());
495 window.defer(cx, {
496 let panel = item.panel.clone();
497 let dock_area = dock_area.clone();
498
499 move |window, cx| {
500 _ = dock_area.update(cx, |this, cx| {
502 if let Ok(tab_panel) = panel.view().downcast::<TabPanel>() {
503 this.subscribe_panel(&tab_panel, window, cx);
504 }
505 });
506 }
507 });
508
509 cx.emit(PanelEvent::LayoutChanged);
510 cx.notify();
511 }
512
513 #[inline]
514 fn reset_current_index(&mut self) {
515 self.dragging_id = None;
516 self.resizing_id = None;
517 }
518
519 fn bring_to_front(
521 &mut self,
522 target_id: Option<EntityId>,
523 cx: &mut Context<Self>,
524 ) -> Option<EntityId> {
525 let Some(old_id) = target_id else {
526 return None;
527 };
528
529 let old_ix = self.panels.iter().position(|item| item.id == old_id)?;
530 if old_ix < self.panels.len() {
531 let item = self.panels.remove(old_ix);
532 self.panels.push(item);
533 let new_ix = self.panels.len() - 1;
534 let new_id = self.panels[new_ix].id;
535 self.history.push(TileChange {
536 tile_id: new_id,
537 old_bounds: None,
538 new_bounds: None,
539 old_order: Some(old_ix),
540 new_order: Some(new_ix),
541 version: 0,
542 });
543 cx.notify();
544 return Some(new_id);
545 }
546 None
547 }
548
549 pub fn undo(&mut self, _: &mut Window, cx: &mut Context<Self>) {
551 self.history.ignore = true;
552
553 if let Some(changes) = self.history.undo() {
554 for change in changes {
555 if let Some(index) = self
556 .panels
557 .iter()
558 .position(|item| item.panel.view().entity_id() == change.tile_id)
559 {
560 if let Some(old_bounds) = change.old_bounds {
561 self.panels[index].bounds = old_bounds;
562 }
563 if let Some(old_order) = change.old_order {
564 let item = self.panels.remove(index);
565 self.panels.insert(old_order, item);
566 }
567 }
568 }
569 cx.emit(PanelEvent::LayoutChanged);
570 }
571
572 self.history.ignore = false;
573 cx.notify();
574 }
575
576 pub fn redo(&mut self, _: &mut Window, cx: &mut Context<Self>) {
578 self.history.ignore = true;
579
580 if let Some(changes) = self.history.redo() {
581 for change in changes {
582 if let Some(index) = self
583 .panels
584 .iter()
585 .position(|item| item.panel.view().entity_id() == change.tile_id)
586 {
587 if let Some(new_bounds) = change.new_bounds {
588 self.panels[index].bounds = new_bounds;
589 }
590 if let Some(new_order) = change.new_order {
591 let item = self.panels.remove(index);
592 self.panels.insert(new_order, item);
593 }
594 }
595 }
596 cx.emit(PanelEvent::LayoutChanged);
597 }
598
599 self.history.ignore = false;
600 cx.notify();
601 }
602
603 pub fn active_panel(&self, cx: &App) -> Option<Arc<dyn PanelView>> {
605 self.panels.last().and_then(|item| {
606 if let Ok(tab_panel) = item.panel.view().downcast::<TabPanel>() {
607 tab_panel.read(cx).active_panel(cx)
608 } else if let Ok(_) = item.panel.view().downcast::<StackPanel>() {
609 None
610 } else {
611 Some(item.panel.clone())
612 }
613 })
614 }
615
616 fn render_resize_handles(
618 &mut self,
619 _: &mut Window,
620 cx: &mut Context<Self>,
621 entity_id: EntityId,
622 item: &TileItem,
623 ) -> Vec<AnyElement> {
624 let item_id = item.id;
625 let item_bounds = item.bounds;
626 let handle_offset = -HANDLE_SIZE + px(1.);
627
628 let mut elements = Vec::new();
629
630 elements.push(
632 div()
633 .id("left-resize-handle")
634 .cursor_ew_resize()
635 .absolute()
636 .top_0()
637 .left(handle_offset)
638 .w(HANDLE_SIZE)
639 .h(item_bounds.size.height)
640 .on_mouse_down(
641 MouseButton::Left,
642 cx.listener({
643 move |this, event: &MouseDownEvent, window, cx| {
644 this.on_resize_handle_mouse_down(
645 ResizeSide::Left,
646 item_id,
647 item_bounds,
648 event,
649 window,
650 cx,
651 );
652 }
653 }),
654 )
655 .on_drag(DragResizing(entity_id), |drag, _, _, cx| {
656 cx.stop_propagation();
657 cx.new(|_| drag.clone())
658 })
659 .on_drag_move(cx.listener(
660 move |this, e: &DragMoveEvent<DragResizing>, window, cx| match e.drag(cx) {
661 DragResizing(id) => {
662 if *id != entity_id {
663 return;
664 }
665
666 let Some(ref drag_data) = this.resizing_drag_data else {
667 return;
668 };
669 if drag_data.side != ResizeSide::Left {
670 return;
671 }
672
673 let pos = e.event.position;
674 let delta = drag_data.last_position.x - pos.x;
675 let new_x = (drag_data.last_bounds.origin.x - delta).max(px(0.0));
676 let size_delta = drag_data.last_bounds.origin.x - new_x;
677 let new_width = (drag_data.last_bounds.size.width + size_delta)
678 .max(MINIMUM_SIZE.width);
679 this.resize(Some(new_x), None, Some(new_width), None, window, cx);
680 }
681 },
682 ))
683 .into_any_element(),
684 );
685
686 elements.push(
688 div()
689 .id("right-resize-handle")
690 .cursor_ew_resize()
691 .absolute()
692 .top_0()
693 .right(handle_offset)
694 .w(HANDLE_SIZE)
695 .h(item_bounds.size.height)
696 .on_mouse_down(
697 MouseButton::Left,
698 cx.listener({
699 move |this, event: &MouseDownEvent, window, cx| {
700 this.on_resize_handle_mouse_down(
701 ResizeSide::Right,
702 item_id,
703 item_bounds,
704 event,
705 window,
706 cx,
707 );
708 }
709 }),
710 )
711 .on_drag(DragResizing(entity_id), |drag, _, _, cx| {
712 cx.stop_propagation();
713 cx.new(|_| drag.clone())
714 })
715 .on_drag_move(cx.listener(
716 move |this, e: &DragMoveEvent<DragResizing>, window, cx| match e.drag(cx) {
717 DragResizing(id) => {
718 if *id != entity_id {
719 return;
720 }
721
722 let Some(ref drag_data) = this.resizing_drag_data else {
723 return;
724 };
725
726 if drag_data.side != ResizeSide::Right {
727 return;
728 }
729
730 let pos = e.event.position;
731 let delta = pos.x - drag_data.last_position.x;
732 let new_width =
733 (drag_data.last_bounds.size.width + delta).max(MINIMUM_SIZE.width);
734 this.resize(None, None, Some(new_width), None, window, cx);
735 }
736 },
737 ))
738 .into_any_element(),
739 );
740
741 elements.push(
743 div()
744 .id("top-resize-handle")
745 .cursor_ns_resize()
746 .absolute()
747 .left(px(0.0))
748 .top(handle_offset)
749 .w(item_bounds.size.width)
750 .h(HANDLE_SIZE)
751 .on_mouse_down(
752 MouseButton::Left,
753 cx.listener({
754 move |this, event: &MouseDownEvent, window, cx| {
755 this.on_resize_handle_mouse_down(
756 ResizeSide::Top,
757 item_id,
758 item_bounds,
759 event,
760 window,
761 cx,
762 );
763 }
764 }),
765 )
766 .on_drag(DragResizing(entity_id), |drag, _, _, cx| {
767 cx.stop_propagation();
768 cx.new(|_| drag.clone())
769 })
770 .on_drag_move(cx.listener(
771 move |this, e: &DragMoveEvent<DragResizing>, window, cx| match e.drag(cx) {
772 DragResizing(id) => {
773 if *id != entity_id {
774 return;
775 }
776
777 let Some(ref drag_data) = this.resizing_drag_data else {
778 return;
779 };
780 if drag_data.side != ResizeSide::Top {
781 return;
782 }
783
784 let pos = e.event.position;
785 let delta = drag_data.last_position.y - pos.y;
786 let new_y = (drag_data.last_bounds.origin.y - delta).max(px(0.));
787 let size_delta = drag_data.last_position.y - new_y;
788 let new_height = (drag_data.last_bounds.size.height + size_delta)
789 .max(MINIMUM_SIZE.width);
790 this.resize(None, Some(new_y), None, Some(new_height), window, cx);
791 }
792 },
793 ))
794 .into_any_element(),
795 );
796
797 elements.push(
799 div()
800 .id("bottom-resize-handle")
801 .cursor_ns_resize()
802 .absolute()
803 .left(px(0.0))
804 .bottom(handle_offset)
805 .w(item_bounds.size.width)
806 .h(HANDLE_SIZE)
807 .on_mouse_down(
808 MouseButton::Left,
809 cx.listener({
810 move |this, event: &MouseDownEvent, window, cx| {
811 this.on_resize_handle_mouse_down(
812 ResizeSide::Bottom,
813 item_id,
814 item_bounds,
815 event,
816 window,
817 cx,
818 );
819 }
820 }),
821 )
822 .on_drag(DragResizing(entity_id), |drag, _, _, cx| {
823 cx.stop_propagation();
824 cx.new(|_| drag.clone())
825 })
826 .on_drag_move(cx.listener(
827 move |this, e: &DragMoveEvent<DragResizing>, window, cx| match e.drag(cx) {
828 DragResizing(id) => {
829 if *id != entity_id {
830 return;
831 }
832
833 let Some(ref drag_data) = this.resizing_drag_data else {
834 return;
835 };
836
837 if drag_data.side != ResizeSide::Bottom {
838 return;
839 }
840
841 let pos = e.event.position;
842 let delta = pos.y - drag_data.last_position.y;
843 let new_height =
844 (drag_data.last_bounds.size.height + delta).max(MINIMUM_SIZE.width);
845 this.resize(None, None, None, Some(new_height), window, cx);
846 }
847 },
848 ))
849 .into_any_element(),
850 );
851
852 elements.push(
854 div()
855 .child(
856 Icon::new(IconName::ResizeCorner)
857 .size_3()
858 .absolute()
859 .right(px(1.))
860 .bottom(px(1.))
861 .text_color(cx.theme().muted_foreground.opacity(0.5)),
862 )
863 .child(
864 div()
865 .id("corner-resize-handle")
866 .cursor_nwse_resize()
867 .absolute()
868 .right(handle_offset)
869 .bottom(handle_offset)
870 .size_3()
871 .on_mouse_down(
872 MouseButton::Left,
873 cx.listener({
874 move |this, event: &MouseDownEvent, window, cx| {
875 this.on_resize_handle_mouse_down(
876 ResizeSide::BottomRight,
877 item_id,
878 item_bounds,
879 event,
880 window,
881 cx,
882 );
883 }
884 }),
885 )
886 .on_drag(DragResizing(entity_id), |drag, _, _, cx| {
887 cx.stop_propagation();
888 cx.new(|_| drag.clone())
889 })
890 .on_drag_move(cx.listener(
891 move |this, e: &DragMoveEvent<DragResizing>, window, cx| {
892 match e.drag(cx) {
893 DragResizing(id) => {
894 if *id != entity_id {
895 return;
896 }
897
898 let Some(ref drag_data) = this.resizing_drag_data else {
899 return;
900 };
901
902 if drag_data.side != ResizeSide::BottomRight {
903 return;
904 }
905
906 let pos = e.event.position;
907 let delta_x = pos.x - drag_data.last_position.x;
908 let delta_y = pos.y - drag_data.last_position.y;
909 let new_width = (drag_data.last_bounds.size.width
910 + delta_x)
911 .max(MINIMUM_SIZE.width);
912 let new_height = (drag_data.last_bounds.size.height
913 + delta_y)
914 .max(MINIMUM_SIZE.height);
915 this.resize(
916 None,
917 None,
918 Some(new_width),
919 Some(new_height),
920 window,
921 cx,
922 );
923 }
924 }
925 },
926 )),
927 )
928 .into_any_element(),
929 );
930
931 elements
932 }
933
934 fn on_resize_handle_mouse_down(
935 &mut self,
936 side: ResizeSide,
937 item_id: EntityId,
938 item_bounds: Bounds<Pixels>,
939 event: &MouseDownEvent,
940 _: &mut Window,
941 cx: &mut Context<'_, Self>,
942 ) {
943 let last_position = event.position;
944 self.resizing_id = Some(item_id);
945 self.resizing_drag_data = Some(ResizeDrag {
946 side,
947 last_position,
948 last_bounds: item_bounds,
949 });
950
951 if let Some(new_id) = self.bring_to_front(self.resizing_id, cx) {
952 self.resizing_id = Some(new_id);
953 }
954 cx.stop_propagation();
955 }
956
957 fn render_drag_bar(
959 &mut self,
960 _: &mut Window,
961 cx: &mut Context<Self>,
962 entity_id: EntityId,
963 item: &TileItem,
964 ) -> AnyElement {
965 let item_id = item.id;
966 let item_bounds = item.bounds;
967
968 h_flex()
969 .id("drag-bar")
970 .absolute()
971 .w_full()
972 .h(DRAG_BAR_HEIGHT)
973 .bg(cx.theme().transparent)
974 .on_mouse_down(
975 MouseButton::Left,
976 cx.listener(move |this, event: &MouseDownEvent, _, cx| {
977 let inner_pos = event.position - this.bounds.origin;
978 this.dragging_id = Some(item_id);
979 this.dragging_initial_mouse = inner_pos;
980 this.dragging_initial_bounds = item_bounds;
981
982 if let Some(new_id) = this.bring_to_front(Some(item_id), cx) {
983 this.dragging_id = Some(new_id);
984 }
985 }),
986 )
987 .on_drag(DragMoving(entity_id), |drag, _, _, cx| {
988 cx.stop_propagation();
989 cx.new(|_| drag.clone())
990 })
991 .on_drag_move(
992 cx.listener(
993 move |this, e: &DragMoveEvent<DragMoving>, _, cx| match e.drag(cx) {
994 DragMoving(id) => {
995 if *id != entity_id {
996 return;
997 }
998 this.update_position(e.event.position, cx);
999 }
1000 },
1001 ),
1002 )
1003 .into_any_element()
1004 }
1005
1006 fn render_panel(
1007 &mut self,
1008 item: &TileItem,
1009 window: &mut Window,
1010 cx: &mut Context<Self>,
1011 ) -> impl IntoElement {
1012 let entity_id = cx.entity_id();
1013 let item_id = item.id;
1014 let panel_view = item.panel.view();
1015
1016 v_flex()
1017 .occlude()
1018 .bg(cx.theme().background)
1019 .border_1()
1020 .border_color(cx.theme().border)
1021 .absolute()
1022 .left(item.bounds.origin.x)
1023 .top(item.bounds.origin.y)
1024 .w(item.bounds.size.width + px(1.))
1026 .h(item.bounds.size.height + px(1.))
1027 .rounded(cx.theme().tile_radius)
1028 .child(h_flex().overflow_hidden().size_full().child(panel_view))
1029 .children(self.render_resize_handles(window, cx, entity_id, &item))
1030 .child(self.render_drag_bar(window, cx, entity_id, &item))
1031 .on_mouse_down(
1032 MouseButton::Left,
1033 cx.listener(move |this, _, _, _| {
1034 this.dragging_id = Some(item_id);
1035 }),
1036 )
1037 .on_mouse_up(
1039 MouseButton::Left,
1040 cx.listener(move |this, _, _, cx| {
1041 if this.dragging_id == Some(item_id) {
1042 this.dragging_id = None;
1043 this.bring_to_front(Some(item_id), cx);
1044 }
1045 }),
1046 )
1047 }
1048
1049 fn on_mouse_up(&mut self, _: &mut Window, cx: &mut Context<'_, Tiles>) {
1051 if self.dragging_id.is_some()
1053 || self.resizing_id.is_some()
1054 || self.resizing_drag_data.is_some()
1055 {
1056 let mut changes_to_push = vec![];
1057
1058 if let Some(dragging_id) = self.dragging_id {
1060 if let Some(idx) = self.panels.iter().position(|p| p.id == dragging_id) {
1061 let initial_bounds = self.dragging_initial_bounds;
1062 let current_bounds = self.panels[idx].bounds;
1063
1064 let aligned_origin = round_point_to_nearest_ten(current_bounds.origin, cx);
1066
1067 if initial_bounds.origin != aligned_origin
1068 || initial_bounds.size != current_bounds.size
1069 {
1070 self.panels[idx].bounds.origin = aligned_origin;
1071
1072 changes_to_push.push(TileChange {
1073 tile_id: self.panels[idx].panel.view().entity_id(),
1074 old_bounds: Some(initial_bounds),
1075 new_bounds: Some(self.panels[idx].bounds),
1076 old_order: None,
1077 new_order: None,
1078 version: 0,
1079 });
1080 }
1081 }
1082 }
1083
1084 if let Some(resizing_id) = self.resizing_id {
1086 if let Some(drag_data) = &self.resizing_drag_data {
1087 if let Some(item) = self.panel(&resizing_id) {
1088 let initial_bounds = drag_data.last_bounds;
1089 let current_bounds = item.bounds;
1090 if initial_bounds.size != current_bounds.size {
1091 changes_to_push.push(TileChange {
1092 tile_id: item.panel.view().entity_id(),
1093 old_bounds: Some(initial_bounds),
1094 new_bounds: Some(current_bounds),
1095 old_order: None,
1096 new_order: None,
1097 version: 0,
1098 });
1099 }
1100 }
1101 }
1102 }
1103
1104 if !changes_to_push.is_empty() {
1106 for change in changes_to_push {
1107 self.history.push(change);
1108 }
1109 }
1110
1111 self.reset_current_index();
1113 self.resizing_drag_data = None;
1114 cx.emit(PanelEvent::LayoutChanged);
1115 cx.notify();
1116 }
1117 }
1118}
1119
1120#[inline]
1121fn round_to_nearest_ten(value: Pixels, cx: &App) -> Pixels {
1122 (value / cx.theme().tile_grid_size).round() * cx.theme().tile_grid_size
1123}
1124
1125#[inline]
1126fn round_point_to_nearest_ten(point: Point<Pixels>, cx: &App) -> Point<Pixels> {
1127 Point::new(
1128 round_to_nearest_ten(point.x, cx),
1129 round_to_nearest_ten(point.y, cx),
1130 )
1131}
1132
1133impl Focusable for Tiles {
1134 fn focus_handle(&self, _cx: &App) -> FocusHandle {
1135 self.focus_handle.clone()
1136 }
1137}
1138impl EventEmitter<PanelEvent> for Tiles {}
1139impl EventEmitter<DismissEvent> for Tiles {}
1140impl Render for Tiles {
1141 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1142 let view = cx.entity().clone();
1143 let panels = self.sorted_panels();
1144 let scroll_bounds =
1145 self.panels
1146 .iter()
1147 .fold(Bounds::default(), |acc: Bounds<Pixels>, item| Bounds {
1148 origin: Point {
1149 x: acc.origin.x.min(item.bounds.origin.x),
1150 y: acc.origin.y.min(item.bounds.origin.y),
1151 },
1152 size: Size {
1153 width: acc.size.width.max(item.bounds.right()),
1154 height: acc.size.height.max(item.bounds.bottom()),
1155 },
1156 });
1157 let scroll_size = scroll_bounds.size - size(scroll_bounds.origin.x, scroll_bounds.origin.y);
1158
1159 div()
1160 .relative()
1161 .bg(cx.theme().tiles)
1162 .child(
1163 div()
1164 .id("tiles")
1165 .track_scroll(&self.scroll_handle)
1166 .size_full()
1167 .top(-px(1.))
1168 .overflow_scroll()
1169 .children(
1170 panels
1171 .into_iter()
1172 .map(|item| self.render_panel(&item, window, cx)),
1173 )
1174 .child({
1175 canvas(
1176 move |bounds, _, cx| view.update(cx, |r, _| r.bounds = bounds),
1177 |_, _, _, _| {},
1178 )
1179 .absolute()
1180 .size_full()
1181 })
1182 .on_drop(cx.listener(move |_, item: &AnyDrag, _, cx| {
1183 cx.emit(DragDrop(item.clone()));
1184 })),
1185 )
1186 .on_mouse_up(
1187 MouseButton::Left,
1188 cx.listener(move |this, _event: &MouseUpEvent, window, cx| {
1189 this.on_mouse_up(window, cx);
1190 }),
1191 )
1192 .child(
1193 div()
1194 .absolute()
1195 .top_0()
1196 .left_0()
1197 .right_0()
1198 .bottom_0()
1199 .child(
1200 Scrollbar::both(&self.scroll_state, &self.scroll_handle)
1201 .scroll_size(scroll_size)
1202 .when_some(self.scrollbar_show, |this, scrollbar_show| {
1203 this.scrollbar_show(scrollbar_show)
1204 }),
1205 ),
1206 )
1207 .size_full()
1208 }
1209}