gpui-ai 0.8.0

AI-native UI components for GPUI applications, built on gpui-component.
Documentation
use gpui::{
    Element as _, IntoElement as _, Render, RenderOnce as _, Role, TestAppContext, Window,
    accesskit, canvas,
};
use gpui_ai::{image_generation::ImageGeneration, loading::LoadingState};
use std::sync::{Arc, Mutex};

struct CapturedNode {
    has_id: bool,
    role: Option<Role>,
    node: accesskit::Node,
}

#[derive(Clone, Copy)]
enum ProgressProbeKind {
    Loading,
    Image,
}

struct ProgressProbe {
    kind: ProgressProbeKind,
    captured: Arc<Mutex<Option<CapturedNode>>>,
}

impl Render for ProgressProbe {
    fn render(&mut self, _: &mut Window, _: &mut gpui::Context<Self>) -> impl gpui::IntoElement {
        let kind = self.kind;
        let captured = self.captured.clone();
        canvas(
            move |_, window, cx| {
                let mut node = accesskit::Node::new(Role::ProgressIndicator);
                let (has_id, role) = match kind {
                    ProgressProbeKind::Loading => {
                        let element = LoadingState::new()
                            .label("Reasoning about suppliers")
                            .render(window, cx)
                            .into_element();
                        let has_id = element.id().is_some();
                        let role = element.a11y_role();
                        element.write_a11y_info(&mut node);
                        (has_id, role)
                    }
                    ProgressProbeKind::Image => {
                        let element = ImageGeneration::new("image")
                            .label("Alpine meadow")
                            .progress(0.42)
                            .render(window, cx)
                            .into_element();
                        let has_id = element.id().is_some();
                        let role = element.a11y_role();
                        element.write_a11y_info(&mut node);
                        (has_id, role)
                    }
                };
                *captured.lock().expect("capture mutex should be available") =
                    Some(CapturedNode { has_id, role, node });
            },
            |_, _, _, _| {},
        )
    }
}

fn capture(kind: ProgressProbeKind, cx: &mut TestAppContext) -> CapturedNode {
    cx.update(gpui_ai::init);
    let captured = Arc::new(Mutex::new(None));
    let result = captured.clone();
    let (_, cx) = cx.add_window_view(move |_, _| ProgressProbe { kind, captured });
    cx.update(|window, cx| window.draw(cx).clear(cx));
    result
        .lock()
        .expect("capture mutex should be available")
        .take()
        .expect("progress node should be captured")
}

#[gpui::test]
fn loading_state_is_a_named_indeterminate_progress_indicator(cx: &mut TestAppContext) {
    let captured = capture(ProgressProbeKind::Loading, cx);

    assert!(captured.has_id);
    assert_eq!(captured.role, Some(Role::ProgressIndicator));
    assert_eq!(captured.node.label(), Some("Reasoning about suppliers"));
    assert_eq!(captured.node.numeric_value(), None);
}

#[gpui::test]
fn image_generation_exposes_its_percentage(cx: &mut TestAppContext) {
    let captured = capture(ProgressProbeKind::Image, cx);

    assert!(captured.has_id);
    assert_eq!(captured.role, Some(Role::ProgressIndicator));
    assert_eq!(captured.node.label(), Some("Alpine meadow"));
    assert_eq!(captured.node.numeric_value(), Some(42.0));
    assert_eq!(captured.node.min_numeric_value(), Some(0.0));
    assert_eq!(captured.node.max_numeric_value(), Some(100.0));
}