feat: add community ui (#52)
Reviewed-on: #52
This commit was merged in pull request #52.
This commit is contained in:
+176
-59
@@ -24,6 +24,7 @@ use crate::menu::DropdownMenu as _;
|
||||
use crate::resizable::{resize_handle, resize_handle_appearance};
|
||||
use crate::tab::Tab;
|
||||
use crate::tab::tab_bar::TabBar;
|
||||
use crate::title_bar::{TRAFFIC_LIGHT_PADDING, title_bar_drag_handlers, window_controls};
|
||||
use crate::{IconName, Selectable, Sizable, StyledExt, h_flex, v_flex};
|
||||
|
||||
mod panel;
|
||||
@@ -31,12 +32,34 @@ pub use panel::*;
|
||||
|
||||
actions!(dock, [ToggleZoom, ClosePanel]);
|
||||
|
||||
pub type TitleBarRenderer = fn(&mut Window, &mut App) -> AnyElement;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct TitleBarChrome {
|
||||
trailing: Cell<Option<TitleBarRenderer>>,
|
||||
}
|
||||
|
||||
impl TitleBarChrome {
|
||||
pub fn set_trailing(&self, renderer: TitleBarRenderer) {
|
||||
self.trailing.set(Some(renderer));
|
||||
}
|
||||
|
||||
fn trailing(&self, window: &mut Window, cx: &mut App) -> Option<AnyElement> {
|
||||
self.trailing.get().map(|render| render(window, cx))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dock_area(
|
||||
id: impl Into<SharedString>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Entity<DockArea> {
|
||||
let shared = Rc::new(SkinShared::default());
|
||||
) -> (Entity<DockArea>, Rc<TitleBarChrome>) {
|
||||
let chrome = Rc::new(TitleBarChrome::default());
|
||||
let shared = Rc::new(SkinShared {
|
||||
area: RefCell::new(None),
|
||||
resizing: Cell::new(None),
|
||||
chrome: chrome.clone(),
|
||||
});
|
||||
let area = cx.new(|cx| {
|
||||
DockArea::new(id, None, window, cx).with_renderer(Rc::new(DockSkin {
|
||||
shared: shared.clone(),
|
||||
@@ -44,7 +67,7 @@ pub fn dock_area(
|
||||
});
|
||||
|
||||
*shared.area.borrow_mut() = Some(area.downgrade());
|
||||
area
|
||||
(area, chrome)
|
||||
}
|
||||
|
||||
pub fn add_panel(
|
||||
@@ -167,6 +190,7 @@ fn right_top_group(node: &PaneNode) -> Option<NodeId> {
|
||||
struct SkinShared {
|
||||
area: RefCell<Option<WeakEntity<DockArea>>>,
|
||||
resizing: Cell<Option<DockPlacement>>,
|
||||
chrome: Rc<TitleBarChrome>,
|
||||
}
|
||||
|
||||
impl SkinShared {
|
||||
@@ -394,6 +418,53 @@ impl TabGroupSkin {
|
||||
}
|
||||
}
|
||||
|
||||
fn is_title_bar_group(&self, group: &TabGroupContext, cx: &App) -> bool {
|
||||
let Some(area) = self.shared.area() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
area.read(cx)
|
||||
.layout(DockPlacement::Center)
|
||||
.and_then(|tree| left_top_group(tree.root()))
|
||||
== Some(group.node())
|
||||
}
|
||||
|
||||
/// Whether this group is the left dock's root with a single panel.
|
||||
///
|
||||
/// Such a group draws no tab bar, so its panel owns the window's top-left
|
||||
/// corner — including the space the macOS traffic lights overlay.
|
||||
fn is_plain_left_group(&self, group: &TabGroupContext, cx: &App) -> bool {
|
||||
let Some(area) = self.shared.area() else {
|
||||
return false;
|
||||
};
|
||||
let area = area.read(cx);
|
||||
|
||||
area.layout(DockPlacement::Left)
|
||||
.map(|tree| tree.root().id())
|
||||
== Some(group.node())
|
||||
&& group.panels().len() == 1
|
||||
}
|
||||
|
||||
/// Whether this group is the topmost-left group on screen, which sits under
|
||||
/// the native macOS traffic lights. The left dock's group is leftmost while
|
||||
/// it is open and holds a panel; the center's is leftmost otherwise.
|
||||
fn is_leftmost_top_group(&self, group: &TabGroupContext, cx: &App) -> bool {
|
||||
let Some(area) = self.shared.area() else {
|
||||
return false;
|
||||
};
|
||||
let area = area.read(cx);
|
||||
|
||||
let left_open =
|
||||
area.is_dock_open(DockPlacement::Left) && !area.is_empty(DockPlacement::Left, cx);
|
||||
let tree = if left_open {
|
||||
area.layout(DockPlacement::Left)
|
||||
} else {
|
||||
area.layout(DockPlacement::Center)
|
||||
};
|
||||
|
||||
tree.and_then(|tree| left_top_group(tree.root())) == Some(group.node())
|
||||
}
|
||||
|
||||
fn render_toolbar(
|
||||
&self,
|
||||
group: &TabGroupContext,
|
||||
@@ -473,16 +544,19 @@ impl TabGroupSkin {
|
||||
let right_button = self.dock_toggle_button(DockPlacement::Right, group, cx);
|
||||
let has_leading = left_button.is_some() || bottom_button.is_some();
|
||||
let drag = tab_drag(group, ix, cx);
|
||||
let is_title_bar = self.is_title_bar_group(group, cx);
|
||||
let needs_traffic_light_padding =
|
||||
cfg!(target_os = "macos") && self.is_leftmost_top_group(group, cx);
|
||||
let trailing_chrome = is_title_bar
|
||||
.then(|| self.shared.chrome.trailing(window, cx))
|
||||
.flatten();
|
||||
|
||||
h_flex()
|
||||
let bar = h_flex()
|
||||
.id("tab-title-bar")
|
||||
.justify_between()
|
||||
.items_center()
|
||||
.line_height(rems(1.0))
|
||||
.h(TABBAR_HEIGHT)
|
||||
.py_2()
|
||||
.pl_3()
|
||||
.pr_2()
|
||||
.rounded_t(cx.theme().radius_lg)
|
||||
.bg(cx.theme().panel_background)
|
||||
.when(left_button.is_some(), |this| this.pl_2())
|
||||
.when(right_button.is_some(), |this| this.pr_2())
|
||||
@@ -496,12 +570,15 @@ impl TabGroupSkin {
|
||||
.children(bottom_button),
|
||||
)
|
||||
})
|
||||
.when(needs_traffic_light_padding, |this| {
|
||||
this.pl(px(TRAFFIC_LIGHT_PADDING))
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
.id("tab")
|
||||
.flex_1()
|
||||
.flex_initial()
|
||||
.min_w_0()
|
||||
.px_2()
|
||||
.min_w_16()
|
||||
.overflow_hidden()
|
||||
.whitespace_nowrap()
|
||||
.child(
|
||||
@@ -524,6 +601,14 @@ impl TabGroupSkin {
|
||||
})
|
||||
}),
|
||||
)
|
||||
.child({
|
||||
let space = div().id("tab-title-space").flex_1().h_full();
|
||||
if is_title_bar {
|
||||
title_bar_drag_handlers(space, window, cx).into_any_element()
|
||||
} else {
|
||||
space.into_any_element()
|
||||
}
|
||||
})
|
||||
.child(
|
||||
h_flex()
|
||||
.flex_shrink_0()
|
||||
@@ -532,7 +617,18 @@ impl TabGroupSkin {
|
||||
.child(self.render_toolbar(group, window, cx))
|
||||
.children(right_button),
|
||||
)
|
||||
.into_any_element()
|
||||
.when_some(trailing_chrome, |this, chrome| this.child(chrome));
|
||||
|
||||
if is_title_bar {
|
||||
h_flex()
|
||||
.h(TABBAR_HEIGHT)
|
||||
.bg(cx.theme().panel_background)
|
||||
.child(bar.flex_1())
|
||||
.child(window_controls())
|
||||
.into_any_element()
|
||||
} else {
|
||||
bar.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
fn render_tabs(
|
||||
@@ -556,13 +652,41 @@ impl TabGroupSkin {
|
||||
.iter()
|
||||
.position(|panel| panel.panel_id(cx) == displayed)
|
||||
});
|
||||
let is_title_bar = self.is_title_bar_group(group, cx);
|
||||
let needs_traffic_light_padding =
|
||||
cfg!(target_os = "macos") && self.is_leftmost_top_group(group, cx);
|
||||
let trailing_chrome = is_title_bar
|
||||
.then(|| self.shared.chrome.trailing(window, cx))
|
||||
.flatten();
|
||||
let empty_space = div()
|
||||
.id("tab-bar-empty-space")
|
||||
.h_full()
|
||||
.flex_grow_1()
|
||||
.min_w_16()
|
||||
.when(droppable, |this| {
|
||||
this.drag_over::<DragPanel>(|this, _, _, cx| this.bg(cx.theme().surface_background))
|
||||
.on_drop({
|
||||
let group = TabGroupContext::clone(group);
|
||||
move |drag: &DragPanel, window, cx| {
|
||||
let ix = (drag.source() == group.node()).then(|| tabs_count - 1);
|
||||
group.drop_panel(drag.clone(), ix, false, window, cx);
|
||||
}
|
||||
})
|
||||
});
|
||||
let empty_space = if is_title_bar {
|
||||
title_bar_drag_handlers(empty_space, window, cx).into_any_element()
|
||||
} else {
|
||||
empty_space.into_any_element()
|
||||
};
|
||||
|
||||
TabBar::new("tab-bar")
|
||||
let bar = TabBar::new("tab-bar")
|
||||
.track_scroll(&self.scroll_handle)
|
||||
.h(TABBAR_HEIGHT)
|
||||
.bg(cx.theme().panel_background)
|
||||
.rounded_t(cx.theme().radius_lg)
|
||||
.when(has_leading, |this| {
|
||||
.when(needs_traffic_light_padding, |this| {
|
||||
this.pl(px(TRAFFIC_LIGHT_PADDING))
|
||||
})
|
||||
.when(is_title_bar || has_leading, |this| {
|
||||
this.prefix(
|
||||
h_flex()
|
||||
.items_center()
|
||||
@@ -639,26 +763,7 @@ impl TabGroupSkin {
|
||||
})
|
||||
})
|
||||
}))
|
||||
.last_empty_space(
|
||||
// Empty space so a panel can be moved past the last tab.
|
||||
div()
|
||||
.id("tab-bar-empty-space")
|
||||
.h_full()
|
||||
.flex_grow_1()
|
||||
.min_w_16()
|
||||
.when(droppable, |this| {
|
||||
this.drag_over::<DragPanel>(|this, _, _, cx| {
|
||||
this.bg(cx.theme().surface_background)
|
||||
})
|
||||
.on_drop({
|
||||
let group = TabGroupContext::clone(group);
|
||||
move |drag: &DragPanel, window, cx| {
|
||||
let ix = (drag.source() == group.node()).then(|| tabs_count - 1);
|
||||
group.drop_panel(drag.clone(), ix, false, window, cx);
|
||||
}
|
||||
})
|
||||
}),
|
||||
)
|
||||
.last_empty_space(empty_space)
|
||||
.when(!collapsed, |this| {
|
||||
this.suffix(
|
||||
h_flex()
|
||||
@@ -669,10 +774,22 @@ impl TabGroupSkin {
|
||||
.px_0p5()
|
||||
.gap_1()
|
||||
.child(self.render_toolbar(group, window, cx))
|
||||
.children(right_button),
|
||||
.children(right_button)
|
||||
.children(trailing_chrome),
|
||||
)
|
||||
})
|
||||
.into_any_element()
|
||||
});
|
||||
|
||||
if is_title_bar {
|
||||
h_flex()
|
||||
.h(TABBAR_HEIGHT)
|
||||
.w_full()
|
||||
.bg(cx.theme().panel_background)
|
||||
.child(bar.flex_1())
|
||||
.child(window_controls())
|
||||
.into_any_element()
|
||||
} else {
|
||||
bar.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
fn dock_toggle_button(
|
||||
@@ -738,28 +855,23 @@ impl TabGroupSkin {
|
||||
}
|
||||
|
||||
impl TabGroupRenderer for TabGroupSkin {
|
||||
fn frame(&self, group: &TabGroupContext, _: &mut Window, cx: &mut App) -> Stateful<Div> {
|
||||
div()
|
||||
.id("tab-panel")
|
||||
.p_1()
|
||||
.rounded(cx.theme().radius_lg)
|
||||
.when(cx.theme().shadow, |this| this.shadow_xs())
|
||||
.when(!group.is_collapsed(), |this| {
|
||||
this.on_action({
|
||||
let group = TabGroupContext::clone(group);
|
||||
move |_: &ToggleZoom, window, cx| group.toggle_zoom(window, cx)
|
||||
})
|
||||
.on_action({
|
||||
let group = TabGroupContext::clone(group);
|
||||
move |_: &ClosePanel, window, cx| {
|
||||
let Some(panel) = group.active_panel() else {
|
||||
return;
|
||||
};
|
||||
let panel = panel.panel_id(cx);
|
||||
group.close(panel, window, cx);
|
||||
}
|
||||
})
|
||||
fn frame(&self, group: &TabGroupContext, _: &mut Window, _cx: &mut App) -> Stateful<Div> {
|
||||
div().id("tab-panel").when(!group.is_collapsed(), |this| {
|
||||
this.on_action({
|
||||
let group = TabGroupContext::clone(group);
|
||||
move |_: &ToggleZoom, window, cx| group.toggle_zoom(window, cx)
|
||||
})
|
||||
.on_action({
|
||||
let group = TabGroupContext::clone(group);
|
||||
move |_: &ClosePanel, window, cx| {
|
||||
let Some(panel) = group.active_panel() else {
|
||||
return;
|
||||
};
|
||||
let panel = panel.panel_id(cx);
|
||||
group.close(panel, window, cx);
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn render_tab_bar(
|
||||
@@ -768,6 +880,12 @@ impl TabGroupRenderer for TabGroupSkin {
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> AnyElement {
|
||||
// The left dock's only panel draws bare, so its content can own the
|
||||
// window's top-left corner instead of a tab bar doing so.
|
||||
if self.is_plain_left_group(group, cx) {
|
||||
return Empty.into_any_element();
|
||||
}
|
||||
|
||||
let visible: Vec<usize> = group
|
||||
.panels()
|
||||
.iter()
|
||||
@@ -811,7 +929,6 @@ impl TabGroupRenderer for TabGroupSkin {
|
||||
.child(
|
||||
div()
|
||||
.size_full()
|
||||
.rounded_b(cx.theme().radius_lg)
|
||||
.bg(cx.theme().panel_background)
|
||||
.overflow_hidden()
|
||||
.child(panel.cached(StyleRefinement::default().v_flex().size_full())),
|
||||
|
||||
Reference in New Issue
Block a user