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, 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, point, px, size, AnyElement, App, AppContext, Bounds, Context,
19 DismissEvent, DragMoveEvent, Empty, EntityId, EventEmitter, FocusHandle, Focusable, Half,
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 pub(crate) panel: Arc<dyn PanelView>,
88 bounds: Bounds<Pixels>,
89 z_index: usize,
90}
91
92impl Debug for TileItem {
93 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
94 f.debug_struct("TileItem")
95 .field("bounds", &self.bounds)
96 .field("z_index", &self.z_index)
97 .finish()
98 }
99}
100
101impl TileItem {
102 pub fn new(panel: Arc<dyn PanelView>, bounds: Bounds<Pixels>) -> Self {
103 Self {
104 panel,
105 bounds,
106 z_index: 0,
107 }
108 }
109
110 pub fn z_index(mut self, z_index: usize) -> Self {
111 self.z_index = z_index;
112 self
113 }
114}
115
116#[derive(Clone, Debug)]
117pub struct AnyDrag {
118 pub value: Arc<dyn Any>,
119}
120
121impl AnyDrag {
122 pub fn new(value: impl Any) -> Self {
123 Self {
124 value: Arc::new(value),
125 }
126 }
127}
128
129pub struct Tiles {
131 focus_handle: FocusHandle,
132 pub(crate) panels: Vec<TileItem>,
133 dragging_index: Option<usize>,
134 dragging_initial_mouse: Point<Pixels>,
135 dragging_initial_bounds: Bounds<Pixels>,
136 resizing_index: Option<usize>,
137 resizing_drag_data: Option<ResizeDrag>,
138 bounds: Bounds<Pixels>,
139 history: History<TileChange>,
140 scroll_state: ScrollbarState,
141 scroll_handle: ScrollHandle,
142}
143
144impl Panel for Tiles {
145 fn panel_name(&self) -> &'static str {
146 "Tiles"
147 }
148
149 fn title(&self, _window: &Window, _cx: &App) -> AnyElement {
150 "Tiles".into_any_element()
151 }
152
153 fn dump(&self, cx: &App) -> PanelState {
154 let panels = self
155 .panels
156 .iter()
157 .map(|item: &TileItem| item.panel.dump(cx))
158 .collect();
159
160 let metas = self
161 .panels
162 .iter()
163 .map(|item: &TileItem| TileMeta {
164 bounds: item.bounds,
165 z_index: item.z_index,
166 })
167 .collect();
168
169 let mut state = PanelState::new(self);
170 state.panel_name = self.panel_name().to_string();
171 state.children = panels;
172 state.info = PanelInfo::Tiles { metas };
173 state
174 }
175}
176
177#[derive(Clone, Debug)]
178pub struct DragDrop(pub AnyDrag);
179
180impl EventEmitter<DragDrop> for Tiles {}
181
182impl Tiles {
183 pub fn new(_: &mut Window, cx: &mut Context<Self>) -> Self {
184 Self {
185 focus_handle: cx.focus_handle(),
186 panels: vec![],
187 dragging_index: None,
188 dragging_initial_mouse: Point::default(),
189 dragging_initial_bounds: Bounds::default(),
190 resizing_index: None,
191 resizing_drag_data: None,
192 bounds: Bounds::default(),
193 history: History::new().group_interval(std::time::Duration::from_millis(100)),
194 scroll_state: ScrollbarState::default(),
195 scroll_handle: ScrollHandle::default(),
196 }
197 }
198
199 pub fn panels(&self) -> &[TileItem] {
200 &self.panels
201 }
202
203 fn sorted_panels(&self) -> Vec<TileItem> {
204 let mut items: Vec<(usize, TileItem)> = self.panels.iter().cloned().enumerate().collect();
205 items.sort_by(|a, b| a.1.z_index.cmp(&b.1.z_index).then_with(|| a.0.cmp(&b.0)));
206 items.into_iter().map(|(_, item)| item).collect()
207 }
208
209 #[inline]
211 pub(crate) fn index_of(&self, panel: Arc<dyn PanelView>) -> Option<usize> {
212 self.panels.iter().position(|p| &p.panel == &panel)
213 }
214
215 pub fn remove(&mut self, panel: Arc<dyn PanelView>, _: &mut Window, cx: &mut Context<Self>) {
217 if let Some(ix) = self.index_of(panel.clone()) {
218 self.panels.remove(ix);
219
220 cx.emit(PanelEvent::LayoutChanged);
221 }
222 }
223
224 fn update_initial_position(
225 &mut self,
226 position: Point<Pixels>,
227 _: &mut Window,
228 cx: &mut Context<'_, Self>,
229 ) {
230 let Some((index, item)) = self.find_at_position(position) else {
231 return;
232 };
233
234 let inner_pos = position - self.bounds.origin;
235 let bounds = item.bounds;
236 self.dragging_index = Some(index);
237 self.dragging_initial_mouse = inner_pos;
238 self.dragging_initial_bounds = bounds;
239 cx.notify();
240 }
241
242 fn update_position(
243 &mut self,
244 mouse_position: Point<Pixels>,
245 _: &mut Window,
246 cx: &mut Context<'_, Self>,
247 ) {
248 let Some(index) = self.dragging_index else {
249 return;
250 };
251
252 let Some(item) = self.panels.get_mut(index) else {
253 return;
254 };
255
256 let previous_bounds = item.bounds;
257 let adjusted_position = mouse_position - self.bounds.origin;
258 let delta = adjusted_position - self.dragging_initial_mouse;
259 let mut new_origin = self.dragging_initial_bounds.origin + delta;
260
261 if new_origin.y < px(0.) {
263 new_origin.y = px(0.);
264 }
265 let min_left = -self.dragging_initial_bounds.size.width + px(64.);
266 if new_origin.x < min_left {
267 new_origin.x = min_left;
268 }
269
270 let final_origin = round_point_to_nearest_ten(new_origin, cx);
271 if final_origin != previous_bounds.origin {
273 item.bounds.origin = final_origin;
274
275 if !self.history.ignore {
277 self.history.push(TileChange {
278 tile_id: item.panel.view().entity_id(),
279 old_bounds: Some(previous_bounds),
280 new_bounds: Some(item.bounds),
281 old_order: None,
282 new_order: None,
283 version: 0,
284 });
285 }
286 }
287
288 cx.notify();
289 }
290
291 fn update_resizing_drag(
292 &mut self,
293 drag_data: ResizeDrag,
294 _: &mut Window,
295 cx: &mut Context<'_, Self>,
296 ) {
297 if let Some((index, _item)) = self.find_at_position(drag_data.last_position) {
298 self.resizing_index = Some(index);
299 self.resizing_drag_data = Some(drag_data);
300 cx.notify();
301 }
302 }
303
304 fn resize(
305 &mut self,
306 new_x: Option<Pixels>,
307 new_y: Option<Pixels>,
308 new_width: Option<Pixels>,
309 new_height: Option<Pixels>,
310 _: &mut Window,
311 cx: &mut Context<'_, Self>,
312 ) {
313 if let Some(index) = self.resizing_index {
314 if let Some(item) = self.panels.get_mut(index) {
315 let previous_bounds = item.bounds;
316 let final_x = if let Some(x) = new_x {
317 round_to_nearest_ten(x, cx)
318 } else {
319 previous_bounds.origin.x
320 };
321 let final_y = if let Some(y) = new_y {
322 round_to_nearest_ten(y, cx)
323 } else {
324 previous_bounds.origin.y
325 };
326 let final_width = if let Some(width) = new_width {
327 round_to_nearest_ten(width, cx)
328 } else {
329 previous_bounds.size.width
330 };
331
332 let final_height = if let Some(height) = new_height {
333 round_to_nearest_ten(height, cx)
334 } else {
335 previous_bounds.size.height
336 };
337
338 if final_width != item.bounds.size.width
340 || final_height != item.bounds.size.height
341 || final_x != item.bounds.origin.x
342 || final_y != item.bounds.origin.y
343 {
344 item.bounds.origin.x = final_x;
345 item.bounds.origin.y = final_y;
346 item.bounds.size.width = final_width;
347 item.bounds.size.height = final_height;
348
349 if !self.history.ignore {
351 self.history.push(TileChange {
352 tile_id: item.panel.view().entity_id(),
353 old_bounds: Some(previous_bounds),
354 new_bounds: Some(item.bounds),
355 old_order: None,
356 new_order: None,
357 version: 0,
358 });
359 }
360 }
361
362 cx.notify();
363 }
364 }
365 }
366
367 pub fn add_item(
368 &mut self,
369 item: TileItem,
370 dock_area: &WeakEntity<DockArea>,
371 window: &mut Window,
372 cx: &mut Context<Self>,
373 ) {
374 let Ok(tab_panel) = item.panel.view().downcast::<TabPanel>() else {
375 panic!("only allows to add TabPanel type")
376 };
377
378 tab_panel.update(cx, |tab_panel, _| {
379 tab_panel.set_in_tiles(true);
380 });
381
382 self.panels.push(item.clone());
383 window.defer(cx, {
384 let panel = item.panel.clone();
385 let dock_area = dock_area.clone();
386
387 move |window, cx| {
388 _ = dock_area.update(cx, |this, cx| {
390 if let Ok(tab_panel) = panel.view().downcast::<TabPanel>() {
391 this.subscribe_panel(&tab_panel, window, cx);
392 }
393 });
394 }
395 });
396
397 cx.emit(PanelEvent::LayoutChanged);
398 cx.notify();
399 }
400
401 fn find_at_position(&self, position: Point<Pixels>) -> Option<(usize, &TileItem)> {
403 let inner_pos = position - self.bounds.origin;
404 let mut panels_with_indices: Vec<(usize, &TileItem)> =
405 self.panels.iter().enumerate().collect();
406
407 panels_with_indices
408 .sort_by(|a, b| b.1.z_index.cmp(&a.1.z_index).then_with(|| b.0.cmp(&a.0)));
409
410 for (index, item) in panels_with_indices {
411 let extended_bounds = Bounds::new(
412 item.bounds.origin,
413 item.bounds.size + size(HANDLE_SIZE, HANDLE_SIZE) / 2.0,
414 );
415 if extended_bounds.contains(&inner_pos) {
416 return Some((index, item));
417 }
418 }
419
420 None
421 }
422
423 #[inline]
424 fn reset_current_index(&mut self) {
425 self.dragging_index = None;
426 self.resizing_index = None;
427 }
428
429 fn bring_to_front(
431 &mut self,
432 target_index: Option<usize>,
433 cx: &mut Context<Self>,
434 ) -> Option<(usize, usize)> {
435 if let Some(old_index) = target_index {
436 if old_index < self.panels.len() {
437 let item = self.panels.remove(old_index);
438 self.panels.push(item);
439 let new_index = self.panels.len() - 1;
440 self.history.push(TileChange {
441 tile_id: self.panels[new_index].panel.view().entity_id(),
442 old_bounds: None,
443 new_bounds: None,
444 old_order: Some(old_index),
445 new_order: Some(new_index),
446 version: 0,
447 });
448 cx.notify();
449 return Some((old_index, new_index));
450 }
451 }
452 None
453 }
454
455 pub fn undo(&mut self, _: &mut Window, cx: &mut Context<Self>) {
457 self.history.ignore = true;
458
459 if let Some(changes) = self.history.undo() {
460 for change in changes {
461 if let Some(index) = self
462 .panels
463 .iter()
464 .position(|item| item.panel.view().entity_id() == change.tile_id)
465 {
466 if let Some(old_bounds) = change.old_bounds {
467 self.panels[index].bounds = old_bounds;
468 }
469 if let Some(old_order) = change.old_order {
470 let item = self.panels.remove(index);
471 self.panels.insert(old_order, item);
472 }
473 }
474 }
475 cx.emit(PanelEvent::LayoutChanged);
476 }
477
478 self.history.ignore = false;
479 cx.notify();
480 }
481
482 pub fn redo(&mut self, _: &mut Window, cx: &mut Context<Self>) {
484 self.history.ignore = true;
485
486 if let Some(changes) = self.history.redo() {
487 for change in changes {
488 if let Some(index) = self
489 .panels
490 .iter()
491 .position(|item| item.panel.view().entity_id() == change.tile_id)
492 {
493 if let Some(new_bounds) = change.new_bounds {
494 self.panels[index].bounds = new_bounds;
495 }
496 if let Some(new_order) = change.new_order {
497 let item = self.panels.remove(index);
498 self.panels.insert(new_order, item);
499 }
500 }
501 }
502 cx.emit(PanelEvent::LayoutChanged);
503 }
504
505 self.history.ignore = false;
506 cx.notify();
507 }
508
509 pub fn active_panel(&self, cx: &App) -> Option<Arc<dyn PanelView>> {
511 self.panels.last().and_then(|item| {
512 if let Ok(tab_panel) = item.panel.view().downcast::<TabPanel>() {
513 tab_panel.read(cx).active_panel(cx)
514 } else if let Ok(_) = item.panel.view().downcast::<StackPanel>() {
515 None
516 } else {
517 Some(item.panel.clone())
518 }
519 })
520 }
521
522 fn render_resize_handles(
524 &mut self,
525 _: &mut Window,
526 cx: &mut Context<Self>,
527 entity_id: EntityId,
528 item: &TileItem,
529 is_occluded: impl Fn(&Bounds<Pixels>) -> bool,
530 ) -> Vec<AnyElement> {
531 let panel_bounds = item.bounds;
532 let right_handle_bounds = Bounds::new(
533 panel_bounds.origin + point(panel_bounds.size.width - HANDLE_SIZE, px(0.0)),
534 size(HANDLE_SIZE, panel_bounds.size.height),
535 );
536
537 let bottom_handle_bounds = Bounds::new(
538 panel_bounds.origin + point(px(0.0), panel_bounds.size.height - HANDLE_SIZE.half()),
539 size(panel_bounds.size.width, HANDLE_SIZE.half()),
540 );
541
542 let corner_handle_bounds = Bounds::new(
543 panel_bounds.origin
544 + point(
545 panel_bounds.size.width - HANDLE_SIZE.half(),
546 panel_bounds.size.height - HANDLE_SIZE.half(),
547 ),
548 size(HANDLE_SIZE.half(), HANDLE_SIZE.half()),
549 );
550 let handle_offset = -HANDLE_SIZE + px(1.);
551
552 let mut elements = Vec::new();
553
554 elements.push(if !is_occluded(&right_handle_bounds) {
556 div()
557 .id("left-resize-handle")
558 .cursor_ew_resize()
559 .absolute()
560 .top_0()
561 .left(handle_offset)
562 .w(HANDLE_SIZE)
563 .h(panel_bounds.size.height)
564 .on_mouse_down(
565 MouseButton::Left,
566 cx.listener({
567 move |this, event: &MouseDownEvent, window, cx| {
568 let last_position = event.position;
569 let drag_data = ResizeDrag {
570 side: ResizeSide::Left,
571 last_position,
572 last_bounds: panel_bounds,
573 };
574 this.update_resizing_drag(drag_data, window, cx);
575 if let Some((_, new_ix)) = this.bring_to_front(this.resizing_index, cx)
576 {
577 this.resizing_index = Some(new_ix);
578 }
579 }
580 }),
581 )
582 .on_drag(DragResizing(entity_id), |drag, _, _, cx| {
583 cx.stop_propagation();
584 cx.new(|_| drag.clone())
585 })
586 .on_drag_move(cx.listener(
587 move |this, e: &DragMoveEvent<DragResizing>, window, cx| match e.drag(cx) {
588 DragResizing(id) => {
589 if *id != entity_id {
590 return;
591 }
592
593 let Some(ref drag_data) = this.resizing_drag_data else {
594 return;
595 };
596 if drag_data.side != ResizeSide::Left {
597 return;
598 }
599
600 let pos = e.event.position;
601 let delta = drag_data.last_position.x - pos.x;
602 let new_x = (drag_data.last_bounds.origin.x - delta).max(px(0.0));
603 let size_delta = drag_data.last_bounds.origin.x - new_x;
604 let new_width = (drag_data.last_bounds.size.width + size_delta)
605 .max(MINIMUM_SIZE.width);
606 this.resize(Some(new_x), None, Some(new_width), None, window, cx);
607 }
608 },
609 ))
610 .into_any_element()
611 } else {
612 div().into_any_element()
613 });
614
615 elements.push(if !is_occluded(&right_handle_bounds) {
617 div()
618 .id("right-resize-handle")
619 .cursor_ew_resize()
620 .absolute()
621 .top_0()
622 .right(handle_offset)
623 .w(HANDLE_SIZE)
624 .h(panel_bounds.size.height)
625 .on_mouse_down(
626 MouseButton::Left,
627 cx.listener({
628 move |this, event: &MouseDownEvent, window, cx| {
629 let last_position = event.position;
630 let drag_data = ResizeDrag {
631 side: ResizeSide::Right,
632 last_position,
633 last_bounds: panel_bounds,
634 };
635 this.update_resizing_drag(drag_data, window, cx);
636 if let Some((_, new_ix)) = this.bring_to_front(this.resizing_index, cx)
637 {
638 this.resizing_index = Some(new_ix);
639 }
640 }
641 }),
642 )
643 .on_drag(DragResizing(entity_id), |drag, _, _, cx| {
644 cx.stop_propagation();
645 cx.new(|_| drag.clone())
646 })
647 .on_drag_move(cx.listener(
648 move |this, e: &DragMoveEvent<DragResizing>, window, cx| match e.drag(cx) {
649 DragResizing(id) => {
650 if *id != entity_id {
651 return;
652 }
653
654 let Some(ref drag_data) = this.resizing_drag_data else {
655 return;
656 };
657
658 if drag_data.side != ResizeSide::Right {
659 return;
660 }
661
662 let pos = e.event.position;
663 let delta = pos.x - drag_data.last_position.x;
664 let new_width =
665 (drag_data.last_bounds.size.width + delta).max(MINIMUM_SIZE.width);
666 this.resize(None, None, Some(new_width), None, window, cx);
667 }
668 },
669 ))
670 .into_any_element()
671 } else {
672 div().into_any_element()
673 });
674
675 elements.push(if !is_occluded(&bottom_handle_bounds) {
677 div()
678 .id("top-resize-handle")
679 .cursor_ns_resize()
680 .absolute()
681 .left(px(0.0))
682 .top(handle_offset)
683 .w(panel_bounds.size.width)
684 .h(HANDLE_SIZE)
685 .on_mouse_down(
686 MouseButton::Left,
687 cx.listener({
688 move |this, event: &MouseDownEvent, window, cx| {
689 let last_position = event.position;
690 let drag_data = ResizeDrag {
691 side: ResizeSide::Top,
692 last_position,
693 last_bounds: panel_bounds,
694 };
695 this.update_resizing_drag(drag_data, window, cx);
696 if let Some((_, new_ix)) = this.bring_to_front(this.resizing_index, cx)
697 {
698 this.resizing_index = Some(new_ix);
699 }
700 }
701 }),
702 )
703 .on_drag(DragResizing(entity_id), |drag, _, _, cx| {
704 cx.stop_propagation();
705 cx.new(|_| drag.clone())
706 })
707 .on_drag_move(cx.listener(
708 move |this, e: &DragMoveEvent<DragResizing>, window, cx| match e.drag(cx) {
709 DragResizing(id) => {
710 if *id != entity_id {
711 return;
712 }
713
714 let Some(ref drag_data) = this.resizing_drag_data else {
715 return;
716 };
717 if drag_data.side != ResizeSide::Top {
718 return;
719 }
720
721 let pos = e.event.position;
722 let delta = drag_data.last_position.y - pos.y;
723 let new_y = (drag_data.last_bounds.origin.y - delta).max(px(0.));
724 let size_delta = drag_data.last_position.y - new_y;
725 let new_height = (drag_data.last_bounds.size.height + size_delta)
726 .max(MINIMUM_SIZE.width);
727 this.resize(None, Some(new_y), None, Some(new_height), window, cx);
728 }
729 },
730 ))
731 .into_any_element()
732 } else {
733 div().into_any_element()
734 });
735
736 elements.push(if !is_occluded(&bottom_handle_bounds) {
738 div()
739 .id("bottom-resize-handle")
740 .cursor_ns_resize()
741 .absolute()
742 .left(px(0.0))
743 .bottom(handle_offset)
744 .w(panel_bounds.size.width)
745 .h(HANDLE_SIZE)
746 .on_mouse_down(
747 MouseButton::Left,
748 cx.listener({
749 move |this, event: &MouseDownEvent, window, cx| {
750 let last_position = event.position;
751 let drag_data = ResizeDrag {
752 side: ResizeSide::Bottom,
753 last_position,
754 last_bounds: panel_bounds,
755 };
756 this.update_resizing_drag(drag_data, window, cx);
757 if let Some((_, new_ix)) = this.bring_to_front(this.resizing_index, cx)
758 {
759 this.resizing_index = Some(new_ix);
760 }
761 }
762 }),
763 )
764 .on_drag(DragResizing(entity_id), |drag, _, _, cx| {
765 cx.stop_propagation();
766 cx.new(|_| drag.clone())
767 })
768 .on_drag_move(cx.listener(
769 move |this, e: &DragMoveEvent<DragResizing>, window, cx| match e.drag(cx) {
770 DragResizing(id) => {
771 if *id != entity_id {
772 return;
773 }
774
775 let Some(ref drag_data) = this.resizing_drag_data else {
776 return;
777 };
778
779 if drag_data.side != ResizeSide::Bottom {
780 return;
781 }
782
783 let pos = e.event.position;
784 let delta = pos.y - drag_data.last_position.y;
785 let new_height =
786 (drag_data.last_bounds.size.height + delta).max(MINIMUM_SIZE.width);
787 this.resize(None, None, None, Some(new_height), window, cx);
788 }
789 },
790 ))
791 .into_any_element()
792 } else {
793 div().into_any_element()
794 });
795
796 elements.push(if !is_occluded(&corner_handle_bounds) {
798 div()
799 .child(
800 Icon::new(IconName::ResizeCorner)
801 .size_3()
802 .absolute()
803 .right(px(1.))
804 .bottom(px(1.))
805 .text_color(cx.theme().muted_foreground.opacity(0.5)),
806 )
807 .child(
808 div()
809 .id("corner-resize-handle")
810 .cursor_nwse_resize()
811 .absolute()
812 .right(handle_offset)
813 .bottom(handle_offset)
814 .size_3()
815 .on_mouse_down(
816 MouseButton::Left,
817 cx.listener({
818 move |this, event: &MouseDownEvent, window, cx| {
819 let last_position = event.position;
820 let drag_data = ResizeDrag {
821 side: ResizeSide::BottomRight,
822 last_position,
823 last_bounds: panel_bounds,
824 };
825 this.update_resizing_drag(drag_data, window, cx);
826 if let Some((_, new_ix)) =
827 this.bring_to_front(this.resizing_index, cx)
828 {
829 this.resizing_index = Some(new_ix);
830 }
831 }
832 }),
833 )
834 .on_drag(DragResizing(entity_id), |drag, _, _, cx| {
835 cx.stop_propagation();
836 cx.new(|_| drag.clone())
837 })
838 .on_drag_move(cx.listener(
839 move |this, e: &DragMoveEvent<DragResizing>, window, cx| {
840 match e.drag(cx) {
841 DragResizing(id) => {
842 if *id != entity_id {
843 return;
844 }
845
846 let Some(ref drag_data) = this.resizing_drag_data else {
847 return;
848 };
849
850 if drag_data.side != ResizeSide::BottomRight {
851 return;
852 }
853
854 let pos = e.event.position;
855 let delta_x = pos.x - drag_data.last_position.x;
856 let delta_y = pos.y - drag_data.last_position.y;
857 let new_width = (drag_data.last_bounds.size.width
858 + delta_x)
859 .max(MINIMUM_SIZE.width);
860 let new_height = (drag_data.last_bounds.size.height
861 + delta_y)
862 .max(MINIMUM_SIZE.height);
863 this.resize(
864 None,
865 None,
866 Some(new_width),
867 Some(new_height),
868 window,
869 cx,
870 );
871 }
872 }
873 },
874 )),
875 )
876 .into_any_element()
877 } else {
878 div().into_any_element()
879 });
880
881 elements
882 }
883
884 fn render_drag_bar(
886 &mut self,
887 _: &mut Window,
888 cx: &mut Context<Self>,
889 entity_id: EntityId,
890 item: &TileItem,
891 is_occluded: &impl Fn(&Bounds<Pixels>) -> bool,
892 ) -> AnyElement {
893 let drag_bar_bounds = Bounds::new(
894 item.bounds.origin,
895 Size {
896 width: item.bounds.size.width,
897 height: DRAG_BAR_HEIGHT,
898 },
899 );
900
901 if !is_occluded(&drag_bar_bounds) {
902 h_flex()
903 .id("drag-bar")
904 .absolute()
905 .w_full()
906 .h(DRAG_BAR_HEIGHT)
907 .bg(cx.theme().transparent)
908 .on_mouse_down(
909 MouseButton::Left,
910 cx.listener(move |this, event: &MouseDownEvent, window, cx| {
911 let last_position = event.position;
912 this.update_initial_position(last_position, window, cx);
913 if let Some((_, new_ix)) = this.bring_to_front(this.dragging_index, cx) {
914 this.dragging_index = Some(new_ix);
915 }
916 }),
917 )
918 .on_drag(DragMoving(entity_id), |drag, _, _, cx| {
919 cx.stop_propagation();
920 cx.new(|_| drag.clone())
921 })
922 .on_drag_move(cx.listener(
923 move |this, e: &DragMoveEvent<DragMoving>, window, cx| match e.drag(cx) {
924 DragMoving(id) => {
925 if *id != entity_id {
926 return;
927 }
928 this.update_position(e.event.position, window, cx);
929 }
930 },
931 ))
932 .into_any_element()
933 } else {
934 div().into_any_element()
935 }
936 }
937
938 fn render_panel(
939 &mut self,
940 item: &TileItem,
941 ix: usize,
942 window: &mut Window,
943 cx: &mut Context<Self>,
944 ) -> impl IntoElement {
945 let entity_id = cx.entity_id();
946 let panel_view = item.panel.view();
947 let is_occluded = {
948 let panels = self.panels.clone();
949 move |bounds: &Bounds<Pixels>| {
950 let this_z = panels[ix].z_index;
951 let this_ix = ix;
952 panels.iter().enumerate().any(|(sub_ix, other_item)| {
953 if sub_ix == this_ix {
954 return false;
955 }
956 let other_is_above = (other_item.z_index > this_z)
957 || (other_item.z_index == this_z && sub_ix > this_ix);
958
959 other_is_above && other_item.bounds.intersects(bounds)
960 })
961 }
962 };
963
964 v_flex()
965 .occlude()
966 .bg(cx.theme().background)
967 .border_1()
968 .border_color(cx.theme().border)
969 .absolute()
970 .left(item.bounds.origin.x)
971 .top(item.bounds.origin.y)
972 .w(item.bounds.size.width + px(1.))
974 .h(item.bounds.size.height + px(1.))
975 .rounded(cx.theme().radius)
976 .child(h_flex().overflow_hidden().size_full().child(panel_view))
977 .children(self.render_resize_handles(window, cx, entity_id, &item, &is_occluded))
978 .child(self.render_drag_bar(window, cx, entity_id, &item, &is_occluded))
979 .on_mouse_up(
981 MouseButton::Left,
982 cx.listener(move |this, _, _, cx| {
983 this.bring_to_front(Some(ix), cx);
984 }),
985 )
986 }
987
988 fn on_mouse_up(&mut self, _: &mut Window, cx: &mut Context<'_, Tiles>) {
990 if self.dragging_index.is_some()
992 || self.resizing_index.is_some()
993 || self.resizing_drag_data.is_some()
994 {
995 let mut changes_to_push = vec![];
996
997 if let Some(index) = self.dragging_index {
999 if let Some(item) = self.panels.get(index) {
1000 let initial_bounds = self.dragging_initial_bounds;
1001 let current_bounds = item.bounds;
1002 if initial_bounds.origin != current_bounds.origin
1003 || initial_bounds.size != current_bounds.size
1004 {
1005 changes_to_push.push(TileChange {
1006 tile_id: item.panel.view().entity_id(),
1007 old_bounds: Some(initial_bounds),
1008 new_bounds: Some(current_bounds),
1009 old_order: None,
1010 new_order: None,
1011 version: 0,
1012 });
1013 }
1014 }
1015 }
1016
1017 if let Some(index) = self.resizing_index {
1019 if let Some(drag_data) = &self.resizing_drag_data {
1020 if let Some(item) = self.panels.get(index) {
1021 let initial_bounds = drag_data.last_bounds;
1022 let current_bounds = item.bounds;
1023 if initial_bounds.size != current_bounds.size {
1024 changes_to_push.push(TileChange {
1025 tile_id: item.panel.view().entity_id(),
1026 old_bounds: Some(initial_bounds),
1027 new_bounds: Some(current_bounds),
1028 old_order: None,
1029 new_order: None,
1030 version: 0,
1031 });
1032 }
1033 }
1034 }
1035 }
1036
1037 if !changes_to_push.is_empty() {
1039 for change in changes_to_push {
1040 self.history.push(change);
1041 }
1042 }
1043
1044 self.reset_current_index();
1046 self.resizing_drag_data = None;
1047 cx.emit(PanelEvent::LayoutChanged);
1048 cx.notify();
1049 }
1050 }
1051}
1052
1053#[inline]
1054fn round_to_nearest_ten(value: Pixels, cx: &App) -> Pixels {
1055 (value / cx.theme().tile_grid_size).round() * cx.theme().tile_grid_size
1056}
1057
1058#[inline]
1059fn round_point_to_nearest_ten(point: Point<Pixels>, cx: &App) -> Point<Pixels> {
1060 Point::new(
1061 round_to_nearest_ten(point.x, cx),
1062 round_to_nearest_ten(point.y, cx),
1063 )
1064}
1065
1066impl Focusable for Tiles {
1067 fn focus_handle(&self, _cx: &App) -> FocusHandle {
1068 self.focus_handle.clone()
1069 }
1070}
1071impl EventEmitter<PanelEvent> for Tiles {}
1072impl EventEmitter<DismissEvent> for Tiles {}
1073impl Render for Tiles {
1074 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1075 let view = cx.entity().clone();
1076 let panels = self.sorted_panels();
1077 let scroll_bounds =
1078 self.panels
1079 .iter()
1080 .fold(Bounds::default(), |acc: Bounds<Pixels>, item| Bounds {
1081 origin: Point {
1082 x: acc.origin.x.min(item.bounds.origin.x),
1083 y: acc.origin.y.min(item.bounds.origin.y),
1084 },
1085 size: Size {
1086 width: acc.size.width.max(item.bounds.right()),
1087 height: acc.size.height.max(item.bounds.bottom()),
1088 },
1089 });
1090 let scroll_size = scroll_bounds.size - size(scroll_bounds.origin.x, scroll_bounds.origin.y);
1091
1092 div()
1093 .relative()
1094 .bg(cx.theme().tiles)
1095 .child(
1096 div()
1097 .id("tiles")
1098 .track_scroll(&self.scroll_handle)
1099 .size_full()
1100 .top(-px(1.))
1101 .overflow_scroll()
1102 .children(
1103 panels
1104 .into_iter()
1105 .enumerate()
1106 .map(|(ix, item)| self.render_panel(&item, ix, window, cx)),
1107 )
1108 .child({
1109 canvas(
1110 move |bounds, _, cx| view.update(cx, |r, _| r.bounds = bounds),
1111 |_, _, _, _| {},
1112 )
1113 .absolute()
1114 .size_full()
1115 })
1116 .on_drop(cx.listener(move |_, item: &AnyDrag, _, cx| {
1117 cx.emit(DragDrop(item.clone()));
1118 })),
1119 )
1120 .on_mouse_up(
1121 MouseButton::Left,
1122 cx.listener(move |this, _event: &MouseUpEvent, window, cx| {
1123 this.on_mouse_up(window, cx);
1124 }),
1125 )
1126 .on_mouse_down(
1127 MouseButton::Left,
1128 cx.listener(move |this, event: &MouseDownEvent, _, cx| {
1129 if this.resizing_index.is_none() && this.dragging_index.is_none() {
1130 let position = event.position;
1131 if let Some((index, _)) = this.find_at_position(position) {
1132 this.bring_to_front(Some(index), cx);
1133 cx.notify();
1134 }
1135 }
1136 }),
1137 )
1138 .child(
1139 div()
1140 .absolute()
1141 .top_0()
1142 .left_0()
1143 .right_0()
1144 .bottom_0()
1145 .child(
1146 Scrollbar::both(&self.scroll_state, &self.scroll_handle)
1147 .scroll_size(scroll_size),
1148 ),
1149 )
1150 .size_full()
1151 }
1152}