Skip to content

OtpInput

OtpInput 是为一次性验证码(OTP)设计的输入组件,会以网格方式显示多个输入框,适合短信验证码、验证器 App 动态码以及 PIN 码输入场景。

导入

rust
use gpui_component::input::{OtpInput, OtpState};

用法

基础 OTP 输入

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

OtpInput::new(&otp_state)

默认值

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

OtpInput::new(&otp_state)

掩码输入

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

OtpInput::new(&otp_state)

不同尺寸

rust
OtpInput::new(&otp_state).small()
OtpInput::new(&otp_state)
OtpInput::new(&otp_state).large()
OtpInput::new(&otp_state).with_size(px(55.))

分组布局

rust
OtpInput::new(&otp_state).groups(1)
OtpInput::new(&otp_state).groups(2)
OtpInput::new(&otp_state).groups(3)

禁用状态

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

不同长度的验证码

rust
let pin_state = cx.new(|cx| OtpState::new(4, window, cx));
OtpInput::new(&pin_state).groups(1)

let sms_state = cx.new(|cx| OtpState::new(6, window, cx));
OtpInput::new(&sms_state)

let auth_state = cx.new(|cx| OtpState::new(8, window, cx));
OtpInput::new(&auth_state).groups(2)

处理 OTP 事件

rust
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);
                this.verify_otp(&code, cx);
            }
        }
        InputEvent::Focus => println!("OTP input focused"),
        InputEvent::Blur => println!("OTP input lost focus"),
        _ => {}
    }
});

程序化控制

rust
otp_state.update(cx, |state, cx| {
    state.set_value("123456", window, cx);
});

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

otp_state.update(cx, |state, cx| {
    state.focus(window, cx);
});

let current_value = otp_state.read(cx).value();

API 参考

OtpState

方法说明
new(length, window, cx)创建指定长度的 OTP 状态
default_value(str)设置初始值
masked(bool)开启掩码显示
set_value(str, window, cx)以代码方式设置值
value()获取当前值
set_masked(bool, window, cx)切换掩码状态
focus(window, cx)聚焦输入框
focus_handle(cx)获取焦点句柄

OtpInput

方法说明
new(state)使用状态实体创建 OTP 输入组件
groups(n)设置可视分组数量,默认值为 2
disabled(bool)设置禁用状态
small()小尺寸
large()大尺寸
with_size(px)自定义单格尺寸

InputEvent

事件说明
Change所有数字输入完毕后触发
Focus输入框获得焦点
Blur输入框失去焦点

示例

短信验证码

rust
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;
        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...")
            })
    }
}

双因素认证

rust
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 码输入

rust
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;

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

            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
                ))
            })
    }
}

行为说明

输入处理

  • 仅数字:只接受 0-9
  • 自动聚焦:输入数字后自动跳到下一个输入框。
  • 退格:删除当前数字并回到前一个输入框。
  • 长度限制:不会超过设定长度。
  • 自动完成:所有输入框填满后触发 Change 事件。

视觉反馈

  • 焦点态:当前输入框显示高亮边框与闪烁光标。
  • 掩码:启用后显示星号而不是数字。
  • 分组:可将输入框按组分隔,提升可读性。
  • 禁用态:禁用后显示灰化样式。

键盘导航

  • 方向键:在输入框之间移动。
  • Tab:切换到下一个可聚焦元素。
  • Shift+Tab:切换到上一个可聚焦元素。
  • Backspace:删除当前数字并向前移动。
  • Delete:清空当前输入框。

常见模式

输入完成后自动提交

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

聚焦时清空旧值

rust
cx.subscribe(&otp_state, |this, state, event: &InputEvent, cx| {
    if let InputEvent::Focus = event {
        state.update(cx, |state, cx| {
            state.set_value("", window, cx);
        });
    }
});

重发验证码计时器

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

// Implementation would include timer logic for resend functionality