All skills
lassejlv avatar

/build-gpui-apps

@52f4cfd
by Lasselassejlv/skills50 stars
1

Build, scaffold, refactor, debug, review, and validate native Rust desktop applications with GPUI. Recommend GPUI Kit and ask before adopting it; use gpui_kit imports after agreement, or preserve the chosen upstream GPUI stack. Includes the merged GPUI Kit component and design skills, full application/Base/Component/Shell documentation, coding and design guides, state, actions, async, input, accessibility, motion, themes, native integration, packaging, auto updates, testing, and production delivery. Use paper-to-gpui when the primary task is faithfully translating a selected Paper.design frame into an existing view.

Use this Skill: https://skilld.dev/gh/lassejlv/skills/build-gpui-apps

This session only. Nothing lands on disk.

referencesgpui-kitupstreamcomponentinput-group.md

≈3.9k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Input Group

Use InputGroup to place text, icons, buttons, or toolbars around an input or textarea inside one frame. For a simple prefix or suffix, use Input.

The examples below define views for an initialized GPUI Kit application. See Getting Started for application setup.

Input with a clear button

Create an InputState once in your view and pass it to InputGroupInput. Subscribe to InputEvent::Change to refresh anything that depends on the text. Keep the returned Subscription in the view so the callback stays active.

This view shows a character count and lets the user clear the input:

use gpui_kit::{
    AppContext as _, ClickEvent, Context, Entity, IntoElement, ParentElement as _,
    Render, Styled as _, Subscription, Window, rems,
};
use gpui_kit::assets::IconName;
use gpui_kit::component::{
    Disableable as _, Icon,
    input::{
        InputEvent, InputGroup, InputGroupAddon, InputGroupAddonAlignment,
        InputGroupButton, InputGroupInput, InputGroupText, InputState,
    },
};

struct SearchField {
    query: Entity<InputState>,
    _change: Subscription,
}

impl SearchField {
    fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
        let query = cx.new(|cx| InputState::new(window, cx).placeholder("Search…"));
        let change = cx.subscribe(&query, |_, _, event: &InputEvent, cx| {
            if matches!(event, InputEvent::Change) {
                cx.notify();
            }
        });
        Self { query, _change: change }
    }

    fn clear(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
        self.query.update(cx, |state, cx| {
            state.set_value("", window, cx);
            state.focus(window, cx);
        });
        cx.notify();
    }
}

impl Render for SearchField {
    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        let count = self.query.read(cx).value().chars().count();
        InputGroup::new("search")
            .max_w(rems(24.))
            .input(InputGroupInput::new(&self.query).aria_label("Search"))
            .addon(InputGroupAddon::new("search-icon")
                .child(Icon::new(IconName::Search).size_4()))
            .addon(InputGroupAddon::new("search-actions")
                .align(InputGroupAddonAlignment::InlineEnd)
                .child(InputGroupText::new().child(format!("{count} characters")))
                .child(InputGroupButton::new("clear").label("Clear")
                    .disabled(count == 0)
                    .on_click(cx.listener(Self::clear))))
    }
}

Read or set the value through the same state:

let value = self.query.read(cx).value();

self.query.update(cx, |state, cx| {
    state.set_value("gpui", window, cx);
});
cx.notify();

InputEvent::Change reports user edits. Setting a value with set_value does not emit this event; call cx.notify() when other content in your view must refresh after a programmatic update.

Parts and alignment

Part Use
InputGroup Combine one input with any number of addons
InputGroupInput An Input placed in the group, using InputState
InputGroupTextarea A Textarea placed in the group, using TextareaState
InputGroupAddon Position text, icons, buttons, or custom content
InputGroupButton A Button with compact input-group presentation
InputGroupText Display helper text, a prefix, suffix, or counter

InputGroupInput and InputGroupTextarea are the ordinary Input and Textarea types under the names the group uses for them, so every builder those controls have — aria_label, content_type, on_paste, cleanable, mask_toggle, Styled methods — works inside a group. The group removes the control's own border, background, and focus ring and draws them around the whole frame instead.

Pass the input to .input(...) and each addon to .addon(...). Use .child(...) or .children(...) inside an addon. A later .input(...) replaces the earlier input; repeated .addon(...) calls keep all addons.

Set an addon's position with .align(InputGroupAddonAlignment::...):

Alignment Position
InlineStart (default) Before the input
InlineEnd After the input
BlockStart Above the input row
BlockEnd Below the input row

You can combine all four positions. Addons on the same side and children within an addon appear in the order you add them. Give each part a stable, distinct ID. Clicking text, icons, or empty space in an addon focuses the input.

For example, add a protocol prefix and domain suffix to a single-line input:

