العناصر
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.
القواعد التي يجب أن تسري أثناء كتابة المستخدم هي خصائص تُعالَج أصليًا: maxLength يحدّ الطول، وsubmitBehavior يحدد ما يفعله زر الإدخال (return). انظر النماذج ولوحة المفاتيح.
#الخصائص
إضافةً إلى الخصائص المشتركة (مع style من النوع TextStyle: تنطبق الخطوط والألوان والمحاذاة وأنماط العرض):
#النص
| الخاصية | Rust | النوع | الافتراضي | الوصف |
|---|---|---|---|---|
value | .value(..) | string | النص، يتحكم به التطبيق. | |
defaultValue | .default_value(..) | string | النص الابتدائي. | |
placeholder | .placeholder(..) | string | يظهر ما دام الحقل فارغًا. | |
multiline | .multiline(..) | boolean | false | عدة أسطر؛ يُدرج زر الإدخال فاصل سطر ما لم يحدد 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 تعني بلا حد). |
#لوحة المفاتيح
| الخاصية | 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" | تسمية زر الإدخال. |
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" (متعدد الأسطر) | ما يفعله زر الإدخال: "submit" يبلّغ عن onSubmit ويُبقي التركيز، و"blurAndSubmit" يُزيل التركيز أيضًا، و"newline" يُدرج فاصل سطر (متعدد الأسطر؛ أما السطر الواحد فيُرسل). يظل الحقل المتعدد الأسطر الذي يُرسل يقبل فواصل الأسطر من Shift+Return أو Option / Alt+Return على لوحات المفاتيح الفعلية. "submit" يُبقي لوحة المفاتيح ظاهرة (مثلًا لإرسال عدة رسائل). |
autoFocus | .auto_focus(..) | boolean | false | يأخذ التركيز عند ظهوره. |
#المؤشر والتحديد
| الخاصية | Rust | النوع | الوصف |
|---|---|---|---|
caret | .caret(..) | boolean | يُرسم المؤشر (الافتراضي true؛ وfalse يخفيه). |
selectedRange | .selected_range((start, end)) | { start: number; end?: number } | التحديد المتحكَّم به، بوحدات UTF-16؛ القيمة الافتراضية لـ end هي start (مؤشر). |
يومض المؤشر ما دامت نافذته ظاهرة ونشطة. في نوافذ GPUI على Linux يبقى ظاهرًا بعد عشر ثوانٍ من دون كتابة أو تحريك، كما في تطبيقات GTK، فلا يكلّف الحقل المركَّز المتروك أي إطارات.
#الألوان
ألوان النص البديل والمؤشر وتمييز التحديد هي مفاتيح أنماط، مع أصناف Tailwind الخاصة بها، وطرق في Rust (يلوّن UIKit المؤشر والتحديد بلون واحد، هو لون المؤشر إن ضُبط):
| مفتاح النمط | Rust | الصنف | الوصف |
|---|---|---|---|
placeholderColor | .placeholder_color(..) | placeholder:text-* | لون النص البديل. |
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 } | ضُغط زر الإدخال في حقل يُرسل (سطر واحد، أو 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 موضعه في النافذة، وهو anchor لـ Overlay (النماذج ولوحة المفاتيح). |
onKeyPress | .on_key_press(|key: &str| ..) | { key } | قبل تغيّر النص: key هو النص المكتوب، "Backspace" أو "Enter". |
onContentSizeChange | .on_content_size_change(|size: &Size<Points>| ..) | { size } | تغيّر حجم النص (بعرض الحقل إن كان متعدد الأسطر) أثناء كتابته؛ ويبلّغ الحقل الفارغ عن سطر فارغ واحد مهما كان نصه البديل. |
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)؛ ويكون مؤشرًا عندما يتساويان. |
نقل التركيز إلى الحقل التالي عند الضغط على زر الإدخال:
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، يتشارك النص ذو السطر الواحد والنصوص البديلة والمؤشرات مقاييس خط الحقل الأصلي، وتُوسَّط معًا داخل صندوق المحتوى.
lineHeightيشارك في التخطيط؛ وتستخدمه المحررات المتعددة الأسطر أيضًا للتباعد بين الفقرات. - تُبعَد الحقول داخل scroll view عن لوحة المفاتيح أصليًا. في غير ذلك استخدم حشوة سفلية أو هامشًا سفليًا بقيمة
env(keyboard-inset-height). disabledيعطّل الحقل (بلا تركيز) إضافةً إلى تطبيق أنماط التعطيل؛ أما الحقلreadonlyفيحتفظ بتركيزه وتحديده.inputAccessoryViewIDينطبق على iOS، حيث تحتوي لوحة المفاتيح على شريط ملحقات.