Skip to content

API Reference

脚本接口的一份清单:有什么,以及它来自哪个模块。其余页面解释每样东西为什么是这个样子——这一页是用来查名字的。

权威不在这一页。runtime 会为自己的版本生成 gpui.d.ts,并在应用加载时尽力刷新到源码旁;gpui-shell types <directory> 执行同一次写入,并会明确报告失败。生成文件的头部带有 gpui-shell 版本,也包含该应用注册的 HostModule 。请忽略这个文件而不要提交,并在脚本顶部写上 // @ts-check 让编辑器照着它检查。manifest 声明的 Git 依赖同样不在这一页:它们由同一次刷新链接进 node_modules,名字、签名与文档都来自 package 自身。见依赖

模块

每个内建模块都以它所暴露的公开 Rust 层命名,所以一条 import 能说明脚本依赖哪一层。gpui 还包含从 JavaScript 使用 GPUI 所需的 shell 桥接: View、留存实体、调度与共享类型。一个名字只属于一个模块,这里不为了方便做 re-export。

js
import { View, div } from "gpui";
import { Button, v_flex } from "gpui-base";
import { fps_monitor } from "gpui-fps";
模块提供
gpuiGPUI 自己的元素,加上这个运行时补上的部分: View、样式接口与调度
gpui-base布局辅助函数、组件与主题
gpui-shellshell 桥接层自有的纯类型概念;没有运行时导出
gpui-fps性能 HUD

有两个名字从不需要 import,但原因不同。window 是真正的全局:没有谁把它交给你,它本来就在作用域里。cx 恰恰相反——它从来不是全局的,只会作为参数到达:render(cx)init(props, cx)、每个处理器的第二个参数、cx.spawn body 的形参。标准运行时模块——fs/promisespathcryptoprocessnetwebsocket 等等——受 Host 授权门控,记录在 Capabilities

API 形态跟随 Rust 原型:App 上的方法放在 cxWindow 上的方法放在 window 全局对象,关联构造器写成 Type.new(...),自由函数保持小写。没有直接 GPUI 或 Base 原型的名字,属于实现它的公开层。表中也会列出仅存在于类型系统的名字,但它们不是运行时可调用的值。

gpui 模块

元素

名称说明
Element通过链式方法构建、只属于当前 render pass 的描述
div()自身不带布局的元素
svg(path)来自应用根目录的矢量图,按周围的文字颜色着色
image(path)来自应用根目录的全彩图片,保留原色
PathBuilderGPUI 的路径构建器类型及其工厂;fill()stroke(width) 都返回一个 PathBuilder
Backgroundsolidstoplinear_gradientpattern_slashcheckerboard

PathBuilder.fill().stroke(width) 返回一个句柄,可链式调用 move_toline_tocurve_tocubic_bezier_toarc_toadd_polygonclosedash_array,最后以 build() 收尾。用 window.paint_path(path, background) 把结果画出来——它是唯一一个通过对象取到的元素构造器,因为它镜像的东西在 Rust 侧就是窗口上的一个方法。

字符串本身也是元素,和 GPUI 里 &str 实现 IntoElement 完全一样:.child("hello") 就是写文本的方式,样式来自持有它的那个元素。

View

名称说明
View每个 View 的基类;继承它,并把子类作为 default export
ViewClass一个具体的 View 子类,也就是 cx.new 接受的东西
Entity对一个嵌套 View 的留存所有权:set_props(props)release()

子类定义只执行一次的 init?(props, cx),以及返回一个 ElementEntity 或字符串、在 View 被置为失效时执行的 render(cx)。可选的 update(props) 在父 View 改变嵌套 View 的 props 时执行。

调度

名称说明
Task一个正在运行的任务:cancel()is_done()
Timerafter(ms, handler, opts?)every(ms, handler, opts?)

焦点

名称说明
FocusHandle脚本自己持有的焦点目标;它的成员

共享类型

名称说明
Length数字(像素)、"12px""1.5rem""50%""auto"
DefiniteLength同上,但不含 "auto"
AbsoluteLength只有像素或 rem
Axis"horizontal""vertical",镜像 gpui::Axis
Color一个 gpui-baseColorToken,或 #rgb / #rrggbb / #rrggbbaa 字面量
Role一个无障碍 role,镜像 gpui::Role 的 snake_case 拼写
Anchor锚定浮层的哪个角固定在它的触发元素上
MouseButton"left""right""middle"
ClickEventclick_countmodifiers
MouseMoveEventpositionlocal_positionboundsmodifiers
MouseButtonEventbuttonclick_countpositionmodifiers,以及元素绘制之后才有的局部几何
ScrollWheelEvent以像素表示的 delta;设备按行上报时还有 delta_lines;以及 touch_phase
KeyEventkeystroke(整个组合键,平台修饰键在所有平台上都拼作 cmd)、keykey_charmodifiersis_held
ActionEventaction——脚本给这个 action 起的名字
KeyBindingcx.bind_keys 的一项:keystrokeaction、可选的 context
Modifiersshiftcontrolaltplatform
Pointxy
Sizewidthheight
PathPathBuilder.build() 产出的不可变原生几何
BackgroundBackground.solid(...) 等工厂创建的可复用原生背景:opacity(factor)color_space(space)
BackgroundStop一个渐变色标,来自 Background.stop(color, percentage)

