Skip to main content

Module keyboard

Module keyboard 

Source
Expand description

Actions, key bindings, key contexts, and focus navigation.

Run it with cargo run --example keyboard.

//! Keyboard input: actions, key bindings, key contexts and focus.
//!
//! Actions are types declared with `actions!`. `KeyBinding::new` maps a
//! keystroke to an action inside a key context, and an ancestor element
//! handles the action with `on_action`. Tab and Shift-Tab move focus between
//! `track_focus` handles, and `focus_visible` draws a ring only when focus
//! arrived from the keyboard. The shortcut next to each label comes from the
//! keymap, so it always matches the binding.
//!
//! Run it with `cargo run --example keyboard`.

use gpui::{
    Action, App, Bounds, Context, FocusHandle, KeyBinding, QuitMode, Window, WindowBounds,
    WindowOptions, actions, colors::Colors, div, prelude::*, px, size,
};

const CONTEXT: &str = "KeyboardExample";

actions!(
    keyboard_example,
    [FocusNext, FocusPrevious, Save, Duplicate, ClearLog]
);

struct Keyboard {
    focus: [FocusHandle; 3],
    log: Vec<&'static str>,
}

impl Keyboard {
    fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
        let focus = [0, 1, 2].map(|index| cx.focus_handle().tab_stop(true).tab_index(index));
        window.focus(&focus[0], cx);
        Self {
            focus,
            log: Vec::new(),
        }
    }

    fn record(&mut self, name: &'static str, cx: &mut Context<Self>) {
        self.log.push(name);
        if self.log.len() > 5 {
            self.log.remove(0);
        }
        cx.notify();
    }
}

fn action_row<A: Action + Clone>(
    index: usize,
    label: &'static str,
    action: A,
    focus: &FocusHandle,
    colors: &Colors,
    cx: &App,
) -> impl IntoElement {
    let shortcut = cx
        .key_bindings()
        .borrow()
        .bindings_for_action(&action)
        .last()
        .map(|binding| {
            binding
                .keystrokes()
                .iter()
                .map(ToString::to_string)
                .collect::<Vec<_>>()
                .join(" ")
        })
        .unwrap_or_default();

    div()
        .id(("row", index))
        .track_focus(focus)
        .focus_visible(|style| style.border_color(colors.selected))
        .flex()
        .justify_between()
        .items_center()
        .px_4()
        .py_3()
        .rounded_lg()
        .border_2()
        .border_color(colors.container)
        .bg(colors.container)
        .cursor_pointer()
        .hover(|style| style.bg(colors.separator))
        .on_click(move |_, window, cx| window.dispatch_action(Box::new(action.clone()), cx))
        .child(label)
        .child(
            div()
                .font_family("Lilex")
                .text_sm()
                .text_color(colors.disabled)
                .child(shortcut),
        )
}

impl Render for Keyboard {
    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        let colors = Colors::for_appearance(window);

        div()
            .size_full()
            .flex()
            .flex_col()
            .gap_6()
            .p_6()
            .bg(colors.background)
            .text_color(colors.text)
            .key_context(CONTEXT)
            .on_action(cx.listener(|this, _: &FocusNext, window, cx| {
                window.focus_next(cx);
                this.record("FocusNext", cx);
            }))
            .on_action(cx.listener(|this, _: &FocusPrevious, window, cx| {
                window.focus_prev(cx);
                this.record("FocusPrevious", cx);
            }))
            .on_action(cx.listener(|this, _: &Save, _, cx| this.record("Save", cx)))
            .on_action(cx.listener(|this, _: &Duplicate, _, cx| this.record("Duplicate", cx)))
            .on_action(cx.listener(|this, _: &ClearLog, _, cx| {
                this.log.clear();
                cx.notify();
            }))
            .child(
                div()
                    .flex()
                    .flex_col()
                    .gap_1()
                    .child(
                        div()
                            .text_xl()
                            .font_weight(gpui::FontWeight::SEMIBOLD)
                            .child("Keyboard"),
                    )
                    .child(div().text_sm().text_color(colors.disabled).child(
                        "Tab and Shift-Tab move focus. Enter or Space runs the focused row.",
                    )),
            )
            .child(
                div()
                    .flex()
                    .flex_col()
                    .gap_2()
                    .child(action_row(0, "Save", Save, &self.focus[0], &colors, cx))
                    .child(action_row(
                        1,
                        "Duplicate",
                        Duplicate,
                        &self.focus[1],
                        &colors,
                        cx,
                    ))
                    .child(action_row(
                        2,
                        "Clear log",
                        ClearLog,
                        &self.focus[2],
                        &colors,
                        cx,
                    )),
            )
            .child(
                div()
                    .flex()
                    .flex_col()
                    .gap_1()
                    .child(
                        div()
                            .text_sm()
                            .text_color(colors.disabled)
                            .child("RECENT ACTIONS"),
                    )
                    .child(
                        div()
                            .font_family("Lilex")
                            .text_sm()
                            .flex()
                            .flex_col()
                            .gap_1()
                            .children(self.log.iter().map(|name| div().child(*name)))
                            .when(self.log.is_empty(), |list| {
                                list.text_color(colors.disabled).child("No actions yet")
                            }),
                    ),
            )
    }
}

fn main() {
    gpui_platform::application()
        .with_quit_mode(QuitMode::LastWindowClosed)
        .run(run);
}

/// Opens the example window. The browser player and the example tests call this too.
pub fn run(cx: &mut App) {
    cx.bind_keys([
        KeyBinding::new("tab", FocusNext, Some(CONTEXT)),
        KeyBinding::new("shift-tab", FocusPrevious, Some(CONTEXT)),
        KeyBinding::new("secondary-s", Save, Some(CONTEXT)),
        KeyBinding::new("secondary-d", Duplicate, Some(CONTEXT)),
        KeyBinding::new("secondary-backspace", ClearLog, Some(CONTEXT)),
    ]);

    let bounds = Bounds::centered(None, size(px(520.), px(420.)), cx);
    cx.open_window(
        WindowOptions::default().window_bounds(WindowBounds::Windowed(bounds)),
        |window, cx| cx.new(|cx| Keyboard::new(window, cx)),
    )
    .unwrap();
    cx.activate(true);
}