use crate::{ActiveTheme, Button, ButtonVariant, IconName, button, icon};
use gpui_kit::{
App, Div, ElementId, FontWeight, IntoElement, RenderOnce, SharedString, Stateful, Window,
WindowControlArea, div, prelude::*, px, rems,
};
use std::time::Duration;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Material {
Sidebar,
#[default]
Toolbar,
Popover,
Hud,
}
pub fn material(kind: Material, cx: &App) -> Div {
let t = cx.macos();
div()
.bg(match kind {
Material::Sidebar | Material::Hud => {
t.inset
.opacity(if t.reduce_transparency { 1. } else { 0.94 })
}
Material::Toolbar | Material::Popover => t.glass_fill(),
})
.border_color(t.glass_border())
.font_family(t.font.clone())
.text_size(rems(0.8125))
.text_color(t.foreground)
}
pub fn native_material(
id: impl Into<ElementId>,
kind: Material,
cx: &App,
) -> crate::native::NativeGlass {
crate::native::native_glass(id, cx)
.corner_radius(if kind == Material::Sidebar {
px(0.)
} else {
px(12.)
})
.clear(kind == Material::Hud)
}
pub fn toolbar(id: impl Into<ElementId>, cx: &App) -> Stateful<Div> {
let t = cx.macos();
div()
.id(id)
.flex()
.items_center()
.w_full()
.min_h(rems(3.25))
.gap(rems(0.625))
.px(rems(0.875))
.py(rems(0.25))
.font_family(t.font.clone())
.text_size(rems(0.8125))
.text_color(t.foreground)
.flex_shrink_0()
}
pub fn toolbar_group(cx: &App) -> Div {
material(Material::Toolbar, cx)
.flex()
.items_center()
.gap(rems(0.125))
.p(rems(0.125))
.rounded_full()
.border_1()
.shadow(cx.macos().control_shadow())
}
pub fn native_toolbar_group(id: impl Into<ElementId>, cx: &App) -> crate::native::NativeGlass {
native_material(id, Material::Toolbar, cx)
.corner_radius(rems(1.125).to_pixels(px(16.)))
.flex()
.items_center()
.gap(rems(0.125))
.p(rems(0.125))
.border_1()
.shadow(cx.macos().control_shadow())
}
pub fn toolbar_button(
id: impl Into<ElementId>,
name: IconName,
label: impl Into<SharedString>,
cx: &App,
) -> Button {
let t = cx.macos();
button(id, "", ButtonVariant::Secondary, cx)
.accessibility_label(label)
.fill_duration(Duration::ZERO)
.size(rems(2.))
.p(rems(0.))
.rounded_full()
.border_0()
.shadow_none()
.bg(t.foreground.opacity(0.))
.hover(|s| s.bg(t.hover_fill()))
.active(|s| s.bg(t.pressed_fill()))
.focus_visible(|s| s.bg(t.hover_fill()).shadow(t.focus_shadow()))
.child(icon(name).size(rems(1.)))
}
pub fn sidebar(id: impl Into<ElementId>, cx: &App) -> Stateful<Div> {
let id = id.into();
div()
.id(id.clone())
.relative()
.font_family(cx.macos().font.clone())
.text_size(rems(0.8125))
.text_color(cx.macos().foreground)
.flex()
.flex_col()
.w(rems(13.75))
.h_full()
.flex_shrink_0()
.p(rems(0.5))
.child(
crate::native::native_glass((id, "sidebar-material"), cx)
.absolute()
.inset_0()
.corner_radius(px(0.)),
)
}
#[derive(IntoElement)]
struct SidebarSymbol {
name: IconName,
selected: bool,
}
impl RenderOnce for SidebarSymbol {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let t = cx.macos();
let accent =
if !window.is_window_active() && !t.reduce_transparency && !t.increased_contrast {
t.accent.blend(if t.is_dark() {
gpui_kit::Hsla::black().opacity(0.4)
} else {
gpui_kit::Hsla::white().opacity(0.4)
})
} else {
t.accent
};
icon(self.name)
.size(rems(
crate::native::sidebar_metrics().icon_column_width.as_f32() / 16.,
))
.font_weight(FontWeight::NORMAL)
.text_color(if self.selected && window.is_window_active() {
t.on_accent
} else {
accent
})
}
}
pub fn sidebar_item(
id: impl Into<ElementId>,
label: impl Into<SharedString>,
name: IconName,
selected: bool,
cx: &App,
) -> Button {
let t = cx.macos();
let metrics = crate::native::sidebar_metrics();
let label = label.into();
button(id, "", ButtonVariant::Secondary, cx)
.accessibility_label(label.clone())
.selected(selected)
.aria_selected(selected)
.selected_fill(t.accent)
.inactive_style(
if selected {
crate::native::sidebar_inactive_selection(cx)
} else {
t.foreground.opacity(0.)
},
if selected {
if t.is_dark() && !t.increased_contrast {
t.accent.blend(gpui_kit::Hsla::white().opacity(0.2))
} else {
t.accent
}
} else {
t.foreground
},
)
.fill_duration(Duration::ZERO)
.bg(if selected {
t.accent
} else {
t.foreground.opacity(0.)
})
.w_full()
.h(rems(metrics.row_height.as_f32() / 16.))
.text_size(rems(metrics.label_size.as_f32() / 16.))
.px(rems(0.5))
.py(rems(0.))
.rounded(rems(0.5))
.border_0()
.border_color(t.foreground.opacity(0.))
.shadow_none()
.justify_start()
.gap(rems(0.25))
.text_color(if selected { t.on_accent } else { t.foreground })
.font_weight(if selected {
FontWeight::SEMIBOLD
} else {
FontWeight::NORMAL
})
.hover(|s| {
s.bg(if selected { t.accent } else { t.hover_fill() })
.border_color(t.foreground.opacity(0.))
})
.active(|s| s.bg(if selected { t.accent } else { t.pressed_fill() }))
.focus_visible(|s| {
s.bg(if selected { t.accent } else { t.hover_fill() })
.border_color(t.accent)
.shadow(t.focus_shadow())
})
.child(SidebarSymbol { name, selected })
.child(div().min_w_0().truncate().child(label))
}
pub fn sidebar_section(title: impl Into<SharedString>, cx: &App) -> Div {
div().flex().flex_col().pt(rems(0.8125)).child(
div()
.flex()
.items_center()
.h(rems(1.1875))
.px(rems(0.375))
.text_size(rems(0.6875))
.font_weight(FontWeight::SEMIBOLD)
.text_color(cx.macos().secondary)
.child(title.into()),
)
}
pub fn title_bar(title: impl Into<SharedString>, cx: &App) -> Div {
let t = cx.macos();
div()
.flex()
.items_center()
.w_full()
.h(rems(3.25))
.pl(rems(5.))
.pr(rems(0.875))
.gap(rems(0.625))
.flex_shrink_0()
.font_family(t.font.clone())
.text_size(rems(0.8125))
.text_color(t.foreground)
.font_weight(FontWeight::SEMIBOLD)
.window_control_area(WindowControlArea::Drag)
.child(div().min_w_0().truncate().child(title.into()))
}
pub fn status_bar(cx: &App) -> Div {
let t = cx.macos();
div()
.flex()
.items_center()
.justify_between()
.w_full()
.min_h(rems(1.75))
.px(rems(0.875))
.gap(rems(0.625))
.border_t_1()
.border_color(t.divider())
.flex_shrink_0()
.bg(t.background)
.font_family(t.font.clone())
.text_size(rems(0.6875))
.text_color(t.secondary)
}
pub fn inspector(title: impl Into<SharedString>, cx: &App) -> Div {
material(Material::Sidebar, cx)
.flex()
.flex_col()
.w(rems(17.5))
.h_full()
.flex_shrink_0()
.gap(rems(0.75))
.p(rems(0.875))
.border_l_1()
.border_color(cx.macos().divider())
.child(div().font_weight(FontWeight::SEMIBOLD).child(title.into()))
}
pub fn settings_group(title: impl Into<SharedString>, cx: &App) -> Div {
let t = cx.macos();
let title = title.into();
div()
.flex()
.flex_col()
.w_full()
.rounded(rems(0.75))
.overflow_hidden()
.bg(t.normal_fill())
.font_family(t.font.clone())
.text_size(rems(0.8125))
.text_color(t.foreground)
.when(!title.is_empty(), |group| {
group.child(
div()
.px(rems(0.875))
.py(rems(0.75))
.font_weight(FontWeight::SEMIBOLD)
.child(title),
)
})
}
pub fn settings_row(
label: impl Into<SharedString>,
description: impl Into<SharedString>,
cx: &App,
) -> Div {
let description = description.into();
div()
.relative()
.flex()
.items_center()
.justify_between()
.min_h(rems(2.875))
.gap(rems(1.))
.px(rems(0.875))
.py(rems(0.625))
.child(
div()
.flex()
.flex_col()
.flex_1()
.min_w_0()
.gap(rems(0.1875))
.child(label.into())
.when(!description.is_empty(), |labels| {
labels.child(
div()
.text_size(rems(0.6875))
.text_color(cx.macos().secondary)
.child(description),
)
}),
)
.child(
div()
.absolute()
.top_0()
.left(rems(0.875))
.right(rems(0.875))
.h(rems(0.0625))
.bg(cx.macos().divider()),
)
}