mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-22 16:02:24 +00:00
refactor(panel): give the detail panel's top zone back to the panel
The panel's title-bar-height top zone carried seven controls belonging to three different layers: four panel tabs, the panel toggle, the "..." menu, and the workspace chip. All seven at chrome scale, in a column the user can drag down to MIN_WIDTH. The row wants 268px there — it overflows by 68px before anything else goes wrong. The layers are the real defect; the crowding is a symptom. Switching to Files is "what does this panel show", the workspace chip is "which machine am I on", and they sat side by side, same size, same weight. Three changes, none of which touch the window layout — both columns stay full-height and the corner controls stay where they are: - The workspace chip moves to the head of the rail, as a full row with the workspace's name rather than a monogram in a corner. The rail below it enumerates that workspace's tabs, so this is where the name belongs; it also stops a window-scoped control from competing for the panel's width. Note it is *not* folded into the repo group headers under it — those are repositories, and one workspace holds several. - The panel's tab tiles drop to TILE_SIZE_SM. That constant exists for tiles inside a panel, which is exactly what these are, and the one size step separates them from the window chrome sharing their row without spending a divider on it. - On macOS panel_title draws nothing. The tile row above it already says which tab you are on, and so does the content — a file tree is Files, a diff is Changes. It cost a whole row: tiles, then a title, then a search box, before one line of content. Changes' file count moves into that tab's tooltip, where it stays readable without switching tabs. Outline's count does not survive: it needs the active leaf, which needs a &Window right_panel_tabs has no reason to take, and its list is one click away. A tab passing `trailing` still gets the row; none currently do. Off macOS nothing changes: the panel hangs below a spanning title bar there, and panel_title is still that panel's tab switcher. The top zone now needs ~164px against a 200px minimum.
This commit is contained in:
+22
-10
@@ -28,10 +28,7 @@ use std::rc::Rc;
|
|||||||
use crate::core::config::{Config, RightPanelTab};
|
use crate::core::config::{Config, RightPanelTab};
|
||||||
use crate::daemon::protocol::PaneProcs;
|
use crate::daemon::protocol::PaneProcs;
|
||||||
use crate::terminal::git_diff::{self, DiffSnapshot};
|
use crate::terminal::git_diff::{self, DiffSnapshot};
|
||||||
use crate::ui::app::{
|
use crate::ui::app::{CONTENT_INSET, TILE_GLYPH_SM, TILE_SIZE_SM, Tty7App, tile_trailing_inset_sm};
|
||||||
CONTENT_INSET, TILE_GLYPH_SM, TILE_SIZE_SM, Tty7App, tile_trailing_inset,
|
|
||||||
tile_trailing_inset_sm,
|
|
||||||
};
|
|
||||||
use crate::ui::scrollbar::with_vertical_scrollbar;
|
use crate::ui::scrollbar::with_vertical_scrollbar;
|
||||||
|
|
||||||
/// Bounds for the panel's width, mirroring the rail's: a floor so the tree never
|
/// Bounds for the panel's width, mirroring the rail's: a floor so the tree never
|
||||||
@@ -249,7 +246,11 @@ impl Tty7App {
|
|||||||
.on_double_click(|_, window, _| window.titlebar_double_click())
|
.on_double_click(|_, window, _| window.titlebar_double_click())
|
||||||
.items_center()
|
.items_center()
|
||||||
.gap(px(2.))
|
.gap(px(2.))
|
||||||
.pl(px(tile_trailing_inset()))
|
// `_sm` because the tabs are body-scale tiles now
|
||||||
|
// (`right_panel_tabs`): the leading inset has to line the
|
||||||
|
// *glyph* up on `CONTENT_INSET`, and a 24px tile holds its
|
||||||
|
// glyph a different distance inside the box than a 32px one.
|
||||||
|
.pl(px(crate::ui::app::tile_trailing_inset_sm()))
|
||||||
.children(self.right_panel_tabs(cx))
|
.children(self.right_panel_tabs(cx))
|
||||||
.child(div().flex_1())
|
.child(div().flex_1())
|
||||||
// The panel is what reaches the window's right edge while
|
// The panel is what reaches the window's right edge while
|
||||||
@@ -375,6 +376,16 @@ impl Tty7App {
|
|||||||
/// of this one meant three stacked headers before a single line of content —
|
/// of this one meant three stacked headers before a single line of content —
|
||||||
/// so the two that were saying "this is a header" merge into one that also
|
/// so the two that were saying "this is a header" merge into one that also
|
||||||
/// says which tab you are on.
|
/// says which tab you are on.
|
||||||
|
///
|
||||||
|
/// **On macOS it draws nothing unless a tab passes `trailing`.** The panel
|
||||||
|
/// there has its own tile row in its top zone, which already says which tab
|
||||||
|
/// you are on — restating it in words underneath was a whole row spent on
|
||||||
|
/// something the selected tile and the content below both already answer
|
||||||
|
/// (a file tree is Files, a diff is Changes). What it did cost was the row:
|
||||||
|
/// the panel opened with tiles, then a title, then a search box, before one
|
||||||
|
/// line of content. The counts it used to carry move into the tab tooltips.
|
||||||
|
/// A tab that has its own control still gets the row, because that control
|
||||||
|
/// has nowhere else to go.
|
||||||
pub(crate) fn panel_title(
|
pub(crate) fn panel_title(
|
||||||
&self,
|
&self,
|
||||||
text: &str,
|
text: &str,
|
||||||
@@ -384,6 +395,9 @@ impl Tty7App {
|
|||||||
) -> AnyElement {
|
) -> AnyElement {
|
||||||
let tabs = (!cfg!(target_os = "macos")).then(|| self.right_panel_tabs(cx));
|
let tabs = (!cfg!(target_os = "macos")).then(|| self.right_panel_tabs(cx));
|
||||||
let has_trailing = trailing.is_some();
|
let has_trailing = trailing.is_some();
|
||||||
|
if tabs.is_none() && !has_trailing {
|
||||||
|
return div().flex_none().into_any_element();
|
||||||
|
}
|
||||||
h_flex()
|
h_flex()
|
||||||
.flex_none()
|
.flex_none()
|
||||||
// Tall enough to seat the chrome-scale tiles when it carries them;
|
// Tall enough to seat the chrome-scale tiles when it carries them;
|
||||||
@@ -396,12 +410,10 @@ impl Tty7App {
|
|||||||
.items_center()
|
.items_center()
|
||||||
.pl(px(CONTENT_INSET))
|
.pl(px(CONTENT_INSET))
|
||||||
// Trailing tiles align on the glyph like every other control in the
|
// Trailing tiles align on the glyph like every other control in the
|
||||||
// window; a label-only header just takes the plain inset. `_SM` for a
|
// window; a label-only header just takes the plain inset. `_SM` covers
|
||||||
// tab's own control, whose glyph sits a different distance inside its
|
// both tile cases now — the tab tiles are body-scale too.
|
||||||
// box than the chrome-scale tab tiles do.
|
|
||||||
.pr(px(match (&tabs, has_trailing) {
|
.pr(px(match (&tabs, has_trailing) {
|
||||||
(Some(_), _) => tile_trailing_inset(),
|
(Some(_), _) | (None, true) => tile_trailing_inset_sm(),
|
||||||
(None, true) => tile_trailing_inset_sm(),
|
|
||||||
(None, false) => CONTENT_INSET,
|
(None, false) => CONTENT_INSET,
|
||||||
}))
|
}))
|
||||||
// The line that separates the header from the tab's content. Only
|
// The line that separates the header from the tab's content. Only
|
||||||
|
|||||||
@@ -823,6 +823,16 @@ impl Tty7App {
|
|||||||
.on_click(cx.listener(|this, _, _window, cx| this.toggle_left_panel(cx))),
|
.on_click(cx.listener(|this, _, _window, cx| this.toggle_left_panel(cx))),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
// The rail's head: which workspace this window is on. A row of its own,
|
||||||
|
// above the search box, because it names the thing the whole column
|
||||||
|
// enumerates. It is deliberately *not* folded into the repo group headers
|
||||||
|
// below — those are repositories, and one workspace holds several of them.
|
||||||
|
let workspace_head = h_flex()
|
||||||
|
.flex_shrink_0()
|
||||||
|
.px(px(crate::ui::app::CONTENT_INSET - 7.))
|
||||||
|
.pt(px(4.))
|
||||||
|
.child(self.workspace_head(cx));
|
||||||
|
|
||||||
// Borderless "Search tabs…" that sits directly on the sunk surface: a
|
// Borderless "Search tabs…" that sits directly on the sunk surface: a
|
||||||
// leading magnifier + an appearance-less input, no box and no divider
|
// leading magnifier + an appearance-less input, no box and no divider
|
||||||
// under the bar, so the control row and list read as one continuous rail
|
// under the bar, so the control row and list read as one continuous rail
|
||||||
@@ -969,6 +979,7 @@ impl Tty7App {
|
|||||||
.child(crate::ui::app::title_bar_drag(
|
.child(crate::ui::app::title_bar_drag(
|
||||||
controls.id("sidebar-titlebar-drag"),
|
controls.id("sidebar-titlebar-drag"),
|
||||||
))
|
))
|
||||||
|
.child(workspace_head)
|
||||||
.child(top_bar)
|
.child(top_bar)
|
||||||
.child(crate::ui::scrollbar::with_vertical_scrollbar(
|
.child(crate::ui::scrollbar::with_vertical_scrollbar(
|
||||||
"tab-sidebar-scrollbar",
|
"tab-sidebar-scrollbar",
|
||||||
|
|||||||
+104
-34
@@ -349,13 +349,14 @@ pub(crate) fn select_workspace_action(index: usize) -> Option<Box<dyn gpui::Acti
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Tty7App {
|
impl Tty7App {
|
||||||
/// Diameter of the workspace avatar, matching the 32px chrome tiles beside
|
/// Diameter of the workspace avatar. Sized to the rail's row glyphs rather
|
||||||
/// it so the corner reads as one row of controls.
|
/// than to a chrome tile: this control is the head of the tab list, so it
|
||||||
const AVATAR_PX: f32 = 26.0;
|
/// reads down the column instead of across a row of buttons.
|
||||||
|
const AVATAR_PX: f32 = 20.0;
|
||||||
|
|
||||||
/// The title-bar workspace control: a monogram of the current workspace
|
/// The rail's head: which workspace this window is on, plus the menu that
|
||||||
/// plus a chevron, opening the one menu that owns everything
|
/// owns everything workspace-scoped — and the app-level entries the "⋯"
|
||||||
/// workspace-scoped — and the app-level entries the "⋯" used to hold.
|
/// used to hold.
|
||||||
///
|
///
|
||||||
/// This exists because the rest of it was too well hidden. Switching lived
|
/// This exists because the rest of it was too well hidden. Switching lived
|
||||||
/// in the command palette, reopening lived on the home page, ending lived
|
/// in the command palette, reopening lived on the home page, ending lived
|
||||||
@@ -363,14 +364,22 @@ impl Tty7App {
|
|||||||
/// defensible, together undiscoverable. Nothing in the window even *said*
|
/// defensible, together undiscoverable. Nothing in the window even *said*
|
||||||
/// which workspace it was, which starts to matter the moment there are two.
|
/// which workspace it was, which starts to matter the moment there are two.
|
||||||
///
|
///
|
||||||
/// A monogram rather than the full name: a fixed-width control can't be
|
/// It used to be a monogram in the window's top-right corner. Two things
|
||||||
/// pushed off the corner by a long repo name, and it sits level with the
|
/// were wrong with that. Physically: the corner it sat in is the *panel's*
|
||||||
/// icon tiles instead of introducing a third shape. The full name is in the
|
/// top zone while the panel is open, so a control that has nothing to do
|
||||||
/// tooltip and checked in the menu.
|
/// with the panel was eating width the panel's own tabs needed — at the
|
||||||
|
/// 200px minimum the row wanted 268px. Semantically: a window's workspace is
|
||||||
|
/// exactly what the rail below enumerates (this workspace's tabs), so the
|
||||||
|
/// name belonged at the head of that column, not across the window from it.
|
||||||
|
///
|
||||||
|
/// Here it can afford the full name — the rail is a column with a width of
|
||||||
|
/// its own, and it truncates rather than pushing anything off an edge. The
|
||||||
|
/// monogram stays as a leading avatar because it is the mark that identifies
|
||||||
|
/// *this* workspace at a glance across windows.
|
||||||
///
|
///
|
||||||
/// While a rename is in flight the control becomes the text field, so the
|
/// While a rename is in flight the control becomes the text field, so the
|
||||||
/// name is edited where it is displayed.
|
/// name is edited where it is displayed.
|
||||||
pub(crate) fn workspace_chip(&self, cx: &mut Context<Self>) -> impl IntoElement + use<> {
|
pub(crate) fn workspace_head(&self, cx: &mut Context<Self>) -> impl IntoElement + use<> {
|
||||||
if let Some(rename) = self.workspace_rename.as_ref() {
|
if let Some(rename) = self.workspace_rename.as_ref() {
|
||||||
// The tile itself becomes the field — same height, same radius, and
|
// The tile itself becomes the field — same height, same radius, and
|
||||||
// the hover fill standing in for "this control is being edited".
|
// the hover fill standing in for "this control is being edited".
|
||||||
@@ -382,10 +391,13 @@ impl Tty7App {
|
|||||||
.id("workspace-rename")
|
.id("workspace-rename")
|
||||||
.flex_shrink_0()
|
.flex_shrink_0()
|
||||||
.items_center()
|
.items_center()
|
||||||
.h(px(32.))
|
// Full rail width, not the old fixed 150px: this control is a row
|
||||||
.w(px(150.))
|
// in a column now, so it takes the column's width like every other
|
||||||
.px(px(8.))
|
// row does.
|
||||||
.rounded_lg()
|
.h(px(30.))
|
||||||
|
.w_full()
|
||||||
|
.px(px(7.))
|
||||||
|
.rounded_md()
|
||||||
.bg(cx.theme().sidebar_accent)
|
.bg(cx.theme().sidebar_accent)
|
||||||
// Swallow mouse-downs (including the double-click that selects a
|
// Swallow mouse-downs (including the double-click that selects a
|
||||||
// word) so they never reach the enclosing TitleBar and zoom the
|
// word) so they never reach the enclosing TitleBar and zoom the
|
||||||
@@ -418,34 +430,55 @@ impl Tty7App {
|
|||||||
|
|
||||||
div()
|
div()
|
||||||
.occlude()
|
.occlude()
|
||||||
.flex_shrink_0()
|
.w_full()
|
||||||
.child(
|
.child(
|
||||||
Button::new("titlebar-workspace")
|
Button::new("rail-workspace-head")
|
||||||
.custom(chrome_tile_variant(cx))
|
.custom(chrome_tile_variant(cx))
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
|
.w_full()
|
||||||
.items_center()
|
.items_center()
|
||||||
.gap(px(3.))
|
.gap(px(6.))
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.flex()
|
.flex()
|
||||||
|
.flex_shrink_0()
|
||||||
.items_center()
|
.items_center()
|
||||||
.justify_center()
|
.justify_center()
|
||||||
.size(px(Self::AVATAR_PX))
|
.size(px(Self::AVATAR_PX))
|
||||||
.rounded_full()
|
.rounded_full()
|
||||||
.bg(cx.theme().secondary)
|
.bg(cx.theme().secondary)
|
||||||
.text_size(px(11.))
|
.text_size(px(10.))
|
||||||
.font_weight(FontWeight::SEMIBOLD)
|
.font_weight(FontWeight::SEMIBOLD)
|
||||||
.child(monogram),
|
.child(monogram),
|
||||||
)
|
)
|
||||||
// A chevron, unlike the toggles beside it: those do
|
// The name shrinks and truncates rather than pushing
|
||||||
// one thing on click, this opens something, and the
|
// the chevron out — same rule the group headers below
|
||||||
|
// it follow, so a long workspace name and a long repo
|
||||||
|
// name behave identically.
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.flex_shrink(1.)
|
||||||
|
.min_w_0()
|
||||||
|
.truncate()
|
||||||
|
.text_size(px(12.5))
|
||||||
|
.font_weight(FontWeight::SEMIBOLD)
|
||||||
|
.child(SharedString::from(current.clone())),
|
||||||
|
)
|
||||||
|
// A chevron, unlike the tiles in the row above: those
|
||||||
|
// do one thing on click, this opens something, and the
|
||||||
// glyph is what says so.
|
// glyph is what says so.
|
||||||
.child(Icon::new(IconName::ChevronDown).size(px(11.))),
|
.child(
|
||||||
|
Icon::new(IconName::ChevronDown)
|
||||||
|
.size(px(11.))
|
||||||
|
.flex_shrink_0()
|
||||||
|
.text_color(cx.theme().muted_foreground),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.xsmall()
|
.xsmall()
|
||||||
.h(px(32.))
|
.w_full()
|
||||||
.rounded_lg()
|
.h(px(30.))
|
||||||
|
.rounded_md()
|
||||||
.tooltip(SharedString::from(current))
|
.tooltip(SharedString::from(current))
|
||||||
.on_click(cx.listener(|this, _, window, cx| {
|
.on_click(cx.listener(|this, _, window, cx| {
|
||||||
this.toggle_switcher(window, cx);
|
this.toggle_switcher(window, cx);
|
||||||
@@ -518,12 +551,15 @@ impl Tty7App {
|
|||||||
.flex_shrink_0()
|
.flex_shrink_0()
|
||||||
.items_center()
|
.items_center()
|
||||||
.gap(px(2.))
|
.gap(px(2.))
|
||||||
// The workspace control leads the corner chrome: it is the only
|
// Two controls, both window-scoped, and that is the whole corner now.
|
||||||
// thing in the window that says *which* workspace this is, which
|
// The workspace chip used to lead this group; it moved to the rail's
|
||||||
// starts to matter the moment there are two. It also absorbed the
|
// head (`tab_sidebar`), where the list it names actually lives. What
|
||||||
// old "⋯" menu, so the corner has one menu instead of two adjacent
|
// forced the move is that this group's other host is the *panel's* top
|
||||||
// ones, and nothing workspace-scoped is left behind a modifier
|
// zone, and a panel the user can drag down to 200px cannot seat the
|
||||||
// gesture or a palette entry the user has to already know about.
|
// panel's four tabs plus three window controls — the row wanted 268px.
|
||||||
|
// Nothing that has no business being scoped to the panel gets to
|
||||||
|
// compete for that width.
|
||||||
|
//
|
||||||
// The "⋯" glyph ends on the window's content inset like every other
|
// The "⋯" glyph ends on the window's content inset like every other
|
||||||
// right edge in the chrome — hence `inset - TILE_PAD`, which puts the
|
// right edge in the chrome — hence `inset - TILE_PAD`, which puts the
|
||||||
// *glyph's ink* there instead of its hit box.
|
// *glyph's ink* there instead of its hit box.
|
||||||
@@ -562,13 +598,37 @@ impl Tty7App {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child(self.app_menu_tile(window, cx))
|
.child(self.app_menu_tile(window, cx))
|
||||||
.child(self.workspace_chip(cx))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The detail panel's tab tiles — icon-only, one per view. Lives here beside
|
/// The detail panel's tab tiles — icon-only, one per view. Lives here beside
|
||||||
/// the rest of the chrome tiles so all of them share one styling helper.
|
/// the rest of the chrome tiles so all of them share one styling helper.
|
||||||
|
///
|
||||||
|
/// Body scale ([`TILE_SIZE_SM`]), not chrome scale. Two reasons, and the
|
||||||
|
/// second is why it changed: these tiles live *inside* a panel, which is what
|
||||||
|
/// that constant is for; and the row they sit in also carries the window's
|
||||||
|
/// own chrome at its trailing edge, so drawing both at 32px made seven
|
||||||
|
/// identical squares out of controls belonging to three different layers.
|
||||||
|
/// One size step is what separates "the panel's own tabs" from "the window's
|
||||||
|
/// buttons" without adding a rule or a divider. It also buys back the 32px
|
||||||
|
/// that made the row overflow a 200px-wide panel.
|
||||||
|
///
|
||||||
|
/// [`TILE_SIZE_SM`]: crate::ui::app::TILE_SIZE_SM
|
||||||
pub(crate) fn right_panel_tabs(&self, cx: &mut Context<Self>) -> Vec<AnyElement> {
|
pub(crate) fn right_panel_tabs(&self, cx: &mut Context<Self>) -> Vec<AnyElement> {
|
||||||
let active_tab = self.right_panel_tab;
|
let active_tab = self.right_panel_tab;
|
||||||
|
// Changed-file count, carried in the Changes tooltip. It used to be a
|
||||||
|
// tally in that tab's header row, and that row is gone on macOS
|
||||||
|
// (`panel_title`) — but this is the one count worth keeping reachable
|
||||||
|
// *without* switching to the tab, since it answers "did I touch anything"
|
||||||
|
// from wherever you are. Outline's count didn't survive the move: it
|
||||||
|
// needs the active leaf, which needs a `&Window` this function doesn't
|
||||||
|
// take, and its list is right there the moment you switch.
|
||||||
|
let changed = match &self.right_panel.diff {
|
||||||
|
Some(Some(snap)) => {
|
||||||
|
let n = snap.files.len() + snap.untracked.len();
|
||||||
|
(n > 0).then_some(n)
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
[
|
[
|
||||||
(
|
(
|
||||||
RightPanelTab::Info,
|
RightPanelTab::Info,
|
||||||
@@ -600,13 +660,23 @@ impl Tty7App {
|
|||||||
.occlude()
|
.occlude()
|
||||||
.flex_shrink_0()
|
.flex_shrink_0()
|
||||||
.child(
|
.child(
|
||||||
chrome_tile(
|
chrome_tile_sized(
|
||||||
Button::new(("right-panel-tab", tab as usize)).icon(icon),
|
Button::new(("right-panel-tab", tab as usize)).icon(icon),
|
||||||
|
crate::ui::app::TILE_SIZE_SM,
|
||||||
|
crate::ui::app::TILE_GLYPH_SM,
|
||||||
active_tab == tab,
|
active_tab == tab,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
.rounded_lg()
|
// `rounded_md` against the chrome tiles' `rounded_lg`: the
|
||||||
.tooltip(label)
|
// corner radius tracks the box, or a 24px tile reads as a
|
||||||
|
// 32px one with its sides shaved off.
|
||||||
|
.rounded_md()
|
||||||
|
.tooltip(match (tab, changed) {
|
||||||
|
(RightPanelTab::Changes, Some(n)) => {
|
||||||
|
SharedString::from(format!("{label} · {n}"))
|
||||||
|
}
|
||||||
|
_ => SharedString::from(label),
|
||||||
|
})
|
||||||
.on_click(cx.listener(move |this, _, _window, cx| {
|
.on_click(cx.listener(move |this, _, _window, cx| {
|
||||||
this.set_right_panel_tab(tab, cx);
|
this.set_right_panel_tab(tab, cx);
|
||||||
})),
|
})),
|
||||||
|
|||||||
Reference in New Issue
Block a user