元素

元素

Native Shell 用一小组元素构建每个界面:View、Text、Image 等原语,以及 Button、Dialog、NavStack 等基础组件。每个原语都对应一个原生视图(iOS 和 Mac Catalyst 上是 UIView 子类,Android 上是 View,桌面上是 AppKit、GTK 或 Win32 视图),由 NSH 运行时创建并布局。应用显示的一切都由它们组成:应用的按钮、卡片、列表行和页面都是它自己的组件,用这些元素绘制、由应用设置样式,因此应用的外观完全由应用决定。

这些页面上的每个示例都同时给出 Rust 和 TSX:选择一个标签页,页面上的所有示例都会随之切换。

use nsh_core::prelude::*;

fn app() -> impl IntoElement {
    let count = signal(0_u32);
    view()
        .flex_1()
        .items_center()
        .justify_center()
        .gap_3()
        .child(text(move || format!("已点击 {} 次", count.get())).text_2xl())
        .child(
            view()
                .rounded_lg()
                .bg(rgb(0x2563eb))
                .px_4()
                .py_2()
                .active(|s| s.opacity(0.6))
                .on_press(move || count.update(|n| *n += 1))
                .child(text("点我").text_color(white())),
        )
}

在 Rust 中,原语是 nsh_core::prelude 中的函数(view()、text(..)、image(..)),基础组件来自 nsh_base crate。样式是有类型的方法(.flex_1()、.gap_3()、.px(px(20.0))、.bg(..)),状态样式接收一个闭包(.active(|s| s.opacity(0.6))),监听器是接收事件的闭包(.on_press(move || ..)、.on_change(move |text: &str| ..)),.child(..) 添加子元素。一段 UI 就是一个返回 impl IntoElement 的普通函数,只运行一次;它在闭包里读取的 signal(text(move || ..)、.bg(move || ..))会更新所绑定的内容。参见用 Rust 构建。

在 TSX 中,每个标签都使用 PascalCase,并指向作用域中的绑定:原语从 nsh 导入,基础组件从 nsh/base 导入。只有 <If> 和 <Each> 是无需导入的全局组件。<view> 这样的小写标签会导致构建错误,因为 TypeScript 只能对照每个名称一个的全局类型来检查它,List、Each 这类泛型组件就无法推断条目类型。编译器通过导入识别这些元素并就地创建原生视图,所以导入它们在运行时没有任何开销。参见 JSX 与组件。

import { Canvas, Image, List, Overlay, Scroll, Shader, Text, Video, View, WebView } from "nsh";
import { Button, Dialog, Menu, NavPage, NavStack, Popover, Sheet, Tooltip, Input } from "nsh/base";

#原语

TSX 从 nsh 导入,Rust 从 nsh_core::prelude 导入。

元素Rust原生视图(iOS / Android)页面
<View>view()UIView / ViewGroup;设置 selectable 后,其中的文字一起选择View、文字选择
<Text>text(content)CoreText 视图 / StaticLayout 视图Text
<Image>image(src)带图片图层的视图 / 图片视图Image
<Scroll>scroll()UIScrollView / ScrollViewScroll
<List>virtual_list(count, |at| row)复用单元格的 UIScrollView / ScrollViewList
<Canvas>canvas(painter)回放 2D 绘制的视图Canvas
<Shader>shader(program)由 WGSL 程序在 GPU 上绘制其区域的视图,绘制在子元素之下Shader
<WebView>web_view(url)、web_view_html(html, base)WKWebView / android.webkit.WebViewWebView
<Video>video(url)AVPlayerLayer / TextureView 上的 MediaPlayerVideo
<Overlay>overlay()窗口(或所在对话框)之上的视图,相对锚点放置Overlay
<If>、<Each>when(..)、each(..)、dynamic(..)控制流:它们选出的元素控制流
<InputAccessoryView>(nsh/mobile)input_accessory_view(id)iOS 上键盘上方的工具栏InputAccessoryView

