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-kitupstreamcomponentotp-input.md

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

OtpInput

A specialized input component for one-time passwords (OTP) that displays multiple input fields in a grid layout. Perfect for SMS verification codes, authenticator app codes, and other numeric verification scenarios.

Import

use gpui_kit::component::input::{OtpInput, OtpState};

Usage

Basic OTP Input

let otp_state = cx.new(|cx| OtpState::new(6, window, cx));

OtpInput::new(&otp_state)

With Default Value

let otp_state = cx.new(|cx|
    OtpState::new(6, window, cx)
        .default_value("123456")
);

OtpInput::new(&otp_state)

Masked OTP Input

let otp_state = cx.new(|cx|
    OtpState::new(6, window, cx)
        .masked(true)
        .default_value("123456")
);

OtpInput::new(&otp_state)

Different Sizes

// Small size
OtpInput::new(&otp_state).small()

// Medium size (default)
OtpInput::new(&otp_state)

// Large size
OtpInput::new(&otp_state).large()

// Custom size
OtpInput::new(&otp_state).with_size(px(55.))

Grouped Layout

// Single group (all fields together)
OtpInput::new(&otp_state).groups(1)

// Two groups (default) - splits fields in half
OtpInput::new(&otp_state).groups(2)

// Three groups - splits fields into thirds
OtpInput::new(&otp_state).groups(3)

Disabled State

OtpInput::new(&otp_state).disabled(true)

Different Length Codes

// 4-digit PIN
let pin_state = cx.new(|cx| OtpState::new(4, window, cx));
OtpInput::new(&pin_state).groups(1)

// 6-digit SMS code (most common)
let sms_state = cx.new(|cx| OtpState::new(6, window, cx));
OtpInput::new(&sms_state)

// 8-digit authenticator code
let auth_state = cx.new(|cx| OtpState::new(8, window, cx));
OtpInput::new(&auth_state).groups(2)

Handle OTP Events

let otp_state = cx.new(|cx| OtpState::new(6, window, cx));

cx.subscribe(&otp_state, |this, state, event: &InputEvent, cx| {
    match event {
        InputEvent::Change => {
            let code = state.read(cx).value();
            if code.len() == 6 {
                println!("Complete OTP: {}", code);
                // Automatically submit when complete
                this.verify_otp(&code, cx);
            }
        }
        InputEvent::Focus => println!("OTP input focused"),
        InputEvent::Blur => println!("OTP input lost focus"),
        _ => {}
    }
});

Programmatic Control

// Set value programmatically
otp_state.update(cx, |state, cx| {
    state.set_value("123456", window, cx);
});

// Toggle masking
otp_state.update(cx, |state, cx| {
    state.set_masked(true, window, cx);
});

// Focus the input
otp_state.update(cx, |state, cx| {
    state.focus(window, cx);
});

// Get current value
let current_value = otp_state.read(cx).value();

API Reference

OtpState

Method Description
new(length, window, cx) Create a new OTP state with specified length
default_value(str) Set initial value
masked(bool) Enable masked display (shows asterisks)
set_value(str, window, cx) Set OTP value programmatically
value() Get current OTP value
set_masked(bool, window, cx) Toggle masked display
focus(window, cx) Focus the OTP input
focus_handle(cx) Get focus handle

OtpInput

Method Description
new(state) Create OTP input with state entity
groups(n) Set number of visual groups (default: 2)
disabled(bool) Set disabled state
small() Small size (6x6 px fields)
large() Large size (11x11 px fields)
with_size(px) Custom field size

InputEvent

Event Description
Change Emitted when OTP is complete (all digits entered)
Focus Input received focus
Blur Input lost focus

Examples

SMS Verification

struct SmsVerification {
    otp_state: Entity<OtpState>,
    phone_number: String,
    is_verifying: bool,
}

impl SmsVerification {
    fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
        let otp_state = cx.new(|cx| OtpState::new(6, window, cx));

        cx.subscribe(&otp_state, |this, state, event: &InputEvent, cx| {
            if let InputEvent::Change = event {
                let code = state.read(cx).value();
                this.verify_sms_code(&code, cx);
            }
        });

        Self {
            otp_state,
            phone_number: "+1234567890".to_string(),
            is_verifying: false,
        }
    }

    fn verify_sms_code(&mut self, code: &str, cx: &mut Context<Self>) {
        self.is_verifying = true;
        // API call to verify SMS code
        println!("Verifying SMS code: {}", code);
        cx.notify();
    }
}

