mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-11-06 00:15:06 +00:00
feat(ui): bring the modal cards onto the v5 design
- Confirmations take v5's alert shape: title and detail as one paragraph, answers beneath, no header row or footer rule, 360px wide and set lower, at eye height. - Dialog buttons take the field's 7px corner and 14px padding; a secondary answer rests on the well's faint fill instead of bare text. - The scrim dims rather than blacks out: 45% dark, 14% light. - The New Workspace form joins the other cards: 12px corner, borderless wells for its fields, an esc cap and the shared 40px footer.
This commit is contained in:
+15
-10
@@ -44,10 +44,11 @@ pub(crate) const FIELD_H: f32 = 28.;
|
||||
const LABEL_GAP: f32 = 6.;
|
||||
const FIELD_GAP: f32 = 14.;
|
||||
|
||||
/// A button: the field's height, a pixel less rounded so a button beside a
|
||||
/// field reads as a different kind of thing.
|
||||
/// A button: the field's height and the field's corner — the v5 confirm
|
||||
/// card's 28px pill-less rectangle, on every card alike.
|
||||
pub(crate) const BUTTON_H: f32 = 28.;
|
||||
const BUTTON_RADIUS: f32 = 6.;
|
||||
const BUTTON_RADIUS: f32 = 7.;
|
||||
const BUTTON_PAD: f32 = 14.;
|
||||
|
||||
/// A key named in a hint: an 18px cap on the faint fill.
|
||||
pub(crate) const KEYCAP: f32 = 18.;
|
||||
@@ -207,8 +208,9 @@ pub(crate) enum Tone {
|
||||
/// already spent on focus and on the rail; a dialog of blue buttons made
|
||||
/// every sheet look like a call to action.
|
||||
Primary,
|
||||
/// Everything else: no fill at rest, the surface's hover rung under the
|
||||
/// pointer.
|
||||
/// Everything else: the well's faint fill at rest, one rung up under the
|
||||
/// pointer. A bare word with no shape read as a link, not as the second
|
||||
/// answer to the question.
|
||||
Secondary,
|
||||
/// The action a card warns about. Red, and only on the one site that has
|
||||
/// earned it — overriding a changed host key.
|
||||
@@ -218,9 +220,9 @@ pub(crate) enum Tone {
|
||||
/// A dialog button. The click handler is attached only while `enabled`: a
|
||||
/// disabled button has to be inert, not merely grey.
|
||||
///
|
||||
/// Disabled, a filled tone sinks to the faint fill with secondary ink — the
|
||||
/// Disabled, every tone sinks to the faint fill with secondary ink — the
|
||||
/// same fall the Commit button takes — so it never advertises an action that
|
||||
/// cannot run, and a secondary one just loses its ink.
|
||||
/// cannot run.
|
||||
pub(crate) fn button(
|
||||
id: impl Into<ElementId>,
|
||||
text: impl Into<SharedString>,
|
||||
@@ -233,7 +235,6 @@ pub(crate) fn button(
|
||||
let theme = cx.theme();
|
||||
let surface: Hsla = gpui::rgb(rungs.base).into();
|
||||
let (fill, hover, ink): (Option<Hsla>, Option<Hsla>, Hsla) = match (tone, enabled) {
|
||||
(Tone::Secondary, false) => (None, None, theme.muted_foreground),
|
||||
// The surface's own next rung, like a well: `muted` is a step off the
|
||||
// window, and on a dark popover it was the card's own colour.
|
||||
(_, false) => (
|
||||
@@ -253,7 +254,11 @@ pub(crate) fn button(
|
||||
theme.danger_foreground,
|
||||
)
|
||||
}
|
||||
(Tone::Secondary, true) => (None, Some(gpui::rgb(rungs.hover).into()), theme.foreground),
|
||||
(Tone::Secondary, true) => (
|
||||
Some(gpui::rgb(rungs.hover).into()),
|
||||
Some(gpui::rgb(rungs.selected).into()),
|
||||
theme.foreground,
|
||||
),
|
||||
};
|
||||
h_flex()
|
||||
.id(id)
|
||||
@@ -261,7 +266,7 @@ pub(crate) fn button(
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.h(px(BUTTON_H))
|
||||
.px(px(12.))
|
||||
.px(px(BUTTON_PAD))
|
||||
.rounded(px(BUTTON_RADIUS))
|
||||
.text_size(rems(TAB_TEXT))
|
||||
.when(tone != Tone::Secondary, |b| {
|
||||
|
||||
+6
-3
@@ -420,11 +420,14 @@ impl Theme {
|
||||
sidebar,
|
||||
rail,
|
||||
popover: self.surface(m.popover),
|
||||
// The v5 scrim: a dim, not a blackout. Dark lands on near-black at
|
||||
// 45%; light on a warm near-black at 14%, just enough to say the
|
||||
// window is waiting without turning the page grey.
|
||||
scrim: Scrim {
|
||||
ink: mix(m.background, 0x000000, 0.82),
|
||||
ink: mix(m.background, 0x000000, 0.92),
|
||||
alpha: match self.dark {
|
||||
true => 0.55,
|
||||
false => 0.30,
|
||||
true => 0.45,
|
||||
false => 0.14,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
+50
-47
@@ -12,15 +12,17 @@
|
||||
//! buttons, so the same app had two ideas of what a dialog looks like, and
|
||||
//! which one you got depended on which question it was.
|
||||
//!
|
||||
//! So all three platforms use this. It is laid out like the other cards, sits
|
||||
//! where they sit, wraps its text, and answers Return and Escape the way the
|
||||
//! native dialogs did — which the call sites were written against.
|
||||
//! So all three platforms use this. It is the other cards' surface, corner,
|
||||
//! scrim and buttons in v5's alert shape — title and detail as one paragraph,
|
||||
//! answers beneath, no header row — wraps its text, and answers Return and
|
||||
//! Escape the way the native dialogs did, which the call sites were written
|
||||
//! against.
|
||||
|
||||
use gpui::{
|
||||
App, Context, EventEmitter, FocusHandle, Focusable, PromptButton, PromptHandle, PromptLevel,
|
||||
PromptResponse, RenderablePromptHandle, Window, div, prelude::*, px, rems,
|
||||
};
|
||||
use gpui_component::{ActiveTheme as _, h_flex};
|
||||
use gpui_component::{ActiveTheme as _, h_flex, v_flex};
|
||||
|
||||
use crate::ui::dialog::{self, Tone};
|
||||
use crate::ui::right_panel::{TAB_TEXT, TEXT};
|
||||
@@ -43,9 +45,12 @@ fn build(
|
||||
handle.with_view(prompt, window, cx)
|
||||
}
|
||||
|
||||
/// A confirmation card is a sentence and two buttons; the worktree form, with
|
||||
/// three fields, is 440.
|
||||
const WIDTH: f32 = 420.;
|
||||
/// A confirmation card is a sentence and two buttons — v5's alert width. The
|
||||
/// worktree form, with three fields, is 440.
|
||||
const ALERT_W: f32 = 360.;
|
||||
|
||||
/// How far down the alert sits, at most.
|
||||
const ALERT_TOP: f32 = 180.;
|
||||
|
||||
pub(crate) struct TextPrompt {
|
||||
message: String,
|
||||
@@ -131,13 +136,10 @@ impl Focusable for TextPrompt {
|
||||
}
|
||||
|
||||
impl Render for TextPrompt {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let theme = cx.theme();
|
||||
let muted = theme.muted_foreground;
|
||||
let border = theme.border;
|
||||
let rungs = dialog::popover_rungs(cx);
|
||||
let escapable = self.cancel_answer().is_some();
|
||||
let has_detail = self.detail.is_some();
|
||||
|
||||
// Answer 0 goes rightmost, where the native dialogs put it and where
|
||||
// `confirm_answers` expects it to land.
|
||||
@@ -162,47 +164,48 @@ impl Render for TextPrompt {
|
||||
}
|
||||
}
|
||||
|
||||
// The other cards' title row, except that the title wraps: theirs are
|
||||
// names, which truncate cleanly, and this one is a question, which
|
||||
// cut short no longer asks anything. One line still sits in the
|
||||
// 48px row exactly where theirs does.
|
||||
let title = h_flex()
|
||||
.flex_none()
|
||||
.items_center()
|
||||
.gap(px(10.))
|
||||
.min_h(px(dialog::HEADER_H))
|
||||
.py(px(12.))
|
||||
.pl(px(dialog::INSET))
|
||||
.pr(px(14.))
|
||||
.when(has_detail, |row| row.border_b_1().border_color(border))
|
||||
// The v5 confirm card: no title row and no footer rule — a question
|
||||
// is one thought, not a form, so it is set as a paragraph with its
|
||||
// answers under it. The title wraps: cut short, a question no longer
|
||||
// asks anything.
|
||||
let text = v_flex()
|
||||
.gap(px(4.))
|
||||
.min_w_0()
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.text_size(rems(TEXT))
|
||||
.font_weight(gpui::FontWeight::MEDIUM)
|
||||
.line_height(rems(TEXT * 1.4))
|
||||
.font_weight(gpui::FontWeight::SEMIBOLD)
|
||||
.child(self.message.clone()),
|
||||
)
|
||||
.when(escapable, |row| row.child(dialog::keycap("esc", cx)));
|
||||
|
||||
let card = dialog::card(WIDTH, cx)
|
||||
.max_w(gpui::relative(0.9))
|
||||
.child(title)
|
||||
.children(self.detail.clone().map(|detail| {
|
||||
dialog::body().child(
|
||||
div()
|
||||
.text_size(rems(TAB_TEXT))
|
||||
.line_height(rems(TAB_TEXT * 1.5))
|
||||
.text_color(muted)
|
||||
.child(detail),
|
||||
)
|
||||
}))
|
||||
.child(
|
||||
dialog::footer(cx)
|
||||
.children(apart)
|
||||
.when(self.answers.len() >= 3, |row| row.child(div().flex_1()))
|
||||
.children(packed),
|
||||
);
|
||||
div()
|
||||
.text_size(rems(TAB_TEXT))
|
||||
.line_height(rems(TAB_TEXT * 1.45))
|
||||
.text_color(muted)
|
||||
.child(detail)
|
||||
}));
|
||||
|
||||
let answers = h_flex()
|
||||
.items_center()
|
||||
.gap(px(8.))
|
||||
.children(apart)
|
||||
.child(div().flex_1())
|
||||
.children(packed);
|
||||
|
||||
let card = dialog::card(ALERT_W, cx)
|
||||
.max_w(gpui::relative(0.9))
|
||||
.gap(px(16.))
|
||||
.pt(px(20.))
|
||||
.px(px(20.))
|
||||
.pb(px(16.))
|
||||
.child(text)
|
||||
.child(answers);
|
||||
|
||||
// Lower than the switcher's drop: an alert is read, not typed into,
|
||||
// and v5 sets it at eye height. A short window pulls it up rather
|
||||
// than pushing the buttons off the bottom.
|
||||
let top = (window.viewport_size().height.as_f32() * 0.22).clamp(16., ALERT_TOP);
|
||||
|
||||
div()
|
||||
.id("text-prompt")
|
||||
@@ -235,7 +238,7 @@ impl Render for TextPrompt {
|
||||
.flex_col()
|
||||
.items_center()
|
||||
.justify_start()
|
||||
.pt(px(crate::ui::switcher::CARD_TOP))
|
||||
.pt(px(top))
|
||||
.child(card)
|
||||
}
|
||||
}
|
||||
|
||||
+18
-9
@@ -2684,11 +2684,15 @@ impl Tty7App {
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.truncate()
|
||||
.text_size(gpui::rems(13. / 16.))
|
||||
.font_weight(gpui::FontWeight::MEDIUM)
|
||||
.text_color(fg)
|
||||
.child(t(L10nKey::AppMenuNewWorkspace)),
|
||||
);
|
||||
)
|
||||
.child(div().pr(px(6.)).child(keycap("esc", cx)));
|
||||
|
||||
let label_col = |text: &'static str| {
|
||||
div()
|
||||
@@ -2698,6 +2702,9 @@ impl Tty7App {
|
||||
.text_color(muted)
|
||||
.child(text)
|
||||
};
|
||||
// The other cards' well — no outline, the faint fill is the shape —
|
||||
// so this form and the worktree prompt draw a field the same way.
|
||||
let well = crate::ui::dialog::well_fill(cx);
|
||||
let field = |inner: gpui::Div| {
|
||||
inner
|
||||
.flex_1()
|
||||
@@ -2705,10 +2712,9 @@ impl Tty7App {
|
||||
.items_center()
|
||||
.gap(px(6.))
|
||||
.px(px(8.))
|
||||
.h(px(30.))
|
||||
.h(px(crate::ui::dialog::FIELD_H))
|
||||
.rounded(crate::ui::rounding::ROW_RADIUS)
|
||||
.border_1()
|
||||
.border_color(border)
|
||||
.bg(well)
|
||||
};
|
||||
|
||||
let name_row = h_flex()
|
||||
@@ -2743,7 +2749,7 @@ impl Tty7App {
|
||||
false => field(h_flex())
|
||||
.id("switcher-form-host")
|
||||
.cursor_pointer()
|
||||
.hover(move |r| r.bg(hover))
|
||||
.hover(move |r| r.bg(picked_bg))
|
||||
.child(host_glyph(chosen_local, chosen_dot))
|
||||
.child(
|
||||
div()
|
||||
@@ -2868,7 +2874,7 @@ impl Tty7App {
|
||||
.gap(px(8.))
|
||||
.child(
|
||||
label_col(t(L10nKey::SwitcherFormHost))
|
||||
.h(px(30.))
|
||||
.h(px(crate::ui::dialog::FIELD_H))
|
||||
.flex()
|
||||
.items_center(),
|
||||
)
|
||||
@@ -2876,8 +2882,8 @@ impl Tty7App {
|
||||
|
||||
let footer = h_flex()
|
||||
.items_center()
|
||||
.px(px(12.))
|
||||
.py(px(8.))
|
||||
.h(px(crate::ui::dialog::FOOTER_H))
|
||||
.px(px(crate::ui::dialog::INSET))
|
||||
.border_t_1()
|
||||
.border_color(border)
|
||||
.text_size(gpui::rems(11. / 16.))
|
||||
@@ -2890,11 +2896,14 @@ impl Tty7App {
|
||||
v_flex()
|
||||
.w(px(card_w))
|
||||
.map(|panel| crate::ui::theme::floating_surface(panel, cx))
|
||||
.rounded(px(CARD_RADIUS))
|
||||
.overflow_hidden()
|
||||
.child(header)
|
||||
.child(
|
||||
v_flex()
|
||||
.p(px(12.))
|
||||
.px(px(crate::ui::dialog::INSET))
|
||||
.pt(px(16.))
|
||||
.pb(px(18.))
|
||||
.gap(px(10.))
|
||||
.child(name_row)
|
||||
.child(host_row),
|
||||
|
||||
Reference in New Issue
Block a user