FocusHandle

cx.focus_handle() 创建,用 track_focus(handle) 交给元素,并用 release() 释放。

方法说明
focus(): void把键盘移到跟踪它的那个元素上
is_focused(): boolean那个元素当前是否持有键盘
release(): boolean释放句柄,并返回它当时是否还活着

gpui-shell 模块

这些是 JavaScript 桥接层自身引入的纯类型概念。它们只用于类型检查;这个模块没有运行时值。

名称说明
LengthStringshell 长度桥接接受的字符串形式
PathCoordinate像素,或所绘元素边界的百分比
Props跨 JavaScript View 桥接传递的属性包
ElementBoundsshell 事件使用的、带 widthheightPoint
ScopePhase"render""event""task""layout""none"
TaskOptions{ owner?: View | null }——任务随之取消的 View。默认是当前运行的 View;null 比任何 View 都活得久
DialogOptions{ escape_dismissable?: boolean, backdrop_dismissable?: boolean },两者默认都是 true
ToastOptions{ title: string, description?: string, level?: "info" | "success" | "warning" | "error", timeout?: number | null, id?: string }level 默认 "info"timeout 默认五秒,null 表示留到被关掉
MotionProperty"opacity""width""height""left""top"
MotionEasing"linear""ease-in""ease-out""ease-in-out"
TransitionPolicydurationdelayeasing
SpringPolicyresponsedampingepsilon

ScopePhase 描述当前 Context 属于哪一种 shell 调用。它和 GPUI 的 DispatchPhase 无关;后者控制事件分发时 capture 与 bubble 的顺序。

cx 上下文

两种 context 的成员相同,但生命周期不同。render 与事件处理器收到的 Context 只属于那次 Host 调用;把它留到调用之后,包括跨越 await,都会得到 stale-context 错误。下面的 AsyncContext 才是为跨越 await 准备的那一种。

成员说明
notify()请求重新渲染;在 render 期间抛异常,因为渲染中通知自己是一个死循环
bind_keys(bindings)安装键绑定并返回安装了几条;对应 App::bind_keys
stop_propagation()让这次事件不再向上传到外层的处理器;对应 App::stop_propagation
propagate()在同一次分发中撤销上面那一步;对应 App::propagate
phase()这次调用处于哪个 ScopePhase
theme()当前 gpui_base::Theme 的语义 token 投影
open_url(url)把一个绝对的 http/https URL 交给系统处理器
read_from_clipboard()剪贴板里的文本,没有文本时是 undefined
write_to_clipboard(text)替换剪贴板里的文本
focus_handle()一个新的 FocusHandle;属于 init 或事件处理器,绝不属于 render
new(Class, props?)创建一个留存的嵌套 View,并返回拥有它的 Entity
spawn(body, opts?)执行 body(cx) 并接管它返回的 promise,让 rejection 得到上报
sleep(ms?)在 GPUI 的 foreground executor 上,ms 之后 resolve
timerTimerafterevery

其中好几个都指名了它所镜像的 GPUI 方法:open_urlApp::open_urlread_from_clipboardwrite_to_clipboardApp::read_from_clipboardApp::write_to_clipboardfocus_handleApp::focus_handle(GPUI 没有 FocusHandle::new,这里同样没有),newAppContext::newspawnApp::spawn

AsyncContext

AsyncContext 继承 Context,不增加任何成员。区别在生命周期,不在接口:普通的 Context 只为一次 Host 调用发言,一旦那次调用返回就明确报错;而 AsyncContext 不指名任何一次调用——用到它时才解析当时正在执行的那一次,只有在一次都没有时才拒绝。它对应 GPUI 的 AsyncApp

有三处会交出一个:initcx.spawn 的 body,以及 cx.timer 的回调。这三处的职责正是「安排或延续比启动它的那次调用活得更久的工作」。

window 全局对象

这个全局对象的类型是 gpui 导出的 Window。调用处不需要 import,也没有谁把它交给你。每次调用都读取当前正在跑的那次 Host 调用,不在任何调用中时抛异常,所以没有句柄要持有,也没有东西会过期。浮层属于窗口,而不属于打开它的那个 View——这就是这些方法在这里、而不在 Context 上的原因。

