use gpui::prelude::*;
use gpui::{div, px, App, Div, ElementId, Hsla, SharedString, Stateful};
use crate::icon::{ensure_font, IconName, FONT_FAMILY};
use crate::style::{TextOverflowExt, MONO_FAMILY};
use crate::theme::{theme, ColorName, Theme};
pub(crate) const ROW_HEIGHT: f32 = 21.0;
pub(crate) const BAR_HEIGHT: f32 = 29.0;
pub(crate) const MONO_SIZE: f32 = 11.0;
pub(crate) const LABEL_SIZE: f32 = 11.0;
pub(crate) const SIDEBAR_WIDTH: f32 = 292.0;
pub(crate) const NAV_WIDTH: f32 = 200.0;
pub(crate) const FLEX_COLUMN_MIN: f32 = 96.0;
#[derive(Debug, Clone, Copy)]
pub(crate) struct Ink {
pub chrome: Hsla,
pub chrome_active: Hsla,
pub content: Hsla,
pub stripe: Hsla,
pub hover: Hsla,
pub selected: Hsla,
pub selected_text: Hsla,
pub border: Hsla,
pub text: Hsla,
pub dim: Hsla,
pub accent: Hsla,
pub success: Hsla,
pub warning: Hsla,
pub danger: Hsla,
pub info: Hsla,
pub tag: Hsla,
pub attr: Hsla,
pub value: Hsla,
pub punct: Hsla,
pub property: Hsla,
pub box_margin: Hsla,
pub box_border: Hsla,
pub box_padding: Hsla,
pub box_content: Hsla,
}
impl Ink {
pub fn new(t: &Theme) -> Self {
let dark = t.scheme.is_dark();
let chrome = if dark {
t.color(ColorName::Dark, 7).hsla()
} else {
t.color(ColorName::Gray, 1).hsla()
};
let content = if dark {
t.color(ColorName::Dark, 8).hsla()
} else {
t.white.hsla()
};
let shade = if dark { 4 } else { 7 };
Ink {
chrome,
chrome_active: if dark {
t.color(ColorName::Dark, 5).hsla()
} else {
t.white.hsla()
},
content,
stripe: if dark {
t.color(ColorName::Dark, 7).hsla()
} else {
t.color(ColorName::Gray, 0).hsla()
},
hover: t.surface_hover().hsla(),
selected: t.primary().hsla(),
selected_text: t.white.hsla(),
border: t.border().hsla(),
text: t.text().hsla(),
dim: t.dimmed().hsla(),
accent: t.primary().hsla(),
success: t.success().hsla(),
warning: t.warning().hsla(),
danger: t.danger().hsla(),
info: t.info().hsla(),
tag: t.color(ColorName::Violet, shade).hsla(),
attr: t.color(ColorName::Orange, shade).hsla(),
value: t.color(ColorName::Blue, shade).hsla(),
punct: t.dimmed().hsla(),
property: t.color(ColorName::Teal, shade).hsla(),
box_margin: t.color(ColorName::Orange, if dark { 8 } else { 2 }).hsla(),
box_border: t.color(ColorName::Yellow, if dark { 8 } else { 2 }).hsla(),
box_padding: t.color(ColorName::Green, if dark { 8 } else { 2 }).hsla(),
box_content: t.color(ColorName::Blue, if dark { 8 } else { 2 }).hsla(),
}
}
pub fn read(cx: &App) -> Self {
Ink::new(theme(cx))
}
}
pub(crate) fn glyph(name: IconName, size: f32, color: Hsla, cx: &App) -> Div {
ensure_font(cx);
div()
.flex()
.items_center()
.justify_center()
.flex_none()
.w(px(size))
.h(px(size))
.font_family(FONT_FAMILY)
.text_size(px(size))
.line_height(px(size))
.text_color(color)
.child(SharedString::new_static(name.glyph()))
}
pub(crate) fn hairline(ink: &Ink) -> Div {
div().h(px(1.0)).w_full().flex_none().bg(ink.border)
}
pub(crate) fn hairline_v(ink: &Ink) -> Div {
div().w(px(1.0)).h_full().flex_none().bg(ink.border)
}
pub(crate) fn tool_button(
id: impl Into<ElementId>,
icon: IconName,
tooltip: &'static str,
active: bool,
ink: &Ink,
cx: &App,
) -> Stateful<Div> {
let fg = if active { ink.accent } else { ink.dim };
let hover_bg = ink.hover;
div()
.id(id.into())
.flex()
.items_center()
.justify_center()
.flex_none()
.w(px(24.0))
.h(px(21.0))
.rounded(px(4.0))
.when(active, |el| el.bg(ink.chrome_active))
.hover(move |st| st.bg(hover_bg))
.tooltip(crate::overlay::tooltip(tooltip))
.child(glyph(icon, 13.0, fg, cx))
}
pub(crate) fn filter_pill(
id: impl Into<ElementId>,
label: impl Into<SharedString>,
active: bool,
ink: &Ink,
) -> Stateful<Div> {
let hover_bg = ink.hover;
div()
.id(id.into())
.flex()
.flex_none()
.items_center()
.h(px(17.0))
.px(px(7.0))
.rounded(px(9.0))
.text_size(px(LABEL_SIZE))
.when(active, |el| {
el.bg(ink.selected).text_color(ink.selected_text)
})
.when(!active, |el| {
el.text_color(ink.dim).hover(move |st| st.bg(hover_bg))
})
.child(label.into())
}
pub(crate) fn filter_bar(ink: &Ink) -> Div {
div()
.flex()
.flex_none()
.items_center()
.gap(px(6.0))
.h(px(26.0))
.px(px(8.0))
.w_full()
.bg(ink.chrome)
.border_b_1()
.border_color(ink.border)
}
pub(crate) fn header_cell(label: impl Into<SharedString>, width: Option<f32>, ink: &Ink) -> Div {
let mut el = div()
.flex()
.items_center()
.h_full()
.px(px(6.0))
.text_size(px(LABEL_SIZE))
.text_color(ink.dim)
.truncate_text()
.child(label.into());
match width {
Some(width) => el = el.w(px(width)).flex_none(),
None => el = el.flex_1().min_w(px(FLEX_COLUMN_MIN)),
}
el
}
pub(crate) fn cell(text: impl Into<SharedString>, width: Option<f32>, color: Hsla) -> Div {
let mut el = div()
.flex()
.items_center()
.h_full()
.px(px(6.0))
.truncate_text()
.text_color(color)
.child(text.into());
match width {
Some(width) => el = el.w(px(width)).flex_none(),
None => el = el.flex_1().min_w(px(FLEX_COLUMN_MIN)),
}
el
}
pub(crate) fn section_header(label: impl Into<SharedString>, ink: &Ink) -> Div {
div()
.flex()
.flex_none()
.items_center()
.h(px(22.0))
.px(px(8.0))
.w_full()
.bg(ink.chrome)
.border_b_1()
.border_color(ink.border)
.text_size(px(LABEL_SIZE))
.text_color(ink.dim)
.child(label.into())
}
pub(crate) fn kv_row(
key: impl Into<SharedString>,
value: impl Into<SharedString>,
ink: &Ink,
) -> Div {
div()
.flex()
.items_start()
.gap(px(6.0))
.px(px(8.0))
.py(px(2.0))
.w_full()
.font_family(MONO_FAMILY)
.text_size(px(MONO_SIZE))
.child(
div()
.flex_none()
.w(px(104.0))
.text_color(ink.dim)
.child(key.into()),
)
.child(div().flex_1().text_color(ink.text).child(value.into()))
}
pub(crate) fn empty_state(message: impl Into<SharedString>, ink: &Ink) -> Div {
div()
.flex()
.flex_1()
.items_center()
.justify_center()
.w_full()
.bg(ink.content)
.text_size(px(13.0))
.text_color(ink.dim)
.child(message.into())
}
pub(crate) fn disclosure(expanded: Option<bool>, ink: &Ink, cx: &App) -> Div {
match expanded {
Some(expanded) => glyph(
if expanded {
IconName::ChevronDown
} else {
IconName::ChevronRight
},
11.0,
ink.dim,
cx,
),
None => div().flex_none().w(px(11.0)).h(px(11.0)),
}
}
pub(crate) fn elide(text: &str, max: usize) -> String {
if text.chars().count() <= max || max < 4 {
return text.to_string();
}
let head = (max - 1) / 2;
let tail = max - 1 - head;
let chars: Vec<char> = text.chars().collect();
let start: String = chars[..head].iter().collect();
let end: String = chars[chars.len() - tail..].iter().collect();
format!("{start}…{end}")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn elide_keeps_both_ends() {
assert_eq!(elide("short", 10), "short");
assert_eq!(elide("abcdefghij", 10), "abcdefghij");
assert_eq!(elide("abcdefghijk", 7), "abc…ijk");
assert_eq!(elide("abcdefghijk", 6), "ab…ijk".to_string());
}
#[test]
fn elide_counts_characters_not_bytes() {
assert_eq!(elide("ααααααααααα", 7).chars().count(), 7);
}
}