fix(ui): apply compact density to title bar and tabs (#194)
Apply compact density consistently to the title bar, tabs, sidebar scrolling and drag geometry. Keep native title-bar dimensions and align the command overlay with the effective title-bar height. Includes layout, hit-target and density-transition regressions. Fixes #327.
This commit is contained in:
1 parent
bf2ed9af9b
commit
ff03f76655
8 files changed
+201
-68
No files matched your search
@@ -363,10 +363,6 @@ fn dock_tree(target: SplitTree<u64>, source: SplitTree<u64>, nav: SplitNav) -> S
|
|||||||
target.joined(source, nav)
|
target.joined(source, nav)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 侧栏 tab 行高与行距(与 `render_sidebar` 的 `h(px(TAB_ROW_H))`、
|
|
||||||
/// `gap_2`(8px) 同源);受约束拖拽按此步距换算让位槽位。
|
|
||||||
pub(super) const TAB_ROW_H: f32 = 34.0;
|
|
||||||
pub(super) const TAB_ROW_PITCH: f32 = TAB_ROW_H + 8.0;
|
|
||||||
/// 右侧抽屉槽位宽度 = 抽屉自身宽度。抽屉贴满右侧整条竖带(上下右都不留卡缝,
|
/// 右侧抽屉槽位宽度 = 抽屉自身宽度。抽屉贴满右侧整条竖带(上下右都不留卡缝,
|
||||||
/// 左侧直接抵住终端卡),所以槽位里不再有额外的卡缝要算进来。
|
/// 左侧直接抵住终端卡),所以槽位里不再有额外的卡缝要算进来。
|
||||||
|
|
||||||
@@ -628,8 +624,9 @@ pub struct NebulaWorkspace {
|
|||||||
sidebar_collapsed: bool,
|
sidebar_collapsed: bool,
|
||||||
/// 只折叠 TABS 分区,不影响整个左栏;与旧壳分区标题的 chevron 同义。
|
/// 只折叠 TABS 分区,不影响整个左栏;与旧壳分区标题的 chevron 同义。
|
||||||
tabs_section_collapsed: bool,
|
tabs_section_collapsed: bool,
|
||||||
/// 标签栏布局:默认沿用左侧栏;Top 将同一组 tab 放进 48px 标题栏。
|
/// 标签栏布局:默认沿用左侧栏;Top 将同一组 tab 放进标题栏。
|
||||||
tabs_position: nebula_settings::TabsPositionName,
|
tabs_position: nebula_settings::TabsPositionName,
|
||||||
|
density: nebula_settings::DensityName,
|
||||||
/// 运行时持久化的侧栏逻辑宽;布局、初始窗口和折叠动画必须同源。
|
/// 运行时持久化的侧栏逻辑宽;布局、初始窗口和折叠动画必须同源。
|
||||||
sidebar_width: f32,
|
sidebar_width: f32,
|
||||||
/// 首次手动切换后才启用折叠动画:启动帧保持静止落位(旧壳同感,
|
/// 首次手动切换后才启用折叠动画:启动帧保持静止落位(旧壳同感,
|
||||||
@@ -930,6 +927,7 @@ impl NebulaWorkspace {
|
|||||||
sidebar_collapsed: false,
|
sidebar_collapsed: false,
|
||||||
tabs_section_collapsed: false,
|
tabs_section_collapsed: false,
|
||||||
tabs_position: runtime.tabs_position,
|
tabs_position: runtime.tabs_position,
|
||||||
|
density: runtime.density,
|
||||||
sidebar_width,
|
sidebar_width,
|
||||||
sidebar_fold_armed: false,
|
sidebar_fold_armed: false,
|
||||||
tabs_fold_armed: false,
|
tabs_fold_armed: false,
|
||||||
@@ -1139,6 +1137,8 @@ impl NebulaWorkspace {
|
|||||||
crate::gpui_shell::apply_app_icon(runtime.app_icon, cx);
|
crate::gpui_shell::apply_app_icon(runtime.app_icon, cx);
|
||||||
self.sidebar_width = runtime.sidebar_width;
|
self.sidebar_width = runtime.sidebar_width;
|
||||||
self.tabs_position = runtime.tabs_position;
|
self.tabs_position = runtime.tabs_position;
|
||||||
|
self.density = runtime.density;
|
||||||
|
self.reveal_active_tab();
|
||||||
self.sync_settings_layout();
|
self.sync_settings_layout();
|
||||||
self.sidebar_resizing = None;
|
self.sidebar_resizing = None;
|
||||||
self.reveal_if_tray_disabled(cx);
|
self.reveal_if_tray_disabled(cx);
|
||||||
|
|||||||
@@ -16,8 +16,6 @@ const PANEL_EMPTY_HEIGHT: f32 = 196.0;
|
|||||||
const PANEL_FIXED_HEIGHT: f32 = 146.0;
|
const PANEL_FIXED_HEIGHT: f32 = 146.0;
|
||||||
const GROUP_NAV_HEIGHT: f32 = 36.0;
|
const GROUP_NAV_HEIGHT: f32 = 36.0;
|
||||||
const PANEL_MARGIN: f32 = 8.0;
|
const PANEL_MARGIN: f32 = 8.0;
|
||||||
// 覆盖层从自绘标题栏下沿开始;固定组件依赖当前将该区域定义为 34px。
|
|
||||||
const WINDOW_TITLE_BAR_HEIGHT: f32 = 34.0;
|
|
||||||
const PANEL_FOOTER_HEIGHT: f32 = 44.0;
|
const PANEL_FOOTER_HEIGHT: f32 = 44.0;
|
||||||
const ROW_HEIGHT: f32 = 62.0;
|
const ROW_HEIGHT: f32 = 62.0;
|
||||||
const ROW_ICON_SIZE: f32 = 16.0;
|
const ROW_ICON_SIZE: f32 = 16.0;
|
||||||
@@ -559,8 +557,12 @@ impl NebulaWorkspace {
|
|||||||
let viewport = window.viewport_size();
|
let viewport = window.viewport_size();
|
||||||
let panel_width =
|
let panel_width =
|
||||||
PANEL_MAX_WIDTH.min((f32::from(viewport.width) - PANEL_MARGIN * 2.0).max(0.0));
|
PANEL_MAX_WIDTH.min((f32::from(viewport.width) - PANEL_MARGIN * 2.0).max(0.0));
|
||||||
|
let title_bar_height = super::window_titlebar::effective_title_bar_height(
|
||||||
|
self.density,
|
||||||
|
crate::platform::window_chrome::layout(window),
|
||||||
|
);
|
||||||
let available_height =
|
let available_height =
|
||||||
(f32::from(viewport.height) - WINDOW_TITLE_BAR_HEIGHT - PANEL_MARGIN * 2.0).max(0.0);
|
(f32::from(viewport.height) - title_bar_height - PANEL_MARGIN * 2.0).max(0.0);
|
||||||
|
|
||||||
let rows = self.command_manager_rows(cx);
|
let rows = self.command_manager_rows(cx);
|
||||||
self.command_manager_selected =
|
self.command_manager_selected =
|
||||||
@@ -688,7 +690,7 @@ impl NebulaWorkspace {
|
|||||||
.child(
|
.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
.absolute()
|
.absolute()
|
||||||
.top(px(WINDOW_TITLE_BAR_HEIGHT + PANEL_MARGIN))
|
.top(px(title_bar_height + PANEL_MARGIN))
|
||||||
.right(px(PANEL_MARGIN))
|
.right(px(PANEL_MARGIN))
|
||||||
.w(px(panel_width))
|
.w(px(panel_width))
|
||||||
.h(px(panel_height))
|
.h(px(panel_height))
|
||||||
|
|||||||
@@ -160,3 +160,53 @@ fn closing_last_regular_tab_keeps_settings_until_it_is_closed(cx: &mut TestAppCo
|
|||||||
cx.simulate_keystrokes("ctrl-shift-w");
|
cx.simulate_keystrokes("ctrl-shift-w");
|
||||||
assert!(cx.read(|cx| cx.windows().is_empty()));
|
assert!(cx.read(|cx| cx.windows().is_empty()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
fn density_changes_real_tab_bounds_and_sidebar_drag_pitch(cx: &mut TestAppContext) {
|
||||||
|
use nebula_settings::{DensityName, TabsPositionName};
|
||||||
|
|
||||||
|
let (_directory, workspace, mut cx) = open_workspace(8, cx);
|
||||||
|
click_tab("top-tab-0", &mut cx);
|
||||||
|
for position in [TabsPositionName::Sidebar, TabsPositionName::Top] {
|
||||||
|
for (density, height, gap, title_height) in [
|
||||||
|
(DensityName::Standard, 34.0, 8.0, 48.0),
|
||||||
|
(DensityName::Compact, 32.0, 4.0, 40.0),
|
||||||
|
(DensityName::Standard, 34.0, 8.0, 48.0),
|
||||||
|
] {
|
||||||
|
workspace.update(&mut cx, |workspace, cx| {
|
||||||
|
workspace.tabs_position = position;
|
||||||
|
workspace.density = density;
|
||||||
|
workspace.sync_settings_layout();
|
||||||
|
workspace.reveal_active_tab();
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
cx.run_until_parked();
|
||||||
|
let sidebar = position == TabsPositionName::Sidebar;
|
||||||
|
let first = tab_bounds(if sidebar { "sidebar-tab-0" } else { "top-tab-0" }, &mut cx);
|
||||||
|
let second = tab_bounds(if sidebar { "sidebar-tab-1" } else { "top-tab-1" }, &mut cx);
|
||||||
|
assert_eq!(f32::from(first.size.height), height);
|
||||||
|
assert_eq!(f32::from(second.size.height), height);
|
||||||
|
if sidebar {
|
||||||
|
assert_eq!(f32::from(second.top() - first.top()), height + gap);
|
||||||
|
}
|
||||||
|
let native = cx.update(|window, _| crate::platform::window_chrome::layout(window));
|
||||||
|
let title = tab_bounds("workspace-titlebar", &mut cx);
|
||||||
|
assert_eq!(
|
||||||
|
f32::from(title.size.height),
|
||||||
|
native.map_or(title_height, |layout| layout.0)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Padding belongs to the tab's actual click target in either density.
|
||||||
|
let point = gpui::point(second.left() + px(8.0), second.bottom() - px(2.0));
|
||||||
|
cx.simulate_click(point, Modifiers::default());
|
||||||
|
workspace.read_with(&cx, |workspace, _| assert_eq!(workspace.active, 1));
|
||||||
|
if sidebar {
|
||||||
|
cx.simulate_mouse_down(point, gpui::MouseButton::Left, Modifiers::default());
|
||||||
|
workspace.read_with(&cx, |workspace, _| {
|
||||||
|
assert_eq!(workspace.tab_drag.as_ref().unwrap().pitch, height + gap);
|
||||||
|
});
|
||||||
|
cx.simulate_mouse_up(point, gpui::MouseButton::Left, Modifiers::default());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -187,6 +187,8 @@ impl NebulaWorkspace {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn render_sidebar(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
fn render_sidebar(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
let row_height = tab_scroll::tab_row_height(self.density);
|
||||||
|
let row_pitch = self.tab_row_pitch();
|
||||||
let theme = cx.theme();
|
let theme = cx.theme();
|
||||||
let muted = theme.muted_foreground;
|
let muted = theme.muted_foreground;
|
||||||
// 数量 chip 数字:旧壳独用 ink_faint(比 ink_dim 再暗一档),chip 背
|
// 数量 chip 数字:旧壳独用 ink_faint(比 ink_dim 再暗一档),chip 背
|
||||||
@@ -330,10 +332,10 @@ impl NebulaWorkspace {
|
|||||||
let (dragged, shift) = match drag {
|
let (dragged, shift) = match drag {
|
||||||
Some((src, _, _)) if ix == src => (true, 0.0),
|
Some((src, _, _)) if ix == src => (true, 0.0),
|
||||||
Some((src, tgt, _)) if src < tgt && ix > src && ix <= tgt => {
|
Some((src, tgt, _)) if src < tgt && ix > src && ix <= tgt => {
|
||||||
(false, -TAB_ROW_PITCH)
|
(false, -row_pitch)
|
||||||
},
|
},
|
||||||
Some((src, tgt, _)) if src > tgt && ix >= tgt && ix < src => {
|
Some((src, tgt, _)) if src > tgt && ix >= tgt && ix < src => {
|
||||||
(false, TAB_ROW_PITCH)
|
(false, row_pitch)
|
||||||
},
|
},
|
||||||
_ => (false, 0.0),
|
_ => (false, 0.0),
|
||||||
};
|
};
|
||||||
@@ -359,7 +361,7 @@ impl NebulaWorkspace {
|
|||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.px_2()
|
.px_2()
|
||||||
.h(px(TAB_ROW_H))
|
.h(px(row_height))
|
||||||
.items_center()
|
.items_center()
|
||||||
// 旧壳 pill 圆角 = UI_CORNER_RADIUS_LOGICAL(8),rounded_md(6)
|
// 旧壳 pill 圆角 = UI_CORNER_RADIUS_LOGICAL(8),rounded_md(6)
|
||||||
// 偏小一圈,选中水洗的轮廓形状会不一样。
|
// 偏小一圈,选中水洗的轮廓形状会不一样。
|
||||||
@@ -389,7 +391,7 @@ impl NebulaWorkspace {
|
|||||||
press_x: f32::from(event.position.x),
|
press_x: f32::from(event.position.x),
|
||||||
press_y: f32::from(event.position.y),
|
press_y: f32::from(event.position.y),
|
||||||
axis: TabDragAxis::Vertical,
|
axis: TabDragAxis::Vertical,
|
||||||
pitch: TAB_ROW_PITCH,
|
pitch: row_pitch,
|
||||||
offset: 0.0,
|
offset: 0.0,
|
||||||
active: false,
|
active: false,
|
||||||
dock: None,
|
dock: None,
|
||||||
@@ -415,7 +417,7 @@ impl NebulaWorkspace {
|
|||||||
.left(px(4.0))
|
.left(px(4.0))
|
||||||
.top(px(7.0))
|
.top(px(7.0))
|
||||||
.w(px(2.5))
|
.w(px(2.5))
|
||||||
.h(px(TAB_ROW_H - 14.0))
|
.h(px(row_height - 14.0))
|
||||||
.rounded_full()
|
.rounded_full()
|
||||||
.bg(color),
|
.bg(color),
|
||||||
)
|
)
|
||||||
@@ -621,7 +623,7 @@ impl NebulaWorkspace {
|
|||||||
// 仍是旧壳的 8px。
|
// 仍是旧壳的 8px。
|
||||||
.px_2()
|
.px_2()
|
||||||
.pb_2()
|
.pb_2()
|
||||||
.gap_2()
|
.gap(px(tab_scroll::tab_row_gap(self.density)))
|
||||||
// 待命阶段(未过阈值)的指针跟踪;激活后由根部罩层独占接管。
|
// 待命阶段(未过阈值)的指针跟踪;激活后由根部罩层独占接管。
|
||||||
.on_mouse_move(cx.listener(|this, event, window, cx| {
|
.on_mouse_move(cx.listener(|this, event, window, cx| {
|
||||||
this.update_tab_drag(event, window, cx);
|
this.update_tab_drag(event, window, cx);
|
||||||
@@ -631,7 +633,7 @@ impl NebulaWorkspace {
|
|||||||
.id("sidebar-tabs-toggle")
|
.id("sidebar-tabs-toggle")
|
||||||
.group(header_group.clone())
|
.group(header_group.clone())
|
||||||
.w_full()
|
.w_full()
|
||||||
.h(px(34.0))
|
.h(px(row_height))
|
||||||
.pb_1()
|
.pb_1()
|
||||||
// 旧壳标题文字从 panel_x + 16px 起;侧栏根已有 8px
|
// 旧壳标题文字从 panel_x + 16px 起;侧栏根已有 8px
|
||||||
// padding,这里再补 8px,箭头不会贴住左边缘。
|
// padding,这里再补 8px,箭头不会贴住左边缘。
|
||||||
@@ -1042,7 +1044,7 @@ mod tests {
|
|||||||
.id("status-probe-row")
|
.id("status-probe-row")
|
||||||
.debug_selector(|| "status-probe-row".to_owned())
|
.debug_selector(|| "status-probe-row".to_owned())
|
||||||
.w(px(200.0))
|
.w(px(200.0))
|
||||||
.h(px(TAB_ROW_H))
|
.h(px(tab_scroll::tab_row_height(nebula_settings::DensityName::Standard)))
|
||||||
.px_2()
|
.px_2()
|
||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
.child(div().flex_1())
|
.child(div().flex_1())
|
||||||
|
|||||||
@@ -14,10 +14,25 @@ use gpui::{
|
|||||||
use crate::display::ui::widgets::{self, OverlayScrollbar};
|
use crate::display::ui::widgets::{self, OverlayScrollbar};
|
||||||
use crate::gpui_shell::prelude::*;
|
use crate::gpui_shell::prelude::*;
|
||||||
|
|
||||||
use super::{NebulaWorkspace, TAB_ROW_H, TAB_ROW_PITCH};
|
use nebula_settings::DensityName;
|
||||||
|
|
||||||
|
use super::NebulaWorkspace;
|
||||||
|
|
||||||
|
pub(super) fn tab_row_height(density: DensityName) -> f32 {
|
||||||
|
match density {
|
||||||
|
DensityName::Standard => 34.0,
|
||||||
|
DensityName::Compact => crate::display::ui::tokens::control::MIN_HIT_TARGET,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn tab_row_gap(density: DensityName) -> f32 {
|
||||||
|
use crate::display::ui::tokens::space;
|
||||||
|
match density {
|
||||||
|
DensityName::Standard => space::XS,
|
||||||
|
DensityName::Compact => space::XXS,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 行间距(pitch − 行高)。旧壳 `gap = s(8)`。
|
|
||||||
const TAB_ROW_GAP: f32 = TAB_ROW_PITCH - TAB_ROW_H;
|
|
||||||
/// 旧壳 `tab_right_pad`:给覆盖式滚动条留的恒定沟槽,出现与否都不改行宽。
|
/// 旧壳 `tab_right_pad`:给覆盖式滚动条留的恒定沟槽,出现与否都不改行宽。
|
||||||
pub(super) const TAB_SCROLL_GUTTER: f32 = 9.0;
|
pub(super) const TAB_SCROLL_GUTTER: f32 = 9.0;
|
||||||
|
|
||||||
@@ -97,9 +112,18 @@ struct TabsWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl NebulaWorkspace {
|
impl NebulaWorkspace {
|
||||||
|
pub(super) fn tab_row_pitch(&self) -> f32 {
|
||||||
|
tab_row_height(self.density) + tab_row_gap(self.density)
|
||||||
|
}
|
||||||
|
|
||||||
fn tabs_window(&self) -> TabsWindow {
|
fn tabs_window(&self) -> TabsWindow {
|
||||||
let want = self.tabs.len();
|
let want = self.tabs.len();
|
||||||
let show = visible_count(want, self.tabs_viewport_h, TAB_ROW_PITCH, TAB_ROW_GAP);
|
let show = visible_count(
|
||||||
|
want,
|
||||||
|
self.tabs_viewport_h,
|
||||||
|
self.tab_row_pitch(),
|
||||||
|
tab_row_gap(self.density),
|
||||||
|
);
|
||||||
let max = max_scroll(want, show);
|
let max = max_scroll(want, show);
|
||||||
let scroll = clamp_scroll(self.tabs_scroll, max);
|
let scroll = clamp_scroll(self.tabs_scroll, max);
|
||||||
TabsWindow { scroll, show, max, want }
|
TabsWindow { scroll, show, max, want }
|
||||||
@@ -137,15 +161,16 @@ impl NebulaWorkspace {
|
|||||||
if window.show == 0 || window.want <= window.show {
|
if window.show == 0 || window.want <= window.show {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let viewport = rows_h(window.show, TAB_ROW_PITCH, TAB_ROW_GAP).max(1.0);
|
let viewport =
|
||||||
let content = rows_h(window.want, TAB_ROW_PITCH, TAB_ROW_GAP);
|
rows_h(window.show, self.tab_row_pitch(), tab_row_gap(self.density)).max(1.0);
|
||||||
|
let content = rows_h(window.want, self.tab_row_pitch(), tab_row_gap(self.density));
|
||||||
let width = self.tabs_list_width.max(1.0);
|
let width = self.tabs_list_width.max(1.0);
|
||||||
let height = self.tabs_viewport_h.max(viewport);
|
let height = self.tabs_viewport_h.max(viewport);
|
||||||
widgets::overlay_scrollbar(
|
widgets::overlay_scrollbar(
|
||||||
(0.0, 0.0, width, height),
|
(0.0, 0.0, width, height),
|
||||||
viewport,
|
viewport,
|
||||||
content,
|
content,
|
||||||
window.scroll as f32 * TAB_ROW_PITCH,
|
window.scroll as f32 * self.tab_row_pitch(),
|
||||||
1.0,
|
1.0,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -191,7 +216,7 @@ impl NebulaWorkspace {
|
|||||||
_window: &mut Window,
|
_window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
let delta_y = f32::from(event.delta.pixel_delta(px(TAB_ROW_PITCH)).y);
|
let delta_y = f32::from(event.delta.pixel_delta(px(self.tab_row_pitch())).y);
|
||||||
let rows = wheel_rows(delta_y);
|
let rows = wheel_rows(delta_y);
|
||||||
if rows == 0 {
|
if rows == 0 {
|
||||||
return;
|
return;
|
||||||
@@ -254,7 +279,7 @@ impl NebulaWorkspace {
|
|||||||
.absolute()
|
.absolute()
|
||||||
.size_full(),
|
.size_full(),
|
||||||
)
|
)
|
||||||
.child(v_flex().w_full().gap_2().children(items))
|
.child(v_flex().w_full().gap(px(tab_row_gap(self.density))).children(items))
|
||||||
.when_some(bar.filter(|_| show_thumb), |list, bar| {
|
.when_some(bar.filter(|_| show_thumb), |list, bar| {
|
||||||
list.child(
|
list.child(
|
||||||
div()
|
div()
|
||||||
@@ -304,7 +329,10 @@ impl NebulaWorkspace {
|
|||||||
}
|
}
|
||||||
// 折叠中/已折叠:裁剪高度不是 `tabs_avail`。写回去会让
|
// 折叠中/已折叠:裁剪高度不是 `tabs_avail`。写回去会让
|
||||||
// `visible_count` 按 20–40px 算出 0/1 行,展开后整列锁死。
|
// `visible_count` 按 20–40px 算出 0/1 行,展开后整列锁死。
|
||||||
if !self.tabs_section_collapsed && !self.tabs_fold_frozen && h >= TAB_ROW_H {
|
if !self.tabs_section_collapsed
|
||||||
|
&& !self.tabs_fold_frozen
|
||||||
|
&& h >= tab_row_height(self.density)
|
||||||
|
{
|
||||||
self.tabs_viewport_h = h;
|
self.tabs_viewport_h = h;
|
||||||
}
|
}
|
||||||
self.tabs_list_width = w;
|
self.tabs_list_width = w;
|
||||||
@@ -349,6 +377,24 @@ mod tests {
|
|||||||
wheel_rows,
|
wheel_rows,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn compact_rows_fit_more_tabs_without_shrinking_hit_targets() {
|
||||||
|
use super::{DensityName, tab_row_gap, tab_row_height};
|
||||||
|
for (density, expected_height, expected_show) in
|
||||||
|
[(DensityName::Standard, 34.0, 2), (DensityName::Compact, 32.0, 3)]
|
||||||
|
{
|
||||||
|
let height = tab_row_height(density);
|
||||||
|
let gap = tab_row_gap(density);
|
||||||
|
let pitch = height + gap;
|
||||||
|
assert_eq!(height, expected_height);
|
||||||
|
assert!(height >= crate::display::ui::tokens::control::MIN_HIT_TARGET);
|
||||||
|
assert_eq!(rows_h(1, pitch, gap), height);
|
||||||
|
let show = visible_count(10, 104.0, pitch, gap);
|
||||||
|
assert_eq!(show, expected_show);
|
||||||
|
assert_eq!(reveal_index(9, 0, show), max_scroll(10, show));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const PITCH: f32 = 42.0;
|
const PITCH: f32 = 42.0;
|
||||||
const GAP: f32 = 8.0;
|
const GAP: f32 = 8.0;
|
||||||
|
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ mod title_bar_panel_control_tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct TopTabGeometryProbe;
|
struct TopTabGeometryProbe(nebula_settings::DensityName);
|
||||||
|
|
||||||
impl Render for TopTabGeometryProbe {
|
impl Render for TopTabGeometryProbe {
|
||||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
@@ -89,9 +89,10 @@ mod title_bar_panel_control_tests {
|
|||||||
div()
|
div()
|
||||||
.debug_selector(|| "top-tab-geometry-probe".to_owned())
|
.debug_selector(|| "top-tab-geometry-probe".to_owned())
|
||||||
.w(px(160.0))
|
.w(px(160.0))
|
||||||
.h(px(super::top_tabs::TOP_TAB_H)),
|
.h(px(super::tab_scroll::tab_row_height(self.0))),
|
||||||
)
|
)
|
||||||
.child(super::top_tabs::top_tab_action_slot(
|
.child(super::top_tabs::top_tab_action_slot(
|
||||||
|
self.0,
|
||||||
super::top_tabs::top_new_tab_control(
|
super::top_tabs::top_new_tab_control(
|
||||||
crate::i18n::UiLanguage::EnUs,
|
crate::i18n::UiLanguage::EnUs,
|
||||||
|_, _, _| {},
|
|_, _, _| {},
|
||||||
@@ -99,6 +100,7 @@ mod title_bar_panel_control_tests {
|
|||||||
.debug_selector(|| "top-new-tab-geometry-probe".to_owned()),
|
.debug_selector(|| "top-new-tab-geometry-probe".to_owned()),
|
||||||
))
|
))
|
||||||
.child(super::top_tabs::top_tab_action_slot(
|
.child(super::top_tabs::top_tab_action_slot(
|
||||||
|
self.0,
|
||||||
h_flex().debug_selector(|| "top-tabs-menu-geometry-probe".to_owned()).child(
|
h_flex().debug_selector(|| "top-tabs-menu-geometry-probe".to_owned()).child(
|
||||||
super::top_tabs::top_tabs_menu_button(false, crate::i18n::UiLanguage::EnUs),
|
super::top_tabs::top_tabs_menu_button(false, crate::i18n::UiLanguage::EnUs),
|
||||||
),
|
),
|
||||||
@@ -113,16 +115,27 @@ mod title_bar_panel_control_tests {
|
|||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
fn top_tab_action_buttons_share_the_tab_vertical_center(cx: &mut TestAppContext) {
|
fn top_tab_action_buttons_share_the_tab_vertical_center(cx: &mut TestAppContext) {
|
||||||
cx.update(gpui_component::init);
|
cx.update(gpui_component::init);
|
||||||
let (_, cx) = cx.add_window_view(|_, _| TopTabGeometryProbe);
|
let (probe, cx) = cx.add_window_view(|_, _| TopTabGeometryProbe::default());
|
||||||
let tab = cx.debug_bounds("top-tab-geometry-probe").expect("tab bounds");
|
for (density, height) in [
|
||||||
let new_tab = cx.debug_bounds("top-new-tab-geometry-probe").expect("new-tab bounds");
|
(nebula_settings::DensityName::Standard, 34.0),
|
||||||
let menu = cx.debug_bounds("top-tabs-menu-geometry-probe").expect("menu bounds");
|
(nebula_settings::DensityName::Compact, 32.0),
|
||||||
|
(nebula_settings::DensityName::Standard, 34.0),
|
||||||
|
] {
|
||||||
|
probe.update(cx, |probe, cx| {
|
||||||
|
probe.0 = density;
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
cx.run_until_parked();
|
||||||
|
let tab = cx.debug_bounds("top-tab-geometry-probe").expect("tab bounds");
|
||||||
|
let new_tab = cx.debug_bounds("top-new-tab-geometry-probe").expect("new-tab bounds");
|
||||||
|
let menu = cx.debug_bounds("top-tabs-menu-geometry-probe").expect("menu bounds");
|
||||||
|
|
||||||
assert_eq!(f32::from(tab.size.height), 34.0);
|
assert_eq!(f32::from(tab.size.height), height);
|
||||||
assert_eq!(f32::from(new_tab.size.height), 32.0);
|
assert_eq!(f32::from(new_tab.size.height), 32.0);
|
||||||
assert_eq!(f32::from(menu.size.height), 32.0);
|
assert_eq!(f32::from(menu.size.height), 32.0);
|
||||||
assert_eq!(vertical_center(new_tab), vertical_center(tab));
|
assert_eq!(vertical_center(new_tab), vertical_center(tab));
|
||||||
assert_eq!(vertical_center(menu), vertical_center(tab));
|
assert_eq!(vertical_center(menu), vertical_center(tab));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
//! 48px 自定义标题栏里的横向标签布局。
|
//! 自定义标题栏里的横向标签布局。
|
||||||
//!
|
//!
|
||||||
//! 这里只负责同一组 workspace tab 的第二种呈现;激活、关闭、重命名、排序
|
//! 这里只负责同一组 workspace tab 的第二种呈现;激活、关闭、重命名、排序
|
||||||
//! 与 dock 都调用 `NebulaWorkspace` 既有动作,不维护平行状态。
|
//! 与 dock 都调用 `NebulaWorkspace` 既有动作,不维护平行状态。
|
||||||
@@ -23,7 +23,6 @@ use super::{
|
|||||||
TabDragAxis, TabPresentation, ToggleShellPicker, pane_header, title_bar_panel_controls,
|
TabDragAxis, TabPresentation, ToggleShellPicker, pane_header, title_bar_panel_controls,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub(super) const TOP_TAB_H: f32 = 34.0;
|
|
||||||
/// 与顶部模式正文卡片的 `px_2` 左边距同源,让首个 tab 和终端左缘对齐。
|
/// 与顶部模式正文卡片的 `px_2` 左边距同源,让首个 tab 和终端左缘对齐。
|
||||||
pub(super) const TOP_TAB_LEFT_INSET: f32 = 8.0;
|
pub(super) const TOP_TAB_LEFT_INSET: f32 = 8.0;
|
||||||
/// 单个 tab 的最小宽。WT 的 TabView 同一条取舍:标签**不压到读不出来**,
|
/// 单个 tab 的最小宽。WT 的 TabView 同一条取舍:标签**不压到读不出来**,
|
||||||
@@ -110,10 +109,13 @@ pub(super) fn top_tabs_menu_button(settings_active: bool, language: UiLanguage)
|
|||||||
.tooltip(language.text(Message::ChromeMore))
|
.tooltip(language.text(Message::ChromeMore))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 紧邻 TabView 的操作按钮占满同一条 34px 行,再在槽内居中 32px 按钮。
|
/// 紧邻 TabView 的操作按钮占满同一条标签行,再在槽内居中 32px 按钮。
|
||||||
/// 外层仍贴标题栏底边,tab 与正文相接的既有布局不变。
|
/// 外层仍贴标题栏底边,tab 与正文相接的既有布局不变。
|
||||||
pub(super) fn top_tab_action_slot(child: impl gpui::IntoElement) -> gpui::Div {
|
pub(super) fn top_tab_action_slot(
|
||||||
h_flex().h(px(TOP_TAB_H)).items_center().child(child)
|
density: nebula_settings::DensityName,
|
||||||
|
child: impl gpui::IntoElement,
|
||||||
|
) -> gpui::Div {
|
||||||
|
h_flex().h(px(super::tab_scroll::tab_row_height(density))).items_center().child(child)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NebulaWorkspace {
|
impl NebulaWorkspace {
|
||||||
@@ -130,6 +132,7 @@ impl NebulaWorkspace {
|
|||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> gpui::AnyElement {
|
) -> gpui::AnyElement {
|
||||||
|
let row_height = super::tab_scroll::tab_row_height(self.density);
|
||||||
let language = crate::gpui_shell::config::ui_language(cx);
|
let language = crate::gpui_shell::config::ui_language(cx);
|
||||||
let theme = cx.theme();
|
let theme = cx.theme();
|
||||||
let muted = theme.muted_foreground;
|
let muted = theme.muted_foreground;
|
||||||
@@ -276,7 +279,7 @@ impl NebulaWorkspace {
|
|||||||
.group(hover_group.clone())
|
.group(hover_group.clone())
|
||||||
.relative()
|
.relative()
|
||||||
.w(px(tab_w))
|
.w(px(tab_w))
|
||||||
.h(px(TOP_TAB_H))
|
.h(px(row_height))
|
||||||
.flex_shrink_0()
|
.flex_shrink_0()
|
||||||
.min_w_0()
|
.min_w_0()
|
||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
@@ -552,7 +555,7 @@ impl NebulaWorkspace {
|
|||||||
// 看不见的入口——tab 一多,用户根本不知道右边还有东西。
|
// 看不见的入口——tab 一多,用户根本不知道右边还有东西。
|
||||||
.when(overflow, |bar| {
|
.when(overflow, |bar| {
|
||||||
bar.child(
|
bar.child(
|
||||||
div().h(px(TOP_TAB_H)).flex().items_center().child(
|
div().h(px(row_height)).flex().items_center().child(
|
||||||
title_bar_panel_controls().h_auto().child(
|
title_bar_panel_controls().h_auto().child(
|
||||||
Button::new("top-tabs-prev")
|
Button::new("top-tabs-prev")
|
||||||
.icon(IconName::ChevronLeft)
|
.icon(IconName::ChevronLeft)
|
||||||
@@ -580,7 +583,7 @@ impl NebulaWorkspace {
|
|||||||
.w(px(tab_viewport_w))
|
.w(px(tab_viewport_w))
|
||||||
.flex_shrink(1.0)
|
.flex_shrink(1.0)
|
||||||
.min_w_0()
|
.min_w_0()
|
||||||
.h(px(TOP_TAB_H))
|
.h(px(row_height))
|
||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
// 待命拖拽在越过 4px 前由此接收移动;激活后根罩层接管。
|
// 待命拖拽在越过 4px 前由此接收移动;激活后根罩层接管。
|
||||||
.on_mouse_move(cx.listener(|this, event, window, cx| {
|
.on_mouse_move(cx.listener(|this, event, window, cx| {
|
||||||
@@ -600,7 +603,7 @@ impl NebulaWorkspace {
|
|||||||
)
|
)
|
||||||
.when(overflow, |bar| {
|
.when(overflow, |bar| {
|
||||||
bar.child(
|
bar.child(
|
||||||
div().h(px(TOP_TAB_H)).flex().items_center().child(
|
div().h(px(row_height)).flex().items_center().child(
|
||||||
title_bar_panel_controls().h_auto().child(
|
title_bar_panel_controls().h_auto().child(
|
||||||
Button::new("top-tabs-next")
|
Button::new("top-tabs-next")
|
||||||
.icon(IconName::ChevronRight)
|
.icon(IconName::ChevronRight)
|
||||||
@@ -622,7 +625,7 @@ impl NebulaWorkspace {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
.child(
|
.child(
|
||||||
top_tab_action_slot(top_new_tab_control(language, cx.listener(
|
top_tab_action_slot(self.density, top_new_tab_control(language, cx.listener(
|
||||||
|this, _, window, cx| {
|
|this, _, window, cx| {
|
||||||
this.add_terminal(window, cx);
|
this.add_terminal(window, cx);
|
||||||
},
|
},
|
||||||
@@ -630,6 +633,7 @@ impl NebulaWorkspace {
|
|||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
top_tab_action_slot(
|
top_tab_action_slot(
|
||||||
|
self.density,
|
||||||
top_tabs_menu_button(settings_active, language).dropdown_menu_with_anchor(
|
top_tabs_menu_button(settings_active, language).dropdown_menu_with_anchor(
|
||||||
gpui::Anchor::TopRight,
|
gpui::Anchor::TopRight,
|
||||||
move |menu, _, _| {
|
move |menu, _, _| {
|
||||||
|
|||||||
@@ -1,5 +1,19 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
fn title_bar_height(density: nebula_settings::DensityName) -> f32 {
|
||||||
|
match density {
|
||||||
|
nebula_settings::DensityName::Standard => 48.0,
|
||||||
|
nebula_settings::DensityName::Compact => 40.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn effective_title_bar_height(
|
||||||
|
density: nebula_settings::DensityName,
|
||||||
|
native_layout: Option<(f32, f32)>,
|
||||||
|
) -> f32 {
|
||||||
|
native_layout.map_or_else(|| title_bar_height(density), |(height, _)| height)
|
||||||
|
}
|
||||||
|
|
||||||
/// Prepaint records the actual pane column before any titlebar paint runs. Keep
|
/// Prepaint records the actual pane column before any titlebar paint runs. Keep
|
||||||
/// this cell for the workspace lifetime, so resizing and panel animations do not
|
/// this cell for the workspace lifetime, so resizing and panel animations do not
|
||||||
/// allocate a new shared slot or duplicate the body's layout calculations.
|
/// allocate a new shared slot or duplicate the body's layout calculations.
|
||||||
@@ -93,9 +107,9 @@ impl NebulaWorkspace {
|
|||||||
) -> gpui::Div {
|
) -> gpui::Div {
|
||||||
let top_tabs = self.tabs_position == nebula_settings::TabsPositionName::Top;
|
let top_tabs = self.tabs_position == nebula_settings::TabsPositionName::Top;
|
||||||
let native_layout = crate::platform::window_chrome::layout(window);
|
let native_layout = crate::platform::window_chrome::layout(window);
|
||||||
|
let title_bar_height = effective_title_bar_height(self.density, native_layout);
|
||||||
let bar = TitleBar::new()
|
let bar = TitleBar::new()
|
||||||
// Leave 8px above and below the existing 32px controls.
|
.h(px(title_bar_height))
|
||||||
.h(px(48.0))
|
|
||||||
.when(!settings_active, |bar| bar.bg(gpui::transparent_black()).border_b_0())
|
.when(!settings_active, |bar| bar.bg(gpui::transparent_black()).border_b_0())
|
||||||
.when(settings_active, |bar| {
|
.when(settings_active, |bar| {
|
||||||
bar.border_b_1().border_color(crate::gpui_shell::theme::settings_hairline(cx))
|
bar.border_b_1().border_color(crate::gpui_shell::theme::settings_hairline(cx))
|
||||||
@@ -103,29 +117,15 @@ impl NebulaWorkspace {
|
|||||||
.when(top_tabs && native_layout.is_none(), |bar| {
|
.when(top_tabs && native_layout.is_none(), |bar| {
|
||||||
bar.pl(px(top_tabs::TOP_TAB_LEFT_INSET))
|
bar.pl(px(top_tabs::TOP_TAB_LEFT_INSET))
|
||||||
})
|
})
|
||||||
.when(top_tabs, |bar| {
|
.when(top_tabs, |bar| bar.child(self.render_top_title_bar(settings_active, window, cx)))
|
||||||
bar.child(self.render_top_title_bar(
|
.when(!top_tabs, |bar| bar.child(self.render_sidebar_title_bar(settings_active, cx)));
|
||||||
settings_active,
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
))
|
|
||||||
})
|
|
||||||
.when(!top_tabs, |bar| {
|
|
||||||
bar.child(self.render_sidebar_title_bar(
|
|
||||||
settings_active,
|
|
||||||
cx,
|
|
||||||
))
|
|
||||||
});
|
|
||||||
|
|
||||||
// AppKit owns both the control group and its geometry. Read the live
|
// AppKit owns both the control group and its geometry. Read the live
|
||||||
// frames so system layout, resize and full-screen transitions agree.
|
// frames so system layout, resize and full-screen transitions agree.
|
||||||
let bar = if let Some((height, inset)) = native_layout {
|
let bar = if let Some((_, inset)) = native_layout { bar.pl(px(inset)) } else { bar };
|
||||||
bar.h(px(height)).pl(px(inset))
|
|
||||||
} else {
|
|
||||||
bar
|
|
||||||
};
|
|
||||||
|
|
||||||
title_bar_frame()
|
title_bar_frame()
|
||||||
|
.debug_selector(|| "workspace-titlebar".to_owned())
|
||||||
.when(!settings_active, |title| title.child(self.titlebar_background.element()))
|
.when(!settings_active, |title| title.child(self.titlebar_background.element()))
|
||||||
.child(bar)
|
.child(bar)
|
||||||
}
|
}
|
||||||
@@ -175,3 +175,19 @@ mod tests {
|
|||||||
assert!(!cx.update(|window, cx| window.selected_text(cx).is_empty()));
|
assert!(!cx.update(|window, cx| window.selected_text(cx).is_empty()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod density_tests {
|
||||||
|
use super::{effective_title_bar_height, title_bar_height};
|
||||||
|
use nebula_settings::DensityName;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn compact_title_bar_retains_space_around_controls() {
|
||||||
|
assert_eq!(title_bar_height(DensityName::Standard), 48.0);
|
||||||
|
assert_eq!(title_bar_height(DensityName::Compact), 40.0);
|
||||||
|
assert_eq!(effective_title_bar_height(DensityName::Standard, None), 48.0);
|
||||||
|
assert_eq!(effective_title_bar_height(DensityName::Compact, None), 40.0);
|
||||||
|
assert_eq!(effective_title_bar_height(DensityName::Compact, Some((34.0, 7.0))), 34.0);
|
||||||
|
assert!(title_bar_height(DensityName::Compact) >= 32.0 + 2.0 * 4.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user