gpui-macos 0.1.1

A complete macOS design system for GPUI Kit: native controls, Liquid Glass surfaces, and desktop layouts.
Documentation
extern crate gpui_kit as gpui;

use gpui_kit::{
    AssetSource, Context, IntoElement, Render, TestAppContext, VisualTestContext, Window,
    WindowBackgroundAppearance, div, prelude::*, px, rems,
};
use gpui_macos::{
    ActiveTheme, ButtonVariant, IconName, Theme, button, open_window, set_zoom, title_bar,
    window_options, zoom_percent,
};

struct NativeWindow;

impl Render for NativeWindow {
    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        div()
            .size_full()
            .flex()
            .flex_col()
            .child(title_bar("Native window", cx).debug_selector(|| "native-titlebar".into()))
            .child(
                button("apply", "Apply", ButtonVariant::Primary, cx)
                    .debug_selector(|| "native-apply".into())
                    .self_start()
                    .w(rems(7.5))
                    .h(rems(1.75)),
            )
    }
}

fn native_window(cx: &mut TestAppContext) -> VisualTestContext {
    cx.update(|cx| {
        gpui_macos::init(cx);
        Theme::light().apply(cx);
    });
    let (window, _) = cx.update(|cx| {
        open_window(window_options("Native window", cx), cx, |_, cx| {
            cx.new(|_| NativeWindow)
        })
        .unwrap()
    });
    let mut visual = VisualTestContext::from_window(window, cx);
    visual.run_until_parked();
    redraw(&mut visual);
    visual
}

fn redraw(cx: &mut VisualTestContext) {
    cx.update(|window, cx| {
        window.refresh();
        window.draw(cx).clear(cx);
    });
}

#[gpui_kit::test]
fn root_zoom_survives_redraws_and_appearance_policy_changes(cx: &mut TestAppContext) {
    let mut cx = native_window(cx);
    cx.update(|window, _| {
        assert!(window.root::<gpui_kit::base::Root>().flatten().is_some());
        assert_eq!(window.rem_size(), px(16.));
        assert_eq!(zoom_percent(window), 100);
    });
    let baseline = cx.debug_bounds("native-apply").unwrap();
    assert_eq!(baseline.size.width, px(120.));
    assert_eq!(baseline.size.height, px(28.));
    assert_eq!(
        cx.debug_bounds("native-titlebar").unwrap().size.height,
        px(52.)
    );

    cx.update(|window, cx| set_zoom(125, window, cx));
    for appearance in [Theme::dark(), Theme::light()] {
        cx.update(|_, cx| appearance.apply(cx));
        for _ in 0..3 {
            redraw(&mut cx);
            cx.update(|window, _| {
                assert_eq!(window.rem_size(), px(20.));
                assert_eq!(zoom_percent(window), 125);
            });
            let scaled = cx.debug_bounds("native-apply").unwrap();
            assert_eq!(scaled.size.width, baseline.size.width * 1.25);
            assert_eq!(scaled.size.height, baseline.size.height * 1.25);
            assert_eq!(
                cx.debug_bounds("native-titlebar").unwrap().size.height,
                px(65.)
            );
        }
    }
    cx.update(|_, cx| Theme::follow_system(cx));
    redraw(&mut cx);
    cx.update(|window, cx| {
        assert!(Theme::is_following_system(cx));
        assert_eq!(window.rem_size(), px(20.));
        assert_eq!(zoom_percent(window), 125);
    });
}

#[gpui_kit::test]
fn root_zoom_clamps_to_supported_bounds_and_keeps_them_after_redraw(cx: &mut TestAppContext) {
    let mut cx = native_window(cx);
    for (requested, expected, rem_size) in [
        (0, 50, 8.),
        (49, 50, 8.),
        (201, 200, 32.),
        (65535, 200, 32.),
    ] {
        cx.update(|window, cx| set_zoom(requested, window, cx));
        redraw(&mut cx);
        redraw(&mut cx);
        cx.update(|window, _| {
            assert_eq!(zoom_percent(window), expected);
            assert_eq!(window.rem_size(), px(rem_size));
        });
        assert_eq!(
            cx.debug_bounds("native-apply").unwrap().size.width,
            px(7.5 * rem_size)
        );
    }
}

#[gpui_kit::test]
fn native_window_options_respect_reduce_transparency(cx: &mut TestAppContext) {
    cx.update(|cx| {
        gpui_macos::init(cx);
        for (reduce, expected) in [
            (false, WindowBackgroundAppearance::Blurred),
            (true, WindowBackgroundAppearance::Opaque),
        ] {
            Theme::light().with_reduce_transparency(reduce).apply(cx);
            let options = window_options("Preferences", cx);
            assert_eq!(options.window_background, expected);
            let titlebar = options.titlebar.unwrap();
            assert_eq!(titlebar.title.as_deref(), Some("Preferences"));
            assert!(titlebar.appears_transparent);
            assert_eq!(
                options.window_min_size.unwrap(),
                gpui_kit::size(px(640.), px(480.))
            );
            if reduce {
                assert_eq!(cx.macos().glass_fill().a, 1.);
            } else {
                assert!(cx.macos().glass_fill().a < 1.);
            }
        }
    });
}

#[gpui_kit::test]
fn system_policy_preserves_custom_accent_and_material_tint(cx: &mut TestAppContext) {
    cx.update(|cx| {
        gpui_macos::init(cx);
        assert!(Theme::is_following_system(cx));
        let accent = gpui_kit::Hsla::from(gpui_kit::rgb(0xaf52de));
        Theme::dark()
            .with_accent(accent)
            .with_glass_tint(0.23)
            .apply(cx);
        assert!(!Theme::is_following_system(cx));
        assert!(cx.macos().is_dark());

        for _ in 0..2 {
            Theme::follow_system(cx);
            assert!(Theme::is_following_system(cx));
            assert_eq!(cx.macos().accent, accent);
            assert_eq!(cx.macos().chart[0], accent);
            assert_eq!(cx.macos().glass_tint, 0.23);
            assert_eq!(
                gpui_kit::base::Theme::global(cx).tokens.colors.primary,
                accent
            );
        }
        cx.macos().clone().apply(cx);
        assert!(!Theme::is_following_system(cx));
        assert_eq!(cx.macos().accent, accent);
        assert_eq!(cx.macos().glass_tint, 0.23);
    });
}

#[test]
fn complete_asset_bundle_resolves_the_lock_glyph() {
    let path = IconName::Lock.path();
    let bytes = gpui_kit::assets::AllAssets.load(&path).unwrap().unwrap();
    assert!(!bytes.is_empty());
    assert!(String::from_utf8_lossy(&bytes).contains("<svg"));
}