padding 和 margin 用 CSS 的 env() 让内容避开系统 UI:env(safe-area-inset-top) 及其他边(安全区),以及 env(keyboard-inset-height)(键盘)。

#Base

TSX 从 nsh/base 导入,Rust 从 nsh_base crate 导入。它们负责行为、状态、焦点和无障碍,外观由应用决定。

组件Rust作用页面
AlertDialognsh_base::alert_dialog()系统的提示框和操作表AlertDialog
<Accordion> 及其部件nsh_base::accordion()、accordion_item() 等在各自标题下展开和收起的一组分区,一次一个或同时多个Accordion
<Avatar>、<AvatarGroup>nsh_base::avatar()、avatar_group()一个人的头像,加载中或失败时显示后备内容;以及只显示前几个的一排头像Avatar
<Button>nsh_base::button()带有状态和角色的可按压控件Button
<Carousel> 及其部件nsh_base::carousel()、carousel_content() 等一次显示一个位置的幻灯片,原生滑动,带上一张 / 下一张、分页、按键和无缝循环Carousel
<Checkbox>nsh_base::checkbox()带指示器的勾选 / 未勾选 / 半选控件Checkbox
<Collapsible>nsh_base::collapsible()内容可展开和收起的区域Collapsible
<Command>nsh_base::command()命令面板:在原生过滤的命令之上放一个搜索框,虚拟化列表,Cmd+K 面板的按键Command
<DataTable>nsh_base::data_table()行列数量不限的表格,可选择、排序,列可调整宽度和移动DataTable
<Dialog>nsh_base::dialog()把子元素呈现在其他一切之上Dialog
<DiffView>nsh_base::diff_view()两段文本之间或补丁中的改动,并排或合并显示DiffView
<DockArea>nsh_base::dock_area()由分栏、标签组和边缘停靠区组成、用户自行安排的工作区Dock
<Editor>nsh_base::editor()带高亮、多光标、折叠和搜索的代码编辑器Editor
<Input>nsh_base::input()原生文本输入框(UITextField 或 UITextView / EditText)Input
<HoverCard>nsh_base::hover_card()预览触发元素所指向的内容,在指针停留在它上面时显示HoverCard
<Link>nsh_base::link()打开页面、网站或应用内路由的按压目标Link
<Menu>、<ContextMenu>、<MenuItem>、<MenuSeparator>、<Submenu>nsh_base::menu()、…菜单:桌面上是带级联子菜单的弹出层,手机上是 sheetMenu
<NavStack>、<NavPage>nsh_base::nav_stack()、nsh_base::nav_page()原生导航:带有平台标题栏和返回手势的页面栈NavStack
<NumberInput>nsh_base::number_input()在输入框里输入、或用按钮和按键步进的数字NumberInput
<OtpInput>nsh_base::otp_input()固定位数的一次性验证码,输入、粘贴或自动填充到格子里OtpInput
<Pagination>nsh_base::pagination()一组内容的各页:上一页、当前页附近的页码、省略号和下一页Pagination
<Tree>nsh_base::tree()包含子项目的项目,可展开和收起,其中一个被选中:虚拟化渲染,在键盘焦点里只占一站Tree
<Plot>、<Line>、<Area>、<Bar>、<Grid>、<Pie>、<Arc>(nsh/base/plot)nsh_base::plot::{plot, line, area, bar, grid, pie, arc}图表的框架与图形,根据应用的数据行原生绘制Plot
<Popover>nsh_base::popover()锚定在触发元素上、显示在一切之上的内容Popover
<Progress>nsh_base::progress()进度条:任务完成了多少,或正在进行Progress
<Radio>、<RadioGroup>nsh_base::radio()、radio_group()一组只能选一个的选项中的一项,以及这一组Radio
<ResizablePanelGroup>、<ResizablePanel>nsh_base::h_resizable()、resizable_panel()并排或上下排列的面板,以及调整它们大小的手柄Resizable
<Scrollbar>nsh_base::scrollbar()为滚动视图或列表提供应用自己外观的滚动条Scrollbar
<Select>nsh_base::select()从一组条目中选出一个值,支持键盘、搜索和分组Select
<Sheet>nsh_base::sheet()位于屏幕一侧边缘、覆盖在背景层之上的面板Sheet
<Slider>nsh_base::slider()沿轨道选取一个值或两个值组成的范围Slider
<Switch>nsh_base::switch()带轨道和滑块的开 / 关控件Switch
<Table> 及其部件nsh_base::table()、table_row() 等由应用写出行和单元格的表格,带有表格的角色Table
<Tab>、<Tabs>nsh_base::tab()、tabs()标签列表中的一个标签页,以及这个列表Tabs
<TextView>nsh_base::text_view()用文本和视图显示 Markdown 或 HTML 文档TextView
<Toast>、<ToastStack>nsh_base::toast()、toast_stack()toast 的语义根;从一侧边缘排成层叠的 toastToast
<Toggle>、<ToggleGroup>nsh_base::toggle()、toggle_group()一个处于开或关的按钮,以及由它们组成的工具栏Toggle
<Toolbar>、<ToolbarGroup>、<ToolbarSeparator>nsh_base::toolbar()、toolbar_group()、toolbar_separator()由按钮、开关按钮和链接组成的一栏,在键盘焦点里只占一站Toolbar
<Tooltip>nsh_base::tooltip()悬停或手指按住时显示在触发元素旁的提示Tooltip

