العناصر
تحديد النص
يجعل View مع selectable النصوص الموجودة بداخله تُحدَّد معًا، كما في صفحات الويب: يمتد تحديد واحد من موضع في نص إلى موضع في نص آخر، عبر العروض (views) وعناصر Text وعناصر TextView، بترتيب القراءة. اجعل سجل محادثة قابلًا للتحديد، فيمكن أن يبدأ التحديد في رسالة وينتهي في التالية؛ ويضع النسخ (Copy) النص المحدد في الحافظة، مع ربط النصوص المتجاورة جنبًا إلى جنب بمسافة جدولة (tab) وربط البقية بفاصل أسطر. يُضبط ذلك في Rust باستخدام view().selectable(true)، وفي TSX باستخدام <View selectable>؛ أما إمكانية تحديد نص منفرد فيحددها النمط userSelect.
import { mount, Scroll, signal, Text, View } from "nsh";
import { Input } from "nsh/base";
import { TextView } from "nsh/base";
import { platform } from "nsh";
import { field, ui } from "./palette";
const NOTES = "- Sync runs **in the background**\n- Conflicts merge *per field*";
function App() {
const pasted = signal("");
return (
<Scroll style={{ flex: 1, backgroundColor: ui().page }}>
<View style={{ width: "100%", maxWidth: 560, alignSelf: "center", padding: 20, paddingTop: 24, gap: 14 }}>
<Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>selectable view</Text>
<Text style={{ color: ui().muted, lineHeight: 20 }}>
{platform.desktop
? "Drag with the mouse from the title into the list: one selection runs across them. Double click a word, triple click a paragraph, Ctrl / Cmd+C copies."
: "Long press a word, then drag the handles across the texts: one selection runs across them. Copy is in the menu."}
</Text>
<View selectable style={{ ...ui().card, borderRadius: 16, padding: 16, gap: 10 }}>
<Text style={{ fontSize: 17, fontWeight: "600", color: ui().text }}>Release notes</Text>
<Text style={{ color: ui().text, lineHeight: 22 }}>Version 2.4 moves the app to offline sync.</Text>
<TextView markdown={NOTES} />
<View style={{ flexDirection: "row", gap: 8 }}>
<Text style={{ userSelect: "none", color: ui().faint }}>
Updated (userSelect none)
</Text>
<Text style={{ color: ui().text }}>October 21</Text>
</View>
</View>
<Text style={{ fontSize: 12, color: ui().muted }}>Paste what you copied: the label marked userSelect none stays out of it.</Text>
<Input
multiline
value={pasted()}
onChange={({ value }) => pasted.set(value)}
placeholder="Paste here"
{...field({ style: { height: 110 } })}
/>
<Text style={{ fontSize: 12, color: ui().muted, fontVariant: ["tabular-nums"] }}>
{pasted().length} characters, {pasted() ? pasted().split("\n").length : 0} lines
</Text>
</View>
</Scroll>
);
}
mount(App);use nsh_base::text_view;
use nsh_core::prelude::*;
fn release_notes(notes: &'static str) -> impl IntoElement {
view()
.selectable(true)
.gap_3()
.p_4()
.child(text("Release notes").font_weight(FontWeight::SEMIBOLD))
.child(text_view().markdown(notes))
.child(
view()
.flex_row()
.gap_2()
.child(text("Updated").select_none().text_color(rgb(0x737373)))
.child(text("October 21")),
)
}import { Text, View } from "nsh";
import { TextView } from "nsh/base";
function ReleaseNotes(props: { notes: string }) {
return (
<View selectable class="gap-3 p-4">
<Text class="font-semibold">Release notes</Text>
<TextView markdown={props.notes} />
<View class="flex-row gap-2">
<Text style={{ userSelect: "none" }} class="text-neutral-500">Updated</Text>
<Text>October 21</Text>
</View>
</View>
);
}تخطيط العرض ومظهره يعودان إلى أنماط التطبيق، ومحتواه هو عناصره الفرعية. أما التحديد نفسه فيعود إلى المنصة: لا يرسم العرض شيئًا خاصًا به من أجله.
#سجل محادثة
يُحدَّد سجل المحادثة داخل عرض واحد قابل للتحديد كأنه صفحة واحدة: يبدأ السحب في رد وينتهي في رد آخر، وينسخ Copy نص كل رسالة بينهما.
use nsh_base::text_view;
use nsh_core::prelude::*;
fn transcript(replies: Signal<Vec<SharedString>>) -> impl IntoElement {
scroll().flex_1().child(
view().selectable(true).p_4().gap_4().child(each(
replies,
|reply: &SharedString| reply.clone(),
|reply, _index| text_view().markdown(move || reply.get()),
)),
)
}import { Scroll, View } from "nsh";
import { TextView } from "nsh/base";
<Scroll class="flex-1">
<View selectable class="gap-4 p-4">
<Each of={replies}>{(reply) => <TextView markdown={reply()} />}</Each>
</View>
</Scroll>#كيف يتم التحديد
| المنصة | كيف يتم التحديد |
|---|---|
| macOS, Windows, Linux, GPUI | اسحب عبر النصوص؛ النقر المزدوج يحدد كلمة، والنقر الثلاثي يحدد فقرة، والسحب بعدهما يستمر كلمةً كلمة أو فقرةً فقرة. Shift+click يمدد التحديد. Ctrl / Cmd+A يحدد كل شيء في العرض، وCtrl / Cmd+C ينسخ، وEscape أو الضغط خارج العرض يلغي التحديد. |
| iOS, Android | الضغط المطوّل يحدد كلمة؛ وتمدده مقابض المنصة، وتتضمن قائمة التحرير فيها Copy وSelect All. |
| Web | تحديد المتصفح نفسه، بإيماءاته وقائمته واختصاراته. |
#ما الذي يحدده
- النصوص المستبعدة: النص الذي يحمل
userSelect: "none"(.select_none()في Rust، والفئةselect-none؛ مثل التسمية (label) أو عنوان الزر) يتجاوزه التحديد وإيماءاته. - العروض المتداخلة: العرض القابل للتحديد داخل عرض آخر يُعد جزءًا من العرض الخارجي، فيشملهما تحديد واحد.
- TextView: يُحدَّد
TextViewمعselectableكوحدة مستقلة: تُحدَّد فقراته وعناصر قوائمه وخلاياه وكتل الشيفرة فيه معًا. وداخل عرض قابل للتحديد ينضم إلى التحديد الخارجي. - النصوص المنفردة: النص الذي يحمل
userSelect: "text"وليس داخل أي عرض قابل للتحديد يُحدَّد وحده، كما يصف Text. - المظهر هو تمييز المنصة في
selectionColorالخاص بكل نص (selection:text-*). - القوائم: في List الافتراضية (virtualized)، يشمل التحديد الصفوف المركّبة (mounted)؛ أما الصفوف التي أُعيد تدويرها وخرجت من مجال الرؤية فتغادره.