Elements
SelectionArea
SelectionArea makes the texts inside it select together, as a web page’s do: one selection runs from a place in one
text to a place in another, across views, Text elements and TextViews, in reading
order. Wrap a chat transcript in it and a selection can start in one message and end in the next; Copy puts the
selected text on the clipboard, with texts side by side joined by a tab and the others by a line break. Rust makes one
with nsh_base::selection_area(), TSX with <SelectionArea> from nsh/base.
import { Input, mount, Scroll, signal, Text, View } from "nsh";
import { SelectionArea, TextView } from "nsh/base";
import { platform } from "nsh/device";
import { 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 }}>selection area</Text>
<Text style={{ color: ui().muted, lineHeight: 20 }}>
{platform.idiom === "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>
<SelectionArea 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 selectable={false} style={{ color: ui().faint }}>
Updated (selectable=false)
</Text>
<Text style={{ color: ui().text }}>October 21</Text>
</View>
</SelectionArea>
<Text style={{ fontSize: 12, color: ui().muted }}>Paste what you copied: the label marked selectable=false stays out of it.</Text>
<Input
multiline
value={pasted()}
onChange={(text) => pasted.set(text)}
placeholder="Paste here"
style={{ ...ui().inset, height: 110, borderRadius: 12, padding: 12, fontSize: 15, color: ui().text, placeholderColor: ui().faint }}
/>
<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::{selection_area, text_view};
use nsh_core::ui::prelude::*;
fn release_notes(notes: &'static str) -> impl IntoElement {
selection_area()
.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").selectable(false).text_color(rgb(0x737373)))
.child(text("October 21")),
)
}import { Text, View } from "nsh";
import { SelectionArea, TextView } from "nsh/base";
function ReleaseNotes(props: { notes: string }) {
return (
<SelectionArea class="gap-3 p-4">
<Text class="font-semibold">Release notes</Text>
<TextView markdown={props.notes} />
<View class="flex-row gap-2">
<Text selectable={false} class="text-neutral-500">Updated</Text>
<Text>October 21</Text>
</View>
</SelectionArea>
);
}SelectionArea comes from nsh/base, the nsh-base crate in Rust. It is a view: its
layout and look are the app’s styles, and its content is its children. The selection itself is the platform’s: the
area draws nothing of its own.
#A chat transcript
A transcript in one area selects as one page: a drag starts in one reply and ends in another, and Copy takes the text of every message in between.
use nsh_base::{selection_area, text_view};
use nsh_core::ui::prelude::*;
fn transcript(replies: Signal<Vec<SharedString>>) -> impl IntoElement {
scroll().flex_1().child(
selection_area().p_4().gap_4().child(each(
replies,
|reply: &SharedString| reply.clone(),
|reply, _index| text_view().markdown(move || reply.get()),
)),
)
}import { Scroll } from "nsh";
import { SelectionArea, TextView } from "nsh/base";
<Scroll class="flex-1">
<SelectionArea class="gap-4 p-4">
<Each of={replies}>{(reply) => <TextView markdown={reply()} />}</Each>
</SelectionArea>
</Scroll>#How a selection is made
| Platform | How a selection is made |
|---|---|
| macOS, Windows, Linux, GPUI | Drag across the texts; a double click selects a word, a triple click a paragraph, and a drag after them goes on by words or paragraphs. Shift+click extends the selection. Ctrl / Cmd+A selects everything in the area, Ctrl / Cmd+C copies, Escape or a press outside clears. |
| iOS, Android | A long press selects a word; the platform’s handles extend it, and its edit menu has Copy and Select All. |
| Web | The browser’s own selection, with its gestures, menu and shortcuts. |
#What it selects
- Texts that stay out: a text with
selectable={false}(.selectable(false)in Rust; a label, a button’s caption) is skipped by the selection and its gestures. - Nested areas: an area inside another is part of the outer one, so one selection covers both.
- TextView: a
TextViewwithselectableis a selection area of its own: its paragraphs, list items, cells and code blocks select together. Inside aSelectionAreait joins the outer selection. - Outside areas: a
selectabletext that is in no area selects on its own, as Text describes. - The look is the platform’s highlight in each text’s
selectionColor(selection:text-*). - Lists: in a virtualized List, a selection covers the rows that are mounted; rows recycled out of view leave it.
#Props
| Prop | Rust | Type | Description |
|---|---|---|---|
children | .child(..) | JSX.Element | The content: any views, with texts at any depth. |
class, style | style methods | string, StyleProp<ViewStyle> | Its layout and look, as a View’s. |