#通用属性

所有原语都接受下列属性(TSX 中为 CommonProps 类型;Rust 中为 Styled、Interactive 和 Accessible trait)。各元素页面只列出该元素自己的属性。Rust 一列给出对应的构建方法。

#样式

属性Rust类型说明
style有类型的 Styled 方法;.inline_style(style)StyleProp<ViewStyle>(<Text> 和 <Input> 为 TextStyle,<Image> 为 ImageStyle)内联样式,或从左到右合并的样式数组(假值项会被跳过;数组可以嵌套,所以 style={[base, props.style]} 中的 props.style 本身也可以是数组)。优先级高于 class。参见样式参考。
class有类型的 Styled 方法(.flex_row().gap_2().p_4())stringTailwind 类名,在原生侧解析("flex-row gap-2 p-4 dark:bg-zinc-900")。后面的类覆盖前面的类。参见 Tailwind。
classList.style_when(when, |s| ..)Record<string, boolean | null | undefined>值为真时才应用的类({ "opacity-50": busy() })。
disabled.disabled(..)boolean忽略按压(也没有按压反馈),应用禁用样式,禁用输入框等控件,屏幕阅读器也会读出它已停用(accessibilityState.disabled)。

#按压与反馈

按压处理在原生侧完成:由平台识别按压并应用应用设定的按压样式,应用代码只在事后收到通知。只有最内层可按压的视图会响应;在滚动视图内,按下会等待约 130 ms,让滚动有机会先开始。

属性Rust类型默认值说明
onPress.on_press(..)(e: PressEvent) => void在视图内完成一次点击。
onLongPress.on_long_press(..)(e: PressEvent) => void手指按住达到 delayLongPress。
onPressDown.on_press_down(..)(e: PressEvent) => void手指在视图上按下。
onPressUp.on_press_up(..)(e: PressEvent) => void手指抬起或移出。
delayLongPress.long_press_delay(ms)number500按压变为长按前的毫秒数。
hitSlop.hit_slop(EdgeInsets::all(px(8.0)))number | Partial<Edges>把触摸区域扩展到边界之外,单位为点。

PressEvent 为 { position, pagePosition, modifiers, clickCount }:指针在视图中和在其根视图中的位置(Point)、按压开始时按住的修饰键,以及连续点击的次数(双击为 2)。平台不报告指针时,它是视图的原点,没有修饰键,点击一次。

样式键 pointerEvents(Rust 中为 .pointer_events(PointerEvents::BoxNone),类为 pointer-events-*)决定视图及其中的视图接收哪些触摸:"none" 都不接收(其中设置了 "auto" 的视图会重新接收),"box-none" 只有其中的视图接收,"box-only" 只有视图本身接收(触摸与手势)。