成员说明
open_dialog(content, options?)打开一个 dialog,并返回栈的新深度
close_dialog()关闭最上层的 dialog,并回答有没有找到
close_all_dialogs()关闭所有 dialog,并回答关掉了几个
has_active_dialog()是否有 dialog 打开;与其余方法不同,它在 render 中合法
open_sheet(content)在右侧打开 sheet,替换掉原本在那里的内容
open_sheet_at(placement, content)同上,贴靠你指定的 gpui-base Placement
close_sheet()关闭 sheet,并回答原本有没有打开
has_active_sheet()sheet 是否打开;在 render 中合法
push_toast(options)弹出一个 toast,并返回它的 id
remove_toast(id)撤回一个 toast,并回答它当时是否还在显示
clear_toasts()撤回所有 toast,并回答撤回了几个
paint_path(path, background)用原生背景绘制不可变几何;对应 Window::paint_path
dispatch_action(action)沿本窗口的焦点路径派发一个 action;对应 Window::dispatch_action
rem_size() / line_height()窗口的排版度量,单位是像素
viewport_size() / bounds()可绘制区域,以及窗口在屏幕上的位置
mouse_position()指针位置,窗口坐标
appearance()"light""dark"
is_window_active() / is_fullscreen() / is_maximized()平台窗口的状态
set_rem_size(size)重新缩放所有以 rem 表达的尺寸
refresh()重绘窗口里的每一个 View
focus_next() / focus_prev()把键盘移到相邻的一个 tab stop
activate_window() / minimize_window() / zoom_window() / toggle_fullscreen()平台窗口控制
localStorageWeb Storage,背后是 Host 放好的一个文件,跨重启存活
sessionStorageWeb Storage,只在内存里,随进程一起消失

上面这些度量——从 rem_size() 一直到 is_maximized()——在 render 中都是合法的:一个要按窗口尺寸决定自身大小的 View,只能在绘制它的那一趟里问。而所有改变窗口的调用在 render 中都会被拒绝,理由和 cx.notify() 一样:一帧去改自己正在绘制的窗口,就是这一帧在和自己较劲。

open_dialogopen_sheetopen_sheet_at 接受的是一个返回元素的函数,而不是元素:dialog 活得比打开它的那次调用久,每次重绘时这个函数都会再执行一次。除了两个 has_active_* 查询与 paint_path,这里的一切在 render 中都不合法。见 Overlays

存储

Web Storage API,原样照搬。两个 store 同时也是裸的全局变量——localStorage.getItem(k)window.localStorage.getItem(k) 是同一次调用——因为在浏览器里也是如此。

成员说明
length已存的键数量
key(index)该位置上的键,越界为 null
getItem(key)值,键不存在时为 null
setItem(key, value)存入,值会被转成字符串
removeItem(key)忘掉一个键
clear()全部忘掉
flush()写入落盘后 resolve

值是字符串,所以有结构的东西照 web 上的写法走 JSON.stringifyJSON.parseflush() 是唯一多出来的成员:浏览器不需要它,因为它的存储从头到尾都是同步的。localStorage 受 capability 管辖,Host 没授权时抛异常;sessionStorage 不受管辖,因为它持有的东西从不离开进程。见 Capabilities

gpui-base 模块

这里的组件拥有行为、焦点,以及屏幕阅读器听到的内容,而自身几乎什么都不画。画面归脚本所有,用样式接口写出来。每个名字都链接到它在 gpui-base 文档里的页面,那里描述了它完整的 Rust 接口与行为。

布局

名称说明
h_flex()一行
v_flex()一列
h_resizable(id)一行带可拖拽分隔条的窗格;尺寸按这个 id 存在窗口里
v_resizable(id)同上,纵向堆叠
resizable_panel()可调整组里的一个窗格,用在别处都不合法

控件

名称说明
Button激活、焦点、disabled 与 selected 状态
Link通过系统浏览器打开的外部 HTTP(S) 资源
Checkbox受控的勾选;勾选标记自己画
Switch受控的 switch
Radio一组中的一个选项;只报告 true,从不报告取消选中
Toggle一个会保持按下的按钮
RadioGroup被报读为一组的一批 radio;自身不持有选中项
ToggleGroup被报读为 toolbar 的一批 toggle
Tabs自身不持有选中项的 tab 列表
Tab一个 tab:selected(...) 进,on_click(...)
Progress只有报读,没有进度条;单独的 Progress.new(...) 什么都不画
ProgressTrack凹槽:一个由你设定尺寸与颜色的普通元素
ProgressIndicator已填充的部分;按你报读的百分比设置它的宽度
Avatar渲染它的 image 槽;没有图片时渲染 fallback。它自己不画圆形、尺寸或背景
AvatarImage图片槽:AvatarImage.new(path),用在别处无效
AvatarFallback兜底槽:一个普通盒子,放首字母、图形或 svg
Pagination一个 navigation landmark,带报读的标签;页码按钮由脚本自己画
pagination_items(current, total, visible?)该画哪些页码、省略号落在哪。visible 默认 7,最小 5;总页数 ≤ 1 时返回空
Accordion一个 group,装 item
AccordionItem一个条目:open(...) 进,trigger 的 on_change(...) 出;它把自己的 open 传给下面两半
AccordionHeader标题:AccordionHeader.new(trigger)aria_level(n) 报读层级(默认 3)
AccordionPanel展开的区域。关闭时不在树里,除非 keep_mounted(true)
AccordionTrigger按钮:报读展开状态,on_change 请求相反的那个
CalendarState留存的日历状态:月网格、当前月份、选中的日期
SliderState留存的 slider 状态,也是一次拖拽写入的地方
Slider根:报读数值,并拥有 release
SliderTrack按下与拖拽的表面
SliderIndicator凹槽,也是每个指针位置据以测量的那个盒子
SliderThumb滑块;shell 给它位置,你给它外观

