//! 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, as Settings →
/// Appearance → Chat width says: 736px, 1152px, or the whole pane.
pub fn content_width() -> Pixels {
match crate::settings::get(|settings| settings.chat_width) {
crate::settings::ChatWidth::Comfortable => px(736.),
crate::settings::ChatWidth::Wide => px(1152.),
crate::settings::ChatWidth::Full => px(100_000.),
}
}
#[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. Like
/// T3 Code's tray it is a solid shelf with a hairline edge, filled
/// a shade above the pane so it reads in every theme.
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(mix(p.s, p.s_hi, 0.75))
.border_1()
.border_color(p.ink.opacity(if p.dark { 0.14 } else { 0.16 }))
.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)
}