按压反馈由应用决定:视图被按下时,activeStyle(或 active: 类,Rust 中为 .active(..))在原生侧生效,无需经过应用代码(见下文)。

#状态样式

视图处于某种状态时,在其自身样式之上、由原生侧应用的样式。每一个都是某个 Tailwind 变体的对象形式。后面的状态优先(selected、checked 与 indeterminate,然后是 hover、focus、active);disabledStyle 优先于所有状态,视图被禁用时其他状态样式不生效。每个状态样式的类型与元素自身的样式相同:<Text> 上是 TextStyle,<Input> 上是 InputStyle,<Image> 上是 ImageStyle,其他元素上是 ViewStyle。

属性Rust类变体生效时机
activeStyle.active(|s| ..)active:被按下时。
hoverStyle.hover(|s| ..)hover:指针悬停在视图上时。
focusStyle.focus(|s| ..)focus:视图获得焦点时。
focusWithinStyle.focus_within(|s| ..)focus-within:视图自身或其内部的视图获得焦点时(Overlay 的内容算在它所在的视图内部)。
disabledStyle.disabled_style(|s| ..)disabled:disabled 时。
checkedStyle.checked(|s| ..)、.checked_style(style)checked:accessibilityState.checked 为 true 时(复选框、开关、单选框;"mixed" 不算勾选)。
indeterminateStyle.indeterminate(|s| ..)、.indeterminate_style(style)indeterminate:accessibilityState.checked 为 "mixed" 时(既非勾选也非未勾选的复选框)。
selectedStyle.selected(|s| ..)selected:accessibilityState.selected 为 true 时(选中的 Button、标签页)。
expandedStyle.expanded(|s| ..)、.expanded_style(style)aria-expanded:accessibilityState.expanded 为 true 时(弹出层打开时 Popover、Menu 的触发器)。
pressedStyle.aria_pressed(|s| ..)、.pressed_style(style)aria-pressed:accessibilityState.pressed 为 true 时(处于开启状态的切换按钮;"mixed" 不算按下)。不同于 activeStyle(正在按压)。
view()
    .rounded_lg()
    .bg(rgb(0x2563eb))
    .px_4()
    .py_2()
    .active(|s| s.opacity(0.6))
    .hover(|s| s.bg(rgb(0x1d4ed8)))
    .on_press(save)
    .child(text("保存").text_color(white()))

#悬停

应用可以得知鼠标移到视图上、在视图上移动和离开视图:macOS、Windows、Linux、GPUI 后端和桌面浏览器都支持;iOS 上是 iPad 的指针或 Mac 的鼠标,Android 上是鼠标或手写笔。悬停样式(hover:、.hover(..))在同样的地方为悬停的视图设置样式。

属性Rust类型说明
onMouseEnter.on_mouse_enter(..)(e: MouseEvent) => void鼠标移到视图或其内部的视图上(DOM 的 mouseenter)。
onMouseMove.on_mouse_move(..)(e: MouseEvent) => void鼠标在视图(或其内部的视图)上移动,无论是否按着按键;或鼠标停在那里、没有按键按下时按下或松开了修饰键(DOM 的 mousemove)。
onMouseLeave.on_mouse_leave(..)(e: MouseEvent) => void鼠标离开视图及其内部的视图,或离开窗口;e 是鼠标最后所在的位置(DOM 的 mouseleave)。

与 CSS 的 :hover 和 DOM 的 mouseenter 一样,鼠标在视图上或在其内部任何内容上时,视图都算作被悬停,因此鼠标下方视图外层的视图也会收到 onMouseEnter(由外向内;onMouseLeave 由内向外)。事件在发生之后才通知应用,触摸从不触发。从鼠标下方移除的视图会收到 onMouseLeave,在 Linux 和 Windows 上要等到鼠标下一次移动。