slider 的四个部件接受同一个 SliderState,而且四个都不能少——没有 SliderIndicator 的 slider 根本拖不动。

文本编辑

名称说明
InputState留存的文本状态:InputState.new({ placeholder, value })
Input包住留存文本状态的框
NumberInput建立在同一个 InputState 上的 spinbutton,三个插槽都有分量
TextareaState留存的多行文本状态;rows 是一个选项
Textarea包住留存多行状态的框
OtpState留存的一次性验证码状态;长度在创建时固定
OtpInput定长验证码,格子由 shell 画、由脚本设定样式

没有专门的数字状态类型:给 InputState 设上 set_stepset_minset_max,它就成了数字状态。

容器与浮层

名称说明
Collapsible仅在 open 时渲染它的 content 插槽;不带 role、箭头或触发器
Popover锚定在触发元素上、由按下打开的浮层
HoverCard同上,但由指针停留打开,并有自己的打开状态
Popup光秃秃的锚定浮层:Popup.new(id, trigger),填入 content 即打开
Selectcombobox 的根:role、报读的打开状态、键盘——但不含任何画面
Combobox同一个根,被报读为一个触发器是可编辑输入框的 combobox
DatePicker日期选择器的根:DatePicker.new(id, focus_handle);它不持有日期

在这些之上动手之前,有两处缺口值得先知道:打开的 SelectCombobox 列表的方向键导航要你自己接(零件都在,见下),而 Enter 与 Escape 到不了 DatePicker。两者都写在各自类型的声明里,也就是它们真正咬人的地方。

表格与列表

名称说明
Table语义表格的根,组合方式与 HTML 组合表格一致
TableHeader表头行组
TableBody表体行组
TableRow一行:.new(id, row_index),从 1 开始
TableHead一个列头:.new(id, column_index),从 1 开始
TableCell一个数据单元格:.new(id, column_index),从 1 开始
TableCaptioncaption 该在的视觉位置;它不带 caption role
v_virtual_list(…)只描述屏幕内内容的纵向列表
h_virtual_list(…)另一个轴上的同一件事;item_sizes 是宽度
VirtualListScrollHandle虚拟列表的滚动位置,跨帧保留
Scrollbarnew(id)horizontal(id)vertical(id)——一条由你自己摆放的滚动条

两种虚拟列表都接受 (id, item_count, item_sizes, get_key, render)render(range, cx) 是这套接口里唯一由 Host 在一帧进行中调用的回调,所以在它内部注册处理器、创建留存状态与调用 cx.notify() 都会被拒绝。

Dock

名称是什么
DockArea.new(id, options?)一个可停靠布局,retained;options{ version?: number }
DockArea.register_panel(name, Class)教会运行时用 Class 重建 name 这块面板;返回加了命名空间的名字
dock_area(area)画出它,并承载六个 chrome handler
dock_content()一侧 dock 自己的面板,在你画的 chrome 里应该出现的位置

area 上的方法是 add_panel(view, options)remove_panel(id)panels()dump()load(state)has_dockis_dock_opentoggle_dockremove_dockdock_sizeset_dock_sizeset_dock_collapsibleis_lockedset_lockedis_zoomedzoom_outon("layout_changed", handler)release()

每一次编辑都在发起它的那次调用返回之后按调用顺序应用——面板的主体来自 cx.new(Class),那时它自己还在构造中——所以 panels()dump() 读到的是本轮编辑之前的布局。见 Dock 与面板

留存句柄

每一个都只创建一次——在 init 或事件处理器里,绝不在 render 里——并且每一个都有 release(): boolean,返回它当时是否还活着。释放之后再用会抛异常。

on(...) 是替换该事件的处理器,而不是再加一个,返回值表示之前是否已经有一个。

InputState

来自 InputState.new(options?),其中 options{ placeholder?: string, value?: string }

方法说明
value(): string当前文本
set_value(next: string): void替换它
on(event, handler): booleanevent"change""submit""focus""blur";handler 收 (event, cx)
set_step(step: number | null): voidNumberInput 的步长,null 表示没有
set_min(min: number | null): void数值下界,或 null
set_max(max: number | null): void数值上界,或 null
set_masked(masked: boolean): void文本是否按密码绘制
set_loading(loading: boolean): void是否显示加载状态

TextareaState

来自 TextareaState.new(options?),其中 options{ placeholder?: string, value?: string, rows?: number }

