Elements

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. It is the same prop as Text’s selectable: Rust sets it with view().selectable(true), TSX with <View selectable>.

el-text-selection.tsx
el-text-selection.tsxLive
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.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>

        <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 selectable={false} style={{ color: ui().faint }}>
              Updated (selectable=false)
            </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 selectable=false stays out of it.</Text>
        <Input
          multiline
          value={pasted()}
          onChange={(text) => pasted.set(text)}
          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").selectable(false).text_color(rgb(0x737373)))
                .child(text("October 21")),
        )
}

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 chat transcript

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()),
        )),
    )
}

#How a selection is made

PlatformHow a selection is made
macOS, Windows, Linux, GPUIDrag 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, AndroidA long press selects a word; the platform’s handles extend it, and its edit menu has Copy and Select All.
WebThe 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 views: a selectable view inside another is part of the outer one, so one selection covers both.
  • TextView: a TextView with selectable selects 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 selectable 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.
  • View: the selectable prop.
  • Text: selectable, selectionColor and the selection of a single text.
  • TextView: documents whose blocks select as one.