MouseEvent 像 GPUI 的 MouseMoveEvent 一样给出鼠标的位置:position 是在视图中的位置(一个 Point,单位为点,已去掉视图的变换),pagePosition 是在视图所在根视图中的位置(与触摸相同),按住的 modifiers(control、alt、shift、platform:Apple 平台上是 Command,其他平台是 Windows 键),以及 pressedButton:按下的按键("left"、"right"、"middle"、"back" 或 "forward",即 GPUI 的 MouseButton),没有按键时为 null;Rust 中为 position、page_position、modifiers 和 pressed_button。在桌面平台和 Web 上,拖动同时也是触摸(onTouchMove、响应者系统),它的移动带着 pressedButton 到达 onMouseMove:只跟随悬停的反馈跳过这些移动即可;iOS 和 Android 的悬停报告的是没有按键按下的指针。移动事件每帧最多一次,总是最新的那次,并且一定在随后的 onMouseLeave 之前到达,因此处理函数可以在每次移动时重绘:

// 图表上的十字线。
let at = signal(None::<Points>);

view()
    .h(px(200.0))
    .on_mouse_move(move |e: &MouseEvent| at.set(Some(e.position.x)))
    .on_mouse_leave(move |_: &MouseEvent| at.set(None))
    .child(when(move || at.get().is_some(), move || {
        view()
            .absolute()
            .left(move || at.get().unwrap_or_default())
            .top_0()
            .bottom_0()
            .w(px(1.0))
            .bg(rgb(0x888888))
    }))

#工具提示

tooltip 为任意元素加上提示:在桌面平台(macOS、Windows、Linux、GPUI 后端和桌面浏览器)上,指针在元素上停留半秒后显示在它上方,指针离开或按下时隐藏;在任何设备上,键盘焦点停在元素上时也会显示,焦点离开或按下 Escape 时隐藏。提示从不接收指针或按压,所以元素的行为与没有它时完全一样。

属性Rust类型说明
tooltip.tooltip(..)JSX.Element提示,一个由应用设置样式的元素:nsh/base 的 Tooltip 就是工具提示本身,带有它的位置、延迟和事件;其他元素按原样显示。
view()
    .on_press(save)
    .tooltip(
        tooltip()
            .rounded_md()
            .bg(rgb(0x111827))
            .px_2()
            .py_1()
            .child(text("保存文档").text_color(white())),
    )
    .child(text("💾"))

Tooltip 决定自己的外观、位置、延迟和事件,与 GPUI 的 .tooltip(..) 相同。提示较多的应用可以写一个自己的小组件,把文字包进它的 Tooltip。

#上下文菜单

onContextMenu 告诉视图:鼠标的辅助按钮在它上面按下了——右键单击、macOS 上的 Control 单击、触控板的双指单击、iPad 指针的辅助点按;在浏览器里还有键盘的菜单键。在这里打开应用自己的菜单;在 TSX 中,Menu.popup 显示系统菜单:

// 应用自己的菜单,放在点击的位置。
let menu_at = signal(None::<Point<Points>>);

view()
    .on_context_menu(move |e: &ContextMenuEvent| menu_at.set(Some(e.page_position)))
    .child(text(row.name.clone()))
属性Rust类型说明
onContextMenu.on_context_menu(..)(e: ContextMenuEvent) => void辅助按钮在视图或其内部的视图上按下。

ContextMenuEvent 包含视图中的 position、根视图中的 pagePosition(Menu.popup 放置菜单的位置:Menu.popup(items, e.pagePosition))以及 modifiers;Rust 中为 position、page_position 和 modifiers。从指针下方的视图向外,最近的一个监听者收到事件。如果比它更近的是文本输入框,或带有编辑菜单的可选中文本(Windows、GPUI 后端、浏览器),则显示它们自己的菜单。平台在按钮按下时根据已挂载的内容做出决定,因此处理函数在事后得到通知,不需要取消任何东西。disabled 的视图把这次点击留给外层最近的、未停用的监听者。在触摸屏上用长按打开菜单:用同一个函数处理 onLongPress。

#键盘焦点