方法说明
value(): string当前文本
set_value(next: string): void替换它
on(event, handler): boolean"change""submit""focus""blur",handler 收 (event, cx)
set_rows(rows: number): void可见行数
set_auto_grow(min_rows: number, max_rows: number): void在这两者之间随内容增高
set_soft_wrap(wrap: boolean): void长行是否折行

SliderState

来自 SliderState.new(options?),其中 options{ min?, max?, step?, scale?: "linear" | "logarithmic", value?: SliderValue }。默认是 0..100、步长 1、从 min 起。"logarithmic" 需要 min 大于零。

方法说明
value(): SliderValue当前值:一个数字,区间滑块则是 [start, end]
set_value(next: SliderValue): void替换它
min_value(): number创建时的下界
max_value(): number上界
step_value(): number步长
on(event, handler): boolean拖动中的 "change" 或结束时的 "release";handler 收 (value, cx)

OtpState

来自 OtpState.new(length, options?),其中 options{ value?: string, masked?: boolean }。长度在创建时就固定了。

方法说明
value(): string目前已输入的数字
set_value(next: string): void替换它们
len(): number它持有几位
is_masked(): boolean是否遮蔽绘制
set_masked(masked: boolean): void改变这一点
focus(): void把键盘移进去
on(event, handler): boolean每次编辑后的 "change"、填满时的 "complete",或 "focus" / "blur";handler 收 (event, cx)

VirtualListScrollHandle

来自 VirtualListScrollHandle.new(),用 track_scroll(handle) 交给列表。

方法说明
scroll_to_item(index: number, strategy?): void在下一帧之前把某一项带到屏幕上;strategy"top"(默认)或 "center"
scroll_to_bottom(): void滚到末尾

日历

CalendarState 存在的理由是 month_days()——哪些日期落在哪一周、相邻月份的日子补在哪里、这个月需要几行。格子由脚本自己画。

js
const grid = this.calendar.month_days()[0];
v_flex().children(grid.map((week) =>
  h_flex().children(week.map((day) =>
    Button.new(day)
      .selected(day === this.calendar.value())
      .on_click((_, cx) => { this.calendar.set_value(day); cx.notify(); })
      .child(String(Number(day.slice(8)))),
  )),
));

base 的 Calendar 元素没有绑定,这是个决定而不是遗漏:它遍历同一份网格,每个格子调用一次渲染回调——一帧最多四十二次跨语言调用,而且发生在 GPUI 的 layout 过程里,为的是一批本身不带任何行为的格子。在这里读到网格自己画,是同样的活,少了那四十二次穿越。

日期一律是 "YYYY-MM-DD":按文本排序即是按时间排序,new Date(s) 能直接读——需要星期名或本地化月份名时用它。

方法说明
month_days()网格:按月分组的“周”,每周固定七天,首尾两周带相邻月份的日子
year() / month()网格对应的年份与月份(1–12)
today()状态创建时读到的今天
value() / set_value(next)选中的日期:一天、[start, end] 区间,或 null
next_month() / prev_month()把网格前后移一个月;在 render 中不合法
on("change", handler)唯一的事件,报告一个日期被选中

主题

名称说明
set_theme(theme)用应用自己的主题替换 gpui-base 当前生效的语义 token
ColorToken已安装调色板定义的语义颜色名称
Themecx.theme() 返回的东西:语义 token,加上 appearanceis_dark
SemanticThemeTokenscolorsspacingradius
ColorTokens每个语义角色一个 Color
SpacingTokensxxs xs sm md lg xl xxl
RadiusTokensnone sm md lg xl full

读主题用 cx.theme()set_theme 留在 gpui-base,因为主题属于这一层;但修改仍然要求当前存在一次 Host 调用,只能从事件处理器或 task 调用,不能在 render 或 layout 中调用。

其他类型

名称说明
ScrollbarMode"scrolling""hover""always"
ItemRange虚拟列表的可见项,写作半开区间 [start, end)
SliderValue一个数字,或区间 slider 的 [start, end]
InputEvent文本状态的事件 payload;submit 事件带可选的 secondaryshift 标志
OtpEvent当前为空的 OTP 事件 payload;值从 OtpState 读取
PartTypegpui-base 中没有自身身份的子部件共同使用的 new() 形态
Placement"top""bottom""left""right",镜像 gpui_base::Placement
ComponentTypegpui-base 中带身份的组件构造器共同使用的 new(id) 形态
DockPlacement"center""left""right""bottom"
DockPanelpanels() 报告的一块面板:idnameplacementnodeindexactive 与三个标志位
DockGroup / DockTab一个标签组与它的一个标签页,也就是 tab_barempty_group 拿到的东西
DockRegion一侧 dock,也就是 dock handler 拿到的东西
DockTile一个 tile,bounds 已经解析好
DockDrop被拖动的面板会落在哪里
TileResizeSide"left""right""top""bottom""bottom_right"

组合模式

其中五个组件不是一个元素,而是一种搭法,上面的表格说不出这件事。下面每段都是能跑起来的最小写法,并且都经过运行时校验。

