元素

Text

Text 绘制一个段落。它是显示字符串的元素:View 的字符串子节点什么也不会画。段落在原生侧排版(iOS 和 Mac Catalyst 上用 CoreText,Android 上用 StaticLayout),并在引擎线程上测量,主线程只负责绘制。Rust 用 text(content) 创建,TSX 用从 nsh 导入的 <Text>。

el-text.tsx
el-text.tsx运行中
import { mount, signal, Text, View } from "nsh";
import { ui } from "./palette";

const LONG =
  "Text is typeset natively (CoreText on iOS, StaticLayout on Android) and measured on the engine thread, so the main thread only draws it.";

function App() {
  const tapped = signal("nothing yet");
  const lines = signal(2);
  const link = () => ({ color: ui().accent, textDecorationLine: "underline" }) as const;
  return (
    <View style={{ flex: 1, backgroundColor: ui().page }}>
      <View style={{ flex: 1, width: "100%", maxWidth: 560, alignSelf: "center", padding: 20, paddingTop: 24, gap: 14 }}>
        <Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>text</Text>

        <Text style={{ fontSize: 16, lineHeight: 24, color: ui().text }}>
          {"Nested text elements are spans: "}
          <Text style={{ fontWeight: "700" }}>bold</Text>
          {", "}
          <Text style={{ fontStyle: "italic", color: ui().violet }}>italic</Text>
          {", and "}
          <Text style={link()} onPress={() => tapped.set("the first link")}>
            links
          </Text>
          {" that take presses of "}
          <Text style={link()} onPress={() => tapped.set("the second link")}>
            their own
          </Text>
          .
        </Text>
        <Text style={{ fontSize: 13, color: ui().muted }}>Pressed: {tapped()}</Text>

        <View style={{ height: 1, backgroundColor: ui().line }} />

        <Text numberOfLines={lines()} style={{ fontSize: 15, color: ui().text }}>
          {LONG}
        </Text>
        <View style={{ flexDirection: "row", gap: 8 }}>
          <View
            onPress={() => lines.set(lines() === 2 ? 0 : 2)}
            activeStyle={{ opacity: 0.6 }}
            style={{ ...ui().raised, borderRadius: 10, paddingHorizontal: 12, paddingVertical: 8 }}
          >
            <Text style={{ color: ui().text }}>{lines() === 2 ? "Show all" : "numberOfLines 2"}</Text>
          </View>
        </View>

        <Text numberOfLines={1} ellipsizeMode="middle" style={{ fontSize: 15, color: ui().muted }}>
          {LONG}
        </Text>

        <Text style={{ fontSize: 17, color: ui().text }}>
          <Text style={{ textDecorationLine: "line-through", textDecorationColor: ui().down }}>struck</Text>{" "}
          <Text style={{ textTransform: "uppercase", letterSpacing: 2 }}>spaced</Text>{" "}
          <Text style={{ fontVariant: ["tabular-nums"] }}>0123456789</Text>
        </Text>

        <Text
          style={{
            fontSize: 28,
            fontWeight: "800",
            color: ui().onAccent,
            backgroundColor: "#c8f26d",
            paddingHorizontal: 10,
            borderRadius: 8,
            textShadowColor: "rgba(255,255,255,0.55)",
            textShadowOffset: { width: 2, height: 2 },
            textShadowRadius: 4,
          }}
        >
          Text shadow
        </Text>
      </View>
    </View>
  );
}

mount(App);
use nsh_core::ui::prelude::*;

text("继续之前请阅读")
    .text_base()
    .line_height(px(24.0))
    .text_color(rgb(0x1f2328))
    .child(text("条款").font_weight(FontWeight::BOLD))
    .child("。")

text(..) 接受字符串、SharedString、signal 或闭包(text(move || format!("{} 项", count.get()))):闭包或 signal 读取的内容变化时,文本就地更新。

#片段

文本里的文本是同一段落中的一个片段(span),而不是单独的视图。片段继承段落的文本样式(color、fontSize、fontWeight 等),并覆盖自己设置的部分。整个文本块是一个原生视图。在 Rust 中,text(..) 先显示自身内容,再显示子元素:.child(text(..)) 添加一个片段,.child("…") 添加一段普通文字。

text("")
    .text_size(px(17.0))
    .child(text("Light ").font_weight(FontWeight::LIGHT))
    .child(text("Bold ").font_weight(FontWeight::BOLD))
    .child(text("Italic").italic().text_color(rgb(0x8250df)))

#片段背景

设置了背景的片段会在字符后面填充它们的方框:高度就是字体的高度(基线以上的 ascent 加基线以下的 descent,与行高无关);片段换到几行,每行各画一个方框,圆角取片段的圆角半径。片段内部的片段沿用它的背景,除非自己另设。背景只影响绘制:文本排版与不设背景时相同,可选中的文本复制时连同片段的字符一起复制。行内代码就是一个片段:

text("提交前先运行 ")
    .text_size(px(15.0))
    .line_height(px(22.0))
    .selectable(true)
    .child(
        text(" cargo test ")
            .font_family("ui-monospace")
            .text_size(px(13.0))
            .bg(rgb(0xeff1f3))
            .rounded(px(4.0)),
    )
    .child("。")

Tailwind 类同样适用:<Text class="bg-gray-100 rounded">。内边距和边框是方框样式,片段上不起作用;在片段内两侧加空格,方框两边就会留出空间。

#链接

带按压处理函数的片段就是链接:按在它的字符上由该片段处理,按在其他位置则由段落处理。

