From 04fd5e1511033846258060e48c0779b123a32793 Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Thu, 23 Oct 2025 19:29:39 +0800 Subject: [PATCH] tiles: Ensure to response correct panel on drag or resize. (#1422) Use `item_id` to store the dragging and resizing item. --- crates/ui/src/dock/tiles.rs | 546 +++++++++++++++--------------------- 1 file changed, 230 insertions(+), 316 deletions(-) diff --git a/crates/ui/src/dock/tiles.rs b/crates/ui/src/dock/tiles.rs index b58b5b26..51fc5930 100644 --- a/crates/ui/src/dock/tiles.rs +++ b/crates/ui/src/dock/tiles.rs @@ -15,14 +15,13 @@ use super::{ DockArea, Panel, PanelEvent, PanelInfo, PanelState, PanelView, StackPanel, TabPanel, TileMeta, }; use gpui::{ - actions, canvas, div, point, px, size, AnyElement, App, AppContext, Bounds, Context, - DismissEvent, DragMoveEvent, Empty, EntityId, EventEmitter, FocusHandle, Focusable, Half, - InteractiveElement, IntoElement, MouseButton, MouseDownEvent, MouseUpEvent, ParentElement, - Pixels, Point, Render, ScrollHandle, Size, StatefulInteractiveElement, Styled, WeakEntity, - Window, + actions, canvas, div, px, size, AnyElement, App, AppContext, Bounds, Context, DismissEvent, + DragMoveEvent, Empty, EntityId, EventEmitter, FocusHandle, Focusable, InteractiveElement, + IntoElement, MouseButton, MouseDownEvent, MouseUpEvent, ParentElement, Pixels, Point, Render, + ScrollHandle, Size, StatefulInteractiveElement, Styled, WeakEntity, Window, }; -actions!(tiles, [Undo, Redo,]); +actions!(tiles, [Undo, Redo]); const MINIMUM_SIZE: Size = size(px(100.), px(100.)); const DRAG_BAR_HEIGHT: Pixels = px(30.); @@ -84,6 +83,7 @@ struct ResizeDrag { /// TileItem is a moveable and resizable panel that can be added to a Tiles view. #[derive(Clone)] pub struct TileItem { + id: EntityId, pub(crate) panel: Arc, bounds: Bounds, z_index: usize, @@ -101,6 +101,7 @@ impl Debug for TileItem { impl TileItem { pub fn new(panel: Arc, bounds: Bounds) -> Self { Self { + id: panel.view().entity_id(), panel, bounds, z_index: 0, @@ -130,10 +131,10 @@ impl AnyDrag { pub struct Tiles { focus_handle: FocusHandle, pub(crate) panels: Vec, - dragging_index: Option, + dragging_id: Option, dragging_initial_mouse: Point, dragging_initial_bounds: Bounds, - resizing_index: Option, + resizing_id: Option, resizing_drag_data: Option, bounds: Bounds, history: History, @@ -184,10 +185,10 @@ impl Tiles { Self { focus_handle: cx.focus_handle(), panels: vec![], - dragging_index: None, + dragging_id: None, dragging_initial_mouse: Point::default(), dragging_initial_bounds: Bounds::default(), - resizing_index: None, + resizing_id: None, resizing_drag_data: None, bounds: Bounds::default(), history: History::new().group_interval(std::time::Duration::from_millis(100)), @@ -208,48 +209,35 @@ impl Tiles { /// Return the index of the panel. #[inline] - pub(crate) fn index_of(&self, panel: Arc) -> Option { - self.panels.iter().position(|p| &p.panel == &panel) + pub(crate) fn index_of(&self, id: &EntityId) -> Option { + self.panels.iter().position(|p| &p.id == id) + } + + #[inline] + pub(crate) fn panel(&self, id: &EntityId) -> Option<&TileItem> { + self.panels.iter().find(|p| &p.id == id) } /// Remove panel from the children. pub fn remove(&mut self, panel: Arc, _: &mut Window, cx: &mut Context) { - if let Some(ix) = self.index_of(panel.clone()) { + if let Some(ix) = self.index_of(&panel.panel_id(cx)) { self.panels.remove(ix); cx.emit(PanelEvent::LayoutChanged); } } - fn update_initial_position( - &mut self, - position: Point, - _: &mut Window, - cx: &mut Context<'_, Self>, - ) { - let Some((index, item)) = self.find_at_position(position) else { - return; - }; - - let inner_pos = position - self.bounds.origin; - let bounds = item.bounds; - self.dragging_index = Some(index); - self.dragging_initial_mouse = inner_pos; - self.dragging_initial_bounds = bounds; - cx.notify(); - } - fn update_position( &mut self, mouse_position: Point, _: &mut Window, cx: &mut Context<'_, Self>, ) { - let Some(index) = self.dragging_index else { + let Some(dragging_id) = self.dragging_id else { return; }; - let Some(item) = self.panels.get_mut(index) else { + let Some(item) = self.panels.iter_mut().find(|p| p.id == dragging_id) else { return; }; @@ -288,19 +276,6 @@ impl Tiles { cx.notify(); } - fn update_resizing_drag( - &mut self, - drag_data: ResizeDrag, - _: &mut Window, - cx: &mut Context<'_, Self>, - ) { - if let Some((index, _item)) = self.find_at_position(drag_data.last_position) { - self.resizing_index = Some(index); - self.resizing_drag_data = Some(drag_data); - cx.notify(); - } - } - fn resize( &mut self, new_x: Option, @@ -310,58 +285,61 @@ impl Tiles { _: &mut Window, cx: &mut Context<'_, Self>, ) { - if let Some(index) = self.resizing_index { - if let Some(item) = self.panels.get_mut(index) { - let previous_bounds = item.bounds; - let final_x = if let Some(x) = new_x { - round_to_nearest_ten(x, cx) - } else { - previous_bounds.origin.x - }; - let final_y = if let Some(y) = new_y { - round_to_nearest_ten(y, cx) - } else { - previous_bounds.origin.y - }; - let final_width = if let Some(width) = new_width { - round_to_nearest_ten(width, cx) - } else { - previous_bounds.size.width - }; + let Some(resizing_id) = self.resizing_id else { + return; + }; + let Some(item) = self.panels.iter_mut().find(|item| item.id == resizing_id) else { + return; + }; - let final_height = if let Some(height) = new_height { - round_to_nearest_ten(height, cx) - } else { - previous_bounds.size.height - }; + let previous_bounds = item.bounds; + let final_x = if let Some(x) = new_x { + round_to_nearest_ten(x, cx) + } else { + previous_bounds.origin.x + }; + let final_y = if let Some(y) = new_y { + round_to_nearest_ten(y, cx) + } else { + previous_bounds.origin.y + }; + let final_width = if let Some(width) = new_width { + round_to_nearest_ten(width, cx) + } else { + previous_bounds.size.width + }; - // Only push to history if size has changed - if final_width != item.bounds.size.width - || final_height != item.bounds.size.height - || final_x != item.bounds.origin.x - || final_y != item.bounds.origin.y - { - item.bounds.origin.x = final_x; - item.bounds.origin.y = final_y; - item.bounds.size.width = final_width; - item.bounds.size.height = final_height; + let final_height = if let Some(height) = new_height { + round_to_nearest_ten(height, cx) + } else { + previous_bounds.size.height + }; - // Only push if not during history operations - if !self.history.ignore { - self.history.push(TileChange { - tile_id: item.panel.view().entity_id(), - old_bounds: Some(previous_bounds), - new_bounds: Some(item.bounds), - old_order: None, - new_order: None, - version: 0, - }); - } - } + // Only push to history if size has changed + if final_width != item.bounds.size.width + || final_height != item.bounds.size.height + || final_x != item.bounds.origin.x + || final_y != item.bounds.origin.y + { + item.bounds.origin.x = final_x; + item.bounds.origin.y = final_y; + item.bounds.size.width = final_width; + item.bounds.size.height = final_height; - cx.notify(); + // Only push if not during history operations + if !self.history.ignore { + self.history.push(TileChange { + tile_id: item.panel.view().entity_id(), + old_bounds: Some(previous_bounds), + new_bounds: Some(item.bounds), + old_order: None, + new_order: None, + version: 0, + }); } } + + cx.notify(); } pub fn add_item( @@ -398,56 +376,38 @@ impl Tiles { cx.notify(); } - /// Find the panel at a given position, considering z-index - fn find_at_position(&self, position: Point) -> Option<(usize, &TileItem)> { - let inner_pos = position - self.bounds.origin; - let mut panels_with_indices: Vec<(usize, &TileItem)> = - self.panels.iter().enumerate().collect(); - - panels_with_indices - .sort_by(|a, b| b.1.z_index.cmp(&a.1.z_index).then_with(|| b.0.cmp(&a.0))); - - for (index, item) in panels_with_indices { - let extended_bounds = Bounds::new( - item.bounds.origin, - item.bounds.size + size(HANDLE_SIZE, HANDLE_SIZE) / 2.0, - ); - if extended_bounds.contains(&inner_pos) { - return Some((index, item)); - } - } - - None - } - #[inline] fn reset_current_index(&mut self) { - self.dragging_index = None; - self.resizing_index = None; + self.dragging_id = None; + self.resizing_id = None; } /// Bring the panel of target_index to front, returns (old_index, new_index) if successful fn bring_to_front( &mut self, - target_index: Option, + target_id: Option, cx: &mut Context, - ) -> Option<(usize, usize)> { - if let Some(old_index) = target_index { - if old_index < self.panels.len() { - let item = self.panels.remove(old_index); - self.panels.push(item); - let new_index = self.panels.len() - 1; - self.history.push(TileChange { - tile_id: self.panels[new_index].panel.view().entity_id(), - old_bounds: None, - new_bounds: None, - old_order: Some(old_index), - new_order: Some(new_index), - version: 0, - }); - cx.notify(); - return Some((old_index, new_index)); - } + ) -> Option { + let Some(old_id) = target_id else { + return None; + }; + + let old_ix = self.panels.iter().position(|item| item.id == old_id)?; + if old_ix < self.panels.len() { + let item = self.panels.remove(old_ix); + self.panels.push(item); + let new_ix = self.panels.len() - 1; + let new_id = self.panels[new_ix].id; + self.history.push(TileChange { + tile_id: new_id, + old_bounds: None, + new_bounds: None, + old_order: Some(old_ix), + new_order: Some(new_ix), + version: 0, + }); + cx.notify(); + return Some(new_id); } None } @@ -526,33 +486,15 @@ impl Tiles { cx: &mut Context, entity_id: EntityId, item: &TileItem, - is_occluded: impl Fn(&Bounds) -> bool, ) -> Vec { - let panel_bounds = item.bounds; - let right_handle_bounds = Bounds::new( - panel_bounds.origin + point(panel_bounds.size.width - HANDLE_SIZE, px(0.0)), - size(HANDLE_SIZE, panel_bounds.size.height), - ); - - let bottom_handle_bounds = Bounds::new( - panel_bounds.origin + point(px(0.0), panel_bounds.size.height - HANDLE_SIZE.half()), - size(panel_bounds.size.width, HANDLE_SIZE.half()), - ); - - let corner_handle_bounds = Bounds::new( - panel_bounds.origin - + point( - panel_bounds.size.width - HANDLE_SIZE.half(), - panel_bounds.size.height - HANDLE_SIZE.half(), - ), - size(HANDLE_SIZE.half(), HANDLE_SIZE.half()), - ); + let item_id = item.id; + let item_bounds = item.bounds; let handle_offset = -HANDLE_SIZE + px(1.); let mut elements = Vec::new(); // Left resize handle - elements.push(if !is_occluded(&right_handle_bounds) { + elements.push( div() .id("left-resize-handle") .cursor_ew_resize() @@ -560,22 +502,19 @@ impl Tiles { .top_0() .left(handle_offset) .w(HANDLE_SIZE) - .h(panel_bounds.size.height) + .h(item_bounds.size.height) .on_mouse_down( MouseButton::Left, cx.listener({ move |this, event: &MouseDownEvent, window, cx| { - let last_position = event.position; - let drag_data = ResizeDrag { - side: ResizeSide::Left, - last_position, - last_bounds: panel_bounds, - }; - this.update_resizing_drag(drag_data, window, cx); - if let Some((_, new_ix)) = this.bring_to_front(this.resizing_index, cx) - { - this.resizing_index = Some(new_ix); - } + this.on_resize_handle_mouse_down( + ResizeSide::Left, + item_id, + item_bounds, + event, + window, + cx, + ); } }), ) @@ -607,13 +546,11 @@ impl Tiles { } }, )) - .into_any_element() - } else { - div().into_any_element() - }); + .into_any_element(), + ); // Right resize handle - elements.push(if !is_occluded(&right_handle_bounds) { + elements.push( div() .id("right-resize-handle") .cursor_ew_resize() @@ -621,22 +558,19 @@ impl Tiles { .top_0() .right(handle_offset) .w(HANDLE_SIZE) - .h(panel_bounds.size.height) + .h(item_bounds.size.height) .on_mouse_down( MouseButton::Left, cx.listener({ move |this, event: &MouseDownEvent, window, cx| { - let last_position = event.position; - let drag_data = ResizeDrag { - side: ResizeSide::Right, - last_position, - last_bounds: panel_bounds, - }; - this.update_resizing_drag(drag_data, window, cx); - if let Some((_, new_ix)) = this.bring_to_front(this.resizing_index, cx) - { - this.resizing_index = Some(new_ix); - } + this.on_resize_handle_mouse_down( + ResizeSide::Right, + item_id, + item_bounds, + event, + window, + cx, + ); } }), ) @@ -667,36 +601,31 @@ impl Tiles { } }, )) - .into_any_element() - } else { - div().into_any_element() - }); + .into_any_element(), + ); // Top resize handle - elements.push(if !is_occluded(&bottom_handle_bounds) { + elements.push( div() .id("top-resize-handle") .cursor_ns_resize() .absolute() .left(px(0.0)) .top(handle_offset) - .w(panel_bounds.size.width) + .w(item_bounds.size.width) .h(HANDLE_SIZE) .on_mouse_down( MouseButton::Left, cx.listener({ move |this, event: &MouseDownEvent, window, cx| { - let last_position = event.position; - let drag_data = ResizeDrag { - side: ResizeSide::Top, - last_position, - last_bounds: panel_bounds, - }; - this.update_resizing_drag(drag_data, window, cx); - if let Some((_, new_ix)) = this.bring_to_front(this.resizing_index, cx) - { - this.resizing_index = Some(new_ix); - } + this.on_resize_handle_mouse_down( + ResizeSide::Top, + item_id, + item_bounds, + event, + window, + cx, + ); } }), ) @@ -728,36 +657,31 @@ impl Tiles { } }, )) - .into_any_element() - } else { - div().into_any_element() - }); + .into_any_element(), + ); // Bottom resize handle - elements.push(if !is_occluded(&bottom_handle_bounds) { + elements.push( div() .id("bottom-resize-handle") .cursor_ns_resize() .absolute() .left(px(0.0)) .bottom(handle_offset) - .w(panel_bounds.size.width) + .w(item_bounds.size.width) .h(HANDLE_SIZE) .on_mouse_down( MouseButton::Left, cx.listener({ move |this, event: &MouseDownEvent, window, cx| { - let last_position = event.position; - let drag_data = ResizeDrag { - side: ResizeSide::Bottom, - last_position, - last_bounds: panel_bounds, - }; - this.update_resizing_drag(drag_data, window, cx); - if let Some((_, new_ix)) = this.bring_to_front(this.resizing_index, cx) - { - this.resizing_index = Some(new_ix); - } + this.on_resize_handle_mouse_down( + ResizeSide::Bottom, + item_id, + item_bounds, + event, + window, + cx, + ); } }), ) @@ -788,13 +712,11 @@ impl Tiles { } }, )) - .into_any_element() - } else { - div().into_any_element() - }); + .into_any_element(), + ); // Corner resize handle - elements.push(if !is_occluded(&corner_handle_bounds) { + elements.push( div() .child( Icon::new(IconName::ResizeCorner) @@ -816,18 +738,14 @@ impl Tiles { MouseButton::Left, cx.listener({ move |this, event: &MouseDownEvent, window, cx| { - let last_position = event.position; - let drag_data = ResizeDrag { - side: ResizeSide::BottomRight, - last_position, - last_bounds: panel_bounds, - }; - this.update_resizing_drag(drag_data, window, cx); - if let Some((_, new_ix)) = - this.bring_to_front(this.resizing_index, cx) - { - this.resizing_index = Some(new_ix); - } + this.on_resize_handle_mouse_down( + ResizeSide::BottomRight, + item_id, + item_bounds, + event, + window, + cx, + ); } }), ) @@ -873,14 +791,35 @@ impl Tiles { }, )), ) - .into_any_element() - } else { - div().into_any_element() - }); + .into_any_element(), + ); elements } + fn on_resize_handle_mouse_down( + &mut self, + side: ResizeSide, + item_id: EntityId, + item_bounds: Bounds, + event: &MouseDownEvent, + _: &mut Window, + cx: &mut Context<'_, Self>, + ) { + let last_position = event.position; + self.resizing_id = Some(item_id); + self.resizing_drag_data = Some(ResizeDrag { + side, + last_position, + last_bounds: item_bounds, + }); + + if let Some(new_id) = self.bring_to_front(self.resizing_id, cx) { + self.resizing_id = Some(new_id); + } + cx.stop_propagation(); + } + /// Produce the drag-bar element for the given panel item fn render_drag_bar( &mut self, @@ -888,78 +827,57 @@ impl Tiles { cx: &mut Context, entity_id: EntityId, item: &TileItem, - is_occluded: &impl Fn(&Bounds) -> bool, ) -> AnyElement { - let drag_bar_bounds = Bounds::new( - item.bounds.origin, - Size { - width: item.bounds.size.width, - height: DRAG_BAR_HEIGHT, - }, - ); + let item_id = item.id; + let item_bounds = item.bounds; - if !is_occluded(&drag_bar_bounds) { - h_flex() - .id("drag-bar") - .absolute() - .w_full() - .h(DRAG_BAR_HEIGHT) - .bg(cx.theme().transparent) - .on_mouse_down( - MouseButton::Left, - cx.listener(move |this, event: &MouseDownEvent, window, cx| { - let last_position = event.position; - this.update_initial_position(last_position, window, cx); - if let Some((_, new_ix)) = this.bring_to_front(this.dragging_index, cx) { - this.dragging_index = Some(new_ix); - } - }), - ) - .on_drag(DragMoving(entity_id), |drag, _, _, cx| { - cx.stop_propagation(); - cx.new(|_| drag.clone()) - }) - .on_drag_move(cx.listener( - move |this, e: &DragMoveEvent, window, cx| match e.drag(cx) { + h_flex() + .id("drag-bar") + .absolute() + .w_full() + .h(DRAG_BAR_HEIGHT) + .bg(cx.theme().transparent) + .on_mouse_down( + MouseButton::Left, + cx.listener(move |this, event: &MouseDownEvent, _, cx| { + let inner_pos = event.position - this.bounds.origin; + this.dragging_id = Some(item_id); + this.dragging_initial_mouse = inner_pos; + this.dragging_initial_bounds = item_bounds; + + if let Some(new_id) = this.bring_to_front(Some(item_id), cx) { + this.dragging_id = Some(new_id); + } + }), + ) + .on_drag(DragMoving(entity_id), |drag, _, _, cx| { + cx.stop_propagation(); + cx.new(|_| drag.clone()) + }) + .on_drag_move( + cx.listener(move |this, e: &DragMoveEvent, window, cx| { + match e.drag(cx) { DragMoving(id) => { if *id != entity_id { return; } this.update_position(e.event.position, window, cx); } - }, - )) - .into_any_element() - } else { - div().into_any_element() - } + } + }), + ) + .into_any_element() } fn render_panel( &mut self, item: &TileItem, - ix: usize, window: &mut Window, cx: &mut Context, ) -> impl IntoElement { let entity_id = cx.entity_id(); + let item_id = item.id; let panel_view = item.panel.view(); - let is_occluded = { - let panels = self.panels.clone(); - move |bounds: &Bounds| { - let this_z = panels[ix].z_index; - let this_ix = ix; - panels.iter().enumerate().any(|(sub_ix, other_item)| { - if sub_ix == this_ix { - return false; - } - let other_is_above = (other_item.z_index > this_z) - || (other_item.z_index == this_z && sub_ix > this_ix); - - other_is_above && other_item.bounds.intersects(bounds) - }) - } - }; v_flex() .occlude() @@ -974,13 +892,22 @@ impl Tiles { .h(item.bounds.size.height + px(1.)) .rounded(cx.theme().radius) .child(h_flex().overflow_hidden().size_full().child(panel_view)) - .children(self.render_resize_handles(window, cx, entity_id, &item, &is_occluded)) - .child(self.render_drag_bar(window, cx, entity_id, &item, &is_occluded)) + .children(self.render_resize_handles(window, cx, entity_id, &item)) + .child(self.render_drag_bar(window, cx, entity_id, &item)) + .on_mouse_down( + MouseButton::Left, + cx.listener(move |this, _, _, _| { + this.dragging_id = Some(item_id); + }), + ) // Here must be mouse up for avoid conflict with Drag event .on_mouse_up( MouseButton::Left, cx.listener(move |this, _, _, cx| { - this.bring_to_front(Some(ix), cx); + if this.dragging_id == Some(item_id) { + this.dragging_id = None; + this.bring_to_front(Some(item_id), cx); + } }), ) } @@ -988,15 +915,15 @@ impl Tiles { /// Handle the mouse up event to finalize drag or resize operations fn on_mouse_up(&mut self, _: &mut Window, cx: &mut Context<'_, Tiles>) { // Check if a drag or resize was active - if self.dragging_index.is_some() - || self.resizing_index.is_some() + if self.dragging_id.is_some() + || self.resizing_id.is_some() || self.resizing_drag_data.is_some() { let mut changes_to_push = vec![]; // Handle dragging - if let Some(index) = self.dragging_index { - if let Some(item) = self.panels.get(index) { + if let Some(dragging_id) = self.dragging_id { + if let Some(item) = self.panel(&dragging_id) { let initial_bounds = self.dragging_initial_bounds; let current_bounds = item.bounds; if initial_bounds.origin != current_bounds.origin @@ -1015,9 +942,9 @@ impl Tiles { } // Handle resizing - if let Some(index) = self.resizing_index { + if let Some(resizing_id) = self.resizing_id { if let Some(drag_data) = &self.resizing_drag_data { - if let Some(item) = self.panels.get(index) { + if let Some(item) = self.panel(&resizing_id) { let initial_bounds = drag_data.last_bounds; let current_bounds = item.bounds; if initial_bounds.size != current_bounds.size { @@ -1102,8 +1029,7 @@ impl Render for Tiles { .children( panels .into_iter() - .enumerate() - .map(|(ix, item)| self.render_panel(&item, ix, window, cx)), + .map(|item| self.render_panel(&item, window, cx)), ) .child({ canvas( @@ -1123,18 +1049,6 @@ impl Render for Tiles { this.on_mouse_up(window, cx); }), ) - .on_mouse_down( - MouseButton::Left, - cx.listener(move |this, event: &MouseDownEvent, _, cx| { - if this.resizing_index.is_none() && this.dragging_index.is_none() { - let position = event.position; - if let Some((index, _)) = this.find_at_position(position) { - this.bring_to_front(Some(index), cx); - cx.notify(); - } - } - }), - ) .child( div() .absolute()