受控控件。 CheckboxSwitchRadioToggle 自身不持有状态:值由你读进去、再写回来。它们什么都不画,所以勾选标记是一个子元素。

js
Checkbox.new("done")
  .checked(this.checked)
  .on_change((checked, cx) => {
    this.checked = checked;
    cx.notify();
  })
  .child(this.checked ? "done" : "not done");

Progress 负责报读,进度条是你的。 root 带的是 role 和屏幕阅读器要念的 0..=100,它自己什么都不画。

js
Progress.new("upload")
  .value(62)
  .child(
    ProgressTrack.new().w(200).h(6).bg(cx.theme().colors.muted)
      .child(ProgressIndicator.new().w(124).h(6).bg(cx.theme().colors.primary)),
  );

滑块是四个部件,四个都不能少——没有 SliderIndicator 的滑块根本拖不动,因为每一个指针位置都是相对它的盒子测量的。四个部件收的是同一份状态。

js
Slider.new(this.volume).child(
  SliderTrack.new(this.volume).w(200).h(16)
    .child(SliderIndicator.new(this.volume).h(4).bg(cx.theme().colors.primary))
    .child(SliderThumb.new(this.volume).w(12).h(12).bg(cx.theme().colors.background)),
);

Select 管键盘,Popup 管那块面。 root 持有 combobox 的语义与展开状态;列表是放在它里面的一个 Popup。它需要两个 focus handle——一个给触发元素,一个给内容——没有第一个,屏幕上就没有任何东西持有键盘。

js
Select.new("mode")
  .accessibility_label("Mode")
  .open(this.open)
  .track_focus(this.trigger)
  .content_focus_handle(this.list)
  .on_open_change((open, cx) => { this.open = open; cx.notify(); })
  .child(
    Popup.new("mode-list", trigger).anchor("bottom_left")
      .when(this.open, (el) => el.content(list)),
  );

展开后用方向键移动高亮这件事要你自己写:base 期待里面的东西用自己的按键绑定来跑高亮,而它不会替你跑。零件都在——把 on_key_down 放在键盘被移过去的那个 content 元素上,自己移动高亮;或者在自己的 key_context 下把 ↑ / ↓ 绑到 action。开箱状态是:指针可用,Escape 关闭,Enter 与 ↓ 展开,高亮不动。

虚拟列表和它的滚动条按名字配对。 列表自己不画滚动条,而且配对在运行前不做任何校验,所以两半都要写。

js
v_flex().relative().h(200)
  .child(
    v_virtual_list("rows", rows.length, 28,
      (index) => rows[index].id,
      (range) => rows.slice(range.start, range.end).map((row) => div().child(row.name)),
    ).size_full(),
  )
  .child(Scrollbar.vertical("rows").absolute().inset_0());

嵌套 View 创建一次,然后作为子元素挂上。 cx.new 属于 init 或事件处理器;实体在任何接受子元素的位置都能当子元素。

js
init(props, cx) {
  this.chart = cx.new(PriceChart, { symbol });
}
render() {
  return v_flex().child(this.chart);
}

gpui-fps 模块

名称说明
fps_monitor()原生 gpui-fps HUD,每个窗口共享一个,固定在右上角

它的父元素必须设置 relative()。HUD 自己拥有完整外观;普通样式与子元素对它不起作用。

元素方法

所有元素共享同一个 prototype,所以下面每个方法在任何元素上都能通过类型检查——某个方法实际适合哪个组件,类型并不表达。交给一个不承接它的组件的行为 builder 会被写进日志,而不是被悄悄丢掉。

元素 builder 方法都返回同一个元素,所以一条链就是一个表达式。map 是例外:与 GPUI 的 FluentBuilder.map 一样,它原样返回回调的结果。元素被用作子元素时即被消费,并且属于构建它的那一趟渲染。

组合

方法作用
map(transform)把当前元素交给 transform,并返回其结果;对应 GPUI 的 fluent builder helper
child(value)添加一个子元素:元素、Entity,或字符串、数字、布尔值
children(iterable)按顺序添加多个
when(condition, branch)condition 为真时应用 branch,让链保持完整
id(name)给这个元素一个稳定的名字,作为它的身份

插槽

插槽不是子元素:元素被组件消费,渲染在组件决定的位置。

方法作用
content(element)CollapsiblePopoverHoverCardPopup 的内容
image(element)Avatar 的图片槽,接受一个 AvatarImage
fallback(element)Avatar 的兜底槽,接受一个 AvatarFallback
header(element)AccordionItem 的 header 槽,接受一个 AccordionHeader
panel(element)AccordionItem 的 panel 槽,接受一个 AccordionPanel
trigger(element)PopoverHoverCard 的触发器
input(element)NumberInput 的编辑器插槽;留空则画出裸编辑器
decrement_button(element)NumberInput 减少按钮的外观——重放到 base 的按钮上,而不是直接渲染
increment_button(element)增加按钮,重放方式相同
controls_right()把两个步进按钮叠放在文本右侧