text("继续即表示你同意")
    .text_size(px(15.0))
    .on_press(move || log("整句话"))
    .child(
        text("服务条款")
            .text_color(rgb(0x0969da))
            .underline()
            .on_press(move || open("terms")),
    )
    .child("。")

#行内视图和图片

文本中的其他元素(视图、图片,即使位于片段内)都是行内附件:按自身尺寸布局,坐落在基线上(或由样式 verticalAlign 决定位置:"text-top"、"text-bottom"、"middle",与 CSS 一致;所在行会撑高以容纳它),并随文字一起换行。它们可以单独响应按压。

text("基于 ")
    .text_base()
    .line_height(px(26.0))
    .child(image(LOGO).size(px(18.0)).rounded(px(4.0)))
    .child(" NSH 构建。状态 ")
    .child(
        view()
            .bg(rgb(0x1a7f37))
            .rounded(px(9.0))
            .px(px(7.0))
            .h(px(18.0))
            .justify_center()
            .child(text("LIVE").text_color(white()).text_size(px(11.0)).font_weight(FontWeight::BOLD)),
    )

被 numberOfLines 或省略号截掉的附件会被隐藏。复制可选中的文本时不包含附件。

#空白字符

JSX 文本遵循 React 的规则:除文本首尾外,每行都会去掉首尾空白,只含空白的行被丢弃,剩下的行用一个空格连接。同一行内的空格会保留({a} {b} 保留中间的空格)。片段需要前导或尾随空格时,写 {" "} 或字符串表达式。Rust 字符串按原样绘制。

#属性

除通用属性外(style 的类型为 TextStyle):

属性Rust类型默认值说明
numberOfLines.number_of_lines(n)number0最多显示的行数;0 表示不限。
ellipsizeMode.ellipsize_mode(EllipsizeMode::Tail)"tail" | "head" | "middle" | "clip""tail"超出 numberOfLines 的文本在哪里截断。
allowFontScaling.allow_font_scaling(..)booleantrue跟随系统的文字大小设置。
maxFontSizeMultiplier.max_font_size_multiplier(..)number0系统缩放的上限;0 表示不限。
adjustsFontSizeToFit.shrink_to_fit(min_scale)booleanfalse缩小字号,直到文本能放进其行数(numberOfLines)和盒子。
minimumFontScale.shrink_to_fit(min_scale)numberadjustsFontSizeToFit 最多缩小到的比例(0..1;至少 0.1)。
selectable.selectable(..)boolean用户可以选择文本的任意范围:长按选中一个词,拖动选择柄扩展,编辑菜单复制。不设置时文字不可选择,但桌面浏览器除外:在那里文字默认可选,交互元素里的除外;设为 false 在那里也不可选择。
selectionColor(样式).selection_color(..)ColorValue平台的强调色选区高亮和选择柄的颜色。
onTextLayout(e: { lines: TextLayoutLine[] }) => void每次布局后给出排好的各行。

Tailwind 类 select-text 等同于 selectable。选区高亮和选择柄的颜色由样式键 selectionColor(selection:text-*)设置,默认为平台的强调色。

#文本样式

TextStyle 在视图样式之上增加了:color、fontSize、fontWeight("normal"、"bold" 或 100–900)、fontStyle、fontFamily、lineHeight、letterSpacing、textAlign、textAlignVertical、textTransform、textDecorationLine、textDecorationColor、textDecorationStyle、textDecorationThickness、textUnderlineOffset、textShadowColor、textShadowOffset、textShadowRadius、fontVariant 和 writingDirection。在 Rust 中它们是 Styled 方法:.text_color(..)、.text_size(..)(或 .text_sm()、.text_lg() 等)、.font_weight(FontWeight::SEMIBOLD)、.italic()、.font_family(..)、.line_height(..)、.letter_spacing(..)、.text_center()、.uppercase()、.underline()、.line_through()、.tabular_nums() 等。参见样式参考和文本。

字体族可以是系统字体族,也可以是应用的字体文件:

// assets/fonts/Inter-Regular.ttf,由注册表随应用发布(`registry.assets(assets!())`)。
const INTER: FontAsset = asset!("fonts/Inter-Regular.ttf");

text("Inter").font_family(INTER.family).text_size(px(17.0))

字体会在第一帧之前注册。参见字体。

#事件

事件Rust载荷触发时机
onTextLayout{ lines: TextLayoutLine[] }段落每次布局之后。
onPress、onLongPress 等.on_press、.on_long_press 等无与 view 相同;在片段上时,指按在其字符上的按压。
onLayout.on_layoutBounds文本块的 frame 发生变化。

TextLayoutLine 为 { text, x, y, width, height, ascender, descender, capHeight, xHeight },坐标位于文本的内容盒中:

const lines = signal(0);
<Text onTextLayout={(e) => lines.set(e.lines.length)}>{article}</Text>;

Rust 用 measure_text(..) 在布局之前测量字符串。

#ref 方法

没有自己的方法。

#平台说明

  • 选区在一个文本块(一个文本及其片段)之内。iOS 显示其编辑菜单(拷贝、全选、查询、翻译、共享),Android 显示浮动工具栏(复制、全选、分享)。
  • 在 iOS 上,非实线单线的文本装饰线(double、dotted、dashed、wavy)由 NSH 自行绘制。
  • iOS 上的文本绘制纯色背景和外阴影。
  • nsh/device 的 fontScale()(Rust 中为 font_scale())给出当前系统的文字缩放比例。