InputGroup::new("website")
    .input(InputGroupInput::new(&self.query).aria_label("Website"))
    .addon(InputGroupAddon::new("protocol")
        .child(InputGroupText::new().child("https://")))
    .addon(InputGroupAddon::new("domain")
        .align(InputGroupAddonAlignment::InlineEnd)
        .child(InputGroupText::new().child(".com")))

Buttons, icons, and menus

Use .label(...) for a text button or .icon(...) for an icon button. Give icon-only buttons an .accessibility_label(...); .tooltip(...) adds a visible hint.

InputGroupButton::new("clear-icon")
    .icon(IconName::X)
    .accessibility_label("Clear search")
    .tooltip("Clear search")
    .on_click(cx.listener(Self::clear))

Buttons size through Sizable like every other control. .xsmall() is the default compact size and .small() the larger one; a button with only an icon is square at either size. .medium() and .large() keep the standard button sizes for a prominent action in a block addon.

Buttons default to ghost styling. Import button::ButtonVariants to use .primary(), .secondary(), or .danger(). Use .outline() for an outline, .disabled(true) to disable an action, and .loading(true) to show progress and prevent repeated clicks. Clicking a button runs its action without moving focus back to the input afterwards.

For an action menu, use .dropdown_menu(...) with the menu API; .dropdown_caret(true) draws the caret after the label. For contextual help, pass an InputGroupButton to Popover's .trigger(...), then add the Popover to an addon.

Textarea with a counter and submit action

Use TextareaState with InputGroupTextarea. .auto_grow(min, max) grows the input between the given row counts; longer content scrolls. Use .rows(n) for a fixed row count or InputGroupTextarea::h(...) for a fixed height.

This complete view counts characters, disables submission for empty or oversized drafts, and displays the submitted text below the composer. Submitting clears and focuses the textarea.

use gpui_kit::{
    AppContext as _, ClickEvent, Context, Entity, IntoElement, ParentElement as _,
    Render, SharedString, Styled as _, Subscription, Window, rems,
};
use gpui_kit::component::{
    Disableable as _, button::ButtonVariants as _, v_flex,
    input::{
        InputEvent, InputGroup, InputGroupAddon, InputGroupAddonAlignment,
        InputGroupButton, InputGroupText, InputGroupTextarea, TextareaState,
    },
};

struct MessageComposer {
    message: Entity<TextareaState>,
    submitted: Option<SharedString>,
    _change: Subscription,
}

impl MessageComposer {
    fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
        let message = cx.new(|cx| {
            TextareaState::new(window, cx)
                .placeholder("Write a message…")
                .auto_grow(2, 6)
        });
        let change = cx.subscribe(&message, |_, _, event: &InputEvent, cx| {
            if matches!(event, InputEvent::Change) {
                cx.notify();
            }
        });
        Self { message, submitted: None, _change: change }
    }

    fn submit(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
        let value = self.message.read(cx).value();
        if value.trim().is_empty() || value.chars().count() > 280 {
            return;
        }
        self.submitted = Some(value);
        self.message.update(cx, |state, cx| {
            state.set_value("", window, cx);
            state.focus(window, cx);
        });
        cx.notify();
    }
}

impl Render for MessageComposer {
    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        let value = self.message.read(cx).value();
        let count = value.chars().count();
        v_flex().max_w(rems(28.)).gap_2()
            .child(InputGroup::new("message")
                .invalid(count > 280)
                .input(InputGroupTextarea::new(&self.message).aria_label("Message"))
                .addon(InputGroupAddon::new("message-footer")
                    .align(InputGroupAddonAlignment::BlockEnd)
                    .child(InputGroupText::new().child(format!("{count}/280")))
                    .child(InputGroupButton::new("submit").ml_auto().primary().label("Submit")
                        .disabled(value.trim().is_empty() || count > 280)
                        .on_click(cx.listener(Self::submit)))))
            .children(self.submitted.as_ref().map(|text| format!("Submitted: {text}")))
    }
}

Use BlockStart for a heading or toolbar above the textarea. Addons stay in place while the text scrolls. See Textarea for more text options.

Disabled, read-only, and validation

Method Effect
.disabled(true) Disables the input and direct InputGroupButton children
.readonly(true) Prevents editing while allowing focus, selection, copying, and addon actions
.invalid(true) Shows an error state while allowing further edits

An input part with .disabled(true) also disables its group. Pass the disabled flag to custom interactive addon content and wrapped controls separately.

Set .invalid(...) from your validation result and show an explanation next to the group. To reject particular edits, use InputState::validate. Give each input an .aria_label(...), even if you also name the group.

Use .content_type(...) on InputGroupInput for hints such as a URL or email address. Password masking is configured with InputState::masked. Both input parts support .context_menu(...) for a custom right-click menu.

On touch devices, long-press the text to select a word, drag the selection handles, and use the edit menu to cut, copy, paste, or select all.

