mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-22 08: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::daemon::protocol::PaneProcs;
|
||||
use crate::terminal::git_diff::{self, DiffSnapshot};
|
||||
use crate::ui::app::{
|
||||
CONTENT_INSET, TILE_GLYPH_SM, TILE_SIZE_SM, Tty7App, tile_trailing_inset,
|
||||
tile_trailing_inset_sm,
|
||||
};
|
||||
use crate::ui::app::{CONTENT_INSET, TILE_GLYPH_SM, TILE_SIZE_SM, Tty7App, tile_trailing_inset_sm};
|
||||
use crate::ui::scrollbar::with_vertical_scrollbar;
|
||||
|
||||
/// 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())
|
||||
.items_center()
|
||||
.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))
|
||||
.child(div().flex_1())
|
||||
// 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 —
|
||||
/// so the two that were saying "this is a header" merge into one that also
|
||||
/// 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(
|
||||
&self,
|
||||
text: &str,
|
||||
@@ -384,6 +395,9 @@ impl Tty7App {
|
||||
) -> AnyElement {
|
||||
let tabs = (!cfg!(target_os = "macos")).then(|| self.right_panel_tabs(cx));
|
||||
let has_trailing = trailing.is_some();
|
||||
if tabs.is_none() && !has_trailing {
|
||||
return div().flex_none().into_any_element();
|
||||
}
|
||||
h_flex()
|
||||
.flex_none()
|
||||
// Tall enough to seat the chrome-scale tiles when it carries them;
|
||||
@@ -396,12 +410,10 @@ impl Tty7App {
|
||||
.items_center()
|
||||
.pl(px(CONTENT_INSET))
|
||||
// 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
|
||||
// tab's own control, whose glyph sits a different distance inside its
|
||||
// box than the chrome-scale tab tiles do.
|
||||
// window; a label-only header just takes the plain inset. `_SM` covers
|
||||
// both tile cases now — the tab tiles are body-scale too.
|
||||
.pr(px(match (&tabs, has_trailing) {
|
||||
(Some(_), _) => tile_trailing_inset(),
|
||||
(None, true) => tile_trailing_inset_sm(),
|
||||
(Some(_), _) | (None, true) => tile_trailing_inset_sm(),
|
||||
(None, false) => CONTENT_INSET,
|
||||
}))
|
||||
// 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))),
|
||||
),
|
||||
);
|
||||
// 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
|
||||
// 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
|
||||
@@ -969,6 +979,7 @@ impl Tty7App {
|
||||
.child(crate::ui::app::title_bar_drag(
|
||||
controls.id("sidebar-titlebar-drag"),
|
||||
))
|
||||
.child(workspace_head)
|
||||
.child(top_bar)
|
||||
.child(crate::ui::scrollbar::with_vertical_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 {
|
||||
/// Diameter of the workspace avatar, matching the 32px chrome tiles beside
|
||||
/// it so the corner reads as one row of controls.
|
||||
const AVATAR_PX: f32 = 26.0;
|
||||
/// Diameter of the workspace avatar. Sized to the rail's row glyphs rather
|
||||
/// than to a chrome tile: this control is the head of the tab list, so it
|
||||
/// 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
|
||||
/// plus a chevron, opening the one menu that owns everything
|
||||
/// workspace-scoped — and the app-level entries the "⋯" used to hold.
|
||||
/// The rail's head: which workspace this window is on, plus the menu that
|
||||
/// owns everything workspace-scoped — and the app-level entries the "⋯"
|
||||
/// used to hold.
|
||||
///
|
||||
/// 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
|
||||
@@ -363,14 +364,22 @@ impl Tty7App {
|
||||
/// defensible, together undiscoverable. Nothing in the window even *said*
|
||||
/// 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
|
||||
/// pushed off the corner by a long repo name, and it sits level with the
|
||||
/// icon tiles instead of introducing a third shape. The full name is in the
|
||||
/// tooltip and checked in the menu.
|
||||
/// It used to be a monogram in the window's top-right corner. Two things
|
||||
/// were wrong with that. Physically: the corner it sat in is the *panel's*
|
||||
/// top zone while the panel is open, so a control that has nothing to do
|
||||
/// 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
|
||||
/// 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() {
|
||||
// The tile itself becomes the field — same height, same radius, and
|
||||
// the hover fill standing in for "this control is being edited".
|
||||
@@ -382,10 +391,13 @@ impl Tty7App {
|
||||
.id("workspace-rename")
|
||||
.flex_shrink_0()
|
||||
.items_center()
|
||||
.h(px(32.))
|
||||
.w(px(150.))
|
||||
.px(px(8.))
|
||||
.rounded_lg()
|
||||
// Full rail width, not the old fixed 150px: this control is a row
|
||||
// in a column now, so it takes the column's width like every other
|
||||
// row does.
|
||||
.h(px(30.))
|
||||
.w_full()
|
||||
.px(px(7.))
|
||||
.rounded_md()
|
||||
.bg(cx.theme().sidebar_accent)
|
||||
// Swallow mouse-downs (including the double-click that selects a
|
||||
// word) so they never reach the enclosing TitleBar and zoom the
|
||||
@@ -418,34 +430,55 @@ impl Tty7App {
|
||||
|
||||
div()
|
||||
.occlude()
|
||||
.flex_shrink_0()
|
||||
.w_full()
|
||||
.child(
|
||||
Button::new("titlebar-workspace")
|
||||
Button::new("rail-workspace-head")
|
||||
.custom(chrome_tile_variant(cx))
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.items_center()
|
||||
.gap(px(3.))
|
||||
.gap(px(6.))
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_shrink_0()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.size(px(Self::AVATAR_PX))
|
||||
.rounded_full()
|
||||
.bg(cx.theme().secondary)
|
||||
.text_size(px(11.))
|
||||
.text_size(px(10.))
|
||||
.font_weight(FontWeight::SEMIBOLD)
|
||||
.child(monogram),
|
||||
)
|
||||
// A chevron, unlike the toggles beside it: those do
|
||||
// one thing on click, this opens something, and the
|
||||
// The name shrinks and truncates rather than pushing
|
||||
// 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.
|
||||
.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()
|
||||
.h(px(32.))
|
||||
.rounded_lg()
|
||||
.w_full()
|
||||
.h(px(30.))
|
||||
.rounded_md()
|
||||
.tooltip(SharedString::from(current))
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.toggle_switcher(window, cx);
|
||||
@@ -518,12 +551,15 @@ impl Tty7App {
|
||||
.flex_shrink_0()
|
||||
.items_center()
|
||||
.gap(px(2.))
|
||||
// The workspace control leads the corner chrome: it is the only
|
||||
// thing in the window that says *which* workspace this is, which
|
||||
// starts to matter the moment there are two. It also absorbed the
|
||||
// old "⋯" menu, so the corner has one menu instead of two adjacent
|
||||
// ones, and nothing workspace-scoped is left behind a modifier
|
||||
// gesture or a palette entry the user has to already know about.
|
||||
// Two controls, both window-scoped, and that is the whole corner now.
|
||||
// The workspace chip used to lead this group; it moved to the rail's
|
||||
// head (`tab_sidebar`), where the list it names actually lives. What
|
||||
// forced the move is that this group's other host is the *panel's* top
|
||||
// zone, and a panel the user can drag down to 200px cannot seat the
|
||||
// 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
|
||||
// right edge in the chrome — hence `inset - TILE_PAD`, which puts the
|
||||
// *glyph's ink* there instead of its hit box.
|
||||
@@ -562,13 +598,37 @@ impl Tty7App {
|
||||
),
|
||||
)
|
||||
.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 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> {
|
||||
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,
|
||||
@@ -600,13 +660,23 @@ impl Tty7App {
|
||||
.occlude()
|
||||
.flex_shrink_0()
|
||||
.child(
|
||||
chrome_tile(
|
||||
chrome_tile_sized(
|
||||
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,
|
||||
cx,
|
||||
)
|
||||
.rounded_lg()
|
||||
.tooltip(label)
|
||||
// `rounded_md` against the chrome tiles' `rounded_lg`: the
|
||||
// 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| {
|
||||
this.set_right_panel_tab(tab, cx);
|
||||
})),
|
||||
|
||||
Reference in New Issue
Block a user