diff --git a/src/ui/code_editor.rs b/src/ui/code_editor.rs index 4cc4586c..0711a4cb 100644 --- a/src/ui/code_editor.rs +++ b/src/ui/code_editor.rs @@ -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) -> 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::().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() } } diff --git a/src/ui/switcher.rs b/src/ui/switcher.rs index 3450f1b9..6abbae0d 100644 --- a/src/ui/switcher.rs +++ b/src/ui/switcher.rs @@ -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, 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::*; diff --git a/src/ui/tab_strip.rs b/src/ui/tab_strip.rs index 522f53dd..0c2d23c9 100644 --- a/src/ui/tab_strip.rs +++ b/src/ui/tab_strip.rs @@ -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, @@ -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.))