Элементы
Input
Input — нативное текстовое поле: UITextField (однострочное) или UITextView (многострочное) на iOS и Mac Catalyst, EditText на Android. Ввод, каретка, выделение, автокоррекция и автозаполнение — штатные средства платформы. В Rust его создаёт nsh_base::input(), в TSX — <Input> из 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 }}
/>
);
}#Управляемый и неуправляемый режимы
- Управляемый: передайте
valueи обновляйте его вonChange. Приложение может переписать то, что ввёл пользователь (обрезать, отформатировать): текст сразу показывает правку и исправляется, когда приходит новоеvalue. Каждая нативная правка несёт счётчик, аvalueот приложения применяется, если оно основано на последней правке, поэтому более новый ввод всегда побеждает медленное обновление. - Неуправляемый: передайте
defaultValue(.default_value(..)или ничего) и читайте текст изonChangeилиonSubmit.
Правила, которые должны соблюдаться во время ввода, — это props, обрабатываемые нативно: maxLength ограничивает длину, а submitBehavior определяет, что делает return. См. формы и клавиатура.
#Props
Помимо общих props (где style имеет тип TextStyle: применяются шрифт, цвет, выравнивание и стили вида):
#Текст
| Prop | Rust | Тип | По умолчанию | Описание |
|---|---|---|---|---|
value | .value(..) | string | Текст, управляемый приложением. | |
defaultValue | .default_value(..) | string | Начальный текст. | |
placeholder | .placeholder(..) | string | Показывается, пока поле пусто. | |
multiline | .multiline(..) | boolean | false | Несколько строк; return вставляет перевод строки, если submitBehavior не указывает иное. |
scrollbar | .scrollbar(..) | boolean | ScrollbarTheme | true | Полоса прокрутки многострочного поля: индикатор платформы или встроенная полоса приложения, если оно её задало (Scrollbar); объект — это собственная тема, false — без полосы. |
maxLength | .max_length(..) | number | Максимальное число символов, которое может ввести пользователь. | |
readonly | .readonly(..) | boolean | false | Пользователь может выделять и копировать текст, но не менять его. |
masked | .masked(..) | boolean | false | Текст скрыт (пароли). |
allowFontScaling | boolean | true | Следовать системному размеру текста. | |
maxFontSizeMultiplier | number | 0 | Верхняя граница системного масштаба (0 — без ограничения). |
#Клавиатура
| Prop | Rust | Тип | По умолчанию | Описание |
|---|---|---|---|---|
keyboardType | .keyboard_type(KeyboardType::Email) | "default" | "number-pad" | "decimal-pad" | "numeric" | "numbers-and-punctuation" | "email-address" | "phone-pad" | "url" | "default" | Раскладка клавиатуры (значения; у numbers-and-punctuation на iOS есть знак минуса). |
returnKeyType | .return_key_type(ReturnKeyType::Next) | "default" | "done" | "go" | "next" | "search" | "send" | "default" | Надпись на клавише return. |
autoCapitalize | .auto_capitalize(AutoCapitalize::None) | "none" | "sentences" | "words" | "characters" | "sentences" | Автоматическая капитализация. |
autoCorrect | .auto_correct(..) | boolean | true | Исправления системы. |
autoComplete | .auto_fill(AutoFill::OneTimeCode) | AutoComplete | TextContentType | Что содержит поле, для автозаполнения на iOS, Android и в вебе: "username", "current-password", "new-password", "one-time-code", "email", "tel", "postal-code", "cc-number", "off", … | |
pasteBehavior | .paste_behavior(PasteBehavior::Report) | "insert" | "report" | "insert" | Что делает вставка: "insert" вставляет вставленный текст, "report" ничего не вставляет, чтобы приложение обработало то, о чём сообщает onPaste. Изображения и файлы никогда не вставляются. |
submitBehavior | .submit_behavior(SubmitBehavior::Submit) | "submit" | "blurAndSubmit" | "newline" | "blurAndSubmit" (однострочное), "newline" (многострочное) | Что делает return: "submit" сообщает onSubmit и сохраняет фокус, "blurAndSubmit" также снимает фокус, "newline" вставляет перевод строки (многострочное; однострочное отправляет). Многострочное поле, которое отправляет, всё равно принимает переводы строк по Shift+Return или Option / Alt+Return на аппаратных клавиатурах. "submit" оставляет клавиатуру открытой (например, чтобы отправить несколько сообщений). |
autoFocus | .auto_focus(..) | boolean | false | Получает фокус при появлении. |
#Каретка и выделение
| Prop | Rust | Тип | Описание |
|---|---|---|---|
caret | .caret(..) | boolean | Каретка отображается (по умолчанию true; false скрывает её). |
selectedRange | .selected_range((start, end)) | { start: number; end?: number } | Управляемое выделение в единицах UTF-16; end по умолчанию равно start (каретка). |
Каретка мигает, пока её окно показано и активно. В окнах GPUI на Linux она остаётся видимой после десяти секунд без ввода и перемещения, как в приложениях GTK, поэтому сфокусированное поле, оставленное без внимания, не тратит кадры.
#Цвета
Цвета placeholder, каретки и подсветки выделения — это ключи стиля с соответствующими классами Tailwind и методы в Rust (UIKit окрашивает каретку и выделение одним цветом — цветом каретки, если он задан):
| Ключ стиля | Rust | Класс | Описание |
|---|---|---|---|
placeholderColor | .placeholder_color(..) | placeholder:text-* | Цвет placeholder. |
caretColor | .caret_color(..) | caret-* | Цвет каретки. |
selectionColor | .selection_color(..) | selection:text-* | Цвет подсветки выделения. |
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
Ключ ios | Rust | Тип | Описание |
|---|---|---|---|
textContentType | .auto_fill(..) | AutoComplete | TextContentType | Названия iOS для autoComplete ("oneTimeCode", "newPassword", …). |
inputAccessoryViewID | .input_accessory(id) | string | nativeID для InputAccessoryView (InputAccessoryView), показываемый над клавиатурой, пока поле в фокусе. |
keyboardAppearance | .keyboard_appearance(KeyboardAppearance::Dark) | "default" | "light" | "dark" | Внешний вид клавиатуры; "default" следует внешнему виду системы. Клавиатуры Android следуют системной теме. |
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" }} />#События
| Событие | Rust | Данные | Когда |
|---|---|---|---|
onChange | .on_change(|text: &str| ..) | { value } | Пользователь изменил текст: value — текущий текст. |
onSubmit | .on_submit(|text: &str| ..) | { text } | Нажата клавиша return в поле, которое отправляет (однострочное или submitBehavior "submit" / "blurAndSubmit"). |
onFocus / onBlur | .on_focus / .on_blur | нет | Поле получило / потеряло фокус. |
onSelectionChange | .on_selection_change(|s: &Selection| ..) | { start, end } | Каретка или выделение переместились (единицы UTF-16). |
onPaste | .on_paste(|e: &PasteEvent| ..) | { text, html, files, uris } | Пользователь вставил: текст, его форматирование в виде HTML (Linux, веб), изображения (скриншот) в виде File, URL file: локальных файлов. Приходит до onChange для вставляемого текста (формы и клавиатура). |
onCaretMove | .on_caret_move(|e: &CaretEvent| ..) | { bounds, pageBounds } | Каретка переместилась вместе с выделением либо при прокрутке или переносе текста: bounds — её положение в поле, pageBounds — в окне, Overlay у anchor (формы и клавиатура). |
onKeyPress | .on_key_press(|key: &str| ..) | { key } | Перед изменением текста: key — набранный текст, "Backspace" или "Enter". |
onContentSizeChange | .on_content_size_change(|size: &Size<Points>| ..) | { size } | Размер текста изменился (при ширине поля, если оно многострочное) по мере ввода; пустое поле сообщает об одной пустой строке независимо от placeholder. |
onCompositionChange | .on_composition_change(|e: &Composition| ..) | { composing, text } | Композиция метода ввода началась, изменилась или закончилась: text — то, что набирается (пиньинь до выбора иероглифа, кана до преобразования), "" после завершения. Зафиксированный текст доходит до onChange до завершения. |
Многострочное поле, которое растёт вместе с текстом:
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 }}
/>;#Методы ref
| Метод | Rust | Описание |
|---|---|---|
focus() | focus() | Фокусирует поле и показывает клавиатуру. |
blur() | blur() | Снимает с него фокус. |
setSelection(start, end?) | set_selection(start, end) | Выделяет start..end (единицы UTF-16); каретка, если они равны. |
Перевод фокуса на следующее поле по 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() из nsh (dismiss_keyboard() в Rust) скрывает клавиатуру, в каком бы поле она ни была открыта.
#Примечания по платформам
- На macOS однострочный текст, placeholder и каретка используют метрики шрифта нативного поля и центрируются вместе внутри области содержимого.
lineHeightучаствует в раскладке; многострочные редакторы также используют его для интервалов между абзацами. - Поля внутри scroll view нативно не заслоняются клавиатурой. В остальных случаях используйте нижний padding или margin, равный
env(keyboard-inset-height). disabledотключает поле (без фокуса) и применяет стили отключённого состояния; поле сreadonlyсохраняет фокус и выделение.inputAccessoryViewIDдействует на iOS, где у клавиатуры есть панель аксессуаров.