在 macOS 和 Web 上,Tab 按视图树的顺序在停靠点之间移动键盘焦点:输入框、可按压的视图,以及所有设置了 focusable 的视图。焦点样式(focus:、.focus(..))为获得焦点的视图设置样式;它可按压时,按 Return 或空格会按下它。点击会按下视图但不让它获得焦点,所以焦点样式只在键盘聚焦时生效。

焦点和按压相互独立。在获得焦点时接收按键的视图(图表、滑块、用方向键移动选中项的列表)设置 focusable,并在获得焦点期间绑定自己的按键(onFocus / onBlur 配合 keymap);点击它时,按压照样交给外层可按压的视图,比如一张点开详情页的卡片。

ref 的 focus() 像 Tab 一样把键盘焦点移到停靠点上(显示它的焦点样式并触发 onFocus),比如在弹层关闭后把焦点还给它的触发器;blur() 让焦点离开它。

焦点在某个视图自身或其内部任意视图上时,这个视图可以用 focus-within:(focusWithinStyle、.focus_within(..))设置样式:比如包住正在编辑的输入框的外框,或其中某个按钮持有焦点时的工具栏。Overlay 的内容无论显示在哪里,都算在 overlay 所在的视图内部,所以弹出层属于包含其触发元素的视图。在 Rust 中,组件用 ref 的 on_focus_in(..) 和 on_focus_out(..) 得知焦点进入、离开一个元素;焦点在元素内部的视图之间移动不算进入或离开。

属性Rust类型默认值说明
focusable.focusable(..)boolean可按压的视图为 true视图是否是 Tab 的停靠点。true 让任何视图成为停靠点,无论它自己接收按压,还是把按压留给外层视图;false 把可按压的视图留给指针(关闭浮层的背景层、用方向键切换的列表行)。
onFocus.on_focus(..)() => voidTab(或 focus())让视图获得了键盘焦点。
onBlur.on_blur(..)() => void键盘焦点离开了视图。
view()
    .rounded_lg()
    .p_3()
    .focus(|s| s.bg(rgb(0xe0f2fe)))
    .on_press(open)
    .child(text("打开"))

// 浮层的背景层:点击关闭浮层,Tab 直接越过它进入内容。
view().absolute().inset_0().focusable(false).on_press(close)

// 卡片里的图表:Tab 停在图表上,点击打开卡片的详情页。
view().on_press(open_page).child(
    view()
        .h(px(160.0))
        .focusable(true)
        .focus(|s| s.bg(rgb(0xe0f2fe)))
        .on_focus(move || focused.set(true))
        .on_blur(move || focused.set(false)),
)

#布局与标识

属性Rust类型说明
onLayout.on_layout(|frame: &Bounds<Points>| ..)(e: { bounds: Bounds }) => void每次布局改变视图位置尺寸后,bounds 给出相对父视图的 frame { origin, size }。
ref.element_ref(&handle)ViewRef | (node: ViewRef) => void元素的 ref(参见 ref 方法):ViewRef,或元素自己基于它的类型。ref={el} 赋值给变量,ref={fn} 调用函数。
nativeID.native_id(id)string为视图命名,供其他元素引用,例如输入框引用的 InputAccessoryView。
testID.test_id(id)stringUI 测试使用的标识(在 Android 上是视图的 resource id)。
key.key(key)string元素在兄弟元素之间的标识。

#驱动动画值的手势

这些属性把手势绑定到原生动画值,值在 UI 线程上变化,每帧都不经过应用代码。参见触摸与手势。

属性Rust类型说明
pan.pan_values(Some(&x), Some(&y)){ x?: AnimatedValue; y?: AnimatedValue }拖动驱动这些值(单位为点)。
onPanStart.on_pan_start(..)(e: PanEvent) => void拖动开始。
onPanEnd.on_pan_end(..)(e: PanEvent) => void拖动结束,带有松手时的速度(例如用来启动 decay)。
pinch.pinch_value(&scale){ scale?: AnimatedValue }双指捏合驱动 scale。
onPinchStart / onPinchEnd.on_pinch_start(..) / .on_pinch_end(..)(e: { scale: number; velocity: number }) => void捏合开始 / 结束。
rotation.rotation_value(&angle){ angle?: AnimatedValue }双指旋转驱动 angle(角度)。
onRotationStart / onRotationEnd.on_rotation_start(..) / .on_rotation_end(..)(e: { rotation: number; velocity: number }) => void旋转开始 / 结束。