事件

方法交付什么
on_click(handler)激活时的 (ClickEvent, cx)
on_mouse_move(handler)指针悬停在元素上时的 (MouseMoveEvent, cx)
on_hover(handler)指针进入与离开时的 (hovered, cx)
on_key_down(handler)该元素持有键盘时按下按键的 (KeyEvent, cx)
on_key_up(handler)同一条焦点路径上松开按键的 (KeyEvent, cx)
on_mouse_down(button, handler)按下该按钮时的 (MouseButtonEvent, cx)
on_mouse_up(button, handler)松开时的 (MouseButtonEvent, cx)
on_mouse_down_out(handler)在该元素之外任意位置按下时的 (MouseButtonEvent, cx)
on_scroll_wheel(handler)滚轮或触控板滚动时的 (ScrollWheelEvent, cx)
on_action(action, handler)该命名 action 被派发到此元素或其内部时的 (ActionEvent, cx)
on_change(handler)开关变化时的 (checked, cx);新值由脚本保存
on_step(handler)("increment" | "decrement", cx),并且它会取代内置的步进
on_item_click(handler)虚拟列表某一行被点击时的 (key, cx),按 key 而不是按下标
on_open_change(handler)脚本之外的东西改变了 Popover 的打开状态时的 (open, cx)
on_confirm(handler)在打开的 SelectCombobox 中按下回车;无参数
on_dismiss(handler)在打开的 SelectCombobox 中按下 Escape,早于 on_open_change(false)
on_resize(handler)可调整组的拖拽结束后的 (sizes, cx)

Actions 与键绑定

一个 action 是比按键高一层的东西。cx.bind_keys 说哪个组合键在什么上下文里意味着 "save",元素上的 on_action("save", ...)"save" 做什么;菜单项或工具栏按钮用 window.dispatch_action("save") 派发同一个名字,就能走到同一个处理器,而两边都不必知道对方存在。

js
init(_props, cx) {
  cx.bind_keys([{ keystroke: "cmd-s", action: "save", context: "Editor" }]);
}

render(_cx) {
  return div()
    .key_context("Editor")
    .track_focus(this.handle)
    .on_action("save", (event, cx) => this.save(cx));
}

context 是一个匹配元素 key_context(...) 的谓词表达式,所以同一个组合键可以在列表里是一个意思、在编辑器里是另一个意思。同一个元素上注册多个 on_action 是可以的,彼此独立;一个它们都没认领的 action 会继续往外层元素传。

上面这组——on_key_downon_key_up、四个指针事件、on_actionkey_context——接线在 divh_flexv_flexButtonLinkCheckboxSwitchRadioToggleTabsTab 上。写在其余组件上的处理器会被记录下来但永远到不了 GPUI,日志里会说明——把它包一层,写在外层元素上。

接线了不等于收得到。按键沿焦点路径传递,所以一个不接受脚本焦点句柄的组件——比如 Tab——听得到按下、永远听不到按键,无论两者接线得多好。

控件状态

方法设置什么
disabled(value)阻止激活并报告该状态;外观自己画
selected(value)Button 的 selected 状态
checked(value)CheckboxSwitchRadio 的受控值
pressed(value)Toggle 的受控状态
value(percent)报读的进度百分比,钳制在 0..=100;它不会让屏幕上任何东西移动
indeterminate(value)Progress 的数值从无障碍树里撤下
open(value)Collapsible 是否渲染内容,或浮层是否正在显示
default_open(value)非受控的 Popover 是否以打开状态开始
keep_mounted(value)关闭的 AccordionPanel 是否留在树里。默认关;开启后它的内容能跨越一次关闭保住滚动位置或半填的输入
start(value)SliderThumb 是区间 slider 的哪一个滑块
href(url)Link 的绝对 HTTP(S) 目标

无障碍

方法报读什么
accessibility_label(text)屏幕阅读器读出的内容;纯图标控件没有它就什么都不会被读出
role(name)这个元素把自己报读成什么——仅限朴素元素、ButtonCheckbox
aria_selected(value)脚本自己搭的列表里某一项的选中状态
aria_active_descendant()在祖先持有键盘时,把本元素报读为当前焦点项
set_position(position, size)从 1 开始的位置与总数——“第 2 个 tab,共 5 个”
row_count(count)Table 的总行数,包含未渲染的行
column_count(count)Table 的总列数
aria_level(level)AccordionHeader 报读的标题层级,默认 3;只报读,不改字号
axis(value)RadioGroupToggleGroup 的方向;只有语义,不做任何布局
tooltip(text)只对指针有效的悬停说明,不能替代 accessibility_label

焦点与键盘

方法作用
track_focus(handle)让这个元素成为该 handle 所指的对象
content_focus_handle(handle)SelectCombobox 打开时把键盘移到哪里
tab_index(index)这个元素在 Tab 顺序中的位置;同时也把它变成一个 tab stop
tab_stop(value)Tab 能否落到这里,不改变它在顺序中的位置

