use crate::ActiveTheme;
use gpui_kit::{
AnyElement, App, Context, Div, ElementId, Hsla, InteractiveElement, Interactivity, IntoElement,
ParentElement, Pixels, RenderOnce, SharedString, Stateful, StatefulInteractiveElement,
StyleRefinement, Styled, Subscription, Window, canvas, div, px, rems,
};
use std::rc::Rc;
#[cfg(target_os = "macos")]
mod macos;
mod panel;
mod portal;
pub use panel::{
GlassPanel, GlassPanelHandle, GlassPanelOptions, NativeGlassPopup, install_glass_popup,
open_glass_panel,
};
pub use portal::install_glass_popup_presenter;
pub fn glass_available(window: &Window) -> bool {
#[cfg(target_os = "macos")]
return macos::glass_available(window);
#[cfg(not(target_os = "macos"))]
{
let _ = window;
false
}
}
pub fn is_glass_popup(window: &Window) -> bool {
#[cfg(target_os = "macos")]
return macos::is_glass_popup(window);
#[cfg(not(target_os = "macos"))]
{
let _ = window;
false
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct SidebarMetrics {
pub row_height: Pixels,
pub icon_column_width: Pixels,
pub label_size: Pixels,
}
impl Default for SidebarMetrics {
fn default() -> Self {
Self {
row_height: px(32.),
icon_column_width: px(24.),
label_size: px(13.),
}
}
}
pub fn sidebar_metrics() -> SidebarMetrics {
#[cfg(target_os = "macos")]
return macos::sidebar_metrics();
#[cfg(not(target_os = "macos"))]
SidebarMetrics::default()
}
pub fn sidebar_inactive_selection(cx: &App) -> Hsla {
let theme = cx.macos();
let dark = theme.is_dark();
if !theme.reduce_transparency && !theme.increased_contrast {
return gpui_kit::rgb(0)
.opacity(if dark { 0.18 } else { 6. / 255. })
.into();
}
#[cfg(target_os = "macos")]
if let Some(color) = macos::sidebar_inactive_selection(dark) {
return color;
}
gpui_kit::rgb(if dark { 0x464646 } else { 0xdcdcdc }).into()
}
pub(crate) fn refresh_sidebar_metrics() {
#[cfg(target_os = "macos")]
macos::refresh_sidebar_metrics();
}
#[derive(IntoElement)]
pub struct NativeGlass {
id: ElementId,
base: Stateful<Div>,
clear: bool,
tint: Option<Hsla>,
}
pub fn native_glass(id: impl Into<ElementId>, cx: &App) -> NativeGlass {
let id = id.into();
let t = cx.macos();
NativeGlass {
base: div()
.id(id.clone())
.relative()
.rounded(rems(0.75))
.border_color(t.glass_border())
.font_family(t.font.clone())
.text_size(rems(0.8125))
.text_color(t.foreground),
id,
clear: false,
tint: None,
}
}
impl NativeGlass {
pub fn corner_radius(mut self, radius: Pixels) -> Self {
let value = f32::from(radius);
let radius = px(if value.is_finite() { value.max(0.) } else { 0. });
self.base = self.base.rounded(radius);
self
}
pub fn clear(mut self, clear: bool) -> Self {
self.clear = clear;
self
}
pub fn tint(mut self, tint: Hsla) -> Self {
self.tint = Some(tint);
self
}
}
impl Styled for NativeGlass {
fn style(&mut self) -> &mut StyleRefinement {
self.base.style()
}
}
impl ParentElement for NativeGlass {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
self.base.extend(elements);
}
}
impl InteractiveElement for NativeGlass {
fn interactivity(&mut self) -> &mut Interactivity {
self.base.interactivity()
}
}
impl StatefulInteractiveElement for NativeGlass {}
impl RenderOnce for NativeGlass {
fn render(mut self, window: &mut Window, cx: &mut App) -> impl IntoElement {
if !cx.macos().reduce_transparency && is_glass_popup(window) {
return self.base;
}
#[cfg(target_os = "macos")]
if !cx.macos().reduce_transparency && glass_available(window) {
let state = window.use_keyed_state((self.id, "native-glass-host"), cx, |window, _| {
macos::GlassUnderlay::new(window).ok()
});
if state.read(cx).is_some() {
let radius = self
.base
.style()
.corner_radii
.top_left
.unwrap_or(px(0.).into());
let rem_size = window.rem_size();
let clear = self.clear;
let tint = self.tint;
return self.base.child(
canvas(
|bounds, window, _| bounds.intersect(&window.content_mask().bounds),
move |bounds, visible, window, cx| {
if let Some(host) = state.read(cx) {
host.update(
bounds,
visible,
native_corner_radius(radius, rem_size, bounds.size),
clear,
tint,
window,
);
}
},
)
.absolute()
.inset_0(),
);
}
}
let _ = window;
self.base = self.base.bg(cx.macos().glass_fill());
self.base
}
}
fn native_corner_radius(
radius: gpui_kit::AbsoluteLength,
rem_size: Pixels,
size: gpui_kit::Size<Pixels>,
) -> Pixels {
let radius = f32::from(radius.to_pixels(rem_size));
px(if radius.is_finite() {
radius
.max(0.)
.min(f32::from(size.width).max(0.) / 2.)
.min(f32::from(size.height).max(0.) / 2.)
} else {
0.
})
}
#[derive(Clone, Debug)]
pub enum NativeToolbarItem {
Button {
id: SharedString,
label: SharedString,
symbol: SharedString,
enabled: bool,
},
Search {
id: SharedString,
label: SharedString,
placeholder: SharedString,
width: Pixels,
enabled: bool,
},
FlexibleSpace,
}
impl NativeToolbarItem {
pub fn button(
id: impl Into<SharedString>,
label: impl Into<SharedString>,
sf_symbol: impl Into<SharedString>,
) -> Self {
Self::Button {
id: id.into(),
label: label.into(),
symbol: sf_symbol.into(),
enabled: true,
}
}
pub fn disabled(mut self, disabled: bool) -> Self {
match &mut self {
Self::Button { enabled, .. } | Self::Search { enabled, .. } => *enabled = !disabled,
Self::FlexibleSpace => {}
}
self
}
pub fn search(
id: impl Into<SharedString>,
label: impl Into<SharedString>,
placeholder: impl Into<SharedString>,
width: Pixels,
) -> Self {
Self::Search {
id: id.into(),
label: label.into(),
placeholder: placeholder.into(),
width,
enabled: true,
}
}
}
pub struct NativeToolbarHandle {
#[cfg(target_os = "macos")]
inner: macos::ToolbarHost,
}
impl NativeToolbarHandle {
pub fn set_enabled(&self, id: &str, enabled: bool) -> bool {
#[cfg(target_os = "macos")]
return self.inner.set_enabled(id, enabled);
#[cfg(not(target_os = "macos"))]
{
let _ = (id, enabled);
false
}
}
pub fn search_text(&self, id: &str) -> Option<SharedString> {
#[cfg(target_os = "macos")]
return self.inner.search_text(id);
#[cfg(not(target_os = "macos"))]
{
let _ = id;
None
}
}
pub fn set_title(&self, title: impl Into<SharedString>) {
let title = title.into();
#[cfg(target_os = "macos")]
self.inner.set_title(&title);
#[cfg(not(target_os = "macos"))]
let _ = title;
}
pub fn set_sidebar_width(&self, width: Pixels) {
#[cfg(target_os = "macos")]
self.inner.set_sidebar_width(width);
#[cfg(not(target_os = "macos"))]
let _ = width;
}
}
type Command = Rc<dyn Fn(SharedString, &mut Window, &mut App)>;
pub fn install_native_toolbar(
id: impl Into<SharedString>,
title: impl Into<SharedString>,
items: Vec<NativeToolbarItem>,
window: &mut Window,
cx: &mut App,
on_command: impl Fn(SharedString, &mut Window, &mut App) + 'static,
) -> anyhow::Result<NativeToolbarHandle> {
let id = id.into();
let title = title.into();
let command: Command = Rc::new(on_command);
#[cfg(target_os = "macos")]
return macos::install_toolbar(id, title, items, window, cx, command)
.map(|inner| NativeToolbarHandle { inner });
#[cfg(not(target_os = "macos"))]
{
let _ = (id, title, items, window, cx, command);
anyhow::bail!("Native AppKit toolbars require macOS")
}
}
pub fn native_toolbar_inset(window: &Window) -> Pixels {
#[cfg(target_os = "macos")]
return macos::toolbar_inset(window);
#[cfg(not(target_os = "macos"))]
{
let _ = window;
Pixels::ZERO
}
}
pub fn observe_activation<V: 'static>(
window: &mut Window,
cx: &mut Context<V>,
mut callback: impl FnMut(&mut V, bool, &mut Window, &mut Context<V>) + 'static,
) -> Subscription {
cx.observe_window_activation(window, move |view, window, cx| {
callback(view, window.is_window_active(), window, cx);
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{ButtonVariant, Theme, button};
use gpui_kit::{Render, TestAppContext, size};
use std::cell::Cell;
struct FallbackHarness {
clicks: usize,
}
#[test]
fn native_glass_corner_geometry_scales_and_matches_capsule_bounds() {
let bounds = size(px(200.), px(40.));
assert_eq!(
native_corner_radius(rems(0.75).into(), px(16.), bounds),
px(12.)
);
assert_eq!(
native_corner_radius(rems(0.75).into(), px(24.), bounds),
px(18.)
);
assert_eq!(
native_corner_radius(rems(999.).into(), px(16.), bounds),
px(20.)
);
assert_eq!(
native_corner_radius(px(f32::NAN).into(), px(16.), bounds),
Pixels::ZERO
);
}
impl Render for FallbackHarness {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
native_glass("fallback-surface", cx)
.w(px(240.))
.h(px(96.))
.p(px(12.))
.corner_radius(px(8.))
.child(
button("native-content", "Continue", ButtonVariant::Secondary, cx)
.debug_selector(|| "native-content".into())
.on_click(cx.listener(|this, _, _, cx| {
this.clicks += 1;
cx.notify();
})),
)
}
}
#[gpui_kit::test]
fn headless_glass_paints_accessible_fallback_and_preserves_child_interaction(
cx: &mut TestAppContext,
) {
cx.update(crate::init);
let (view, cx) = cx.add_window_view(|_, _| FallbackHarness { clicks: 0 });
for dark in [false, true] {
for reduce_transparency in [false, true] {
cx.update(|window, cx| {
let theme = if dark { Theme::dark() } else { Theme::light() };
theme
.with_reduce_transparency(reduce_transparency)
.apply(cx);
assert!(!glass_available(window));
assert_eq!(native_toolbar_inset(window), Pixels::ZERO);
window.draw(cx).clear(cx);
let fill = cx.macos().glass_fill();
let surface = window
.painted_quads()
.into_iter()
.find(|quad| {
quad.bounds.size == size(px(240.), px(96.)).scale(window.scale_factor())
})
.expect("Fallback must paint the laid-out material surface");
assert_eq!(surface.background.as_solid(), Some(fill));
if reduce_transparency {
assert_eq!(fill.a, 1., "Reduced transparency must be fully opaque");
}
});
}
}
let command = cx.debug_bounds("native-content").unwrap().center();
cx.simulate_click(command, Default::default());
cx.update(|window, cx| {
window.draw(cx).clear(cx);
assert_eq!(view.read(cx).clicks, 1);
});
}
#[gpui_kit::test]
fn headless_toolbar_install_is_rejected_without_emitting_commands(cx: &mut TestAppContext) {
cx.update(crate::init);
let (_, cx) = cx.add_window_view(|_, _| FallbackHarness { clicks: 0 });
let calls = Rc::new(Cell::new(0));
let observed = calls.clone();
cx.update(|window, cx| {
let result = install_native_toolbar(
"headless-toolbar",
"Test",
vec![
NativeToolbarItem::button("add", "Add", "plus"),
NativeToolbarItem::search("search", "Search", "Search", px(200.)),
],
window,
cx,
move |_, _, _| observed.set(observed.get() + 1),
);
assert!(
result.is_err(),
"Headless windows have no AppKit toolbar host"
);
assert_eq!(native_toolbar_inset(window), Pixels::ZERO);
window.draw(cx).clear(cx);
});
assert_eq!(calls.get(), 0);
}
}