Эта страница переведена машинным способом и может содержать ошибки. Читать оригинал на английском

Элементы

Input

Input — нативное текстовое поле: UITextField (однострочное) или UITextView (многострочное) на iOS и Mac Catalyst, EditText на Android. Ввод, каретка, выделение, автокоррекция и автозаполнение — штатные средства платформы. В Rust его создаёт nsh_base::input(), в TSX — <Input> из nsh/base.

el-input.tsx
el-input.tsxLive
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))
}

#Управляемый и неуправляемый режимы

  • Управляемый: передайте value и обновляйте его в onChange. Приложение может переписать то, что ввёл пользователь (обрезать, отформатировать): текст сразу показывает правку и исправляется, когда приходит новое value. Каждая нативная правка несёт счётчик, а value от приложения применяется, если оно основано на последней правке, поэтому более новый ввод всегда побеждает медленное обновление.
  • Неуправляемый: передайте defaultValue (.default_value(..) или ничего) и читайте текст из onChange или onSubmit.

Правила, которые должны соблюдаться во время ввода, — это props, обрабатываемые нативно: maxLength ограничивает длину, а submitBehavior определяет, что делает return. См. формы и клавиатура.

#Props

Помимо общих props (где style имеет тип TextStyle: применяются шрифт, цвет, выравнивание и стили вида):

#Текст

PropRustТипПо умолчаниюОписание
value.value(..)stringТекст, управляемый приложением.
defaultValue.default_value(..)stringНачальный текст.
placeholder.placeholder(..)stringПоказывается, пока поле пусто.
multiline.multiline(..)booleanfalseНесколько строк; return вставляет перевод строки, если submitBehavior не указывает иное.
scrollbar.scrollbar(..)boolean | ScrollbarThemetrueПолоса прокрутки многострочного поля: индикатор платформы или встроенная полоса приложения, если оно её задало (Scrollbar); объект — это собственная тема, false — без полосы.
maxLength.max_length(..)numberМаксимальное число символов, которое может ввести пользователь.
readonly.readonly(..)booleanfalseПользователь может выделять и копировать текст, но не менять его.
masked.masked(..)booleanfalseТекст скрыт (пароли).
allowFontScalingbooleantrueСледовать системному размеру текста.
maxFontSizeMultipliernumber0Верхняя граница системного масштаба (0 — без ограничения).

#Клавиатура

PropRustТипПо умолчаниюОписание
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(..)booleantrueИсправления системы.
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(..)booleanfalseПолучает фокус при появлении.

#Каретка и выделение

PropRustТипОписание
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))

#iOS

Ключ iosRustТипОписание
textContentType.auto_fill(..)AutoComplete | TextContentTypeНазвания iOS для autoComplete ("oneTimeCode", "newPassword", …).
inputAccessoryViewID.input_accessory(id)stringnativeID для 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)

#События

Событие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()

#Методы 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))

Keyboard.dismiss() из nsh (dismiss_keyboard() в Rust) скрывает клавиатуру, в каком бы поле она ни была открыта.

#Примечания по платформам

  • На macOS однострочный текст, placeholder и каретка используют метрики шрифта нативного поля и центрируются вместе внутри области содержимого. lineHeight участвует в раскладке; многострочные редакторы также используют его для интервалов между абзацами.
  • Поля внутри scroll view нативно не заслоняются клавиатурой. В остальных случаях используйте нижний padding или margin, равный env(keyboard-inset-height).
  • disabled отключает поле (без фокуса) и применяет стили отключённого состояния; поле с readonly сохраняет фокус и выделение.
  • inputAccessoryViewID действует на iOS, где у клавиатуры есть панель аксессуаров.