gpui_component/dock/
tiles.rs

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