impl Render for SmsVerification {
    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        v_flex()
            .gap_4()
            .child(format!("Enter the 6-digit code sent to {}", self.phone_number))
            .child(OtpInput::new(&self.otp_state))
            .when(self.is_verifying, |this| {
                this.child("Verifying...")
            })
    }
}

Two-Factor Authentication

struct TwoFactorAuth {
    otp_state: Entity<OtpState>,
    is_masked: bool,
}

impl TwoFactorAuth {
    fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
        let otp_state = cx.new(|cx|
            OtpState::new(6, window, cx)
                .masked(true)
        );

        Self {
            otp_state,
            is_masked: true,
        }
    }

    fn toggle_visibility(&mut self, window: &mut Window, cx: &mut Context<Self>) {
        self.is_masked = !self.is_masked;
        self.otp_state.update(cx, |state, cx| {
            state.set_masked(self.is_masked, window, cx);
        });
    }
}

impl Render for TwoFactorAuth {
    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        v_flex()
            .gap_4()
            .child("Enter your authenticator code")
            .child(OtpInput::new(&self.otp_state))
            .child(
                Button::new("toggle-visibility")
                    .label(if self.is_masked { "Show" } else { "Hide" })
                    .on_click(cx.listener(Self::toggle_visibility))
            )
    }
}

PIN Entry

struct PinEntry {
    pin_state: Entity<OtpState>,
    attempts: usize,
    max_attempts: usize,
}

impl PinEntry {
    fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
        let pin_state = cx.new(|cx|
            OtpState::new(4, window, cx)
                .masked(true)
        );

        cx.subscribe(&pin_state, |this, state, event: &InputEvent, cx| {
            if let InputEvent::Change = event {
                let pin = state.read(cx).value();
                this.verify_pin(&pin, cx);
            }
        });

        Self {
            pin_state,
            attempts: 0,
            max_attempts: 3,
        }
    }

    fn verify_pin(&mut self, pin: &str, cx: &mut Context<Self>) {
        self.attempts += 1;

        // Simulate PIN verification
        if pin == "1234" {
            println!("PIN verified successfully!");
        } else {
            println!("Incorrect PIN. Attempts: {}/{}", self.attempts, self.max_attempts);

            // Clear PIN on incorrect attempt
            self.pin_state.update(cx, |state, cx| {
                state.set_value("", window, cx);
            });
        }

        cx.notify();
    }
}

impl Render for PinEntry {
    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        let is_locked = self.attempts >= self.max_attempts;

        v_flex()
            .gap_4()
            .child("Enter your 4-digit PIN")
            .child(
                OtpInput::new(&self.pin_state)
                    .groups(1)
                    .disabled(is_locked)
            )
            .when(is_locked, |this| {
                this.child("Too many attempts. Please try again later.")
            })
            .when(self.attempts > 0 && !is_locked, |this| {
                this.child(format!(
                    "Incorrect PIN. {} attempts remaining.",
                    self.max_attempts - self.attempts
                ))
            })
    }
}

Behavior

Input Handling

  • Numeric Only: Accepts only digits (0-9)
  • Auto-Focus: Automatically moves to next field when digit is entered
  • Backspace: Removes current digit and moves to previous field
  • Length Limit: Prevents input beyond specified length
  • Auto-Complete: Emits Change event when all fields are filled

Visual Feedback

  • Focus Indicator: Blue border and blinking cursor on active field
  • Masking: Shows asterisk icons instead of numbers when enabled
  • Grouping: Visual separation of fields into groups for better readability
  • Disabled State: Grayed out appearance when disabled

Keyboard Navigation

  • Arrow Keys: Navigate between fields
  • Tab: Move to next focusable element
  • Shift+Tab: Move to previous focusable element
  • Backspace: Delete current digit and move backward
  • Delete: Clear current field

Common Patterns

Auto-Submit on Complete

cx.subscribe(&otp_state, |this, state, event: &InputEvent, cx| {
    if let InputEvent::Change = event {
        let code = state.read(cx).value();
        if code.len() == 6 {
            // Auto-submit when complete
            this.submit_verification_code(&code, cx);
        }
    }
});

Clear on Focus

cx.subscribe(&otp_state, |this, state, event: &InputEvent, cx| {
    if let InputEvent::Focus = event {
        // Clear previous value when user starts entering new code
        state.update(cx, |state, cx| {
            state.set_value("", window, cx);
        });
    }
});

Resend Code Timer

struct OtpWithResend {
    otp_state: Entity<OtpState>,
    resend_timer: Option<Timer>,
    can_resend: bool,
}

// Implementation would include timer logic for resend functionality

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/otp-input) 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