| title | Text selection |
|---|---|
| description | Selectable views: one selection across the texts inside a view, across views, Text elements and TextViews, as a web page's texts select. |
| order | 130 |
| group | primitives |
| demo | el-text-selection |
A View with selectable 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. Make a chat transcript selectable 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 sets it with view().selectable(true), TSX with <View selectable>; whether a single
text can be selected is its userSelect style.
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>
);
}The view's layout and look are the app's styles, and its content is its children. The selection itself is the platform's: the view draws nothing of its own for it.
A transcript in one selectable view 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::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>| 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 view, 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. |
- Texts that stay out: a text with
userSelect: "none"(.select_none()in Rust, the classselect-none; a label, a button's caption) is skipped by the selection and its gestures. - Nested views: a selectable view inside another is part of the outer one, so one selection covers both.
- TextView: a
TextViewwithselectableselects as one of its own: its paragraphs, list items, cells and code blocks select together. Inside a selectable view it joins the outer selection. - Single texts: a text with
userSelect: "text"in no selectable view 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.