Vectrum
Sign In
228 lines · 7.2 KB · Rust
//! The composer's surroundings, laid out like T3 Code's: banners docked on
//! top of it that tuck under its edge, the context strip hanging below it,
//! and the small controls in its footer and in that strip.

use gpui_kit::component::{Icon, IconName, h_flex, tooltip::Tooltip, v_flex};
use gpui_kit::prelude::FluentBuilder as _;
use gpui_kit::*;

use crate::ui::metal::{self, MetalButton, Palette};

/// How far docked banners and the context strip sit in from the composer's
/// sides.
pub const INSET: f32 = 22.;
/// How far a docked banner tucks under whatever sits below it.
pub const OVERLAP: f32 = 17.;
/// The icon column of a banner row.
pub const ICON_COLUMN: f32 = 24.;
/// The widest the conversation and the composer grow.
pub const CONTENT_WIDTH: f32 = 736.;

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Tone {
    Neutral,
    Warning,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Density {
    Default,
    Comfortable,
    Spacious,
}

/// `a` moved `t` of the way towards `b`.
pub fn mix(a: Hsla, b: Hsla, t: f32) -> Hsla {
    let (a, b): (Rgba, Rgba) = (a.into(), b.into());
    let lerp = |x: f32, y: f32| x + (y - x) * t;
    Rgba { r: lerp(a.r, b.r), g: lerp(a.g, b.g), b: lerp(a.b, b.b), a: lerp(a.a, b.a) }.into()
}

/// A surface docked on top of the composer (the dock insets it from the
/// composer's sides): rounded on top, its bottom tucked under the composer
/// (or under the next docked surface). `joined` drops the rounded top where it sits under another
/// docked surface, so the stack reads as one card.
pub fn banner(tone: Tone, density: Density, joined: bool, p: &Palette) -> Div {
    let (pad_x, pad_y) = match density {
        Density::Default => (4., 4.),
        Density::Comfortable => (4., 5.),
        Density::Spacious => (12., 12.),
    };
    let (top, bottom) = (p.s_hi, p.s);
    let (top, bottom, ring) = match tone {
        Tone::Neutral => (top, bottom, None),
        Tone::Warning => (mix(top, p.amber, 0.08), mix(bottom, p.amber, 0.08), Some(p.amber.opacity(0.28))),
    };
    let mut shadow = p.raise_lite.clone();
    if let Some(ring) = ring {
        shadow.push(BoxShadow::new(px(0.), px(0.), ring).spread_radius(px(1.)).inset());
    }
    v_flex()
        .mb(px(-OVERLAP))
        .px(px(pad_x))
        .pt(px(pad_y))
        .pb(px(pad_y + OVERLAP))
        .when(!joined, |this| this.rounded_t(px(16.)))
        .bg(metal::vertical((top, bottom)))
        .shadow(shadow)
        .text_size(px(12.))
        .line_height(px(16.))
        .text_color(p.ink)
}

/// One row of a docked banner: an icon column, the content, and actions on
/// the right.
pub fn row() -> Div {
    h_flex().w_full().min_h(px(ICON_COLUMN)).gap(px(4.))
}

/// The icon column of a banner row.
pub fn icon_cell(icon: Option<impl IntoElement>, p: &Palette) -> Div {
    div()
        .flex()
        .flex_none()
        .items_center()
        .justify_center()
        .size(px(ICON_COLUMN))
        .text_color(p.ink3)
        .children(icon)
}

/// The content column of a banner row.
pub fn content() -> Div {
    h_flex().flex_1().min_w_0().gap(px(4.))
}

/// The chevron that says a banner folds: down while open, up while folded.
pub fn toggle_icon(expanded: bool, p: &Palette) -> Div {
    div()
        .flex()
        .flex_none()
        .items_center()
        .justify_center()
        .size(px(24.))
        .text_color(p.ink3)
        .child(Icon::new(if expanded { IconName::ChevronDown } else { IconName::ChevronUp }).size(px(14.)))
}

/// A count in a banner's actions: `2/5`, `3`.
pub fn count(text: impl Into<SharedString>, p: &Palette) -> Div {
    div()
        .flex()
        .flex_none()
        .items_center()
        .justify_center()
        .min_w(px(ICON_COLUMN))
        .font_weight(FontWeight::MEDIUM)
        .text_color(p.ink3)
        .child(text.into())
}

/// A small icon button in a banner's actions, like its dismiss X.
pub fn icon_button(id: impl Into<ElementId>, icon: impl Into<Icon>, tooltip: &'static str) -> MetalButton {
    MetalButton::new(id).ghost().mini().size(px(24.)).icon(icon).tooltip(tooltip)
}

/// A footer control of the composer: a ghost button with an icon, a label
/// and a caret, like T3 Code's model, reasoning and access triggers.
pub fn control(id: impl Into<ElementId>) -> MetalButton {
    MetalButton::new(id).ghost().compact().h(px(28.)).px(px(10.)).text_size(px(13.))
}

/// The hairline between footer controls.
pub fn separator(p: &Palette) -> Div {
    metal::vrule(p).mx(px(2.))
}

/// The strip hanging below the composer, tucked under its bottom edge.
pub fn strip(p: &Palette) -> Div {
    h_flex()
        .mx(px(INSET))
        .mt(px(-16.))
        .pt(px(20.))
        .pb(px(4.))
        .pl(px(4.))
        .pr(px(8.))
        .gap(px(4.))
        .rounded_b(px(18.))
        .bg(metal::vertical(p.region))
        .shadow(p.region_sh.clone())
        .text_size(px(12.))
        .text_color(p.ink3)
}

/// A control in the strip: a small ghost button, icon, label and caret.
pub fn strip_control(id: impl Into<ElementId>) -> MetalButton {
    MetalButton::new(id).ghost().mini().h(px(24.)).px(px(7.)).gap(px(4.)).text_size(px(12.))
}

/// A strip control that can no longer be changed: just its icon and label.
pub fn strip_label(
    id: impl Into<ElementId>,
    icon: impl Into<Icon>,
    label: impl Into<SharedString>,
    tooltip: impl Into<SharedString>,
    p: &Palette,
) -> Stateful<Div> {
    let tooltip: SharedString = tooltip.into();
    h_flex()
        .id(id)
        .flex_none()
        .h(px(24.))
        .px(px(7.))
        .gap(px(4.))
        .text_size(px(12.))
        .text_color(p.ink3)
        .child(icon.into().size(px(12.)))
        .child(div().max_w(px(240.)).truncate().child(label.into()))
        .tooltip(move |window, cx| Tooltip::new(tooltip.clone()).build(window, cx))
}

/// The vertical hairline between strip controls.
pub fn strip_separator(p: &Palette) -> Div {
    div().flex_none().w(px(1.)).h(px(14.)).mx(px(2.)).bg(p.rule)
}

/// A group label in a picker menu: `Reasoning`, `Workspace`.
pub fn menu_label(text: impl Into<SharedString>, p: &Palette) -> Div {
    div()
        .px(px(8.))
        .pt(px(6.))
        .pb(px(4.))
        .text_size(px(12.))
        .font_weight(FontWeight::MEDIUM)
        .text_color(p.ink3)
        .child(text.into())
}

/// One choice in a picker menu. Like T3 Code's, the current choice is shown
/// by a tint rather than a check.
pub fn menu_item(id: impl Into<ElementId>, selected: bool, enabled: bool, p: &Palette) -> Stateful<Div> {
    h_flex()
        .id(id)
        .min_h(px(28.))
        .px(px(8.))
        .py(px(4.))
        .gap(px(8.))
        .rounded(px(8.))
        .text_size(px(13.5))
        .text_color(p.ink)
        .when(selected, |this| this.bg(p.ink.opacity(0.08)))
        .map(|this| {
            if enabled {
                this.cursor_pointer().hover(|style| style.bg(p.menu_row_hover))
            } else {
                this.opacity(0.64)
            }
        })
}

/// The hairline between groups of a picker menu.
pub fn menu_separator(p: &Palette) -> Div {
    div().mx(px(8.)).my(px(4.)).child(metal::rule(p))
}

/// The small outlined badge T3 Code marks a model's default option with.
pub fn outline_badge(text: impl Into<SharedString>, p: &Palette) -> Div {
    metal::tag(text, p)
}