PanEvent 为 { cancelled, translation, velocity, value? }(Rust 中为 cancelled、translation、velocity_x、velocity_y、value_x、value_y):系统是否取消了拖动、自开始以来的位移和以点每秒计的速度(Point),以及此刻所绑定值的数值,即 pan 绑定的轴的 { x?, y? }(拖动可能在动画中途抓住一个值)。

#原始触摸与响应者系统

对于上面属性覆盖不到的手势,每个元素都支持触摸与响应者属性: onTouchStart、onTouchMove、onTouchEnd、onTouchCancel(以及对应的 …Capture 形式)、 onStartShouldSetResponder、onMoveShouldSetResponder(以及 …Capture)、onResponderGrant、onResponderReject、 onResponderStart、onResponderMove、onResponderEnd、onResponderRelease、onResponderTerminate 和 onResponderTerminationRequest。每个处理函数都会收到一个 GestureResponderEvent ({ identifier, locationX, locationY, pageX, pageY, target, timestamp, touches, changedTouches, nativeEvent, stopPropagation() });…ShouldSetResponder 回调还会收到 gestureState(dx、dy、vx、vy 等)。 onResponderGrant 返回 true 可阻止原生滚动视图接管触摸。panActivation ({ activeOffsetX, activeOffsetY, failOffsetX, failOffsetY })让视图只在平台激活它的拖动后才成为响应者, 滚动视图里的拖动因此把另一个轴留给滚动视图(触摸与手势)。 PanResponder 中的 PanResponder 就构建在它们之上。在 Rust 中,.on_touch(..) 接收每次触摸(TouchEvent),.claim_touches(..) 决定视图何时接管触摸,.on_responder_grant(..)、.on_responder_move(..)、.on_responder_release(..) 等跟随响应者状态。

#无障碍

属性Rust类型说明
accessible.accessible(..)boolean视图对辅助技术而言是一个整体元素,子元素不再单独暴露。
accessibilityLabel.label(..)string屏幕阅读器朗读的内容。没有标签的视图会朗读其中的文本。
accessibilityHint.hint(..)string激活该元素会发生什么。
role.role(AccessibilityRole::Button)AccessibilityRole元素是什么("button"、"header"、"link"、"switch" 等)。
accessibilityState.accessibility_state(..){ disabled?, selected?, checked?, busy?, expanded?, pressed?, current? }辅助技术报告的状态(checked 和 pressed 可以是 "mixed";current 为 true 或 "page"、"step"、"location"、"date"、"time")。
accessibilityLevel.accessibility_level(..)number标题的级别(1 为最高),树节点的深度(顶层为 1)。
accessibilityValue.accessibility_value(..){ min?, max?, now?, text? }范围类或文本类元素的值。
accessibilityElementsHiddenboolean对辅助技术隐藏该视图及其子树。
importantForAccessibility"auto" | "yes" | "no" | "no-hide-descendants"是否暴露该视图。
accessibilityActions{ name: string; label?: string }[]提供给辅助技术的自定义操作。
onAccessibilityAction.on_accessibility_action(..)(e: { actionName: string }) => void用户触发了 accessibilityActions 中的某个操作。

参见无障碍。

#平台专属属性

在 TSX 中,只在某个平台上存在的属性放在以该平台命名的对象里,这样共享属性在各平台上保持一致。只有当前平台的对象会被发送,其他的会被忽略。Rust 的构建器以方法的形式提供它们,在各自的平台上生效。

