gpui_component/dock/
tiles.rs

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/// TileItem is a moveable and resizable panel that can be added to a Tiles view.
85#[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
129/// Tiles is a canvas that can contain multiple panels, each of which can be dragged and resized.
130pub 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    /// Return the index of the panel.
210    #[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    /// Remove panel from the children.
216    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        // Avoid out of bounds
262        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        // Only push to history if bounds have changed
272        if final_origin != previous_bounds.origin {
273            item.bounds.origin = final_origin;
274
275            // Only push if not during history operations
276            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                // Only push to history if size has changed
339                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                    // Only push if not during history operations
350                    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                // Subscribe to the panel's layout change event.
389                _ = 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    /// Find the panel at a given position, considering z-index
402    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    /// Bring the panel of target_index to front, returns (old_index, new_index) if successful
430    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    /// Handle the undo action
456    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    /// Handle the redo action
483    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    /// Returns the active panel, if any.
510    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    /// Produce a vector of AnyElement representing the three possible resize handles
523    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        // Left resize handle
555        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        // Right resize handle
616        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        // Top resize handle
676        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        // Bottom resize handle
737        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        // Corner resize handle
797        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    /// Produce the drag-bar element for the given panel item
885    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            // More 1px to account for the border width when 2 panels are too close
973            .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            // Here must be mouse up for avoid conflict with Drag event
980            .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    /// Handle the mouse up event to finalize drag or resize operations
989    fn on_mouse_up(&mut self, _: &mut Window, cx: &mut Context<'_, Tiles>) {
990        // Check if a drag or resize was active
991        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            // Handle dragging
998            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            // Handle resizing
1018            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            // Push changes to history if any
1038            if !changes_to_push.is_empty() {
1039                for change in changes_to_push {
1040                    self.history.push(change);
1041                }
1042            }
1043
1044            // Reset drag and resize state
1045            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}