In Rust, both input parts also support .on_paste(...) to handle clipboard images and files before text is inserted. Return true to consume the paste, or false to allow the default text insertion. The handler is not called while the input is disabled or read-only. See Paste Hook for an attachment example and web limitations.

Sizes and custom styles

The default group size is Medium. Import Sizable to use .xsmall(), .small(), .large(), or .with_size(Size::Medium); the size sets the frame height, the text size, and the insets the addons share with the control. Colors, corners, the focus ring, and the invalid ring follow your Theme. Use Styled methods to set the group's width, spacing, and other appearance.

The control keeps its own Styled methods for the text it edits, and each addon, button, and text part styles itself the same way:

use gpui_kit::component::{ActiveTheme as _, Sizable as _, StyledExt as _};

InputGroup::new("styled-search")
    .small()
    .max_w(rems(24.))
    .input(InputGroupInput::new(&self.query)
        .aria_label("Search")
        .px_3()
        .text_base())
    .addon(InputGroupAddon::new("styled-actions")
        .align(InputGroupAddonAlignment::InlineEnd)
        .child(InputGroupButton::new("styled-clear").label("Clear").icon(IconName::X)
            .font_semibold()
            .on_click(cx.listener(Self::clear))))

Placeholder, caret, and selection colors follow the Theme. Import FocusableExt and use .focus_ring(false) to hide the default ring.

JavaScript

Import the same parts from gpui-component. Create text states in View.init. Use .value(...) and .on_change(...) for a controlled input:

import { View } from "gpui-kit";
import {
  InputState, InputGroup, InputGroupInput, InputGroupAddon, InputGroupButton,
} from "gpui-component";

export default class Search extends View {
  init() {
    this.input = InputState("Search…");
    this.query = "";
  }

  render() {
    return new InputGroup("search")
      .input(new InputGroupInput(this.input)
        .aria_label("Search").value(this.query)
        .on_change((value, cx) => { this.query = value; cx.notify(); }))
      .addon(new InputGroupAddon("actions").align("inline-end")
        .child(new InputGroupButton("clear").label("Clear")
          .disabled(this.query.length === 0)
          .on_click((_event, cx) => { this.query = ""; cx.notify(); })));
  }
}

Programmatic .value(...) updates do not call on_change. Setting the same value keeps the selection and undo history. Omit .value(...) to let the input keep its own value, and use on_change(value, cx) when you need to react to edits.

InputGroupTextarea accepts TextareaState and supports .rows(n) and .auto_grow(min, max). Both input parts provide .placeholder(...). InputGroupInput also provides .masked(bool) and .content_type(...), with values such as email_address, url, and new_password.

Set group and button size with .size("small"); available values are xsmall, small, medium, and large. Button icons take an asset path, such as .icon("icons/search.svg"). Style methods apply to each part directly, as in Rust:

new InputGroupInput(this.input).px(12).text_base();

new InputGroupButton("clear").label("Clear").icon("icons/x.svg").font_semibold();

Run gpui-component-shell types <application> to generate editor completion.

Inline references

To combine inline references with attachments or send buttons, pass an Input or Textarea containing tokens to InputGroup. You can customize its labels as usual:

use gpui_kit::component::{
    IconName,
    input::{InputToken, InputGroup, Textarea},
};

InputGroup::new("composer")
    .input(Textarea::new(&state)
        .token(|token, _, _| InputToken::new(token).icon(IconName::File)))

The JavaScript group controls also expose token and on_token_click. Retain the same input state across redraws; call set_value with saved content to restore a draft. See atomic inline tokens.

Documentation license: original prose and illustrations for which GPUI Kit holds licensing rights are also offered under CC BY 4.0. When copying or adapting, credit GPUI Kit, link the source (https://gpui-kit.com/component/input-group) and https://creativecommons.org/licenses/by/4.0/, and indicate changes. Code examples and software source use Apache-2.0; third-party material retains its terms; existing Apache-2.0 permissions remain.

Bundled from GPUI Kit. Documentation prose: CC BY 4.0; code examples: Apache-2.0. Changes: documentation links localized, asset URLs made absolute, and this attribution added.

Source: SKILL.md on GitHub

No alerts3d3 checks · Risk SAFE
  • Gen Agent Trust Hub3d

    The skill provides comprehensive documentation and reference material for building native desktop applications using Rust and GPUI. No security risks, prompt injections, or malicious behaviors were identified. External dependencies originate from reputable organizations in the Rust ecosystem.

  • Socket3d

    No alerts

  • Snyk3d

    Risk: LOW · No issues

Signed by skilld at 52f4cfd. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub yesterday.

Activeupdated 4 days ago

README badge

README badge for lassejlv/skills/build-gpui-apps