属性说明
ios={{…}}iOS 和 Mac Catalyst。<Input>、Dialog 和 WebView 有各自的键。
android={{…}}Android。所有元素都支持 ripple、elevation 和 accessibilityLiveRegion(见下表)。
desktop={{…}}桌面平台和桌面浏览器;在移动端被忽略。所有元素都支持 windowDrag(见下表)。
web={{…}}浏览器;在其他平台被忽略。
android 键Rust类型说明
ripple.ripple(Ripple { .. }){ color?, borderless?, radius?, foreground? }按下时的 Material 水波纹。
elevation.elevation(px(2.0))number根据视图形状生成的 Material 阴影(单位为点)。
accessibilityLiveRegion"none" | "polite" | "assertive"视图变化如何播报给 TalkBack。
desktop 键Rust类型说明
windowDrag.window_drag(..)boolean按住视图拖动窗口,像按住标题栏一样;false 让可拖动视图中的一部分保持可点击。见自定义标题栏。
use nsh_core::Ripple;

view()
    .rounded_lg()
    .bg(white())
    .p_4()
    .elevation(px(2.0))
    .ripple(Ripple { color: Some(rgba(0x00000022).into()), ..Ripple::default() })
    .on_press(save)
    .child(text("保存"))

参见平台相关代码。

#ref 方法

在 TSX 中 ref 收到元素的 ref,类型是元素自己的:View、Text、Image、Canvas、Overlay 和 Shader 是 ViewRef(focus、blur、measure),其他元素是在 ViewRef 之上加上自身方法的类型:InputRef(Input)、ScrollRef(Scroll)、ListRef(List)、WebViewRef(WebView)、VideoRef(Video)和 NativeComponentRef(原生组件)。在 Rust 中,element_ref() 创建一个 ElementRef 句柄,由 .element_ref(&handle) 附加到元素上。它们的方法向原生视图发送命令,与同一轮中先前的改动按顺序执行。各元素页面会说明哪些方法适用。ref 变量请按元素自己的类型声明(<Input> 用 let field!: InputRef):声明为 ViewRef 的变量不能用在 ref 带有自己方法的元素上。

方法RustRef说明
focus() / blur()focus() / blur()ViewRef让 <Input> 获取焦点(弹出键盘),或让可按下的视图获得键盘焦点 / 失去焦点。
measure()measure()ViewRef视图的位置。
setSelection(start, end?)set_selection(start, end)InputRef选中 start..end(UTF-16 单位);省略 end 时为光标。
scrollTo({ x?, y?, animated? })scroll_to(point, animated)ScrollRef、ListRef滚动到某个偏移量。
zoomTo({ scale, x?, y?, animated? })zoom_to(scale, center, animated)ScrollRef(可缩放的 <Scroll>)围绕内容中的某点缩放到 scale。
scrollToOffset({ offset, animated? })scroll_to_offset(offset, animated)ListRef沿列表方向滚动到某个偏移量。
scrollToIndex({ index, animated?, viewPosition?, viewOffset? })scroll_to_index(index, position, offset, animated)ListRef把某一项滚动到可见区域。
scrollToEnd({ animated? })scroll_to_end(animated)ScrollRef、ListRef滚动到内容末尾。
reload()、goBack()、goForward()、stopLoading()、injectJavaScript(script)、postMessage(data)reload()、go_back()、go_forward()、stop_loading()、inject_java_script(..)、post_message(..)WebViewRef控制页面。
seek(seconds)seek(seconds)VideoRef跳转到某个时间点。
command(name, args?)command(name, args)NativeComponentRef调用已注册原生组件的命令(nativeComponent)。
let field = element_ref();

view()
    .on_press(move || field.focus())
    .child(input().element_ref(&field).placeholder("点击这一行开始输入"))

#为什么处理函数不会阻塞 UI

主线程从不等待应用代码。当系统需要立即得到答复时(这段文本能否修改、返回键能否返回上一页、这个 sheet 能否关闭),NSH 会根据事先设置好的属性(maxLength、popOnBack、closeOnSwipe 等)在原生侧作答,事件随后才送达应用。正因如此,这类行为都是属性,而不是处理函数的返回值。