gpui-kit 0.8.0

GPUI Kit: one dependency for building desktop applications with GPUI, GPUI Base, GPUI Component and default assets.
Documentation
use crate::common;

use gpui_kit::component::{
    IndexPath,
    list::{List, ListDelegate, ListItem, ListState},
};
use gpui_kit::test::{TestEventExt, TestQueryExt, TestSupportExt, TestWindowExt};
use gpui_kit::{
    App, AppContext, Context, Entity, Focusable, Modifiers, MouseButton, TestAppContext, Window,
    div, prelude::*, px, size,
};

const DOCUMENTS: [&str; 3] = ["Roadmap", "Release notes", "Meeting notes"];

#[derive(Default)]
struct Documents {
    selected: Option<IndexPath>,
    confirmed: Vec<(IndexPath, bool)>,
    canceled: usize,
}

impl ListDelegate for Documents {
    type Item = ListItem;

    fn items_count(&self, _: usize, _: &App) -> usize {
        DOCUMENTS.len()
    }

    fn render_item(
        &mut self,
        ix: IndexPath,
        _: &mut Window,
        _: &mut Context<ListState<Self>>,
    ) -> Option<Self::Item> {
        let name = DOCUMENTS[ix.row];
        Some(ListItem::new(name).accessibility_label(name).child(name))
    }

    fn set_selected_index(
        &mut self,
        ix: Option<IndexPath>,
        _: &mut Window,
        _: &mut Context<ListState<Self>>,
    ) {
        self.selected = ix;
    }

    fn confirm(&mut self, secondary: bool, _: &mut Window, _: &mut Context<ListState<Self>>) {
        self.confirmed.push((
            self.selected.expect("selection precedes confirm"),
            secondary,
        ));
    }

    fn cancel(&mut self, _: &mut Window, _: &mut Context<ListState<Self>>) {
        self.canceled += 1;
    }
}

struct DocumentPicker {
    list: Entity<ListState<Documents>>,
}

impl Render for DocumentPicker {
    fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
        div()
            .size_full()
            .child(div().h(px(160.)).child(List::new(&self.list)))
            .child(div().id("outside-list").test_support().h(px(80.)).w_full())
    }
}

fn open_picker(
    cx: &mut TestAppContext,
) -> (
    gpui_kit::WindowHandle<gpui_kit::base::Root>,
    Entity<DocumentPicker>,
) {
    cx.update(gpui_kit::init);
    common::open_window(cx, Some(size(px(320.), px(240.))), |window, cx| {
        cx.new(|cx| {
            let list = cx.new(|cx| ListState::new(Documents::default(), window, cx));
            list.focus_handle(cx).focus(window, cx);
            DocumentPicker { list }
        })
    })
}

#[gpui_kit::test]
fn list_activation_uses_modifiers_at_release_and_not_at_pointer_down(cx: &mut TestAppContext) {
    let (handle, picker) = open_picker(cx);
    cx.update_window(handle.into(), |_, window, cx| {
        let list = picker.read(cx).list.clone();
        window.render_frame(cx);
        let path = window.find_by_label("Release notes").path().to_vec();
        let position = window.find_by_label("Release notes").bounds().center();

        for (held_at_down, held_at_up, secondary) in [
            (Modifiers::default(), Modifiers::secondary_key(), true),
            (Modifiers::secondary_key(), Modifiers::default(), false),
        ] {
            let count = list.read(cx).delegate().confirmed.len();
            window.change_modifiers(held_at_down, cx);
            window.pointer_down(position, MouseButton::Left, cx);
            assert_eq!(list.read(cx).delegate().confirmed.len(), count);
            window.change_modifiers(held_at_up, cx);
            assert_eq!(window.find_by_label("Release notes").path(), path);
            window.pointer_up(position, MouseButton::Left, cx);
            assert_eq!(list.read(cx).selected_index(), Some(IndexPath::new(1)));
            assert_eq!(
                list.read(cx).delegate().confirmed[count],
                (IndexPath::new(1), secondary)
            );
            assert_eq!(list.read(cx).delegate().confirmed.len(), count + 1);
            assert!(list.focus_handle(cx).is_focused(window));
        }
        window.change_modifiers(Modifiers::default(), cx);
    })
    .unwrap();
}

#[gpui_kit::test]
fn list_drag_out_release_does_not_activate_or_replace_selection(cx: &mut TestAppContext) {
    let (handle, picker) = open_picker(cx);
    cx.update_window(handle.into(), |_, window, cx| {
        let list = picker.read(cx).list.clone();
        window.render_frame(cx);
        window.click("Roadmap", cx);
        let position = window.find_by_label("Meeting notes").bounds().center();
        let outside = window.find("outside-list").bounds().center();
        window.change_modifiers(Modifiers::secondary_key(), cx);
        window.pointer_down(position, MouseButton::Left, cx);
        window.pointer_move(outside, Some(MouseButton::Left), cx);
        window.pointer_up(outside, MouseButton::Left, cx);
        assert_eq!(list.read(cx).selected_index(), Some(IndexPath::new(0)));
        assert_eq!(
            list.read(cx).delegate().confirmed,
            [(IndexPath::new(0), false)]
        );
        assert!(list.focus_handle(cx).is_focused(window));

        window.change_modifiers(Modifiers::default(), cx);
        window.click("Meeting notes", cx);
        assert_eq!(list.read(cx).selected_index(), Some(IndexPath::new(2)));
        assert_eq!(
            list.read(cx).delegate().confirmed,
            [(IndexPath::new(0), false), (IndexPath::new(2), false)]
        );
    })
    .unwrap();
}

#[gpui_kit::test]
fn list_repeated_navigation_confirms_current_item_and_cancel_clears_selection(
    cx: &mut TestAppContext,
) {
    let (handle, picker) = open_picker(cx);
    cx.update_window(handle.into(), |_, window, cx| {
        let list = picker.read(cx).list.clone();
        window.key_down("down", false, cx);
        assert_eq!(list.read(cx).selected_index(), Some(IndexPath::new(0)));
        window.key_down("down", true, cx);
        assert_eq!(list.read(cx).selected_index(), Some(IndexPath::new(1)));
        window.key_up("down", cx);
        assert_eq!(list.read(cx).selected_index(), Some(IndexPath::new(1)));
        assert!(list.read(cx).delegate().confirmed.is_empty());
        assert!(window.find_by_label("Release notes").visible());

        window.change_modifiers(Modifiers::secondary_key(), cx);
        window.key_down("enter", false, cx);
        assert_eq!(
            list.read(cx).delegate().confirmed,
            [(IndexPath::new(1), true)]
        );
        window.key_up("enter", cx);
        assert_eq!(list.read(cx).delegate().confirmed.len(), 1);
        window.change_modifiers(Modifiers::default(), cx);
        window.key_down("escape", false, cx);
        assert_eq!(list.read(cx).selected_index(), None);
        assert_eq!(list.read(cx).delegate().selected, None);
        assert_eq!(list.read(cx).delegate().canceled, 1);
        window.key_up("escape", cx);
        window.press("enter", cx);
        assert_eq!(list.read(cx).delegate().confirmed.len(), 1);
        assert!(list.focus_handle(cx).is_focused(window));
    })
    .unwrap();
}