mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-11-06 00:15:06 +00:00
fix(ui): tidy menus, the switcher and the editor's conflict strip
- New Tab menu: host endpoints elide against fixed caps instead of being clipped mid-glyph at the panel edge; notes are right-aligned again. - Switcher: a workspace's tab count reads '1 tab', not a bare 1 beside the slot number. - Editor 'changed on disk' strip: neutral with an amber dot and dialog buttons, Keep mine as the safe primary, instead of an amber wash with a library-default outlined button. - Title bar search keeps its full label with a document docked.
This commit is contained in:
+56
-26
@@ -5,7 +5,7 @@ use std::sync::Arc;
|
||||
use gpui::prelude::*;
|
||||
use gpui::{
|
||||
AnyElement, Context, Entity, Focusable as _, PromptLevel, SharedString, Subscription, Window,
|
||||
div, px,
|
||||
div, px, rems,
|
||||
};
|
||||
use gpui_component::button::{Button, ButtonVariants as _};
|
||||
use gpui_component::input::{Input, InputEvent, InputState, Position, TabSize};
|
||||
@@ -1550,42 +1550,72 @@ impl Tty7App {
|
||||
}
|
||||
|
||||
fn render_editor_conflict_banner(&self, cx: &mut Context<Self>) -> AnyElement {
|
||||
use crate::ui::dialog::{self, Tone};
|
||||
let tab_ix = self.active;
|
||||
let ix = self.tab_code().map(|c| c.active).unwrap_or(0);
|
||||
let theme = cx.theme();
|
||||
let rungs = cx.global::<crate::ui::presets::Surfaces>().window;
|
||||
// The floating notices' grammar, laid flat: a neutral strip over a
|
||||
// hairline, and the state carried by one amber dot. A strip tinted
|
||||
// amber end to end, with a white outlined button from the component
|
||||
// library beside a bare-text one, was three visual languages in one
|
||||
// 32px row.
|
||||
//
|
||||
// Keep mine is the filled answer because it is the safe one: Reload
|
||||
// throws away every unsaved edit in the file, so it is offered, not
|
||||
// pressed on the reader.
|
||||
h_flex()
|
||||
.flex_none()
|
||||
.w_full()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.px_2()
|
||||
.py_1()
|
||||
.bg(cx.theme().warning.opacity(0.15))
|
||||
.gap(px(8.))
|
||||
.pl(px(dialog::INSET))
|
||||
.pr(px(6.))
|
||||
.h(px(dialog::FOOTER_H))
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().border)
|
||||
.text_sm()
|
||||
.child(div().flex_1().child(crate::ui::i18n::t(
|
||||
crate::ui::i18n::L10nKey::FileChangedOnDisk,
|
||||
)))
|
||||
.border_color(theme.border)
|
||||
.text_size(rems(crate::ui::right_panel::TAB_TEXT))
|
||||
.child(
|
||||
Button::new("editor-conflict-reload")
|
||||
.label(crate::ui::i18n::t(crate::ui::i18n::L10nKey::Reload))
|
||||
.small()
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.editor_reload_from_disk(tab_ix, ix, window, cx);
|
||||
})),
|
||||
div()
|
||||
.flex_none()
|
||||
.size(px(6.))
|
||||
.rounded_full()
|
||||
.bg(theme.warning),
|
||||
)
|
||||
.child(
|
||||
Button::new("editor-conflict-keep")
|
||||
.label(crate::ui::i18n::t(crate::ui::i18n::L10nKey::KeepMine))
|
||||
.ghost()
|
||||
.small()
|
||||
.on_click(cx.listener(move |this, _, _w, cx| {
|
||||
if let Some(f) = this.tab_code_mut().and_then(|c| c.files.get_mut(ix)) {
|
||||
f.conflict = false;
|
||||
cx.notify();
|
||||
}
|
||||
})),
|
||||
div()
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.truncate()
|
||||
.child(crate::ui::i18n::t(
|
||||
crate::ui::i18n::L10nKey::FileChangedOnDisk,
|
||||
)),
|
||||
)
|
||||
.child(dialog::button(
|
||||
"editor-conflict-reload",
|
||||
crate::ui::i18n::t(crate::ui::i18n::L10nKey::Reload),
|
||||
Tone::Secondary,
|
||||
true,
|
||||
rungs,
|
||||
cx,
|
||||
cx.listener(move |this, _, window, cx| {
|
||||
this.editor_reload_from_disk(tab_ix, ix, window, cx);
|
||||
}),
|
||||
))
|
||||
.child(dialog::button(
|
||||
"editor-conflict-keep",
|
||||
crate::ui::i18n::t(crate::ui::i18n::L10nKey::KeepMine),
|
||||
Tone::Primary,
|
||||
true,
|
||||
rungs,
|
||||
cx,
|
||||
cx.listener(move |this, _, _w, cx| {
|
||||
if let Some(f) = this.tab_code_mut().and_then(|c| c.files.get_mut(ix)) {
|
||||
f.conflict = false;
|
||||
cx.notify();
|
||||
}
|
||||
}),
|
||||
))
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
+12
-7
@@ -2567,9 +2567,10 @@ impl Tty7App {
|
||||
.gap(px(2.))
|
||||
.text_size(gpui::rems(11.5 / 16.))
|
||||
.text_color(muted)
|
||||
.when(!row.tabs.is_empty(), |c| {
|
||||
c.child(row.tabs.len().to_string())
|
||||
})
|
||||
// In words, the way the tab column's header says it: a
|
||||
// bare `1` here sat a few pixels from the slot number after
|
||||
// the name, and the two read as the same thing twice.
|
||||
.when(!row.tabs.is_empty(), |c| c.child(tab_count(row.tabs.len())))
|
||||
.children(badge.map(|(label, _here)| {
|
||||
div()
|
||||
.text_size(gpui::rems(11. / 16.))
|
||||
@@ -2973,10 +2974,7 @@ impl Tty7App {
|
||||
.font_weight(gpui::FontWeight::MEDIUM)
|
||||
.child(row.name.clone()),
|
||||
)
|
||||
.child(div().flex_shrink_0().child(match row.tabs.len() {
|
||||
1 => t(L10nKey::SwitcherTabCountOne).to_string(),
|
||||
n => t_fmt(L10nKey::SwitcherTabCount, &[("n", &n.to_string())]),
|
||||
})),
|
||||
.child(div().flex_shrink_0().child(tab_count(row.tabs.len()))),
|
||||
);
|
||||
|
||||
for (nth, i) in hits.iter().enumerate() {
|
||||
@@ -3447,6 +3445,13 @@ fn keycap(label: impl Into<gpui::SharedString>, cx: &App) -> AnyElement {
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
/// "1 tab", "3 tabs" — how both columns count a workspace's tabs.
|
||||
fn tab_count(n: usize) -> String {
|
||||
match n {
|
||||
1 => t(L10nKey::SwitcherTabCountOne).to_string(),
|
||||
n => t_fmt(L10nKey::SwitcherTabCount, &[("n", &n.to_string())]),
|
||||
}
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
+50
-13
@@ -1,7 +1,7 @@
|
||||
use gpui::{
|
||||
Animation, AnimationExt as _, AnyElement, App, Axis, Bounds, Context, FontWeight, MouseButton,
|
||||
Pixels, SharedString, Window, canvas, deferred, div, ease_out_quint, linear_color_stop,
|
||||
linear_gradient, prelude::*, px, relative,
|
||||
linear_gradient, prelude::*, px,
|
||||
};
|
||||
use gpui_component::button::{Button, ButtonCustomVariant, ButtonVariants as _};
|
||||
use gpui_component::input::Input;
|
||||
@@ -1005,10 +1005,19 @@ fn menu_hosts(
|
||||
/// Both halves are cut rather than allowed to push: the panel stops at
|
||||
/// [`MENU_W`], and a descriptive host name next to a long `user@host:port`
|
||||
/// asks for more than that. Which half gives way is the whole point — the name
|
||||
/// is what the reader is picking by, so the endpoint is capped at half the row
|
||||
/// and elides first, and the name takes everything left over. Sized the other
|
||||
/// way round (name growing from nothing, endpoint at its natural width) a long
|
||||
/// address squeezes the name down to `..` and the row names nothing at all.
|
||||
/// is what the reader is picking by, so it keeps its width up to
|
||||
/// [`MENU_NAME_W`], and the endpoint takes whatever is left and elides first.
|
||||
/// Sized the other way round (name growing from nothing, endpoint at its
|
||||
/// natural width) a long address squeezes the name down to `..` and the row
|
||||
/// names nothing at all.
|
||||
///
|
||||
/// Both caps are fixed lengths, never a share of the row. The panel is sized
|
||||
/// to fit its content, so while the text is laid out no row has a width yet:
|
||||
/// anything capped by `relative(..)` or by flex shrinking measures at full
|
||||
/// length, is squeezed afterwards, and is clipped mid-glyph at the panel edge
|
||||
/// (`git@ssh.github.con`) instead of ending in an ellipsis. A pixel cap is
|
||||
/// known up front, so the text elides against it. The two caps and the row's
|
||||
/// insets add up to [`MENU_W`].
|
||||
///
|
||||
/// An empty note drops the right half entirely rather than leaving a zero-width
|
||||
/// child to hold the `gap_3` open — the name is then free to use the full row,
|
||||
@@ -1020,19 +1029,43 @@ fn menu_row(label: SharedString, note: SharedString, cx: &gpui::App) -> impl Int
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.gap_3()
|
||||
.child(div().flex_1().min_w_0().truncate().child(label))
|
||||
.child(
|
||||
div()
|
||||
.flex_shrink_0()
|
||||
.max_w(match note.is_empty() {
|
||||
true => MENU_W - MENU_ROW_INSET,
|
||||
false => MENU_NAME_W,
|
||||
})
|
||||
.truncate()
|
||||
.child(label),
|
||||
)
|
||||
.when(!note.is_empty(), |this| {
|
||||
// Pushed right by the cell, not by `text_right`: a line set
|
||||
// with `truncate` ignores its alignment and hugs the name.
|
||||
this.child(
|
||||
div()
|
||||
.flex_shrink_0()
|
||||
.max_w(relative(0.5))
|
||||
.truncate()
|
||||
.text_color(muted)
|
||||
.child(note),
|
||||
h_flex().flex_1().justify_end().child(
|
||||
div()
|
||||
.max_w(MENU_NOTE_W)
|
||||
.truncate()
|
||||
.text_color(muted)
|
||||
.child(note),
|
||||
),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// The most of a [`menu_row`] its name keeps before it elides — the larger
|
||||
/// share, since the name is what the row is picked by.
|
||||
const MENU_NAME_W: Pixels = px(172.);
|
||||
|
||||
/// The most the endpoint beside a name gets: room for `user@host:port` on a
|
||||
/// typical host, and the gap and the insets take the rest of [`MENU_W`].
|
||||
const MENU_NOTE_W: Pixels = px(150.);
|
||||
|
||||
/// What the panel spends on either side of a row's content: its own 5px
|
||||
/// inset and the row's 8px padding, twice.
|
||||
const MENU_ROW_INSET: Pixels = px(26.);
|
||||
|
||||
/// The words behind the status dot's colour.
|
||||
pub(crate) fn agent_status_label(
|
||||
status: Option<crate::core::cli_agent::AgentStatus>,
|
||||
@@ -2444,7 +2477,11 @@ impl Tty7App {
|
||||
.id("titlebar-search")
|
||||
.occlude()
|
||||
.w(px(TITLEBAR_SEARCH_W))
|
||||
.max_w(gpui::relative(0.5))
|
||||
// Enough of the bar that the label and its chord still
|
||||
// fit with a document docked beside the terminal; at a
|
||||
// half it read `Search Everywhe…` in exactly the
|
||||
// layout people search from most.
|
||||
.max_w(gpui::relative(0.6))
|
||||
.h(px(TITLEBAR_SEARCH_H))
|
||||
.items_center()
|
||||
.gap(px(7.))
|
||||
|
||||
Reference in New Issue
Block a user