//! 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, 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, 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, icon: impl Into, 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) -> 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) -> 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, icon: impl Into, label: impl Into, tooltip: impl Into, p: &Palette, ) -> Stateful
{ 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, 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, selected: bool, enabled: bool, p: &Palette) -> Stateful
{ 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, p: &Palette) -> Div { metal::tag(text, p) }