update
This commit is contained in:
+21
-280
@@ -16,17 +16,17 @@ use gpui::{
|
||||
use gpui_component::animation::{Lerp, ease_out_cubic};
|
||||
use gpui_component::button::{Button, ButtonVariants as _};
|
||||
use gpui_component::menu::{DropdownMenu, PopupMenu};
|
||||
use gpui_component::tab::{Tab, TabBar};
|
||||
use gpui_component::{
|
||||
ActiveTheme, AxisExt, Disableable, ElementExt, IconName, Placement, Selectable, Sizable,
|
||||
h_flex, v_flex,
|
||||
};
|
||||
|
||||
use super::tab_bar::TabBar;
|
||||
use super::{
|
||||
AnyDrag, ClosePanel, DockArea, DockEvent, DockPlacement, DropTarget, Panel, PanelControl,
|
||||
PanelEvent, PanelState, PanelView, StackPanel, ToggleZoom,
|
||||
};
|
||||
use crate::{PanelInfo, TAB_BAR_HEIGHT, t, window_controls};
|
||||
use crate::{PanelInfo, t, window_controls};
|
||||
|
||||
#[derive(Clone)]
|
||||
struct TabState {
|
||||
@@ -41,7 +41,7 @@ struct TabState {
|
||||
pub(crate) struct DragPanel {
|
||||
pub(crate) panel: Arc<dyn PanelView>,
|
||||
pub(crate) tab_panel: Entity<TabPanel>,
|
||||
drag_offset: Rc<Cell<Point<Pixels>>>,
|
||||
pub(crate) drag_offset: Rc<Cell<Point<Pixels>>>,
|
||||
drag_session_id: u64,
|
||||
}
|
||||
|
||||
@@ -150,7 +150,7 @@ pub struct TabPanel {
|
||||
zoomed: bool,
|
||||
collapsed: bool,
|
||||
/// When drag move, will get the placement of the panel to be split
|
||||
will_split_placement: Option<Placement>,
|
||||
pub(crate) will_split_placement: Option<Placement>,
|
||||
drop_placeholder_animation: Option<DropPlaceholderAnimation>,
|
||||
drop_placeholder_animation_name: SharedString,
|
||||
|
||||
@@ -160,7 +160,7 @@ pub struct TabPanel {
|
||||
title_bar_bounds: Option<Bounds<Pixels>>,
|
||||
/// Bounds of the tab bar's trailing empty space (right after the last
|
||||
/// tab), which marks where the draggable region starts.
|
||||
title_bar_strip_bounds: Option<Bounds<Pixels>>,
|
||||
pub(crate) title_bar_strip_bounds: Option<Bounds<Pixels>>,
|
||||
/// Bounds of the tab bar's suffix (toolbar) area, which marks where the
|
||||
/// draggable region ends.
|
||||
title_bar_suffix_bounds: Option<Bounds<Pixels>>,
|
||||
@@ -342,7 +342,7 @@ impl TabPanel {
|
||||
self.active_ix
|
||||
}
|
||||
|
||||
fn set_active_ix(&mut self, ix: usize, window: &mut Window, cx: &mut Context<Self>) {
|
||||
pub(crate) fn set_active_ix(&mut self, ix: usize, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if ix == self.active_ix {
|
||||
return;
|
||||
}
|
||||
@@ -845,10 +845,8 @@ impl TabPanel {
|
||||
self.tab_bar_scroll_handle.scroll_to_item(visible_ix);
|
||||
}
|
||||
|
||||
let tabs_count = self.panels.len();
|
||||
|
||||
// TabBar lays out its scrollable content at content width, so the
|
||||
// strip after the last tab only spans `min_w_16` — the rest of the
|
||||
// The tab strip lays out its scrollable content at content width, so
|
||||
// the area after the last tab only spans `min_w_16` — the rest of the
|
||||
// tab bar has no element at all. Cover that dead zone with a
|
||||
// measured overlay so the whole non-interactive area can drag the
|
||||
// window. Its span is [last tab's right edge, suffix's left edge].
|
||||
@@ -898,11 +896,13 @@ impl TabPanel {
|
||||
}
|
||||
})
|
||||
.child(
|
||||
TabBar::new("tab-bar")
|
||||
.h(TAB_BAR_HEIGHT)
|
||||
.pill()
|
||||
.small()
|
||||
.track_scroll(&self.tab_bar_scroll_handle)
|
||||
TabBar::new("tab-bar", cx.entity())
|
||||
.panels(self.panels.clone())
|
||||
.active_panel(state.active_panel.clone())
|
||||
.collapsed(self.collapsed)
|
||||
.draggable(state.draggable)
|
||||
.droppable(state.droppable)
|
||||
.scroll_handle(&self.tab_bar_scroll_handle)
|
||||
.prefix(
|
||||
h_flex()
|
||||
.items_center()
|
||||
@@ -916,132 +916,6 @@ impl TabPanel {
|
||||
.children(left_dock_button)
|
||||
.child(self.render_prev_next_tab_buttons(cx)),
|
||||
)
|
||||
.children(self.panels.iter().enumerate().filter_map(|(ix, panel)| {
|
||||
let mut active = state.active_panel.as_ref() == Some(panel);
|
||||
let droppable = self.collapsed;
|
||||
|
||||
if !panel.visible(cx) {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Always not show active tab style, if the panel is collapsed
|
||||
if self.collapsed {
|
||||
active = false;
|
||||
}
|
||||
|
||||
Some(
|
||||
// Note: upstream also calls the crate-private `Tab::ix(ix)` and
|
||||
// `Tab::tab_bar_prefix(...)` here. `TabBar` re-applies `ix` per
|
||||
// child itself, and `tab_bar_prefix` only clears the first
|
||||
// tab's left border when the bar has no prefix — both are
|
||||
// inaccessible from outside gpui-component, so they are
|
||||
// omitted.
|
||||
Tab::new()
|
||||
.map(|this| {
|
||||
if let Some(tab_name) = panel.tab_name(cx) {
|
||||
this.child(tab_name)
|
||||
} else {
|
||||
this.child(panel.title(window, cx))
|
||||
}
|
||||
})
|
||||
.selected(active)
|
||||
.on_click(cx.listener({
|
||||
move |view, _, window, cx| {
|
||||
view.set_active_ix(ix, window, cx);
|
||||
}
|
||||
}))
|
||||
.when(!droppable, |this| {
|
||||
this.when(state.draggable, |this| {
|
||||
this.on_drag(
|
||||
DragPanel::new(panel.clone(), view.clone()),
|
||||
|drag, offset, _, cx| {
|
||||
cx.stop_propagation();
|
||||
drag.drag_offset.set(offset);
|
||||
cx.new(|_| drag.clone())
|
||||
},
|
||||
)
|
||||
})
|
||||
.when(
|
||||
state.droppable,
|
||||
|this| {
|
||||
this.drag_over::<DragPanel>(|this, _, _, cx| {
|
||||
this.rounded_l_none()
|
||||
.border_l_2()
|
||||
.border_r_0()
|
||||
.border_color(cx.theme().drag_border)
|
||||
})
|
||||
.on_drop(cx.listener(
|
||||
move |this, drag: &DragPanel, window, cx| {
|
||||
this.will_split_placement = None;
|
||||
this.on_drop(drag, Some(ix), true, window, cx)
|
||||
},
|
||||
))
|
||||
.drag_over::<AnyDrag>(|this, _, _, cx| {
|
||||
this.rounded_l_none()
|
||||
.border_l_2()
|
||||
.border_r_0()
|
||||
.border_color(cx.theme().drag_border)
|
||||
})
|
||||
.on_drop(cx.listener(
|
||||
|this, item: &AnyDrag, _, cx| {
|
||||
this.will_split_placement = None;
|
||||
this.emit_drag_drop(item, None, cx);
|
||||
},
|
||||
))
|
||||
},
|
||||
)
|
||||
}),
|
||||
)
|
||||
}))
|
||||
.last_empty_space(
|
||||
// Empty space to allow dropping at the last tab. Its left
|
||||
// edge (right after the last tab) marks the start of the
|
||||
// title-bar drag overlay.
|
||||
div()
|
||||
.id("tab-bar-empty-space")
|
||||
.h_full()
|
||||
.flex_grow_1()
|
||||
.min_w_16()
|
||||
.on_prepaint({
|
||||
let view = view.clone();
|
||||
move |bounds, _, cx| {
|
||||
view.update(cx, |this, cx| {
|
||||
if this.title_bar_strip_bounds != Some(bounds) {
|
||||
this.title_bar_strip_bounds = Some(bounds);
|
||||
cx.notify();
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
.when(state.droppable, |this| {
|
||||
let view = view.clone();
|
||||
this.drag_over::<DragPanel>(|this, _, _, cx| {
|
||||
this.bg(cx.theme().tokens.drop_target)
|
||||
})
|
||||
.on_drop(cx.listener(
|
||||
move |this, drag: &DragPanel, window, cx| {
|
||||
this.will_split_placement = None;
|
||||
|
||||
let ix = if drag.tab_panel == view {
|
||||
Some(tabs_count - 1)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
this.on_drop(drag, ix, false, window, cx)
|
||||
},
|
||||
))
|
||||
.drag_over::<AnyDrag>(|this, _, _, cx| {
|
||||
this.bg(cx.theme().tokens.drop_target)
|
||||
})
|
||||
.on_drop(
|
||||
cx.listener(|this, item: &AnyDrag, _, cx| {
|
||||
this.will_split_placement = None;
|
||||
this.emit_drag_drop(item, None, cx);
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.when(!self.collapsed, |this| {
|
||||
this.suffix(
|
||||
h_flex()
|
||||
@@ -1249,7 +1123,7 @@ impl TabPanel {
|
||||
|
||||
/// Report a host-owned drag landing on this TabPanel. `placement` is `None`
|
||||
/// to merge into this tab group instead of splitting.
|
||||
fn emit_drag_drop(
|
||||
pub(crate) fn emit_drag_drop(
|
||||
&mut self,
|
||||
item: &AnyDrag,
|
||||
placement: Option<Placement>,
|
||||
@@ -1269,7 +1143,7 @@ impl TabPanel {
|
||||
/// Handle the drop event when dragging a panel
|
||||
///
|
||||
/// - `active` - When true, the panel will be active after the drop
|
||||
fn on_drop(
|
||||
pub(crate) fn on_drop(
|
||||
&mut self,
|
||||
drag: &DragPanel,
|
||||
ix: Option<usize>,
|
||||
@@ -1575,11 +1449,9 @@ impl Render for TabPanel {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::Mutex;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
use gpui::{Axis, TestAppContext, VisualTestContext, WindowHandle, WindowOptions, size};
|
||||
use gpui_component::tab::{Tab, TabBar};
|
||||
use gpui_component::{Root, Theme, v_flex};
|
||||
use gpui::{Axis, TestAppContext, VisualTestContext, WindowHandle};
|
||||
use gpui_component::{Root, Theme};
|
||||
|
||||
use super::*;
|
||||
use crate::DockItem;
|
||||
@@ -2291,140 +2163,9 @@ mod tests {
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ProbeFlags {
|
||||
empty_down: AtomicBool,
|
||||
empty_click: AtomicBool,
|
||||
control_down: AtomicBool,
|
||||
control_click: AtomicBool,
|
||||
}
|
||||
|
||||
struct ProbeView {
|
||||
flags: Entity<ProbeFlags>,
|
||||
}
|
||||
|
||||
impl Render for ProbeView {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let flags = self.flags.clone();
|
||||
|
||||
let empty = title_bar_drag_handlers(
|
||||
div().id("empty-space").h_full().flex_grow_1().min_w_16(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.debug_selector(|| "empty-space".into())
|
||||
.on_mouse_down(MouseButton::Left, {
|
||||
let flags = flags.clone();
|
||||
move |_, _, cx| flags.update(cx, |f, _| f.empty_down.store(true, Ordering::SeqCst))
|
||||
})
|
||||
.on_click({
|
||||
let flags = flags.clone();
|
||||
move |_, _, cx| flags.update(cx, |f, _| f.empty_click.store(true, Ordering::SeqCst))
|
||||
});
|
||||
|
||||
let control =
|
||||
title_bar_drag_handlers(div().id("control-space").h_8().flex_grow_1(), window, cx)
|
||||
.debug_selector(|| "control-space".into())
|
||||
.on_mouse_down(MouseButton::Left, {
|
||||
let flags = flags.clone();
|
||||
move |_, _, cx| {
|
||||
flags.update(cx, |f, _| f.control_down.store(true, Ordering::SeqCst))
|
||||
}
|
||||
})
|
||||
.on_click({
|
||||
let flags = flags.clone();
|
||||
move |_, _, cx| {
|
||||
flags.update(cx, |f, _| f.control_click.store(true, Ordering::SeqCst))
|
||||
}
|
||||
});
|
||||
|
||||
v_flex()
|
||||
.size_full()
|
||||
.child(
|
||||
TabBar::new("probe-bar")
|
||||
.child(Tab::new().label("Alpha"))
|
||||
.child(Tab::new().label("Beta"))
|
||||
.suffix(div())
|
||||
.last_empty_space(empty),
|
||||
)
|
||||
.child(control)
|
||||
}
|
||||
}
|
||||
|
||||
/// Diagnostic: verify that the tab bar's `last_empty_space` element
|
||||
/// receives mouse events when wrapped by `title_bar_drag_handlers`.
|
||||
#[gpui::test]
|
||||
fn tab_bar_empty_space_receives_events(cx: &mut TestAppContext) {
|
||||
let (flags, handle) = cx.update(|cx| {
|
||||
cx.set_global(Theme::default());
|
||||
let flags = cx.new(|_| ProbeFlags::default());
|
||||
let handle = cx.open_window(
|
||||
WindowOptions {
|
||||
window_bounds: Some(gpui::WindowBounds::Windowed(gpui::Bounds {
|
||||
origin: gpui::Point::default(),
|
||||
size: size(px(800.), px(100.)),
|
||||
})),
|
||||
..Default::default()
|
||||
},
|
||||
|window, cx| {
|
||||
let flags = flags.clone();
|
||||
let content = cx.new(|_| ProbeView { flags });
|
||||
cx.new(|cx| Root::new(content, window, cx))
|
||||
},
|
||||
);
|
||||
(flags, handle.unwrap())
|
||||
});
|
||||
let mut cx = VisualTestContext::from_window(handle.into(), cx);
|
||||
|
||||
cx.run_until_parked();
|
||||
cx.update(|window, cx| {
|
||||
_ = window.draw(cx);
|
||||
});
|
||||
|
||||
let empty_bounds = cx
|
||||
.debug_bounds("empty-space")
|
||||
.expect("empty-space must be laid out");
|
||||
let control_bounds = cx
|
||||
.debug_bounds("control-space")
|
||||
.expect("control-space must be laid out");
|
||||
let empty_center = empty_bounds.center();
|
||||
let control_center = control_bounds.center();
|
||||
|
||||
// Control: a plain div with the same handlers, outside the TabBar.
|
||||
cx.simulate_click(control_center, Default::default());
|
||||
// Target: the TabBar's last_empty_space.
|
||||
cx.simulate_click(empty_center, Default::default());
|
||||
|
||||
let (empty_down, empty_click, control_down, control_click) = cx.read(|cx| {
|
||||
let flags = flags.read(cx);
|
||||
(
|
||||
flags.empty_down.load(Ordering::SeqCst),
|
||||
flags.empty_click.load(Ordering::SeqCst),
|
||||
flags.control_down.load(Ordering::SeqCst),
|
||||
flags.control_click.load(Ordering::SeqCst),
|
||||
)
|
||||
});
|
||||
assert!(
|
||||
control_down,
|
||||
"control mouse_down must fire at {control_center:?}"
|
||||
);
|
||||
assert!(
|
||||
control_click,
|
||||
"control click must fire at {control_center:?}"
|
||||
);
|
||||
assert!(
|
||||
empty_down,
|
||||
"empty-space mouse_down must fire at {empty_center:?}"
|
||||
);
|
||||
assert!(
|
||||
empty_click,
|
||||
"empty-space click must fire at {empty_center:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// The title-bar drag overlay must cover the dead zone between the last
|
||||
/// tab and the suffix (TabBar's scrollable content is content-width, so
|
||||
/// that area has no element of its own).
|
||||
/// tab and the suffix (the tab strip's scrollable content is
|
||||
/// content-width, so that area has no element of its own).
|
||||
#[gpui::test]
|
||||
fn title_bar_drag_overlay_covers_the_dead_zone(cx: &mut TestAppContext) {
|
||||
let fixture = setup(cx);
|
||||
|
||||
Reference in New Issue
Block a user