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