feat: out-of-box experience #2
Generated
+1
@@ -1829,6 +1829,7 @@ version = "1.0.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"gpui",
|
"gpui",
|
||||||
|
"gpui-base",
|
||||||
"gpui-component",
|
"gpui-component",
|
||||||
"itertools 0.13.0",
|
"itertools 0.13.0",
|
||||||
"serde",
|
"serde",
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ reqwest_client = { git = "https://github.com/zed-industries/zed" }
|
|||||||
# `tree-sitter-languages` enables syntax highlighting for the TextView
|
# `tree-sitter-languages` enables syntax highlighting for the TextView
|
||||||
# code preview (fenced code blocks are highlighted with tree-sitter).
|
# code preview (fenced code blocks are highlighted with tree-sitter).
|
||||||
gpui-component = { git = "https://github.com/longbridge/gpui-component", features = ["tree-sitter-languages"] }
|
gpui-component = { git = "https://github.com/longbridge/gpui-component", features = ["tree-sitter-languages"] }
|
||||||
|
gpui-base = { git = "https://github.com/longbridge/gpui-component" }
|
||||||
|
|
||||||
dock = { path = "crates/dock" }
|
dock = { path = "crates/dock" }
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ publish.workspace = true
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
gpui-component.workspace = true
|
gpui-component.workspace = true
|
||||||
|
gpui-base.workspace = true
|
||||||
|
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ mod panel;
|
|||||||
mod resize_handle;
|
mod resize_handle;
|
||||||
mod stack_panel;
|
mod stack_panel;
|
||||||
mod state;
|
mod state;
|
||||||
|
mod tab_bar;
|
||||||
mod tab_panel;
|
mod tab_panel;
|
||||||
mod window_controls;
|
mod window_controls;
|
||||||
|
|
||||||
@@ -53,7 +54,7 @@ pub(crate) fn t(key: &'static str) -> &'static str {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The fixed height of the tab bar, which doubles as the window title bar.
|
/// The fixed height of the tab bar, which doubles as the window title bar.
|
||||||
pub(crate) const TAB_BAR_HEIGHT: Pixels = px(44.);
|
pub const TAB_BAR_HEIGHT: Pixels = px(44.);
|
||||||
|
|
||||||
/// A host-owned drag item, dragged into the dock by the application.
|
/// A host-owned drag item, dragged into the dock by the application.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
|||||||
@@ -0,0 +1,469 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use gpui::prelude::FluentBuilder;
|
||||||
|
use gpui::{
|
||||||
|
AnyElement, App, AppContext, ElementId, Entity, InteractiveElement as _, IntoElement,
|
||||||
|
ParentElement as _, RenderOnce, ScrollHandle, StatefulInteractiveElement, Styled as _, Window,
|
||||||
|
div, px,
|
||||||
|
};
|
||||||
|
use gpui_base::{InteractiveElementExt, Tab, Tabs};
|
||||||
|
use gpui_component::{ActiveTheme, ElementExt, h_flex};
|
||||||
|
|
||||||
|
use super::{AnyDrag, DragPanel, PanelView, TabPanel};
|
||||||
|
use crate::TAB_BAR_HEIGHT;
|
||||||
|
|
||||||
|
/// The dock's custom tab bar, built on gpui-base's unstyled [`Tab`]/[`Tabs`].
|
||||||
|
///
|
||||||
|
/// The dock owns the pill presentation; the layout mirrors gpui-component's
|
||||||
|
/// `TabBar`: a prefix (dock toggle / tab navigation), a scrollable tab strip
|
||||||
|
/// with a trailing drop target, then a suffix (panel toolbar).
|
||||||
|
///
|
||||||
|
/// Tabs are draggable to move panels between groups and double as drop
|
||||||
|
/// targets, so panel-level events are forwarded to the owning [`TabPanel`].
|
||||||
|
#[derive(IntoElement)]
|
||||||
|
pub(crate) struct TabBar {
|
||||||
|
id: ElementId,
|
||||||
|
panels: Vec<Arc<dyn PanelView>>,
|
||||||
|
active_panel: Option<Arc<dyn PanelView>>,
|
||||||
|
collapsed: bool,
|
||||||
|
draggable: bool,
|
||||||
|
droppable: bool,
|
||||||
|
tab_panel: Entity<TabPanel>,
|
||||||
|
scroll_handle: ScrollHandle,
|
||||||
|
prefix: Option<AnyElement>,
|
||||||
|
suffix: Option<AnyElement>,
|
||||||
|
empty_space: Option<AnyElement>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TabBar {
|
||||||
|
pub(crate) fn new(id: impl Into<ElementId>, tab_panel: Entity<TabPanel>) -> Self {
|
||||||
|
Self {
|
||||||
|
id: id.into(),
|
||||||
|
panels: Vec::new(),
|
||||||
|
active_panel: None,
|
||||||
|
collapsed: false,
|
||||||
|
draggable: false,
|
||||||
|
droppable: false,
|
||||||
|
tab_panel,
|
||||||
|
scroll_handle: ScrollHandle::new(),
|
||||||
|
prefix: None,
|
||||||
|
suffix: None,
|
||||||
|
empty_space: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The panels to show as tabs, in strip order.
|
||||||
|
pub(crate) fn panels(mut self, panels: Vec<Arc<dyn PanelView>>) -> Self {
|
||||||
|
self.panels = panels;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The currently active panel; its tab is rendered as the filled pill.
|
||||||
|
pub(crate) fn active_panel(mut self, active_panel: Option<Arc<dyn PanelView>>) -> Self {
|
||||||
|
self.active_panel = active_panel;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Collapsed tab panels render no suffix or trailing drop target, and
|
||||||
|
/// their tabs lose the active style and all interactions.
|
||||||
|
pub(crate) fn collapsed(mut self, collapsed: bool) -> Self {
|
||||||
|
self.collapsed = collapsed;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the tabs can start a panel drag.
|
||||||
|
pub(crate) fn draggable(mut self, draggable: bool) -> Self {
|
||||||
|
self.draggable = draggable;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the tabs and trailing space accept drops.
|
||||||
|
pub(crate) fn droppable(mut self, droppable: bool) -> Self {
|
||||||
|
self.droppable = droppable;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Track the strip's scroll state with the given handle, so callers can
|
||||||
|
/// scroll a tab into view with [`ScrollHandle::scroll_to_item`].
|
||||||
|
pub(crate) fn scroll_handle(mut self, scroll_handle: &ScrollHandle) -> Self {
|
||||||
|
self.scroll_handle = scroll_handle.clone();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Element shown before the tab strip.
|
||||||
|
pub(crate) fn prefix(mut self, prefix: impl IntoElement) -> Self {
|
||||||
|
self.prefix = Some(prefix.into_any_element());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Element shown after the tab strip.
|
||||||
|
pub(crate) fn suffix(mut self, suffix: impl IntoElement) -> Self {
|
||||||
|
self.suffix = Some(suffix.into_any_element());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Replace the trailing empty space (the drop target after the last tab).
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) fn empty_space(mut self, empty_space: impl IntoElement) -> Self {
|
||||||
|
self.empty_space = Some(empty_space.into_any_element());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_tab(
|
||||||
|
&self,
|
||||||
|
ix: usize,
|
||||||
|
panel: Arc<dyn PanelView>,
|
||||||
|
active: bool,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut App,
|
||||||
|
) -> Tab {
|
||||||
|
// While collapsed, tabs lose the active style and all interactions.
|
||||||
|
let droppable = self.collapsed;
|
||||||
|
let tab_panel = self.tab_panel.clone();
|
||||||
|
|
||||||
|
// The `ix` element id keeps each tab's identity stable across renders.
|
||||||
|
Tab::new(ix)
|
||||||
|
.h_6()
|
||||||
|
.px_3()
|
||||||
|
.text_sm()
|
||||||
|
.whitespace_nowrap()
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.justify_center()
|
||||||
|
.gap_1()
|
||||||
|
.flex_shrink_0()
|
||||||
|
.overflow_hidden()
|
||||||
|
.rounded(cx.theme().radius)
|
||||||
|
.text_color(cx.theme().foreground)
|
||||||
|
.map(|this| {
|
||||||
|
if let Some(tab_name) = panel.tab_name(cx) {
|
||||||
|
this.child(tab_name)
|
||||||
|
} else {
|
||||||
|
this.child(panel.title(window, cx))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// Pill presentation: the selected tab is the filled pill, the
|
||||||
|
// rest are transparent until hovered.
|
||||||
|
.styles(|styles| {
|
||||||
|
styles.selected(|style| {
|
||||||
|
style
|
||||||
|
.text_color(cx.theme().tab_active_foreground)
|
||||||
|
.bg(cx.theme().tab_active)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.hover(|this| {
|
||||||
|
if active {
|
||||||
|
this
|
||||||
|
} else {
|
||||||
|
this.text_color(cx.theme().secondary_foreground)
|
||||||
|
.bg(cx.theme().secondary_hover)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.selected(active)
|
||||||
|
.on_click(move |_, window, cx| {
|
||||||
|
tab_panel.update(cx, |view, cx| view.set_active_ix(ix, window, cx));
|
||||||
|
})
|
||||||
|
.when(!droppable, |this| {
|
||||||
|
this.when(self.draggable, |this| {
|
||||||
|
this.on_drag(
|
||||||
|
DragPanel::new(panel.clone(), self.tab_panel.clone()),
|
||||||
|
|drag, offset, _, cx| {
|
||||||
|
cx.stop_propagation();
|
||||||
|
drag.drag_offset.set(offset);
|
||||||
|
cx.new(|_| drag.clone())
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.when(self.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({
|
||||||
|
let tab_panel = self.tab_panel.clone();
|
||||||
|
move |drag: &DragPanel, window, cx| {
|
||||||
|
tab_panel.update(cx, |view, cx| {
|
||||||
|
view.will_split_placement = None;
|
||||||
|
view.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({
|
||||||
|
let tab_panel = self.tab_panel.clone();
|
||||||
|
move |item: &AnyDrag, _, cx| {
|
||||||
|
tab_panel.update(cx, |view, cx| {
|
||||||
|
view.will_split_placement = None;
|
||||||
|
view.emit_drag_drop(item, None, cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_empty_space(&self) -> AnyElement {
|
||||||
|
let tabs_count = self.panels.len();
|
||||||
|
|
||||||
|
// The strip after the last tab is a drop target for panels and
|
||||||
|
// host-owned drag items. Its left edge (right after the last tab)
|
||||||
|
// marks the start of the title-bar drag overlay.
|
||||||
|
let mut empty = div()
|
||||||
|
.id("tab-bar-empty-space")
|
||||||
|
.h_full()
|
||||||
|
.flex_grow_1()
|
||||||
|
.min_w_16()
|
||||||
|
.on_prepaint({
|
||||||
|
let view = self.tab_panel.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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if self.droppable {
|
||||||
|
empty = empty
|
||||||
|
.drag_over::<DragPanel>(|this, _, _, cx| this.bg(cx.theme().tokens.drop_target))
|
||||||
|
.on_drop({
|
||||||
|
let view = self.tab_panel.clone();
|
||||||
|
move |drag: &DragPanel, window, cx| {
|
||||||
|
view.update(cx, |this, cx| {
|
||||||
|
this.will_split_placement = None;
|
||||||
|
|
||||||
|
// Dropping a panel from this same tab group onto
|
||||||
|
// the strip moves it after the last tab.
|
||||||
|
let ix = if drag.tab_panel == cx.entity() {
|
||||||
|
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({
|
||||||
|
let view = self.tab_panel.clone();
|
||||||
|
move |item: &AnyDrag, _, cx| {
|
||||||
|
view.update(cx, |this, cx| {
|
||||||
|
this.will_split_placement = None;
|
||||||
|
this.emit_drag_drop(item, None, cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
empty.into_any_element()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderOnce for TabBar {
|
||||||
|
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
|
let tabs: Vec<_> = self
|
||||||
|
.panels
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter_map(|(ix, panel)| {
|
||||||
|
let mut active = self.active_panel.as_ref() == Some(panel);
|
||||||
|
if !panel.visible(cx) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
// Always not show active tab style, if the panel is collapsed
|
||||||
|
if self.collapsed {
|
||||||
|
active = false;
|
||||||
|
}
|
||||||
|
Some(self.render_tab(ix, panel.clone(), active, window, cx))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let empty_space = match self.empty_space {
|
||||||
|
Some(empty_space) => empty_space,
|
||||||
|
None => self.render_empty_space(),
|
||||||
|
};
|
||||||
|
|
||||||
|
Tabs::new(self.id)
|
||||||
|
.px(px(-1.))
|
||||||
|
.h(TAB_BAR_HEIGHT)
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.text_color(cx.theme().tab_foreground)
|
||||||
|
.when_some(self.prefix, |this, prefix| this.child(prefix))
|
||||||
|
.child(
|
||||||
|
h_flex().id("tabs").flex_1().overflow_x_hidden().child(
|
||||||
|
h_flex()
|
||||||
|
.id("tabs-inner")
|
||||||
|
.relative()
|
||||||
|
.gap(px(4.))
|
||||||
|
.overflow_x_scroll()
|
||||||
|
.lock_scroll_axis()
|
||||||
|
.track_scroll(&self.scroll_handle)
|
||||||
|
.children(tabs)
|
||||||
|
.when(!self.collapsed, |this| this.child(empty_space)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.when_some(self.suffix, |this, suffix| {
|
||||||
|
this.when(!self.collapsed, |this| this.child(suffix))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
|
||||||
|
use gpui::{
|
||||||
|
Context, Entity, MouseButton, Render, TestAppContext, VisualTestContext, WindowOptions,
|
||||||
|
div, px, size,
|
||||||
|
};
|
||||||
|
use gpui_component::{Root, Theme, v_flex};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use crate::DockArea;
|
||||||
|
use crate::tab_panel::title_bar_drag_handlers;
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct ProbeFlags {
|
||||||
|
empty_down: AtomicBool,
|
||||||
|
empty_click: AtomicBool,
|
||||||
|
control_down: AtomicBool,
|
||||||
|
control_click: AtomicBool,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ProbeView {
|
||||||
|
flags: Entity<ProbeFlags>,
|
||||||
|
tab_panel: Entity<TabPanel>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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(TAB_BAR_HEIGHT)
|
||||||
|
.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", self.tab_panel.clone()).empty_space(empty))
|
||||||
|
.child(control)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Diagnostic: verify that the tab bar's trailing empty space receives
|
||||||
|
/// mouse events when wrapped by `title_bar_drag_handlers`, i.e. the
|
||||||
|
/// strip's scroll containers do not swallow them.
|
||||||
|
#[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 dock_area = cx.new(|cx| DockArea::new("probe-dock", None, window, cx));
|
||||||
|
let tab_panel =
|
||||||
|
cx.new(|cx| TabPanel::new(None, dock_area.downgrade(), window, cx));
|
||||||
|
let content = cx.new(|_| ProbeView { flags, tab_panel });
|
||||||
|
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 tab bar.
|
||||||
|
cx.simulate_click(control_center, Default::default());
|
||||||
|
// Target: the tab bar's empty-space strip.
|
||||||
|
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:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+21
-280
@@ -16,17 +16,17 @@ use gpui::{
|
|||||||
use gpui_component::animation::{Lerp, ease_out_cubic};
|
use gpui_component::animation::{Lerp, ease_out_cubic};
|
||||||
use gpui_component::button::{Button, ButtonVariants as _};
|
use gpui_component::button::{Button, ButtonVariants as _};
|
||||||
use gpui_component::menu::{DropdownMenu, PopupMenu};
|
use gpui_component::menu::{DropdownMenu, PopupMenu};
|
||||||
use gpui_component::tab::{Tab, TabBar};
|
|
||||||
use gpui_component::{
|
use gpui_component::{
|
||||||
ActiveTheme, AxisExt, Disableable, ElementExt, IconName, Placement, Selectable, Sizable,
|
ActiveTheme, AxisExt, Disableable, ElementExt, IconName, Placement, Selectable, Sizable,
|
||||||
h_flex, v_flex,
|
h_flex, v_flex,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use super::tab_bar::TabBar;
|
||||||
use super::{
|
use super::{
|
||||||
AnyDrag, ClosePanel, DockArea, DockEvent, DockPlacement, DropTarget, Panel, PanelControl,
|
AnyDrag, ClosePanel, DockArea, DockEvent, DockPlacement, DropTarget, Panel, PanelControl,
|
||||||
PanelEvent, PanelState, PanelView, StackPanel, ToggleZoom,
|
PanelEvent, PanelState, PanelView, StackPanel, ToggleZoom,
|
||||||
};
|
};
|
||||||
use crate::{PanelInfo, TAB_BAR_HEIGHT, t, window_controls};
|
use crate::{PanelInfo, t, window_controls};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct TabState {
|
struct TabState {
|
||||||
@@ -41,7 +41,7 @@ struct TabState {
|
|||||||
pub(crate) struct DragPanel {
|
pub(crate) struct DragPanel {
|
||||||
pub(crate) panel: Arc<dyn PanelView>,
|
pub(crate) panel: Arc<dyn PanelView>,
|
||||||
pub(crate) tab_panel: Entity<TabPanel>,
|
pub(crate) tab_panel: Entity<TabPanel>,
|
||||||
drag_offset: Rc<Cell<Point<Pixels>>>,
|
pub(crate) drag_offset: Rc<Cell<Point<Pixels>>>,
|
||||||
drag_session_id: u64,
|
drag_session_id: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,7 +150,7 @@ pub struct TabPanel {
|
|||||||
zoomed: bool,
|
zoomed: bool,
|
||||||
collapsed: bool,
|
collapsed: bool,
|
||||||
/// When drag move, will get the placement of the panel to be split
|
/// 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: Option<DropPlaceholderAnimation>,
|
||||||
drop_placeholder_animation_name: SharedString,
|
drop_placeholder_animation_name: SharedString,
|
||||||
|
|
||||||
@@ -160,7 +160,7 @@ pub struct TabPanel {
|
|||||||
title_bar_bounds: Option<Bounds<Pixels>>,
|
title_bar_bounds: Option<Bounds<Pixels>>,
|
||||||
/// Bounds of the tab bar's trailing empty space (right after the last
|
/// Bounds of the tab bar's trailing empty space (right after the last
|
||||||
/// tab), which marks where the draggable region starts.
|
/// 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
|
/// Bounds of the tab bar's suffix (toolbar) area, which marks where the
|
||||||
/// draggable region ends.
|
/// draggable region ends.
|
||||||
title_bar_suffix_bounds: Option<Bounds<Pixels>>,
|
title_bar_suffix_bounds: Option<Bounds<Pixels>>,
|
||||||
@@ -342,7 +342,7 @@ impl TabPanel {
|
|||||||
self.active_ix
|
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 {
|
if ix == self.active_ix {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -845,10 +845,8 @@ impl TabPanel {
|
|||||||
self.tab_bar_scroll_handle.scroll_to_item(visible_ix);
|
self.tab_bar_scroll_handle.scroll_to_item(visible_ix);
|
||||||
}
|
}
|
||||||
|
|
||||||
let tabs_count = self.panels.len();
|
// 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
|
||||||
// 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
|
|
||||||
// tab bar has no element at all. Cover that dead zone with a
|
// tab bar has no element at all. Cover that dead zone with a
|
||||||
// measured overlay so the whole non-interactive area can drag the
|
// measured overlay so the whole non-interactive area can drag the
|
||||||
// window. Its span is [last tab's right edge, suffix's left edge].
|
// window. Its span is [last tab's right edge, suffix's left edge].
|
||||||
@@ -898,11 +896,13 @@ impl TabPanel {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.child(
|
.child(
|
||||||
TabBar::new("tab-bar")
|
TabBar::new("tab-bar", cx.entity())
|
||||||
.h(TAB_BAR_HEIGHT)
|
.panels(self.panels.clone())
|
||||||
.pill()
|
.active_panel(state.active_panel.clone())
|
||||||
.small()
|
.collapsed(self.collapsed)
|
||||||
.track_scroll(&self.tab_bar_scroll_handle)
|
.draggable(state.draggable)
|
||||||
|
.droppable(state.droppable)
|
||||||
|
.scroll_handle(&self.tab_bar_scroll_handle)
|
||||||
.prefix(
|
.prefix(
|
||||||
h_flex()
|
h_flex()
|
||||||
.items_center()
|
.items_center()
|
||||||
@@ -916,132 +916,6 @@ impl TabPanel {
|
|||||||
.children(left_dock_button)
|
.children(left_dock_button)
|
||||||
.child(self.render_prev_next_tab_buttons(cx)),
|
.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| {
|
.when(!self.collapsed, |this| {
|
||||||
this.suffix(
|
this.suffix(
|
||||||
h_flex()
|
h_flex()
|
||||||
@@ -1249,7 +1123,7 @@ impl TabPanel {
|
|||||||
|
|
||||||
/// Report a host-owned drag landing on this TabPanel. `placement` is `None`
|
/// Report a host-owned drag landing on this TabPanel. `placement` is `None`
|
||||||
/// to merge into this tab group instead of splitting.
|
/// to merge into this tab group instead of splitting.
|
||||||
fn emit_drag_drop(
|
pub(crate) fn emit_drag_drop(
|
||||||
&mut self,
|
&mut self,
|
||||||
item: &AnyDrag,
|
item: &AnyDrag,
|
||||||
placement: Option<Placement>,
|
placement: Option<Placement>,
|
||||||
@@ -1269,7 +1143,7 @@ impl TabPanel {
|
|||||||
/// Handle the drop event when dragging a panel
|
/// Handle the drop event when dragging a panel
|
||||||
///
|
///
|
||||||
/// - `active` - When true, the panel will be active after the drop
|
/// - `active` - When true, the panel will be active after the drop
|
||||||
fn on_drop(
|
pub(crate) fn on_drop(
|
||||||
&mut self,
|
&mut self,
|
||||||
drag: &DragPanel,
|
drag: &DragPanel,
|
||||||
ix: Option<usize>,
|
ix: Option<usize>,
|
||||||
@@ -1575,11 +1449,9 @@ impl Render for TabPanel {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
|
||||||
|
|
||||||
use gpui::{Axis, TestAppContext, VisualTestContext, WindowHandle, WindowOptions, size};
|
use gpui::{Axis, TestAppContext, VisualTestContext, WindowHandle};
|
||||||
use gpui_component::tab::{Tab, TabBar};
|
use gpui_component::{Root, Theme};
|
||||||
use gpui_component::{Root, Theme, v_flex};
|
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::DockItem;
|
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
|
/// 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
|
/// tab and the suffix (the tab strip's scrollable content is
|
||||||
/// that area has no element of its own).
|
/// content-width, so that area has no element of its own).
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
fn title_bar_drag_overlay_covers_the_dead_zone(cx: &mut TestAppContext) {
|
fn title_bar_drag_overlay_covers_the_dead_zone(cx: &mut TestAppContext) {
|
||||||
let fixture = setup(cx);
|
let fixture = setup(cx);
|
||||||
|
|||||||
@@ -6,13 +6,13 @@
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use dock::{Panel, PanelEvent};
|
||||||
use gpui::prelude::*;
|
use gpui::prelude::*;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, App, Context, Entity, EventEmitter, FocusHandle, Focusable, Pixels, Render,
|
AnyElement, App, Context, Entity, EventEmitter, FocusHandle, Focusable, Pixels, Render,
|
||||||
ScrollStrategy, SharedString, Size, WeakEntity, Window, div, px, size,
|
ScrollStrategy, SharedString, Size, WeakEntity, Window, div, px, size,
|
||||||
};
|
};
|
||||||
use gpui_component::clipboard::Clipboard;
|
use gpui_component::clipboard::Clipboard;
|
||||||
use dock::{Panel, PanelEvent};
|
|
||||||
use gpui_component::list::ListItem;
|
use gpui_component::list::ListItem;
|
||||||
use gpui_component::resizable::{resizable_panel, v_resizable};
|
use gpui_component::resizable::{resizable_panel, v_resizable};
|
||||||
use gpui_component::scroll::{ScrollableElement, Scrollbar};
|
use gpui_component::scroll::{ScrollableElement, Scrollbar};
|
||||||
@@ -486,8 +486,6 @@ impl CommitDiffView {
|
|||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.px_4()
|
.px_4()
|
||||||
.pt_2()
|
|
||||||
.pb_4()
|
|
||||||
.w_full()
|
.w_full()
|
||||||
.gap_4()
|
.gap_4()
|
||||||
.child(
|
.child(
|
||||||
@@ -609,8 +607,8 @@ impl Render for CommitDiffView {
|
|||||||
v_resizable("commit-diff")
|
v_resizable("commit-diff")
|
||||||
.child(
|
.child(
|
||||||
resizable_panel()
|
resizable_panel()
|
||||||
.size(px(170.))
|
.size(px(180.))
|
||||||
.size_range(px(100.)..px(400.))
|
.size_range(px(120.)..px(420.))
|
||||||
.flex_none()
|
.flex_none()
|
||||||
.bg(cx.theme().background)
|
.bg(cx.theme().background)
|
||||||
.child(self.render_header(cx)),
|
.child(self.render_header(cx)),
|
||||||
|
|||||||
@@ -941,8 +941,7 @@ impl RepoDetailView {
|
|||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.px_4()
|
.px_4()
|
||||||
.pt_2()
|
.pb_4()
|
||||||
.pb_2()
|
|
||||||
.w_full()
|
.w_full()
|
||||||
.gap_8()
|
.gap_8()
|
||||||
.border_b_1()
|
.border_b_1()
|
||||||
@@ -1213,16 +1212,19 @@ impl Render for RepoDetailView {
|
|||||||
v_flex()
|
v_flex()
|
||||||
.id("repo")
|
.id("repo")
|
||||||
.size_full()
|
.size_full()
|
||||||
|
.gap_4()
|
||||||
.child(self.render_header(cx))
|
.child(self.render_header(cx))
|
||||||
.child(match self.active_tab {
|
.map(|this| match self.active_tab {
|
||||||
0 => h_flex()
|
0 => this.child(
|
||||||
.flex_1()
|
h_flex()
|
||||||
.w_full()
|
.flex_1()
|
||||||
.overflow_hidden()
|
.w_full()
|
||||||
.child(Self::render_tree_column(tree_state, view, cx))
|
.overflow_hidden()
|
||||||
.child(self.render_content_column(pane_title, cx))
|
.child(Self::render_tree_column(tree_state, view, cx))
|
||||||
.into_any_element(),
|
.child(self.render_content_column(pane_title, cx))
|
||||||
_ => self.render_commits_tab(cx),
|
.into_any_element(),
|
||||||
|
),
|
||||||
|
_ => this.child(self.render_commits_tab(cx)),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use assets::CustomIconName;
|
use assets::CustomIconName;
|
||||||
use dock::{DockArea, DockPlacement, Panel, PanelEvent, title_bar_drag_handlers};
|
use dock::{DockArea, DockPlacement, Panel, PanelEvent, TAB_BAR_HEIGHT, title_bar_drag_handlers};
|
||||||
use gpui::prelude::*;
|
use gpui::prelude::*;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
App, ClickEvent, Context, ElementId, EventEmitter, FocusHandle, Focusable, Render,
|
App, ClickEvent, Context, ElementId, EventEmitter, FocusHandle, Focusable, Render,
|
||||||
@@ -11,7 +11,7 @@ use gpui_component::avatar::Avatar;
|
|||||||
use gpui_component::button::{Button, ButtonVariants};
|
use gpui_component::button::{Button, ButtonVariants};
|
||||||
use gpui_component::input::InputState;
|
use gpui_component::input::InputState;
|
||||||
use gpui_component::{ActiveTheme, Icon, IconName, Sizable, StyledExt, h_flex, v_flex};
|
use gpui_component::{ActiveTheme, Icon, IconName, Sizable, StyledExt, h_flex, v_flex};
|
||||||
use signed_state::{Backend, BackendEvent, ProfileStore};
|
use signed_state::{Backend, BackendEvent, Profile, ProfileStore};
|
||||||
|
|
||||||
use super::RepoListView;
|
use super::RepoListView;
|
||||||
|
|
||||||
@@ -100,6 +100,42 @@ impl SidebarPanel {
|
|||||||
fn open_import(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
fn open_import(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
import_dialog::open(window, cx);
|
import_dialog::open(window, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Render the user avatar and name in the sidebar, wrapped in the window titlebar drag area.
|
||||||
|
fn render_user(
|
||||||
|
&self,
|
||||||
|
profile: &Profile,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> impl IntoElement {
|
||||||
|
let name = profile.name();
|
||||||
|
let picture = profile.picture();
|
||||||
|
|
||||||
|
title_bar_drag_handlers(
|
||||||
|
h_flex()
|
||||||
|
.id("user")
|
||||||
|
.h(TAB_BAR_HEIGHT)
|
||||||
|
.when(cfg!(target_os = "macos"), |this| this.pl(px(80.)))
|
||||||
|
.child(
|
||||||
|
div().child(
|
||||||
|
Button::new("user").text().dropdown_caret(true).child(
|
||||||
|
h_flex()
|
||||||
|
.gap_1()
|
||||||
|
.child(
|
||||||
|
Avatar::new()
|
||||||
|
.name(name.clone())
|
||||||
|
.when_some(picture, |this, url| this.src(url))
|
||||||
|
.small()
|
||||||
|
.border_0(),
|
||||||
|
)
|
||||||
|
.child(div().text_xs().font_semibold().child(name)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Panel for SidebarPanel {
|
impl Panel for SidebarPanel {
|
||||||
@@ -180,30 +216,7 @@ impl Render for SidebarPanel {
|
|||||||
div()
|
div()
|
||||||
.flex_1()
|
.flex_1()
|
||||||
.when_some(profile.as_ref(), |this, profile| {
|
.when_some(profile.as_ref(), |this, profile| {
|
||||||
let name = profile.name();
|
this.child(self.render_user(profile, window, cx))
|
||||||
let picture = profile.picture();
|
|
||||||
|
|
||||||
this.child(title_bar_drag_handlers(
|
|
||||||
h_flex()
|
|
||||||
.id("sidebar-profile")
|
|
||||||
.h_12()
|
|
||||||
.px_3()
|
|
||||||
.gap_2()
|
|
||||||
// The macOS traffic lights overlay the
|
|
||||||
// top-left corner of the window; keep the
|
|
||||||
// profile content clear of them.
|
|
||||||
.when(cfg!(target_os = "macos"), |this| this.pl(px(80.)))
|
|
||||||
.child(
|
|
||||||
Avatar::new()
|
|
||||||
.name(name.clone())
|
|
||||||
.when_some(picture, |this, url| this.src(url))
|
|
||||||
.small()
|
|
||||||
.border_0(),
|
|
||||||
)
|
|
||||||
.child(div().text_sm().child(name)),
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
))
|
|
||||||
})
|
})
|
||||||
.child(
|
.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
|
|||||||
+6
-1
@@ -1,6 +1,7 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use assets::Assets;
|
use assets::Assets;
|
||||||
|
use dock::TAB_BAR_HEIGHT;
|
||||||
use gpui::*;
|
use gpui::*;
|
||||||
use gpui_component::theme;
|
use gpui_component::theme;
|
||||||
use gpui_platform::application;
|
use gpui_platform::application;
|
||||||
@@ -53,7 +54,11 @@ fn main() {
|
|||||||
app_id: Some("Signed".to_owned()),
|
app_id: Some("Signed".to_owned()),
|
||||||
titlebar: Some(TitlebarOptions {
|
titlebar: Some(TitlebarOptions {
|
||||||
title: Some(SharedString::new_static("Signed")),
|
title: Some(SharedString::new_static("Signed")),
|
||||||
traffic_light_position: Some(point(px(9.0), px(9.0))),
|
// AppKit's traffic-light buttons are 14 pt tall; offset them so their vertical center matches the tab bar.
|
||||||
|
traffic_light_position: Some(point(
|
||||||
|
px(9.0),
|
||||||
|
px(TAB_BAR_HEIGHT / px(2.) - 14. / 2.),
|
||||||
|
)),
|
||||||
appears_transparent: true,
|
appears_transparent: true,
|
||||||
}),
|
}),
|
||||||
app_owns_titlebar_drag: true,
|
app_owns_titlebar_drag: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user