Elementos
Input
Input é um campo de texto nativo: um UITextField (linha única) ou UITextView (múltiplas linhas) no iOS e no Mac Catalyst, e um EditText no Android. Digitação, cursor, seleção, autocorreção e preenchimento automático são os da própria plataforma. Em Rust, cria-se um com nsh_base::input(), e em TSX com <Input> de nsh/base.
import { type InputRef, type JSX, mount, signal, Text, View } from "nsh";
import { Input } from "nsh/base";
import { button, buttonText, field, Inspector, Panel, Stage, ui, wide } from "./palette";
function App() {
const name = signal("");
const password = signal("");
const notes = signal("");
const submitted = signal("");
let passwordInput!: InputRef;
const signIn = () => submitted.set(`Signed in as ${name() || "nobody"}`);
const nameField = () => (
<Input
{...field()}
value={name()}
onChange={({ value }) => name.set(value)}
placeholder="Ada Lovelace"
autoCapitalize="words"
maxLength={20}
returnKeyType="next"
onSubmit={() => passwordInput.focus()}
/>
);
const count = () => <Text style={{ fontSize: 12, color: ui().muted }}>{name().length} / 20</Text>;
const passwordField = () => (
<Input
ref={passwordInput}
{...field()}
value={password()}
onChange={({ value }) => password.set(value)}
masked
autoComplete="current-password"
returnKeyType="done"
onSubmit={signIn}
/>
);
const notesField = () => (
<Input
{...field({ style: { height: wide() ? 80 : 100 } })}
multiline
value={notes()}
onChange={({ value }) => notes.set(value)}
placeholder="Anything else?"
/>
);
const signInButton = () => (
<View onPress={signIn} disabled={password().length === 0} {...button("primary", { style: { marginTop: 4 } })}>
<Text style={buttonText("primary")}>Sign in</Text>
</View>
);
/** A label above its field, and what describes it below. */
const group = (label: string, input: JSX.Element, after?: JSX.Element) => (
<View style={{ gap: 6 }}>
<Text style={{ fontSize: 13, color: ui().muted }}>{label}</Text>
{input}
{after}
</View>
);
return (
<View style={{ flex: 1, backgroundColor: ui().page }}>
{() =>
wide() ? (
<View style={{ flex: 1, padding: 20, paddingTop: 24, gap: 14 }}>
<Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>input</Text>
<Text style={{ color: ui().muted }}>Tab moves between the fields; Return in the name goes to the password</Text>
<View style={{ flex: 1, flexDirection: "row", gap: 16 }}>
<Stage width={340} gap={16}>
{group("Name (controlled, maxLength 20)", nameField(), count())}
{group("Password", passwordField())}
{group("Notes (multiline)", notesField())}
{signInButton()}
</Stage>
<Inspector>
<Panel title="value">
<Text style={{ fontSize: 13, color: ui().text }}>name {name() ? `“${name()}”` : "—"}</Text>
<Text style={{ fontSize: 13, color: ui().text, fontVariant: ["tabular-nums"] }}>password {password().length} characters</Text>
<Text style={{ fontSize: 13, color: ui().text, fontVariant: ["tabular-nums"] }}>notes {notes().length} characters</Text>
</Panel>
<Panel title="onSubmit" grow>
<Text style={{ fontSize: 13, color: ui().up }}>{submitted()}</Text>
</Panel>
</Inspector>
</View>
</View>
) : (
<View style={{ flex: 1, width: "100%", maxWidth: 480, alignSelf: "center", padding: 20, paddingTop: 24, gap: 10 }}>
<Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>input</Text>
<Text style={{ color: ui().muted }}>Name (controlled, maxLength 20)</Text>
{nameField()}
{count()}
<Text style={{ color: ui().muted }}>Password</Text>
{passwordField()}
<Text style={{ color: ui().muted }}>Notes (multiline)</Text>
{notesField()}
{signInButton()}
<Text style={{ color: ui().up }}>{submitted()}</Text>
</View>
)
}
</View>
);
}
mount(App);use nsh_base::input;
use nsh_core::prelude::*;
fn name_field() -> impl IntoElement {
let name = signal(String::new());
input()
.value(name)
.on_change(move |text: &str| name.set(text.to_string()))
.placeholder("Your name")
.auto_capitalize(AutoCapitalize::Words)
.return_key_type(ReturnKeyType::Done)
.text_base()
.p_3()
.border_1()
.border_color(rgb(0xd0d7de))
.rounded(px(8.0))
}import { signal } from "nsh";
import { Input } from "nsh/base";
function NameField() {
const name = signal("");
return (
<Input
value={name()}
onChange={({ value }) => name.set(value)}
placeholder="Your name"
autoCapitalize="words"
returnKeyType="done"
style={{ fontSize: 16, padding: 12, borderWidth: 1, borderColor: "#d0d7de", borderRadius: 8 }}
/>
);
}#Controlado e não controlado
- Controlado: passe
valuee atualize-o emonChange. O app pode reescrever o que o usuário digitou (remover espaços, formatar): o texto mostra a edição imediatamente e é corrigido quando o novovaluechega. Cada edição nativa carrega um contador, e umvaluevindo do app é aplicado quando se baseia na edição mais recente, de modo que a digitação mais nova sempre vence uma atualização lenta. - Não controlado: passe
defaultValue(.default_value(..), ou nada) e leia o texto deonChangeouonSubmit.
Regras que devem valer enquanto o usuário digita são props, tratadas nativamente: maxLength limita o comprimento e submitBehavior decide o que o return faz. Veja formulários e o teclado.
#Props
Além das props comuns (com style tipado como TextStyle: fonte, cor, alinhamento e os estilos de view se aplicam):
#Texto
| Prop | Rust | Tipo | Padrão | Descrição |
|---|---|---|---|---|
value |
.value(..) |
string |
O texto, controlado pelo app. | |
defaultValue |
.default_value(..) |
string |
O texto inicial. | |
placeholder |
.placeholder(..) |
string |
Exibido enquanto o input está vazio. | |
multiline |
.multiline(..) |
boolean |
false |
Várias linhas; o return insere uma quebra de linha, a menos que submitBehavior diga o contrário. |
scrollbar |
.scrollbar(..) |
boolean | ScrollbarTheme |
true |
A barra de rolagem de um input multilinha: o indicador da plataforma, ou a barra integrada do app quando ele definiu uma (Scrollbar); um objeto é um tema próprio, false nenhuma. |
maxLength |
.max_length(..) |
number |
O máximo de caracteres que o usuário pode digitar. | |
readonly |
.readonly(..) |
boolean |
false |
O usuário pode selecionar e copiar o texto, mas não alterá-lo. |
masked |
.masked(..) |
boolean |
false |
O texto fica oculto (senhas). |
allowFontScaling |
boolean |
true |
Segue o tamanho de texto do sistema. | |
maxFontSizeMultiplier |
number |
0 |
O limite superior da escala do sistema (0 significa sem limite). |
#Teclado
| Prop | Rust | Tipo | Padrão | Descrição |
|---|---|---|---|---|
keyboardType |
.keyboard_type(KeyboardType::Email) |
"default" | "number-pad" | "decimal-pad" | "numeric" | "numbers-and-punctuation" | "email-address" | "phone-pad" | "url" |
"default" |
O layout do teclado (os valores; numbers-and-punctuation tem um sinal de menos no iOS). |
returnKeyType |
.return_key_type(ReturnKeyType::Next) |
"default" | "done" | "go" | "next" | "search" | "send" |
"default" |
O rótulo da tecla return. |
autoCapitalize |
.auto_capitalize(AutoCapitalize::None) |
"none" | "sentences" | "words" | "characters" |
"sentences" |
Capitalização automática. |
autoCorrect |
.auto_correct(..) |
boolean |
true |
As correções do sistema. |
autoComplete |
.auto_fill(AutoFill::OneTimeCode) |
AutoComplete | TextContentType |
O que o input contém, para preenchimento automático no iOS, no Android e na web: "username", "current-password", "new-password", "one-time-code", "email", "tel", "postal-code", "cc-number", "off", … |
|
pasteBehavior |
.paste_behavior(PasteBehavior::Report) |
"insert" | "report" |
"insert" |
O que uma colagem faz: "insert" insere o texto colado, "report" não insere nada, para que o app trate o que onPaste informa. Imagens e arquivos nunca são inseridos. |
submitBehavior |
.submit_behavior(SubmitBehavior::Submit) |
"submit" | "blurAndSubmit" | "newline" |
"blurAndSubmit" (linha única), "newline" (multilinha) |
O que o return faz: "submit" reporta onSubmit e mantém o foco, "blurAndSubmit" também remove o foco, "newline" insere uma quebra de linha (multilinha; uma linha única envia). Um input multilinha que envia ainda aceita quebras de linha com Shift+Return ou Option / Alt+Return em teclados físicos. "submit" mantém o teclado aberto (por exemplo, para enviar várias mensagens). |
autoFocus |
.auto_focus(..) |
boolean |
false |
Recebe o foco quando aparece. |
#Cursor e seleção
| Prop | Rust | Tipo | Descrição |
|---|---|---|---|
caret |
.caret(..) |
boolean |
O cursor é desenhado (padrão true; false o oculta). |
selectedRange |
.selected_range((start, end)) |
{ start: number; end?: number } |
A seleção controlada, em unidades UTF-16; end tem start como padrão (um cursor). |
O cursor pisca enquanto sua janela está visível e ativa. Em janelas GPUI no Linux, ele permanece visível após dez segundos sem digitar ou movê-lo, como em apps GTK, de modo que um campo focado e deixado sozinho não consome quadros.
#Cores
As cores do placeholder, do cursor e do destaque da seleção são chaves de estilo, com suas classes Tailwind, e métodos em Rust (o UIKit tinge o cursor e a seleção com uma única cor, a do cursor quando definida):
| Chave de estilo | Rust | Classe | Descrição |
|---|---|---|---|
placeholderColor |
.placeholder_color(..) |
placeholder:text-* |
A cor do placeholder. |
caretColor |
.caret_color(..) |
caret-* |
A cor do cursor. |
selectionColor |
.selection_color(..) |
selection:text-* |
A cor do destaque da seleção. |
input()
.placeholder("Search")
.text_color(rgb(0x18181b))
.placeholder_color(rgb(0x9ca3af))
.caret_color(rgb(0x2563eb))
.selection_color(rgb(0x93c5fd))<Input placeholder="Search" class="text-zinc-900 placeholder:text-zinc-400 selection:text-blue-600" />
<Input placeholder="Search" style={{ placeholderColor: "#9ca3af", caretColor: "#2563eb", selectionColor: "#93c5fd" }} />#iOS
Chave ios |
Rust | Tipo | Descrição |
|---|---|---|---|
textContentType |
.auto_fill(..) |
AutoComplete | TextContentType |
Os nomes do iOS para autoComplete ("oneTimeCode", "newPassword", …). |
inputAccessoryViewID |
.input_accessory(id) |
string |
O nativeID de um InputAccessoryView (InputAccessoryView) exibido sobre o teclado enquanto o input tem foco. |
keyboardAppearance |
.keyboard_appearance(KeyboardAppearance::Dark) |
"default" | "light" | "dark" |
A aparência do teclado; "default" segue a aparência do sistema. Os teclados do Android seguem o tema do sistema. |
use nsh_core::element::AutoFill;
input().auto_fill(AutoFill::OneTimeCode).keyboard_type(KeyboardType::Number)<Input autoComplete="one-time-code" keyboardType="number-pad" ios={{ textContentType: "oneTimeCode" }} />#Eventos
| Evento | Rust | Payload | Quando |
|---|---|---|---|
onChange |
.on_change(|text: &str| ..) |
{ value } |
O usuário alterou o texto: value é o texto atual. |
onSubmit |
.on_submit(|text: &str| ..) |
{ text } |
A tecla return foi pressionada em um input que envia (linha única, ou submitBehavior "submit" / "blurAndSubmit"). |
onFocus / onBlur |
.on_focus / .on_blur |
nenhum | O input ganhou / perdeu o foco. |
onSelectionChange |
.on_selection_change(|s: &Selection| ..) |
{ start, end } |
O cursor ou a seleção se moveu (unidades UTF-16). |
onPaste |
.on_paste(|e: &PasteEvent| ..) |
{ text, html, files, uris } |
O usuário colou: o texto, sua formatação como HTML (Linux, web), imagens (uma captura de tela) como Files, URLs file: de arquivos locais. Ocorre antes do onChange do texto que ela insere (formulários e teclado). |
onCaretMove |
.on_caret_move(|e: &CaretEvent| ..) |
{ bounds, pageBounds } |
O cursor se moveu, junto com a seleção ou conforme o texto rolou ou quebrou: bounds é onde ele está no input, pageBounds na janela, o anchor de um Overlay (formulários e teclado). |
onKeyPress |
.on_key_press(|key: &str| ..) |
{ key } |
Antes de o texto mudar: key é o texto digitado, "Backspace" ou "Enter". |
onContentSizeChange |
.on_content_size_change(|size: &Size<Points>| ..) |
{ size } |
O tamanho do texto mudou (na largura do input quando multilinha), conforme é digitado; um input vazio reporta uma linha vazia, seja qual for o seu placeholder. |
onCompositionChange |
.on_composition_change(|e: &Composition| ..) |
{ composing, text } |
A composição de um método de entrada começou, mudou ou terminou: text é o que está sendo composto (pinyin antes de um caractere ser escolhido, kana antes da conversão), "" quando ela termina. O texto confirmado chega a onChange antes do fim. |
Um input multilinha que cresce com o seu texto:
let height = signal(px(40.0));
input()
.multiline(true)
.on_content_size_change(move |size: &Size<Points>| {
height.set(px((size.height.0 + 16.0).clamp(40.0, 120.0)));
})
.h(move || height.get())
.px_3()
.py_2()const height = signal(40);
<Input
multiline
onContentSizeChange={({ size }) => height.set(Math.min(120, Math.max(40, size.height + 16)))}
style={{ height: height(), paddingHorizontal: 12, paddingVertical: 8 }}
/>;#Métodos de ref
| Método | Rust | Descrição |
|---|---|---|
focus() |
focus() |
Foca o input e mostra o teclado. |
blur() |
blur() |
Remove o foco dele. |
setSelection(start, end?) |
set_selection(start, end) |
Seleciona start..end (unidades UTF-16); um cursor quando são iguais. |
Mover o foco para o próximo campo ao pressionar return:
use nsh_core::element::AutoFill;
let password = element_ref();
view()
.child(input().return_key_type(ReturnKeyType::Next).on_submit(move |_: &str| password.focus()))
.child(input().element_ref(&password).masked(true).auto_fill(AutoFill::Password))let password!: InputRef;
<>
<Input returnKeyType="next" onSubmit={() => password.focus()} />
<Input ref={password} masked autoComplete="current-password" />
</>;Keyboard.dismiss() de nsh (dismiss_keyboard() em Rust) oculta o teclado, qualquer que seja o input que o tenha.
#Notas de plataforma
- No macOS, o texto de linha única, os placeholders e os cursores compartilham as métricas de fonte do campo nativo e são centralizados juntos dentro da caixa de conteúdo.
lineHeightparticipa do layout; editores multilinha também o usam para o espaçamento entre parágrafos. - Inputs dentro de uma scroll view são mantidos fora do alcance do teclado de forma nativa. Em outros lugares, use um padding ou margin inferior de
env(keyboard-inset-height). disableddesabilita o input (sem foco), além de aplicar os estilos de desabilitado; um inputreadonlymantém seu foco e sua seleção.inputAccessoryViewIDse aplica no iOS, onde o teclado tem uma barra de acessórios.