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