滚动与面板

方法作用
overflow_scroll()接管双轴的滚轮与触控滚动
overflow_x_scroll() / overflow_y_scroll()单轴上的同一件事
overflow_scrollbar()双轴滚动并绘制基础层的滚动条
overflow_x_scrollbar() / overflow_y_scrollbar()单轴上的同一件事
mode(value)Scrollbar 的显示策略;不写则跟随主题
scroll_size(width, height)Scrollbar 据以计算滑块的内容尺寸
viewport_from_layout()Scrollbar 从自身的盒子取 viewport
track_scroll(handle)给虚拟列表一个脚本可以驱动的滚动位置
with_item_to_measure_index(index)虚拟列表在它滚动的那个轴上测量哪一项
size_range(min, max?)resizable_panel() 可被拖拽的范围,单位为像素

锚定浮层

方法设置什么
anchor(value)哪个角固定在触发元素上;无论怎样都会被钳进窗口
mouse_button(value)哪个指针按键打开 Popover
open_delay(ms)指针要在 HoverCard 触发器上停留多久;默认 600
close_delay(ms)HoverCard 关闭前等待多久;默认 300
overlay_closable(value)在打开的 Popover 之外按下是否将其关闭

Dock 命令

dock 的 chrome 画出来的元素做什么。缓存的 chrome 描述没有脚本事件处理器的生命周期,所以其中不能注册事件处理器——取而代之的是不携带任何脚本值的命令,由 base 完成实际动作。每一个的第一个参数都是它所在 handler 拿到的那个对象;它们只能挂在 divh_flexv_flex 上。

方法触发作用
select_tab(group, index)点击显示那个标签页
close_panel(group, panel_id)点击关闭该面板(如果它所在的 group 允许)
toggle_zoom(group)点击放大 group,或还原
drag_tab(group, index)拖动让该元素成为这个标签页的拖动源
drop_tab(group, index?)放下在此接收被拖来的面板;不给 index 就追加到末尾
toggle_dock(dock)点击展开或收起这侧 dock
resize_dock(dock)拖动拖动 dock 的边;每个位置都由 base 钳制
move_tile(tile)拖动在画布上移动这个 tile
resize_tile(tile, side)拖动拖动某条边或某个角
raise_tile(tile)按下把这个 tile 提到最上层
toggle_tile_zoom(tile)点击让 tile 放大占满所在 dock
close_tile(tile)点击关闭这个 tile

Dock chrome

六个 handler,全都可选,且只能挂在 dock_area(...) 上。每一个都会先在 GPUI 的 layout pass 内部被调用,拿到的是 base 已经解析好的状态;描述会缓存到该状态或 handler 改变为止。

方法画什么
tab_bar(handler)一个 group 当前显示面板上方的标签栏
empty_group(handler)没有可显示面板的 group 显示什么
drop_indicator(handler)被拖动的面板会落在哪里
dock(handler)一侧 dock 包住内容的外框;把 dock_content() 放进去
tile_drag_bar(handler)拖动 tile 用的那条拖拽条
tile_resize_handles(handler)tile 的缩放把手

动效

方法作用
transition(property, policy)完全在原生 GPUI 代码里,对之后的目标变化做动画
spring(property, policy?)改用弹簧

property 取 "opacity""width""height""left""top" 之一,每一帧都不会进入 JavaScript。

样式模板

每一个都接受一个函数,函数收到一个游离的元素用来收集样式;返回值会被忽略,所以写成一条链或写成块状函数体都可以。

方法作用于什么
hover(declare)指针悬停在元素上时
active(declare)元素被按下时
focus(declare)元素持有焦点时
range_style(declare)SliderIndicator 已填充的部分——只管它长什么样,从不管它在哪里
cell_style(declare)OtpInput 的每个格子;没有它屏幕上什么都没有
cell_active_style(declare)叠在上面一层,用于下一个数字将落入的那个格子
caret_style(declare)那个格子为空时,里面闪烁的光标

样式方法

元素上其余的一切都是样式。它们分成两族,而且从不重叠:

  • 带参数的方法,手工绑定:size、padding、margin、position、flex、border、radius 与 paint 各族。每个方法接受哪种长度类型跟随它的 Rust 签名,所以 .p("auto") 是类型错误,理由与它在运行时抛异常完全相同。
  • 无参方法,从 GPUI 的反射表生成:flex_colitems_centergap_2rounded_mdtext_smsize_fulltruncate 以及这一族的其余成员。生成的声明就是当前构建所用 GPUI 版本的完整清单。

两者都记录在 Styling 里,还有长度与颜色的语法,以及调色板定义的 token。

HostModule

Host 在 Rust 侧注册的模块,按名字 import,和其它模块没有区别:

js
import { quotes } from "market";

它不属于任何内建模块。生成的类型声明里每个注册过的模块各有一段 declare module,所以模块名和每一个导出名都会被检查。见 HostModule