gpui_component/dock/
tiles.rs

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