--- url: /docs/components/accordion.md description: The accordion uses collapse internally to make it collapsible. --- # Accordion An accordion component that allows users to show and hide sections of content. It uses collapse functionality internally to create collapsible panels. ## Import ```rust use gpui_kit::component::accordion::Accordion; ``` ## Usage ### Basic Accordion ```rust Accordion::new("my-accordion") .item(|item| { item.title("Section 1") .child("Content for section 1") }) .item(|item| { item.title("Section 2") .child("Content for section 2") }) .item(|item| { item.title("Section 3") .child("Content for section 3") }) ``` ### Multiple Open Items By default, only one accordion item can be open at a time. Use `multiple()` to allow multiple items to be open: ```rust Accordion::new("my-accordion") .multiple(true) .item(|item| item.title("Section 1").child("Content 1")) .item(|item| item.title("Section 2").child("Content 2")) ``` ### With Borders ```rust Accordion::new("my-accordion") .bordered(true) .item(|item| item.title("Section 1").child("Content 1")) ``` ### Different Sizes ```rust use gpui_kit::component::{Sizable as _, Size}; Accordion::new("my-accordion") .small() .item(|item| item.title("Small Section").child("Content")) Accordion::new("my-accordion") .large() .item(|item| item.title("Large Section").child("Content")) ``` ### Handle Toggle Events ```rust Accordion::new("my-accordion") .on_toggle_click(|open_indices, window, cx| { println!("Open items: {:?}", open_indices); }) .item(|item| item.title("Section 1").child("Content 1")) ``` ### Disabled State ```rust Accordion::new("my-accordion") .disabled(true) .item(|item| item.title("Disabled Section").child("Content")) ``` ## API Reference * [Accordion] * [AccordionItem] ### Sizing Implements [Sizable] trait: * `small()` - Small size * `medium()` - Medium size (default) * `large()` - Large size * `xsmall()` - Extra small size ## Examples ### With Custom Icons ```rust Accordion::new("my-accordion") .item(|item| { item.title( h_flex() .gap_2() .child(Icon::new(IconName::Settings)) .child("Settings") ) .child("Settings content here") }) ``` ### Nested Accordions ```rust Accordion::new("outer") .item(|item| { item.title("Parent Section") .child( Accordion::new("inner") .item(|item| item.title("Child 1").child("Content")) .item(|item| item.title("Child 2").child("Content")) ) }) ``` [Accordion]: https://docs.rs/gpui-component/latest/gpui_component/accordion/struct.Accordion.html [AccordionItem]: https://docs.rs/gpui-component/latest/gpui_component/accordion/struct.AccordionItem.html [Sizable]: https://docs.rs/gpui-component/latest/gpui_component/trait.Sizable.html --- --- url: /zh-CN/docs/components/accordion.md description: 内部基于 collapse 实现的可折叠面板组件。 --- # Accordion Accordion 是一个可折叠内容组件,允许用户展开和收起多个内容区块。它内部基于 collapse 功能实现,适合 FAQ、设置分组和分段内容展示。 ## 导入 ```rust use gpui_kit::component::accordion::Accordion; ``` ## 用法 ### 基础 Accordion ```rust Accordion::new("my-accordion") .item(|item| { item.title("Section 1") .child("Content for section 1") }) .item(|item| { item.title("Section 2") .child("Content for section 2") }) .item(|item| { item.title("Section 3") .child("Content for section 3") }) ``` ### 允许同时展开多个项 默认情况下,一次只能展开一个项。使用 `multiple()` 可以允许多个项同时展开: ```rust Accordion::new("my-accordion") .multiple(true) .item(|item| item.title("Section 1").child("Content 1")) .item(|item| item.title("Section 2").child("Content 2")) ``` ### 带边框 ```rust Accordion::new("my-accordion") .bordered(true) .item(|item| item.title("Section 1").child("Content 1")) ``` ### 不同尺寸 ```rust use gpui_kit::component::{Sizable as _, Size}; Accordion::new("my-accordion") .small() .item(|item| item.title("Small Section").child("Content")) Accordion::new("my-accordion") .large() .item(|item| item.title("Large Section").child("Content")) ``` ### 处理切换事件 ```rust Accordion::new("my-accordion") .on_toggle_click(|open_indices, window, cx| { println!("Open items: {:?}", open_indices); }) .item(|item| item.title("Section 1").child("Content 1")) ``` ### 禁用状态 ```rust Accordion::new("my-accordion") .disabled(true) .item(|item| item.title("Disabled Section").child("Content")) ``` ## API 参考 * [Accordion] * [AccordionItem] ### 尺寸 实现了 [Sizable] trait: * `small()`:小尺寸 * `medium()`:中尺寸,默认值 * `large()`:大尺寸 * `xsmall()`:超小尺寸 ## 示例 ### 自定义图标标题 ```rust Accordion::new("my-accordion") .item(|item| { item.title( h_flex() .gap_2() .child(Icon::new(IconName::Settings)) .child("Settings") ) .child("Settings content here") }) ``` ### 嵌套 Accordion ```rust Accordion::new("outer") .item(|item| { item.title("Parent Section") .child( Accordion::new("inner") .item(|item| item.title("Child 1").child("Content")) .item(|item| item.title("Child 2").child("Content")) ) }) ``` [Accordion]: https://docs.rs/gpui-component/latest/gpui_component/accordion/struct.Accordion.html [AccordionItem]: https://docs.rs/gpui-component/latest/gpui_component/accordion/struct.AccordionItem.html [Sizable]: https://docs.rs/gpui-component/latest/gpui_component/trait.Sizable.html --- --- url: /zh-CN/base/primitives/accordion.md description: 由可独立设置样式的标题、触发器和面板组成的折叠组。 --- # Accordion 由可独立设置样式的标题、触发器和面板组成的折叠组。 和所有 GPUI Base 原语一样,Accordion 只提供行为和语义结构,不规定产品视觉语言。请使用 GPUI 样式并组合导出的部件,使其符合你的设计系统。 ## 示例 原生示例和页面上方的 WASM 预览共用同一份实现: ```bash cargo run -p gpui-base --example components -- accordion ``` ## 导入 ```rust use gpui_kit::base::{Accordion, AccordionHeader, AccordionItem, AccordionPanel, AccordionTrigger}; ``` ## 结构与 API 示例组合上述公开类型。GPUI 的标准样式和事件 trait 负责表现,Base 类型负责交互结构。权威实现位于 [`components/accordion.rs`](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/components/accordion.rs),原生与浏览器预览编译的是同一文件。 ## 状态与事件 管理每一项的展开状态,并在触发器激活时更新它。 受控状态应保存在父渲染类型或 GPUI entity 中;在回调中更新并调用 `cx.notify()`,不要在每次渲染时重建持久 entity。 ## 完整 Rust 示例 ## 可访问性 让触发器可聚焦、可用键盘操作,并向辅助技术暴露展开状态。 ## 注意事项 在支持的位置使用稳定元素 ID,并在消费端设计系统中验证焦点、悬停、按下、选中、禁用、减少动态效果和高对比度状态。 --- --- url: /base/primitives/accordion.md description: >- A disclosure group composed from independently styleable header, trigger, and panel parts. --- # Accordion A disclosure group composed from independently styleable header, trigger, and panel parts. Like every `gpui-base` primitive, Accordion supplies behavior and semantic structure without imposing a product visual language. Apply GPUI styles and compose the exported parts to match your design system. ## Example The [single native Cargo entrypoint](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/components.rs) selects this primitive from the [shared showcase implementation](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/mod.rs). The same showcase is compiled once for the WASM preview above. ```bash cargo run -p gpui-base --example components -- accordion ``` ## Import ```rust use gpui_kit::base::{Accordion, AccordionHeader, AccordionItem, AccordionPanel, AccordionTrigger}; ``` ## Anatomy and API The example composes `Accordion`, `AccordionHeader`, `AccordionItem`, `AccordionPanel`, `AccordionTrigger`. GPUI's standard styling and event traits provide presentation; these base types provide the interaction structure. The authoritative module is [`components/accordion.rs`](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/components/accordion.rs). Native and browser previews compile this same file. ## State and events Controlled by `AccordionItem::open`; `AccordionTrigger::on_change` reports the next expanded state. Keep controlled state on the parent render type or in a GPUI entity. Update it in callbacks and call `cx.notify()`; do not recreate persistent entities during every render. ## Complete Rust example The complete implementation used by the runnable showcase is embedded directly from Rust source: The command above supplies application initialization, window creation, and shared `BaseShowcase` state. ## Accessibility Name every trigger, expose expanded state, and remove collapsed panel content from the focus order. ## Notes Use stable element IDs where accepted. Verify focus, hover, active, selected, disabled, reduced-motion, and high-contrast appearances in the consuming design system. --- --- url: /docs/components/alert.md description: Displays a callout for user attention. --- # Alert A versatile alert component for displaying important messages to users. Supports multiple variants (info, success, warning, error), custom icons, optional titles, closable functionality, and banner mode. Perfect for notifications, status messages, and user feedback. ## Import ```rust use gpui_kit::component::alert::Alert; ``` ## Usage ### Basic Alert ```rust Alert::new("alert-id", "This is a basic alert message.") ``` ### Alert with Title ```rust Alert::new("alert-with-title", "Your changes have been saved successfully.") .title("Success!") ``` ### Alert Variants ```rust // Info alert (blue) Alert::info("info-alert", "This is an informational message.") .title("Information") // Success alert (green) Alert::success("success-alert", "Your operation completed successfully.") .title("Success!") // Warning alert (yellow/orange) Alert::warning("warning-alert", "Please review your settings before proceeding.") .title("Warning") // Error alert (red) Alert::error("error-alert", "An error occurred while processing your request.") .title("Error") ``` ### Alert Sizes ```rust use gpui_kit::component::{alert::Alert, Sizable as _}; Alert::info("alert", "Message content") .xsmall() .title("XSmall Alert") Alert::info("alert", "Message content") .small() .title("Small Alert") Alert::info("alert", "Message content") .title("Medium Alert") Alert::info("alert", "Message content") .large() .title("Large Alert") ``` ### Closable Alerts When you add an `on_close` handler, a close button appears on the alert: ```rust Alert::info("closable-alert", "This alert can be dismissed.") .title("Dismissible") .on_close(|_event, _window, _cx| { println!("Alert was closed"); // Handle alert dismissal }) ``` ### Banner Mode Banner alerts take full width and don't display titles: ```rust Alert::info("banner-alert", "This is a banner alert that spans the full width.") .banner() Alert::success("banner-success", "Operation completed successfully!") .banner() Alert::warning("banner-warning", "System maintenance scheduled for tonight.") .banner() Alert::error("banner-error", "Service temporarily unavailable.") .banner() ``` ### Custom Icons ```rust use gpui_kit::component::IconName; Alert::new("custom-icon", "Meeting scheduled for tomorrow at 3 PM.") .title("Calendar Reminder") .icon(IconName::Calendar) ``` ### With Markdown Content We can use `TextView` to render formatted (Markdown or HTML) text within the alert, for displaying lists, bold text, links, etc. ```rust use gpui_kit::component::text::markdown; Alert::error( "error-with-markdown", markdown( "Please verify your billing information and try again.\n\ - Check your card details\n\ - Ensure sufficient funds\n\ - Verify billing address" ), ) .title("Payment Failed") ``` ### Conditional Visibility ```rust Alert::info("conditional-alert", "This alert may be hidden.") .title("Conditional") .visible(should_show_alert) // boolean condition ``` ## API Reference * [Alert] ## Examples ### Form Validation Errors ```rust Alert::error( "validation-error", "Please correct the following errors before submitting:\n\ - Email address is required\n\ - Password must be at least 8 characters\n\ - Terms of service must be accepted" ) .title("Validation Failed") ``` ### Success Notification ```rust Alert::success("save-success", "Your profile has been updated successfully.") .title("Changes Saved") .on_close(|_, _, _| { // Auto-dismiss after showing }) ``` ### System Status Banner ```rust Alert::warning( "maintenance-banner", "Scheduled maintenance will occur tonight from 2:00 AM to 4:00 AM EST. \ Some services may be temporarily unavailable." ) .banner() .large() ``` ### Interactive Alert with Custom Action ```rust Alert::info("update-available", "A new version of the application is available.") .title("Update Available") .icon(IconName::Download) .on_close(cx.listener(|this, _, _, cx| { // Handle update or dismiss this.handle_update_notification(cx); })) ``` ### Multi-line Content with Formatting ```rust use gpui_kit::component::text::markdown; Alert::warning( "security-alert", markdown( "**Security Notice**: Unusual activity detected on your account.\n\n\ Recent activity:\n\ - Login from new device (Chrome on Windows)\n\ - Location: San Francisco, CA\n\ - Time: Today at 2:30 PM\n\n\ If this wasn't you, please [change your password](/) immediately." ) ) .title("Security Alert") .icon(IconName::Shield) ``` [Alert]: https://docs.rs/gpui-component/latest/gpui_component/alert/struct.Alert.html --- --- url: /zh-CN/docs/components/alert.md description: 用于吸引用户注意的重要提示组件。 --- # Alert Alert 是一个通用提示组件,用于展示重要消息。它支持多种变体、可选标题、自定义图标、可关闭行为以及横幅模式,适合通知、状态提示和操作反馈场景。 ## 导入 ```rust use gpui_kit::component::alert::Alert; ``` ## 用法 ### 基础 Alert ```rust Alert::new("alert-id", "This is a basic alert message.") ``` ### 带标题 ```rust Alert::new("alert-with-title", "Your changes have been saved successfully.") .title("Success!") ``` ### 不同变体 ```rust Alert::info("info-alert", "This is an informational message.") .title("Information") Alert::success("success-alert", "Your operation completed successfully.") .title("Success!") Alert::warning("warning-alert", "Please review your settings before proceeding.") .title("Warning") Alert::error("error-alert", "An error occurred while processing your request.") .title("Error") ``` ### Alert 尺寸 ```rust use gpui_kit::component::{alert::Alert, Sizable as _}; Alert::info("alert", "Message content") .xsmall() .title("XSmall Alert") Alert::info("alert", "Message content") .small() .title("Small Alert") Alert::info("alert", "Message content") .title("Medium Alert") Alert::info("alert", "Message content") .large() .title("Large Alert") ``` ### 可关闭提示 只要设置 `on_close`,Alert 就会显示关闭按钮: ```rust Alert::info("closable-alert", "This alert can be dismissed.") .title("Dismissible") .on_close(|_event, _window, _cx| { println!("Alert was closed"); }) ``` ### 横幅模式 横幅模式会占满可用宽度,并且不显示标题: ```rust Alert::info("banner-alert", "This is a banner alert that spans the full width.") .banner() Alert::success("banner-success", "Operation completed successfully!") .banner() Alert::warning("banner-warning", "System maintenance scheduled for tonight.") .banner() Alert::error("banner-error", "Service temporarily unavailable.") .banner() ``` ### 自定义图标 ```rust use gpui_kit::component::IconName; Alert::new("custom-icon", "Meeting scheduled for tomorrow at 3 PM.") .title("Calendar Reminder") .icon(IconName::Calendar) ``` ### 使用 Markdown 内容 可以配合 `TextView` 渲染 Markdown 或 HTML 内容: ```rust use gpui_kit::component::text::markdown; Alert::error( "error-with-markdown", markdown( "Please verify your billing information and try again.\n\ - Check your card details\n\ - Ensure sufficient funds\n\ - Verify billing address" ), ) .title("Payment Failed") ``` ### 条件显示 ```rust Alert::info("conditional-alert", "This alert may be hidden.") .title("Conditional") .visible(should_show_alert) ``` ## API 参考 * [Alert] ## 示例 ### 表单校验错误 ```rust Alert::error( "validation-error", "Please correct the following errors before submitting:\n\ - Email address is required\n\ - Password must be at least 8 characters\n\ - Terms of service must be accepted" ) .title("Validation Failed") ``` ### 成功提示 ```rust Alert::success("save-success", "Your profile has been updated successfully.") .title("Changes Saved") .on_close(|_, _, _| { // Auto-dismiss after showing }) ``` ### 系统状态横幅 ```rust Alert::warning( "maintenance-banner", "Scheduled maintenance will occur tonight from 2:00 AM to 4:00 AM EST. \ Some services may be temporarily unavailable." ) .banner() .large() ``` ### 交互式提示 ```rust Alert::info("update-available", "A new version of the application is available.") .title("Update Available") .icon(IconName::Download) .on_close(cx.listener(|this, _, _, cx| { this.handle_update_notification(cx); })) ``` ### 多行格式化内容 ```rust use gpui_kit::component::text::markdown; Alert::warning( "security-alert", markdown( "**Security Notice**: Unusual activity detected on your account.\n\n\ Recent activity:\n\ - Login from new device (Chrome on Windows)\n\ - Location: San Francisco, CA\n\ - Time: Today at 2:30 PM\n\n\ If this wasn't you, please [change your password](/) immediately." ) ) .title("Security Alert") .icon(IconName::Shield) ``` [Alert]: https://docs.rs/gpui-component/latest/gpui_component/alert/struct.Alert.html --- --- url: /zh-CN/base/primitives/alert-dialog.md description: 用于需要用户明确决定之操作的模态确认界面。 --- # Alert Dialog 用于需要用户明确决定之操作的模态确认界面。 和所有 GPUI Base 原语一样,Alert Dialog 只提供行为和语义结构,不规定产品视觉语言。请使用 GPUI 样式并组合导出的部件,使其符合你的设计系统。 ## 示例 原生示例和页面上方的 WASM 预览共用同一份实现: ```bash cargo run -p gpui-base --example components -- alert-dialog ``` ## 导入 ```rust use gpui_kit::base::{AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogDescription, AlertDialogPopup, AlertDialogTitle, AlertDialogTrigger}; ``` ## 结构与 API 示例组合上述公开类型。GPUI 的标准样式和事件 trait 负责表现,Base 类型负责交互结构。权威实现位于 [`components/alert-dialog.rs`](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/components/alert-dialog.rs),原生与浏览器预览编译的是同一文件。 ## 状态与事件 打开状态和确认/取消结果由应用管理。 受控状态应保存在父渲染类型或 GPUI entity 中;在回调中更新并调用 `cx.notify()`,不要在每次渲染时重建持久 entity。 ## 完整 Rust 示例 ## 可访问性 将焦点限制在模态层内,提供标题与说明,并确保取消操作始终可用。 ## 注意事项 在支持的位置使用稳定元素 ID,并在消费端设计系统中验证焦点、悬停、按下、选中、禁用、减少动态效果和高对比度状态。 --- --- url: /base/primitives/alert-dialog.md description: A modal confirmation surface for actions that need an explicit decision. --- # Alert Dialog A modal confirmation surface for actions that need an explicit decision. Like every `gpui-base` primitive, Alert Dialog supplies behavior and semantic structure without imposing a product visual language. Apply GPUI styles and compose the exported parts to match your design system. ## Example The [single native Cargo entrypoint](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/components.rs) selects this primitive from the [shared showcase implementation](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/mod.rs). The same showcase is compiled once for the WASM preview above. ```bash cargo run -p gpui-base --example components -- alert-dialog ``` ## Import ```rust use gpui_kit::base::{AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogDescription, AlertDialogPopup, AlertDialogTitle, AlertDialogTrigger}; ``` ## Anatomy and API The example composes `AlertDialog`, `AlertDialogAction`, `AlertDialogCancel`, `AlertDialogDescription`, `AlertDialogPopup`, `AlertDialogTitle`, `AlertDialogTrigger`. GPUI's standard styling and event traits provide presentation; these base types provide the interaction structure. The authoritative module is [`components/alert_dialog.rs`](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/components/alert_dialog.rs). Native and browser previews compile this same file. ## State and events Opening and dismissal are managed by `AlertDialog`; application action buttons decide when destructive work is committed. Keep controlled state on the parent render type or in a GPUI entity. Update it in callbacks and call `cx.notify()`; do not recreate persistent entities during every render. ## Complete Rust example The complete implementation used by the runnable showcase is embedded directly from Rust source: The command above supplies application initialization, window creation, and shared `BaseShowcase` state. ## Accessibility Provide title and description, trap focus, offer cancel, and restore focus to the opener. ## Notes Use stable element IDs where accepted. Verify focus, hover, active, selected, disabled, reduced-motion, and high-contrast appearances in the consuming design system. --- --- url: /docs/components/alert-dialog.md description: >- A modal dialog that interrupts the user with important content and expects a response. --- # AlertDialog AlertDialog is a modal dialog component that interrupts the user with important content and expects a response. It is built on top of the [Dialog] component with opinionated defaults and a simplified API. ## Differences from Dialog AlertDialog provides these defaults on top of Dialog: * Not overlay closable by default (can be changed with `overlay_closable(true)`) * No close button by default (can be changed with `close_button(true)`) * Footer buttons are center-aligned (Dialog uses right-alignment) * Simplified API focused on alert and confirmation scenarios ## Import ```rust use gpui_kit::component::dialog::{AlertDialog, DialogAction, DialogClose}; use gpui_kit::component::WindowExt; ``` ## Usage ### Setup Application Root View Like Dialog, you need to set up your application's root view to render the dialog layer. See [Dialog documentation](./dialog.md#setup-application-root-view) for details. ### Basic AlertDialog (Declarative API) Create a fully declarative AlertDialog using `trigger` and `content`: ```rust use gpui_kit::component::dialog::{AlertDialog, DialogHeader, DialogTitle, DialogDescription, DialogFooter}; AlertDialog::new(cx) .trigger( Button::new("show-alert") .outline() .label("Show Alert") ) .content(|content, _, cx| { content .child( DialogHeader::new() .child(DialogTitle::new().child("Are you absolutely sure?")) .child(DialogDescription::new().child( "This action cannot be undone. \ This will permanently delete your account from our servers." )) ) .child( DialogFooter::new() .child( Button::new("cancel") .outline() .label("Cancel") .on_click(|_, window, cx| { window.close_dialog(cx); }) ) .child( Button::new("ok") .primary() .label("Continue") .on_click(|_, window, cx| { window.push_notification("Confirmed", cx); window.close_dialog(cx); }) ) ) }) ``` ### Using DialogAction and DialogClose `DialogAction` and `DialogClose` are wrapper components that simplify button click handling by automatically triggering the appropriate actions: * **DialogClose**: Wraps a button to trigger the `Cancel` action, invoking `on_cancel` callback * **DialogAction**: Wraps a button to trigger the `Confirm` action, invoking `on_ok` callback These components eliminate the need to manually call `window.close_dialog(cx)`: ```rust AlertDialog::new(cx) .trigger(Button::new("show-alert").outline().label("Show Alert")) .on_ok(|_, window, cx| { window.push_notification("You confirmed!", cx); true // Return true to close dialog }) .on_cancel(|_, window, cx| { window.push_notification("You cancelled!", cx); true }) .content(|content, _, cx| { content .child( DialogHeader::new() .child(DialogTitle::new().child("Confirm Action")) .child(DialogDescription::new().child("Do you want to proceed?")) ) .child( DialogFooter::new() .child( DialogClose::new().child( Button::new("cancel").outline().label("Cancel") ) ) .child( DialogAction::new().child( Button::new("ok").primary().label("Confirm") ) ) ) }) ``` **Benefits:** * No need to manually close the dialog * Automatically connects to `on_ok` and `on_cancel` callbacks * Cleaner, more declarative code * Supports returning `false` from callbacks to prevent closing ### Basic AlertDialog (Imperative API) Open a dialog imperatively using `WindowExt::open_alert_dialog`: ```rust window.open_alert_dialog(cx, |alert, _, _| { alert .title("Delete File") .description("Are you sure you want to delete this file? This action cannot be undone.") .show_cancel(true) .on_ok(|_, window, cx| { window.push_notification("File deleted", cx); true // Return true to close dialog }) }) ``` ### Custom Button Props Use `button_props` to customize button text and styles: ```rust use gpui_kit::component::dialog::DialogButtonProps; use gpui_kit::component::button::ButtonVariant; window.open_alert_dialog(cx, |alert, _, _| { alert .title("Delete Account") .description("This will permanently delete your account and all associated data.") .button_props( DialogButtonProps::default() .ok_text("Delete") .ok_variant(ButtonVariant::Danger) .cancel_text("Keep") .show_cancel(true) ) .on_ok(|_, window, cx| { window.push_notification("Account deleted", cx); true }) }) ``` ### AlertDialog with Icon Using icon in declarative API: ```rust use gpui_kit::component::{Icon, IconName, ActiveTheme}; AlertDialog::new(cx) .w(px(320.)) .trigger(Button::new("permission").outline().label("Request Permission")) .on_ok(|_, window, cx| { window.push_notification("Permission granted", cx); true }) .content(|content, _, cx| { content .child( DialogHeader::new() .items_center() .child( Icon::new(IconName::TriangleAlert) .size_10() .text_color(cx.theme().warning) ) .child(DialogTitle::new().child("Network Permission Required")) .child(DialogDescription::new().child( "We need your permission to access the network to provide better services." )) ) .child( DialogFooter::new() .v_flex() .child( DialogAction::new().child( Button::new("allow").w_full().primary().label("Allow") ) ) .child( DialogClose::new().child( Button::new("deny").w_full().outline().label("Don't Allow") ) ) ) }) ``` Using icon in imperative API: ```rust window.open_alert_dialog(cx, |alert, _, cx| { alert .title("Warning") .description("This action requires confirmation.") .icon( Icon::new(IconName::AlertTriangle) .size_8() .text_color(cx.theme().warning) ) }) ``` ### Destructive Action Confirmation ```rust AlertDialog::new(cx) .trigger( Button::new("delete-account") .outline() .danger() .label("Delete Account") ) .on_ok(|_, window, cx| { window.push_notification("Account deletion initiated", cx); true }) .content(|content, _, _| { content .child( DialogHeader::new() .child(DialogTitle::new().child("Delete Account")) .child(DialogDescription::new().child( "This will permanently delete your account \ and all associated data. This action cannot be undone." )) ) .child( DialogFooter::new() .child( DialogClose::new().child( Button::new("cancel").flex_1().outline().label("Cancel") ) ) .child( DialogAction::new().child( Button::new("delete") .flex_1() .outline() .danger() .label("Delete Forever") ) ) ) }) ``` ### Custom Width ```rust AlertDialog::new(cx) .width(px(500.)) .trigger(Button::new("custom-width").label("Custom Width")) .content(|content, _, _| { // ... dialog content }) ``` ### Controlling Dialog Close Behavior #### Allow Overlay Click to Close ```rust window.open_alert_dialog(cx, |alert, _, _| { alert .title("Notice") .description("Click outside this dialog or press ESC to close it.") .overlay_closable(true) }) ``` #### Disable Keyboard ESC to Close ```rust window.open_alert_dialog(cx, |alert, _, _| { alert .title("Important Notice") .description("Please read this carefully before proceeding.") .keyboard(false) }) ``` #### Show Close Button ```rust window.open_alert_dialog(cx, |alert, _, _| { alert .title("Information") .description("Some information...") .close_button(true) }) ``` ### Prevent Dialog from Closing Return `false` from `on_ok` or `on_cancel` callbacks to prevent the dialog from closing: ```rust use gpui_kit::component::dialog::DialogButtonProps; window.open_alert_dialog(cx, |alert, _, _| { alert .title("Processing") .description("A process is running. Click Continue to stop it or Cancel to keep waiting.") .button_props( DialogButtonProps::default() .ok_text("Continue") .show_cancel(true) ) .on_ok(|_, window, cx| { // Return false to prevent closing window.push_notification("Cannot close: Process still running", cx); false }) .on_cancel(|_, window, cx| { window.push_notification("Waiting...", cx); false }) }) ``` ### Dialog Close Callback Use `on_close` to execute actions after the dialog closes (called after `on_ok` or `on_cancel`): ```rust window.open_alert_dialog(cx, |alert, _, _| { alert .title("Confirm") .description("Are you sure?") .on_close(|_, window, cx| { window.push_notification("Dialog closed", cx); }) }) ``` ## API Reference ### AlertDialog | Method | Description | | ------------------------ | ------------------------------------------------------------- | | `new(cx)` | Create a new AlertDialog | | `trigger(element)` | Set trigger element that opens the dialog when clicked | | `content(builder)` | Set dialog content using a builder function (declarative API) | | `title(title)` | Set dialog title (imperative API) | | `description(desc)` | Set dialog description (imperative API) | | `icon(icon)` | Set dialog icon (imperative API) | | `button_props(props)` | Set button properties (text, style, visibility) | | `show_cancel(bool)` | Show/hide cancel button, default `false` | | `width(px)` | Set dialog width, default `420px` | | `overlay_closable(bool)` | Allow clicking overlay to close, default `false` | | `close_button(bool)` | Show/hide close button, default `false` | | `keyboard(bool)` | Support ESC key to close, default `true` | | `on_ok(callback)` | Set OK button callback, return `true` to close dialog | | `on_cancel(callback)` | Set cancel button callback, return `true` to close dialog | | `on_close(callback)` | Set callback after dialog closes | ### DialogButtonProps | Method | Description | | ------------------------- | ---------------------------------------- | | `ok_text(text)` | Set OK button text, default "OK" | | `cancel_text(text)` | Set cancel button text, default "Cancel" | | `ok_variant(variant)` | Set OK button variant | | `cancel_variant(variant)` | Set cancel button variant | | `show_cancel(bool)` | Show/hide cancel button | | `on_ok(callback)` | Set OK callback | | `on_cancel(callback)` | Set cancel callback | ### DialogAction A wrapper component that automatically triggers the `Confirm` action when its child element is clicked. This invokes the `on_ok` callback set on the AlertDialog. **Usage:** ```rust DialogAction::new().child( Button::new("ok").primary().label("Confirm") ) ``` **Behavior:** * Dispatches `Confirm` action on click * Invokes the `on_ok` callback * Dialog closes if callback returns `true` * Dialog stays open if callback returns `false` ### DialogClose A wrapper component that automatically triggers the `Cancel` action when its child element is clicked. This invokes the `on_cancel` callback set on the AlertDialog. **Usage:** ```rust DialogClose::new().child( Button::new("cancel").outline().label("Cancel") ) ``` **Behavior:** * Dispatches `Cancel` action on click * Invokes the `on_cancel` callback * Dialog closes if callback returns `true` (or if no callback is set) * Dialog stays open if callback returns `false` ## Examples ### Delete Confirmation Using imperative API with button props: ```rust Button::new("delete") .danger() .label("Delete") .on_click(|_, window, cx| { window.open_alert_dialog(cx, |alert, _, _| { alert .title("Delete File?") .description("This action cannot be undone.") .button_props( DialogButtonProps::default() .ok_text("Delete") .ok_variant(ButtonVariant::Danger) .show_cancel(true) ) .on_ok(|_, window, cx| { // Perform delete operation window.push_notification("File deleted", cx); true }) }); }) ``` Or using declarative API with DialogAction/DialogClose: ```rust AlertDialog::new(cx) .trigger(Button::new("delete").danger().label("Delete")) .on_ok(|_, window, cx| { window.push_notification("File deleted", cx); true }) .content(|content, _, cx| { content .child( DialogHeader::new() .child(DialogTitle::new().child("Delete File?")) .child(DialogDescription::new().child("This action cannot be undone.")) ) .child( DialogFooter::new() .child( DialogClose::new().child( Button::new("cancel").outline().label("Cancel") ) ) .child( DialogAction::new().child( Button::new("delete-confirm").danger().label("Delete") ) ) ) }) ``` ### Session Timeout ```rust window.open_alert_dialog(cx, |alert, _, _| { alert .content(|content, _, _| { content .child( DialogHeader::new() .items_center() .child(DialogTitle::new().child("Session Expired")) .child(DialogDescription::new().child( "Your session has expired due to inactivity. \ Please log in again to continue." )) ) .child( DialogFooter::new() .child( Button::new("sign-in") .label("Sign in") .primary() .flex_1() .on_click(|_, window, cx| { window.push_notification("Redirecting to login...", cx); window.close_dialog(cx); }) ) ) }) }) ``` ### Update Available ```rust AlertDialog::new(cx) .trigger(Button::new("update").outline().label("Update Available")) .on_cancel(|_, window, cx| { window.push_notification("Update postponed", cx); true }) .on_ok(|_, window, cx| { window.push_notification("Starting update...", cx); true }) .content(|content, _, _| { content .child( DialogHeader::new() .child(DialogTitle::new().child("Update Available")) .child(DialogDescription::new().child( "A new version (v2.0.0) is available. \ This update includes new features and bug fixes." )) ) .child( DialogFooter::new() .child( DialogClose::new().child( Button::new("later").flex_1().outline().label("Later") ) ) .child( DialogAction::new().child( Button::new("update-now").flex_1().primary().label("Update Now") ) ) ) }) ``` ## Best Practices 1. **Choose the Right API**: Use imperative API (`open_alert_dialog`) for simple confirmations; use declarative API (`trigger` + `content`) for complex layouts or integration with other components 2. **Use DialogAction and DialogClose**: Prefer wrapping buttons with `DialogAction` and `DialogClose` over manual `window.close_dialog()` calls for cleaner, more declarative code 3. **Clarify Intent**: Use appropriate button variants (e.g., `ButtonVariant::Danger` for delete operations) to communicate the importance of actions 4. **Provide Clear Descriptions**: Ensure users understand the consequences of their actions, especially for destructive operations 5. **Use Icons Wisely**: Icons can enhance attention for warnings and errors, but use them appropriately 6. **Prevent Closing Carefully**: Only prevent dialog closing when user confirmation is truly necessary (e.g., a process is running) 7. **Maintain Consistency**: Keep dialog button order and styles consistent throughout your application ## Related Components * [Dialog] - More flexible dialog component * [DialogHeader] - Dialog header component * [DialogTitle] - Dialog title component * [DialogDescription] - Dialog description component * [DialogFooter] - Dialog footer component * [DialogAction] - Wrapper component for confirm/OK buttons * [DialogClose] - Wrapper component for cancel/close buttons [AlertDialog]: https://docs.rs/gpui-component/latest/gpui_component/dialog/struct.AlertDialog.html [Dialog]: https://docs.rs/gpui-component/latest/gpui_component/dialog/struct.Dialog.html [DialogHeader]: https://docs.rs/gpui-component/latest/gpui_component/dialog/struct.DialogHeader.html [DialogTitle]: https://docs.rs/gpui-component/latest/gpui_component/dialog/struct.DialogTitle.html [DialogDescription]: https://docs.rs/gpui-component/latest/gpui_component/dialog/struct.DialogDescription.html [DialogFooter]: https://docs.rs/gpui-component/latest/gpui_component/dialog/struct.DialogFooter.html [DialogAction]: https://docs.rs/gpui-component/latest/gpui_component/dialog/struct.DialogAction.html [DialogClose]: https://docs.rs/gpui-component/latest/gpui_component/dialog/struct.DialogClose.html --- --- url: /zh-CN/docs/components/alert-dialog.md description: 使用于重要确认场景的模态对话框组件。 --- # AlertDialog AlertDialog 是一个用于中断用户并请求明确响应的模态对话框组件。它构建在 [Dialog] 之上,提供更明确的默认行为和更精简的 API,适合确认、警告和危险操作提示。 ## 与 Dialog 的区别 AlertDialog 基于 Dialog 提供了以下默认值: * 默认不允许点击遮罩关闭,可通过 `overlay_closable(true)` 修改 * 默认不显示关闭按钮,可通过 `close_button(true)` 修改 * 底部按钮居中对齐,而 Dialog 默认为右对齐 * API 更聚焦在确认和提示场景 ## 导入 ```rust use gpui_kit::component::dialog::{AlertDialog, DialogAction, DialogClose}; use gpui_kit::component::WindowExt; ``` ## 用法 ### 配置应用根视图 与 Dialog 一样,你需要在应用根视图中渲染 dialog layer。具体可参考 [Dialog 文档](./dialog.md#setup-application-root-view)。 ### 基础 AlertDialog:声明式 API 通过 `trigger` 和 `content` 创建声明式 AlertDialog: ```rust use gpui_kit::component::dialog::{AlertDialog, DialogHeader, DialogTitle, DialogDescription, DialogFooter}; AlertDialog::new(cx) .trigger( Button::new("show-alert") .outline() .label("Show Alert") ) .content(|content, _, cx| { content .child( DialogHeader::new() .child(DialogTitle::new().child("Are you absolutely sure?")) .child(DialogDescription::new().child( "This action cannot be undone. \ This will permanently delete your account from our servers." )) ) .child( DialogFooter::new() .child( Button::new("cancel") .outline() .label("Cancel") .on_click(|_, window, cx| { window.close_dialog(cx); }) ) .child( Button::new("ok") .primary() .label("Continue") .on_click(|_, window, cx| { window.push_notification("Confirmed", cx); window.close_dialog(cx); }) ) ) }) ``` ### 使用 DialogAction 和 DialogClose `DialogAction` 与 `DialogClose` 是包装组件,可自动触发对应按钮行为: * `DialogClose`:触发取消操作,并调用 `on_cancel` * `DialogAction`:触发确认操作,并调用 `on_ok` 这样就不需要手动调用 `window.close_dialog(cx)`: ```rust AlertDialog::new(cx) .trigger(Button::new("show-alert").outline().label("Show Alert")) .on_ok(|_, window, cx| { window.push_notification("You confirmed!", cx); true }) .on_cancel(|_, window, cx| { window.push_notification("You cancelled!", cx); true }) .content(|content, _, cx| { content .child( DialogHeader::new() .child(DialogTitle::new().child("Confirm Action")) .child(DialogDescription::new().child("Do you want to proceed?")) ) .child( DialogFooter::new() .child( DialogClose::new().child( Button::new("cancel").outline().label("Cancel") ) ) .child( DialogAction::new().child( Button::new("ok").primary().label("Confirm") ) ) ) }) ``` 优点: * 不需要手动关闭对话框 * 自动连接 `on_ok` 和 `on_cancel` * 代码更简洁 * 支持在回调返回 `false` 时阻止关闭 ### 基础 AlertDialog:命令式 API 通过 `WindowExt::open_alert_dialog` 直接打开: ```rust window.open_alert_dialog(cx, |alert, _, _| { alert .title("Delete File") .description("Are you sure you want to delete this file? This action cannot be undone.") .show_cancel(true) .on_ok(|_, window, cx| { window.push_notification("File deleted", cx); true }) }) ``` ### 自定义按钮属性 ```rust use gpui_kit::component::dialog::DialogButtonProps; use gpui_kit::component::button::ButtonVariant; window.open_alert_dialog(cx, |alert, _, _| { alert .title("Delete Account") .description("This will permanently delete your account and all associated data.") .button_props( DialogButtonProps::default() .ok_text("Delete") .ok_variant(ButtonVariant::Danger) .cancel_text("Keep") .show_cancel(true) ) .on_ok(|_, window, cx| { window.push_notification("Account deleted", cx); true }) }) ``` ### 带图标的 AlertDialog 声明式写法: ```rust use gpui_kit::component::{Icon, IconName, ActiveTheme}; AlertDialog::new(cx) .w(px(320.)) .trigger(Button::new("permission").outline().label("Request Permission")) .on_ok(|_, window, cx| { window.push_notification("Permission granted", cx); true }) .content(|content, _, cx| { content .child( DialogHeader::new() .items_center() .child( Icon::new(IconName::TriangleAlert) .size_10() .text_color(cx.theme().warning) ) .child(DialogTitle::new().child("Network Permission Required")) .child(DialogDescription::new().child( "We need your permission to access the network to provide better services." )) ) .child( DialogFooter::new() .v_flex() .child( DialogAction::new().child( Button::new("allow").w_full().primary().label("Allow") ) ) .child( DialogClose::new().child( Button::new("deny").w_full().outline().label("Don't Allow") ) ) ) }) ``` 命令式写法: ```rust window.open_alert_dialog(cx, |alert, _, cx| { alert .title("Warning") .description("This action requires confirmation.") .icon( Icon::new(IconName::AlertTriangle) .size_8() .text_color(cx.theme().warning) ) }) ``` ### 危险操作确认 ```rust AlertDialog::new(cx) .trigger( Button::new("delete-account") .outline() .danger() .label("Delete Account") ) .on_ok(|_, window, cx| { window.push_notification("Account deletion initiated", cx); true }) .content(|content, _, _| { content .child( DialogHeader::new() .child(DialogTitle::new().child("Delete Account")) .child(DialogDescription::new().child( "This will permanently delete your account \ and all associated data. This action cannot be undone." )) ) .child( DialogFooter::new() .child( DialogClose::new().child( Button::new("cancel").flex_1().outline().label("Cancel") ) ) .child( DialogAction::new().child( Button::new("delete") .flex_1() .outline() .danger() .label("Delete Forever") ) ) ) }) ``` ### 自定义宽度 ```rust AlertDialog::new(cx) .width(px(500.)) .trigger(Button::new("custom-width").label("Custom Width")) .content(|content, _, _| { // ... dialog content }) ``` ### 控制关闭行为 #### 允许点击遮罩关闭 ```rust window.open_alert_dialog(cx, |alert, _, _| { alert .title("Notice") .description("Click outside this dialog or press ESC to close it.") .overlay_closable(true) }) ``` #### 禁用 ESC 关闭 ```rust window.open_alert_dialog(cx, |alert, _, _| { alert .title("Important Notice") .description("Please read this carefully before proceeding.") .keyboard(false) }) ``` #### 显示关闭按钮 ```rust window.open_alert_dialog(cx, |alert, _, _| { alert .title("Information") .description("Some information...") .close_button(true) }) ``` ### 阻止对话框关闭 如果 `on_ok` 或 `on_cancel` 返回 `false`,对话框不会关闭: ```rust use gpui_kit::component::dialog::DialogButtonProps; window.open_alert_dialog(cx, |alert, _, _| { alert .title("Processing") .description("A process is running. Click Continue to stop it or Cancel to keep waiting.") .button_props( DialogButtonProps::default() .ok_text("Continue") .show_cancel(true) ) .on_ok(|_, window, cx| { window.push_notification("Cannot close: Process still running", cx); false }) .on_cancel(|_, window, cx| { window.push_notification("Waiting...", cx); false }) }) ``` ### Dialog 关闭回调 ```rust window.open_alert_dialog(cx, |alert, _, _| { alert .title("Confirm") .description("Are you sure?") .on_close(|_, window, cx| { window.push_notification("Dialog closed", cx); }) }) ``` ## API 参考 ### AlertDialog | 方法 | 说明 | | ------------------------ | ------------------------------------------------------------- | | `new(cx)` | 创建新的 AlertDialog | | `trigger(element)` | 设置触发对话框的元素 | | `content(builder)` | 通过 builder 函数设置内容 | | `title(title)` | 设置标题,命令式 API | | `description(desc)` | 设置描述,命令式 API | | `icon(icon)` | 设置图标,命令式 API | | `button_props(props)` | 设置按钮文本、样式和可见性 | | `show_cancel(bool)` | 显示或隐藏取消按钮,默认 `false` | | `width(px)` | 设置宽度,默认 `420px` | | `overlay_closable(bool)` | 是否允许点击遮罩关闭,默认 `false` | | `close_button(bool)` | 是否显示关闭按钮,默认 `false` | | `keyboard(bool)` | 是否支持 ESC 关闭,默认 `true` | | `on_ok(callback)` | 设置确认回调,返回 `true` 时关闭 | | `on_cancel(callback)` | 设置取消回调,返回 `true` 时关闭 | | `on_close(callback)` | 设置关闭后的回调 | ### DialogButtonProps | 方法 | 说明 | | ------------------------- | ---------------------------------------- | | `ok_text(text)` | 设置确认按钮文案,默认 `"OK"` | | `cancel_text(text)` | 设置取消按钮文案,默认 `"Cancel"` | | `ok_variant(variant)` | 设置确认按钮变体 | | `cancel_variant(variant)` | 设置取消按钮变体 | | `show_cancel(bool)` | 显示或隐藏取消按钮 | | `on_ok(callback)` | 设置确认回调 | | `on_cancel(callback)` | 设置取消回调 | ### DialogAction 点击其子元素时自动触发 `Confirm`,调用 AlertDialog 的 `on_ok`。 ```rust DialogAction::new().child( Button::new("ok").primary().label("Confirm") ) ``` 行为: * 派发 `Confirm` * 调用 `on_ok` * 回调返回 `true` 时关闭 * 返回 `false` 时保持打开 ### DialogClose 点击其子元素时自动触发 `Cancel`,调用 AlertDialog 的 `on_cancel`。 ```rust DialogClose::new().child( Button::new("cancel").outline().label("Cancel") ) ``` 行为: * 派发 `Cancel` * 调用 `on_cancel` * 回调返回 `true` 时关闭;如果没有设置回调也会关闭 * 返回 `false` 时保持打开 ## 示例 ### 删除确认 命令式 API: ```rust Button::new("delete") .danger() .label("Delete") .on_click(|_, window, cx| { window.open_alert_dialog(cx, |alert, _, _| { alert .title("Delete File?") .description("This action cannot be undone.") .button_props( DialogButtonProps::default() .ok_text("Delete") .ok_variant(ButtonVariant::Danger) .show_cancel(true) ) .on_ok(|_, window, cx| { window.push_notification("File deleted", cx); true }) }); }) ``` 声明式 API: ```rust AlertDialog::new(cx) .trigger(Button::new("delete").danger().label("Delete")) .on_ok(|_, window, cx| { window.push_notification("File deleted", cx); true }) .content(|content, _, cx| { content .child( DialogHeader::new() .child(DialogTitle::new().child("Delete File?")) .child(DialogDescription::new().child("This action cannot be undone.")) ) .child( DialogFooter::new() .child( DialogClose::new().child( Button::new("cancel").outline().label("Cancel") ) ) .child( DialogAction::new().child( Button::new("delete-confirm").danger().label("Delete") ) ) ) }) ``` ### 会话超时 ```rust window.open_alert_dialog(cx, |alert, _, _| { alert .content(|content, _, _| { content .child( DialogHeader::new() .items_center() .child(DialogTitle::new().child("Session Expired")) .child(DialogDescription::new().child( "Your session has expired due to inactivity. \ Please log in again to continue." )) ) .child( DialogFooter::new() .child( Button::new("sign-in") .label("Sign in") .primary() .flex_1() .on_click(|_, window, cx| { window.push_notification("Redirecting to login...", cx); window.close_dialog(cx); }) ) ) }) }) ``` ### 有更新可用 ```rust AlertDialog::new(cx) .trigger(Button::new("update").outline().label("Update Available")) .on_cancel(|_, window, cx| { window.push_notification("Update postponed", cx); true }) .on_ok(|_, window, cx| { window.push_notification("Starting update...", cx); true }) .content(|content, _, _| { content .child( DialogHeader::new() .child(DialogTitle::new().child("Update Available")) .child(DialogDescription::new().child( "A new version (v2.0.0) is available. \ This update includes new features and bug fixes." )) ) .child( DialogFooter::new() .child( DialogClose::new().child( Button::new("later").flex_1().outline().label("Later") ) ) .child( DialogAction::new().child( Button::new("update-now").flex_1().primary().label("Update Now") ) ) ) }) ``` ## 最佳实践 1. 简单确认场景优先使用命令式 `open_alert_dialog`。 2. 复杂布局或需要和其它组件联动时使用声明式 `trigger` + `content`。 3. 优先使用 `DialogAction` 和 `DialogClose`,而不是手动关闭对话框。 4. 对危险操作使用明确的按钮样式和文案。 5. 对结果不可逆的操作提供清晰说明。 6. 只有在确有必要时才阻止对话框关闭。 7. 保持整个应用中的按钮顺序和交互一致。 ## 相关组件 * [Dialog] * [DialogHeader] * [DialogTitle] * [DialogDescription] * [DialogFooter] * [DialogAction] * [DialogClose] [AlertDialog]: https://docs.rs/gpui-component/latest/gpui_component/dialog/struct.AlertDialog.html [Dialog]: https://docs.rs/gpui-component/latest/gpui_component/dialog/struct.Dialog.html [DialogHeader]: https://docs.rs/gpui-component/latest/gpui_component/dialog/struct.DialogHeader.html [DialogTitle]: https://docs.rs/gpui-component/latest/gpui_component/dialog/struct.DialogTitle.html [DialogDescription]: https://docs.rs/gpui-component/latest/gpui_component/dialog/struct.DialogDescription.html [DialogFooter]: https://docs.rs/gpui-component/latest/gpui_component/dialog/struct.DialogFooter.html [DialogAction]: https://docs.rs/gpui-component/latest/gpui_component/dialog/struct.DialogAction.html [DialogClose]: https://docs.rs/gpui-component/latest/gpui_component/dialog/struct.DialogClose.html --- --- url: /shell/api.md description: >- Every name a script can import or reach — the four built-in modules, the cx and window globals, and the element methods that are not styles. --- # API Reference An inventory of the script surface: what exists, and which module it comes from. The other pages explain why each thing works the way it does — this one is for looking a name up. The authority is not this page. The runtime generates `gpui-kit.d.ts` for its own version and refreshes it beside your source when the application loads. That refresh is best-effort; `gpui-shell types ` performs the same write and reports a failure. The generated header names the `gpui-shell` version and includes that application's HostModule registrations. Keep the file ignored, and put `// @ts-check` at the top of a script to have an editor check against it. The manifest's Git dependencies are not listed here either: they are linked into `node_modules` by the same refresh, and their names, signatures and documentation come from the packages themselves. See [Dependencies](./dependencies.md). ## The modules Each built-in module names the public Rust layer it exposes, so an import says which layer a script depends on. The `gpui-kit` module also carries the shell bridge needed to use GPUI from JavaScript: Views, retained entities, scheduling and shared types. A name belongs to exactly one module; nothing is re-exported for convenience. ```js import { View, div } from "gpui-kit"; import { Button, v_flex } from "gpui-base"; import { fps_monitor } from "gpui-fps"; ``` | Module | Provides | | ------------ | -------------------------------------------------------------------------------------- | | `gpui-kit` | GPUI's own elements, plus what this runtime adds: Views, the style surface, scheduling | | `gpui-base` | Layout helpers, components and the theme | | `gpui-shell` | Type-only concepts owned by the shell bridge; it has no run-time exports | | `gpui-fps` | The performance overlay | Two names are never imported, for two different reasons. `window` is a real global: nothing hands it to you, it is simply in scope. `cx` is the opposite — it is never a global, and only ever arrives as an argument: `render(cx)`, `init(props, cx)`, the second argument of every handler, the parameter of a `cx.spawn` body. The standard-runtime modules — `fs/promises`, `path`, `crypto`, `process`, `net`, `websocket` and the rest — are gated by the host's grant and are documented in [Capabilities](./capabilities.md). API shape follows the Rust original: a method on `App` is a method on `cx`, a method on `Window` is on the `window` global, an associated constructor is `Type.new(...)`, and a free function stays lowercase. Names with no direct GPUI or Base original belong to the module for the layer that implements them. Type-only names appear in these tables too, but are never run-time values. ## The `gpui-kit` module ### Elements | Name | What it is | | ------------- | ---------------------------------------------------------------------------------------------------- | | `Element` | A render-pass-owned description built by chaining methods | | `div()` | An element with no layout of its own | | `svg(path)` | A vector image from the application root, tinted by the surrounding text color | | `image(path)` | A full-color image from the application root, colors preserved | | `PathBuilder` | The GPUI path-builder type and its factory: `fill()` and `stroke(width)` each return a `PathBuilder` | | `Background` | `solid`, `stop`, `linear_gradient`, `pattern_slash`, `checkerboard` | `PathBuilder.fill()` and `.stroke(width)` return a handle that chains `move_to`, `line_to`, `curve_to`, `cubic_bezier_to`, `arc_to`, `add_polygon`, `close` and `dash_array`, and ends in `build()`. Paint the result with `window.paint_path(path, background)` — the one element constructor reached through an object, because the thing it mirrors is a method on the window. A string is an element too, exactly as `&str` implements `IntoElement` in GPUI: `.child("hello")` is how text is written, and the style comes from the element holding it. ### Views | Name | What it is | | ----------- | ------------------------------------------------------------------------- | | `View` | The base class of every View; subclass it and default-export the subclass | | `ViewClass` | A concrete `View` subclass, as `cx.new` takes it | | `Entity` | Retained ownership of one nested View: `set_props(props)`, `release()` | A subclass defines `init?(props, cx)`, which runs once, and `render(cx)`, which returns one `Element`, `Entity` or string and runs when the View is invalidated. An optional `update(props)` runs when a parent changes a nested View's props. ### Scheduling | Name | What it is | | ------- | ----------------------------------------------------------- | | `Task` | A running task: `cancel()`, `is_done()` | | `Timer` | `after(ms, handler, opts?)` and `every(ms, handler, opts?)` | ### Focus | Name | What it is | | ------------- | ----------------------------------------------------------- | | `FocusHandle` | A focus target the script owns; [its members](#focushandle) | ### Shared types | Name | What it is | | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------- | | `Length` | A number (pixels), `"12px"`, `"1.5rem"`, `"50%"` or `"auto"` | | `DefiniteLength` | The same without `"auto"` | | `AbsoluteLength` | Pixels or rems only | | `Axis` | `"horizontal"` or `"vertical"`, mirroring `gpui_kit::Axis` | | `Color` | A `gpui-base` `ColorToken`, or a `#rgb` / `#rrggbb` / `#rrggbbaa` literal | | `Role` | An accessibility role, mirroring `gpui_kit::Role` in snake\_case | | `Anchor` | Which corner of an anchored surface is pinned to its trigger | | `MouseButton` | `"left"`, `"right"` or `"middle"` | | `ClickEvent` | `click_count`, `modifiers` | | `MouseMoveEvent` | `position`, `local_position`, `bounds`, `modifiers` | | `MouseButtonEvent` | `button`, `click_count`, `position`, `modifiers`, and the local geometry once painted | | `ScrollWheelEvent` | `delta` in pixels, `delta_lines` when the device reported lines, `touch_phase` | | `KeyEvent` | `keystroke` (the whole chord; the platform modifier is spelled `cmd` on every platform), `key`, `key_char`, `modifiers`, `is_held` | | `ActionEvent` | `action` — the script's own name for it | | `KeyBinding` | One entry of `cx.bind_keys`: `keystroke`, `action`, optional `context` | | `Size` | `width`, `height` | | `Modifiers` | `shift`, `control`, `alt`, `platform` | | `Point` | `x`, `y` | | `Path` | Immutable native geometry produced by `PathBuilder.build()` | | `Background` | A reusable native background from `Background.solid(...)` or another factory: `opacity(factor)`, `color_space(space)` | | `BackgroundStop` | One gradient stop, from `Background.stop(color, percentage)` | #### `FocusHandle` Created with `cx.focus_handle()`, handed to an element with `track_focus(handle)`, and released with `release()`. | Method | What it does | | ----------------------- | ------------------------------------------------- | | `focus(): void` | Moves the keyboard onto the element tracking it | | `is_focused(): boolean` | Whether that element currently has it | | `release(): boolean` | Releases it and reports whether it was still live | ## The `gpui-shell` module These are type-only concepts introduced by the JavaScript bridge itself. Import them only for type checking; the module has no run-time values. | Name | What it is | | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `LengthString` | The string forms accepted by the shell's length bridge | | `PathCoordinate` | Pixels, or a percentage of the painted element's bounds | | `Props` | The property bag carried across the JavaScript View bridge | | `ElementBounds` | A shell event `Point` with `width` and `height` | | `ScopePhase` | `"render"`, `"event"`, `"task"`, `"layout"` or `"none"` | | `TaskOptions` | `{ owner?: View \| null }` — the View the task is cancelled with. Defaults to the running View; `null` outlives every View | | `DialogOptions` | `{ escape_dismissable?: boolean, backdrop_dismissable?: boolean }`, both `true` by default | | `ToastOptions` | `{ title: string, description?: string, level?: "info" \| "success" \| "warning" \| "error", timeout?: number \| null, id?: string }`. `level` defaults to `"info"`; `timeout` to five seconds, and `null` keeps it until dismissed | | `MotionProperty` | `"opacity"`, `"width"`, `"height"`, `"left"`, `"top"` | | `MotionEasing` | `"linear"`, `"ease-in"`, `"ease-out"`, `"ease-in-out"` | | `TransitionPolicy` | `duration`, `delay`, `easing` | | `SpringPolicy` | `response`, `damping`, `epsilon` | `ScopePhase` describes which shell call owns the current `Context`. It is unrelated to GPUI's `DispatchPhase`, which controls capture and bubble ordering during event dispatch. ## The `cx` context There are two context lifetimes with the same methods. `Context`, received by `render` and event handlers, belongs to that host call; retaining it beyond the call, including across an `await`, reports a stale-context error. `AsyncContext`, described below, is the flavour intended to survive an `await`. | Member | What it is | | -------------------------- | -------------------------------------------------------------------------------------------------- | | `notify()` | Requests a re-render; throws during `render`, because notifying yourself while rendering is a loop | | `bind_keys(bindings)` | Installs key bindings and answers how many; `App::bind_keys` | | `stop_propagation()` | Keeps this event from reaching the handlers above; `App::stop_propagation` | | `propagate()` | Undoes that within the same dispatch; `App::propagate` | | `phase()` | Which `ScopePhase` the call is in | | `theme()` | The current `gpui_kit::base::Theme` semantic token projection | | `open_url(url)` | Hands an absolute `http`/`https` URL to the system handler | | `read_from_clipboard()` | The clipboard's text, or `undefined` when it holds none | | `write_to_clipboard(text)` | Replaces the clipboard's text | | `focus_handle()` | A new `FocusHandle`; belongs in `init` or an event handler, never in `render` | | `new(Class, props?)` | Creates a retained nested View and answers the `Entity` that owns it | | `spawn(body, opts?)` | Runs `body(cx)` and adopts the promise it returns, so a rejection is reported | | `sleep(ms?)` | Resolves after `ms` on GPUI's foreground executor | | `timer` | The `Timer`: `after` and `every` | Several of these name the GPUI method they mirror: `open_url` is `App::open_url`, `read_from_clipboard` and `write_to_clipboard` are `App::read_from_clipboard` and `App::write_to_clipboard`, `focus_handle` is `App::focus_handle` (GPUI has no `FocusHandle::new`, and neither does this), `new` is `AppContext::new`, and `spawn` is `App::spawn`. ### `AsyncContext` `AsyncContext` extends `Context` and adds no members. The difference is lifetime, not surface: an ordinary `Context` speaks for one host call and reports clearly once that call has returned, while an `AsyncContext` names no call at all — it resolves whichever is running when a member is used, and refuses only when none is. It is the mirror of GPUI's `AsyncApp`. Three places hand one out: `init`, the body of `cx.spawn`, and the callbacks of `cx.timer`. Those are the three whose job is to set up or continue work that outlives the call it was started from. ## The `window` global The global has the `Window` type exported by `gpui-kit`. Nothing hands it to you and there is nothing to import at the call site. Every call reads the host call that is running now and throws outside one, so there is no handle to hold and nothing that can go stale. An overlay belongs to the window rather than to the View that opened it, which is why these are here and not on `Context`. | Member | What it is | | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | | `open_dialog(content, options?)` | Opens a dialog and answers the stack's new depth | | `close_dialog()` | Closes the topmost dialog, and answers whether it found one | | `close_all_dialogs()` | Closes every dialog, and answers how many | | `has_active_dialog()` | Whether any dialog is open; legal from `render`, unlike the rest | | `open_sheet(content)` | Opens the sheet on the right, replacing whatever was there | | `open_sheet_at(placement, content)` | The same, anchored at the `gpui-base` `Placement` you name | | `close_sheet()` | Closes the sheet, and answers whether one was open | | `has_active_sheet()` | Whether the sheet is open; legal from `render` | | `push_toast(options)` | Posts a toast and answers its id | | `remove_toast(id)` | Retracts one toast, and answers whether it was still showing | | `clear_toasts()` | Retracts every toast, and answers how many | | `paint_path(path, background)` | Paints immutable geometry with a native background; `Window::paint_path` | | `dispatch_action(action)` | Dispatches an action down this window's focus path; `Window::dispatch_action` | | `rem_size()` / `line_height()` | The window's type metrics, in pixels | | `viewport_size()` / `bounds()` | The drawable area, and where the window sits on screen | | `mouse_position()` | Where the pointer is, in window coordinates | | `appearance()` | `"light"` or `"dark"` | | `is_window_active()` / `is_fullscreen()` / `is_maximized()` | The platform window's state | | `set_rem_size(size)` | Rescales everything expressed in rems | | `refresh()` | Redraws every View in the window | | `focus_next()` / `focus_prev()` | Moves the keyboard one tab stop | | `activate_window()` / `minimize_window()` / `zoom_window()` / `toggle_fullscreen()` | Platform window controls | | `localStorage` | Web Storage backed by a file the host placed; survives a restart | | `sessionStorage` | Web Storage held in memory; goes with the process | The measurements — everything from `rem_size()` down to `is_maximized()` — are legal from `render`, because a View that sizes itself from the window has to ask during the pass that draws it. Everything that *changes* the window is refused there, for the reason `cx.notify()` is: a frame that changes the window it is drawing into is a frame arguing with itself. `open_dialog`, `open_sheet` and `open_sheet_at` take a **function returning an element**, not an element: a dialog outlives the call that opened it, and the function runs again whenever it redraws. Everything here except the two `has_active_*` queries and `paint_path` is illegal from `render`. See [Overlays](./overlays.md). ### Storage The [Web Storage API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API), unchanged. Both stores are also bare globals — `localStorage.getItem(k)` and `window.localStorage.getItem(k)` are the same call — because that is true in a browser too. | Member | What it is | | --------------------- | ---------------------------------------------- | | `length` | How many keys are stored | | `key(index)` | The key at that position, or `null` | | `getItem(key)` | The value, or `null` when the key is unset | | `setItem(key, value)` | Stores it, converting the value to a string | | `removeItem(key)` | Forgets one key | | `clear()` | Forgets all of them | | `flush()` | Resolves once the writes have reached the disk | Values are strings, so structure goes through `JSON.stringify` and `JSON.parse` exactly as it would on the web. `flush()` is the one addition: a browser never needs it, because its storage is synchronous all the way down. `localStorage` is capability-gated and throws when the host did not grant it; `sessionStorage` never is, because nothing it holds leaves the process. See [Capabilities](./capabilities.md#storage). ## The `gpui-base` module The components here own behavior, focus and what a screen reader hears, and draw next to nothing themselves. The picture is the script's, written with the [style surface](./styling.md). Each name links to the component's own page in the [gpui-base documentation](../base/index.md), which is where its full Rust surface and its behavior are described. ### Layout | Name | What it is | | ------------------------------------------------------ | ----------------------------------------------------------------------------- | | `h_flex()` | A row | | `v_flex()` | A column | | [`h_resizable(id)`](../base/primitives/resizable.md) | A row of panes with draggable dividers; sizes live in the window under the id | | [`v_resizable(id)`](../base/primitives/resizable.md) | The same, stacked | | [`resizable_panel()`](../base/primitives/resizable.md) | One pane of a resizable group, and legal nowhere else | ### Controls | Name | What it is | | ----------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | | [`Button`](../base/primitives/button.md) | Activation, focus, disabled and selected state | | [`Link`](../base/primitives/link.md) | An external HTTP(S) resource opened through the system browser | | [`Checkbox`](../base/primitives/checkbox.md) | A controlled toggle; draw the indicator yourself | | [`Switch`](../base/primitives/switch.md) | A controlled switch | | [`Radio`](../base/primitives/radio.md) | One option in a group; reports `true` only, never a deselection | | [`Toggle`](../base/primitives/toggle.md) | A button that stays down | | [`RadioGroup`](../base/primitives/radio-group.md) | A set of radios announced as one group; holds no selection | | [`ToggleGroup`](../base/primitives/toggle-group.md) | A set of toggles announced as a toolbar | | [`Tabs`](../base/primitives/tabs.md) | A tab list that holds no selection of its own | | [`Tab`](../base/primitives/tabs.md) | One tab: `selected(...)` in, `on_click(...)` out | | [`Progress`](../base/primitives/progress.md) | The announcement, not the bar; `Progress.new(...)` alone draws nothing | | [`ProgressTrack`](../base/primitives/progress.md) | The groove: a plain element you size and color | | [`ProgressIndicator`](../base/primitives/progress.md) | The filled part; set its width from the percentage you announced | | [`Avatar`](../base/primitives/avatar.md) | Renders its `image` slot, or its `fallback` when there is none; no circle, size or background of its own | | [`AvatarImage`](../base/primitives/avatar.md) | The image slot: `AvatarImage.new(path)`, and legal nowhere else | | [`AvatarFallback`](../base/primitives/avatar.md) | The fallback slot: an ordinary box holding initials, a shape or an `svg` | | [`Pagination`](../base/primitives/pagination.md) | A navigation landmark carrying the announced label; the page buttons are yours | | `pagination_items(current, total, visible?)` | Which page numbers to draw and where the gaps fall. `visible` defaults to 7, floors at 5; one page or fewer answers nothing | | [`Accordion`](../base/primitives/accordion.md) | A group holding items | | [`AccordionItem`](../base/primitives/accordion.md) | One item: `open(...)` in, the trigger's `on_change(...)` out; it passes its `open` down to both halves | | [`AccordionHeader`](../base/primitives/accordion.md) | The heading: `AccordionHeader.new(trigger)`, with `aria_level(n)` announcing its level (default 3) | | [`AccordionPanel`](../base/primitives/accordion.md) | The revealed region. Out of the tree while shut, unless `keep_mounted(true)` | | [`AccordionTrigger`](../base/primitives/accordion.md) | The button: announces the expanded state, and `on_change` asks for the other one | | [`CalendarState`](../base/primitives/calendar.md) | Retained calendar state: the month grid, the month being shown, and the chosen date | | [`SliderState`](../base/primitives/slider.md) | Retained slider state, and where a drag writes | | [`Slider`](../base/primitives/slider.md) | The root: announces the value and owns the release | | [`SliderTrack`](../base/primitives/slider.md) | The press and drag surface | | [`SliderIndicator`](../base/primitives/slider.md) | The groove, and the box every pointer position is measured against | | [`SliderThumb`](../base/primitives/slider.md) | The knob; the shell gives it a place, you give it a look | All four slider parts take the same `SliderState`, and all four are needed — a slider with no `SliderIndicator` cannot be moved at all. ### Text editing | Name | What it is | | --------------------------------------------------- | ------------------------------------------------------------------------------- | | [`InputState`](../base/primitives/input.md) | Retained text state: `InputState.new({ placeholder, value })` | | [`Input`](../base/primitives/input.md) | The frame around retained text state | | [`NumberInput`](../base/primitives/number-input.md) | A spinbutton over the same `InputState`, with three slots that all carry weight | | [`TextareaState`](../base/primitives/textarea.md) | Retained multi-line text state; `rows` is an option | | [`Textarea`](../base/primitives/textarea.md) | The frame around retained multi-line state | | [`OtpState`](../base/primitives/otp-input.md) | Retained one-time-code state; the length is fixed when it is created | | [`OtpInput`](../base/primitives/otp-input.md) | A fixed-length code whose cells the shell draws and the script styles | There is no numeric state type: an `InputState` becomes a number state by being given `set_step`, `set_min` and `set_max`. ### Containers and overlays | Name | What it is | | -------------------------------------------------- | --------------------------------------------------------------------------------------- | | [`Collapsible`](../base/primitives/collapsible.md) | Renders its `content` slot only while `open`; no role, chevron or trigger | | [`Popover`](../base/primitives/popover.md) | A surface anchored to a trigger and opened by a press | | [`HoverCard`](../base/primitives/hover-card.md) | The same, opened by resting the pointer, with its own open state | | [`Popup`](../base/primitives/popup.md) | The bare anchored surface: `Popup.new(id, trigger)`, opened by filling `content` | | [`Select`](../base/primitives/select.md) | A combobox root: the role, the announced open state, the keyboard — none of the picture | | [`Combobox`](../base/primitives/combobox.md) | The same root, announced as a combobox whose trigger is an editable field | | [`DatePicker`](../base/primitives/date-picker.md) | A date-picker root: `DatePicker.new(id, focus_handle)`; it holds no date | Two gaps are worth knowing before you build on these: arrow-key navigation of an open `Select` or `Combobox` list is yours to wire (the pieces are there — see below), and Enter and Escape do not reach a `DatePicker`. Both are described where they bite, in the declarations for each type. ### Tables and lists | Name | What it is | | ---------------------------------------------------- | ---------------------------------------------------------------------- | | [`Table`](../base/primitives/table.md) | A semantic table root, composed the way HTML composes one | | [`TableHeader`](../base/primitives/table.md) | The header row group | | [`TableBody`](../base/primitives/table.md) | The body row group | | [`TableRow`](../base/primitives/table.md) | One row: `.new(id, row_index)`, one-based | | [`TableHead`](../base/primitives/table.md) | One column header: `.new(id, column_index)`, one-based | | [`TableCell`](../base/primitives/table.md) | One data cell: `.new(id, column_index)`, one-based | | [`TableCaption`](../base/primitives/table.md) | The visual slot a caption belongs in; it carries no caption role | | [`v_virtual_list(…)`](../base/virtual-list.md) | A vertical list that describes only what is on screen | | [`h_virtual_list(…)`](../base/virtual-list.md) | The same along the other axis; `item_sizes` are widths | | [`VirtualListScrollHandle`](../base/virtual-list.md) | A virtual list's scroll position, kept across frames | | [`Scrollbar`](../base/primitives/scrollbar.md) | `new(id)`, `horizontal(id)`, `vertical(id)` — a bar you place yourself | Both virtual lists take `(id, item_count, item_sizes, get_key, render)`. `render(range, cx)` is the only callback in this API that the host calls *during* a frame, which is why handlers, retained state and `cx.notify()` are all refused inside it. ### Dock | Name | What it is | | -------------------------------------- | -------------------------------------------------------------------------------------------- | | `DockArea.new(id, options?)` | A dockable layout, retained: `options` is `{ version?: number }` | | `DockArea.register_panel(name, Class)` | Teaches the runtime to rebuild `name`'s panel from `Class`; answers with the namespaced name | | `dock_area(area)` | Draws one, and carries the six chrome handlers | | `dock_content()` | Where a dock's own panels go inside the chrome drawn around them | The area's methods are `add_panel(view, options)`, `remove_panel(id)`, `panels()`, `dump()`, `load(state)`, `has_dock`, `is_dock_open`, `toggle_dock`, `remove_dock`, `dock_size`, `set_dock_size`, `set_dock_collapsible`, `is_locked`, `set_locked`, `is_zoomed`, `zoom_out`, `on("layout_changed", handler)` and `release()`. **Every edit is applied once the call that made it has returned**, in the order the calls were made — a panel's body comes from `cx.new(Class)`, which is still being constructed — so `panels()` and `dump()` read the layout as it was before this turn's edits. See [Dock and Panels](./dock.md). ### Retained handles Each is created once — in `init` or an event handler, never in `render` — and every one of them has `release(): boolean`, which returns whether it was still live. Using a handle after releasing it throws. `on(...)` replaces the handler for that event rather than adding a second one, and answers whether there was one before. #### `InputState` From `InputState.new(options?)`, where `options` is `{ placeholder?: string, value?: string }`. | Method | What it does | | -------------------------------------- | ----------------------------------------------------------------------------------------- | | `value(): string` | The current text | | `set_value(next: string): void` | Replaces it | | `on(event, handler): boolean` | `event` is `"change"`, `"submit"`, `"focus"` or `"blur"`; the handler takes `(event, cx)` | | `set_step(step: number \| null): void` | The `NumberInput` step, or `null` for none | | `set_min(min: number \| null): void` | The numeric floor, or `null` | | `set_max(max: number \| null): void` | The numeric ceiling, or `null` | | `set_masked(masked: boolean): void` | Whether the text is drawn as a password | | `set_loading(loading: boolean): void` | Whether the field shows its loading state | #### `TextareaState` From `TextareaState.new(options?)`, where `options` is `{ placeholder?: string, value?: string, rows?: number }`. | Method | What it does | | --------------------------------------------------------- | -------------------------------------------------------------------- | | `value(): string` | The current text | | `set_value(next: string): void` | Replaces it | | `on(event, handler): boolean` | `"change"`, `"submit"`, `"focus"` or `"blur"`, handler `(event, cx)` | | `set_rows(rows: number): void` | The visible row count | | `set_auto_grow(min_rows: number, max_rows: number): void` | Grows with its content between the two | | `set_soft_wrap(wrap: boolean): void` | Whether long lines wrap | #### `SliderState` From `SliderState.new(options?)`, where `options` is `{ min?, max?, step?, scale?: "linear" | "logarithmic", value?: SliderValue }`. The defaults are `0..100` in steps of `1`, starting at `min`. A `"logarithmic"` scale needs a `min` above zero. | Method | What it does | | ------------------------------------ | -------------------------------------------------------------------------- | | `value(): SliderValue` | The current value: a number, or `[start, end]` for a range | | `set_value(next: SliderValue): void` | Replaces it | | `min_value(): number` | The floor it was built with | | `max_value(): number` | The ceiling | | `step_value(): number` | The step | | `on(event, handler): boolean` | `"change"` while dragging or `"release"` at the end; handler `(value, cx)` | #### `OtpState` From `OtpState.new(length, options?)`, where `options` is `{ value?: string, masked?: boolean }`. The length is fixed at creation. | Method | What it does | | ----------------------------------- | ---------------------------------------------------------------------------------------------------- | | `value(): string` | The digits entered so far | | `set_value(next: string): void` | Replaces them | | `len(): number` | How many digits it holds | | `is_masked(): boolean` | Whether they are drawn masked | | `set_masked(masked: boolean): void` | Changes that | | `focus(): void` | Moves the keyboard into it | | `on(event, handler): boolean` | `"change"` after each edit, `"complete"` when filled, or `"focus"` / `"blur"`; handler `(event, cx)` | #### `VirtualListScrollHandle` From `VirtualListScrollHandle.new()`, handed to a list with `track_scroll(handle)`. | Method | What it does | | ------------------------------------------------ | ------------------------------------------------------------------------------------------- | | `scroll_to_item(index: number, strategy?): void` | Puts an item on screen before the next frame; `strategy` is `"top"` (default) or `"center"` | | `scroll_to_bottom(): void` | Scrolls to the end | ### Calendar `CalendarState` exists for `month_days()` — which dates fall in which week, where the neighbouring months' days go, and how many weeks this month needs. You draw the cells. ```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's `Calendar` element is **not** bound, and that is a decision rather than an omission: it walks the same grid calling a renderer once per cell — up to forty-two crossings into JavaScript per frame, from inside GPUI's layout pass, for cells that carry no behavior. Reading the grid here and drawing it yourself is the same work without them. Dates are `"YYYY-MM-DD"`: sorting them as text sorts them by time, and `new Date(s)` reads one — which is where a weekday name or a localized month label comes from. | Method | What it does | | ------------------------------- | ----------------------------------------------------------------------------------------------------------------- | | `month_days()` | The grid, as months of weeks of days. Every week is seven days; the first and last carry the neighbouring months' | | `year()` / `month()` | The year and month (1–12) the grid is for | | `today()` | Today, as the state read it when it was created | | `value()` / `set_value(next)` | The selection: one day, a `[start, end]` range, or `null` | | `next_month()` / `prev_month()` | Moves the grid a month either way; illegal from `render` | | `on("change", handler)` | The only event, reporting a date being selected | ### Theme | Name | What it is | | --------------------- | ------------------------------------------------------------------------------ | | `set_theme(theme)` | Replaces `gpui-base`'s active semantic tokens with an application-owned theme | | `ColorToken` | The semantic color names defined by the installed palette | | `Theme` | What `cx.theme()` answers: the semantic tokens plus `appearance` and `is_dark` | | `SemanticThemeTokens` | `colors`, `spacing`, `radius` | | `ColorTokens` | One `Color` per semantic role | | `SpacingTokens` | `xxs` `xs` `sm` `md` `lg` `xl` `xxl` | | `RadiusTokens` | `none` `sm` `md` `lg` `xl` `full` | Reading the theme is `cx.theme()`. `set_theme` remains in `gpui-base` because the theme belongs to that layer. Mutation still requires a live host call and is legal only from an event handler or task, never from `render` or layout. ### Other types | Name | What it is | | ----------------------- | ------------------------------------------------------------------------------------------------------------- | | `ScrollbarMode` | `"scrolling"`, `"hover"` or `"always"` | | `ItemRange` | A virtual list's visible items, as a half-open `[start, end)` | | `SliderValue` | A number, or `[start, end]` for a range slider | | `InputEvent` | The text-state event payload; submit events carry optional `secondary` and `shift` flags | | `OtpEvent` | The currently empty OTP event payload; read the value from `OtpState` | | `PartType` | The shared `new()` shape used by `gpui-base` sub-parts without their own identity | | `Placement` | `"top"`, `"bottom"`, `"left"` or `"right"`, mirroring `gpui_kit::base::Placement` | | `ComponentType` | The shared `new(id)` shape used by identity-bearing `gpui-base` component constructors | | `DockPlacement` | `"center"`, `"left"`, `"right"` or `"bottom"` | | `DockPanel` | One panel as `panels()` reports it: `id`, `name`, `placement`, `node`, `index`, `active`, and its three flags | | `DockGroup` / `DockTab` | A tab group and one of its tabs, as `tab_bar` and `empty_group` are given them | | `DockRegion` | One dock, as the `dock` handler is given it | | `DockTile` | One tile, with already-resolved bounds | | `DockDrop` | Where a dragged panel would land | | `TileResizeSide` | `"left"`, `"right"`, `"top"`, `"bottom"` or `"bottom_right"` | ### Composition patterns Five of these components are not one element but an arrangement, and the tables above cannot say so. Each snippet below is the smallest thing that works; all of them were checked against the runtime. **A controlled control.** `Checkbox`, `Switch`, `Radio` and `Toggle` hold no state: you read the value in and write it back out. Nothing is drawn for you, so the indicator is a child. ```js Checkbox.new("done") .checked(this.checked) .on_change((checked, cx) => { this.checked = checked; cx.notify(); }) .child(this.checked ? "done" : "not done"); ``` **`Progress` announces; the bar is yours.** The root carries the role and the `0..=100` a screen reader reads, and draws nothing at all. ```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)), ); ``` **A slider is four parts, and all four are needed** — a slider with no `SliderIndicator` cannot be moved, because that is the box every pointer position is measured against. All four take the same state. ```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` owns the keyboard, `Popup` owns the surface.** The root holds the combobox role and the open state; the list is a `Popup` inside it. It needs two focus handles — one for the trigger, one for the content — and without the first nothing on screen has the keyboard. ```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)), ); ``` Arrow-key navigation of an open list is yours to write: base expects whatever is inside to run the highlight from its own key bindings, and nothing does that for you. The pieces are here — put `on_key_down` on the content element the keyboard was moved to, or bind ↑ / ↓ to actions under a `key_context` of your own. Out of the box the pointer works, Escape closes, Enter and ↓ open, and the highlight does not move. **A virtual list and its scrollbar are paired by name.** The list paints no bar of its own, and nothing checks the pairing before it runs, so both halves are needed. ```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()); ``` **A nested View is created once and mounted as a child.** `cx.new` belongs in `init` or an event handler; the entity is a child wherever a child is taken. ```js init(props, cx) { this.chart = cx.new(PriceChart, { symbol }); } render() { return v_flex().child(this.chart); } ``` ## The `gpui-fps` module | Name | What it is | | --------------- | ----------------------------------------------------------------------------- | | `fps_monitor()` | The native `gpui-fps` HUD, shared once per window and pinned to the top right | Its parent must be `relative()`. The HUD owns its own presentation; ordinary styles and children do not apply to it. ## Element methods Every element shares one prototype, so every method below type-checks on every element — which component a method actually suits is not expressed by the types. A behavior builder handed to a component that does not honour it is reported in the log rather than dropped in silence. Element builder methods answer the same element, so a chain is one expression. `map` is the exception: like GPUI's `FluentBuilder.map`, it returns exactly what its callback returns. An element is consumed when it is used as a child and belongs to the render pass that built it. ### Composition | Method | What it does | | ------------------------- | ------------------------------------------------------------------------------------------------------- | | `map(transform)` | Passes the current element to `transform` and returns its result, matching GPUI's fluent builder helper | | `child(value)` | Adds one child: an element, an `Entity`, or a string, number or boolean | | `children(iterable)` | Adds several, in order | | `when(condition, branch)` | Applies `branch` when `condition` is truthy, keeping the chain in one piece | | `id(name)` | A stable name for this element, used as its identity | ### Slots A slot is not a child: the element is consumed by the component and rendered where the component decides. | Method | What it does | | --------------------------- | ------------------------------------------------------------------------------------------ | | `content(element)` | The content of a `Collapsible`, `Popover`, `HoverCard` or `Popup` | | `image(element)` | An `Avatar`'s image slot; takes an `AvatarImage` | | `fallback(element)` | An `Avatar`'s fallback slot; takes an `AvatarFallback` | | `header(element)` | An `AccordionItem`'s header slot; takes an `AccordionHeader` | | `panel(element)` | An `AccordionItem`'s panel slot; takes an `AccordionPanel` | | `trigger(element)` | The trigger of a `Popover` or `HoverCard` | | `input(element)` | The editor slot of a `NumberInput`; empty draws the bare editor | | `decrement_button(element)` | The look of a `NumberInput`'s decrement button — replayed onto base's button, not rendered | | `increment_button(element)` | The increment button, replayed the same way | | `controls_right()` | Stacks both step buttons to the right of the text | ### Events | Method | What it delivers | | -------------------------------- | ----------------------------------------------------------------------------------- | | `on_click(handler)` | `(ClickEvent, cx)` on activation | | `on_mouse_move(handler)` | `(MouseMoveEvent, cx)` while the element is hovered | | `on_hover(handler)` | `(hovered, cx)` on both pointer entry and exit | | `on_key_down(handler)` | `(KeyEvent, cx)` while this element holds the keyboard | | `on_key_up(handler)` | `(KeyEvent, cx)` on the same focus path | | `on_mouse_down(button, handler)` | `(MouseButtonEvent, cx)` on a press of that button | | `on_mouse_up(button, handler)` | `(MouseButtonEvent, cx)` on its release | | `on_mouse_down_out(handler)` | `(MouseButtonEvent, cx)` on a press anywhere outside this element | | `on_scroll_wheel(handler)` | `(ScrollWheelEvent, cx)` on wheel or trackpad scrolling | | `on_action(action, handler)` | `(ActionEvent, cx)` when that named action is dispatched to this element or into it | | `on_change(handler)` | `(checked, cx)` on a toggle; the script owns the new value | | `on_step(handler)` | `("increment" \| "decrement", cx)`, and it **replaces** built-in stepping | | `on_item_click(handler)` | `(key, cx)` when a virtual list row is clicked, keyed rather than indexed | | `on_open_change(handler)` | `(open, cx)` when something other than the script changed a `Popover`'s open state | | `on_confirm(handler)` | Enter in an open `Select` or `Combobox`; no payload | | `on_dismiss(handler)` | Escape in an open `Select` or `Combobox`, before `on_open_change(false)` | | `on_resize(handler)` | `(sizes, cx)` once a resizable group's drag has ended | ### Actions and key bindings An action is the level above a keystroke. `cx.bind_keys` says which chord means `"save"`, in which context; `on_action("save", ...)` on an element says what `"save"` does. A menu item or a toolbar button dispatching the same name through `window.dispatch_action("save")` reaches the same handler, and neither end has to know about the other. ```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` is a predicate matched against the `key_context(...)` an element declares, so one chord can mean one thing in a list and another in an editor. Registering several `on_action`s on one element is fine and they are independent; an action none of them claims carries on to an element further out. That group — `on_key_down`, `on_key_up`, the four pointer handlers, `on_action` and `key_context` — is wired on `div`, `h_flex`, `v_flex`, `Button`, `Link`, `Checkbox`, `Switch`, `Radio`, `Toggle`, `Tabs` and `Tab`. On any other component the handler is recorded and never reaches GPUI, and the log says so — wrap it and write the handler on the wrapper. Wired is not the same as reachable. A key travels the focus path, so a component that accepts no focus handle — `Tab` — hears presses and never hears keys, however well both are wired. ### Control state | Method | What it sets | | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | `disabled(value)` | Blocks activation and reports the state; draw it yourself | | `selected(value)` | The selected state of a `Button` | | `checked(value)` | The controlled value of a `Checkbox`, `Switch` or `Radio` | | `pressed(value)` | The controlled state of a `Toggle` | | `value(percent)` | The announced progress percentage, clamped to `0..=100`; it moves nothing on screen | | `indeterminate(value)` | Withdraws a `Progress` value from the accessibility tree | | `open(value)` | Whether a `Collapsible` renders its content, or a surface is showing | | `default_open(value)` | Whether an uncontrolled `Popover` starts open | | `keep_mounted(value)` | Whether a shut `AccordionPanel` stays in the tree. Off by default; on, its content keeps a scroll position or a half-typed field across a close | | `start(value)` | Which thumb of a range slider a `SliderThumb` is | | `href(url)` | The absolute HTTP(S) target of a `Link` | ### Accessibility | Method | What it announces | | ------------------------------ | ----------------------------------------------------------------------------------------- | | `accessibility_label(text)` | What a screen reader says; an icon-only control announces nothing without it | | `role(name)` | What this element announces itself as — plain elements, `Button` and `Checkbox` only | | `aria_selected(value)` | The selected state of an option in a list the script built | | `aria_active_descendant()` | This element as the focused one while an ancestor holds the keyboard | | `set_position(position, size)` | One-based position and total size — "tab 2 of 5" | | `row_count(count)` | A `Table`'s total rows, including unrendered ones | | `column_count(count)` | A `Table`'s total columns | | `aria_level(level)` | An `AccordionHeader`'s announced heading level, default 3; it announces, it sizes nothing | | `axis(value)` | A `RadioGroup`'s or `ToggleGroup`'s orientation; semantic only, it lays out nothing | | `tooltip(text)` | A pointer-only hover label, and no substitute for `accessibility_label` | ### Focus and keyboard | Method | What it does | | ------------------------------ | --------------------------------------------------------------------- | | `track_focus(handle)` | Makes this element what the handle means | | `content_focus_handle(handle)` | Where a `Select` or `Combobox` moves the keyboard when it opens | | `tab_index(index)` | Where this element sits in the Tab order; it also makes it a tab stop | | `tab_stop(value)` | Whether Tab can land here, without changing its place in the order | ### Scrolling and panels | Method | What it does | | --------------------------------------------------- | ---------------------------------------------------------------- | | `overflow_scroll()` | Owns wheel and touch scrolling on both axes | | `overflow_x_scroll()` / `overflow_y_scroll()` | The same on one axis | | `overflow_scrollbar()` | Scrolls both axes and paints base-layer bars | | `overflow_x_scrollbar()` / `overflow_y_scrollbar()` | The same on one axis | | `mode(value)` | A `Scrollbar`'s visibility policy; omitted, it follows the theme | | `scroll_size(width, height)` | The content size a `Scrollbar` measures its thumb against | | `viewport_from_layout()` | Makes a `Scrollbar` take its viewport from its own box | | `track_scroll(handle)` | Gives a virtual list a scroll position the script can drive | | `with_item_to_measure_index(index)` | Which item a virtual list measures across the axis it scrolls | | `size_range(min, max?)` | How far a `resizable_panel()` may be dragged, in pixels | ### Anchored surfaces | Method | What it sets | | ------------------------- | ------------------------------------------------------------------------- | | `anchor(value)` | Which corner is pinned to the trigger; clamped into the window either way | | `mouse_button(value)` | Which pointer button opens a `Popover` | | `open_delay(ms)` | How long the pointer must rest on a `HoverCard` trigger; default 600 | | `close_delay(ms)` | How long a `HoverCard` waits before closing; default 300 | | `overlay_closable(value)` | Whether pressing outside an open `Popover` closes it | ### Dock commands What an element a dock's chrome drew *does*. A cached chrome description has no script event-handler lifetime, so it may not register one — a command carries no script value instead, and base does the work. Every one takes the object its handler was given as its first argument, and they belong on a `div`, an `h_flex` or a `v_flex`. | Method | On | What it does | | ------------------------------ | ----- | ------------------------------------------------- | | `select_tab(group, index)` | click | Displays that tab | | `close_panel(group, panel_id)` | click | Closes the panel, if its group allows it | | `toggle_zoom(group)` | click | Zooms the group in, or back out | | `drag_tab(group, index)` | drag | Makes the element the drag source for that tab | | `drop_tab(group, index?)` | drop | Accepts a dragged panel here; no index appends | | `toggle_dock(dock)` | click | Opens or closes the dock | | `resize_dock(dock)` | drag | Drags the dock's edge; base clamps every position | | `move_tile(tile)` | drag | Moves the tile around its canvas | | `resize_tile(tile, side)` | drag | Drags one edge or corner | | `raise_tile(tile)` | press | Brings the tile above the others | | `toggle_tile_zoom(tile)` | click | Zooms the tile to fill its dock | | `close_tile(tile)` | click | Closes the tile | ### Dock chrome Six handlers, all optional, and legal only on a `dock_area(...)`. Each is first called from inside GPUI's layout pass and given base's resolved state. Its description is cached until that state or handler changes. | Method | Draws | | ------------------------------ | --------------------------------------------------------------------- | | `tab_bar(handler)` | The tab bar above a group's displayed panel | | `empty_group(handler)` | What a group with no displayed panel shows | | `drop_indicator(handler)` | Where a dragged panel would land | | `dock(handler)` | One dock's frame around its content; place `dock_content()` inside it | | `tile_drag_bar(handler)` | The strip a tile is dragged by | | `tile_resize_handles(handler)` | A tile's resize affordances | ### Motion | Method | What it does | | ------------------------------ | ---------------------------------------------------------- | | `transition(property, policy)` | Animates later target changes entirely in native GPUI code | | `spring(property, policy?)` | Springs them instead | The property is one of `"opacity"`, `"width"`, `"height"`, `"left"`, `"top"`, and the frames never enter JavaScript. ### Style templates Each takes a function that receives a detached element to collect styles on; its return value is ignored, so a chain and a block body both work. | Method | What it styles | | ---------------------------- | ------------------------------------------------------------------------ | | `hover(declare)` | While the pointer is over the element | | `active(declare)` | While the element is pressed | | `focus(declare)` | While the element has focus | | `range_style(declare)` | The filled part of a `SliderIndicator` — how it looks, never where it is | | `cell_style(declare)` | Every cell of an `OtpInput`; without it there is nothing on screen | | `cell_active_style(declare)` | Layered on top, for the cell the next digit lands in | | `caret_style(declare)` | The blinking mark in that cell while it is empty | ### Style methods Everything else on an element is a style. There are two families, and they never overlap: * **Methods that take an argument**, bound by hand: the size, padding, margin, position, flex, border, radius and paint families. Which length type each accepts follows its Rust signature, so `.p("auto")` is a type error for the same reason it throws at run time. * **No-argument methods**, generated from GPUI's reflection table: `flex_col`, `items_center`, `gap_2`, `rounded_md`, `text_sm`, `size_full`, `truncate` and the rest. The generated declarations are the inventory for the GPUI version in your build. Both are covered in [Styling](./styling.md), along with the length and color grammars and the tokens the palette defines. ## HostModule registrations A module the host registered in Rust is imported by name, like any other module: ```js import { quotes } from "market"; ``` It is not part of any built-in module. The generated declarations carry one `declare module` per registered module, so both the module name and every export name are checked. See [HostModule](./host-module.md). --- --- url: /zh-CN/shell/api.md description: 脚本能 import 或触及的每个名字——四个内置模块、cx 与 window 全局对象,以及那些不是样式的元素方法。 --- # API Reference 脚本接口的一份清单:有什么,以及它来自哪个模块。其余页面解释每样东西为什么是这个样子——这一页是用来查名字的。 权威不在这一页。runtime 会为自己的版本生成 `gpui-kit.d.ts`,并在应用加载时尽力刷新到源码旁;`gpui-shell types ` 执行同一次写入,并会明确报告失败。生成文件的头部带有 `gpui-shell` 版本,也包含该应用注册的 HostModule 。请忽略这个文件而不要提交,并在脚本顶部写上 `// @ts-check` 让编辑器照着它检查。manifest 声明的 Git 依赖同样不在这一页:它们由同一次刷新链接进 `node_modules`,名字、签名与文档都来自 package 自身。见[依赖](./dependencies.md)。 ## 模块 每个内建模块都以它所暴露的公开 Rust 层命名,所以一条 import 能说明脚本依赖哪一层。`gpui-kit` 还包含从 JavaScript 使用 GPUI 所需的 shell 桥接: View、留存实体、调度与共享类型。一个名字只属于一个模块,这里不为了方便做 re-export。 ```js import { View, div } from "gpui-kit"; import { Button, v_flex } from "gpui-base"; import { fps_monitor } from "gpui-fps"; ``` | 模块 | 提供 | | ------------ | ---------------------------------------------------------------- | | `gpui-kit` | GPUI 自己的元素,加上这个运行时补上的部分: View、样式接口与调度 | | `gpui-base` | 布局辅助函数、组件与主题 | | `gpui-shell` | shell 桥接层自有的纯类型概念;没有运行时导出 | | `gpui-fps` | 性能 HUD | 有两个名字从不需要 import,但原因不同。`window` 是真正的全局:没有谁把它交给你,它本来就在作用域里。`cx` 恰恰相反——它从来不是全局的,只会作为参数到达:`render(cx)`、`init(props, cx)`、每个处理器的第二个参数、`cx.spawn` body 的形参。标准运行时模块——`fs/promises`、`path`、`crypto`、`process`、`net`、`websocket` 等等——受 Host 授权门控,记录在 [Capabilities](./capabilities.md)。 API 形态跟随 Rust 原型:`App` 上的方法放在 `cx`,`Window` 上的方法放在 `window` 全局对象,关联构造器写成 `Type.new(...)`,自由函数保持小写。没有直接 GPUI 或 Base 原型的名字,属于实现它的公开层。表中也会列出仅存在于类型系统的名字,但它们不是运行时可调用的值。 ## `gpui-kit` 模块 ### 元素 | 名称 | 说明 | | ------------- | ----------------------------------------------------------------------------------- | | `Element` | 通过链式方法构建、只属于当前 render pass 的描述 | | `div()` | 自身不带布局的元素 | | `svg(path)` | 来自应用根目录的矢量图,按周围的文字颜色着色 | | `image(path)` | 来自应用根目录的全彩图片,保留原色 | | `PathBuilder` | GPUI 的路径构建器类型及其工厂;`fill()` 与 `stroke(width)` 都返回一个 `PathBuilder` | | `Background` | `solid`、`stop`、`linear_gradient`、`pattern_slash`、`checkerboard` | `PathBuilder.fill()` 与 `.stroke(width)` 返回一个句柄,可链式调用 `move_to`、`line_to`、`curve_to`、`cubic_bezier_to`、`arc_to`、`add_polygon`、`close` 与 `dash_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)`,以及返回一个 `Element`、`Entity` 或字符串、在 View 被置为失效时执行的 `render(cx)`。可选的 `update(props)` 在父 View 改变嵌套 View 的 props 时执行。 ### 调度 | 名称 | 说明 | | ------- | ---------------------------------------------------------- | | `Task` | 一个正在运行的任务:`cancel()`、`is_done()` | | `Timer` | `after(ms, handler, opts?)` 与 `every(ms, handler, opts?)` | ### 焦点 | 名称 | 说明 | | ------------- | ------------------------------------------------ | | `FocusHandle` | 脚本自己持有的焦点目标;[它的成员](#focushandle) | ### 共享类型 | 名称 | 说明 | | ------------------ | -------------------------------------------------------------------------------------------------------- | | `Length` | 数字(像素)、`"12px"`、`"1.5rem"`、`"50%"` 或 `"auto"` | | `DefiniteLength` | 同上,但不含 `"auto"` | | `AbsoluteLength` | 只有像素或 rem | | `Axis` | `"horizontal"` 或 `"vertical"`,镜像 `gpui_kit::Axis` | | `Color` | 一个 `gpui-base` 的 `ColorToken`,或 `#rgb` / `#rrggbb` / `#rrggbbaa` 字面量 | | `Role` | 一个无障碍 role,镜像 `gpui_kit::Role` 的 snake\_case 拼写 | | `Anchor` | 锚定浮层的哪个角固定在它的触发元素上 | | `MouseButton` | `"left"`、`"right"` 或 `"middle"` | | `ClickEvent` | `click_count`、`modifiers` | | `MouseMoveEvent` | `position`、`local_position`、`bounds`、`modifiers` | | `MouseButtonEvent` | `button`、`click_count`、`position`、`modifiers`,以及元素绘制之后才有的局部几何 | | `ScrollWheelEvent` | 以像素表示的 `delta`;设备按行上报时还有 `delta_lines`;以及 `touch_phase` | | `KeyEvent` | `keystroke`(整个组合键,平台修饰键在所有平台上都拼作 `cmd`)、`key`、`key_char`、`modifiers`、`is_held` | | `ActionEvent` | `action`——脚本给这个 action 起的名字 | | `KeyBinding` | `cx.bind_keys` 的一项:`keystroke`、`action`、可选的 `context` | | `Modifiers` | `shift`、`control`、`alt`、`platform` | | `Point` | `x`、`y` | | `Size` | `width`、`height` | | `Path` | 由 `PathBuilder.build()` 产出的不可变原生几何 | | `Background` | 由 `Background.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 桥接层自身引入的纯类型概念。它们只用于类型检查;这个模块没有运行时值。 | 名称 | 说明 | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `LengthString` | shell 长度桥接接受的字符串形式 | | `PathCoordinate` | 像素,或所绘元素边界的百分比 | | `Props` | 跨 JavaScript View 桥接传递的属性包 | | `ElementBounds` | shell 事件使用的、带 `width` 与 `height` 的 `Point` | | `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"` | | `TransitionPolicy` | `duration`、`delay`、`easing` | | `SpringPolicy` | `response`、`damping`、`epsilon` | `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_kit::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 | | `timer` | `Timer`:`after` 与 `every` | 其中好几个都指名了它所镜像的 GPUI 方法:`open_url` 是 `App::open_url`,`read_from_clipboard` 与 `write_to_clipboard` 是 `App::read_from_clipboard` 与 `App::write_to_clipboard`,`focus_handle` 是 `App::focus_handle`(GPUI 没有 `FocusHandle::new`,这里同样没有),`new` 是 `AppContext::new`,`spawn` 是 `App::spawn`。 ### `AsyncContext` `AsyncContext` 继承 `Context`,不增加任何成员。区别在生命周期,不在接口:普通的 `Context` 只为一次 Host 调用发言,一旦那次调用返回就明确报错;而 `AsyncContext` 不指名任何一次调用——用到它时才解析当时正在执行的那一次,只有在一次都没有时才拒绝。它对应 GPUI 的 `AsyncApp`。 有三处会交出一个:`init`、`cx.spawn` 的 body,以及 `cx.timer` 的回调。这三处的职责正是「安排或延续比启动它的那次调用活得更久的工作」。 ## `window` 全局对象 这个全局对象的类型是 `gpui-kit` 导出的 `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()` | 平台窗口控制 | | `localStorage` | Web Storage,背后是 Host 放好的一个文件,跨重启存活 | | `sessionStorage` | Web Storage,只在内存里,随进程一起消失 | 上面这些度量——从 `rem_size()` 一直到 `is_maximized()`——在 `render` 中都是合法的:一个要按窗口尺寸决定自身大小的 View,只能在绘制它的那一趟里问。而所有*改变*窗口的调用在 `render` 中都会被拒绝,理由和 `cx.notify()` 一样:一帧去改自己正在绘制的窗口,就是这一帧在和自己较劲。 `open_dialog`、`open_sheet` 与 `open_sheet_at` 接受的是**一个返回元素的函数**,而不是元素:dialog 活得比打开它的那次调用久,每次重绘时这个函数都会再执行一次。除了两个 `has_active_*` 查询与 `paint_path`,这里的一切在 `render` 中都不合法。见 [Overlays](./overlays.md)。 ### 存储 [Web Storage API](https://developer.mozilla.org/zh-CN/docs/Web/API/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.stringify` 与 `JSON.parse`。`flush()` 是唯一多出来的成员:浏览器不需要它,因为它的存储从头到尾都是同步的。`localStorage` 受 capability 管辖,Host 没授权时抛异常;`sessionStorage` 不受管辖,因为它持有的东西从不离开进程。见 [Capabilities](./capabilities.md#storage)。 ## `gpui-base` 模块 这里的组件拥有行为、焦点,以及屏幕阅读器听到的内容,而自身几乎什么都不画。画面归脚本所有,用[样式接口](./styling.md)写出来。每个名字都链接到它在 [gpui-base 文档](../../base/index.md)里的页面,那里描述了它完整的 Rust 接口与行为。 ### 布局 | 名称 | 说明 | | --------------------------------------------------------- | -------------------------------------------------- | | `h_flex()` | 一行 | | `v_flex()` | 一列 | | [`h_resizable(id)`](../../base/primitives/resizable.md) | 一行带可拖拽分隔条的窗格;尺寸按这个 id 存在窗口里 | | [`v_resizable(id)`](../../base/primitives/resizable.md) | 同上,纵向堆叠 | | [`resizable_panel()`](../../base/primitives/resizable.md) | 可调整组里的一个窗格,用在别处都不合法 | ### 控件 | 名称 | 说明 | | -------------------------------------------------------- | ---------------------------------------------------------------------------------------- | | [`Button`](../../base/primitives/button.md) | 激活、焦点、disabled 与 selected 状态 | | [`Link`](../../base/primitives/link.md) | 通过系统浏览器打开的外部 HTTP(S) 资源 | | [`Checkbox`](../../base/primitives/checkbox.md) | 受控的勾选;勾选标记自己画 | | [`Switch`](../../base/primitives/switch.md) | 受控的 switch | | [`Radio`](../../base/primitives/radio.md) | 一组中的一个选项;只报告 `true`,从不报告取消选中 | | [`Toggle`](../../base/primitives/toggle.md) | 一个会保持按下的按钮 | | [`RadioGroup`](../../base/primitives/radio-group.md) | 被报读为一组的一批 radio;自身不持有选中项 | | [`ToggleGroup`](../../base/primitives/toggle-group.md) | 被报读为 toolbar 的一批 toggle | | [`Tabs`](../../base/primitives/tabs.md) | 自身不持有选中项的 tab 列表 | | [`Tab`](../../base/primitives/tabs.md) | 一个 tab:`selected(...)` 进,`on_click(...)` 出 | | [`Progress`](../../base/primitives/progress.md) | 只有报读,没有进度条;单独的 `Progress.new(...)` 什么都不画 | | [`ProgressTrack`](../../base/primitives/progress.md) | 凹槽:一个由你设定尺寸与颜色的普通元素 | | [`ProgressIndicator`](../../base/primitives/progress.md) | 已填充的部分;按你报读的百分比设置它的宽度 | | [`Avatar`](../../base/primitives/avatar.md) | 渲染它的 `image` 槽;没有图片时渲染 `fallback`。它自己不画圆形、尺寸或背景 | | [`AvatarImage`](../../base/primitives/avatar.md) | 图片槽:`AvatarImage.new(path)`,用在别处无效 | | [`AvatarFallback`](../../base/primitives/avatar.md) | 兜底槽:一个普通盒子,放首字母、图形或 `svg` | | [`Pagination`](../../base/primitives/pagination.md) | 一个 navigation landmark,带报读的标签;页码按钮由脚本自己画 | | `pagination_items(current, total, visible?)` | 该画哪些页码、省略号落在哪。`visible` 默认 7,最小 5;总页数 ≤ 1 时返回空 | | [`Accordion`](../../base/primitives/accordion.md) | 一个 group,装 item | | [`AccordionItem`](../../base/primitives/accordion.md) | 一个条目:`open(...)` 进,trigger 的 `on_change(...)` 出;它把自己的 `open` 传给下面两半 | | [`AccordionHeader`](../../base/primitives/accordion.md) | 标题:`AccordionHeader.new(trigger)`,`aria_level(n)` 报读层级(默认 3) | | [`AccordionPanel`](../../base/primitives/accordion.md) | 展开的区域。关闭时不在树里,除非 `keep_mounted(true)` | | [`AccordionTrigger`](../../base/primitives/accordion.md) | 按钮:报读展开状态,`on_change` 请求相反的那个 | | [`CalendarState`](../../base/primitives/calendar.md) | 留存的日历状态:月网格、当前月份、选中的日期 | | [`SliderState`](../../base/primitives/slider.md) | 留存的 slider 状态,也是一次拖拽写入的地方 | | [`Slider`](../../base/primitives/slider.md) | 根:报读数值,并拥有 release | | [`SliderTrack`](../../base/primitives/slider.md) | 按下与拖拽的表面 | | [`SliderIndicator`](../../base/primitives/slider.md) | 凹槽,也是每个指针位置据以测量的那个盒子 | | [`SliderThumb`](../../base/primitives/slider.md) | 滑块;shell 给它位置,你给它外观 | slider 的四个部件接受同一个 `SliderState`,而且四个都不能少——没有 `SliderIndicator` 的 slider 根本拖不动。 ### 文本编辑 | 名称 | 说明 | | ------------------------------------------------------ | ----------------------------------------------------------- | | [`InputState`](../../base/primitives/input.md) | 留存的文本状态:`InputState.new({ placeholder, value })` | | [`Input`](../../base/primitives/input.md) | 包住留存文本状态的框 | | [`NumberInput`](../../base/primitives/number-input.md) | 建立在同一个 `InputState` 上的 spinbutton,三个插槽都有分量 | | [`TextareaState`](../../base/primitives/textarea.md) | 留存的多行文本状态;`rows` 是一个选项 | | [`Textarea`](../../base/primitives/textarea.md) | 包住留存多行状态的框 | | [`OtpState`](../../base/primitives/otp-input.md) | 留存的一次性验证码状态;长度在创建时固定 | | [`OtpInput`](../../base/primitives/otp-input.md) | 定长验证码,格子由 shell 画、由脚本设定样式 | 没有专门的数字状态类型:给 `InputState` 设上 `set_step`、`set_min` 与 `set_max`,它就成了数字状态。 ### 容器与浮层 | 名称 | 说明 | | ----------------------------------------------------- | ----------------------------------------------------------------- | | [`Collapsible`](../../base/primitives/collapsible.md) | 仅在 `open` 时渲染它的 `content` 插槽;不带 role、箭头或触发器 | | [`Popover`](../../base/primitives/popover.md) | 锚定在触发元素上、由按下打开的浮层 | | [`HoverCard`](../../base/primitives/hover-card.md) | 同上,但由指针停留打开,并有自己的打开状态 | | [`Popup`](../../base/primitives/popup.md) | 光秃秃的锚定浮层:`Popup.new(id, trigger)`,填入 `content` 即打开 | | [`Select`](../../base/primitives/select.md) | combobox 的根:role、报读的打开状态、键盘——但不含任何画面 | | [`Combobox`](../../base/primitives/combobox.md) | 同一个根,被报读为一个触发器是可编辑输入框的 combobox | | [`DatePicker`](../../base/primitives/date-picker.md) | 日期选择器的根:`DatePicker.new(id, focus_handle)`;它不持有日期 | 在这些之上动手之前,有两处缺口值得先知道:打开的 `Select` 或 `Combobox` 列表的方向键导航要你自己接(零件都在,见下),而 Enter 与 Escape 到不了 `DatePicker`。两者都写在各自类型的声明里,也就是它们真正咬人的地方。 ### 表格与列表 | 名称 | 说明 | | ------------------------------------------------------- | --------------------------------------------------------------------- | | [`Table`](../../base/primitives/table.md) | 语义表格的根,组合方式与 HTML 组合表格一致 | | [`TableHeader`](../../base/primitives/table.md) | 表头行组 | | [`TableBody`](../../base/primitives/table.md) | 表体行组 | | [`TableRow`](../../base/primitives/table.md) | 一行:`.new(id, row_index)`,从 1 开始 | | [`TableHead`](../../base/primitives/table.md) | 一个列头:`.new(id, column_index)`,从 1 开始 | | [`TableCell`](../../base/primitives/table.md) | 一个数据单元格:`.new(id, column_index)`,从 1 开始 | | [`TableCaption`](../../base/primitives/table.md) | caption 该在的视觉位置;它不带 caption role | | [`v_virtual_list(…)`](../../base/virtual-list.md) | 只描述屏幕内内容的纵向列表 | | [`h_virtual_list(…)`](../../base/virtual-list.md) | 另一个轴上的同一件事;`item_sizes` 是宽度 | | [`VirtualListScrollHandle`](../../base/virtual-list.md) | 虚拟列表的滚动位置,跨帧保留 | | [`Scrollbar`](../../base/primitives/scrollbar.md) | `new(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_dock`、`is_dock_open`、`toggle_dock`、`remove_dock`、`dock_size`、`set_dock_size`、`set_dock_collapsible`、`is_locked`、`set_locked`、`is_zoomed`、`zoom_out`、`on("layout_changed", handler)` 与 `release()`。 **每一次编辑都在发起它的那次调用返回之后按调用顺序应用**——面板的主体来自 `cx.new(Class)`,那时它自己还在构造中——所以 `panels()` 与 `dump()` 读到的是本轮编辑之前的布局。见 [Dock 与面板](./dock.md)。 ### 留存句柄 每一个都只创建一次——在 `init` 或事件处理器里,绝不在 `render` 里——并且每一个都有 `release(): boolean`,返回它当时是否还活着。释放之后再用会抛异常。 `on(...)` 是替换该事件的处理器,而不是再加一个,返回值表示之前是否已经有一个。 #### `InputState` 来自 `InputState.new(options?)`,其中 `options` 是 `{ placeholder?: string, value?: string }`。 | 方法 | 说明 | | -------------------------------------- | ---------------------------------------------------------------------------------- | | `value(): string` | 当前文本 | | `set_value(next: string): void` | 替换它 | | `on(event, handler): boolean` | `event` 为 `"change"`、`"submit"`、`"focus"` 或 `"blur"`;handler 收 `(event, cx)` | | `set_step(step: number \| null): void` | `NumberInput` 的步长,`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` | 已安装调色板定义的语义颜色名称 | | `Theme` | `cx.theme()` 返回的东西:语义 token,加上 `appearance` 与 `is_dark` | | `SemanticThemeTokens` | `colors`、`spacing`、`radius` | | `ColorTokens` | 每个语义角色一个 `Color` | | `SpacingTokens` | `xxs` `xs` `sm` `md` `lg` `xl` `xxl` | | `RadiusTokens` | `none` `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 事件带可选的 `secondary` 与 `shift` 标志 | | `OtpEvent` | 当前为空的 OTP 事件 payload;值从 `OtpState` 读取 | | `PartType` | `gpui-base` 中没有自身身份的子部件共同使用的 `new()` 形态 | | `Placement` | `"top"`、`"bottom"`、`"left"` 或 `"right"`,镜像 `gpui_kit::base::Placement` | | `ComponentType` | `gpui-base` 中带身份的组件构造器共同使用的 `new(id)` 形态 | | `DockPlacement` | `"center"`、`"left"`、`"right"` 或 `"bottom"` | | `DockPanel` | `panels()` 报告的一块面板:`id`、`name`、`placement`、`node`、`index`、`active` 与三个标志位 | | `DockGroup` / `DockTab` | 一个标签组与它的一个标签页,也就是 `tab_bar` 与 `empty_group` 拿到的东西 | | `DockRegion` | 一侧 dock,也就是 `dock` handler 拿到的东西 | | `DockTile` | 一个 tile,bounds 已经解析好 | | `DockDrop` | 被拖动的面板会落在哪里 | | `TileResizeSide` | `"left"`、`"right"`、`"top"`、`"bottom"` 或 `"bottom_right"` | ### 组合模式 其中五个组件不是一个元素,而是一种搭法,上面的表格说不出这件事。下面每段都是能跑起来的最小写法,并且都经过运行时校验。 **受控控件。** `Checkbox`、`Switch`、`Radio` 与 `Toggle` 自身不持有状态:值由你读进去、再写回来。它们什么都不画,所以勾选标记是一个子元素。 ```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)` | `Collapsible`、`Popover`、`HoverCard` 或 `Popup` 的内容 | | `image(element)` | `Avatar` 的图片槽,接受一个 `AvatarImage` | | `fallback(element)` | `Avatar` 的兜底槽,接受一个 `AvatarFallback` | | `header(element)` | `AccordionItem` 的 header 槽,接受一个 `AccordionHeader` | | `panel(element)` | `AccordionItem` 的 panel 槽,接受一个 `AccordionPanel` | | `trigger(element)` | `Popover` 或 `HoverCard` 的触发器 | | `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)` | 在打开的 `Select` 或 `Combobox` 中按下回车;无参数 | | `on_dismiss(handler)` | 在打开的 `Select` 或 `Combobox` 中按下 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_down`、`on_key_up`、四个指针事件、`on_action` 与 `key_context`——接线在 `div`、`h_flex`、`v_flex`、`Button`、`Link`、`Checkbox`、`Switch`、`Radio`、`Toggle`、`Tabs` 与 `Tab` 上。写在其余组件上的处理器会被记录下来但永远到不了 GPUI,日志里会说明——把它包一层,写在外层元素上。 接线了不等于收得到。按键沿焦点路径传递,所以一个不接受脚本焦点句柄的组件——比如 `Tab`——听得到按下、永远听不到按键,无论两者接线得多好。 ### 控件状态 | 方法 | 设置什么 | | ---------------------- | -------------------------------------------------------------------------------------------------- | | `disabled(value)` | 阻止激活并报告该状态;外观自己画 | | `selected(value)` | `Button` 的 selected 状态 | | `checked(value)` | `Checkbox`、`Switch` 或 `Radio` 的受控值 | | `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)` | 这个元素把自己报读成什么——仅限朴素元素、`Button` 与 `Checkbox` | | `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)` | `RadioGroup` 或 `ToggleGroup` 的方向;只有语义,不做任何布局 | | `tooltip(text)` | 只对指针有效的悬停说明,不能替代 `accessibility_label` | ### 焦点与键盘 | 方法 | 作用 | | ------------------------------ | -------------------------------------------------------- | | `track_focus(handle)` | 让这个元素成为该 handle 所指的对象 | | `content_focus_handle(handle)` | `Select` 或 `Combobox` 打开时把键盘移到哪里 | | `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 拿到的那个对象;它们只能挂在 `div`、`h_flex` 或 `v_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_col`、`items_center`、`gap_2`、`rounded_md`、`text_sm`、`size_full`、`truncate` 以及这一族的其余成员。生成的声明就是当前构建所用 GPUI 版本的完整清单。 两者都记录在 [Styling](./styling.md) 里,还有长度与颜色的语法,以及调色板定义的 token。 ## HostModule Host 在 Rust 侧注册的模块,按名字 import,和其它模块没有区别: ```js import { quotes } from "market"; ``` 它不属于任何内建模块。生成的类型声明里每个注册过的模块各有一段 `declare module`,所以模块名和每一个导出名都会被检查。见 [HostModule](./host-module.md)。 --- --- url: /apps.md description: >- Desktop apps built with GPUI Kit — from a production trading terminal to database clients, terminals, and system utilities. --- --- --- url: /docs/components/attachment.md description: >- A composable file and media attachment surface with lifecycle states, previews, and actions. --- # Attachment `Attachment` presents one file or media item. It provides stable layout for a media preview, metadata, and optional actions while leaving upload state, selection, retry, and navigation in the application. Each public slot is styleable and accepts arbitrary GPUI children. The component is intentionally a composition primitive. `AttachmentActions` does not invent an attachment-specific action model; put `Button`, `Link`, or another semantic control inside it. `AttachmentGroup` only owns horizontal spacing and scrolling. Selection and preview behavior remain application concerns. ## Import ```rust use gpui_kit::{Axis, ParentElement as _, Styled as _}; use gpui_kit::component::{ ActiveTheme as _, Colorize as _, Icon, IconName, Sizable as _, Size, attachment::{ Attachment, AttachmentActions, AttachmentContent, AttachmentDescription, AttachmentGroup, AttachmentMedia, AttachmentStatus, AttachmentTitle, }, button::{Button, ButtonVariants as _}, badge::Badge, progress::Progress, shimmer::ShimmerStyle, spinner::Spinner, }; ``` ## Anatomy and basic usage The typed builders make the common file shape explicit: ```rust Attachment::new() .media(AttachmentMedia::new().child(Icon::new(IconName::FileText))) .content( AttachmentContent::new() .title(AttachmentTitle::new("quarterly-report.pdf")) .description(AttachmentDescription::new("PDF · 2.4 MB")), ) .actions( AttachmentActions::new().child( Button::new("remove-report") .ghost() .xsmall() .icon(IconName::Close) .label("Remove"), ), ) ``` The slots are optional. A media-only attachment, metadata-only attachment, or action-only attachment is valid when the product needs it: ```rust Attachment::new() .media(AttachmentMedia::new().child(Icon::new(IconName::FileText))); Attachment::new().content( AttachmentContent::new() .title(AttachmentTitle::new("notes.txt")) .description(AttachmentDescription::new("TXT · 12 KB")), ) ``` The default state is: | Property | Default | Meaning | | --- | --- | --- | | Status | `Complete` | The item is ready. | | Size | `Medium` | Uses the standard conversation density. | | Axis | `Horizontal` | Media, metadata, and actions share one row. | | Media/content/actions | absent | Add only the slots the item needs. | | Surface | `background` and `foreground` | The card surface, separated by the border like shadcn's `bg-card`. | | Radius | `radius_2xl()` (`radius_xl` for `XSmall`) | Shared semantic radius. | `Attachment` sizes itself to its content and never owns a product-level file model. Keep the file ID and state in the parent view, then render the current record into this element. ## Media and image previews Use children for an icon-style media slot and `src(...)` for an image preview: ```rust Attachment::new() .media( AttachmentMedia::new() .src("https://example.com/previews/sdk.svg") .overlay(Icon::new(IconName::Download)), ) .content( AttachmentContent::new() .title(AttachmentTitle::new("sdk-preview.svg")) .description(AttachmentDescription::new("SVG · 1280 × 720")), ) ``` The image is rendered with `ObjectFit::Cover` inside the media bounds. Children and `overlay(...)` are painted above the image. `overlay(...)` centers an element over the whole media area, which is useful for a spinner, play icon, or preview action: ```rust Attachment::new() .status(AttachmentStatus::Uploading) .axis(Axis::Vertical) .media( AttachmentMedia::new() .src(preview_url) .overlay(Spinner::new().small()), ) ``` Only a source image is dimmed while `Uploading`, `Processing`, or `Failed`. Overlays and custom children keep full contrast. With no source, the media slot is a themed muted area; in a failed state it uses the destructive semantic surface and foreground so an error icon remains legible. `AttachmentMedia` is independently styleable. Use `with_size(...)` to override the inherited media size, or use normal GPUI refinements for a custom preview ratio and surface: ```rust AttachmentMedia::new() .with_size(Size::Large) .aspect_ratio(16. / 9.) .rounded(cx.theme().radius_lg) .child(Icon::new(IconName::Image)) ``` An explicit media size takes precedence over the attachment size. A vertical attachment makes the media full width and square by default; the media's own style can replace that geometry when the application has a different preview design. ## Lifecycle states `AttachmentStatus` has five explicit states. The parent status is passed to the typed title, description, media, and action layout during rendering: | State | Surface/layout behavior | Recommended content | | --- | --- | --- | | `Pending` | Dashed border; preview is not dimmed. | “Ready to upload” and a start action. | | `Uploading` | Preview dims; typed title shimmers. | Progress value and a Cancel button. | | `Processing` | Preview dims; typed title shimmers. | “Processing…” and a non-destructive wait state. | | `Failed` | Destructive border/description; preview dims when present. | Error reason plus Retry or Remove. | | `Complete` | Ready surface; preview is full opacity. | File metadata and normal actions. | ```rust Attachment::new() .status(AttachmentStatus::Uploading) .media(AttachmentMedia::new().child(Icon::new(IconName::FileText))) .content( AttachmentContent::new() .title(AttachmentTitle::new("design-assets.zip")) .description(AttachmentDescription::new("Uploading · 68%")) .child(Progress::new("attachment-progress").value(68.)), ) .actions( AttachmentActions::new() .child(Button::new("cancel-upload").ghost().xsmall().label("Cancel")), ) ``` The status helpers are useful when application state maps to presentation: ```rust match status { AttachmentStatus::Pending => "Ready to upload", AttachmentStatus::Uploading => "Uploading…", AttachmentStatus::Processing => "Processing…", AttachmentStatus::Failed => "Upload failed", AttachmentStatus::Complete => "Ready", } ``` `is_pending()`, `is_uploading()`, `is_processing()`, `is_failed()`, `is_complete()`, and `is_in_progress()` are pure readers. They do not update the attachment or the application upload task. ## Status inheritance and overrides Titles and descriptions added through their typed builders inherit the parent status. An explicit child status wins over the inherited value: ```rust Attachment::new() .status(AttachmentStatus::Failed) .content( AttachmentContent::new() .title(AttachmentTitle::new("archive.zip")) .description( AttachmentDescription::new("Previous upload completed") .status(AttachmentStatus::Complete), ), ) ``` Use typed `.title(...)` and `.description(...)` whenever loading shimmer or failure coloring should follow the attachment. The generic `.child(...)` form still accepts arbitrary elements, but it cannot inspect the erased child's status and therefore does not inherit automatically: ```rust AttachmentContent::new() .title(AttachmentTitle::new("status-aware-title")) .description(AttachmentDescription::new("status-aware-description")) .child(custom_metadata_element) ``` Customize an in-progress title with a reusable shimmer style: ```rust AttachmentTitle::new("transcript.pdf") .with_shimmer_style( ShimmerStyle::new() .duration(std::time::Duration::from_secs(3)) .spread(0.45) .reverse(true) .once(false), ) ``` `AttachmentDescription` uses the destructive semantic color only for an explicit or inherited `Failed` status. The words in the description should still state what happened; color is a supporting cue. ## Sizes and axes `Attachment` implements `Sizable`. The convenience builders map to `Size`: ```rust Attachment::new().xsmall(); Attachment::new().small(); Attachment::new(); // medium (default) Attachment::new().large(); Attachment::new().w_72() // application-owned width when a fixed measure is needed ``` The named sizes adjust gap, typography, padding, media baseline, and radius as one scale. Use them to keep attachments aligned with other component densities. `Size::Size(...)` is a custom density value, not a width setter; use the normal GPUI width refinements (`w_72()`, `w(...)`, or a parent layout) when the product needs a fixed measure. Named sizes are preferable for a coherent theme. Horizontal is the default and keeps the media, metadata, and actions in one row. Vertical moves the preview above the metadata and places actions over the preview's upper trailing corner: ```rust Attachment::new() .axis(Axis::Vertical) .large() .media(AttachmentMedia::new().src(preview_url)) .content( AttachmentContent::new() .title(AttachmentTitle::new("presentation.png")) .description(AttachmentDescription::new("PNG · 1920 × 1080")), ) .actions( AttachmentActions::new() .child(Button::new("remove-presentation").ghost().xsmall().label("Remove")), ) ``` The vertical default is square media. Set a media aspect ratio or size when the content needs a landscape preview. `AttachmentContent` and `AttachmentActions` remain independent slots, so an application can omit one or place additional controls in either. ## Content and actions `AttachmentContent` keeps titles and descriptions in a vertical metadata stack. It also accepts custom children for progress, badges, or a second line: ```rust AttachmentContent::new() .title(AttachmentTitle::new("report.pdf")) .description(AttachmentDescription::new("PDF · 2.4 MB")) .child(Badge::new().count(3)) ``` Use `AttachmentActions` for one or more existing semantic controls: ```rust AttachmentActions::new() .child(Button::new("download").ghost().xsmall().label("Download")) .child(Button::new("remove").danger().xsmall().label("Remove")) ``` `AttachmentActions` only supplies layout and does not make its children focusable, clickable, or disabled. A tooltip is supplemental; the current `Button` implementation derives its accessibility label from `.label(...)`, so use a visible label when an action must have a named accessible control. An icon-only button with only `.tooltip(...)` is not a substitute for that label. ## Whole-card click Set `.id(...)` and `.on_click(...)` to make the whole card activate, e.g. to open a preview. The click layer is painted below `AttachmentActions`, so action buttons stay independently clickable: ```rust Attachment::new() .id("design-attachment") .on_click(|_, window, cx| { // Open the preview. }) .content( AttachmentContent::new() .title(AttachmentTitle::new("design-mockups.png")) .description(AttachmentDescription::new("PNG · 1.8 MB")), ) .actions( AttachmentActions::new() .child(Button::new("remove").ghost().xsmall().icon(IconName::Close)), ) ``` The handler takes effect only together with `.id(...)`; click state needs that stable identity. A clickable card shows a muted hover surface so it reads as interactive. What activation means — a dialog, a browser, a file viewer, or a selection — stays with the application. Keep destructive and secondary commands in `AttachmentActions` so they never depend on the card's primary activation, and offer the card's primary action as a `Button` or `Link` somewhere reachable from the keyboard: the click layer itself is a pointer convenience and takes no focus. ## Groups `AttachmentGroup` provides a horizontally scrollable row with the shared group gap. Its ID is required because it owns GPUI's element-local scroll state: ```rust AttachmentGroup::new("message-attachments") .child(first_attachment) .child(second_attachment) .child(third_attachment) ``` The group is `w_full()`, `min_w_0()`, and uses horizontal scrolling. It does not provide selection, snapping, reorder handles, a “+N more” overflow label, or a preview dialog. Compose those behaviors in an application-owned wrapper. Keep the ID stable for the lifetime of the conversation row. ## Custom styling and theme tokens `Attachment`, `AttachmentGroup`, and every named slot implement `Styled`. Refinements are applied after component defaults, which gives developers control over the surface, spacing, media geometry, typography, and action layout: ```rust Attachment::new() .w_full() .rounded(cx.theme().radius_lg) .bg(cx.theme().group_box) .border_color(cx.theme().ring) .media( AttachmentMedia::new() .rounded(cx.theme().radius_lg) .bg(cx.theme().primary.opacity(0.12)) .text_color(cx.theme().primary) .child(Icon::new(IconName::FileText)), ) .content( AttachmentContent::new() .title(AttachmentTitle::new("custom-theme.json").text_color(cx.theme().primary)) .description(AttachmentDescription::new("JSON · 16 KB")), ) ``` Prefer semantic roles from `cx.theme()` (`background`, `muted`, `border`, `destructive`, `foreground`, and their foreground counterparts) to raw colors. The component's default radii, spacing, and typography follow the shared design scale; application-specific density can be expressed with `Size` and typed style refinements at the composition boundary. Use `AttachmentContent::title(...)` and `.description(...)` for status-aware metadata, `.child(...)` for arbitrary custom content, child `.status(...)` for an explicit override, `AttachmentTitle::with_shimmer_style(...)` for loading motion, and `AttachmentMedia::overlay(...)` for controls above an image. ## Accessibility and state guidance * Include the file name and useful type/size information in text. An icon-only media preview is not enough to identify the attachment. * Put upload, retry, remove, download, and preview actions in semantic `Button` or `Link` controls. A tooltip is supplemental; for the current `Button` API, use `.label(...)` when the action needs an accessible name. * Describe `Pending`, `Uploading`, `Processing`, and `Failed` in text or a control state. The dashed border, opacity, shimmer, and destructive color are supporting cues. * Keep progress determinate when the application knows a byte or item count; use `Progress` as a child rather than duplicating progress semantics in `Attachment`. * Loading shimmer is disabled by `ShimmerText` when reduced motion is enabled. Keep a readable title and description visible in that mode. * Ensure a vertical overlay action remains reachable from the keyboard; it must not be available only through image hover. ## Component boundaries These boundaries are deliberate: * Use `Button` directly instead of an attachment-specific action component. This preserves Button variants, sizes, loading, disabled behavior, focus, and event handling. * Use `Progress` directly instead of an attachment-specific progress wrapper. * Use `.id(...)` with `.on_click(...)` for whole-card activation. The card only reports the click; whether that opens a dialog, a browser, a file viewer, or toggles a selection stays with the application. * Use `AttachmentGroup` only for the shared horizontal row and overflow. Use an application-owned container for selection, reordering, snapping, or custom scroll controls. ## API reference ### `Attachment` | Method | Default | Purpose | | --- | --- | --- | | `new()` | `Complete`, `Medium`, `Horizontal`, no slots | Create an attachment. | | `id(ElementId)` | none | Stable identity for the whole-card click layer. | | `on_click(handler)` | none | Whole-card activation; requires `id(...)` and stays below the actions. | | `status(AttachmentStatus)` | `Complete` | Set lifecycle styling. | | `axis(Axis)` | `Horizontal` | Choose horizontal or vertical layout. | | `with_size(Size)` | `Medium` | Set a named or custom size. | | `xsmall()` / `small()` / `large()` | — | Sizable shortcuts. | | `media(AttachmentMedia)` | none | Add a preview slot. | | `content(AttachmentContent)` | none | Add metadata. | | `actions(AttachmentActions)` | none | Add action controls. | ### `AttachmentMedia` | Method | Default | Purpose | | --- | --- | --- | | `new()` | no source, no children | Create a media slot. | | `src(ImageSource)` | none | Render an image preview. | | `with_size(Size)` | inherited attachment size | Override media density. | | `overlay(element)` | none | Center an element over the media. | | `child(element)` | — | Add an icon or custom content above the preview. | | `Styled` methods | themed muted media | Refine geometry, radius, background, and typography. | ### `AttachmentContent`, `AttachmentTitle`, and `AttachmentDescription` | Method | Default | Purpose | | --- | --- | --- | | `AttachmentContent::new()` | empty vertical metadata stack | Create content. | | `.title(AttachmentTitle)` | — | Add a status-aware single-line title. | | `.description(AttachmentDescription)` | — | Add a status-aware single-line description. | | `AttachmentTitle::new(text)` | no explicit child status | Create a title. | | `AttachmentTitle::status(status)` | inherits parent | Override title loading state. | | `AttachmentTitle::with_shimmer_style(style)` | default shimmer | Customize title animation. | | `AttachmentDescription::new(text)` | no explicit child status | Create a description. | | `AttachmentDescription::status(status)` | inherits parent | Override description color state. | | `.child(element)` | — | Add progress, badges, or custom metadata. | ### `AttachmentActions` and `AttachmentGroup` | Method | Default | Purpose | | --- | --- | --- | | `AttachmentActions::new()` | empty action layout | Create the action slot. | | `.child(element)` | — | Add Button, Link, or another control. | | `AttachmentGroup::new(id)` | stable ID required | Create a horizontal scrolling group. | | `AttachmentGroup::child(element)` | — | Add attachments to the group. | ### Related types * \[`AttachmentStatus`] — `Pending`, `Uploading`, `Processing`, `Failed`, and `Complete`. * \[`Size`] — `XSmall`, `Small`, `Medium`, `Large`, or a custom `Pixels` value. * \[`Axis`] — `Horizontal` or `Vertical` from GPUI. * \[`ShimmerStyle`] — shared loading animation configuration. [Attachment]: https://docs.rs/gpui-component/latest/gpui_component/attachment/struct.Attachment.html [AttachmentMedia]: https://docs.rs/gpui-component/latest/gpui_component/attachment/struct.AttachmentMedia.html [AttachmentContent]: https://docs.rs/gpui-component/latest/gpui_component/attachment/struct.AttachmentContent.html [AttachmentTitle]: https://docs.rs/gpui-component/latest/gpui_component/attachment/struct.AttachmentTitle.html [AttachmentDescription]: https://docs.rs/gpui-component/latest/gpui_component/attachment/struct.AttachmentDescription.html [AttachmentActions]: https://docs.rs/gpui-component/latest/gpui_component/attachment/struct.AttachmentActions.html [AttachmentGroup]: https://docs.rs/gpui-component/latest/gpui_component/attachment/struct.AttachmentGroup.html [AttachmentStatus]: https://docs.rs/gpui-component/latest/gpui_component/attachment/enum.AttachmentStatus.html [Size]: https://docs.rs/gpui-component/latest/gpui_component/enum.Size.html [Axis]: https://docs.rs/gpui/latest/gpui/enum.Axis.html [ShimmerStyle]: https://docs.rs/gpui-component/latest/gpui_component/shimmer/struct.ShimmerStyle.html --- --- url: /zh-CN/docs/components/attachment.md description: 支持上传状态、预览和操作的可组合文件与媒体附件表面。 --- # Attachment `Attachment` 用于在会话中展示文件或媒体条目。根组件负责状态、方向、尺寸和整体 surface;`AttachmentMedia`、`AttachmentContent`、`AttachmentActions` 分别提供预览、元数据和操作入口。应用可以在这些 slot 中继续组合 `Button`、`Progress`、`Icon` 和其他 GPUI element。 ## 适用场景 * 文件上传队列、消息附件、图片预览和处理中的媒体。 * 需要让标题、描述和预览随着上传生命周期改变外观的场景。 * 需要在横向紧凑卡片与纵向预览卡片之间复用同一套语义结构的场景。 选择文件、打开预览、取消上传和重试等业务行为仍由应用通过子控件持有;`Attachment` 不保存文件模型或网络状态。 ## 导入 ```rust use gpui_kit::{Axis, ParentElement as _, Styled as _}; use gpui_kit::component::{ ActiveTheme as _, Icon, IconName, Size, Sizable as _, StyledExt as _, attachment::{ Attachment, AttachmentActions, AttachmentContent, AttachmentDescription, AttachmentGroup, AttachmentMedia, AttachmentStatus, AttachmentTitle, }, button::{Button, ButtonVariants as _}, progress::Progress, shimmer::ShimmerStyle, }; ``` ## 组合结构 ```text Attachment ├── AttachmentMedia # 图标、图片预览和 overlay ├── AttachmentContent # 标题、描述以及任意附加内容 │ ├── AttachmentTitle │ ├── AttachmentDescription │ └── Progress(可选) └── AttachmentActions # Button 或其他操作 ``` 具名方法保留状态继承和方向布局;`.child(...)` 仍可添加任意 element,但会擦除具体类型。 ## 基础文件附件 最小的文件附件可以只提供元数据: ```rust Attachment::new() .content( AttachmentContent::new() .title(AttachmentTitle::new("quarterly-report.pdf")) .description(AttachmentDescription::new("PDF · 2.4 MB")), ) ``` 常见的文件卡片同时提供类型图标和移除操作: ```rust Attachment::new() .media(AttachmentMedia::new().child(Icon::new(IconName::FileText))) .content( AttachmentContent::new() .title(AttachmentTitle::new("quarterly-report.pdf")) .description(AttachmentDescription::new("PDF · 2.4 MB")), ) .actions( AttachmentActions::new().child( Button::new("remove-report") .ghost() .xsmall() .icon(IconName::Close) .label("移除") .tooltip("移除附件"), ), ) ``` `Attachment::new()` 默认状态为 `Complete`、方向为 `Axis::Horizontal`、尺寸为 `Size::Medium`。 ## 媒体和图片预览 将 `ImageSource` 传给 `.src(...)` 即可显示图片;媒体 slot 也可以继续保留图标或其他 child: ```rust Attachment::new() .media( AttachmentMedia::new() .src(preview_source) .child(Icon::new(IconName::Image)), ) .content( AttachmentContent::new() .title(AttachmentTitle::new("preview.png")) .description(AttachmentDescription::new("PNG · 1280 × 720")), ) ``` 有图片源时,图片使用 `ObjectFit::Cover` 填满媒体区域。没有图片源时,媒体 slot 仍然可以展示图标;如果附件状态为 `Failed`,没有图片源的媒体区域使用 destructive 语义颜色。 ### 预览上的 overlay `.overlay(...)` 会把内容居中覆盖在整个媒体区域上。overlay 不会随图片的 loading opacity 一起变暗,适合 Spinner、播放按钮或错误提示: ```rust Attachment::new() .status(AttachmentStatus::Uploading) .media( AttachmentMedia::new() .src(preview_source) .overlay(Progress::new("preview-progress").value(68.)), ) ``` 也可以将 overlay 与自定义 `div()`、`Button` 组合。加载、处理和失败状态会降低图片本身的透明度;`Pending` 与 `Complete` 保持完整不透明。 ## 上传生命周期 `AttachmentStatus` 只表达通用生命周期,具体的文件数据和请求状态由应用持有: | 状态 | 用途 | 默认视觉提示 | | --- | --- | --- | | `Pending` | 已选择,等待上传。 | 边框使用 dashed 样式。 | | `Uploading` | 正在传输文件。 | 标题显示 shimmer;图片预览变暗。 | | `Processing` | 上传完成,服务端正在处理。 | 标题显示 shimmer;图片预览变暗。 | | `Failed` | 上传或处理失败。 | 边框与描述使用 destructive 语义色;无图片媒体也使用 destructive 色。 | | `Complete` | 已准备好供用户使用。 | 普通完成状态。 | 状态文字应写入描述,不能只依赖边框颜色: ```rust Attachment::new() .status(AttachmentStatus::Uploading) .content( AttachmentContent::new() .title(AttachmentTitle::new("design-assets.zip")) .description(AttachmentDescription::new("上传中 · 68%")) .child(Progress::new("attachment-progress").value(68.)), ) ``` `Progress::loading(true)` 可用于不确定进度;确定百分比时使用 `.value(...)`。Progress 是独立组件,因此应用可以按自己的请求模型更新数值或状态。 `AttachmentStatus` 还提供 `is_pending()`、`is_uploading()`、`is_processing()`、`is_failed()`、`is_complete()` 和 `is_in_progress()`,适合在应用状态层生成描述或决定操作按钮。 ## 尺寸与方向 Attachment 实现 `Sizable`,支持 `xsmall()`、`small()`、默认 medium、`large()`,也可以使用 `with_size(Size::...)`: ```rust Attachment::new() .xsmall() .content( AttachmentContent::new() .title(AttachmentTitle::new("compact.txt")) .description(AttachmentDescription::new("TXT · 4 KB")), ); Attachment::new() .large() .media(AttachmentMedia::new().src(preview_source)) .content( AttachmentContent::new() .title(AttachmentTitle::new("presentation.pptx")) .description(AttachmentDescription::new("演示文稿")), ) ``` 尺寸使用共享的 design scale;应根据界面密度选择语义尺寸,避免在应用里为每个附件写独立高度。 `Axis::Horizontal` 适合消息中的紧凑附件,`Axis::Vertical` 适合图片预览: ```rust Attachment::new() .axis(Axis::Vertical) .media(AttachmentMedia::new().src(preview_source)) .content( AttachmentContent::new() .title(AttachmentTitle::new("preview.png")) .description(AttachmentDescription::new("PNG · 1280 × 720")), ) ``` 纵向模式下有 content 时根组件使用主题 scale 中的预览宽度,没有 content 时使用更紧凑的方形媒体尺寸;媒体区域保持 `aspect_ratio(1.)`。这些是可覆盖的默认布局,不应被当作组件外部的固定像素契约。 `AttachmentMedia` 也实现 `Sizable`,因此可以独立覆盖预览尺寸: ```rust Attachment::new() .media( AttachmentMedia::new() .with_size(Size::Large) .src(preview_source), ) ``` 显式 media 尺寸优先于根 Attachment 的尺寸;其他 `Styled` refinement 仍会在默认值之后应用。 ## 内容、长文件名与操作 `AttachmentTitle` 和 `AttachmentDescription` 默认单行并截断。长文件名应提供可见的描述或 Tooltip,避免把卡片横向撑开: ```rust Attachment::new() .content( AttachmentContent::new() .title( AttachmentTitle::new("2026-08-25-very-long-export-name.json"), ) .description(AttachmentDescription::new("JSON · 18.4 MB")), ) ``` 标题会自动单行截断。需要在 hover 或辅助说明中显示完整文件名时,应由应用在外层交互容器上组合 Tooltip,或把完整名称放入可见描述;只要内容需要自动继承状态,就应继续使用 `.title(...)` 和 `.description(...)`。 操作 slot 可以包含多个 Button: ```rust Attachment::new() .content( AttachmentContent::new() .title(AttachmentTitle::new("failed-upload.zip")) .description(AttachmentDescription::new("上传失败,请重试")), ) .actions( AttachmentActions::new() .child(Button::new("retry").small().label("重试")) .child( Button::new("remove") .ghost() .xsmall() .icon(IconName::Close) .label("移除") .tooltip("移除附件"), ), ) ``` `AttachmentActions` 不定义专用的 `AttachmentAction`,因此可以直接复用 Button 的 variant、尺寸、disabled、loading、tooltip 和事件 API。 ## 整卡点击 设置 `.id(...)` 和 `.on_click(...)` 可以让整张卡片响应点击(例如打开预览)。点击层绘制在 `AttachmentActions` 之下,操作按钮仍然独立可点: ```rust Attachment::new() .id("design-attachment") .on_click(|_, window, cx| { // 打开预览。 }) .content( AttachmentContent::new() .title(AttachmentTitle::new("design-mockups.png")) .description(AttachmentDescription::new("PNG · 1.8 MB")), ) .actions( AttachmentActions::new() .child(Button::new("remove").ghost().xsmall().icon(IconName::Close)), ) ``` 点击状态需要稳定标识,因此 handler 只在配合 `.id(...)` 时生效。可点击的卡片 hover 时会显示 muted 底色,让它读起来是可交互的。点击意味着什么——对话框、浏览器、文件预览还是选择——由应用决定。删除、重试等次要操作应留在 `AttachmentActions` 中,不要依赖整卡点击;同时把卡片的主操作以 `Button` 或 `Link` 的形式提供在键盘可达的位置——点击层只是指针便利,不参与焦点。 ## 状态继承与局部覆盖 通过具名 `.title(...)` 和 `.description(...)` 添加的 child 会继承父级状态: ```rust Attachment::new() .status(AttachmentStatus::Failed) .content( AttachmentContent::new() .title(AttachmentTitle::new("archive.zip")) .description(AttachmentDescription::new("上传失败")), ) ``` 如果某个 child 的状态与父级不同,可以显式覆盖: ```rust Attachment::new() .status(AttachmentStatus::Failed) .content( AttachmentContent::new() .title(AttachmentTitle::new("archive.zip")) .description( AttachmentDescription::new("文件已恢复") .status(AttachmentStatus::Complete), ), ) ``` 显式 child 状态优先于继承状态。普通 `.child(AttachmentTitle::new(...))` 会擦除类型,因此不会自动继承父级状态;需要状态感知表现时使用具名 builder。 标题在 `Uploading` 和 `Processing` 时使用 `ShimmerText`。可以复用 `ShimmerStyle` 调整动画: ```rust AttachmentTitle::new("design-assets.zip") .with_shimmer_style( ShimmerStyle::new() .duration(std::time::Duration::from_secs(3)) .spread(0.45) .reverse(true) .once(true), ) ``` ## 分组 `AttachmentGroup` 是可横向滚动的附件行,需要稳定的 element id 保存滚动状态: ```rust AttachmentGroup::new("message-attachments") .child(first_attachment) .child(second_attachment) .child(third_attachment) ``` 当附件数量可能超出消息宽度时使用该组件。选择、拖拽排序、snap 或自定义滚动按钮属于应用容器,不由 `AttachmentGroup` 保存。 ## 自定义样式与主题 token 根组件和所有公开 slot 都实现 `Styled`,调用方 refinement 会在默认布局后应用: ```rust Attachment::new() .rounded(cx.theme().radius_lg) .bg(cx.theme().group_box) .border_color(cx.theme().border) .content( AttachmentContent::new() .gap_1() .title( AttachmentTitle::new("custom-theme.txt") .text_color(cx.theme().foreground), ) .description( AttachmentDescription::new("使用主题 token") .text_color(cx.theme().muted_foreground), ), ) ``` 优先使用 `cx.theme()` 的语义颜色、圆角和共享尺寸;不要在调用点写固定 hex 颜色或按单个组件复制 spacing scale。可分别调整: * `Attachment`:整体宽度、背景、边框、圆角、padding 和 gap。 * `AttachmentMedia`:预览尺寸、圆角、背景和 overlay。 * `AttachmentContent`:内容宽度、文字层级和元数据间距。 * `AttachmentTitle` / `AttachmentDescription`:截断、字体和语义颜色。 * `AttachmentActions`:操作间距、位置和按钮布局。 * `AttachmentGroup`:横向 gap、padding 和滚动容器样式。 ## 组件边界 此 API 有意将以下职责留给组合层: * 直接使用 `Button`,不增加 `AttachmentAction`,保留 Button 的完整 variant、尺寸、事件和可访问性选项。 * 整卡点击通过 `.id(...)` 加 `.on_click(...)` 提供;卡片只上报点击,打开对话框、浏览器、预览还是切换选择由应用决定。 * 直接使用 `Progress`,不增加附件专属进度包装。 * `AttachmentGroup` 只负责横向间距和 overflow,不保存选择、拖拽、snap 或业务数据。 这些边界让附件仍然保持可组合,应用可以根据产品行为选择普通 Button、Link、Popover 或自己的容器。 ## 可访问性 * 文件名和状态应以文本表达;失败状态不能只靠 destructive 边框或文字颜色。 * icon-only action 应提供可见的 `.label(...)` 或其他可读名称;tooltip 只作为“移除附件”“重试上传”等补充提示。 * `Progress` 的百分比和不确定状态应通过可读文本或其他状态说明补充,不能只展示一条进度条。 * 纵向图片预览上的 overlay Button 仍应可聚焦,不要让装饰层遮蔽操作目标。 * `AttachmentGroup` 的横向滚动应能够通过键盘和系统滚动输入访问;不要把唯一入口做成 hover 才出现的按钮。 * 上传和处理中的 shimmer 会遵循系统 reduced motion;应用自定义 overlay 动画时也应提供静态结果。 ## API 参考 ### `Attachment` | 方法 | 说明 | | --- | --- | | `new()` | 创建 `Complete`、横向、medium 尺寸的附件。 | | `id(ElementId)` | 设置整卡点击层的稳定标识。 | | `on_click(handler)` | 整卡点击;需配合 `id(...)`,绘制在 actions 之下。 | | `status(AttachmentStatus)` | 设置根生命周期状态。 | | `axis(Axis)` | 设置 `Horizontal` 或 `Vertical` 布局。 | | `media(AttachmentMedia)` | 设置预览 slot。 | | `content(AttachmentContent)` | 设置元数据 slot。 | | `actions(AttachmentActions)` | 设置操作 slot。 | | `xsmall()` / `small()` / `large()` | 通过 `Sizable` 选择语义尺寸。 | | `Styled` | 调整根 surface 和布局。 | ### `AttachmentMedia` | 方法 | 说明 | | --- | --- | | `new()` | 创建空媒体 slot。 | | `src(ImageSource)` | 设置图片预览源。 | | `overlay(element)` | 在媒体区域上方居中添加 overlay。 | | `with_size(Size)` | 覆盖从根组件继承的媒体尺寸。 | | `child(element)` | 添加图标或其他媒体 child。 | | `Styled` | 调整媒体背景、圆角和尺寸。 | ### `AttachmentContent` | 方法 | 说明 | | --- | --- | | `new()` | 创建空元数据 slot。 | | `title(AttachmentTitle)` | 添加会继承状态的标题。 | | `description(AttachmentDescription)` | 添加会继承状态的描述。 | | `child(element)` | 添加任意自定义内容,不参与状态继承。 | | `Styled` | 调整内容布局和文字 refinement。 | ### `AttachmentTitle` / `AttachmentDescription` | 方法 | 说明 | | --- | --- | | `new(text)` | 创建单行标题或描述。 | | `status(AttachmentStatus)` | 显式覆盖从父级继承的状态。 | | `with_shimmer_style(ShimmerStyle)` | 自定义标题 loading shimmer;仅 `AttachmentTitle` 提供。 | | `Styled` | 调整文字样式、颜色、截断等。 | ### `AttachmentActions` / `AttachmentGroup` | 类型 | 方法 | 说明 | | --- | --- | --- | | `AttachmentActions` | `new()` / `child(element)` | 创建操作 slot 并组合 Button 或其他控件。 | | `AttachmentGroup` | `new(id)` / `child(element)` | 创建带稳定 id 的横向滚动附件组。 | | 两者 | `Styled` | 调整间距、位置和容器布局。 | ### 类型链接 * [Attachment] * [AttachmentStatus] * [AttachmentMedia] * [AttachmentContent] * [AttachmentTitle] * [AttachmentDescription] * [AttachmentActions] * [AttachmentGroup] [Attachment]: https://docs.rs/gpui-component/latest/gpui_component/attachment/struct.Attachment.html [AttachmentStatus]: https://docs.rs/gpui-component/latest/gpui_component/attachment/enum.AttachmentStatus.html [AttachmentMedia]: https://docs.rs/gpui-component/latest/gpui_component/attachment/struct.AttachmentMedia.html [AttachmentContent]: https://docs.rs/gpui-component/latest/gpui_component/attachment/struct.AttachmentContent.html [AttachmentTitle]: https://docs.rs/gpui-component/latest/gpui_component/attachment/struct.AttachmentTitle.html [AttachmentDescription]: https://docs.rs/gpui-component/latest/gpui_component/attachment/struct.AttachmentDescription.html [AttachmentActions]: https://docs.rs/gpui-component/latest/gpui_component/attachment/struct.AttachmentActions.html [AttachmentGroup]: https://docs.rs/gpui-component/latest/gpui_component/attachment/struct.AttachmentGroup.html --- --- url: /base/primitives/avatar.md description: An image with composable fallback content for a person or entity. --- # Avatar An image with composable fallback content for a person or entity. Like every `gpui-base` primitive, Avatar supplies behavior and semantic structure without imposing a product visual language. Apply GPUI styles and compose the exported parts to match your design system. ## Example The [single native Cargo entrypoint](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/components.rs) selects this primitive from the [shared showcase implementation](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/mod.rs). The same showcase is compiled once for the WASM preview above. ```bash cargo run -p gpui-base --example components -- avatar ``` ## Import ```rust use gpui_kit::base::{Avatar, AvatarFallback, AvatarImage}; ``` ## Anatomy and API The example composes `Avatar`, `AvatarFallback`, `AvatarImage`. GPUI's standard styling and event traits provide presentation; these base types provide the interaction structure. The authoritative module is [`components/avatar.rs`](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/components/avatar.rs). Native and browser previews compile this same file. ## State and events `Avatar` is presentational. Supply fallback content for the image-loading and image-error paths. Keep controlled state on the parent render type or in a GPUI entity. Update it in callbacks and call `cx.notify()`; do not recreate persistent entities during every render. ## Complete Rust example The complete implementation used by the runnable showcase is embedded directly from Rust source: The command above supplies application initialization, window creation, and shared `BaseShowcase` state. ## Accessibility Fallback text should identify the entity; decorative avatars should not duplicate nearby labels. ## Notes Use stable element IDs where accepted. Verify focus, hover, active, selected, disabled, reduced-motion, and high-contrast appearances in the consuming design system. --- --- url: /docs/components/avatar.md description: Displays a user avatar image with fallback options. --- # Avatar The Avatar component displays user profile images with intelligent fallbacks. When no image is provided, it shows user initials or a placeholder icon. The component supports various sizes and can be grouped together for team displays. ## Import ```rust use gpui_kit::component::avatar::{Avatar, AvatarGroup}; ``` ## Usage ### Basic Avatar You can create an [Avatar] by providing an image source URL and a user name: ```rust Avatar::new() .name("John Doe") .src("https://example.com/avatar.jpg") ``` ### Avatar with Fallback Text When no image source is provided, the Avatar displays user initials with an automatically generated color background: ```rust // Shows "JD" initials with colored background Avatar::new() .name("John Doe") // Shows "JS" initials Avatar::new() .name("Jane Smith") ``` ### Avatar Placeholder For anonymous users or when no name is provided: ```rust use gpui_kit::component::IconName; // Default user icon placeholder Avatar::new() // Custom placeholder icon Avatar::new() .placeholder(IconName::Building2) ``` ### Avatar Sizes ```rust Avatar::new() .name("John Doe") .xsmall() Avatar::new() .name("John Doe") .small() Avatar::new() .name("John Doe") // 48px (default medium) Avatar::new() .name("John Doe") .large() // Custom size Avatar::new() .name("John Doe") .with_size(px(100.)) ``` ### Custom Styling ```rust Avatar::new() .src("https://example.com/avatar.jpg") .with_size(px(100.)) .border_3() .border_color(cx.theme().foreground) .shadow_sm() .rounded(px(20.)) // Custom border radius ``` ## AvatarGroup The [AvatarGroup] component allows you to display multiple avatars in a compact, overlapping layout: ### Basic Group ```rust AvatarGroup::new() .child(Avatar::new().src("https://example.com/user1.jpg")) .child(Avatar::new().src("https://example.com/user2.jpg")) .child(Avatar::new().src("https://example.com/user3.jpg")) .child(Avatar::new().name("John Doe")) ``` ### Group with Limit ```rust AvatarGroup::new() .limit(3) // Show maximum 3 avatars .child(Avatar::new().src("https://example.com/user1.jpg")) .child(Avatar::new().src("https://example.com/user2.jpg")) .child(Avatar::new().src("https://example.com/user3.jpg")) .child(Avatar::new().src("https://example.com/user4.jpg")) // Hidden .child(Avatar::new().src("https://example.com/user5.jpg")) // Hidden ``` ### Group with Ellipsis Show an ellipsis indicator when avatars are hidden due to the limit. In this example, only 3 avatars are shown, and "..." indicates there are more: ```rust AvatarGroup::new() .limit(3) .ellipsis() // Shows "..." when limit is exceeded .child(Avatar::new().src("https://example.com/user1.jpg")) .child(Avatar::new().src("https://example.com/user2.jpg")) .child(Avatar::new().src("https://example.com/user3.jpg")) .child(Avatar::new().src("https://example.com/user4.jpg")) .child(Avatar::new().src("https://example.com/user5.jpg")) ``` ### Group Sizes The \[Sizeable] trait can also be applied to AvatarGroup, and it will set the size for all contained avatars. ```rust // Extra small group AvatarGroup::new() .xsmall() .child(Avatar::new().name("A")) .child(Avatar::new().name("B")) .child(Avatar::new().name("C")) // Small group AvatarGroup::new() .small() .child(Avatar::new().name("A")) .child(Avatar::new().name("B")) // Medium group (default) AvatarGroup::new() .child(Avatar::new().name("A")) .child(Avatar::new().name("B")) // Large group AvatarGroup::new() .large() .child(Avatar::new().name("A")) .child(Avatar::new().name("B")) ``` ### Adding Multiple Avatars ```rust let avatars = vec![ Avatar::new().src("https://example.com/user1.jpg"), Avatar::new().src("https://example.com/user2.jpg"), Avatar::new().name("John Doe"), ]; AvatarGroup::new() .children(avatars) .limit(5) .ellipsis() ``` ## API Reference * [Avatar] * [AvatarGroup] ## Examples ### Team Display ```rust use gpui_kit::component::{h_flex, v_flex}; v_flex() .gap_4() .child("Development Team") .child( AvatarGroup::new() .limit(4) .ellipsis() .child(Avatar::new().name("Alice Johnson").src("https://example.com/alice.jpg")) .child(Avatar::new().name("Bob Smith").src("https://example.com/bob.jpg")) .child(Avatar::new().name("Charlie Brown")) .child(Avatar::new().name("Diana Prince")) .child(Avatar::new().name("Eve Wilson")) ) ``` ### User Profile Header ```rust h_flex() .items_center() .gap_4() .child( Avatar::new() .src("https://example.com/profile.jpg") .name("John Doe") .large() .border_2() .border_color(cx.theme().primary) ) .child( v_flex() .child("John Doe") .child("Software Engineer") ) ``` ### Anonymous User ```rust use gpui_kit::component::IconName; Avatar::new() .placeholder(IconName::UserCircle) .medium() ``` ### Avatar with Custom Colors ```rust // The avatar automatically generates colors based on the name // Different names will get different colors from the color palette Avatar::new().name("Alice") // Gets one color Avatar::new().name("Bob") // Gets a different color Avatar::new().name("Charlie") // Gets another color ``` [Avatar]: https://docs.rs/gpui-component/latest/gpui_component/avatar/struct.Avatar.html [AvatarGroup]: https://docs.rs/gpui-component/latest/gpui_component/avatar/struct.AvatarGroup.html [Sizable]: https://docs.rs/gpui-component/latest/gpui_component/trait.Sizable.html --- --- url: /zh-CN/docs/components/avatar.md description: 支持图片、姓名首字母和占位图标的头像组件。 --- # Avatar Avatar 用于显示用户头像图片,并在无图片时自动回退为姓名首字母或占位图标。组件支持多种尺寸,也可以通过 AvatarGroup 组合展示团队或成员列表。 ## 导入 ```rust use gpui_kit::component::avatar::{Avatar, AvatarGroup}; ``` ## 用法 ### 基础 Avatar 通过图片地址和用户名创建头像: ```rust Avatar::new() .name("John Doe") .src("https://example.com/avatar.jpg") ``` ### 使用首字母回退 当未提供图片时,Avatar 会显示用户名首字母,并自动生成背景颜色: ```rust Avatar::new() .name("John Doe") Avatar::new() .name("Jane Smith") ``` ### 占位头像 适用于匿名用户或没有姓名的场景: ```rust use gpui_kit::component::IconName; Avatar::new() Avatar::new() .placeholder(IconName::Building2) ``` ### 不同尺寸 ```rust Avatar::new() .name("John Doe") .xsmall() Avatar::new() .name("John Doe") .small() Avatar::new() .name("John Doe") Avatar::new() .name("John Doe") .large() Avatar::new() .name("John Doe") .with_size(px(100.)) ``` ### 自定义样式 ```rust Avatar::new() .src("https://example.com/avatar.jpg") .with_size(px(100.)) .border_3() .border_color(cx.theme().foreground) .shadow_sm() .rounded(px(20.)) ``` ## AvatarGroup [AvatarGroup] 可以以紧凑、重叠的方式显示多个头像。 ### 基础分组 ```rust AvatarGroup::new() .child(Avatar::new().src("https://example.com/user1.jpg")) .child(Avatar::new().src("https://example.com/user2.jpg")) .child(Avatar::new().src("https://example.com/user3.jpg")) .child(Avatar::new().name("John Doe")) ``` ### 限制数量 ```rust AvatarGroup::new() .limit(3) .child(Avatar::new().src("https://example.com/user1.jpg")) .child(Avatar::new().src("https://example.com/user2.jpg")) .child(Avatar::new().src("https://example.com/user3.jpg")) .child(Avatar::new().src("https://example.com/user4.jpg")) .child(Avatar::new().src("https://example.com/user5.jpg")) ``` ### 使用省略标记 当超过限制数量时,可显示 `...` 提示还有更多成员: ```rust AvatarGroup::new() .limit(3) .ellipsis() .child(Avatar::new().src("https://example.com/user1.jpg")) .child(Avatar::new().src("https://example.com/user2.jpg")) .child(Avatar::new().src("https://example.com/user3.jpg")) .child(Avatar::new().src("https://example.com/user4.jpg")) .child(Avatar::new().src("https://example.com/user5.jpg")) ``` ### 分组尺寸 [Sizable] trait 也可用于 AvatarGroup,并会作用于内部所有头像: ```rust AvatarGroup::new() .xsmall() .child(Avatar::new().name("A")) .child(Avatar::new().name("B")) .child(Avatar::new().name("C")) AvatarGroup::new() .small() .child(Avatar::new().name("A")) .child(Avatar::new().name("B")) AvatarGroup::new() .child(Avatar::new().name("A")) .child(Avatar::new().name("B")) AvatarGroup::new() .large() .child(Avatar::new().name("A")) .child(Avatar::new().name("B")) ``` ### 批量添加头像 ```rust let avatars = vec![ Avatar::new().src("https://example.com/user1.jpg"), Avatar::new().src("https://example.com/user2.jpg"), Avatar::new().name("John Doe"), ]; AvatarGroup::new() .children(avatars) .limit(5) .ellipsis() ``` ## API 参考 * [Avatar] * [AvatarGroup] ## 示例 ### 团队成员展示 ```rust use gpui_kit::component::{h_flex, v_flex}; v_flex() .gap_4() .child("Development Team") .child( AvatarGroup::new() .limit(4) .ellipsis() .child(Avatar::new().name("Alice Johnson").src("https://example.com/alice.jpg")) .child(Avatar::new().name("Bob Smith").src("https://example.com/bob.jpg")) .child(Avatar::new().name("Charlie Brown")) .child(Avatar::new().name("Diana Prince")) .child(Avatar::new().name("Eve Wilson")) ) ``` ### 用户资料头部 ```rust h_flex() .items_center() .gap_4() .child( Avatar::new() .src("https://example.com/profile.jpg") .name("John Doe") .large() .border_2() .border_color(cx.theme().primary) ) .child( v_flex() .child("John Doe") .child("Software Engineer") ) ``` ### 匿名用户 ```rust use gpui_kit::component::IconName; Avatar::new() .placeholder(IconName::UserCircle) .medium() ``` ### 自动配色 ```rust Avatar::new().name("Alice") Avatar::new().name("Bob") Avatar::new().name("Charlie") ``` [Avatar]: https://docs.rs/gpui-component/latest/gpui_component/avatar/struct.Avatar.html [AvatarGroup]: https://docs.rs/gpui-component/latest/gpui_component/avatar/struct.AvatarGroup.html [Sizable]: https://docs.rs/gpui-component/latest/gpui_component/trait.Sizable.html --- --- url: /zh-CN/base/primitives/avatar.md description: 带可组合后备内容的人物或实体图像。 --- # Avatar 带可组合后备内容的人物或实体图像。 和所有 GPUI Base 原语一样,Avatar 只提供行为和语义结构,不规定产品视觉语言。请使用 GPUI 样式并组合导出的部件,使其符合你的设计系统。 ## 示例 原生示例和页面上方的 WASM 预览共用同一份实现: ```bash cargo run -p gpui-base --example components -- avatar ``` ## 导入 ```rust use gpui_kit::base::{Avatar, AvatarFallback, AvatarImage}; ``` ## 结构与 API 示例组合上述公开类型。GPUI 的标准样式和事件 trait 负责表现,Base 类型负责交互结构。权威实现位于 [`components/avatar.rs`](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/components/avatar.rs),原生与浏览器预览编译的是同一文件。 ## 状态与事件 图像加载失败时显示后备内容。 受控状态应保存在父渲染类型或 GPUI entity 中;在回调中更新并调用 `cx.notify()`,不要在每次渲染时重建持久 entity。 ## 完整 Rust 示例 ## 可访问性 为有信息含义的图像提供替代文本;纯装饰图像应从可访问树中隐藏。 ## 注意事项 在支持的位置使用稳定元素 ID,并在消费端设计系统中验证焦点、悬停、按下、选中、禁用、减少动态效果和高对比度状态。 --- --- url: /docs/components/badge.md description: >- A red dot that indicates the number of unread messages, status, or other notifications. --- # Badge A versatile badge component that can display counts, dots, or icons on elements. Perfect for indicating notifications, status, or other contextual information on avatars, icons, or other UI elements. ## Import ```rust use gpui_kit::component::badge::Badge; ``` ## Usage ### Badge with Count Use `count` to display a numeric badge, if the count is greater than zero (`> 0`) the badge will be shown, otherwise it will be hidden. There is a default maximum count of `99`, any count above this will be displayed as `99+`. You can customize this maximum using the [max](https://docs.rs/gpui-component/latest/gpui_component/badge/struct.Badge.html#method.max) method. ```rust Badge::new() .count(3) .child(Icon::new(IconName::Bell)) ``` ### Variants * Default: Displays a numeric count. * Dot: A small dot indicator, typically used for status. * Icon: Displays an icon instead of a number. ```rust // Number badge (default) Badge::new() .count(5) .child(Avatar::new().src("https://example.com/avatar.jpg")) // Dot badge Badge::new() .dot() .child(Icon::new(IconName::Inbox)) // Icon badge Badge::new() .icon(IconName::Check) .child(Avatar::new().src("https://example.com/avatar.jpg")) ``` ### Badge Sizes The Badge is also implemented with the [Sizable] trait, allowing you to set small, medium (default), or large sizes. ```rust // Small badge Badge::new() .small() .count(1) .child(Avatar::new().small()) // Medium badge (default) Badge::new() .count(5) .child(Avatar::new()) // Large badge Badge::new() .large() .count(10) .child(Avatar::new().large()) ``` ### Badge Colors ```rust use gpui_kit::component::ActiveTheme; // Custom colors Badge::new() .count(3) .color(cx.theme().blue) .child(Avatar::new()) Badge::new() .icon(IconName::Star) .color(cx.theme().yellow) .child(Avatar::new()) Badge::new() .dot() .color(cx.theme().green) .child(Icon::new(IconName::Bell)) ``` ### Badge on Icons ```rust use gpui_kit::component::{Icon, IconName}; // Badge with count on icon Badge::new() .count(3) .child(Icon::new(IconName::Bell).large()) // Badge with high count (shows max) Badge::new() .count(103) .child(Icon::new(IconName::Inbox).large()) // Custom max count Badge::new() .count(150) .max(999) .child(Icon::new(IconName::Mail)) ``` ### Badge on Avatars ```rust use gpui_kit::component::avatar::Avatar; // Basic count badge Badge::new() .count(5) .child(Avatar::new().src("https://example.com/avatar.jpg")) // Status badge with icon Badge::new() .icon(IconName::Check) .color(cx.theme().green) .child(Avatar::new().src("https://example.com/avatar.jpg")) // Online indicator with dot Badge::new() .dot() .color(cx.theme().green) .child(Avatar::new().src("https://example.com/avatar.jpg")) ``` ### Complex Nested Badges ```rust // Badge on badge for complex status Badge::new() .count(212) .large() .child( Badge::new() .icon(IconName::Check) .large() .color(cx.theme().cyan) .child(Avatar::new().large().src("https://example.com/avatar.jpg")) ) // Multiple status indicators Badge::new() .count(2) .color(cx.theme().green) .large() .child( Badge::new() .icon(IconName::Star) .large() .color(cx.theme().yellow) .child(Avatar::new().large().src("https://example.com/avatar.jpg")) ) ``` ## API Reference * [Badge] ## Examples ### Notification Indicators ```rust // Unread messages Badge::new() .count(12) .child(Icon::new(IconName::Mail).large()) // New notifications Badge::new() .count(3) .color(cx.theme().red) .child(Icon::new(IconName::Bell).large()) // High priority with custom max Badge::new() .count(1234) .max(999) .color(cx.theme().orange) .child(Icon::new(IconName::AlertTriangle)) ``` ### Status Indicators ```rust // Online status Badge::new() .dot() .color(cx.theme().green) .child(Avatar::new().src("https://example.com/user.jpg")) // Verified status Badge::new() .icon(IconName::CheckCircle) .color(cx.theme().blue) .child(Avatar::new().src("https://example.com/verified-user.jpg")) // Warning status Badge::new() .icon(IconName::AlertTriangle) .color(cx.theme().yellow) .child(Avatar::new().src("https://example.com/user.jpg")) ``` ### Different Badge Positions ```rust // The badge automatically positions itself based on variant: // - Dot: top-right corner (small dot) // - Number: top-right with dynamic sizing // - Icon: bottom-right corner with border ``` ### Count Formatting ```rust // Numbers 1-99 show as-is Badge::new().count(5) // Shows "5" Badge::new().count(99) // Shows "99" // Numbers above max show with "+" Badge::new().count(100) // Shows "99+" (default max) Badge::new().count(1000).max(999) // Shows "999+" // Zero count hides the badge Badge::new().count(0) // Badge not visible ``` [Badge]: https://docs.rs/gpui_component/latest/gpui_component/badge/struct.Badge.html [Sizable]: https://docs.rs/gpui-component/latest/gpui_component/trait.Sizable.html --- --- url: /zh-CN/docs/components/badge.md description: 用于显示未读数、状态点或图标提示的小徽标组件。 --- # Badge Badge 是一个通用徽标组件,可在头像、图标或其他元素上显示数字、圆点或图标。适合用来表示通知数、状态或上下文提示信息。 ## 导入 ```rust use gpui_kit::component::badge::Badge; ``` ## 用法 ### 显示数字 使用 `count` 显示数字徽标。只有当数字大于 0 时才会显示;否则自动隐藏。 默认最大值是 `99`,超过后显示为 `99+`。你也可以通过 `max` 自定义上限。 ```rust Badge::new() .count(3) .child(Icon::new(IconName::Bell)) ``` ### 不同变体 * 默认:显示数字 * Dot:显示状态圆点 * Icon:显示图标 ```rust Badge::new() .count(5) .child(Avatar::new().src("https://example.com/avatar.jpg")) Badge::new() .dot() .child(Icon::new(IconName::Inbox)) Badge::new() .icon(IconName::Check) .child(Avatar::new().src("https://example.com/avatar.jpg")) ``` ### 不同尺寸 Badge 也实现了 [Sizable] trait: ```rust Badge::new() .small() .count(1) .child(Avatar::new().small()) Badge::new() .count(5) .child(Avatar::new()) Badge::new() .large() .count(10) .child(Avatar::new().large()) ``` ### 颜色 ```rust use gpui_kit::component::ActiveTheme; Badge::new() .count(3) .color(cx.theme().blue) .child(Avatar::new()) Badge::new() .icon(IconName::Star) .color(cx.theme().yellow) .child(Avatar::new()) Badge::new() .dot() .color(cx.theme().green) .child(Icon::new(IconName::Bell)) ``` ### 用在图标上 ```rust use gpui_kit::component::{Icon, IconName}; Badge::new() .count(3) .child(Icon::new(IconName::Bell).large()) Badge::new() .count(103) .child(Icon::new(IconName::Inbox).large()) Badge::new() .count(150) .max(999) .child(Icon::new(IconName::Mail)) ``` ### 用在头像上 ```rust use gpui_kit::component::avatar::Avatar; Badge::new() .count(5) .child(Avatar::new().src("https://example.com/avatar.jpg")) Badge::new() .icon(IconName::Check) .color(cx.theme().green) .child(Avatar::new().src("https://example.com/avatar.jpg")) Badge::new() .dot() .color(cx.theme().green) .child(Avatar::new().src("https://example.com/avatar.jpg")) ``` ### 复杂嵌套 ```rust Badge::new() .count(212) .large() .child( Badge::new() .icon(IconName::Check) .large() .color(cx.theme().cyan) .child(Avatar::new().large().src("https://example.com/avatar.jpg")) ) Badge::new() .count(2) .color(cx.theme().green) .large() .child( Badge::new() .icon(IconName::Star) .large() .color(cx.theme().yellow) .child(Avatar::new().large().src("https://example.com/avatar.jpg")) ) ``` ## API 参考 * [Badge] ## 示例 ### 通知提示 ```rust Badge::new() .count(12) .child(Icon::new(IconName::Mail).large()) Badge::new() .count(3) .color(cx.theme().red) .child(Icon::new(IconName::Bell).large()) Badge::new() .count(1234) .max(999) .color(cx.theme().orange) .child(Icon::new(IconName::AlertTriangle)) ``` ### 状态提示 ```rust Badge::new() .dot() .color(cx.theme().green) .child(Avatar::new().src("https://example.com/user.jpg")) Badge::new() .icon(IconName::CheckCircle) .color(cx.theme().blue) .child(Avatar::new().src("https://example.com/verified-user.jpg")) Badge::new() .icon(IconName::AlertTriangle) .color(cx.theme().yellow) .child(Avatar::new().src("https://example.com/user.jpg")) ``` ### 显示位置 ```rust // Badge 会根据变体自动选择位置: // - Dot:右上角小圆点 // - Number:右上角数字徽标 // - Icon:右下角图标徽标 ``` ### 数字格式 ```rust Badge::new().count(5) Badge::new().count(99) Badge::new().count(100) Badge::new().count(1000).max(999) Badge::new().count(0) ``` [Badge]: https://docs.rs/gpui_component/latest/gpui_component/badge/struct.Badge.html [Sizable]: https://docs.rs/gpui-component/latest/gpui_component/trait.Sizable.html --- --- url: /docs/components/bubble.md description: 'A composable chat surface for text, rich content, and reaction controls.' --- # Bubble `Bubble` is the surface-level primitive for a conversation. It owns the alignment, the maximum content width, and the position of an optional reaction region. `BubbleContent` owns the visible surface. Keeping those responsibilities separate lets an application replace the content layout without reimplementing message alignment. `Bubble` is a presentational element. It does not own a message record, a collapsed state, a reaction model, or a click action. Compose those behaviors with application state and existing controls such as `Button`, `Link`, `Collapsible`, `Tooltip`, and `Popover`. ## Import ```rust use gpui_kit::{div, ParentElement as _, Styled as _}; use gpui_kit::component::{ ActiveTheme as _, Colorize as _, Sizable as _, bubble::{ Bubble, BubbleContent, BubbleGroup, BubbleReactionSide, BubbleReactions, BubbleVariant, }, button::{Button, ButtonVariants as _}, message::MessageAlignment, }; ``` ## Anatomy and basic usage The shortest form adds children to the typed content slot: ```rust Bubble::new() .alignment(MessageAlignment::Start) .child("Can you review this draft?") ``` Use `content(...)` when the surface needs its own layout or style target: ```rust Bubble::new() .alignment(MessageAlignment::Start) .content( BubbleContent::new().child( gpui_kit::component::h_flex() .gap_2() .child("Can you review this draft?") .child("📎"), ), ) ``` The root is `min_w_0`, grows to the available width only for `Ghost`, and otherwise has a maximum width of 80% of its parent. Long text wraps inside the content slot when its child allows wrapping. An application that needs a different conversation measure can refine the root with `w(...)`, `max_w(...)`, or a child-specific layout. The default state is: | Property | Default | Meaning | | --- | --- | --- | | Alignment | unset | The parent may supply alignment; a standalone bubble does not force an edge. | | Variant | `Filled` | Primary semantic surface. | | Reactions | none | No reaction region is rendered. | | Maximum width | `0.8` of the parent | Applies to regular variants. | | Surface radius | `cx.theme().radius_2xl()` | Follows the active theme. | | Content padding | `px_3()` / `py_2()` | Applied by `BubbleContent` for regular variants. | ## Alignment `MessageAlignment::Start` and `MessageAlignment::End` are shared with `Message`: ```rust Bubble::new() .alignment(MessageAlignment::Start) .with_variant(BubbleVariant::Secondary) .child("Incoming message"); Bubble::new() .alignment(MessageAlignment::End) .child("Outgoing message") ``` When a bubble is placed in `MessageContent::bubble(...)`, `Message` propagates its alignment to the content surface. Leave the bubble alignment unset in that case so the message remains the single owner of horizontal placement. Set it explicitly when a bubble is used on its own or when a custom parent intentionally overrides the message row. ## Variants `BubbleVariant` selects semantic colors and surface treatment. It does not change the content model: ```rust Bubble::new() .with_variant(BubbleVariant::Filled) .child("Primary response"); Bubble::new() .with_variant(BubbleVariant::Secondary) .child("Neutral incoming response"); Bubble::new() .with_variant(BubbleVariant::Muted) .child("Low-emphasis context"); Bubble::new() .with_variant(BubbleVariant::Tinted) .child("Subtle selected or emphasized response"); Bubble::new() .with_variant(BubbleVariant::Outline) .child("A response that needs a visible boundary"); Bubble::new() .with_variant(BubbleVariant::Ghost) .child("A full-width, unframed message surface"); Bubble::new() .with_variant(BubbleVariant::Destructive) .child("The operation failed; explain what the user can do next.") ``` `Filled` is the default. `Ghost` removes the surface padding, border, and radius, and can occupy the full row. `Destructive` uses the semantic destructive color with a theme-aware translucent surface; its meaning must also be present in text or another non-color cue. All other variants keep the regular content surface and derive colors from the active theme. ## Rich content and long messages Bubble children are arbitrary GPUI elements. Compose text, code, files, buttons, or custom layouts without a bubble-specific content enum: ```rust use gpui_kit::{div, Styled as _}; use gpui_kit::component::{h_flex, v_flex, Icon, IconName}; Bubble::new() .content( BubbleContent::new().child( h_flex() .gap_3() .items_start() .child(Icon::new(IconName::FileText)) .child( v_flex() .min_w_0() .child("design-notes.pdf") .child(div().text_sm().child("PDF · 2.4 MB")), ), ), ) ``` For a long response, keep the child `min_w_0()` and choose wrapping or truncation at the content boundary. `Bubble` does not truncate arbitrary children. An application layout can expose a `Show more` affordance by wrapping the content in `Collapsible`; the bubble itself has no hidden-text state. ```rust // The state and trigger belong to the application. The same Bubble can be // rendered in the expanded and collapsed states. Bubble::new() .with_variant(BubbleVariant::Ghost) .content(BubbleContent::new().child(long_response_element)) ``` ## Groups `BubbleGroup` is a styleable vertical stack. It does not infer sender identity or remove headers; the application decides which consecutive bubbles belong to one sender: ```rust BubbleGroup::new() .child( Bubble::new() .alignment(MessageAlignment::Start) .with_variant(BubbleVariant::Secondary) .child("The first paragraph belongs to Alice."), ) .child( Bubble::new() .alignment(MessageAlignment::Start) .with_variant(BubbleVariant::Secondary) .child("The second paragraph uses the same group."), ) ``` Use `MessageGroup` when the repeated unit is a complete message with avatar, header, body, and footer. Use `BubbleGroup` when only the surface stack is being repeated. ## Reactions and interactive content `BubbleReactions` positions a region at the top or bottom edge. Put semantic controls inside it. Use the typed `action(Button)` builder for a button that should read as part of the reaction surface: ```rust Bubble::new() .alignment(MessageAlignment::Start) .with_variant(BubbleVariant::Outline) .child("This response has feedback.") .reactions( BubbleReactions::new() .side(BubbleReactionSide::Bottom) .alignment(MessageAlignment::End) .action( Button::new("bubble-like") .ghost() .small() .label("Like · 2"), ) .action( Button::new("bubble-copy") .ghost() .small() .label("Copy"), ), ) ``` The defaults for `BubbleReactions` are `Bottom` and `End`. For a top-attached region aligned to the leading edge: ```rust BubbleReactions::new() .side(BubbleReactionSide::Top) .alignment(MessageAlignment::Start) .action(Button::new("bubble-more").ghost().xsmall().label("More")) ``` `action(Button)` tells `BubbleReactions` that the child is a semantic action. When a reaction region contains any typed action, the container removes its decorative content padding and applies the active theme's full/pill radius to each typed button, so the buttons and reaction surface read as one control group. The supplied `Button` remains customizable: its variant, size, icon, `.on_click(...)` callback, and `.tooltip(...)` are preserved. The typed action owns the pill corner radius so the button stays joined to the reaction surface; use the generic path below when a button needs a different radius. Multiple actions can be added with repeated `.action(...)` calls. Use `.child(...)` for emoji, text, a custom element, or an overlay composition that is not a direct `Button`. This generic path remains backward-compatible and does not opt that child into the compact action treatment. If the same region also contains any `.action(...)`, the whole reaction region still uses the compact surface layout: ```rust BubbleReactions::new() .child("👍 2") .action( Button::new("bubble-reply") .ghost() .xsmall() .label("Reply"), ) ``` Nested interactive wrappers such as `Popover` remain on the generic `.child(...)` path because `action(...)` accepts a direct `Button`. If the wrapper's trigger should share the reaction geometry, opt into that layout explicitly with `p_0()` on the reaction region and a theme-derived full radius on the trigger button. The same escape hatch gives an arbitrary button its own radius or surface treatment. ```rust BubbleReactions::new().p_0().child( gpui_kit::component::popover::Popover::new("bubble-more") .trigger( Button::new("bubble-more-trigger") .ghost() .xsmall() .label("More") .rounded(cx.theme().radius_full()), ) .child(Button::new("bubble-copy").label("Copy")), ) ``` The reaction container supplies the default spacing, rounded semantic surface, and contrast border. Caller `Styled` refinements are applied after those defaults, so an application can customize the reaction surface or the `Button` itself. There is no separate `BubbleAction` component or reaction data model; the application owns counts, selected state, and submitted actions. Button focus, disabled state, and keyboard activation remain the responsibility of `Button`. The current Button accessibility label comes from its visible `.label(...)` value; a tooltip is supplemental. For a URL inside a bubble use `Link`; for an in-app command use `Button`. A tooltip or popover can wrap the relevant child using the existing overlay components. ## Custom styling and theme tokens `Bubble`, `BubbleContent`, `BubbleGroup`, and `BubbleReactions` implement `Styled`. Refinements are applied after the component defaults, so callers can adjust spacing, width, typography, borders, backgrounds, and shadows at the appropriate part boundary: ```rust Bubble::new() .w_full() .content( BubbleContent::new() .rounded(cx.theme().radius_lg) .bg(cx.theme().group_box) .text_color(cx.theme().group_box_foreground) .border_1() .border_color(cx.theme().border) .px_4() .py_3() .child("Application-owned surface treatment"), ) ``` Use semantic theme roles (`primary`, `muted`, `group_box`, `border`, `destructive`, and their foreground colors) instead of raw palette values. Radii come from the active theme, so a custom theme can make all conversation surfaces more square or more rounded consistently. The component uses the shared spacing and typography scale; a product-specific scale should be owned by the surrounding design-system layer and passed through its own builders. Style the group and reaction region independently when the composition needs a different rhythm: ```rust BubbleGroup::new() .gap_3() .child(Bubble::new().child("First")) .child(Bubble::new().child("Second")); BubbleReactions::new() .px_2() .bg(cx.theme().background) .border_color(cx.theme().ring) .action(Button::new("bubble-reaction").ghost().xsmall().label("👍")) ``` ## Accessibility and state guidance * Use visible text, an icon with a label, or an accessible `Button` label to communicate reactions and actions. Color and a bubble variant are not sufficient status announcements. * Keep keyboard actions inside `Button`, `Link`, `Collapsible`, `Tooltip`, or `Popover`. `Bubble` and `BubbleReactions` are layout elements and do not create focus targets themselves. * Preserve readable contrast when overriding a surface. Pair a custom background with the matching semantic foreground token or an explicitly verified theme role. * For loading or generated content, render a meaningful text label and use `ShimmerText` or `Marker` for motion. Respect the application's reduced-motion behavior; the shimmer utility renders static text when reduced motion is requested. * A failed or destructive bubble should include the error and the next action, not only a red surface. ## When to use another component Use `Message` when sender identity, metadata, or a footer belongs to the same row. Use `Marker` for a compact status or timeline boundary. Use `GroupBox` or an application-owned surface for a non-conversational document. Use a plain `div()`/`h_flex()` when the row has no shared bubble behavior; adding a bubble only to obtain padding makes the hierarchy harder to read. ## API reference ### `Bubble` | Method | Default | Purpose | | --- | --- | --- | | `new()` | filled, no alignment, no reactions | Create a bubble. | | `alignment(MessageAlignment)` | unset | Place the bubble at the leading or trailing edge. | | `with_variant(BubbleVariant)` | `Filled` | Select the semantic surface treatment. | | `content(BubbleContent)` | empty typed content | Replace the visible content surface; direct children move into it. | | `reactions(BubbleReactions)` | none | Attach a reaction region. | `Bubble` also implements `ParentElement` for the direct `.child(...)` form and `Styled` for root layout refinements. ### `BubbleContent` | Method | Default | Purpose | | --- | --- | --- | | `new()` | empty | Create the visible surface slot. | | `.child(...)` | — | Add arbitrary GPUI elements. | | `Styled` methods | component defaults | Refine padding, radius, colors, typography, and layout. | The parent `Bubble` supplies its variant and alignment to this slot. A standalone `BubbleContent` therefore has the default `Filled` treatment. ### `BubbleGroup` | Method | Default | Purpose | | --- | --- | --- | | `new()` | empty vertical stack | Create a group. | | `.child(...)` | — | Add consecutive bubbles. | | `Styled` methods | `gap_2()` | Refine group spacing and layout. | ### `BubbleReactions` | Method | Default | Purpose | | --- | --- | --- | | `new()` | bottom, end aligned | Create a reaction region. | | `side(BubbleReactionSide)` | `Bottom` | Attach it above or below the bubble. | | `alignment(MessageAlignment)` | `End` | Align children along the bubble edge. | | `action(Button)` | — | Add a typed action that shares the reaction surface and full/pill radius. | | `.child(...)` | — | Add emoji, text, or arbitrary GPUI elements. | | `Styled` methods | themed reaction surface | Refine spacing, colors, and layout. | ### Related types * \[`BubbleVariant`] — `Filled`, `Secondary`, `Muted`, `Tinted`, `Outline`, `Ghost`, and `Destructive`. * \[`BubbleReactionSide`] — `Top` or `Bottom`. * \[`MessageAlignment`] — `Start` or `End`. [Bubble]: https://docs.rs/gpui-component/latest/gpui_component/bubble/struct.Bubble.html [BubbleContent]: https://docs.rs/gpui-component/latest/gpui_component/bubble/struct.BubbleContent.html [BubbleGroup]: https://docs.rs/gpui-component/latest/gpui_component/bubble/struct.BubbleGroup.html [BubbleReactions]: https://docs.rs/gpui-component/latest/gpui_component/bubble/struct.BubbleReactions.html [BubbleVariant]: https://docs.rs/gpui-component/latest/gpui_component/bubble/enum.BubbleVariant.html [BubbleReactionSide]: https://docs.rs/gpui-component/latest/gpui_component/bubble/enum.BubbleReactionSide.html [MessageAlignment]: https://docs.rs/gpui-component/latest/gpui_component/message/enum.MessageAlignment.html --- --- url: /zh-CN/docs/components/bubble.md description: 可承载文本、富内容和 reaction 控件的聊天消息表面。 --- # Bubble `Bubble` 是聊天内容的可组合表面。它负责对齐、内容宽度和 reaction 的定位;`BubbleContent` 负责背景、边框、圆角、padding 和文字样式。应用可以把 `Button`、`Link`、`Collapsible`、`Tooltip` 或 `Popover` 放在 bubble 内部,而不需要为每一种消息内容增加专用组件。 ## 适用场景 * 用于一条消息中的文本、代码、文件摘要或其他富内容。 * 用 `BubbleGroup` 堆叠同一发送者的连续消息。 * 用 `BubbleReactions` 把可聚焦的 `Button` 放在 bubble 的上方或下方。 * 已经有 `Message` 时,让 `Message` 负责发送者、header、footer 和整体对齐;Bubble 只负责消息表面。 如果内容只是普通的图标和文本行,或需要完整的消息生命周期状态,请直接组合 `h_flex()`、`Marker`、`Message` 等更合适的组件。 ## 导入 基础组合需要以下类型: ```rust use gpui_kit::{ParentElement as _, Styled as _}; use gpui_kit::component::{ bubble::{ Bubble, BubbleContent, BubbleGroup, BubbleReactionSide, BubbleReactions, BubbleVariant, }, button::{Button, ButtonVariants as _}, collapsible::Collapsible, h_flex, link::Link, message::MessageAlignment, popover::Popover, ActiveTheme as _, Colorize as _, IconName, Sizable as _, StyledExt as _, v_flex, }; ``` 这些 trait import 让文中的 `child(...)`、主题读取、语义尺寸和 Button variant builder 与实际 crate API 对齐。 ## 结构 ```text Bubble ├── BubbleContent # 可见的内容表面 └── BubbleReactions # 可选,附着在表面边缘 └── Button / 其他交互控件 ``` `Bubble` 的直接 `.child(...)` 会把 child 添加到 `BubbleContent`。需要精确控制 surface 样式、或需要区分多个富内容区域时,使用 `.content(BubbleContent::new()...)`。 ## 基础用法 最短的文本 bubble 可以直接添加 child: ```rust Bubble::new() .alignment(MessageAlignment::Start) .child("可以帮我检查一下吗?") ``` 显式创建 `BubbleContent` 适合需要调整 surface 的场景: ```rust Bubble::new() .alignment(MessageAlignment::Start) .content( BubbleContent::new() .child("这里可以继续添加任意 GPUI element。"), ) ``` `Bubble::new()` 默认使用 `BubbleVariant::Filled`。Bubble 默认不绑定对齐,适合由外层 `Message` 传播对齐;独立使用时应设置 `.alignment(...)`。 ## 对齐 `Bubble` 与 `Message` 共用 `MessageAlignment`: ```rust Bubble::new() .alignment(MessageAlignment::Start) .child("收到的消息"); Bubble::new() .alignment(MessageAlignment::End) .child("发出的消息") ``` | 值 | 含义 | | --- | --- | | `MessageAlignment::Start` | 放在消息行的起始侧。 | | `MessageAlignment::End` | 放在消息行的结束侧。 | | 未设置 | 保留给父级布局决定;放进 `MessageContent` 时通常使用这一方式。 | Bubble 的普通 variant 最大宽度为可用宽度的 80%,`Ghost` variant 会占满父级宽度。宽度仍可以在 `Bubble` 或 `BubbleContent` 上通过 `Styled` refinement 调整。 ## 样式变体 | Variant | 适合表达的语义 | 默认 surface | | --- | --- | --- | | `Filled` | 主要消息内容,默认值。 | `primary` 与 `primary_foreground`。 | | `Secondary` | 次级强调的消息。 | `muted` 底与 `secondary_foreground`(主题的 `secondary` 是按钮角色,比 shadcn 的会话 secondary 深一档)。 | | `Muted` | 低强调度的普通内容。 | `muted` 与普通前景色。 | | `Tinted` | 轻微使用 primary 色调的内容。 | 由主题背景与 primary 混合。 | | `Outline` | 需要清晰边界但不需要填充色的内容。 | 背景色与 `border`。 | | `Ghost` | 作为消息布局中的无表面富内容。 | 无 padding、边框和背景。 | | `Destructive` | 失败、拒绝或无效结果。 | 语义 destructive 色。 | ```rust for variant in [ BubbleVariant::Filled, BubbleVariant::Secondary, BubbleVariant::Muted, BubbleVariant::Tinted, BubbleVariant::Outline, BubbleVariant::Ghost, BubbleVariant::Destructive, ] { let bubble = Bubble::new() .alignment(MessageAlignment::Start) .with_variant(variant) .child("同一内容可以切换不同语义表面"); // 在应用的 view 中渲染 bubble。 } ``` 在实际界面中应根据内容语义选择 variant,不要仅为增加色彩而并列使用所有 variant。`Destructive` 也应配合文字说明,不能只依靠颜色表达失败。 ## 富内容 `BubbleContent` 接受任意 GPUI element,因此代码块、文件摘要和多段内容都可以由应用组合: ```rust use gpui_kit::{div, ParentElement as _, Styled as _}; use gpui_kit::component::{h_flex, v_flex, ActiveTheme as _, Sizable as _, StyledExt as _}; Bubble::new() .alignment(MessageAlignment::Start) .content( BubbleContent::new().child( v_flex() .gap_2() .child("请查看下面的文件:") .child( h_flex() .gap_2() .child("📄") .child("quarterly-report.pdf"), ) .child( div() .text_xs() .text_color(cx.theme().muted_foreground) .child("PDF · 2.4 MB"), ), ), ) ``` 较长文本仍由调用方决定换行、截断和最小宽度。可将 `min_w_0()`、`max_w_full()` 等 refinement 放在富内容的内部容器上,使内容不会把消息行撑开。 ### 链接和按钮 链接和应用命令应该保留自己的语义:URL 使用 `Link`,应用操作使用 `Button`。它们可以直接放进 `BubbleContent`: ```rust Bubble::new() .alignment(MessageAlignment::Start) .content( BubbleContent::new() .child("文档已更新:") .child( Link::new("release-notes") .href("https://example.com/release-notes") .child("查看 release notes"), ) .child( Button::new("retry") .ghost() .small() .label("重试"), ), ) ``` 不要把整个 bubble 变成一个不可区分的点击区域。每个操作都应有明确的焦点目标、标签和结果。 ### 折叠内容 Bubble 没有专用的 `ShowMore` API。需要折叠长文本时,直接组合 `Collapsible`,并由应用状态控制 `open`: ```rust Bubble::new() .alignment(MessageAlignment::Start) .content( BubbleContent::new().child( Collapsible::new() .open(show_details) .child( Button::new("toggle-details") .ghost() .small() .label(if show_details { "收起详情" } else { "显示详情" }), ) .content( div() .text_sm() .child("这里放置较长的诊断信息或工具输出。"), ), ), ) ``` `Collapsible::open(...)` 是静态配置;切换状态、键盘 action 和状态持有仍属于应用 view。 ### Tooltip 和 Popover Tooltip 应附着在具体的 icon button 或 link 上。Button 支持通过 `tooltip(...)` 添加提示: ```rust Bubble::new() .content( BubbleContent::new().child( Button::new("copy-message") .ghost() .icon(IconName::Copy) .label("复制") .tooltip("复制消息"), ), ) ``` 需要显示更多操作或上下文内容时,可以组合 `Popover`。Popover 的触发器与内容由应用提供: ```rust Popover::new("message-options") .trigger( Button::new("message-options-trigger") .ghost() .icon(IconName::Ellipsis) .label("更多操作"), ) .child(Button::new("copy").label("复制")) .child(Button::new("report").label("报告问题")) ``` Popover 需要在包含 `Root` 的窗口中使用,触发器应保持可聚焦;菜单项的权限、关闭后续行为和业务状态由应用负责。 ## 分组 `BubbleGroup` 只负责以统一间距堆叠连续 bubble,不保存发送者或时间信息: ```rust BubbleGroup::new() .child( Bubble::new() .alignment(MessageAlignment::Start) .with_variant(BubbleVariant::Muted) .child("第一条消息"), ) .child( Bubble::new() .alignment(MessageAlignment::Start) .with_variant(BubbleVariant::Muted) .child("同一发送者的第二条消息"), ) ``` 跨发送者、头像、header 和 footer 的组合应使用 `MessageGroup` 或应用自己的消息列表。 ## Reactions `BubbleReactions` 负责 reaction 区域的边缘定位与基础表面。对于需要和 reaction 表面连成一体的按钮,使用类型明确的 `action(Button)`: ```rust Bubble::new() .alignment(MessageAlignment::Start) .child("看起来没问题。") .reactions( BubbleReactions::new() .side(BubbleReactionSide::Bottom) .alignment(MessageAlignment::End) .action( Button::new("like") .ghost() .small() .label("👍 2"), ) .action( Button::new("reply") .ghost() .small() .label("回复"), ), ) ``` reaction 可以附着到上边缘: ```rust Bubble::new() .child("需要在上方显示状态的消息") .reactions( BubbleReactions::new() .side(BubbleReactionSide::Top) .alignment(MessageAlignment::Start) .action(Button::new("status").ghost().label("处理中")), ) ``` `action(Button)` 会让 `BubbleReactions` 识别出这是语义操作。当 reaction 区域 包含任意一个类型化操作时,组件会移除装饰性内边距,并将每个类型化 Button 的圆角设为当前主题的最大圆角(`radius_full()`),使按钮和外层表面连成一个 整体。传入的 `Button` 仍可以自定义变体、尺寸、图标、`.on_click(...)` 点击回调和 `.tooltip(...)`;类型化操作会保留这个胶囊圆角以维持整体外观,需要不同圆角时 使用下面的通用路径。多个按钮可以重复调用 `.action(...)`。 需要放入 emoji、文本、自定义子元素或直接 `Button` 之外的包装组件时,继续使用 `.child(...)`。这个通用路径保持向后兼容,也不会自动启用操作按钮的紧凑样式。 即使同一区域混合了 `.child(...)`,只要存在一个 `.action(...)`,整个 reaction 区域 仍会采用紧凑表面布局;普通子元素继续按通用路径渲染: ```rust BubbleReactions::new() .child("👍 2") .action( Button::new("reply") .ghost() .small() .label("回复"), ) ``` 像 `Popover` 这样的嵌套交互包装组件也应继续使用 `.child(...)`,因为 `action(...)` 接收的是直接的 `Button`。如果包装组件的触发按钮需要和 reaction 表面共用几何样式,可以在 `BubbleReactions` 上显式使用 `p_0()`,并在触发按钮上 使用主题的最大圆角;需要给普通 Button 保留其他圆角或表面样式时也使用这个通用路径: ```rust BubbleReactions::new().p_0().child( gpui_kit::component::popover::Popover::new("bubble-more") .trigger( Button::new("bubble-more-trigger") .ghost() .small() .label("更多") .rounded(cx.theme().radius_full()), ) .child(Button::new("bubble-copy").label("复制")), ) ``` reaction 表面的默认样式仍由组件提供;调用方的 `Styled` 样式调整会在默认值之后 应用,因此可以继续调整 `BubbleReactions` 或 `Button` 的其他样式。`.action(...)` 会统一管理 Button 的胶囊圆角;需要恢复额外内边距或使用不同圆角时,使用 `.child(...)`,或在 reaction 区域上明确调用 `px_2()`、`p_0()` 等样式方法。 `BubbleReactions` 不额外提供 `BubbleAction` 或 reaction 数据模型;计数、选中状态、 提交动作和错误提示由 Button 外层的应用状态负责。 ## 自定义样式与主题 token 所有公开 part 都实现 `Styled`。默认样式先应用,调用方 refinement 后应用,因此可以只调整需要改变的部分: ```rust Bubble::new() .alignment(MessageAlignment::Start) .px_2() .content( BubbleContent::new() .rounded(cx.theme().radius_lg) .bg(cx.theme().muted) .text_color(cx.theme().foreground) .border_color(cx.theme().border) .child("遵循当前主题的自定义消息表面"), ) ``` 优先从 `cx.theme()` 读取语义颜色、圆角和间距。Bubble 的外层布局、可见 surface 和 reactions 各自有样式入口,应用可以在不复制组件内部布局的情况下调整背景、文字、边框、间距、最大宽度和 reaction 位置。 ## 可访问性 * Reaction 使用 `Button`;可见 `.label(...)` 是控件的可读名称,tooltip 只作为补充提示。`👍 2` 这类文本应同时说明它是“点赞”动作。 * 链接使用 `Link`,应用操作使用 `Button`,避免用普通 `div` 模拟控件。 * `Destructive`、`Tinted` 等颜色只提供视觉层次;失败、状态或结果必须在文本中说明。 * 折叠区域的触发器应是可聚焦的 `Button`,并在状态改变时更新“展开/收起”等可读标签。 * 需要键盘操作的 Popover 触发器和内容应遵循 `Root` 提供的焦点与 overlay 生命周期。 * 系统启用 reduced motion 时,Bubble 本身没有额外动画;应用为 child 增加动画时也应提供静态结果。 ## 何时不需要 Bubble * 只有状态文本和分隔线:使用 `Marker`。 * 需要头像、发送者、时间和送达状态:使用 `Message`,将 Bubble 放进 `MessageContent`。 * 只有一个独立操作:使用 `Button` 或 `Link`。 * 只需要一组普通横向内容:使用 `h_flex()` 或 `v_flex()`,避免为简单布局增加 Bubble 表面。 ## API 参考 ### `Bubble` | 方法 | 说明 | | --- | --- | | `new()` | 创建默认的 filled bubble;默认不设置对齐。 | | `alignment(MessageAlignment)` | 设置起始侧或结束侧对齐。 | | `with_variant(BubbleVariant)` | 设置 bubble surface variant。 | | `content(BubbleContent)` | 替换可见内容 surface;已添加的直接 children 会并入其中。 | | `reactions(BubbleReactions)` | 添加可选 reaction 区域。 | | `child(element)` | 通过 `ParentElement` 将 child 添加到 `BubbleContent`。 | | `Styled` | 调整外层布局、宽度、间距和其他 GPUI 样式。 | ### `BubbleContent` | 方法 | 说明 | | --- | --- | | `new()` | 创建空的内容 surface。 | | `child(element)` | 添加任意 GPUI element。 | | `Styled` | 调整 padding、背景、文字、边框和圆角。 | ### `BubbleGroup` | 方法 | 说明 | | --- | --- | | `new()` | 创建连续 bubble 的垂直 stack。 | | `child(element)` | 按顺序添加 bubble 或其他 element。 | | `Styled` | 调整 stack 间距、宽度和布局。 | ### `BubbleReactions` | 方法 | 说明 | | --- | --- | | `new()` | 创建默认在底部、结束侧对齐的 reaction 区域。 | | `side(BubbleReactionSide)` | 选择 `Top` 或 `Bottom`。 | | `alignment(MessageAlignment)` | 选择 reaction 区域的起始侧或结束侧对齐。 | | `action(Button)` | 添加类型化操作;与 reaction 表面共用主题最大圆角,并自动使用紧凑布局。 | | `child(element)` | 添加 emoji、文本或任意 GPUI 子元素;保留通用组合能力。 | | `Styled` | 调整 reaction 表面与定位 refinement。 | ### 类型链接 * [Bubble] * [BubbleContent] * [BubbleGroup] * [BubbleReactions] * [BubbleVariant] * [BubbleReactionSide] [Bubble]: https://docs.rs/gpui-component/latest/gpui_component/bubble/struct.Bubble.html [BubbleContent]: https://docs.rs/gpui-component/latest/gpui_component/bubble/struct.BubbleContent.html [BubbleGroup]: https://docs.rs/gpui-component/latest/gpui_component/bubble/struct.BubbleGroup.html [BubbleReactions]: https://docs.rs/gpui-component/latest/gpui_component/bubble/struct.BubbleReactions.html [BubbleVariant]: https://docs.rs/gpui-component/latest/gpui_component/bubble/enum.BubbleVariant.html [BubbleReactionSide]: https://docs.rs/gpui-component/latest/gpui_component/bubble/enum.BubbleReactionSide.html --- --- url: /base/primitives/button.md description: 'An unstyled, accessible pressable with semantic state and keyboard activation.' --- # Button An unstyled, accessible pressable with semantic state and keyboard activation. Like every `gpui-base` primitive, Button supplies behavior and semantic structure without imposing a product visual language. Apply GPUI styles and compose the exported parts to match your design system. ## Example The [single native Cargo entrypoint](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/components.rs) selects this primitive from the [shared showcase implementation](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/mod.rs). The same showcase is compiled once for the WASM preview above. ```bash cargo run -p gpui-base --example components -- button ``` ## Import ```rust use gpui_kit::base::{Button}; ``` ## Anatomy and API The example composes `Button`. GPUI's standard styling and event traits provide presentation; these base types provide the interaction structure. The authoritative module is [`components/button.rs`](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/components/button.rs). Native and browser previews compile this same file. ## State and events Activation uses GPUI click handling. Styling for hover, active, focus, and disabled states remains application-owned. Keep controlled state on the parent render type or in a GPUI entity. Update it in callbacks and call `cx.notify()`; do not recreate persistent entities during every render. ## Complete Rust example The complete implementation used by the runnable showcase is embedded directly from Rust source: The command above supplies application initialization, window creation, and shared `BaseShowcase` state. ## Accessibility Provide an accessible name, preserve keyboard activation, and expose disabled state. ## Notes Use stable element IDs where accepted. Verify focus, hover, active, selected, disabled, reduced-motion, and high-contrast appearances in the consuming design system. --- --- url: /docs/components/button.md description: Displays a button or a component that looks like a button. --- # Button The [Button] element with multiple variants, sizes, and states. Supports icons, loading states, and can be grouped together. ## Import ```rust use gpui_kit::component::button::{Button, ButtonGroup}; ``` ## Usage ### Basic Button ```rust Button::new("my-button") .label("Click me") .on_click(|_, _, _| { println!("Button clicked!"); }) ``` ### Variants ```rust // Primary button Button::new("btn-primary").primary().label("Primary") // Secondary button (default) Button::new("btn-secondary").label("Secondary") // Danger button Button::new("btn-danger").danger().label("Delete") // Warning button Button::new("btn-warning").warning().label("Warning") // Success button Button::new("btn-success").success().label("Success") // Info button Button::new("btn-info").info().label("Info") // Ghost button Button::new("btn-ghost").ghost().label("Ghost") // Link button Button::new("btn-link").link().label("Link") // Text button Button::new("btn-text").text().label("Text") ``` ### Outline Buttons Outline style is not a variant itself, but can be combined with other variants. ```rust Button::new("btn").primary().outline().label("Primary Outline") Button::new("btn").danger().outline().label("Danger Outline") ``` ### Compact Button The `compact` method reduces the padding of the button for a more condensed appearance. ```rust // Compact (reduced padding) Button::new("btn") .label("Compact") .compact() ``` ### Sizeable The Button supports the [Sizable] trait for different sizes. ```rust Button::new("btn").xsmall().label("Extra Small") Button::new("btn").small().label("Small") Button::new("btn").label("Medium") // default Button::new("btn").large().label("Large") ``` ### With Icons The `icon` method supports multiple types, allowing you to use different visual indicators: * **[Icon] / [IconName]** - Static icons for actions and visual cues * **[Spinner]** - Animated loading indicator for async operations * **[ProgressCircle]** - Circular progress indicator showing completion percentage All icon types automatically adapt to the button's size and can be customized with colors and other properties. #### Icon Types ```rust use gpui_kit::component::{Icon, IconName}; // Using IconName (simplest) Button::new("btn") .icon(IconName::Check) .label("Confirm") // Using Icon with custom size Button::new("btn") .icon(Icon::new(IconName::Heart)) .label("Like") // Icon only (no label) Button::new("btn") .icon(IconName::Search) ``` #### Spinner Icon Use a [Spinner] to indicate loading or processing state: ```rust use gpui_kit::component::spinner::Spinner; // Basic spinner Button::new("btn") .icon(Spinner::new()) .label("Loading...") // Spinner with custom color Button::new("btn") .icon(Spinner::new().color(cx.theme().blue)) .label("Processing") // Spinner with icon Button::new("btn") .icon(Spinner::new().icon(IconName::LoaderCircle)) .label("Syncing") ``` #### ProgressCircle Icon Use a [ProgressCircle] to show progress percentage: ```rust use gpui_kit::component::progress::ProgressCircle; // Basic progress circle Button::new("btn") .icon(ProgressCircle::new("install-progress").value(45.0)) .label("Installing...") // Progress circle with custom color Button::new("btn") .primary() .icon( ProgressCircle::new("download-progress") .value(75.0) .color(cx.theme().primary_foreground) ) .label("Downloading") // Different sizes Button::new("btn") .small() .icon(ProgressCircle::new("progress-1").value(60.0)) .label("Installing...") Button::new("btn") .large() .icon(ProgressCircle::new("progress-2").value(80.0)) .label("Installing...") ``` #### Dynamic Icon Updates Icons can be updated dynamically based on component state: ```rust struct InstallButton { progress: f32, is_installing: bool, } impl InstallButton { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let button = Button::new("install-btn") .label(if self.is_installing { "Installing..." } else { "Install" }); if self.is_installing { button.icon( ProgressCircle::new("install-progress") .value(self.progress) ) } else { button.icon(IconName::Download) } } } ``` #### Loading State with Icons When a button is in loading state, it automatically handles icon transitions: ```rust // If icon is already a Spinner or ProgressCircle, it will be shown during loading Button::new("btn") .icon(Spinner::new()) .label("Processing") .loading(true) // Spinner will continue to show // If icon is a regular Icon, it will be replaced with a Spinner during loading Button::new("btn") .icon(IconName::Save) .label("Saving") .loading(true) // Icon will be replaced with Spinner ``` ### With a dropdown caret icon The `.dropdown_caret` method can allows adding a dropdown caret icon to end of the button. ```rust Button::new("btn") .label("Options") .dropdown_caret(true) ``` ### Button States There have `disabled`, `loading`, `selected` state for buttons to indicate different statuses. ```rust // Disabled Button::new("btn") .label("Disabled") .disabled(true) // Loading Button::new("btn") .label("Loading") .loading(true) // Selected Button::new("btn") .label("Selected") .selected(true) ``` ## Button Group ```rust ButtonGroup::new("btn-group") .child(Button::new("btn1").label("One")) .child(Button::new("btn2").label("Two")) .child(Button::new("btn3").label("Three")) ``` ### Toggle Button Group ```rust ButtonGroup::new("toggle-group") .multiple(true) // Allow multiple selections .child(Button::new("btn1").label("Option 1").selected(true)) .child(Button::new("btn2").label("Option 2")) .child(Button::new("btn3").label("Option 3")) .on_click(|selected_indices, _, _| { println!("Selected: {:?}", selected_indices); }) ``` ## Custom Variant ```rust use gpui_kit::component::button::ButtonCustomVariant; let custom = ButtonCustomVariant::new(cx) .color(cx.theme().magenta) .foreground(cx.theme().primary_foreground) .border(cx.theme().magenta) .hover(cx.theme().magenta.opacity(0.1)) .active(cx.theme().magenta); Button::new("custom-btn") .custom(custom) .label("Custom Button") ``` ## API Reference * [Button] * [ButtonGroup] * [ButtonCustomVariant] ## Examples ### With Tooltip ```rust Button::new("btn") .label("Hover me") .tooltip("This is a helpful tooltip") ``` ### Custom Children ```rust Button::new("btn") .child( h_flex() .items_center() .gap_2() .child("Custom Content") .child(IconName::ChevronDown) .child(IconName::Eye) ) ``` [Button]: https://docs.rs/gpui-component/latest/gpui_component/button/struct.Button.html [ButtonGroup]: https://docs.rs/gpui-component/latest/gpui_component/button/struct.ButtonGroup.html [ButtonCustomVariant]: https://docs.rs/gpui-component/latest/gpui_component/button/struct.ButtonCustomVariant.html [Sizable]: https://docs.rs/gpui-component/latest/gpui_component/trait.Sizable.html [Spinner]: https://docs.rs/gpui-component/latest/gpui_component/spinner/struct.Spinner.html [ProgressCircle]: https://docs.rs/gpui-component/latest/gpui_component/progress/struct.ProgressCircle.html [Icon]: https://docs.rs/gpui-component/latest/gpui_component/icon/struct.Icon.html [IconName]: https://docs.rs/gpui-component/latest/gpui_component/icon/enum.IconName.html --- --- url: /zh-CN/docs/components/button.md description: 显示一个按钮,或外观表现为按钮的组件。 --- # Button [Button] 是一个支持多种变体、尺寸和状态的按钮组件。它支持图标、加载态,也可以与 [ButtonGroup] 组合使用。 ## 导入 ```rust use gpui_kit::component::button::{Button, ButtonGroup}; ``` ## 用法 ### 基础按钮 ```rust Button::new("my-button") .label("Click me") .on_click(|_, _, _| { println!("Button clicked!"); }) ``` ### 变体 ```rust // Primary button Button::new("btn-primary").primary().label("Primary") // Secondary button (default) Button::new("btn-secondary").label("Secondary") // Danger button Button::new("btn-danger").danger().label("Delete") // Warning button Button::new("btn-warning").warning().label("Warning") // Success button Button::new("btn-success").success().label("Success") // Info button Button::new("btn-info").info().label("Info") // Ghost button Button::new("btn-ghost").ghost().label("Ghost") // Link button Button::new("btn-link").link().label("Link") // Text button Button::new("btn-text").text().label("Text") ``` ### Outline 按钮 `outline` 不是独立变体,而是可以和其它变体叠加使用: ```rust Button::new("btn").primary().outline().label("Primary Outline") Button::new("btn").danger().outline().label("Danger Outline") ``` ### 紧凑模式 `compact` 会减少按钮内边距,使按钮更紧凑: ```rust Button::new("btn") .label("Compact") .compact() ``` ### 尺寸 Button 支持 [Sizable] trait: ```rust Button::new("btn").xsmall().label("Extra Small") Button::new("btn").small().label("Small") Button::new("btn").label("Medium") // default Button::new("btn").large().label("Large") ``` ### 图标 `icon` 方法支持多种图标类型: * **[Icon] / [IconName]** - 静态图标 * **[Spinner]** - 加载中的旋转图标 * **[ProgressCircle]** - 环形进度图标 这些图标会自动适配按钮尺寸,也可以继续定制颜色和其他属性。 #### 基础图标 ```rust use gpui_kit::component::{Icon, IconName}; Button::new("btn") .icon(IconName::Check) .label("Confirm") Button::new("btn") .icon(Icon::new(IconName::Heart)) .label("Like") Button::new("btn") .icon(IconName::Search) ``` #### Spinner ```rust use gpui_kit::component::spinner::Spinner; Button::new("btn") .icon(Spinner::new()) .label("Loading...") Button::new("btn") .icon(Spinner::new().color(cx.theme().blue)) .label("Processing") Button::new("btn") .icon(Spinner::new().icon(IconName::LoaderCircle)) .label("Syncing") ``` #### ProgressCircle ```rust use gpui_kit::component::progress::ProgressCircle; Button::new("btn") .icon(ProgressCircle::new("install-progress").value(45.0)) .label("Installing...") Button::new("btn") .primary() .icon( ProgressCircle::new("download-progress") .value(75.0) .color(cx.theme().primary_foreground) ) .label("Downloading") ``` ### 动态更新图标 图标可以随组件状态动态变化: ```rust struct InstallButton { progress: f32, is_installing: bool, } impl InstallButton { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let button = Button::new("install-btn") .label(if self.is_installing { "Installing..." } else { "Install" }); if self.is_installing { button.icon( ProgressCircle::new("install-progress") .value(self.progress) ) } else { button.icon(IconName::Download) } } } ``` ### 加载态 按钮进入 `loading(true)` 时,会自动处理图标切换: ```rust Button::new("btn") .icon(Spinner::new()) .label("Processing") .loading(true) Button::new("btn") .icon(IconName::Save) .label("Saving") .loading(true) ``` ### 下拉箭头 `.dropdown_caret(true)` 可以在按钮右侧增加一个下拉箭头: ```rust Button::new("btn") .label("Options") .dropdown_caret(true) ``` ### 状态 Button 常见状态包括 `disabled`、`loading` 和 `selected`: ```rust Button::new("btn") .label("Disabled") .disabled(true) Button::new("btn") .label("Loading") .loading(true) Button::new("btn") .label("Selected") .selected(true) ``` ## Button Group ```rust ButtonGroup::new("btn-group") .child(Button::new("btn1").label("One")) .child(Button::new("btn2").label("Two")) .child(Button::new("btn3").label("Three")) ``` ### 切换式按钮组 ```rust ButtonGroup::new("toggle-group") .multiple(true) .child(Button::new("btn1").label("Option 1").selected(true)) .child(Button::new("btn2").label("Option 2")) .child(Button::new("btn3").label("Option 3")) .on_click(|selected_indices, _, _| { println!("Selected: {:?}", selected_indices); }) ``` ## 自定义变体 ```rust use gpui_kit::component::button::ButtonCustomVariant; let custom = ButtonCustomVariant::new(cx) .color(cx.theme().magenta) .foreground(cx.theme().primary_foreground) .border(cx.theme().magenta) .hover(cx.theme().magenta.opacity(0.1)) .active(cx.theme().magenta); Button::new("custom-btn") .custom(custom) .label("Custom Button") ``` ## 示例 ### Tooltip ```rust Button::new("btn") .label("Hover me") .tooltip("This is a helpful tooltip") ``` ### 自定义子内容 ```rust Button::new("btn") .child( h_flex() .items_center() .gap_2() .child("Custom Content") .child(IconName::ChevronDown) .child(IconName::Eye) ) ``` [Button]: https://docs.rs/gpui-component/latest/gpui_component/button/struct.Button.html [ButtonGroup]: https://docs.rs/gpui-component/latest/gpui_component/button/struct.ButtonGroup.html [ButtonCustomVariant]: https://docs.rs/gpui-component/latest/gpui_component/button/struct.ButtonCustomVariant.html [Sizable]: https://docs.rs/gpui-component/latest/gpui_component/trait.Sizable.html [Spinner]: https://docs.rs/gpui-component/latest/gpui_component/spinner/struct.Spinner.html [ProgressCircle]: https://docs.rs/gpui-component/latest/gpui_component/progress/struct.ProgressCircle.html [Icon]: https://docs.rs/gpui-component/latest/gpui_component/icon/struct.Icon.html [IconName]: https://docs.rs/gpui-component/latest/gpui_component/icon/enum.IconName.html --- --- url: /zh-CN/base/primitives/button.md description: 无样式、可访问且支持语义状态和键盘激活的按钮。 --- # Button `Button` 提供按钮行为和语义结构,不强加产品视觉语言。请使用 GPUI 样式并组合导出的部件,使其符合你的设计系统。 ## 示例 原生示例和页面上方的 WASM 预览共用同一份实现: ```bash cargo run -p gpui-base --example components -- button ``` ## 导入 ```rust use gpui_kit::base::Button; ``` ## 结构与 API 示例组合了 `Button`。GPUI 的标准样式和事件 trait 负责表现,Base 类型负责交互结构。权威实现位于 [`components/button.rs`](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/components/button.rs)。 ## 状态与事件 激活使用 GPUI 点击处理。悬停、按下、焦点和禁用样式由应用负责。受控状态应保存在父渲染类型或 GPUI entity 中;在回调中更新并调用 `cx.notify()`,不要在每次渲染时重建持久 entity。 ## 完整 Rust 示例 ## 可访问性 提供可访问名称,保留键盘激活能力,并正确暴露禁用状态。 ## 注意事项 在支持的位置使用稳定元素 ID,并在消费端设计系统中验证焦点、悬停、按下、选中、禁用、减少动态效果和高对比度状态。 --- --- url: /base/primitives/calendar.md description: A state-driven date grid with selection matchers and custom item rendering. --- # Calendar A state-driven date grid with selection matchers and custom item rendering. Like every `gpui-base` primitive, Calendar supplies behavior and semantic structure without imposing a product visual language. Apply GPUI styles and compose the exported parts to match your design system. ## Example The [single native Cargo entrypoint](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/components.rs) selects this primitive from the [shared showcase implementation](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/mod.rs). The same showcase is compiled once for the WASM preview above. ```bash cargo run -p gpui-base --example components -- calendar ``` ## Import ```rust use gpui_kit::base::{Calendar, CalendarState}; ``` ## Anatomy and API The example composes `Calendar`, `CalendarState`. GPUI's standard styling and event traits provide presentation; these base types provide the interaction structure. The authoritative module is [`components/calendar.rs`](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/components/calendar.rs). Native and browser previews compile this same file. ## State and events Selection lives in `CalendarState`; configure matching and update the state from calendar item interaction. Keep controlled state on the parent render type or in a GPUI entity. Update it in callbacks and call `cx.notify()`; do not recreate persistent entities during every render. ## Complete Rust example The complete implementation used by the runnable showcase is embedded directly from Rust source: The command above supplies application initialization, window creation, and shared `BaseShowcase` state. ## Accessibility Label dates and selected, disabled, and today states; retain arrow-key navigation. ## Notes Use stable element IDs where accepted. Verify focus, hover, active, selected, disabled, reduced-motion, and high-contrast appearances in the consuming design system. --- --- url: /docs/components/calendar.md description: >- A flexible calendar component for displaying months, navigating dates, and selecting single dates or date ranges. --- # Calendar A standalone calendar component that provides a rich interface for date selection and navigation. The Calendar component supports single date selection, date range selection, multiple month views, custom disabled dates, and comprehensive keyboard navigation. * [CalendarState]: For managing calendar state and selection. * [Calendar]: For rendering the calendar UI. ## Import ```rust use gpui_kit::component::{ calendar::{Calendar, CalendarState, CalendarEvent, Date, Matcher}, }; ``` ## Usage ### Basic Calendar ```rust let state = cx.new(|cx| CalendarState::new(window, cx)); Calendar::new(&state) ``` ### Calendar with Initial Date ```rust use chrono::Local; let state = cx.new(|cx| { let mut state = CalendarState::new(window, cx); state.set_date(Local::now().naive_local().date(), window, cx); state }); Calendar::new(&state) ``` ### Date Range Calendar ```rust use chrono::{Local, Days}; let state = cx.new(|cx| { let mut state = CalendarState::new(window, cx); let now = Local::now().naive_local().date(); state.set_date( Date::Range(Some(now), now.checked_add_days(Days::new(7))), window, cx ); state }); Calendar::new(&state) ``` ### Multiple Months Display ```rust // Show 2 months side by side Calendar::new(&state) .number_of_months(2) // Show 3 months Calendar::new(&state) .number_of_months(3) ``` ### Calendar Sizes ```rust Calendar::new(&state).large() Calendar::new(&state) // medium (default) Calendar::new(&state).small() ``` ## Date Restrictions ### Disabled Weekends ```rust let state = cx.new(|cx| { CalendarState::new(window, cx) .disabled_matcher(vec![0, 6]) // Sunday=0, Saturday=6 }); Calendar::new(&state) ``` ### Disabled Specific Weekdays ```rust // Disable Sundays, Wednesdays, and Saturdays let state = cx.new(|cx| { CalendarState::new(window, cx) .disabled_matcher(vec![0, 3, 6]) }); Calendar::new(&state) ``` ### Disabled Date Range ```rust use chrono::{Local, Days}; let now = Local::now().naive_local().date(); let state = cx.new(|cx| { CalendarState::new(window, cx) .disabled_matcher(Matcher::range( Some(now), now.checked_add_days(Days::new(7)), )) }); Calendar::new(&state) ``` ### Disabled Date Interval ```rust // Disable dates outside the interval (before/after specified dates) let state = cx.new(|cx| { CalendarState::new(window, cx) .disabled_matcher(Matcher::interval( Some(now.checked_sub_days(Days::new(30)).unwrap()), now.checked_add_days(Days::new(30)) )) }); Calendar::new(&state) ``` ### Custom Disabled Dates ```rust // Disable first 5 days of each month let state = cx.new(|cx| { CalendarState::new(window, cx) .disabled_matcher(Matcher::custom(|date| { date.day0() < 5 // day0() returns 0-based day })) }); Calendar::new(&state) // Disable all Mondays let state = cx.new(|cx| { CalendarState::new(window, cx) .disabled_matcher(Matcher::custom(|date| { date.weekday() == chrono::Weekday::Mon })) }); Calendar::new(&state) // Disable past dates let state = cx.new(|cx| { CalendarState::new(window, cx) .disabled_matcher(Matcher::custom(|date| { *date < Local::now().naive_local().date() })) }); Calendar::new(&state) ``` ## Month/Year Navigation The Calendar automatically provides navigation controls: * **Previous/Next Month**: Arrow buttons in the header * **Month Selection**: Click on month name to open month picker * **Year Selection**: Click on year to open year picker * **Year Pages**: Navigate through 20-year pages in year view ### Custom Year Range ```rust let state = cx.new(|cx| { CalendarState::new(window, cx) .year_range((2020, 2030)) // Limit to specific year range }); Calendar::new(&state) ``` ## Handle Selection Events ```rust let state = cx.new(|cx| CalendarState::new(window, cx)); cx.subscribe(&state, |view, _, event, _| { match event { CalendarEvent::Selected(date) => { match date { Date::Single(Some(selected_date)) => { println!("Date selected: {}", selected_date); } Date::Range(Some(start), Some(end)) => { println!("Range selected: {} to {}", start, end); } Date::Range(Some(start), None) => { println!("Range start: {}", start); } _ => { println!("Selection cleared"); } } } } }); Calendar::new(&state) ``` ## Advanced Examples ### Business Days Only Calendar ```rust use chrono::Weekday; let state = cx.new(|cx| { CalendarState::new(window, cx) .disabled_matcher(Matcher::custom(|date| { matches!(date.weekday(), Weekday::Sat | Weekday::Sun) })) }); Calendar::new(&state) ``` ### Holiday Calendar ```rust use chrono::NaiveDate; use std::collections::HashSet; // Define holidays let holidays: HashSet = [ NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(), // New Year NaiveDate::from_ymd_opt(2024, 7, 4).unwrap(), // Independence Day NaiveDate::from_ymd_opt(2024, 12, 25).unwrap(), // Christmas ].into_iter().collect(); let state = cx.new(|cx| { CalendarState::new(window, cx) .disabled_matcher(Matcher::custom(move |date| { holidays.contains(date) })) }); Calendar::new(&state) ``` ### Multi-Month Range Selector ```rust let state = cx.new(|cx| { let mut state = CalendarState::new(window, cx); state.set_date(Date::Range(None, None), window, cx); // Range mode state }); Calendar::new(&state) .number_of_months(3) // Show 3 months for easier range selection ``` ### Quarterly View Calendar ```rust let state = cx.new(|cx| CalendarState::new(window, cx)); // Update to show current quarter's months Calendar::new(&state) .number_of_months(3) ``` ## Custom Styling ```rust use gpui_kit::{px, relative}; Calendar::new(&calendar) .p_4() // Custom padding .bg(cx.theme().secondary) // Custom background .border_2() // Custom border .border_color(cx.theme().primary) // Custom border color .rounded(px(12.)) // Custom border radius .w(px(400.)) // Custom width .h(px(350.)) // Custom height ``` ## API Reference * [Calendar] * [CalendarState] * [RangeMatcher] ## Examples ### Event Planning Calendar ```rust let event_calendar = cx.new(|cx| { let mut state = CalendarState::new(window, cx); // Disable past dates and weekends state = state.disabled_matcher(Matcher::custom(|date| { let now = Local::now().naive_local().date(); *date < now || matches!(date.weekday(), Weekday::Sat | Weekday::Sun) })); state }); Calendar::new(&event_calendar) .large() // Easier to see and interact with ``` ### Vacation Booking Calendar ```rust let vacation_calendar = cx.new(|cx| { let mut state = CalendarState::new(window, cx); state.set_date(Date::Range(None, None), window, cx); // Range mode state }); Calendar::new(&vacation_calendar) .number_of_months(2) // Show 2 months for range selection ``` ### Report Date Range Selector ```rust let report_calendar = cx.new(|cx| { let mut state = CalendarState::new(window, cx) .year_range((2020, 2025)); // Limit to business years state.set_date(Date::Range(None, None), window, cx); state }); Calendar::new(&report_calendar) .number_of_months(3) .small() // Compact for dashboard use ``` ### Availability Calendar ```rust use std::collections::HashSet; let unavailable_dates: HashSet = get_unavailable_dates(); let availability_calendar = cx.new(|cx| { CalendarState::new(window, cx) .disabled_matcher(Matcher::custom(move |date| { unavailable_dates.contains(date) })) }); Calendar::new(&availability_calendar) .number_of_months(2) ``` The Calendar component provides a foundation for any date-related UI requirements, from simple date pickers to complex scheduling interfaces. [Calendar]: https://docs.rs/gpui-component/latest/gpui_component/calendar/struct.Calendar.html [CalendarState]: https://docs.rs/gpui-component/latest/gpui_component/calendar/struct.CalendarState.html [RangeMatcher]: https://docs.rs/gpui-component/latest/gpui_component/calendar/struct.RangeMatcher.html --- --- url: /zh-CN/docs/components/calendar.md description: 用于展示月份、浏览日期和选择单日或区间的灵活日历组件。 --- # Calendar Calendar 是一个独立的日历组件,支持单日选择、日期区间选择、多月视图、禁用日期规则以及完整的键盘导航能力。 * [CalendarState] 负责状态与选择管理 * [Calendar] 负责渲染日历界面 ## 导入 ```rust use gpui_kit::component::{ calendar::{Calendar, CalendarState, CalendarEvent, Date, Matcher}, }; ``` ## 用法 ### 基础日历 ```rust let state = cx.new(|cx| CalendarState::new(window, cx)); Calendar::new(&state) ``` ### 初始日期 ```rust use chrono::Local; let state = cx.new(|cx| { let mut state = CalendarState::new(window, cx); state.set_date(Local::now().naive_local().date(), window, cx); state }); Calendar::new(&state) ``` ### 日期区间 ```rust use chrono::{Local, Days}; let state = cx.new(|cx| { let mut state = CalendarState::new(window, cx); let now = Local::now().naive_local().date(); state.set_date( Date::Range(Some(now), now.checked_add_days(Days::new(7))), window, cx ); state }); Calendar::new(&state) ``` ### 多月显示 ```rust Calendar::new(&state) .number_of_months(2) Calendar::new(&state) .number_of_months(3) ``` ### 尺寸 ```rust Calendar::new(&state).large() Calendar::new(&state) Calendar::new(&state).small() ``` ## 日期限制 ### 禁用周末 ```rust let state = cx.new(|cx| { CalendarState::new(window, cx) .disabled_matcher(vec![0, 6]) }); ``` ### 禁用日期区间 ```rust use chrono::{Local, Days}; let now = Local::now().naive_local().date(); let state = cx.new(|cx| { CalendarState::new(window, cx) .disabled_matcher(Matcher::range( Some(now), now.checked_add_days(Days::new(7)), )) }); ``` ### 自定义禁用规则 ```rust let state = cx.new(|cx| { CalendarState::new(window, cx) .disabled_matcher(Matcher::custom(|date| { date.weekday() == chrono::Weekday::Mon })) }); ``` ## 月份与年份导航 Calendar 自带这些导航能力: * 上一月 / 下一月按钮 * 点击月份切换月视图 * 点击年份切换年视图 * 在年视图中按页浏览年份 ### 自定义年份范围 ```rust let state = cx.new(|cx| { CalendarState::new(window, cx) .year_range((2020, 2030)) }); ``` ## 监听选择事件 ```rust let state = cx.new(|cx| CalendarState::new(window, cx)); cx.subscribe(&state, |view, _, event, _| { match event { CalendarEvent::Selected(date) => { match date { Date::Single(Some(selected_date)) => { println!("Date selected: {}", selected_date); } Date::Range(Some(start), Some(end)) => { println!("Range selected: {} to {}", start, end); } _ => {} } } } }); ``` ## 示例 ### 仅工作日 ```rust use chrono::Weekday; let state = cx.new(|cx| { CalendarState::new(window, cx) .disabled_matcher(Matcher::custom(|date| { matches!(date.weekday(), Weekday::Sat | Weekday::Sun) })) }); ``` ### 假期禁用 ```rust use chrono::NaiveDate; use std::collections::HashSet; let holidays: HashSet = [ NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(), NaiveDate::from_ymd_opt(2024, 7, 4).unwrap(), NaiveDate::from_ymd_opt(2024, 12, 25).unwrap(), ].into_iter().collect(); ``` ### 多月区间选择 ```rust let state = cx.new(|cx| { let mut state = CalendarState::new(window, cx); state.set_date(Date::Range(None, None), window, cx); state }); Calendar::new(&state) .number_of_months(3) ``` [Calendar]: https://docs.rs/gpui-component/latest/gpui_component/calendar/struct.Calendar.html [CalendarState]: https://docs.rs/gpui-component/latest/gpui_component/calendar/struct.CalendarState.html [RangeMatcher]: https://docs.rs/gpui-component/latest/gpui_component/calendar/struct.RangeMatcher.html --- --- url: /zh-CN/base/primitives/calendar.md description: 支持选择匹配器和自定义日期项渲染的状态驱动日期网格。 --- # Calendar 支持选择匹配器和自定义日期项渲染的状态驱动日期网格。 和所有 GPUI Base 原语一样,Calendar 只提供行为和语义结构,不规定产品视觉语言。请使用 GPUI 样式并组合导出的部件,使其符合你的设计系统。 ## 示例 原生示例和页面上方的 WASM 预览共用同一份实现: ```bash cargo run -p gpui-base --example components -- calendar ``` ## 导入 ```rust use gpui_kit::base::{Calendar, CalendarState}; ``` ## 结构与 API 示例组合上述公开类型。GPUI 的标准样式和事件 trait 负责表现,Base 类型负责交互结构。权威实现位于 [`components/calendar.rs`](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/components/calendar.rs),原生与浏览器预览编译的是同一文件。 ## 状态与事件 CalendarState 保存可见月份和选择;回调负责同步受控值。 受控状态应保存在父渲染类型或 GPUI entity 中;在回调中更新并调用 `cx.notify()`,不要在每次渲染时重建持久 entity。 ## 完整 Rust 示例 ## 可访问性 保留日期网格语义、方向键导航、焦点与选中状态。 ## 注意事项 在支持的位置使用稳定元素 ID,并在消费端设计系统中验证焦点、悬停、按下、选中、禁用、减少动态效果和高对比度状态。 --- --- url: /shell/capabilities.md description: >- The default-deny model, the fs / storage / clipboard / process surface, where storage lives, and what the sandbox withholds. --- # Capabilities A script gets **nothing** by default. No file access, no clipboard, no process execution, no network. `Capabilities::default()` is the empty set, and an assertion holds it there. The one exception is storage, and only at the manifest layer: an application that does not mention `storage` gets its own `localStorage`, the way a browser hands one to every origin without being asked. That is a convention about what an author has to *write*, not a hole in the model — the Rust `Capabilities` still deny it until a host says otherwise, and a manifest may still say `"storage": false`. See [Storage](#storage). The host grants what it grants, because only the host knows how far it trusts the code it is about to run. What it hands *out* — its own Rust, exposed on purpose — is [HostModule](./host-module.md). A View freezes its capabilities when it is loaded; changing the default affects applications loaded afterward, never code that is already running under an approved grant. ```rust gpui_kit::shell::set_capabilities( Capabilities::new() .read_roots([application_root.clone()]) .write_roots([data_directory.clone()]) .storage(true) .exit(true), ); ``` ## What a locally run application is granted Running a directory from the command line is an explicit act of trust — the same as `node app.js` — so `gpui-shell ` grants a specific, narrow set: | | | | ----------------- | -------------------------------------------------------- | | Read | The application directory, and its own storage directory | | Write | Its own storage directory | | Storage | Granted | | Clipboard | **Not** granted | | Process execution | **Not** granted | | Exit request | Granted | | Network | **Not** granted | An application can therefore read its own sources and assets and use its own storage, and nothing else. It is deliberately narrower than "everything", because an installed plugin will one day run through the same code path with a manifest deciding instead — and a grant that is generous for a local run would be the wrong default to inherit. ## Refusals name the fix Every denial ends in the thing to declare, not just the fact of the refusal: ```text filesystem read is not granted; declare capabilities.fs.read in the manifest ``` ```text `/etc/passwd` is outside every granted read root; add its directory to capabilities.fs.read in the manifest ``` ```text storage is not granted; set capabilities.storage to true ``` ```text running `git` is not granted; add it to capabilities.fs.execute in the manifest ``` ```text process.exit() is not granted; set capabilities.process.exit to true in the manifest ``` ## The manifest A directory is recognized by **`gpui-shell.json`**. The manifest is inert data — discovery reads identity, optional version metadata, Git dependencies, and requested permissions without executing the entry module. It recognizes `id`, `name`, `version`, `shell-version`, `entry`, `dependencies`, and `capabilities`; only `id`, `name`, and `entry` are required: ```json { "id": "com.example.quotes", "name": "Quotes", "version": "1.0.0", "shell-version": "0.6.0", "entry": "main.js", "dependencies": { "omarchy-ui": "huacnlee/omarchy-ui" }, "capabilities": { "fs": { "read": ["${pluginDir}"], "write": ["${dataDir}"] }, "network": { "hosts": ["stream.example.com"], "http": [ { "scheme": "https", "host": "api.example.com", "methods": ["GET"], "path_prefixes": ["/v1/"] } ] }, "storage": true, "clipboard": { "read": false, "write": true }, "process": { "exit": false } } } ``` `dependencies` maps a bare module name to a JavaScript package fetched from Git before the entry module runs — `import { Title } from "omarchy-ui"`. The string form takes strict GitHub shorthand or a full Git URL with an optional `#ref`; the object form with an explicit `branch` or `tag` remains supported. Every load also links the package where an editor finds it, so the import carries the package's own types and documentation. See [Dependencies](./dependencies.md) for version selection, the package entry, the cache, and what an editor sees. Every grant in that block defaults to *denied* when omitted, except `storage`, which defaults to granted — write `"storage": false` to refuse it. Unknown fields, invalid reverse-DNS ids, invalid explicitly declared SemVer values, incompatible `shell-version` values, escaping entries, and unknown `${...}` placeholders invalidate the manifest before code runs. Omitted `version` is reported as `unknown`. Omitted `shell-version` accepts the current runtime; when present, it names the oldest compatible gpui-shell release the application requires. Compatibility follows SemVer: `0.x` applications stay on the same minor line; stable releases stay on the same major line. The standalone CLI refuses an invalid manifest instead of executing its entry with silently different assumptions. Each scoped `network.http` rule binds the request scheme and effective port as well as its host, method and path. `scheme` defaults to `https`; `port` defaults to that scheme's standard port and only needs to be written for a non-default endpoint. ## `fs` ```js import * as fs from "fs/promises"; ``` Every call returns a promise. `await` them, or chain `.then` — and see the note below about `render`. | Call | Resolves to | | ------------------------------------------- | ------------------------------- | | `fs.readFile(path)` | `Uint8Array` | | `fs.readFile(path, "utf8")` | UTF-8 text | | `fs.writeFile(path, contents)` | — | | `fs.readdir(path)` | Names sorted by name | | `fs.readdir(path, { withFileTypes: true })` | `Dirent[]` with `isDirectory()` | | `fs.exists(path)` | `true` / `false` | | `fs.unlink(path)` | — | | `fs.rmdir(path)` | — | | `fs.mkdir(path, options?)` | — | ```js const source = await fs.readFile("notes.md", "utf8"); await fs.writeFile("notes.md", source + "\n"); ``` A relative path resolves against a granted root; an absolute one must already be inside one. Every path in the surface goes through **one resolver**, so there is no second place for a traversal bug to hide. It normalizes the path — `../../etc/passwd` is rejected before it reaches the filesystem — and then settles containment against the filesystem rather than against the string, because a grant is a promise about a *directory*: `data/escape/passwd` is lexically inside the root and reads `/etc/passwd` if `escape` is a symlink. The deepest part of the path that exists is resolved, links and all, and has to still be under the root; a symlink that resolves to nothing is refused rather than guessed at. **The grant is a handle, not a string.** The resolver hands back an open directory that cannot be made to name anything outside itself, and every read, write, listing, removal and mkdir runs against *that* — so a path is never resolved twice and there is no window between deciding it is allowed and using it. That matters because the obvious implementation does not work. Checking the path and then calling `std::fs` resolves it twice: a link already in place is caught by the check, and one that replaces a directory component *between* the two is followed out of the root by the second resolution. This is [`cap-std`](https://docs.rs/cap-std), which is `openat2(RESOLVE_BENEATH)` on Linux and a per-component `openat` walk elsewhere. Three of these behave in a way worth stating, each for the same reason: **A denied path throws rather than answering `false`.** "You may not look" and "it is not there" are different facts, and collapsing them would let a script map the filesystem outside its roots one boolean at a time. **Removing a file and removing a directory are two calls**, as they are in Rust, because "remove" alone does not say whether a directory is in scope. `remove_dir` takes an empty one and nothing else: write access is granted per root, so a recursive remove would turn one mistyped path into the loss of an application's whole data directory. A script that means it walks the tree itself. **`mkdir` means what it means everywhere else.** Bare, it creates one directory and fails if the parent is missing; `{ recursive: true }` creates the parents too. It was `create_dir_all` — a name that said what it did, but only by not being the name every script author already knows. **`read_dir` is sorted.** A script that renders a listing should not have to sort it, and should not inherit the filesystem's arbitrary order. **Every call returns a promise.** The syscall runs off the main thread, because a disk has no bound on how long it takes and blocking here would stop the frame and the VM together — somewhere the interrupt budget cannot even see, since the time is spent in the kernel. A **denial still throws at the call site** rather than rejecting. The capability check costs nothing and stays on the calling thread, and a rejected promise nobody awaited is a denial nobody sees. `readFile` refuses a file over 64 MiB, naming it and the limit. The alternative to a ceiling is a string that has to fit in the JavaScript heap — which is itself capped — so the failure without one is an out-of-memory inside the VM rather than a sentence you can act on. `writeFile` accepts at most 8 MiB per call. `readdir` stops at 10,000 entries or 1 MiB of UTF-8 name bytes, whichever comes first, so an adversarial directory cannot turn one promise into unbounded allocation. ::: tip Still do not read a file from `render` `render` describes the interface; it cannot await. Read in `init` or an event handler, keep the result on the View, and `cx.notify()` when it arrives. ::: ## Storage The [Web Storage API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API), as a browser has it. There is nothing to import: `localStorage` and `sessionStorage` are globals, and also live on `window`. ```js localStorage.setItem("todolist.items", JSON.stringify(items)); const saved = localStorage.getItem("todolist.items"); // null when the key is unset localStorage.removeItem("todolist.items"); localStorage.length; localStorage.key(0); localStorage.clear(); ``` | Member | Description | | ------------------- | ---------------------------------------------- | | `length` | How many keys are stored | | `key(index)` | The key at that position, or `null` | | `getItem(key)` | The value, or `null` when the key is unset | | `setItem(key, val)` | Stores it, converting the value to a string | | `removeItem(key)` | Forgets one key | | `clear()` | Forgets all of them | | `flush()` | Resolves once the writes have reached the disk | **The two differ only in how long they last.** `localStorage` is a file the host placed, and it survives a restart. `sessionStorage` is memory that goes with the process. That is also why only one of them is a capability: nothing `sessionStorage` holds ever leaves the process, so there is nothing to grant, and it works on a host that granted nothing. **Values are strings**, exactly as on the web — `setItem` converts whatever it is handed. Anything with structure goes through `JSON.stringify` on the way in and `JSON.parse` on the way out, which is the same code you would write in a browser: ```js localStorage.setItem( "window", JSON.stringify({ title: "Notes", size: [640, 480] }), ); const window = JSON.parse(localStorage.getItem("window") ?? "{}"); ``` Every member is synchronous, and deliberately: `getItem` is reachable from `render`, so the values are cached in memory and a read answers from there. A file read per render would be absurd. **A mutation schedules the write rather than performing it.** The file is written on a background thread — to a temporary file, renamed over the target, so a crash mid-write leaves the previous settings intact rather than a truncated one — and one write is in flight at a time, so a burst of `setItem` calls becomes one file rather than one file each. Whatever changed while a write was on its way is written by the next one. `await localStorage.flush()` when you need to know it landed. This is the one addition to the browser's interface, and it exists because a browser never has to answer the question — its storage is synchronous all the way down. It is a **barrier, not a second writer**: it waits for everything written so far to reach the disk and rejects with the write's own error if it does not. Starting its own write instead would race the automatic one through the same temporary file, with nothing ordering them — and the older revision could land last and undo the newer. The cache and its wait queue are bounded: one storage file may serialize to at most 8 MiB, contain at most 4,096 keys, and hold at most 1 MiB in any one value. At most 1,024 unresolved `flush()` barriers may wait at once; another is rejected instead of growing an unbounded waiter list. ### Where storage lives Storage is per application, and the host chooses the location — an application cannot name its own, or two applications could collide on purpose. **The host names the application, and its data follows that name:** ```rust let data = gpui_kit::shell::set_bundle_id("com.example.notes")?; gpui_kit::shell::set_capabilities(Capabilities::new().write_roots([data])); ``` | Platform | Location | | -------------------- | -------------------------------------------------------------------------------- | | Linux and other Unix | `$XDG_DATA_HOME/gpui-shell/apps//store.json`, defaulting to `~/.local/share` | | macOS | `~/Library/Application Support/gpui-shell/apps//store.json` | | Windows | `%APPDATA%\gpui-shell\apps\\store.json` | The id is the identity, so the data survives the directory being renamed, moved, or replaced by an upgrade — which is what a user means by "my settings". Keying on the path instead means an upgrade silently starts them over. **The runtime does not go looking for the id in a file.** Only the layer that installed the application knows what it is called; a runtime that read it out of a manifest of its own choosing would be claiming authority over something it does not own. A host that was merely *pointed at* a directory — this command line, a dev server — has no such name, and there the path really is the identity. `gpui_kit::shell::bundle_id_for_path(root)` builds one from the directory's name and a digest of its full path, so the same directory always reaches the same data and two checkouts of one source stay apart. That is right while you are editing something and wrong once it is installed, which is exactly the difference declaring a real id makes. The id may hold `a-z`, `0-9`, `.`, `-` and `_`, and no `..`. That is not tidiness: it is joined onto the user's data directory, so an unchecked one reaches the rest of it. Data lives there rather than inside the application because an application directory may be read-only, is often a git checkout, and is not where a user expects their data to be. ### Degrading when it is not granted `localStorage` that has not been granted throws, and a well-written application treats that as a fact about its host rather than an error: ```js // storage.js — from the bundled example export function load() { try { const saved = localStorage.getItem(KEY); if (saved === null) return []; const items = JSON.parse(saved); return Array.isArray(items) ? items : []; } catch (error) { console.warn( `todolist: storage unavailable, starting empty (${error.message})`, ); return []; } } ``` The example's footer then says so on screen — "Not saved — this host did not grant storage, so the list lasts for this run only" — which is the right shape: absorb the refusal at the boundary, and tell the user the truth. ## The clipboard ```js cx.write_to_clipboard("copied"); const text = cx.read_from_clipboard(); // undefined when the clipboard holds no text ``` Named after `App::write_to_clipboard` and `App::read_from_clipboard`, and on `cx` because that is where GPUI keeps them. Nothing to import. Read and write are **separate grants**, and a denial names the half that is missing: ```text writing the clipboard is not granted; declare capabilities.clipboard.write in the manifest ``` The clipboard needs a live host call — GPUI's `App` only exists for the duration of one — so a `cx` with none reports that plainly instead of panicking: ```text cx.read_from_clipboard() needs a live host call; call it from render, an event handler or a task ``` ## `console` ```js console.info("loaded", count, { source: "disk" }); console.warn("could not save"); ``` `debug`, `log`, `info`, `warn` and `error`. A global, as it is in every other JavaScript runtime, and nothing to import — the shell used to export the same object a second time as `gpui.log`, which bought a name and nothing else. **No capability is required**: a script that can run can already say something, and denying it would cost the author their diagnostics and nothing else. Extra arguments are appended space-separated, the way `console.log` behaves. Structured values print as JSON, because that is what an author reading a log wants to see. Output goes through `tracing` with the target `gpui_kit::shell::script`, so script output is separable from host output in a log filter. **A host with no `tracing` subscriber installed discards all of it** — along with the runtime's own reports of throwing handlers, unhandled rejections and illegal-phase calls. The `gpui-shell` binary installs a stderr sink at `INFO`, or `DEBUG` under `--dev`. ## `process` ```js import process from "process"; // also available as a bare global const { code, stdout, stderr } = await process.run("git", ["status"]); process.exit(0); ``` `process.run` returns a promise, for a sharper version of the reason `fs` does. A file read has no bound; a child process has less — it can compute for minutes, wait on input that never comes, or outlive the window. Waiting for one on this thread would stop the frame and the VM together, in the kernel, where the interrupt budget cannot see it. Output is **captured, not inherited**: a script that runs a command almost always wants what it said, and in a windowed application a child writing to the host's stdout is writing somewhere no user will look. `code` is `0` on success and `-1` when a signal killed it, which has no exit code of its own. Execution is bounded: 30 seconds, 8 MiB of stdout and 8 MiB of stderr. Reaching a bound kills and reaps the child and rejects the promise. Cancelling owned work or tearing down its runtime also terminates the child. The child starts with a cleared environment rather than inheriting host secrets; the shell does not expose an option to add environment variables. It is gated on an execute grant, which is one of three: denied (the default), an allowlist of command names, or unrestricted. A denied command **throws at the call** rather than rejecting, like a denied `fs` path — a rejected promise nobody awaited is a denial nobody sees. `process.exit` is **a request, never `exit(2)`** inside the runtime. It hands the code to a handler the host installed, which decides what to do — close the plugin's panel, close the window, end the process. One plugin must not be able to take the host process down, and the host may have unsaved state. The handler is not optional: a host that grants the capability without installing one makes the call **fail**, naming the omission. A request nobody answers is worse than a denial, because a script cannot tell the two apart. The `gpui-shell` binary installs the policy that suits a host which *is* the process — it ends it, with the code the script asked for. The name is a deliberate collision. `process` is what a JavaScript author — or a model generating JavaScript — reaches for, so the runtime puts its own capability-gated surface there rather than leaving the name free to look like Node's and behave differently. `process.exit` has its own `capabilities.process.exit` grant. Filesystem access never implies permission to close a panel, window, or process. ## The sandbox Beyond the capability grants, the runtime trims the language itself. All of it applies **unless development mode is on**. **No dynamic code.** `globalThis.eval` is deleted outright — a `ReferenceError` cannot be mistaken for a working `eval` by feature detection, which a throwing stub could be. All four function compilers are replaced: `Function`, and the constructors reachable through `(async function(){}).constructor`, `(function*(){}).constructor` and the async-generator equivalent. `Function` is *replaced* rather than deleted, keeping the real `Function.prototype`, so `x instanceof Function` and `.call` / `.apply` / `.bind` keep working and only construction throws. **Frozen built-in prototypes.** `Object`, `Array`, `Function`, `String` and `Number` prototypes are frozen. One VM will host several plugins, which makes those prototypes shared mutable state: one plugin adding an enumerable property to `Object.prototype` changes `for...in` for every other plugin and for the runtime's own prelude. The cost is real — a library that patches `Array.prototype` stops working, at import time — so a host that knowingly runs one can turn the freeze off and keep every other part of the sandbox. **Module resolution is confined to the application root.** `import "./ui.js"` resolves relative to the importing file; anything that resolves outside the application directory is refused. Dynamic `import()` stays callable on purpose — it is how lazy loading will work — and is confined by the same resolver. **Resource limits**, so a runaway script reports rather than taking the window with it: | Limit | Value | | ------------------------------------------------------------- | -------------------------------------------------------------------------- | | Heap | 256 MiB — a leak becomes a catchable JavaScript exception, not an OOM kill | | Interpreter stack | 1 MiB — deep recursion becomes a `RangeError`, not a native stack overflow | | Loaded JavaScript module | 8 MiB per source file | | Outstanding host tasks | 1,024 per runtime | | Time in one call: render and layout | 50 ms | | Time in one call: event and task | 500 ms | | Time in one call: outside any call, such as module evaluation | 5 s | The clock restarts on every host call, which is what lets the render path have a tighter budget than an event handler. **The interrupt cannot be swallowed by a `catch` block** — that is measured by a test, because if it could be, the interrupt would not be a defence at all. Each WebSocket also has an 8-command queue shared by `read`, `write`, and `close`; when full, a new operation rejects and tells the caller to wait for outstanding work. There is no quickjs-libc `std`: quickjs-libc is not compiled into the build. The runtime does provide the small audited `os` module listed below. ::: tip Development mode `--dev` enables source watching and calls `gpui_kit::shell::set_development_mode(true)` before constructing the runtime. That restores dynamic-code constructors and leaves built-in prototypes writable. Development mode never relaxes capability gating. It makes the language easier to poke at; it does not hand out access nobody declared, because a grant the author never wrote down is a grant that will be missing in production. ::: ## Network and safe standard APIs Global `fetch(url, options?)` is promise-based and returns `{ status, ok, url, , json() }`. Its grant is narrower than raw networking: every request and redirect must match a declared HTTP host, method, and exact path or path prefix; HTTPS never downgrades to HTTP, and authorization or caller-supplied headers never cross origins. `net.connect(host, port)` and the named `WebSocket.connect(url, { headers? })` export from `websocket` use `capabilities.network.hosts`. `WebSocket` is not installed as a browser global and is not a constructor. Raw TCP `read()` returns a `Uint8Array`, or `null` at EOF, so transport chunks never undergo lossy text decoding. WebSockets support text and `Uint8Array` messages and serialize writes through one actor. They do not follow redirects. Connect, handshake, and write operations have a 30-second timeout. A socket permits one outstanding `read()` at a time; a second is rejected immediately instead of competing for the next message. Credential and handshake-control headers are refused. Raw TCP and WebSocket access are intentionally broader than an HTTP request grant. DNS resolution is a bounded process-wide service: all applications share two resolver workers and a 64-request queue. Queueing observes each connection's existing deadline, so saturation fails as a timeout instead of growing memory or threads without limit. This is resource containment, not per-application quality-of-service; a host that runs mutually untrusted applications in one process does not get DNS fairness between them. The runtime also provides `buffer`, `path`, `url`, `crypto`, `zlib`, `console`, `process`, and `os`. These are the audited LLRT/host-backed subset declared in generated `gpui-kit.d.ts`; `node:` aliases and arbitrary Node built-ins are not part of the shell contract. ## Not there yet * **Prompting the user.** Grants are decided before the application loads; nothing asks at the moment of use. --- --- url: /docs/components/chart.md description: >- Beautiful charts and graphs for data visualization including line, bar, area, pie, radar, candlestick, and sankey charts. --- # Chart A comprehensive charting library providing Line, Bar, Area, Pie, Radar, Candlestick, and Sankey charts for data visualization. The charts feature smooth animations, customizable styling, tooltips, legends, and automatic theming that adapts to your application's theme. ## Import ```rust use gpui_kit::component::chart::{ LineChart, BarChart, AreaChart, PieChart, RadarChart, CandlestickChart, SankeyChart, }; ``` ## Chart Types ### LineChart A line chart displays data points connected by straight line segments, perfect for showing trends over time. #### Basic Line Chart ```rust #[derive(Clone)] struct DataPoint { x: String, y: f64, } let data = vec![ DataPoint { x: "Jan".to_string(), y: 100.0 }, DataPoint { x: "Feb".to_string(), y: 150.0 }, DataPoint { x: "Mar".to_string(), y: 120.0 }, ]; LineChart::new(data) .x(|d| d.x.clone()) .y(|d| d.y) ``` #### Line Chart Variants ```rust // Basic curved line (default) LineChart::new(data) .x(|d| d.month.clone()) .y(|d| d.value) // Linear interpolation LineChart::new(data) .x(|d| d.month.clone()) .y(|d| d.value) .linear() // Step after interpolation LineChart::new(data) .x(|d| d.month.clone()) .y(|d| d.value) .step_after() // With dots at data points LineChart::new(data) .x(|d| d.month.clone()) .y(|d| d.value) .dot() // Custom stroke color LineChart::new(data) .x(|d| d.month.clone()) .y(|d| d.value) .stroke(cx.theme().success) ``` #### Tick Control ```rust // Show every tick LineChart::new(data) .x(|d| d.month.clone()) .y(|d| d.value) .tick_margin(1) // Show every 2nd tick LineChart::new(data) .x(|d| d.month.clone()) .y(|d| d.value) .tick_margin(2) ``` ### BarChart A bar chart uses rectangular bars to show comparisons among categories. Bars can be oriented vertically or horizontally via the `alignment` option. #### Basic Bar Chart ```rust BarChart::new(data) .band(|d| d.category.clone()) .value(|d| d.value) ``` #### Bar Chart Customization ```rust // Custom fill colors // // The `fill` closure receives the datum, the bar's bounds (in pixel space, // relative to the chart), the chart's bounds, and the bar's `BarAlignment`. // Any value convertible to `Background` may be returned (solid color, gradient, // pattern, etc.). BarChart::new(data) .band(|d| d.category.clone()) .value(|d| d.value) .fill(|d, _bar_bounds, _chart_bounds, _alignment| d.color) // With value labels on bars BarChart::new(data) .band(|d| d.category.clone()) .value(|d| d.value) .label(|d| format!("{}", d.value)) // Custom tick spacing BarChart::new(data) .band(|d| d.category.clone()) .value(|d| d.value) .tick_margin(2) // Hide the band-axis line and labels BarChart::new(data) .band(|d| d.category.clone()) .value(|d| d.value) .label_axis(false) ``` #### Bar Chart Gradient Fills For gradient fills aligned to the bar's orientation, use `fill_gradient`. The closure receives the datum, the chart's full data range, and a `chart_to_bar` helper that maps a chart-value coordinate to a bar-local gradient position (`0.0` is the bar's base, `1.0` is its tip). The gradient angle is derived from the bar's `BarAlignment` so stop-0 sits at the base and stop-1 at the tip. ```rust use gpui_kit::linear_color_stop; // Per-bar gradient: every bar fades from a translucent base to its full color // at the tip, regardless of its value. BarChart::new(data) .band(|d| d.category.clone()) .value(|d| d.value) .fill_gradient(|d, _chart_range, _chart_to_bar| { let c = d.color; [ linear_color_stop(c.opacity(0.3), 0.0), linear_color_stop(c, 1.0), ] }) // Chart-wide gradient: each bar shows the slice of a single gradient // spanning the chart's full data range. Stops outside `[0, 1]` are clipped // to the bar with colors interpolated at the clip points. BarChart::new(data) .band(|d| d.category.clone()) .value(|d| d.value) .fill_gradient(|d, chart_range, chart_to_bar| { let c = d.color; [ linear_color_stop(c.opacity(0.3), chart_to_bar(*chart_range.start())), linear_color_stop(c, chart_to_bar(*chart_range.end())), ] }) ``` `fill` and `fill_gradient` are mutually exclusive — setting one clears the other. #### Bar Chart Alignment `BarAlignment` controls the bar orientation and the side where the baseline sits. Import it from `gpui_kit::component::plot::shape`. ```rust use gpui_kit::component::plot::shape::BarAlignment; // Default: vertical bars growing upward from the bottom BarChart::new(data) .band(|d| d.category.clone()) .value(|d| d.value) .alignment(BarAlignment::Bottom) // Vertical bars growing downward from the top BarChart::new(data) .band(|d| d.category.clone()) .value(|d| d.value) .alignment(BarAlignment::Top) // Horizontal bars growing rightward from the left BarChart::new(data) .band(|d| d.category.clone()) .value(|d| d.value) .alignment(BarAlignment::Left) // Horizontal bars growing leftward from the right BarChart::new(data) .band(|d| d.category.clone()) .value(|d| d.value) .alignment(BarAlignment::Right) ``` #### Bar Chart Corner Radii Round the bar rectangles. Pass any value convertible into `Corners` — use a single `px(..)` for uniform rounding, or construct `Corners` manually to round only specific corners (e.g. just the tip end of each bar). ```rust use gpui_kit::{px, Corners}; // Uniform 4px rounded corners on every bar BarChart::new(data) .band(|d| d.category.clone()) .value(|d| d.value) .corner_radii(px(4.)) // Round only the top corners (tip end for bottom-aligned bars) BarChart::new(data) .band(|d| d.category.clone()) .value(|d| d.value) .corner_radii(Corners { top_left: px(4.), top_right: px(4.), bottom_left: px(0.), bottom_right: px(0.), }) ``` #### Bar Chart Negative Values Bars grow from zero rather than from the edge of the plot, so negative values extend to the opposite side of the zero line. The band-axis line follows zero, and each category label moves to whichever side its own bar leaves empty. No configuration is needed — a data set containing negative values renders this way. ```rust // `growth` may be negative; bars below the zero line are drawn downward BarChart::new(data) .band(|d| d.quarter.clone()) .value(|d| d.growth) .label(|d| format!("{:+.0}%", d.growth)) ``` #### Bar Chart Value Axis Show tick labels for the value scale with `value_axis`, and control how many even intervals the scale is divided into with `value_tick_count`. The count drives both the grid line spacing and the tick labels, so the two always agree. Note that `value_tick_count` is a count, whereas `tick_margin` is a stride over the band-axis categories — `tick_margin(2)` keeps every second category label. ```rust // Value labels left of vertical bars, below horizontal ones BarChart::new(data) .band(|d| d.category.clone()) .value(|d| d.value) .value_axis(true) // Divide the value scale into 6 intervals instead of the default 4 BarChart::new(data) .band(|d| d.category.clone()) .value(|d| d.value) .value_axis(true) .value_tick_count(6) ``` ### AreaChart An area chart displays quantitative data visually, similar to a line chart but with the area below the line filled. #### Basic Area Chart ```rust AreaChart::new(data) .x(|d| d.time.clone()) .y(|d| d.value) ``` #### Stacked Area Charts ```rust // Multi-series area chart AreaChart::new(data) .x(|d| d.date.clone()) .y(|d| d.desktop) // First series .stroke(cx.theme().chart_1) .fill(cx.theme().chart_1.opacity(0.4)) .y(|d| d.mobile) // Second series .stroke(cx.theme().chart_2) .fill(cx.theme().chart_2.opacity(0.4)) ``` #### Area Chart Styling ```rust use gpui_kit::{linear_gradient, linear_color_stop}; // With gradient fill AreaChart::new(data) .x(|d| d.month.clone()) .y(|d| d.value) .fill(linear_gradient( 0., linear_color_stop(cx.theme().chart_1.opacity(0.4), 1.), linear_color_stop(cx.theme().background.opacity(0.3), 0.), )) // Different interpolation styles AreaChart::new(data) .x(|d| d.month.clone()) .y(|d| d.value) .linear() // or .step_after() ``` ### PieChart A pie chart displays data as slices of a circular chart, ideal for showing proportions. #### Basic Pie Chart ```rust PieChart::new(data) .value(|d| d.amount as f32) .outer_radius(100.) ``` #### Donut Chart ```rust PieChart::new(data) .value(|d| d.amount as f32) .outer_radius(100.) .inner_radius(60.) // Creates donut effect ``` #### Pie Chart Customization ```rust // Custom colors PieChart::new(data) .value(|d| d.amount as f32) .outer_radius(100.) .color(|d| d.color) // With padding between slices PieChart::new(data) .value(|d| d.amount as f32) .outer_radius(100.) .inner_radius(60.) .pad_angle(4. / 100.) // 4% padding ``` ### RadarChart A radar chart displays multivariate data as closed polygons around a center, ideal for comparing multiple series across several dimensions. #### Basic Radar Chart ```rust RadarChart::new(data) .label(|d| d.month.clone()) .value(|d| d.desktop) ``` #### Multiple Series ```rust // Each `.value()` call adds a series, paired with the matching // `.stroke()` / `.fill()` calls. Colors default to the theme // chart colors, cycled per series. RadarChart::new(data) .label(|d| d.month.clone()) .value(|d| d.desktop) .stroke(cx.theme().chart_1) .value(|d| d.mobile) .stroke(cx.theme().chart_2) ``` #### Element Labels `label` accepts either a string or a custom element. Return `element.into_any_element()` to render anything you like around the outer ring — an icon, several lines, per-dimension colors. ```rust RadarChart::new(data) .label({ let foreground = cx.theme().foreground; let muted_foreground = cx.theme().muted_foreground; move |d: &Device| { v_flex() .items_center() .child(div().text_xs().text_color(foreground).child(d.month.clone())) .child( div() .text_xs() .text_color(muted_foreground) .child(format!("{:.0}", d.desktop)), ) .into_any_element() } }) .value(|d| d.desktop) ``` Each label is measured at its natural size and pushed radially outward from its dimension, so even a tall one clears the outer ring. Element labels style themselves, so `.label_color()` does not apply to them, and they supply no tooltip title (a string label does). The ring is not shrunk to make room: the default outer radius is 40% of the chart's height, so a label much taller than a line of text needs a smaller `.outer_radius()` to keep it inside the chart's bounds. #### Radar Chart Customization ```rust // Vertex dots and custom fill RadarChart::new(data) .label(|d| d.month.clone()) .value(|d| d.desktop) .stroke(cx.theme().chart_2) .fill(cx.theme().chart_2.opacity(0.2)) .dot() // Fixed outer ring value and grid rings RadarChart::new(data) .label(|d| d.month.clone()) .value(|d| d.desktop) .max_value(400.) .grid_levels(5) .outer_radius(120.) ``` ### CandlestickChart A candlestick chart displays financial data using OHLC (Open, High, Low, Close) values, perfect for visualizing stock prices and market trends. #### Basic Candlestick Chart ```rust #[derive(Clone)] struct StockPrice { pub date: String, pub open: f64, pub high: f64, pub low: f64, pub close: f64, } let data = vec![ StockPrice { date: "Jan".to_string(), open: 100.0, high: 110.0, low: 95.0, close: 105.0 }, StockPrice { date: "Feb".to_string(), open: 105.0, high: 115.0, low: 100.0, close: 112.0 }, StockPrice { date: "Mar".to_string(), open: 112.0, high: 120.0, low: 108.0, close: 115.0 }, ]; CandlestickChart::new(data) .x(|d| d.date.clone()) .open(|d| d.open) .high(|d| d.high) .low(|d| d.low) .close(|d| d.close) ``` #### Candlestick Chart Customization ```rust // Adjust body width ratio (default: 0.6) CandlestickChart::new(data) .x(|d| d.date.clone()) .open(|d| d.open) .high(|d| d.high) .low(|d| d.low) .close(|d| d.close) .body_width_ratio(0.4) // Narrower bodies // Custom tick spacing CandlestickChart::new(data) .x(|d| d.date.clone()) .open(|d| d.open) .high(|d| d.high) .low(|d| d.low) .close(|d| d.close) .tick_margin(2) // Show every 2nd tick ``` #### Candlestick Chart Colors The candlestick chart automatically uses theme colors: * **Bullish** (close > open): `bullish` color (green) * **Bearish** (close < open): `bearish` color (red) ### SankeyChart A sankey diagram visualizes flows between nodes, ideal for financial statements, energy flows, and traffic analysis. The layout algorithm mirrors [d3-sankey](https://github.com/d3/d3-sankey). #### Basic Sankey Chart ```rust use gpui_kit::component::plot::shape::SankeyLink; #[derive(Clone)] struct FlowNode { pub name: SharedString, } let nodes = vec![ FlowNode { name: "Revenue".into() }, FlowNode { name: "Gross Profit".into() }, FlowNode { name: "Cost".into() }, ]; // Links reference nodes by their index in `nodes`. let links = vec![ SankeyLink::new(0, 1, 45.0), SankeyLink::new(0, 2, 55.0), ]; SankeyChart::new(nodes, links) .node_label(|d| d.name.clone()) .value_label(|_, value| format!("{:.1}", value).into()) ``` The value label is drawn above the name label. Its closure receives the node's computed throughput (the larger of incoming and outgoing flow). #### Node Alignment ```rust use gpui_kit::component::plot::shape::SankeyAlign; // Justify (default): nodes without outgoing links move to the last column SankeyChart::new(nodes, links).node_align(SankeyAlign::Justify) // Left: nodes stay at their topological depth SankeyChart::new(nodes, links).node_align(SankeyAlign::Left) // Also available: SankeyAlign::Right, SankeyAlign::Center ``` #### Sankey Chart Styling ```rust SankeyChart::new(nodes, links) .node_width(8.) // Node bar width (default: 10) .node_padding(20.) // Vertical gap between nodes in a column (default: 16) .node_corner_radius(px(2.)) // Corner radius of node bars (default: 0) .node_color(|d| d.color) // Per-node color; defaults to the theme chart palette .link_opacity(0.4) // Ribbon opacity (default: 0.3) .min_link_width(2.) // Minimum ribbon thickness (default: 1) .iterations(10) // Layout relaxation passes (default: 6) ``` Link ribbons are filled with a horizontal gradient from the source node color to the target node color. #### Custom Labels For full control over the label lines, use `labels` — one `SankeyLabel` per line, top to bottom, each with its own color and font size. It takes precedence over `node_label`/`value_label` when set. For example, a financial-statement label with a year-over-year change line: ```rust use gpui_kit::component::chart::SankeyLabel; SankeyChart::new(nodes, links).labels(move |d: &FlowNode, value| { let arrow = if d.growth >= 0. { "▲" } else { "▼" }; let growth_color = if d.growth >= 0. { green } else { red }; vec![ SankeyLabel::new(format!("{:.1}", value)), SankeyLabel::new(format!("{} {:+.2}%", arrow, d.growth)).color(growth_color), SankeyLabel::new(d.name.clone()).color(muted), ] }) ``` Line color defaults to the theme foreground and font size to 10; the chart keeps handling placement, alignment and margin reservation. A first/last-column label wider than its reserved margin is truncated with a trailing ellipsis rather than drawn outside the plot, so break or shorten long labels yourself if you want the full text on multiple lines. #### Compressing Large Value Ranges Node heights are linear in flow value by default, so a large value range (e.g. 200:1) leaves the small flows nearly invisible and the dominant flow oversized. Set `value_scale(SankeyValueScale::Sqrt)` to compress the range — the component sizes nodes by the square root of the value, so small flows stay visible without pre-transforming the data, and labels still receive the raw values: ```rust use gpui_kit::component::plot::shape::SankeyValueScale; SankeyChart::new(nodes, links).value_scale(SankeyValueScale::Sqrt) ``` Every node stays exactly filled by its ribbons under either scale, so children always match their parent's height. ## Data Structures ### Example Data Types ```rust // Time series data #[derive(Clone)] struct DailyDevice { pub date: String, pub desktop: f64, pub mobile: f64, } // Category data with styling #[derive(Clone)] struct MonthlyDevice { pub month: String, pub desktop: f64, pub color_alpha: f32, } impl MonthlyDevice { pub fn color(&self, base_color: Hsla) -> Hsla { base_color.alpha(self.color_alpha) } } // Financial data #[derive(Clone)] struct StockPrice { pub date: String, pub open: f64, pub high: f64, pub low: f64, pub close: f64, pub volume: u64, } // Sankey flow: nodes are referenced by index (from gpui_kit::component::plot::shape) pub struct SankeyLink { pub source: usize, pub target: usize, pub value: f64, } ``` ## Chart Configuration ### Container Setup ```rust fn chart_container( title: &str, chart: impl IntoElement, center: bool, cx: &mut Context, ) -> impl IntoElement { v_flex() .flex_1() .h_full() .border_1() .border_color(cx.theme().border) .rounded(cx.theme().radius_lg) .p_4() .child( div() .when(center, |this| this.text_center()) .font_semibold() .child(title.to_string()), ) .child( div() .when(center, |this| this.text_center()) .text_color(cx.theme().muted_foreground) .text_sm() .child("Data period label"), ) .child(div().flex_1().py_4().child(chart)) .child( div() .when(center, |this| this.text_center()) .font_semibold() .text_sm() .child("Summary statistic"), ) .child( div() .when(center, |this| this.text_center()) .text_color(cx.theme().muted_foreground) .text_sm() .child("Additional context"), ) } ``` ### Theme Integration ```rust // Charts automatically use theme colors let chart = LineChart::new(data) .x(|d| d.date.clone()) .y(|d| d.value) .stroke(cx.theme().chart_1); // Uses theme chart colors // Available theme chart colors: // cx.theme().chart_1 // cx.theme().chart_2 // cx.theme().chart_3 // ... up to chart_5 ``` ## API Reference * [LineChart] * [BarChart] * [AreaChart] * [PieChart] * [RadarChart] * [CandlestickChart] * \[SankeyChart] ## Examples ### Sales Dashboard ```rust #[derive(Clone)] struct SalesData { month: String, revenue: f64, profit: f64, region: String, } fn sales_dashboard(data: Vec, cx: &mut Context) -> impl IntoElement { v_flex() .gap_4() .child( h_flex() .gap_4() .child( chart_container( "Monthly Revenue", LineChart::new(data.clone()) .x(|d| d.month.clone()) .y(|d| d.revenue) .stroke(cx.theme().chart_1) .dot(), false, cx, ) ) .child( chart_container( "Profit Breakdown", PieChart::new(data.clone()) .value(|d| d.profit as f32) .outer_radius(80.) .color(|d| match d.region.as_str() { "North" => cx.theme().chart_1, "South" => cx.theme().chart_2, "East" => cx.theme().chart_3, "West" => cx.theme().chart_4, _ => cx.theme().chart_5, }), true, cx, ) ) ) .child( chart_container( "Regional Performance", BarChart::new(data) .band(|d| d.region.clone()) .value(|d| d.revenue) .fill(|d, _, _, _| match d.region.as_str() { "North" => cx.theme().chart_1, "South" => cx.theme().chart_2, "East" => cx.theme().chart_3, "West" => cx.theme().chart_4, _ => cx.theme().chart_5, }) .label(|d| format!("${:.0}k", d.revenue / 1000.)), false, cx, ) ) } ``` ### Multi-Series Time Chart ```rust #[derive(Clone)] struct DeviceUsage { date: String, desktop: f64, mobile: f64, tablet: f64, } fn device_usage_chart(data: Vec, cx: &mut Context) -> impl IntoElement { chart_container( "Device Usage Over Time", AreaChart::new(data) .x(|d| d.date.clone()) .y(|d| d.desktop) .stroke(cx.theme().chart_1) .fill(linear_gradient( 0., linear_color_stop(cx.theme().chart_1.opacity(0.4), 1.), linear_color_stop(cx.theme().background.opacity(0.3), 0.), )) .y(|d| d.mobile) .stroke(cx.theme().chart_2) .fill(linear_gradient( 0., linear_color_stop(cx.theme().chart_2.opacity(0.4), 1.), linear_color_stop(cx.theme().background.opacity(0.3), 0.), )) .y(|d| d.tablet) .stroke(cx.theme().chart_3) .fill(linear_gradient( 0., linear_color_stop(cx.theme().chart_3.opacity(0.4), 1.), linear_color_stop(cx.theme().background.opacity(0.3), 0.), )) .tick_margin(3), false, cx, ) } ``` ### Financial Chart ```rust #[derive(Clone)] struct StockData { date: String, price: f64, volume: u64, } #[derive(Clone)] struct StockOHLC { date: String, open: f64, high: f64, low: f64, close: f64, } fn stock_chart(ohlc_data: Vec, price_data: Vec, cx: &mut Context) -> impl IntoElement { v_flex() .gap_4() .child( chart_container( "Stock Price - Candlestick", CandlestickChart::new(ohlc_data.clone()) .x(|d| d.date.clone()) .open(|d| d.open) .high(|d| d.high) .low(|d| d.low) .close(|d| d.close) .tick_margin(3), false, cx, ) ) .child( chart_container( "Stock Price - Line", LineChart::new(price_data.clone()) .x(|d| d.date.clone()) .y(|d| d.price) .stroke(cx.theme().chart_1) .linear() .tick_margin(5), false, cx, ) ) .child( chart_container( "Trading Volume", BarChart::new(price_data) .band(|d| d.date.clone()) .value(|d| d.volume as f64) .fill(|d, _, _, _| { if d.volume > 1000000 { cx.theme().chart_1 } else { cx.theme().muted_foreground.opacity(0.6) } }) .tick_margin(5), false, cx, ) ) } ``` ## Customization Options ### Color Schemes ```rust // Theme-based colors (recommended) LineChart::new(data) .x(|d| d.x.clone()) .y(|d| d.y) .stroke(cx.theme().chart_1) // Custom color palette let colors = [ cx.theme().success, cx.theme().warning, cx.theme().destructive, cx.theme().info, cx.theme().chart_1, ]; BarChart::new(data) .band(|d| d.category.clone()) .value(|d| d.value) .fill(|d, _, _, _| colors[d.category_index % colors.len()]) ``` ### Responsive Design ```rust // Container with responsive sizing div() .flex_1() .min_h(px(300.)) .max_h(px(600.)) .w_full() .child( LineChart::new(data) .x(|d| d.x.clone()) .y(|d| d.y) ) ``` ### Grid and Axis Styling Charts automatically include: * Grid lines with dashed appearance * X-axis labels with smart positioning * Y-axis scaling starting from zero * Responsive tick spacing based on `tick_margin` ## Performance Considerations ### Large Datasets ```rust // For large datasets, consider data sampling let sampled_data: Vec<_> = data .iter() .step_by(5) // Show every 5th point .cloned() .collect(); LineChart::new(sampled_data) .x(|d| d.date.clone()) .y(|d| d.value) .tick_margin(3) // Reduce tick density ``` ### Memory Optimization ```rust // Use efficient data accessors LineChart::new(data) .x(|d| d.date.clone()) // Clone only when necessary .y(|d| d.value) // Direct field access ``` ## Integration Examples ### With State Management ```rust struct ChartComponent { data: Vec, chart_type: ChartType, time_range: TimeRange, } impl ChartComponent { fn render_chart(&self, cx: &mut Context) -> impl IntoElement { match self.chart_type { ChartType::Line => LineChart::new(self.filtered_data()) .x(|d| d.date.clone()) .y(|d| d.value) .into_any_element(), ChartType::Bar => BarChart::new(self.filtered_data()) .band(|d| d.date.clone()) .value(|d| d.value) .into_any_element(), ChartType::Area => AreaChart::new(self.filtered_data()) .x(|d| d.date.clone()) .y(|d| d.value) .into_any_element(), } } fn filtered_data(&self) -> Vec { self.data .iter() .filter(|d| self.time_range.contains(&d.date)) .cloned() .collect() } } ``` ### Real-time Updates ```rust struct LiveChart { data: Vec, max_points: usize, } impl LiveChart { fn add_data_point(&mut self, point: DataPoint) { self.data.push(point); if self.data.len() > self.max_points { self.data.remove(0); // Remove oldest point } } fn render(&self, cx: &mut Context) -> impl IntoElement { LineChart::new(self.data.clone()) .x(|d| d.timestamp.clone()) .y(|d| d.value) .linear() .dot() } } ``` [LineChart]: https://docs.rs/gpui-component/latest/gpui_component/chart/struct.LineChart.html [BarChart]: https://docs.rs/gpui-component/latest/gpui_component/chart/struct.BarChart.html [AreaChart]: https://docs.rs/gpui-component/latest/gpui_component/chart/struct.AreaChart.html [PieChart]: https://docs.rs/gpui-component/latest/gpui_component/chart/struct.PieChart.html [RadarChart]: https://docs.rs/gpui-component/latest/gpui_component/chart/struct.RadarChart.html [CandlestickChart]: https://docs.rs/gpui-component/latest/gpui_component/chart/struct.CandlestickChart.html --- --- url: /zh-CN/docs/components/chart.md description: 支持折线图、柱状图、面积图、饼图、雷达图、K 线图和桑基图的数据可视化组件。 --- # Chart Chart 是一组完整的数据可视化组件,提供 Line、Bar、Area、Pie、Radar、Candlestick 和 Sankey 图表。它们支持动画、自定义样式、主题配色和多种展示方式,适合仪表盘、统计分析和行情场景。 ## 导入 ```rust use gpui_kit::component::chart::{ LineChart, BarChart, AreaChart, PieChart, RadarChart, CandlestickChart, SankeyChart, }; ``` ## 图表类型 ### LineChart 折线图用于展示随时间变化的趋势。 #### 基础折线图 ```rust #[derive(Clone)] struct DataPoint { x: String, y: f64, } let data = vec![ DataPoint { x: "Jan".to_string(), y: 100.0 }, DataPoint { x: "Feb".to_string(), y: 150.0 }, DataPoint { x: "Mar".to_string(), y: 120.0 }, ]; LineChart::new(data) .x(|d| d.x.clone()) .y(|d| d.y) ``` #### 折线图变体 ```rust LineChart::new(data) .x(|d| d.month.clone()) .y(|d| d.value) LineChart::new(data) .x(|d| d.month.clone()) .y(|d| d.value) .linear() LineChart::new(data) .x(|d| d.month.clone()) .y(|d| d.value) .step_after() LineChart::new(data) .x(|d| d.month.clone()) .y(|d| d.value) .dot() LineChart::new(data) .x(|d| d.month.clone()) .y(|d| d.value) .stroke(cx.theme().success) ``` #### 刻度控制 ```rust LineChart::new(data) .x(|d| d.month.clone()) .y(|d| d.value) .tick_margin(1) LineChart::new(data) .x(|d| d.month.clone()) .y(|d| d.value) .tick_margin(2) ``` ### BarChart 柱状图通过矩形条形对比不同类别的数据,并可通过 `alignment` 选项切换垂直或水平方向。 #### 基础柱状图 ```rust BarChart::new(data) .band(|d| d.category.clone()) .value(|d| d.value) ``` #### 自定义柱状图 ```rust // 自定义填充颜色 // // `fill` 闭包接收四个参数:数据项、柱子的像素边界(相对于图表原点)、 // 图表的像素边界,以及柱子的 `BarAlignment`。返回值可以是任何能转换为 // `Background` 的类型(纯色、渐变、图案等)。 BarChart::new(data) .band(|d| d.category.clone()) .value(|d| d.value) .fill(|d, _bar_bounds, _chart_bounds, _alignment| d.color) // 显示数值标签 BarChart::new(data) .band(|d| d.category.clone()) .value(|d| d.value) .label(|d| format!("{}", d.value)) // 自定义刻度间距 BarChart::new(data) .band(|d| d.category.clone()) .value(|d| d.value) .tick_margin(2) // 隐藏分类轴的轴线和标签 BarChart::new(data) .band(|d| d.category.clone()) .value(|d| d.value) .label_axis(false) ``` #### 柱状图渐变填充 如需让渐变方向跟随柱子方向,请使用 `fill_gradient`。闭包接收三个参数:数据项、图表的完整数据范围(`chart_range`),以及一个 `chart_to_bar` 辅助函数(将图表数值坐标映射为柱子局部的渐变位置,其中 `0.0` 表示柱子的基线端,`1.0` 表示尖端)。渐变方向由柱子的 `BarAlignment` 推导,使 stop-0 始终位于基线端、stop-1 位于尖端。 ```rust use gpui_kit::linear_color_stop; // 单柱渐变:每个柱子都从半透明基线渐变到完全不透明的尖端, // 与该柱子的具体数值无关。 BarChart::new(data) .band(|d| d.category.clone()) .value(|d| d.value) .fill_gradient(|d, _chart_range, _chart_to_bar| { let c = d.color; [ linear_color_stop(c.opacity(0.3), 0.0), linear_color_stop(c, 1.0), ] }) // 跨图表渐变:每根柱子展示同一条覆盖整个图表数值范围的渐变中 // 对应自身值域的那一段。超出 `[0, 1]` 的 stop 会被裁剪到柱子内, // 颜色会在裁剪点处插值,使整体效果保持连续。 BarChart::new(data) .band(|d| d.category.clone()) .value(|d| d.value) .fill_gradient(|d, chart_range, chart_to_bar| { let c = d.color; [ linear_color_stop(c.opacity(0.3), chart_to_bar(*chart_range.start())), linear_color_stop(c, chart_to_bar(*chart_range.end())), ] }) ``` `fill` 与 `fill_gradient` 互斥——设置其中一个会清空另一个。 #### 柱状图对齐方式 `BarAlignment` 用于控制柱子的方向以及基线所在的一侧,需从 `gpui_kit::component::plot::shape` 导入。 ```rust use gpui_kit::component::plot::shape::BarAlignment; // 默认:垂直方向 - 向上 BarChart::new(data) .band(|d| d.category.clone()) .value(|d| d.value) .alignment(BarAlignment::Bottom) // 垂直方向 - 向下 BarChart::new(data) .band(|d| d.category.clone()) .value(|d| d.value) .alignment(BarAlignment::Top) // 水平方向 - 向右 BarChart::new(data) .band(|d| d.category.clone()) .value(|d| d.value) .alignment(BarAlignment::Left) // 水平方向 - 向左 BarChart::new(data) .band(|d| d.category.clone()) .value(|d| d.value) .alignment(BarAlignment::Right) ``` #### 柱状图圆角 为柱状条形设置圆角。可传入任意可转换为 `Corners` 的值—— 使用单个 `px(..)` 表示四角统一圆角,或手动构造 `Corners` 仅对特定角进行圆角处理(例如仅对柱顶一端进行圆角)。 ```rust use gpui_kit::{px, Corners}; // 所有柱条统一 4px 圆角 BarChart::new(data) .band(|d| d.category.clone()) .value(|d| d.value) .corner_radii(px(4.)) // 仅顶部圆角(适用于底部对齐柱状图的柱顶一端) BarChart::new(data) .band(|d| d.category.clone()) .value(|d| d.value) .corner_radii(Corners { top_left: px(4.), top_right: px(4.), bottom_left: px(0.), bottom_right: px(0.), }) ``` #### 柱状图负值 柱条从零点而非绘图区边缘开始生长,因此负值会向零线的另一侧延伸。 分类轴线跟随零点位置,每个分类标签也会移动到自身柱条未占用的那一侧。 无需任何配置——数据中包含负值时即以此方式渲染。 ```rust // `growth` 可为负值;零线以下的柱条向下绘制 BarChart::new(data) .band(|d| d.quarter.clone()) .value(|d| d.growth) .label(|d| format!("{:+.0}%", d.growth)) ``` #### 柱状图数值轴 使用 `value_axis` 显示数值刻度标签,并通过 `value_tick_count` 控制数值轴被 均分为多少个区间。该数量同时决定网格线间距和刻度标签,两者始终保持一致。 注意 `value_tick_count` 是一个数量,而 `tick_margin` 是分类轴上的步长—— `tick_margin(2)` 表示每隔一个分类保留一个标签。 ```rust // 纵向柱状图的数值标签位于左侧,横向柱状图位于下方 BarChart::new(data) .band(|d| d.category.clone()) .value(|d| d.value) .value_axis(true) // 将数值轴均分为 6 个区间(默认为 4) BarChart::new(data) .band(|d| d.category.clone()) .value(|d| d.value) .value_axis(true) .value_tick_count(6) ``` ### AreaChart 面积图类似折线图,但会填充曲线下方的区域。 #### 基础面积图 ```rust AreaChart::new(data) .x(|d| d.time.clone()) .y(|d| d.value) ``` #### 多系列面积图 ```rust AreaChart::new(data) .x(|d| d.date.clone()) .y(|d| d.desktop) .stroke(cx.theme().chart_1) .fill(cx.theme().chart_1.opacity(0.4)) .y(|d| d.mobile) .stroke(cx.theme().chart_2) .fill(cx.theme().chart_2.opacity(0.4)) ``` #### 样式 ```rust use gpui_kit::{linear_gradient, linear_color_stop}; AreaChart::new(data) .x(|d| d.month.clone()) .y(|d| d.value) .fill(linear_gradient( 0., linear_color_stop(cx.theme().chart_1.opacity(0.4), 1.), linear_color_stop(cx.theme().background.opacity(0.3), 0.), )) AreaChart::new(data) .x(|d| d.month.clone()) .y(|d| d.value) .linear() ``` ### PieChart 饼图适合展示占比关系。 #### 基础饼图 ```rust PieChart::new(data) .value(|d| d.amount as f32) .outer_radius(100.) ``` #### 环形图 ```rust PieChart::new(data) .value(|d| d.amount as f32) .outer_radius(100.) .inner_radius(60.) ``` #### 自定义 ```rust PieChart::new(data) .value(|d| d.amount as f32) .outer_radius(100.) .color(|d| d.color) PieChart::new(data) .value(|d| d.amount as f32) .outer_radius(100.) .inner_radius(60.) .pad_angle(4. / 100.) ``` ### RadarChart 雷达图以围绕中心的闭合多边形展示多维数据,适合对比多个系列在各维度上的表现。 #### 基础雷达图 ```rust RadarChart::new(data) .label(|d| d.month.clone()) .value(|d| d.desktop) ``` #### 多系列 ```rust // 每次调用 `.value()` 新增一个系列,与随后的 `.stroke()` / `.fill()` // 一一配对。颜色默认按主题图表色循环取用。 RadarChart::new(data) .label(|d| d.month.clone()) .value(|d| d.desktop) .stroke(cx.theme().chart_1) .value(|d| d.mobile) .stroke(cx.theme().chart_2) ``` #### 元素标签 `label` 既接受字符串,也接受自定义元素。返回 `element.into_any_element()` 即可在外圈周围渲染任意内容——图标、多行、按维度换色都可以。 ```rust RadarChart::new(data) .label({ let foreground = cx.theme().foreground; let muted_foreground = cx.theme().muted_foreground; move |d: &Device| { v_flex() .items_center() .child(div().text_xs().text_color(foreground).child(d.month.clone())) .child( div() .text_xs() .text_color(muted_foreground) .child(format!("{:.0}", d.desktop)), ) .into_any_element() } }) .value(|d| d.desktop) ``` 每个标签按自然尺寸测量,并沿径向朝外推开,所以即使很高也不会压到外圈上。 元素标签自带样式,因此 `.label_color()` 对它无效,也不会提供 tooltip 标题(字符串标签会)。 外圈不会为标签自动让位:默认外圈半径是图表高度的 40%,所以标签比单行文字高很多时, 需要调小 `.outer_radius()` 才能让它留在图表范围内。 #### 自定义 ```rust // 顶点圆点与自定义填充 RadarChart::new(data) .label(|d| d.month.clone()) .value(|d| d.desktop) .stroke(cx.theme().chart_2) .fill(cx.theme().chart_2.opacity(0.2)) .dot() // 固定外圈最大值与网格环数 RadarChart::new(data) .label(|d| d.month.clone()) .value(|d| d.desktop) .max_value(400.) .grid_levels(5) .outer_radius(120.) ``` ### CandlestickChart K 线图适合展示金融行情中的 OHLC 数据。 #### 基础 K 线图 ```rust #[derive(Clone)] struct StockPrice { pub date: String, pub open: f64, pub high: f64, pub low: f64, pub close: f64, } let data = vec![ StockPrice { date: "Jan".to_string(), open: 100.0, high: 110.0, low: 95.0, close: 105.0 }, StockPrice { date: "Feb".to_string(), open: 105.0, high: 115.0, low: 100.0, close: 112.0 }, StockPrice { date: "Mar".to_string(), open: 112.0, high: 120.0, low: 108.0, close: 115.0 }, ]; CandlestickChart::new(data) .x(|d| d.date.clone()) .open(|d| d.open) .high(|d| d.high) .low(|d| d.low) .close(|d| d.close) ``` #### 自定义 ```rust CandlestickChart::new(data) .x(|d| d.date.clone()) .open(|d| d.open) .high(|d| d.high) .low(|d| d.low) .close(|d| d.close) .body_width_ratio(0.4) CandlestickChart::new(data) .x(|d| d.date.clone()) .open(|d| d.open) .high(|d| d.high) .low(|d| d.low) .close(|d| d.close) .tick_margin(2) ``` 涨跌颜色会自动使用主题中的 bullish 和 bearish 配色。 ### SankeyChart 桑基图用于展示节点之间的流量关系,适合财报资金流向、能源流动和流量分析等场景。布局算法对标 [d3-sankey](https://github.com/d3/d3-sankey)。 #### 基础桑基图 ```rust use gpui_kit::component::plot::shape::SankeyLink; #[derive(Clone)] struct FlowNode { pub name: SharedString, } let nodes = vec![ FlowNode { name: "营业收入".into() }, FlowNode { name: "毛利润".into() }, FlowNode { name: "营业成本".into() }, ]; // 连接通过节点在 `nodes` 中的索引引用节点。 let links = vec![ SankeyLink::new(0, 1, 45.0), SankeyLink::new(0, 2, 55.0), ]; SankeyChart::new(nodes, links) .node_label(|d| d.name.clone()) .value_label(|_, value| format!("{:.1}", value).into()) ``` 数值标签显示在名称标签上方,闭包会收到节点的吞吐量(进出流量的较大值)。 #### 节点对齐 ```rust use gpui_kit::component::plot::shape::SankeyAlign; // Justify(默认):没有出边的节点移到最后一列 SankeyChart::new(nodes, links).node_align(SankeyAlign::Justify) // Left:节点保持在自己的拓扑深度列 SankeyChart::new(nodes, links).node_align(SankeyAlign::Left) // 还支持:SankeyAlign::Right、SankeyAlign::Center ``` #### 样式 ```rust SankeyChart::new(nodes, links) .node_width(8.) // 节点条宽度(默认 10) .node_padding(20.) // 同列节点垂直间距(默认 16) .node_corner_radius(px(2.)) // 节点条圆角(默认 0) .node_color(|d| d.color) // 每个节点的颜色,默认循环主题图表配色 .link_opacity(0.4) // 连接带透明度(默认 0.3) .min_link_width(2.) // 连接带最小粗细(默认 1) .iterations(10) // 布局松弛迭代次数(默认 6) ``` 连接带使用从源节点颜色到目标节点颜色的水平渐变填充。 #### 自定义标签 需要完全控制标签行时使用 `labels`——每行一个 `SankeyLabel`,从上到下排列,每行可单独设置颜色和字号。设置后优先于 `node_label`/`value_label`。例如带同比涨跌幅行的财报标签: ```rust use gpui_kit::component::chart::SankeyLabel; SankeyChart::new(nodes, links).labels(move |d: &FlowNode, value| { let arrow = if d.growth >= 0. { "▲" } else { "▼" }; let growth_color = if d.growth >= 0. { green } else { red }; vec![ SankeyLabel::new(format!("{:.1}", value)), SankeyLabel::new(format!("{} {:+.2}%", arrow, d.growth)).color(growth_color), SankeyLabel::new(d.name.clone()).color(muted), ] }) ``` 行颜色默认为主题前景色,字号默认 10;摆位、对齐和边距预留仍由组件负责。首/末列标签若超出预留边距,会被截断并加省略号,而不会画到图表外;若想让长标签完整分多行显示,请自行折断或缩短。 #### 压缩数值跨度 节点高度默认与流量值成线性关系,数值跨度很大时(如 200:1)小流量几乎不可见、主流量过大。设置 `value_scale(SankeyValueScale::Sqrt)` 即可压缩跨度——组件按值的平方根来定节点高度,小流量保持可见,且无需预处理数据,标签仍显示真实值: ```rust use gpui_kit::component::plot::shape::SankeyValueScale; SankeyChart::new(nodes, links).value_scale(SankeyValueScale::Sqrt) ``` 无论用哪种缩放,每个节点都被其连接精确填满,所以子节点高度始终与父节点匹配。 ## 数据结构示例 ```rust #[derive(Clone)] struct DailyDevice { pub date: String, pub desktop: f64, pub mobile: f64, } #[derive(Clone)] struct MonthlyDevice { pub month: String, pub desktop: f64, pub color_alpha: f32, } impl MonthlyDevice { pub fn color(&self, base_color: Hsla) -> Hsla { base_color.alpha(self.color_alpha) } } #[derive(Clone)] struct StockPrice { pub date: String, pub open: f64, pub high: f64, pub low: f64, pub close: f64, pub volume: u64, } // 桑基图连接:通过索引引用节点(来自 gpui_kit::component::plot::shape) pub struct SankeyLink { pub source: usize, pub target: usize, pub value: f64, } ``` ## 图表配置 ### 容器布局 ```rust fn chart_container( title: &str, chart: impl IntoElement, center: bool, cx: &mut Context, ) -> impl IntoElement { v_flex() .flex_1() .h_full() .border_1() .border_color(cx.theme().border) .rounded(cx.theme().radius_lg) .p_4() .child( div() .when(center, |this| this.text_center()) .font_semibold() .child(title.to_string()), ) .child( div() .when(center, |this| this.text_center()) .text_color(cx.theme().muted_foreground) .text_sm() .child("Data period label"), ) .child(div().flex_1().py_4().child(chart)) .child( div() .when(center, |this| this.text_center()) .font_semibold() .text_sm() .child("Summary statistic"), ) .child( div() .when(center, |this| this.text_center()) .text_color(cx.theme().muted_foreground) .text_sm() .child("Additional context"), ) } ``` ### 主题集成 ```rust let chart = LineChart::new(data) .x(|d| d.date.clone()) .y(|d| d.value) .stroke(cx.theme().chart_1); ``` 可用主题色通常包括 `chart_1` 到 `chart_5`。 ## API 参考 * [LineChart] * [BarChart] * [AreaChart] * [PieChart] * [RadarChart] * [CandlestickChart] * \[SankeyChart] ## 示例 ### 销售仪表盘 ```rust #[derive(Clone)] struct SalesData { month: String, revenue: f64, profit: f64, region: String, } fn sales_dashboard(data: Vec, cx: &mut Context) -> impl IntoElement { v_flex() .gap_4() .child( h_flex() .gap_4() .child( chart_container( "Monthly Revenue", LineChart::new(data.clone()) .x(|d| d.month.clone()) .y(|d| d.revenue) .stroke(cx.theme().chart_1) .dot(), false, cx, ) ) .child( chart_container( "Profit Breakdown", PieChart::new(data.clone()) .value(|d| d.profit as f32) .outer_radius(80.) .color(|d| match d.region.as_str() { "North" => cx.theme().chart_1, "South" => cx.theme().chart_2, "East" => cx.theme().chart_3, "West" => cx.theme().chart_4, _ => cx.theme().chart_5, }), true, cx, ) ) ) .child( chart_container( "Regional Performance", BarChart::new(data) .band(|d| d.region.clone()) .value(|d| d.revenue) .fill(|d, _, _, _| match d.region.as_str() { "North" => cx.theme().chart_1, "South" => cx.theme().chart_2, "East" => cx.theme().chart_3, "West" => cx.theme().chart_4, _ => cx.theme().chart_5, }) .label(|d| format!("${:.0}k", d.revenue / 1000.)), false, cx, ) ) } ``` ### 多系列时间图 ```rust #[derive(Clone)] struct DeviceUsage { date: String, desktop: f64, mobile: f64, tablet: f64, } fn device_usage_chart(data: Vec, cx: &mut Context) -> impl IntoElement { chart_container( "Device Usage Over Time", AreaChart::new(data) .x(|d| d.date.clone()) .y(|d| d.desktop) .stroke(cx.theme().chart_1) .fill(linear_gradient( 0., linear_color_stop(cx.theme().chart_1.opacity(0.4), 1.), linear_color_stop(cx.theme().background.opacity(0.3), 0.), )) .y(|d| d.mobile) .stroke(cx.theme().chart_2) .fill(linear_gradient( 0., linear_color_stop(cx.theme().chart_2.opacity(0.4), 1.), linear_color_stop(cx.theme().background.opacity(0.3), 0.), )) .y(|d| d.tablet) .stroke(cx.theme().chart_3) .fill(linear_gradient( 0., linear_color_stop(cx.theme().chart_3.opacity(0.4), 1.), linear_color_stop(cx.theme().background.opacity(0.3), 0.), )) .tick_margin(3), false, cx, ) } ``` ### 金融图表 ```rust #[derive(Clone)] struct StockData { date: String, price: f64, volume: u64, } #[derive(Clone)] struct StockOHLC { date: String, open: f64, high: f64, low: f64, close: f64, } fn stock_chart(ohlc_data: Vec, price_data: Vec, cx: &mut Context) -> impl IntoElement { v_flex() .gap_4() .child( chart_container( "Stock Price - Candlestick", CandlestickChart::new(ohlc_data.clone()) .x(|d| d.date.clone()) .open(|d| d.open) .high(|d| d.high) .low(|d| d.low) .close(|d| d.close) .tick_margin(3), false, cx, ) ) .child( chart_container( "Stock Price - Line", LineChart::new(price_data.clone()) .x(|d| d.date.clone()) .y(|d| d.price) .stroke(cx.theme().chart_1) .linear() .tick_margin(5), false, cx, ) ) .child( chart_container( "Trading Volume", BarChart::new(price_data) .band(|d| d.date.clone()) .value(|d| d.volume as f64) .fill(|d, _, _, _| { if d.volume > 1000000 { cx.theme().chart_1 } else { cx.theme().muted_foreground.opacity(0.6) } }) .tick_margin(5), false, cx, ) ) } ``` ## 自定义选项 ### 配色 ```rust LineChart::new(data) .x(|d| d.x.clone()) .y(|d| d.y) .stroke(cx.theme().chart_1) let colors = [ cx.theme().success, cx.theme().warning, cx.theme().destructive, cx.theme().info, cx.theme().chart_1, ]; BarChart::new(data) .band(|d| d.category.clone()) .value(|d| d.value) .fill(|d, _, _, _| colors[d.category_index % colors.len()]) ``` ### 响应式容器 ```rust div() .flex_1() .min_h(px(300.)) .max_h(px(600.)) .w_full() .child( LineChart::new(data) .x(|d| d.x.clone()) .y(|d| d.y) ) ``` ### 默认样式 图表默认会自动包含: * 虚线网格 * 自动定位的 X 轴标签 * 从 0 开始的 Y 轴刻度 * 基于 `tick_margin` 的刻度稀疏控制 ## 性能建议 ### 大数据集 ```rust let sampled_data: Vec<_> = data .iter() .step_by(5) .cloned() .collect(); LineChart::new(sampled_data) .x(|d| d.date.clone()) .y(|d| d.value) .tick_margin(3) ``` ### 内存优化 ```rust LineChart::new(data) .x(|d| d.date.clone()) .y(|d| d.value) ``` ## 集成示例 ### 结合状态管理 ```rust struct ChartComponent { data: Vec, chart_type: ChartType, time_range: TimeRange, } impl ChartComponent { fn render_chart(&self, cx: &mut Context) -> impl IntoElement { match self.chart_type { ChartType::Line => LineChart::new(self.filtered_data()) .x(|d| d.date.clone()) .y(|d| d.value) .into_any_element(), ChartType::Bar => BarChart::new(self.filtered_data()) .band(|d| d.date.clone()) .value(|d| d.value) .into_any_element(), ChartType::Area => AreaChart::new(self.filtered_data()) .x(|d| d.date.clone()) .y(|d| d.value) .into_any_element(), } } fn filtered_data(&self) -> Vec { self.data .iter() .filter(|d| self.time_range.contains(&d.date)) .cloned() .collect() } } ``` ### 实时更新 ```rust struct LiveChart { data: Vec, max_points: usize, } impl LiveChart { fn add_data_point(&mut self, point: DataPoint) { self.data.push(point); if self.data.len() > self.max_points { self.data.remove(0); } } fn render(&self, cx: &mut Context) -> impl IntoElement { LineChart::new(self.data.clone()) .x(|d| d.timestamp.clone()) .y(|d| d.value) .linear() .dot() } } ``` [LineChart]: https://docs.rs/gpui-component/latest/gpui_component/chart/struct.LineChart.html [BarChart]: https://docs.rs/gpui-component/latest/gpui_component/chart/struct.BarChart.html [AreaChart]: https://docs.rs/gpui-component/latest/gpui_component/chart/struct.AreaChart.html [PieChart]: https://docs.rs/gpui-component/latest/gpui_component/chart/struct.PieChart.html [RadarChart]: https://docs.rs/gpui-component/latest/gpui_component/chart/struct.RadarChart.html [CandlestickChart]: https://docs.rs/gpui-component/latest/gpui_component/chart/struct.CandlestickChart.html --- --- url: /base/primitives/checkbox.md description: A controlled tri-state check control with a separately styled indicator. --- # Checkbox A controlled tri-state check control with a separately styled indicator. Like every `gpui-base` primitive, Checkbox supplies behavior and semantic structure without imposing a product visual language. Apply GPUI styles and compose the exported parts to match your design system. ## Example The [single native Cargo entrypoint](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/components.rs) selects this primitive from the [shared showcase implementation](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/mod.rs). The same showcase is compiled once for the WASM preview above. ```bash cargo run -p gpui-base --example components -- checkbox ``` ## Import ```rust use gpui_kit::base::{Checkbox, CheckboxIndicator}; ``` ## Anatomy and API The example composes `Checkbox`, `CheckboxIndicator`. GPUI's standard styling and event traits provide presentation; these base types provide the interaction structure. The authoritative module is [`components/checkbox.rs`](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/components/checkbox.rs). Native and browser previews compile this same file. ## State and events Pass the controlled value to `checked`; `on_change` emits `CheckboxState`, including indeterminate. Keep controlled state on the parent render type or in a GPUI entity. Update it in callbacks and call `cx.notify()`; do not recreate persistent entities during every render. ## Complete Rust example The complete implementation used by the runnable showcase is embedded directly from Rust source: The command above supplies application initialization, window creation, and shared `BaseShowcase` state. ## Accessibility Keep label and control associated and expose checked, unchecked, indeterminate, and disabled states. ## Notes Use stable element IDs where accepted. Verify focus, hover, active, selected, disabled, reduced-motion, and high-contrast appearances in the consuming design system. --- --- url: /docs/components/checkbox.md description: A control that allows the user to toggle between checked and not checked. --- # Checkbox A checkbox component for binary choices. Supports labels, disabled state, and different sizes. ## Import ```rust use gpui_kit::component::checkbox::Checkbox; ``` ## Usage ### Basic Checkbox ```rust Checkbox::new("my-checkbox") .label("Accept terms and conditions") .checked(false) .on_click(|checked, _, _| { println!("Checkbox is now: {}", checked); }) ``` The `on_click` callback is triggered when the user toggles the checkbox, receiving the **new checked state**. ### Controlled Checkbox ```rust struct MyView { is_checked: bool, } impl Render for MyView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { Checkbox::new("checkbox") .label("Option") .checked(self.is_checked) .on_click(cx.listener(|view, checked, _, cx| { view.is_checked = *checked; cx.notify(); })) } } ``` ### Different Sizes ```rust Checkbox::new("cb").text_xs().label("Extra Small") Checkbox::new("cb").text_sm().label("Small") Checkbox::new("cb").label("Medium") // default Checkbox::new("cb").text_lg().label("Large") ``` ### Disabled State ```rust Checkbox::new("checkbox") .label("Disabled checkbox") .disabled(true) .checked(false) ``` ### Without Label ```rust Checkbox::new("checkbox") .checked(true) ``` ### Custom Tab Order ```rust Checkbox::new("checkbox") .label("Custom tab order") .tab_index(2) .tab_stop(true) ``` ## API Reference * [Checkbox] ### Styling Implements `Sizable` and `Disableable` traits: * `text_xs()` - Extra small text * `text_sm()` - Small text * `text_base()` - Base text (default) * `text_lg()` - Large text * `disabled(bool)` - Disabled state ## Examples ### Checkbox List ```rust v_flex() .gap_2() .child(Checkbox::new("cb1").label("Option 1").checked(true)) .child(Checkbox::new("cb2").label("Option 2").checked(false)) .child(Checkbox::new("cb3").label("Option 3").checked(false)) ``` ### Form Integration ```rust struct FormView { agree_terms: bool, subscribe: bool, } v_flex() .gap_3() .child( Checkbox::new("terms") .label("I agree to the terms and conditions") .checked(self.agree_terms) .on_click(cx.listener(|view, checked, _, cx| { view.agree_terms = *checked; cx.notify(); })) ) .child( Checkbox::new("subscribe") .label("Subscribe to newsletter") .checked(self.subscribe) .on_click(cx.listener(|view, checked, _, cx| { view.subscribe = *checked; cx.notify(); })) ) ``` [Checkbox]: https://docs.rs/gpui-component/latest/gpui_component/checkbox/struct.Checkbox.html --- --- url: /zh-CN/base/primitives/checkbox.md description: 指示器可独立设置样式的受控三态复选框。 --- # Checkbox 指示器可独立设置样式的受控三态复选框。 和所有 GPUI Base 原语一样,Checkbox 只提供行为和语义结构,不规定产品视觉语言。请使用 GPUI 样式并组合导出的部件,使其符合你的设计系统。 ## 示例 原生示例和页面上方的 WASM 预览共用同一份实现: ```bash cargo run -p gpui-base --example components -- checkbox ``` ## 导入 ```rust use gpui_kit::base::{Checkbox, CheckboxIndicator}; ``` ## 结构与 API 示例组合上述公开类型。GPUI 的标准样式和事件 trait 负责表现,Base 类型负责交互结构。权威实现位于 [`components/checkbox.rs`](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/components/checkbox.rs),原生与浏览器预览编译的是同一文件。 ## 状态与事件 父级持有选中、未选中或不确定状态,并在激活时更新。 受控状态应保存在父渲染类型或 GPUI entity 中;在回调中更新并调用 `cx.notify()`,不要在每次渲染时重建持久 entity。 ## 完整 Rust 示例 ## 可访问性 暴露复选框角色、当前状态、标签和禁用状态。 ## 注意事项 在支持的位置使用稳定元素 ID,并在消费端设计系统中验证焦点、悬停、按下、选中、禁用、减少动态效果和高对比度状态。 --- --- url: /zh-CN/docs/components/checkbox.md description: 用于切换选中与未选中状态的复选框组件。 --- # Checkbox Checkbox 是一个用于二元选择的复选框组件,支持标签、禁用状态和不同文字尺寸。 ## 导入 ```rust use gpui_kit::component::checkbox::Checkbox; ``` ## 用法 ### 基础 Checkbox ```rust Checkbox::new("my-checkbox") .label("Accept terms and conditions") .checked(false) .on_click(|checked, _, _| { println!("Checkbox is now: {}", checked); }) ``` `on_click` 会在用户切换状态时触发,接收到的是切换后的新状态。 ### 受控 Checkbox ```rust struct MyView { is_checked: bool, } impl Render for MyView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { Checkbox::new("checkbox") .label("Option") .checked(self.is_checked) .on_click(cx.listener(|view, checked, _, cx| { view.is_checked = *checked; cx.notify(); })) } } ``` ### 不同尺寸 ```rust Checkbox::new("cb").text_xs().label("Extra Small") Checkbox::new("cb").text_sm().label("Small") Checkbox::new("cb").label("Medium") Checkbox::new("cb").text_lg().label("Large") ``` ### 禁用状态 ```rust Checkbox::new("checkbox") .label("Disabled checkbox") .disabled(true) .checked(false) ``` ### 不带标签 ```rust Checkbox::new("checkbox") .checked(true) ``` ### 自定义 Tab 顺序 ```rust Checkbox::new("checkbox") .label("Custom tab order") .tab_index(2) .tab_stop(true) ``` ## API 参考 * [Checkbox] ### 样式 实现了 `Sizable` 和 `Disableable` trait: * `text_xs()`:超小字号 * `text_sm()`:小字号 * `text_base()`:默认字号 * `text_lg()`:大字号 * `disabled(bool)`:禁用状态 ## 示例 ### 复选框列表 ```rust v_flex() .gap_2() .child(Checkbox::new("cb1").label("Option 1").checked(true)) .child(Checkbox::new("cb2").label("Option 2").checked(false)) .child(Checkbox::new("cb3").label("Option 3").checked(false)) ``` ### 表单集成 ```rust struct FormView { agree_terms: bool, subscribe: bool, } v_flex() .gap_3() .child( Checkbox::new("terms") .label("I agree to the terms and conditions") .checked(self.agree_terms) .on_click(cx.listener(|view, checked, _, cx| { view.agree_terms = *checked; cx.notify(); })) ) .child( Checkbox::new("subscribe") .label("Subscribe to newsletter") .checked(self.subscribe) .on_click(cx.listener(|view, checked, _, cx| { view.subscribe = *checked; cx.notify(); })) ) ``` [Checkbox]: https://docs.rs/gpui-component/latest/gpui_component/checkbox/struct.Checkbox.html --- --- url: /docs/components/clipboard.md description: >- A button component that helps you copy text or other content to your clipboard. --- # Clipboard The Clipboard component provides an easy way to copy text or other data to the user's clipboard. It renders as a button with a copy icon that changes to a checkmark when content is successfully copied. The component supports both static values and dynamic content through callback functions. ## Import ```rust use gpui_kit::component::clipboard::Clipboard; ``` ## Usage ### Basic Clipboard ```rust Clipboard::new("my-clipboard") .value("Text to copy") .on_copied(|value, window, cx| { window.push_notification(format!("Copied: {}", value), cx) }) ``` ### Using Dynamic Values The `value_fn` method allows you to provide a closure that generates the content to be copied at the time of the copy action. * This is useful when the content to be copied depends on the current state of the application. * And in some cases, it may have a larger overhead to compute, so you only want to do it when the user actually clicks the copy button. ```rust let state = some_state.clone(); Clipboard::new("dynamic-clipboard") .value_fn(move |_, cx| { state.read(cx).get_current_value() }) .on_copied(|value, window, cx| { window.push_notification(format!("Copied: {}", value), cx) }) ``` ### With Custom Content ```rust use gpui_kit::component::label::Label; h_flex() .gap_2() .child(Label::new("Share URL")) .child(Icon::new(IconName::Share)) .child( Clipboard::new("custom-clipboard") .value("https://example.com") ) ``` ### In Input Fields The Clipboard component is commonly used as a suffix in input fields: ```rust use gpui_kit::component::input::{InputState, Input}; let url_state = cx.new(|cx| InputState::new(window, cx).default_value("https://github.com")); Input::new(&url_state) .suffix( Clipboard::new("url-clipboard") .value_fn({ let state = url_state.clone(); move |_, cx| state.read(cx).value() }) .on_copied(|value, window, cx| { window.push_notification(format!("URL copied: {}", value), cx) }) ) ``` ## API Reference * [Clipboard] ## Examples ### Simple Text Copy ```rust Clipboard::new("simple") .value("Hello, World!") ``` ### With User Feedback ```rust h_flex() .gap_2() .child(Label::new("Your API Key:")) .child( Clipboard::new("feedback") .value("sk-1234567890abcdef") .on_copied(|_, window, cx| { window.push_notification("API key copied to clipboard", cx) }) ) ``` ### Form Field Integration ```rust use gpui_kit::component::{ input::{InputState, Input}, h_flex, label::Label }; let api_key = "sk-1234567890abcdef"; h_flex() .gap_2() .items_center() .child(Label::new("API Key:")) .child( Input::new(&input_state) .value(api_key) .readonly(true) .suffix( Clipboard::new("api-key-copy") .value(api_key) .on_copied(|_, window, cx| { window.push_notification("API key copied!", cx) }) ) ) ``` ### Dynamic Content Copy ```rust struct AppState { current_url: String, } let app_state = cx.new(|_| AppState { current_url: "https://example.com".to_string() }); Clipboard::new("current-url") .value_fn({ let state = app_state.clone(); move |_, cx| { SharedString::from(state.read(cx).current_url.clone()) } }) .on_copied(|url, window, cx| { window.push_notification(format!("Shared: {}", url), cx) }) ``` ## Data Types The Clipboard component currently supports copying text strings to the clipboard. It uses GPUI's `ClipboardItem::new_string()` method, which handles: * Plain text strings * UTF-8 encoded content * Cross-platform clipboard integration [Clipboard]: https://docs.rs/gpui-component/latest/gpui_component/clipboard/struct.Clipboard.html --- --- url: /zh-CN/docs/components/clipboard.md description: 用于复制文本或其它内容到剪贴板的按钮组件。 --- # Clipboard Clipboard 组件提供了一个简单的复制按钮,可将文本或其它数据复制到用户剪贴板。它默认显示复制图标,在复制成功后会切换为勾选图标。组件既支持静态值,也支持通过回调动态生成复制内容。 ## 导入 ```rust use gpui_kit::component::clipboard::Clipboard; ``` ## 用法 ### 基础 Clipboard ```rust Clipboard::new("my-clipboard") .value("Text to copy") .on_copied(|value, window, cx| { window.push_notification(format!("Copied: {}", value), cx) }) ``` ### 动态值 `value_fn` 允许你在用户点击复制按钮时再动态生成内容: * 适合依赖当前应用状态的值 * 也适合那些计算开销较大、不想在每次渲染时都计算的内容 ```rust let state = some_state.clone(); Clipboard::new("dynamic-clipboard") .value_fn(move |_, cx| { state.read(cx).get_current_value() }) .on_copied(|value, window, cx| { window.push_notification(format!("Copied: {}", value), cx) }) ``` ### 自定义组合内容 ```rust use gpui_kit::component::label::Label; h_flex() .gap_2() .child(Label::new("Share URL")) .child(Icon::new(IconName::Share)) .child( Clipboard::new("custom-clipboard") .value("https://example.com") ) ``` ### 用在输入框中 Clipboard 很适合作为输入框后缀: ```rust use gpui_kit::component::input::{InputState, Input}; let url_state = cx.new(|cx| InputState::new(window, cx).default_value("https://github.com")); Input::new(&url_state) .suffix( Clipboard::new("url-clipboard") .value_fn({ let state = url_state.clone(); move |_, cx| state.read(cx).value() }) .on_copied(|value, window, cx| { window.push_notification(format!("URL copied: {}", value), cx) }) ) ``` ## API 参考 * [Clipboard] ## 示例 ### 复制简单文本 ```rust Clipboard::new("simple") .value("Hello, World!") ``` ### 带反馈提示 ```rust h_flex() .gap_2() .child(Label::new("Your API Key:")) .child( Clipboard::new("feedback") .value("sk-1234567890abcdef") .on_copied(|_, window, cx| { window.push_notification("API key copied to clipboard", cx) }) ) ``` ### 表单字段集成 ```rust use gpui_kit::component::{ input::{InputState, Input}, h_flex, label::Label }; let api_key = "sk-1234567890abcdef"; h_flex() .gap_2() .items_center() .child(Label::new("API Key:")) .child( Input::new(&input_state) .value(api_key) .readonly(true) .suffix( Clipboard::new("api-key-copy") .value(api_key) .on_copied(|_, window, cx| { window.push_notification("API key copied!", cx) }) ) ) ``` ### 复制动态内容 ```rust struct AppState { current_url: String, } let app_state = cx.new(|_| AppState { current_url: "https://example.com".to_string() }); Clipboard::new("current-url") .value_fn({ let state = app_state.clone(); move |_, cx| { SharedString::from(state.read(cx).current_url.clone()) } }) .on_copied(|url, window, cx| { window.push_notification(format!("Shared: {}", url), cx) }) ``` ## 数据类型 Clipboard 当前主要支持复制文本字符串,内部使用 GPUI 的 `ClipboardItem::new_string()`,可处理: * 纯文本 * UTF-8 编码内容 * 跨平台剪贴板写入 [Clipboard]: https://docs.rs/gpui-component/latest/gpui_component/clipboard/struct.Clipboard.html --- --- url: /docs/coding-guides.md description: Architecture and coding conventions for maintainable GPUI Kit applications --- # Coding Guides This guide describes the application architecture and code patterns that have proved durable in GPUI Kit. It is written for both engineers and coding agents. Read [Design Guides](./design-guides.md) first: code structure should preserve product intent, not replace it. This is a normative guide. **Must** marks lifecycle, correctness, or ecosystem constraints; **should** is the default architecture and requires a concrete reason to depart from it. Current source and API docs remain authoritative for exact signatures. ## Architecture at a glance Dependencies point downward. Higher layers own domain meaning and orchestration; lower layers own reusable presentation or behavior. Do not make a reusable component depend on an application screen, or make `gpui-base` depend on a theme from GPUI Component. Use these boundaries: * **app shell:** compose windows and feature crates while keeping feature logic out; * **feature crate:** keep one capability's model, services, views, commands, dialogs, and workflow behind one public boundary; * **app component:** a repeated domain-aware pattern; * **gpui-component:** themed, general-purpose UI; * **gpui-base:** reusable behavior and geometry without product presentation. ### Organize large applications by capability In a large Rust application, a feature should usually be a crate, not another file in a global `views`, `models`, or `modals` directory. Keep the model, views, commands, dialogs, and workflow for one capability together. A dialog that edits a workspace belongs to the workspace feature; only the reusable dialog primitive belongs to the UI library. ```text crates/ ├── app/ │ └── src/main.rs # Compose windows and features ├── workspace/ │ └── src/ │ ├── lib.rs # The feature's public boundary │ ├── model.rs │ ├── commands.rs │ ├── workspace_view.rs │ └── rename_dialog.rs ├── search/ │ └── src/ │ ├── lib.rs │ ├── model.rs │ ├── commands.rs │ ├── search_view.rs │ └── filters.rs ├── settings/ │ └── src/ │ ├── lib.rs │ ├── model.rs │ ├── settings_view.rs │ └── account_dialog.rs └── shared/ └── src/ ├── lib.rs └── recent_items.rs # A stable capability with multiple owners ``` Do not invert this into global `models/`, `views/`, `modals/`, and `commands/` directories. Those folders classify files by implementation role while scattering every feature across the application. The application shell composes feature crates but contains little feature logic. A feature may depend on stable shared capabilities and UI foundations; it must not depend on the shell or reach into a sibling feature's internals. When two features need to communicate, prefer an explicit command, event, data type, or small shared service over a dependency between their views. Extract a shared crate only after the capability has a coherent name and more than one real owner. Crate boundaries are engineering boundaries. They let Cargo rebuild and test a smaller dependency subgraph, make ownership visible in `Cargo.toml`, and limit the review and regression surface of a change. They also make removal honest: a feature that cannot be detached without searching through global view and modal directories was never isolated. Do not create a crate for every screen or helper. Split where a capability has its own state and lifecycle, a stable public seam, or enough implementation to benefit from independent compilation and tests. Keep dependencies acyclic and pointing toward smaller, more stable crates. ## Bootstrap and root ownership Initialize GPUI Component once, before creating component-backed views, and put `Root` at the first level of each window: ```rust app.run(move |cx| { gpui_kit::init(cx); cx.spawn(async move |cx| { cx.open_window(WindowOptions::default(), |window, cx| { let workspace = cx.new(|cx| Workspace::new(window, cx)); cx.new(|cx| Root::new(workspace, window, cx)) }) .expect("failed to open window"); }) .detach(); }); ``` `Root` coordinates window-level component facilities such as overlays and notifications. Do not create a separate root for each page inside one window. It also coordinates modal focus restoration, focus traps, tooltip/menu layers, and window-scoped text selection. Bypassing it can produce behavior that looks correct at rest but fails when overlays nest or focus changes quickly. ## Understand GPUI's phases and contexts GPUI is retained state with declarative rendering. An entity survives across frames; the element tree returned by `render` is a fresh description of the current frame. Keep that distinction explicit. * `Context` mutates the current entity, creates listeners tied to it, emits its events, and notifies its observers. * `App` gives access to application globals and entity reads/updates without implying ownership by the rendered element. * `Window` owns focus, actions, input dispatch, element-keyed state, measurement, and animation-frame requests for that window. * layout, prepaint, and paint are later phases; use their hooks only when resolved geometry is genuinely required. Never retain `&mut Window`, `&mut App`, or `&mut Context<_>` beyond the call in which it is provided. Retain typed handles—`Entity`, `WeakEntity`, `FocusHandle`, scroll handles, or domain IDs—instead. ## Choose the right unit ### Use `RenderOnce` for value-like elements Use a `RenderOnce`/`IntoElement` component when all inputs can be supplied by the caller and the element does not need to retain application state between frames. This is the normal choice for presentational wrappers and small controls. ```rust #[derive(IntoElement)] struct EmptyState { title: SharedString, } impl RenderOnce for EmptyState { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { div() .v_flex() .gap_2() .items_center() .text_color(cx.theme().muted_foreground) .child(self.title) } } ``` ### Use `Entity` for retained behavior Use an entity-backed `Render` view when behavior spans frames or needs observation, subscriptions, focus, async work, history, measurement, or incremental updates. Store entities in an owning view rather than recreating them in `render`. ```rust struct SearchView { query: Entity, } impl SearchView { fn new(window: &mut Window, cx: &mut Context) -> Self { let query = cx.new(|cx| InputState::new(window, cx).placeholder("Search…")); Self { query } } } ``` Do not turn every visual fragment into an entity. Entity boundaries have lifecycle and coordination costs; use them where retained identity matters. ### Elements, views, and behavior systems are different Do not force every component into one template. The ecosystem contains: * semantic elements such as Button, Checkbox, Link, and Tabs; * compound behavior roots such as Dialog, Popover, Select, and Combobox; * entity-backed systems such as Input, Table, Tree, Dock, and notifications; * infrastructure such as positioning, virtualization, scrolling, focus traps, motion, history, and measurement. An element may be internally complex and still be value-like to its caller. A stateful system may expose render callbacks so applications own presentation without reimplementing behavior. Choose the public seam from the behavior, not from how many `div`s appear in its renderer. ## State ownership Put each state in the narrowest owner that can keep it correct: * domain state belongs to a model or feature view; * transient view state belongs to the view that renders it; * reusable behavioral state belongs to the component state designed for it; * tiny element-local state may use GPUI keyed element state; * shared application services may be stored as GPUI globals. Prefer controlled values for ordinary selection and toggles: pass the current value into the component, receive a requested change, update the owner, and render again. A callback reports intent; it should not create a second hidden source of truth. ```rust Checkbox::new("show-hidden") .checked(self.show_hidden) .label("Show hidden files") .on_click(cx.listener(|this, checked, _, cx| { this.show_hidden = *checked; cx.notify(); })) ``` Call `cx.notify()` after a mutation that changes rendering. Use `cx.emit(...)` for a semantic event that an owner should handle, and `cx.subscribe(...)` or `cx.observe(...)` when the lifetime should follow an entity. Keep returned subscriptions alive when the API requires it. Do not notify merely because a value was read or derived. Avoid unconditional notification from `render`; it schedules another render and can create a permanent redraw loop. When several fields form one invariant, update them together and notify once. A reusable state type that cannot receive a context should make that limitation explicit and require its owner to emit/notify. ### Avoid state feedback loops Text input, selection, filters, and controlled popups commonly have two paths: an external owner updates the value, and user interaction requests a new value. Do not send an owner-supplied value back through the user callback during sync. Track the origin or compare coherent snapshots so each logical change is reported once. Make callbacks re-entrancy-safe when a callback can synchronously close, replace, or update the component that invoked it. ## Stable identity An `ElementId` is part of behavior. It gives an element stable identity and keys element-local or component state. A component may also use it as one input to its own focus, measurement, or animation identity; focus and scrolling are otherwise owned by their dedicated handles. * Use stable domain IDs for rows, tabs, tree nodes, and repeated controls. * Namespace child IDs with their owning object when the same control repeats. * Never derive identity from a translated label or a mutable list index when items can be inserted or reordered. * Do not generate a fresh random ID during `render`. ```rust Button::new(("delete-project", project.id)) .danger() .label("Delete") ``` A changed ID means a changed UI identity. Treat that reset as deliberate. The same rule applies to transition channels, overlay tokens, scroll handles, and persistence IDs. If two independently retained behaviors share a key, they can overwrite each other's state; if one behavior changes keys every frame, it never accumulates state. ## Rendering and composition Keep `render` declarative: read current state, derive presentation values, and compose elements. Move domain operations, parsing, and non-trivial mutation to named methods or services. ```rust impl Render for ProjectView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { div() .v_flex() .size_full() .child(self.render_toolbar(cx)) .child(self.render_content(cx)) } } ``` Extract a render helper when it names a meaningful region and reduces the amount of state a reader must hold at once. Extract a new component when the region has its own reusable contract or retained lifecycle—not merely because a builder chain is long. Use GPUI Component's fluent traits consistently (`Sizable`, `Disableable`, `Selectable`, and component-specific builders). Prefer `.when(...)` and `.when_some(...)` for small conditional refinements; use ordinary Rust control flow when branches represent substantially different interfaces. Compose from the standard semantic component before building a custom surface. Do not reproduce a menu, select, dropdown, or command palette from generic `div`s merely to match one screenshot. Reusing the component preserves its item geometry, focus transfer, keyboard navigation, selection, disabled state, dismissal, and accessibility contract. If the standard component cannot express a recurring valid pattern, improve its explicit API instead of styling arbitrary descendants at each call site. Render callbacks supplied by application code should be side-effect-free. A list item renderer, menu builder, or dock panel renderer may run whenever its owner needs to measure or redraw. It must not perform a business operation, append data, or register an unbounded subscription. ## Behavior and presentation boundary The durable Base rule is: > Base owns reusable behavior and the geometry required to implement it. The > presentation layer owns the product's visual language. “Headless” does not mean “one empty `div`.” Popup collision, keyboard navigation, editing, virtualization, resize arithmetic, focus trapping, and dock reconciliation require internal structure and state. Moving that work to every caller would not create flexibility; it would duplicate fragile behavior. Conversely, Base must not choose brand colors, typography, density, final icons, component variants, or application composition. Expose presentation through `Styled`, typed semantic-state styles, explicit parts, child slots, and item renderers. Do not inspect arbitrary descendants to discover titles, descriptions, or close buttons—make semantic parts explicit. ## Theme and styling Read semantic values from the active theme and apply layout with GPUI's `Styled` methods: ```rust div() .bg(cx.theme().background) .text_color(cx.theme().foreground) .border_1() .border_color(cx.theme().border) .rounded(cx.theme().radius) ``` Rules: * do not hard-code product colors, corner radii, spacing, or control geometry; * application code must not introduce raw hex, `rgb`/`rgba`, or `hsla`; read a semantic color from `cx.theme()` or add the missing role to the product theme; * application layout should use GPUI's rem-based scale helpers (`p_2()`, `gap_3()`, `w_64()`, `text_sm()`) instead of direct `px(...)` values; * use semantic tokens for meaning, not palette position; * keep state-independent geometry in the ordinary builder chain; * use GPUI `hover`, `active`, `focus`, and `focus_visible` modifiers for runtime interaction states; * use a component's semantic state styles for checked, selected, pressed, or disabled appearance; * keep popup ownership in explicit state so its trigger can render the open or pressed appearance until dismissal; * guard hover/active refinements when a disabled control must not react; * keep component variants few and meaningful rather than adding a variant for every call site. * bind primary styling to the decision area's real default commit and Enter action; do not derive it from action count, frequency, or toolbar position. * keep Badge and Alert variants semantic and scarce. Ordinary metadata stays neutral; do not map every enum case or section to a different color merely because a variant exists. The effective precedence is instance style, active semantic value states, disabled state, then GPUI runtime interaction refinements. Later layers only replace fields they set. Prefer `Theme::semantic_tokens()` for new application-owned presentation. The semantic token surface contains generic color roles plus radius, spacing, typography, and shadow scales; it deliberately avoids component names. Legacy component-specific theme values still exist for compatibility but should not become the extension point for every application widget. There is one current ownership caveat: `Theme::spacing_tokens()` projects the default scale, and `Theme::apply_semantic_tokens(...)` does not store custom spacing or elevation scales. An application that customizes those scales must retain its own `SemanticThemeTokens` (or narrower design-system state) and make that state available to its components. Do not write a custom spacing snapshot into the global theme and expect a later `cx.theme().semantic_tokens()` call to return it. If code mutates the global GPUI Component theme directly, call `Theme::sync_base(cx)` afterward so Base-owned scrollbars and resize handles receive the new projection. `Theme::change(...)` performs this projection as part of a complete theme change. An outward focus ring needs physical room. An ancestor with `overflow_hidden()` clips it. Prefer layouts that leave room; if a product must clip heavily, use the theme's focus-ring policy and retain the focused border instead of silently hiding all keyboard focus. ### Base font is the application zoom control `Root::render` calls `window.set_rem_size(cx.theme().font_size)`. Therefore the theme's base font is not only body typography; it is the reference length for the application's rem-based design scale. This deliberately follows the useful part of Tailwind's model: named type, spacing, and size steps share one relative base instead of becoming unrelated pixel constants. Change zoom by updating the base font and refreshing the window: ```rust Theme::global_mut(cx).font_size = px(18.); Theme::sync_base(cx); window.refresh(); ``` The base font itself is a pixel value because it anchors the scale. Descendant application UI should normally use relative helpers—`text_sm()`, `gap_2()`, `px_3()`, `h_8()`, `size_4()`—so type, whitespace, controls, and icons respond together. A custom component that combines rem-based text with fixed-pixel padding or icon geometry must document why that part should not zoom. Treat every direct `px(...)` and raw color constructor in application UI as a review finding. Accept it only for a documented physical/platform boundary, measured runtime geometry, raster/data color, or the theme/token definition itself. Convenience and matching a screenshot are not valid exceptions. Anything cached from resolved layout must include `window.rem_size()` in its invalidation key, directly or through a revision that changes with it. This includes wrapped row heights, text shaping/layout, virtual-list measurement, popup and dialog geometry, icon sizing derived from text, and custom canvas metrics. The Command component's variable-height rows are an ecosystem example: they remeasure when rem changes because the same fixed width wraps differently at a larger base font. Do not confuse this application zoom with Dock panel zoom. Dock zoom is a stateful layout operation that makes one tab group or tile fill the DockArea while keeping the container chrome and the way back out. It must not modify the window rem size. ## Events, actions, and focus Use pointer callbacks for pointer-specific behavior. Use GPUI Actions for commands that should support key bindings, menus, or dispatch from multiple inputs. Keep action handlers close to the view that owns the command. Model one logical desktop command once. A toolbar Button, `DropdownMenu` item, `ContextMenu` item, menu-bar item, and key binding should dispatch the same Action or call the same owner method instead of copying five mutations. Derive their label, icon, shortcut, and enabled state from one command policy where practical, so the entry points cannot disagree. The menu owns navigation and dismissal; the feature owner still owns whether the command is allowed and what it does. Preserve semantic roles in the element choice. Use `Button` for commands even when the desired treatment is quiet—select `outline`, `ghost`, or an icon presentation instead of replacing it with `Link`. GPUI Kit applications reserve `Link` for targets opened by a browser or mail client, such as a URL, web document, or email address. Use the relevant navigation component for an in-app destination and `Button`/`Action` for a command. This is a product convention, not a limitation of `gpui_kit::base::Link`, whose `open_with` seam can route a destination elsewhere. Only stop propagation when a nested interaction must prevent its parent from handling the same event. Blanket propagation stops break menus, selection, dragging, and window-level commands in ways that are difficult to diagnose. Make focus ownership explicit: * retain a `FocusHandle` in the entity that owns keyboard interaction; * register key contexts and actions on the appropriate focused region; * transfer focus when opening an overlay and restore it on dismissal; * render a visible `focus_visible` state; * do not request focus unconditionally from `render`. Attach a `key_context` and its `on_action` handlers to the same focused region. Bindings are contextual: a registered Action without the intended focus path is not a working keyboard interaction. Composite widgets should implement the complete navigation model—arrow movement, Home/End or page movement where appropriate, confirmation, cancellation, and Tab behavior—rather than a few isolated shortcuts. Modal surfaces must trap focus and restore the previous valid focus target on dismissal. Nested overlays dismiss from the top. Handle rapid close/open sequences without restoring focus through an intermediate, already-closing surface. ## Async work and side effects Start async work from an event, lifecycle hook, or named method—not as an unconditional side effect of `render`. Capture weak entities when work should not keep a closed view alive. When the task completes, update state through the GPUI context, handle the case where the entity or window no longer exists, and notify once after the coherent state change. Represent async operations with explicit states such as idle, loading, loaded, and failed. Preserve usable previous data during refresh when possible. Prevent duplicate destructive submissions and surface recoverable errors in the UI; do not rely on logs as user feedback. Use background executors for expensive parsing or computation, but keep GPUI entity mutation on the appropriate application context. Results can arrive after the request, document, view, or selection has changed; attach a revision or identity and reject stale work rather than applying it to new state. ## Layout, measurement, and scrolling `h_flex` centres its children on the cross axis; `v_flex` leaves flexbox's default, `stretch`. This matches Zed's `h_flex`, and it is what a row of controls wants, so a row of icon and label says nothing. It is not what a row of full-height columns wants: a column placed in a bare `h_flex` does not fill the row's height, so a column taller than the row is centred and its top — commonly a header — is clipped off the top of the window, with nothing near the column to say why. A row whose children are columns says `items_stretch()`: ```rust h_flex() .items_stretch() .size_full() .child(sidebar) .child(content) ``` Most UI should use GPUI layout rather than measuring itself. Measurement is a deep behavior tool for popups, virtualization, editors, resize handles, charts, and similar components whose correctness depends on resolved geometry. * Put measurement and geometry in the layer that owns the behavior. * Observe bounds in prepaint only when ordinary layout cannot express the relationship. * Never mutate unrelated application state every prepaint. * Treat measured data as frame- or revision-scoped; it can become stale after typography, rem size, width, theme, or content changes. * Centralize shared geometry such as popup flipping and viewport clamping so every overlay follows the same edge policy. For alignment invariants, prefer construction over correction: sibling regions should consume the same spacing token or shared inset instead of repeating equivalent literals. Add geometry assertions or visual regression coverage for critical repeated edges, columns, and gaps. Exercise more than the default window: rem zoom and display scaling can turn fractional coordinates into a one-physical-pixel drift even when the default screenshot looks aligned. Measure the resolved result when reviewing precision, but do not encode a measured correction as a raw `px(...)` nudge. Trace the mismatch to duplicated padding, nested insets, border ownership, font metrics, or rounding, then fix the structural owner. `h_flex()` and `v_flex()` are not mirror images: `h_flex()` centers its children on the cross axis, `v_flex()` leaves them stretching. A column placed in a row therefore takes its content's height, not the row's, and a column taller than the row is centered — its header is pushed off the top edge and clipped. Give a full-height column `h_full()`, or give the row `items_start()` or `items_stretch()`, whenever the child owns a header, a footer, or a scroll region that must resolve against the row's height. Every scrollable region must have one owner. In flex layouts, apply `min_w_0()` or `min_h_0()` to the flexible child that is allowed to shrink. A flex item only drops its content-based automatic minimum size when its own overflow is not visible, so an ordinary flexible child refuses to shrink around long content until you say so. `Scrollable` handles this for its own wrapper, but a plain `div` between it and the flex container still needs the minimum released. Avoid accidental nested scrolling; route wheel input to the intended axis and preserve platform/wasm differences when an API is not portable. Attach `Scrollable` to the element that owns the full panel, editor, or window viewport so its scrollbar resolves against the region edge. Put content inset inside that scroll owner rather than wrapping the scroll owner in a padded container. A scrollbar floating between content and the panel boundary usually reveals the wrong scroll owner or padding on the wrong layer. ## Lists, tables, and large data Use virtualization when data can grow beyond a small, bounded collection. Keep row identity separate from visible position and avoid cloning the full data set on every render. Let a stateful list or table own navigation, selection, scroll coordination, and visible-range calculation while item renderers own row presentation. Separate: * source data and domain IDs; * filtering/sorting state; * selection state; * viewport/scroll state; * row rendering. This keeps updates local and prevents the view tree from becoming the data model. Virtualization is a behavioral contract, not just a performance switch. Item measurement must be invalidated when width, typography, rem size, or row content changes. Keyboard selection and scroll-to-item must operate in model coordinates even when most elements do not exist in the current frame. ## Public API design For reusable components: * constructors should establish valid defaults; * builders take and return `Self` and use domain language; * callbacks describe requested changes and include pointer events only when modifiers or pointer details are meaningful; * evolvable behavioral seams use private fields, builders for construction, and readers for inspection; * boolean readers use `is_` or `has_` where a same-named builder exists; * non-boolean setters use `with_` when readers need the plain field name; * explicit compound parts are preferable to inspecting arbitrary descendants; * adding reusable behavior must not force a product-level visual choice. Private fields are the default for behavioral state that must evolve without breaking callers. Public fields are appropriate for deliberately record-like configuration, theme tokens, geometry, and serialized schemas when direct construction is part of the contract and the compatibility cost is accepted. Use `#[non_exhaustive]` when callers may inspect a record but should not depend on exhaustive construction or matching. Keep public module paths stable while reorganizing internals: use a module seam with deliberate re-exports so folders can change without forcing downstream imports to change. Prefer platform control terminology and established project naming over web-framework vocabulary. ## Platform and capability boundaries Do not assume every native or web target supports the same facility. Window decorations, accessibility bridges, system notifications, clipboard behavior, scroll gestures, fonts, and timing can differ. Put platform-specific code behind a narrow capability seam and define the fallback behavior. A platform branch must preserve the semantic contract even if presentation differs. For example, a system notification may have different retraction support, but the application still needs a coherent delivery state. Test both the shared state machine and the platform adapter where possible. ## File and naming conventions * Name views and entities after product concepts: `ProjectList`, `ProjectEditor`, `SettingsState`. * Name event handlers after intent: `confirm_delete`, `open_project`, `on_query_changed`. * Keep one main responsibility per module; split a file when state ownership or lifecycle can no longer be understood without reading unrelated behavior. * Keep component module, state, events, and focused tests together when they change together. * Document invariants and surprising lifecycle constraints; do not narrate obvious builder calls. * Use `rustfmt` and satisfy the workspace's Clippy rules. Avoid broad `allow` attributes that conceal unrelated warnings. ### Vocabulary is part of the API Use the same word for the same concept across components. Before naming a new method, search GPUI, `gpui-base`, and GPUI Component for the established term; prefer macOS/Windows control terminology where the ecosystem has no precedent. Localized documentation preserves exact API identifiers and established UI framework terms when translation would reduce precision. Format identifiers as code, explain retained terms when needed, and do not mix languages merely to make ordinary prose sound technical. | Concept | Naming pattern | Example | | --- | --- | --- | | Value-like rendered control | noun | `Button`, `Checkbox`, `Tab` | | Retained behavioral model | `State` | `InputState`, `TableState` | | Imperative shared reference | `Handle` | `DialogHandle`, scroll handle | | Semantic notification | `Event` | `TableEvent`, `SelectEvent` | | Keyboard command | verb or intent noun | `Confirm`, `Cancel`, `SelectNext` | | Pluggable data/behavior owner | `Delegate` / `Provider` | `TableDelegate`, `CompletionProvider` | | Application-supplied presentation | `render_` or `_renderer` | `render_item` | | Construction | `new`, or a semantic constructor | `new`, `horizontal`, `vertical` | | Fluent property | noun/adjective | `label`, `disabled`, `selected`, `placement` | | General non-boolean replacement builder | `with_` | `with_size`, `with_mode` | | In-place mutation | `set_` | `set_items`, `set_selected_index` | | Boolean reader | `is_` / `has_` | `is_open`, `is_closable`, `has_selection` | | Plain value reader | field noun | `placement`, `selected_value` | | Callback registration | `on_` | `on_click`, `on_open_change` | | Rendering a named region | `render_` | `render_toolbar`, `render_content` | For new APIs, fluent builders omit `set_` because they consume and return `Self`; mutation through `&mut self` uses `set_`. Preserve established public names when changing them would cause needless churn. Existing builder names such as `set_position` are compatibility exceptions, not patterns for new APIs. A boolean reader is either `has_`, when the value holds something, or `is_`, when it describes a state or a permission. Reach for the adjective whenever the action has one: `is_closable` over `can_close`, `is_zoomable` over `can_zoom`, `is_copyable` over `can_copy`. When the action is a verb phrase with no adjective form, name the thing it needs instead: `has_definition`, not `can_go_to_definition`. Do not add new `can_` readers. Boolean builders may use the field name (`disabled(bool)`) while their readers use `is_disabled()`. For a public seam struct containing non-boolean fields, use `with_item_ix(...)` for construction and `item_ix()` for reading so setter and getter names never collide. Prefer `_ix` for new local or internal zero-based indices, preserve established public terms such as `selected_index`, and do not introduce `_idx`. If callers never construct the seam value, do not publish a builder merely for symmetry. ### Let the enclosing name carry the context A name is read inside something. A field is read inside its type and a parameter inside its method, so neither repeats what encloses it: `with_item_ix(ix)`, not `with_item_ix(item_ix)`. Keep one type's fields at the same level of abbreviation. A single field spelled out in full becomes the odd one out, and a reader goes looking for the distinction that made it different. Because a builder is named `with_`, shortening a field shortens its builder with it and the pair stays matched. Shorten only where the enclosing name really does disambiguate. When a short form is also the established term for a *different* quantity elsewhere in the ecosystem, say which one you mean in the doc comment rather than lengthening the identifier — the doc is read at the call, and it can explain what a longer name could only hint at. ### Use precise domain words * **selected** is persistent membership or the active item; **focused** is the current keyboard target; **hovered** is pointer presence; **confirmed** is an activation result. Never use them interchangeably. * **open/close** describes an overlay or disclosure state; **show/hide** is for transient presentation requests; **expand/collapse** describes structure. * **disabled** prevents interaction; **read-only** permits navigation and selection but prevents editing; **loading** prevents duplicate work while an operation is pending. * **index** is a current positional coordinate; **id** is stable identity; `IndexPath` represents hierarchical position. Do not persist or key reorderable data by index. * **value** is controlled domain data; **presentation** is a read-only snapshot prepared for rendering; **state** is retained behavior. * **placement** is a side or anchor policy; **position** is resolved geometry. * **size** is a semantic control tier; **width/height/bounds** are geometry. * **child/children** follows GPUI composition; named slots such as `header`, `footer`, `trigger`, and `content` carry additional semantics. Avoid vague public names such as `data`, `item2`, `handle_action`, `update_ui`, `process`, `manager`, or `config` when a narrower domain term exists. `Manager` is appropriate only when a type truly coordinates a collection or lifecycle, as `ToastManager` does. ### Type and module style * Rust types and Actions use `UpperCamelCase`; modules, functions, methods, fields, and local variables use `snake_case`; constants use `SCREAMING_SNAKE_CASE`. * A module named after a component owns its public seam. Internal folders may split state, element, geometry, platform adapter, and tests without leaking those folder names into imports. * Use singular module names for one component concept and established ecosystem names for families (`input`, `table`, `dock`). * Suffix type-erased wrappers with `Any` only when they erase a real type boundary, such as `AnyInputState` or `AnyElement`. * Suffix identifiers with `Id`, zero-based indices with `ix`, and collections with meaningful plurals. Do not alternate `idx`, `index`, and `ix` in one subsystem. * Name predicates positively when possible. A positive `enabled`/`visible` contract is easier to compose than multiple negatives, but preserve established API terms such as `disabled` where they match control semantics. ### Callback and event wording Use `on_click` only for a genuine click-level contract. A controlled semantic primitive in Base should prefer `on_change(next_value, ...)`; a styled compatibility component may retain `on_click` when pointer details or existing API expectations matter. Do not invent a `ClickEvent` for a model-driven change. Name before/after lifecycle hooks precisely. `on_will_change` can veto or prepare; `on_change` observes a requested/current value contract; `on_confirm` commits a choice; `on_dismiss` closes a transient surface. Document whether a callback runs before internal state changes, after them, or instead of them, and whether it may synchronously re-enter the component. ### Documentation and copy style Public docs should begin with what a type does and who owns its state. Examples must use current, compilable APIs and show stable IDs. Document defaults, platform limitations, focus behavior, callback ordering, and any requirement to call `notify`, `emit`, or a theme synchronization method. Follow the [interface-language rules](./design-guides.md#interface-language) for labels, commands, confirmation dialogs, capitalization, and ellipses. Keep one canonical term for each domain object, command, and state. Translation keys describe stable intent (`dialog.delete_project.title`), not a source-language sentence or a screen coordinate. Never assemble a sentence from translated fragments or reuse one key for meanings that happen to share the same English text. Localize intent, not syntax. Give every locale control over word order, pluralization, punctuation, and the amount of context it needs. Review strings inside the component and with realistic data. Tests or linting should catch missing keys, unintended CJK text in English resources, three-dot ellipses, unreviewed ALL CAPS, and inconsistent fixed terms; human review still decides whether repetition is justified by context. Verify every string inside its component with realistic content, text expansion, and application zoom. ## Testing strategy Test at the lowest layer that can prove the behavior: 1. pure tests for state transitions, geometry, parsing, and ordering; 2. GPUI context tests for entities, events, and subscriptions; 3. `VisualTestContext` interaction tests for focus, keyboard, pointer, layout, and rendered state; 4. example or application smoke tests for complete workflows. For an interactive component, cover the semantic contract rather than its implementation details: pointer and keyboard activation, controlled value changes, disabled behavior, focus movement, event count/order, stable identity, and important empty or failure states. Add a regression test before fixing a bug whenever the failure can be reproduced deterministically. For UI behavior that depends on the real window system, test through the accessibility tree by role, label, value, enabled state, focus, and selection. Re-read the tree after every state-changing action because element indexes are snapshots. Use screenshots for visual facts the semantic tree cannot express; use coordinate input only as a fallback. Report automated and manual evidence separately. ## Performance rules * Do not mutate state or notify unconditionally in `render`. * Avoid rebuilding entities, subscriptions, focus handles, and expensive data structures per frame. * Notify the narrowest owning entity after a coherent state change. * Virtualize long collections and render only the visible range. * Avoid cloning large strings or collections solely to satisfy a closure; capture stable handles or shared data. * Measure before adding caches. A cache must have a clear invalidation owner. * Keep animation work bounded and honor reduced motion. ## Common failure modes Avoid these patterns: * one entity containing the entire application's unrelated state; * business logic and network requests embedded in a long `render` method; * random or index-based `ElementId` values for reorderable content; * literal colors and radii that break custom themes; * custom clickable `div`s where a semantic component already supplies focus, keyboard, disabled, and accessibility behavior; * duplicated local state that drifts from a controlled model value; * `cx.notify()` loops caused by mutation during every render; * nested scroll containers without explicit ownership; * a new component variant for a one-off screen; * confirmation dialogs for reversible, low-risk actions; * tests that call internal methods but never exercise keyboard or pointer behavior. ## Rules for coding agents Before editing, an agent must read the nearest implementation, its tests, the re-export seam, and the relevant component documentation. It must search the current source for signatures instead of translating a React, CSS, or old GPUI example by analogy. For each change, the agent should be able to name: 1. the behavior owner and presentation owner; 2. the retained identity and state lifecycle; 3. the pointer, keyboard, focus, and accessibility contract; 4. the layout and overflow owner; 5. the theme tokens and intentional exceptions; 6. the test that would fail if the behavior regressed. Generated code must be reviewed and tested by a person. “Compiles” is not a UI quality bar, and a broad refactor that merely makes generated code look tidy is not a substitute for matching the repository's architecture. ## Implementation checklist Before opening a change for review, confirm that: * state and side-effect ownership are explicit; * `RenderOnce` versus `Entity` is chosen deliberately; * repeated elements have stable domain-based IDs; * theme tokens and component sizes replace isolated visual literals; * keyboard actions, focus, disabled state, and overlays work together; * loading, empty, error, and cancellation paths are represented; * long data sets use an appropriate virtualized component; * public API additions preserve dependency direction and encapsulation; * tests prove behavior at the appropriate layer; * formatting, Clippy, targeted tests, and relevant examples pass. See [Getting Started](./getting-started.md) for application setup and the component pages for current API details. --- --- url: /base/primitives/collapsible.md description: >- A composable region that shows or hides content without prescribing its trigger styling. --- # Collapsible A composable region that shows or hides content without prescribing its trigger styling. Like every `gpui-base` primitive, Collapsible supplies behavior and semantic structure without imposing a product visual language. Apply GPUI styles and compose the exported parts to match your design system. ## Example The [single native Cargo entrypoint](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/components.rs) selects this primitive from the [shared showcase implementation](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/mod.rs). The same showcase is compiled once for the WASM preview above. ```bash cargo run -p gpui-base --example components -- collapsible ``` ## Import ```rust use gpui_kit::base::{Collapsible}; ``` ## Anatomy and API The example composes `Collapsible`. GPUI's standard styling and event traits provide presentation; these base types provide the interaction structure. The authoritative module is [`components/collapsible.rs`](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/components/collapsible.rs). Native and browser previews compile this same file. ## State and events Pass the controlled expanded value to `open`; update it from the trigger callback. Keep controlled state on the parent render type or in a GPUI entity. Update it in callbacks and call `cx.notify()`; do not recreate persistent entities during every render. ## Complete Rust example The complete implementation used by the runnable showcase is embedded directly from Rust source: The command above supplies application initialization, window creation, and shared `BaseShowcase` state. ## Accessibility Name the trigger, expose expanded state, and remove hidden content from focus order. ## Notes Use stable element IDs where accepted. Verify focus, hover, active, selected, disabled, reduced-motion, and high-contrast appearances in the consuming design system. --- --- url: /docs/components/collapsible.md description: An interactive element which expands/collapses. --- # Collapsible An interactive element which expands/collapses. ## Import ```rust use gpui_kit::component::collapsible::Collapsible; ``` ## Usage ### Basic Use ```rust Collapsible::new() .max_w_128() .gap_1() .open(self.open) .child( "This is a collapsible component. \ Click the header to expand or collapse the content.", ) .content( "This is the full content of the Collapsible component. \ It is only visible when the component is expanded. \n\ You can put any content you like here, including text, images, \ or other UI elements.", ) .child( h_flex().justify_center().child( Button::new("toggle1") .icon(IconName::ChevronDown) .label("Show more") .when(open, |this| { this.icon(IconName::ChevronUp).label("Show less") }) .xsmall() .link() .on_click({ cx.listener(move |this, _, _, cx| { this.open = !this.open; cx.notify(); }) }), ), ) ``` We can use `open` method to control the collapsed state. If false, the `content` method added child elements will be hidden. ### Animated reveal Opt into a reversible, measured height reveal with a stable motion ID: ```rust Collapsible::new() .motion_id("advanced-options") .open(self.open) .content(options) ``` The content remains mounted while closed so it can be measured and immediately reverse if toggled mid-animation. Without `motion_id`, the component keeps the immediate mount/unmount behavior. See the [GPUI Base Motion guide](/base/motion) for timing, reduced-motion, and performance details. [Collapsible]: https://docs.rs/gpui-component/latest/gpui_component/collapsible/struct.Collapsible.html --- --- url: /zh-CN/base/primitives/collapsible.md description: 不规定触发器样式的可组合显隐区域。 --- # Collapsible 不规定触发器样式的可组合显隐区域。 和所有 GPUI Base 原语一样,Collapsible 只提供行为和语义结构,不规定产品视觉语言。请使用 GPUI 样式并组合导出的部件,使其符合你的设计系统。 ## 示例 原生示例和页面上方的 WASM 预览共用同一份实现: ```bash cargo run -p gpui-base --example components -- collapsible ``` ## 导入 ```rust use gpui_kit::base::{Collapsible}; ``` ## 结构与 API 示例组合上述公开类型。GPUI 的标准样式和事件 trait 负责表现,Base 类型负责交互结构。权威实现位于 [`components/collapsible.rs`](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/components/collapsible.rs),原生与浏览器预览编译的是同一文件。 ## 状态与事件 父级持有打开状态;内容可结合 Motion 做进入与退出过渡。 受控状态应保存在父渲染类型或 GPUI entity 中;在回调中更新并调用 `cx.notify()`,不要在每次渲染时重建持久 entity。 ## 完整 Rust 示例 ## 可访问性 触发器应暴露展开状态以及它所控制内容的关系。 ## 注意事项 在支持的位置使用稳定元素 ID,并在消费端设计系统中验证焦点、悬停、按下、选中、禁用、减少动态效果和高对比度状态。 --- --- url: /zh-CN/docs/components/collapsible.md description: 可展开和收起内容的交互式组件。 --- # Collapsible Collapsible 是一个用于展开和收起内容的交互式组件。 ## 导入 ```rust use gpui_kit::component::collapsible::Collapsible; ``` ## 用法 ### 基础用法 ```rust Collapsible::new() .max_w_128() .gap_1() .open(self.open) .child( "This is a collapsible component. \ Click the header to expand or collapse the content.", ) .content( "This is the full content of the Collapsible component. \ It is only visible when the component is expanded. \n\ You can put any content you like here, including text, images, \ or other UI elements.", ) .child( h_flex().justify_center().child( Button::new("toggle1") .icon(IconName::ChevronDown) .label("Show more") .when(open, |this| { this.icon(IconName::ChevronUp).label("Show less") }) .xsmall() .link() .on_click({ cx.listener(move |this, _, _, cx| { this.open = !this.open; cx.notify(); }) }), ), ) ``` 可以通过 `open` 方法控制当前是否展开。若值为 `false`,则通过 `content` 添加的子内容会被隐藏。 ### 展开动画 使用稳定 motion ID 可选择启用支持中途反向的测量式高度展开: ```rust Collapsible::new() .motion_id("advanced-options") .open(self.open) .content(options) ``` 启用后,内容在关闭时仍保持挂载,以便测量自然高度,并可在动画途中切换时立即反向。不调用 `motion_id` 时仍使用即时挂载/卸载行为。timing、reduced motion 和性能细节见 [GPUI Base 动画与动效](/zh-CN/base/motion)。 [Collapsible]: https://docs.rs/gpui-component/latest/gpui_component/collapsible/struct.Collapsible.html --- --- url: /base/primitives/color-picker.md description: State and interaction foundations for selecting colors in a custom picker UI. --- # Color Picker State and interaction foundations for selecting colors in a custom picker UI. Like every `gpui-base` primitive, Color Picker supplies behavior and semantic structure without imposing a product visual language. Apply GPUI styles and compose the exported parts to match your design system. ## Example The [single native Cargo entrypoint](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/components.rs) selects this primitive from the [shared showcase implementation](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/mod.rs). The same showcase is compiled once for the WASM preview above. ```bash cargo run -p gpui-base --example components -- color-picker ``` ## Import ```rust use gpui_kit::base::{ColorPicker, ColorPickerEvent, ColorPickerState, ColorSwatch}; ``` ## Anatomy and API The example composes `ColorPicker`, `ColorSwatch`, and `ColorPickerState`. GPUI's standard styling and event traits provide presentation; these base types provide the interaction structure. `ColorPicker` is the controlled root: it carries the trigger's accessibility semantics and focus, opens on Confirm, and dismisses on Cancel. `ColorSwatch` is one selectable color in a palette, carrying radio semantics, an accessible hex name, and the hover and activation callbacks a picker previews and commits with. The authoritative module is [`components/color_picker.rs`](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/components/color_picker.rs). Native and browser previews compile this same file. ## State and events `ColorPickerState` owns the committed color, the transient preview shown while the user hovers or edits, the controlled open state, and the active panel. It also owns a hex `InputState` and four component `SliderState`s and keeps all of them in sync, so an application renders those with its own input and slider presentation rather than reconciling them itself. Committing a color emits `ColorPickerEvent::Change`. A color supplied to `default_value` cannot reach the hex field and sliders without a window, so call `sync_pending_value` from render; it is a no-op once nothing is pending. Retain the state's entity on the parent view. Keep controlled state on the parent render type or in a GPUI entity. Update it in callbacks and call `cx.notify()`; do not recreate persistent entities during every render. ## Complete Rust example The complete implementation used by the runnable showcase is embedded directly from Rust source: The command above supplies application initialization, window creation, and shared `BaseShowcase` state. ## Accessibility Provide a textual color value and keyboard controls; never communicate selection by color alone. The root exposes the trigger's expanded state, and each swatch exposes its hex value as its accessible name plus its selected state, so a palette never depends on color alone. ## Notes Use stable element IDs where accepted. Verify focus, hover, active, selected, disabled, reduced-motion, and high-contrast appearances in the consuming design system. --- --- url: /zh-CN/base/primitives/color-picker.md description: 为自定义颜色选择器提供状态与交互基础。 --- # Color Picker 为自定义颜色选择器提供状态与交互基础。 和所有 GPUI Base 原语一样,Color Picker 只提供行为和语义结构,不规定产品视觉语言。请使用 GPUI 样式并组合导出的部件,使其符合你的设计系统。 ## 示例 原生示例和页面上方的 WASM 预览共用同一份实现: ```bash cargo run -p gpui-base --example components -- color-picker ``` ## 导入 ```rust use gpui_kit::base::{ColorPicker, ColorPickerEvent, ColorPickerState, ColorSwatch}; ``` ## 结构与 API 示例组合上述公开类型。GPUI 的标准样式和事件 trait 负责表现,Base 类型负责交互结构。权威实现位于 [`components/color-picker.rs`](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/components/color-picker.rs),原生与浏览器预览编译的是同一文件。 ## 状态与事件 ColorPickerState 保存颜色,ColorPickerEvent 报告用户变更。 受控状态应保存在父渲染类型或 GPUI entity 中;在回调中更新并调用 `cx.notify()`,不要在每次渲染时重建持久 entity。 ## 完整 Rust 示例 ## 可访问性 为色样提供文本名称或数值,并确保键盘用户可以完成选择。 ## 注意事项 在支持的位置使用稳定元素 ID,并在消费端设计系统中验证焦点、悬停、按下、选中、禁用、减少动态效果和高对比度状态。 --- --- url: /docs/components/color-picker.md description: >- A comprehensive color selection interface with support for multiple color formats, presets, and alpha channel. --- # ColorPicker A versatile color picker component that provides an intuitive interface for color selection. Features include color palettes, hex input, featured colors, and support for various color formats including RGB, HSL, and hex values with alpha channel support. ## Import ```rust use gpui_kit::component::color_picker::{ColorPicker, ColorPickerState, ColorPickerEvent}; ``` ## Usage ### Basic Color Picker ```rust use gpui_kit::{Entity, Window, Context}; // Create color picker state let color_picker = cx.new(|cx| ColorPickerState::new(window, cx) .default_value(cx.theme().primary) ); // Create the color picker component ColorPicker::new(&color_picker) ``` ### With Event Handling ```rust use gpui_kit::{Subscription, Entity}; let color_picker = cx.new(|cx| ColorPickerState::new(window, cx)); let _subscription = cx.subscribe(&color_picker, |this, _, ev, _| match ev { ColorPickerEvent::Change(color) => { if let Some(color) = color { println!("Selected color: {}", color.to_hex()); // Handle color change } } }); ColorPicker::new(&color_picker) ``` ### Setting Default Color ```rust use gpui_kit::Hsla; let color_picker = cx.new(|cx| ColorPickerState::new(window, cx) .default_value(cx.theme().blue) // Set default color ); ``` ### Different Sizes ```rust // Small color picker ColorPicker::new(&color_picker).small() // Medium color picker (default) ColorPicker::new(&color_picker) // Large color picker ColorPicker::new(&color_picker).large() // Extra small color picker ColorPicker::new(&color_picker).xsmall() ``` ### With Custom Featured Colors ```rust use gpui_kit::Hsla; let featured_colors = vec![ cx.theme().red, cx.theme().green, cx.theme().blue, cx.theme().yellow, // Add your custom colors ]; ColorPicker::new(&color_picker) .featured_colors(featured_colors) ``` ### With Icon Instead of Color Square ```rust use gpui_kit::component::IconName; ColorPicker::new(&color_picker) .icon(IconName::Palette) ``` ### With Label ```rust ColorPicker::new(&color_picker) .label("Background Color") ``` ### Custom Anchor Position ```rust use gpui_kit::Anchor; ColorPicker::new(&color_picker) .anchor(Anchor::TopRight) // Dropdown opens to top-right ``` ## Color Selection Interface ### Color Palettes The color picker includes predefined color palettes organized by color family: * **Stone**: Neutral grays and stone colors * **Red**: Red color variations from light to dark * **Orange**: Orange color variations * **Yellow**: Yellow color variations * **Green**: Green color variations * **Cyan**: Cyan color variations * **Blue**: Blue color variations * **Purple**: Purple color variations * **Pink**: Pink color variations Each palette provides multiple shades and tints of the base color, allowing for precise color selection. ### Featured Colors Section A customizable section at the top of the picker that displays frequently used or brand colors. If not specified, defaults to theme colors: * Primary colors from the current theme * Light variants of theme colors * Essential UI colors (red, blue, green, yellow, cyan, magenta) ### Hex Input Field A text input field that allows direct entry of hex color values: * Supports standard 6-digit hex format (#RRGGBB) * Real-time validation and preview * Updates color picker state automatically * Press Enter to confirm selection ## Color Formats ### RGB (Red, Green, Blue) Colors are internally represented using GPUI's `Hsla` format but can be converted to RGB: ```rust let color = cx.theme().blue; // Access RGB components through Hsla methods ``` ### HSL (Hue, Saturation, Lightness) Native format used by the color picker: ```rust use gpui_kit::Hsla; // Create HSL color let color = Hsla::hsl(240.0, 100.0, 50.0); // Blue color // Access components let hue = color.h; let saturation = color.s; let lightness = color.l; ``` ### Hex Format Standard web hex format with # prefix: ```rust // Convert color to hex let hex_string = color.to_hex(); // Returns "#3366FF" // Parse hex string to color if let Ok(color) = Hsla::parse_hex("#3366FF") { // Use parsed color } ``` ## Alpha Channel Full alpha channel support for transparency: ```rust use gpui_kit::hsla; // Create color with alpha let semi_transparent = hsla(0.5, 0.8, 0.6, 0.7); // 70% opacity // Modify existing color opacity let transparent_blue = cx.theme().blue.opacity(0.5); ``` The color picker preserves alpha values when selecting colors and allows modification through the alpha component of HSLA colors. ## API Reference * [ColorPicker] * [ColorPickerState] * [ColorPickerEvent] ## Examples ### Color Theme Editor ```rust struct ThemeEditor { primary_color: Entity, secondary_color: Entity, accent_color: Entity, } impl ThemeEditor { fn new(window: &mut Window, cx: &mut Context) -> Self { let primary_color = cx.new(|cx| ColorPickerState::new(window, cx) .default_value(cx.theme().primary) ); let secondary_color = cx.new(|cx| ColorPickerState::new(window, cx) .default_value(cx.theme().secondary) ); let accent_color = cx.new(|cx| ColorPickerState::new(window, cx) .default_value(cx.theme().accent) ); Self { primary_color, secondary_color, accent_color, } } fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .gap_4() .child( h_flex() .gap_2() .items_center() .child("Primary Color:") .child(ColorPicker::new(&self.primary_color)) ) .child( h_flex() .gap_2() .items_center() .child("Secondary Color:") .child(ColorPicker::new(&self.secondary_color)) ) .child( h_flex() .gap_2() .items_center() .child("Accent Color:") .child(ColorPicker::new(&self.accent_color)) ) } } ``` ### Brand Color Selector ```rust use gpui_kit::component::{Sizable as _}; let brand_colors = vec![ Hsla::parse_hex("#FF6B6B").unwrap(), // Brand Red Hsla::parse_hex("#4ECDC4").unwrap(), // Brand Teal Hsla::parse_hex("#45B7D1").unwrap(), // Brand Blue Hsla::parse_hex("#96CEB4").unwrap(), // Brand Green Hsla::parse_hex("#FFEAA7").unwrap(), // Brand Yellow ]; ColorPicker::new(&color_picker) .featured_colors(brand_colors) .label("Brand Color") .large() ``` ### Toolbar Color Picker ```rust use gpui_kit::component::{Sizable as _, IconName); ColorPicker::new(&text_color_picker) .icon(IconName::Type) .small() .anchor(Anchor::BottomLeft) ``` ### Color Palette Builder ```rust struct ColorPalette { colors: Vec>, } impl ColorPalette { fn add_color(&mut self, window: &mut Window, cx: &mut Context) { let color_picker = cx.new(|cx| ColorPickerState::new(window, cx)); // Subscribe to color changes cx.subscribe(&color_picker, |this, _, ev, _| match ev { ColorPickerEvent::Change(color) => { if let Some(color) = color { this.update_palette_preview(); } } }); self.colors.push(color_picker); cx.notify(); } fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { h_flex() .gap_2() .children( self.colors.iter().map(|color_picker| { ColorPicker::new(color_picker).small() }) ) .child( Button::new("add-color") .icon(IconName::Plus) .ghost() .on_click(cx.listener(|this, _, window, cx| { this.add_color(window, cx); })) ) } } ``` ### With Color Validation ```rust let color_picker = cx.new(|cx| ColorPickerState::new(window, cx)); let _subscription = cx.subscribe(&color_picker, |this, _, ev, _| match ev { ColorPickerEvent::Change(color) => { if let Some(color) = color { // Validate color accessibility if this.validate_contrast(color) { this.apply_color(color); } else { this.show_contrast_warning(); } } } }); ``` [ColorPicker]: https://docs.rs/gpui-component/latest/gpui_component/color_picker/struct.ColorPicker.html [ColorPickerState]: https://docs.rs/gpui-component/latest/gpui_component/color_picker/struct.ColorPickerState.html [ColorPickerEvent]: https://docs.rs/gpui-component/latest/gpui_component/color_picker/enum.ColorPickerEvent.html --- --- url: /zh-CN/docs/components/color-picker.md description: 支持多种颜色格式、预设色和透明通道的颜色选择组件。 --- # ColorPicker ColorPicker 是一个通用的颜色选择组件,提供直观的颜色选择界面。它支持颜色面板、十六进制输入、精选颜色,以及 RGB、HSL 和十六进制格式,并支持 alpha 透明通道。 ## 导入 ```rust use gpui_kit::component::color_picker::{ColorPicker, ColorPickerState, ColorPickerEvent}; ``` ## 用法 ### 基础 Color Picker ```rust use gpui_kit::{Entity, Window, Context}; let color_picker = cx.new(|cx| ColorPickerState::new(window, cx) .default_value(cx.theme().primary) ); ColorPicker::new(&color_picker) ``` ### 处理事件 ```rust use gpui_kit::{Subscription, Entity}; let color_picker = cx.new(|cx| ColorPickerState::new(window, cx)); let _subscription = cx.subscribe(&color_picker, |this, _, ev, _| match ev { ColorPickerEvent::Change(color) => { if let Some(color) = color { println!("Selected color: {}", color.to_hex()); // Handle color change } } }); ColorPicker::new(&color_picker) ``` ### 设置默认颜色 ```rust use gpui_kit::Hsla; let color_picker = cx.new(|cx| ColorPickerState::new(window, cx) .default_value(cx.theme().blue) ); ``` ### 不同尺寸 ```rust ColorPicker::new(&color_picker).small() ColorPicker::new(&color_picker) ColorPicker::new(&color_picker).large() ColorPicker::new(&color_picker).xsmall() ``` ### 自定义精选颜色 ```rust use gpui_kit::Hsla; let featured_colors = vec![ cx.theme().red, cx.theme().green, cx.theme().blue, cx.theme().yellow, ]; ColorPicker::new(&color_picker) .featured_colors(featured_colors) ``` ### 用图标替代色块 ```rust use gpui_kit::component::IconName; ColorPicker::new(&color_picker) .icon(IconName::Palette) ``` ### 带标签 ```rust ColorPicker::new(&color_picker) .label("Background Color") ``` ### 自定义锚点位置 ```rust use gpui_kit::Anchor; ColorPicker::new(&color_picker) .anchor(Anchor::TopRight) ``` ## 颜色选择界面 ### 调色板 组件内置多组颜色家族: * **Stone**:中性色与石灰灰阶 * **Red**:红色系 * **Orange**:橙色系 * **Yellow**:黄色系 * **Green**:绿色系 * **Cyan**:青色系 * **Blue**:蓝色系 * **Purple**:紫色系 * **Pink**:粉色系 每个颜色家族都提供多个深浅层级,方便精准选色。 ### 精选颜色区域 顶部的精选颜色区域可用于放置品牌色或常用色。若未指定,则默认使用当前主题中的核心颜色: * 当前主题的主色 * 主题颜色的浅色变体 * 常用界面色,如 red、blue、green、yellow、cyan、magenta ### Hex 输入框 组件提供十六进制输入框,可直接输入颜色值: * 支持标准 6 位格式 `#RRGGBB` * 实时校验并预览 * 会自动同步到组件状态 * 按 Enter 确认 ## 颜色格式 ### RGB 颜色内部使用 GPUI 的 `Hsla` 表示,但可以转换为 RGB 相关值: ```rust let color = cx.theme().blue; // Access RGB components through Hsla methods ``` ### HSL ColorPicker 原生使用 HSL/HSLA 表示: ```rust use gpui_kit::Hsla; let color = Hsla::hsl(240.0, 100.0, 50.0); let hue = color.h; let saturation = color.s; let lightness = color.l; ``` ### Hex 标准 Web 十六进制格式: ```rust let hex_string = color.to_hex(); if let Ok(color) = Hsla::parse_hex("#3366FF") { // Use parsed color } ``` ## Alpha 通道 支持透明度: ```rust use gpui_kit::hsla; let semi_transparent = hsla(0.5, 0.8, 0.6, 0.7); let transparent_blue = cx.theme().blue.opacity(0.5); ``` ColorPicker 在选择颜色时会保留 alpha 值,也可通过 HSLA 的 alpha 分量进一步修改。 ## API 参考 * [ColorPicker] * [ColorPickerState] * [ColorPickerEvent] ## 示例 ### 主题颜色编辑器 ```rust struct ThemeEditor { primary_color: Entity, secondary_color: Entity, accent_color: Entity, } impl ThemeEditor { fn new(window: &mut Window, cx: &mut Context) -> Self { let primary_color = cx.new(|cx| ColorPickerState::new(window, cx) .default_value(cx.theme().primary) ); let secondary_color = cx.new(|cx| ColorPickerState::new(window, cx) .default_value(cx.theme().secondary) ); let accent_color = cx.new(|cx| ColorPickerState::new(window, cx) .default_value(cx.theme().accent) ); Self { primary_color, secondary_color, accent_color, } } fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .gap_4() .child( h_flex() .gap_2() .items_center() .child("Primary Color:") .child(ColorPicker::new(&self.primary_color)) ) .child( h_flex() .gap_2() .items_center() .child("Secondary Color:") .child(ColorPicker::new(&self.secondary_color)) ) .child( h_flex() .gap_2() .items_center() .child("Accent Color:") .child(ColorPicker::new(&self.accent_color)) ) } } ``` ### 品牌色选择器 ```rust use gpui_kit::component::Sizable as _; let brand_colors = vec![ Hsla::parse_hex("#FF6B6B").unwrap(), Hsla::parse_hex("#4ECDC4").unwrap(), Hsla::parse_hex("#45B7D1").unwrap(), Hsla::parse_hex("#96CEB4").unwrap(), Hsla::parse_hex("#FFEAA7").unwrap(), ]; ColorPicker::new(&color_picker) .featured_colors(brand_colors) .label("Brand Color") .large() ``` ### 工具栏颜色选择器 ```rust use gpui_kit::component::{Sizable as _, IconName}; ColorPicker::new(&text_color_picker) .icon(IconName::Type) .small() .anchor(Anchor::BottomLeft) ``` ### 调色板构建器 ```rust struct ColorPalette { colors: Vec>, } impl ColorPalette { fn add_color(&mut self, window: &mut Window, cx: &mut Context) { let color_picker = cx.new(|cx| ColorPickerState::new(window, cx)); cx.subscribe(&color_picker, |this, _, ev, _| match ev { ColorPickerEvent::Change(color) => { if let Some(color) = color { this.update_palette_preview(); } } }); self.colors.push(color_picker); cx.notify(); } fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { h_flex() .gap_2() .children( self.colors.iter().map(|color_picker| { ColorPicker::new(color_picker).small() }) ) .child( Button::new("add-color") .icon(IconName::Plus) .ghost() .on_click(cx.listener(|this, _, window, cx| { this.add_color(window, cx); })) ) } } ``` ### 颜色校验 ```rust let color_picker = cx.new(|cx| ColorPickerState::new(window, cx)); let _subscription = cx.subscribe(&color_picker, |this, _, ev, _| match ev { ColorPickerEvent::Change(color) => { if let Some(color) = color { if this.validate_contrast(color) { this.apply_color(color); } else { this.show_contrast_warning(); } } } }); ``` [ColorPicker]: https://docs.rs/gpui-component/latest/gpui_component/color_picker/struct.ColorPicker.html [ColorPickerState]: https://docs.rs/gpui-component/latest/gpui_component/color_picker/struct.ColorPickerState.html [ColorPickerEvent]: https://docs.rs/gpui-component/latest/gpui_component/color_picker/enum.ColorPickerEvent.html --- --- url: /base/primitives/combobox.md description: >- A text input paired with keyboard-navigable suggestions and selection behavior. --- # Combobox A text input paired with keyboard-navigable suggestions and selection behavior. Like every `gpui-base` primitive, Combobox supplies behavior and semantic structure without imposing a product visual language. Apply GPUI styles and compose the exported parts to match your design system. ## Example The [single native Cargo entrypoint](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/components.rs) selects this primitive from the [shared showcase implementation](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/mod.rs). The same showcase is compiled once for the WASM preview above. ```bash cargo run -p gpui-base --example components -- combobox ``` ## Import ```rust use gpui_kit::base::{Combobox}; ``` ## Anatomy and API The example composes `Combobox`. GPUI's standard styling and event traits provide presentation; these base types provide the interaction structure. The authoritative module is [`components/combobox.rs`](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/components/combobox.rs). Native and browser previews compile this same file. ## State and events The input state owns query text while the delegate supplies choices, filtering, and selection. Keep controlled state on the parent render type or in a GPUI entity. Update it in callbacks and call `cx.notify()`; do not recreate persistent entities during every render. ## Complete Rust example The complete implementation used by the runnable showcase is embedded directly from Rust source: The command above supplies application initialization, window creation, and shared `BaseShowcase` state. ## Accessibility Synchronize input, popup, active option, and selected value; make every option keyboard reachable. ## Notes Use stable element IDs where accepted. Verify focus, hover, active, selected, disabled, reduced-motion, and high-contrast appearances in the consuming design system. --- --- url: /docs/components/combobox.md description: An autocomplete input paired with a searchable dropdown list. --- # Combobox A searchable dropdown for selecting one or multiple values from a list. ## Select vs Combobox | Feature | Select | Combobox | | --- | --- | --- | | Searchable | ✓ (optional) | ✓ (optional) | | Multi-select | — | ✓ (`.multiple(true)`) | | Custom trigger rendering | — | ✓ | | Custom item rendering | — | ✓ | | Footer action slot | — | ✓ | Use `Select` for simple single-value picking. Use `Combobox` when you need multi-select, a fully custom trigger, or custom item rendering. ## Import ```rust use gpui_kit::component::combobox::{ Combobox, ComboboxState, ComboboxEvent, ComboboxTriggerCtx, }; use gpui_kit::component::searchable_list::{ SearchableListItem, SearchableVec, SearchableGroup, }; ``` ## Usage ### Basic Single-Select ```rust let state = cx.new(|cx| { ComboboxState::new( SearchableVec::new(vec!["Next.js", "SvelteKit", "Nuxt.js"]), vec![], // no initial selection window, cx, ) .searchable(true) }); Combobox::new(&state) .placeholder("Select framework...") .search_placeholder("Search...") .w_full() ``` ### Multi-Select Pass `.multiple(true)` to enable multi-select mode. Clicking an item toggles it; the dropdown stays open until the user presses Escape or clicks outside. ```rust let state = cx.new(|cx| { ComboboxState::new( SearchableVec::new(vec!["React", "Vue", "Angular"]), vec![IndexPath::new(0)], // pre-selected window, cx, ) .multiple(true) .searchable(true) }); Combobox::new(&state).placeholder("Select frameworks") ``` ### Pre-selected Item Pass index paths of items to pre-select: ```rust let state = cx.new(|cx| { ComboboxState::new(items, vec![IndexPath::new(0)], window, cx) }); ``` ### Grouped Items Use `SearchableGroup` to group items under a heading: ```rust let grouped = SearchableVec::new(vec![ SearchableGroup::new("Fruits").items(vec![ FoodItem::new("Apples"), FoodItem::new("Bananas"), ]), SearchableGroup::new("Vegetables").items(vec![ FoodItem::new("Carrots"), FoodItem::new("Spinach"), ]), ]); let state = cx.new(|cx| { ComboboxState::new(grouped, vec![], window, cx).searchable(true) }); Combobox::new(&state) ``` ### Implementing `SearchableListItem` Built-in implementations exist for `String`, `SharedString`, and `&'static str`. For custom types implement the trait: ```rust #[derive(Clone)] struct Country { name: SharedString, code: SharedString, } impl SearchableListItem for Country { type Value = SharedString; fn title(&self) -> SharedString { self.name.clone() } fn value(&self) -> &SharedString { &self.code } fn matches(&self, query: &str) -> bool { self.name.to_lowercase().contains(query) || self.code.to_lowercase().contains(query) } } ``` ### Disabled Items Return `true` from `disabled()` on items that should not be selectable: ```rust impl SearchableListItem for MyItem { // ... fn disabled(&self) -> bool { self.is_unavailable } } ``` ### Custom Check Icon ```rust Combobox::new(&state) .check_icon(Icon::new(IconName::CircleCheck)) ``` ### Footer Action Render a persistent action at the bottom of the dropdown (e.g. an "Add new" button): ```rust Combobox::new(&state) .footer(|_, cx| { Button::new("add-new") .ghost() .label("New item") .icon(Icon::new(IconName::Plus)) .w_full() .justify_start() .into_any_element() }) ``` ### Custom Trigger Override the entire trigger element. `ComboboxTriggerCtx` exposes the current selection, open/disabled flags, and size: ```rust Combobox::new(&state) .render_trigger(|ctx, _, cx| { h_flex() .w_full() .items_center() .gap_2() .when(ctx.selection.is_empty(), |this| { this.text_color(cx.theme().muted_foreground) .child("Select...") }) .children(ctx.selection.iter().map(|(_, item)| { div() .bg(cx.theme().accent) .rounded_sm() .px_1p5() .py_0p5() .text_sm() .child(item.title()) })) .into_any_element() }) ``` ### Sizes ```rust Combobox::new(&state).large() Combobox::new(&state) // medium (default) Combobox::new(&state).small() ``` ### Cleanable ```rust Combobox::new(&state).cleanable(true) // show clear button when a value is selected ``` ### Disabled ```rust Combobox::new(&state).disabled(true) ``` ### Events Both `Change` (fired on every toggle) and `Confirm` (fired when the dropdown closes) carry the full selection as `Vec`. ```rust cx.subscribe_in(&state, window, |view, _, event, window, cx| { match event { ComboboxEvent::Change(values) => { // fired on every toggle } ComboboxEvent::Confirm(values) => { // fired when the dropdown closes } } }); ``` ### Mutating Programmatically Values are resolved through the current delegate. Values that cannot be found are ignored. `set_selected_values` clears the search query first, so an active search never decides which values can be selected. Index paths address the list as it is currently displayed, so `set_selected_indices`, `add_selected_index` and `remove_selected_index` act on the visible rows and leave the query alone. ```rust // Replace the entire selection by value state.update(cx, |s, cx| { s.set_selected_values(&["React", "Angular"], window, cx); }); // Replace the entire selection by index path state.update(cx, |s, cx| { s.set_selected_indices(vec![IndexPath::new(0), IndexPath::new(2)], window, cx); }); // Add / remove individual items state.update(cx, |s, cx| { s.add_selected_index(IndexPath::new(1), cx); s.remove_selected_index(IndexPath::new(0), cx); }); // Clear all selections state.update(cx, |s, cx| { s.clear_selection(cx); }); // Read all selected values (multi-select) let values = state.read(cx).selected_values(); // Vec // Read the first selected value (single-select convenience) let value = state.read(cx).selected_value(); // Option ``` ## Keyboard Shortcuts | Key | Action | | --------- | ---------------------------------------- | | `Tab` | Focus trigger | | `Enter` | Open menu or confirm highlighted item | | `Up/Down` | Navigate options (opens menu if closed) | | `Escape` | Close menu | ## Theming * `background` — Dropdown input background * `input` — Trigger border color * `foreground` — Text color * `muted_foreground` — Placeholder and disabled text * `border` — Menu border * `radius` — Border radius --- --- url: /zh-CN/base/primitives/combobox.md description: 结合文本输入、键盘导航建议和选择行为的组合框。 --- # Combobox 结合文本输入、键盘导航建议和选择行为的组合框。 和所有 GPUI Base 原语一样,Combobox 只提供行为和语义结构,不规定产品视觉语言。请使用 GPUI 样式并组合导出的部件,使其符合你的设计系统。 ## 示例 原生示例和页面上方的 WASM 预览共用同一份实现: ```bash cargo run -p gpui-base --example components -- combobox ``` ## 导入 ```rust use gpui_kit::base::{Combobox}; ``` ## 结构与 API 示例组合上述公开类型。GPUI 的标准样式和事件 trait 负责表现,Base 类型负责交互结构。权威实现位于 [`components/combobox.rs`](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/components/combobox.rs),原生与浏览器预览编译的是同一文件。 ## 状态与事件 持久状态保存查询、候选项、焦点项和选择;输入与选择事件由应用处理。 受控状态应保存在父渲染类型或 GPUI entity 中;在回调中更新并调用 `cx.notify()`,不要在每次渲染时重建持久 entity。 ## 完整 Rust 示例 ## 可访问性 保留组合框语义、活动后代关系以及上下键、Enter 和 Escape 操作。 ## 注意事项 在支持的位置使用稳定元素 ID,并在消费端设计系统中验证焦点、悬停、按下、选中、禁用、减少动态效果和高对比度状态。 --- --- url: /zh-CN/docs/components/combobox.md description: 带有可搜索下拉列表的自动补全输入组件。 --- # Combobox 支持从可搜索列表中选择一个或多个值的下拉选择组件。 ## Select 与 Combobox 的区别 | 功能 | Select | Combobox | | --- | --- | --- | | 可搜索 | ✓(可选) | ✓(可选) | | 多选 | — | ✓(`.multiple(true)`) | | 自定义触发器渲染 | — | ✓ | | 自定义列表项渲染 | — | ✓ | | 底部操作插槽 | — | ✓ | 需要简单单选时用 `Select`;需要多选、完全自定义触发器或自定义列表项渲染时用 `Combobox`。 ## 导入 ```rust use gpui_kit::component::combobox::{ Combobox, ComboboxState, ComboboxEvent, ComboboxTriggerCtx, }; use gpui_kit::component::searchable_list::{ SearchableListItem, SearchableVec, SearchableGroup, }; ``` ## 用法 ### 基础单选 ```rust let state = cx.new(|cx| { ComboboxState::new( SearchableVec::new(vec!["Next.js", "SvelteKit", "Nuxt.js"]), vec![], // 无初始选中 window, cx, ) .searchable(true) }); Combobox::new(&state) .placeholder("选择框架...") .search_placeholder("搜索...") .w_full() ``` ### 多选 通过 `.multiple(true)` 开启多选模式。点击列表项会切换其选中状态,下拉菜单保持展开直到按下 Escape 或点击外部。 ```rust let state = cx.new(|cx| { ComboboxState::new( SearchableVec::new(vec!["React", "Vue", "Angular"]), vec![IndexPath::new(0)], // 预选项 window, cx, ) .multiple(true) .searchable(true) }); Combobox::new(&state).placeholder("选择框架") ``` ### 预选项 通过索引路径指定预选的列表项: ```rust let state = cx.new(|cx| { ComboboxState::new(items, vec![IndexPath::new(0)], window, cx) }); ``` ### 分组列表项 使用 `SearchableGroup` 对列表项进行分组: ```rust let grouped = SearchableVec::new(vec![ SearchableGroup::new("水果").items(vec![ FoodItem::new("苹果"), FoodItem::new("香蕉"), ]), SearchableGroup::new("蔬菜").items(vec![ FoodItem::new("胡萝卜"), FoodItem::new("菠菜"), ]), ]); let state = cx.new(|cx| { ComboboxState::new(grouped, vec![], window, cx).searchable(true) }); Combobox::new(&state) ``` ### 实现 `SearchableListItem` `String`、`SharedString` 和 `&'static str` 已内置实现了 `SearchableListItem`。自定义类型需手动实现该 trait: ```rust #[derive(Clone)] struct Country { name: SharedString, code: SharedString, } impl SearchableListItem for Country { type Value = SharedString; fn title(&self) -> SharedString { self.name.clone() } fn value(&self) -> &SharedString { &self.code } fn matches(&self, query: &str) -> bool { self.name.to_lowercase().contains(query) || self.code.to_lowercase().contains(query) } } ``` ### 禁用列表项 在列表项的 `disabled()` 方法中返回 `true` 即可将该项设为不可选: ```rust impl SearchableListItem for MyItem { // ... fn disabled(&self) -> bool { self.is_unavailable } } ``` ### 自定义勾选图标 ```rust Combobox::new(&state) .check_icon(Icon::new(IconName::CircleCheck)) ``` ### 底部操作按钮 在下拉菜单底部渲染一个固定操作项(如"新建"按钮): ```rust Combobox::new(&state) .footer(|_, cx| { Button::new("add-new") .ghost() .label("新建项目") .icon(Icon::new(IconName::Plus)) .w_full() .justify_start() .into_any_element() }) ``` ### 自定义触发器 完全覆盖触发器元素的渲染。`ComboboxTriggerCtx` 包含当前选中状态、开关标志和尺寸信息: ```rust Combobox::new(&state) .render_trigger(|ctx, _, cx| { h_flex() .w_full() .items_center() .gap_2() .when(ctx.selection.is_empty(), |this| { this.text_color(cx.theme().muted_foreground) .child("请选择...") }) .children(ctx.selection.iter().map(|(_, item)| { div() .bg(cx.theme().accent) .rounded_sm() .px_1p5() .py_0p5() .text_sm() .child(item.title()) })) .into_any_element() }) ``` ### 尺寸 ```rust Combobox::new(&state).large() Combobox::new(&state) // 默认(medium) Combobox::new(&state).small() ``` ### 可清除 ```rust Combobox::new(&state).cleanable(true) // 有选中值时显示清除按钮 ``` ### 禁用状态 ```rust Combobox::new(&state).disabled(true) ``` ### 事件监听 `Change`(每次切换时触发)和 `Confirm`(下拉菜单关闭时触发)均携带完整的选中值列表 `Vec`。 ```rust cx.subscribe_in(&state, window, |view, _, event, window, cx| { match event { ComboboxEvent::Change(values) => { // 每次切换时触发 } ComboboxEvent::Confirm(values) => { // 下拉菜单关闭时触发 } } }); ``` ### 程序化操控 值会通过当前 delegate 解析,无法找到的值会被忽略。 `set_selected_values` 会先清除搜索关键词,因此正在进行的搜索不会决定哪些值可以被选中。 index path 定位的是列表当前显示的内容,所以 `set_selected_indices`、`add_selected_index` 和 `remove_selected_index` 作用于可见行,不会改动搜索关键词。 ```rust // 按值替换整个选中集合 state.update(cx, |s, cx| { s.set_selected_values(&["React", "Angular"], window, cx); }); // 按索引路径替换整个选中集合 state.update(cx, |s, cx| { s.set_selected_indices(vec![IndexPath::new(0), IndexPath::new(2)], window, cx); }); // 增加 / 移除单个选项 state.update(cx, |s, cx| { s.add_selected_index(IndexPath::new(1), cx); s.remove_selected_index(IndexPath::new(0), cx); }); // 清空选中 state.update(cx, |s, cx| { s.clear_selection(cx); }); // 读取所有选中值(多选) let values = state.read(cx).selected_values(); // Vec // 读取第一个选中值(单选便利方法) let value = state.read(cx).selected_value(); // Option ``` ## 键盘快捷键 | 按键 | 操作 | | ---------- | -------------------------------- | | `Tab` | 聚焦触发器 | | `Enter` | 打开菜单或确认当前高亮项 | | `↑ / ↓` | 在选项间导航(未打开时自动打开) | | `Escape` | 关闭菜单 | ## 主题样式 * `background` — 触发器背景 * `input` — 触发器边框颜色 * `foreground` — 文字颜色 * `muted_foreground` — 占位符和禁用文字颜色 * `border` — 菜单边框颜色 * `radius` — 圆角 --- --- url: /docs/components/command.md description: A command palette — a filtered list of commands and quick actions. --- # Command A command palette is a filtered list of commands with groups, Action-derived keybinding hints, and keyboard navigation. Use it inline or compose it into an existing dialog for a `⌘K`-style menu. On invalidation, Command creates and layout-measures every flattened row; `v_virtual_list` then renders and paints only viewport rows. `Command` owns the entries and presentation policy. `CommandState` owns the interaction state: query input, focus, selection, scrolling, and loading. ## Import ```rust use gpui_kit::component::command::{Command, CommandEntry, CommandGroup, CommandItem, CommandState}; ``` ## Composition Build the palette structure directly on `Command`; create an empty state once and reuse it while the palette is shown. ```text Command ├── CommandItem // ungrouped ├── CommandGroup │ ├── CommandItem │ └── CommandItem ├── separator └── CommandGroup ├── CommandItem └── CommandItem CommandState // query, focus, selection, scrolling ``` ## Usage ### Inline Define Actions and bindings in application setup. The default row resolves an Action's active binding in the Command focus scope and then at application scope, rendering a `Kbd` hint only when it finds one. ```rust use gpui_kit::{actions, KeyBinding}; actions!(my_app, [OpenProfile, OpenBilling]); // During application setup: cx.bind_keys([ KeyBinding::new("cmd-p", OpenProfile, Some("Command")), KeyBinding::new("cmd-b", OpenBilling, Some("Command")), ]); let state = cx.new(|cx| CommandState::new(window, cx)); Command::new(&state) .group( CommandGroup::new().label("Suggestions") .item(CommandItem::new().label("Calendar").icon(IconName::Calendar)) .item(CommandItem::new().label("Search Emoji").icon(IconName::Search)) .item(CommandItem::new().label("Calculator").disabled(true)), ) .separator() .group( CommandGroup::new().label("Settings") .item( CommandItem::new().label("Profile") .icon(IconName::User) .action(Box::new(OpenProfile)), ) .item( CommandItem::new().label("Billing") .action(Box::new(OpenBilling)), ), ) .placeholder("Type a command or search...") .empty(|_, _, cx| { v_flex() .items_center() .gap_2() .child(Icon::new(IconName::Search).size_8()) .child("No results found.") }) .w(px(380.)) ``` Do not provide a manually formatted shortcut string. `CommandItem::action` provides both the executable behavior and, for the default row, the displayed binding. A custom row owns its complete presentation, including any key hint. ### Quick Actions Without Search Disable search for a compact action palette. It has no search field, retains all entries, and `state.focus(window, cx)` focuses the Command frame so its arrow, Enter, and Escape actions remain available. ```rust let actions = cx.new(|cx| CommandState::new(window, cx)); Command::new(&actions) .searchable(false) .items([ CommandItem::new().label("New File").icon(IconName::Plus), CommandItem::new().label("Duplicate").icon(IconName::Copy), CommandItem::new().label("Move to Trash").icon(IconName::Delete), ]) .w(px(380.)) ``` With the default `.searchable(true)`, `state.focus(window, cx)` and \[`Focusable::focus_handle`] target the search input instead. A non-searchable palette never invokes `on_query`. ### In a Dialog Compose the palette with the existing \[`WindowExt::open_dialog`] API. `header` renders above the optional search field and list; `footer` renders below the list. In a searchable palette, Escape clears a non-empty query. Otherwise— including a non-searchable palette with a hidden programmatic query—Command calls `on_cancel` and then propagates Cancel. Let the hosting Dialog perform dismissal—do not close it again in `on_cancel`. ```rust use gpui_kit::component::WindowExt as _; let state = self.command_state.clone(); window.open_dialog(cx, move |dialog, _, _| { let state = state.clone(); dialog.close_button(false).p_0().content(move |content, _, _| { content.child( Command::new(&state) .bordered(false) .placeholder("Type a command or search...") .items([ CommandItem::new().label("Profile"), CommandItem::new().label("Billing"), ]) .on_confirm(|index, window, cx| { window.push_notification(format!("Selected {index}"), cx); }) // Record local cleanup only; Dialog handles the propagated Cancel. .on_cancel(|window, cx| { window.push_notification("Command palette cancelled", cx); }) .header(|state, _, cx| { h_flex() .justify_between() .px_3() .py_2() .border_b_1() .border_color(cx.theme().border) .child("Commands") .child(format!("{} matches", state.matched_count())) }) .footer(|_, _, cx| { h_flex() .gap_3() .px_3() .py_2() .border_t_1() .border_color(cx.theme().border) .child("↑↓ Navigate") .child("Enter Select") .child("Escape Close") }), ) }) }); ``` ### Callbacks and Actions Callbacks are configured on `Command`, not subscribed from `CommandState`. They notify the palette owner directly: ```rust Command::new(&state) .items(entries) .on_query(|query, window, cx| { // Start or update an application-owned search. }) .on_select(|index, window, cx| { // Preview the newly highlighted IndexPath. }) .on_confirm(|index, window, cx| { // Finish with this IndexPath, whether or not it has an Action. }) .on_cancel(|window, cx| { // Clean up local palette state before Cancel propagates. }) ``` An `IndexPath` always addresses the model supplied by the latest `Command` render, before local filtering. Items passed to `.items(...)` are in section 0, with `row` equal to their position in that iterator. Explicit groups use their group and item positions; when both forms are mixed, they follow the implicit ungrouped section. Filtering changes what is visible, not these coordinates. `on_query` runs only when a searchable query actually changes. Refiltering can move the highlight, so its `on_select` runs first when the selected `IndexPath` changes; then `on_query` runs. These callbacks, and `on_confirm`, are delivered after the current `CommandState` update releases its lease. Keyboard and pointer highlight changes run `on_select` but never dispatch an Action. While the source window remains live, confirming an enabled item dispatches its Action first and then invokes `on_confirm`; if that Action closes the window, the callback cannot be delivered. An item without an Action still invokes `on_confirm`. In a searchable palette, Escape clears a non-empty query. Otherwise—including a non-searchable palette with a hidden programmatic query—it invokes `on_cancel`, then propagates Cancel. ### Dynamic Entries Keep asynchronous or changing entries in the owner view, then reconstruct the Command from the owner's current data when that view renders. Do not mutate the state with an entry builder or `set_entries`. ```rust struct StockSearch { state: Entity, results: Vec, } impl StockSearch { fn render_palette(&self, owner: WeakEntity) -> Command { let results = self.results.clone(); Command::new(&self.state) .items(results) .on_query(move |query, window, cx| { _ = owner.update(cx, |this, cx| this.search(query, window, cx)); }) } } ``` The installed model remains in `CommandState` while query, selection, and scrolling change, so those interactions do not need an owner rerender. A later owner render installs the new model, preserves the selected `IndexPath` when it is still present, and remeasures rows. ## Searching Command uses a case-insensitive substring match against each item's label and keywords. Empty queries match every item. A group whose items all filter out hides its heading; a separator left leading, trailing, or adjacent to another separator is omitted. ```rust CommandItem::new().label("Profile") .keywords(["account", "user"]) ``` For custom or remote search, update owner-held entries in `on_query` and call `state.set_loading(true, window, cx)` while waiting so the empty message is suppressed. Render the new entries when the response arrives. ## Custom Rows and Virtualization `CommandItem::child` replaces an item's icon and label content with a lazy child factory. The factory can run more than once for measurement, viewport entry, and typography or width invalidation, so it must be side-effect-free. On invalidation, Command creates and layout-measures every flattened row before supplying independent sizes to `v_virtual_list`. Custom rows may therefore have different intrinsic heights; `v_virtual_list` still renders and paints only viewport rows. Build them for the available list width and keep their rendered content stable until the owner updates the entries. ```rust Command::new(&state) .item(CommandItem::new().label("compact").child(|_, _| { h_flex().w_full().py_1().child("Compact custom row") })) .item(CommandItem::new().label("expanded").child(|_, cx| { v_flex() .w_full() .py_4() .child("Expanded custom row") .child(div().text_xs().text_color(cx.theme().muted_foreground).child("Extra detail")) })) ``` ## Command | Method | Signature and description | | --- | --- | | `new` | `new(&Entity) -> Command` creates a palette for a state. | | `item` / `items` | `item(CommandItem) -> Self` and `items(impl IntoIterator) -> Self` add ungrouped entries. | | `group` / `separator` | `group(CommandGroup) -> Self` adds a group; `separator() -> Self` adds a divider. | | `searchable` | `searchable(bool) -> Self` shows or hides the search field and local filtering. Default: `true`. | | `on_query` | `on_query(F) -> Self`, where `F: Fn(&str, &mut Window, &mut App) + 'static`, runs after a searchable query changes. | | `on_select` | `on_select(F) -> Self`, where `F: Fn(IndexPath, &mut Window, &mut App) + 'static`, runs when the highlighted path changes. | | `on_confirm` | `on_confirm(F) -> Self`, with the same `IndexPath` callback bounds; while the source window remains live, runs after the confirmed Action dispatches. | | `on_cancel` | `on_cancel(F) -> Self`, where `F: Fn(&mut Window, &mut App) + 'static`, runs before Cancel propagates when Escape does not clear a searchable query. | | `placeholder` | `placeholder(impl Into) -> Self` sets the search-field placeholder. | | `empty` | `empty(F) -> Self` renders custom content when there are no matches. | | `max_h` | `max_h(impl Into) -> Self` sets the list maximum. Default: `18.75rem` (300px). | | `bordered` | `bordered(bool) -> Self` draws the surrounding border and rounding. Default: `true`. | | `header` | `header(F) -> Self`, where `F: Fn(&CommandState, &mut Window, &mut App) -> E + 'static` and `E: IntoElement`; renders above search and list. | | `footer` | `footer(F) -> Self`, with the same callback bounds; renders below the list. | `Command` implements \[`Styled`], so `w`, `max_w`, `bg`, and other styles apply to the palette frame. ## CommandItem | Method | Description | | --- | --- | | `new` | Creates an item; Command generates its internal rendering identity. | | `label` | Sets the visible label and default search text. | | `icon` | Sets the leading icon for the default row. | | `action` | `action(Box) -> Self` sets the behavior dispatched on click or confirm. The default row displays its resolved binding. | | `checked` | Draws a trailing check. A resolved Action binding uses that position instead. | | `keywords` | Adds default-match terms. | | `disabled` | `Disableable::disabled(bool) -> Self` makes the item non-interactive and skips it during keyboard navigation. | | `child` | `child(F) -> Self`, where `F: Fn(&mut Window, &mut App) -> E + 'static` and `E: IntoElement`; lazily replaces the default row content. | ## CommandGroup | Method | Description | | --- | --- | | `new` | Creates an unlabeled group. | | `label` | Sets the group heading, which hides when all items filter out. | | `item` / `items` | Add one or many `CommandItem`s to the group. | | `heading` | Returns the optional heading. | `CommandEntry` is the public enum for an item, group, or separator. It is useful when an owner stores a mixed dynamic entry collection; replay each variant onto a newly constructed `Command` during rendering. ## CommandState | Method | Signature and description | | --- | --- | | `new` | `new(&mut Window, &mut Context) -> Self` creates empty interaction state. | | `query` / `set_query` | Read the query, or `set_query(query, window, cx)` as if it were typed. | | `selected_index` | Returns the highlighted item's original `IndexPath`; section identifies the top-level entry and row identifies the item within a group. | | `matched_count` | Returns the number of matching items. | | `focus` | `focus(&self, &mut Window, &mut App)` focuses the input when searchable, otherwise the Command frame. | | `set_loading` / `is_loading` | Show or read the search spinner; loading suppresses the empty message. | ## Keyboard Shortcuts | Key | Action | | --- | --- | | `↑` / `↓` | Move the highlight, wrapping around and skipping disabled items. | | `Enter` | Confirm the highlighted item. | | `Escape` | In a searchable palette, clear a non-empty query; otherwise call `on_cancel` and propagate `Cancel`. | ## Best Practices 1. Build static entries, groups, separators, searchability, and filters on `Command`. 2. Keep dynamic entries and asynchronous results in the palette owner; rebuild `Command` from them when rendering. 3. Bind real `Action`s instead of supplying shortcut text, so hints and dispatch stay in sync. 4. Keep `child` factories side-effect-free and use them for rows that need custom presentation or variable heights. 5. Let a hosting Dialog own cancellation after `on_cancel`; use header and footer for application-owned status and hints. 6. Give each independently rendered palette its own \[`CommandState`]. [Command]: https://docs.rs/gpui-component/latest/gpui_component/command/struct.Command.html [CommandState]: https://docs.rs/gpui-component/latest/gpui_component/command/struct.CommandState.html [CommandGroup]: https://docs.rs/gpui-component/latest/gpui_component/command/struct.CommandGroup.html [WindowExt::open_dialog]: https://docs.rs/gpui-component/latest/gpui_component/trait.WindowExt.html#tymethod.open_dialog [Focusable::focus_handle]: https://docs.rs/gpui/latest/gpui/trait.Focusable.html#tymethod.focus_handle [Styled]: https://docs.rs/gpui/latest/gpui/trait.Styled.html --- --- url: /zh-CN/docs/components/command.md description: 命令面板 —— 经过过滤的命令与快捷操作列表。 --- # Command 命令面板是带有分组、由 Action 派生的快捷键提示和键盘导航的命令过滤列表。可以内嵌使用,也可以组合到现有对话框中,作为 `⌘K` 风格的菜单。失效时,Command 会创建并布局测量每一条扁平化的行;随后 `v_virtual_list` 只渲染和绘制视口行。 `Command` 拥有条目和展示策略。`CommandState` 拥有交互状态:搜索输入、焦点、选择、滚动和加载状态。 ## 引入 ```rust use gpui_kit::component::command::{Command, CommandEntry, CommandGroup, CommandItem, CommandState}; ``` ## 组合方式 直接在 `Command` 上构建面板结构;创建一个空状态,并在面板显示期间复用它。 ```text Command ├── CommandItem // 未分组 ├── CommandGroup │ ├── CommandItem │ └── CommandItem ├── separator └── CommandGroup ├── CommandItem └── CommandItem CommandState // 查询、焦点、选择、滚动 ``` ## 用法 ### 内嵌 在应用初始化时定义 Action 和绑定。默认行会先在 Command 焦点作用域、再在应用作用域解析 Action 的当前绑定;只有找到绑定时才渲染 `Kbd` 提示。 ```rust use gpui_kit::{actions, KeyBinding}; actions!(my_app, [OpenProfile, OpenBilling]); // During application setup: cx.bind_keys([ KeyBinding::new("cmd-p", OpenProfile, Some("Command")), KeyBinding::new("cmd-b", OpenBilling, Some("Command")), ]); let state = cx.new(|cx| CommandState::new(window, cx)); Command::new(&state) .group( CommandGroup::new().label("Suggestions") .item(CommandItem::new().label("Calendar").icon(IconName::Calendar)) .item(CommandItem::new().label("Search Emoji").icon(IconName::Search)) .item(CommandItem::new().label("Calculator").disabled(true)), ) .separator() .group( CommandGroup::new().label("Settings") .item( CommandItem::new().label("Profile") .icon(IconName::User) .action(Box::new(OpenProfile)), ) .item( CommandItem::new().label("Billing") .action(Box::new(OpenBilling)), ), ) .placeholder("Type a command or search...") .empty(|_, _, cx| { v_flex() .items_center() .gap_2() .child(Icon::new(IconName::Search).size_8()) .child("No results found.") }) .w(px(380.)) ``` 不要提供手工格式化的快捷键字符串。`CommandItem::action` 同时提供可执行行为,并为默认行提供显示的绑定。自定义行拥有完整的展示内容,包括任何按键提示。 ### 无搜索的快捷操作 为紧凑的操作面板关闭搜索。它没有搜索框,保留全部条目,并且 `state.focus(window, cx)` 会聚焦 Command 外框,因此仍可使用方向键、Enter 和 Escape 操作。 ```rust let actions = cx.new(|cx| CommandState::new(window, cx)); Command::new(&actions) .searchable(false) .items([ CommandItem::new().label("New File").icon(IconName::Plus), CommandItem::new().label("Duplicate").icon(IconName::Copy), CommandItem::new().label("Move to Trash").icon(IconName::Delete), ]) .w(px(380.)) ``` 默认的 `.searchable(true)` 下,`state.focus(window, cx)` 和 \[`Focusable::focus_handle`] 会改为聚焦搜索输入框。不可搜索的面板不会调用 `on_query`。 ### 在对话框中 使用现有的 \[`WindowExt::open_dialog`] API 组合命令面板。`header` 渲染在可选搜索框和列表之上;`footer` 渲染在列表之下。在可搜索面板中,Escape 会清空非空查询。否则——包括具有隐藏的程序化查询的不可搜索面板——Command 会调用 `on_cancel`,然后传播 Cancel。应由宿主 Dialog 完成关闭——不要在 `on_cancel` 中再次关闭它。 ```rust use gpui_kit::component::WindowExt as _; let state = self.command_state.clone(); window.open_dialog(cx, move |dialog, _, _| { let state = state.clone(); dialog.close_button(false).p_0().content(move |content, _, _| { content.child( Command::new(&state) .bordered(false) .placeholder("Type a command or search...") .items([ CommandItem::new().label("Profile"), CommandItem::new().label("Billing"), ]) .on_confirm(|index, window, cx| { window.push_notification(format!("Selected {index}"), cx); }) // Record local cleanup only; Dialog handles the propagated Cancel. .on_cancel(|window, cx| { window.push_notification("Command palette cancelled", cx); }) .header(|state, _, cx| { h_flex() .justify_between() .px_3() .py_2() .border_b_1() .border_color(cx.theme().border) .child("Commands") .child(format!("{} matches", state.matched_count())) }) .footer(|_, _, cx| { h_flex() .gap_3() .px_3() .py_2() .border_t_1() .border_color(cx.theme().border) .child("↑↓ Navigate") .child("Enter Select") .child("Escape Close") }), ) }) }); ``` ### 回调与 Action 回调配置在 `Command` 上,而不是从 `CommandState` 订阅。它们直接通知面板所有者: ```rust Command::new(&state) .items(entries) .on_query(|query, window, cx| { // Start or update an application-owned search. }) .on_select(|index, window, cx| { // Preview the newly highlighted IndexPath. }) .on_confirm(|index, window, cx| { // Finish with this IndexPath, whether or not it has an Action. }) .on_cancel(|window, cx| { // Clean up local palette state before Cancel propagates. }) ``` `IndexPath` 始终对应最近一次 `Command` render 传入的模型,而不是内部过滤后的可见位置。 通过 `.items(...)` 传入的条目位于 section 0,`row` 等于它在该迭代器中的位置; 显式 group 使用其 group 与 item 位置;两种形式混用时,显式 group 排在隐式未分组 section 之后。 搜索过滤只改变可见内容,不改变这些坐标。 `on_query` 只在可搜索查询实际变化时运行。重新过滤可能移动高亮,因此所选 `IndexPath` 变化时会先运行 `on_select`,再运行 `on_query`。这些回调与 `on_confirm` 都会在当前 `CommandState` 更新释放其租用后交付。键盘和指针导致的高亮变化会运行 `on_select`,但从不分发 Action。只要来源窗口仍然存活,确认已启用条目时,会先分发其 Action,再调用 `on_confirm`;如果该 Action 关闭窗口,回调将无法交付。没有 Action 的条目仍会调用 `on_confirm`。在可搜索面板中,Escape 会先清空非空查询;否则调用 `on_cancel` 并继续传播 Cancel。 ### 动态条目 将异步或变化的条目保存在所有者视图中,然后在该视图渲染时,根据所有者的当前数据重新构建 Command。不要通过条目构建器或 `set_entries` 修改 state。 ```rust struct StockSearch { state: Entity, results: Vec, } impl StockSearch { fn render_palette(&self, owner: WeakEntity) -> Command { let results = self.results.clone(); Command::new(&self.state) .items(results) .on_query(move |query, window, cx| { _ = owner.update(cx, |this, cx| this.search(query, window, cx)); }) } } ``` 当查询、选择和滚动改变时,已安装的模型会保留在 `CommandState` 中,因此这些交互不需要重新渲染所有者。之后的所有者渲染会安装新模型;若所选 `IndexPath` 仍存在则保留选择,并重新测量行。 ## 搜索 Command 默认在条目的 label 和 keywords 中进行忽略大小写的子串匹配。空查询会匹配全部条目。分组中的条目全被过滤时,其标题会隐藏;过滤后位于首尾或相邻的分隔线不会显示。 ```rust CommandItem::new().label("Profile") .keywords(["account", "user"]) ``` 自定义或远程搜索时,在 `on_query` 中更新所有者持有的条目,并在等待时调用 `state.set_loading(true, window, cx)`,以隐藏空状态文案。响应到达后渲染新条目。 ## 自定义行与虚拟滚动 `CommandItem::child` 会用惰性子元素工厂替换条目的图标和 label 内容。该工厂可能因测量、进入视口、排版或宽度失效而多次运行,因此必须无副作用。 失效时,Command 会在向 `v_virtual_list` 提供独立尺寸前创建并布局测量每一条扁平化的行。因此,自定义行可以拥有不同的固有高度;`v_virtual_list` 仍只渲染和绘制视口行。应按列表可用宽度构建行,并在所有者更新条目前保持其渲染内容稳定。 ```rust Command::new(&state) .item(CommandItem::new().label("compact").child(|_, _| { h_flex().w_full().py_1().child("Compact custom row") })) .item(CommandItem::new().label("expanded").child(|_, cx| { v_flex() .w_full() .py_4() .child("Expanded custom row") .child(div().text_xs().text_color(cx.theme().muted_foreground).child("Extra detail")) })) ``` ## Command | 方法 | 签名与说明 | | --- | --- | | `new` | `new(&Entity) -> Command` 为 state 创建面板。 | | `item` / `items` | `item(CommandItem) -> Self` 与 `items(impl IntoIterator) -> Self` 添加未分组条目。 | | `group` / `separator` | `group(CommandGroup) -> Self` 添加分组;`separator() -> Self` 添加分隔线。 | | `searchable` | `searchable(bool) -> Self` 显示或隐藏搜索框和本地过滤。默认:`true`。 | | `on_query` | `on_query(F) -> Self`,其中 `F: Fn(&str, &mut Window, &mut App) + 'static`,在可搜索查询变化后运行。 | | `on_select` | `on_select(F) -> Self`,其中 `F: Fn(IndexPath, &mut Window, &mut App) + 'static`,在高亮路径变化时运行。 | | `on_confirm` | `on_confirm(F) -> Self`,使用相同的 `IndexPath` 回调约束;在确认的 Action 分发后运行。 | | `on_cancel` | `on_cancel(F) -> Self`,其中 `F: Fn(&mut Window, &mut App) + 'static`,在 Escape 不会清空可搜索查询时,于 Cancel 传播前运行。 | | `placeholder` | `placeholder(impl Into) -> Self` 设置搜索框占位文本。 | | `empty` | `empty(F) -> Self` 渲染无匹配时的自定义内容。 | | `max_h` | `max_h(impl Into) -> Self` 设置列表最大高度。默认:`18.75rem`(300px)。 | | `bordered` | `bordered(bool) -> Self` 绘制外边框和圆角。默认:`true`。 | | `header` | `header(F) -> Self`,其中 `F: Fn(&CommandState, &mut Window, &mut App) -> E + 'static`、`E: IntoElement`;渲染在搜索框和列表之上。 | | `footer` | `footer(F) -> Self`,使用相同的回调约束;渲染在列表之下。 | `Command` 实现了 \[`Styled`],因此 `w`、`max_w`、`bg` 和其他样式可作用于面板外框。 ## CommandItem | 方法 | 说明 | | --- | --- | | `new` | 创建条目;Command 在内部生成渲染 identity。 | | `label` | 设置可见 label 和默认搜索文本。 | | `icon` | 为默认行设置前置图标。 | | `action` | `action(Box) -> Self` 设置点击或确认时分发的行为。默认行会显示其解析后的绑定。 | | `checked` | 绘制尾部勾选。解析后的 Action 绑定会占用该位置。 | | `keywords` | 添加默认匹配词。 | | `disabled` | `Disableable::disabled(bool) -> Self` 使条目不可交互,并在键盘导航时跳过。 | | `child` | `child(F) -> Self`,其中 `F: Fn(&mut Window, &mut App) -> E + 'static`、`E: IntoElement`;惰性替换默认行内容。 | ## CommandGroup | 方法 | 说明 | | --- | --- | | `new` | 创建无标题分组。 | | `label` | 设置分组标题;所有条目被过滤时标题隐藏。 | | `item` / `items` | 向分组添加一个或多个 `CommandItem`。 | | `heading` | 返回可选标题。 | `CommandEntry` 是 item、group 或 separator 的公共枚举。当所有者保存混合的动态条目集合时很有用;渲染时应将每个变体重新应用到新构建的 `Command`。 ## CommandState | 方法 | 签名与说明 | | --- | --- | | `new` | `new(&mut Window, &mut Context) -> Self` 创建空的交互状态。 | | `query` / `set_query` | 读取查询,或通过 `set_query(query, window, cx)` 模拟输入。 | | `selected_index` | 返回高亮条目在原始 entries 中的 `IndexPath`;section 表示顶层 entry,row 表示分组内条目。 | | `matched_count` | 返回匹配条目数。 | | `focus` | `focus(&self, &mut Window, &mut App)`:可搜索时聚焦输入框,否则聚焦 Command 外框。 | | `set_loading` / `is_loading` | 显示或读取搜索加载动画;加载时隐藏空状态文案。 | ## 键盘快捷键 | 按键 | 行为 | | --- | --- | | `↑` / `↓` | 移动高亮,循环并跳过禁用项。 | | `Enter` | 确认当前高亮项。 | | `Escape` | 在可搜索面板中清空非空查询;否则调用 `on_cancel` 并传播 `Cancel`。 | ## 最佳实践 1. 在 `Command` 上构建静态条目、分组、分隔线、搜索能力和过滤器。 2. 将动态条目和异步结果保存在面板所有者中;渲染时据此重新构建 `Command`。 3. 绑定真实的 `Action`,而不是提供快捷键文本,以使提示和分发保持同步。 4. 保持 `child` 工厂无副作用;当行需要自定义展示或可变高度时使用它。 5. 在 `on_cancel` 后让宿主 Dialog 拥有取消行为;使用 header 和 footer 承载应用自有的状态和提示。 6. 每个独立渲染的面板使用各自的 \[`CommandState`]。 [Command]: https://docs.rs/gpui-component/latest/gpui_component/command/struct.Command.html [CommandState]: https://docs.rs/gpui-component/latest/gpui_component/command/struct.CommandState.html [CommandGroup]: https://docs.rs/gpui-component/latest/gpui_component/command/struct.CommandGroup.html [WindowExt::open_dialog]: https://docs.rs/gpui-component/latest/gpui_component/trait.WindowExt.html#tymethod.open_dialog [Focusable::focus_handle]: https://docs.rs/gpui/latest/gpui/trait.Focusable.html#tymethod.focus_handle [Styled]: https://docs.rs/gpui/latest/gpui/trait.Styled.html --- --- url: /docs/comparison.md description: 'How GPUI Kit compares with Iced, egui and Qt 6.' --- # Comparison How GPUI Kit compares with other desktop UI frameworks. The table is maintained by hand; please open an issue or a pull request if you spot a mistake or something outdated. | Features | GPUI Kit | [Iced] | [egui] | [Qt 6] | | --------------------- | ------------------------------ | ------------------ | --------------------- | ------------------------------------------------- | | Language | Rust | Rust | Rust | C++/QML | | Core Render | GPUI | wgpu | wgpu | QT | | License | Apache 2.0 | MIT | MIT/Apache 2.0 | [Commercial/LGPL](https://www.qt.io/qt-licensing) | | Min Binary Size \[^1] | 12MB | 11MB | 5M | 20MB \[^2] | | Cross-Platform | Yes | Yes | Yes | Yes | | Documentation | Simple | Simple | Simple | Good | | Web | Yes (WASM) | Yes | Yes | Yes | | UI Style | Modern | Basic | Basic | Basic | | CJK Support | Yes | Yes | Bad | Yes | | Chart | Yes | No | No | Yes | | Table (Large dataset) | Yes(Virtual Rows, Columns) | No | Yes(Virtual Rows) | Yes(Virtual Rows, Columns) | | Table Column Resize | Yes | No | Yes | Yes | | Text base | Rope | [COSMIC Text] \[^3] | trait TextBuffer \[^4] | [QTextDocument] | | CodeEditor | Simple | Simple | Simple | Basic API | | Dock Layout | Yes | Yes | Yes | Yes | | Syntax Highlight | [Tree Sitter] | [Syntect] | [Syntect] | [QSyntaxHighlighter] | | Markdown Rendering | Yes | Yes | Basic | No | | Markdown mix HTML | Yes | No | No | No | | HTML Rendering | Basic | No | No | Basic | | Text Selection | TextView | No | Any Label | Yes | | Custom Theme | Yes | Yes | Yes | Yes | | Built Themes | Yes | No | No | No | | I18n | Yes | Yes | Yes | Yes | [Iced]: https://github.com/iced-rs/iced [egui]: https://github.com/emilk/egui [QT 6]: https://www.qt.io/product/qt6 [Tree Sitter]: https://tree-sitter.github.io/tree-sitter/ [Syntect]: https://github.com/trishume/syntect [QSyntaxHighlighter]: https://doc.qt.io/qt-6/qsyntaxhighlighter.html [QTextDocument]: https://doc.qt.io/qt-6/qtextdocument.html [COSMIC Text]: https://github.com/pop-os/cosmic-text \[^1]: Release builds by use simple hello world example. \[^2]: [Reducing Binary Size of Qt Applications](https://www.qt.io/blog/reducing-binary-size-of-qt-applications-part-3-more-platforms) \[^3]: Iced Editor: \[^4]: egui TextBuffer: --- --- url: /docs/components.md --- # Components ### Basic Components * [Accordion](accordion) - Collapsible content panels * [Alert](alert) - Alert messages with different variants * [Attachment](attachment) - File and media attachment surfaces * [Avatar](avatar) - User avatars with fallback text * [Badge](badge) - Count badges and indicators * [Bubble](bubble) - Chat message surface with alignment and reactions * [Button](button) - Interactive buttons with multiple variants * [Checkbox](checkbox) - Binary selection control * [Collapsible](collapsible) - Expandable/collapsible content * [DropdownButton](dropdown_button) - Button with dropdown menu * [Icon](icon) - Icon display component * [Image](image) - Image display with fallbacks * [Kbd](kbd) - Keyboard shortcut display * [Label](label) - Text labels for form elements * [Marker](marker) - Conversation status and separator marker * [Message](message) - Composable chat message structure * [MessageScroller](message-scroller) - Tail-following virtualized message list * [Pagination](pagination) - Page navigation controls * [Progress](progress) - Progress bars * [Radio](radio) - Single selection from multiple options * [Rating](rating) - Interactive star rating component * [Skeleton](skeleton) - Loading placeholders * [Slider](slider) - Value selection from a range * [Spinner](spinner) - Loading and status spinners * [Stepper](stepper) - Step-by-step progress indicator * [Switch](switch) - Toggle on/off control * [Tag](tag) - Labels and categories * [TextView](text-view) - Markdown and HTML text rendering * [Toggle](toggle) - Toggle button states * [Tooltip](tooltip) - Helpful hints on hover ### Form Components * [Input](input) - An input field or a component that looks like an input field. * [Textarea](textarea) - Multi-line text with fixed rows or auto-grow. * [Editor](editor) - Source-code editing with highlighting, gutter, and folding. * [Select](select) - A list of options for the user to pick. * [Combobox](combobox) - Searchable single-select or multi-select dropdown. * [NumberInput](number-input) - Numeric input with increment/decrement * [DatePicker](date-picker) - Date selection with calendar * [OtpInput](otp-input) - One-time password input * [ColorPicker](color-picker) - Color selection interface * [Form](form) - Form container and layout ### Layout Components * [DescriptionList](description-list) - Key-value pair display * [GroupBox](group-box) - Grouped content with borders * [Dialog](dialog) - Dialog and modal windows * [Notification](notification) - Toast notifications * [Popover](popover) - Floating content display * [Resizable](resizable) - Resizable panels and containers * [Scrollable](scrollable) - Scrollable containers * [Sheet](sheet) - Slide-in panel from edges * [Sidebar](sidebar) - Navigation sidebar * [StatusBar](status-bar) - Bottom status bar with left/center/right regions ### Advanced Components * [Calendar](calendar) - Calendar display and navigation * [Command](command) - Command palette for search and quick actions * [Chart](chart) - Data visualization charts (Line, Bar, Area, Pie, Candlestick) * [List](list) - List display with items * [Menu](menu) - Menu and context menu and dropdown menu. * [Settings](settings) - Settings UI * [DataTable](data-table) - High-performance data tables * [Dock](/docs/dock) - Production-ready dock layouts with tabs, splits, and persistent state * [Tabs](tabs) - Tabbed interface * [Tree](tree) - Hierarchical tree data display * [VirtualList](virtual-list) - Virtualized list for large datasets --- --- url: /docs/context.md description: Learn about the Window and Context in GPUI. --- # Context The [Window], [App], [Context] and [Entity] are most important things in GPUI, it appears everywhere. * [Window] - The current window instance, which for handle the **Window Level** things. * [App] - The current application instance, which for handle the **Application Level** things. * [Context] - The Entity Context instance, which for handle the **Context Level** things. * [Entity] - The Entity instance, which for handle the **Entity Level** things. For example: ```rs fn new(window: &mut Window, cx: &mut App) {} impl RenderOnce for MyElement { fn render(self, window: &mut Window, cx: &mut App) {} } impl Render for MyView { fn render(&mut self, window: &mut Window, cx: &mut Context) {} } ``` :::info As you can see, we always use `cx` to represent `App` and `Context`, which is the standard naming convention for GPUI, we can follow this convention to make our code more readable and maintainable. ::: [Window]: https://docs.rs/gpui/latest/gpui/struct.Window.html [App]: https://docs.rs/gpui/latest/gpui/struct.App.html [Context]: https://docs.rs/gpui/latest/gpui/struct.Context.html [Entity]: https://docs.rs/gpui/latest/gpui/struct.Entity.html --- --- url: /zh-CN/docs/context.md description: 了解 GPUI 中的 Window 和 Context。 --- # Context 在 GPUI 中,[Window]、[App]、[Context] 和 [Entity] 是最常见、也最重要的几个核心概念。 * [Window] - 当前窗口实例,负责处理 **窗口级** 行为 * [App] - 当前应用实例,负责处理 **应用级** 行为 * [Context] - 某个 Entity 的 `Context` 实例,负责处理 **Context 级** 行为 * [Entity] - 某个实体本身,负责处理 **实体级** 状态和逻辑 例如: ```rs fn new(window: &mut Window, cx: &mut App) {} impl RenderOnce for MyElement { fn render(self, window: &mut Window, cx: &mut App) {} } impl Render for MyView { fn render(&mut self, window: &mut Window, cx: &mut Context) {} } ``` :::info 可以看到,在 GPUI 里通常都会用 `cx` 来表示 `App` 或 `Context`。 这是 GPUI 里约定俗成的命名习惯,继续沿用这个写法会让代码更统一,也更容易阅读。 ::: [Window]: https://docs.rs/gpui/latest/gpui/struct.Window.html [App]: https://docs.rs/gpui/latest/gpui/struct.App.html [Context]: https://docs.rs/gpui/latest/gpui/struct.Context.html [Entity]: https://docs.rs/gpui/latest/gpui/struct.Entity.html --- --- url: /docs/components/data-table.md description: >- High-performance data table with virtual scrolling, sorting, filtering, and column management. --- # Data Table A comprehensive data table component designed for handling large datasets with high performance. Features virtual scrolling, column configuration, sorting, filtering, row/column/cell selection, and custom cell rendering. Perfect for displaying tabular data with thousands of rows while maintaining smooth performance. ## Key Features * **Multiple Selection Modes**: Row, column, and individual cell selection * **Virtual Scrolling**: Handle thousands of rows with smooth performance * **Column Management**: Resizable, movable, and fixed columns * **Sorting**: Built-in column sorting support * **Keyboard Navigation**: Full keyboard support for all selection modes * **Custom Cell Rendering**: Render any content in table cells * **Context Menus**: Right-click support for rows and cells * **Infinite Loading**: Load more data as user scrolls * **Events**: Comprehensive event system for user interactions ## Import ```rust use gpui_kit::component::table::{ DataTable, TableState, TableDelegate, Column, ColumnSort, ColumnFixed, TableEvent }; ``` ## Usage ### Basic Table To create a table, you need to implement the `TableDelegate` trait and provide column definitions, and use `TableState` to manage the table state. ```rust use std::ops::Range; use gpui_kit::{App, Context, Window, IntoElement}; use gpui_kit::component::table::{DataTable, TableDelegate, Column, ColumnSort}; struct MyData { id: usize, name: String, age: u32, email: String, } struct MyTableDelegate { data: Vec, columns: Vec, } impl MyTableDelegate { fn new() -> Self { Self { data: vec![ MyData { id: 1, name: "John".to_string(), age: 30, email: "john@example.com".to_string() }, MyData { id: 2, name: "Jane".to_string(), age: 25, email: "jane@example.com".to_string() }, ], columns: vec![ Column::new("id", "ID").width(60.), Column::new("name", "Name").width(150.).sortable(), Column::new("age", "Age").width(80.).sortable(), Column::new("email", "Email").width(200.), ], } } } impl TableDelegate for MyTableDelegate { fn columns_count(&self, _: &App) -> usize { self.columns.len() } fn rows_count(&self, _: &App) -> usize { self.data.len() } fn column(&self, col_ix: usize, _: &App) -> Column { self.columns[col_ix].clone() } fn render_td(&mut self, row_ix: usize, col_ix: usize, _: &mut Window, _: &mut Context>) -> impl IntoElement { let row = &self.data[row_ix]; let col = &self.columns[col_ix]; match col.key.as_ref() { "id" => row.id.to_string(), "name" => row.name.clone(), "age" => row.age.to_string(), "email" => row.email.clone(), _ => "".to_string(), } } } // Create the table let delegate = MyTableDelegate::new(); let state = cx.new(|cx| TableState::new(delegate, window, cx)); ``` ### Column Configuration Columns provide extensive configuration options: ```rust // Basic column Column::new("id", "ID") // Sortable column Column::new("name", "Name") .sortable() .width(150.) // Right-aligned column Column::new("price", "Price") .text_right() .sortable() // Fixed column (pinned to left) Column::new("actions", "Actions") .fixed(ColumnFixed::Left) .resizable(false) .movable(false) // Column with custom padding Column::new("description", "Description") .width(200.) .paddings(px(8.)) // Non-resizable column Column::new("status", "Status") .width(100.) .resizable(false) // Custom sort orders Column::new("created", "Created") .ascending() // Default ascending // or Column::new("modified", "Modified") .descending() // Default descending ``` ### Virtual Scrolling for Large Datasets The table automatically handles virtual scrolling for optimal performance: ```rust struct LargeDataDelegate { data: Vec, // Could be 10,000+ items columns: Vec, } impl TableDelegate for LargeDataDelegate { fn rows_count(&self, _: &App) -> usize { self.data.len() // No performance impact regardless of size } // Only visible rows are rendered fn render_td(&mut self, row_ix: usize, col_ix: usize, _: &mut Window, _: &mut Context>) -> impl IntoElement { // This is only called for visible rows // Efficiently render cell content let row = &self.data[row_ix]; format_cell_data(row, col_ix) } // Track visible range for optimizations fn visible_rows_changed(&mut self, visible_range: Range, _: &mut Window, _: &mut Context>) { // Only update data for visible rows if needed // This is called when user scrolls } } ``` ### Sorting Implementation Implement sorting in your delegate: ```rust impl TableDelegate for MyTableDelegate { fn perform_sort(&mut self, col_ix: usize, sort: ColumnSort, _: &mut Window, _: &mut Context>) { let col = &self.columns[col_ix]; match col.key.as_ref() { "name" => { match sort { ColumnSort::Ascending => self.data.sort_by(|a, b| a.name.cmp(&b.name)), ColumnSort::Descending => self.data.sort_by(|a, b| b.name.cmp(&a.name)), ColumnSort::Default => { // Reset to original order or default sort self.data.sort_by(|a, b| a.id.cmp(&b.id)); } } } "age" => { match sort { ColumnSort::Ascending => self.data.sort_by(|a, b| a.age.cmp(&b.age)), ColumnSort::Descending => self.data.sort_by(|a, b| b.age.cmp(&a.age)), ColumnSort::Default => self.data.sort_by(|a, b| a.id.cmp(&b.id)), } } _ => {} } } } ``` ### ContextMenu ```rust impl TableDelegate for MyTableDelegate { // Context menu for right-click fn context_menu(&mut self, row_ix: usize, menu: PopupMenu, _: &mut Window, _: &mut Context>) -> PopupMenu { let row = &self.data[row_ix]; menu.menu(format!("Edit {}", row.name), Box::new(EditRowAction(row_ix))) .menu("Delete", Box::new(DeleteRowAction(row_ix))) .separator() .menu("Duplicate", Box::new(DuplicateRowAction(row_ix))) } } ``` ### Cell Rendering Create rich cell content with custom rendering: ```rust impl TableDelegate for MyTableDelegate { fn render_td(&mut self, row_ix: usize, col_ix: usize, _: &mut Window, cx: &mut Context>) -> impl IntoElement { let row = &self.data[row_ix]; let col = &self.columns[col_ix]; match col.key.as_ref() { "status" => { // Custom status badge let (color, text) = match row.status { Status::Active => (cx.theme().green, "Active"), Status::Inactive => (cx.theme().red, "Inactive"), Status::Pending => (cx.theme().yellow, "Pending"), }; div() .px_2() .py_1() .rounded(px(4.)) .bg(color.opacity(0.1)) .text_color(color) .child(text) } "progress" => { // Progress bar div() .w_full() .h(px(8.)) .bg(cx.theme().muted) .rounded(px(4.)) .child( div() .h_full() .w(percentage(row.progress)) .bg(cx.theme().primary) .rounded(px(4.)) ) } "actions" => { // Action buttons h_flex() .gap_1() .child(Button::new(format!("edit-{}", row_ix)).text().icon(IconName::Edit)) .child(Button::new(format!("delete-{}", row_ix)).text().icon(IconName::Trash)) } "avatar" => { // User avatar with image h_flex() .items_center() .gap_2() .child( div() .w(px(32.)) .h(px(32.)) .rounded_full() .bg(cx.theme().accent) .flex() .items_center() .justify_center() .child(row.name.chars().next().unwrap_or('?').to_string()) ) .child(row.name.clone()) } _ => row.get_field_value(col.key.as_ref()).into_any_element(), } } } ``` ### Selection Modes The table supports three distinct selection modes: ```rust // Row selection mode (default) let state = cx.new(|cx| { TableState::new(delegate, window, cx) .row_selectable(true) // Enable row selection .col_selectable(false) .cell_selectable(false) }); // Column selection mode let state = cx.new(|cx| { TableState::new(delegate, window, cx) .row_selectable(false) .col_selectable(true) // Enable column selection .cell_selectable(false) }); // Cell selection mode let state = cx.new(|cx| { TableState::new(delegate, window, cx) .row_selectable(true) // Keep row selection for row selector column .col_selectable(false) .cell_selectable(true) // Enable cell selection }); ``` ### Column Resizing and Moving Enable dynamic column management: ```rust // Configure table features let state = cx.new(|cx| { TableState::new(delegate, window, cx) .col_resizable(true) // Allow column resizing .col_movable(true) // Allow column reordering .sortable(true) // Enable sorting .col_selectable(true) // Allow column selection .row_selectable(true) // Allow row selection }); // Listen for column changes cx.subscribe_in(&state, window, |view, table, event, _, cx| { match event { TableEvent::ColumnWidthsChanged(widths) => { // Save column widths to user preferences save_column_widths(widths); } TableEvent::MoveColumn(from_ix, to_ix) => { // Save column order save_column_order(from_ix, to_ix); } _ => {} } }).detach(); ``` ### Infinite Loading / Pagination Implement loading more data as user scrolls: ```rust impl TableDelegate for MyTableDelegate { fn has_more(&self, _: &App) -> bool { self.has_more_data } fn load_more_threshold(&self) -> usize { 50 // Load more when 50 rows from bottom } fn load_more(&mut self, _: &mut Window, cx: &mut Context>) { if self.loading { return; // Prevent multiple loads } self.loading = true; // Spawn async task to load data cx.spawn(async move |view, cx| { let new_data = fetch_more_data().await; cx.update(|cx| { view.update(cx, |view, _| { let delegate = view.table.delegate_mut(); delegate.data.extend(new_data); delegate.loading = false; delegate.has_more_data = !new_data.is_empty(); }); }) }).detach(); } fn loading(&self, _: &App) -> bool { self.loading } } ``` ### Table Styling Customize table appearance. `DataTable` implements `Sizable`: use preset sizes such as `.small()` and `.large()` for standard density, or pass a custom pixel size to set a uniform header and body row height. ```rust use gpui_kit::px; use gpui_kit::component::Sizable as _; let state = cx.new(|cx| { TableState::new(delegate, window, cx) }); // In render DataTable::new(&state) .with_size(px(48.)) // Custom uniform row height .stripe(true) // Alternating row colors .bordered(true) // Border around table .scrollbar_visible(true, true) // Vertical, horizontal scrollbars ``` ## Examples ### Financial Data Table ```rust struct StockData { symbol: String, price: f64, change: f64, change_percent: f64, volume: u64, } impl TableDelegate for StockTableDelegate { fn render_td(&mut self, row_ix: usize, col_ix: usize, _: &mut Window, cx: &mut Context>) -> impl IntoElement { let stock = &self.stocks[row_ix]; let col = &self.columns[col_ix]; match col.key.as_ref() { "symbol" => div().font_weight(FontWeight::BOLD).child(stock.symbol.clone()), "price" => div().text_right().child(format!("${:.2}", stock.price)), "change" => { let color = if stock.change >= 0.0 { cx.theme().green } else { cx.theme().red }; div() .text_right() .text_color(color) .child(format!("{:+.2}", stock.change)) } "change_percent" => { let color = if stock.change_percent >= 0.0 { cx.theme().green } else { cx.theme().red }; div() .text_right() .text_color(color) .child(format!("{:+.1}%", stock.change_percent * 100.0)) } "volume" => div().text_right().child(format!("{:,}", stock.volume)), _ => div(), } } } ``` ### User Management Table ```rust struct UserTableDelegate { users: Vec, columns: Vec, } impl UserTableDelegate { fn new() -> Self { Self { users: Vec::new(), columns: vec![ Column::new("avatar", "").width(50.).resizable(false).movable(false), Column::new("name", "Name").width(150.).sortable().fixed_left(), Column::new("email", "Email").width(200.).sortable(), Column::new("role", "Role").width(100.).sortable(), Column::new("status", "Status").width(100.), Column::new("last_login", "Last Login").width(120.).sortable(), Column::new("actions", "Actions").width(100.).resizable(false), ], } } } ``` ### Cell Selection Enable individual cell selection for more granular control: ```rust let state = cx.new(|cx| { TableState::new(delegate, window, cx) .cell_selectable(true) // Enable cell selection .row_selectable(true) // Also allow row selection }); // Listen for cell events cx.subscribe_in(&state, window, |view, table, event, _, cx| { match event { TableEvent::SelectCell(row_ix, col_ix) => { println!("Selected cell: ({}, {})", row_ix, col_ix); } TableEvent::DoubleClickedCell(row_ix, col_ix) => { // Open editor or detail view open_cell_editor(row_ix, col_ix); } TableEvent::RightClickedCell(row_ix, col_ix) => { // Show cell-specific context menu show_cell_context_menu(row_ix, col_ix); } TableEvent::ClearSelection => { println!("Selection cleared"); } _ => {} } }).detach(); ``` #### Cell Selection Features When cell selection is enabled: * **Click to select**: Click on any cell to select it * **Row selector column**: A dedicated column appears on the left for selecting entire rows * **Keyboard navigation**: Arrow keys navigate between cells (not rows/columns) * **Double-click support**: Trigger actions like editing by double-clicking cells * **Right-click support**: Show context menus specific to cell content * **Visual feedback**: Selected cells show highlight with border #### Programmatic Cell Selection ```rust // Get the currently selected cell if let Some((row_ix, col_ix)) = state.read(cx).selected_cell() { println!("Current cell: ({}, {})", row_ix, col_ix); } // Select a specific cell programmatically state.update(cx, |state, cx| { state.set_selected_cell(5, 3, cx); // Select row 5, column 3 }); // Clear all selections state.update(cx, |state, cx| { state.clear_selection(cx); }); ``` #### Non-selectable Columns Prevent specific columns from being selected (useful for action columns): ```rust Column::new("actions", "Actions") .width(100.) .selectable(false) // This column's cells cannot be selected .resizable(false) ``` #### Cell Selection with Custom Rendering ```rust impl TableDelegate for MyTableDelegate { fn render_td(&mut self, row_ix: usize, col_ix: usize, _: &mut Window, cx: &mut Context>) -> impl IntoElement { let row = &self.data[row_ix]; let col = &self.columns[col_ix]; // Render different content based on whether cell is selected let is_selected = cx.entity().read(cx).selected_cell() == Some((row_ix, col_ix)); match col.key.as_ref() { "editable_field" => { if is_selected { // Show input when selected Input::new(format!("cell-{}-{}", row_ix, col_ix)) .value(row.field_value.clone()) .into_any_element() } else { // Show plain text when not selected div().child(row.field_value.clone()).into_any_element() } } _ => div().child(row.get_value(col.key.as_ref())).into_any_element() } } } ``` ## Keyboard Shortcuts ### Row Selection Mode (default) * `↑/↓` - Navigate rows * `←/→` - Navigate columns * `Home` - Jump to first row/column * `End` - Jump to last row/column * `PageUp/PageDown` - Navigate by page * `Escape` - Clear selection ### Cell Selection Mode * `↑/↓` - Navigate up/down within current column * `←/→` - Navigate left/right within current row * `Tab` - Move to next cell (right, then next row) * `Shift+Tab` - Move to previous cell * `Home` - Jump to first cell in current row * `End` - Jump to last cell in current row * `PageUp/PageDown` - Navigate by page within current column * `Escape` - Clear selection ## API Reference ### Core Types * [DataTable] - The data table component * [TableState] - Table state management * [TableDelegate] - Trait for implementing table data source * [Column] - Column configuration * [TableEvent] - Table events (selection, clicks, etc.) ### Column Types * [ColumnSort] - Column sort direction enum * [ColumnFixed] - Column fixed position enum ### Methods #### TableState * `new(delegate, window, cx)` - Create a new table state * `cell_selectable(bool)` - Enable/disable cell selection * `row_selectable(bool)` - Enable/disable row selection * `col_selectable(bool)` - Enable/disable column selection * `selected_cell()` - Get currently selected cell * `set_selected_cell(row_ix, col_ix, cx)` - Select a specific cell * `selected_row()` - Get currently selected row * `selected_col()` - Get currently selected column * `clear_selection(cx)` - Clear all selections * `scroll_to_row(row_ix, cx)` - Scroll to specific row * `scroll_to_col(col_ix, cx)` - Scroll to specific column #### Column * `new(key, name)` - Create a new column * `width(pixels)` - Set column width * `sortable()` - Make column sortable * `ascending()` - Set default sort to ascending * `descending()` - Set default sort to descending * `text_right()` - Right-align column text * `text_center()` - Center-align column text * `fixed(ColumnFixed)` - Pin column to left * `resizable(bool)` - Enable/disable column resizing * `movable(bool)` - Enable/disable column moving * `selectable(bool)` - Enable/disable column/cell selection * `paddings(edges)` - Set custom padding * `min_width(pixels)` - Set minimum width * `max_width(pixels)` - Set maximum width ### Events * `SelectRow(usize)` - Row selected * `DoubleClickedRow(usize)` - Row double-clicked * `SelectColumn(usize)` - Column selected * `SelectCell(usize, usize)` - Cell selected (row\_ix, col\_ix) * `DoubleClickedCell(usize, usize)` - Cell double-clicked (row\_ix, col\_ix) * `RightClickedCell(usize, usize)` - Cell right-clicked (row\_ix, col\_ix) * `RightClickedRow(Option)` - Row right-clicked * `ColumnWidthsChanged(Vec)` - Column widths changed * `MoveColumn(usize, usize)` - Column moved (from\_ix, to\_ix) [DataTable]: https://docs.rs/gpui-component/latest/gpui_component/table/struct.DataTable.html [TableState]: https://docs.rs/gpui-component/latest/gpui_component/table/struct.TableState.html [TableDelegate]: https://docs.rs/gpui-component/latest/gpui_component/table/trait.TableDelegate.html [Column]: https://docs.rs/gpui-component/latest/gpui_component/table/struct.Column.html [TableEvent]: https://docs.rs/gpui-component/latest/gpui_component/table/enum.TableEvent.html [ColumnSort]: https://docs.rs/gpui-component/latest/gpui_component/table/enum.ColumnSort.html [ColumnFixed]: https://docs.rs/gpui-component/latest/gpui_component/table/enum.ColumnFixed.html --- --- url: /zh-CN/docs/components/data-table.md description: 支持虚拟滚动、排序、筛选和列管理的高性能数据表格。 --- # DataTable DataTable 是一个面向大数据集场景的高性能表格组件。它支持虚拟滚动、列配置、排序、筛选、行/列/单元格选择、自定义单元格渲染以及右键菜单,适合展示成千上万条数据同时保持流畅体验。 ## 核心特性 * 多种选择模式:支持行、列和单元格选择 * 虚拟滚动:适合大体量数据集 * 列管理:支持固定列、调整列宽、列移动 * 排序:内建排序能力 * 键盘导航:完整键盘交互支持 * 自定义单元格:每个单元格都可以渲染任意 GPUI 内容 * 右键菜单:支持行和单元格上下文菜单 * 无限加载:滚动到底部时按需加载更多数据 ## 导入 ```rust use gpui_kit::component::table::{ DataTable, TableState, TableDelegate, Column, ColumnSort, ColumnFixed, TableEvent }; ``` ## 用法 ### 基础表格 要创建一个 DataTable,需要实现 `TableDelegate`,提供列定义和数据渲染逻辑,再通过 `TableState` 管理状态。 ```rust use std::ops::Range; use gpui_kit::{App, Context, Window, IntoElement}; use gpui_kit::component::table::{DataTable, TableDelegate, Column, ColumnSort}; struct MyData { id: usize, name: String, age: u32, email: String, } struct MyTableDelegate { data: Vec, columns: Vec, } impl MyTableDelegate { fn new() -> Self { Self { data: vec![ MyData { id: 1, name: "John".to_string(), age: 30, email: "john@example.com".to_string() }, MyData { id: 2, name: "Jane".to_string(), age: 25, email: "jane@example.com".to_string() }, ], columns: vec![ Column::new("id", "ID").width(60.), Column::new("name", "Name").width(150.).sortable(), Column::new("age", "Age").width(80.).sortable(), Column::new("email", "Email").width(200.), ], } } } impl TableDelegate for MyTableDelegate { fn columns_count(&self, _: &App) -> usize { self.columns.len() } fn rows_count(&self, _: &App) -> usize { self.data.len() } fn column(&self, col_ix: usize, _: &App) -> Column { self.columns[col_ix].clone() } fn render_td(&mut self, row_ix: usize, col_ix: usize, _: &mut Window, _: &mut Context>) -> impl IntoElement { let row = &self.data[row_ix]; let col = &self.columns[col_ix]; match col.key.as_ref() { "id" => row.id.to_string(), "name" => row.name.clone(), "age" => row.age.to_string(), "email" => row.email.clone(), _ => "".to_string(), } } } let delegate = MyTableDelegate::new(); let state = cx.new(|cx| TableState::new(delegate, window, cx)); ``` ## 列配置 列支持丰富的配置能力: ```rust Column::new("id", "ID") Column::new("name", "Name") .sortable() .width(150.) Column::new("price", "Price") .text_right() .sortable() Column::new("actions", "Actions") .fixed(ColumnFixed::Left) .resizable(false) .movable(false) ``` 常用能力包括: * `sortable()` 开启排序 * `width()` 设置宽度 * `fixed(ColumnFixed::Left)` 固定到左侧 * `resizable(false)` 禁止调整列宽 * `movable(false)` 禁止拖动列顺序 * `text_right()` / `text_center()` 设置对齐 ## 虚拟滚动 DataTable 默认面向大数据场景设计。即使数据量达到数千或上万行,也只会渲染当前可见区域附近的内容。 ```rust impl TableDelegate for LargeDataDelegate { fn rows_count(&self, _: &App) -> usize { self.data.len() } fn render_td(&mut self, row_ix: usize, col_ix: usize, _: &mut Window, _: &mut Context>) -> impl IntoElement { let row = &self.data[row_ix]; format_cell_data(row, col_ix) } fn visible_rows_changed(&mut self, visible_range: Range, _: &mut Window, _: &mut Context>) { // 可在这里根据可见区做数据预取或缓存更新 } } ``` ## 排序 排序逻辑需要由你的 `TableDelegate` 实现: ```rust impl TableDelegate for MyTableDelegate { fn perform_sort(&mut self, col_ix: usize, sort: ColumnSort, _: &mut Window, _: &mut Context>) { let col = &self.columns[col_ix]; match col.key.as_ref() { "name" => match sort { ColumnSort::Ascending => self.data.sort_by(|a, b| a.name.cmp(&b.name)), ColumnSort::Descending => self.data.sort_by(|a, b| b.name.cmp(&a.name)), ColumnSort::Default => self.data.sort_by(|a, b| a.id.cmp(&b.id)), }, "age" => match sort { ColumnSort::Ascending => self.data.sort_by(|a, b| a.age.cmp(&b.age)), ColumnSort::Descending => self.data.sort_by(|a, b| b.age.cmp(&a.age)), ColumnSort::Default => self.data.sort_by(|a, b| a.id.cmp(&b.id)), }, _ => {} } } } ``` ## 右键菜单 你可以为行或单元格提供右键菜单: ```rust impl TableDelegate for MyTableDelegate { fn context_menu(&mut self, row_ix: usize, menu: PopupMenu, _: &mut Window, _: &mut Context>) -> PopupMenu { let row = &self.data[row_ix]; menu.menu(format!("Edit {}", row.name), Box::new(EditRowAction(row_ix))) .menu("Delete", Box::new(DeleteRowAction(row_ix))) .separator() .menu("Duplicate", Box::new(DuplicateRowAction(row_ix))) } } ``` ## 自定义单元格 DataTable 的一个重要能力是每个单元格都可以渲染复杂内容: ```rust impl TableDelegate for MyTableDelegate { fn render_td(&mut self, row_ix: usize, col_ix: usize, _: &mut Window, cx: &mut Context>) -> impl IntoElement { let row = &self.data[row_ix]; let col = &self.columns[col_ix]; match col.key.as_ref() { "status" => { let (color, text) = match row.status { Status::Active => (cx.theme().green, "Active"), Status::Inactive => (cx.theme().red, "Inactive"), Status::Pending => (cx.theme().yellow, "Pending"), }; div() .px_2() .py_1() .rounded(px(4.)) .bg(color.opacity(0.1)) .text_color(color) .child(text) } _ => row.get_field_value(col.key.as_ref()).into_any_element(), } } } ``` ## 选择模式 DataTable 支持三种主要选择模式: ```rust let state = cx.new(|cx| { TableState::new(delegate, window, cx) .row_selectable(true) .col_selectable(false) .cell_selectable(false) }); let state = cx.new(|cx| { TableState::new(delegate, window, cx) .row_selectable(false) .col_selectable(true) .cell_selectable(false) }); let state = cx.new(|cx| { TableState::new(delegate, window, cx) .row_selectable(true) .col_selectable(false) .cell_selectable(true) }); ``` ### 单元格选择 启用 `cell_selectable(true)` 后: * 点击单元格可以直接选中 * 可以用方向键在单元格之间移动 * 可以监听双击和右键事件 * 可以程序化设置当前选中单元格 ```rust if let Some((row_ix, col_ix)) = state.read(cx).selected_cell() { println!("Current cell: ({}, {})", row_ix, col_ix); } state.update(cx, |state, cx| { state.set_selected_cell(5, 3, cx); }); ``` ## 列宽调整与列移动 ```rust let state = cx.new(|cx| { TableState::new(delegate, window, cx) .col_resizable(true) .col_movable(true) .sortable(true) .col_selectable(true) .row_selectable(true) }); ``` 可以通过事件监听这些变化: ```rust cx.subscribe_in(&state, window, |view, table, event, _, cx| { match event { TableEvent::ColumnWidthsChanged(widths) => { save_column_widths(widths); } TableEvent::MoveColumn(from_ix, to_ix) => { save_column_order(from_ix, to_ix); } _ => {} } }).detach(); ``` ## 无限加载 如果你的数据来自分页接口或流式加载,可以在 delegate 中实现按需加载: ```rust impl TableDelegate for MyTableDelegate { fn has_more(&self, _: &App) -> bool { self.has_more_data } fn load_more_threshold(&self) -> usize { 50 } fn loading(&self, _: &App) -> bool { self.loading } } ``` ## 表格样式 `DataTable` 实现了 `Sizable`:可以用 `.small()`、`.large()` 等预设尺寸调整表格密度,也可以传入自定义像素值来设置统一的表头和表体行高。 ```rust use gpui_kit::px; use gpui_kit::component::Sizable as _; DataTable::new(&state) .with_size(px(48.)) .stripe(true) .bordered(true) .scrollbar_visible(true, true) ``` ## 键盘快捷键 ### 行选择模式 * `↑/↓` 在行之间移动 * `←/→` 在列之间移动 * `Home` / `End` 跳到首尾 * `PageUp/PageDown` 按页移动 * `Escape` 清除选中 ### 单元格选择模式 * `↑/↓` 在当前列中上下移动 * `←/→` 在当前行中左右移动 * `Tab` 移动到下一个单元格 * `Shift+Tab` 移动到上一个单元格 * `Escape` 清除选中 ## API 参考 ### 核心类型 * [DataTable] - 表格组件本体 * [TableState] - 表格状态管理 * [TableDelegate] - 数据源和渲染协议 * [Column] - 列定义 * [TableEvent] - 表格事件 ### 常见方法 #### TableState * `new(delegate, window, cx)` * `cell_selectable(bool)` * `row_selectable(bool)` * `col_selectable(bool)` * `selected_cell()` * `set_selected_cell(row_ix, col_ix, cx)` * `clear_selection(cx)` * `scroll_to_row(row_ix, cx)` * `scroll_to_col(col_ix, cx)` #### Column * `new(key, name)` * `width(pixels)` * `sortable()` * `ascending()` * `descending()` * `text_right()` * `text_center()` * `fixed(ColumnFixed)` * `resizable(bool)` * `movable(bool)` * `selectable(bool)` [DataTable]: https://docs.rs/gpui-component/latest/gpui_component/table/struct.DataTable.html [TableState]: https://docs.rs/gpui-component/latest/gpui_component/table/struct.TableState.html [TableDelegate]: https://docs.rs/gpui-component/latest/gpui_component/table/trait.TableDelegate.html [Column]: https://docs.rs/gpui-component/latest/gpui_component/table/struct.Column.html [TableEvent]: https://docs.rs/gpui-component/latest/gpui_component/table/enum.TableEvent.html [ColumnSort]: https://docs.rs/gpui-component/latest/gpui_component/table/enum.ColumnSort.html [ColumnFixed]: https://docs.rs/gpui-component/latest/gpui_component/table/enum.ColumnFixed.html --- --- url: /base/primitives/date-picker.md description: A focus-aware date input that composes calendar behavior with a popup. --- # Date Picker A focus-aware date input that composes calendar behavior with a popup. Like every `gpui-base` primitive, Date Picker supplies behavior and semantic structure without imposing a product visual language. Apply GPUI styles and compose the exported parts to match your design system. ## Example The [single native Cargo entrypoint](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/components.rs) selects this primitive from the [shared showcase implementation](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/mod.rs). The same showcase is compiled once for the WASM preview above. ```bash cargo run -p gpui-base --example components -- date-picker ``` ## Import ```rust use gpui_kit::base::{DatePicker}; ``` ## Anatomy and API The example composes `DatePicker`. GPUI's standard styling and event traits provide presentation; these base types provide the interaction structure. The authoritative module is [`components/date_picker.rs`](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/components/date_picker.rs). Native and browser previews compile this same file. ## State and events The picker combines focus/input state with calendar selection. Retain its entities on the parent view. Keep controlled state on the parent render type or in a GPUI entity. Update it in callbacks and call `cx.notify()`; do not recreate persistent entities during every render. ## Complete Rust example The complete implementation used by the runnable showcase is embedded directly from Rust source: The command above supplies application initialization, window creation, and shared `BaseShowcase` state. ## Accessibility Label the input, announce locale-appropriate dates, and make the calendar keyboard operable. ## Notes Use stable element IDs where accepted. Verify focus, hover, active, selected, disabled, reduced-motion, and high-contrast appearances in the consuming design system. --- --- url: /zh-CN/base/primitives/date-picker.md description: 将日历行为与弹出层组合的焦点感知日期输入。 --- # Date Picker 将日历行为与弹出层组合的焦点感知日期输入。 和所有 GPUI Base 原语一样,Date Picker 只提供行为和语义结构,不规定产品视觉语言。请使用 GPUI 样式并组合导出的部件,使其符合你的设计系统。 ## 示例 原生示例和页面上方的 WASM 预览共用同一份实现: ```bash cargo run -p gpui-base --example components -- date-picker ``` ## 导入 ```rust use gpui_kit::base::{DatePicker}; ``` ## 结构与 API 示例组合上述公开类型。GPUI 的标准样式和事件 trait 负责表现,Base 类型负责交互结构。权威实现位于 [`components/date-picker.rs`](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/components/date-picker.rs),原生与浏览器预览编译的是同一文件。 ## 状态与事件 应用持有日期与打开状态,并处理输入或日历产生的变更。 受控状态应保存在父渲染类型或 GPUI entity 中;在回调中更新并调用 `cx.notify()`,不要在每次渲染时重建持久 entity。 ## 完整 Rust 示例 ## 可访问性 输入应有标签;弹出日历需保留键盘导航和清晰焦点。 ## 注意事项 在支持的位置使用稳定元素 ID,并在消费端设计系统中验证焦点、悬停、按下、选中、禁用、减少动态效果和高对比度状态。 --- --- url: /docs/components/date-picker.md description: >- A date picker component for selecting single dates or date ranges with calendar interface. --- # DatePicker A flexible date picker component with calendar interface that supports single date selection, date range selection, custom date formatting, disabled dates, and preset ranges. ## Import ```rust use gpui_kit::component::{ date_picker::{DatePicker, DatePickerState, DateRangePreset, DatePickerEvent}, calendar::{Date, Matcher}, }; ``` ## Usage ### Basic Date Picker ```rust let date_picker = cx.new(|cx| DatePickerState::new(window, cx)); DatePicker::new(&date_picker) ``` ### With Initial Date ```rust use chrono::Local; let date_picker = cx.new(|cx| { let mut picker = DatePickerState::new(window, cx); picker.set_date(Local::now().naive_local().date(), window, cx); picker }); DatePicker::new(&date_picker) ``` ### Date Range Picker ```rust use chrono::{Local, Days}; // Range mode picker let range_picker = cx.new(|cx| DatePickerState::range(window, cx)); DatePicker::new(&range_picker) .number_of_months(2) // Show 2 months for easier range selection // With initial range let range_picker = cx.new(|cx| { let now = Local::now().naive_local().date(); let mut picker = DatePickerState::new(window, cx); picker.set_date( (now, now.checked_add_days(Days::new(7)).unwrap()), window, cx, ); picker }); DatePicker::new(&range_picker) .number_of_months(2) ``` ### With Custom Date Format ```rust let date_picker = cx.new(|cx| { DatePickerState::new(window, cx) .date_format("%Y-%m-%d") // ISO format }); DatePicker::new(&date_picker) // Other format examples: // "%m/%d/%Y" -> 12/25/2023 // "%B %d, %Y" -> December 25, 2023 // "%d %b %Y" -> 25 Dec 2023 ``` ### With Placeholder ```rust DatePicker::new(&date_picker) .placeholder("Select a date...") ``` ### Cleanable Date Picker ```rust DatePicker::new(&date_picker) .cleanable(true) // Show clear button when date is selected ``` ### Different Sizes ```rust DatePicker::new(&date_picker).large() DatePicker::new(&date_picker) // medium (default) DatePicker::new(&date_picker).small() ``` ### Disabled State ```rust DatePicker::new(&date_picker).disabled(true) ``` ### Custom Appearance ```rust // Without default styling DatePicker::new(&date_picker).appearance(false) // Use in custom container div() .border_b_2() .px_6() .py_3() .border_color(cx.theme().border) .bg(cx.theme().secondary) .child(DatePicker::new(&date_picker).appearance(false)) ``` ## Date Restrictions ### Disabled Weekends ```rust use gpui_kit::component::calendar; let date_picker = cx.new(|cx| { DatePickerState::new(window, cx) .disabled_matcher(vec![0, 6]) // Sunday=0, Saturday=6 }); DatePicker::new(&date_picker) ``` ### Disabled Date Range ```rust use chrono::{Local, Days}; let now = Local::now().naive_local().date(); let date_picker = cx.new(|cx| { DatePickerState::new(window, cx) .disabled_matcher(calendar::Matcher::range( Some(now), now.checked_add_days(Days::new(7)), )) }); DatePicker::new(&date_picker) ``` ### Disabled Date Interval ```rust let date_picker = cx.new(|cx| { DatePickerState::new(window, cx) .disabled_matcher(calendar::Matcher::interval( Some(now), now.checked_add_days(Days::new(5)) )) }); DatePicker::new(&date_picker) ``` ### Custom Disabled Dates ```rust // Disable first 5 days of each month let date_picker = cx.new(|cx| { DatePickerState::new(window, cx) .disabled_matcher(calendar::Matcher::custom(|date| { date.day0() < 5 })) }); DatePicker::new(&date_picker) // Disable all Mondays let date_picker = cx.new(|cx| { DatePickerState::new(window, cx) .disabled_matcher(calendar::Matcher::custom(|date| { date.weekday() == chrono::Weekday::Mon })) }); ``` ## Custom Year Range By default, the date picker shows 50 years before and after the current year in year selection mode. Use `set_year_range` to configure a different range — for example, a birthday picker that goes back to 1900. The `range` argument uses a **half-open interval** `(start, end)` where `end` is **exclusive**. Pass `(1900, current_year + 1)` to include `current_year`. ```rust use chrono::Datelike; // Birthday picker: allow years from 1900 to the current year (inclusive) let birthday_picker = cx.new(|cx| { let current_year = chrono::Local::now().year(); let mut picker = DatePickerState::new(window, cx) .date_format("%Y-%m-%d"); picker.set_year_range((1900, current_year + 1), window, cx); picker }); DatePicker::new(&birthday_picker) .cleanable(true) .placeholder("Select birthday") ``` `set_year_range` works for both single-date and range-mode pickers. ## Preset Ranges ### Single Date Presets ```rust use chrono::{Utc, Duration}; let presets = vec![ DateRangePreset::single( "Yesterday", (Utc::now() - Duration::days(1)).naive_local().date(), ), DateRangePreset::single( "Last Week", (Utc::now() - Duration::weeks(1)).naive_local().date(), ), DateRangePreset::single( "Last Month", (Utc::now() - Duration::days(30)).naive_local().date(), ), ]; DatePicker::new(&date_picker) .presets(presets) ``` ### Date Range Presets ```rust let range_presets = vec![ DateRangePreset::range( "Last 7 Days", (Utc::now() - Duration::days(7)).naive_local().date(), Utc::now().naive_local().date(), ), DateRangePreset::range( "Last 30 Days", (Utc::now() - Duration::days(30)).naive_local().date(), Utc::now().naive_local().date(), ), DateRangePreset::range( "Last 90 Days", (Utc::now() - Duration::days(90)).naive_local().date(), Utc::now().naive_local().date(), ), ]; DatePicker::new(&date_picker) .number_of_months(2) .presets(range_presets) ``` ## Handle Date Selection Events ```rust let date_picker = cx.new(|cx| DatePickerState::new(window, cx)); cx.subscribe(&date_picker, |view, _, event, _| { match event { DatePickerEvent::Change(date) => { match date { Date::Single(Some(selected_date)) => { println!("Single date selected: {}", selected_date); } Date::Range(Some(start), Some(end)) => { println!("Date range selected: {} to {}", start, end); } Date::Range(Some(start), None) => { println!("Range start selected: {}", start); } _ => { println!("Date cleared"); } } } } }); ``` ## Multiple Months Display ```rust // Show 2 months side by side (useful for date ranges) DatePicker::new(&date_picker) .number_of_months(2) // Show 3 months DatePicker::new(&date_picker) .number_of_months(3) ``` ## Advanced Examples ### Business Days Only ```rust use chrono::Weekday; let business_days_picker = cx.new(|cx| { DatePickerState::new(window, cx) .disabled_matcher(calendar::Matcher::custom(|date| { matches!(date.weekday(), Weekday::Sat | Weekday::Sun) })) }); DatePicker::new(&business_days_picker) .placeholder("Select business day") ``` ### Date Range with Max Duration ```rust use chrono::Days; let max_30_days_picker = cx.new(|cx| DatePickerState::range(window, cx)); cx.subscribe(&max_30_days_picker, |view, picker, event, _| { match event { DatePickerEvent::Change(Date::Range(Some(start), Some(end))) => { let duration = end.signed_duration_since(*start).num_days(); if duration > 30 { // Reset to start date only if range exceeds 30 days picker.update(cx, |state, cx| { state.set_date(Date::Range(Some(*start), None), window, cx); }); } } _ => {} } }); DatePicker::new(&max_30_days_picker) .number_of_months(2) .placeholder("Select up to 30 days") ``` ### Quarter Presets ```rust use chrono::{NaiveDate, Datelike}; fn quarter_start(year: i32, quarter: u32) -> NaiveDate { let month = (quarter - 1) * 3 + 1; NaiveDate::from_ymd_opt(year, month, 1).unwrap() } fn quarter_end(year: i32, quarter: u32) -> NaiveDate { let month = quarter * 3; let start = NaiveDate::from_ymd_opt(year, month, 1).unwrap(); NaiveDate::from_ymd_opt(year, month, start.days_in_month()).unwrap() } let year = Local::now().year(); let quarterly_presets = vec![ DateRangePreset::range("Q1", quarter_start(year, 1), quarter_end(year, 1)), DateRangePreset::range("Q2", quarter_start(year, 2), quarter_end(year, 2)), DateRangePreset::range("Q3", quarter_start(year, 3), quarter_end(year, 3)), DateRangePreset::range("Q4", quarter_start(year, 4), quarter_end(year, 4)), ]; DatePicker::new(&date_picker) .presets(quarterly_presets) ``` ## Examples ### Event Date Picker ```rust let event_date = cx.new(|cx| { let mut picker = DatePickerState::new(window, cx) .date_format("%B %d, %Y") .disabled_matcher(calendar::Matcher::custom(|date| { // Disable past dates *date < Local::now().naive_local().date() })); picker }); DatePicker::new(&event_date) .placeholder("Choose event date") .cleanable(true) ``` ### Booking System Date Range ```rust let booking_range = cx.new(|cx| DatePickerState::range(window, cx)); let booking_presets = vec![ DateRangePreset::range("This Weekend", /* weekend dates */), DateRangePreset::range("Next Week", /* next week dates */), DateRangePreset::range("This Month", /* this month dates */), ]; DatePicker::new(&booking_range) .number_of_months(2) .presets(booking_presets) .placeholder("Select check-in and check-out dates") ``` ### Financial Period Selector ```rust let financial_period = cx.new(|cx| { DatePickerState::range(window, cx) .date_format("%Y-%m-%d") }); DatePicker::new(&financial_period) .number_of_months(3) .presets(quarterly_presets) .placeholder("Select reporting period") ``` --- --- url: /zh-CN/docs/components/date-picker.md description: 支持单日期和日期范围选择的日历选择器组件。 --- # DatePicker DatePicker 是一个灵活的日期选择组件,内置日历界面,支持单日期选择、日期范围选择、自定义格式、禁用日期和预设范围。 ## 导入 ```rust use gpui_kit::component::{ date_picker::{DatePicker, DatePickerState, DateRangePreset, DatePickerEvent}, calendar::{Date, Matcher}, }; ``` ## 用法 ### 基础 Date Picker ```rust let date_picker = cx.new(|cx| DatePickerState::new(window, cx)); DatePicker::new(&date_picker) ``` ### 设置初始日期 ```rust use chrono::Local; let date_picker = cx.new(|cx| { let mut picker = DatePickerState::new(window, cx); picker.set_date(Local::now().naive_local().date(), window, cx); picker }); DatePicker::new(&date_picker) ``` ### 日期范围选择 ```rust use chrono::{Local, Days}; let range_picker = cx.new(|cx| DatePickerState::range(window, cx)); DatePicker::new(&range_picker) .number_of_months(2) let range_picker = cx.new(|cx| { let now = Local::now().naive_local().date(); let mut picker = DatePickerState::new(window, cx); picker.set_date( (now, now.checked_add_days(Days::new(7)).unwrap()), window, cx, ); picker }); DatePicker::new(&range_picker) .number_of_months(2) ``` ### 自定义日期格式 ```rust let date_picker = cx.new(|cx| { DatePickerState::new(window, cx) .date_format("%Y-%m-%d") }); DatePicker::new(&date_picker) // Other format examples: // "%m/%d/%Y" -> 12/25/2023 // "%B %d, %Y" -> December 25, 2023 // "%d %b %Y" -> 25 Dec 2023 ``` ### 占位文本 ```rust DatePicker::new(&date_picker) .placeholder("Select a date...") ``` ### 可清空 ```rust DatePicker::new(&date_picker) .cleanable(true) ``` ### 不同尺寸 ```rust DatePicker::new(&date_picker).large() DatePicker::new(&date_picker) DatePicker::new(&date_picker).small() ``` ### 禁用状态 ```rust DatePicker::new(&date_picker).disabled(true) ``` ### 自定义外观 ```rust DatePicker::new(&date_picker).appearance(false) div() .border_b_2() .px_6() .py_3() .border_color(cx.theme().border) .bg(cx.theme().secondary) .child(DatePicker::new(&date_picker).appearance(false)) ``` ## 日期限制 ### 禁用周末 ```rust use gpui_kit::component::calendar; let date_picker = cx.new(|cx| { DatePickerState::new(window, cx) .disabled_matcher(vec![0, 6]) }); DatePicker::new(&date_picker) ``` ### 禁用日期区间 ```rust use chrono::{Local, Days}; let now = Local::now().naive_local().date(); let date_picker = cx.new(|cx| { DatePickerState::new(window, cx) .disabled_matcher(calendar::Matcher::range( Some(now), now.checked_add_days(Days::new(7)), )) }); DatePicker::new(&date_picker) ``` ### 禁用日期间隔 ```rust let date_picker = cx.new(|cx| { DatePickerState::new(window, cx) .disabled_matcher(calendar::Matcher::interval( Some(now), now.checked_add_days(Days::new(5)) )) }); DatePicker::new(&date_picker) ``` ### 自定义禁用规则 ```rust let date_picker = cx.new(|cx| { DatePickerState::new(window, cx) .disabled_matcher(calendar::Matcher::custom(|date| { date.day0() < 5 })) }); DatePicker::new(&date_picker) let date_picker = cx.new(|cx| { DatePickerState::new(window, cx) .disabled_matcher(calendar::Matcher::custom(|date| { date.weekday() == chrono::Weekday::Mon })) }); ``` ## 自定义年份范围 默认情况下,日期选择器在年份选择模式下会显示以今天为中心前后各 50 年。可通过 `set_year_range` 配置更大的范围——例如用于生日选择,需要回溯到 1900 年。 `range` 参数使用**半开区间** `(start, end)`,`end` **不包含**在内。若要包含当前年份,传入 `(1900, current_year + 1)`。 ```rust use chrono::Datelike; // 生日选择器:允许选择 1900 年到当前年(含) let birthday_picker = cx.new(|cx| { let current_year = chrono::Local::now().year(); let mut picker = DatePickerState::new(window, cx) .date_format("%Y-%m-%d"); picker.set_year_range((1900, current_year + 1), window, cx); picker }); DatePicker::new(&birthday_picker) .cleanable(true) .placeholder("选择生日") ``` `set_year_range` 对单日期和范围两种模式均有效。 ## 预设范围 ### 单日期预设 ```rust use chrono::{Utc, Duration}; let presets = vec![ DateRangePreset::single( "Yesterday", (Utc::now() - Duration::days(1)).naive_local().date(), ), DateRangePreset::single( "Last Week", (Utc::now() - Duration::weeks(1)).naive_local().date(), ), DateRangePreset::single( "Last Month", (Utc::now() - Duration::days(30)).naive_local().date(), ), ]; DatePicker::new(&date_picker) .presets(presets) ``` ### 日期范围预设 ```rust let range_presets = vec![ DateRangePreset::range( "Last 7 Days", (Utc::now() - Duration::days(7)).naive_local().date(), Utc::now().naive_local().date(), ), DateRangePreset::range( "Last 30 Days", (Utc::now() - Duration::days(30)).naive_local().date(), Utc::now().naive_local().date(), ), DateRangePreset::range( "Last 90 Days", (Utc::now() - Duration::days(90)).naive_local().date(), Utc::now().naive_local().date(), ), ]; DatePicker::new(&date_picker) .number_of_months(2) .presets(range_presets) ``` ## 处理选择事件 ```rust let date_picker = cx.new(|cx| DatePickerState::new(window, cx)); cx.subscribe(&date_picker, |view, _, event, _| { match event { DatePickerEvent::Change(date) => { match date { Date::Single(Some(selected_date)) => { println!("Single date selected: {}", selected_date); } Date::Range(Some(start), Some(end)) => { println!("Date range selected: {} to {}", start, end); } Date::Range(Some(start), None) => { println!("Range start selected: {}", start); } _ => { println!("Date cleared"); } } } } }); ``` ## 显示多个月份 ```rust DatePicker::new(&date_picker) .number_of_months(2) DatePicker::new(&date_picker) .number_of_months(3) ``` ## 高级示例 ### 仅工作日可选 ```rust use chrono::Weekday; let business_days_picker = cx.new(|cx| { DatePickerState::new(window, cx) .disabled_matcher(calendar::Matcher::custom(|date| { matches!(date.weekday(), Weekday::Sat | Weekday::Sun) })) }); DatePicker::new(&business_days_picker) .placeholder("Select business day") ``` ### 限制最大范围 ```rust use chrono::Days; let max_30_days_picker = cx.new(|cx| DatePickerState::range(window, cx)); cx.subscribe(&max_30_days_picker, |view, picker, event, _| { match event { DatePickerEvent::Change(Date::Range(Some(start), Some(end))) => { let duration = end.signed_duration_since(*start).num_days(); if duration > 30 { picker.update(cx, |state, cx| { state.set_date(Date::Range(Some(*start), None), window, cx); }); } } _ => {} } }); DatePicker::new(&max_30_days_picker) .number_of_months(2) .placeholder("Select up to 30 days") ``` ### 季度预设 ```rust use chrono::{NaiveDate, Datelike}; fn quarter_start(year: i32, quarter: u32) -> NaiveDate { let month = (quarter - 1) * 3 + 1; NaiveDate::from_ymd_opt(year, month, 1).unwrap() } fn quarter_end(year: i32, quarter: u32) -> NaiveDate { let month = quarter * 3; let start = NaiveDate::from_ymd_opt(year, month, 1).unwrap(); NaiveDate::from_ymd_opt(year, month, start.days_in_month()).unwrap() } let year = Local::now().year(); let quarterly_presets = vec![ DateRangePreset::range("Q1", quarter_start(year, 1), quarter_end(year, 1)), DateRangePreset::range("Q2", quarter_start(year, 2), quarter_end(year, 2)), DateRangePreset::range("Q3", quarter_start(year, 3), quarter_end(year, 3)), DateRangePreset::range("Q4", quarter_start(year, 4), quarter_end(year, 4)), ]; DatePicker::new(&date_picker) .presets(quarterly_presets) ``` ## 示例 ### 事件日期选择 ```rust let event_date = cx.new(|cx| { let mut picker = DatePickerState::new(window, cx) .date_format("%B %d, %Y") .disabled_matcher(calendar::Matcher::custom(|date| { *date < Local::now().naive_local().date() })); picker }); DatePicker::new(&event_date) .placeholder("Choose event date") .cleanable(true) ``` ### 预订系统日期范围 ```rust let booking_range = cx.new(|cx| DatePickerState::range(window, cx)); let booking_presets = vec![ DateRangePreset::range("This Weekend", /* weekend dates */), DateRangePreset::range("Next Week", /* next week dates */), DateRangePreset::range("This Month", /* this month dates */), ]; DatePicker::new(&booking_range) .number_of_months(2) .presets(booking_presets) .placeholder("Select check-in and check-out dates") ``` ### 财务周期选择 ```rust let financial_period = cx.new(|cx| { DatePickerState::range(window, cx) .date_format("%Y-%m-%d") }); DatePicker::new(&financial_period) .number_of_months(3) .presets(quarterly_presets) .placeholder("Select reporting period") ``` --- --- url: /shell/dependencies.md description: >- Shell packages — what makes a Git repository one, and how a manifest names, selects, fetches and imports it, down to what an editor sees. --- # Dependencies An application imports its own files by relative path. Every other import it writes comes from one of two places: a **built-in module** the runtime provides — `gpui-kit`, `gpui-base`, `gpui-shell`, `gpui-fps`, and the standard runtime's `fs/promises`, `path`, `crypto`, `net`, `websocket` — or a **dependency**, a JavaScript package the manifest declares and gpui-shell fetches from Git before the entry module is evaluated. There is no registry, no package manager and no install step. A dependency is a Git remote, a ref, and the name a script imports it by. ## Shell package A dependency is any Git repository the manifest points at. `omarchy-ui` is a particular kind of one, and that kind has a name: **a shell package** — a JavaScript package written for gpui-shell rather than for Node or a browser, the way a crate is written for Cargo. Five things make a repository one: | A shell package | Because | | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | | Ships ES modules, and needs no build step | The runtime evaluates the checkout's files as they are, and `require` does not exist | | Has a root `package.json` with `"type": "module"` and a `main` | It makes the declaration one line, and names the entry for the runtime and the editor | | Imports only the built-in modules and its own files | Nothing else resolves — it cannot reach the application that imported it | | Treats `gpui-kit` and `gpui-base` as provided, never as dependencies | They come from the runtime that loaded it, at the version the host chose | | Declares no capabilities of its own | Its `fs` and `fetch` calls run under the consuming application's grants | Nothing reads the name: a dependency is recognized by being declared, not by being labelled. It is what one author writes so another can find the package, and `gpui-shell` is the repository topic that spells it for a search engine. [`omarchy-ui`](https://github.com/huacnlee/omarchy-ui) is one, and it is the example on the rest of this page. ## Declaring a dependency `omarchy-ui` is a package of presentation components and theme utilities. One line in `gpui-shell.json` adds it: ```json { "id": "com.example.projects", "name": "Projects", "entry": "main.js", "dependencies": { "omarchy-ui": "huacnlee/omarchy-ui" } } ``` The map key is the bare module name — nothing inside the package chooses it. The manifest names the package the way an `as` clause names an import, so two applications may reach the same remote under different names, and renaming a repository does not rename the import: ```js import { AppShell, Button, CenteredWorkspace, MutedText, PageColumn, Surface, Title, } from "omarchy-ui"; export function render(cx) { const card = new Surface() .children([ new Title("Projects").build(cx), new MutedText("Choose a project to continue").build(cx), new Button("project-create") .label("Create project…") .onClick((_event, context) => context.notify()) .build(cx), ]) .build(cx); const page = new PageColumn("projects-page").child(card).build(cx); return new AppShell() .content( new CenteredWorkspace("projects-workspace").content(page).build(cx), ) .build(cx); } ``` Nothing else changes. The script is still the script from [Getting Started](./getting-started.md); the dependency only widens what it may import. ## What resolves, and to what | Written | Resolves to | | -------------------------------------------------------- | -------------------------------------------------------------------------- | | `"omarchy-ui"` | The package entry — see [Package entry](#package-entry) | | `"omarchy-ui/src/style"` | That file inside the checkout; the `.js` extension is optional | | `"./theme.js"` inside a package | A file inside that package's own checkout | | `"gpui-kit"` inside a package | The built-in module, exactly as in application code | | Another declared dependency name | The other package's entry — declared packages can see one another | | An application file, by bare name, from inside a package | Refused: a package cannot reach back into the application that imported it | A specifier that resolves outside the checkout it started in is refused before the module is loaded, so `../` cannot walk out of a package and into the cache beside it. Inside the application directory the same boundary is the application root, which is the rule [the sandbox](./capabilities.md#the-sandbox) already applies to relative imports. **A dependency is not a second sandbox.** It is evaluated in the application's own context and holds exactly the grants the manifest holds: a package that reads a file is reading it under your `fs.read` scope. Declaring a dependency is trusting its code the way importing a Rust crate is, and the ref you pin is what decides which code that is. ## Selecting a version The string form is a strict GitHub shorthand or a full Git URL, each with an optional `#ref`: ```json { "dependencies": { "default-main": "huacnlee/omarchy-ui", "named-ref": "huacnlee/omarchy-ui#v1.2.0", "commit": "https://github.com/huacnlee/omarchy-ui#0123456789abcdef0123456789abcdef01234567", "remote-head": "https://github.com/huacnlee/omarchy-ui" } } ``` | Form | Selects | | -------------------------- | ------------------------------ | | `owner/repository` | `main` | | `owner/repository#ref` | That branch, tag or commit-ish | | `https://…/repository` | The remote's `HEAD` | | `https://…/repository#ref` | That branch, tag or commit-ish | Shorthand is deliberately strict: exactly one `owner/repository` pair of alphanumerics, `.`, `-` and `_`, at most one `#`, no surrounding whitespace, and a fragment that is a valid Git ref name. Anything else is a manifest error rather than a URL guessed from a typo. A full URL may be any Git transport, `ssh://` and `git@host:owner/repo` included. **A branch, a tag or a remote `HEAD` is re-fetched and re-resolved on every application load; a commit ID always selects that commit.** Depending on a branch means the code changes under you the next time the window opens, which is convenient while you develop a package and a supply-chain decision once you ship one. Pin a tag or a commit for anything you do not control. Fetching needs `git` on the host's `PATH`, and it happens before script capabilities exist — it is gpui-shell running Git on the application's behalf, not the script reaching the network, so it is not covered by `capabilities.network` and does not need `fs.execute`. With nothing to fetch (the cache already holds the commit) a load performs no network access at all; with a moving ref and no network, the fetch fails and the application does not load. ## Package entry After the checkout exists, gpui-shell reads the package's root `package.json` and takes a string `main` as the entry. `omarchy-ui` publishes: ```json { "type": "module", "main": "src/index.js", "types": "src/index.d.ts" } ``` So `import { Title } from "omarchy-ui"` evaluates `src/index.js`. When `package.json` is missing, or has no `main`, the entry is the root `index.js`. The runtime reads `main`; an editor reads `types`. Both come out of the same file, which is why a package that ships `.d.ts` files needs nothing from the application to be fully typed at the call site. Malformed JSON, a non-string `main`, and an entry that is absent, not a file, or escapes the checkout all fail the load before any application JavaScript runs. ## The object form The original object form remains supported unchanged. It requires exactly one explicit `branch` or `tag`, and its repository-relative `entry` defaults to `index.js`: ```json { "dependencies": { "omarchy-ui": { "git": "https://github.com/huacnlee/omarchy-ui", "tag": "v1.2.0", "entry": "src/index.js" } } } ``` Existing manifests do not need to migrate. Moving to the string form means the package publishes its entry itself, through `package.json` `main` or a root `index.js`, instead of every consumer repeating it. ## The cache ```text ~/.gpui-shell/cache/dependencies/ ├── locks/.lock ├── mirrors/.git └── checkouts/// ``` `` is a SHA-256 of the exact fragment-free URL, which is both the remote's identity and its cache identity. A per-remote lock serializes mirror updates. Checkouts are commit-addressed and never rewritten, so concurrent launches and an older hot-reload generation each keep reading the tree they started with. The mirror's configured origin is verified against the manifest on every use, and it is the raw configured value that is compared — Git's `url.*.insteadOf` may still choose a different effective fetch URL, which is how a mirror or an internal host substitution keeps working. Git runs non-interactively, with credential prompts disabled and a 30-second limit per command, so a repository that wants a password fails with a message instead of hanging a window that is waiting to open. Nothing prunes this cache automatically. It is content-addressed, so deleting it is safe: the next load re-fetches what it needs. ## What an editor sees The runtime answers `import { Title } from "omarchy-ui"` from the manifest. An editor answers it by walking `node_modules` up from the importing file, and it has never heard of `gpui-shell.json`. Left alone, a correct import is underlined as a module that cannot be found, and every name behind it loses its type, its parameter hints and its documentation. So every load — and `gpui-shell types` — links each materialized checkout into the application's `node_modules` under the name the manifest gave it: ```text projects/ ├── gpui-shell.json ├── main.js ├── gpui-kit.d.ts generated by the runtime — ignore it ├── jsconfig.json scaffolded once, then yours └── node_modules/ └── omarchy-ui → ~/.gpui-shell/cache/dependencies/checkouts// ``` The editor then reads the same files the runtime is about to execute, so the signatures and JSDoc it shows are the package's own and cannot drift from what runs. Only entries gpui-shell wrote are ever replaced or removed — a symlink into its own dependency cache, or a directory carrying its marker file. An installed package of the same name is left alone, and the link of a dependency the manifest no longer declares goes away. Where the platform refuses a symlink, such as an unprivileged Windows process without developer mode, gpui-shell writes a small package that re-exports the checkout instead: a bare import types the same way, and only a package-subpath import is left unresolved. A `jsconfig.json` is scaffolded when the directory has neither that nor a `tsconfig.json`, and it is written once — an existing configuration is never replaced. It is not decoration. An inferred `moduleResolution` can land on the one that never looks in `node_modules`, which underlines a dependency the runtime resolves fine; and the default `lib` hands a script the browser's globals, whose declarations collide with the ones `gpui-kit.d.ts` makes, so the file describing the API is itself reported as the error. `node_modules` is generated, like `gpui-kit.d.ts`. Ignore both: ```text gpui-kit.d.ts node_modules/ ``` The directory is called `node_modules` because that is the one place every editor looks; no package manager is involved and nothing comes from a registry. It also buys quiet: TypeScript treats what it resolves there as an external library, so a dependency's own implicit-`any` diagnostics stay out of your own. ## When fetching and linking happen | Invocation | Fetches and links | On failure | | ----------------------------------------------- | ----------------- | ---------------------------------------- | | `gpui-shell ` | Yes | Load fails; linking alone is best-effort | | `gpui-shell check ` | Yes | Reported as a check failure | | `gpui-shell types ` | Yes | Reported, with an exit status | | An embedded host's `ShellRuntime::load` | Yes | Load fails; linking alone is best-effort | | `gpui_kit::shell::write_dependency_links(root)` | Yes | Returned as an error to the caller | Fetching is what a load depends on, so a dependency that cannot be materialized fails the load. Writing the editor links is not: a read-only application directory is a reason to lose editor types, not a reason to refuse to run. `gpui-shell types` exists for exactly the case where that difference matters — it does the same work and reports what it could not do. Hot-reload picks up a package the same way it picks up an application file: each load is a new module generation, so restarting the application is enough to move a branch dependency forward. ## What can fail Every one of these is reported before the application's JavaScript is evaluated: | Message | Cause | | ---------------------------------------------------------- | ----------------------------------------------------------------- | | `GitHub shorthand must contain exactly owner/repository …` | A shorthand with a path, a scheme, or an invalid character | | `a string dependency #Git ref must not be empty` | A trailing `#` | | `could not clone Git dependency …` | Git failed: no such remote, no credentials, no network | | `git timed out after 30 seconds …` | A hung fetch, usually an interactive credential prompt | | `Git dependency … cache origin is …, expected …` | Two manifests disagree about one cache entry; remove it and retry | | `Git dependency … package.json main must be a string` | A `main` that is an object, or a path escaping the checkout | | `Git dependency … has no entry …` | `main`, or an object form's `entry`, names nothing | | `cannot resolve module … from …` | A subpath import with no such file, or one leaving the checkout | ## Publishing a shell package A shell package is a plain Git repository; `omarchy-ui` has no build output, no lockfile and no publish step. Beyond the five things that make it one, what makes it comfortable to depend on: * **A root `package.json` with `main`**, so consumers write one line and no `entry`. `"type": "module"` alongside it keeps the editor and the runtime agreeing that the source is ES modules. * **A single entry that re-exports the public surface.** `src/index.js` decides what a consumer can name; anything else in the checkout stays reachable by subpath, which is a useful escape hatch and a poor public API. * **Types beside the source**, through `types` in `package.json`. Generated `.d.ts` files or JSDoc both work, and both arrive at the call site through the link — a consumer's `jsconfig.json` needs no `paths` entry. * **Tags for releases**, so consumers can pin `#v1.2.0` instead of tracking `main`. * **A `gpui-shell` topic on the repository**, so someone looking for a shell package can find it. ## Read next | Page | What it covers | | --------------------------------------- | ------------------------------------------------------------------------- | | [Capabilities](./capabilities.md) | The rest of the manifest: identity, versions, and what a script may reach | | [Getting Started](./getting-started.md) | `gpui-shell types`, `check`, and the declarations a dependency joins | | [API Reference](./api.md) | The built-in modules a package imports alongside yours | --- --- url: /docs/components/description-list.md description: Use to display details with a tidy layout for key-value pairs. --- # DescriptionList A versatile component for displaying key-value pairs in a structured, organized layout. Supports both horizontal and vertical layouts, multiple columns, borders, and different sizes. Perfect for showing detailed information like metadata, specifications, or summary data. ## Import ```rust use gpui_kit::component::description_list::{DescriptionList, DescriptionItem, DescriptionText}; ``` ## Usage ### Basic Description List ```rust DescriptionList::new() .item("Name", "GPUI Kit", 1) .item("Version", "0.1.0", 1) .item("License", "Apache-2.0", 1) ``` ### Using DescriptionItem Builder ```rust DescriptionList::new() .children([ DescriptionItem::new("Name").value("GPUI Kit"), DescriptionItem::new("Description").value("UI components for building desktop applications"), DescriptionItem::new("Version").value("0.1.0"), ]) ``` ### Different Layouts ```rust // Horizontal layout (default) DescriptionList::horizontal() .item("Platform", "macOS, Windows, Linux", 1) .item("Repository", "https://github.com/longbridge/gpui-kit", 1) // Vertical layout DescriptionList::vertical() .item("Name", "GPUI Kit", 1) .item("Description", "A comprehensive Rust desktop framework", 1) ``` ### Multiple Columns with Spans ```rust DescriptionList::new() .columns(3) .child(DescriptionItem::new("Name").value("GPUI Kit").span(1)) .children([ DescriptionItem::new("Version").value("0.1.0").span(1), DescriptionItem::new("License").value("Apache-2.0").span(1), DescriptionItem::new("Description") .value("Full-featured UI components for desktop applications") .span(3), // Spans all 3 columns DescriptionItem::new("Repository") .value("https://github.com/longbridge/gpui-kit") .span(2), // Spans 2 columns ]) ``` ### With Separators ```rust DescriptionList::new() .item("Name", "GPUI Kit", 1) .item("Version", "0.1.0", 1) .separator() // Add a visual separator .item("Author", "Longbridge", 1) .item("License", "Apache-2.0", 1) ``` ### Different Sizes ```rust // Large size DescriptionList::new() .large() .item("Title", "Large Description List", 1) // Medium size (default) DescriptionList::new() .item("Title", "Medium Description List", 1) // Small size DescriptionList::new() .small() .item("Title", "Small Description List", 1) ``` ### Without Borders ```rust DescriptionList::new() .bordered(false) // Remove borders for a cleaner look .item("Name", "GPUI Kit", 1) .item("Type", "UI Library", 1) ``` ### Custom Label Width (Horizontal Layout) ```rust use gpui_kit::px; DescriptionList::horizontal() .label_width(px(200.0)) // Set custom label width .item("Very Long Label Name", "Short Value", 1) .item("Short", "Very long value that needs more space", 1) ``` ### Rich Content with Custom Elements ```rust use gpui_kit::component::text::markdown; DescriptionList::new() .columns(2) .children([ DescriptionItem::new("Name").value("GPUI Kit"), DescriptionItem::new("Description").value( markdown( "UI components for building **fantastic** desktop applications.", ).into_any_element() ), ]) ``` ### Complex Example with Mixed Content ```rust DescriptionList::new() .columns(3) .label_width(px(150.0)) .children([ DescriptionItem::new("Project Name").value("GPUI Kit").span(1), DescriptionItem::new("Version").value("0.1.0").span(1), DescriptionItem::new("Status").value("Active").span(1), DescriptionItem::Separator, // Full-width separator DescriptionItem::new("Description").value( "A comprehensive Rust desktop framework built on GPUI" ).span(3), DescriptionItem::new("Repository").value( "https://github.com/longbridge/gpui-kit" ).span(2), DescriptionItem::new("License").value("Apache-2.0").span(1), DescriptionItem::new("Platforms").value("macOS, Windows, Linux").span(2), DescriptionItem::new("Language").value("Rust").span(1), ]) ``` ## Examples ### User Profile Information ```rust DescriptionList::new() .columns(2) .bordered(true) .children([ DescriptionItem::new("Full Name").value("John Doe"), DescriptionItem::new("Email").value("john@example.com"), DescriptionItem::new("Phone").value("+1 (555) 123-4567"), DescriptionItem::new("Department").value("Engineering"), DescriptionItem::Separator, DescriptionItem::new("Bio").value( "Senior software engineer with 10+ years of experience in Rust and system programming." ).span(2), ]) ``` ### System Information ```rust DescriptionList::vertical() .small() .bordered(false) .children([ DescriptionItem::new("Operating System").value("macOS 14.0"), DescriptionItem::new("Architecture").value("Apple Silicon (M2)"), DescriptionItem::new("Memory").value("16 GB"), DescriptionItem::new("Storage").value("512 GB SSD"), DescriptionItem::new("GPU").value("Apple M2 10-core GPU"), ]) ``` ### Product Specifications ```rust DescriptionList::new() .columns(3) .large() .children([ DescriptionItem::new("Model").value("MacBook Pro").span(1), DescriptionItem::new("Year").value("2023").span(1), DescriptionItem::new("Screen Size").value("14-inch").span(1), DescriptionItem::new("Processor").value("Apple M2 Pro").span(2), DescriptionItem::new("Base Price").value("$1,999").span(1), DescriptionItem::Separator, DescriptionItem::new("Key Features").value( "Liquid Retina XDR display, ProMotion technology, P3 wide color gamut" ).span(3), ]) ``` ### Configuration Settings ```rust DescriptionList::horizontal() .label_width(px(180.0)) .bordered(false) .children([ DescriptionItem::new("Theme").value("Dark Mode"), DescriptionItem::new("Font Size").value("14px"), DescriptionItem::new("Auto Save").value("Enabled"), DescriptionItem::new("Backup Frequency").value("Every 30 minutes"), DescriptionItem::new("Language").value("English (US)"), ]) ``` ## Design Guidelines * Use horizontal layout for simple key-value pairs * Use vertical layout when values are lengthy or complex * Limit columns to 3-4 for optimal readability * Use separators to group related information * Keep labels concise and descriptive * Use consistent spacing with the size prop * Consider removing borders for embedded contexts --- --- url: /zh-CN/docs/components/description-list.md description: 用于以整齐布局展示键值对详情信息的组件。 --- # DescriptionList DescriptionList 是一个用于展示键值对的通用组件,支持横向和纵向布局、多列、分隔线和不同尺寸,适合展示元数据、规格信息和摘要数据。 ## 导入 ```rust use gpui_kit::component::description_list::{DescriptionList, DescriptionItem, DescriptionText}; ``` ## 用法 ### 基础列表 ```rust DescriptionList::new() .item("Name", "GPUI Kit", 1) .item("Version", "0.1.0", 1) .item("License", "Apache-2.0", 1) ``` ### 使用 DescriptionItem Builder ```rust DescriptionList::new() .children([ DescriptionItem::new("Name").value("GPUI Kit"), DescriptionItem::new("Description").value("UI components for building desktop applications"), DescriptionItem::new("Version").value("0.1.0"), ]) ``` ### 不同布局 ```rust DescriptionList::horizontal() .item("Platform", "macOS, Windows, Linux", 1) .item("Repository", "https://github.com/longbridge/gpui-kit", 1) DescriptionList::vertical() .item("Name", "GPUI Kit", 1) .item("Description", "A comprehensive Rust desktop framework", 1) ``` ### 多列和跨列 ```rust DescriptionList::new() .columns(3) .child(DescriptionItem::new("Name").value("GPUI Kit").span(1)) .children([ DescriptionItem::new("Version").value("0.1.0").span(1), DescriptionItem::new("License").value("Apache-2.0").span(1), DescriptionItem::new("Description") .value("Full-featured UI components for desktop applications") .span(3), DescriptionItem::new("Repository") .value("https://github.com/longbridge/gpui-kit") .span(2), ]) ``` ### 分隔线 ```rust DescriptionList::new() .item("Name", "GPUI Kit", 1) .item("Version", "0.1.0", 1) .separator() .item("Author", "Longbridge", 1) .item("License", "Apache-2.0", 1) ``` ### 不同尺寸 ```rust DescriptionList::new() .large() .item("Title", "Large Description List", 1) DescriptionList::new() .item("Title", "Medium Description List", 1) DescriptionList::new() .small() .item("Title", "Small Description List", 1) ``` ### 无边框 ```rust DescriptionList::new() .bordered(false) .item("Name", "GPUI Kit", 1) .item("Type", "UI Library", 1) ``` ### 自定义标签宽度 ```rust use gpui_kit::px; DescriptionList::horizontal() .label_width(px(200.0)) .item("Very Long Label Name", "Short Value", 1) .item("Short", "Very long value that needs more space", 1) ``` ### 富文本内容 ```rust use gpui_kit::component::text::markdown; DescriptionList::new() .columns(2) .children([ DescriptionItem::new("Name").value("GPUI Kit"), DescriptionItem::new("Description").value( markdown( "UI components for building **fantastic** desktop applications.", ).into_any_element() ), ]) ``` ### 混合内容示例 ```rust DescriptionList::new() .columns(3) .label_width(px(150.0)) .children([ DescriptionItem::new("Project Name").value("GPUI Kit").span(1), DescriptionItem::new("Version").value("0.1.0").span(1), DescriptionItem::new("Status").value("Active").span(1), DescriptionItem::Separator, DescriptionItem::new("Description").value( "A comprehensive Rust desktop framework built on GPUI" ).span(3), DescriptionItem::new("Repository").value( "https://github.com/longbridge/gpui-kit" ).span(2), DescriptionItem::new("License").value("Apache-2.0").span(1), DescriptionItem::new("Platforms").value("macOS, Windows, Linux").span(2), DescriptionItem::new("Language").value("Rust").span(1), ]) ``` ## 示例 ### 用户资料信息 ```rust DescriptionList::new() .columns(2) .bordered(true) .children([ DescriptionItem::new("Full Name").value("John Doe"), DescriptionItem::new("Email").value("john@example.com"), DescriptionItem::new("Phone").value("+1 (555) 123-4567"), DescriptionItem::new("Department").value("Engineering"), DescriptionItem::Separator, DescriptionItem::new("Bio").value( "Senior software engineer with 10+ years of experience in Rust and system programming." ).span(2), ]) ``` ### 系统信息 ```rust DescriptionList::vertical() .small() .bordered(false) .children([ DescriptionItem::new("Operating System").value("macOS 14.0"), DescriptionItem::new("Architecture").value("Apple Silicon (M2)"), DescriptionItem::new("Memory").value("16 GB"), DescriptionItem::new("Storage").value("512 GB SSD"), DescriptionItem::new("GPU").value("Apple M2 10-core GPU"), ]) ``` ### 产品规格 ```rust DescriptionList::new() .columns(3) .large() .children([ DescriptionItem::new("Model").value("MacBook Pro").span(1), DescriptionItem::new("Year").value("2023").span(1), DescriptionItem::new("Screen Size").value("14-inch").span(1), DescriptionItem::new("Processor").value("Apple M2 Pro").span(2), DescriptionItem::new("Base Price").value("$1,999").span(1), DescriptionItem::Separator, DescriptionItem::new("Key Features").value( "Liquid Retina XDR display, ProMotion technology, P3 wide color gamut" ).span(3), ]) ``` ### 配置项展示 ```rust DescriptionList::horizontal() .label_width(px(180.0)) .bordered(false) .children([ DescriptionItem::new("Theme").value("Dark Mode"), DescriptionItem::new("Font Size").value("14px"), DescriptionItem::new("Auto Save").value("Enabled"), DescriptionItem::new("Backup Frequency").value("Every 30 minutes"), DescriptionItem::new("Language").value("English (US)"), ]) ``` ## 设计建议 * 简单键值对优先使用横向布局。 * 值较长或结构较复杂时优先使用纵向布局。 * 列数尽量控制在 3 到 4 列以内。 * 使用分隔线对相关信息分组。 * 标签保持简洁且语义明确。 * 使用尺寸属性统一间距和密度。 * 嵌入式场景下可考虑关闭边框。 --- --- url: /docs/design-guides.md description: Product and interaction design guidance for GPUI Kit applications --- # Design Guides Use this guide before choosing components or writing layout code. It records the product judgment accumulated through years of GPUI Kit desktop work: an interface should feel native, restrained, precise, and understandable without guesswork. This is a normative guide. **Must** identifies a correctness or ecosystem constraint, **should** is the default that needs a concrete reason to override, and **may** is an optional technique. Component API documentation remains the authority for individual methods. The rules build on behavior in `gpui-base`, the GPUI Component theme and component system, and familiar desktop interaction. Shadcn contributes useful methods—open code, composition, and dependable defaults—but does not determine how a GPUI application should look. When influences conflict, preserve GPUI's lifecycle constraints and the interaction people already understand. ## Design thesis Build interfaces that feel native, quiet, and precise. Let content, hierarchy, and interaction carry the experience; decoration should support them rather than compete with them. 1. **Clarity before personality.** Make the primary task and next action clear before adding brand expression. 2. **Composition before invention.** Start with established components and compose them into product-specific workflows. Create a new primitive only when its behavior is genuinely new. 3. **Tokens before values.** Colors, radii, typography, and spacing should form a system. Avoid isolated literals that cannot respond to themes. 4. **Desktop before web convention.** Preserve keyboard access, window chrome, menus, dense data views, resizable regions, and persistent navigation where the task benefits from them. 5. **State must be visible.** Hover, focus, selection, disabled, loading, validation, and destructive states need distinct and consistent treatment. ## Learning from Shadcn Shadcn's most useful contribution is not a particular border color. It is a way of building a system: * own the top layer of the interface instead of fighting a sealed abstraction; * compose small, predictable parts into product-specific components; * provide defaults that already form one visual language; * keep the code and composition legible to both people and AI; * separate behavior primitives from the styled layer. GPUI Kit applies those ideas through a Rust library and the split between `gpui-base` and `gpui-component`. Applications normally compose or wrap the published components; contributors move genuinely reusable behavior into Base and keep visual policy above it. Do not copy these web assumptions blindly: | Web habit | Native GPUI default | | --- | --- | | Pointing-hand cursor on every button | Default arrow cursor; pointing hand for links | | Page navigation as the main structure | Persistent windows, panes, sidebars, tabs, and menus | | Browser focus and scrolling as a fallback | Explicit focus ownership and region-owned scrolling | | Mobile-first single column | Resizable desktop shell with a defined minimum window size | | Hover-revealed critical actions | Keyboard- and pointer-reachable actions that do not depend on hover | | A row of hover-only icon buttons | A visible primary action plus `DropdownMenu` or `ContextMenu` for secondary commands | | Link-styled text for application commands | `Button`, `outline`, or `ghost`; Link only for URLs, web resources, or email addresses | | Large touch density everywhere | Medium density by default; compact only where information work benefits | | CSS overrides across descendants | Typed builders, semantic parts, and application composition | ## Start from the task Before drawing a screen, write down: * the user's primary task; * the object being viewed or changed; * the actions that must remain immediately available; * the information required to make a decision; * empty, loading, error, offline, read-only, and permission-denied states; * the keyboard path through the workflow. Organize the window around those answers. Do not begin with a dashboard grid or a component catalogue. A good desktop interface exposes the user's mental model: documents, accounts, projects, messages, settings, or another stable object—not the internal service architecture. Design the primary task before distributing controls. Its visual weight, location, and information depth should match its importance to the product. A core result must not be reduced to a small count, an icon in a corner, or a weak footer action while secondary content consumes the page. When a result set is the product's main value, consider a summary region or card that exposes the count, representative results, meaningful state, and a clear next action. For every proposed action, name its visible object, current state, scope, and result. If the interface does not show or clearly imply those things, the action is premature. Do not expose a capability merely because the backend has it; first design how the object enters the user's mental model. ## Visual language ### Hierarchy Prefer a small number of clear levels: * **window or page title** identifies the current object or workspace; * **section title** separates meaningful regions; * **body text** carries the work; * **muted text** provides secondary metadata and help; * **labels** identify controls and values. Use size, weight, spacing, and separators before adding color or containers. Avoid nesting cards inside cards: most desktop regions need only a background, a hairline boundary, and intentional spacing. Evaluate hierarchy across the whole feature, not component by component. Hide accent color and decoration during review: the primary task, current selection, result summary, and next action should still be obvious from structure. A screen that contains individually plausible controls can still fail when they do not form one reading order and one decision path. Treat emphasis as a limited budget. A local surface needs one clear focal point, not a field of competing highlights. If everything is colored, badged, bold, boxed, or promoted to an alert, nothing reads as important. Establish priority with structure and proximity first; spend stronger color and components only where a distinction changes what the user notices or does. ### Color and themes Read colors from `cx.theme()` and use them by semantic role: * `background` and `foreground` for the main surface and text; * `group_box`, `popover`, `sidebar`, and their foreground tokens for their named surfaces; * `muted` and `muted_foreground` for supporting information; * `primary` for the principal action or selection emphasis; * `danger`, `warning`, `success`, and `info` only for their meanings; * `border`, `input`, and focus-ring tokens for structure and interaction. Do not use a semantic status color as decoration. Do not encode meaning by color alone. Verify every custom surface in light and dark themes and with custom theme values; never assume that foreground is black or background is white. Use Badge for a short state, count, or classification that benefits from rapid scanning—not for every label, metadata value, filter, or section title. Keep most badges neutral; reserve semantic variants for states that truly carry success, warning, danger, or informational meaning. A row of multicolored badges is usually a missing hierarchy or grouping decision. Application UI should not contain raw hex, `rgb`/`rgba`, or `hsla` colors. Resolve colors from `cx.theme()` by semantic role. If the required role does not exist, define it in the product's theme/token layer rather than embedding a palette value at the call site. Raw colors belong only inside theme definitions or in audited data/raster content whose color is itself the data. ### Radius, spacing, and density Derive corner radii from the active theme. This preserves a product's ability to become square or more rounded as one coherent system. Use `radius_full()` for circles and pills rather than a literal maximum radius. Use a compact spacing scale and repeat it. Related label/control pairs should be closer than separate groups; separate groups should be closer than separate sections. Prefer component sizes (`xsmall`, `small`, default medium, `large`) over one-off heights. Use compact variants for toolbars and data-dense screens, not to squeeze an unclear layout into less space. The shared semantic scale is intentionally small: spacing progresses through roughly 2, 4, 8, 12, 16, 24, and 32 pixels, while typography stays near 12, 14, 16, 18, and 20 pixels. Treat these as relationships rather than permission to scatter their current values through feature code. GPUI Component currently projects a fixed default `SpacingTokens` scale from its global `Theme`; unlike colors and radii, `Theme::apply_semantic_tokens` does not persist a custom spacing scale. An application that needs different spacing must own that full token snapshot and use it consistently in its application components. ### Spatial grammar Spacing expresses relationship. Choose a gap from the semantic scale by asking what the two things mean to each other: | Relationship | Typical token | Current scale | Examples | | --- | --- | --- | --- | | Optical correction | `xxs` | 2 px | icon baseline, compact separator | | Parts of one control | `xs` | 4 px | menu icon/label, title/description | | Closely related controls | `sm` | 8 px | button icon/label, dialog actions | | One content group | `md` | 12 px | notification columns, compact form rows | | Separate groups in one section | `lg` | 16 px | panel padding, form groups | | Separate sections | `xl` | 24 px | major blocks in a page or inspector | | Major region boundary | `xxl` | 32 px | empty-state breathing room, page bands | These values describe the current default scale, not literals to repeat. Use `cx.theme().spacing_tokens()` or the corresponding GPUI scale helpers for the ecosystem default. A product-owned scale should preserve the ordering and relationships and must be passed through the application's own design-system context rather than assumed to persist in the global GPUI Component theme. Use these rules when resolving horizontal and vertical space: 1. **Inside before outside.** A component's padding belongs to the component; the gap between components belongs to their parent. 2. **Vertical rhythm shows grouping.** The gap between a title and its description is smaller than the gap from that description to the next section. Equal gaps imply equal relationships. 3. **Horizontal space supports scanning.** Repeated rows keep icons, labels, values, badges, and trailing actions on stable columns. 4. **Leading and trailing are semantic.** Think in reading-order edges even when the current API uses left/right; this keeps future RTL adaptation possible. 5. **Do not double padding.** A card placed in an already padded panel should not automatically add another full panel inset. 6. **Use optical alignment sparingly.** A one- or two-pixel correction is valid for icon or glyph geometry, but document why it differs from the scale. Common compositions in the current system illustrate the relationships: * button contents use 4 px at small sizes and 8 px at normal sizes; * dialog headers and footers use an 8 px internal gap; * compact list and menu rows use 4 px vertical and 8–12 px horizontal padding; * sheet headers use about 16 px leading and 12 px trailing space, leaving room for a close affordance, while footers use 16 px horizontal and 12 px vertical space; * notifications use 16 px horizontal padding and a 12 px column gap because icon, message, and action are distinct groups. Do not treat these as copy-and-paste recipes for every surface. They reveal the system: controls are tighter internally, rows optimize scanning, and containers spend more space at their boundary than between their contents. ### Proportion and layer hierarchy Start with content requirements, then set proportions. Avoid arbitrary halves when one pane has a clearly different role. * A navigation sidebar should be wide enough for stable labels but visibly subordinate to the work area. Give it a minimum, preferred, and maximum width rather than a percentage alone. * In master–detail layouts, let the collection remain scannable and give the detail pane the surplus. A roughly one-third/two-thirds starting point is often useful, but content constraints are authoritative. * Inspectors and auxiliary sheets should not cover the primary object by default. They should be resizable or dismissible when their content grows. * Dialog width comes from the decision: short confirmation, medium form, or a dedicated window/page for complex work. Do not enlarge a dialog simply to create whitespace. * Reserve the strongest elevation for the topmost decision layer. Within a layer, use background and hairlines—not successively larger shadows—to show hierarchy. Define three size constraints for every major region: the minimum at which its task still works, a comfortable default, and how it consumes surplus. Persist user-controlled splits when they represent workflow preference, and clamp restored values against the current window. ### Alignment details Alignment is a structural system, not a final polish pass. Establish a small set of alignment spines for each surface: shared leading and trailing edges, text baselines, center lines, and fixed functional lanes. Elements at the same level should attach to the same spine from top to bottom or leading to trailing, even when they are different component types. Vertical red lines sit beside shared edges or control centers for content, status, time, and trailing actions. Horizontal lines sit beneath text baselines or pass through a row center to show bottom and vertical-center alignment. The compact comparison isolates a one-rendered-pixel drift that must be corrected at its structural owner. * Give sibling regions a shared content inset. A heading, toolbar, list row, empty state, and footer that describe the same level should not each invent a slightly different leading edge. * Repeat column geometry through the whole region. Headers, rows, summaries, loading states, and inline editors should reserve the same lanes for identity, metadata, status, numbers, and actions. * Align related controls across rows and sections. Form labels, fields, descriptions, and validation messages should reveal a stable vertical grid when the page is scanned from top to bottom. * Keep horizontal bands coherent. Items sharing a toolbar, title bar, status bar, or row should use one baseline or center line instead of individually tuned offsets. * Introduce indentation only for real hierarchy, containment, or disclosure. Decorative indentation makes siblings look subordinate and breaks the surface's reading line. * When a nested level ends, return exactly to the parent spine. Do not let accumulated padding drift across nested containers. * Preserve the spine through optional content. Missing icons, badges, descriptions, or trailing actions must not move the remaining labels; use intentional slots or lanes when cross-row comparison matters. * Align major regions with one another where their hierarchy matches. Sidebar headers, content titles, split panes, toolbars, and bottom bars need not share every coordinate, but coincident levels should form visible continuous lines. Not every edge should align. A child can indent, a primary value can lead its supporting metadata, and a destructive decision can gain separation. Such exceptions must communicate hierarchy or meaning; they must not result from uncoordinated component padding. Start with the shared spine, then make the exception explicit. Treat exact alignment and repeated gaps as quality invariants. When two edges or spaces are intended to be equal, a one-rendered-pixel difference is a defect, not an acceptable optical approximation. Inspect resolved bounds with a measurement tool at representative window sizes, zoom levels, and display scale factors. Compare coordinates and distances; do not approve alignment only from a casual screenshot. The rendered-pixel tolerance is a verification rule, not permission to patch the code with raw pixel offsets. Equal relationships should resolve from the same `rem` helper, spacing token, grid definition, or shared component inset. Fix the common owner when they differ. Account for fractional layout and device rounding so intended spines land on the same physical pixel instead of drifting at particular zoom levels. * Align text by baselines, not by bounding-box centers, when mixed sizes share a row. * Center icons in a fixed slot so labels do not move when icons differ in intrinsic width. * Right-align comparable numbers; left-align prose and identifiers unless the locale requires otherwise. * Keep trailing row actions and disclosure indicators in fixed-width lanes. * Align form controls by their interactive frame, not by help text below them. * Use `justify_between` only when the two sides truly own opposite edges; it should not disguise missing structure in the middle. * Hairlines belong on the boundary owner. Two adjacent regions must not each draw the same separator. * A scrollbar belongs to the region that scrolls and sits against that panel, editor, or window's trailing edge. Content padding may inset text and rows; it must not pull the scrollbar into the middle of the surface. Reserve a deliberate scrollbar gutter when content needs clearance. ### Density tiers Medium is the ecosystem default. Change density for the whole local context, not one isolated control: * **comfortable / large:** onboarding, sparse forms, prominent decisions; * **standard / medium:** most application chrome and workflows; * **compact / small:** toolbars, menus, tables, and repeated professional data; * **extra compact / xsmall:** exceptional high-density utilities, never the automatic choice for an entire application. The current controls demonstrate a bounded scale rather than arbitrary sizing: buttons commonly move through approximately 20, 24, and 32 px frames; input and data controls may extend to about 44 px at large size; table rows use about 26, 30, 32, and 40 px. Use the component's `Size` API so typography, icon, padding, and hit target change together. A custom height that changes only the outer box is usually incomplete. ### Zoom, base font, and `rem` A well-designed `rem` system preserves hierarchy while the interface zooms. Zoom is successful when the relationship between title and body, control and icon, inner and outer spacing, primary and secondary regions still feels the same at every scale—not merely when every object becomes larger. GPUI Component adopts the relative-scale idea familiar from Tailwind. The theme's base `font_size` becomes the window's `rem` through `Root`, and GPUI scale helpers such as `text_sm()`, `gap_2()`, `p_4()`, `h_8()`, and `size_4()` resolve against it. This gives typography, spacing, controls, and icons one shared zoom axis. Design in ratios: * type steps keep the same hierarchy around the base body size; * spacing steps keep the same grouping relationships around the type; * control frames, icons, and hit targets scale with their labels; * pane minima and comfortable widths account for the scaled content; * corner radii and focus treatment remain optically consistent with the control frame. Do not implement zoom by changing text size alone. A larger label inside a fixed-height button, a larger document inside fixed pane minima, or larger rows inside a stale virtual-list measurement destroys the original rhythm and can clip content. Conversely, multiplying every physical pixel—including hairlines—can make the interface visually heavy. As a rule, application layout should not call `px(...)` directly. Use GPUI's rem-based scale helpers (`p_2`, `gap_3`, `w_64`, `text_sm`, and related builders) or semantic component sizes. Use fixed pixels only when the value represents a physical or raster boundary: a one-device-pixel hairline, platform window inset, bitmap dimension, minimum hit-test tolerance, or geometry that must match an external surface. These are audited, documented exceptions. Product spacing, typography, icon size, and ordinary control geometry stay on the relative scale. Test interface zoom at several base-font values, not just the default. Verify hierarchy, wrapping, truncation, minimum window size, pane resizing, focus-ring clearance, popup placement, and virtualized row measurement. Also distinguish interface zoom from Dock's panel zoom: Dock zoom makes one container fill its area while retaining its chrome; it does not change `rem` or application scale. ### Surfaces and elevation Use elevation to explain stacking, not importance. The base window surface is flat; separators and background contrast define its regions. Popovers, menus, dialogs, and notifications may use progressively stronger shadows because they sit above other content. Do not put a shadow on every card. All surfaces of the same kind should share one treatment. GPUI Component, for example, deliberately gives popup families one themed popover surface so Popover, Select, Combobox, DatePicker, and menus do not drift apart. When an application invents another anchored surface, reuse that semantic treatment instead of approximating it with unrelated border and shadow literals. ### Typography and icons Use the platform UI font for interface text and monospace only for code, identifiers, shortcuts, and aligned numeric data. Keep body text readable and avoid excessive uppercase or letter spacing, especially for CJK text. Use one icon family in a product. Icons supplement labels; they should not replace unfamiliar actions with guesswork. Icon-only buttons require a tooltip and an accessible name. Use filled or colored icons to communicate a state, not merely to make a toolbar lively. ## Layout patterns ### Choose a stable shell Most applications should use one of these shells: * **single workspace:** toolbar or title bar above one primary view; * **sidebar workspace:** persistent navigation beside a changing detail view; * **master–detail:** resizable collection and detail panes; * **document workspace:** tabs or a dock area for multiple long-lived objects; * **utility window:** one focused task with a short, fixed action path. Keep global navigation stable while content changes. Give the primary work area the remaining space with `flex_1()` and `min_w_0()` / `min_h_0()` where overflowing children must shrink. Use `Scrollable`, `VirtualList`, `Table`, or `DockArea` for their intended behavior instead of rebuilding scrolling or pane management from nested `div`s. ### Responsive desktop windows Desktop does not mean fixed-size. Decide what happens as a window narrows: 1. preserve the primary task; 2. allow resizable regions to reach a documented minimum; 3. collapse secondary labels or inspectors; 4. move low-frequency actions into a menu; 5. scroll only the region whose content actually overflows. Do not hide an action without providing another path to it. Avoid making the entire window scroll when only a list or document body should scroll. GPUI flex layouts have the same intrinsic-size pressure found in other layout systems: a `flex_1()` child may still refuse to shrink around long content. Design and implementation must agree on which panes may shrink, truncate, wrap, or scroll. A clipped region also clips an outward focus ring; never trade away keyboard visibility merely to simplify overflow. ### Forms and settings Use a visible label for each field and place help or validation next to the field it describes. Align related fields, but do not force long labels into a narrow fixed column. Use the appropriate control: `Checkbox` for independent choices, `RadioGroup` for a small visible set, `Select` for a longer set, and `Switch` for a setting that takes effect immediately. Disable submission while an operation is in flight, keep the user's input, and show the result near the action. Reserve dialogs for short, focused decisions; use a full page or sheet for workflows that need exploration or many fields. ## Components and composition Follow the Shadcn principle that components are building material rather than a sealed design system. GPUI Component supplies coherent defaults, while the application owns composition and product semantics. * Use component variants by meaning. Primary is reserved for the explicit default commit in a decision area—normally the action invoked by Enter. A lone, frequent, or desirable action is not automatically primary. An `Add` command in a management toolbar normally uses a default Button; a form's default `Create` commit may use primary. Use `danger` for destructive commitment and `ghost` for quiet toolbar actions. * Prefer explicit compound parts and render callbacks over styling arbitrary descendants. * Keep a repeated pattern consistent across the product. Wrap it in an application component when it carries domain language or policy. * Use the standard component for its semantic role. A menu, dropdown menu, popover, select, and command palette are not interchangeable boxes; each owns different selection, focus, keyboard, dismissal, and layout contracts. * Preserve the component family's geometry. Menu rows share vertical and horizontal padding, height, icon and checkmark slots, separators, radius, and state treatment. Do not imitate one menu with a custom popup whose spacing only approximates the system. * Do not wrap a library component merely to rename every method or freeze all of its capabilities. * Move reusable behavior without product styling to `gpui-base`; keep themed, opinionated presentation in GPUI Component or the application. ## Interaction states ### Make the result understandable before the click A control should predict its result. Use familiar desktop controls and placement so people can act without learning the interface first. Its label names the action and object, its state shows availability, and its feedback confirms the same outcome. Do not label a Button `Save` if it opens a configuration flow, or `Delete` if it only removes an item from a group. Name the scope when context does not make it clear. Respond immediately to activation, prevent duplicate submission during longer work, and show the result near the object that changed. Add a success message only when the result itself is not visible. Every interactive control should be designed for: | State | Design requirement | | --- | --- | | Rest | Clear affordance without visual noise | | Hover | Subtle pointer feedback, never the only cue | | Pressed | Immediate press feedback | | Open / pressed | Persistent feedback while an attached popup is open | | Focus visible | High-contrast keyboard focus ring | | Selected / checked | Persistent state distinct from hover | | Disabled | Lower emphasis and no misleading hover/pressed response | | Loading | Preserve context, prevent duplicate action, explain long waits | | Error | State what happened and how to recover | Use GPUI's focus system and Actions for commands that should work from the keyboard. Match familiar desktop shortcuts, expose shortcuts in menus or tooltips, and keep focus in a logical place after opening or dismissing an overlay. Selection is part of the information model, not optional polish. Tabs, segmented choices, selectable rows, filters, and navigation destinations must show a persistent selected state. A Button that owns a dropdown must remain visibly pressed or open until the popup closes; hover alone cannot explain the relationship between trigger and surface. For destructive actions, distinguish between reversible and irreversible work. Prefer undo or a temporary notification for reversible changes. Use an `AlertDialog` when the consequence is serious and cannot be undone; name the specific object and consequence in the confirmation copy. ### Pointer conventions Use the default arrow cursor for buttons, checkboxes, menu items, tabs, and other native controls. Use a pointing hand for links and content that behaves as a link. Use text, resize, grab, and prohibited cursors only when they describe the active manipulation. A cursor reinforces an affordance; it does not replace the control's visible state or accessible role. Keep hover effects modest because keyboard and accessibility interaction has no hover. Do not reveal the only copy of a destructive or essential action on hover. Contextual row actions may become quieter at rest if the same commands remain available through selection, keyboard, or a context menu. ### Prefer desktop command surfaces over hover toolbars Use command frequency and scope to choose where an action lives: * keep the primary or frequent action visible as a labeled Button or familiar toolbar control; * put secondary actions for the current region behind a visible `DropdownMenu` trigger; * put commands that act on the object under the pointer in a `ContextMenu`; * expose the same important command through an Action/key binding when it has a natural keyboard form; * use a hover-revealed icon only as a shortcut to a command that remains reachable elsewhere. This is more than a visual preference. GPUI Component's menu system already owns directional keyboard navigation, confirmation and cancellation, disabled items, separators, submenus, shortcut presentation, focus transfer and restoration, and nested-menu dismissal. A custom strip of hover buttons must rebuild those behaviors and is invisible to keyboard-only and many assistive technology workflows. Choose `DropdownMenu` when users need a visible indication that more commands exist—for example a toolbar overflow, document actions, or account menu. Choose `ContextMenu` for selection- or object-scoped commands such as rename, duplicate, reveal, or remove. The context menu must not be the only way to perform an essential command; provide a menu-bar, toolbar, keyboard, or detail view path as appropriate. Do not put every action into a menu to make a screen look minimal. Discovery and speed matter: the main action stays visible, dangerous items remain clearly labeled and separated, and a menu item should use the same verb, icon, shortcut, enabled state, and result everywhere it appears. ### Button means application action; Link means external resource Use a Button when activation changes application state, confirms a decision, opens a tool, submits data, or runs a command. Choose its treatment by local hierarchy: * primary Button for the one emphasized commitment in a decision area; * default Button for ordinary visible actions; * outline Button when an action needs a clear boundary with less emphasis; * ghost Button for familiar, low-emphasis toolbar and inline actions; * icon Button only for a well-known symbol, with an accessible name and tooltip. Do not assign primary because a Button is the only action on screen, because it is placed at the top right, or because the team wants more clicks. Primary communicates default commitment and keyboard behavior. If activation is merely an ordinary command such as adding an item, opening a tool, or refreshing a view, use a default, outline, or ghost Button according to its local hierarchy. Use an underlined Link only for an external resource target: a URL, web page, online documentation, or email address. It uses the pointing-hand cursor because its contract is leaving the current application context for that resource. Do not use Link styling to make a functional command look quiet. A link-shaped Delete, Save, Refresh, Add, Open-menu, or in-app navigation action hides the control's affordance and exposes the wrong accessibility role. “View” does not make an in-app destination a Link. A full report, analysis, details panel, or local record still opens through a Button, row, card, tab, or disclosure control. Use concise context-aware labels such as `Full analysis` when the containing card already establishes what opens; reserve underlining for a resource that actually opens in a browser or mail client. All internal navigation—sidebar rows, tabs, breadcrumbs, list items, opening a local view, or switching workspaces—must use the corresponding native component or a Button/Action. Visual emphasis is chosen through Button variant or the navigation component's selected state, never by lying about semantics. ## Feedback and overlays Choose the smallest surface that fits the decision: * tooltip: a short explanation or shortcut; * popover: contextual controls that do not interrupt the task; * menu: a compact list of actions; * notification: asynchronous status that does not require a decision; * dialog: a focused decision or short form; * alert dialog: explicit confirmation of a consequential action; * sheet: supplementary work that benefits from more persistent space. An Alert interrupts the visual hierarchy even when it does not open a modal. Use it for important, exceptional information that needs attention in the current task, not as a decorated container for ordinary descriptions, tips, or empty space. Prefer inline help, muted text, or a normal section when the content does not require immediate notice or action. Avoid stacking overlays. Escape should dismiss the topmost dismissible layer, and focus should return to the trigger or the next logical target. An overlay action must refer to an object or state the overlay actually shows. For example, expose `Clear history` only when a distinct recent-history section is visible and contains entries. Search results, recent items, and favorites are different collections; label and separate them instead of merging them into one unexplained list. Hide an inapplicable action or disable it with a useful reason—do not park an ambiguous trash icon in a footer. Footer space is not a catch-all for capabilities that lacked a place in the design. A footer may present shortcuts, status, or actions that apply to the whole surface, but each item must answer: what is its object, why is it available now, what scope does it affect, and what visible state changes after activation? ## Motion Motion explains change; it is not ambient decoration. Use short transitions for appearance, dismissal, expansion, and spatial continuity. Avoid animating large layout changes when opacity or transform communicates the same relationship. Honor reduced-motion preferences, never require animation to understand state, and do not add a default animation to every component. Motion policy belongs to the styled or application layer. Base may own the lifecycle mechanism or geometry needed for a transition, but it should not decide that every product fades or slides. Give independently animated values stable identity, and make interruption reverse smoothly from the currently sampled value rather than restarting from an old endpoint. ## Designing data-heavy interfaces Dense does not mean cramped. In tables, trees, command palettes, editors, and docks: * keep headers and primary row identity visually stable; * align comparable values and use tabular numerals where appropriate; * distinguish focus, hover, active row, and multi-selection; * keep sorting and filtering visible and reversible; * preserve selection by domain identity across filtering and reordering; * virtualize large collections without changing keyboard semantics; * use progressive disclosure for secondary columns and inspectors; * provide a useful empty state that explains the next action. Choose a table for comparison across consistent fields, a list for scanning heterogeneous items, a tree for real hierarchy, and a dock only when users need to arrange long-lived tools or documents. Do not use a complex data component as a visual style. ## Interface language Words are part of the interface architecture. Write the vocabulary for a feature as a system—destinations, objects, commands, states, and outcomes—not as isolated translations of implementation features. Prefer the shortest wording that remains accurate in its actual context. ### Let context carry context Do not repeat information that the surrounding surface already establishes. A sidebar destination is usually the object or domain itself: use `Users`, not `User Management`; `Shortcuts`, not `Shortcut Configuration Management`. A column whose rows already contain actions can omit a generic `Operation` heading. A dialog titled `Delete “Roadmap”?` does not need body text that asks the same question again. This is context economy, not deletion for its own sake. Add text when it changes the decision: identify the affected scope, an irreversible consequence, an unusual prerequisite, or a way to recover. Every extra word should answer a question the current layout does not already answer. Use nouns for destinations and objects (`Users`, `Appearance`, `Orders`), verbs for commands (`Save`, `Duplicate`, `Export`), and adjectives or short phrases for states (`Offline`, `Up to date`, `Pending review`). Avoid wrappers such as `Management`, `Module`, `Page`, `Function`, `Operation`, and `System` unless the word distinguishes a real domain concept. ### Write each language, do not translate its shape Start from shared intent, hierarchy, and terminology, then compose each locale as natural interface language. Do not preserve the source language's word order, number of words, politeness filler, or grammatical category. English `Users` can express a Chinese feature concept that would literally expand to “user management”; fidelity means preserving purpose, not preserving tokens. Remove words supplied by the enclosing information architecture. Inside a `Settings` surface, a destination is often simply `Account`, not `Account Settings` and never the unnatural singular `Account Setting`. The correct English label is chosen from its role and neighbors, not from the standalone source phrase. Maintain a small product lexicon for recurring objects, commands, and states. Use the same term in the toolbar, menu, context menu, dialog, shortcut search, and documentation unless the context genuinely changes its meaning. Review copy in the rendered surface: neighboring labels often reveal repetition or inconsistent scope that a locale file cannot. In localized technical writing, preserve an established framework term when a translation would be less precise. Keep API identifiers in their original form and format them as code. Do not retain ordinary foreign words merely to sound technical. Explain a retained term on first use when needed, then use the same form throughout the interface, documentation, and API examples. ### Buttons and confirmation dialogs Button labels are short by default—usually one or two words—and describe the result, not the gesture or the component. Prefer `Save`, `Move`, or `Delete` to `Click to save`, `Perform move`, or `Confirm deletion`. Use `Cancel` consistently for the action that leaves without committing. Reserve `OK` for acknowledging purely informational content. Short is a default, not a character limit. A deliberately longer label is better when its words expose a consequence or distinguish choices that users could otherwise confuse, for example `Delete from this group` versus `Delete everywhere`, or `Restart without saving`. Length must buy decision-critical information; it must not restate the dialog title or body. Use the most specific concise result as the confirmation label when possible: | Context | Weak | Prefer | | --- | --- | --- | | Delete dialog | `Yes`, `Sure`, `Confirm deletion` | `Delete` | | Unsaved changes | `Confirm`, `Yes` | `Discard changes` | | Pure acknowledgement | `Confirm operation` | `OK` or `Done` | | Complex consent whose result has no clear verb | `Yes` | `Confirm` | `Confirm` is a useful fallback when the surrounding dialog fully names a complex commitment and no shorter result verb is accurate. It should not replace a clear command. `Sure` is conversational rather than a stable English command and is too ambiguous for the standard vocabulary. A confirmation dialog should form one compact decision: * title: the decision or condition, such as `Delete “Roadmap”?`; * body: only new scope, consequence, or recovery information; * actions: `Cancel` and the result, such as `Delete`; * destructive styling: applied to the destructive result, not substituted for precise wording. Avoid generic titles such as `Notice`, `Warning`, `Error`, and `Confirmation` when the actual condition can be named. Avoid ritual phrases such as “Are you sure you want to…”, “Would you like to…”, “Please note that…”, and “successfully” when the structure or state already communicates them. Courtesy should come from a calm, respectful tone, not repeated `please`. ### Capitalization, punctuation, and symbols Use sentence case for English UI by default: `Reset layout`, not `Reset Layout` or `RESET LAYOUT`. Preserve proper nouns and established acronyms. Follow a platform convention such as title case for native menu commands only when the platform integration benefits from it, and apply that convention consistently within the component class. ALL CAPS can provide restrained typographic emphasis for very short section labels, eyebrows, statuses, established acronyms, and code-like identifiers. Its compact shape and measured tracking can form a level similar to bold type, but it does not belong on Buttons, long headings, sentences, or dense lists. Do not combine uppercase, strong color, and bold weight in the same region, and do not transform every string automatically: product names, acronyms, and localized content must preserve their intended casing. Labels, buttons, menu items, tabs, headings, placeholders, and short states do not take a final period. Complete explanatory, warning, and error sentences do. Avoid exclamation marks in routine success and failure messages. In Chinese, use full-width punctuation in sentences and omit terminal punctuation from short control labels by the same semantic rule. Use the single ellipsis character (`…`), not three periods. Append it to every Button or MenuItem that opens a dialog, sheet, or separate window, and to a command that requires more input or choices before it can complete, such as `Settings…` or `Export…`. An immediately executed command does not take an ellipsis. Use an indeterminate progress indicator, not decorative dots, to communicate ongoing work. Errors should say what happened and, when useful, the next recovery action. Success feedback should name the resulting state only when that state is not already visible. Prefer `Couldn’t save. Check your connection and try again.` to a technical code or a long apology; omit a `Saved successfully` toast when the document visibly becomes saved. ## Internationalization and platform fit Copy must survive expansion, CJK typography, and different shortcut notation. Do not size a control from one English label. Keep text out of raster assets, avoid concatenating translated fragments, and let labels wrap or truncate only where the product defines a recovery path such as a tooltip. Respect platform differences that carry meaning: Command versus Control, native window decorations, system appearance, scrollbar behavior, menus, and notification capabilities. Keep the product's information architecture stable across platforms, but do not erase familiar platform behavior for superficial pixel equality. ## Guidance for AI-generated interfaces An AI changing a GPUI interface should first inspect the nearest feature, theme tokens, and component documentation. It should state the primary task, state owner, component composition, and keyboard path before generating code. It must not infer an API from React/Shadcn examples or invent a GPUI method because the name seems plausible. AI output is incomplete until a human can explain why the hierarchy, density, component choice, and exceptional literal values belong in this product. A visually plausible screenshot is not proof: keyboard behavior, focus, dynamic content, themes, resizing, and failure states are part of the design. ## Accessibility checklist Before considering a screen complete, verify that: * every action is reachable and operable by keyboard; * focus order follows visual and task order; * focus remains visible and is restored after overlays; * controls have names, and icon-only controls have tooltips; * text and meaningful boundaries have sufficient contrast; * status is not communicated by color alone; * disabled and read-only states are distinguishable; * labels, errors, and descriptions remain near their controls; * content remains usable with longer translations and larger text; * pointer targets are comfortably sized even in a dense layout. ## Design review checklist A review does not inventory components; it judges whether the interface made the right decisions. Ask, in order: 1. **Is the task clear?** Can a new user recognize the purpose, primary action, and next step without learning, guessing, or experimenting? 2. **Does every action keep its promise?** Do the label, control, state, scope, feedback, and result describe one consistent outcome? 3. **Is hierarchy decisive and restrained?** Does the core feature receive the space it deserves while strong color, bold type, badges, alerts, and primary Buttons remain scarce? 4. **Could the interface do less, better?** Can an entry point, option, or state be removed, combined, or deferred without weakening the complete task? 5. **Is the structure exact?** Do peers share alignment spines, equal gaps stay equal to the rendered pixel, and scrollbars sit at the edge of their actual scrolling region? 6. **Does it follow the component system?** Do standard controls retain their geometry, states, keyboard behavior, and dismissal model, with appearance supplied by theme and scale tokens? 7. **Does it remain usable in every state and constraint?** Verify keyboard and focus behavior, empty/loading/failure/permission states, longer translations, zoom, minimum window size, and reduced motion. 8. **Has it been tested in a real window?** Complete the task with real components, copy, and representative content—not only an ideal screenshot. Continue with [Coding Guides](./coding-guides.md) to translate these design decisions into GPUI architecture and code. --- --- url: /base/primitives/dialog.md description: >- A composable modal surface with focus management, backdrop, title, and close parts. --- # Dialog A composable modal surface with focus management, backdrop, title, and close parts. Like every `gpui-base` primitive, Dialog supplies behavior and semantic structure without imposing a product visual language. Apply GPUI styles and compose the exported parts to match your design system. ## Example The [single native Cargo entrypoint](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/components.rs) selects this primitive from the [shared showcase implementation](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/mod.rs). The same showcase is compiled once for the WASM preview above. ```bash cargo run -p gpui-base --example components -- dialog ``` ## Import ```rust use gpui_kit::base::{Dialog, DialogBackdrop, DialogClose, DialogDescription, DialogPopup, DialogTitle, DialogTrigger}; ``` ## Anatomy and API The example composes `Dialog`, `DialogBackdrop`, `DialogClose`, `DialogDescription`, `DialogPopup`, `DialogTitle`, `DialogTrigger`. GPUI's standard styling and event traits provide presentation; these base types provide the interaction structure. The authoritative module is [`components/dialog.rs`](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/components/dialog.rs). Native and browser previews compile this same file. ## State and events `Dialog` manages modal presentation and dismissal; application callbacks own submitted work. Keep controlled state on the parent render type or in a GPUI entity. Update it in callbacks and call `cx.notify()`; do not recreate persistent entities during every render. ## Complete Rust example The complete implementation used by the runnable showcase is embedded directly from Rust source: The command above supplies application initialization, window creation, and shared `BaseShowcase` state. ## Accessibility Provide title, initial and return focus, a focus trap, Escape policy, and explicit close action. ## Notes Use stable element IDs where accepted. Verify focus, hover, active, selected, disabled, reduced-motion, and high-contrast appearances in the consuming design system. --- --- url: /docs/components/dialog.md description: A dialog dialog for displaying content in a layer above the app. --- # Dialog Dialog component for creating dialogs, confirmations, and alerts. Supports overlay, keyboard shortcuts, and various customizations. ## Import ```rust use gpui_kit::component::dialog::DialogButtonProps; use gpui_kit::component::WindowExt; ``` ## Usage ### Setup application root view for display of dialogs You need to set up your application's root view to render the dialog layer. This is typically done in your main application struct's render method. The [Root::render\_dialog\_layer](https://docs.rs/gpui-component/latest/gpui_component/struct.Root.html#method.render_dialog_layer) function handles rendering any active dialogs on top of your app content. ```rust use gpui_kit::component::TitleBar; struct MyApp { view: AnyView, } impl Render for MyApp { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let dialog_layer = Root::render_dialog_layer(window, cx); div() .size_full() .child( v_flex() .size_full() .child(TitleBar::new()) .child(div().flex_1().overflow_hidden().child(self.view.clone())), ) // Render the dialog layer on top of the app content .children(dialog_layer) } } ``` ### Basic Dialog ```rust window.open_dialog(cx, |dialog, _, _| { dialog .title("Welcome") .child("This is a dialog dialog.") }) ``` ### Form Dialog ```rust let input = cx.new(|cx| InputState::new(window, cx)); window.open_dialog(cx, |dialog, _, _| { dialog .title("User Information") .child( v_flex() .gap_3() .child("Please enter your details:") .child(Input::new(&input)) ) .footer(|_, _, _, _| { vec![ Button::new("ok") .primary() .label("Submit") .on_click(|_, window, cx| { window.close_dialog(cx); }), Button::new("cancel") .label("Cancel") .on_click(|_, window, cx| { window.close_dialog(cx); }), ] }) }) ``` ### Dialog with Icon ```rust window.open_dialog(cx, |dialog, _, cx| { dialog .child( h_flex() .gap_3() .child(Icon::new(IconName::TriangleAlert) .size_6() .text_color(cx.theme().warning)) .child("This action cannot be undone.") ) }) ``` ### Scrollable Dialog ```rust use gpui_kit::component::text::markdown; window.open_dialog(cx, |dialog, window, cx| { dialog .h(px(450.)) .title("Long Content") .child(markdown(long_markdown_text)) }) ``` ### Dialog Options ```rust window.open_dialog(cx, |dialog, _, _| { dialog .title("Custom Dialog") .overlay(true) // Show overlay (default: true) .overlay_closable(true) // Click overlay to close (default: true) .keyboard(true) // ESC to close (default: true) .close_button(false) // Show close button (default: true) .child("Dialog content") }) ``` ### Nested Dialogs ```rust window.open_dialog(cx, |dialog, _, _| { dialog .title("First Dialog") .child("This is the first dialog") .footer(|_, _, _, _| { vec![ Button::new("open-another") .label("Open Another Dialog") .on_click(|_, window, cx| { window.open_dialog(cx, |dialog, _, _| { dialog .title("Second Dialog") .child("This is nested") }); }), ] }) }) ``` ### Custom Styling ```rust window.open_dialog(cx, |dialog, _, cx| { dialog .rounded(cx.theme().radius_lg) .bg(cx.theme().cyan) .text_color(cx.theme().info_foreground) .title("Custom Style") .child("Styled dialog content") }) ``` ### Custom Padding ```rust window.open_dialog(cx, |dialog, _, _| { dialog .p_3() // Custom padding .title("Custom Padding") .child("Dialog with custom spacing") }) ``` ### Close Dialog Programmatically The `close_dialog` method can be used to close the active dialog from anywhere within the window context. ```rust // Close top level active dialog. window.close_dialog(cx); // Close and perform action Button::new("submit") .primary() .label("Submit") .on_click(|_, window, cx| { // Do something window.close_dialog(cx); }) ``` ## Declarative API The Dialog component now supports a declarative API that provides a more React-like component composition pattern using dedicated header, title, description, and footer components. ### Import ```rust use gpui_kit::component::dialog::{ Dialog, DialogHeader, DialogTitle, DialogDescription, DialogFooter, }; ``` ### Trigger-based Dialog The trigger-based approach allows you to create a dialog that opens when a trigger element is clicked. The dialog is defined inline with the trigger. ```rust Dialog::new(cx) .trigger( Button::new("open-dialog") .outline() .label("Open Dialog") ) .content(|content, _, cx| { content .child( DialogHeader::new() .child(DialogTitle::new().child("Account Created")) .child(DialogDescription::new().child( "Your account has been created successfully!", )) ) .child( DialogFooter::new() .border_t_1() .border_color(cx.theme().border) .bg(cx.theme().muted) .child( Button::new("cancel") .outline() .label("Cancel") .on_click(|_, window, cx| { window.close_dialog(cx); }) ) .child( Button::new("ok") .primary() .label("Save Changes") ) ) }) ``` ### Content Builder Pattern Use the content builder pattern with `window.open_dialog` for more control over dialog creation: ```rust window.open_dialog(cx, |dialog, _, _| { dialog .w(px(400.)) .content(|content, _, _| { content .child( DialogHeader::new() .child(DialogTitle::new().child("Custom Width")) .child(DialogDescription::new().child( "This dialog has a custom width of 400px.", )) ) .child(div().child( "Content area with custom width configuration." )) .child( DialogFooter::new() .justify_center() .child( Button::new("cancel") .flex_1() .outline() .label("Cancel") .on_click(|_, window, cx| { window.close_dialog(cx); }) ) .child( Button::new("done") .flex_1() .primary() .label("Done") .on_click(|_, window, cx| { window.close_dialog(cx); }) ) ) }) }) ``` ### Declarative Components #### DialogHeader Container for the dialog's title and description section. ```rust DialogHeader::new() .child(DialogTitle::new().child("Title")) .child(DialogDescription::new().child("Description")) ``` #### DialogTitle Displays the main title of the dialog with semantic styling. ```rust DialogTitle::new() .child("Account Settings") ``` #### DialogDescription Displays descriptive text below the title with muted styling. ```rust DialogDescription::new() .child("Update your account settings and preferences here.") ``` #### DialogFooter Container for action buttons and footer content. Automatically applies proper spacing and alignment. ```rust DialogFooter::new() .bg(cx.theme().muted) .border_t_1() .border_color(cx.theme().border) .child(Button::new("cancel").outline().label("Cancel")) .child(Button::new("save").primary().label("Save")) ``` ### Form Dialog with Declarative API ```rust let name_input = cx.new(|cx| InputState::new(window, cx)); let email_input = cx.new(|cx| InputState::new(window, cx)); Dialog::new(cx) .trigger(Button::new("edit-profile").label("Edit Profile")) .content(|content, _, cx| { content .child( DialogHeader::new() .child(DialogTitle::new().child("Edit Profile")) .child(DialogDescription::new().child( "Make changes to your profile here. Click save when done." )) ) .child( v_flex() .gap_4() .py_4() .child( v_flex() .gap_2() .child("Name") .child(Input::new(&name_input).placeholder("Enter your name")) ) .child( v_flex() .gap_2() .child("Email") .child(Input::new(&email_input).placeholder("Enter your email")) ) ) .child( DialogFooter::new() .child(Button::new("cancel").outline().label("Cancel")) .child(Button::new("save").primary().label("Save Changes")) ) }) ``` ### Styled Footer Customize the footer appearance with background colors, borders, and alignment: ```rust DialogFooter::new() .justify_center() // Center align buttons .bg(cx.theme().muted) // Background color .border_t_1() // Top border .border_color(cx.theme().border) .child(Button::new("btn1").flex_1().label("Cancel")) .child(Button::new("btn2").flex_1().primary().label("Confirm")) ``` ### DialogContent Container The `DialogContent` component provides a flexible container for dialog body content: ```rust use gpui_kit::component::dialog::DialogContent; window.open_dialog(cx, |dialog, _, _| { dialog.content(|content, _, cx| { content .child(DialogHeader::new() .child(DialogTitle::new().child("Settings")) .child(DialogDescription::new().child("Configure your preferences")) ) .child( div() .py_4() .child("Main content area") ) .child(DialogFooter::new() .child(Button::new("close").label("Close")) ) }) }) ``` ## API Reference - Declarative Components ### Dialog | Method | Description | | ------------------------ | ----------------------------------------------------- | | `new(cx)` | Create a new Dialog (no longer requires window param) | | `trigger(element)` | Set trigger element that opens the dialog | | `content(builder)` | Set content using a builder function | | `w(px)` / `width(px)` | Set dialog width | | `max_w(px)` | Set maximum width | | `margin_top(px)` | Set top margin | | `overlay(bool)` | Show/hide overlay (default: true) | | `overlay_closable(bool)` | Allow closing by clicking overlay (default: true) | | `keyboard(bool)` | Allow closing with ESC key (default: true) | | `close_button(bool)` | Show/hide close button (default: true) | ### DialogContent Container for dialog body content. Automatically applies padding and flex layout. ```rust DialogContent::new() .child(DialogHeader::new()...) .child(/* your content */) .child(DialogFooter::new()...) ``` ### DialogHeader Container for title and description. Automatically applies vertical flex layout with proper gap. ```rust DialogHeader::new() .child(DialogTitle::new().child("Title")) .child(DialogDescription::new().child("Description")) ``` ### DialogTitle Displays the dialog title with semantic styling (font-semibold, proper line-height). ```rust DialogTitle::new() .child("Dialog Title") ``` ### DialogDescription Displays descriptive text with muted foreground color and proper text sizing. ```rust DialogDescription::new() .child("This is a description text that provides more context.") ``` ### DialogFooter Container for footer buttons with automatic spacing and alignment. ```rust DialogFooter::new() .justify_end() // Right align (default) .child(Button::new("btn1").label("Cancel")) .child(Button::new("btn2").primary().label("OK")) ``` ## Breaking Changes ### Dialog::new() Signature Change The `Dialog::new()` constructor no longer requires a `window` parameter: ```rust // Old API (deprecated) Dialog::new(window, cx) // New API Dialog::new(cx) ``` ### Content Builder Function The `.content()` method now accepts a builder function instead of a pre-built `DialogContent`: ```rust // Old approach (still works) dialog.child(DialogHeader::new()...) // New declarative API dialog.content(|content, window, cx| { content .child(DialogHeader::new()...) .child(DialogFooter::new()...) }) ``` ## Best Practices 1. **Use Declarative Components**: Prefer `DialogHeader`, `DialogTitle`, `DialogDescription`, and `DialogFooter` for consistent styling 2. **Trigger-based for Simple Cases**: Use the trigger pattern for straightforward dialogs that open from a button 3. **Builder Pattern for Complex Dialogs**: Use `window.open_dialog` with content builder for dialogs requiring complex logic or state 4. **Semantic Structure**: Always include `DialogHeader` with title and description for accessibility 5. **Consistent Footer**: Use `DialogFooter` for all action buttons to maintain visual consistency 6. **Proper Sizing**: Explicitly set dialog width when content requires specific dimensions --- --- url: /zh-CN/base/primitives/dialog.md description: 带焦点管理、遮罩、标题和关闭部件的可组合模态界面。 --- # Dialog 带焦点管理、遮罩、标题和关闭部件的可组合模态界面。 和所有 GPUI Base 原语一样,Dialog 只提供行为和语义结构,不规定产品视觉语言。请使用 GPUI 样式并组合导出的部件,使其符合你的设计系统。 ## 示例 原生示例和页面上方的 WASM 预览共用同一份实现: ```bash cargo run -p gpui-base --example components -- dialog ``` ## 导入 ```rust use gpui_kit::base::{Dialog, DialogBackdrop, DialogClose, DialogDescription, DialogPopup, DialogTitle, DialogTrigger}; ``` ## 结构与 API 示例组合上述公开类型。GPUI 的标准样式和事件 trait 负责表现,Base 类型负责交互结构。权威实现位于 [`components/dialog.rs`](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/components/dialog.rs),原生与浏览器预览编译的是同一文件。 ## 状态与事件 打开状态可以受控;触发器、关闭按钮与遮罩根据产品策略更新它。 受控状态应保存在父渲染类型或 GPUI entity 中;在回调中更新并调用 `cx.notify()`,不要在每次渲染时重建持久 entity。 ## 完整 Rust 示例 ## 可访问性 打开后管理焦点,关联标题与说明,并提供键盘可达的关闭方式。 ## 注意事项 在支持的位置使用稳定元素 ID,并在消费端设计系统中验证焦点、悬停、按下、选中、禁用、减少动态效果和高对比度状态。 --- --- url: /zh-CN/docs/components/dialog.md description: 在应用内容上方显示浮层内容的对话框组件。 --- # Dialog Dialog 用于创建普通对话框、确认框和提示弹窗。它支持遮罩层、键盘快捷键以及多种自定义能力。 ## 导入 ```rust use gpui_kit::component::dialog::DialogButtonProps; use gpui_kit::component::WindowExt; ``` ## 用法 ### 在应用根视图中渲染 Dialog 图层 如果你要展示对话框,需要在应用根视图中渲染 dialog layer。通常这会放在主应用结构体的 `render` 方法里。 [Root::render\_dialog\_layer](https://docs.rs/gpui-component/latest/gpui_component/struct.Root.html#method.render_dialog_layer) 会把当前激活的对话框渲染在应用内容之上。 ```rust use gpui_kit::component::TitleBar; struct MyApp { view: AnyView, } impl Render for MyApp { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let dialog_layer = Root::render_dialog_layer(window, cx); div() .size_full() .child( v_flex() .size_full() .child(TitleBar::new()) .child(div().flex_1().overflow_hidden().child(self.view.clone())), ) .children(dialog_layer) } } ``` ### 基础对话框 ```rust window.open_dialog(cx, |dialog, _, _| { dialog .title("Welcome") .child("This is a dialog dialog.") }) ``` ### 表单对话框 ```rust let input = cx.new(|cx| InputState::new(window, cx)); window.open_dialog(cx, |dialog, _, _| { dialog .title("User Information") .child( v_flex() .gap_3() .child("Please enter your details:") .child(Input::new(&input)) ) .footer(|_, _, _, _| { vec![ Button::new("ok") .primary() .label("Submit") .on_click(|_, window, cx| { window.close_dialog(cx); }), Button::new("cancel") .label("Cancel") .on_click(|_, window, cx| { window.close_dialog(cx); }), ] }) }) ``` ### 带图标的对话框 ```rust window.open_dialog(cx, |dialog, _, cx| { dialog .child( h_flex() .gap_3() .child(Icon::new(IconName::TriangleAlert) .size_6() .text_color(cx.theme().warning)) .child("This action cannot be undone.") ) }) ``` ### 可滚动内容 ```rust use gpui_kit::component::text::markdown; window.open_dialog(cx, |dialog, window, cx| { dialog .h(px(450.)) .title("Long Content") .child(markdown(long_markdown_text)) }) ``` ### 常用选项 ```rust window.open_dialog(cx, |dialog, _, _| { dialog .title("Custom Dialog") .overlay(true) .overlay_closable(true) .keyboard(true) .close_button(false) .child("Dialog content") }) ``` ### 嵌套对话框 ```rust window.open_dialog(cx, |dialog, _, _| { dialog .title("First Dialog") .child("This is the first dialog") .footer(|_, _, _, _| { vec![ Button::new("open-another") .label("Open Another Dialog") .on_click(|_, window, cx| { window.open_dialog(cx, |dialog, _, _| { dialog .title("Second Dialog") .child("This is nested") }); }), ] }) }) ``` ### 自定义样式 ```rust window.open_dialog(cx, |dialog, _, cx| { dialog .rounded(cx.theme().radius_lg) .bg(cx.theme().cyan) .text_color(cx.theme().info_foreground) .title("Custom Style") .child("Styled dialog content") }) ``` ### 自定义内边距 ```rust window.open_dialog(cx, |dialog, _, _| { dialog .p_3() .title("Custom Padding") .child("Dialog with custom spacing") }) ``` ### 代码中主动关闭 ```rust window.close_dialog(cx); Button::new("submit") .primary() .label("Submit") .on_click(|_, window, cx| { window.close_dialog(cx); }) ``` ## 声明式 API 现在 Dialog 也支持声明式写法,可以通过 header、title、description、footer 等组件来组织内容。 ### 导入 ```rust use gpui_kit::component::dialog::{ Dialog, DialogHeader, DialogTitle, DialogDescription, DialogFooter, }; ``` ### 触发器模式 ```rust Dialog::new(cx) .trigger( Button::new("open-dialog") .outline() .label("Open Dialog") ) .content(|content, _, cx| { content .child( DialogHeader::new() .child(DialogTitle::new().child("Account Created")) .child(DialogDescription::new().child( "Your account has been created successfully!", )) ) .child( DialogFooter::new() .border_t_1() .border_color(cx.theme().border) .bg(cx.theme().muted) .child( Button::new("cancel") .outline() .label("Cancel") .on_click(|_, window, cx| { window.close_dialog(cx); }) ) .child( Button::new("ok") .primary() .label("Save Changes") ) ) }) ``` ### 内容构建器模式 ```rust window.open_dialog(cx, |dialog, _, _| { dialog .w(px(400.)) .content(|content, _, _| { content .child( DialogHeader::new() .child(DialogTitle::new().child("Custom Width")) .child(DialogDescription::new().child( "This dialog has a custom width of 400px.", )) ) .child(div().child( "Content area with custom width configuration." )) .child( DialogFooter::new() .justify_center() .child( Button::new("cancel") .flex_1() .outline() .label("Cancel") .on_click(|_, window, cx| { window.close_dialog(cx); }) ) .child( Button::new("done") .flex_1() .primary() .label("Done") .on_click(|_, window, cx| { window.close_dialog(cx); }) ) ) }) }) ``` ### 相关子组件 #### DialogHeader 用于容纳标题和描述区域: ```rust DialogHeader::new() .child(DialogTitle::new().child("Title")) .child(DialogDescription::new().child("Description")) ``` #### DialogTitle 用于显示主标题: ```rust DialogTitle::new() .child("Account Settings") ``` #### DialogDescription 用于显示标题下方的说明文字: ```rust DialogDescription::new() .child("Update your account settings and preferences here.") ``` #### DialogFooter 用于放置底部操作按钮和页脚内容: ```rust DialogFooter::new() .bg(cx.theme().muted) .border_t_1() .border_color(cx.theme().border) .child(Button::new("cancel").outline().label("Cancel")) .child(Button::new("save").primary().label("Save")) ``` ## API 变化 ### Dialog::new() 签名变化 `Dialog::new()` 现在不再需要 `window` 参数: ```rust // Old API (deprecated) Dialog::new(window, cx) // New API Dialog::new(cx) ``` ### content 构建方式变化 `.content()` 现在接收 builder function,而不是预先构造好的 `DialogContent`: ```rust // Old approach (still works) dialog.child(DialogHeader::new()...) // New declarative API dialog.content(|content, window, cx| { content .child(DialogHeader::new()...) .child(DialogFooter::new()...) }) ``` ## 最佳实践 1. 优先使用 `DialogHeader`、`DialogTitle`、`DialogDescription` 和 `DialogFooter` 2. 简单场景优先用 trigger 模式 3. 复杂逻辑或复杂状态更适合 `window.open_dialog` + builder 4. 尽量保持语义结构完整,标题和说明建议成对出现 5. 所有操作按钮尽量放在 `DialogFooter` 中保持一致性 6. 对内容尺寸敏感的弹窗,建议显式设置宽度 --- --- url: /base/dock.md description: >- A dockable workspace — splits, tab groups, tiles, and edge docks — whose layout is pure data and whose appearance is entirely yours. --- # Dock A dockable workspace: nested splits, tab groups with draggable tabs, a free-positioning tiles canvas, and left/right/bottom docks that fold away. `gpui-base` owns all of the behavior and draws none of it. The layout is not a tree of views. It is a value — a `PaneTree` — that you can build, compare, serialize, and edit without a `Window` or an `App` in sight. `DockArea` reconciles that value into live entities, and three renderer traits supply every pixel. This page is long because Dock is the largest system in `gpui-base`. If you only need to stand one up, [Get started](#get-started) and [Supply the appearance](#supply-the-appearance) are enough. ## The model Three container shapes, and nothing else: | Container | Holds | Notes | | --------- | -------------------------------- | ------------------------------------------ | | `Split` | Other containers, along one axis | Each child slot has an optional fixed size | | `Tabs` | Panels, one displayed at a time | Carries the displayed index | | `Tiles` | Panels at free positions | Each tile has bounds and a z-index | There is no leaf variant, so **a panel can only ever live inside a `Tabs` or a `Tiles`**. A region whose center is a single panel is still a `Tabs` holding one panel. Four regions exist: the center, plus an optional left, right and bottom dock. Each is one independent `PaneTree`. Two identities, both stable: * **`NodeId`** addresses a container. It survives every edit and every normalization rule, so a container still present after a drag carries the id it had before. Ids are allocated globally, so a node id is unambiguous across all four regions. * **`PanelId`** addresses a panel. It wraps the panel entity's `EntityId`, so it identifies that panel for as long as the entity lives — across any number of moves between groups and regions. Neither the tree nor any node stores a GPUI entity handle. ### Key types | Type | Role | | --------------------------------------------------------- | --------------------------------------------------------------------------- | | `PaneTree` | One region's layout, as pure data | | `PaneNode` / `PaneRef` | A node, and the borrowed projection you `match` on | | `NodeId` / `PanelId` | Stable container and panel identity | | `DockArea` | Owns the trees, reconciles them into entities, routes drags and persistence | | `DockLayout` | Describes a layout without constructing anything | | `Panel` | What a dockable view implements — behavior only | | `PanelView` | Object-safe panel handle, `Arc` | | `TabGroup` / `TilesState` | The entity behind a `Tabs` node and a `Tiles` node | | `DockAreaRenderer` / `TabGroupRenderer` / `TilesRenderer` | Where every visual decision goes | | `DockContext` / `TabGroupContext` / `TileContext` | Resolved state and callbacks handed to a renderer | | `DockAreaState` | The serializable form of a whole area | ## Get started ```rust use std::rc::Rc; use gpui_kit::base::dock::{DockArea, DockLayout, DockPlacement}; let area = cx.new(|cx| { DockArea::new("workspace", Some(1), window, cx).with_renderer(Rc::new(MySkin)) }); area.update(cx, |area, cx| { area.set_center( DockLayout::h_split() .child(DockLayout::tabs().panel(files.clone()), Some(px(240.))) .child(DockLayout::tabs().panel(editor.clone()), None), window, cx, ); area.set_dock( DockPlacement::Bottom, DockLayout::tabs().panel(terminal.clone()), window, cx, ); }); ``` `DockArea::new` takes an id (yours, for your own persistence) and an optional schema version. An area built without `.with_renderer(...)` still docks, drags, resizes and persists — it simply draws nothing but the panels themselves. ## Describing a layout `DockLayout` builds a tree without touching `window` or `cx`, because building a tree constructs no entities. ```rust DockLayout::h_split() .child(DockLayout::tabs().panel(explorer.clone()), Some(px(240.))) .child( DockLayout::v_split() .child( DockLayout::tabs() .panel(editor.clone()) .panel(diff.clone()) .active_index(1), None, ) .child(DockLayout::tabs().panel(console.clone()), Some(px(180.))), None, ) ``` | Builder | Produces | | ------------------------- | --------------------------------- | | `h_split()` / `v_split()` | A split along that axis | | `child(layout, size)` | Adds a child container to a split | | `tabs()` | A tab group | | `panel(entity)` | Adds a panel to a tab group | | `active_index(ix)` | Which tab starts displayed | | `tiles()` | A tiles canvas | | `tile(entity, bounds)` | Places a panel on a canvas | Misuse — a panel added to a split, a child added to a tab group — trips a `debug_assert!` and is otherwise ignored. ### Slot sizes The `size` in `child(layout, size)` is the slot's extent **along the split's axis**: width in an `h_split`, height in a `v_split`. * `Some(px(240.))` fixes it. * `None` leaves it unconstrained — the slot shares what is left with its other unconstrained siblings. A layout with every slot `None` divides the space evenly. When a panel is later dropped beside an existing one with no size in mind, it takes half of what it lands next to. ### Normalization Every edit runs one collapse pass to a fixpoint before returning. The rules, applied bottom up: 1. An empty `Tabs`, `Tiles`, or `Split` is removed from its parent. 2. A `Split` with one child is replaced by that child, which keeps its own `NodeId` and inherits the slot size. 3. A `Split` whose child is a `Split` of the same axis splices that child's children into itself, scaling their sizes to fill the slot. 4. `active_ix` is clamped to the panel count. 5. The center's root stays a `Split` even when empty; a dock's root is unconstrained. Two consequences worth designing around. **You never need to avoid redundant nesting** — wrapping a node in a same-axis split is harmless, because rule 3 flattens it, which is why `split_at` needs no "reuse the parent" special case. And **there is no window in which a caller can observe a malformed tree**: no empty container, no one-child split, no out-of-range active index. Normalization is idempotent, so `normalize(normalize(t)) == normalize(t)`. ## Panels The whole of a panel's obligation to base is a stable name. ```rust struct FilesPanel { focus_handle: FocusHandle } impl Panel for FilesPanel { fn panel_name(&self) -> &'static str { "FilesPanel" } } impl EventEmitter for FilesPanel {} impl Focusable for FilesPanel { fn focus_handle(&self, _: &App) -> FocusHandle { self.focus_handle.clone() } } impl Render for FilesPanel { /* ... */ } ``` `panel_name` identifies the panel in persisted layouts. **Once chosen, never change it** — it is the key a saved file is read back through. ### Every hook | Method | Default | When it runs | | ------------------------ | ---------- | ---------------------------------------------------------- | | `panel_name()` | *required* | Any time the panel is identified or written out | | `visible(cx)` | `true` | Every render pass | | `closable(cx)` | `true` | Before a close is offered or applied | | `zoomable(cx)` | `true` | Before a zoom is applied | | `on_added_to(group, ..)` | no-op | When the panel joins a tab group, with a weak handle on it | | `set_active(active, ..)` | no-op | On each real edge of "is the displayed tab" | | `set_zoomed(zoomed, ..)` | no-op | When the group displaying it zooms in or out | | `on_removed(..)` | no-op | When the panel leaves the dock for good | | `dump(cx)` | name only | On `DockArea::dump` | ### Lifecycle contracts These are precise, and worth reading once: **`set_active` fires on edges only.** It is called with the frame-end net state: exactly one notification per real change, delivered on the next tick. Never same-value repeats, never a false-then-true flip within one frame. A panel that is hidden but occupies the active slot still receives `true`, even though rendering falls back to the first visible panel. **A removed panel is not told `false`.** `on_removed` is the deactivation signal. If you release resources in `set_active(false)`, release them in `on_removed` too. **A moved panel never hears `on_removed`.** Dragging a panel from one group to another does not take it out of the dock, so it is told `on_added_to` again with the new group and nothing else. `on_removed` means gone: closed, or displaced by a wholesale `set_center`, `set_dock`, `remove_dock` or `load`. **`on_added_to` precedes any `set_active`,** so a panel can store the handle and act on its first activation. **`set_zoomed` reaches only the displayed panel.** A group has one zoom state and it is the visible panel that fills the dock. Panels in the group's other tabs hear nothing, and a panel that was not displayed when the zoom changed is never told retroactively. **`closable` is permission, not a guarantee.** A container can still refuse — the last group of a dock does, so a dock cannot be emptied by closing. **A hidden panel keeps its place.** `visible` returning `false` leaves the panel in the tree and in its group; it reappears where it was. A container whose panels are *all* hidden gives up its slot, recursively — a nested split whose every leaf is hidden takes no space. ## The dock area ### Installing layouts ```rust area.set_center(layout, window, cx); area.set_dock(DockPlacement::Left, layout, window, cx); area.remove_dock(DockPlacement::Left, window, cx); ``` Each replaces whatever was there. Panels that were displaced — and are not part of the new layout — receive `on_removed`. ### Adding and moving panels ```rust area.add_panel(panel, DockPlacement::Left, Some(px(240.)), window, cx); area.add_tile(panel, DockPlacement::Center, bounds, window, cx); area.remove_panel(panel, window, cx); area.move_panel(panel_id, target, window, cx); area.split_at(node, panel_id, Placement::Right, window, cx); ``` `add_panel` lands the panel in the region's first tab group, creating the region if it has none. `add_tile` needs a tiles canvas in that region; without one it is a no-op. Both have `_view` variants taking an `Arc` for callers holding an erased handle. ### Docks ```rust area.has_dock(DockPlacement::Left); area.is_dock_open(DockPlacement::Left); area.toggle_dock(DockPlacement::Left, window, cx); area.dock_size(DockPlacement::Left); area.set_dock_size(DockPlacement::Left, px(280.), window, cx); area.set_dock_collapsible(DockPlacement::Left, true, window, cx); ``` A closed dock keeps its tree and its size; reopening restores both. ### Zoom A zoom names a **container**, not a panel — a group survives its displayed panel closing, and the next tab takes over still zoomed. ```rust area.set_zoomed_in(node, window, cx); area.set_zoomed_out(window, cx); area.is_zoomed(); area.zoomed_group(); // Option area.zoomed_tile(); // Option ``` The usual entry point is not these but `TabGroupContext::toggle_zoom` / `TileContext::toggle_zoom`, which a skin already has wherever it draws a zoom control. Zoom ends when the zoomed container leaves the dock, or when the container clears it — not when some unrelated panel is removed. ### Locking `area.set_locked(true, window, cx)` freezes rearrangement: no drags, no drops, no closes. Reads and rendering are unaffected. ### Queries ```rust area.layout(DockPlacement::Center); // Option<&PaneTree> area.panel(panel_id); // Option<&Arc> area.is_empty(DockPlacement::Left, cx); area.is_locked(); area.bounds(); ``` ## Editing the tree directly Everything above ultimately goes through `PaneTree`. You can drive it yourself — for a command palette, a keyboard shortcut, a restored session: ```rust tree.insert_panel(panel, InsertTarget::Tabs { node, ix: None, activate: true }); tree.remove_panel(panel); tree.move_panel(panel, target); tree.split(node, panel, Placement::Right, Some(px(320.))); tree.set_active(node, 2); tree.set_sizes(node, vec![Some(px(200.)), None]); tree.set_tile_bounds(panel, bounds); tree.bring_to_front(panel); ``` `InsertTarget` says where a panel lands: | Variant | Meaning | | --------------------------------- | -------------------------------------------------- | | `Tabs { node, ix, activate }` | Into an existing tab group, optionally at an index | | `Split { node, placement, size }` | Beside a node, in a new tab group | | `Tile { node, bounds }` | Onto a tiles canvas at those bounds | Every edit returns an `EditResult`: `changed()`, plus `created_nodes()`, `removed_nodes()`, `removed_panels()`, `activated()`, `deactivated()`. **`removed_panels` excludes moves** — a moved panel's entity survives, so it must not receive `on_removed`. Reading a tree: ```rust tree.root(); // &PaneNode tree.node_ids(); // Vec, pre-order tree.panels(); // impl Iterator tree.find_node(node_id); // Option<&PaneNode> tree.find_panel_node(panel_id); // Option match node.kind() { PaneRef::Split { axis, children, sizes } => { /* ... */ } PaneRef::Tabs { panels, active_ix } => { /* ... */ } PaneRef::Tiles { panels } => { /* ... */ } } ``` ## Supply the appearance Nothing in `gpui_kit::base::dock` paints a color, a border, or a size. Three traits carry appearance in. ### `DockAreaRenderer` | Method | Supplies | Default | | --------------------- | ------------------------------------------------------------------ | ------------------------------ | | `frame` | The area's outermost element | Bare `div` | | `center_frame` | The column holding the center and bottom dock | Bare `div` | | `split_frame` | One split's frame | Bare `div` | | `render_split_handle` | The divider between two slots | `None` → base's one-pixel line | | `render_dock` | One dock's chrome: title strip, collapse affordance, resize handle | The content, unwrapped | | `build_placeholder` | The stand-in for a panel this build cannot construct | `None` → draws nothing | | `tab_group_renderer` | *required* | — | | `tiles_renderer` | *required* | — | ### `TabGroupRenderer` | Method | Supplies | Default | | ----------------------- | ---------------------------------------- | ---------------------------- | | `frame` | The group's outer element | Bare `div` | | `content_frame` | The element the displayed panel sits in | Bare `div` | | `render_tab_bar` | The tab strip | Nothing | | `render_active_panel` | How the displayed panel is placed | The panel, filling the frame | | `render_drop_indicator` | The highlight showing where a drop lands | Nothing | | `render_empty` | What an empty group shows | Nothing | ### `TilesRenderer` | Method | Supplies | Default | | ----------------------- | ---------------------------------------- | ----------- | | `frame` | The canvas | Bare `div` | | `tile_frame` | One tile's outer element | Bare `div` | | `render_drag_bar` | *required* — the strip that moves a tile | — | | `render_resize_handles` | The tile's resize affordances | Nothing | | `panel_frame` | The element the panel sits in | Bare `div` | | `render_overlay` | Anything drawn above every tile | Nothing | | `grid_size` | Snap granularity | No snapping | ### Contexts A renderer never sees a drag event or a mouse position. Base attaches drag sources, drop hit-testing, focus and keyboard handling to the very elements the renderer returns, and hands it resolved state plus callbacks: **`TabGroupContext`** — `node()`, `panels()`, `active_ix()`, `active_panel()`, `drop_indicator()`, `is_zoomed()`, `is_collapsed()`, `can_close()`, `is_locked()`, `is_draggable()`, `is_droppable()`; and the actions `select_tab()`, `close()`, `toggle_zoom()`, `drag_panel()`, `drop_panel()`, `drop_item()`. **`TileContext`** — `node()`, `panel()`, `panel_id()`, `bounds()`, `z_index()`, `is_moving()`, `is_resizing()`, `can_close()`, `is_zoomed()`, `can_zoom()`; and `begin_move()` / `move_to()` / `end_move()`, `begin_resize()` / `resize_to()` / `end_resize()`, `bring_to_front()`, `toggle_zoom()`, `close()`. **`DockContext`** — `placement()`, `size()`, `is_open()`, `is_collapsible()`; and `toggle()`, `resize_to()`. ```rust impl TabGroupRenderer for MySkin { fn render_tab_bar(&self, group: &TabGroupContext, _: &mut Window, cx: &mut App) -> AnyElement { h_flex() .children(group.panels().iter().enumerate().map(|(ix, panel)| { div() .child(my_title(panel, cx)) .when(ix == group.active_ix(), |this| this.font_semibold()) .on_click({ let group = group.clone(); move |_, window, cx| group.select_tab(ix, window, cx) }) })) .into_any_element() } } ``` Every hook is optional in the same way: decline one and you get base's minimum for it. `render_split_handle` is the clearest case — return `None` and the divider falls back to a one-pixel line colored from `Theme::resizable`, so a skin with no opinion about dividers implements nothing, while one that has an opinion replaces the paint without touching the hit area, the cursor, or the drag. ## Drag and drop A tab drag has three parts, and a skin supplies only the middle one. **Starting.** `TabGroupContext::drag_panel(ix, cx)` returns a `DragPanel` if that tab may be dragged (it declines when the group is locked, or when the panel is the last one holding a dock open). Hand it to GPUI's `on_drag`. The preview view you return is yours; base's own `DragPanel` renders nothing, because a preview is appearance. **Landing.** While a drag hovers, base resolves where it would land and exposes it as `TabGroupContext::drop_indicator()` — a `DropIndicator` carrying the rectangle the panel would occupy. Paint it in `render_drop_indicator`; the coordinates are relative to the content frame, so that frame must be positioned. **Applying.** `drop_panel()` on release turns the hover into a `TabGroupEvent::Drop { panel, source, target }`, which the area applies as a single `PaneTree::move_panel`. Dropping onto the middle merges into the group; dropping towards an edge splits there; dropping onto the tab strip inserts at that index. **Host-owned drags.** Anything of your own can be dropped into the dock. Wrap it in `AnyDrag`, and `drop_item()` reports it as `DockEvent::DragDrop { item, target }` where `target` says whether it landed on a tab group or the tiles canvas. The dock does not interpret the payload. ## Tiles canvas A `Tiles` node holds panels at free positions instead of stacking them as tabs. `TilesState` owns the geometry: moving, resizing from any of five sides, magnetic snapping to neighbors and to a grid, z-ordering, per-gesture undo history, and single-tile zoom. A tile's drag bar is the one required piece of a `TilesRenderer`, because a tile with no drag bar cannot be moved. Everything else has a default. ```rust impl TilesRenderer for MySkin { fn render_drag_bar(&self, tile: &TileContext, _: &mut Window, _: &mut App) -> AnyElement { div() .h(px(28.)) .on_mouse_down(MouseButton::Left, { let tile = tile.clone(); move |event, window, cx| tile.begin_move(event.position, window, cx) }) .into_any_element() } fn grid_size(&self, _: &App) -> Pixels { px(8.) } } ``` Snapping geometry is exported for skins that want to preview a drop: `magnetic_snap`, `snap_edge`, `round_to_grid`, `compute_resized_bounds`, `apply_boundary_constraints`. ## Events | Emitter | Event | Meaning | | ------------ | ----------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | | `DockArea` | `LayoutChanged` | Something changed. Fires on **every** edit, including each step of a tile drag — debounce before writing to disk | | `DockArea` | `DragDrop { item, target }` | A host-owned drag landed | | `TabGroup` | `Drop` / `DragDrop` / `ClosePanel` / `ActiveChanged` / `ZoomIn` / `ZoomOut` | A group's intent, applied by the area | | `TilesState` | `BoundsChanged` / `BringToFront` / `ClosePanel` / `DragDrop` / `ZoomIn` / `ZoomOut` | A canvas's intent | | `Panel` | `ZoomIn` / `ZoomOut` / `LayoutChanged` | A panel's own signal | Container events are the container asking the area for something; the area is what actually edits the tree. A host normally subscribes only to `DockEvent`. ## Persistence ```rust let state: DockAreaState = area.read(cx).dump(cx); let json = serde_json::to_string(&state)?; let state: DockAreaState = serde_json::from_str(&json)?; area.update(cx, |area, cx| area.load(state, window, cx))?; ``` `DockAreaState` carries the version you passed to `DockArea::new`, the center, and each dock with its placement, size and open state. Every node writes its shape and every panel writes whatever its `dump` returned. Panels are rebuilt through a global registry, keyed by `panel_name`: ```rust register_panel(cx, "FilesPanel", |context, window, cx| { let state = context.state(); // the PanelState this panel dumped Arc::new(FilesPanel::restore(state, cx)) as Arc }); ``` Three behaviors worth knowing: * **An unregistered panel is not dropped.** It becomes a placeholder carrying the original state forward, so a layout saved by a build that had a panel yours does not know still round-trips intact instead of losing it. * **Slot sizes are resolved on the way out.** `dump` writes the sizes the split is actually drawn at, not the ones the tree was built from, and never writes a zero. * **`LayoutChanged` fires far more often than you want to save.** Debounce, or save on a timer or on window close. ## How this compares Docking layouts are well-trodden. Where implementations differ is how far the layout engine is separated from what draws it — and that choice has consequences you can observe. ### Architecture | Project | Stack | Engine and rendering | What a consumer can change | | ---------------------------------------------------------------- | ---------------- | ----------------------------------------------------------------------------- | ---------------------------------------------------------------------- | | [Qt](https://doc.qt.io/qt-6/qdockwidget.html) | C++, retained | `QMainWindow` owns four fixed dock areas; a `QDockWidget` *is* a widget | Subclass the widget; styling via QSS | | [AvalonDock](https://github.com/Dirkster99/AvalonDock) | C#/WPF, retained | `LayoutRoot` tree of layout elements | XAML templates and themes | | [Dear ImGui](https://github.com/ocornut/imgui/wiki/Docking) | C++, immediate | `DockSpace()` is a region any window may dock into; nodes are engine-internal | Style vars and colors | | [egui\_dock](https://docs.rs/egui_dock/) | Rust, immediate | `DockState` holds surfaces, each with a `Tree` of `Node`s | `TabViewer` renders tab bodies; a `Style` struct tunes the chrome | | [dockview](https://dockview.dev/) | TypeScript, web | Framework-agnostic engine behind thin adapters | CSS variables, theme object, replace the tab component | | [FlexLayout](https://github.com/caplin/FlexLayout) | TypeScript, web | JSON model beside a React renderer | `onRenderTab` callbacks and CSS | | [golden-layout](https://golden-layout.com/) | TypeScript, web | Engine owns its DOM outright | CSS overrides | | [rc-dock](https://github.com/ticlo/rc-dock) | TypeScript, web | `BoxData` / `PanelData` / `TabData` model | Custom tab rendering and CSS | | [VS Code](https://code.visualstudio.com/api/ux-guidelines/panel) | TypeScript, app | Workbench owns the layout | Contributed views, themed via CSS | | [Zed](https://zed.dev/) | Rust, app | `PaneGroup` built into the application | Not reusable outside its host | | **`gpui-base`** | Rust, retained | Pure-data `PaneTree`; the engine paints nothing | Renderer traits return elements — there is no default look to override | Three families are visible in that table. **Application-owned** engines (VS Code, Zed) are the most capable and the least reusable — you cannot lift them out of their host. **Widget-tree** engines (Qt, AvalonDock, golden-layout) make the dockable thing a widget, so the layout *is* the view hierarchy. **Model-and-renderer** engines (FlexLayout, rc-dock, dockview, egui\_dock, and this one) keep a separate description of the layout and hand rendering to something else. `gpui-base` sits at the far end of the third family: the engine paints nothing at all. In a library that draws its own chrome, customization is a set of overrides layered onto a default appearance, and you are limited to the seams it chose to expose. Here a renderer returns elements and base attaches behavior to *those* elements, so two unrelated appearances can sit over one behavior — `crates/component/src/dock` and the example below are exactly that. ### The closest relative [egui\_dock](https://docs.rs/egui_dock/) is worth a paragraph of its own, because it arrives at nearly the same shape from the other side of the retained/immediate divide. It keeps a `DockState` holding surfaces, each surface a `Tree`, each tree a hierarchy of `Node`s split into leaf and split variants — which is this design, down to the vocabulary, and it even calls its render entry point `DockArea`. Two differences matter. Its `TabViewer` renders **tab bodies**, while the crate itself draws the tab bars and splitters through a `Style` struct; here the split is the other way round — panels render themselves, and the skin draws all the chrome, with no `Style` struct because there is nothing built in to configure. And because egui is immediate-mode, its tree is walked and re-emitted every frame by construction; here the tree is a value that changes only when edited, and reconciliation against a stable `NodeId` cache is what keeps entities alive across edits. egui\_dock also has something this does not: **undocking a tab into a floating OS window**, modeled as additional surfaces. That is a real capability gap, not a design difference. ### What the data model buys The layout being a value rather than a widget tree is not an aesthetic preference. Three properties follow: **A drag does not reset what it did not touch.** When containers *are* views — the Qt and AvalonDock model — rearranging the layout means creating and dropping views, so a drag can reset state (scroll offsets, focus, in-progress input) in panels that merely shared a parent with the one being moved. Here identity is a `NodeId` that survives every edit and every normalization rule, so reconciliation is a diff against the entity cache: a steady-state pass creates and drops nothing. **Collapse is a pure function, not a deferred cascade.** When the last panel leaves a group, the group must remove itself from its parent, which may empty the parent in turn. With containers as views this is mutual recursion between two types reaching upward through parent handles — and those handles must be installed after construction, which in GPUI means a deferred pass, which means a window in which the tree disagrees with itself. `normalize` is one post-order pass to a fixpoint: no parent pointers, no deferred work, and the tree is self-consistent the instant an edit returns. **Editing costs no rendering.** `insert_panel`, `move_panel`, `split` and the rest operate on a value. They allocate no entities and request no layout, so a sequence of edits can run and be inspected before anything is drawn. The same property is why the whole layout algebra is tested as plain `#[test]` with no `TestAppContext` — which is why the collapse rules have the coverage they do. The cost, stated plainly: an edit clones the tree once to diff it, and normalization walks it until it reaches a fixpoint (two passes on realistic layouts, with a hard ceiling well above that). For dock-sized trees — tens of nodes — both are negligible against a frame, and they buy the three properties above. This would be the wrong shape for a structure with thousands of nodes. ### Naming The vocabulary follows the neighborhood where it can, which matters if you already know one of these systems: | Concept | Qt | egui\_dock | dockview | VS Code | Zed | `gpui-base` | | ------------------------- | -------------------- | ----------- | ------------- | --------------- | ----------- | ----------- | | Window-level container | `QMainWindow` | `DockState` | `DockviewApi` | Workbench | `Workspace` | `DockArea` | | Tree arranging containers | — | `Tree` | Gridview | — | `PaneGroup` | `PaneTree` | | Tree node | — | `Node` | — | — | `Member` | `PaneNode` | | Tab group | (stacked docks) | `LeafNode` | Group | View Container | `Pane` | `TabGroup` | | Content in a tab | `QDockWidget` | `Tab` | `Panel` | `View` | `Item` | `Panel` | | Edge region | `Qt::DockWidgetArea` | — | — | Panel / Sidebar | `Dock` | `Dock` | One caution, because the field uses the word inconsistently: here a **`Panel` is the dockable content**, matching dockview. VS Code calls the *bottom region* a panel; rc-dock calls the *tab container* one. Translate the word before porting concepts from either. Qt is the outlier worth noting: it has no separate node type at all, because `QDockWidget` is both the content and the thing the layout arranges. That is the design this one is furthest from. ## Runnable example Everything above, on `gpui-base` alone — panels, a layout, and a skin implementing all three renderer traits. Nothing in it depends on `gpui-component`, which is the point: base is usable on its own, and a host that wants a different look writes a different skin. ```bash cargo run -p gpui-base dock ``` Source: [`showcase/components/dock.rs`](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/components/dock.rs). It is the same file the preview at the top of this page compiles to WebAssembly. ## Integration checklist * Give every panel a `panel_name` you will never change, and register a builder for it before calling `load`. * Install a renderer, or accept that nothing but the panels themselves is drawn. * Release resources in `on_removed`, not only in `set_active(false)` — a departing panel is never told `false`. * Debounce `DockEvent::LayoutChanged` before persisting; it fires on every step of a drag. * Prefer `visible` over removal when a panel should come back in the same place. * Position the element you return from `content_frame`, or the drop indicator will have nothing to anchor to. --- --- url: /zh-CN/base/dock.md description: 用持久布局树、面板生命周期和可替换渲染器构建停靠工作区。 --- # Dock Dock 是 `gpui-base` 的无样式停靠布局基础。它把可持久化布局模型、面板生命周期、拖放与查询行为放在 Base,把标签、边框、图标、空状态和其他产品视觉交给应用提供的 renderer。 ## 模型 布局是一棵由分栏、标签组、面板、边缘 dock 和可选 tiles 画布组成的树。稳定面板 ID 是移动、恢复、查询和事件关联的基础;不要用当前位置作为身份。 ### 关键类型 `DockArea` 承载工作区和交互状态;布局节点描述结构;面板 trait/工厂负责由持久数据创建内容;renderer trait 把模型映射为应用外观。 ## 开始使用 先创建持久 `DockArea` entity,注册面板工厂和 renderer,再安装初始布局。渲染期间只读取模型;添加、移动、关闭、缩放和锁定操作放在回调中执行并通知 GPUI。 ## 描述布局 用嵌套节点声明水平/垂直分栏、标签组和面板。分栏尺寸属于相邻槽位的约束数据,不属于面板视觉样式。载入外部或旧版本布局后先规范化:移除空节点、折叠无意义嵌套并修正非法比例,同时保留稳定 ID。 ## 面板与生命周期 面板提供身份、标题/元数据、渲染内容以及可选的激活、停用、关闭、保存和恢复 hook。hook 可能发生在拖放、布局替换、窗口关闭或恢复期间,必须保持幂等,不能依赖一次 render 创建的临时状态。资源与订阅跟随持久 entity 生命周期。 ## DockArea 操作 可以整体安装布局,也可以向指定标签组添加面板、在节点间移动面板、打开或关闭边缘 dock、缩放当前面板、锁定结构,以及按 ID 查询面板或节点。修改前验证目标仍存在,因为事件发生后布局可能已经变化。 ## 直接编辑布局树 直接编辑适合批量迁移和恢复,不适合普通交互。编辑后必须规范化并通过 `DockArea` 安装,使索引、焦点、事件和渲染状态同步更新;不要绕过宿主只修改一份外部副本。 ## 提供外观 `DockAreaRenderer` 负责工作区边框、边缘 dock 和空状态;`TabGroupRenderer` 负责标签、活动态、关闭入口和标签拖动反馈;`TilesRenderer` 负责画布节点与连接。renderer 接收只读上下文和明确回调,不应拥有领域状态,也不应在 render 中修改布局。 ## 拖放与 Tiles 画布 拖动数据使用稳定面板身份。命中测试决定插入标签、分割方向或边缘 dock,提交前再次验证源与目标。Tiles 用于自由布局场景,位置与连接是模型数据;视觉、网格和选中反馈仍由 renderer 提供。 ## 事件与持久化 订阅布局、活动面板、面板关闭和拖放事件来同步应用状态。持久化稳定 ID、节点种类、分栏比例、标签顺序、活动项、边缘 dock 与面板自有数据,不要序列化 GPUI entity、焦点句柄或 renderer。恢复时容忍未知面板类型,并对版本化数据做迁移和规范化。 ## 架构取舍 Dock 的数据模型比单一分栏组件更重,但换来可查询、可移动、可持久化和可替换表现。Base 使用中性的 tree/panel/tab 命名,产品可以在 renderer 和面板工厂层映射成自己的术语。 ## 可运行示例 ```bash cargo run -p gpui-base --example dock ``` ## 集成检查清单 * 面板、标签组和需要持久化的节点使用稳定 ID。 * 领域状态保存在面板或应用 entity,renderer 只负责表现。 * 布局修改只发生在事件回调,并在安装/恢复后规范化。 * 处理未知面板、空布局、关闭否决、拖放目标失效和版本迁移。 * 验证键盘焦点、标签顺序、锁定、缩放、边缘 dock、减少动态效果和高对比度。 --- --- url: /docs/dock.md description: >- Production-ready dock layouts with styled tabs, split panes, edge docks, and persistent state. --- # Dock Dock builds application workspaces from draggable tab groups, nested splits, and collapsible left, right, and bottom docks. It is the layout foundation used by Longbridge in production, not an isolated UI demo. `gpui-base` owns the data model, layout calculation, and drag-and-drop behavior. `gpui-component` supplies the polished controls and visual language. Use `gpui_kit::component::dock` when you want a Dock ready to fit into a real application. For the renderer-independent architecture and custom-renderer API, see [Dock — gpui-base](/base/dock). ## Create a dock area Create the area through `DockSkin`. Keep the returned skin if you want to change its appearance later. ```rust use gpui_kit::component::dock::{DockArea, DockSkin}; struct Workspace { dock_area: Entity, dock_skin: Rc, } impl Workspace { fn new(window: &mut Window, cx: &mut Context) -> Self { let (dock_area, dock_skin) = DockSkin::dock_area("main-dock", Some(1), window, cx); Self { dock_area, dock_skin } } } ``` The optional version belongs to your saved layout schema. Increase it when your application can no longer restore an older layout. ## Define a panel A styled Dock panel implements `BasePanel` for identity and persistence, and `Panel` for its title, tab, and toolbar presentation. ```rust use gpui_kit::component::dock::{BasePanel, Panel, PanelEvent}; struct FilesPanel { focus_handle: FocusHandle, } impl EventEmitter for FilesPanel {} impl Focusable for FilesPanel { fn focus_handle(&self, _: &App) -> FocusHandle { self.focus_handle.clone() } } impl BasePanel for FilesPanel { fn panel_name(&self) -> &'static str { "FilesPanel" } } impl Panel for FilesPanel { fn title(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { "Files" } } impl Render for FilesPanel { fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { div().size_full().p_3().child("Project files") } } ``` Wrap styled panels with `panel_handle`. This preserves the `gpui-component` panel chrome when the base Dock stores the panel behind its renderer-independent handle. ## Describe the initial layout `DockLayout` is a value: compose it before a window exists, serialize it, compare it, or generate it from application state. Tabs and splits can be nested freely. ```rust use gpui_kit::component::dock::{DockLayout, panel_handle}; let files = cx.new(|cx| FilesPanel { focus_handle: cx.focus_handle(), }); let editor = cx.new(|cx| EditorPanel::new(cx)); let layout = DockLayout::h_split() .child( DockLayout::tabs().panel_view(panel_handle(files), cx), Some(px(240.)), ) .child( DockLayout::tabs().panel_view(panel_handle(editor), cx), None, ); self.dock_area.update(cx, |area, cx| { area.set_center(layout, window, cx); }); ``` Use `h_split()` and `v_split()` for rows and columns, `tabs()` for a tab group, and `tiles()` for a free-positioning canvas. A `None` split size fills the remaining space. The Dock area also supports left, right, and bottom regions through `DockPlacement`. Panels can be added, removed, activated, zoomed, and moved at runtime; user operations emit `DockEvent`, including `LayoutChanged` for persistence. ## Restore and persist layouts Dump the entire workspace as `DockAreaState`, store it with Serde, then load it on the next launch. ```rust use gpui_kit::component::dock::DockAreaState; // Save. let state = self.dock_area.read(cx).dump(cx); let json = serde_json::to_string_pretty(&state)?; // Restore. let state: DockAreaState = serde_json::from_str(&json)?; self.dock_area.update(cx, |area, cx| { area.load(state, window, cx) })?; ``` Register every restorable panel name during application initialization. The registered factory recreates the styled panel behind a `panel_handle`. ```rust register_panel(cx, "FilesPanel", |state, window, cx| { let panel = cx.new(|cx| FilesPanel::from_state(state, window, cx)); panel_handle(panel) }); ``` Dock state retains compatibility with layouts saved by earlier releases. Keep a sensible fallback layout for removed application panels or deliberate schema changes. ## Style the workspace `DockSkin` keeps rendering decisions outside the layout engine. You can configure the common panel presentation without changing Dock behavior: ```rust self.dock_skin.set_panel_style(PanelStyle::default(), cx); self.dock_skin.set_toggle_button_visible(true, cx); self.dock_skin .set_tiles_scrollbar_mode(Some(ScrollbarMode::Auto), cx); ``` For complete control, implement the renderer traits in `gpui-base`. The same layout data and operations can then drive an entirely different Dock style. ## Runnable example The repository includes a complete workspace with edge docks, runtime panel operations, layout persistence, and keyboard actions: ```sh cargo run --example dock ``` See [`crates/story/examples/dock.rs`](https://github.com/longbridge/gpui-kit/blob/main/crates/story/examples/dock.rs) for the full implementation. --- --- url: /zh-CN/docs/dock.md description: 支持标签页、分割面板、边缘 Dock 与状态持久化的生产级工作区布局。 --- # Dock Dock 用可拖动标签组、嵌套分割和可收起的左、右、底部 Dock 构建应用工作区。它是 Longbridge 在商业产品中长期使用的布局基础,而不是一个脱离实际项目的 UI Demo。 `gpui-base` 负责数据模型、布局计算和拖放行为,`gpui-component` 提供完整控件与统一视觉。需要直接用于真实应用的 Dock 时,请使用 `gpui_kit::component::dock`。 如果你需要了解与渲染器无关的架构或实现自定义渲染器,请阅读英文版 [Dock — gpui-base](/base/dock)。 ## 创建 DockArea 通过 `DockSkin` 创建 DockArea。如果之后需要调整外观,请保留返回的 skin。 ```rust use gpui_kit::component::dock::{DockArea, DockSkin}; struct Workspace { dock_area: Entity, dock_skin: Rc, } impl Workspace { fn new(window: &mut Window, cx: &mut Context) -> Self { let (dock_area, dock_skin) = DockSkin::dock_area("main-dock", Some(1), window, cx); Self { dock_area, dock_skin } } } ``` 可选的 version 属于你的持久化布局 schema。当应用无法再恢复旧布局时再增加它。 ## 定义 Panel 带样式的 Dock Panel 通过 `BasePanel` 提供身份与持久化能力,通过 `Panel` 提供标题、标签页和工具栏表现。 ```rust use gpui_kit::component::dock::{BasePanel, Panel, PanelEvent}; struct FilesPanel { focus_handle: FocusHandle, } impl EventEmitter for FilesPanel {} impl Focusable for FilesPanel { fn focus_handle(&self, _: &App) -> FocusHandle { self.focus_handle.clone() } } impl BasePanel for FilesPanel { fn panel_name(&self) -> &'static str { "FilesPanel" } } impl Panel for FilesPanel { fn title(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { "Files" } } impl Render for FilesPanel { fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { div().size_full().p_3().child("Project files") } } ``` 请用 `panel_handle` 包装带样式的 Panel。这样 base Dock 通过与渲染无关的 handle 保存 Panel 时,仍会保留 `gpui-component` 的完整面板外观。 ## 描述初始布局 `DockLayout` 是纯数据:可以在 Window 存在之前组合,也可以序列化、比较或从应用状态生成。标签组与分割布局可以任意嵌套。 ```rust use gpui_kit::component::dock::{DockLayout, panel_handle}; let files = cx.new(|cx| FilesPanel { focus_handle: cx.focus_handle(), }); let editor = cx.new(|cx| EditorPanel::new(cx)); let layout = DockLayout::h_split() .child( DockLayout::tabs().panel_view(panel_handle(files), cx), Some(px(240.)), ) .child( DockLayout::tabs().panel_view(panel_handle(editor), cx), None, ); self.dock_area.update(cx, |area, cx| { area.set_center(layout, window, cx); }); ``` 用 `h_split()` 和 `v_split()` 创建横向与纵向分割,用 `tabs()` 创建标签组,用 `tiles()` 创建可自由定位的画布。分割尺寸为 `None` 时会填满剩余空间。 DockArea 还通过 `DockPlacement` 支持左、右和底部区域。运行时可以添加、移除、激活、最大化或移动 Panel;用户操作会触发 `DockEvent`,其中 `LayoutChanged` 可用于持久化。 ## 恢复与持久化 将整个工作区导出为 `DockAreaState`,通过 Serde 保存,并在下次启动时恢复。 ```rust use gpui_kit::component::dock::DockAreaState; // 保存。 let state = self.dock_area.read(cx).dump(cx); let json = serde_json::to_string_pretty(&state)?; // 恢复。 let state: DockAreaState = serde_json::from_str(&json)?; self.dock_area.update(cx, |area, cx| { area.load(state, window, cx) })?; ``` 应用初始化时需要注册每一种可恢复的 Panel。注册的工厂通过 `panel_handle` 重新创建带样式的 Panel。 ```rust register_panel(cx, "FilesPanel", |state, window, cx| { let panel = cx.new(|cx| FilesPanel::from_state(state, window, cx)); panel_handle(panel) }); ``` Dock 状态兼容旧版本保存的布局。对于已经从应用移除的 Panel 或主动调整的 schema,仍应准备合理的默认布局作为回退。 ## 调整工作区样式 `DockSkin` 将渲染决策与布局引擎分离。你可以在不改变 Dock 行为的情况下配置常用面板外观: ```rust self.dock_skin.set_panel_style(PanelStyle::default(), cx); self.dock_skin.set_toggle_button_visible(true, cx); self.dock_skin .set_tiles_scrollbar_mode(Some(ScrollbarMode::Auto), cx); ``` 如果需要完全不同的视觉,可以实现 `gpui-base` 的渲染器 traits;同一份布局数据和操作逻辑仍然可以复用。 ## 可运行示例 仓库内提供了包含边缘 Dock、运行时 Panel 操作、布局持久化与键盘操作的完整工作区: ```sh cargo run --example dock ``` 完整实现见 [`crates/story/examples/dock.rs`](https://github.com/longbridge/gpui-kit/blob/main/crates/story/examples/dock.rs)。 --- --- url: /shell/dock.md description: >- A dockable layout drawn entirely by script — panels that survive a restart, chrome you draw yourself, and commands instead of callbacks. --- # Dock and Panels A View that can only fill a window is not much of an application. A **dock area** turns a script View into a *panel*: draggable, dockable, zoomable, and still where the user left it after a restart. ```js import { View, div } from "gpui-kit"; import { DockArea, dock_area, v_flex } from "gpui-base"; class Notes extends View { render() { return div().p(16).child("Notes"); } } export default class Workspace extends View { init(_props, cx) { DockArea.register_panel("notes", Notes); this.dock = DockArea.new("workspace"); this.dock.add_panel(cx.new(Notes), { name: "notes", placement: "left", size: 240, }); } render() { return dock_area(this.dock).size_full(); } } ``` That already docks, drags, resizes, zooms and persists. It draws no tab bar, because **base draws no chrome at all** — see [Drawing the chrome](#drawing-the-chrome). ## What base brings, and what it does not `gpui_kit::base::dock` has the hard half of a docking system: a layout that is **pure data**, a `PanelRegistry` that rebuilds a panel from a name in a persisted file, and a per-panel payload that rides along with it. Containers are addressed by a stable node id and panels by a stable panel id, so a drag rearranges a value rather than tearing down and rebuilding Views. What it does not have is a look. The engine paints nothing — no tab bar, no dock frame, no drag handle, no drop hint — and hands every one of those back to you as a callback that returns elements. That is not a limitation to work around; it is why the whole thing is usable from a script at all. Appearance is not a set of overrides on a default look, because there is no default look. ## The area is retained `DockArea.new(id)` creates state that lives across frames, like `InputState` does, and for a reason none of the other handles share: **the layout is what the user changed.** A drag, a resize, a closed tab and a collapsed dock all happen without your script rendering. A dock rebuilt from a description would put every one of them back the way the last render described it. So it is created once, in `init`, and `render` only *draws* it: ```js init() { this.dock = DockArea.new("workspace", { version: 1 }); } render() { return dock_area(this.dock).size_full(); } ``` `DockArea.new` needs a live host call, so it belongs in `init` or an event handler — never in `render`. So does every method that changes the layout; calling one from `render` is refused where it was written rather than producing a frame that draws one layout and describes another. ## Edits take effect when the call returns A panel's body comes from `cx.new(Class)`, which is itself still being constructed when you hand it over, and `load` builds panels of its own. Neither can happen while your script is running. So **every edit is queued and applied once the call that made it has returned**, in the order the calls were made. The practical consequence is one line long: `panels()` and `dump()` read the layout as it was *before* this turn's edits. ```js init(_props, cx) { this.dock = DockArea.new("workspace"); this.dock.add_panel(cx.new(Notes), { name: "notes" }); this.dock.panels(); // still empty — the add has not been applied this.dock.on("layout_changed", (cx) => { this.dock.panels(); // three panels, a dock size, a moved tab cx.notify(); }); } ``` `layout_changed` fires on every edit, including each step of a tile drag, so save on a timer rather than on the event. ## Panels A panel is a View that a dock happens to be holding. `add_panel` takes the View and says where it goes: ```js this.dock.add_panel(cx.new(Editor, { file }), { name: "editor", // required — what a saved layout files it under placement: "center", // "center" | "left" | "right" | "bottom" size: 240, // seeds the dock's extent when the panel is the first in it closable: true, zoomable: true, visible: true, }); ``` `name` is required because it is not decoration: it is what a saved layout writes and what `register_panel` finds the class again by. It is namespaced for you — `shell:/` — so two applications that both call a panel `inbox` never collide, and no script panel can shadow a host one. `panels()` reports what is there, including where: ```js this.dock.panels(); // [{ id, name, placement, node, index, active, visible, closable, zoomable }, …] ``` `id` is what `remove_panel(id)` takes, and what a close button hands to `close_panel`. ## Surviving a restart Two halves, and you need both. **Register the class**, so a saved layout can rebuild it: ```js DockArea.register_panel("editor", Editor); ``` **Save and restore the layout**, which is plain data: ```js init(_props, cx) { DockArea.register_panel("editor", Editor); this.dock = DockArea.new("workspace", { version: 1 }); const saved = localStorage.getItem("layout"); if (saved) this.dock.load(JSON.parse(saved)); else this.dock.add_panel(cx.new(Editor), { name: "editor" }); this.dock.on("layout_changed", () => localStorage.setItem("layout", JSON.stringify(this.dock.dump()))); } ``` A panel's own state rides along with its position. Two optional methods on the View class carry it: | Method | When | Note | | ------------------- | ------------------------------- | --------------------------------------------------------------------------------------------- | | `serialize()` | The layout is saved | Runs **without a host call**: return plain data and touch nothing else — no entities, no `cx` | | `deserialize(data)` | Right after the View is rebuilt | A real host call, so this one may touch entities | `version` is yours to bump when the shape of what you save changes; base refuses to load a layout written under a different one, so an old file is ignored rather than half-understood. ### An uninstalled application keeps its place This is the property worth designing around. If nothing is registered under a panel's name — the application was uninstalled, or a class was renamed — the panel is **not dropped**. A draw-nothing placeholder stands in and reports the state it was handed, so the next save writes the panel — name, payload and position — back out unchanged. Uninstall an application, use the window for a week, reinstall it: its panels come back where they were, with the state they had. The same holds one step further in. A panel that *is* registered but whose class throws on construction is carried forward the same way, so a broken script costs that panel's contents for the session rather than its place in the layout. ## Drawing the chrome Six handlers, all optional, hung on the `dock_area(...)` element: | Handler | Draws | | -------------------------------- | ------------------------------------------- | | `tab_bar(group => …)` | The tab bar above a group's displayed panel | | `empty_group(group => …)` | What a group with no displayed panel shows | | `drop_indicator(drop => …)` | Where a dragged panel would land | | `dock(dock => …)` | One dock's frame around its content | | `tile_drag_bar(tile => …)` | The strip a tile is dragged by | | `tile_resize_handles(tile => …)` | A tile's resize affordances | Each is first called from inside GPUI's layout pass and is given base's **resolved** state — never a drag event, a mouse position or a hit test, because base attaches all of that to the elements it gets back. The resulting description is cached by handler and resolved state, so unchanged frames replay it in Rust without entering JavaScript. ```js dock_area(this.dock) .size_full() .tab_bar((group, cx) => h_flex() .h(30) .bg(cx.theme().colors.secondary) .children( group.tabs .filter((tab) => tab.visible) .map((tab) => h_flex() .id("tab-" + tab.id) .px(10) .items_center() .bg( tab.active ? cx.theme().colors.background : cx.theme().colors.secondary, ) .select_tab(group, tab.index) .drag_tab(group, tab.index) .child(tab.name) .child( div() .id("x-" + tab.id) .close_panel(group, tab.id) .child("×"), ), ), ), ); ``` ### Commands, not callbacks Look at that tab again: it carries `select_tab` and `drag_tab`, not `on_click`. That is the one rule of this API worth understanding rather than memorising. A chrome description is cached and can outlive the handler call that produced it. A script callback registered inside one would therefore have no sound event lifetime, and every changed native state could create another. Registering one is refused where it is written; chrome uses native commands instead. A **command** carries no script value at all. It names a container in the area and what to ask it, and base does the work: | Command | On | Does | | ------------------------------ | ----- | ---------------------------------------------- | | `select_tab(group, index)` | click | Displays that tab | | `close_panel(group, panel_id)` | click | Closes the panel, if its group allows it | | `toggle_zoom(group)` | click | Zooms the group in, or back out | | `drag_tab(group, index)` | drag | Makes the element the drag source for that tab | | `drop_tab(group, index?)` | drop | Accepts a dragged panel here; no index appends | | `toggle_dock(dock)` | click | Opens or closes the dock | | `resize_dock(dock)` | drag | Drags the dock's edge | | `move_tile(tile)` | drag | Moves the tile around its canvas | | `resize_tile(tile, side)` | drag | Drags one edge or corner | | `raise_tile(tile)` | press | Brings the tile above the others | | `toggle_tile_zoom(tile)` | click | Zooms the tile to fill its dock | | `close_tile(tile)` | click | Closes the tile | Every one takes the object its handler was given as its first argument. They belong on a `div`, an `h_flex` or a `v_flex`: a `Button` builds its own interior and has nowhere to put one. Base clamps, snaps and rounds everything a drag produces before the next frame sees it, so a resize handle is a hit area and a colour and nothing else. ### The dock handler places its own content `dock` is the only handler handed an element as well as state, and whatever it returns *replaces* the dock's content. Put `dock_content()` where the panels belong: ```js .dock((dock, cx) => v_flex() .size_full() .relative() .child( h_flex() .h(30) .justify_between() .child(dock.placement.toUpperCase()) .child(div().id("collapse").toggle_dock(dock).child(dock.open ? "–" : "+")), ) .child(dock_content().flex_1()) .child(div().absolute().right(0).w(4).h_full().cursor_col_resize().resize_dock(dock)), ) ``` A handler that forgets `dock_content()` still shows its panels — they are drawn after what it returned, with a warning — rather than silently losing them. ## Tiles A region can be a free-floating canvas instead of a tab group. Pass `bounds` and the panel becomes a tile: ```js this.dock.add_panel(cx.new(Chart), { name: "chart", placement: "center", bounds: { x: 40, y: 40, width: 320, height: 240 }, }); ``` Tiles need their own two handlers, because base draws nothing there either: `tile_drag_bar` (whose height is fixed at base's drag-bar height, which the snapping arithmetic assumes) and `tile_resize_handles`. Both get a `tile` with **already-resolved** bounds. ## The whole surface ```js area.add_panel(view, options); area.remove_panel(id); area.panels(); area.dump(); area.load(state); area.has_dock(placement); area.is_dock_open(placement); area.toggle_dock(placement); area.remove_dock(placement); area.dock_size(placement); area.set_dock_size(placement, size); area.set_dock_collapsible(placement, collapsible); area.is_locked(); area.set_locked(locked); area.is_zoomed(); area.zoom_out(); area.on("layout_changed", handler); area.release(); ``` A locked area cannot be rearranged or dropped into. Dock and tile resizing stays available, so “lock layout” freezes where panels live without freezing their usable size. ## A complete example ```bash cargo run -p gpui-shell -- examples/js_dock ``` `examples/js_dock/` is a workspace: a file list in the left dock, documents in the center, a tab bar and dock frame drawn in `ui.js`, and a layout written to `localStorage` on a timer. It is the shortest complete thing that uses every part of this page. ## From Rust `gpui_kit::shell::dock` is public, so a host can reach the same seam without a script. `ScriptPanel` wraps a `ScriptView` as a `gpui_kit::base::dock::Panel`; `register_panel(application, panel, script, cx)` teaches the registry to rebuild it from a `PanelScript`; `ScriptDockSkin` forwards all three of base's renderer traits to one `DockChrome`. `tab_group_data`, `dock_data`, `tile_data` and `drop_indicator_data` are the JSON conversions the engine hands to script code, and are useful to a host writing its own binding. --- --- url: /zh-CN/shell/dock.md description: 完全由脚本绘制的可停靠布局——重启后仍在原处的面板、自己画的 chrome,以及用命令代替回调。 --- # Dock 与面板 只能铺满整个窗口的 View 算不上一个应用。**dock area** 把脚本 View 变成*面板*:可拖动、可停靠、可放大,重启之后仍然停在用户上次放的位置。 ```js import { View, div } from "gpui-kit"; import { DockArea, dock_area, v_flex } from "gpui-base"; class Notes extends View { render() { return div().p(16).child("Notes"); } } export default class Workspace extends View { init(_props, cx) { DockArea.register_panel("notes", Notes); this.dock = DockArea.new("workspace"); this.dock.add_panel(cx.new(Notes), { name: "notes", placement: "left", size: 240, }); } render() { return dock_area(this.dock).size_full(); } } ``` 这样就已经能停靠、拖动、调整大小、放大与持久化了。它不会画出标签栏,因为 **base 完全不画 chrome**——见[绘制 chrome](#绘制-chrome)。 ## base 给了什么,没给什么 `gpui_kit::base::dock` 已经把停靠系统里难做的那一半做好了:一棵**纯数据**的布局树、一个能按持久化文件里的名字重建面板的 `PanelRegistry`,以及跟着每块面板走的一份 payload。容器用稳定的 node id 寻址,面板用稳定的 panel id 寻址,因此一次拖动改的是一个值,而不是拆掉再重建一堆 View。 它没有的是外观。引擎什么都不画——没有标签栏、没有 dock 外框、没有拖拽条、没有落点提示——这些全都作为「返回元素的回调」交还给你。这不是需要绕开的限制,而正是这套东西能被脚本用起来的原因:外观不是覆盖在某个默认外观之上的一层,因为根本没有默认外观。 ## area 是 retained 的 `DockArea.new(id)` 创建的是跨帧存活的状态,和 `InputState` 一样;而且理由是其他 handle 都没有的一条:**布局是用户改的**。拖动、调整大小、关掉一个标签页、折叠一侧 dock,全都发生在脚本没有渲染的时候。一个从描述里重建出来的 dock,会把这些统统还原成上一次渲染所描述的样子。 所以它在 `init` 里创建一次,`render` 只负责*画*: ```js init() { this.dock = DockArea.new("workspace", { version: 1 }); } render() { return dock_area(this.dock).size_full(); } ``` `DockArea.new` 需要一次活的 Host 调用,所以它属于 `init` 或事件处理器——绝不能放在 `render` 里。每个会改动布局的方法同样如此;从 `render` 里调用会在写下它的那一行被拒绝,而不是产出一帧「画的是一套布局、描述的是另一套」的画面。 ## 编辑在调用返回时生效 面板的主体来自 `cx.new(Class)`——你把它交出去的那一刻,它自己都还在构造中;`load` 还会再构造更多面板。这些都不可能在脚本正在运行时发生。所以**每一次编辑都会排队,等到发起它的那次调用返回之后再按调用顺序应用**。 实际影响只有一句话:`panels()` 和 `dump()` 读到的是本轮编辑*之前*的布局。 ```js init(_props, cx) { this.dock = DockArea.new("workspace"); this.dock.add_panel(cx.new(Notes), { name: "notes" }); this.dock.panels(); // 还是空的——这次 add 尚未应用 this.dock.on("layout_changed", (cx) => { this.dock.panels(); // 三块面板、一个 dock 尺寸、一次标签页移动 cx.notify(); }); } ``` `layout_changed` 会在每次编辑时触发,包括 tile 拖动的每一步,所以要用定时器落盘,而不是在事件里直接写。 ## 面板 面板就是恰好被 dock 拿在手里的一个 View。`add_panel` 接收这个 View 并说明它去哪里: ```js this.dock.add_panel(cx.new(Editor, { file }), { name: "editor", // 必填——保存布局时用它归档 placement: "center", // "center" | "left" | "right" | "bottom" size: 240, // 当这块面板是该 dock 里的第一块时,用它作为初始尺寸 closable: true, zoomable: true, visible: true, }); ``` `name` 必填,因为它不是装饰:保存布局写的是它,`register_panel` 也靠它找回类。命名空间由运行时加好——`shell:/`——所以两个都把面板叫 `inbox` 的应用永远不会撞车,脚本面板也不可能盖掉 Host 面板。 `panels()` 报告现在有什么,以及在哪里: ```js this.dock.panels(); // [{ id, name, placement, node, index, active, visible, closable, zoomable }, …] ``` `id` 就是 `remove_panel(id)` 要的那个,也是关闭按钮交给 `close_panel` 的那个。 ## 熬过一次重启 两半,缺一不可。 **注册类**,让保存的布局能重建它: ```js DockArea.register_panel("editor", Editor); ``` **保存并恢复布局**,它就是纯数据: ```js init(_props, cx) { DockArea.register_panel("editor", Editor); this.dock = DockArea.new("workspace", { version: 1 }); const saved = localStorage.getItem("layout"); if (saved) this.dock.load(JSON.parse(saved)); else this.dock.add_panel(cx.new(Editor), { name: "editor" }); this.dock.on("layout_changed", () => localStorage.setItem("layout", JSON.stringify(this.dock.dump()))); } ``` 面板自己的状态会跟着位置一起走。 View 类上有两个可选方法负责这件事: | 方法 | 何时调用 | 说明 | | ------------------- | --------------- | -------------------------------------------------------------------------- | | `serialize()` | 保存布局时 | 运行时**没有 Host 调用**:返回纯数据,别碰别的——不要碰 entity,不要碰 `cx` | | `deserialize(data)` | View 刚重建之后 | 有一次真正的 Host 调用,因此可以碰 entity | `version` 由你在保存格式变化时递增;base 会拒绝加载在别的 version 下写出的布局,于是旧文件是被忽略,而不是被一知半解地读进来。 ### 卸载了的应用仍然保留位置 这是最值得围绕着设计的一条性质。 如果某块面板的名字下没有注册任何东西——应用被卸载了,或者类被改名了——这块面板**不会被丢掉**。一个什么都不画的占位面板会顶上,并原样报告它拿到的状态,于是下一次保存会把这块面板的名字、payload 和位置原封不动写回去。卸载一个应用,把窗口用上一周,再装回来:它的面板会回到原来的位置,带着原来的状态。 再往里一步也是同样的承诺:一块*已经*注册、但类在构造时抛异常的面板,会以同样的方式被带下去——一个写坏的脚本,代价是这一次会话里看不到那块面板的内容,而不是丢掉它在布局里的位置。 ## 绘制 chrome 六个 handler,全都可选,挂在 `dock_area(...)` 元素上: | Handler | 画什么 | | -------------------------------- | ----------------------------------- | | `tab_bar(group => …)` | 一个 group 当前显示面板上方的标签栏 | | `empty_group(group => …)` | 没有可显示面板的 group 显示什么 | | `drop_indicator(drop => …)` | 被拖动的面板会落在哪里 | | `dock(dock => …)` | 一侧 dock 包住内容的外框 | | `tile_drag_bar(tile => …)` | 拖动 tile 用的那条拖拽条 | | `tile_resize_handles(tile => …)` | tile 的缩放把手 | 每一个都会先在 GPUI 的 layout pass 内部被调用,拿到的是 base **已经解析好的**状态——从不包含拖拽事件、鼠标位置或命中测试,因为 base 会把这些自己挂到拿回去的元素上。生成的描述按 handler 与解析后的状态缓存;未变化的帧只在 Rust 中重放,不会进入 JavaScript。 ```js dock_area(this.dock) .size_full() .tab_bar((group, cx) => h_flex() .h(30) .bg(cx.theme().colors.secondary) .children( group.tabs .filter((tab) => tab.visible) .map((tab) => h_flex() .id("tab-" + tab.id) .px(10) .items_center() .bg( tab.active ? cx.theme().colors.background : cx.theme().colors.secondary, ) .select_tab(group, tab.index) .drag_tab(group, tab.index) .child(tab.name) .child( div() .id("x-" + tab.id) .close_panel(group, tab.id) .child("×"), ), ), ), ); ``` ### 命令,不是回调 再看一眼上面那个标签页:它带的是 `select_tab` 和 `drag_tab`,不是 `on_click`。这是这套 API 里唯一一条值得理解、而不是死记的规则。 chrome 描述会被缓存,并且可以比生成它的 handler 调用活得更久。因此,在其中注册的脚本回调没有可靠的事件生命周期,而且每次原生状态变化都可能再创建一个。这样的注册会在写下它的那一行被拒绝;chrome 改用原生命令。 **命令**完全不携带脚本值。它只是指名 area 里的某个容器、以及要请它做什么,剩下的由 base 完成: | 命令 | 触发 | 作用 | | ------------------------------ | ---- | --------------------------------------------- | | `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 的边 | | `move_tile(tile)` | 拖动 | 在画布上移动这个 tile | | `resize_tile(tile, side)` | 拖动 | 拖动某条边或某个角 | | `raise_tile(tile)` | 按下 | 把这个 tile 提到最上层 | | `toggle_tile_zoom(tile)` | 点击 | 让 tile 放大占满所在 dock | | `close_tile(tile)` | 点击 | 关闭这个 tile | 每一个的第一个参数都是它所在 handler 拿到的那个对象。它们只能挂在 `div`、`h_flex` 或 `v_flex` 上:`Button` 自己构造内部结构,没有地方安放这些命令。 拖动产生的一切,base 都会在下一帧看到它之前钳制、吸附并取整,所以一个缩放把手只是一块命中区加一点颜色,仅此而已。 ### dock handler 自己安放内容 `dock` 是唯一一个除了状态之外还会拿到一个元素的 handler,而它返回什么就*替换*掉这侧 dock 的内容。把 `dock_content()` 放在面板该出现的位置: ```js .dock((dock, cx) => v_flex() .size_full() .relative() .child( h_flex() .h(30) .justify_between() .child(dock.placement.toUpperCase()) .child(div().id("collapse").toggle_dock(dock).child(dock.open ? "–" : "+")), ) .child(dock_content().flex_1()) .child(div().absolute().right(0).w(4).h_full().cursor_col_resize().resize_dock(dock)), ) ``` 忘了写 `dock_content()` 的 handler 仍然会显示它的面板——面板会画在它返回的内容之后,并带一条警告——而不是悄悄丢掉。 ## Tiles 一个区域也可以是自由浮动的画布,而不是标签组。传入 `bounds`,面板就成为一个 tile: ```js this.dock.add_panel(cx.new(Chart), { name: "chart", placement: "center", bounds: { x: 40, y: 40, width: 320, height: 240 }, }); ``` tile 需要自己的那两个 handler,因为 base 在那里同样什么都不画:`tile_drag_bar`(高度固定为 base 的拖拽条高度,吸附算法以此为前提)与 `tile_resize_handles`。两者拿到的 `tile` 里,bounds 都是**已经解析好的**。 ## 完整接口 ```js area.add_panel(view, options); area.remove_panel(id); area.panels(); area.dump(); area.load(state); area.has_dock(placement); area.is_dock_open(placement); area.toggle_dock(placement); area.remove_dock(placement); area.dock_size(placement); area.set_dock_size(placement, size); area.set_dock_collapsible(placement, collapsible); area.is_locked(); area.set_locked(locked); area.is_zoomed(); area.zoom_out(); area.on("layout_changed", handler); area.release(); ``` 被锁定的 area 不能重新排列,也不能接受放入操作;dock 和 tile 仍可调整大小。因此「锁定布局」固定的是面板所在位置,而不是面板的可用尺寸。 ## 一个完整的例子 ```bash cargo run -p gpui-shell -- examples/js_dock ``` `examples/js_dock/` 是一个工作区:左侧 dock 里是文件列表,中间是文档,标签栏与 dock 外框画在 `ui.js` 里,布局用定时器写进 `localStorage`。它是用上本页每一部分的最短的完整程序。 ## 从 Rust 使用 `gpui_kit::shell::dock` 是公开的,因此 Host 不写脚本也能接到同一处接缝。`ScriptPanel` 把 `ScriptView` 包成 `gpui_kit::base::dock::Panel`;`register_panel(application, panel, script, cx)` 教会注册表用一个 `PanelScript` 重建它;`ScriptDockSkin` 把 base 的三个 renderer trait 统一转发给一个 `DockChrome`。`tab_group_data`、`dock_data`、`tile_data` 与 `drop_indicator_data` 是引擎交给脚本代码的那几个 JSON 转换,Host 自己写绑定时同样用得上。 --- --- url: /docs/components/dropdown_button.md description: >- A DropdownButton is a combination of a button and a trigger button. It allows us to display a dropdown menu when the trigger is clicked, but the left Button can still respond to independent events. --- # DropdownButton A [DropdownButton] is a combination of a button and a trigger button. It allows us to display a dropdown menu when the trigger is clicked, but the left Button can still respond to independent events. Shared variant and size can be set on the DropdownButton. Action-specific options such as its label, icon, tooltip, loading state and click handler belong to the inner [Button]. ## Import ```rust use gpui_kit::component::button::{Button, DropdownButton}; ``` ## Usage ```rust use gpui_kit::Anchor; DropdownButton::new("dropdown") .button(Button::new("btn").label("Click Me")) .dropdown_menu(|menu, _, _| { menu.menu("Option 1", Box::new(MyAction)) .menu("Option 2", Box::new(MyAction)) .separator() .menu("Option 3", Box::new(MyAction)) }) ``` ### Variants Same as [Button], DropdownButton supports different variants. ```rust DropdownButton::new("dropdown") .primary() .button(Button::new("btn").label("Primary")) .dropdown_menu(|menu, _, _| { menu.menu("Option 1", Box::new(MyAction)) }) ``` Leaving the variant or size unset on the DropdownButton uses the inner button's value for both halves. ### Inner button options ```rust DropdownButton::new("dropdown") .button( Button::new("btn") .label("Save") .compact() .loading(is_saving) .tooltip("Save the current view") .on_click(|_, _, _| println!("Saved")), ) .dropdown_menu(|menu, _, _| { menu.menu("Save as…", Box::new(MyAction)) }) ``` ### With custom anchor ```rust DropdownButton::new("dropdown") .button(Button::new("btn").label("Click Me")) .dropdown_menu_with_anchor(Anchor::BottomRight, |menu, _, _| { menu.menu("Option 1", Box::new(MyAction)) }) ``` [Button]: https://docs.rs/gpui-component/latest/gpui_component/button/struct.Button.html [DropdownButton]: https://docs.rs/gpui-component/latest/gpui_component/button/struct.DropdownButton.html [Sizable]: https://docs.rs/gpui-component/latest/gpui_component/trait.Sizable.html --- --- url: /zh-CN/docs/components/dropdown_button.md description: DropdownButton 由一个主按钮和一个触发下拉菜单的按钮组合而成。 --- # DropdownButton [DropdownButton] 是一个组合型按钮组件。点击左侧主按钮时可以执行独立动作,点击右侧触发按钮时则会展开下拉菜单。 共享变体和尺寸可以设置在 DropdownButton 上。文案、图标、提示、加载状态和点击回调等动作自身的选项属于内层 [Button]。 ## 导入 ```rust use gpui_kit::component::button::{Button, DropdownButton}; ``` ## 用法 ```rust use gpui_kit::Anchor; DropdownButton::new("dropdown") .button(Button::new("btn").label("Click Me")) .dropdown_menu(|menu, _, _| { menu.menu("Option 1", Box::new(MyAction)) .menu("Option 2", Box::new(MyAction)) .separator() .menu("Option 3", Box::new(MyAction)) }) ``` ### 变体 与 [Button] 一样,DropdownButton 支持不同视觉变体: ```rust DropdownButton::new("dropdown") .primary() .button(Button::new("btn").label("Primary")) .dropdown_menu(|menu, _, _| { menu.menu("Option 1", Box::new(MyAction)) }) ``` DropdownButton 上不设置变体或尺寸时,内层按钮的值会应用到两半。 ### 内层按钮选项 ```rust DropdownButton::new("dropdown") .button( Button::new("btn") .label("Save") .compact() .loading(is_saving) .tooltip("Save the current view") .on_click(|_, _, _| println!("Saved")), ) .dropdown_menu(|menu, _, _| { menu.menu("Save as…", Box::new(MyAction)) }) ``` ### 自定义锚点 ```rust DropdownButton::new("dropdown") .button(Button::new("btn").label("Click Me")) .dropdown_menu_with_anchor(Anchor::BottomRight, |menu, _, _| { menu.menu("Option 1", Box::new(MyAction)) }) ``` [Button]: https://docs.rs/gpui-component/latest/gpui_component/button/struct.Button.html [DropdownButton]: https://docs.rs/gpui-component/latest/gpui_component/button/struct.DropdownButton.html [Sizable]: https://docs.rs/gpui-component/latest/gpui_component/trait.Sizable.html --- --- url: /base/primitives/editor.md description: >- An unstyled source-code editor with language, gutter, folding, and decoration support. --- # Editor `Editor` is the source-code editing control. It builds on the shared text engine and adds a language, line-number gutter, folding, whitespace display, text decorations, highlighting, search infrastructure, diagnostics, and LSP hooks. Use [Input](./input.md) for single-line values and [Textarea](./textarea.md) for ordinary multi-line text. ## Import ```rust use gpui_kit::base::input::{Editor, EditorState, TabSize}; ``` ## Basic usage ```rust let editor = cx.new(|cx| { EditorState::new(window, cx) .language("rust") .line_number(true) .folding(true) .tab_size(TabSize { tab_size: 4, hard_tabs: false, }) .default_value("fn main() {\n println!(\"Hello\");\n}") }); Editor::new(&editor) ``` ## Whitespace and decorations ```rust let editor = cx.new(|cx| { EditorState::new(window, cx) .language("rust") .show_whitespaces(true) .default_value(source) }); let decorations = editor.update(cx, |state, cx| { state.create_decorations_collection(initial_decorations, cx) }); ``` Decoration collections track ranges as the text changes. Keep the returned collection alive for as long as its decorations should remain active. ## Highlighting and language features `InputHighlighterFactory`, `InputHighlighter`, diagnostic types, and the LSP provider traits are low-level extension seams for design-system authors. They operate on the shared `InputBaseState`; applications using the styled component normally configure these through their editor integration rather than through ordinary text fields. The runnable showcase demonstrates this seam with `syntect`. It selects the WASM-compatible `fancy-regex` backend rather than the native Oniguruma backend, so the same Rust highlighting adapter runs in the desktop example and the Base WASM example. Syntect only identifies syntax scopes: the adapter maps those to semantic names and resolves their styles through `HighlightStyleResolver`, so the application theme remains the source of colors and font styles. The adapter is intentionally simple and reparses the short sample after each edit; production integrations can keep incremental parser state in their `InputHighlighter` implementation. ## Font The editor has no font setting of its own: it paints with the ambient text style, so the family, size, weight, and line height come from the element the application wraps it in. ```rust div() .font_family("JetBrains Mono") .text_size(px(13.)) .child(Editor::new(&editor)) ``` A relative `line_height` keeps the rows in step with the glyphs at any size; an absolute one stays put. For a ready-made monospace treatment, see the [`gpui-component` Editor](../../docs/components/editor.md). ## Presentation The application owns editor colors, gutter appearance, fold icons, and overlay content. Use `InputEditorStyle`, `FoldIconRenderer`, and the provider traits to connect those adapters. For the repository's ready-made visual treatment, see the [`gpui-component` Editor](../../docs/components/editor.md). ## Runnable example ```bash cargo run -p gpui-base --example components -- editor ``` --- --- url: /docs/components/editor.md description: 'Source-code editor with syntax highlighting, gutter, folding, and decorations.' --- # Editor `Editor` is the styled source-code control. Use [`Input`](./input.md) for single-line values and [`Textarea`](./textarea.md) for ordinary multi-line text. ## Import ```rust use gpui_kit::component::input::{Editor, EditorState, TabSize}; ``` ## Basic usage ```rust let editor = cx.new(|cx| { EditorState::new(window, cx) .language("rust") .line_number(true) .folding(true) .tab_size(TabSize { tab_size: 4, hard_tabs: false, }) .default_value("fn main() {\n println!(\"Hello\");\n}") }); Editor::new(&editor).h(px(320.)) ``` The language set via `.language()` selects syntax highlighting. Enable the matching Cargo feature, such as `tree-sitter-rust` or `tree-sitter-markdown`; use `tree-sitter-languages` to bundle all built-in grammars. ## Editor options ```rust let editor = cx.new(|cx| { EditorState::new(window, cx) .language("json") .line_number(true) .folding(true) .show_whitespaces(true) .default_value(source) }); ``` ## Decorations ```rust let decorations = editor.update(cx, |state, cx| { state.create_decorations_collection(initial_decorations, cx) }); ``` Keep the returned `TextDecorationCollection` alive while the decorations are needed. Its ranges follow subsequent text edits. ## Value and events ```rust let source = editor.read(cx).value(); editor.update(cx, |state, cx| { state.set_value(new_source, window, cx); }); cx.subscribe(&editor, |this, state, event: &InputEvent, cx| { if matches!(event, InputEvent::Change) { this.source = state.read(cx).value(); cx.notify(); } }); ``` ## Font The editor paints its code in the theme's monospace font — `mono_font_family` at `mono_font_size` — with rows 1.5 times the font size. That is only the default: a text style set on the editor refines over it, and the gutter and row height follow the size. ```rust Editor::new(&editor).text_sm() Editor::new(&editor) .font_family("JetBrains Mono") .text_size(px(15.)) ``` These are the ordinary [`Styled`](https://docs.rs/gpui/latest/gpui/trait.Styled.html) methods every element has, so `font_weight` and `line_height` work the same way. ## Appearance ```rust Editor::new(&editor) .h(px(480.)) .bordered(true) .disabled(false) .readonly(false) .aria_label("Rust source") ``` Use `readonly` to preview a file without allowing changes. Unlike `disabled`, a read-only editor keeps the normal appearance and still can be focused, selected, copied and searched, it only rejects the changes made by the user. The programmatic APIs such as `set_value` keep working. ```rust Editor::new(&editor).readonly(true) ``` Editor focus does not add the single-line Input focus-border treatment. The gutter, current-line background, and scrollbars are painted as one aligned editor surface. Input-only adornments such as `prefix`, `suffix`, mask toggle, and clear button are intentionally absent. Compose toolbars and actions around `Editor`. --- --- url: /zh-CN/base/primitives/editor.md description: 支持语言、行号槽、折叠和装饰的无样式源代码编辑器。 --- # Editor `Editor` 是源代码编辑控件。它建立在共享文本引擎之上,增加语言、行号槽、折叠、空白字符显示、文本装饰、高亮、搜索基础、诊断与 LSP 扩展。单行值使用 [Input](./input.md),普通多行文本使用 [Textarea](./textarea.md)。 ## 导入 ```rust use gpui_kit::base::input::{Editor, EditorState, TabSize}; ``` ## 基本用法 ```rust let editor = cx.new(|cx| { EditorState::new(window, cx) .language("rust") .line_number(true) .folding(true) .tab_size(TabSize { tab_size: 4, hard_tabs: false }) .default_value("fn main() {\n println!(\"Hello\");\n}") }); Editor::new(&editor) ``` ## 空白字符与装饰 通过 `show_whitespaces(true)` 显示空白字符,通过 `create_decorations_collection` 创建随文本编辑自动跟踪范围的装饰集合。只要装饰仍需生效,就应保留返回的 collection。 ## 高亮与语言功能 `InputHighlighterFactory`、`InputHighlighter`、诊断类型和 LSP provider trait 是提供给设计系统作者的底层扩展点,作用于共享的 `InputBaseState`。样式组件的应用通常应通过编辑器集成配置它们,而不是普通文本框。 可运行展示使用 `syntect`,在 WASM 中选择兼容的 `fancy-regex` 后端。Syntect 只识别语法 scope;适配器把它们映射为语义名称,再由 `HighlightStyleResolver` 从应用主题解析颜色和字体样式。示例会在每次编辑后重新解析短代码;生产集成可以在 `InputHighlighter` 中保留增量解析状态。 ## 字体与表现 Editor 没有独立字体设置,而是使用环境文本样式。可在外层元素设置 `font_family`、`text_size`、字重和行高。应用负责编辑器颜色、行号槽、折叠图标和覆盖层;使用 `InputEditorStyle`、`FoldIconRenderer` 与 provider trait 接入。现成视觉方案参见 [`gpui-component` Editor](../../docs/components/editor.md)。 ## 可运行示例 ```bash cargo run -p gpui-base --example components -- editor ``` --- --- url: /zh-CN/docs/components/editor.md description: 支持语法高亮、行号、折叠和文本装饰的源代码编辑器。 --- # Editor `Editor` 用于编辑源代码。单行输入请使用 [Input](./input.md),普通多行文本请使用 [Textarea](./textarea.md)。 ## 导入 ```rust use gpui_kit::component::input::{Editor, EditorState, TabSize}; ``` ## 基础用法 ```rust let editor = cx.new(|cx| { EditorState::new(window, cx) .language("rust") .line_number(true) .folding(true) .tab_size(TabSize { tab_size: 4, hard_tabs: false, }) .default_value("fn main() {\n println!(\"Hello\");\n}") }); Editor::new(&editor).h(px(320.)) ``` 使用 `.language()` 指定语法高亮语言。应用需要启用对应的 Cargo feature,例如 `tree-sitter-rust` 或 `tree-sitter-markdown`;也可以使用 `tree-sitter-languages` 包含全部内置语法。 ## 编辑器选项 ```rust let editor = cx.new(|cx| { EditorState::new(window, cx) .language("json") .line_number(true) .folding(true) .show_whitespaces(true) .default_value(source) }); ``` ## 文本装饰 ```rust let decorations = editor.update(cx, |state, cx| { state.create_decorations_collection(initial_decorations, cx) }); ``` 需要装饰存在多久,就应将返回的 `TextDecorationCollection` 保留多久;文本修改后,其 range 会自动跟随内容变化。 ## 值与事件 ```rust let source = editor.read(cx).value(); editor.update(cx, |state, cx| { state.set_value(new_source, window, cx); }); ``` `EditorState` 会发出 `InputEvent::Change`、`Focus` 和 `Blur` 等事件。 ## 字体 Editor 默认使用主题中的等宽字体 —— `mono_font_family` 和 `mono_font_size`,行高为字号的 1.5 倍。这只是默认值:在 Editor 上设置的文本样式会覆盖它,gutter 和行高都跟随字号变化。 ```rust Editor::new(&editor).text_sm() Editor::new(&editor) .font_family("JetBrains Mono") .text_size(px(15.)) ``` 这些就是所有元素都有的 [`Styled`](https://docs.rs/gpui/latest/gpui/trait.Styled.html) 方法,`font_weight`、`line_height` 用法相同。 ## 外观 ```rust Editor::new(&editor) .h(px(480.)) .bordered(true) .disabled(false) .readonly(false) .aria_label("Rust 源代码") ``` 预览文件但不允许修改时使用 `readonly`。与 `disabled` 不同,只读编辑器保持正常外观,仍然可以聚焦、选中、复制和搜索,只是拒绝用户对内容的修改。`set_value` 等程序调用不受影响。 ```rust Editor::new(&editor).readonly(true) ``` Editor 聚焦时不会应用单行 Input 的焦点边框效果。gutter、当前行背景和滚动条会作为同一个编辑器表面对齐绘制。 前后缀、密码显示切换和清除按钮只属于单行 Input。Editor 的工具栏和操作按钮应组合在组件外部。 --- --- url: /docs/element_id.md description: To introduce the ElementId concept in GPUI. --- # ElementId The [ElementId] is a unique identifier for a GPUI element. It is used to reference elements in the GPUI component tree. Before you start using GPUI and GPUI Component, you need to understand the [ElementId]. For example: ```rs div().id("my-element").child("Hello, World!") ``` In this case, the `div` element has an `id` of `"my-element"`. The add `id` is used for GPUI for binding events, for example `on_click` or `on_mouse_move`, the `element` with `id` in GPUI we call [Stateful\][Stateful]. We also use `id` (actually, it uses [GlobalElementId] internally in GPUI) to manage the `state` in some elements, by using `window.use_keyed_state`, so it is important to keep the `id` unique. ## Unique The `id` should be unique within the layout scope (In a same [Stateful\][Stateful] parent). For example we have a list with multiple items: ```rs div().id("app").child( div().id("list1").child(vec![ div().id(1).child("Item 1"), div().id(2).child("Item 2"), div().id(3).child("Item 3"), ]) ).child( div().id("list2").child(vec![ div().id(1).child("Item 1"), ]) ) ``` In this case, we can named the child items with a very simple id, because they are have a parent `list1` element with an `id`. GPUI internal will generate [GlobalElementId] with the parent elements's `id`, in this example, the `Item 1` will have global\_id: ```rs ["app", "list1", 1] ``` And the `Item 1` in `list2` will have global\_id: ```rs ["app", "list2", 1] ``` So we can named the child items with a very simple id. [ElementId]: https://docs.rs/gpui/latest/gpui/enum.ElementId.html [GlobalElementId]: https://docs.rs/gpui/latest/gpui/struct.GlobalElementId.html [Stateful]: https://docs.rs/gpui/latest/gpui/struct.Stateful.html [Stateful]: https://docs.rs/gpui/latest/gpui/struct.Stateful.html --- --- url: /zh-CN/docs/element_id.md description: 介绍 GPUI 中的 ElementId 概念。 --- # ElementId [ElementId] 是 GPUI 元素的唯一标识符,用于在 GPUI 组件树中引用具体元素。 在开始使用 GPUI 和 GPUI Component 之前,最好先理解 [ElementId] 的作用。 例如: ```rs div().id("my-element").child("Hello, World!") ``` 在这个例子里,`div` 元素的 `id` 是 `"my-element"`。为元素添加 `id` 后,GPUI 才能将事件绑定到它上面,例如 `on_click` 或 `on_mouse_move`。在 GPUI 中,带有 `id` 的元素通常称为 [Stateful\][Stateful]。 我们也会在某些组件内部使用 `id` 来管理状态。实际上 GPUI 会在内部使用 [GlobalElementId],并通过 `window.use_keyed_state` 这类机制保存状态,因此 `id` 保持唯一非常重要。 ## 唯一性 `id` 需要在当前布局作用域内唯一,也就是在同一个 [Stateful\][Stateful] 父节点下不能重复。 例如下面这个包含多个列表项的结构: ```rs div().id("app").child( div().id("list1").child(vec![ div().id(1).child("Item 1"), div().id(2).child("Item 2"), div().id(3).child("Item 3"), ]) ).child( div().id("list2").child(vec![ div().id(1).child("Item 1"), ]) ) ``` 这里子项可以使用很简单的 `id`,因为它们已经处于带有 `id` 的父元素之下。 GPUI 内部会结合父元素的 `id` 自动生成 [GlobalElementId]。在这个例子中,`list1` 里的 `Item 1` 对应的 `global_id` 是: ```rs ["app", "list1", 1] ``` 而 `list2` 里的 `Item 1` 对应的 `global_id` 是: ```rs ["app", "list2", 1] ``` 因此,只要父级路径不同,子元素就可以复用较简单的局部 `id`。 [ElementId]: https://docs.rs/gpui/latest/gpui/enum.ElementId.html [GlobalElementId]: https://docs.rs/gpui/latest/gpui/struct.GlobalElementId.html [Stateful]: https://docs.rs/gpui/latest/gpui/struct.Stateful.html [Stateful]: https://docs.rs/gpui/latest/gpui/struct.Stateful.html --- --- url: /shell/elements.md description: >- Constructors, composition with child / children / when, and why an element description can only be used once. --- # Elements An element in `gpui-shell` is a **description**, not an object. It exists for one `render` call and is consumed when it is used. This page covers what you can build, how to compose it, and what the runtime does when a description is used twice. ## Constructors Each module carries what its own crate provides: ```js import { div, svg, image } from "gpui-kit"; import { h_flex, v_flex, Button, Link, Checkbox, Switch, Input, InputState, } from "gpui-base"; import { fps_monitor } from "gpui-fps"; ``` Functions are lowercase, and component types are capitalized and constructed through `.new`. That mirrors the Rust side one for one: `div()` is a free function there too, and `Button::new(id)` is an associated function on a type. | Constructor | From | Produces | | ------------------ | ----------- | --------------------------------------------------------------------------- | | `div()` | `gpui-kit` | An element with no layout of its own | | `"a string"` | `gpui-kit` | Text. A string is an element, so it goes straight into `.child(...)` | | `svg(path)` | `gpui-kit` | A theme-tinted vector icon from the application's own directory | | `image(path)` | `gpui-kit` | A full-colour image from the application's own directory | | `h_flex()` | `gpui-base` | A row | | `v_flex()` | `gpui-base` | A column | | `Button.new(id)` | `gpui-base` | A base `Button`: activation, focus, disabled and selected state, no styling | | `Link.new(id)` | `gpui-base` | A focusable external HTTP(S) link; set its target with `.href(url)` | | `Checkbox.new(id)` | `gpui-base` | A base controlled checkbox, no styling and no indicator | | `Switch.new(id)` | `gpui-base` | A base controlled switch, no styling | | `Input.new(state)` | `gpui-base` | A text field backed by an [`InputState`](./state.md#retained-state) | | `fps_monitor()` | `gpui-fps` | The native `gpui-fps` performance HUD, shared once per window | This is the set you need to get started, not the whole of it. The full inventory of bound components — `Select`, `Combobox`, `Tabs`, `Table`, `VirtualList`, `Slider`, `Popover`, `Avatar`, `Accordion`, `Pagination`, `CalendarState` and the rest — is in the [API reference](./api.md#the-gpui-base-module). ### Performance monitor `fps_monitor()` exposes the native `gpui-fps` HUD without moving its sampling or painting into JavaScript. The monitor is created on first use and reused per window. Render it at most once in a window, inside a `relative()` parent: ```js div().relative().size_full().child(content).child(fps_monitor()); ``` It is pinned to the top right by default. Use the existing anchor vocabulary to move it, for example `fps_monitor().anchor("bottom_left")`. The HUD owns its presentation; ordinary element styles, children, and interaction states do not apply to it. ### Why `.new(id)` and not `new Button(id)` The JavaScript habit would be `new Button(id)`. The runtime does not offer it, and the reason is the whole subject of this page: `new` promises an object with an identity — something you can keep, store on the instance, and use again. That is exactly what a description is not. `Button.new(id)` reads as "construct a description", which is what it does, and it matches the Rust spelling character for character. Views are the opposite case, and use the standard form: `class Counter extends View`. A View genuinely does have an identity and cross-frame state, and it is owned by GPUI. Two construction shapes in one file, because the two kinds of thing have different lifetimes. ### Ids The `id` given to `Button`, `Link`, `Checkbox` and `Switch` identifies the element across renders, which is how GPUI preserves focus and element state. Keep it stable and unique among siblings — `` `item-${item.id}` `` rather than an array index that shifts when the list is filtered. Anything else — a `div`, an `h_flex` — is identified by **where it sits in the tree your render built**. That is enough while the tree keeps its shape, and stops being enough the moment a conditional child appears above it: every element below shifts, and the pressed state, the focus and anything else keyed by identity shift with them. `.id(name)` is how you say which element this is rather than where it landed: ```js div() .id("toolbar") .active((el) => el.opacity(0.7)); ``` Name anything whose identity has to survive its neighbours changing. `Button`, `Link`, `Checkbox` and `Switch` already have an identity from `new(id)` and ignore this one (with a warning, rather than silently). ### Text **A string is an element.** GPUI implements `IntoElement` for `&str`, `String` and `SharedString`, so text is written by handing the string to whatever holds it, and there is no `text()` to call: ```js v_flex().child(`${this.remaining} of ${this.items.length} remaining`).child(42); ``` The style comes from the element holding it, exactly as it does in Rust: ```js div().text_size(12).font_semibold().child("AAPL"); ``` A string child is materialized as a `div` containing it, which is what `div().child(s)` already says. ### Images ```js svg("icons/check.svg").w(14).h(14).flex_none(); image("images/brand.png").w(120).h(40); ``` Both paths resolve against the **application root** — the directory handed to `gpui-shell` — not against the file that called the constructor. That asymmetry surprises people, so it is worth stating plainly: `import "./ui.js"` resolves relative to the importing file, the way every JavaScript module system does, while `svg("icons/check.svg")` and `image("images/brand.png")` resolve relative to the application root, the way a web application's public directory does. The runtime cannot tell which module called an asset constructor, so per-file asset paths are not available to it. Paths outside the application directory are rejected. A missing file is reported once per path with the location it was looked for, rather than silently drawing nothing. One asset may contain at most 16 MiB. Listing the asset tree is bounded at 10,000 entries and 1 MiB of UTF-8 name bytes, so asset discovery cannot grow memory without limit. Use `svg()` for a monochrome icon: it inherits the surrounding text colour, so an icon inside a dark button comes out light without the script saying so twice. Use `image()` when the source colours must be preserved, such as a logo, photo or illustration. ```js renderIcon(cx) { return div() .bg(cx.theme().colors.foreground) .text_color(cx.theme().colors.surface) .child(svg("icons/check.svg").w(11).h(11)); // draws in `surface` } ``` ## Composition | Method | Effect | | -------------------------- | ------------------------------------------------ | | `.child(element)` | Adds one child. The child is consumed | | `.children(iterable)` | Adds several, in order | | `.when(condition, branch)` | Applies `branch` only when `condition` is truthy | ```js v_flex() .gap(8) .child(this.header()) .children(this.visible().map((item) => this.row(item))) .when(this.items.length === 0, (el) => el.child("Nothing yet")); ``` `.when` exists so a conditional does not break the chain in two. `branch` **must return the element** — a branch that returns nothing throws immediately, rather than quietly dropping everything it built: ```text when(...) must return the element ``` This mirrors GPUI's own `FluentBuilder` and the repository's Rust style rule: keep element construction as one fluent chain. For a condition that chooses between two elements, an ordinary ternary is clearer than `when`: ```js .child( visible.length === 0 ? emptyState("No items yet", "Type above and press Add.") : v_flex().children(visible.map((item) => this.row(item))), ) ``` ## Behavior methods These are not styles; they report state to the base layer, which handles the interaction and leaves the appearance to you. | Method | On | Effect | | --------------------------------- | ------------------------------------------ | ------------------------------------------------------------- | | `.on_click(handler)` | `Button` | `handler(event, cx)`, on click **and** on keyboard activation | | `.on_change(handler)` | `Checkbox`, `Switch` | `handler(checked, cx)`; the script stores the value | | `.disabled(value)` | `Button`, `Checkbox`, `Switch` | Blocks activation and reports the state | | `.selected(value)` | `Button` | Reports the selected state | | `.checked(value)` | `Checkbox`, `Switch` | The controlled value | | `.accessibility_label(text)` | `Button`, `Checkbox` | What a screen reader announces | | `.tooltip(text)` | `div`, `h_flex`, `v_flex`, `Button` | A label shown after the pointer rests on the element | | `.id(name)` | `div`, `h_flex`, `v_flex` | A stable identity, instead of position in the tree | | `.overflow_scrollbar()` | `div`, `h_flex`, `v_flex` | Scrolls both axes and paints native scrollbars | | `.overflow_x_scrollbar()` | `div`, `h_flex`, `v_flex` | Scrolls horizontally and paints a native scrollbar | | `.overflow_y_scrollbar()` | `div`, `h_flex`, `v_flex` | Scrolls vertically and paints a native scrollbar | | `.on_key_down(handler)` | [input-capable](#where-input-is-installed) | `handler(event, cx)` while this element holds the keyboard | | `.on_key_up(handler)` | [input-capable](#where-input-is-installed) | The same on release | | `.on_mouse_down(button, handler)` | [input-capable](#where-input-is-installed) | A press of `"left"`, `"right"` or `"middle"` | | `.on_mouse_up(button, handler)` | [input-capable](#where-input-is-installed) | Its release | | `.on_mouse_down_out(handler)` | [input-capable](#where-input-is-installed) | A press anywhere **outside** this element | | `.on_scroll_wheel(handler)` | [input-capable](#where-input-is-installed) | Wheel and trackpad scrolling over it | | `.on_action(action, handler)` | [input-capable](#where-input-is-installed) | A named action dispatched to it or into it | | `.key_context(name)` | [input-capable](#where-input-is-installed) | The key-binding context this element and its subtree sit in | Disabled, selected and checked **appearance** is yours to draw. The base layer only reports the state; nothing changes on screen unless the script says so: ```js Button.new("clear") .disabled(this.completed === 0) .when(this.completed === 0, (el) => el.opacity(0.4)) .child("Clear completed"); ``` `.accessibility_label` matters most on an icon-only control, which announces nothing without it: ```js Button.new(`remove-${item.id}`) .accessibility_label(`Remove “${item.caption}”`) .child(svg("icons/trash.svg").w(14).h(14)); ``` ### Controlled values report intent A base checkbox does not change its own state. It reports what the user asked for, and the script decides: ```js Checkbox.new(`item-${item.id}`) .checked(item.done) // the value comes from script state .on_change((done, cx) => { // the callback is a request this.toggle(item.id, done, cx); }) .child(indicator(item.done)) .child(label(item.caption)); ``` The runtime never quietly maintains a checked flag on the script's behalf. If it did, script authors and Rust authors would hold different mental models of the same control inside one application. ### Event objects An `on_click` handler receives a plain object whose field names mirror the Rust struct: ```js .on_click((event, cx) => { // event.click_count === 1 // event.modifiers === { shift, control, alt, platform } }); ``` `platform` is Command on macOS and the Windows key elsewhere. Only semantics the base layer has already normalized are exposed — Base treats "Enter activates the button" and "the button was clicked" as the same callback, and the script does not see the difference. A key handler receives the chord twice over. `keystroke` is the whole thing in the spelling a binding is written in; `key` and `modifiers` are the same chord taken apart, for when only one half matters: ```js .on_key_down((event, cx) => { if (event.keystroke === "cmd-s") { this.save(); cx.stop_propagation(); } }); ``` **The platform modifier is spelled `cmd` everywhere**, including Linux and Windows. GPUI spells it for the platform it was built for — `cmd-`, `super-`, `win-` — which is right for a keymap a person reads and wrong for a string a program compares: one script file runs on all three, so `event.keystroke === "cmd-s"` has to mean the same thing in all three. A pointer handler receives the button, how many presses are in the current sequence, and where it landed. `local_position` and `bounds` are absent until the element has been painted once: ```js .on_mouse_down("right", (event, cx) => { // event.button === "right" // event.click_count === 1 // event.local_position?.x — relative to this element this.openMenuAt(event.position, cx); }); ``` A scroll handler receives pixels either way, and the original line count when the device reported lines: ```js .on_scroll_wheel((event, cx) => { this.offset += event.delta.y; // always pixels // event.delta_lines?.y — only when the device said lines cx.notify(); }); ``` ### Where input is installed The eight methods above are GPUI's own `InteractiveElement` builders, and the shell installs them on `div`, `h_flex`, `v_flex`, `Button`, `Link`, `Checkbox`, `Switch`, `Radio`, `Toggle`, `Tabs` and `Tab`. Every other component builds its own base type and hangs its own listeners on it, so a handler written on one of those is recorded in the description and never reaches GPUI. The log says so rather than leaving you to find it: ```text `on_key_down` is not wired on a Select: the shell installs GPUI's input listeners on the element it owns outright, which is a plain `div`, `h_flex` or `v_flex`. Wrap it and write `on_key_down` on the wrapper ``` **Wired is not the same as reachable.** A key event travels the focus path and a pointer event travels the hitbox, so a component that accepts no focus handle — `Tab` is one — hears presses and never hears keys, however well both are wired. Which components accept a focus handle is covered under [Focus and accessibility](#focus-and-accessibility). ### Actions and key bindings An action is the level above a keystroke. `cx.bind_keys` says which chord means `"save"`, in which context; `on_action` says what `"save"` does. A menu item or a toolbar button dispatching the same name reaches the same handler, and neither end knows about the other: ```js init(_props, cx) { cx.bind_keys([ { keystroke: "cmd-s", action: "save", context: "Editor" }, { keystroke: "ctrl-k ctrl-c", action: "comment", context: "Editor" }, ]); } render(_cx) { return div() .key_context("Editor") .track_focus(this.handle) .on_action("save", (_event, cx) => this.save(cx)) .child( Button.new("save") .on_click(() => window.dispatch_action("save")) .child("Save"), ); } ``` `context` is a predicate matched against the `key_context(...)` an element declares, so one chord can mean one thing in a list and another in an editor. The keymap belongs to the application rather than to a window, so a chord bound in one View is live wherever its predicate matches. Registering several `on_action`s on one element is fine and they are independent. An action none of them claims carries on to an element further out, which is what lets an inner pane handle Save while the window around it handles Quit. The whole binding list is validated before any of it is installed: a keymap half-applied because the fourth entry had a typo is a worse state than one not applied, and a script cannot see which half made it. ::: tip Use arrow functions for handlers An arrow function does not bind its own `this`, so `this` inside the handler is still the View instance. A `function () {}` handler gets the wrong `this`. This is the single most common mistake in scripts written for this runtime, by people and by models alike. ::: ## Focus and accessibility A script owns its own focus targets. `cx.focus_handle()` creates one — `App::focus_handle` in GPUI, which has no `FocusHandle::new` for this to mirror — it lives on the View the way an [`InputState`](./state.md#retained-state) does, and `.track_focus(handle)` gives it to an element: ```js init(props, cx) { this.search = cx.focus_handle(); } render() { return Button.new("search") .tab_index(1) .track_focus(this.search) .child("Search"); } ``` `cx.focus_handle()` needs a live host call, and a handle created inside `render` would be a new one on every frame — so the focus it tracked would be dropped by the next repaint. It belongs in `init` or in an event handler; calling it in `render` throws. | On the handle | Answers | | --------------------- | ----------------------------------------------- | | `handle.focus()` | Moves the keyboard onto the element tracking it | | `handle.is_focused()` | Whether that element currently has the keyboard | | `handle.release()` | Drops the handle | `Tab` and `Shift-Tab` are handled by the window root, which walks the order below in both directions and honours the focus trap of an open dialog or sheet. | Method | On | Effect | | --------------------------- | ------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- | | `.track_focus(handle)` | `div`, `h_flex`, `v_flex`, `Button`, `Checkbox`, `Radio`, `Toggle` | Binds the element to a handle the script owns | | `.tab_index(n)` | those, and `Link`, `Switch` | Where the element sits in the window's Tab order; it also makes the element a tab stop | | `.tab_stop(value)` | the same set | Whether Tab can land on it at all. `false` keeps its place in the order without making it reachable | | `.role(name)` | `div`, `h_flex`, `v_flex`, `Button`, `Checkbox` | What the element announces itself as | | `.aria_selected(value)` | `div`, `h_flex`, `v_flex` | The selected state of an option in a list the script built | | `.aria_active_descendant()` | `div`, `h_flex`, `v_flex` | Announces this element as the focused one while an ancestor holds the keyboard — the highlighted option of a combobox whose input keeps focus | The sets differ because the components differ. `Button`, `Checkbox`, `Radio` and `Toggle` build their focus handle from a value you can replace; `Link` and `Switch` build their own and have no builder to replace it. Every component except `Button` and `Checkbox` announces a role of its own — a `Tab` is a tab, a `Radio` is a radio — and only those two treat the role as an override, which is what lets a button announce itself as a menu item. A call a component cannot honour is **reported in the log**, not silently dropped: ```text `role` is not wired on a Tab: base's Tab owns this part of its own focus and accessibility. Put it on an element around it ``` The plain elements take all six, which is how a script builds the listbox, toolbar or dialog the base layer has no component for: ```js div() .id(`cadence-${index}`) .role("list_box_option") .aria_selected(index === this.chosen) .when(index === this.chosen, (el) => el.aria_active_descendant()) .child(name); ``` Role names mirror `gpui_kit::Role` in snake\_case — `list_box`, `list_box_option`, `combo_box`, `menu_item` — and the whole set is in `gpui-kit.d.ts` as the `Role` union, so an editor completes them and a name that is not one fails at the call site: ```text unknown accessibility role `listbox`; the names mirror gpui_kit::Role in snake_case — see the Role type in gpui-kit.d.ts ``` ## Elements are single-use This is the rule that most often surprises a new reader, so here is what it looks like and why it holds. ```js const row = h_flex().child("hello"); v_flex().child(row).child(row); // throws ``` ```text element `h_flex` was already added to a parent; elements are single-use values ``` Storing one across frames fails the same way: ```js init() { this.header = h_flex().child("Todo"); // wrong } render() { return v_flex().child("Todo list").child(this.header); } ``` ```text this element belongs to a previous render pass; elements are single-use values and must be rebuilt each time render runs ``` One rough edge worth knowing about: the arena is cleared and its indices reused on every pass, so a stale element occasionally holds the index the runtime has just handed to the node it is being attached to. The misuse is still caught, but the message reads `an element cannot be added to itself` instead. Both mean the same thing — the element belongs to a pass that has ended. ### Why The restriction comes from GPUI itself: `RenderOnce::render` takes `self` **by value**, and `.child()` takes its child by value. In Rust the compiler enforces that with move semantics: using a moved value is a compile error. JavaScript has no move semantics and no compiler, so the runtime enforces the same rule at run time — and the description arena already has the bookkeeping needed to do it, because it marks a node as parented the moment it is attached. The alternative would be to copy the description on reuse. That was rejected: it would make the same script mean different things in Rust and in JavaScript, and reuse is almost always a mistake rather than an intention. ### The shape that works Build in `render`, and factor repetition into **functions that return a new element each time**: ```js const label = (value, cx) => div().text_size(12).text_color(cx.theme().colors.foreground).child(value); render(cx) { return v_flex() .child(label("first", cx)) .child(label("second", cx)); } ``` That is how the [example application](https://github.com/longbridge/gpui-kit/tree/main/examples/js_todolist) is written: `ui.js` exports `button`, `label`, `icon`, `checkbox` and the rest as functions, and `main.js` calls them. It reads like a component library and costs nothing, because a function call is where a fresh description comes from. ## Callbacks belong to their render A handler passed to `.on_click` belongs to the description that render produced — not to a frame. That description is [replayed by every frame until something invalidates it](./state.md#when-render-runs), and the handler stays callable for all of them. The description records only an id; the closure Rust assembles holds a weak reference to the runtime plus that id. The description a render replaced is kept one generation longer, because an event can be dispatched against a frame that has already been superseded. An event that arrives later than that is dropped with a `debug` log rather than an error — the author did nothing wrong, and there is nothing for them to fix. The practical consequence is that a rendered callback is not a subscription. For something that must outlive the pass that created it — reacting to an input's `change` event, say — see [State and Views](./state.md#input-events). ## Unknown methods are errors A method that is neither a style nor one of the behavior methods above fails at the call site, with a suggestion when there is a close one: ```text unknown element method `items_centre` (did you mean `items_center`?) ``` ```text unknown element method `on_clicked`; it is neither a style method nor one of child, children, when, on_click, on_change, disabled, selected, checked, id ``` This matters more than it looks. A mistyped style name changes nothing on screen — it simply fails to — and without a diagnostic it is invisible. See [Styling](./styling.md#unknown-methods) for how the runtime produces that message without paying for it on every render. ## Not there yet The element surface now includes Tabs, Table, Progress, form controls, anchored Popover/HoverCard surfaces, Textarea, Scrollbar, PathBuilder, VirtualList, and a [dock area](./dock.md) whose chrome the script draws. Still missing, deliberately: * the higher-level List and Tree systems and the remaining `gpui-base` components; * `gpui.memo`, which would let an unchanged subtree skip the script work that rebuilds its description. Focus is now the script's to own, but not all of it. Still missing: * **Keyboard navigation inside a composite, which is yours to write.** Tab and Shift-Tab move between controls; the arrow keys that move *within* a listbox, a menu or a tab list do not appear on their own. The pieces exist now — `on_key_down`, `cx.bind_keys` and `key_context` — but turning ↑ / ↓ into a moving highlight is still the script's job. * **The first Tab into an unfocused window.** While nothing at all holds focus, the root's Tab binding has no dispatch path to reach; focus has to arrive some other way first — a click, or `handle.focus()`. * **`Tab`, `Tabs`, and the table, group and progress parts** stay out of the Tab order. Base leaves them out of keyboard focus, and `tab_index` on one of them is reported rather than honoured. * **`track_focus` on `Link` and `Switch`**, for the same reason: they build their own handle and expose no builder to replace it. --- --- url: /shell/examples.md description: >- Complete standalone and embedded applications, including retained state, HostModule registrations, and native motion. --- # Examples Three examples ship with the repository, and together they cover a standalone application, a script backed by host state, and motion whose frames never enter JavaScript. | | Runs as | Shows | | --- | --- | --- | | [Todo list](#the-todo-list) | A standalone application | The whole script surface: retained input, a dialog, a toast, gated storage, assets, types | | [Workspace](#the-workspace) | A standalone application | A dockable layout: panels that survive a restart, and every piece of its chrome drawn by script | | [Quote board](#the-quote-board) | A panel inside the gallery | The host half: HostModule registrations, one entity read from two languages, live cost counters | | [Native motion](#native-motion) | A separate gallery script View | Pixel target transitions and springs retained and sampled by GPUI | ## A complete application The examples here are each built to show one thing. For a whole product in one repository — OAuth, a live WebSocket quote feed, a virtualized watchlist, retained nested Views for the price chart, and its own Rust host binary — see [**longbridge/longbridge-lite**](https://github.com/longbridge/longbridge-lite). It is a read-only Longbridge desktop client of a few thousand lines of JavaScript, and it is the largest thing written against this runtime. ## The todo list ```bash cargo run -p gpui-shell -- examples/js_todolist ``` `examples/js_todolist/` exists to exercise the whole runtime rather than to be minimal — if something in `gpui-shell` is broken, this is where it shows first. ```text main.js the View: state, filtering, every handler ui.js the presentation layer, exported as functions storage.js persistence, and what to do when it is not granted confirm.js the confirmation dialog, a View of its own icons/ four SVGs, resolved against the application root gpui-kit.d.ts generated; jsconfig.json and types.d.ts wire up typing ``` Four things in it are worth copying. **`ui.js` is a component library made of functions.** It exports `label`, `muted`, `title`, `button`, `iconButton`, `checkbox`, `field`, `row`, `surface`, `rule` and `emptyState`, and `main.js` reads like it is using a component library: ```js export const label = (value, cx) => div().text_size(12).line_height(1).text_color(cx.theme().colors.foreground).child(value); export const surface = (cx) => v_flex().flex_1().bg(cx.theme().colors.surface).border(1).border_color(cx.theme().colors.border).overflow_hidden(); ``` `main.js` passes the current `cx` to these helpers, which read tokens directly through `cx.theme()`. That costs nothing, because [a fresh description is exactly what a function call produces](./elements.md). It is also the answer to "the base layer ships no styled widgets" — you write the styled layer once, in your own file, and stop repeating it. **Storage absorbs a refusal instead of checking for permission.** `store` throws when the host did not grant it, and that is a fact about the host rather than an error in the application: ```js export function load() { try { const saved = store.get(KEY); return Array.isArray(saved) ? saved : []; } catch (error) { console.warn(`todolist: storage unavailable, starting empty (${error.message})`); return []; } } ``` `save()` returns whether the write landed, and the footer says so on screen — "Not saved — this host did not grant storage, so the list lasts for this run only". Absorb the refusal at the boundary, then tell the user the truth. **A dialog is a function, not an element.** `confirm.js` default-exports a function that returns the content function; `main.js` opens it with `window.open_dialog(confirmClear(count, onConfirm))`. The count and the callback are closed over rather than handed across. See [Overlays](./overlays.md). **Types are set up, and it is three files.** `jsconfig.json` turns on `checkJs`, `gpui-kit.d.ts` is generated by `gpui-shell types`, and `types.d.ts` holds the application's own shapes — `Todo`, `Filter`. Editor completion and `checkJs` errors work from there with no build step. ## The workspace ```bash cargo run -p gpui-shell -- examples/js_dock ``` `examples/js_dock/` is a dockable workspace — a file list on the left, documents in the center, and a layout that comes back the way you left it. ```text main.js the workspace: panels, the dock, and persistence ui.js the chrome: tabs, the dock frame, the drop hint ``` Three things in it are the point. **Base draws no chrome, so all of it is in `ui.js`.** The tab bar, the dock's title strip, the collapse control, the resize handle and the drop hint are ordinary elements written with the ordinary style surface. An area with none of them still docks, drags, resizes and persists; it simply paints nothing but the panels. **A tab carries commands, not handlers.** A chrome description is cached until its native state changes, so a script event handler inside it would have no sound lifetime. `select_tab(group, tab.index)` and `close_panel(group, tab.id)` carry no script value at all — they name a container and what to ask it. **A panel is a View with two extra methods.** `Document.serialize()` returns its caption and its edit count; `deserialize(data)` takes them back after a restart. Everything else about the panel — where it sits, whether it is displayed — is the layout's business and never reaches the script. See [Dock and Panels](./dock.md) for the whole surface. ## The quote board ```bash cargo run -- shell ``` The gallery's Shell story runs two panels side by side: the left one drawn by `shell_story.rs` in Rust, the right one by `crates/story/js/quotes/main.js` in JavaScript, reading the same data. The script owns no state at all. The board is a Rust `Entity`, imported from the [HostModule](./host-module.md) the story registered before the runtime started: ```text import { quotes, ticks, watch, watch_all } from "market"; ``` Theme values come from the call-scoped `cx.theme()` Snapshot, not a second HostModule. Because both panels read one entity, any disagreement between them is visible immediately — which is what makes this a test rather than a demo. Editing `main.js` changes the right-hand panel with no `cargo build` in between; the story has a "Reload script" button next to the panel. Underneath sits the counter readout this documentation quotes throughout: script runs a second against frames a second, with a feed selector to move one without the other. That is [the performance claim](./index.md#performance-the-script-is-not-in-the-frame) made visible in a running window. ## Native motion `crates/story/js/motion/main.js` is intentionally a separate `ScriptView` from the quote benchmark, so animation activity cannot contaminate the render-frequency measurement. It lets you switch between `.transition(...)` and `.spring(...)`, then retargets opacity and pixel-valued width, height, left, and top. The script runs once to publish the new target. GPUI schedules and samples every following animation frame natively, with no JavaScript re-entry. The example uses only numeric pixel targets — no `rem`, percentages, or `auto` — and stable ids so retained channels survive description rebuilds. ## Where to start Copy `examples/js_todolist` into a directory of your own and run it — it is a complete application with types already wired. Strip `main.js` back to a `View` with an `init` and a `render`, keep `ui.js`, and build up from there. For a host, `crates/story/src/stories/shell_story.rs` is a working reference for the other side: it builds a runtime, exports HostModule registrations, mounts a `ScriptView`, and reloads it on demand. [Hosting](./hosting.md) walks through the same calls. --- --- url: /docs/components/focus-trap.md description: >- A utility element that traps keyboard focus within a container, preventing Tab navigation from escaping. --- # Focus Trap Focus trap utility for constraining keyboard focus within a specific container. Essential for modal dialogs, sheets, and overlay components to provide proper keyboard navigation accessibility. **Note:** [Dialog](/docs/components/dialog) and [Sheet](/docs/components/sheet) components have focus trap built-in. You only need to manually use `focus_trap()` for custom modal-like components. ## Import ```rust use gpui_kit::component::FocusTrapElement; ``` ## Usage ### Basic Focus Trap ```rust let container_handle = cx.focus_handle(); v_flex() .child(Button::new("btn1").label("Button 1")) .child(Button::new("btn2").label("Button 2")) .child(Button::new("btn3").label("Button 3")) .focus_trap("trap1", &container_handle) // Pressing Tab will cycle: btn1 -> btn2 -> btn3 -> btn1 // Focus will not escape to elements outside this container ``` ### Multiple Focus Traps You can have multiple independent focus trap areas in your application. Each trap operates independently: ```rust let trap1_handle = cx.focus_handle(); let trap2_handle = cx.focus_handle(); v_flex() .gap_4() // First focus trap area .child( h_flex() .gap_2() .child(Button::new("trap1-1").label("Area 1 - Button 1")) .child(Button::new("trap1-2").label("Area 1 - Button 2")) .child(Button::new("trap1-3").label("Area 1 - Button 3")) .focus_trap("trap1", &trap1_handle) ) // Second focus trap area .child( h_flex() .gap_2() .child(Button::new("trap2-1").label("Area 2 - Button 1")) .child(Button::new("trap2-2").label("Area 2 - Button 2")) .focus_trap("trap2", &trap2_handle) ) ``` ### Focus Trap with Dialog [Dialog] components have focus trap built-in automatically. You don't need to manually add `focus_trap()`: ```rust window.open_dialog(cx, |dialog, _, _| { dialog .title("Settings") .child( v_flex() .gap_3() .child(Button::new("save").label("Save")) .child(Button::new("cancel").label("Cancel")) .child(Button::new("reset").label("Reset")) ) // Dialog internally uses focus_trap() // Tab navigation automatically cycles: save -> cancel -> reset -> save }) ``` ### Focus Trap with Sheet [Sheet] components also have focus trap built-in automatically: ```rust window.open_sheet(cx, |sheet, _, _| { sheet .title("Filter Options") .child( v_flex() .gap_2() .child(Checkbox::new("option1").label("Option 1")) .child(Checkbox::new("option2").label("Option 2")) .child(Button::new("apply").label("Apply Filters")) ) // Sheet internally uses focus_trap() // Focus automatically cycles within the sheet panel }) ``` ## How It Works The focus trap system consists of three key components: 1. **FocusTrapContainer**: Wraps any container element and registers it as a focus trap area 2. **FocusTrapManager**: Global state manager that tracks all active focus traps 3. **Root Integration**: The [Root] view intercepts Tab/Shift-Tab events and enforces focus cycling When Tab or Shift-Tab is pressed: 1. [Root] detects if the currently focused element is inside a focus trap 2. If yes, it calculates the next focusable element within the same trap 3. If focus would escape the trap, it cycles back to the beginning (Tab) or end (Shift-Tab) 4. This prevents focus from leaving the trapped container ### Built-in Focus Trap Components The following components have focus trap functionality built-in and don't require manual `focus_trap()` calls: * **[Dialog]** - Modal dialogs automatically trap focus (see `dialog.rs:437`) * **[Sheet]** - Side panels automatically trap focus (see `sheet.rs:197`) ## API Reference * [FocusTrapElement](https://docs.rs/gpui-component/latest/gpui_component/trait.FocusTrapElement.html) * [FocusTrapContainer](https://docs.rs/gpui-component/latest/gpui_component/struct.FocusTrapContainer.html) ## Examples ### Custom Modal with Focus Trap ```rust struct CustomModal { container_handle: FocusHandle, } impl CustomModal { fn new(cx: &mut App) -> Self { Self { container_handle: cx.focus_handle(), } } } impl Render for CustomModal { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { div() .absolute() .inset_0() .flex() .items_center() .justify_center() .child( v_flex() .gap_4() .p_6() .bg(cx.theme().background) .rounded(cx.theme().radius_lg) .shadow_lg() .border_1() .border_color(cx.theme().border) .child("This is a modal dialog") .child( h_flex() .gap_2() .child(Button::new("ok").primary().label("OK")) .child(Button::new("cancel").label("Cancel")) ) .focus_trap("modal", &self.container_handle) ) } } ``` ### Nested Focus Traps Focus traps support nesting. When multiple traps are active, the innermost trap takes precedence: ```rust let outer_handle = cx.focus_handle(); let inner_handle = cx.focus_handle(); div() .child( v_flex() .gap_4() .p_4() .border_1() .border_color(cx.theme().border) .child(Button::new("outer-1").label("Outer Button 1")) .child( // Inner trap takes precedence when focused h_flex() .gap_2() .p_4() .bg(cx.theme().accent.opacity(0.1)) .child(Button::new("inner-1").label("Inner Button 1")) .child(Button::new("inner-2").label("Inner Button 2")) .focus_trap("inner", &inner_handle) ) .child(Button::new("outer-2").label("Outer Button 2")) .focus_trap("outer", &outer_handle) ) ``` ### Conditional Focus Trap You can conditionally apply focus trapping based on application state: ```rust struct ModalView { is_modal: bool, container_handle: FocusHandle, } impl Render for ModalView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let content = v_flex() .gap_2() .child(Button::new("btn1").label("Button 1")) .child(Button::new("btn2").label("Button 2")) .child(Button::new("btn3").label("Button 3")); if self.is_modal { // Apply focus trap when in modal mode content.focus_trap("conditional", &self.container_handle) .into_any_element() } else { // Normal behavior without focus trap content.into_any_element() } } } ``` ## Accessibility Notes * Focus trapping is essential for modal dialogs and overlays to meet WCAG accessibility guidelines * Always provide a way to close or dismiss trapped focus areas (ESC key, close button) * The first focusable element in the trap should receive focus when the trap is activated * Use focus traps sparingly - only for truly modal interactions * Ensure keyboard navigation order is logical within the trapped area ## See Also * [Root View System](/docs/root) - Manages focus trap behavior at the window level * [Dialog](/docs/components/dialog) - Uses focus trap automatically * [Sheet](/docs/components/sheet) - Uses focus trap automatically * [focus-trap-react](https://github.com/focus-trap/focus-trap-react) - Similar concept for React applications [Root]: https://docs.rs/gpui-component/latest/gpui_component/struct.Root.html [FocusTrapElement]: https://docs.rs/gpui-component/latest/gpui_component/trait.FocusTrapElement.html [Dialog]: /docs/components/dialog [Sheet]: /docs/components/sheet --- --- url: /zh-CN/docs/components/focus-trap.md description: 将键盘焦点限制在指定容器内的工具元素。 --- # Focus Trap Focus Trap 是一个用于将键盘焦点限制在特定容器内的工具能力,可防止用户通过 Tab 键把焦点移出当前区域。它对对话框、侧边面板和自定义覆盖层的可访问性非常重要。 **注意:** [Dialog](/docs/components/dialog) 和 [Sheet](/docs/components/sheet) 已内置 focus trap。只有在构建自定义类模态组件时,才需要手动使用 `focus_trap()`。 ## 导入 ```rust use gpui_kit::component::FocusTrapElement; ``` ## 用法 ### 基础 Focus Trap ```rust let container_handle = cx.focus_handle(); v_flex() .child(Button::new("btn1").label("Button 1")) .child(Button::new("btn2").label("Button 2")) .child(Button::new("btn3").label("Button 3")) .focus_trap("trap1", &container_handle) // Pressing Tab will cycle: btn1 -> btn2 -> btn3 -> btn1 // Focus will not escape to elements outside this container ``` ### 多个 Focus Trap 你可以在同一个应用中放置多个彼此独立的 focus trap 区域: ```rust let trap1_handle = cx.focus_handle(); let trap2_handle = cx.focus_handle(); v_flex() .gap_4() .child( h_flex() .gap_2() .child(Button::new("trap1-1").label("Area 1 - Button 1")) .child(Button::new("trap1-2").label("Area 1 - Button 2")) .child(Button::new("trap1-3").label("Area 1 - Button 3")) .focus_trap("trap1", &trap1_handle) ) .child( h_flex() .gap_2() .child(Button::new("trap2-1").label("Area 2 - Button 1")) .child(Button::new("trap2-2").label("Area 2 - Button 2")) .focus_trap("trap2", &trap2_handle) ) ``` ### 与 Dialog 配合 [Dialog] 已自动内置 focus trap,无需手动添加: ```rust window.open_dialog(cx, |dialog, _, _| { dialog .title("Settings") .child( v_flex() .gap_3() .child(Button::new("save").label("Save")) .child(Button::new("cancel").label("Cancel")) .child(Button::new("reset").label("Reset")) ) }) ``` ### 与 Sheet 配合 [Sheet] 也已自动内置 focus trap: ```rust window.open_sheet(cx, |sheet, _, _| { sheet .title("Filter Options") .child( v_flex() .gap_2() .child(Checkbox::new("option1").label("Option 1")) .child(Checkbox::new("option2").label("Option 2")) .child(Button::new("apply").label("Apply Filters")) ) }) ``` ## 工作原理 Focus trap 系统主要由三部分组成: 1. **FocusTrapContainer**:包装容器并将其注册为焦点陷阱区域。 2. **FocusTrapManager**:全局状态管理器,用于跟踪当前所有活跃的 focus trap。 3. **Root Integration**:由 [Root] 视图拦截 Tab/Shift-Tab 事件,并执行焦点循环。 当用户按下 Tab 或 Shift-Tab 时: 1. [Root] 会判断当前焦点是否位于某个 focus trap 中。 2. 如果是,则只计算该 trap 内部的下一个可聚焦元素。 3. 当焦点即将离开 trap 时,会循环回到开头或末尾。 4. 这样就能阻止焦点逸出当前容器。 ### 已内置 Focus Trap 的组件 以下组件已经内置 focus trap,不需要手动调用: * **[Dialog]** * **[Sheet]** ## API 参考 * [FocusTrapElement](https://docs.rs/gpui-component/latest/gpui_component/trait.FocusTrapElement.html) * [FocusTrapContainer](https://docs.rs/gpui-component/latest/gpui_component/struct.FocusTrapContainer.html) ## 示例 ### 自定义模态框 ```rust struct CustomModal { container_handle: FocusHandle, } impl CustomModal { fn new(cx: &mut App) -> Self { Self { container_handle: cx.focus_handle(), } } } impl Render for CustomModal { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { div() .absolute() .inset_0() .flex() .items_center() .justify_center() .child( v_flex() .gap_4() .p_6() .bg(cx.theme().background) .rounded(cx.theme().radius_lg) .shadow_lg() .border_1() .border_color(cx.theme().border) .child("This is a modal dialog") .child( h_flex() .gap_2() .child(Button::new("ok").primary().label("OK")) .child(Button::new("cancel").label("Cancel")) ) .focus_trap("modal", &self.container_handle) ) } } ``` ### 嵌套 Focus Trap 当多个 trap 嵌套时,最内层 trap 优先: ```rust let outer_handle = cx.focus_handle(); let inner_handle = cx.focus_handle(); div() .child( v_flex() .gap_4() .p_4() .border_1() .border_color(cx.theme().border) .child(Button::new("outer-1").label("Outer Button 1")) .child( h_flex() .gap_2() .p_4() .bg(cx.theme().accent.opacity(0.1)) .child(Button::new("inner-1").label("Inner Button 1")) .child(Button::new("inner-2").label("Inner Button 2")) .focus_trap("inner", &inner_handle) ) .child(Button::new("outer-2").label("Outer Button 2")) .focus_trap("outer", &outer_handle) ) ``` ### 条件启用 Focus Trap ```rust struct ModalView { is_modal: bool, container_handle: FocusHandle, } impl Render for ModalView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let content = v_flex() .gap_2() .child(Button::new("btn1").label("Button 1")) .child(Button::new("btn2").label("Button 2")) .child(Button::new("btn3").label("Button 3")); if self.is_modal { content.focus_trap("conditional", &self.container_handle) .into_any_element() } else { content.into_any_element() } } } ``` ## 可访问性说明 * Focus trap 对模态对话框和覆盖层满足 WCAG 要求非常关键。 * 始终要提供关闭方式,例如 ESC、关闭按钮或取消按钮。 * 激活 trap 后,应让首个可聚焦元素获得焦点。 * 不要滥用 focus trap,只在真正的模态交互中使用。 * 保证容器内部的键盘导航顺序合理。 ## 另请参阅 * [Root View System](/docs/root) * [Dialog](/docs/components/dialog) * [Sheet](/docs/components/sheet) * [focus-trap-react](https://github.com/focus-trap/focus-trap-react) [Root]: https://docs.rs/gpui-component/latest/gpui_component/struct.Root.html [FocusTrapElement]: https://docs.rs/gpui-component/latest/gpui_component/trait.FocusTrapElement.html [Dialog]: /docs/components/dialog [Sheet]: /docs/components/sheet --- --- url: /docs/components/form.md description: >- Flexible form container with support for field layout, validation, and multi-column layouts. --- # Form A comprehensive form component that provides structured layout for form fields with support for vertical/horizontal layouts, validation, field groups, and responsive multi-column layouts. ## Import ```rust use gpui_kit::component::form::{field, v_form, h_form, Form, Field}; ``` ## Usage ### Basic Form ```rust v_form() .child( field() .label("Name") .child(Input::new(&name_input)) ) .child( field() .label("Email") .child(Input::new(&email_input)) .required(true) ) ``` ### Horizontal Form Layout ```rust h_form() .label_width(px(120.)) .child( field() .label("First Name") .child(Input::new(&first_name)) ) .child( field() .label("Last Name") .child(Input::new(&last_name)) ) ``` ### Multi-Column Form ```rust v_form() .columns(2) // Two-column layout .child( field() .label("First Name") .child(Input::new(&first_name)) ) .child( field() .label("Last Name") .child(Input::new(&last_name)) ) .child( field() .label("Bio") .col_span(2) // Span across both columns .child(Input::new(&bio_input)) ) ``` ## Form Container and Layout ### Vertical Layout (Default) ```rust v_form() .gap(px(12.)) .child(field().label("Name").child(input)) .child(field().label("Email").child(email_input)) ``` ### Horizontal Layout ```rust h_form() .label_width(px(100.)) .child(field().label("Name").child(input)) .child(field().label("Email").child(email_input)) ``` ### Custom Sizing ```rust v_form() .large() // Large form size .label_text_size(rems(1.2)) .child(field().label("Title").child(input)) v_form() .small() // Small form size .child(field().label("Code").child(input)) ``` ## Form Validation ### Required Fields ```rust field() .label("Email") .required(true) // Shows asterisk (*) next to label .child(Input::new(&email_input)) ``` ### Field Descriptions ```rust field() .label("Password") .description("Must be at least 8 characters long") .child(Input::new(&password_input)) ``` ### Dynamic Descriptions ```rust field() .label("Bio") .description_fn(|_, _| { div().child("Use at most 100 words to describe yourself.") }) .child(Input::new(&bio_input)) ``` ### Field Visibility ```rust field() .label("Admin Settings") .visible(user.is_admin()) // Conditionally show field .child(Switch::new("admin-mode")) ``` ## Submit Handling ### Basic Submit Pattern ```rust struct FormView { name_input: Entity, email_input: Entity, } impl FormView { fn submit(&mut self, cx: &mut Context) { let name = self.name_input.read(cx).value(); let email = self.email_input.read(cx).value(); // Validate inputs if name.is_empty() || email.is_empty() { // Show validation error return; } // Submit form data self.handle_submit(name, email, cx); } } // Form with submit button v_form() .child(field().label("Name").child(Input::new(&self.name_input))) .child(field().label("Email").child(Input::new(&self.email_input))) .child( field() .label_indent(false) .child( Button::new("submit") .primary() .child("Submit") .on_click(cx.listener(|this, _, _, cx| this.submit(cx))) ) ) ``` ### Form with Action Buttons ```rust v_form() .child(field().label("Title").child(Input::new(&title))) .child(field().label("Content").child(Input::new(&content))) .child( field() .label_indent(false) .child( h_flex() .gap_2() .child(Button::new("save").primary().child("Save")) .child(Button::new("cancel").child("Cancel")) .child(Button::new("preview").outline().child("Preview")) ) ) ``` ## Field Groups ### Related Fields ```rust v_form() .child( field() .label("Name") .child( h_flex() .gap_2() .child(div().flex_1().child(Input::new(&first_name))) .child(div().flex_1().child(Input::new(&last_name))) ) ) .child( field() .label("Address") .items_start() // Align to start for multi-line content .child( v_flex() .gap_2() .child(Input::new(&street)) .child( h_flex() .gap_2() .child(div().flex_1().child(Input::new(&city))) .child(div().w(px(100.)).child(Input::new(&zip))) ) ) ) ``` ### Custom Field Components ```rust field() .label("Theme Color") .child(ColorPicker::new(&color_state).small()) field() .label("Birth Date") .description("We'll send you a birthday gift!") .child(DatePicker::new(&date_state)) field() .label("Notifications") .child( v_flex() .gap_2() .child(Switch::new("email").label("Email notifications")) .child(Switch::new("push").label("Push notifications")) .child(Switch::new("sms").label("SMS notifications")) ) ``` ### Conditional Fields ```rust v_form() .child( field() .label("Account Type") .child(Select::new(&account_type)) ) .child( field() .label("Company Name") .visible(is_business_account) // Show only for business accounts .child(Input::new(&company_name)) ) .child( field() .label("Tax ID") .visible(is_business_account) .required(is_business_account) .child(Input::new(&tax_id)) ) ``` ## Grid Layout and Positioning ### Column Spanning ```rust v_form() .columns(3) // Three-column grid .child(field().label("First").child(input1)) .child(field().label("Second").child(input2)) .child(field().label("Third").child(input3)) .child( field() .label("Full Width") .col_span(3) // Spans all three columns .child(Input::new(&full_width)) ) ``` ### Column Positioning ```rust v_form() .columns(4) .child(field().label("A").child(input_a)) .child(field().label("B").child(input_b)) .child( field() .label("Positioned") .col_start(1) // Start at column 1 .col_span(2) // Span 2 columns .child(input_positioned) ) ``` ### Responsive Layout ```rust v_form() .columns(if is_mobile { 1 } else { 2 }) .child(field().label("Name").child(name_input)) .child(field().label("Email").child(email_input)) .child( field() .label("Bio") .when(!is_mobile, |field| field.col_span(2)) .child(bio_input) ) ``` ## Examples ### User Registration Form ```rust struct RegistrationForm { first_name: Entity, last_name: Entity, email: Entity, password: Entity, confirm_password: Entity, terms_accepted: bool, } impl Render for RegistrationForm { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_form() .large() .child( field() .label("Personal Information") .label_indent(false) .child( h_flex() .gap_3() .child( div().flex_1().child( Input::new(&self.first_name) .placeholder("First name") ) ) .child( div().flex_1().child( Input::new(&self.last_name) .placeholder("Last name") ) ) ) ) .child( field() .label("Email") .required(true) .child(Input::new(&self.email)) ) .child( field() .label("Password") .required(true) .description("Must be at least 8 characters") .child(Input::new(&self.password)) ) .child( field() .label("Confirm Password") .required(true) .child(Input::new(&self.confirm_password)) ) .child( field() .label_indent(false) .child( Checkbox::new("terms") .label("I agree to the Terms of Service") .checked(self.terms_accepted) .on_click(cx.listener(|this, checked, _, cx| { this.terms_accepted = *checked; cx.notify(); })) ) ) .child( field() .label_indent(false) .child( Button::new("register") .primary() .large() .w_full() .child("Create Account") ) ) } } ``` ### Settings Form with Sections ```rust v_form() .column(2) .child( field() .label("Profile") .label_indent(false) .col_span(2) .child(Separator::horizontal()) ) .child( field() .label("Display Name") .child(Input::new(&display_name)) ) .child( field() .label("Email") .child(Input::new(&email)) ) .child( field() .label("Bio") .col_span(2) .items_start() .child(Input::new(&bio)) ) .child( field() .label("Preferences") .label_indent(false) .col_span(2) .child(Separator::horizontal()) ) .child( field() .label("Theme") .child(Select::new(&theme_state)) ) .child( field() .label("Language") .child(Select::new(&language_state)) ) .child( field() .label_indent(false) .child(Switch::new("notifications").label("Enable notifications")) ) .child( field() .label_indent(false) .child(Switch::new("marketing").label("Marketing emails")) ) ``` ### Contact Form ```rust v_form() .child( field() .label("Contact Information") .child( h_flex() .gap_2() .child( Select::new(&prefix_state) .w(px(80.)) ) .child( div().flex_1().child( Input::new(&name_input) .placeholder("Your name") ) ) ) ) .child( field() .label("Email") .required(true) .child(Input::new(&email_input)) ) .child( field() .label("Subject") .child(Select::new(&subject_state)) ) .child( field() .label("Message") .required(true) .items_start() .description("Please describe your inquiry in detail") .child(Input::new(&message_input)) ) .child( field() .label_indent(false) .child( h_flex() .gap_2() .justify_between() .child( Checkbox::new("copy") .label("Send me a copy") ) .child( h_flex() .gap_2() .child(Button::new("cancel").child("Cancel")) .child(Button::new("send").primary().child("Send Message")) ) ) ) ``` --- --- url: /zh-CN/docs/components/form.md description: 支持字段布局、校验和多列排布的灵活表单容器。 --- # Form Form 是一个完整的表单布局组件,适合组织字段、描述、校验提示以及多列响应式表单结构。它支持纵向和横向布局、字段分组以及列跨度控制。 ## 导入 ```rust use gpui_kit::component::form::{field, v_form, h_form, Form, Field}; ``` ## 用法 ### 基础表单 ```rust v_form() .child( field() .label("Name") .child(Input::new(&name_input)) ) .child( field() .label("Email") .child(Input::new(&email_input)) .required(true) ) ``` ### 横向布局 ```rust h_form() .label_width(px(120.)) .child( field() .label("First Name") .child(Input::new(&first_name)) ) .child( field() .label("Last Name") .child(Input::new(&last_name)) ) ``` ### 多列表单 ```rust v_form() .columns(2) .child( field() .label("First Name") .child(Input::new(&first_name)) ) .child( field() .label("Last Name") .child(Input::new(&last_name)) ) .child( field() .label("Bio") .col_span(2) .child(Input::new(&bio_input)) ) ``` ## 容器与布局 ### 纵向布局 ```rust v_form() .gap(px(12.)) .child(field().label("Name").child(input)) .child(field().label("Email").child(email_input)) ``` ### 横向布局 ```rust h_form() .label_width(px(100.)) .child(field().label("Name").child(input)) .child(field().label("Email").child(email_input)) ``` ### 自定义尺寸 ```rust v_form() .large() .label_text_size(rems(1.2)) .child(field().label("Title").child(input)) v_form() .small() .child(field().label("Code").child(input)) ``` ## 校验与说明 ### 必填字段 ```rust field() .label("Email") .required(true) .child(Input::new(&email_input)) ``` ### 字段描述 ```rust field() .label("Password") .description("Must be at least 8 characters long") .child(Input::new(&password_input)) ``` ### 动态描述 ```rust field() .label("Bio") .description_fn(|_, _| { div().child("Use at most 100 words to describe yourself.") }) .child(Input::new(&bio_input)) ``` ### 字段可见性 ```rust field() .label("Admin Settings") .visible(user.is_admin()) .child(Switch::new("admin-mode")) ``` ## 提交处理 ### 基础提交模式 ```rust struct FormView { name_input: Entity, email_input: Entity, } impl FormView { fn submit(&mut self, cx: &mut Context) { let name = self.name_input.read(cx).value(); let email = self.email_input.read(cx).value(); if name.is_empty() || email.is_empty() { return; } self.handle_submit(name, email, cx); } } v_form() .child(field().label("Name").child(Input::new(&self.name_input))) .child(field().label("Email").child(Input::new(&self.email_input))) .child( field() .label_indent(false) .child( Button::new("submit") .primary() .child("Submit") .on_click(cx.listener(|this, _, _, cx| this.submit(cx))) ) ) ``` ### 操作按钮组 ```rust v_form() .child(field().label("Title").child(Input::new(&title))) .child(field().label("Content").child(Input::new(&content))) .child( field() .label_indent(false) .child( h_flex() .gap_2() .child(Button::new("save").primary().child("Save")) .child(Button::new("cancel").child("Cancel")) .child(Button::new("preview").outline().child("Preview")) ) ) ``` ## 字段分组 ### 相关字段组合 ```rust v_form() .child( field() .label("Name") .child( h_flex() .gap_2() .child(div().flex_1().child(Input::new(&first_name))) .child(div().flex_1().child(Input::new(&last_name))) ) ) .child( field() .label("Address") .items_start() .child( v_flex() .gap_2() .child(Input::new(&street)) .child( h_flex() .gap_2() .child(div().flex_1().child(Input::new(&city))) .child(div().w(px(100.)).child(Input::new(&zip))) ) ) ) ``` ### 自定义字段组件 ```rust field() .label("Theme Color") .child(ColorPicker::new(&color_state).small()) field() .label("Birth Date") .description("We'll send you a birthday gift!") .child(DatePicker::new(&date_state)) ``` ### 条件字段 ```rust v_form() .child( field() .label("Account Type") .child(Select::new(&account_type)) ) .child( field() .label("Company Name") .visible(is_business_account) .child(Input::new(&company_name)) ) ``` ## 网格与定位 ### 列跨度 ```rust v_form() .columns(3) .child(field().label("First").child(input1)) .child(field().label("Second").child(input2)) .child(field().label("Third").child(input3)) .child( field() .label("Full Width") .col_span(3) .child(Input::new(&full_width)) ) ``` ### 响应式布局 ```rust v_form() .columns(if is_mobile { 1 } else { 2 }) .child(field().label("Name").child(name_input)) .child(field().label("Email").child(email_input)) .child( field() .label("Bio") .when(!is_mobile, |field| field.col_span(2)) .child(bio_input) ) ``` ## 示例 ### 注册表单 ```rust struct RegistrationForm { first_name: Entity, last_name: Entity, email: Entity, password: Entity, confirm_password: Entity, terms_accepted: bool, } ``` ### 设置表单 ```rust v_form() .column(2) .child( field() .label("Profile") .label_indent(false) .col_span(2) .child(Separator::horizontal()) ) .child( field() .label("Display Name") .child(Input::new(&display_name)) ) ``` --- --- url: /base/getting-started.md description: 'Install, initialize, and render your first gpui-base control.' --- # Getting Started ## Install Use the repository revision of GPUI that matches `gpui-base`: ```toml [dependencies] gpui-base = { git = "https://github.com/longbridge/gpui-kit" } gpui = { git = "https://github.com/zed-industries/zed" } gpui_platform = { git = "https://github.com/zed-industries/zed", features = ["font-kit"] } ``` ## Initialize Call `gpui_kit::base::init` once before opening windows. If the application already calls `gpui_kit::component::init`, base initialization is included. ```rust use gpui_kit::AppContext as _; fn main() { gpui_platform::application().run(|cx| { gpui_kit::base::init(cx); // Open your application window here. }); } ``` ## Render and style a control Base controls intentionally have no product-specific padding, colors, or radius. Style them with ordinary GPUI methods: ```rust use gpui_kit::prelude::*; use gpui_kit::{px, rgb}; use gpui_kit::base::Button; Button::new("save") .px_3() .py_2() .rounded(px(6.)) .bg(rgb(0x2563eb)) .text_color(rgb(0xffffff)) .on_click(|_, _, _| println!("save")) .child("Save") ``` Keep each `ElementId` stable across renders so GPUI can preserve element and focus state. Controlled components such as Checkbox, Switch, Radio, and Toggle report the next value through callbacks; store that value in your view and pass it back on the next render. ## Default color tokens `gpui-base` provides readable light and dark semantic palettes through `ColorTokens::light()` and `ColorTokens::dark()`. `ColorTokens::default()` uses the light palette. Both palettes use `Hsla` values and match the semantic roles of the default `gpui-component` themes. ```rust use gpui_kit::base::{ColorTokens, SemanticThemeTokens, Theme}; // Pick the palette that matches the application's current appearance. let colors = if is_dark { ColorTokens::dark() } else { ColorTokens::light() }; Theme::global_mut(cx).tokens = SemanticThemeTokens { colors, ..Default::default() }; ``` The palette contains semantic roles rather than component-specific colors: `background` and `foreground`, `surface` and `surface_foreground`, `primary`, `secondary`, `muted`, `accent`, `destructive`, `border`, `input`, `ring`, and `selection`, including the corresponding foreground roles. Base components derive what they can from these roles — a link takes `primary`, for instance — rather than adding a component-specific token for it. `selection` is its own role because no other one can stand in for it: it is painted under the glyphs and has to stay legible there, which neither `accent` nor `ring` guarantees. Calling `gpui_kit::component::init` projects its active light or dark theme into the same Base tokens automatically. Applications that use only `gpui-base` should install the matching palette when their appearance mode changes. ## Run the shared examples The examples used by this website also run as a native GPUI application: ```sh cargo run -p gpui-base --example components -- button ``` Replace `button` with a primitive slug from the [primitive catalog](./primitives/index.md). The website compiles the same showcase for `wasm32-unknown-unknown` and loads it on each primitive page. --- --- url: /shell/getting-started.md description: >- Add the runtime to a Rust application, write the script it loads, and check that script without opening a window. --- # Getting Started `gpui-shell` is first of all a way to give a Rust GPUI application JavaScript extension points: the host builds the runtime, decides what a script may reach, and mounts script Views where it wants them. Running a script directory on its own — the `gpui-shell` binary below — is the development convenience that comes with that, not the point of it. ## Add the runtime to a Rust application A host does four things: initialize the library, build a runtime, grant the capabilities it is willing to grant, and mount a script View under a `ShellRoot`. The `gpui-shell` binary is itself just a thin host that does exactly this. ```rust use gpui_kit::shell::{Capabilities, ShellRuntime}; gpui_platform::application() .with_assets(gpui_kit::shell::AppAssets::new(root.clone())) .run(move |cx| { // Initializes gpui-base, the shell's default token palette, and the // style reflection table. gpui_kit::shell::init(cx); let runtime = ShellRuntime::new(cx).expect("script runtime"); // Nothing is permitted until the host says so. gpui_kit::shell::set_store_path(store_directory.join("store.json")); gpui_kit::shell::set_capabilities( Capabilities::new() .read_roots([root.clone()]) .write_roots([store_directory.clone()]) .store(true), ); cx.open_window(Default::default(), move |window, cx| { runtime.load(&root, window, cx) }) .expect("window"); }); ``` Two of those lines carry rules rather than mechanics. **`runtime.load(...)` returns the window's `ShellRoot`**, the same role `Root` has in a `gpui-component` window. It owns the dialog stack, the sheet, the toast stack, focus restoration and Tab navigation. A manifest selects the application entry and records capability requests; it never approves those requests. Both manifest-backed and bare directories run under the host's current default policy, and a bare directory uses `main.js`. **Capabilities default to empty.** `Capabilities::default()` grants nothing at all — no file, no storage, no clipboard, no process. The host decides, because only the host knows how far it trusts the code it is about to run. See [Capabilities](./capabilities.md). Install a `tracing` subscriber too. The runtime reports script errors, unhandled promise rejections and illegal-phase calls through `tracing`; with no subscriber, every one of them is discarded and the symptom is a View that quietly stopped responding. ## The script it loads One file is enough. Create a directory with a `main.js` in it: ```js // hello/main.js import { View } from "gpui-kit"; import { v_flex, Button } from "gpui-base"; export default class Hello extends View { init() { this.clicks = 0; } render(cx) { return v_flex() .size_full() .items_center() .justify_center() .gap(12) .bg(cx.theme().colors.background) .child( div() .text_color(cx.theme().colors.foreground) .child(`Clicked ${this.clicks} times`), ) .child( Button.new("click") .h(28) .px(12) .items_center() .justify_center() .border(1) .border_color(cx.theme().colors.border) .bg(cx.theme().colors.surface) .text_color(cx.theme().colors.foreground) .on_click((_event, cx) => { this.clicks += 1; cx.notify(); }) .child("Click me"), ); } } ``` ```bash cargo run -p gpui-shell -- hello ``` Four things in that file are worth naming now, because everything else builds on them. **One module per crate that provides it.** `"gpui-kit"` holds GPUI's own elements and what the runtime adds — `View`, `div`, `text`, storage, scheduling. `"gpui-base"` holds gpui-base's layout helpers, components and theme — `v_flex`, `Button`, `InputState`. `"gpui-fps"` holds its performance overlay. A name belongs to exactly one of them, so an import line says which layer a script depends on. The runtime also supplies a deliberately small JavaScript-standard layer: `buffer`, `path`, `url`, `crypto`, `zlib`, `console`, `process`, `os`, `fs/promises`, `net`, `websocket`, and global `fetch`. Application-relative imports remain confined to the application directory. Node-prefixed aliases such as `node:fs`, package lookup, and CommonJS `require` are not part of the contract. **`main.js` must `export default` a class extending `View`.** `init` runs once when the View is created; `render` returns one element, retained `Entity` or string, and runs when the View is invalidated rather than on every frame — see [When `render` runs](./state.md#when-render-runs). **Style methods are `snake_case`, your own code is `camelCase`.** `items_center`, `on_click`, `text_color`, `gap_2` keep their Rust spelling, because the no-argument style surface is generated from GPUI's reflection table rather than written by hand. Anything the application declares itself — variables, methods, object keys — is ordinary JavaScript. The contrast is deliberate: a `snake_case` call is host surface, a `camelCase` one is your code. **Nothing repaints on its own.** There are no signals, no `useState`, no dependency arrays. Change state, then call `cx.notify()`. ## Running a script on its own A script directory can also be run directly, without writing a host. This is how the bundled example runs, and how a script is usually developed before it is loaded by the application that will own it. `gpui-shell` is not published to crates.io, so clone the repository and run it from the root: ```bash cargo run -p gpui-shell -- examples/js_todolist ``` That opens a window with a working todo list: a text field with retained state, controlled checkboxes, a confirmation dialog, a toast, icons loaded from the application's own directory, and storage that falls back to memory when it has not been granted. It exists to exercise the runtime rather than to be minimal — if something is broken, it shows there first. The argument is a **directory**, not a file. The runtime resolves that directory, reads `main.js` by default, takes the class that module default-exports, constructs one instance, and mounts it as the window's root View. If the directory contains `gpui-shell.json`, the binary validates that manifest first and uses its declared `entry` and capabilities. ## Check a script without running it JavaScript has no compiler, and this runtime does not add one. What it adds is the thing a compiler would have done for you: ```bash cargo run -p gpui-shell -- check hello ``` `check` loads the application and renders one frame into a window that is never shown, then exits `0` on success and `1` on failure. Because the script surface is dynamic — an unknown style method, a wrongly typed argument and a reused element are all runtime facts — building and rendering once is the only honest way to check it. What it catches: * syntax errors, with the script's own stack; * unresolved imports, and imports that escape the application directory; * a missing or malformed default export; * unknown style methods, with a `did you mean` suggestion; * wrongly typed style arguments, such as `.p("auto")`; * an element used twice. It opens no window, so it is usable from an editor, from CI, or from an agent loop. Add `--print-spec` to print the element description that was built: ```bash cargo run -p gpui-shell -- check hello --print-spec ``` That output is the arena's own debug dump — the tree of components and recorded operations, before anything is materialized. It is useful when the question is "what did my chain actually record?". ## Generate TypeScript declarations ```bash cargo run -p gpui-shell -- types hello ``` This writes `gpui-kit.d.ts` next to the application. Put `// @ts-check` at the top of a script and an editor will complete the whole API and reject a mistyped style method, a colour token that does not exist, or `.p("auto")` — at the call site, before it runs. It also sets up everything else the editor needs: each Git dependency the manifest declares is fetched and linked into `node_modules` under its declared name, so `import { style } from "omarchy-ui"` resolves to the same files the runtime will execute and carries the package's own types, parameters and JSDoc; and a `jsconfig.json` is scaffolded when the directory has neither that nor a `tsconfig.json`. See [Dependencies](./dependencies.md). The declarations can be trusted because they are **generated from the tables the runtime dispatches through**, not transcribed from this documentation: * style method names come from the same list the JavaScript prelude loops over to build the element prototype; * each parametric method's argument type is *probed* — the generator asks the runtime which literals that method accepts, so the difference between a length, a definite length, an absolute length, a colour and a bare number is decided by the code that enforces it; * the colour union comes from the installed palette's token names. Three things the declarations deliberately do not express, because no type could: whether a capability is **granted** (a denied `fs.readFile` still type-checks), the **lifetime** of an element or a `cx` (TypeScript has no affine types, so reusing an element still type-checks and still throws), and **which component a method suits** (every element shares one prototype, so `.checked(true)` is declared on all of them and is simply inert on a `div`). Regenerate the file after upgrading the runtime; the output is deterministic, so the diff is reviewable. ## Hot-reload ```bash cargo run -p gpui-shell -- hello --watch cargo run -p gpui-shell -- hello --dev # implies --watch ``` `--watch` polls the application directory four times a second, debounces a burst of writes for 200 ms, and reloads. A reload re-reads **every** module, entry point included — a hot-reload that quietly served a stale import would be worse than none, because it looks like it worked. A reload does all of its fallible work before it touches the live View. If the new code fails to load, the previous View keeps running, the error goes to stderr, and a toast with a stable id reports it in the window; the next successful reload retracts that toast. A broken save never costs you the window. `--dev` implies `--watch` and enables development mode before the runtime is constructed. It restores dynamic-code constructors and leaves built-in prototypes writable, while capability checks remain unchanged. See [Capabilities](./capabilities.md#the-sandbox). ## Command reference ```text gpui-shell [--watch] [--dev] gpui-shell check [--print-spec] gpui-shell types gpui-shell --help | --version ``` | Argument | Meaning | | -------------- | ------------------------------------------------------------------------ | | `` | The application root, or the `main.js` inside it | | `check` | Load and render once without a window; exit `0` or `1` | | `types` | Write `gpui-kit.d.ts`, link the manifest's dependencies, scaffold config | | `--watch` | Reload when the sources change | | `--dev` | Development mode; implies `--watch` | | `--print-spec` | With `check`, also print the element description that was built | --- --- url: /docs/getting-started.md description: Learn how to set up and use GPUI Component in your project --- # Getting Started ## Installation Add dependencies to your `Cargo.toml`: ```toml [dependencies] gpui-kit = "0.6" anyhow = "1.0" ``` :::tip `gpui-kit` always pulls in GPUI and `gpui-base`, and by default `gpui-component` and the default icon set. To manage your own assets, keep only the features you need: ```toml gpui-kit = { version = "0.6", default-features = false, features = ["component"] } ``` See [Icons & Assets](./assets.md) for more details. ::: ## Quick Start Here's a simple example to get you started: ```rust use gpui_kit::component::button::*; use gpui_kit::component::*; use gpui_kit::*; pub struct HelloWorld; impl Render for HelloWorld { fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { div() .v_flex() .gap_2() .size_full() .items_center() .justify_center() .child("Hello, World!") .child( Button::new("ok") .primary() .label("Let's Go!") .on_click(|_, _, _| println!("Clicked!")), ) } } fn main() { let app = gpui_kit::application().with_assets(gpui_kit::assets::Assets); app.run(move |cx| { // This must be called before using any GPUI Component features. gpui_kit::init(cx); cx.spawn(async move |cx| { cx.open_window(WindowOptions::default(), |window, cx| { let view = cx.new(|_| HelloWorld); // This first level on the window, should be a Root. cx.new(|cx| Root::new(view, window, cx)) }) .expect("Failed to open window"); }) .detach(); }); } ``` :::info Make sure to call `gpui_kit::init(cx);` at first line inside the `app.run` closure. This initializes the GPUI Component system. This is required for theming and other global settings to work correctly. ::: ## Basic Concepts ### Stateless Elements GPUI Component uses stateless [RenderOnce] elements, making them simple and predictable. State management is handled at the view level, not in individual components. They are all implemented [IntoElement] types. For example: ```rs struct MyView; impl Render for MyView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { div() .child(Button::new("btn").label("Click Me")) .child(Tag::secondary().child("Secondary")) } } ``` ### Stateful Components There are some stateful components like `Dropdown`, `List`, and `Table` that manage their own internal state for convenience, these components implement the [Render] trait. Those components to use are a bit different, we need create the \[Entity] and hold it in the view struct. ```rs struct MyView { input: Entity, } impl MyView { fn new(window: &Window, cx: &mut Context) -> Self { let input = cx.new(|cx| InputState::new(window, cx).default_value("Hello 世界")); Self { input } } } impl Render for MyView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { self.input.clone() } } ``` ### Theming All components support theming through the built-in `Theme` system: ```rust use gpui_kit::component::{ActiveTheme, Theme}; // Access theme colors in your components cx.theme().primary cx.theme().background cx.theme().foreground ``` ### Sizing Most components support multiple sizes: ```rust Button::new("btn").small() Button::new("btn").medium() // default Button::new("btn").large() Button::new("btn").xsmall() ``` ### Variants Components offer different visual variants: ```rust Button::new("btn").primary() Button::new("btn").danger() Button::new("btn").warning() Button::new("btn").success() Button::new("btn").ghost() Button::new("btn").outline() ``` ## Icons :::info Icons are not bundled with GPUI Component to keep the library lightweight. Continue read [Icons & Assets](./assets.md) to learn how to add icons to your project. ::: GPUI Component has an `Icon` element, but does not include SVG files by default. The examples use [Lucide](https://lucide.dev) icons. You can use any icons you like by naming the SVG files as defined in `IconName`. Add the icons you need to your project. ```rust use gpui_kit::component::{Icon, IconName}; Icon::new(IconName::Check) Icon::new(IconName::Search).small() ``` ## Next Steps Explore the component documentation to learn more about each component: * [Button](./components/button) - Interactive button component * [Input](./components/input) - Text input with validation * [Dialog](./components/dialog) - Dialog and modal windows * [DataTable](./components/data-table) - High-performance data tables * [More components...](./components/index) ## Development To run the component gallery: ```bash cargo run ``` More examples can be found in the `examples` directory: ```bash cargo run --example ``` [RenderOnce]: https://docs.rs/gpui/latest/gpui/trait.RenderOnce.html [IntoElement]: https://docs.rs/gpui/latest/gpui/trait.IntoElement.html [Render]: https://docs.rs/gpui/latest/gpui/trait.Render.html --- --- url: /zh-CN/base.md description: GPUI Kit Rust 桌面框架中无样式的行为与基础设施层。 --- # GPUI Base `gpui-base` 是 GPUI Kit 的无样式基础层。它提供交互行为、受控状态、焦点管理、无障碍语义、动画、虚拟列表和主题 token,同时将布局与视觉设计完整留给应用。 ## 如何选择 | 使用 | 适用场景 | | --- | --- | | `gpui-base` | 需要创建自己的设计系统,并掌控每个视觉选择 | | `gpui-component` | 需要一套具有完整视觉设计、可直接使用的组件 | 依赖始终由上层指向基础层:`gpui-component` 构建于 `gpui-base` 之上,应用也可以直接使用任意一层。 ## 基本原则 * **行为内置**:控件提供一致的指针、键盘、焦点和状态行为。 * **表现由应用决定**:直接组合 GPUI 样式方法和 children,不需要覆盖默认视觉。 * **部件可组合**:primitive 暴露有意义的子部件,而不是把结构隐藏在单体组件中。 * **状态明确**:受控输入报告变化,最终状态由 view 持有。 ## 开始使用 从[入门指南](./getting-started.md)开始,使用 [TextView](./text-view.md) 渲染可选择的 Markdown 与 HTML,或阅读[文本选择](./text-selection.md)为自定义 renderer 接入窗口级选择。每个页面都提供 Rust 代码和可运行的 WASM 示例。 [History 与 Undo History](./history.md) 介绍两种用途明确不同的状态结构:`History` 是包含根条目、当前条目、后退与前进分支的导航轨迹,`UndoHistory` 则记录分组的 undo 与 redo 事务。 --- --- url: /base.md description: >- The unstyled behavior and infrastructure foundation of GPUI Kit, the Rust desktop framework. --- # GPUI Base `gpui-base` is the unstyled foundation of GPUI Kit, the Rust desktop application framework. It provides interaction behavior, controlled state, focus management, accessibility semantics, animation, virtual lists, and theme tokens while leaving layout and visual design to your application. ## Choose the right layer | Use | When | | --- | --- | | `gpui-base` | You are building a design system and want to own every visual choice. | | `gpui-component` | You want a complete set of styled, ready-to-use desktop components. | The dependency points one way: `gpui-component` builds on `gpui-base`. Applications can use either layer directly. ## Principles * **Behavior is built in.** Controls provide consistent pointer, keyboard, focus, and state behavior. * **Presentation is yours.** Compose GPUI style methods and children without fighting default visuals. * **Parts stay composable.** Primitives expose their meaningful subparts instead of hiding markup behind a monolith. * **State stays explicit.** Controlled inputs report changes and your view owns the resulting state. ## Start building Follow [Getting started](./getting-started.md), render selectable Markdown and HTML with [TextView](./text-view.md), learn how to add [window-level text selection](./text-selection.md) to custom renderers, then explore the [primitive catalog](./primitives/index.md). Each page includes Rust snippets and a live WASM example backed by the same example crate that can run natively. Three systems are larger than a primitive and have pages of their own. [Motion](./motion.md) provides typed transitions, springs, keyframes, presence, and sequencing. [Virtual List](./virtual-list.md) renders lists of any length by drawing only what is on screen, with per-item sizes rather than a uniform row height. [Dock](./dock.md) is a full workspace shell — nested splits, tab groups, a tiles canvas and edge docks — whose layout is pure data you can build and serialize without a window, and whose every pixel comes from renderer traits you implement. [History and Undo History](./history.md) are smaller but deliberately distinct: `History` is a root/current/back/forward navigation trail, while `UndoHistory` records grouped undo and redo transactions. --- --- url: /docs.md description: >- A comprehensive Rust framework for building fantastic, high-performance desktop applications with GPUI. --- # GPUI Kit GPUI Kit (aka: GPUI Component) is a comprehensive Rust desktop application framework built on GPUI. It combines a complete UI system with application-grade data, layout, content, and editing capabilities, and it ships as three crates that build on each other, all reachable through the single `gpui-kit` dependency: * **`gpui-base`**: Unstyled behavior, controlled state, focus, overlays, virtual lists, dock infrastructure, and semantic design tokens. * **`gpui-component`**: GPUI Component, the complete styled component library with 60+ controls, themes, data tables, dock layout, and a code editor. * **`gpui-shell`**: Opens a Rust host to JavaScript extensions, one granted capability at a time. Use `gpui-component` for polished controls with one coherent visual language, or build your own design system on the reusable behavior and infrastructure in `gpui-base`. This section documents GPUI Component; see [GPUI Base](/base/) and [GPUI Shell](/shell/) for the other two layers. ## Features * **60+ UI Components**: Forms, navigation, overlays, feedback, layout, and more. * **Production Ready**: Used to build Longbridge Pro from day one and refined in a publicly shipped commercial desktop application. * **Native Feel**: Modern controls inspired by macOS and Windows. * **120 FPS**: GPU-accelerated interfaces that remain smooth under load. * **Data Tables**: Virtual scrolling, fixed and resizable columns, sorting, and cell selection across hundreds of thousands of rows. * **Virtual Lists**: Render only the visible range, including differently sized items. * **Code Editor**: 200K lines, Tree-sitter highlighting, diagnostics, completion, and hover. * **Dock Layout**: Resizable panels, draggable tabs, nested splits, edge docks, and freeform Tiles. * **Rich Content**: Native Markdown and HTML, syntax highlighting, and charts. * **Design Freedom**: Use the complete visual system or build your own on `gpui-base`. * **Typed Motion**: CSS-aligned easing, timing, keyframes, springs, presence, and measured reveal with allocation-free steady sampling. * **Cross Platform**: Ship one Rust codebase to macOS, Windows, and Linux. ## Quick Example Add `gpui-kit` to your `Cargo.toml`: ```toml [dependencies] gpui-kit = "0.6" ``` Then create a simple "Hello, World!" application with a button: ```rust use gpui_kit::*; use gpui_kit::component::button::*; use gpui_kit::component::*; pub struct HelloWorld; impl Render for HelloWorld { fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { div() .v_flex() .gap_2() .size_full() .items_center() .justify_center() .child("Hello, World!") .child( Button::new("ok") .primary() .label("Let's Go!") .on_click(|_, _, _| println!("Clicked!")), ) } } fn main() { gpui_kit::application().run(move |cx| { // This must be called before using any GPUI Component features. gpui_kit::init(cx); cx.spawn(async move |cx| { cx.open_window(WindowOptions::default(), |window, cx| { let view = cx.new(|_| HelloWorld); // This first level on the window, should be a Root. cx.new(|cx| Root::new(view, window, cx)) }) .expect("Failed to open window"); }) .detach(); }); } ``` ## Community & Support Learn how to build interruptible 120 FPS animation in the [GPUI Base Motion guide](/base/motion). * [GitHub Repository](https://github.com/longbridge/gpui-kit) * [Issue Tracker](https://github.com/longbridge/gpui-kit/issues) * [Contributing Guide](https://github.com/longbridge/gpui-kit/blob/main/CONTRIBUTING.md) ## License Apache-2.0 --- --- url: /zh-CN.md description: 用 Rust 构建卓越、高性能的桌面应用程序 --- --- --- url: /README.md --- # GPUI Kit website To install dependencies: ```bash bun install ``` To run: ```bash bun run dev ``` This project was created using `bun init` in bun v1.2.23. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime. --- --- url: /shell.md description: >- Makes a Rust GPUI application extensible in JavaScript, rendered by GPUI itself — no WebView, no DOM. Plugins first, standalone script applications second. --- # GPUI Shell `gpui-shell` exists to make a Rust GPUI application **extensible in JavaScript**. **The primary goal is plugin extension.** A host application compiles and ships once. After that, a new panel, a side tool or a piece of business logic arrives as a script loaded into the same process — no rebuild, no binary to redistribute, and no fork for a contributor who only wants to add a panel. **The secondary goal is writing a whole application in JavaScript.** The CLI runs an application directory on its own, which is a usable path in itself and also how a plugin is developed: get the script running standalone, then mount it in a host. **It is not an Electron or a Tauri.** There is no WebView, no DOM, no HTML or CSS, no browser engine, and no Node.js. A script never renders. It describes an interface once, and Rust replays that description into real GPUI elements on every frame after it — the same element model a Rust application on `gpui-base` builds, through the same GPU renderer. JavaScript is the application layer here, not the rendering layer, which is why a repaint costs no JavaScript at all and taking the whole runtime costs [+13.5 MiB of binary](./engine.md#what-linking-it-costs). Both goals rest on the same split. `gpui-shell` is built directly on [`gpui-base`](/base/), with [QuickJS](https://github.com/quickjs-ng/quickjs) running on the host's own thread. The host builds the runtime and grants what a script may reach; the script draws real interface inside the same process. Rust keeps rendering, layout, text editing, virtualization, focus, overlays and every system capability; the script owns composition, presentation and business logic. ```js import { View } from "gpui-kit"; import { v_flex, Button } from "gpui-base"; export default class Counter extends View { init() { this.count = 0; } render(cx) { return v_flex() .size_full() .items_center() .justify_center() .gap(20) .bg(cx.theme().colors.background) .child( div() .text_3xl() .text_color(cx.theme().colors.foreground) .child(`${this.count}`), ) .child( Button.new("increment") .h(32) .px(14) .items_center() .justify_center() .bg(cx.theme().colors.primary) .text_color(cx.theme().colors.primary_foreground) .rounded(6) .on_click((_event, cx) => { this.count += 1; cx.notify(); }) .child("Increment"), ); } } ``` ## Why plugins come first `crates/base/src/dock` already holds half of what a plugin system needs: a layout that is pure data, a `PanelRegistry` that rebuilds a panel from a name in a persisted file, and a per-panel `serde_json::Value` that rides along with it. The missing half is that a panel's implementation has to be compiled into the host binary — nobody can contribute one without forking it. `gpui-shell` supplies that half. Plugins-first is not a positioning statement. It is the reason behind decisions that would each have gone another way for a runtime aimed only at standalone scripts: | Decision | Why it follows from plugins | | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | | `Capabilities::default()` is the empty set, and the host grants | A plugin is code someone else wrote; the grant has to be the host's, not a self-declaration in the plugin's own manifest | | A separate `Policy` per plugin, and unload cancels every task carrying it | Several plugins share one runtime, so grants must not bleed between them | | A script fault is a recoverable exception, and the host process survives | One broken plugin should not take the application with it | | A repaint replays a Snapshot and never enters the VM | The host answers for the frame budget, so a plugin's JavaScript cannot sit on it | | `HostModule` lends the host's own Rust to a script | Only meaningful when the script runs inside a host — a standalone application has no host to borrow from | | Dock panels keep their place and state across an uninstall | Plugins get installed and removed; a panel comes back where it was, with what it had | | The foundation ships no presentation, so the script owns all of it | A plugin has to look like part of its host, which takes control of every pixel | A standalone script application uses few of these. What it gains is the iteration speed — hot reload, `check`, and a generated `gpui-kit.d.ts` — which is why it sits second: it is where a plugin is developed and proven, rather than the point of the runtime. Text editing, syntax highlighting, LSP, virtualization and motion sampling stay in Rust. That line is a division of responsibility rather than a limit on the script: the host owns everything that has to sit close to the GPU and the system, so a plugin never becomes a variable in the application's performance or stability. ::: warning Plugins are the goal, not yet the whole interface The machinery below a plugin is built and tested — manifest parsing and discovery, load and unload, a per-plugin policy and data directory. A script now contributes panels and draws a dock's chrome: `DockArea`, `dock_area(...)` and `DockArea.register_panel` are public, and a layout with a script's panels in it survives a restart. What is still missing is the rest of the contribution registry (`gpui.command`, `gpui.keymap`), the authorization UI, and a CLI that uses `PluginManager`. **What runs end to end today is the standalone path, docks included.** See [Dock and Panels](./dock.md). ::: ## What defines it ### Architecture: the script describes, the host renders A script never holds a GPUI element. It records a **description** of one — every call in a builder chain writes an operation into an arena, and Rust replays those operations into real elements when a frame needs them. Layout, painting, hit testing, scrolling, IME and text editing stay in Rust and never call back into the script. [How a script becomes an interface](#how-a-script-becomes-an-interface) traces one pass of that. The engine is a parameter of the design rather than a part of it. QuickJS is the only one today, but everything above the seam — the arena, the materializer, the call scope, the style table, the theme, the capability model, the overlay host, hot-reload — names no VM anywhere in its source. See [The engine seam](./engine.md). ### Capability: a whole application layer, not a widget set A script gets what a Rust application built on `gpui-base` gets: elements and layout, links and controls, a fluent style surface over semantic theme tokens, View state through `init` / `render` / `cx.notify()`, retained host state such as a text input's rope and selection, dialogs, a sheet and toasts, asynchronous tasks, native transitions and springs, and gated filesystem, storage, clipboard, process, HTTP, TCP and WebSocket surfaces. Around that: `--watch` hot-reloads on save, `gpui-shell.json` declares identity and least-privilege capabilities before code runs, a generated `gpui-kit.d.ts` describes the whole API to an editor or a model, and `check` reports mistakes before the application runs. ::: tip `gpui-kit.d.ts` can go in `.gitignore` — it is generated. ::: ### Performance: the script is not in the frame `render` does **not** run once per frame. It describes the interface once into a Snapshot, and until the next `cx.notify()` every repaint replays that Snapshot in Rust. A pointer crossing a button, a blinking cursor, a scrolling list and a native transition or spring advancing do not run JavaScript. The runtime counts the two events separately, and the gallery's Shell story (`cargo run -- shell`) puts both counters on screen: | What the interface is doing | Frames a second | JavaScript runs a second | | ------------------------------------------------- | --------------- | ------------------------ | | Repainting, with nothing JavaScript reads changed | 60 | 0 | | Prices moving every 50 ms | 60 | 19 | The frame count belongs to the display, the JavaScript count to the data. In the second row the other 41 frames replay a description that already exists. Cost is therefore paid per user action rather than per frame. On a 443-node panel, running `render` and recording the whole interface into a Snapshot takes 1.1 ms, paid only when state changes; each frame after it takes 1.3 ms, which is rendering itself — turning the Snapshot into elements, laying out, painting, with no JavaScript in it. | | Cost per frame | | ------------------ | ------------------------------------------------------------------- | | Without a Snapshot | 1.1 ms (JS render) + 1.3 ms (Rust render) = **2.4 ms/frame render** | | With a Snapshot | **1.3 ms** | Growing the panel does not change that. The [benchmark](./engine.md#the-measurement) covers sizes up to 8,403 nodes, no frame at any of them runs JavaScript, and the smallest size is asserted on every CI build. ### Size: a script runtime for +13.5 MiB A host that runs a real script application ships a **26.1 MiB** binary and holds **81 MiB** resident, QuickJS and the whole Standard Runtime included. Taking the dependency costs **+13.5 MiB of binary and +14 MiB of memory** over the same application without it. That figure is a constant, not a proportion: the component gallery — five times the size — adds the same 13.5 MiB. [What linking it costs](./engine.md#what-linking-it-costs) gives the pair it was measured on, and where the megabytes go. All figures here were taken on a MacBook Pro (M3, 8 cores, 24 GB): the frame and run counts from the Shell story, the milliseconds from a release build of the benchmark, the binary and memory figures from release builds of `examples/hello_world` and the `gpui-shell` CLI. ### Security: nothing by default, and a language trimmed to match `Capabilities::default()` is the empty set — no file access, no storage, no clipboard, no process execution, no network. The host decides the grant before loading a View, which then keeps that grant for its lifetime; every path in the `fs` surface goes through **one** resolver that refuses anything landing outside a granted root. Below the grants, the sandbox trims the language itself, because one VM will eventually host several plugins: `eval` and all four function compilers are gone, the built-in prototypes are frozen so one plugin cannot change `Object.prototype` for another, module resolution is confined to the application directory, and the heap (256 MiB), interpreter stack (1 MiB) and time in a single call (50 ms in `render`) are capped. That time limit is an interrupt a `catch` block cannot swallow, which is measured by a test. See [Capabilities](./capabilities.md). ## How a script becomes an interface The diagram traces one frame, and the shape of it explains most of this documentation. GPUI elements are values that are **consumed** when used: `RenderOnce::render` takes `self` by value, `.child()` takes its child by value, and a View rebuilds its whole element tree on every redraw. A JavaScript object can therefore never *be* a GPUI element — there is nothing for it to hold onto. So the script does not build elements. It **describes** them. Every call in a builder chain records one operation into an arena of element descriptions; the object the script holds carries nothing but an integer index into that arena. When GPUI asks the View to render, Rust replays the recorded operations into real elements, hands them to GPUI, and clears the arena. Layout, painting, hit testing, scrolling and IME never return to the script. Three consequences follow directly, and each has a page below: * **Elements are single-use.** The description is gone at the end of the pass, so a stored element throws on its next use rather than drawing something unexpected. See [Elements](./elements.md). * **The `cx` handed to a call belongs to that call.** It carries a generation number, checked against the live call stack, so a `cx` kept across an `await` reports a clear error instead of touching a dead stack frame. See [State and Views](./state.md). * **Callbacks belong to the render that registered them.** They are replaced wholesale by the next render, which is what keeps script closures from accumulating in the host. See [Elements](./elements.md). All three fall out of binding a script to an element model that consumes its values. ## Presentation belongs to the script Most scripting layers hand a script a set of finished widgets and let it arrange them. This one has none to hand over, because the layer underneath it has none either. `gpui-base` controls carry no visual style at all. `Button::new("save")` in Rust has no padding, no background, no radius and no size, and that is the contract. The JavaScript bindings preserve it exactly: `Button.new("save")` with no styling draws nothing but its children. The consequence is the point. **Because the foundation ships no presentation, the script owns all of it** — every colour, every pixel of spacing, every hover state, every corner radius. That is the same trade a Rust application makes when it builds on `gpui-base` instead of `gpui-component`; the difference is that here the trade is made in a file you can save and see the result of immediately, with no `cargo build` in between. What the script gains in exchange for the extra typing is the whole application layer. Changing a button's radius does not mean going back to Rust. ## Where it fits * **Adding plugin support to an existing GPUI application — the primary case.** Plugins run inside the host process under capabilities the host grants one at a time, starting from none. Extending the product stops meaning a fork or a new release: interface and business logic ship as script and change without recompiling or redistributing a binary, and a failing plugin surfaces as a recoverable error rather than taking the host down. * **Writing a complete application in JavaScript on `gpui-shell` — the secondary case.** The whole application layer — elements, styling, View state, overlays and system APIs — while rendering, text editing, virtualization and every animation frame stay in Rust. It is also where a plugin is written and proven before it is mounted in a host. ## Where it sits ```text JavaScript application main.js · Views · styles · business logic │ import { … } from "gpui-kit" ▼ gpui-shell engine seam · element descriptions · call scope style table · theme tokens · capabilities ShellRoot (dialogs, sheet, toasts) · scheduler │ ▼ gpui-base behavior · state · infrastructure (no style) │ ▼ gpui elements · styling · rendering · GPU · platform ``` `gpui-shell` sits beside `gpui-component` rather than beneath it: both are consumers of `gpui-base`, and both supply a presentation layer that Base does not. `gpui-component` supplies one in Rust, finished and coherent. `gpui-shell` supplies the machinery for a script to supply its own. ## Read next | Page | What it covers | | --------------------------------------- | ------------------------------------------------------------------------------------------------------ | | [Getting started](./getting-started.md) | Running the example, the smallest application, `check` and `types` | | [Examples](./examples.md) | The two applications in the repository, and what to copy from them | | [Elements](./elements.md) | Constructors, `child` / `children` / `when`, and why an element is single-use | | [Styling](./styling.md) | The fluent style surface, lengths, colour tokens and state styles | | [State and Views](./state.md) | `init` / `render`, `cx.notify()`, retained state, async | | [Overlays](./overlays.md) | Dialogs, the sheet, toasts, and the phase rule | | [Capabilities](./capabilities.md) | `gpui-shell.json`, default deny, filesystem, storage, process and network APIs | | [Dependencies](./dependencies.md) | Shell packages: what makes one, how a manifest names and pins it, and the types an editor gets | | [Hosting](./hosting.md) | The Rust side in full: mounting, refreshing, metrics, exit, hot-reload | | [HostModule](./host-module.md) | Lending the host's own Rust to a script, and the plain-data boundary | | [Dock and Panels](./dock.md) | A script View as a dockable panel, the chrome you draw for it, and what survives a restart | | [Performance](./performance.md) | What a script costs: invalidation against description size, the View as the boundary, and the counters | | [The engine seam](./engine.md) | QuickJS, why the seam exists, and the measurements that tell script cost from frame cost | ## Status The crate is at milestone **M0**: a feasibility baseline, not a stable interface. It is not published to crates.io, and the script API is expected to change. What is documented here exists and works; what is missing is called out on the page where you would go looking for it. The design is specified in the [GPUI Shell design document](https://github.com/longbridge/gpui-kit/blob/main/docs/gpui-shell.md), and the crate lives at [`crates/shell`](https://github.com/longbridge/gpui-kit/tree/main/crates/shell). --- --- url: /zh-CN/shell.md description: >- 让 Rust 的 GPUI 应用可以用 JavaScript 扩展,界面仍由 GPUI 自己渲染——没有 WebView,也没有 DOM。首要目标是插件,其次才是纯脚本应用。 --- # GPUI Shell `gpui-shell` 的存在,是为了让一个用 Rust 写的 GPUI 应用**能被 JavaScript 扩展**。 **首要目标是插件扩展。** Host 应用编译一次、发布一次,此后新增一块面板、一个侧边工具或一段业务逻辑,都以脚本的形式加载进同一个进程——不必重新编译,不必重新分发二进制,想加一块面板的人也不必 fork 整个 Host。 **次要目标是用 JavaScript 写完整的应用。** CLI 可以直接跑起一个应用目录。这本身就是一条能用的路径,同时也是插件的开发方式:先把脚本单独跑通,再挂进 Host。 **它不是 Electron,也不是 Tauri。** 没有 WebView,没有 DOM,没有 HTML 与 CSS,没有浏览器引擎,也没有 Node.js。脚本从不负责渲染,它只把界面**描述**一次,此后每一帧都由 Rust 把这份描述重放成真正的 GPUI 元素——和一个基于 `gpui-base` 的 Rust 应用所构建的,是同一套元素模型、同一个 GPU 渲染器。在这里 JavaScript 是应用层,不是渲染层:所以一次重绘完全不执行 JavaScript,而带上整个运行时也只多 [13.5 MiB 二进制](./engine.md#链接它要付多少)。 这两个目标建立在同一条分工上。`gpui-shell` 直接构建在 [`gpui-base`](/base/) 之上,[QuickJS](https://github.com/quickjs-ng/quickjs) 跑在 Host 自己的线程上。由 Host 构建运行时、决定脚本能碰到什么,而脚本在同一个进程里画出真正的界面。Rust 负责渲染、布局、文本编辑、虚拟化、焦点、浮层以及全部系统能力;脚本负责界面组合、视觉呈现与业务逻辑。 ```js import { View } from "gpui-kit"; import { v_flex, Button } from "gpui-base"; export default class Counter extends View { init() { this.count = 0; } render(cx) { return v_flex() .size_full() .items_center() .justify_center() .gap(20) .bg(cx.theme().colors.background) .child( div() .text_3xl() .text_color(cx.theme().colors.foreground) .child(`${this.count}`), ) .child( Button.new("increment") .h(32) .px(14) .items_center() .justify_center() .bg(cx.theme().colors.primary) .text_color(cx.theme().colors.primary_foreground) .rounded(6) .on_click((_event, cx) => { this.count += 1; cx.notify(); }) .child("Increment"), ); } } ``` ## 为什么插件优先 `crates/base/src/dock` 已经具备了插件系统所需的一半:布局是纯数据,`PanelRegistry` 能按持久化文件里的名字重建面板,每块面板还带着一份属于自己的 `serde_json::Value`。缺的另一半是——面板的实现必须编进 Host 的二进制,因此没有人能在不 fork 的前提下贡献一块面板。`gpui-shell` 补的正是这一半。 「插件优先」不是一句定位口号。下面这些设计决策,如果只面向独立脚本,每一条都可以是另一种选择;放在插件的语境下才成为必然: | 设计决策 | 为什么它由插件推导而来 | | ----------------------------------------------------- | ------------------------------------------------------------------------------- | | `Capabilities::default()` 是空集,由 Host 授予 | 插件是别人写的代码,授权必须来自 Host,而不能由插件在自己的 manifest 里声明即得 | | 每个插件一份独立 `Policy`,卸载即取消它名下的全部任务 | 多个插件共用同一个运行时,授权之间不能相互渗透 | | 脚本出错是可恢复的异常,Host 进程存活 | 一个插件写崩了,不该把整个应用一起带走 | | 重绘只重放快照,从不进入 VM | 帧预算由 Host 负责,插件的 JavaScript 不能压在上面 | | `HostModule` 把 Host 自己的 Rust 借给脚本 | 只有脚本跑在 Host 内部时才有意义——独立应用没有 Host 可借 | | Dock 面板在应用被卸载后仍保留位置与状态 | 插件会被装了又卸;重新装回来时,面板还在原来的位置,状态也还在 | | 基座不提供任何视觉,呈现权整个交给脚本 | 插件要长得像 Host 的一部分,就必须能掌控每一个像素 | 独立脚本应用用得上其中的很少几条。它真正获得的是迭代速度——hot-reload、`check`,以及自动生成的 `gpui-kit.d.ts`。这也是它排在第二位的原因:它是插件被开发和验证的地方,而不是这套运行时的目的本身。 文本编辑、语法高亮、LSP、虚拟化与动画采样都留在 Rust。这条线是职责划分,而不是对脚本的限制:所有必须贴着 GPU 与系统运行的部分都归 Host,插件因此不会成为应用性能与稳定性上的变量。 ::: warning 插件是目标,但接口还没有全部开放 插件之下的机制已经建成并有测试覆盖——manifest 解析与发现、加载与卸载、每个插件独立的 policy 与数据目录。脚本现在已经可以贡献面板并绘制 dock 的 chrome:`DockArea`、`dock_area(...)` 与 `DockArea.register_panel` 都已公开,带着脚本面板的布局也能熬过一次重启。还缺的是贡献注册表的其余部分(`gpui.command`、`gpui.keymap`)、授权 UI,以及一个用上 `PluginManager` 的 CLI。**今天能完整跑通的是独立脚本这条路径,dock 也在其中。** 见 [Dock 与面板](./dock.md)。 ::: ## 核心特点 ### 架构:脚本负责描述,Host 负责渲染 脚本从不持有 GPUI 元素,它记录的是元素的**描述**——builder 链上的每一次调用都会往一块 arena 里写入一条操作,等某一帧需要时,Rust 再把这些操作重放成真实元素。布局、绘制、命中测试、滚动、IME 与文本编辑全部留在 Rust,不会回调进脚本。[一次渲染是怎么走完的](#一次渲染是怎么走完的)完整走了一遍这个过程。 引擎是这套设计的一个参数,而不是其中一部分。今天只有 QuickJS 一种,但这条分界线之上的全部模块——arena、把描述变成真实元素的 `materialize`、CallScope、样式表、主题、能力模型、浮层 Host 、hot-reload——源码里都没有出现任何 VM 的名字。见 [The Engine Seam](./engine.md)。 ### 能力:一整层应用层,而不是一套控件 脚本拿到的,正是一个基于 `gpui-base` 的 Rust 应用能拿到的东西:元素与布局、链接与控件、建立在语义主题 token 之上的流式样式接口、通过 `init` / `render` / `cx.notify()` 管理的 View 状态、由 Host 留存的状态(例如文本输入的 rope 与选区)、dialog / sheet / toast、异步任务、原生 transition 与 spring,以及需要授权才能用的文件、存储、剪贴板、进程、HTTP、TCP 与 WebSocket 接口。 围绕它的还有:`--watch` 保存文件即 hot-reload,`gpui-shell.json` 在代码运行前声明身份与最小权限,自动生成的 `gpui-kit.d.ts` 把整套 API 描述给编辑器或模型,`check` 则在应用跑起来之前就报出问题。 ::: tip `gpui-kit.d.ts` 可以加进 `.gitignore`,它是自动生成的。 ::: ### 性能:脚本不在每一帧里 `render` **不是**每帧跑一次。它把界面描述一次、存进一份 Snapshot;在下一次 `cx.notify()` 之前,每一次重绘都由 Rust 重放这份 Snapshot。指针划过按钮、光标闪烁、列表滚动、原生 transition 或 spring 推进,这些重绘都不执行 JavaScript。 运行时把两件事分开计数,gallery 的 Shell story(`cargo run -- shell`)把这两个数摆在界面上: | 界面在做什么 | 每秒画的帧 | 每秒跑的 JavaScript | | ----------------------------------- | ---------- | ------------------- | | 只是重绘,JavaScript 的数据没有变化 | 60 | 0 | | 价格每 50 ms 变动一次 | 60 | 19 | 帧数取决于屏幕,JavaScript 的次数取决于数据。第二行里另外 41 帧重放的是已有的描述。 成本因此按用户操作计,而不是按帧计。443 节点的面板,跑一遍 `render`、把整个界面记进 Snapshot 要 1.1 ms,只在状态变化时付;之后每一帧 1.3 ms,那是渲染本身——把 Snapshot 变成元素、布局、绘制,其中没有 JavaScript。 | | 每帧成本 | | ------------- | ------------------------------------------------------------------- | | 没有 Snapshot | 1.1 ms (JS render) + 1.3 ms (Rust render) = **2.4 ms/frame render** | | 有 Snapshot | **1.3 ms** | 面板变大也不改变这条性质:[基准测试](./engine.md#那次实测)覆盖到 8,403 个节点,各档的每一帧都不执行 JavaScript,最小一档由每次 CI 运行的断言保证。 ### 体积:一个脚本运行时只要 +13.5 MiB 跑一个真实脚本应用的 Host,二进制 **26.1 MiB**、常驻内存 **81 MiB**——QuickJS 和整个标准运行时都在里面。相比同一个应用不带它的版本,取这个依赖的代价是**二进制 +13.5 MiB、内存 +14 MiB**。 这个数是个常数,不是比例:组件 gallery——体量是它的五倍——增加的同样是 13.5 MiB。[链接它要付多少](./engine.md#链接它要付多少)给出了测量所用的那一对程序,以及这些兆字节都去了哪里。 以上数字都取自一台 MacBook Pro(M3,8 核,24 GB):帧数与次数来自 Shell story,毫秒数来自 release 构建的基准测试,二进制与内存数字来自 `examples/hello_world` 和 `gpui-shell` CLI 的 release 构建。 ### 安全:默认什么都没有,语言本身也一并收紧 `Capabilities::default()` 是空集——没有文件访问、没有存储、没有剪贴板、不能执行进程、没有网络。 Host 在加载 View 之前决定授权, View 随后在自己的整个生命周期里保持这份授权;`fs` 接口上的每一条路径都走**同一个**解析器,任何落在授权根之外的结果都会被拒绝。 在授权之下,沙箱还收紧了语言本身——因为一个 VM 早晚要同时承载多个插件:`eval` 与四个函数编译器全部移除,内置原型被冻结,避免一个插件改动 `Object.prototype` 波及另一个;模块解析被限制在应用目录内;堆(256 MiB)、解释器栈(1 MiB)与单次调用耗时(`render` 为 50 ms)都有上限。其中的耗时上限是一个 `catch` 无法吞掉的中断,这一点由测试保证。见 [Capabilities](./capabilities.md)。 ## 一次渲染是怎么走完的 这张图画的是一帧的过程,而这张图的形状基本解释了本节文档的其余部分。 GPUI 的元素是**被消费**的值:`RenderOnce::render` 按值取走 `self`,`.child()` 按值取走子元素, View 每次重绘都从零重建整棵元素树。因此一个 JavaScript 对象永远不可能**就是**一个 GPUI 元素——它没有东西可以长期持有。 所以脚本不构建元素,而是**描述**元素。builder 链上的每一次调用,都会把一条操作记录进一块元素描述 arena;脚本手里的对象只带一个指向 arena 的整数下标。当 GPUI 要求 View 渲染时,Rust 把这些记录下来的操作重放成真实元素、交给 GPUI,然后整块清空 arena。布局、绘制、命中测试、滚动与 IME 全程不再回到脚本。 由此直接推出三条结论,每条对应下面一个页面: * **元素是一次性的。** 描述在本次渲染结束时就消失了,所以被保存下来的元素在下次使用时抛出异常,而不是画出一个意料之外的东西。见 [Elements](./elements.md)。 * **`cx` 只属于产生它的那次调用。** 它带着一个 generation 编号,每次使用都与实时的调用栈比对;一个跨过 `await` 仍在使用的 `cx` 会给出明确错误,而不是去访问一个早已失效的栈帧。见 [State and Views](./state.md)。 * **回调属于注册它的那次渲染。** 下一次渲染会整体替换它们,这正是脚本闭包不会在 Host 里堆积的原因。见 [Elements](./elements.md)。 这三条都是“把脚本绑到一个会消费其值的元素模型上”必然的结果。 ## 呈现权在脚本一侧 大多数脚本层的做法,是把一批做好的控件交给脚本去摆放。这里没有这样的控件可交,因为它下面那一层同样没有。 `gpui-base` 的控件完全不带视觉样式。Rust 里的 `Button::new("save")` 没有内边距、没有背景、没有圆角、没有尺寸,这就是接口约定。JavaScript 绑定原样保留了这一点:`Button.new("save")` 不写样式时,除了它的子元素之外什么都不画。 结论才是重点:**因为基础层不提供任何呈现,呈现权就完整地落在脚本一侧**——颜色、间距、hover 状态、圆角,全部由脚本决定。这与 Rust 应用选择基于 `gpui-base` 而不是 `gpui-component` 时做的取舍完全一样;区别在于,这里的取舍写在一个存盘就能立刻看到结果的文件里,中间不需要 `cargo build`。 多打的字换来的是整个应用层。改一个按钮的圆角,不必再回到 Rust。 ## 适用场景 * **为已有的 GPUI 应用增加插件能力——首要场景。** 插件跑在 Host 进程内,能力由 Host 一项一项授予,起点是什么都没有。扩展产品不再意味着 fork 或者发一个新版本:界面与业务逻辑以脚本形式交付,改动不需要重新编译、也不需要重新分发二进制;插件出错会呈现为一个可恢复的错误,而不是把 Host 一起带走。 * **基于 `gpui-shell` 编写纯 JavaScript 的应用——次要场景。** 整个应用层——元素、样式、 View 状态、浮层与系统接口——都在 JavaScript 一侧,而渲染、文本编辑、虚拟化与每一个动画帧仍留在 Rust。这里也是一个插件在挂进 Host 之前被写出来、被验证的地方。 ## 它在架构中的位置 ```text JavaScript 应用 main.js · views · 样式 · 业务逻辑 │ import { … } from "gpui-kit" ▼ gpui-shell 引擎分界线 · 元素描述 · CallScope 样式表 · 主题 token · 能力模型 ShellRoot(dialog / sheet / toast)· 调度器 │ ▼ gpui-base 行为 · 状态 · 基础设施(无样式) │ ▼ gpui 元素 · 样式 · 渲染 · GPU · 平台 ``` `gpui-shell` 与 `gpui-component` 是并列关系,而不是在它下游:两者都是 `gpui-base` 的使用者,都补上了 Base 不提供的那一层呈现。`gpui-component` 用 Rust 提供了一套成品且统一的呈现;`gpui-shell` 提供的是让脚本自己去提供呈现的那套机制。 ## 接着读 | 页面 | 内容 | | --------------------------------------- | ---------------------------------------------------------------------------------- | | [Getting Started](./getting-started.md) | 运行示例、最小应用、`check` 与 `types` | | [Examples](./examples.md) | 仓库里的独立应用、 Host 状态与原生动画示例 | | [Elements](./elements.md) | 构造器、`child` / `children` / `when`,以及元素为什么是一次性的 | | [Styling](./styling.md) | 流式样式接口、长度与颜色、语义 token、状态样式 | | [State and Views](./state.md) | `init` / `render`、`cx.notify()`、留存状态、异步 | | [Overlays](./overlays.md) | dialog、sheet、toast,以及 phase 规则 | | [Capabilities](./capabilities.md) | `gpui-shell.json`、默认拒绝、文件、存储、进程与网络 API | | [依赖](./dependencies.md) | shell package:什么样的仓库算一个,manifest 如何命名与钉住它,以及编辑器拿到的类型 | | [Hosting](./hosting.md) | Rust 这一侧的全貌:挂载、刷新、指标、退出、hot-reload | | [HostModule](./host-module.md) | 把 Host 自己的 Rust 借给脚本,以及那条纯数据边界 | | [Dock 与面板](./dock.md) | 把脚本 View 变成可停靠面板、为它绘制 chrome,以及重启后什么会留下 | | [Performance](./performance.md) | 脚本的成本:失效频率乘以描述规模、 View 这条边界,以及那几个计数器 | | [The Engine Seam](./engine.md) | QuickJS、这条分界线存在的理由,以及把脚本成本与帧成本分开的三项实测 | ## 当前状态 该 crate 处于 **M0** 里程碑:一条可行性基线,而不是稳定接口。它没有发布到 crates.io,脚本 API 预计还会变化。本节文档写到的都是已经实现并可用的部分;缺失的部分,会写在你最可能去找它的那一页上。 设计详见 [GPUI Shell 设计文档](https://github.com/longbridge/gpui-kit/blob/main/docs/gpui-shell.md),代码位于 [`crates/shell`](https://github.com/longbridge/gpui-kit/tree/main/crates/shell)。 --- --- url: /docs/components/group-box.md description: >- A styled container element with an optional title to group related content together. --- # GroupBox The GroupBox component is a versatile container that groups related content together with optional borders, backgrounds, and titles. It provides visual organization and semantic grouping for form controls, settings panels, and other related UI elements. ## Import ```rust use gpui_kit::component::group_box::{GroupBox, GroupBoxVariant, GroupBoxVariants as _}; ``` ## Usage ### Basic GroupBox ```rust GroupBox::new() .child("Subscriptions") .child(Checkbox::new("all").label("All")) .child(Checkbox::new("newsletter").label("Newsletter")) .child(Button::new("save").primary().label("Save")) ``` ### GroupBox Variants ```rust // Normal variant (default) - no background or border GroupBox::new() .child("Content without visual container") // Fill variant - with background color GroupBox::new() .fill() .title("Settings") .child("Content with background") // Outline variant - with border, no background GroupBox::new() .outline() .title("Preferences") .child("Content with border") ``` ### With Title ```rust GroupBox::new() .fill() .title("Account Settings") .child( h_flex() .justify_between() .child("Make profile private") .child(Switch::new("privacy").checked(false)) ) .child(Button::new("save").primary().label("Save Changes")) ``` ### Custom ID ```rust GroupBox::new() .id("user-preferences") .outline() .title("User Preferences") .child("Preference controls...") ``` ### Custom Title Styling ```rust use gpui_kit::{StyleRefinement, relative}; GroupBox::new() .outline() .title("Custom Title") .title_style( StyleRefinement::default() .font_semibold() .line_height(relative(1.0)) .px_3() .text_color(cx.theme().accent) ) .child("Content with custom title styling") ``` ### Custom Content Styling ```rust GroupBox::new() .fill() .title("Custom Content Area") .content_style( StyleRefinement::default() .rounded_xl() .py_3() .px_4() .border_2() .border_color(cx.theme().accent) ) .child("Content with custom styling") ``` ### Complex Example ```rust GroupBox::new() .id("notification-settings") .outline() .bg(cx.theme().group_box) .rounded_xl() .p_5() .title("Notification Preferences") .title_style( StyleRefinement::default() .font_semibold() .line_height(relative(1.0)) .px_3() ) .content_style( StyleRefinement::default() .rounded_xl() .py_3() .px_4() .border_2() ) .child( v_flex() .gap_3() .child( h_flex() .justify_between() .child("Email notifications") .child(Switch::new("email").checked(true)) ) .child( h_flex() .justify_between() .child("Push notifications") .child(Switch::new("push").checked(false)) ) .child( h_flex() .justify_between() .child("SMS notifications") .child(Switch::new("sms").checked(false)) ) ) .child( h_flex() .justify_end() .gap_2() .child(Button::new("cancel").label("Cancel")) .child(Button::new("save").primary().label("Save Settings")) ) ``` ## Examples ### Form Section ```rust GroupBox::new() .fill() .title("Personal Information") .child( v_flex() .gap_4() .child( h_flex() .gap_2() .child(Input::new("first-name").placeholder("First Name")) .child(Input::new("last-name").placeholder("Last Name")) ) .child(Input::new("email").placeholder("Email Address")) .child( h_flex() .justify_end() .child(Button::new("update").primary().label("Update Profile")) ) ) ``` ### Settings Panel ```rust GroupBox::new() .outline() .title("Display Settings") .child( v_flex() .gap_3() .child( h_flex() .justify_between() .child(Label::new("Theme")) .child( RadioGroup::horizontal("theme") .child(Radio::new("light").label("Light")) .child(Radio::new("dark").label("Dark")) .child(Radio::new("auto").label("Auto")) ) ) .child( h_flex() .justify_between() .child(Label::new("Font Size")) .child( Select::new("font-size") .option("small", "Small") .option("medium", "Medium") .option("large", "Large") ) ) ) ``` ### Subscription Management ```rust GroupBox::new() .title("Email Subscriptions") .child( v_flex() .gap_2() .child(Checkbox::new("newsletter").label("Weekly Newsletter")) .child(Checkbox::new("updates").label("Product Updates")) .child(Checkbox::new("security").label("Security Alerts")) .child(Checkbox::new("marketing").label("Marketing Communications")) ) .child( h_flex() .justify_between() .mt_4() .child(Button::new("unsubscribe-all").link().label("Unsubscribe All")) .child(Button::new("save").primary().label("Update Preferences")) ) ``` ### Without Title ```rust GroupBox::new() .outline() .child( h_flex() .justify_between() .items_center() .child("Enable two-factor authentication") .child(Switch::new("2fa").checked(false)) ) ``` ## Styling The GroupBox component supports extensive customization through both built-in variants and custom styling: ### Theme Integration ```rust // Using theme colors GroupBox::new() .fill() .bg(cx.theme().group_box) .title("Themed Group Box") ``` ### Custom Appearance ```rust GroupBox::new() .outline() .border_2() .border_color(cx.theme().accent) .rounded(cx.theme().radius_lg) .title("Custom Styled Group Box") .title_style( StyleRefinement::default() .text_color(cx.theme().accent) .font_bold() ) ``` ## Best Practices 1. **Use titles for clarity** - Always include a descriptive title when grouping form controls 2. **Choose appropriate variants** - Use `fill()` for primary content groups, `outline()` for secondary groupings 3. **Maintain visual hierarchy** - Use GroupBox to create clear sections without overwhelming the interface 4. **Group related content** - Only group logically related controls and information 5. **Consider spacing** - The component automatically handles internal spacing, but consider external margins 6. **Responsive design** - GroupBox adapts well to different screen sizes and container widths ## Related Components * **Form**: Use GroupBox within forms to organize sections * **Dialog**: GroupBox works well within dialogs for organizing content * **Accordion**: For collapsible grouped content, consider using Accordion instead * **Card**: For elevated content containers with more visual weight --- --- url: /zh-CN/docs/components/group-box.md description: 带可选标题的分组容器组件,用于组织相关内容。 --- # GroupBox GroupBox 是一个用于组织相关内容的容器组件,支持标题、边框、背景和自定义样式,适合表单分区、设置面板和语义分组场景。 ## 导入 ```rust use gpui_kit::component::group_box::{GroupBox, GroupBoxVariant, GroupBoxVariants as _}; ``` ## 用法 ### 基础 GroupBox ```rust GroupBox::new() .child("Subscriptions") .child(Checkbox::new("all").label("All")) .child(Checkbox::new("newsletter").label("Newsletter")) .child(Button::new("save").primary().label("Save")) ``` ### 不同变体 ```rust GroupBox::new() .child("Content without visual container") GroupBox::new() .fill() .title("Settings") .child("Content with background") GroupBox::new() .outline() .title("Preferences") .child("Content with border") ``` ### 带标题 ```rust GroupBox::new() .fill() .title("Account Settings") .child( h_flex() .justify_between() .child("Make profile private") .child(Switch::new("privacy").checked(false)) ) .child(Button::new("save").primary().label("Save Changes")) ``` ### 自定义 ID ```rust GroupBox::new() .id("user-preferences") .outline() .title("User Preferences") .child("Preference controls...") ``` ### 自定义标题样式 ```rust use gpui_kit::{StyleRefinement, relative}; GroupBox::new() .outline() .title("Custom Title") .title_style( StyleRefinement::default() .font_semibold() .line_height(relative(1.0)) .px_3() .text_color(cx.theme().accent) ) .child("Content with custom title styling") ``` ### 自定义内容区域样式 ```rust GroupBox::new() .fill() .title("Custom Content Area") .content_style( StyleRefinement::default() .rounded_xl() .py_3() .px_4() .border_2() .border_color(cx.theme().accent) ) .child("Content with custom styling") ``` ### 复杂示例 ```rust GroupBox::new() .id("notification-settings") .outline() .bg(cx.theme().group_box) .rounded_xl() .p_5() .title("Notification Preferences") .title_style( StyleRefinement::default() .font_semibold() .line_height(relative(1.0)) .px_3() ) .content_style( StyleRefinement::default() .rounded_xl() .py_3() .px_4() .border_2() ) .child( v_flex() .gap_3() .child( h_flex() .justify_between() .child("Email notifications") .child(Switch::new("email").checked(true)) ) .child( h_flex() .justify_between() .child("Push notifications") .child(Switch::new("push").checked(false)) ) .child( h_flex() .justify_between() .child("SMS notifications") .child(Switch::new("sms").checked(false)) ) ) .child( h_flex() .justify_end() .gap_2() .child(Button::new("cancel").label("Cancel")) .child(Button::new("save").primary().label("Save Settings")) ) ``` ## 示例 ### 表单分区 ```rust GroupBox::new() .fill() .title("Personal Information") .child( v_flex() .gap_4() .child( h_flex() .gap_2() .child(Input::new("first-name").placeholder("First Name")) .child(Input::new("last-name").placeholder("Last Name")) ) .child(Input::new("email").placeholder("Email Address")) .child( h_flex() .justify_end() .child(Button::new("update").primary().label("Update Profile")) ) ) ``` ### 设置面板 ```rust GroupBox::new() .outline() .title("Display Settings") .child( v_flex() .gap_3() .child( h_flex() .justify_between() .child(Label::new("Theme")) .child( RadioGroup::horizontal("theme") .child(Radio::new("light").label("Light")) .child(Radio::new("dark").label("Dark")) .child(Radio::new("auto").label("Auto")) ) ) .child( h_flex() .justify_between() .child(Label::new("Font Size")) .child( Select::new("font-size") .option("small", "Small") .option("medium", "Medium") .option("large", "Large") ) ) ) ``` ### 邮件订阅管理 ```rust GroupBox::new() .title("Email Subscriptions") .child( v_flex() .gap_2() .child(Checkbox::new("newsletter").label("Weekly Newsletter")) .child(Checkbox::new("updates").label("Product Updates")) .child(Checkbox::new("security").label("Security Alerts")) .child(Checkbox::new("marketing").label("Marketing Communications")) ) .child( h_flex() .justify_between() .mt_4() .child(Button::new("unsubscribe-all").link().label("Unsubscribe All")) .child(Button::new("save").primary().label("Update Preferences")) ) ``` ### 无标题分组 ```rust GroupBox::new() .outline() .child( h_flex() .justify_between() .items_center() .child("Enable two-factor authentication") .child(Switch::new("2fa").checked(false)) ) ``` ## 样式 GroupBox 既支持内置变体,也支持自定义样式。 ### 主题集成 ```rust GroupBox::new() .fill() .bg(cx.theme().group_box) .title("Themed Group Box") ``` ### 自定义外观 ```rust GroupBox::new() .outline() .border_2() .border_color(cx.theme().accent) .rounded(cx.theme().radius_lg) .title("Custom Styled Group Box") .title_style( StyleRefinement::default() .text_color(cx.theme().accent) .font_bold() ) ``` ## 最佳实践 1. 对明确分组的表单项使用标题。 2. 主要内容分区可用 `fill()`,次级分区可用 `outline()`。 3. 用 GroupBox 建立清晰层级,但避免视觉过载。 4. 只把逻辑相关的内容放进同一个分组。 5. 组件会处理内部间距,但外部间距仍需要按页面布局控制。 6. GroupBox 能较好适配不同容器宽度和响应式布局。 ## 相关组件 * **Form**:可在表单中用 GroupBox 做分区 * **Dialog**:适合在对话框中组织内容 * **Accordion**:需要可折叠分组时可考虑使用 * **Card**:需要更强视觉容器感时可考虑 Card --- --- url: /base/history.md description: >- Browser-style navigation trails and grouped undo/redo transactions for application state. --- # History and Undo History `History` and `UndoHistory` keep two different kinds of application state. Both are independent of GPUI and leave applying a returned value to the caller, but their operations intentionally have different meanings: * `History` is a browser-style linear trail of locations, with back and forward navigation. * `UndoHistory` records changes as undo transactions, including changes grouped into one user action. ## Import ```rust use gpui_kit::base::{History, UndoHistory}; ``` ## Which one to use Choose the type from the meaning of the state, not from the names of the UI commands that operate on it: * Use `History` when each entry is a location and moving backward or forward returns the location reached. The current root must remain available. * Use `UndoHistory` when each entry is a reversible change and undo or redo must return every change in one user transaction. * Use a domain-specific manager when grouping depends on richer semantics than time or explicit boundaries. The Input component, for example, has a private transaction manager that understands typing, deletion, selection, and IME composition. Within gpui-component, `NavStack` uses `History` for page navigation, while Dock's tiles canvas uses `UndoHistory` to reverse grouped move and resize changes. Input deliberately keeps its specialized private undo manager. ## `History`: a navigation trail Push every location the user visits. The current entry is the last value in the trail. For example, after visiting `A -> B -> C`, `C` is current and going back returns the new current entry, `B`: ```rust let mut history = History::new(); history.push("A"); history.push("B"); history.push("C"); assert_eq!(history.back(), Some("B")); assert_eq!(history.current(), Some(&"B")); ``` `back()` never moves past the root entry; it returns `None` there. `forward()` restores the nearest entry that was left behind. Pushing a new entry after going back drops that forward branch, just as a browser does after opening a new page. `max_entries` bounds the root-to-current entries: lowering it removes the oldest active entries immediately, and moving forward at the limit removes the oldest active entry before restoring the next one. `entries()` iterates from the root to the current entry. With the full `A -> B -> C` trail, it yields `A`, `B`, then `C`; `entries().rev()` yields `C`, `B`, then `A`. `forward_entries()` iterates from the nearest forward entry to the furthest. Use `retain` to remove invalid locations, `replace_current` to update the current location in place, and `remove_current` to remove it without discarding the forward branch. | Method | Does | | -------------------------------------------- | ------------------------------------------------------------------------------- | | `new()` | Creates an empty trail. `max_entries` defaults to 1000. | | `max_entries(n)` | Caps root-to-current entries and immediately removes the oldest excess entries. | | `push(entry)` | Makes `entry` current and drops the forward branch. | | `back()`, `forward()` | Move through the trail and return the resulting current entry. | | `current()` | Returns the current entry. | | `can_back()`, `can_forward()` | Report whether movement in that direction is available. | | `entries()`, `forward_entries()` | Iterate the current trail and forward branch in navigation order. | | `replace_current(entry)`, `remove_current()` | Update or remove the current entry. | | `retain(keep)`, `clear()` | Remove rejected entries from both sides, or empty the trail. | ## `UndoHistory`: grouped undo and redo Push a value for each change your application must reverse. To make a drag one undoable action, explicitly group all of its updates. `undo()` returns the group's changes newest first so the most recent change is reverted first; `redo()` returns the same group oldest first so it is applied in its original order: ```rust let mut history = UndoHistory::new(); history.start_grouping(); history.push("move from x=0 to x=10"); history.push("move from x=10 to x=20"); history.end_grouping(); assert_eq!( history.undo(), Some(vec!["move from x=10 to x=20", "move from x=0 to x=10"]), ); assert_eq!( history.redo(), Some(vec!["move from x=0 to x=10", "move from x=10 to x=20"]), ); ``` For changes whose boundary is not explicit, `group_interval` combines consecutive pushes close enough in time. A successful undo or redo ends that timed grouping window, so the next push starts a new transaction. Explicit grouping is separate: while it is active, a push still appends to the current transaction, including after an undo. A new push clears redo transactions. While replaying changes, use `set_ignoring(true)` to prevent the replay itself from being recorded. | Method | Does | | ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `new()` | Creates an empty undo history. `max_undos` defaults to 1000. | | `max_undos(n)` | Caps undo transactions and immediately removes the oldest excess transactions. Redo preserves this cap. | | `group_interval(duration)` | Groups consecutive nearby pushes into one transaction. | | `start_grouping()`, `end_grouping()` | Make subsequent pushes append to the current transaction; ending grouping stops that explicit append behavior. On an empty history, as in the example above, the first push starts the transaction. | | `push(change)` | Records a change in the current or a new transaction and clears redo. | | `undo()`, `redo()` | Return the latest transaction newest-first for undo, oldest-first for redo. | | `can_undo()`, `can_redo()` | Report whether a transaction is available. | | `set_ignoring(bool)`, `is_ignoring()` | Control whether pushes are recorded. | | `clear()` | Empties undo and redo transactions. | --- --- url: /zh-CN/base/history.md description: 用于应用状态的浏览器式导航轨迹和分组 undo/redo 事务。 --- # History 与 Undo History `History` 与 `UndoHistory` 分别保存两种不同的应用状态。两者都不持有 GPUI 状态,且都由调用方把返回的值应用到模型;但它们的操作含义不同: * `History` 是浏览器式的线性导航轨迹,支持后退和前进。 * `UndoHistory` 将改动记录为 undo 事务,并能把多个改动合成一次用户操作。 ## 引入 ```rust use gpui_kit::base::{History, UndoHistory}; ``` ## 如何选择 应根据状态的含义选择类型,而不是根据操作它的 UI 命令名称选择: * 当每个条目表示一个位置,后退或前进需要返回到达的位置,并且必须保留当前根条目时,使用 `History`。 * 当每个条目表示一项可逆改动,并且 undo 或 redo 需要返回一次用户事务中的全部改动时,使用 `UndoHistory`。 * 当分组依赖比时间或显式边界更丰富的领域语义时,使用领域专用的管理器。例如 Input 使用私有事务管理器来理解输入、删除、选择区和 IME 组合输入。 在 gpui-component 内部,`NavStack` 使用 `History` 进行页面导航;Dock 的 tiles canvas 使用 `UndoHistory` 回退分组的移动和缩放改动;Input 则有意保留其专用的私有 undo manager。 ## `History`:导航轨迹 每到一个位置就 push 一条。当前条目是轨迹中的最后一个值。例如访问完 `A -> B -> C` 后,当前是 `C`;后退会返回新的当前条目 `B`: ```rust let mut history = History::new(); history.push("A"); history.push("B"); history.push("C"); assert_eq!(history.back(), Some("B")); assert_eq!(history.current(), Some(&"B")); ``` `back()` 不会越过根条目,到根时返回 `None`。`forward()` 会恢复最近一个此前离开的条目。后退后再 push 新条目会丢弃前进分支,和浏览器打开新页面时的行为相同。`max_entries` 限制从根到当前的活动条目:降低上限会立即删除最旧的多余活动条目;达到上限时前进,会先删除最旧的活动条目,再恢复下一个条目。 `entries()` 按从根到当前条目的顺序迭代。完整的 `A -> B -> C` 轨迹会依次得到 `A`、`B`、`C`;`entries().rev()` 则得到 `C`、`B`、`A`。`forward_entries()` 从最近的前进条目到最远的前进条目迭代。用 `retain` 删除已失效的位置,用 `replace_current` 原地更新当前的位置,用 `remove_current` 删除当前条目而不丢弃前进分支。 | 方法 | 作用 | | -------------------------------------------- | -------------------------------------------------- | | `new()` | 创建空轨迹。`max_entries` 默认是 1000。 | | `max_entries(n)` | 限制从根到当前的条目数,并立即删除最旧的多余条目。 | | `push(entry)` | 让 `entry` 成为当前条目,并丢弃前进分支。 | | `back()`、`forward()` | 在轨迹中移动,返回移动后的当前条目。 | | `current()` | 返回当前条目。 | | `can_back()`、`can_forward()` | 判断对应方向是否可以移动。 | | `entries()`、`forward_entries()` | 按导航顺序迭代当前轨迹和前进分支。 | | `replace_current(entry)`、`remove_current()` | 更新或删除当前条目。 | | `retain(keep)`、`clear()` | 从两侧删除不保留的条目,或清空轨迹。 | ## `UndoHistory`:分组 undo 与 redo 每次需要由应用回退的改动都 push 一条。要把一次拖拽作为一项可撤销操作,请显式地把它的所有更新分组。`undo()` 以最新在前的顺序返回一个组里的改动,确保最近的改动先被回退;`redo()` 按最旧在前的顺序返回同一组,以原始顺序重新应用: ```rust let mut history = UndoHistory::new(); history.start_grouping(); history.push("从 x=0 移到 x=10"); history.push("从 x=10 移到 x=20"); history.end_grouping(); assert_eq!( history.undo(), Some(vec!["从 x=10 移到 x=20", "从 x=0 移到 x=10"]), ); assert_eq!( history.redo(), Some(vec!["从 x=0 移到 x=10", "从 x=10 移到 x=20"]), ); ``` 对于边界不明确的改动,`group_interval` 会把时间间隔足够短的连续 push 合成一个事务。成功 undo 或 redo 会结束这段定时分组窗口,下一次 push 会创建新事务。显式分组与此独立:只要显式分组仍在进行,push 就会继续追加到当前事务,包括刚完成 undo 之后。新的 push 会清空 redo 事务。回放改动时,用 `set_ignoring(true)` 防止回放本身被再次记录。 | 方法 | 作用 | | ------------------------------------- | ------------------------------------------------------------------------------------------------------------ | | `new()` | 创建空 undo 历史。`max_undos` 默认是 1000。 | | `max_undos(n)` | 限制 undo 事务数并立即删除最旧的多余事务;redo 也会遵守该上限。 | | `group_interval(duration)` | 将相隔很近的连续 push 合并为一个事务。 | | `start_grouping()`、`end_grouping()` | 让后续 push 追加到当前事务;结束分组会停止这种显式追加行为。和上例一样,空历史中的第一个 push 会创建该事务。 | | `push(change)` | 在当前或一个新事务中记录改动,并清空 redo。 | | `undo()`、`redo()` | undo 时最新改动在前,redo 时最旧改动在前地返回最新事务。 | | `can_undo()`、`can_redo()` | 判断是否有可用事务。 | | `set_ignoring(bool)`、`is_ignoring()` | 控制是否记录 push。 | | `clear()` | 清空 undo 与 redo 事务。 | --- --- url: /shell/hosting.md description: >- The Rust side in full — runtime lifetime, mounting script Views, refreshing them from host state, metrics, exit requests and hot-reload. --- # Hosting [Getting Started](./getting-started.md) shows the four lines that put a script View on screen. This page is the rest of the Rust surface: what to call, when, and the two or three places where the obvious call is the wrong one. ## The runtime One `ShellRuntime` owns one VM. It is an `Rc` with interior mutability — neither `Send` nor `Sync` — so it lives on the thread that owns the `App`. ```rust gpui_kit::shell::init(cx); // gpui-base, the token palette, the style table let runtime = ShellRuntime::new(cx)?; // one VM, installed as this App's default ``` `new(cx)` lets callbacks, HostModule registrations and hot reload find the default runtime without the host threading a handle through every layer. A host deliberately managing more than one VM can create additional runtimes with `new_isolated()` and retain those handles itself. `gpui-shell` uses GPUI's inspector reflection table to expose the fluent style methods, including in release builds. Depending on this crate therefore enables the `gpui-base/inspector` feature for the unified Cargo dependency graph. This is required for the JavaScript style surface; embedders should account for the additional release-build instrumentation and dependencies. ## Loading and instantiating For the usual application window, loading is one operation and returns its `ShellRoot` directly: ```rust cx.open_window(options, move |window, cx| { let root = runtime.load(&app_root, window, cx); #[cfg(debug_assertions)] if let Ok(watch) = runtime.watch(&root, window, cx) { watch.forget(); } root })?; ``` If `gpui-shell.json` exists, `load` validates its identity metadata and applies its entry. Its capabilities are requests, not approval: both paths run under the host's current default policy, and without a manifest the entry is `main.js`. Either path refreshes `gpui-kit.d.ts`; a load failure renders the selectable error surface instead of panicking the host. A host that needs to handle the structured error itself uses `try_load`. A failure root has no application to watch, so `watch` returns `Err`; ignoring that error here keeps the selectable failure surface mounted. The lower-level methods below are for a host that needs to assemble a script View into an existing Rust composition. Loading turns source into a **View type** — the class the script default-exports. Instantiating turns that type into a **View object**, one live instance: ```rust let view_type = runtime.load_app(&root, "main.js")?; // a directory let view_type = runtime.load_source("inline", source)?; // a string, for tests let object = runtime.instantiate(&view_type, window, cx)?; ``` `load_app` resolves the directory, reads the entry file, and evaluates the module. Every failure here is a `ShellError` carrying the script's own stack — a syntax error, an import that resolves outside the application root, a missing or misshapen default export. Instantiating runs the script's `init`, which means it needs a live `Window`: it may create retained state such as an `InputState`. ## Mounting A script View is a GPUI View like any other, and it goes **under a `ShellRoot`**: ```rust cx.open_window(options, move |window, cx| { let object = runtime.instantiate(&view_type, window, cx).expect("view"); let content = cx.new(|_| ScriptView::new(runtime.clone(), object)); cx.new(|cx| ShellRoot::new(content.into(), window, cx)) }) ``` `ShellRoot` owns the dialog stack, the sheet, the toast stack, focus restoration and Tab navigation — the same role `Root` plays for a `gpui-component` window. `window.open_dialog` and friends reach it, so a script mounted under any other root View gets a refusal naming the reason rather than a silent no-op. The host can drive the same surfaces directly, which is how a plugin panel and the host's own UI end up in one stack: ```rust root.update(cx, |root, cx| { root.open_dialog(view.into(), window, cx); root.push_toast(ToastRequest::new("Saved").with_level(ToastLevel::Success), window, cx); root.close_all_dialogs(window, cx); }); ``` ## Refreshing a View from host state This is the one call that is easy to get wrong, and the mistake is silent. ```text cx.notify() ── draw this View again (no script runs) view.refresh(cx) ── and its description is stale (the script runs) ``` Because a script `render` is [not a frame render](./state.md#when-render-runs), a plain `cx.notify()` repaints the Snapshot that already exists. If the host changed something the script *reads* — an entity behind a HostModule, a setting, a document — the View must be told the description itself is out of date: ```rust runtime.refresh(&root, cx)?; ``` The runtime checks that `root` contains one of its applications, then invalidates that script View and schedules a repaint. Keeping the typed `ScriptView` private prevents host code from downcasting the root content or refreshing a View from another runtime by mistake. Getting it wrong in the other direction is visible immediately — the interface simply does not update — which is the same failure mode as a forgotten `cx.notify()` in GPUI itself. ## What a script may reach The three host settings have different lifetimes. Capabilities are frozen into each newly loaded View. The store handle and HostModule registry are live host configuration shared with that View, so replacing either affects its next call: ```rust gpui_kit::shell::set_capabilities( Capabilities::new() .read_roots([app_root.clone()]) .write_roots([data_dir.clone()]) .store(true), ); gpui_kit::shell::set_store_path(data_dir.join("store.json")); gpui_kit::shell::export_module(market_module(&market))?; ``` All three default to nothing: no file access, no storage location, no HostModule registrations. See [Capabilities](./capabilities.md) and [HostModule](./host-module.md). The standalone binary also checks `/gpui-shell.json`. Its recognized fields supply application identity, optional application/Shell version metadata, the entry point, and capability requests; only `id`, `name`, and `entry` are required. Embedders may instead construct a `Policy` directly when each loaded application needs a distinct grant and module registry. ## Watching what it costs The runtime counts two events separately, and the gap between them is the point: ```rust let reading = runtime.read_metrics(); reading.script_renders(); // follows cx.notify(), reloads, theme changes reading.materializations(); // follows frames reading.script_render_time(); // total time inside script `render` reading.native_time(); // of which, inside HostModule registrations reading.slowest_script_render(); reading.structure_repeat_rate(); // how often a rebuild described the shape it replaced ``` `RuntimeMetrics::since(&earlier)` gives the delta between two readings, which is how a per-second rate is built. There is no reset: the counters belong to the runtime, and zeroing them would move them under anything else that is reading. To measure one stretch, keep a baseline and subtract — the Shell story takes one whenever its feed changes, so its readout answers "what is this feed costing" rather than "what has this window done since it opened". A regression test can assert on `script_renders` directly; that is what keeps [the benchmark's third figure](./engine.md#the-measurement) honest. `structure_repeats()` and `structure_changes()` answer a different question: of the rebuilds that had a previous description to compare with, how many produced the same *shape* — the same components, the same builder methods, the same tree — and differed only in the values inside it. Nothing in the runtime acts on the answer; it is there to size [where the Snapshot cache stops](./performance.md#where-the-snapshot-cache-stops). A View's first build has no predecessor and is counted in neither. ## Building for development A debug build of a host is roughly **three times slower per script render** than a release build, and the whole difference is in two dependencies. Measured on a live application — a market terminal re-rendering on every quote tick — with the runtime's own [`RuntimeMetrics`](#watching-what-it-costs): | `[profile.dev.package]` | mean script render | mean materialize | | -------------------------------- | ------------------ | ---------------- | | nothing, or `rquickjs` alone | 31.5 ms | 3.9 ms | | `rquickjs-sys` + `rquickjs-core` | **11.3 ms** | **1.2 ms** | | release, for comparison | 11.0 ms | 1.2 ms | So: ```toml [profile.dev.package] rquickjs-sys = { opt-level = 3 } rquickjs-core = { opt-level = 3 } ``` **`rquickjs` on its own does nothing**, which is the trap: it is a thin facade that re-exports `rquickjs-core`, so naming it optimises neither the interpreter nor the bindings. `rquickjs-sys` compiles QuickJS itself — C source, built through `cc`, which reads the profile's optimisation level for *that* package — and `rquickjs-core` is where every value that crosses the boundary is converted. An unoptimised interpreter is what makes an unoptimised build feel like a different product. The `llrt_*` crates do **not** need this. They were measured with the same application and made no difference beyond the noise: `fs`, `net`, `crypto` and the rest are not on the render path, so optimising them buys nothing a script author would feel. These settings only take effect in the **workspace root that builds the binary**. A library cannot impose a profile on the application that depends on it, so `gpui-shell` cannot set this for you — every host has to write it down itself. ## Exit requests `process.exit(code)` from a script is **a request, never `exit(2)`**. One plugin must not be able to take the host process down, and the host may have unsaved state. The runtime hands the request to the host, and the host decides: ```rust gpui_kit::shell::on_exit_request(|request, window, cx| { match request.view() { Some(view) => close_the_panel_showing(view, window, cx), None => cx.quit(), } }); ``` `request.code()` is the exit code the script asked for, and `request.view()` names the View it came from, when there is one — a plugin host closes *that* plugin's panel, where one that quit the window would let a plugin end someone else's work. **A host that grants exit without installing a handler is told at the call**, not never: `process.exit()` throws, naming `on_exit_request`. A request nobody answers is a lie told in the flattering direction — the script gets a success and nothing happens. ## Hot-reload One call starts it, and it is the same one the `--watch` flag uses: ```rust runtime.watch(&root, window, cx)?.forget(); ``` `runtime.watch` reads the resolved directory and manifest entry retained by the loaded root, so there is no second copy of that metadata to drift. It has no hidden build-mode policy: the CLI enables watching after `--watch`, while an embedded host can put the call behind `#[cfg(debug_assertions)]`. The returned `Watcher` owns the watch: dropping it stops the loop, which is what a host unmounting a panel wants, while `.forget()` lets it run for as long as the View does. The loop also ends on its own when the View, the runtime or the window goes away, because it holds all three weakly. A reload re-reads **every** module, entry point included — a hot-reload that quietly served a stale import would be worse than none, because it looks like it worked. It does all of its fallible work before touching the live View: if the new code fails to load, the previous View keeps running, the error goes to `tracing`, and a toast with a stable id reports it in the window. The next successful reload retracts that toast. The View survives a reload. `ScriptView::replace_object` swaps what the script produced while keeping the entity, and with it the window, the focus and the element identities. Plugin unload is a stronger lifecycle boundary than removing one View: the manager cancels every outstanding task carrying that plugin's `Policy`, including owner-less work, before dropping the plugin. No continuation may retain or exercise an unloaded plugin's authority. ## When a script fails A script that throws does not take the interface with it. The last good Snapshot stays mounted and the failure is reported over it, so the reader keeps their scroll, their focus, and whatever they were reading. The runtime does not re-run a failing `render` until something invalidates the View again. Install a `tracing` subscriber. The runtime reports script errors, unhandled promise rejections and illegal-phase calls through `tracing` with the target `gpui_kit::shell::script`; with no subscriber every one of them is discarded, and the symptom is a View that quietly stopped responding. ## Not there yet * **A supervisor for scripts that hang.** The interpreter's own interrupt cuts a call off, but nothing restarts a runtime that keeps hitting it. --- --- url: /zh-CN/shell/hosting.md description: Rust 这一侧的全貌——运行时的生命周期、挂载脚本 View、从 Host 状态刷新它、指标、退出请求与 hot-reload。 --- # Hosting [Getting Started](./getting-started.md) 给的是把脚本 View 放上屏幕的那四行。这一页是 Rust 接口的其余部分:该调什么、什么时候调,以及那两三处“看起来该调的那个其实是错的”。 ## 运行时 一个 `ShellRuntime` 拥有一个 VM。它是一个带内部可变性的 `Rc`——既不是 `Send` 也不是 `Sync`——所以它待在拥有 `App` 的那个线程上。 ```rust gpui_kit::shell::init(cx); // gpui-base、默认 token 调色板、样式表 let runtime = ShellRuntime::new(cx)?; // 一个 VM,并注册为当前 App 的默认 runtime ``` `new(cx)` 让回调、 HostModule 与 hot reload 不必由 Host 层层传递句柄,也能找到默认 runtime。明确管理多个 VM 的 Host 可以用 `new_isolated()` 创建其他 runtime,并自行保留这些句柄。 `gpui-shell` 通过 GPUI 的 inspector reflection table 暴露 fluent style 方法,release 构建也不例外。因此,依赖这个 crate 会为 Cargo 统一后的依赖图启用 `gpui-base/inspector` feature。这是 JavaScript 样式接口正常工作的必要条件;嵌入方 应把 release 构建中新增的检测代码与依赖计入构建成本。 ## 加载与实例化 普通应用窗口只需一次加载,并直接获得它的 `ShellRoot`: ```rust cx.open_window(options, move |window, cx| { let root = runtime.load(&app_root, window, cx); #[cfg(debug_assertions)] if let Ok(watch) = runtime.watch(&root, window, cx) { watch.forget(); } root })?; ``` 存在 `gpui-shell.json` 时,`load` 会验证其中的身份信息,并采用其 entry。capabilities 是能力请求,不等于 Host 已经批准;两条路径都按 Host 当前的默认 policy 运行,没有 manifest 时入口为 `main.js`。两条路径都会刷新 `gpui-kit.d.ts`;加载失败会渲染可选择文字的错误界面,而不是让 Host panic。需要自行处理结构化错误的 Host 使用 `try_load`。失败状态的 root 没有可供监听的应用,因此 `watch` 会返回 `Err`;这里忽略这个错误,才能保留可选择的失败界面。 下面的低层方法只供需要把脚本 View 装进既有 Rust 组合的 Host 使用。 加载把源码变成一个**View 类型**——脚本 default 导出的那个类。实例化把这个类型变成一个**View 对象**,也就是一个活的实例: ```rust let view_type = runtime.load_app(&root, "main.js")?; // 一个目录 let view_type = runtime.load_source("inline", source)?; // 一个字符串,测试用 let object = runtime.instantiate(&view_type, window, cx)?; ``` `load_app` 会解析目录、读取入口文件、求值该模块。这里的每一种失败都是一个带着脚本自身调用栈的 `ShellError`——语法错误、解析到应用根目录之外的 import、缺失或形态不对的 default 导出。 实例化会执行脚本的 `init`,因此它需要一个活的 `Window`:`init` 里可能会创建 `InputState` 这类留存状态。 ## 挂载 脚本 View 和别的 GPUI View 没有两样,它挂在**一个 `ShellRoot` 之下**: ```rust cx.open_window(options, move |window, cx| { let object = runtime.instantiate(&view_type, window, cx).expect("view"); let content = cx.new(|_| ScriptView::new(runtime.clone(), object)); cx.new(|cx| ShellRoot::new(content.into(), window, cx)) }) ``` `ShellRoot` 持有 dialog 栈、sheet、toast 栈、焦点恢复与 Tab 导航——正是 `Root` 对一个 `gpui-component` 窗口所起的作用。`window.open_dialog` 这一类调用要经由它找到根 View,所以挂在别的根 View 之下的脚本会拿到一条讲清原因的拒绝,而不是悄无声息地没反应。 Host 也可以直接驱动同样这几个界面,插件面板与 Host 自己的 UI 因此落在同一个栈里: ```rust root.update(cx, |root, cx| { root.open_dialog(view.into(), window, cx); root.push_toast(ToastRequest::new("Saved").with_level(ToastLevel::Success), window, cx); root.close_all_dialogs(window, cx); }); ``` ## Host 状态变了,怎么刷新 View 这是最容易调错的一个,而且调错了不会报错。 ```text cx.notify() ── 把这个 View 再画一遍 (不跑脚本) view.refresh(cx) ── 而且它的描述已经过期了 (脚本会跑) ``` 因为脚本的一次 `render` [不等于一帧渲染](./state.md#render-什么时候执行),光调 `cx.notify()` 重绘的是已经存在的那份 Snapshot。如果 Host 改动的是脚本**会读到**的东西——某个 HostModule 背后的实体、一项设置、一份文档——就必须告诉 View:描述本身已经过期了。 ```rust runtime.refresh(&root, cx)?; ``` runtime 会先确认 `root` 装载的是它自己的应用,再让脚本 View 失效并安排重绘。 Host 不需要拿到具体的 `ScriptView`,也不会因为手工 downcast 或混用另一个 runtime 的 View 而刷新错误对象。 反过来调错则立刻看得见——界面就是不更新——这与 GPUI 里忘了调 `cx.notify()` 是同一种失败方式。 ## 脚本能碰到什么 三项 Host 设置的生命周期不同。Capabilities 会在每个新 View 加载时冻结;store handle 与 HostModule registry 则是该 View 共享的实时 Host 配置,替换后会在下一次调用生效: ```rust gpui_kit::shell::set_capabilities( Capabilities::new() .read_roots([app_root.clone()]) .write_roots([data_dir.clone()]) .store(true), ); gpui_kit::shell::set_store_path(data_dir.join("store.json")); gpui_kit::shell::export_module(market_module(&market))?; ``` 三项的默认都是“什么都没有”:没有文件访问、没有存储位置、没有 HostModule 。见 [Capabilities](./capabilities.md) 与 [HostModule](./host-module.md)。 独立二进制还会检查 `/gpui-shell.json`。其中已识别的字段提供应用身份、可选的应用/Shell 版本元数据、entry 与 capability 请求;只有 `id`、`name` 和 `entry` 必填。Embedder 若要让每个加载的应用拥有不同 grant 与 HostModule registry,也可以直接构造 `Policy`。 ## 观察它花了多少 运行时把两件事分开计数,而这两个数之间的差就是重点: ```rust let reading = runtime.read_metrics(); reading.script_renders(); // 跟着 cx.notify()、重载、主题变化走 reading.materializations(); // 跟着帧走 reading.script_render_time(); // 脚本 render 里的总耗时 reading.native_time(); // 其中花在 HostModule 里的部分 reading.slowest_script_render(); reading.structure_repeat_rate(); // 一次重建产出的结构,与它替换掉的那份是否相同 ``` `RuntimeMetrics::since(&earlier)` 给出两次读数之间的差值,每秒速率就是这么算的。这里没有重置:计数器属于运行时,把它们清零会把正在读它们的其他人一起挪动。要量某一段,就自己留一个基线再相减——Shell story 每次切换 feed 都会取一次基线,所以它的读数回答的是“这个 feed 要花多少”,而不是“这个窗口从打开到现在干了多少”。 回归测试可以直接对 `script_renders` 做断言;[基准测试里的第三个数](./engine.md#那次实测)靠的正是这一点。 `structure_repeats()` 与 `structure_changes()` 回答的是另一个问题:在那些有上一份描述可比的重建里,有多少次产出的**结构**完全相同——相同的组件、相同的 builder 方法、相同的树,只有其中的取值变了。运行时不会因为这个答案而少做任何事;它存在,是为了给[Snapshot 缓存止步于哪里](./performance.md#snapshot-缓存止步于哪里)量个尺寸。 View 的第一次构建没有前一份可比,两个计数都不计它。 ## 开发构建的配置 Host 的 debug 构建,**单次脚本渲染大约比 release 慢三倍**,而差距全部来自两个依赖。 在一个实时应用上实测——一个每笔行情都重渲染的行情终端,用运行时自带的 [`RuntimeMetrics`](#观察它花了多少): | `[profile.dev.package]` | 平均脚本渲染 | 平均物化 | | -------------------------------- | ------------ | ---------- | | 不配,或只写 `rquickjs` | 31.5 ms | 3.9 ms | | `rquickjs-sys` + `rquickjs-core` | **11.3 ms** | **1.2 ms** | | release(对照) | 11.0 ms | 1.2 ms | 所以: ```toml [profile.dev.package] rquickjs-sys = { opt-level = 3 } rquickjs-core = { opt-level = 3 } ``` **只写 `rquickjs` 没有任何作用**,这正是坑所在:它是一个薄门面,把 `rquickjs-core` 重新导出而已,写它既没优化到解释器,也没优化到绑定。`rquickjs-sys` 编译的是 QuickJS 本体——C 源码,经 `cc` 构建,而 `cc` 读的是**那个包**在 profile 里的优化级别; `rquickjs-core` 则是每一个跨界值的转换所在。没优化的解释器,正是让 debug 构建 用起来像另一个产品的原因。 `llrt_*` 那批**不需要**这么做。同一个应用上实测,它们带来的差异在噪声范围内: `fs`、`net`、`crypto` 之类根本不在渲染路径上,优化它们换不来脚本作者能感知的东西。 这些设置只在**构建出二进制的那个 workspace 根**生效。库无法替依赖它的应用设定 profile, 所以 `gpui-shell` 没办法替你配好——每个 Host 都得自己写一遍。 ## 退出请求 脚本里的 `process.exit(code)` 是**一个请求,绝不是 `exit(2)`**。一个插件不能把 Host 进程带走,而 Host 可能还有未保存的状态。运行时把这个请求交给 Host,由 Host 决定怎么办: ```rust gpui_kit::shell::on_exit_request(|request, window, cx| { match request.view() { Some(view) => close_the_panel_showing(view, window, cx), None => cx.quit(), } }); ``` `request.code()` 是脚本要求的退出码,`request.view()` 在有的情况下会指出请求来自哪个 View——插件 Host 关掉的应该是**那个**插件的面板,若换成关窗口,就等于让一个插件终结了别人的工作。 **授权了 exit 却没装处理器的 Host,会在调用现场被告知**,而不是永远不知道:`process.exit()` 会抛出异常,并点名 `on_exit_request`。一个没人回应的请求,是朝着讨好方向说的谎——脚本拿到了成功,而什么都没发生。 ## Hot-reload 一个调用就能开起来,`--watch` 用的也是这一个: ```rust runtime.watch(&root, window, cx)?.forget(); ``` `runtime.watch` 从已加载的 root 读取解析后的目录与 manifest entry,不再让 Host 维护第二份可能漂移的元数据。它不暗藏构建模式策略:CLI 在解析到 `--watch` 后启用监听,嵌入式 Host 则可以把调用放进 `#[cfg(debug_assertions)]`。返回的 `Watcher` 持有这次监听:把它 drop 掉,循环就停;`.forget()` 则让它跟随 View 继续运行。 View、运行时或窗口任意一个消失时,循环也会自己结束。 一次重载会重新读取**每一个**模块,入口也在内——一个悄悄用了旧 import 的 hot-reload 比没有更糟,因为它看起来是成功的。它会先把所有可能失败的活干完,再去碰活着的那个 View:新代码加载失败时,上一个 View 继续运行,错误进 `tracing`,窗口里由一条固定 id 的 toast 报出来;下一次成功的重载会撤掉这条 toast。 View 本身能挺过重载。`ScriptView::replace_object` 只换掉脚本产出的那部分,实体保留下来,随之保留的还有窗口、焦点与元素身份。 插件 unload 是比移除单个 view 更强的生命周期边界:manager 会在丢弃插件前取消所有携带该插件 `Policy` 的 outstanding task,包括没有 owner 的工作。任何 continuation 都不能继续保留或使用已卸载插件的权限。 ## 脚本出错的时候 抛异常的脚本不会把界面一起带走。最后一份可用的 Snapshot 仍然挂在那里,失败信息报在它上面,读者的滚动位置、焦点、正在读的内容都还在。在有什么让 View 失效之前,运行时不会重跑那个失败的 `render`。 记得装一个 `tracing` subscriber。运行时通过 `tracing` 报告脚本错误、未处理的 promise rejection 与非法 phase 调用,target 是 `gpui_kit::shell::script`;没有 subscriber 的话这些全部被丢弃,症状就是一个悄悄不再响应的 View。 ## 还没有的东西 * **给卡住的脚本做监管。** 解释器自己的中断会切断一次调用,但没有东西会去重启一个反复撞上中断的运行时。 --- --- url: /shell/host-module.md description: >- How a host lends its own Rust to a script — registration, the import that reaches it, the plain-data boundary, and the rules a Host function runs under. --- # HostModule [Capabilities](./capabilities.md) is the half that says what a script may **not** reach. This is the other half: what the host chooses to hand it. A script cannot load a native extension. `dlopen`-ed Rust has no stable ABI, and once it is inside the process it holds every permission the process holds — a sandbox that permits that does not mean anything. So the direction is reversed. **The host registers, at compile time, the Rust it is willing to expose**, and a script reaches exactly that and nothing else. ```rust use gpui_kit::shell::{HostModule, HostValue}; gpui_kit::shell::export_module( HostModule::new("workspace") .function("project_name", |_| Ok(HostValue::from("gpui-component"))) .function("version", |_| Ok(HostValue::from("0.1.0"))), )?; ``` ```js import { project_name } from "workspace"; project_name(); // "gpui-component" ``` A registered module is an ordinary ES module, resolved by the same loader that answers `gpui-kit` and `path`. One call registers one module, and a repeated name replaces the earlier module rather than merging into it — a host with three of them calls `export_module` three times. The rest of this page is what that costs and what it refuses. ## Why an import rather than a lookup The obvious alternative is a runtime registry lookup answering with a bag of functions: ```js // The shape this does not have. const workspace = native("workspace"); workspace.projectName(); // typo: throws, eventually ``` ```js // The shape it has. import { projectName } from "workspace"; // typo: fails to link ``` It loses twice, and both times on *when* you find out: * **A misspelled export would be a run-time failure.** `workspace.projectName()` type-checks, loads, renders, and then throws on the frame that first reaches it — which, for a name only one branch touches, can be a long way from the edit that caused it. An import is resolved when the module graph is linked, so the same typo stops the application before its first line runs, naming the module and the export. * **The type declarations would have nothing to say.** Only the Host knows what it registered, so a lookup could offer no better than `Record any>` — leaving an application that wants real types to hand-write a `.d.ts` that nothing checks against the registry. A module specifier is a name declarations *can* be written against, so they are [generated from the registry itself](#typing-them) and the typo is red in the editor. What the import does **not** freeze is the function behind the name. Every export is a forwarding stub that resolves through the registry on each call, so withdrawing a module still takes effect immediately: a script holding an imported function gets a refusal, not the withdrawn closure. Only the *set of names* is fixed, at the moment the importing module is linked — which is why a host calls `export_module` **before** it loads an application. ## The registry is the grant The default registry is **empty**, the same shape as `Capabilities::default()`. A host that registers nothing has granted no extension surface, and a script that imports a module is told so by name: ```text HostModule `market` is not available: this Host registered none. HostModule access is granted by the embedding application, with gpui_kit::shell::export_module(...). ``` Register something and the message changes to name what does exist: ```text unknown HostModule `marker`; this Host registered: market, theme ``` ```text HostModule `market` has no function `quote`; it provides: quotes, ticks, watch, watch_all ``` There is deliberately no per-module capability to grant on top of this. The host chose the list, so **the list is the grant** — and revoking one is a matter of exporting a module of the same name, or clearing the set, which takes effect on the next call rather than the next restart. For a multi-application host, each public `Policy` carries its own frozen capabilities and its own module registry — built with `Policy::with_host_module`, one module at a time, the same way. That is how two plugins in one runtime receive different authority without swapping thread-local state across `await` boundaries. Identity and requested system permissions live in `gpui-shell.json`; HostModule registrations do not, because contributions are executable behavior registered by the host. ## Names the runtime keeps A HostModule shares one specifier namespace with the built-in modules and the [Standard Runtime](./engine.md), and the resolver reaches those first. So registering `path` would not shadow the real `path` — it would register a module nothing can ever import, silently. `export_module` refuses such a name instead, and says who owns it: ```text `path` is one of the runtime's own module names and cannot be registered: a script importing it reaches the runtime, never this module. The reserved names are: gpui, gpui-base, gpui-fps, buffer, console, crypto, fs/promises, net, os, path, process, url, websocket, zlib ``` The full list is `gpui_kit::shell::RESERVED_SPECIFIERS`. Everything else is yours — and cannot be shadowed by a file in the application directory either, because HostModule registrations resolve before the application's own files. ## The boundary is plain data A Host function receives `HostArguments` and returns a `HostValue`: null, boolean, number, string, array, or object. Those six cases are the intersection of what a script engine and JSON can both carry, which is what lets one registry serve any engine behind the [seam](./engine.md). It never receives a script handle. A handle would let the host keep a reference to a script value past the call that produced it — and past the call scope that made the surrounding context valid. Arguments come out by position, with the type check and the error message included: | Call | Yields | | ---------------------- | -------------------------------------------------------------------- | | `arguments.string(0)` | `&str`, or an error naming what arrived instead | | `arguments.number(0)` | `f64` | | `arguments.integer(0)` | `i64`, refusing a fractional number | | `arguments.boolean(0)` | `bool` | | `arguments.value(0)` | The raw `HostValue`, for a function that accepts more than one shape | | `arguments.get(0)` | `Option<&HostValue>`, for an optional argument | Returning a record is a builder rather than a map, because an object frequently *is* the row a script renders and insertion order should be the host's to decide: ```rust use gpui_kit::shell::HostObject; HostObject::new() .field("symbol", "AAPL.US") .field("last", 224.22) .field("watched", true) ``` An error is a message, not a type: `HostError::new("no such symbol")` reaches the script as a thrown `Error` the script can catch. ## Three rules a Host function runs under **It must not call back into the script engine.** A host call happens inside a script call, which is inside a host call; re-entering the VM from there would run script code with an engine frame already on the stack, in the middle of a render pass. Holding no script handle makes that hard to express by accident, and the dispatcher refuses a nested call outright so a host that finds another route gets a diagnosable error rather than undefined behavior. **Reading and writing host state is the point.** A function reaches the ambient `App` through `gpui_kit::shell::with_current_app`, which is `None` outside a live call: ```rust fn with_app(read: impl FnOnce(&mut App) -> R) -> Result { gpui_kit::shell::with_current_app(read) .ok_or_else(|| HostError::new("only reachable while a script call is in progress")) } ``` **`cx.notify()` from inside one is delivered after the call unwinds.** So a Host function may mutate an entity and ask the Views watching it to re-render, without that re-render happening underneath the script that called it. ## Work that should not hold the thread `function` is synchronous: it returns a value, and the script gets that value. A slow one holds the thread that renders. `async_function` returns a future instead, and the script gets a promise: ```rust HostModule::new("db") .declarations("export function query(sql: string): Promise;") .async_function("query", |arguments| { // Synchronous half: on the main thread, inside the caller's scope. It // may read host state, and refusing here throws at the call site. let sql = arguments.string(0)?.to_owned(); let pool = with_app(|cx| cx.global::().handle())?; // Asynchronous half: on GPUI's background executor. Ok(async move { Ok(pool.query(&sql).await?.into_host_value()) }) }) ``` ```js import { query } from "db"; const rows = await query("select 1"); ``` ### The split is the design The closure runs on the main thread and returns the future. So the arguments are checked, and whatever the work needs is copied out, while `with_current_app` still answers. The future is then `Send + 'static` and driven elsewhere, where there is no `App` and no script engine to reach for. That is the same rule as [the three above](#three-rules-a-host-function-runs-under), made physical rather than enforced. A synchronous body is held to "do not re-enter the engine" by a run-time guard; an asynchronous one cannot express the violation, because on a background thread there is nothing to re-enter. ### What the script sees * **A refusal from the synchronous half throws at the call site.** `arguments.string(0)?` failing is a `TypeError` where the call was written, not a rejected promise the script has to await to hear about. * **A failure from the future rejects the promise**, carrying `module.function` in the message, so `try`/`catch` around the `await` works normally. * **A cancelled call stays pending for ever.** If the View goes away or its application is reloaded, the continuation never runs and no error is invented for code that was asked to stop — the same answer `cx.sleep` gives. Declare the return type as a `Promise` yourself. The registry checks that the names on both sides agree; it does not read signatures, so nothing catches a declaration that leaves the `Promise` off. ## Typing them A module describes its own TypeScript face, in Rust, beside the registration: ```rust HostModule::new("market") .declarations(r#" /** One row of the board, as it crosses the boundary. */ export interface Quote { symbol: string; last: string; watched: boolean } /** Every row on the board. */ export function quotes(): Quote[]; /** Flips one row's watched flag and answers the new value. */ export function watch(symbol: string): boolean; "#) .function("quotes", /* … */) .function("watch", /* … */) ``` The generated `gpui-kit.d.ts` emits that verbatim inside `declare module "market"`, so `import { quotes } from "market"` is checked exactly the way `import { div } from "gpui-kit"` is. Writing it here rather than in a `.d.ts` beside the script is what keeps the two halves one thing. A `.d.ts` would be a second file, in a second language, with nothing holding it to the registry. `export_module` compares the declared exports with the registered ones and refuses a mismatch: ```text HostModule `market` declares a different set of functions than it registers; registered but not declared: quotes; declared but not registered: prices ``` Renaming a function on one side is now a sentence at start-up rather than an editor that keeps completing a function the host deleted. Declaring nothing is allowed and costs only precision. An undeclared module is emitted with permissive signatures: ```ts declare module "audit" { import { HostValue } from "gpui-kit"; export function observe(...args: HostValue[]): HostValue; } ``` which still checks the module name and every export name — and is honest about the shape, since `HostValue` is exactly what crosses the boundary. `any` would be wider than the runtime: a script passing a function would type-check and then be refused at the call. ## A real one The gallery's Shell story registers one market module, and it is the entire extension surface its script has. Theme values come from `cx.theme()` instead. This is the host side: ```rust fn market_module(market: &Entity) -> HostModule { let read = market.clone(); let flip = market.clone(); HostModule::new("market") .declarations(MARKET_TYPES) .function("quotes", move |_| with_app(|cx| read.read(cx).to_host_value())) .function("watch", move |arguments| { let symbol = arguments.string(0)?; with_app(|cx| { flip.update(cx, |market, cx| { let watched = market.watch(&symbol)?; // Delivered after this call unwinds, so it cannot re-enter // the engine: the story and the script view re-render together. cx.notify(); Ok(HostValue::from(watched)) }) })? }) } gpui_kit::shell::export_module(market_module(&market))?; ``` And this is the script that uses it — the same `Market` entity a Rust panel beside it is rendering from: ```js import { quotes, watch } from "market"; const rows = quotes(); const watched = rows.filter((quote) => quote.watched).length; ``` Run it with `cargo run -- shell`. The two panels read one entity through two paths, which is what makes a mismatch between them visible immediately. ## Not there yet * **Classes and object identity.** A module exports functions. Exporting a class would mean handing the script a live host object, which the plain-data boundary above rules out; a factory function returning a record does the same work today. * **Per-function grants inside one registry.** A policy grants the registry the host assembled; it does not add another permission switch for each function. * **Streaming or callbacks into the host.** A script cannot hand a function to a HostModule; the module can only be called. --- --- url: /zh-CN/shell/host-module.md description: Host 如何把自己的 Rust 借给脚本——注册、脚本侧的 import、纯数据边界,以及 Host function 运行时受到的约束。 --- # HostModule [Capabilities](./capabilities.md) 管的是脚本**不能**碰什么。这一篇讲的是另一半:Host 主动递出去的东西。 脚本无法加载 native 扩展。`dlopen` 进来的 Rust 没有稳定 ABI,而且一旦进了进程,它就持有进程的全部权限——允许这种事的沙箱等于没有沙箱。所以方向是反的:**Host 在编译期注册它愿意暴露的那部分 Rust**,脚本能够到的就只有这些,一点不多。 ```rust use gpui_kit::shell::{HostModule, HostValue}; gpui_kit::shell::export_module( HostModule::new("workspace") .function("project_name", |_| Ok(HostValue::from("gpui-component"))) .function("version", |_| Ok(HostValue::from("0.1.0"))), )?; ``` ```js import { project_name } from "workspace"; project_name(); // "gpui-component" ``` 注册好的模块就是一个普通的 ES module,由解析 `gpui-kit` 和 `path` 的同一个 loader 负责。一次调用注册一个模块,重名会替换掉先前那个而不是合并进去——有三个模块的 Host 就调三次 `export_module`。本页余下的部分讲它的代价和它拒绝的东西。 ## 为什么是 import 而不是查表 显而易见的另一种做法是 runtime registry lookup,返回一包函数: ```js // 这里没有采用的形态。 const workspace = native("workspace"); workspace.projectName(); // 拼错了:迟早会抛 ``` ```js // 实际采用的形态。 import { projectName } from "workspace"; // 拼错了:链接阶段就失败 ``` 它输两次,而且都输在**你什么时候才发现**上: * **导出名拼错了要到运行时才炸。** `workspace.projectName()` 能通过类型检查、能加载、能渲染,然后在第一次真正走到它的那一帧抛出——对于只有某个分支才会碰到的名字,那一帧可能离出错的那次编辑很远。import 在模块图链接时解析,所以同样的拼写错误会在应用跑第一行之前就把它拦住,并指名模块和导出。 * **类型声明什么都说不了。** 只有 Host 知道自己注册了什么,所以 lookup 最多只能给出 `Record any>`;想要真类型的应用只能手写一份 `.d.ts`,而没有任何东西拿它跟 registry 对过账。而 module specifier 是一个类型声明**可以**写在上面的名字,所以声明[直接从 registry 生成](#给它们写类型),拼错在编辑器里就是红的。 import **没有**冻结的是名字背后的那个函数。每个导出都是一个转发桩,每次调用都重新经过注册表,所以撤销一个模块仍然立即生效:脚本手里那个已经 import 进来的函数会得到一次拒绝,而不是那个已被收回的闭包。被固定下来的只有**名字的集合**,固定在 import 它的那个模块被链接的时刻——这也是 Host 必须**先**调用 `export_module`、再加载应用的原因。 ## 注册表本身就是授权 默认注册表是**空的**,和 `Capabilities::default()` 同一个形状。什么都没注册的 Host 就是没有授予任何扩展面,脚本 import 一个模块时会被指名告知: ```text HostModule `market` is not available: this Host registered none. HostModule access is granted by the embedding application, with gpui_kit::shell::export_module(...). ``` 注册了东西之后,消息就变成告诉你有什么: ```text unknown HostModule `marker`; this Host registered: market, theme ``` ```text HostModule `market` has no function `quote`; it provides: quotes, ticks, watch, watch_all ``` 这上面刻意没有再叠一层"每个模块单独授权"。名单是 Host 定的,所以**名单就是授权**——撤销某一项的办法是导出一个同名模块、或者清空整个集合,下一次调用即生效,不必重启。 对于要跑多个应用的 Host,每个公开的 `Policy` 各自带着自己冻结的 capabilities 和自己的模块注册表——用 `Policy::with_host_module` 一个一个加进去,形状和上面一样。这就是同一个 runtime 里的两个插件如何拿到不同权限、而不需要在 `await` 边界上来回换 thread-local 状态。身份和申请的系统权限写在 `gpui-shell.json` 里;HostModule 不在其中,因为它是 Host 注册的可执行行为。 ## runtime 自己留用的名字 HostModule 和内置模块、[Standard Runtime](./engine.md) 共用同一个 specifier 命名空间,而 resolver 先走到后两者。所以注册一个 `path` 并不会遮蔽真正的 `path`——它只会注册一个永远没人能 import 到的模块,而且悄无声息。 `export_module` 直接拒绝这样的名字,并说清楚它归谁: ```text `path` is one of the runtime's own module names and cannot be registered: a script importing it reaches the runtime, never this module. The reserved names are: gpui, gpui-base, gpui-fps, buffer, console, crypto, fs/promises, net, os, path, process, url, websocket, zlib ``` 完整名单是 `gpui_kit::shell::RESERVED_SPECIFIERS`。除此之外的名字都归你——也不会被应用目录里的同名文件遮蔽,因为 HostModule 的解析顺序在应用自己的文件之前。 ## 边界上只有纯数据 Host function 收到的是 `HostArguments`,返回的是 `HostValue`:null、布尔、数字、字符串、数组、对象。这六种是脚本引擎和 JSON 都能承载的交集,也正是同一份注册表能服务[引擎接缝](./engine.md)后面任意引擎的原因。 它永远不会收到脚本句柄。句柄会让 Host 把一个脚本值的引用留到产生它的那次调用之后——也留到那个让周围上下文有效的 call scope 之后。 参数按位置取出,类型检查和错误消息都是现成的: | 调用 | 得到 | | ---------------------- | -------------------------------------------- | | `arguments.string(0)` | `&str`,或一个说明实际来的是什么的错误 | | `arguments.number(0)` | `f64` | | `arguments.integer(0)` | `i64`,拒绝带小数的数字 | | `arguments.boolean(0)` | `bool` | | `arguments.value(0)` | 原始的 `HostValue`,给那些接受多种形状的函数 | | `arguments.get(0)` | `Option<&HostValue>`,给可选参数 | 返回一条记录用的是 builder 而不是 map,因为对象往往**就是**脚本要渲染的那一行,字段顺序应该由 Host 说了算: ```rust use gpui_kit::shell::HostObject; HostObject::new() .field("symbol", "AAPL.US") .field("last", 224.22) .field("watched", true) ``` 错误是一句话,不是一个类型:`HostError::new("no such symbol")` 到了脚本那边就是一个可以 catch 的 `Error`。 ## Host function 的三条规矩 **不许回调进脚本引擎。** 一次 host 调用发生在一次脚本调用里面,而后者又在一次 Host 调用里面;从这里重新进入 VM,就是在引擎栈帧还在、渲染过程还没结束的时候去跑脚本代码。不持有任何脚本句柄让这件事很难被误写出来,而 dispatcher 干脆直接拒绝嵌套调用,这样即使 Host 找到了别的路径,得到的也是一个可诊断的错误而不是未定义行为。 **读写 Host 状态才是重点。** 函数通过 `gpui_kit::shell::with_current_app` 拿到环境里的 `App`,不在一次活跃调用中时它是 `None`: ```rust fn with_app(read: impl FnOnce(&mut App) -> R) -> Result { gpui_kit::shell::with_current_app(read) .ok_or_else(|| HostError::new("only reachable while a script call is in progress")) } ``` **从里面发出的 `cx.notify()` 在调用退栈之后才送达。** 所以 Host function 可以改一个 entity 并请求所有观察它的 View 重渲染,而这次重渲染不会发生在调用它的那段脚本的下面。 ## 不该占住线程的活 `function` 是同步的:它返回一个值,脚本拿到那个值。慢的那种会占住渲染线程。 `async_function` 返回的是一个 future,脚本拿到的是 promise: ```rust HostModule::new("db") .declarations("export function query(sql: string): Promise;") .async_function("query", |arguments| { // 同步的一半:在主线程上,在调用方的 scope 里。可以读 Host 状态, // 在这里拒绝就是在调用点抛出。 let sql = arguments.string(0)?.to_owned(); let pool = with_app(|cx| cx.global::().handle())?; // 异步的一半:在 GPUI 的后台执行器上。 Ok(async move { Ok(pool.query(&sql).await?.into_host_value()) }) }) ``` ```js import { query } from "db"; const rows = await query("select 1"); ``` ### 切成两半就是这个设计本身 闭包在主线程上跑,返回 future。所以参数检查、以及把工作需要的东西复制出来,都发生在 `with_current_app` 还答得上话的时候。之后 future 是 `Send + 'static`,在别处被驱动,那里既没有 `App` 也没有脚本引擎可碰。 这跟[上面那三条](#host-function-的三条规矩)是同一条规矩,只不过从"强制执行"变成了"物理上做不到"。同步的函数体靠一个运行时守卫被摁住"不许重进引擎";异步的那一半根本没法把这件事表达出来,因为后台线程上没有引擎可进。 ### 脚本看到什么 * **同步那一半的拒绝,在调用点抛出。** `arguments.string(0)?` 失败是写下这次调用的地方抛 `TypeError`,而不是一个要 await 才听得到的 rejected promise。 * **future 的失败会 reject 这个 promise**,消息里带着 `module.function`,所以 `await` 外面包 `try`/`catch` 就是正常写法。 * **被取消的调用会永远 pending。** View 消失、或者它的应用被重载,那么续体不会执行,也不会给一段被要求停下来的代码编造一个错误——跟 `cx.sleep` 的答案一致。 返回类型里的 `Promise` 要你自己写。注册表只核对两边的名字,不读签名,所以声明里漏掉 `Promise` 不会被任何东西抓到。 ## 给它们写类型 模块在 Rust 里、紧挨着注册代码,描述自己的 TypeScript 面貌: ```rust HostModule::new("market") .declarations(r#" /** One row of the board, as it crosses the boundary. */ export interface Quote { symbol: string; last: string; watched: boolean } /** Every row on the board. */ export function quotes(): Quote[]; /** Flips one row's watched flag and answers the new value. */ export function watch(symbol: string): boolean; "#) .function("quotes", /* … */) .function("watch", /* … */) ``` 生成的 `gpui-kit.d.ts` 会把这段原样放进 `declare module "market"`,于是 `import { quotes } from "market"` 得到的检查和 `import { div } from "gpui-kit"` 完全一样。 把它写在这里、而不是脚本旁边的 `.d.ts` 里,是让两半保持为一件事的关键。`.d.ts` 会是第二个文件、第二种语言,而且没有任何东西把它绑在注册表上。`export_module` 会拿声明的导出和实际注册的对账,不一致就拒绝: ```text HostModule `market` declares a different set of functions than it registers; registered but not declared: quotes; declared but not registered: prices ``` 现在改了一边的函数名,得到的是启动时的一句话,而不是一个还在不断补全某个 Host 早就删掉的函数的编辑器。 不写声明也可以,代价只是精度。没有声明的模块会以宽松签名生成: ```ts declare module "audit" { import { HostValue } from "gpui-kit"; export function observe(...args: HostValue[]): HostValue; } ``` 模块名和每一个导出名仍然是被检查的——而且这个形状是诚实的,因为跨越边界的东西正好就是 `HostValue`(脚本这边这个类型,就是 Rust 那边的同名类型)。写成 `any` 会比运行时更宽:脚本传一个函数过来能通过类型检查,然后在调用时被拒绝。 ## 一个真实的例子 Gallery 的 Shell story 注册了一个 market 模块,这就是它那段脚本拥有的全部扩展面。主题值走的是 `cx.theme()`。 Host 侧长这样: ```rust fn market_module(market: &Entity) -> HostModule { let read = market.clone(); let flip = market.clone(); HostModule::new("market") .declarations(MARKET_TYPES) .function("quotes", move |_| with_app(|cx| read.read(cx).to_host_value())) .function("watch", move |arguments| { let symbol = arguments.string(0)?; with_app(|cx| { flip.update(cx, |market, cx| { let watched = market.watch(&symbol)?; // 在这次调用退栈之后才送达,所以它不会重新进入引擎: // story 和脚本 View 会一起重渲染。 cx.notify(); Ok(HostValue::from(watched)) }) })? }) } gpui_kit::shell::export_module(market_module(&market))?; ``` 用它的脚本是这样——读的是旁边那个 Rust 面板正在渲染的同一个 `Market` entity: ```js import { quotes, watch } from "market"; const rows = quotes(); const watched = rows.filter((quote) => quote.watched).length; ``` `cargo run -- shell` 跑起来。两个面板通过两条路径读同一个 entity,一旦对不上就会立刻看出来。 ## 还没有的东西 * **类和对象身份。** 模块导出的是函数。导出一个类意味着把一个活的 Host 对象交给脚本,这被上面那条纯数据边界排除了;今天用一个返回记录的工厂函数就能做同样的事。 * **同一注册表内的按函数授权。** policy 授予的是 Host 组装好的那个注册表,不会再为每个函数加一个开关。 * **向 Host 流式传输或回调。** 脚本不能把函数交给 HostModule;模块只能被调用。 --- --- url: /base/primitives/hover-card.md description: A delayed floating card associated with a pointer or keyboard trigger. --- # Hover Card A delayed floating card associated with a pointer or keyboard trigger. Like every `gpui-base` primitive, Hover Card supplies behavior and semantic structure without imposing a product visual language. Apply GPUI styles and compose the exported parts to match your design system. ## Example The [single native Cargo entrypoint](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/components.rs) selects this primitive from the [shared showcase implementation](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/mod.rs). The same showcase is compiled once for the WASM preview above. ```bash cargo run -p gpui-base --example components -- hover-card ``` ## Import ```rust use gpui_kit::base::{HoverCard}; ``` ## Anatomy and API The example composes `HoverCard`. GPUI's standard styling and event traits provide presentation; these base types provide the interaction structure. The authoritative module is [`components/hover_card.rs`](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/components/hover_card.rs). Native and browser previews compile this same file. ## State and events Pointer or focus entry schedules opening and exit schedules dismissal. Keep controlled state on the parent render type or in a GPUI entity. Update it in callbacks and call `cx.notify()`; do not recreate persistent entities during every render. ## Complete Rust example The complete implementation used by the runnable showcase is embedded directly from Rust source: The command above supplies application initialization, window creation, and shared `BaseShowcase` state. ## Accessibility Expose it from keyboard focus and duplicate essential information outside hover-only content. ## Notes Use stable element IDs where accepted. Verify focus, hover, active, selected, disabled, reduced-motion, and high-contrast appearances in the consuming design system. --- --- url: /zh-CN/base/primitives/hover-card.md description: 与指针或键盘触发器关联的延迟浮动卡片。 --- # Hover Card 与指针或键盘触发器关联的延迟浮动卡片。 和所有 GPUI Base 原语一样,Hover Card 只提供行为和语义结构,不规定产品视觉语言。请使用 GPUI 样式并组合导出的部件,使其符合你的设计系统。 ## 示例 原生示例和页面上方的 WASM 预览共用同一份实现: ```bash cargo run -p gpui-base --example components -- hover-card ``` ## 导入 ```rust use gpui_kit::base::{HoverCard}; ``` ## 结构与 API 示例组合上述公开类型。GPUI 的标准样式和事件 trait 负责表现,Base 类型负责交互结构。权威实现位于 [`components/hover-card.rs`](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/components/hover-card.rs),原生与浏览器预览编译的是同一文件。 ## 状态与事件 悬停或聚焦触发器后延迟打开,离开后关闭。 受控状态应保存在父渲染类型或 GPUI entity 中;在回调中更新并调用 `cx.notify()`,不要在每次渲染时重建持久 entity。 ## 完整 Rust 示例 ## 可访问性 不要把完成任务所必需的操作只放在 Hover Card 中;键盘焦点也应能触发。 ## 注意事项 在支持的位置使用稳定元素 ID,并在消费端设计系统中验证焦点、悬停、按下、选中、禁用、减少动态效果和高对比度状态。 --- --- url: /docs/components/hover-card.md description: >- A floating overlay that displays rich content when hovering over a trigger element. --- # HoverCard HoverCard component for displaying rich content that appears when the mouse hovers over a trigger element. Ideal for previewing user profiles, link previews, and other contextual information without requiring a click. Features configurable delays for both opening and closing to prevent flickering during quick mouse movements. This is most like the [Popover] component, but triggered by hover instead of click, and with timing controls for a smoother user experience. ## Import ```rust use gpui_kit::component::hover_card::HoverCard; ``` ## Usage ### Basic HoverCard ```rust use gpui_kit::{ParentElement as _, Styled as _}; use gpui_kit::component::{hover_card::HoverCard, v_flex}; HoverCard::new("basic") .trigger( div() .child("Hover over me") .text_color(cx.theme().primary) .cursor_pointer() .text_sm() ) .child( v_flex() .gap_2() .child( div() .child("This is a hover card") .font_semibold() .text_sm() ) .child( div() .child("You can display rich content when hovering over a trigger element.") .text_color(cx.theme().muted_foreground) .text_sm() ) ) ``` ### User Profile Preview A common use case is showing user profiles when hovering over a username, similar to GitHub or Twitter: ```rust use gpui_kit::{px, relative, Styled as _}; use gpui_kit::component::{ avatar::Avatar, hover_card::HoverCard, h_flex, v_flex, }; h_flex() .child("Hover over ") .text_sm() .child( HoverCard::new("user-profile") .trigger( div() .child("@huacnlee") .cursor_pointer() .text_color(cx.theme().link) ) .child( h_flex() .w(px(320.)) .gap_4() .items_start() .child( Avatar::new() .src("https://avatars.githubusercontent.com/u/5518?s=64") ) .child( v_flex() .gap_1() .line_height(relative(1.)) .child(div().child("Jason Lee").font_semibold()) .child( div() .child("@huacnlee") .text_color(cx.theme().muted_foreground) .text_sm() ) .child("The author of GPUI Kit.") ) ) ) .child(" to see their profile") ``` ### Custom Timing Adjust the opening and closing delays to suit your needs: ```rust use std::time::Duration; use gpui_kit::Styled as _; use gpui_kit::component::{ button::{Button, ButtonVariants as _}, h_flex, }; h_flex() .gap_4() .child( HoverCard::new("fast-open") .open_delay(Duration::from_millis(200)) .close_delay(Duration::from_millis(100)) .trigger(Button::new("fast").label("Fast Open (200ms)").outline()) .child(div().child("This hover card opens after 200ms").text_sm()) ) .child( HoverCard::new("slow-open") .open_delay(Duration::from_secs(1)) .close_delay(Duration::from_secs_f32(0.5)) .trigger(Button::new("slow").label("Slow Open (1000ms)").outline()) .child(div().child("This hover card opens after 1000ms").text_sm()) ) ``` ### Positioning HoverCard supports 6 positioning options using the [Anchor] type: * TopLeft * TopCenter * TopRight * BottomLeft * BottomCenter * BottomRight Imagine the card has a pointer tip (like a speech bubble's tail). The anchor is where that tip sits relative to the trigger — `TopCenter` places it at the trigger's top center, `BottomRight` at the bottom-right, and so on. The card then hangs off that point. For example, `Anchor::TopLeft` places the card just below the trigger, left-aligned to it: ```text [ Trigger ] ┌──────────────┐ │ Hover Card │ └──────────────┘ ``` ### Custom Content Builder For performance optimization, you can provide a content builder function for more complex case, which only calls when the HoverCard is opened: ```rust HoverCard::new("complex") .trigger(Button::new("btn").label("Hover me")) .content(|state, window, cx| { v_flex() .child("Dynamic content") .child(format!("Open: {}", state.is_open())) }) ``` ### Styling HoverCard inherits all `Styled` trait methods: ```rust HoverCard::new("styled") .trigger(Button::new("btn").label("Styled")) .w(px(400.)) .max_h(px(500.)) .text_sm() .gap_2() .child("Styled content") ``` Disable default appearance and apply custom styles: ```rust HoverCard::new("custom-styled") .appearance(false) // Disable default popover styling .trigger(Button::new("btn").label("Custom")) .bg(cx.theme().background) .border_2() .border_color(cx.theme().primary) .rounded(px(12.)) .p_4() .child("Custom styled content") ``` ## API Reference ### HoverCard Methods * `new(id: impl Into)` - Create a new HoverCard with a unique ID * `trigger(trigger: T)` - Set the element that triggers the hover * `content(content: F)` - Set a content builder function that receives `(&mut HoverCardState, &mut Window, &mut Context)` * `open_delay(duration: Duration)` - Set delay before showing (default: 600ms) * `close_delay(duration: Duration)` - Set delay before hiding (default: 300ms) * `anchor(anchor: impl Into)` - Set positioning (default: TopCenter) * `on_open_change(callback: F)` - Callback when open state changes, receives `(&bool, &mut Window, &mut App)` * `appearance(appearance: bool)` - Enable/disable default styling (default: true) ### HoverCardState Methods * `is_open() -> bool` - Check if the hover card is currently open ## Behavior Details ### Hover Timing The HoverCard uses a sophisticated timing system to provide a smooth user experience: 1. **Open Delay (600ms default)**: Prevents the card from flickering when the mouse quickly passes over the trigger 2. **Close Delay (300ms default)**: Gives users time to move their mouse from the trigger to the content area without the card closing 3. **Interactive Content**: Users can move their mouse into the content area, and the card will remain open as long as the mouse is either on the trigger or in the content ### Edge Cases Handled * **Quick Mouse Sweep**: If the mouse quickly moves across the trigger, the card won't open (cancelled by the open delay) * **Trigger to Content Movement**: The card stays open when moving the mouse from the trigger to the content area * **Rapid Hovering**: Multiple rapid hover events are debounced using an epoch-based timer system * **Multiple HoverCards**: Each HoverCard has independent state, so multiple cards can coexist without interfering ## Best Practices 1. **Use appropriate delays**: * Standard content: 600ms open, 300ms close * Quick previews: 500ms open, 200ms close * Tooltips: 300ms open, 100ms close 2. **Keep content concise**: HoverCards should provide preview information, not full content 3. **Make triggers visually distinct**: Use colors, underlines, or cursor changes to indicate hoverable elements 4. **Consider accessibility**: HoverCards are visual-only and don't support keyboard navigation. For keyboard-accessible content, consider using a Popover instead 5. **Avoid nested HoverCards**: They can create confusing user experiences ## Differences from [Popover] | Feature | HoverCard | Popover | | ------------------------ | ---------------- | ------------------ | | Trigger | Mouse hover | Click/right-click | | Keyboard navigation | No | Yes (with focus) | | Dismiss on outside click | No | Yes (configurable) | | Timing delays | Yes (open/close) | No | | Primary use case | Previews | Actions/forms | [Popover]: ./popover.md [Anchor]: https://docs.rs/gpui-component/latest/gpui_component/enum.Anchor.html [Avatar]: ./avatar.md --- --- url: /zh-CN/docs/components/hover-card.md description: 鼠标悬停时显示富内容浮层的组件。 --- # HoverCard HoverCard 用于在鼠标悬停到触发元素时显示富内容浮层,适合做用户资料预览、链接预览和上下文信息展示。它支持打开和关闭延迟,以减少鼠标快速移动时的闪烁问题。 它和 [Popover] 很像,但触发方式是 hover 而不是 click,并且提供了更细的时间控制。 ## 导入 ```rust use gpui_kit::component::hover_card::HoverCard; ``` ## 用法 ### 基础 HoverCard ```rust use gpui_kit::{ParentElement as _, Styled as _}; use gpui_kit::component::{hover_card::HoverCard, v_flex}; HoverCard::new("basic") .trigger( div() .child("Hover over me") .text_color(cx.theme().primary) .cursor_pointer() .text_sm() ) .child( v_flex() .gap_2() .child( div() .child("This is a hover card") .font_semibold() .text_sm() ) .child( div() .child("You can display rich content when hovering over a trigger element.") .text_color(cx.theme().muted_foreground) .text_sm() ) ) ``` ### 用户资料预览 这是一个很常见的场景,类似 GitHub 或 Twitter 的用户悬停预览: ```rust use gpui_kit::{px, relative, Styled as _}; use gpui_kit::component::{ avatar::Avatar, hover_card::HoverCard, h_flex, v_flex, }; h_flex() .child("Hover over ") .text_sm() .child( HoverCard::new("user-profile") .trigger( div() .child("@huacnlee") .cursor_pointer() .text_color(cx.theme().link) ) .child( h_flex() .w(px(320.)) .gap_4() .items_start() .child( Avatar::new() .src("https://avatars.githubusercontent.com/u/5518?s=64") ) .child( v_flex() .gap_1() .line_height(relative(1.)) .child(div().child("Jason Lee").font_semibold()) .child( div() .child("@huacnlee") .text_color(cx.theme().muted_foreground) .text_sm() ) .child("The author of GPUI Kit.") ) ) ) .child(" to see their profile") ``` ### 自定义时间控制 你可以按需调整打开和关闭延迟: ```rust use std::time::Duration; use gpui_kit::Styled as _; use gpui_kit::component::{ button::{Button, ButtonVariants as _}, h_flex, }; h_flex() .gap_4() .child( HoverCard::new("fast-open") .open_delay(Duration::from_millis(200)) .close_delay(Duration::from_millis(100)) .trigger(Button::new("fast").label("Fast Open (200ms)").outline()) .child(div().child("This hover card opens after 200ms").text_sm()) ) .child( HoverCard::new("slow-open") .open_delay(Duration::from_secs(1)) .close_delay(Duration::from_secs_f32(0.5)) .trigger(Button::new("slow").label("Slow Open (1000ms)").outline()) .child(div().child("This hover card opens after 1000ms").text_sm()) ) ``` ### 定位 HoverCard 支持通过 [Anchor] 设置 6 种定位: * TopLeft * TopCenter * TopRight * BottomLeft * BottomCenter * BottomRight 可以把卡片想象成有一个箭头尖角(像对话气泡的小三角)。anchor 指的就是这个尖角相对于触发器落在哪个点——`TopCenter` 把它放在触发器的顶部中间,`BottomRight` 放在右下角,以此类推。卡片就从这个点挂出来。 例如 `Anchor::TopLeft` 会让卡片出现在触发器正下方,并与其左对齐: ```text [ Trigger ] ┌──────────────┐ │ Hover Card │ └──────────────┘ ``` ### 使用动态内容构建器 对于较复杂的内容,可以使用 `content` builder,在 HoverCard 打开时再生成内容: ```rust HoverCard::new("complex") .trigger(Button::new("btn").label("Hover me")) .content(|state, window, cx| { v_flex() .child("Dynamic content") .child(format!("Open: {}", state.is_open())) }) ``` ### 样式 HoverCard 继承了 `Styled` trait 的所有方法: ```rust HoverCard::new("styled") .trigger(Button::new("btn").label("Styled")) .w(px(400.)) .max_h(px(500.)) .text_sm() .gap_2() .child("Styled content") ``` 关闭默认外观并自定义样式: ```rust HoverCard::new("custom-styled") .appearance(false) .trigger(Button::new("btn").label("Custom")) .bg(cx.theme().background) .border_2() .border_color(cx.theme().primary) .rounded(px(12.)) .p_4() .child("Custom styled content") ``` ## API 参考 ### HoverCard 方法 * `new(id: impl Into)`:创建一个新的 HoverCard * `trigger(trigger: T)`:设置触发元素 * `content(content: F)`:设置内容构建器 * `open_delay(duration: Duration)`:设置显示延迟,默认 600ms * `close_delay(duration: Duration)`:设置隐藏延迟,默认 300ms * `anchor(anchor: impl Into)`:设置定位,默认 `TopCenter` * `on_open_change(callback: F)`:打开状态变化回调 * `appearance(appearance: bool)`:是否启用默认样式,默认 `true` ### HoverCardState 方法 * `is_open() -> bool`:判断当前是否打开 ## 行为细节 ### Hover 时间控制 HoverCard 的时间控制主要解决悬停交互中的抖动问题: 1. **Open Delay**:防止鼠标快速扫过时意外打开 2. **Close Delay**:允许用户从 trigger 移动到内容区域时不立刻关闭 3. **Interactive Content**:只要鼠标在 trigger 或内容区域内,浮层就保持打开 ### 已处理的边界场景 * **快速划过**:因为有打开延迟,不会误触发 * **从触发器移动到内容**:浮层不会马上关闭 * **频繁悬停**:通过基于 epoch 的定时器机制做了去抖 * **多个 HoverCard 同时存在**:每个 HoverCard 都维护独立状态,互不影响 ## 最佳实践 1. 为不同场景设置合适的延迟。 2. HoverCard 适合预览信息,不适合承载完整流程。 3. 让触发器具备清晰的可悬停视觉反馈。 4. HoverCard 不支持键盘导航;如需键盘可达性,优先考虑 Popover。 5. 尽量避免嵌套 HoverCard,以免交互混乱。 ## 与 [Popover] 的区别 | 特性 | HoverCard | Popover | | ------------------------ | ---------------- | ------------------ | | 触发方式 | 鼠标悬停 | 点击或右键 | | 键盘导航 | 不支持 | 支持 | | 点击外部关闭 | 不适用 | 支持,可配置 | | 时间延迟 | 支持 | 不支持 | | 主要用途 | 预览信息 | 操作和表单 | [Popover]: ./popover.md [Anchor]: https://docs.rs/gpui-component/latest/gpui_component/enum.Anchor.html [Avatar]: ./avatar.md --- --- url: /docs/i18n.md description: Add or override GPUI Component translations from your application. --- # I18n GPUI Component includes translations for its components, currently `en`, `zh-CN`, and `zh-HK`. Applications can add a language or override individual translations without copying the complete built-in locale files. This feature requires `rust-i18n` 4.2 or later. ## Add the dependency Add `rust-i18n` to the application crate that owns your locale files: ```toml [dependencies] gpui-kit = "0.6" rust-i18n = "4.2" ``` ## Create an application locale file Create `locales/ui.yml` in your application crate. Put component translations under the `gpui_component` namespace: ```yaml _version: 2 gpui_component: Calendar: week.0: fr: Di month.January: fr: Janvier DatePicker: placeholder: fr: Sélectionner une date ``` The namespace must be `gpui_component`, matching the Rust crate name `gpui-component` with hyphens converted to underscores. Translation keys can be found in [GPUI Component's built-in locale file](https://github.com/longbridge/gpui-kit/blob/main/crates/component/locales/ui.yml). ## Register the extension Initialize the application's locales at the crate root: ```rust rust_i18n::i18n!("locales", fallback = "en"); ``` Register them before initializing GPUI Component: ```rust app.run(move |cx| { rust_i18n::extend!(gpui_component); gpui_kit::init(cx); // Open windows and initialize the rest of the application. }); ``` Call `extend!` only once during application startup. ## Lookup priority The application's translations take priority over the component's built-in translations: ```text Application locales (locales/ui.yml) │ │ key not found ▼ GPUI Component built-in locales ``` This deep-merge behavior means that: * A new locale, such as `fr`, can contain only the keys your application needs. * A translation with the same locale and key overrides the built-in value. * Keys not supplied by the application continue to use the built-in value. * Future GPUI Component translations remain available without being copied into the application. For example, defining only `gpui_component.Calendar.month.January.en` changes January's English label while all other English calendar labels still come from GPUI Component. ### The namespace applies to component lookups only `extend!` changes how GPUI Component resolves *its* keys. It does not give the application's own `t!` calls access to the built-in translations: ```rust // Inside a GPUI Component component: application value first, built-in second. t!("Calendar.month.February") // -> "February" // In application code: reads the application's locale files only. t!("gpui_component.Calendar.month.February") // -> the key, unless you defined it ``` Read component strings by rendering the component, not by looking the key up yourself. ## Select a locale `gpui-component` re-exports the locale accessors, so an application does not have to reach for `rust-i18n` to switch languages: ```rust gpui_kit::component::set_locale("fr"); let current = gpui_kit::component::locale(); ``` Components then resolve their translated labels using the selected locale and the priority described above. The active locale is global state that GPUI does not track, so changing it does not schedule a repaint on its own. A view that displays translated text must notify itself: ```rust gpui_kit::component::set_locale("fr"); cx.notify(); ``` --- --- url: /docs/components/icon.md description: 'Display SVG icons with various sizes, colors, and transformations.' --- # Icon A flexible icon component that renders SVG icons from the built-in icon library. Icons are based on Lucide.dev and support customization of size, color, and rotation. The component requires SVG files to be provided by the user in the assets bundle. Before you start, please make sure you have read: [Icons & Assets](../assets.md) to understand how use SVG in GPUI & GPUI Component application. ## Import ```rust use gpui_kit::component::{Icon, IconName}; ``` ## Usage ### Basic Icon ```rust // Using IconName enum directly IconName::Heart // Or creating an Icon explicitly Icon::new(IconName::Heart) ``` ### Icon with Custom Size ```rust // Predefined sizes Icon::new(IconName::Search).xsmall() // size_3() Icon::new(IconName::Search).small() // size_3p5() Icon::new(IconName::Search).medium() // size_4() (default) Icon::new(IconName::Search).large() // size_6() // Custom pixel size Icon::new(IconName::Search).with_size(px(20.)) ``` ### Icon with Custom Color ```rust // Using theme colors Icon::new(IconName::Heart) .text_color(cx.theme().red) // Using custom colors Icon::new(IconName::Star) .text_color(gpui_kit::red()) ``` ### Rotated Icons ```rust use gpui_kit::Radians; // Rotate by radians Icon::new(IconName::ArrowUp) .rotate(Radians::from_degrees(90.)) // Transform with custom transformation Icon::new(IconName::ChevronRight) .transform(Transformation::rotate(Radians::PI)) ``` ### Custom SVG Path ```rust // Using a custom SVG file from assets Icon::new(Icon::empty()) .path("icons/my-custom-icon.svg") ``` ## Available Icons The `IconName` enum provides access to a curated set of icons. Here are some commonly used ones: ### Navigation * `ArrowUp`, `ArrowDown`, `ArrowLeft`, `ArrowRight` * `ChevronUp`, `ChevronDown`, `ChevronLeft`, `ChevronRight` * `ChevronsUpDown` ### Actions * `Check`, `Close`, `Plus`, `Minus` * `Copy`, `Delete`, `Search`, `Replace` * `Maximize`, `Minimize`, `WindowRestore` ### Files & Folders * `File`, `Folder`, `FolderOpen`, `FolderClosed` * `BookOpen`, `Inbox` ### UI Elements * `Menu`, `Settings`, `Settings2`, `Ellipsis`, `EllipsisVertical` * `Eye`, `EyeOff`, `Bell`, `Info` ### Social & External * `GitHub`, `Globe`, `ExternalLink` * `Heart`, `HeartOff`, `Star`, `StarOff` * `ThumbsUp`, `ThumbsDown` ### Status & Alerts * `CircleCheck`, `CircleX`, `TriangleAlert` * `Loader`, `LoaderCircle` ### Panels & Layout * `PanelLeft`, `PanelRight`, `PanelBottom` * `PanelLeftOpen`, `PanelRightOpen`, `PanelBottomOpen` * `LayoutDashboard`, `Frame` ### Users & Profile * `User`, `CircleUser`, `Bot` ### Other * `Calendar`, `Map`, `Palette`, `Inspector` * `Sun`, `Moon`, `Building2` ## Icon Sizes The Icon component supports several predefined sizes: | Size | Method | CSS Class | Pixels | | ----------- | --------------------- | ------------ | ------ | | Extra Small | `.xsmall()` | `size_3()` | 12px | | Small | `.small()` | `size_3p5()` | 14px | | Medium | `.medium()` (default) | `size_4()` | 16px | | Large | `.large()` | `size_6()` | 24px | | Custom | `.with_size(px(n))` | - | n px | ## Build you own `IconName`. You can define your own `IconName` to have more specific icons for your application. We have `IconNamed` trait for you to implement for your. ```rust use gpui_kit::component::IconNamed; pub enum IconName { Encounters, Monsters, Spells, } impl IconNamed for IconName { fn path(self) -> gpui_kit::SharedString { match self { IconName::Encounters => "icons/encounters.svg", IconName::Monsters => "icons/monsters.svg", IconName::Spells => "icons/spells.svg", } .into() } } // This allows for the following interactions (works with anything that has the `.icon(icon)` method. Button::new("my-button").icon(IconName::Spells); Icon::new(IconName::Monsters); ``` If you want to directly `render` a custom `IconName` you must implement the `RenderOnce` trait and derive `IntoElement` on the `IconName`. ```rust impl RenderOnce for IconName { fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement { Icon::empty().path(self.path()) } } // Now you can use it directly in your element tree: div() .child(IconName::Monsters) ``` ## Examples ### Icon in Button ```rust use gpui_kit::component::button::Button; Button::new("like-btn") .icon( Icon::new(IconName::Heart) .text_color(cx.theme().red) .large() ) .label("Like") ``` ### Animated Loading Icon ```rust Icon::new(IconName::LoaderCircle) .text_color(cx.theme().muted_foreground) .medium() // Add rotation animation in your render logic ``` ### Status Icons ```rust // Success Icon::new(IconName::CircleCheck) .text_color(cx.theme().green) // Error Icon::new(IconName::CircleX) .text_color(cx.theme().red) // Warning Icon::new(IconName::TriangleAlert) .text_color(cx.theme().yellow) ``` ### Navigation Icons ```rust // Back button Icon::new(IconName::ArrowLeft) .medium() .text_color(cx.theme().foreground) // Dropdown indicator Icon::new(IconName::ChevronDown) .small() .text_color(cx.theme().muted_foreground) ``` ### Custom Icon from Assets ```rust // Using a custom SVG file Icon::empty() .path("icons/my-brand-logo.svg") .large() .text_color(cx.theme().primary) ``` ## Notes * Icons are rendered as SVG elements and support full CSS styling * The default size matches the current text size if no explicit size is set * Icons are flex-shrink-0 by default to prevent unwanted shrinking in flex layouts * All icon paths are relative to the assets bundle root * Icons from Lucide.dev are designed to work well at 16px and scale nicely to other sizes --- --- url: /zh-CN/docs/assets.md --- # Icon GPUI Component 中的 [IconName] 和 [Icon] 提供了一套可直接在 GPUI 应用中使用的图标接口。 但为了尽量减小应用体积,`gpui-component` 默认 **不会内置任何图标资源**。 因此仓库把图标资源拆分到了独立的 [gpui-kit-assets] crate 中。这样你可以自行决定: * 直接使用默认内置图标资源 * 完全不引入图标资源 * 自己维护一套 SVG 资源 ## 使用默认内置资源 [gpui-kit-assets] 提供了一个默认的资源实现,包含 `assets/icons` 目录下的全部图标文件。 如果要使用默认资源,需要在 `Cargo.toml` 中添加: ```toml [dependencies] gpui-component = { git = "https://github.com/longbridge/gpui-kit" } gpui-kit-assets = { git = "https://github.com/longbridge/gpui-kit" } ``` 然后在创建 GPUI 应用时,通过 `with_assets` 注册资源源: ```rs use gpui_kit::*; use gpui_kit::assets::Assets; let app = gpui_kit::application().with_assets(Assets); ``` 完成后,你就可以像平常一样使用 `IconName` 和 `Icon`。这些图标会从默认打包资源中读取。 继续阅读下面的 [使用图标](#使用图标) 小节查看实际示例。 ## 自定义资源 如果你只想带上一小部分图标,或者希望使用项目自己的 SVG 资源,可以自己构建资源源。 仓库中的 [assets] 目录包含了目前支持的全部 SVG 图标文件,文件名与 [IconName] 枚举一一对应。 你可以: * 直接从 [assets] 目录拷贝需要的 SVG * 或按 [IconName] 的命名规则准备自己的 SVG 文件 在 GPUI 应用中,通常可以结合 [rust-embed] 将这些 SVG 嵌入可执行文件,并通过 `AssetSource` 提供加载能力。 ```rs use anyhow::anyhow; use gpui_kit::*; use gpui_kit::component::{v_flex, IconName, Root}; use rust_embed::RustEmbed; use std::borrow::Cow; /// An asset source that loads assets from the `./assets` folder. #[derive(RustEmbed)] #[folder = "./assets"] #[include = "icons/**/*.svg"] pub struct Assets; impl AssetSource for Assets { fn load(&self, path: &str) -> Result>> { if path.is_empty() { return Ok(None); } Self::get(path) .map(|f| Some(f.data)) .ok_or_else(|| anyhow!("could not find asset at path \"{path}\"")) } fn list(&self, path: &str) -> Result> { Ok(Self::iter() .filter_map(|p| p.starts_with(path).then(|| p.into())) .collect()) } } ``` 同样需要在创建应用时调用 `with_assets`: ```rs fn main() { // Register Assets to GPUI application. let app = gpui_kit::application().with_assets(Assets); app.run(move |cx| { // We must initialize gpui_component before using it. gpui_kit::init(cx); cx.spawn(async move |cx| { cx.open_window(WindowOptions::default(), |window, cx| { let view = cx.new(|_| Example); // The first level on the window must be Root. cx.new(|cx| Root::new(view, window, cx)) }) .expect("Failed to open window"); }) .detach(); }); } ``` ## 使用图标 完成资源注册后,就可以在应用中直接使用图标: ```rs pub struct Example; impl Render for Example { fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { v_flex() .gap_2() .size_full() .items_center() .justify_center() .text_center() .child(IconName::Inbox) .child(IconName::Bot) } } ``` ## 参考资源 * [Lucide Icons](https://lucide.dev/) - GPUI Component 的图标集主要基于 Lucide 开源图标库 [rust-embed]: https://docs.rs/rust-embed/latest/rust_embed/ [IconName]: https://docs.rs/gpui_component/latest/gpui_component/icon/enum.IconName.html [Icon]: https://docs.rs/gpui_component/latest/gpui_component/icon/struct.Icon.html [assets]: https://github.com/longbridge/gpui-kit/tree/main/crates/assets/assets/ [gpui-kit-assets]: https://crates.io/crates/gpui-kit-assets --- --- url: /zh-CN/docs/components/icon.md description: 以不同尺寸、颜色和变换方式显示 SVG 图标。 --- # Icon Icon 是一个灵活的图标组件,用于渲染内置图标库中的 SVG 图标。图标基于 Lucide.dev,并支持尺寸、颜色与旋转等定制。组件依赖你在资源包中提供对应的 SVG 文件。 在开始之前,建议先阅读 [Icons & Assets](../assets.md),了解如何在 GPUI 与 GPUI Component 应用中使用 SVG。 ## 导入 ```rust use gpui_kit::component::{Icon, IconName}; ``` ## 用法 ### 基础图标 ```rust IconName::Heart Icon::new(IconName::Heart) ``` ### 自定义尺寸 ```rust Icon::new(IconName::Search).xsmall() Icon::new(IconName::Search).small() Icon::new(IconName::Search).medium() Icon::new(IconName::Search).large() Icon::new(IconName::Search).with_size(px(20.)) ``` ### 自定义颜色 ```rust Icon::new(IconName::Heart) .text_color(cx.theme().red) Icon::new(IconName::Star) .text_color(gpui_kit::red()) ``` ### 旋转图标 ```rust use gpui_kit::Radians; Icon::new(IconName::ArrowUp) .rotate(Radians::from_degrees(90.)) Icon::new(IconName::ChevronRight) .transform(Transformation::rotate(Radians::PI)) ``` ### 自定义 SVG 路径 ```rust Icon::new(Icon::empty()) .path("icons/my-custom-icon.svg") ``` ## 可用图标 `IconName` 枚举内置了一组常见图标: ### 导航 * `ArrowUp`、`ArrowDown`、`ArrowLeft`、`ArrowRight` * `ChevronUp`、`ChevronDown`、`ChevronLeft`、`ChevronRight` * `ChevronsUpDown` ### 操作 * `Check`、`Close`、`Plus`、`Minus` * `Copy`、`Delete`、`Search`、`Replace` * `Maximize`、`Minimize`、`WindowRestore` ### 文件与文件夹 * `File`、`Folder`、`FolderOpen`、`FolderClosed` * `BookOpen`、`Inbox` ### UI 元素 * `Menu`、`Settings`、`Settings2`、`Ellipsis`、`EllipsisVertical` * `Eye`、`EyeOff`、`Bell`、`Info` ### 社交与外链 * `GitHub`、`Globe`、`ExternalLink` * `Heart`、`HeartOff`、`Star`、`StarOff` * `ThumbsUp`、`ThumbsDown` ### 状态与提醒 * `CircleCheck`、`CircleX`、`TriangleAlert` * `Loader`、`LoaderCircle` ### 面板与布局 * `PanelLeft`、`PanelRight`、`PanelBottom` * `PanelLeftOpen`、`PanelRightOpen`、`PanelBottomOpen` * `LayoutDashboard`、`Frame` ### 用户与身份 * `User`、`CircleUser`、`Bot` ### 其它 * `Calendar`、`Map`、`Palette`、`Inspector` * `Sun`、`Moon`、`Building2` ## 图标尺寸 | 尺寸 | 方法 | CSS Class | 像素 | | ----------- | --------------------- | ------------ | ------ | | 超小 | `.xsmall()` | `size_3()` | 12px | | 小 | `.small()` | `size_3p5()` | 14px | | 中 | `.medium()` | `size_4()` | 16px | | 大 | `.large()` | `size_6()` | 24px | | 自定义 | `.with_size(px(n))` | - | n px | ## 自定义 `IconName` 如果你需要更贴合业务的图标命名,可以自己定义 `IconName` 并实现 `IconNamed` trait。 ```rust use gpui_kit::component::IconNamed; pub enum IconName { Encounters, Monsters, Spells, } impl IconNamed for IconName { fn path(self) -> gpui_kit::SharedString { match self { IconName::Encounters => "icons/encounters.svg", IconName::Monsters => "icons/monsters.svg", IconName::Spells => "icons/spells.svg", } .into() } } Button::new("my-button").icon(IconName::Spells); Icon::new(IconName::Monsters); ``` 如果你希望在元素树中直接 `render` 自定义 `IconName`,还需要实现 `RenderOnce` 并为 `IconName` 派生 `IntoElement`: ```rust impl RenderOnce for IconName { fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement { Icon::empty().path(self.path()) } } div() .child(IconName::Monsters) ``` ## 示例 ### 按钮中的图标 ```rust use gpui_kit::component::button::Button; Button::new("like-btn") .icon( Icon::new(IconName::Heart) .text_color(cx.theme().red) .large() ) .label("Like") ``` ### 旋转加载图标 ```rust Icon::new(IconName::LoaderCircle) .text_color(cx.theme().muted_foreground) .medium() ``` ### 状态图标 ```rust Icon::new(IconName::CircleCheck) .text_color(cx.theme().green) Icon::new(IconName::CircleX) .text_color(cx.theme().red) Icon::new(IconName::TriangleAlert) .text_color(cx.theme().yellow) ``` ### 导航图标 ```rust Icon::new(IconName::ArrowLeft) .medium() .text_color(cx.theme().foreground) Icon::new(IconName::ChevronDown) .small() .text_color(cx.theme().muted_foreground) ``` ### 来自资源包的自定义图标 ```rust Icon::empty() .path("icons/my-brand-logo.svg") .large() .text_color(cx.theme().primary) ``` ## 说明 * 图标以 SVG 形式渲染,可使用完整的样式能力。 * 如果未显式指定尺寸,默认尺寸会跟随当前文字大小。 * 图标默认带有 `flex-shrink-0`,避免在 Flex 布局中被意外压缩。 * 所有图标路径都相对于 assets bundle 根目录。 * Lucide.dev 图标在 16px 下效果最佳,并且在其它尺寸下也有良好缩放表现。 --- --- url: /docs/assets.md --- # Icons & Assets The [IconName] and [Icon] in GPUI Component provide a comprehensive set of icons and assets that can be easily integrated into your GPUI applications. But for minimal size applications, **we have not embedded any icon assets by default** in `gpui-component` crate. We split the icon assets into a separate crate [gpui-kit-assets] to allow developers to choose whether to include the icon assets in their applications or if you don't need the icons at all, you can build your own assets. ## Use default bundled assets The [gpui-kit-assets] crate provides a default bundled assets implementation that includes all the icon files in the `assets/icons` folder. To use the default bundled assets, you need to add the `gpui-kit-assets` crate as a dependency in your `Cargo.toml`: ```toml [dependencies] gpui-component = { git = "https://github.com/longbridge/gpui-kit" } gpui-kit-assets = { git = "https://github.com/longbridge/gpui-kit" } ``` Then we need call the `with_assets` method when creating the GPUI application to register the asset source: ```rs use gpui_kit::*; use gpui_kit::assets::Assets; let app = gpui_kit::application().with_assets(Assets); ``` Now, we can use `IconName` and `Icon` in our application as usual, the all icon assets are loaded from the default bundled assets. Continue [Use the icons](#use-the-icons) section to see how to use the icons in your application. ## Build you own assets You may have a specific set of icons that you want to use in your application, or you may want to reduce the size of your application binary by including only the icons you need. In this case, you can build your own assets by following these steps. The [assets](https://github.com/longbridge/gpui-kit/tree/main/crates/assets/assets/) folder in source code contains all the available icons in SVG format, every file is that GPUI Component support, it matched with the [IconName] enum. You can download the SVG files you need from the [assets] folder, or you can use your own SVG files by following the [IconName] naming convention. In GPUI application, we can use the [rust-embed] crate to embed the SVG files into the application binary. And GPUI Application providers an `AssetSource` trait to load the assets. ```rs use anyhow::anyhow; use gpui_kit::*; use gpui_kit::component::{v_flex, IconName, Root}; use rust_embed::RustEmbed; use std::borrow::Cow; /// An asset source that loads assets from the `./assets` folder. #[derive(RustEmbed)] #[folder = "./assets"] #[include = "icons/**/*.svg"] pub struct Assets; impl AssetSource for Assets { fn load(&self, path: &str) -> Result>> { if path.is_empty() { return Ok(None); } Self::get(path) .map(|f| Some(f.data)) .ok_or_else(|| anyhow!("could not find asset at path \"{path}\"")) } fn list(&self, path: &str) -> Result> { Ok(Self::iter() .filter_map(|p| p.starts_with(path).then(|| p.into())) .collect()) } } ``` We need call the `with_assets` method when creating the GPUI application to register the asset source: ```rs fn main() { // Register Assets to GPUI application. let app = gpui_kit::application().with_assets(Assets); app.run(move |cx| { // We must initialize gpui_component before using it. gpui_kit::init(cx); cx.spawn(async move |cx| { cx.open_window(WindowOptions::default(), |window, cx| { let view = cx.new(|_| Example); // The first level on the window must be Root. cx.new(|cx| Root::new(view, window, cx)) }) .expect("Failed to open window"); }) .detach(); }); } ``` ## Use the icons Now we can use the icons in our application: ```rs pub struct Example; impl Render for Example { fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { v_flex() .gap_2() .size_full() .items_center() .justify_center() .text_center() .child(IconName::Inbox) .child(IconName::Bot) } } ``` ## Resources * [Lucide Icons](https://lucide.dev/) - The icon set used in GPUI Component is based on the open-source Lucide Icons library, which provides a wide range of customizable SVG icons. [rust-embed]: https://docs.rs/rust-embed/latest/rust_embed/ [IconName]: https://docs.rs/gpui_component/latest/gpui_component/icon/enum.IconName.html [Icon]: https://docs.rs/gpui_component/latest/gpui_component/icon/struct.Icon.html [assets]: https://github.com/longbridge/gpui-kit/tree/main/crates/assets/assets/ [gpui-kit-assets]: https://crates.io/crates/gpui-kit-assets --- --- url: /docs/components/image.md description: >- A flexible image display component with loading states, fallbacks, and responsive sizing options. --- # Image The Image component provides a robust way to display images with comprehensive fallback handling, loading states, and responsive sizing. Built on GPUI's native image support, it handles various image sources including URLs, local files, and SVG graphics with proper error handling and accessibility features. ## Import ```rust use gpui_kit::{img, ImageSource, ObjectFit}; use gpui_kit::component::{v_flex, h_flex, div, Icon, IconName}; ``` ## Usage ### Basic Image ```rust // Simple image from URL img("https://example.com/image.jpg") // Local image file img("assets/logo.png") // SVG image img("icons/star.svg") ``` ### Image with Sizing ```rust // Fixed dimensions img("https://example.com/photo.jpg") .w(px(300.)) .h(px(200.)) // Responsive width with aspect ratio img("https://example.com/banner.jpg") .w(relative(1.)) // Full width .max_w(px(800.)) .h(px(400.)) // Square image img("https://example.com/avatar.jpg") .size(px(100.)) // 100x100px ``` ### Object Fit Options Control how images are scaled and positioned within their containers: ```rust // Cover - scales to fill container, may crop img("https://example.com/photo.jpg") .w(px(300.)) .h(px(200.)) .object_fit(ObjectFit::Cover) // Contain - scales to fit within container, preserves aspect ratio img("https://example.com/photo.jpg") .w(px(300.)) .h(px(200.)) .object_fit(ObjectFit::Contain) // Fill - stretches to fill container, may distort img("https://example.com/photo.jpg") .w(px(300.)) .h(px(200.)) .object_fit(ObjectFit::Fill) // Scale Down - acts like contain, but never scales up img("https://example.com/photo.jpg") .w(px(300.)) .h(px(200.)) .object_fit(ObjectFit::ScaleDown) // None - original size, may overflow or be smaller than container img("https://example.com/photo.jpg") .w(px(300.)) .h(px(200.)) .object_fit(ObjectFit::None) ``` ### Image with Fallback Handling ```rust // Basic fallback with placeholder fn image_with_fallback(src: &str, alt_text: &str) -> impl IntoElement { div() .w(px(300.)) .h(px(200.)) .bg(cx.theme().surface) .border_1() .border_color(cx.theme().border) .rounded(px(8.)) .overflow_hidden() .child( img(src) .w_full() .h_full() .object_fit(ObjectFit::Cover) // Add error handling in practice ) } // Fallback with icon placeholder fn image_with_icon_fallback(src: &str) -> impl IntoElement { div() .size(px(200.)) .bg(cx.theme().surface) .border_1() .border_color(cx.theme().border) .rounded(px(8.)) .flex() .items_center() .justify_center() .child( img(src) .size_full() .object_fit(ObjectFit::Cover) // On error, show icon: // Icon::new(IconName::Image) // .size(px(48.)) // .text_color(cx.theme().muted_foreground) ) } ``` ### Loading States ```rust // Image with loading skeleton fn image_with_loading(src: &str, is_loading: bool) -> impl IntoElement { div() .w(px(400.)) .h(px(300.)) .rounded(px(8.)) .overflow_hidden() .map(|this| { if is_loading { this.bg(cx.theme().muted) .flex() .items_center() .justify_center() .child("Loading...") } else { this.child( img(src) .w_full() .h_full() .object_fit(ObjectFit::Cover) ) } }) } // Progressive loading with placeholder fn progressive_image(src: &str, placeholder_src: &str) -> impl IntoElement { div() .relative() .w(px(400.)) .h(px(300.)) .rounded(px(8.)) .overflow_hidden() .child( // Low-quality placeholder img(placeholder_src) .absolute() .inset_0() .w_full() .h_full() .object_fit(ObjectFit::Cover) .opacity(0.5) ) .child( // High-quality image img(src) .absolute() .inset_0() .w_full() .h_full() .object_fit(ObjectFit::Cover) ) } ``` ### Responsive Images ```rust // Responsive grid images fn responsive_image_grid() -> impl IntoElement { div() .grid() .grid_cols(3) .gap_4() .child( img("https://example.com/photo1.jpg") .w_full() .aspect_ratio(1.0) // Square aspect ratio .object_fit(ObjectFit::Cover) .rounded(px(8.)) ) .child( img("https://example.com/photo2.jpg") .w_full() .aspect_ratio(1.0) .object_fit(ObjectFit::Cover) .rounded(px(8.)) ) .child( img("https://example.com/photo3.jpg") .w_full() .aspect_ratio(1.0) .object_fit(ObjectFit::Cover) .rounded(px(8.)) ) } // Hero image with text overlay fn hero_image() -> impl IntoElement { div() .relative() .w_full() .h(px(500.)) .rounded(px(12.)) .overflow_hidden() .child( img("https://example.com/hero-image.jpg") .absolute() .inset_0() .w_full() .h_full() .object_fit(ObjectFit::Cover) ) .child( div() .absolute() .inset_0() .bg(rgba(0, 0, 0, 0.4)) // Dark overlay .flex() .items_center() .justify_center() .child( v_flex() .items_center() .gap_4() .child("Hero Title") .child("Subtitle text here") ) ) } ``` ### Image Gallery ```rust // Simple image gallery fn image_gallery(images: Vec<&str>) -> impl IntoElement { v_flex() .gap_6() .child( // Main image div() .w_full() .h(px(400.)) .rounded(px(12.)) .overflow_hidden() .child( img(images[0]) .w_full() .h_full() .object_fit(ObjectFit::Cover) ) ) .child( // Thumbnail row h_flex() .gap_3() .children( images.iter().map(|src| { div() .size(px(80.)) .rounded(px(6.)) .overflow_hidden() .border_2() .border_color(cx.theme().border) .cursor_pointer() .hover(|this| this.border_color(cx.theme().primary)) .child( img(*src) .size_full() .object_fit(ObjectFit::Cover) ) }) ) ) } ``` ### SVG Images ```rust // SVG icon with custom styling img("assets/icons/logo.svg") .size(px(64.)) .text_color(cx.theme().primary) // SVG color // Inline SVG handling img("data:image/svg+xml;base64,...") .w(px(32.)) .h(px(32.)) // SVG with animation-friendly setup img("assets/spinner.svg") .size(px(24.)) .text_color(cx.theme().primary) // Add rotation animation in practice ``` ## API Reference ### Core Image Function | Function | Description | | ------------- | ------------------------------------- | | `img(source)` | Create image element from ImageSource | ### Image Sources (ImageSource) | Type | Description | Example | | ----------- | ---------------------- | --------------------------------- | | String/\&str | URL or file path | `"https://example.com/image.jpg"` | | SharedUri | Shared URI reference | `SharedUri::from("file://path")` | | Local Path | Local file system path | `"assets/logo.png"` | | Data URI | Base64 encoded image | `"data:image/png;base64,..."` | ### Sizing Methods | Method | Description | | --------------- | ------------------------- | | `w(length)` | Set width | | `h(length)` | Set height | | `size(length)` | Set both width and height | | `w_full()` | Full width of container | | `h_full()` | Full height of container | | `size_full()` | Full size of container | | `max_w(length)` | Maximum width | | `max_h(length)` | Maximum height | | `min_w(length)` | Minimum width | | `min_h(length)` | Minimum height | ### Object Fit Options | Value | Description | | ---------------------- | --------------------------------- | | `ObjectFit::Cover` | Scale to fill container, may crop | | `ObjectFit::Contain` | Scale to fit within container | | `ObjectFit::Fill` | Stretch to fill container | | `ObjectFit::ScaleDown` | Like contain, but never scale up | | `ObjectFit::None` | Original size | ### Styling Methods | Method | Description | | --------------------- | ----------------------- | | `rounded(radius)` | Border radius | | `border_1()` | 1px border | | `border_color(color)` | Border color | | `opacity(value)` | Image opacity (0.0-1.0) | | `shadow_sm()` | Small shadow | | `shadow_lg()` | Large shadow | ## Examples ### Product Image Card ```rust use gpui_kit::component::{v_flex, div, Icon, IconName}; fn product_card(image_src: &str, title: &str, price: &str) -> impl IntoElement { v_flex() .gap_3() .p_4() .bg(cx.theme().card) .rounded(px(12.)) .shadow_sm() .child( div() .relative() .w_full() .h(px(200.)) .rounded(px(8.)) .overflow_hidden() .bg(cx.theme().muted) .child( img(image_src) .w_full() .h_full() .object_fit(ObjectFit::Cover) ) .child( // Wishlist button div() .absolute() .top_2() .right_2() .size(px(32.)) .bg(rgba(255, 255, 255, 0.9)) .rounded_full() .flex() .items_center() .justify_center() .cursor_pointer() .child(Icon::new(IconName::Heart).size(px(16.))) ) ) .child(title) .child(price) } ``` ### Avatar with Image ```rust fn custom_avatar(src: &str, name: &str, size: f32) -> impl IntoElement { div() .size(px(size)) .rounded_full() .overflow_hidden() .border_2() .border_color(cx.theme().background) .shadow_sm() .child( img(src) .size_full() .object_fit(ObjectFit::Cover) ) } ``` ### Image Comparison Slider ```rust fn image_comparison(before_src: &str, after_src: &str) -> impl IntoElement { div() .relative() .w_full() .h(px(400.)) .rounded(px(12.)) .overflow_hidden() .child( // Before image img(before_src) .absolute() .inset_0() .w_full() .h_full() .object_fit(ObjectFit::Cover) ) .child( // After image with clip div() .absolute() .top_0() .left_0() .w(relative(0.5)) // Show 50% initially .h_full() .overflow_hidden() .child( img(after_src) .w(px(800.)) // Full width of container .h_full() .object_fit(ObjectFit::Cover) ) ) .child( // Separator line div() .absolute() .top_0() .left(relative(0.5)) .w(px(2.)) .h_full() .bg(cx.theme().primary) ) } ``` ### Error Handling Pattern ```rust enum ImageState { Loading, Loaded, Error, } fn robust_image(src: &str, state: ImageState) -> impl IntoElement { div() .w(px(300.)) .h(px(200.)) .bg(cx.theme().muted) .rounded(px(8.)) .border_1() .border_color(cx.theme().border) .flex() .items_center() .justify_center() .map(|this| { match state { ImageState::Loading => { this.child( v_flex() .items_center() .gap_2() .child(Icon::new(IconName::Loader2).size(px(24.))) .child("Loading...") ) } ImageState::Loaded => { this.p_0() .overflow_hidden() .child( img(src) .w_full() .h_full() .object_fit(ObjectFit::Cover) ) } ImageState::Error => { this.child( v_flex() .items_center() .gap_2() .child( Icon::new(IconName::ImageOff) .size(px(32.)) .text_color(cx.theme().muted_foreground) ) .child("Failed to load image") ) } } }) } ``` ## Best Practices ### Image Optimization * Use appropriate image dimensions for display size * Compress images without sacrificing quality * Consider using modern image formats (WebP, AVIF) * Implement responsive images for different screen sizes ### Error Handling * Always provide meaningful fallbacks for failed image loads * Use skeleton loading states to maintain layout stability * Implement retry mechanisms for temporary network failures * Provide user feedback for permanent load failures ### Performance * Use lazy loading for images not immediately visible * Implement proper caching strategies * Consider using placeholder images during loading * Optimize image sizes for their display context ### User Experience * Maintain consistent aspect ratios in image grids * Provide smooth loading transitions * Use appropriate object-fit values for content type * Consider providing zoom functionality for detailed images ## Implementation Notes ### GPUI Integration * Built on GPUI's native image rendering capabilities * Supports all GPUI ImageSource types automatically * Inherits GPUI's styling and layout system * Compatible with GPUI's animation and interaction systems ### SVG Support * Full support for SVG graphics with proper scaling * SVG images can be styled with text colors for theming * Vector graphics maintain sharpness at all sizes * Supports both external SVG files and inline data URIs ### Memory Management * GPUI handles image caching and memory management automatically * Large images are efficiently managed by the graphics backend * No manual memory cleanup required for image components ### Cross-Platform Compatibility * Consistent behavior across Windows, macOS, and Linux * Native image format support varies by platform * Uses platform-optimized rendering where available --- --- url: /zh-CN/docs/components/image.md description: 支持加载状态、回退内容和响应式尺寸的灵活图片展示组件。 --- # Image Image 组件为图片展示提供了更稳健的封装,支持加载态、回退内容、响应式尺寸以及多种图片来源。它基于 GPUI 原生图片能力构建,可处理 URL、本地文件和 SVG 等资源,并便于结合主题与布局系统做统一样式控制。 ## 导入 ```rust use gpui_kit::{img, ImageSource, ObjectFit}; use gpui_kit::component::{v_flex, h_flex, div, Icon, IconName}; ``` ## 用法 ### 基础图片 ```rust // 来自 URL 的图片 img("https://example.com/image.jpg") // 本地图片文件 img("assets/logo.png") // SVG 图片 img("icons/star.svg") ``` ### 设置尺寸 ```rust // 固定尺寸 img("https://example.com/photo.jpg") .w(px(300.)) .h(px(200.)) // 响应式宽度并限制最大宽度 img("https://example.com/banner.jpg") .w(relative(1.)) .max_w(px(800.)) .h(px(400.)) // 正方形图片 img("https://example.com/avatar.jpg") .size(px(100.)) ``` ### Object Fit 选项 用于控制图片在容器中的缩放与定位方式: ```rust // Cover:填满容器,可能裁剪 img("https://example.com/photo.jpg") .w(px(300.)) .h(px(200.)) .object_fit(ObjectFit::Cover) // Contain:完整显示,保持比例 img("https://example.com/photo.jpg") .w(px(300.)) .h(px(200.)) .object_fit(ObjectFit::Contain) // Fill:拉伸填满,可能变形 img("https://example.com/photo.jpg") .w(px(300.)) .h(px(200.)) .object_fit(ObjectFit::Fill) // ScaleDown:类似 contain,但不会放大 img("https://example.com/photo.jpg") .w(px(300.)) .h(px(200.)) .object_fit(ObjectFit::ScaleDown) // None:保持原始尺寸 img("https://example.com/photo.jpg") .w(px(300.)) .h(px(200.)) .object_fit(ObjectFit::None) ``` ### 回退内容 ```rust fn image_with_fallback(src: &str, alt_text: &str) -> impl IntoElement { div() .w(px(300.)) .h(px(200.)) .bg(cx.theme().surface) .border_1() .border_color(cx.theme().border) .rounded(px(8.)) .overflow_hidden() .child( img(src) .w_full() .h_full() .object_fit(ObjectFit::Cover) // 实际项目中可在这里补充错误处理 ) } fn image_with_icon_fallback(src: &str) -> impl IntoElement { div() .size(px(200.)) .bg(cx.theme().surface) .border_1() .border_color(cx.theme().border) .rounded(px(8.)) .flex() .items_center() .justify_center() .child( img(src) .size_full() .object_fit(ObjectFit::Cover) // 加载失败时可改为显示图标占位 ) } ``` ### 加载状态 ```rust fn image_with_loading(src: &str, is_loading: bool) -> impl IntoElement { div() .w(px(400.)) .h(px(300.)) .rounded(px(8.)) .overflow_hidden() .map(|this| { if is_loading { this.bg(cx.theme().muted) .flex() .items_center() .justify_center() .child("Loading...") } else { this.child( img(src) .w_full() .h_full() .object_fit(ObjectFit::Cover) ) } }) } fn progressive_image(src: &str, placeholder_src: &str) -> impl IntoElement { div() .relative() .w(px(400.)) .h(px(300.)) .rounded(px(8.)) .overflow_hidden() .child( img(placeholder_src) .absolute() .inset_0() .w_full() .h_full() .object_fit(ObjectFit::Cover) .opacity(0.5) ) .child( img(src) .absolute() .inset_0() .w_full() .h_full() .object_fit(ObjectFit::Cover) ) } ``` ### 响应式图片 ```rust fn responsive_image_grid() -> impl IntoElement { div() .grid() .grid_cols(3) .gap_4() .child( img("https://example.com/photo1.jpg") .w_full() .aspect_ratio(1.0) .object_fit(ObjectFit::Cover) .rounded(px(8.)) ) .child( img("https://example.com/photo2.jpg") .w_full() .aspect_ratio(1.0) .object_fit(ObjectFit::Cover) .rounded(px(8.)) ) .child( img("https://example.com/photo3.jpg") .w_full() .aspect_ratio(1.0) .object_fit(ObjectFit::Cover) .rounded(px(8.)) ) } fn hero_image() -> impl IntoElement { div() .relative() .w_full() .h(px(500.)) .rounded(px(12.)) .overflow_hidden() .child( img("https://example.com/hero-image.jpg") .absolute() .inset_0() .w_full() .h_full() .object_fit(ObjectFit::Cover) ) .child( div() .absolute() .inset_0() .bg(rgba(0, 0, 0, 0.4)) .flex() .items_center() .justify_center() .child( v_flex() .items_center() .gap_4() .child("Hero Title") .child("Subtitle text here") ) ) } ``` ### 图片画廊 ```rust fn image_gallery(images: Vec<&str>) -> impl IntoElement { v_flex() .gap_6() .child( div() .w_full() .h(px(400.)) .rounded(px(12.)) .overflow_hidden() .child( img(images[0]) .w_full() .h_full() .object_fit(ObjectFit::Cover) ) ) .child( h_flex() .gap_3() .children( images.iter().map(|src| { div() .size(px(80.)) .rounded(px(6.)) .overflow_hidden() .border_2() .border_color(cx.theme().border) .cursor_pointer() .hover(|this| this.border_color(cx.theme().primary)) .child( img(*src) .size_full() .object_fit(ObjectFit::Cover) ) }) ) ) } ``` ### SVG 图片 ```rust img("assets/icons/logo.svg") .size(px(64.)) .text_color(cx.theme().primary) img("data:image/svg+xml;base64,...") .w(px(32.)) .h(px(32.)) img("assets/spinner.svg") .size(px(24.)) .text_color(cx.theme().primary) // 实际使用中可叠加旋转动画 ``` ## API 参考 ### 核心函数 | 函数 | 说明 | | --- | --- | | `img(source)` | 基于 `ImageSource` 创建图片元素 | ### 图片来源(ImageSource) | 类型 | 说明 | 示例 | | --- | --- | --- | | String / \&str | URL 或文件路径 | `"https://example.com/image.jpg"` | | SharedUri | 共享 URI 引用 | `SharedUri::from("file://path")` | | Local Path | 本地文件系统路径 | `"assets/logo.png"` | | Data URI | Base64 编码图片 | `"data:image/png;base64,..."` | ### 尺寸方法 | 方法 | 说明 | | --- | --- | | `w(length)` | 设置宽度 | | `h(length)` | 设置高度 | | `size(length)` | 同时设置宽高 | | `w_full()` | 占满容器宽度 | | `h_full()` | 占满容器高度 | | `size_full()` | 占满容器尺寸 | | `max_w(length)` | 设置最大宽度 | | `max_h(length)` | 设置最大高度 | | `min_w(length)` | 设置最小宽度 | | `min_h(length)` | 设置最小高度 | ### Object Fit 选项 | 值 | 说明 | | --- | --- | | `ObjectFit::Cover` | 填满容器,可能裁剪 | | `ObjectFit::Contain` | 完整显示在容器内 | | `ObjectFit::Fill` | 拉伸填满容器 | | `ObjectFit::ScaleDown` | 类似 contain,但不会放大 | | `ObjectFit::None` | 保持原始尺寸 | ### 样式方法 | 方法 | 说明 | | --- | --- | | `rounded(radius)` | 设置圆角 | | `border_1()` | 1px 边框 | | `border_color(color)` | 设置边框颜色 | | `opacity(value)` | 设置透明度(0.0-1.0) | | `shadow_sm()` | 小阴影 | | `shadow_lg()` | 大阴影 | ## 最佳实践 ### 图片优化 * 根据展示尺寸提供合适分辨率的图片 * 在保证质量的前提下尽量压缩资源 * 优先考虑 WebP、AVIF 等现代格式 * 为不同屏幕尺寸准备响应式图片 ### 错误处理 * 为加载失败提供明确回退内容 * 使用骨架屏保持布局稳定 * 对临时网络错误考虑重试机制 * 对永久失败给出可理解的用户提示 ### 性能 * 对首屏外图片使用懒加载 * 配合缓存策略减少重复请求 * 加载期间可使用低质量占位图 * 图片尺寸应与实际展示上下文匹配 ### 用户体验 * 图片网格中保持一致的宽高比 * 使用平滑的加载过渡 * 根据内容类型选择合适的 `object-fit` * 细节图可考虑提供缩放能力 --- --- url: /base/primitives/input.md description: >- An unstyled single-line text input with masking, validation, and number stepping. --- # Input `Input` is the single-line text control in `gpui-base`. It owns editing behavior, focus, selection, keyboard input, IME, masking, validation, and events while the application supplies presentation. Use [Textarea](./textarea.md) for ordinary multi-line text and [Editor](./editor.md) for source code. ## Import ```rust use gpui_kit::base::input::{Input, InputEvent, InputState}; ``` ## Basic usage Create the persistent state once, then render `Input` with that entity: ```rust let input = cx.new(|cx| { InputState::new(window, cx) .placeholder("Account name") .default_value("Ada") }); Input::new(&input) ``` Read and update the value through the state: ```rust let value = input.read(cx).value(); input.update(cx, |state, cx| { state.set_value("Grace", window, cx); }); ``` ## Masking and validation ```rust let password = cx.new(|cx| { InputState::new(window, cx) .placeholder("Password") .masked(true) .validate(|value, _| value.chars().count() >= 8) }); ``` For formatted values, combine `mask_pattern`, `pattern`, `min`, `max`, `step`, or `step_by` as appropriate. `unmask_value()` returns the underlying value of a masked input. ## Events `InputState` emits `InputEvent::Change`, `PressEnter`, `Focus`, and `Blur`. ```rust cx.subscribe(&input, |this, state, event: &InputEvent, cx| { if matches!(event, InputEvent::Change) { this.value = state.read(cx).value(); cx.notify(); } }); ``` ## Presentation `gpui-base` does not install product styling. Supply `InputEditorStyle` to the state and compose the control inside your own frame. If you want the ready-made theme, sizing, borders, prefix/suffix slots, and clear button, use the styled [`gpui-component` Input](../../docs/components/input.md). ## Runnable example ```bash cargo run -p gpui-base --example components -- input ``` The implementation is in [`crates/base/examples/showcase/components/input.rs`](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/components/input.rs). --- --- url: /docs/components/input.md description: 'Text input component with validation, masking, and various features.' --- # Input A single-line text input with validation, masking, prefix/suffix elements, and different visual states. Use [Textarea](./textarea.md) for ordinary multi-line text and [Editor](./editor.md) for source code. ## Import ```rust use gpui_kit::component::input::{Input, InputState}; ``` ## Usage ### Basic Input ```rust let input = cx.new(|cx| InputState::new(window, cx)); Input::new(&input) ``` ### With Placeholder ```rust let input = cx.new(|cx| InputState::new(window, cx) .placeholder("Enter your name...") ); Input::new(&input) ``` ### With Default Value ```rust let input = cx.new(|cx| InputState::new(window, cx) .default_value("John Doe") ); Input::new(&input) ``` ### Cleanable Input ```rust Input::new(&input) .cleanable(true) // Show clear button when input has value ``` ### With Prefix and Suffix ```rust use gpui_kit::component::{Icon, IconName}; // With prefix icon Input::new(&input) .prefix(Icon::new(IconName::Search).small()) // With suffix button Input::new(&input) .suffix( Button::new("info") .ghost() .icon(IconName::Info) .xsmall() ) // With both Input::new(&input) .prefix(Icon::new(IconName::Search).small()) .suffix(Button::new("btn").ghost().icon(IconName::Info).xsmall()) ``` ### Password Input (Masked) ```rust let input = cx.new(|cx| InputState::new(window, cx) .masked(true) .default_value("password123") ); Input::new(&input) .content_type(InputContentType::Password) .mask_toggle() // Shows toggle button to reveal password ``` While the value is masked, the input keeps it out of the clipboard and out of the selection: Copy and Cut do nothing (and are disabled in the context menu), a word-wise delete takes everything before the caret, and a double click selects the whole value instead of one word. Paste and Select All keep working, and revealing the value with `mask_toggle` restores all of them. ### Input Sizes ```rust Input::new(&input).large() Input::new(&input) // medium (default) Input::new(&input).small() ``` ### Disabled Input ```rust Input::new(&input).disabled(true) ``` ### Read-only Input Unlike `disabled`, a read-only input keeps the normal appearance and still can be focused, selected and copied, it only rejects the changes made by the user. ```rust Input::new(&input).readonly(true) ``` ### Clean on ESC ```rust let input = cx.new(|cx| InputState::new(window, cx) .clean_on_escape() // Clear input when ESC is pressed ); Input::new(&input) ``` ### Input Validation ```rust // Validate float numbers let input = cx.new(|cx| InputState::new(window, cx) .validate(|s, _| s.parse::().is_ok()) ); // Regex pattern validation let input = cx.new(|cx| InputState::new(window, cx) .pattern(regex::Regex::new(r"^[a-zA-Z0-9]*$").unwrap()) ); ``` ### Input Masking ```rust // Phone number mask let input = cx.new(|cx| InputState::new(window, cx) .mask_pattern("(999)-999-9999") ); // Custom pattern: AAA-###-AAA (A=letter, #=digit, 9=digit optional) let input = cx.new(|cx| InputState::new(window, cx) .mask_pattern("AAA-###-AAA") ); // Number with thousands separator use gpui_kit::component::input::MaskPattern; let input = cx.new(|cx| InputState::new(window, cx) .mask_pattern(MaskPattern::Number { separator: Some(','), fraction: Some(3), }) ); ``` ### Handle Input Events ```rust let input = cx.new(|cx| InputState::new(window, cx)); cx.subscribe_in(&input, window, |view, state, event, window, cx| { match event { InputEvent::Change => { let text = state.read(cx).value(); println!("Input changed: {}", text); } InputEvent::PressEnter { secondary } => { println!("Enter pressed, secondary: {}", secondary); } InputEvent::Focus => println!("Input focused"), InputEvent::Blur => println!("Input blurred"), } }); ``` ### Custom Appearance ```rust // Without default styling Input::new(&input).appearance(false) // Use in custom container div() .border_b_2() .px_6() .py_3() .border_color(cx.theme().border) .bg(cx.theme().secondary) .child(Input::new(&input).appearance(false)) ``` ### Context Menu ```rust // The built-in context menu can be disabled. let input = cx.new(|cx| InputState::new(window, cx).context_menu(false)); // Or you can define a custom context menu. Input::new(&input).context_menu(|menu, window, cx| { // You can define your own actions and even utilize // built-in actions (cut, copy, paste, etc.) // to avoid having to re-implement that functionality. menu.menu("Custom Action", Box::new(CustomAction)) .separator() .menu("Cut", Box::new(input::Cut)) .menu("Copy", Box::new(input::Copy)) .menu("Paste", Box::new(input::Paste)) }) ``` ## Examples ### Search Input ```rust let search = cx.new(|cx| InputState::new(window, cx) .placeholder("Search...") ); Input::new(&search) .prefix(Icon::new(IconName::Search).small()) ``` ### Currency Input ```rust let amount = cx.new(|cx| InputState::new(window, cx) .mask_pattern(MaskPattern::Number { separator: Some(','), fraction: Some(2), }) ); div() .child(Input::new(&amount)) .child(format!("Value: {}", amount.read(cx).value())) ``` ### Form with Multiple Inputs ```rust struct FormView { name_input: Entity, email_input: Entity, } v_flex() .gap_3() .child(Input::new(&self.name_input)) .child(Input::new(&self.email_input)) ``` --- --- url: /zh-CN/base/primitives/input.md description: 支持掩码、验证和数字步进的无样式单行文本输入。 --- # Input `Input` 是 `gpui-base` 的单行文本控件。它负责编辑、焦点、选择、键盘输入、IME、掩码、验证与事件,表现由应用提供。普通多行文本使用 [Textarea](./textarea.md),源代码使用 [Editor](./editor.md)。 ## 导入 ```rust use gpui_kit::base::input::{Input, InputEvent, InputState}; ``` ## 基本用法 持久状态只创建一次,再用对应 entity 渲染 `Input`: ```rust let input = cx.new(|cx| { InputState::new(window, cx) .placeholder("Account name") .default_value("Ada") }); Input::new(&input) ``` 通过状态读取和更新值: ```rust let value = input.read(cx).value(); input.update(cx, |state, cx| state.set_value("Grace", window, cx)); ``` ## 掩码与验证 ```rust let password = cx.new(|cx| { InputState::new(window, cx) .placeholder("Password") .masked(true) .validate(|value, _| value.chars().count() >= 8) }); ``` 格式化值可按需组合 `mask_pattern`、`pattern`、`min`、`max`、`step` 或 `step_by`。`unmask_value()` 返回掩码输入的底层值。 ## 事件 `InputState` 会发出 `InputEvent::Change`、`PressEnter`、`Focus` 和 `Blur`。订阅事件后读取新值,并在更新宿主状态后调用 `cx.notify()`。 ## 表现 `gpui-base` 不安装产品样式。向状态提供 `InputEditorStyle`,并把控件组合进自己的边框容器。若需要现成主题、尺寸、边框、前后缀槽位和清除按钮,请使用 [`gpui-component` Input](../../docs/components/input.md)。 ## 可运行示例 ```bash cargo run -p gpui-base --example components -- input ``` 实现位于 [`input.rs`](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/components/input.rs)。 --- --- url: /zh-CN/docs/components/input.md description: 带校验、掩码和多种扩展能力的文本输入组件。 --- # Input Input 是一个单行文本输入组件,支持校验、输入掩码、前后缀元素以及多种交互状态。普通多行文本请使用 [Textarea](./textarea.md),源代码编辑请使用 [Editor](./editor.md)。 ## 导入 ```rust use gpui_kit::component::input::{Input, InputState}; ``` ## 用法 ### 基础输入框 ```rust let input = cx.new(|cx| InputState::new(window, cx)); Input::new(&input) ``` ### Placeholder ```rust let input = cx.new(|cx| InputState::new(window, cx) .placeholder("Enter your name...") ); Input::new(&input) ``` ### 默认值 ```rust let input = cx.new(|cx| InputState::new(window, cx) .default_value("John Doe") ); Input::new(&input) ``` ### 可清空 ```rust Input::new(&input) .cleanable(true) ``` ### 前缀和后缀 ```rust use gpui_kit::component::{Icon, IconName}; Input::new(&input) .prefix(Icon::new(IconName::Search).small()) Input::new(&input) .suffix( Button::new("info") .ghost() .icon(IconName::Info) .xsmall() ) Input::new(&input) .prefix(Icon::new(IconName::Search).small()) .suffix(Button::new("btn").ghost().icon(IconName::Info).xsmall()) ``` ### 密码输入 ```rust let input = cx.new(|cx| InputState::new(window, cx) .masked(true) .default_value("password123") ); Input::new(&input) .content_type(InputContentType::Password) .mask_toggle() ``` 掩码状态下,输入框不会让明文进入剪贴板,也不会通过选区暴露内容:Copy 和 Cut 不执行任何操作(上下文菜单中同样置灰),按词删除会删掉光标之前的全部内容,双击 则选中整个值而不是其中一个词。Paste 和 Select All 不受影响,通过 `mask_toggle` 显示明文后,上述操作全部恢复。 ### 尺寸 ```rust Input::new(&input).large() Input::new(&input) Input::new(&input).small() ``` ### 禁用态 ```rust Input::new(&input).disabled(true) ``` ### 只读态 与 `disabled` 不同,只读输入框保持正常外观,仍然可以聚焦、选中和复制,只是拒绝用户对内容的修改。 ```rust Input::new(&input).readonly(true) ``` ### 按 ESC 清空 ```rust let input = cx.new(|cx| InputState::new(window, cx) .clean_on_escape() ); Input::new(&input) ``` ### 输入校验 ```rust let input = cx.new(|cx| InputState::new(window, cx) .validate(|s, _| s.parse::().is_ok()) ); let input = cx.new(|cx| InputState::new(window, cx) .pattern(regex::Regex::new(r"^[a-zA-Z0-9]*$").unwrap()) ); ``` ### 输入掩码 ```rust let input = cx.new(|cx| InputState::new(window, cx) .mask_pattern("(999)-999-9999") ); let input = cx.new(|cx| InputState::new(window, cx) .mask_pattern("AAA-###-AAA") ); use gpui_kit::component::input::MaskPattern; let input = cx.new(|cx| InputState::new(window, cx) .mask_pattern(MaskPattern::Number { separator: Some(','), fraction: Some(3), }) ); ``` ### 监听事件 ```rust let input = cx.new(|cx| InputState::new(window, cx)); cx.subscribe_in(&input, window, |view, state, event, window, cx| { match event { InputEvent::Change => { let text = state.read(cx).value(); println!("Input changed: {}", text); } InputEvent::PressEnter { secondary } => { println!("Enter pressed, secondary: {}", secondary); } InputEvent::Focus => println!("Input focused"), InputEvent::Blur => println!("Input blurred"), } }); ``` ### 自定义外观 ```rust Input::new(&input).appearance(false) div() .border_b_2() .px_6() .py_3() .border_color(cx.theme().border) .bg(cx.theme().secondary) .child(Input::new(&input).appearance(false)) ``` ## 示例 ### 搜索输入框 ```rust let search = cx.new(|cx| InputState::new(window, cx) .placeholder("Search...") ); Input::new(&search) .prefix(Icon::new(IconName::Search).small()) ``` ### 金额输入 ```rust let amount = cx.new(|cx| InputState::new(window, cx) .mask_pattern(MaskPattern::Number { separator: Some(','), fraction: Some(2), }) ); div() .child(Input::new(&amount)) .child(format!("Value: {}", amount.read(cx).value())) ``` ### 多输入表单 ```rust struct FormView { name_input: Entity, email_input: Entity, } v_flex() .gap_3() .child(Input::new(&self.name_input)) .child(Input::new(&self.email_input)) ``` --- --- url: /docs/installation.md --- # Installation Before you start to build your application with `gpui-component`, you need to install the library. ## System Requirements We can development application on macOS, Windows or Linux. ### macOS * macOS 15 or later * Xcode command line tools ## Windows * Windows 10 or later There have a bootstrap script to help install the required toolchain and dependencies. You can run the script in PowerShell: ```ps .\script\install-window.ps1 ``` ## Linux Run `./script/bootstrap` to install system dependencies. ## Rust and Cargo We use Rust programming language to build the `gpui-component` library. Make sure you have Rust and Cargo installed on your system. * Rust 1.90 or later * Cargo (comes with Rust) To install the `gpui-component` library, you can use Cargo, the Rust package manager. Add the following line to your `Cargo.toml` file under the `[dependencies]` section: ```toml gpui-kit = "0.6" ``` `gpui-kit` depends on the matching GPUI crates for you, so your application never lists GPUI itself. `use gpui_kit::*;` is GPUI, and the layers are reachable by name: `gpui_kit::component` (the styled components), `gpui_kit::base`, `gpui_kit::assets` and `gpui_kit::platform`. ## Faster development builds Debug builds compile GPUI, the component library and the text stack without optimizations, which makes a `cargo run` build of your application render noticeably slower than a release build. Optimize just those crates while your own code stays a fast, debuggable debug build. Profiles only take effect in the root `Cargo.toml` of your application (or workspace): ```toml [profile.dev.package] gpui-pre = { opt-level = 3 } gpui-component = { opt-level = 3 } gpui-kit = { opt-level = 3 } gpui-kit-assets = { opt-level = 3 } gpui-pre-macros = { opt-level = 3 } gpui-pre-platform = { opt-level = 3 } rustybuzz = { opt-level = 3 } taffy = { opt-level = 3 } ttf-parser = { opt-level = 3 } ``` --- --- url: /docs/components/kbd.md description: Displays keyboard shortcuts with platform-specific formatting. --- # Kbd A component for displaying keyboard shortcuts and key combinations with proper platform-specific formatting. Automatically adapts the display to match the conventions of macOS (using symbols) or Windows/Linux (using text labels). ## Import ```rust use gpui_kit::component::kbd::Kbd; use gpui_kit::Keystroke; ``` ## Usage ### Basic Keyboard Shortcut ```rust // Create from a keystroke let kbd = Kbd::new(Keystroke::parse("cmd-shift-p").unwrap()); // Or convert directly from keystroke let kbd: Kbd = Keystroke::parse("escape").unwrap().into(); ``` ### Common Shortcuts ```rust // Command palette Kbd::new(Keystroke::parse("cmd-shift-p").unwrap()) // New tab Kbd::new(Keystroke::parse("cmd-t").unwrap()) // Zoom controls Kbd::new(Keystroke::parse("cmd--").unwrap()) // Zoom out Kbd::new(Keystroke::parse("cmd-+").unwrap()) // Zoom in // Navigation Kbd::new(Keystroke::parse("escape").unwrap()) Kbd::new(Keystroke::parse("enter").unwrap()) Kbd::new(Keystroke::parse("backspace").unwrap()) ``` ### Multiple Modifiers ```rust // Complex combinations Kbd::new(Keystroke::parse("cmd-ctrl-shift-a").unwrap()) Kbd::new(Keystroke::parse("cmd-alt-backspace").unwrap()) Kbd::new(Keystroke::parse("ctrl-alt-shift-a").unwrap()) ``` ### Arrow Keys and Function Keys ```rust // Arrow keys Kbd::new(Keystroke::parse("left").unwrap()) Kbd::new(Keystroke::parse("right").unwrap()) Kbd::new(Keystroke::parse("up").unwrap()) Kbd::new(Keystroke::parse("down").unwrap()) // Function keys Kbd::new(Keystroke::parse("f12").unwrap()) Kbd::new(Keystroke::parse("secondary-f12").unwrap()) // Page navigation Kbd::new(Keystroke::parse("pageup").unwrap()) Kbd::new(Keystroke::parse("pagedown").unwrap()) ``` ### Without Visual Styling ```rust // Display only the key text without the styled background Kbd::new(Keystroke::parse("cmd-s").unwrap()) .appearance(false) ``` ### From Action Bindings ```rust use gpui_kit::{Action, Window, FocusHandle}; // Get first keybinding for an action if let Some(kbd) = Kbd::binding_for_action(&MyAction {}, None, window) { // Display the bound shortcut } // Get keybinding for action within a specific context if let Some(kbd) = Kbd::binding_for_action(&MyAction {}, Some("Editor"), window) { // Display context-specific shortcut } // Get keybinding for action within a focus handle if let Some(kbd) = Kbd::binding_for_action_in(&MyAction {}, &focus_handle, window) { // Display shortcut for focused element } ``` ## Platform Differences The Kbd component automatically formats shortcuts according to platform conventions: ### macOS * Uses symbols: ⌃ ⌥ ⇧ ⌘ * No separators between modifiers * Order: Control, Option, Shift, Command * Special keys: ⌫ (backspace), ⎋ (escape), ⏎ (enter), ← → ↑ ↓ (arrows) ### Windows/Linux * Uses text labels: Ctrl, Alt, Shift, Win * Plus sign (+) separators * Order: Ctrl, Alt, Shift, Win * Special keys: Backspace, Esc, Enter, Left, Right, Up, Down ### Examples by Platform | Input | macOS | Windows/Linux | | ------------------- | ----- | ----------------- | | `cmd-a` | ⌘A | Win+A | | `ctrl-shift-a` | ⌃⇧A | Ctrl+Shift+A | | `cmd-alt-backspace` | ⌥⌘⌫ | Win+Alt+Backspace | | `escape` | ⎋ | Esc | | `enter` | ⏎ | Enter | | `left` | ← | Left | ## Examples ### Keyboard Shortcut Help ```rust use gpui_kit::{div, h_flex, v_flex}; // Display common shortcuts v_flex() .gap_2() .child( h_flex() .gap_2() .items_center() .child("Open command palette:") .child(Kbd::new(Keystroke::parse("cmd-shift-p").unwrap())) ) .child( h_flex() .gap_2() .items_center() .child("Save file:") .child(Kbd::new(Keystroke::parse("cmd-s").unwrap())) ) .child( h_flex() .gap_2() .items_center() .child("Find in files:") .child(Kbd::new(Keystroke::parse("cmd-shift-f").unwrap())) ) ``` ### Menu Item with Shortcut ```rust h_flex() .justify_between() .items_center() .child("New File") .child(Kbd::new(Keystroke::parse("cmd-n").unwrap())) ``` ### Inline Documentation ```rust div() .child("Press ") .child(Kbd::new(Keystroke::parse("escape").unwrap())) .child(" to cancel or ") .child(Kbd::new(Keystroke::parse("enter").unwrap())) .child(" to confirm.") ``` ### Custom Styling ```rust Kbd::new(Keystroke::parse("cmd-k").unwrap()) .text_color(cx.theme().accent) .border_color(cx.theme().accent) .bg(cx.theme().accent.opacity(0.1)) ``` ### Text-Only Format ```rust // Get formatted text without styling let shortcut_text = Kbd::format(&Keystroke::parse("cmd-shift-p").unwrap()); div().child(format!("Shortcut: {}", shortcut_text)) ``` ## Styling The Kbd component uses the following default styles: * Border with theme border color * Muted foreground text color * Background with theme background color * Small rounded corners * Centered text alignment * Extra small font size * Minimal padding (0.5px vertical, 1px horizontal) * Minimum width of 5 units * Flex shrink disabled to maintain size All styles can be customized using the `Styled` trait methods. --- --- url: /zh-CN/docs/components/kbd.md description: 以符合平台习惯的方式显示键盘快捷键。 --- # Kbd Kbd 用于展示键盘快捷键和组合键,并会自动根据平台采用合适的显示格式。macOS 会使用符号,Windows 和 Linux 则使用文本标签,便于文档、菜单和帮助面板保持一致的快捷键表达。 ## 导入 ```rust use gpui_kit::component::kbd::Kbd; use gpui_kit::Keystroke; ``` ## 用法 ### 基础快捷键 ```rust let kbd = Kbd::new(Keystroke::parse("cmd-shift-p").unwrap()); let kbd: Kbd = Keystroke::parse("escape").unwrap().into(); ``` ### 常见快捷键 ```rust Kbd::new(Keystroke::parse("cmd-shift-p").unwrap()) Kbd::new(Keystroke::parse("cmd-t").unwrap()) Kbd::new(Keystroke::parse("cmd--").unwrap()) Kbd::new(Keystroke::parse("cmd-+").unwrap()) Kbd::new(Keystroke::parse("escape").unwrap()) Kbd::new(Keystroke::parse("enter").unwrap()) Kbd::new(Keystroke::parse("backspace").unwrap()) ``` ### 多修饰键 ```rust Kbd::new(Keystroke::parse("cmd-ctrl-shift-a").unwrap()) Kbd::new(Keystroke::parse("cmd-alt-backspace").unwrap()) Kbd::new(Keystroke::parse("ctrl-alt-shift-a").unwrap()) ``` ### 方向键与功能键 ```rust Kbd::new(Keystroke::parse("left").unwrap()) Kbd::new(Keystroke::parse("right").unwrap()) Kbd::new(Keystroke::parse("up").unwrap()) Kbd::new(Keystroke::parse("down").unwrap()) Kbd::new(Keystroke::parse("f12").unwrap()) Kbd::new(Keystroke::parse("secondary-f12").unwrap()) Kbd::new(Keystroke::parse("pageup").unwrap()) Kbd::new(Keystroke::parse("pagedown").unwrap()) ``` ### 关闭默认外观 ```rust Kbd::new(Keystroke::parse("cmd-s").unwrap()) .appearance(false) ``` ### 从 Action 绑定读取 ```rust use gpui_kit::{Action, Window, FocusHandle}; if let Some(kbd) = Kbd::binding_for_action(&MyAction {}, None, window) { // 显示该 action 绑定的快捷键 } if let Some(kbd) = Kbd::binding_for_action(&MyAction {}, Some("Editor"), window) { // 显示特定上下文中的快捷键 } if let Some(kbd) = Kbd::binding_for_action_in(&MyAction {}, &focus_handle, window) { // 显示焦点元素上的快捷键 } ``` ## 平台差异 ### macOS * 使用符号:⌃ ⌥ ⇧ ⌘ * 修饰键之间不加分隔符 * 顺序为 Control、Option、Shift、Command * 特殊键使用 ⌫、⎋、⏎、← → ↑ ↓ 等符号 ### Windows / Linux * 使用文本标签:Ctrl、Alt、Shift、Win * 修饰键之间使用 `+` * 顺序为 Ctrl、Alt、Shift、Win * 特殊键显示为 Backspace、Esc、Enter、Left、Right、Up、Down ### 平台示例 | 输入 | macOS | Windows / Linux | | --- | --- | --- | | `cmd-a` | ⌘A | Win+A | | `ctrl-shift-a` | ⌃⇧A | Ctrl+Shift+A | | `cmd-alt-backspace` | ⌥⌘⌫ | Win+Alt+Backspace | | `escape` | ⎋ | Esc | | `enter` | ⏎ | Enter | | `left` | ← | Left | ## 示例 ### 快捷键帮助面板 ```rust use gpui_kit::{div, h_flex, v_flex}; v_flex() .gap_2() .child( h_flex() .gap_2() .items_center() .child("Open command palette:") .child(Kbd::new(Keystroke::parse("cmd-shift-p").unwrap())) ) .child( h_flex() .gap_2() .items_center() .child("Save file:") .child(Kbd::new(Keystroke::parse("cmd-s").unwrap())) ) .child( h_flex() .gap_2() .items_center() .child("Find in files:") .child(Kbd::new(Keystroke::parse("cmd-shift-f").unwrap())) ) ``` ### 带快捷键的菜单项 ```rust h_flex() .justify_between() .items_center() .child("New File") .child(Kbd::new(Keystroke::parse("cmd-n").unwrap())) ``` ### 行内说明 ```rust div() .child("Press ") .child(Kbd::new(Keystroke::parse("escape").unwrap())) .child(" to cancel or ") .child(Kbd::new(Keystroke::parse("enter").unwrap())) .child(" to confirm.") ``` ### 自定义样式 ```rust Kbd::new(Keystroke::parse("cmd-k").unwrap()) .text_color(cx.theme().accent) .border_color(cx.theme().accent) .bg(cx.theme().accent.opacity(0.1)) ``` ### 仅获取文本格式 ```rust let shortcut_text = Kbd::format(&Keystroke::parse("cmd-shift-p").unwrap()); div().child(format!("Shortcut: {}", shortcut_text)) ``` ## 样式 Kbd 默认包含以下样式: * 使用主题边框颜色绘制边框 * 使用 muted 前景色显示文字 * 使用主题背景色作为底色 * 小圆角 * 文本居中 * 超小字号 * 极小的内边距 * 最小宽度为 5 * 禁止 flex shrink,避免压缩失真 所有样式都可以通过 `Styled` trait 的方法覆盖。 --- --- url: /docs/components/label.md description: Text labels for form elements with highlighting and styling options. --- # Label A versatile label component for displaying text with support for secondary text, highlighting, masking, and customizable styling. Perfect for form labels, captions, and general text display with optional/required indicators. ## Import ```rust use gpui_kit::component::label::{Label, HighlightsMatch}; ``` ## Usage ### Basic Label ```rust Label::new("This is a label") ``` ### Label with Secondary Text ```rust // Label with optional indicator Label::new("Company Address") .secondary("(optional)") // Label with required indicator Label::new("Email Address") .secondary("(required)") ``` ### Text Alignment ```rust // Left aligned (default) Label::new("Text align left") // Center aligned Label::new("Text align center") .text_center() // Right aligned Label::new("Text align right") .text_right() ``` ### Text Highlighting ```rust // Full text highlighting (finds all matches) Label::new("Hello World Hello") .highlights("Hello") // Prefix highlighting (only matches at start) Label::new("Hello World") .highlights(HighlightsMatch::Prefix("Hello".into())) // Highlight with secondary text Label::new("Company Name") .secondary("(optional)") .highlights("Company") ``` ### Color and Styling ```rust use gpui_kit::component::green_500; // Custom text color Label::new("Color Label") .text_color(green_500()) // Font styling Label::new("Font Size Label") .text_size(px(20.)) .font_semibold() .line_height(rems(1.8)) ``` ### Masked Labels ```rust // For sensitive information Label::new("9,182,1 USD") .text_2xl() .masked(true) // Shows as "•••••••••••" // Toggle masking programmatically Label::new("500 USD") .text_xl() .masked(self.masked) ``` ### Multi-line Text ```rust // Text wrapping with line height div().w(px(200.)).child( Label::new( "Label should support text wrap in default, \ if the text is too long, it should wrap to the next line." ) .line_height(rems(1.8)) ) ``` ### Different Sizes ```rust // Using text size utilities Label::new("Extra Large").text_2xl() Label::new("Large").text_xl() Label::new("Medium").text_base() // default Label::new("Small").text_sm() Label::new("Extra Small").text_xs() ``` ## API Reference ### Label | Method | Description | | ------------------- | ------------------------------------------------------------- | | `new(text)` | Create a new label with text | | `secondary(text)` | Add secondary text (usually for optional/required indicators) | | `masked(bool)` | Show/hide text with bullet characters | | `highlights(match)` | Highlight matching text | ### HighlightsMatch | Variant | Description | | -------------- | ------------------------------------------------ | | `Full(text)` | Highlights all occurrences of the text | | `Prefix(text)` | Highlights only if text appears at the beginning | | Method | Description | | ------------- | ------------------------------- | | `as_str()` | Get the search text as string | | `is_prefix()` | Check if this is a prefix match | ### Styling Methods (via Styled trait) | Method | Description | | --------------------- | --------------------------- | | `text_color(color)` | Set text color | | `text_size(size)` | Set font size | | `text_center()` | Center align text | | `text_right()` | Right align text | | `font_semibold()` | Set font weight to semibold | | `font_bold()` | Set font weight to bold | | `line_height(height)` | Set line height | | `text_xs()` | Extra small text size | | `text_sm()` | Small text size | | `text_base()` | Base text size (default) | | `text_lg()` | Large text size | | `text_xl()` | Extra large text size | | `text_2xl()` | 2x large text size | ## Examples ### Form Labels ```rust // Required field Label::new("Email Address") .secondary("*") .text_color(cx.theme().destructive) // Optional field Label::new("Phone Number") .secondary("(optional)") // Field with description Label::new("Password") .secondary("(minimum 8 characters)") ``` ### Search Highlighting ```rust // Interactive search highlighting let search_term = "Hello"; Label::new("Hello World Hello Universe") .highlights(search_term) // Highlights all "Hello" occurrences ``` ### Sensitive Information ```rust // Financial data with toggle h_flex() .child( Label::new("$9,182.50 USD") .text_2xl() .masked(self.is_masked) ) .child( Button::new("toggle-mask") .ghost() .icon(if self.is_masked { IconName::EyeOff } else { IconName::Eye }) .on_click(|this, _, _, _| { this.is_masked = !this.is_masked; }) ) ``` ### Multi-language Support ```rust // Supports Unicode text Label::new("这是一个标签") // Chinese text Label::new("こんにちは世界") // Japanese text Label::new("🌍 Hello World 🚀") // Emojis ``` ### Status Indicators ```rust // Success status Label::new("✓ Verified") .text_color(cx.theme().success) // Warning status Label::new("⚠ Pending Review") .text_color(cx.theme().warning) // Error status Label::new("✗ Failed") .text_color(cx.theme().destructive) ``` ### Custom Layouts ```rust // Flex layout with labels h_flex() .justify_between() .child(Label::new("Total Amount")) .child(Label::new("$1,234.56").font_semibold()) // Grid layout v_flex() .gap_2() .child(Label::new("Name:").font_semibold()) .child(Label::new("John Doe")) .child(Label::new("Email:").font_semibold()) .child(Label::new("john@example.com")) ``` --- --- url: /zh-CN/docs/components/label.md description: 支持高亮、次要文本和样式定制的文本标签组件。 --- # Label Label 是一个灵活的文本标签组件,可用于表单标签、说明文字和通用文本展示。它支持次要文本、高亮、掩码显示以及丰富的样式定制。 ## 导入 ```rust use gpui_kit::component::label::{Label, HighlightsMatch}; ``` ## 用法 ### 基础标签 ```rust Label::new("This is a label") ``` ### 带次要文本 ```rust Label::new("Company Address") .secondary("(optional)") Label::new("Email Address") .secondary("(required)") ``` ### 文本对齐 ```rust Label::new("Text align left") Label::new("Text align center") .text_center() Label::new("Text align right") .text_right() ``` ### 文本高亮 ```rust Label::new("Hello World Hello") .highlights("Hello") Label::new("Hello World") .highlights(HighlightsMatch::Prefix("Hello".into())) Label::new("Company Name") .secondary("(optional)") .highlights("Company") ``` ### 颜色与字体样式 ```rust use gpui_kit::component::green_500; Label::new("Color Label") .text_color(green_500()) Label::new("Font Size Label") .text_size(px(20.)) .font_semibold() .line_height(rems(1.8)) ``` ### 掩码文本 ```rust Label::new("9,182,1 USD") .text_2xl() .masked(true) Label::new("500 USD") .text_xl() .masked(self.masked) ``` ### 多行文本 ```rust div().w(px(200.)).child( Label::new( "Label should support text wrap in default, \ if the text is too long, it should wrap to the next line." ) .line_height(rems(1.8)) ) ``` ### 不同尺寸 ```rust Label::new("Extra Large").text_2xl() Label::new("Large").text_xl() Label::new("Medium").text_base() Label::new("Small").text_sm() Label::new("Extra Small").text_xs() ``` ## API 参考 ### Label | 方法 | 说明 | | ------------------- | ------------------------------------------------------------- | | `new(text)` | 使用文本创建标签 | | `secondary(text)` | 添加次要文本,常用于 optional 或 required 标识 | | `masked(bool)` | 使用圆点字符隐藏文本 | | `highlights(match)` | 高亮匹配内容 | ### HighlightsMatch | 变体 | 说明 | | -------------- | ------------------------------------------------ | | `Full(text)` | 高亮所有匹配内容 | | `Prefix(text)` | 仅在文本开头匹配时高亮 | | 方法 | 说明 | | ------------- | ------------------------------- | | `as_str()` | 获取匹配字符串 | | `is_prefix()` | 判断是否为前缀匹配 | ### 样式方法(来自 Styled trait) | 方法 | 说明 | | --------------------- | --------------------------- | | `text_color(color)` | 设置文字颜色 | | `text_size(size)` | 设置字体大小 | | `text_center()` | 居中对齐 | | `text_right()` | 右对齐 | | `font_semibold()` | 半粗体 | | `font_bold()` | 粗体 | | `line_height(height)` | 设置行高 | | `text_xs()` | 超小字号 | | `text_sm()` | 小字号 | | `text_base()` | 默认字号 | | `text_lg()` | 大字号 | | `text_xl()` | 超大字号 | | `text_2xl()` | 2 倍大字号 | ## 示例 ### 表单标签 ```rust Label::new("Email Address") .secondary("*") .text_color(cx.theme().destructive) Label::new("Phone Number") .secondary("(optional)") Label::new("Password") .secondary("(minimum 8 characters)") ``` ### 搜索高亮 ```rust let search_term = "Hello"; Label::new("Hello World Hello Universe") .highlights(search_term) ``` ### 敏感信息 ```rust h_flex() .child( Label::new("$9,182.50 USD") .text_2xl() .masked(self.is_masked) ) .child( Button::new("toggle-mask") .ghost() .icon(if self.is_masked { IconName::EyeOff } else { IconName::Eye }) .on_click(|this, _, _, _| { this.is_masked = !this.is_masked; }) ) ``` ### 多语言支持 ```rust Label::new("这是一个标签") Label::new("こんにちは世界") Label::new("🌍 Hello World 🚀") ``` ### 状态提示 ```rust Label::new("✓ Verified") .text_color(cx.theme().success) Label::new("⚠ Pending Review") .text_color(cx.theme().warning) Label::new("✗ Failed") .text_color(cx.theme().destructive) ``` ### 自定义布局 ```rust h_flex() .justify_between() .child(Label::new("Total Amount")) .child(Label::new("$1,234.56").font_semibold()) v_flex() .gap_2() .child(Label::new("Name:").font_semibold()) .child(Label::new("John Doe")) .child(Label::new("Email:").font_semibold()) .child(Label::new("john@example.com")) ``` --- --- url: /base/primitives/link.md description: An accessible link-like control with application-defined styling. --- # Link An accessible link-like control with application-defined styling. Like every `gpui-base` primitive, Link supplies behavior and semantic structure without imposing a product visual language. Apply GPUI styles and compose the exported parts to match your design system. ## Example The [single native Cargo entrypoint](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/components.rs) selects this primitive from the [shared showcase implementation](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/mod.rs). The same showcase is compiled once for the WASM preview above. ```bash cargo run -p gpui-base --example components -- link ``` ## Import ```rust use gpui_kit::base::{Link}; ``` ## Anatomy and API The example composes `Link`. GPUI's standard styling and event traits provide presentation; these base types provide the interaction structure. The authoritative module is [`components/link.rs`](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/components/link.rs). Native and browser previews compile this same file. ## State and events The link emits activation while the application defines URL or in-app navigation. Keep controlled state on the parent render type or in a GPUI entity. Update it in callbacks and call `cx.notify()`; do not recreate persistent entities during every render. ## Complete Rust example The complete implementation used by the runnable showcase is embedded directly from Rust source: The command above supplies application initialization, window creation, and shared `BaseShowcase` state. ## Accessibility Use links for navigation, meaningful text, and a visible focus style. ## Notes Use stable element IDs where accepted. Verify focus, hover, active, selected, disabled, reduced-motion, and high-contrast appearances in the consuming design system. --- --- url: /zh-CN/base/primitives/link.md description: 样式由应用定义的可访问链接控件。 --- # Link 样式由应用定义的可访问链接控件。 和所有 GPUI Base 原语一样,Link 只提供行为和语义结构,不规定产品视觉语言。请使用 GPUI 样式并组合导出的部件,使其符合你的设计系统。 ## 示例 原生示例和页面上方的 WASM 预览共用同一份实现: ```bash cargo run -p gpui-base --example components -- link ``` ## 导入 ```rust use gpui_kit::base::{Link}; ``` ## 结构与 API 示例组合上述公开类型。GPUI 的标准样式和事件 trait 负责表现,Base 类型负责交互结构。权威实现位于 [`components/link.rs`](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/components/link.rs),原生与浏览器预览编译的是同一文件。 ## 状态与事件 激活交由应用的导航或打开 URL 行为处理。 受控状态应保存在父渲染类型或 GPUI entity 中;在回调中更新并调用 `cx.notify()`,不要在每次渲染时重建持久 entity。 ## 完整 Rust 示例 ## 可访问性 使用清晰链接文本,保持键盘焦点,并正确表达目标与禁用状态。 ## 注意事项 在支持的位置使用稳定元素 ID,并在消费端设计系统中验证焦点、悬停、按下、选中、禁用、减少动态效果和高对比度状态。 --- --- url: /docs/components/list.md description: >- A flexible list component that displays a series of items with support for sections, search, selection, and infinite scrolling. --- # List A powerful List component that provides a virtualized, searchable list interface with support for sections, headers, footers, selection states, and infinite scrolling. The component is built on a delegate pattern that allows for flexible data management and custom item rendering. ## Import ```rust use gpui_kit::component::list::{List, ListState, ListDelegate, ListItem, ListEvent, ListSeparatorItem}; use gpui_kit::component::IndexPath; ``` ## Usage ### Basic List To create a list, you need to implement the `ListDelegate` trait for your data: ```rust struct MyListDelegate { items: Vec, selected_index: Option, } impl ListDelegate for MyListDelegate { type Item = ListItem; fn items_count(&self, _section: usize, _cx: &App) -> usize { self.items.len() } fn render_item( &mut self, ix: IndexPath, _window: &mut Window, _cx: &mut Context>, ) -> Option { self.items.get(ix.row).map(|item| { ListItem::new(ix) .child(Label::new(item.clone())) .selected(Some(ix) == self.selected_index) }) } fn set_selected_index( &mut self, ix: Option, _window: &mut Window, cx: &mut Context>, ) { self.selected_index = ix; cx.notify(); } } // Create the list let delegate = MyListDelegate { items: vec!["Item 1".into(), "Item 2".into(), "Item 3".into()], selected_index: None, }; /// Create a list state. let state = cx.new(|cx| ListState::new(delegate, window, cx)); ``` Now use \[List] to render list: ```rs div().child(List::new(&state)) ``` ### List with Sections **Note:** Sections with `items_count` of 0 will be automatically hidden (no header or footer will be rendered for empty sections). ```rust impl ListDelegate for MyListDelegate { type Item = ListItem; fn sections_count(&self, _cx: &App) -> usize { 3 // Number of sections } fn items_count(&self, section: usize, _cx: &App) -> usize { match section { 0 => 5, 1 => 3, 2 => 7, _ => 0, } } fn render_section_header( &mut self, section: usize, _window: &mut Window, cx: &mut Context>, ) -> Option { let title = match section { 0 => "Section 1", 1 => "Section 2", 2 => "Section 3", _ => return None, }; Some( h_flex() .px_2() .py_1() .gap_2() .text_sm() .text_color(cx.theme().muted_foreground) .child(Icon::new(IconName::Folder)) .child(title) ) } fn render_section_footer( &mut self, section: usize, _window: &mut Window, cx: &mut Context>, ) -> Option { Some( div() .px_2() .py_1() .text_xs() .text_color(cx.theme().muted_foreground) .child(format!("End of section {}", section + 1)) ) } } ``` ### List Items with Icons and Actions ```rust fn render_item( &mut self, ix: IndexPath, _window: &mut Window, cx: &mut Context>, ) -> Option { self.items.get(ix.row).map(|item| { ListItem::new(ix) .child( h_flex() .items_center() .gap_2() .child(Icon::new(IconName::File)) .child(Label::new(item.title.clone())) ) .suffix(|_, _| { Button::new("action") .ghost() .small() .icon(IconName::MoreHorizontal) }) .selected(Some(ix) == self.selected_index) .on_click(cx.listener(move |this, _, window, cx| { this.delegate_mut().select_item(ix, window, cx); })) }) } ``` ### List with Search The list automatically includes a search input by default. Implement `perform_search` to handle queries: And you should use `searchable(true)` when creating the `ListState` to show search input. ```rust impl ListDelegate for MyListDelegate { fn perform_search( &mut self, query: &str, _window: &mut Window, _cx: &mut Context>, ) -> Task<()> { // Filter items based on query self.filtered_items = self.all_items .iter() .filter(|item| item.to_lowercase().contains(&query.to_lowercase())) .cloned() .collect(); Task::ready(()) } } let state = cx.new(|cx| ListState::new(delegate, window, cx).searchable(true)); List::new(&state) ``` ### List with Loading State ```rust impl ListDelegate for MyListDelegate { fn loading(&self, _cx: &App) -> bool { self.is_loading } fn render_loading( &mut self, _window: &mut Window, _cx: &mut Context>, ) -> impl IntoElement { // Custom loading view v_flex() .justify_center() .items_center() .py_4() .child(Skeleton::new().h_4().w_full()) .child(Skeleton::new().h_4().w_3_4()) } } ``` ### Infinite Scrolling ```rust impl ListDelegate for MyListDelegate { fn has_more(&self, _cx: &App) -> bool { self.has_more_data } fn load_more_threshold(&self) -> usize { 20 // Trigger when 20 items from bottom } fn load_more(&mut self, window: &mut Window, cx: &mut Context>) { if self.is_loading { return; } self.is_loading = true; cx.spawn_in(window, async move |view, window| { // Simulate API call Timer::after(Duration::from_secs(1)).await; view.update_in(window, |view, _, cx| { // Add more items view.delegate_mut().load_more_items(); view.delegate_mut().is_loading = false; cx.notify(); }); }).detach(); } } ``` ### List Events ```rust // Subscribe to list events let _subscription = cx.subscribe(&state, |_, _, event: &ListEvent, _| { match event { ListEvent::Select(ix) => { println!("Item selected at: {:?}", ix); } ListEvent::Confirm(ix) => { println!("Item confirmed at: {:?}", ix); } ListEvent::Cancel => { println!("Selection cancelled"); } } }); ``` ### Different Item Styles ```rust // Basic item with hover effect ListItem::new(ix) .child(Label::new("Basic Item")) .selected(is_selected) // Item with check icon ListItem::new(ix) .child(Label::new("Checkable Item")) .check_icon(IconName::Check) .confirmed(is_confirmed) // Disabled item ListItem::new(ix) .child(Label::new("Disabled Item")) .disabled(true) // Separator item ListSeparatorItem::new() .child( div() .h_px() .w_full() .bg(cx.theme().border) ) ``` ### Drag and Drop Reordering `ListItem` implements GPUI's `InteractiveElement` and `StatefulInteractiveElement` traits, so all native interaction APIs such as `on_drag`, `on_drop`, `drag_over` and `on_hover` are directly available: ```rust #[derive(Clone)] struct DragItem { ix: IndexPath, name: SharedString, } impl Render for DragItem { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { // The preview element that follows the cursor while dragging. div() .px_2() .py_1() .bg(cx.theme().accent) .text_color(cx.theme().accent_foreground) .rounded(cx.theme().radius) .child(self.name.clone()) } } // In `render_item` of your `ListDelegate`: ListItem::new(ix) .child(Label::new(item.name.clone())) .on_drag(DragItem { ix, name: item.name.clone() }, |drag, _, _, cx| { cx.new(|_| drag.clone()) }) .drag_over::(|style, _, _, cx| style.bg(cx.theme().drop_target)) .on_drop(cx.listener(move |this, drag: &DragItem, _, cx| { this.delegate_mut().move_item(drag.ix, ix); cx.notify(); })) ``` ### Custom Empty State ```rust impl ListDelegate for MyListDelegate { fn render_empty(&mut self, _window: &mut Window, cx: &mut Context>) -> impl IntoElement { v_flex() .size_full() .justify_center() .items_center() .gap_2() .child(Icon::new(IconName::Search).size_16().text_color(cx.theme().muted_foreground)) .child( Label::new("No items found") .text_color(cx.theme().muted_foreground) ) .child( Label::new("Try adjusting your search terms") .text_sm() .text_color(cx.theme().muted_foreground.opacity(0.7)) ) } } ``` ## Configuration Options ### List Configuration ```rust List::new(&state) .max_h(px(400.)) // Set maximum height .scrollbar_visible(false) // Hide scrollbar .paddings(Edges::all(px(8.))) // Set internal padding ``` ### Scrolling Control ```rust // Scroll to specific item state.update(cx, |state, cx| { state.scroll_to_item( IndexPath::new(0).section(1), // Row 0 of section 1 ScrollStrategy::Center, window, cx, ); }); // Scroll to selected item state.update(cx, |state, cx| { state.scroll_to_selected_item(window, cx); }); // Set selected index without scrolling state.update(cx, |state, cx| { state.set_selected_index(Some(IndexPath::new(5)), window, cx); }); ``` ## Examples ### File Browser List ```rust struct FileBrowserDelegate { files: Vec, selected: Option, } #[derive(Clone)] struct FileInfo { name: String, is_directory: bool, size: Option, } impl ListDelegate for FileBrowserDelegate { type Item = ListItem; fn render_item(&mut self, ix: IndexPath, window: &mut Window, cx: &mut Context>) -> Option { self.files.get(ix.row).map(|file| { let icon = if file.is_directory { IconName::Folder } else { IconName::File }; ListItem::new(ix) .child( h_flex() .items_center() .justify_between() .w_full() .child( h_flex() .items_center() .gap_2() .child(Icon::new(icon)) .child(Label::new(file.name.clone())) ) .when_some(file.size, |this, size| { this.child( Label::new(format_size(size)) .text_sm() .text_color(cx.theme().muted_foreground) ) }) ) .selected(Some(ix) == self.selected) }) } } ``` ### Contact List with Sections ```rust struct ContactListDelegate { contacts_by_letter: BTreeMap>, selected: Option, } impl ListDelegate for ContactListDelegate { type Item = ListItem; fn sections_count(&self, _cx: &App) -> usize { self.contacts_by_letter.len() } fn render_section_header(&mut self, section: usize, _window: &mut Window, cx: &mut Context>) -> Option { let letter = self.contacts_by_letter.keys().nth(section)?; Some( div() .px_3() .py_2() .bg(cx.theme().background) .border_b_1() .border_color(cx.theme().border) .child( Label::new(letter.to_string()) .text_lg() .text_color(cx.theme().accent_foreground) .font_weight(FontWeight::BOLD) ) ) } } ``` --- --- url: /zh-CN/docs/components/list.md description: 支持分组、搜索、选择和无限滚动的灵活列表组件。 --- # List List 是一个功能完整的列表组件,支持虚拟化展示、搜索、分组、头部和底部区域、选择状态以及无限滚动。它基于 delegate 模式构建,数据管理和项渲染都可以按业务场景自由扩展。 ## 导入 ```rust use gpui_kit::component::list::{List, ListState, ListDelegate, ListItem, ListEvent, ListSeparatorItem}; use gpui_kit::component::IndexPath; ``` ## 用法 ### 基础列表 创建列表前,需要先为数据实现 `ListDelegate`: ```rust struct MyListDelegate { items: Vec, selected_index: Option, } impl ListDelegate for MyListDelegate { type Item = ListItem; fn items_count(&self, _section: usize, _cx: &App) -> usize { self.items.len() } fn render_item( &mut self, ix: IndexPath, _window: &mut Window, _cx: &mut Context>, ) -> Option { self.items.get(ix.row).map(|item| { ListItem::new(ix) .child(Label::new(item.clone())) .selected(Some(ix) == self.selected_index) }) } fn set_selected_index( &mut self, ix: Option, _window: &mut Window, cx: &mut Context>, ) { self.selected_index = ix; cx.notify(); } } let delegate = MyListDelegate { items: vec!["Item 1".into(), "Item 2".into(), "Item 3".into()], selected_index: None, }; let state = cx.new(|cx| ListState::new(delegate, window, cx)); ``` 渲染列表: ```rs div().child(List::new(&state)) ``` ### 分组列表 注意:`items_count` 为 `0` 的 section 会被自动隐藏,不会渲染 header 或 footer。 ```rust impl ListDelegate for MyListDelegate { type Item = ListItem; fn sections_count(&self, _cx: &App) -> usize { 3 } fn items_count(&self, section: usize, _cx: &App) -> usize { match section { 0 => 5, 1 => 3, 2 => 7, _ => 0, } } fn render_section_header( &mut self, section: usize, _window: &mut Window, cx: &mut Context>, ) -> Option { let title = match section { 0 => "Section 1", 1 => "Section 2", 2 => "Section 3", _ => return None, }; Some( h_flex() .px_2() .py_1() .gap_2() .text_sm() .text_color(cx.theme().muted_foreground) .child(Icon::new(IconName::Folder)) .child(title) ) } } ``` ### 带图标和操作的列表项 ```rust fn render_item( &mut self, ix: IndexPath, _window: &mut Window, cx: &mut Context>, ) -> Option { self.items.get(ix.row).map(|item| { ListItem::new(ix) .child( h_flex() .items_center() .gap_2() .child(Icon::new(IconName::File)) .child(Label::new(item.title.clone())) ) .suffix(|_, _| { Button::new("action") .ghost() .small() .icon(IconName::MoreHorizontal) }) .selected(Some(ix) == self.selected_index) .on_click(cx.listener(move |this, _, window, cx| { this.delegate_mut().select_item(ix, window, cx); })) }) } ``` ### 可搜索列表 实现 `perform_search` 处理查询逻辑,并在 `ListState` 上启用 `searchable(true)`: ```rust impl ListDelegate for MyListDelegate { fn perform_search( &mut self, query: &str, _window: &mut Window, _cx: &mut Context>, ) -> Task<()> { self.filtered_items = self.all_items .iter() .filter(|item| item.to_lowercase().contains(&query.to_lowercase())) .cloned() .collect(); Task::ready(()) } } let state = cx.new(|cx| ListState::new(delegate, window, cx).searchable(true)); List::new(&state) ``` ### 加载状态 ```rust impl ListDelegate for MyListDelegate { fn loading(&self, _cx: &App) -> bool { self.is_loading } fn render_loading( &mut self, _window: &mut Window, _cx: &mut Context>, ) -> impl IntoElement { v_flex() .justify_center() .items_center() .py_4() .child(Skeleton::new().h_4().w_full()) .child(Skeleton::new().h_4().w_3_4()) } } ``` ### 无限滚动 ```rust impl ListDelegate for MyListDelegate { fn has_more(&self, _cx: &App) -> bool { self.has_more_data } fn load_more_threshold(&self) -> usize { 20 } fn load_more(&mut self, window: &mut Window, cx: &mut Context>) { if self.is_loading { return; } self.is_loading = true; cx.spawn_in(window, async move |view, window| { Timer::after(Duration::from_secs(1)).await; view.update_in(window, |view, _, cx| { view.delegate_mut().load_more_items(); view.delegate_mut().is_loading = false; cx.notify(); }); }).detach(); } } ``` ### 列表事件 ```rust let _subscription = cx.subscribe(&state, |_, _, event: &ListEvent, _| { match event { ListEvent::Select(ix) => { println!("Item selected at: {:?}", ix); } ListEvent::Confirm(ix) => { println!("Item confirmed at: {:?}", ix); } ListEvent::Cancel => { println!("Selection cancelled"); } } }); ``` ### 拖拽重排 `ListItem` 实现了 GPUI 的 `InteractiveElement` 和 `StatefulInteractiveElement` trait, 因此 `on_drag`、`on_drop`、`drag_over`、`on_hover` 等原生交互 API 均可直接使用: ```rust #[derive(Clone)] struct DragItem { ix: IndexPath, name: SharedString, } impl Render for DragItem { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { // 拖拽时跟随光标的预览元素。 div() .px_2() .py_1() .bg(cx.theme().accent) .text_color(cx.theme().accent_foreground) .rounded(cx.theme().radius) .child(self.name.clone()) } } // 在 `ListDelegate` 的 `render_item` 中: ListItem::new(ix) .child(Label::new(item.name.clone())) .on_drag(DragItem { ix, name: item.name.clone() }, |drag, _, _, cx| { cx.new(|_| drag.clone()) }) .drag_over::(|style, _, _, cx| style.bg(cx.theme().drop_target)) .on_drop(cx.listener(move |this, drag: &DragItem, _, cx| { this.delegate_mut().move_item(drag.ix, ix); cx.notify(); })) ``` ### 自定义空状态 ```rust impl ListDelegate for MyListDelegate { fn render_empty(&mut self, _window: &mut Window, cx: &mut Context>) -> impl IntoElement { v_flex() .size_full() .justify_center() .items_center() .gap_2() .child(Icon::new(IconName::Search).size_16().text_color(cx.theme().muted_foreground)) .child( Label::new("No items found") .text_color(cx.theme().muted_foreground) ) .child( Label::new("Try adjusting your search terms") .text_sm() .text_color(cx.theme().muted_foreground.opacity(0.7)) ) } } ``` ## 配置选项 ### 列表配置 ```rust List::new(&state) .max_h(px(400.)) .scrollbar_visible(false) .paddings(Edges::all(px(8.))) ``` ### 滚动控制 ```rust state.update(cx, |state, cx| { state.scroll_to_item( IndexPath::new(0).section(1), ScrollStrategy::Center, window, cx, ); }); state.update(cx, |state, cx| { state.scroll_to_selected_item(window, cx); }); state.update(cx, |state, cx| { state.set_selected_index(Some(IndexPath::new(5)), window, cx); }); ``` --- --- url: /docs/components/marker.md description: >- A compact composable row for conversation status, notifications, loading, and separators. --- # Marker `Marker` is a lightweight row for status text, timeline boundaries, unread labels, and system notices. It deliberately accepts arbitrary children instead of defining an application-specific status enum. `MarkerIcon` and `MarkerContent` are optional typed slots for the common icon-and-label shape; direct children remain available for custom composition. `Marker` is a layout and loading primitive. It does not own a notification record, an unread count, a click action, or a live status store. Compose those with application state and existing `Badge`, `Button`, `Link`, or navigation components. ## Import ```rust use gpui_kit::{ParentElement as _, StyleRefinement, Styled as _}; use gpui_kit::component::{ ActiveTheme as _, Icon, IconName, Sizable as _, badge::Badge, button::{Button, ButtonVariants as _}, marker::{Marker, MarkerContent, MarkerIcon, MarkerLoadingStyle, MarkerVariant}, shimmer::{ShimmerStyle, ShimmerText}, spinner::Spinner, }; use std::time::Duration; ``` ## Anatomy and basic usage The typed form keeps icon and content style targets independent: ```rust Marker::new() .icon(MarkerIcon::new().child(Icon::new(IconName::CircleCheck))) .content(MarkerContent::new().text("Online")) ``` Direct children are useful when a marker needs an application-specific layout: ```rust Marker::new() .child(Icon::new(IconName::Info)) .child("Conversation archived") ``` The default state is: | Property | Default | Meaning | | --- | --- | --- | | Variant | `Plain` | A full-width status row without divider decoration. | | Loading | `false` | No automatic loading effect. | | Loading style | `Spinner` | Used when loading is enabled. | | Icon slot | absent | A spinner is inserted only for spinner loading with no icon. | | Content | absent | Add text or arbitrary child content. | | Row minimum height | `rems(1.)` | Follows the shared typography scale. | | Row gap | `gap_2()` | Shared compact spacing. | Use `MarkerContent::text(...)` for text that should receive the loading shimmer. Use `.child(...)` for arbitrary elements or text that should keep its own rendering behavior. ## Variants ### Plain `Plain` is the default compact status row: ```rust Marker::new() .text_color(cx.theme().success) .icon(MarkerIcon::new().child(Icon::new(IconName::CircleCheck))) .content(MarkerContent::new().text("Synced")) ``` The library does not define `Online`, `Read`, `Typing`, or `Synced` values. The application supplies the words, icon, and semantic color so the same primitive can serve different domains. ### Separator `Separator` adds a flexible line on each side of the content: ```rust Marker::new() .with_variant(MarkerVariant::Separator) .content(MarkerContent::new().text("Today")) ``` The line is an internal 1-pixel decorative element. The label remains the semantic content. Use `separator_style(...)` to refine the two lines without having to recreate their layout: ```rust Marker::new() .with_variant(MarkerVariant::Separator) .separator_style( StyleRefinement::default() .bg(cx.theme().ring), ) .content(MarkerContent::new().text("Yesterday")) ``` ### Border `Border` adds a semantic bottom border and compact bottom padding: ```rust Marker::new() .with_variant(MarkerVariant::Border) .icon(MarkerIcon::new().child(Icon::new(IconName::Info))) .content(MarkerContent::new().text("3 unread messages")) ``` The border is a visual boundary. Keep the unread count and meaning in text so the state does not depend on color or a line alone. ## Loading styles Set `loading(true)` without changing the marker's variant or normal layout: ```rust Marker::new() .loading(true) .with_loading_style(MarkerLoadingStyle::Spinner) .content(MarkerContent::new().text("Loading messages…")); Marker::new() .loading(true) .with_loading_style(MarkerLoadingStyle::Shimmer) .content(MarkerContent::new().text("Thinking…")) ``` Spinner behavior is intentionally predictable: * `Spinner` is the default `MarkerLoadingStyle`. * If loading uses `Spinner` and no `MarkerIcon` was supplied, a compact `Spinner::new().xsmall()` is inserted automatically. * If the application supplies `MarkerIcon`, that icon wins and no automatic spinner is added. * `MarkerVariant::Separator` still renders its divider lines while loading. * `MarkerVariant::Border` still renders its border while loading. Shimmer is text-aware when content was added with `.text(...)`: ```rust Marker::new() .loading(true) .with_loading_style(MarkerLoadingStyle::Shimmer) .content(MarkerContent::new().text("Generating a response…")) ``` Arbitrary `MarkerContent` children are still supported. When there is no typed text child, the content slot receives a gentle opacity animation instead. Icons and separator lines stay static. When reduced motion is enabled, text is rendered without animation and the marker remains readable. ## Shimmer configuration Use one `ShimmerStyle` for a marker's text effect: ```rust Marker::new() .loading(true) .with_loading_style(MarkerLoadingStyle::Shimmer) .with_shimmer_style( ShimmerStyle::new() .duration(Duration::from_secs(3)) .highlight_color(cx.theme().primary) .spread(0.45) .reverse(true) .once(false), ) .content(MarkerContent::new().text("Processing files…")) ``` The `ShimmerStyle` defaults are a two-second repeating sweep, theme-aware highlight color, `0.3` normalized spread, left-to-right direction, and looping. `duration(...)` clamps values below one millisecond. `spread(...)` accepts a relative `f32` (clamped to `0.05..=1.0`) or an absolute `Pixels` half-width; non-finite values leave the current spread unchanged. `reverse(true)` changes the direction, and `once(true)` stops after one sweep. For a marker-independent loading label, use `ShimmerText` directly: ```rust ShimmerText::new("Uploading report.pdf…") .with_shimmer_style(ShimmerStyle::new().spread(0.4)) .text_sm() .text_color(cx.theme().muted_foreground) ``` `ShimmerText` inherits typography and text color through `Styled`, preserves wrapping and truncation, and uses the active theme's background and foreground to keep the highlight readable in light and dark modes. ## Icons, content, and interactive children `MarkerIcon` is a compact `size_4()` slot. `MarkerContent` is a `min_w_0()` slot, so a long label can choose its own wrapping or truncation: ```rust Marker::new() .icon(MarkerIcon::new().child(Icon::new(IconName::Bell))) .content( MarkerContent::new() .child("Unread notifications") .child(Badge::new().count(3)), ) ``` Interactive children are allowed, but `Marker` does not make the row itself a control: ```rust Marker::new() .content( MarkerContent::new() .text("New messages") .child(Button::new("open-messages").ghost().xsmall().label("Open")), ) ``` Use `Button` for an in-app command and `Link` for a URL. Keep focus and action semantics on those controls. If a whole marker should be clickable, compose a semantic control around the content at the application boundary instead of adding a click listener to this layout element. ## Custom styling and theme tokens `Marker`, `MarkerIcon`, and `MarkerContent` implement `Styled`. Refinements are applied after the default layout and theme colors: ```rust Marker::new() .px_3() .py_2() .rounded(cx.theme().radius) .bg(cx.theme().accent) .text_color(cx.theme().accent_foreground) .icon(MarkerIcon::new().child(Icon::new(IconName::Star))) .content(MarkerContent::new().text("Pinned message")) ``` The separator lines have a separate `StyleRefinement`, so their color and height can be customized without changing the content or marker's own surface: ```rust Marker::new() .with_variant(MarkerVariant::Separator) .separator_style( StyleRefinement::default() .bg(cx.theme().border), ) .content(MarkerContent::new().text("New day")) ``` Prefer semantic theme roles (`muted_foreground`, `border`, `ring`, `accent`, and their foreground tokens) to raw colors. Radius, spacing, typography, and separator geometry follow the shared design scale; typed style refinements can adapt a marker to a denser toolbar or a larger empty-state boundary. ## Accessibility and motion guidance * Include the status, boundary, or unread count in text. Icons, border lines, opacity, and color are supporting cues only. * A marker is presentational by default. Set `.id(...)` and `.role(Role::Status)` on a row that reports streaming or loading progress so assistive technology announces its updates; the role needs the stable identity an id provides. * Keep interactive content in `Button` or `Link` so it receives keyboard focus, activation, and disabled state. For the current `Button` API, use a visible `.label(...)` when the action needs an accessible name; a tooltip is supplemental. * Do not use `Marker` as an unlabeled icon-only status. Add a visible or accessible text label when the icon has meaning. * `MarkerContent::text(...)` remains visible when reduced motion is enabled; only the shimmer frame updates are skipped. Arbitrary children also retain their static content. * Loading text should describe the operation (“Generating…”, “Uploading…”) rather than communicate only through animation. * Keep sufficient contrast after custom styling in both light and dark themes. ## When to use another component * Use `Badge` for only a count, dot, or short classification. * Use `Separator::horizontal().label(...)` when the product needs only a labeled divider and no marker loading or icon composition. * Use `Tag` for a standalone labeled status that is not part of a conversation row. * Use `h_flex()` when the row has no shared marker behavior. * Use `Message` or `Bubble` when the content is a conversational message with sender identity or a message surface. ## API reference ### `Marker` | Method | Default | Purpose | | --- | --- | --- | | `new()` | `Plain`, not loading, spinner style | Create a marker. | | `with_variant(MarkerVariant)` | `Plain` | Choose plain, separator, or border treatment. | | `loading(bool)` | `false` | Enable or disable loading rendering. | | `with_loading_style(MarkerLoadingStyle)` | `Spinner` | Choose spinner or shimmer. | | `with_shimmer_style(ShimmerStyle)` | default style | Configure text shimmer. | | `separator_style(StyleRefinement)` | theme border line | Refine separator lines. | | `id(ElementId)` | none | Give the marker a stable identity for the accessibility tree. | | `role(Role)` | presentational | Announce the row to assistive technology, e.g. `Role::Status` for streaming updates; requires `id(...)`. | | `icon(MarkerIcon)` | none | Add a typed icon slot. | | `content(MarkerContent)` | none | Add a typed content slot. | | `.child(element)` | — | Add arbitrary children. | | `Styled` methods | compact themed row | Refine the marker's layout, colors, and typography. | ### `MarkerIcon` | Method | Default | Purpose | | --- | --- | --- | | `new()` | empty `size_4()` slot | Create an icon slot. | | `.child(element)` | — | Add an icon, badge, spinner, or custom element. | | `Styled` methods | `size_4()` compact slot | Refine icon geometry and layout. | ### `MarkerContent` | Method | Default | Purpose | | --- | --- | --- | | `new()` | empty `min_w_0()` slot | Create content. | | `text(text)` | static text until loading is enabled | Add text that can receive shimmer. | | `.child(element)` | — | Add arbitrary rich content. | | `Styled` methods | inherited text and compact layout | Refine wrapping, colors, spacing, and typography. | ### Related types * \[`MarkerVariant`] — `Plain`, `Separator`, and `Border`. * \[`MarkerLoadingStyle`] — `Spinner` or `Shimmer`. * \[`ShimmerStyle`] and \[`ShimmerText`] — reusable loading text controls. [Marker]: https://docs.rs/gpui-component/latest/gpui_component/marker/struct.Marker.html [MarkerIcon]: https://docs.rs/gpui-component/latest/gpui_component/marker/struct.MarkerIcon.html [MarkerContent]: https://docs.rs/gpui-component/latest/gpui_component/marker/struct.MarkerContent.html [MarkerVariant]: https://docs.rs/gpui-component/latest/gpui_component/marker/enum.MarkerVariant.html [MarkerLoadingStyle]: https://docs.rs/gpui-component/latest/gpui_component/marker/enum.MarkerLoadingStyle.html [ShimmerStyle]: https://docs.rs/gpui-component/latest/gpui_component/shimmer/struct.ShimmerStyle.html [ShimmerText]: https://docs.rs/gpui-component/latest/gpui_component/shimmer/struct.ShimmerText.html --- --- url: /zh-CN/docs/components/marker.md description: 用于会话状态、通知边界和分隔标记的紧凑组合行。 --- # Marker `Marker` 是一种轻量的全宽会话行,适合状态文字、时间线边界、未读提示和系统消息。它只提供通用布局与视觉变体,不定义 `Online`、`Typing`、`Read` 等业务状态;图标、内容、颜色和交互行为由应用组合。 ## 适用场景 * 一行简短的系统状态或会话提示。 * 带有左右装饰线的日期、未读边界或阶段标题。 * 需要 Spinner 或文字 shimmer 的 loading 状态。 * 需要在行中放置 `Button` 或 `Link`,但不希望把整行变成一个按钮。 只有一个数量或状态标签时,`Badge` 或 `Tag` 更直接;只有一条带文字的分隔线时,使用 `Separator`。 ## 导入 ```rust use gpui_kit::{ParentElement as _, StyleRefinement, Styled as _}; use gpui_kit::component::{ button::{Button, ButtonVariants as _}, marker::{Marker, MarkerContent, MarkerIcon, MarkerLoadingStyle, MarkerVariant}, shimmer::{ShimmerStyle, ShimmerText}, spinner::Spinner, ActiveTheme as _, Colorize as _, Icon, IconName, Sizable as _, StyledExt as _, }; ``` ## 结构 ```text Marker ├── MarkerIcon # 可选,紧凑图标 slot ├── MarkerContent # 文本或富内容 slot └── 任意 child # Button、Link 或其他 GPUI element ``` `MarkerIcon` 和 `MarkerContent` 是有默认尺寸与文字布局的具名 slot;`.child(...)` 仍可用于应用自己的组合。`Marker` 本身保持为语义容器,不会替应用创建状态 enum。 ## Plain、Separator 与 Border `MarkerVariant` 提供三个有明确用途的表面: | Variant | 用途 | 默认布局 | | --- | --- | --- | | `Plain` | 普通状态或系统消息,默认值。 | 全宽紧凑行。 | | `Separator` | 日期、阶段或未读边界。 | 内容两侧显示主题边框线。 | | `Border` | 需要底部边界的状态行。 | 内容下方显示语义边框。 | ```rust Marker::new() .content(MarkerContent::new().text("会话已归档")); Marker::new() .with_variant(MarkerVariant::Separator) .content(MarkerContent::new().text("今天")); Marker::new() .with_variant(MarkerVariant::Border) .content(MarkerContent::new().text("3 条未读消息")) ``` Separator 的装饰线是内部实现,不携带语义内容。文本本身应说明它代表的日期、边界或状态。 ## 状态内容与图标 应用可以组合 Icon、Spinner、Badge 或自己的富内容: ```rust Marker::new() .text_color(cx.theme().primary) .icon( MarkerIcon::new() .child(Icon::new(IconName::CircleCheck)), ) .content(MarkerContent::new().text("在线")); Marker::new() .icon(MarkerIcon::new().child(Spinner::new().xsmall())) .content(MarkerContent::new().text("Alice 正在输入…")) ``` `MarkerIcon` 会保留紧凑的图标槽尺寸。图标和文字的间距、文字字号与行高跟随共享设计系统;自定义 child 不会被 Marker 改写其内部语义。 ## Loading 样式 通过 `.loading(true)` 启用 loading;loading 不会改变 Marker 的 variant 或普通布局。默认样式是 `Spinner`: ```rust Marker::new() .loading(true) .with_loading_style(MarkerLoadingStyle::Spinner) .content(MarkerContent::new().text("正在加载消息…")) ``` 没有显式 `MarkerIcon` 时,Spinner loading 会自动添加紧凑 Spinner;如果已经组合 `MarkerIcon`,显式图标优先: ```rust Marker::new() .loading(true) .with_loading_style(MarkerLoadingStyle::Spinner) .icon(MarkerIcon::new().child(Icon::new(IconName::LoaderCircle))) .content(MarkerContent::new().text("同步中…")) ``` 需要文字高光时使用 `Shimmer`: ```rust Marker::new() .loading(true) .with_loading_style(MarkerLoadingStyle::Shimmer) .content(MarkerContent::new().text("正在思考…")) ``` 只有通过 `MarkerContent::text(...)` 添加的文本会使用平滑移动的 shimmer。普通 child 仍然可用,但 loading 时会使用轻微透明度变化;图标和 Separator 装饰线保持静止。 ## 配置 Shimmer Marker 可以接受一个可复用的 `ShimmerStyle`。默认动画周期为两秒、spread 为 `0.3`、方向为从左到右并循环播放: ```rust use std::time::Duration; Marker::new() .loading(true) .with_loading_style(MarkerLoadingStyle::Shimmer) .with_shimmer_style( ShimmerStyle::new() .duration(Duration::from_secs(3)) .highlight_color(cx.theme().primary) .spread(0.45) .reverse(true) .once(true), ) .content(MarkerContent::new().text("正在处理…")) ``` 可以分别调整单项配置: ```rust // 更慢的循环 ShimmerStyle::new().duration(Duration::from_secs(4)); // 更窄的高光带;值会限制在 0.05..=1.0 ShimmerStyle::new().spread(0.15); // 使用当前主题中的语义色 ShimmerStyle::new().highlight_color(cx.theme().primary); // 从右向左移动,并只完成一次 ShimmerStyle::new().reverse(true).once(true) ``` `duration(Duration::ZERO)` 会被限制为至少一毫秒;`spread` 可传相对比例 `f32`(限制在 `0.05..=1.0`)或绝对宽度 `Pixels`,非有限值会保留当前值。显式 highlight color 适合产品有明确强调色的场景,默认值会根据文本色和当前主题计算。 ## 独立使用 ShimmerText 需要在 Marker 之外显示 thinking 或上传文案时,直接使用 `ShimmerText`: ```rust ShimmerText::new("正在上传 report.pdf…") .with_shimmer_style(ShimmerStyle::new().spread(0.4)) .text_sm() .text_color(cx.theme().muted_foreground) ``` 也可以使用 `duration(...)`、`highlight_color(...)`、`spread(...)`、`reverse(...)` 和 `once(...)` 的快捷 builder: ```rust ShimmerText::new("Generating…") .duration(std::time::Duration::from_secs(3)) .highlight_color(cx.theme().primary) .spread(0.35) .reverse(true) ``` `ShimmerText` 实现 `Styled`,会继承周围的字号、字体、文字颜色、换行和截断样式。完整参数与 reduced motion 行为见 \[Shimmer] 文档。 ## 链接和按钮 Marker 可以包含交互 child,但交互应保持局部、明确: ```rust Marker::new() .with_variant(MarkerVariant::Border) .content( MarkerContent::new() .text("同步失败") .child( Button::new("retry-sync") .ghost() .small() .label("重试"), ), ) ``` URL 使用 `Link`,应用操作使用 `Button`。Marker 不会自动给任意 child 添加 loading、selected 或 focus 语义。 ## 分隔线样式 `separator_style(...)` 只作用于 Separator 两侧的内部装饰线;内容本身的颜色和布局通过 `Marker`、`MarkerContent` 的 `Styled` refinement 调整: ```rust Marker::new() .with_variant(MarkerVariant::Separator) .separator_style( StyleRefinement::default() .bg(cx.theme().muted_foreground.opacity(0.35)), ) .content( MarkerContent::new() .text_color(cx.theme().muted_foreground) .text("2026 年 8 月 25 日"), ) ``` 如果需要完全不同的分隔布局,可以使用 `h_flex()` 与 `Separator`;不要把内部装饰线当作应用状态的独立节点。 ## 自定义样式与主题 token `Marker`、`MarkerIcon` 和 `MarkerContent` 都实现 `Styled`。默认样式之后应用调用方 refinement,因此只覆盖必要部分: ```rust Marker::new() .px_3() .py_2() .rounded(cx.theme().radius_lg) .bg(cx.theme().muted) .text_color(cx.theme().foreground) .icon( MarkerIcon::new() .text_color(cx.theme().primary) .child(Icon::new(IconName::Star)), ) .content(MarkerContent::new().text("已置顶消息")) ``` 优先使用 `cx.theme()` 的语义颜色与主题圆角。Marker 的外层布局、图标 slot、内容 slot 和 Separator 装饰线各自可以定制;这些 refinement 不会改变 loading 状态的业务所有权。 ## 可访问性与 reduced motion * 状态、进度、日期和未读边界必须有可读文本;颜色和装饰线只能提供辅助层次。 * Marker 默认是纯展示元素。表示流式或加载进度的行可以设置 `.id(...)` 加 `.role(Role::Status)`,让辅助技术播报其更新;role 依赖 id 提供的稳定标识。 * 只有图标的交互 child 使用 `Button` 并提供可见的 `.label(...)` 或其他可读名称;tooltip 只作为补充提示。 * Separator 线是装饰性的,语义应来自 `MarkerContent` 中的文字。 * `MarkerContent::text(...)` 的 Shimmer 在系统启用 reduced motion 时会显示静态文字,不请求动画帧。 * Marker 的普通 child 可能是自定义动画;应用应在 reduced motion 下提供清晰的静态状态。 * 不要让整个 Marker 成为 hover 才能发现的唯一操作入口;将操作放入明确可聚焦的 Button 或 Link。 ## 何时不需要 Marker * 只有数量、圆点或短状态时使用 `Badge`。 * 独立的标签状态使用 `Tag`。 * 只有一条带文字的分隔线时使用 `Separator::horizontal().label(...)`。 * 应用专属的 icon + text 行没有共享 Marker 语义时,使用 `h_flex()`。 * 需要头像、发送者、消息内容和 footer 时,使用 `Message`。 当这些内容需要统一的会话行表面,或要在 plain、separator、border 之间切换时,再使用 Marker。 ## API 参考 ### `Marker` | 方法 | 说明 | | --- | --- | | `new()` | 创建默认的 plain marker。 | | `with_variant(MarkerVariant)` | 设置 `Plain`、`Separator` 或 `Border`。 | | `loading(bool)` | 开启或关闭 loading。默认关闭。 | | `with_loading_style(MarkerLoadingStyle)` | 选择 `Spinner` 或 `Shimmer`。 | | `with_shimmer_style(ShimmerStyle)` | 配置文字 shimmer。 | | `separator_style(StyleRefinement)` | 调整 Separator 的内部装饰线。 | | `id(ElementId)` | 设置稳定标识,让 marker 进入无障碍树。 | | `role(Role)` | 设置辅助技术播报的 role;流式更新用 `Role::Status`,需要配合 `id(...)`,默认纯展示。 | | `icon(MarkerIcon)` | 添加配置过的图标 slot。 | | `content(MarkerContent)` | 添加配置过的内容 slot。 | | `child(element)` | 添加任意 child。 | | `Styled` | 调整 marker 的布局、颜色、间距和 surface。 | ### `MarkerIcon` / `MarkerContent` | 类型 | 方法 | 说明 | | --- | --- | --- | | `MarkerIcon` | `new()` / `child(element)` | 创建紧凑图标 slot。 | | `MarkerContent` | `new()` / `text(text)` | 创建内容 slot,`text` 允许 Shimmer 直接替换文字渲染。 | | 两者 | `Styled` | 调整各自的尺寸、颜色、文字和布局。 | ### 类型链接 * [Marker] * [MarkerVariant] * [MarkerLoadingStyle] * [MarkerIcon] * [MarkerContent] * [ShimmerStyle] * [ShimmerText] [Marker]: https://docs.rs/gpui-component/latest/gpui_component/marker/struct.Marker.html [MarkerVariant]: https://docs.rs/gpui-component/latest/gpui_component/marker/enum.MarkerVariant.html [MarkerLoadingStyle]: https://docs.rs/gpui-component/latest/gpui_component/marker/enum.MarkerLoadingStyle.html [MarkerIcon]: https://docs.rs/gpui-component/latest/gpui_component/marker/struct.MarkerIcon.html [MarkerContent]: https://docs.rs/gpui-component/latest/gpui_component/marker/struct.MarkerContent.html [ShimmerStyle]: https://docs.rs/gpui-component/latest/gpui_component/shimmer/struct.ShimmerStyle.html [ShimmerText]: https://docs.rs/gpui-component/latest/gpui_component/shimmer/struct.ShimmerText.html --- --- url: /docs/components/menu.md description: >- Context menus and popup menus with support for icons, shortcuts, submenus, and various menu item types. --- # PopupMenu The Menu component provides both context menus (right-click menus) and popup menus with comprehensive features including icons, keyboard shortcuts, submenus, separators, checkable items, and custom elements. Built with accessibility and keyboard navigation in mind. ## Import ```rust use gpui_kit::component::{ menu::{PopupMenu, PopupMenuItem, ContextMenuExt, DropdownMenu}, Button }; use gpui_kit::{actions, Action}; ``` ## Usage ### ContextMenu Context menus appear when right-clicking on an element: ```rust use gpui_kit::component::menu::ContextMenuExt; div() .id("my-element") .child("Right click me") .context_menu(|menu, window, cx| { menu.menu("Copy", Box::new(Copy)) .menu("Paste", Box::new(Paste)) .separator() .menu("Delete", Box::new(Delete)) }) ``` ### DropdownMenu Dropdown menus are triggered by buttons or other interactive elements: ```rust use gpui_kit::component::popup_menu::{PopupMenuExt as _, PopupMenuItem}; let view = cx.entity(); Button::new("menu-btn") .label("Open Menu") .dropdown_menu(|menu, window, cx| { menu.menu("New File", Box::new(NewFile)) .menu("Open File", Box::new(OpenFile)) .link("Documentation", "https://gpui-kit.com/") .separator() .item(PopupMenuItem::new("Custom Action") .on_click(window.listener_for(&view, |this, _, window, cx| { // Custom action logic here this. }) ) .separator() .menu("Exit", Box::new(Exit)) }) ``` :::tip As you see, the each menu item is associated with an [Action], we choice this design to better integrate with GPUI's action and key binding system, allowing menu items to automatically display keyboard shortcuts when applicable. So, the [Action] is the recommended way to define menu item behaviors. However, if you prefer not to use [Action]s, you can create custom menu items using the `item` method along with [PopupMenuItem]. There have a `on_click` callback to handle the click event directly. ::: ### Anchor Position Control where the dropdown menu appears relative to the trigger: ```rust use gpui_kit::Anchor; Button::new("menu-btn") .label("Options") .dropdown_menu_with_anchor(Anchor::TopRight, |menu, window, cx| { menu.menu("Option 1", Box::new(Action1)) .menu("Option 2", Box::new(Action2)) }) ``` ### Icons Add icons to menu items for better visual clarity: ```rust use gpui_kit::component::IconName; menu.menu_with_icon("Search", IconName::Search, Box::new(Search)) .menu_with_icon("Settings", IconName::Settings, Box::new(OpenSettings)) .separator() .menu_with_icon("Help", IconName::Help, Box::new(ShowHelp)) ``` ### Disabled State Create disabled menu items that cannot be activated: ```rust menu.menu("Available Action", Box::new(Action1)) .menu_with_disabled("Disabled Action", Box::new(Action2), true) .menu_with_icon_and_disabled( "Unavailable", IconName::Lock, Box::new(Action3), true ) ``` ### Check state Create menu items that show a check state: ```rust let is_enabled = true; menu.menu_with_check("Enable Feature", is_enabled, Box::new(ToggleFeature)) .menu_with_check("Show Sidebar", sidebar_visible, Box::new(ToggleSidebar)) ``` By default, the check icon will be shown on the left side of the menu item, if this menu item has an icon, the check icon will replace the icon on the left side. There also have a `check_side` option for you to config the check icon to be shown on the right side: ```rust menu.check_size(Side::Right) .menu_with_check("Enable Feature", is_enabled, Box::new(ToggleFeature)) ``` ### Separators Use separators to group related menu items: ```rust menu.menu("New", Box::new(NewFile)) .menu("Open", Box::new(OpenFile)) .separator() // Groups file operations .menu("Copy", Box::new(Copy)) .menu("Paste", Box::new(Paste)) .separator() // Groups edit operations .menu("Exit", Box::new(Exit)) ``` ### Labels Add non-interactive labels to organize menu sections: ```rust menu.label("File Operations") .menu("New", Box::new(NewFile)) .menu("Open", Box::new(OpenFile)) .separator() .label("Edit Operations") .menu("Copy", Box::new(Copy)) .menu("Paste", Box::new(Paste)) ``` ### Link MenuItem Create menu items that open external links: ```rust menu.link("Documentation", "https://docs.example.com") .link_with_icon( "GitHub", IconName::GitHub, "https://github.com/example/repo" ) .separator() .external_link_icon(false) // Hide external link icons .link("Support", "https://support.example.com") ``` ### Custom Element Create custom menu items with complex content: ```rust use gpui_kit::component::{h_flex, v_flex}; menu.menu_element(Box::new(CustomAction), |window, cx| { v_flex() .child("Custom Element") .child( div() .text_xs() .text_color(cx.theme().muted_foreground) .child("This is a subtitle") ) }) .menu_element_with_icon( IconName::Info, Box::new(InfoAction), |window, cx| { h_flex() .gap_1() .child("Status") .child( div() .text_sm() .text_color(cx.theme().success) .child("✓ Connected") ) } ) ``` ### Keyboard Shortcuts Menu items automatically display keyboard shortcuts if they're bound to actions: ```rust // First define your actions and key bindings actions!(my_app, [Copy, Paste, Cut]); // In your app initialization cx.bind_keys([ KeyBinding::new("ctrl-c", Copy, Some("editor")), KeyBinding::new("ctrl-v", Paste, Some("editor")), KeyBinding::new("ctrl-x", Cut, Some("editor")), ]); // The menu will automatically show shortcuts menu.action_context(focus_handle) // Set context for shortcuts .menu("Copy", Box::new(Copy)) // Will show "Ctrl+C" .menu("Paste", Box::new(Paste)) // Will show "Ctrl+V" .menu("Cut", Box::new(Cut)) // Will show "Ctrl+X" ``` ### Submenus Create nested menus with submenu support: ```rust menu.submenu("File", window, cx, |submenu, window, cx| { submenu.menu("New", Box::new(NewFile)) .menu("Open", Box::new(OpenFile)) .separator() .menu("Recent", Box::new(ShowRecent)) }) .submenu("Edit", window, cx, |submenu, window, cx| { submenu.menu("Undo", Box::new(Undo)) .menu("Redo", Box::new(Redo)) }) ``` ### Submenus with Icons Add icons to submenu headers: ```rust menu.submenu_with_icon( Some(IconName::Folder.into()), "Project", window, cx, |submenu, window, cx| { submenu.menu("Open Project", Box::new(OpenProject)) .menu("Close Project", Box::new(CloseProject)) } ) ``` ### Scrollable Menus :::warning When you have enabled `scrollable()` on a menu, avoid using submenus within it, as this can lead to usability issues. ::: For menus with many items, enable scrolling: ```rust Button::new("large-menu") .label("Many Options") .dropdown_menu(|menu, window, cx| { let mut menu = menu .scrollable(true) .max_h(px(300.)) .label("Select an option"); for i in 0..100 { menu = menu.menu( format!("Option {}", i), Box::new(SelectOption(i)) ); } menu }) ``` ### Menu Sizing Control menu dimensions: ```rust menu.min_w(px(200.)) // Minimum width .max_w(px(400.)) // Maximum width .max_h(px(300.)) // Maximum height .scrollable(true) // Enable scrolling when content exceeds max height ``` ### Action Context Set the focus context for handling menu actions: ```rust let focus_handle = cx.focus_handle(); menu.action_context(focus_handle) .menu("Copy", Box::new(Copy)) .menu("Paste", Box::new(Paste)) ``` ## API Reference * [PopupMenu] * [context\_menu][context_menu] * [PopupMenuItem] ## Examples ### File Manager Context Menu ```rust div() .id("file-manager") .child("Right-click for options") .context_menu(|menu, window, cx| { menu.menu_with_icon("Open", IconName::FolderOpen, Box::new(Open)) .separator() .menu_with_icon("Copy", IconName::Copy, Box::new(Copy)) .menu_with_icon("Cut", IconName::Scissors, Box::new(Cut)) .menu_with_icon("Paste", IconName::Clipboard, Box::new(Paste)) .separator() .submenu("New", window, cx, |submenu, window, cx| { submenu.menu_with_icon("File", IconName::File, Box::new(NewFile)) .menu_with_icon("Folder", IconName::Folder, Box::new(NewFolder)) }) .separator() .menu_with_icon("Delete", IconName::Trash, Box::new(Delete)) .separator() .menu("Properties", Box::new(ShowProperties)) }) ``` ### Add MenuItem without action Sometimes you may not like to define an action for a menu item, you just want add a `on_click` handler, in this case, the `item` and [PopupMenuItem] can help you: ```rust use gpui_kit::component::{menu::PopupMenuItem, Button}; Button::new("custom-item-menu") .label("Options") .dropdown_menu(|menu, window, cx| { menu.item( PopupMenuItem::new("Custom Action") .disabled(false) .icon(IconName::Star) .on_click(|window, cx| { // Custom click handler logic println!("Custom Action Clicked!"); }) ) .separator() .menu("Standard Action", Box::new(StandardAction)) }) ``` ### Editor Menu with Shortcuts ```rust // Define actions with keyboard shortcuts actions!(editor, [Save, SaveAs, Find, Replace, ToggleWordWrap]); // Set up key bindings cx.bind_keys([ KeyBinding::new("ctrl-s", Save, Some("editor")), KeyBinding::new("ctrl-shift-s", SaveAs, Some("editor")), KeyBinding::new("ctrl-f", Find, Some("editor")), KeyBinding::new("ctrl-h", Replace, Some("editor")), ]); // Create menu with automatic shortcuts let editor_focus = cx.focus_handle(); Button::new("editor-menu") .label("Edit") .dropdown_menu(|menu, window, cx| { menu.action_context(editor_focus) .menu("Save", Box::new(Save)) // Shows "Ctrl+S" .menu("Save As...", Box::new(SaveAs)) // Shows "Ctrl+Shift+S" .separator() .menu("Find", Box::new(Find)) // Shows "Ctrl+F" .menu("Replace", Box::new(Replace)) // Shows "Ctrl+H" .separator() .menu_with_check("Word Wrap", true, Box::new(ToggleWordWrap)) }) ``` ### Settings Menu with Custom Elements ```rust Button::new("settings") .label("Settings") .dropdown_menu(|menu, window, cx| { menu.label("Display") .menu_element_with_check(dark_mode, Box::new(ToggleDarkMode), |window, cx| { h_flex() .gap_2() .child("Dark Mode") .child( div() .text_xs() .text_color(cx.theme().muted_foreground) .child(if dark_mode { "On" } else { "Off" }) ) }) .separator() .label("Account") .menu_element_with_icon( IconName::User, Box::new(ShowProfile), |window, cx| { v_flex() .child("John Doe") .child( div() .text_xs() .text_color(cx.theme().muted_foreground) .child("john@example.com") ) } ) .separator() .link_with_icon("Help Center", IconName::Help, "https://help.example.com") .menu("Sign Out", Box::new(SignOut)) }) ``` ## Keyboard Shortcuts | Key | Action | | ----------------- | --------------------------------- | | `↑` / `↓` | Navigate menu items | | `←` / `→` | Navigate submenus | | `Enter` / `Space` | Activate menu item | | `Escape` | Close menu | | `Tab` | Close menu and focus next element | ## Best Practices 1. **Group Related Items**: Use separators to group related functionality 2. **Consistent Icons**: Use consistent iconography across your application 3. **Logical Order**: Place most common actions at the top 4. **Keyboard Shortcuts**: Provide shortcuts for frequently used actions 5. **Context Awareness**: Show only relevant items for the current context 6. **Progressive Disclosure**: Use submenus for complex hierarchies 7. **Clear Labels**: Use descriptive, action-oriented labels 8. **Reasonable Limits**: Use scrollable menus for more than 10-15 items [PopupMenu]: https://docs.rs/gpui-component/latest/gpui_component/menu/struct.PopupMenu.html [PopupMenuItem]: https://docs.rs/gpui-component/latest/gpui_component/menu/struct.PopupMenuItem.html [context_menu]: https://docs.rs/gpui-component/latest/gpui_component/menu/trait.ContextMenuExt.html#method.context_menu [Action]: https://docs.rs/gpui/latest/gpui/trait.Action.html --- --- url: /zh-CN/docs/components/menu.md description: 支持图标、快捷键、子菜单和多种菜单项类型的上下文菜单与弹出菜单。 --- # PopupMenu Menu 组件同时提供上下文菜单和弹出菜单,支持图标、键盘快捷键、子菜单、分隔线、勾选项以及自定义元素,并内置可访问性与键盘导航支持。 ## 导入 ```rust use gpui_kit::component::{ menu::{PopupMenu, PopupMenuItem, ContextMenuExt, DropdownMenu}, Button }; use gpui_kit::{actions, Action}; ``` ## 用法 ### ContextMenu 右键点击元素时显示上下文菜单: ```rust use gpui_kit::component::menu::ContextMenuExt; div() .id("my-element") .child("Right click me") .context_menu(|menu, window, cx| { menu.menu("Copy", Box::new(Copy)) .menu("Paste", Box::new(Paste)) .separator() .menu("Delete", Box::new(Delete)) }) ``` ### DropdownMenu 下拉菜单通常由按钮或其它可交互元素触发: ```rust use gpui_kit::component::popup_menu::{PopupMenuExt as _, PopupMenuItem}; let view = cx.entity(); Button::new("menu-btn") .label("Open Menu") .dropdown_menu(|menu, window, cx| { menu.menu("New File", Box::new(NewFile)) .menu("Open File", Box::new(OpenFile)) .link("Documentation", "https://gpui-kit.com/") .separator() .item(PopupMenuItem::new("Custom Action") .on_click(window.listener_for(&view, |this, _, window, cx| { // Custom action logic here this. }) ) .separator() .menu("Exit", Box::new(Exit)) }) ``` :::tip 每个菜单项都可以关联一个 [Action]。这种设计可以更好地接入 GPUI 的 action 与快捷键系统,在适用时自动显示对应快捷键。 因此,推荐优先使用 [Action] 定义菜单行为。 如果你不想使用 [Action],也可以通过 `item` 方法配合 [PopupMenuItem] 创建自定义菜单项,并使用 `on_click` 直接处理点击事件。 ::: ### 锚点位置 控制下拉菜单相对触发器的显示位置: ```rust use gpui_kit::Anchor; Button::new("menu-btn") .label("Options") .dropdown_menu_with_anchor(Anchor::TopRight, |menu, window, cx| { menu.menu("Option 1", Box::new(Action1)) .menu("Option 2", Box::new(Action2)) }) ``` ### 图标 ```rust use gpui_kit::component::IconName; menu.menu_with_icon("Search", IconName::Search, Box::new(Search)) .menu_with_icon("Settings", IconName::Settings, Box::new(OpenSettings)) .separator() .menu_with_icon("Help", IconName::Help, Box::new(ShowHelp)) ``` ### 禁用状态 ```rust menu.menu("Available Action", Box::new(Action1)) .menu_with_disabled("Disabled Action", Box::new(Action2), true) .menu_with_icon_and_disabled( "Unavailable", IconName::Lock, Box::new(Action3), true ) ``` ### 勾选状态 ```rust let is_enabled = true; menu.menu_with_check("Enable Feature", is_enabled, Box::new(ToggleFeature)) .menu_with_check("Show Sidebar", sidebar_visible, Box::new(ToggleSidebar)) ``` 默认情况下,勾选图标显示在菜单项左侧;如果菜单项已有图标,勾选图标会替换左侧图标。 也可以通过 `check_side` 将勾选图标放到右侧: ```rust menu.check_size(Side::Right) .menu_with_check("Enable Feature", is_enabled, Box::new(ToggleFeature)) ``` ### 分隔线 ```rust menu.menu("New", Box::new(NewFile)) .menu("Open", Box::new(OpenFile)) .separator() .menu("Copy", Box::new(Copy)) .menu("Paste", Box::new(Paste)) .separator() .menu("Exit", Box::new(Exit)) ``` ### 标签 ```rust menu.label("File Operations") .menu("New", Box::new(NewFile)) .menu("Open", Box::new(OpenFile)) .separator() .label("Edit Operations") .menu("Copy", Box::new(Copy)) .menu("Paste", Box::new(Paste)) ``` ### 链接菜单项 ```rust menu.link("Documentation", "https://docs.example.com") .link_with_icon( "GitHub", IconName::GitHub, "https://github.com/example/repo" ) .separator() .external_link_icon(false) .link("Support", "https://support.example.com") ``` ### 自定义元素 ```rust use gpui_kit::component::{h_flex, v_flex}; menu.menu_element(Box::new(CustomAction), |window, cx| { v_flex() .child("Custom Element") .child( div() .text_xs() .text_color(cx.theme().muted_foreground) .child("This is a subtitle") ) }) .menu_element_with_icon( IconName::Info, Box::new(InfoAction), |window, cx| { h_flex() .gap_1() .child("Status") .child( div() .text_sm() .text_color(cx.theme().success) .child("✓ Connected") ) } ) ``` ### 键盘快捷键 ```rust actions!(my_app, [Copy, Paste, Cut]); cx.bind_keys([ KeyBinding::new("ctrl-c", Copy, Some("editor")), KeyBinding::new("ctrl-v", Paste, Some("editor")), KeyBinding::new("ctrl-x", Cut, Some("editor")), ]); menu.action_context(focus_handle) .menu("Copy", Box::new(Copy)) .menu("Paste", Box::new(Paste)) .menu("Cut", Box::new(Cut)) ``` ### 子菜单 ```rust menu.submenu("File", window, cx, |submenu, window, cx| { submenu.menu("New", Box::new(NewFile)) .menu("Open", Box::new(OpenFile)) .separator() .menu("Recent", Box::new(ShowRecent)) }) .submenu("Edit", window, cx, |submenu, window, cx| { submenu.menu("Undo", Box::new(Undo)) .menu("Redo", Box::new(Redo)) }) ``` ### 带图标的子菜单 ```rust menu.submenu_with_icon( Some(IconName::Folder.into()), "Project", window, cx, |submenu, window, cx| { submenu.menu("Open Project", Box::new(OpenProject)) .menu("Close Project", Box::new(CloseProject)) } ) ``` ### 可滚动菜单 :::warning 启用 `scrollable()` 后,尽量不要在同一个菜单里再使用子菜单,否则容易影响可用性。 ::: ```rust Button::new("large-menu") .label("Many Options") .dropdown_menu(|menu, window, cx| { let mut menu = menu .scrollable(true) .max_h(px(300.)) .label("Select an option"); for i in 0..100 { menu = menu.menu( format!("Option {}", i), Box::new(SelectOption(i)) ); } menu }) ``` ### 菜单尺寸 ```rust menu.min_w(px(200.)) .max_w(px(400.)) .max_h(px(300.)) .scrollable(true) ``` ### Action 上下文 ```rust let focus_handle = cx.focus_handle(); menu.action_context(focus_handle) .menu("Copy", Box::new(Copy)) .menu("Paste", Box::new(Paste)) ``` ## API 参考 * [PopupMenu] * [context\_menu][context_menu] * [PopupMenuItem] ## 示例 ### 文件管理器上下文菜单 ```rust div() .id("file-manager") .child("Right-click for options") .context_menu(|menu, window, cx| { menu.menu_with_icon("Open", IconName::FolderOpen, Box::new(Open)) .separator() .menu_with_icon("Copy", IconName::Copy, Box::new(Copy)) .menu_with_icon("Cut", IconName::Scissors, Box::new(Cut)) .menu_with_icon("Paste", IconName::Clipboard, Box::new(Paste)) .separator() .submenu("New", window, cx, |submenu, window, cx| { submenu.menu_with_icon("File", IconName::File, Box::new(NewFile)) .menu_with_icon("Folder", IconName::Folder, Box::new(NewFolder)) }) .separator() .menu_with_icon("Delete", IconName::Trash, Box::new(Delete)) .separator() .menu("Properties", Box::new(ShowProperties)) }) ``` ### 不使用 action 添加菜单项 ```rust use gpui_kit::component::{menu::PopupMenuItem, Button}; Button::new("custom-item-menu") .label("Options") .dropdown_menu(|menu, window, cx| { menu.item( PopupMenuItem::new("Custom Action") .disabled(false) .icon(IconName::Star) .on_click(|window, cx| { println!("Custom Action Clicked!"); }) ) .separator() .menu("Standard Action", Box::new(StandardAction)) }) ``` ### 带快捷键的编辑器菜单 ```rust actions!(editor, [Save, SaveAs, Find, Replace, ToggleWordWrap]); cx.bind_keys([ KeyBinding::new("ctrl-s", Save, Some("editor")), KeyBinding::new("ctrl-shift-s", SaveAs, Some("editor")), KeyBinding::new("ctrl-f", Find, Some("editor")), KeyBinding::new("ctrl-h", Replace, Some("editor")), ]); let editor_focus = cx.focus_handle(); Button::new("editor-menu") .label("Edit") .dropdown_menu(|menu, window, cx| { menu.action_context(editor_focus) .menu("Save", Box::new(Save)) .menu("Save As...", Box::new(SaveAs)) .separator() .menu("Find", Box::new(Find)) .menu("Replace", Box::new(Replace)) .separator() .menu_with_check("Word Wrap", true, Box::new(ToggleWordWrap)) }) ``` ### 带自定义元素的设置菜单 ```rust Button::new("settings") .label("Settings") .dropdown_menu(|menu, window, cx| { menu.label("Display") .menu_element_with_check(dark_mode, Box::new(ToggleDarkMode), |window, cx| { h_flex() .gap_2() .child("Dark Mode") .child( div() .text_xs() .text_color(cx.theme().muted_foreground) .child(if dark_mode { "On" } else { "Off" }) ) }) .separator() .label("Account") .menu_element_with_icon( IconName::User, Box::new(ShowProfile), |window, cx| { v_flex() .child("John Doe") .child( div() .text_xs() .text_color(cx.theme().muted_foreground) .child("john@example.com") ) } ) .separator() .link_with_icon("Help Center", IconName::Help, "https://help.example.com") .menu("Sign Out", Box::new(SignOut)) }) ``` ## 键盘快捷键 | 按键 | 行为 | | --- | --- | | `↑` / `↓` | 在菜单项之间移动 | | `←` / `→` | 在子菜单之间移动 | | `Enter` / `Space` | 激活当前菜单项 | | `Escape` | 关闭菜单 | | `Tab` | 关闭菜单并聚焦下一个元素 | ## 最佳实践 1. 使用分隔线对相关操作分组。 2. 在整个应用中保持图标语义一致。 3. 将最常用的操作放在靠前位置。 4. 为高频操作提供快捷键。 5. 根据上下文只展示相关菜单项。 6. 对复杂层级使用子菜单而不是把所有项堆在一起。 7. 使用清晰、动作导向的文案。 8. 菜单项很多时开启滚动并设置合理高度。 [PopupMenu]: https://docs.rs/gpui-component/latest/gpui_component/menu/struct.PopupMenu.html [PopupMenuItem]: https://docs.rs/gpui-component/latest/gpui_component/menu/struct.PopupMenuItem.html [context_menu]: https://docs.rs/gpui-component/latest/gpui_component/menu/trait.ContextMenuExt.html#method.context_menu [Action]: https://docs.rs/gpui/latest/gpui/trait.Action.html --- --- url: /docs/components/message.md description: >- Compose sender identity, metadata, rich content, and actions into an aligned chat message. --- # Message `Message` is the row-level composition primitive for a conversation. It owns the horizontal alignment and the vertical stack that contains optional sender identity, metadata, content, and footer slots. It does not own a sender model, timestamp formatting, delivery state, reaction state, or message actions. Applications provide those values and compose existing components inside the slots. This keeps the message layout reusable across direct messages, group chat, assistant responses, system notices, and generated content. ## Import ```rust use gpui_kit::{ParentElement as _, StyleRefinement, Styled as _}; use gpui_kit::component::{ ActiveTheme as _, Colorize as _, Sizable as _, attachment::{Attachment, AttachmentContent, AttachmentTitle}, avatar::Avatar, bubble::{Bubble, BubbleVariant}, button::{Button, ButtonVariants as _}, message::{ Message, MessageAlignment, MessageAvatar, MessageContent, MessageFooter, MessageGroup, MessageHeader, }, }; ``` ## Anatomy and basic usage All named slots are optional, so a minimal message can contain only a body: ```rust Message::new().content( MessageContent::new().bubble(Bubble::new().child("Can you review this?")), ) ``` A complete message commonly combines sender identity, metadata, a bubble, and a delivery footer: ```rust Message::new() .avatar_slot( MessageAvatar::new() .child(Avatar::new().name("Alice").size_8()), ) .header( MessageHeader::new() .child("Alice") .child("10:24 AM"), ) .content( MessageContent::new().bubble( Bubble::new() .with_variant(BubbleVariant::Secondary) .child("Can you review this draft?"), ), ) .footer(MessageFooter::new().child("Read")) ``` The default state is: | Property | Default | Meaning | | --- | --- | --- | | Alignment | `MessageAlignment::Start` | Place the message at the leading edge. | | Avatar/header/content/footer | absent | Add only the slots needed by the product. | | Outer layout | full width, `min_w_0()`, `gap_2()` | Keeps rows usable in a virtual list. | | Inner stack gap | `rems(0.625)` | Separates metadata, body, and footer. | | Header/footer inset | enabled, `px_3()` | Aligns metadata with a regular bubble surface. | | Avatar baseline | `min_w_8()`, circular muted surface | Gives sender identity a stable column. | `Message` applies its alignment to the complete row and to the named content stack. It reverses the outer row for `End`, so the avatar and message stack remain a single aligned unit. ## Alignment Use `Start` for incoming content and `End` for outgoing content: ```rust Message::new() .alignment(MessageAlignment::Start) .avatar(Avatar::new().name("Alice").size_8()) .header(MessageHeader::new().child("Alice").child("10:24 AM")) .content(MessageContent::new().bubble( Bubble::new() .with_variant(BubbleVariant::Secondary) .child("Incoming message"), )); Message::new() .alignment(MessageAlignment::End) .avatar(Avatar::new().name("You").size_8()) .header(MessageHeader::new().child("You").child("10:25 AM")) .content(MessageContent::new().bubble(Bubble::new().child("Outgoing message"))) .footer(MessageFooter::new().child("Delivered")) ``` `MessageAlignment` is also accepted by `Bubble`. When the body is a typed `MessageContent::bubble(...)`, the message propagates its alignment to that bubble's surface. Leave the bubble's own alignment unset in this composition so the row has one clear owner of placement. ## Avatar, header, content, and footer ### Avatar `.avatar(...)` wraps any element in `MessageAvatar`. Use `.avatar_slot(...)` when the slot itself needs styling or multiple children: ```rust Message::new() .avatar_slot( MessageAvatar::new() .bg(cx.theme().transparent) .child(Avatar::new().name("System").size_8()), ) .content(MessageContent::new().child("A system update")) ``` The avatar reserves the shared `size-8` baseline and always sits flush with the bottom edge of the message content; the footer renders below the avatar row, indented to the content column. The message does not require an avatar; omit it for assistant messages, compact group chat, or system rows where identity is already present elsewhere. ### Header `MessageHeader` is an arbitrary horizontal metadata row. It defaults to an extra-small, medium-weight, muted style with a `px_3()` inset: ```rust MessageHeader::new() .child("Alice") .child("·") .child("10:24 AM") ``` The header does not format dates or infer sender names. Use application-owned formatting and compose a `Tooltip` around timestamps when the full date is useful. ### Content `MessageContent` is a full-width, minimum-width-safe vertical stack. It accepts arbitrary elements and has a typed `.bubble(...)` convenience that records whether a ghost bubble is present: ```rust MessageContent::new() .bubble(Bubble::new().child("First paragraph")) .bubble(Bubble::new().child("Second paragraph")) ``` Use `.child(...)` for attachments, code blocks, images, or custom rich content: ```rust MessageContent::new() .bubble(Bubble::new().child("Here is the file:")) .child( Attachment::new().content( AttachmentContent::new() .title(AttachmentTitle::new("quarterly-report.pdf")), ), ) ``` Typed bubbles are useful when the surrounding header and footer should respond to the `Ghost` variant. Arbitrary `.child(...)` values are still fully composable, but their concrete type is erased and they do not set that ghost-surface metadata. ### Footer `MessageFooter` is another arbitrary horizontal metadata row. Use it for delivery state, reactions, or actions composed from existing controls: ```rust MessageFooter::new() .child("Delivered") .child(Button::new("reply").ghost().xsmall().label("Reply")) .child(Button::new("copy").ghost().xsmall().label("Copy")) ``` Footer uses the same extra-small muted default and `px_3()` inset as the header. The footer does not own a delivery-state enum or action semantics. ## Rich content and actions Compose the existing component that owns each behavior: ```rust Message::new() .content( MessageContent::new() .bubble(Bubble::new().child("The export is ready.")) .child( Attachment::new() .content(AttachmentContent::new().title(AttachmentTitle::new("export.zip"))), ), ) .footer( MessageFooter::new() .child(Button::new("download-export").label("Download")) .child(Button::new("share-export").ghost().label("Share")), ) ``` Use `Button` for commands, `Link` for URLs, `Attachment` for files, and `Bubble` for conversational surfaces. This keeps disabled, loading, focus, keyboard, and accessible-name behavior on the control that owns it. A message does not become clickable merely because it contains a button. Long or multiline content remains the responsibility of the child element. Keep custom children `min_w_0()` when they contain long text or horizontal layouts; `Message` already applies `w_full()` and `min_w_0()` to its own row and stack. ## Grouping `MessageGroup` is a styleable vertical stack for consecutive messages. It does not decide which sender owns a message or automatically remove metadata: ```rust MessageGroup::new() .child( Message::new() .avatar(Avatar::new().name("Alice").size_8()) .header(MessageHeader::new().child("Alice")) .content(MessageContent::new().bubble( Bubble::new() .with_variant(BubbleVariant::Secondary) .child("The first message."), )), ) .child( Message::new() .avatar_slot(MessageAvatar::new().bg(cx.theme().transparent)) .content(MessageContent::new().bubble( Bubble::new() .with_variant(BubbleVariant::Secondary) .child("The follow-up keeps the same sender context."), )), ) ``` Use `BubbleGroup` when only the bubbles are grouped and there is no message header, avatar, or footer. Use `MessageGroup` when each item is a full row. ## Ghost surfaces and content insets The typed `MessageContent::bubble(...)` builder records a ghost bubble. In that case, `Message` removes the default header and footer insets so metadata lines up with the unframed content: ```rust Message::new() .header(MessageHeader::new().child("System").child("Just now")) .content(MessageContent::new().bubble( Bubble::new() .with_variant(BubbleVariant::Ghost) .child("The conversation has been archived."), )) .footer(MessageFooter::new().child("No further action required")) ``` Override this behavior explicitly on either named metadata slot: ```rust MessageHeader::new() .content_inset(true) .child("Keep the regular header inset"); MessageFooter::new() .content_inset(false) .child("Align the footer with a custom surface") ``` `content_inset(...)` takes precedence over inherited ghost behavior. A typed ghost bubble is required for automatic inheritance; an arbitrary child that happens to look like a ghost surface cannot be inspected by `Message`. The inner slot stack can also be refined independently: ```rust Message::new() .with_stack_style(StyleRefinement::default().gap_3()) .content(MessageContent::new().child("A wider message rhythm")) ``` ## Custom styling and theme tokens `Message`, `MessageGroup`, `MessageAvatar`, `MessageHeader`, `MessageContent`, and `MessageFooter` implement `Styled`. Style the part that owns the visual decision: ```rust Message::new() .p_3() .rounded(cx.theme().radius_lg) .bg(cx.theme().muted.opacity(0.35)) .header(MessageHeader::new().px_0().child("System")) .content(MessageContent::new().child("Archived")) .footer(MessageFooter::new().px_0().child("Just now")) ``` Use `with_stack_style(...)` for the vertical stack, slot refinements for header/content/footer typography and spacing, and the child component's own API for bubble, attachment, or button surfaces. Radius, spacing, typography, and colors should come from the active semantic theme or shared scale. Avoid raw colors at message call sites so the same composition works in light and dark themes. ## Accessibility and state guidance * Keep sender identity and message content in readable text. An avatar alone should not be the only indication of who sent a message. * Put commands in semantic `Button` or `Link` controls. For the current `Button` API, use a visible `.label(...)` when a footer action needs an accessible name; a tooltip is supplemental. * Delivery, failure, streaming, and unread states belong in text or semantic controls. Do not communicate them with alignment, color, or opacity alone. * Preserve the header/footer inset when it is the visual relationship that aligns metadata with the surface. If a custom surface removes it, verify the reading order and keyboard order still match the visual order. * Keep multiline content readable at the application's minimum window width; use `min_w_0()` on nested horizontal content and avoid hover-only actions. * Motion for generated content belongs to `ShimmerText` or another motion-aware component. Reduced-motion behavior should leave the message text present and understandable. ## Component boundaries The GPUI component intentionally does not add provider or domain layers: * `Message` owns row alignment and slot layout. * The application owns sender records, timestamps, delivery state, reactions, permissions, message IDs, and persistence. * `Bubble`, `Attachment`, `Button`, `Link`, and `Marker` own their own visual or behavioral primitives and are composed through message slots. * `MessageGroup` only supplies a vertical stack. It does not infer sender changes or collapse headers. If a product needs a specific “assistant message” or “group chat message” with fixed metadata policy, wrap `Message` in an application component. Keep that domain policy out of the general-purpose primitive. ## API reference ### `Message` | Method | Default | Purpose | | --- | --- | --- | | `new()` | `Start`, no slots | Create a message row. | | `alignment(MessageAlignment)` | `Start` | Set leading or trailing alignment. | | `with_stack_style(StyleRefinement)` | component stack defaults | Refine the inner vertical stack. | | `avatar(element)` | none | Wrap an element in `MessageAvatar`. | | `avatar_slot(MessageAvatar)` | none | Set a fully configured avatar slot. | | `header(MessageHeader)` | none | Set sender and metadata content. | | `content(MessageContent)` | none | Set the message body. | | `footer(MessageFooter)` | none | Set delivery, reactions, or actions. | `Message` also implements `Styled` for the outer row. ### `MessageGroup` | Method | Default | Purpose | | --- | --- | --- | | `new()` | empty vertical stack | Create a message group. | | `.child(element)` | — | Add complete messages. | | `Styled` methods | `gap_2()` | Refine group spacing and layout. | ### `MessageAvatar` | Method | Default | Purpose | | --- | --- | --- | | `new()` | empty circular `size_8` baseline | Create an identity slot. | | `.child(element)` | — | Add Avatar or another identity element. | | `Styled` methods | muted surface and full radius | Refine size, background, and alignment. | ### `MessageHeader` and `MessageFooter` | Method | Default | Purpose | | --- | --- | --- | | `new()` | empty extra-small metadata row | Create the slot. | | `content_inset(bool)` | inherited or `true` | Keep or remove the default `px_3()` inset. | | `.child(element)` | — | Add text, metadata, reactions, or controls. | | `Styled` methods | muted, medium-weight, `text_xs()` | Refine the slot. | ### `MessageContent` | Method | Default | Purpose | | --- | --- | --- | | `new()` | empty full-width vertical stack | Create the body slot. | | `bubble(Bubble)` | — | Add a typed bubble and propagate ghost metadata. | | `.child(element)` | — | Add arbitrary rich content. | | `Styled` methods | `min_w_0()`, `gap(rems(0.625))` | Refine body layout. | ### Related types * \[`MessageAlignment`] — `Start` or `End`. * \[`Bubble`] — conversational surface content. * \[`Attachment`] — files and media. * \[`MessageScroller`] — virtualized conversation rows and tail following. [Message]: https://docs.rs/gpui-component/latest/gpui_component/message/struct.Message.html [MessageGroup]: https://docs.rs/gpui-component/latest/gpui_component/message/struct.MessageGroup.html [MessageAvatar]: https://docs.rs/gpui-component/latest/gpui_component/message/struct.MessageAvatar.html [MessageHeader]: https://docs.rs/gpui-component/latest/gpui_component/message/struct.MessageHeader.html [MessageContent]: https://docs.rs/gpui-component/latest/gpui_component/message/struct.MessageContent.html [MessageFooter]: https://docs.rs/gpui-component/latest/gpui_component/message/struct.MessageFooter.html [MessageAlignment]: https://docs.rs/gpui-component/latest/gpui_component/message/enum.MessageAlignment.html [Bubble]: https://docs.rs/gpui-component/latest/gpui_component/bubble/struct.Bubble.html [Attachment]: https://docs.rs/gpui-component/latest/gpui_component/attachment/struct.Attachment.html [MessageScroller]: https://docs.rs/gpui-component/latest/gpui_component/message_scroller/struct.MessageScroller.html --- --- url: /zh-CN/docs/components/message.md description: 将发送者身份、元信息、富内容和操作组合成对齐的聊天消息。 --- # Message `Message` 为聊天与会话界面提供消息行结构。它负责整体对齐、头像位置和具名 slot 的默认间距;应用负责消息数据、发送者、时间戳、送达状态和操作逻辑。 ## 适用场景 * 需要把头像、发送者、时间、bubble、附件和 footer 组合成一条消息。 * 需要同时支持接收消息和发送消息的起始侧/结束侧布局。 * 需要把多个连续消息堆叠为一个发送者分组。 * 需要以 Ghost Bubble 展示 Markdown、代码或没有卡片表面的富内容。 只有一条状态提示时使用 `Marker`;只有一个内容 surface 时使用 `Bubble`;不要为每种消息类型创建专用的 Message wrapper。 ## 导入 ```rust use gpui_kit::{ParentElement as _, StyleRefinement, Styled as _}; use gpui_kit::component::{ ActiveTheme as _, Colorize as _, Sizable as _, StyledExt as _, attachment::{Attachment, AttachmentContent, AttachmentTitle}, avatar::Avatar, bubble::{Bubble, BubbleVariant}, button::{Button, ButtonVariants as _}, message::{ Message, MessageAlignment, MessageAvatar, MessageContent, MessageFooter, MessageGroup, MessageHeader, }, }; ``` ## 结构 ```text Message ├── MessageAvatar # 可选,发送者身份 └── inner stack ├── MessageHeader # 可选,发送者与时间 ├── MessageContent # 可选,Bubble、附件、Markdown 等 └── MessageFooter # 可选,送达状态、reaction、操作 ``` 所有具名 slot 都可以继续添加任意 GPUI element,并分别实现 `Styled`。`Message` 不持有消息模型,也不会替应用决定 header 或 footer 的文本。 ## 基础用法 一条完整消息可以同时提供 avatar、header、content 和 footer: ```rust Message::new() .avatar_slot( MessageAvatar::new() .child(Avatar::new().name("Alice").size_8()), ) .header( MessageHeader::new() .child("Alice") .child("10:24"), ) .content( MessageContent::new().bubble( Bubble::new().child("可以帮我检查一下吗?"), ), ) .footer(MessageFooter::new().child("已读")) ``` 只需要头像和内容时,可以使用便利的 `.avatar(...)`: ```rust Message::new() .avatar(Avatar::new().name("Alice").size_8()) .content(MessageContent::new().bubble( Bubble::new().child("收到的消息"), )) ``` ## 对齐 `MessageAlignment` 会作用于消息行和 MessageContent 内的 Bubble: ```rust Message::new() .alignment(MessageAlignment::Start) .avatar(Avatar::new().name("Alice")) .content(MessageContent::new().bubble( Bubble::new().child("对方的消息"), )); Message::new() .alignment(MessageAlignment::End) .avatar(Avatar::new().name("我")) .content(MessageContent::new().bubble( Bubble::new().with_variant(BubbleVariant::Secondary).child("我发送的消息"), )) ``` | 值 | 用途 | | --- | --- | | `Start` | 接收消息或起始侧消息。 | | `End` | 发送消息或结束侧消息。组件会反转头像与内容的行方向。 | `MessageContent` 内的 Bubble 可以不设置自己的 alignment,让 Message 统一传播布局。独立使用 Bubble 时再显式设置 alignment。 ## Avatar、Header、Content 与 Footer ### Avatar `.avatar(...)` 会把任意 element 包装进 `MessageAvatar`;需要调整 slot 自身时使用 `.avatar_slot(...)`: ```rust Message::new() .avatar_slot( MessageAvatar::new() .p_0() .child(Avatar::new().name("Support").size_8()), ) .content(MessageContent::new().bubble( Bubble::new().child("我们已经处理了你的请求。"), )) ``` `MessageAvatar` 保留共享的 avatar 尺寸基线,并始终与消息内容的底边对齐;footer 渲染在头像行之下、按内容列缩进。身份 fallback、头像图片和名称文字由 `Avatar` 负责。 ### Header Header 适合放发送者、时间和其他低强调元信息: ```rust Message::new() .header( MessageHeader::new() .child("Alice") .child("·") .child("10:24"), ) .content(MessageContent::new().bubble( Bubble::new().child("消息内容"), )) ``` Header 默认有水平内容 inset。需要和 Ghost Bubble 对齐时,Message 会根据 content 自动处理;也可以显式调用 `.content_inset(false)` 或 `.content_inset(true)`。 ### Content Content 是消息主体,可以包含多个 Bubble、附件、图片、代码块或应用自己的富文本 renderer: ```rust Message::new() .content( MessageContent::new() .bubble(Bubble::new().child("先看结论:")) .bubble( Bubble::new() .with_variant(BubbleVariant::Ghost) .child("这是第二段无表面富内容。"), ), ) ``` `MessageContent::bubble(...)` 会保留 Bubble 的 Ghost 元信息,用于协调 Header 和 Footer 的 inset;用普通 `.child(...)` 添加的 Bubble 不参与这个类型级传播。 ### Footer Footer 可放送达状态、reaction、Button 或其他次级信息: ```rust Message::new() .content(MessageContent::new().bubble( Bubble::new().child("需要回复的内容"), )) .footer( MessageFooter::new() .child("未读") .child(Button::new("reply").ghost().small().label("回复")), ) ``` Footer 内的 Button、Link 和其他控件由应用提供自己的事件、disabled、loading 和标签状态。 ## 富内容、附件与操作 Message 通过组合现有组件表达不同内容,不增加重复的消息专用控件: ```rust Message::new() .avatar(Avatar::new().name("Alice")) .header(MessageHeader::new().child("Alice").child("刚刚")) .content( MessageContent::new() .bubble(Bubble::new().child("请查看这个文件。")) .child( Attachment::new().content( AttachmentContent::new() .title(AttachmentTitle::new("quarterly-report.pdf")), ), ), ) .footer( MessageFooter::new() .child(Button::new("download").outline().small().label("下载")), ) ``` 需要 Markdown、代码或 HTML 时,在 `MessageContent` 中放入应用选择的 text renderer;Message 只提供布局和 alignment,不改变富文本的选择、复制和交互行为。 ## 消息分组 `MessageGroup` 用于堆叠同一发送者的连续消息: ```rust MessageGroup::new() .child( Message::new() .avatar(Avatar::new().name("Alice")) .content(MessageContent::new().bubble( Bubble::new().child("第一条消息"), )), ) .child( Message::new() .content(MessageContent::new().bubble( Bubble::new().child("同一发送者的第二条消息"), )), ) ``` 分组只负责垂直 stack 和共享间距;发送者变化、分组边界、时间戳和 avatar 是否重复显示由应用决定。需要不同发送者之间的间距时,在外层列表或自定义 group style 中表达。 ## Ghost surface 与内容 inset Ghost Bubble 没有背景、边框、padding,可用于 Markdown、代码或需要与消息行直接对齐的富内容: ```rust Message::new() .header( MessageHeader::new() .child("系统") .child("刚刚"), ) .content( MessageContent::new().bubble( Bubble::new() .with_variant(BubbleVariant::Ghost) .child("已完成索引更新。"), ), ) .footer(MessageFooter::new().child("无需进一步操作")) ``` 当 `MessageContent::bubble(...)` 中包含 Ghost Bubble 时,Header 和 Footer 默认移除水平 inset;这样元信息与无表面内容左边缘对齐。调用方可以覆盖该行为: ```rust Message::new() .header(MessageHeader::new().content_inset(true).child("保留 inset")) .content(MessageContent::new().bubble( Bubble::new().with_variant(BubbleVariant::Ghost).child("内容"), )) .footer(MessageFooter::new().content_inset(false).child("移除 inset")) ``` `.content_inset(...)` 是 slot 的显式设置,优先于 Message 根据 Ghost Bubble 推导的默认值。`.px_0()` 等普通 `Styled` refinement 仍可用于更细的布局调整。 ## 自定义样式与主题 token `Message`、`MessageGroup`、`MessageAvatar`、`MessageHeader`、`MessageContent` 与 `MessageFooter` 都实现 `Styled`。具名 slot 之间的 stack 使用 `with_stack_style(...)`: ```rust Message::new() .with_stack_style(StyleRefinement::default().gap_3()) .p_3() .rounded(cx.theme().radius_lg) .bg(cx.theme().muted.opacity(0.35)) .avatar_slot( MessageAvatar::new() .bg(cx.theme().secondary) .child(Avatar::new().name("A")), ) .header(MessageHeader::new().px_0().child("Alice · 10:24")) .content(MessageContent::new().bubble( Bubble::new().child("遵循当前主题的消息 surface"), )) ``` 推荐使用 `cx.theme()` 的语义颜色、圆角和共享 design scale。Message 的外层、inner stack、avatar、header、content 和 footer 都有独立的样式入口,调用方可以调整表面、间距、文字层级和对齐,而不需要复制 Message 的布局实现。 ## 组件边界 * `Message` 不持有发送者、时间戳、送达状态、reaction 或操作状态;这些数据由应用生成对应的 child。 * `MessageContent::bubble(...)` 是专门用于 Bubble 的类型化便利入口,用于传播 Ghost surface 元信息;其他 element 使用普通 `.child(...)`。 * 应用操作使用 `Button`,URL 使用 `Link`,附件使用 `Attachment`;不创建消息专用的 Action、Link 或 Attachment wrapper。 * 需要消息列表、尾部跟随、未读定位或历史加载时,使用 `MessageScroller` 管理虚拟列表;Message 只负责单行布局。 ## 可访问性 * Avatar 是身份辅助信息,不应是唯一的发送者标识;Header 应提供可读发送者或系统来源。 * 时间、送达状态、失败状态和未读信息应作为可读文本提供,不能只用颜色、位置或 icon 表达。 * Footer 中的 icon-only Button 应提供可见的 `.label(...)` 或其他可读名称,tooltip 只作为补充提示;发送者操作应使用明确的 Button/Link 语义。 * Bubble、Attachment 和富文本 child 的键盘行为由各自组件负责;Message 不会自动为普通 `div` 增加焦点。 * 应用自定义消息动画时,应在 reduced motion 下保持静态结果;Message 自身没有额外动画。 * 长消息和代码内容应保持可读的换行、选择和滚动策略,不要依赖 hover 才能访问完整内容。 ## API 参考 ### `Message` | 方法 | 说明 | | --- | --- | | `new()` | 创建默认起始侧对齐的消息。 | | `alignment(MessageAlignment)` | 设置起始侧或结束侧对齐。 | | `with_stack_style(StyleRefinement)` | 调整 Header、Content、Footer 内部 stack。 | | `avatar(element)` | 将任意 element 包装进 `MessageAvatar`。 | | `avatar_slot(MessageAvatar)` | 设置完整的 avatar slot。 | | `header(MessageHeader)` | 设置 header slot。 | | `content(MessageContent)` | 设置 content slot。 | | `footer(MessageFooter)` | 设置 footer slot。 | | `Styled` | 调整消息行自身的布局与 surface。 | ### `MessageGroup` | 方法 | 说明 | | --- | --- | | `new()` | 创建连续消息的垂直 stack。 | | `child(element)` | 按顺序添加消息。 | | `Styled` | 调整分组间距、宽度和布局。 | ### `MessageAvatar` | 方法 | 说明 | | --- | --- | | `new()` | 创建身份 slot。 | | `child(element)` | 添加 Avatar 或其他身份内容。 | | `Styled` | 调整 slot 的尺寸、背景和位置。 | ### `MessageHeader` / `MessageFooter` | 方法 | 说明 | | --- | --- | | `new()` | 创建对应的元信息或次级内容 slot。 | | `content_inset(bool)` | 显式保留或移除默认水平 inset。 | | `child(element)` | 添加文本、状态或操作。 | | `Styled` | 调整文字、间距和布局。 | ### `MessageContent` | 方法 | 说明 | | --- | --- | | `new()` | 创建消息主体 slot。 | | `bubble(Bubble)` | 添加 Bubble,并参与 Ghost surface 的 inset 协调。 | | `child(element)` | 添加任意富内容,不参与 Bubble 类型元信息传播。 | | `Styled` | 调整主体的 stack、宽度和对齐。 | ### 类型链接 * [Message] * [MessageAlignment] * [MessageGroup] * [MessageAvatar] * [MessageHeader] * [MessageContent] * [MessageFooter] [Message]: https://docs.rs/gpui-component/latest/gpui_component/message/struct.Message.html [MessageAlignment]: https://docs.rs/gpui-component/latest/gpui_component/message/enum.MessageAlignment.html [MessageGroup]: https://docs.rs/gpui-component/latest/gpui_component/message/struct.MessageGroup.html [MessageAvatar]: https://docs.rs/gpui-component/latest/gpui_component/message/struct.MessageAvatar.html [MessageHeader]: https://docs.rs/gpui-component/latest/gpui_component/message/struct.MessageHeader.html [MessageContent]: https://docs.rs/gpui-component/latest/gpui_component/message/struct.MessageContent.html [MessageFooter]: https://docs.rs/gpui-component/latest/gpui_component/message/struct.MessageFooter.html --- --- url: /docs/components/message-scroller.md description: >- A virtualized message list with tail following, history insertion, unread navigation, and customizable jump controls. --- # MessageScroller `MessageScroller` coordinates a variable-height virtual list with the behavior conversation screens usually need: follow the live tail, keep the current anchor while older history is inserted, navigate to an unread row, and expose whether the reader has left the tail. The application owns the message collection, stable message IDs, unread meaning, row renderer, composer, and empty/error states. The component owns only virtual-list bookkeeping and the optional jump-to-latest affordance. There is one state entity per scroller. ## Import ```rust use std::{rc::Rc, time::Duration}; use gpui_kit::{ IntoElement as _, ParentElement as _, StyleRefinement, Styled as _, prelude::FluentBuilder as _, }; use gpui_kit::component::{ ActiveTheme as _, button::ButtonVariants as _, message_scroller::{MessageScroller, MessageScrollerState}, Sizable as _, v_flex, }; ``` ## Create state and choose the starting position Create the state beside the application-owned message vector. The constructor receives the entity context because GPUI's list scroll handler notifies the entity after the list releases its internal borrow: ```rust let scroller = cx.new(|cx| MessageScrollerState::new(messages.len(), cx)); cx.observe(&scroller, |_, _, cx| cx.notify()).detach(); ``` `MessageScrollerState::new(...)` starts with tail following enabled. That is the expected starting position for a live conversation. A saved thread or a deep link can choose a row after the initial data has been installed: ```rust let saved_index = messages .iter() .position(|message| message.id == saved_message_id) .unwrap_or(messages.len().saturating_sub(1)); scroller.update(cx, |state, cx| { state.reset(messages.len(), cx); let _ = state.scroll_to_item(saved_index, cx); }); ``` There is no `starting_position` field or persisted scroll-offset API. Keep the application's saved message ID or index, then resolve it to the current index after records are loaded. `reset(...)` replaces the known row count and re-engages tail following; call `scroll_to_item(...)` after it when the product needs a different initial row. ## Render rows and an empty state Pass an indexed renderer. GPUI virtualizes the rows, so the closure is called for the rows needed by the current viewport and overdraw region: ```rust let messages = Rc::new(messages.clone()); MessageScroller::new( "conversation", scroller.clone(), move |index, _window, _cx| { let Some(message) = messages.get(index) else { return gpui_kit::div().into_any_element(); }; gpui_kit::div() .id(("message-row", message.id)) .min_w_0() .child(render_message(message)) .into_any_element() }, ) .w_full() .h_96() ``` The row ID in this example belongs to the application. `MessageScroller` does not retain an index-to-ID map; the ID lets an application-owned row keep its own element-local state when data changes. An empty list is valid (`MessageScrollerState::new(0, cx)`), but the scroller does not invent an empty placeholder. Render the empty, loading, error, or permission-denied state in the surrounding view and mount the scroller once there are rows: ```rust if messages.is_empty() { empty_conversation_view.into_any_element() } else { MessageScroller::new("conversation", scroller.clone(), render_message) .into_any_element() } ``` Keep the empty state separate from the scroll region so it can provide a meaningful action such as “Start a new conversation” without pretending there is a message to scroll to. ## Append, streaming, and follow-tail behavior Update application data and virtual-list count together. Appending while the reader follows the tail keeps the latest row visible. Appending after the reader scrolls up preserves the reader's position and makes the built-in jump button available: ```rust messages.push(new_message); scroller.update(cx, |state, cx| { let _ = state.append(1, cx); }); cx.notify(); ``` Streaming token growth changes a row's height without changing the item count. Update the message body, then remeasure that row: ```rust messages[index].body.push_str(next_token); scroller.update(cx, |state, cx| { let _ = state.remeasure_items(index..index + 1, cx); }); cx.notify(); ``` `remeasure_items(...)` preserves an item anchor while recalculating the selected rows. Use `remeasure(...)` after a global width, typography, or theme change that can affect many row heights. When streaming creates a new message rather than growing an existing one, call `append(1, cx)` first; remeasure the row again if its first render and later content have different heights. The state readers make the follow-tail decision visible to the surrounding view: ```rust let following_tail = scroller.read(cx).is_following_tail(); let show_new_messages = scroller.read(cx).is_scrolled_up(); ``` `is_following_tail()` is true when the list follows appended content. `is_scrolled_up()` is true when there is scrollable content below the current viewport and the reader is away from the end. The component does not decide whether to show a toast, unread count, or “new messages” copy; use these readers to drive an application-owned indicator. Resume following and move to the latest row explicitly: ```rust scroller.update(cx, |state, cx| state.scroll_to_end(cx)); ``` Normal scrolling to the end also allows GPUI's list to resume tail following. ## Prepend earlier history Insert older records at the front and tell the state the number of inserted rows. `prepend(...)` uses GPUI list splicing to preserve the visible item anchor: ```rust let earlier_messages = load_earlier_messages(); let count = earlier_messages.len(); messages.splice(0..0, earlier_messages); scroller.update(cx, |state, cx| { let _ = state.prepend(count, cx); }); cx.notify(); ``` Use `splice(old_range, count, cx)` for replacements or deletions elsewhere in the collection. The range is half-open and must stay within the current item count; invalid ranges return `false` and leave the state unchanged. For a “Load earlier” control, keep the loading state in the application, fetch the records, splice the vector, then call `prepend`. Do not call `reset` for ordinary history pagination because reset intentionally returns to tail following and loses the incremental anchor semantics. ## Unread and arbitrary navigation Unread identity belongs to the application. Resolve a stable message ID to the current vector index, then use `scroll_to_item(...)`: ```rust if let Some(index) = messages.iter().position(|message| message.id == unread_id) { scroller.update(cx, |state, cx| { let _ = state.scroll_to_item(index, cx); }); } ``` `scroll_to_item(...)` returns `false` for an out-of-range index. It is the single navigation primitive: an unread boundary, a search result, a bookmarked message, a reply target, and a deep link all resolve to an index in the application first. The current API does not expose turn anchors, peek previews, visible IDs, or stable-ID navigation. Map domain IDs to the current index in the application; keep an index map if lookup cost matters. The scroller does not know whether a row is a turn, a reply, an unread boundary, or a search result. ## Dynamic row heights and structural updates Rows may contain multiline text, attachments, streamed content, or an application-owned composer and can therefore have different heights. The underlying GPUI list measures rendered rows. Keep the renderer's height-affecting data in the owning view and notify the state after a mutation: | Change | State operation | | --- | --- | | Add rows at the tail | `append(count, cx)` | | Add rows at the front | `prepend(count, cx)` | | Replace/delete a range | `splice(range, count, cx)` | | Token growth in known rows | `remeasure_items(range, cx)` | | Global width/font/theme change | `remeasure(cx)` | | Replace the whole conversation | `reset(item_count, cx)` | Do not mutate the vector length without the matching state operation. The renderer receives an index, so data and virtual-list count must remain aligned for the same render pass. ## Jump-to-latest controls The built-in jump button is enabled by default and appears when `is_scrolled_up()` becomes true. It is a configured `Button` with a secondary variant, icon-button sizing, full radius, arrow-down icon, theme border/background, and a localized tooltip label. It keeps the scroll action owned by the state: ```rust MessageScroller::new("conversation", scroller.clone(), render_message) .with_jump_button_label("Jump to newest") .with_jump_button_transition(Duration::from_millis(250)) ``` Use `Duration::ZERO` to disable the enter/leave transition. Reduced-motion preferences use the final state immediately regardless of the configured duration. Refine its style after the built-in defaults: ```rust MessageScroller::new("conversation", scroller.clone(), render_message) .with_jump_button_style( StyleRefinement::default() .bg(cx.theme().primary) .border_color(cx.theme().primary) .text_color(cx.theme().primary_foreground), ) ``` Use the renderer callback when the application needs a different Button variant, size, icon, or instance style. The callback receives the fully configured button and must return a `Button`; the built-in scroll action stays attached: ```rust MessageScroller::new("conversation", scroller.clone(), render_message) .with_jump_button_renderer(|button| button.outline().small().label("Latest")) ``` During its leave transition the built-in button is rendered disabled while its opacity reaches zero. A renderer should preserve that state rather than force a disabled button to be active. The button's accessible name comes from its `.label(...)` value; `with_jump_button_label(...)` supplies the tooltip label only. Set a visible label in `with_jump_button_renderer(...)` when the jump action needs a named accessible control. The current public renderer callback can change Button styling and content, but it does not expose a separate accessibility-label builder. Disable the built-in affordance when the surrounding view provides its own: ```rust MessageScroller::new("conversation", scroller.clone(), render_message) .jump_button(false) ``` Compose an application-owned button from `is_scrolled_up()` and `scroll_to_end(...)` when its placement, text, or accessibility contract needs to be product-specific. ## Scrollbar and style slots The root implements `Styled`, and the internal regions have separate style refinements: ```rust MessageScroller::new("conversation", scroller.clone(), render_message) .p_2() .bg(cx.theme().group_box) .with_content_style(StyleRefinement::default().bg(cx.theme().background)) .with_list_style(StyleRefinement::default().p_4()) .with_row_style(StyleRefinement::default().pb_6()) .scrollbar(false) ``` The boundaries are: * Root `Styled` methods refine the full-width element that owns the viewport. * `with_content_style(...)` refines the viewport containing the list and optional vertical scrollbar. * `with_list_style(...)` refines the GPUI virtual list after its default `px_3()` / `py_2()` padding. GPUI lists offset rows only vertically, so the horizontal padding component — the default and any refinement — is carried by every row wrapper. * `with_row_style(...)` refines the full-width wrapper around each rendered row; the default wrapper includes `pb_8()` between rows, like a CSS gap. The list's own bottom padding owns the gap between the last row and whatever sits below the transcript. * `scrollbar(false)` hides the built-in vertical scrollbar; it does not disable scrolling or remove keyboard/wheel interaction. * `with_bottom_fade(color)` fades the transcript's bottom edge into the given color, so a partially visible row melts into the surrounding surface instead of clipping mid-line. It shows only while the reader is away from the live edge — at the bottom nothing is clipped. Pass the color of the surface behind the scroller; the fade is off by default. Use theme roles such as `group_box`, `background`, `border`, and `foreground` for custom surfaces. Keep content padding in the surrounding conversation shell when it belongs to the shell's header/composer relationship; use list or row style when it belongs to every transcript row. ## Virtualization, accessibility, and application boundaries `MessageScroller` delegates viewport layout, variable-height measurement, scroll anchoring, and overdraw to GPUI's `ListState`. Only visible rows and the configured overdraw region need rendering. Keep row closures deterministic and avoid doing network work or mutating the message collection during rendering. For keyboard and screen-reader behavior: * Keep the scroller inside a layout with a real height and `min_h_0()` so the scroll region can receive wheel and keyboard navigation. * The transcript viewport announces itself as a log region (`Role::Log`), so assistive technology can treat appended rows as live additions.- Wheel scrolling over the transcript is contained: while the list can move, the event never scrolls an ancestor scroller; at the top or bottom edge it chains to the ancestor, matching platform scroll containers. * Give rows meaningful text and stable application IDs; an index by itself is not a user-facing label. * Give the jump control an explicit visible label when it must be exposed as a named accessible action. Its tooltip is supplemental. * Place “Load earlier”, retry, composer, and unread controls outside the list in semantic `Button` or `Link` controls. * Keep empty, loading, error, and permission states readable without relying on animation or scrollbar position. The component intentionally has no React-style Provider, Viewport, Content, or Item exports. GPUI's list already supplies those layers; an indexed renderer is the item boundary. It also has no turn-anchor, peek, visible-range, or stable-ID API. Those concepts vary by product and belong in the application model around this component. ## API reference ### `MessageScrollerState` | Method | Default/return | Purpose | | --- | --- | --- | | `new(item_count, cx)` | tail following enabled | Create state for the current row count. | | `item_count()` | current count | Read the virtual-list row count. | | `is_scrolled_up()` | `false` until away from tail | Report whether a jump/new-content affordance is useful. | | `is_following_tail()` | `true` initially | Report whether appended rows are followed. | | `reset(item_count, cx)` | re-engages tail | Replace the row count and reset list state. | | `splice(range, count, cx)` | `true` if valid | Replace a half-open range while preserving list bookkeeping. | | `append(count, cx)` | `splice` at tail | Add rows at the end. | | `prepend(count, cx)` | `splice` at index 0 | Add earlier rows while preserving the current anchor. | | `remeasure(cx)` | — | Remeasure all rows after global layout changes. | | `remeasure_items(range, cx)` | `true` if valid | Remeasure selected dynamic rows. | | `scroll_to_item(index, cx)` | `false` if out of range | Navigate to an arbitrary row index. | | `scroll_to_end(cx)` | tail following enabled | Move to the latest row and resume following. | ### `MessageScroller` | Method | Default | Purpose | | --- | --- | --- | | `new(id, state, renderer)` | scrollbar and jump button enabled | Create a virtualized scroller. | | `scrollbar(bool)` | `true` | Show or hide the internal scrollbar. | | `jump_button(bool)` | `true` | Show or hide the built-in jump control. | | `with_jump_button_label(label)` | `Jump to latest` | Set the jump tooltip/localized label. | | `with_content_style(style)` | empty refinement | Style the viewport and scrollbar region. | | `with_list_style(style)` | list `px_3()` / `py_2()` | Style the virtual list. | | `with_row_style(style)` | row `pb_8()` | Style every rendered row wrapper. | | `with_jump_button_style(style)` | themed secondary button | Refine the built-in button. | | `with_jump_button_renderer(callback)` | default Button | Adjust the configured button while keeping its action. | | `with_jump_button_transition(duration)` | 200 ms | Set enter/leave duration; reduced motion skips it. | | `with_bottom_fade(color)` | off | Fade the bottom edge into the surrounding surface color. | | `Styled` methods | full-size, clipped root | Style the outer scroller element. | [MessageScroller]: https://docs.rs/gpui-component/latest/gpui_component/message_scroller/struct.MessageScroller.html [MessageScrollerState]: https://docs.rs/gpui-component/latest/gpui_component/message_scroller/struct.MessageScrollerState.html --- --- url: /zh-CN/docs/components/message-scroller.md description: 支持尾部跟随、未读定位和稳定 prepend 的虚拟消息列表。 --- # MessageScroller `MessageScroller` 将 GPUI 的可变高度虚拟列表与会话常见的尾部跟随行为组合起来。它负责虚拟 row、滚动状态、append/prepend 的结构同步和可选的“跳到最新”按钮;消息数据、稳定消息 ID、未读规则和请求状态仍由应用持有。 ## 适用场景与所有权 适合以下会话列表: * 新消息到达时,用户在底部会自动跟随,向上阅读时保持当前位置。 * 历史消息从顶部插入时,当前可见内容保持稳定。 * 流式响应导致已有 row 高度变化,需要局部重新测量。 * 需要未读边界、任意 index 定位或自定义跳转按钮。 `MessageScroller` 不保存消息模型、消息 ID 到 index 的映射、网络请求、未读持久化或空/错误状态。应用负责把数据变化与状态 Entity 的变化放在同一个更新流程中。 ## 导入 ```rust use std::time::Duration; use gpui_kit::{div, IntoElement as _, ParentElement as _, StyleRefinement, Styled as _}; use gpui_kit::component::{ ActiveTheme as _, button::ButtonVariants as _, message_scroller::{MessageScroller, MessageScrollerState}, Sizable as _, StyledExt as _, }; ``` ## 创建状态 把 `MessageScrollerState` 存在应用 view 的 Entity 中,与消息集合一起拥有: ```rust let scroller = cx.new(|cx| MessageScrollerState::new(messages.len(), cx)); // 父 view 读取滚动状态或渲染 scroller 时,观察 Entity 以响应滚动事件。 cx.observe(&scroller, |_, _, cx| cx.notify()).detach(); ``` 状态构造器会把 GPUI `ListState` 设置为尾部跟随,并安装一次延迟 scroll handler。GPUI 触发 handler 时可能仍持有内部 list 借用,因此不要把 Entity 更新直接塞进 list 的同步回调之外的自定义借用逻辑。 ## 渲染 row 传入按 index 渲染 row 的闭包。GPUI 会虚拟化 row,应用只需要渲染当前 index 对应的消息: ```rust MessageScroller::new( "conversation", scroller.clone(), move |index, window, cx| { render_message(&messages[index], window, cx) }, ) .w_full() .h_96() ``` `render_message(...)` 可以返回 `Message`、`MessageGroup` 或应用自己的 row。row 的稳定 element ID 应由应用根据消息 ID 生成;`MessageScroller` 只接收 index,不保存 index 到 ID 的映射。 空列表不会调用 row renderer。空状态、加载历史、网络错误和权限提示应由应用在 scroller 外层或 `item_count == 0` 的分支中组合: ```rust if messages.is_empty() { div() .size_full() .flex() .items_center() .justify_center() .child("还没有消息") .into_any_element() } else { MessageScroller::new("conversation", scroller.clone(), render_message) .into_any_element() } ``` 不要把空状态硬编码进 `MessageScroller`,这样应用才能区分“没有消息”“正在加载”和“加载失败”。 ## Append、流式响应与重新测量 消息数据和列表结构必须同步更新: ```rust messages.push(new_message); scroller.update(cx, |state, cx| { state.append(1, cx); }); cx.notify(); ``` 只有列表正在跟随尾部时,`append(...)` 才会自动滚到新的尾部;用户向上阅读时,新消息保留在列表底部并使“跳到最新”按钮出现。 流式响应通常不会增加 row 数量,而是改变现有消息的文本和高度。更新消息后重新测量对应的 index: ```rust messages[index].content.push_str(delta); scroller.update(cx, |state, cx| { state.remeasure_items(index..index + 1, cx); }); cx.notify(); ``` 如果字体、窗口宽度或文本布局全局变化,重新测量所有 row: ```rust scroller.update(cx, |state, cx| state.remeasure(cx)); ``` `remeasure_items(...)` 和 `remeasure(...)` 只标记布局重新计算,不改变应用消息数据。调用方应保证 range 在当前 `item_count()` 范围内。 ## 尾部跟随 状态提供两个 reader: ```rust let following_tail = scroller.read(cx).is_following_tail(); let scrolled_up = scroller.read(cx).is_scrolled_up(); ``` `is_following_tail()` 表示新增 row 是否会推动 viewport;`is_scrolled_up()` 表示用户已经离开最新内容且当前不在末尾。应用可以用后者显示自己的提示或通知,但通常直接保留内置跳转按钮即可。 用户滚动到末尾后,list 会恢复尾部跟随。调用 `scroll_to_end(...)` 会显式恢复跟随模式并滚到最新 row: ```rust scroller.update(cx, |state, cx| state.scroll_to_end(cx)); ``` “正在生成”的状态、暂停自动滚动和“新消息”提示属于应用交互;MessageScroller 只负责尾部跟随的列表行为。 ## Prepend 历史消息 加载更早消息时,先在应用数据开头插入,再告诉状态增加了多少 row: ```rust messages.splice(0..0, earlier_messages); scroller.update(cx, |state, cx| { state.prepend(earlier_count, cx); }); cx.notify(); ``` `prepend(...)` 会通过 GPUI list 的 splice 保留当前 item 锚点,使用户正在阅读的内容尽量保持在原来的 viewport 位置。不要只更新 `messages` 而忘记更新 scroller;否则 renderer 的 index 与 list 的 row 数量会失去同步。 任意结构替换使用 `splice(...)`: ```rust // 用新的 3 条记录替换 index 10..12 的两条记录。 messages.splice(10..12, replacement_messages); scroller.update(cx, |state, cx| { state.splice(10..12, 3, cx); }); ``` range 必须满足 `start <= end <= item_count()`;无效 range 会返回 `false`,且不改变状态。`append(...)` 和 `prepend(...)` 同样返回是否成功。 ## 未读与 index 定位 未读 ID 和消息 ID 属于应用模型。先把 ID 转成当前 index,再调用 `scroll_to_item(...)`: ```rust if let Some(index) = messages .iter() .position(|message| message.id == first_unread_id) { scroller.update(cx, |state, cx| { state.scroll_to_item(index, cx); }); } ``` `scroll_to_item(...)` 以 index 为 viewport 起始位置并暂停尾部跟随;靠近末尾时受可用滚动范围限制,index 超出当前数量时返回 `false`。它是唯一的定位原语:未读边界、搜索结果、书签消息、回复目标、深链接都先在应用侧解析成 index。组件没有 ID-native 的定位、turn anchor、peek 或可见 ID API;这些行为应由应用维护 ID/index 映射,并在需要时组合自己的 header、提示或高亮。 ## Reset 与初始位置 切换会话或重新加载一组完全不同的数据时,先替换应用数据,再 `reset(...)`: ```rust messages = load_thread(thread_id); scroller.update(cx, |state, cx| { state.reset(messages.len(), cx); }); cx.notify(); ``` `reset(...)` 会重新安装 row 数量并恢复尾部跟随。若产品需要从未读位置或已保存 index 打开会话,可在 reset 后由应用调用 `scroll_to_item(...)`;已保存位置的持久化和恢复条件不属于组件状态。 ## 跳到最新按钮 默认会在用户离开尾部时显示内置按钮。可以本地化标签、关闭按钮或自定义样式: ```rust MessageScroller::new("conversation", scroller.clone(), render_message) .with_jump_button_label("跳到最新") .with_jump_button_style( StyleRefinement::default() .border_color(cx.theme().border), ) .with_jump_button_transition(Duration::from_millis(250)) ``` `Duration::ZERO` 会关闭按钮的进入/离开过渡;系统启用 reduced motion 时直接使用最终状态。 需要完全由应用提供按钮时,可以关闭内置入口,并根据 `is_scrolled_up()` 和 `scroll_to_end(...)` 组合自己的 Button: ```rust MessageScroller::new("conversation", scroller.clone(), render_message) .jump_button(false) ``` `with_jump_button_renderer(...)` 接收已经配置好默认行为的 `Button`,因此可以调整 variant、语义尺寸、图标、可见 label 或实例样式,同时保留内置滚动操作: ```rust MessageScroller::new("conversation", scroller.clone(), render_message) .with_jump_button_label("跳到最新") .with_jump_button_renderer(|button| { button.outline().large().label("跳到最新") }) ``` 如果只需要修改 Button 的样式,优先使用 `with_jump_button_style(...)`;需要替换 label 或 variant 时使用 renderer。 ## Scrollbar、列表和 row 样式 三个 style slot 对应不同布局边界: ```rust MessageScroller::new("conversation", scroller.clone(), render_message) .scrollbar(false) .with_content_style( StyleRefinement::default().bg(cx.theme().background), ) .with_list_style( StyleRefinement::default().px_4().py_3(), ) .with_row_style( StyleRefinement::default().pb_6(), ) ``` | Builder | 作用范围 | | --- | --- | | `with_content_style(...)` | 内部 viewport 与 scrollbar 所在的容器。 | | `with_list_style(...)` | GPUI list 本身,包括默认 list padding 的 refinement。 | | `with_row_style(...)` | 每个 renderer row 外层的全宽包装。 | | `Styled` | `MessageScroller` 根容器。 | 组件默认只在 row 之间保留 `pb_8()` 间距(类似 CSS gap),最后一行到消息区下方内容的间距由 list 自己的底部 padding 承担。自定义 `with_row_style(...)` 时应明确自己是否要额外增加间距,避免重复 padding。GPUI list 只在垂直方向偏移 row,因此 list padding 的水平分量(默认值与 refinement 均是)由每个 row 包装层承载。 `with_bottom_fade(color)` 让消息区底缘渐隐到指定颜色:被裁切一半的 row 融入 scroller 背后的表面,而不是在行中间生硬截断。渐隐只在读者离开末尾时显示——滚到最底时下方没有被裁内容,不再遮挡最后一行。传入 scroller 所在表面的颜色;默认关闭。 ## 虚拟化、性能与可变高度 * `MessageScroller` 使用 GPUI `list(...)`,只创建 viewport 附近的 row;不需要额外 Provider、Viewport、Content 或 Item wrapper。 * renderer 应保持轻量,不要在 render 闭包中同步执行网络、文件读取或昂贵解析;先在应用状态层准备数据。 * 消息内容变化但 row 数量不变时使用 `remeasure_items(...)`,全局字体或宽度变化时使用 `remeasure(...)`。 * prepend 前后保持同一消息 ID 到数据记录的顺序,避免应用在 list 更新期间重排未涉及的消息。 * `item_count()` 是 list 记录的事实来源;应用数组长度与它不一致时应先用 `reset` 或 `splice` 修复结构。 ## 可访问性 * 默认跳转按钮是可聚焦的 `Button`;`with_jump_button_label(...)` 只设置本地化 tooltip。若使用 renderer 替换为 icon-only 外观,应通过 `.label("跳到最新")` 保留可读名称,或关闭内置按钮后由应用提供自己的带 label Button。 * “跳到最新”“加载更早消息”“正在生成”“加载失败”等状态应提供文本或明确的 Button label,不依赖滚动位置和颜色。 * 键盘用户应能访问消息 row 中的 Link、Button、附件操作和应用自定义滚动入口。 * 消息区 viewport 以 log 区域(`Role::Log`)对外声明,辅助技术可以把追加的 row 当作实时新增内容播报。- 消息区上的滚轮事件是被包含的:list 还能滚动时事件不会带动外层滚动容器;到达顶部或底部边缘后才交给外层,与平台滚动容器的链式行为一致。 * 空状态和错误状态应由应用渲染可读内容;不要让一个空的虚拟列表看起来像加载失败。 * 自定义 jump transition、row 动画或流式高亮时,遵循系统 reduced motion,并提供静态最终状态。 ## 组件边界 GPUI 版本保留必要的滚动行为,省略 React primitive 中重复的 Provider、Viewport、Content、Item 和 Button 导出: * `Entity` 提供状态所有权与通知,不需要 React Context。 * GPUI `list(...)` 已经负责 viewport、虚拟内容、item 测量和滚动锚点。 * index renderer 已经是 row 边界,再增加 `MessageScrollerItem` 只会包装任意内容。 * 跳转操作复用现有 `Button`,应用可以关闭内置按钮并自行组合。 * 消息 ID、未读 ID、错误状态和历史加载属于业务域,由应用保留。 ## API 参考 ### `MessageScrollerState` | 方法 | 说明 | | --- | --- | | `new(item_count, cx)` | 创建指定 row 数量并启用尾部跟随的 Entity 状态。 | | `item_count()` | 返回当前虚拟 row 数量。 | | `is_scrolled_up()` | 判断是否离开尾部且当前不在末尾。 | | `is_following_tail()` | 判断是否会继续跟随新增内容。 | | `reset(item_count, cx)` | 重置 row 数量并恢复尾部跟随。 | | `splice(old_range, count, cx)` | 用指定数量替换已有 range;无效 range 返回 `false`。 | | `append(count, cx)` | 在尾部增加 row。 | | `prepend(count, cx)` | 在开头增加 row,并保留当前滚动锚点。 | | `remeasure(cx)` | 标记所有 row 重新测量。 | | `remeasure_items(range, cx)` | 标记指定 range 重新测量。 | | `scroll_to_item(index, cx)` | 定位到指定 index;越界返回 `false`。 | | `scroll_to_end(cx)` | 恢复尾部跟随并滚到最新内容。 | ### `MessageScroller` | 方法 | 说明 | | --- | --- | | `new(id, state, renderer)` | 创建虚拟消息列表;renderer 接收 `(index, window, cx)`。 | | `scrollbar(bool)` | 显示或隐藏内置 scrollbar。 | | `jump_button(bool)` | 显示或隐藏内置“跳到最新”按钮。 | | `with_jump_button_label(label)` | 设置按钮使用的本地化 tooltip 文本;不会替代 Button 的可读 label。 | | `with_content_style(style)` | 调整 viewport 容器。 | | `with_list_style(style)` | 调整 GPUI list。 | | `with_row_style(style)` | 调整每个 row 外层包装。 | | `with_jump_button_style(style)` | 在按钮默认样式之后应用 refinement。 | | `with_jump_button_renderer(renderer)` | 修改已配置行为的 Button,并保留滚动操作。 | | `with_jump_button_transition(duration)` | 设置按钮显示/隐藏过渡;零时长关闭过渡。 | | `with_bottom_fade(color)` | 底缘渐隐到周围表面的颜色;默认关闭。 | | `Styled` | 调整根容器。 | ### 类型链接 * [MessageScroller] * [MessageScrollerState] [MessageScroller]: https://docs.rs/gpui-component/latest/gpui_component/message_scroller/struct.MessageScroller.html [MessageScrollerState]: https://docs.rs/gpui-component/latest/gpui_component/message_scroller/struct.MessageScrollerState.html --- --- url: /base/motion.md description: >- Typed transitions, springs, keyframes, presence, stagger, and reduced-motion behavior in gpui-base. --- # Motion `gpui-base` owns deterministic motion sampling and lifecycle while leaving every visual choice to the application. It provides stable keyed state, interruption, reversal, animation-frame requests, and reduced-motion behavior without imposing product timing or styling. Run the interactive companion for this guide: ```bash cargo run -p gpui-base --example motion ``` The example contains five separate demos. Use the tabs at the top to inspect one capability at a time. ## Capability map | Demo | API | What it demonstrates | | --- | --- | --- | | Sliding time | `transition` | Four independently rolling digits, 08:00–20:00, with targets changing faster than the transition settles | | Spring | `spring` | A segmented-control indicator that preserves velocity when rapidly retargeted | | Keyframes | `Keyframes`, `Timing`, `animate_keyframes` | A repeating multi-stop activity signal | | Stagger | `Stagger` | Allocation-free timing offsets across a list | | Presence | `Presence` | Exit animation that keeps content mounted until it becomes absent | The library also exposes `Easing`, `Discrete`, `MotionTransform`, and `MotionReveal`. They compose with the same primitives rather than requiring separate animation runtimes. ## Target transitions Use `transition` for a value moving toward a target over a known duration. Every independently animated value needs a stable ID. ```rust let opacity = transition( ("save-dialog", "opacity"), if open { 1.0 } else { 0.0 }, Transition::new(Duration::from_millis(180)).easing(Easing::EaseOut), window, cx, ); ``` Retargeting starts at the currently sampled value. Direct reversal shortens the return duration, so reversing early does not spend a full duration retracing a short distance. `transition_with_status` additionally returns `Idle`, `Delayed`, `Running`, or `Finished`. `Easing` includes CSS keyword curves, cubic Bézier curves, all CSS step positions, and piecewise `linear()` stops. Invalid parameters return typed errors. ## Springs Use `spring` when the target may change while moving. It preserves both position and velocity, which makes it suitable for selection indicators and settling spatial values. ```rust let x = spring( "selected-indicator", selected_x, Spring::new(Duration::from_millis(420)).with_damping(0.72), window, cx, ); ``` Do not make a pointer-controlled value chase the pointer through a spring. Set `with_travel(false)` during direct manipulation and restore travel after release. `with_damping` requires a finite, non-negative ratio; `with_epsilon` requires a finite value greater than zero and interprets it in the target's own units. The builders panic for invalid trusted constants. Use `try_with_damping` and `try_with_epsilon` for configuration or user-provided values. Normalized values normally keep the `0.001` default; pixel motion can use a coarser tolerance such as `0.1`. ## Keyframes and timing `Keyframes` describes validated value stops. `Timing` uses absolute elapsed time and supports signed delays, finite or infinite iterations, and normal, reverse, or alternating playback. ```rust let frames = Keyframes::try_new([ Keyframe::new(0.0, 0.25), Keyframe::new(0.45, 1.0).ease(Easing::EaseOut), Keyframe::new(1.0, 0.25), ])?; let opacity = animate_keyframes( "activity", &frames, Timing::new(Duration::from_millis(1400)) .iterations(IterationCount::Infinite), window, cx, ).value; ``` Offsets must start at `0`, end at `1`, and be monotonic. Use `Discrete` when a value cannot be interpolated. `animate_keyframes` retains its playback start time under the supplied stable ID. Re-rendering with the same ID continues the current sequence. To replay it, include an application-owned generation in the ID, such as `("notification-enter", generation)`, and increment that generation for each replay. ## Presence and stagger `Presence` separates logical visibility from physical mounting. Its phases are entering, present, exiting, and absent. Render while `should_render()` is true and use `progress` for the chosen visual properties. Reopening during exit reverses from the current sample. `Stagger` calculates a delay for an index from the first, last, center, or a chosen origin. It does not allocate a schedule or own list identity: ```rust let stagger = Stagger::new(Duration::from_millis(80), StaggerOrigin::First); let delay = stagger.delay(index, item_count); ``` ## Measured reveal `MotionReveal` measures a child at its natural size and clips its visible height by progress. `Collapsible::motion_id(...)` is the convenient control-level facade. Without a motion ID, the control keeps immediate mount/unmount behavior. ## Reduced motion and performance Transitions, springs, keyframes, presence, and reveal-compatible controls honor GPUI's reduced-motion preference. Finite motion snaps to the target, synchronizes retained state, and leaves no pending animation frame. Motion must never be the only way state is communicated. The pure steady sampling paths measured by the benchmark—timing/easing, keyframe lookup, analytic spring integration, and stagger delay calculation—are allocation-free. Keyed transition, spring, presence, and reveal lifecycles are covered by GPUI retained-state and frame-request tests because those updates belong to the framework lifecycle rather than the pure sampler. Sampling uses absolute elapsed time, and keyframe lookup uses binary search. Run the release benchmark with: ```bash cargo bench -p gpui-base --bench motion ``` Choose the smallest suitable primitive: `transition` for duration-based targets, `spring` for changing spatial targets, keyframes for authored sequences, `Presence` for exit-before-unmount, and `Stagger` for list choreography. ## Benchmark results Measured on Linux x86\_64 with a release build, 31 batches, and 200 iterations per batch: | Workload | Median | P95 | Worst | Allocations | | --- | ---: | ---: | ---: | ---: | | 1,000 scalar timing + easing samples | 26.490 µs | 26.567 µs | 27.290 µs | 0 | | 1,000 keyframe samples, 2 frames | 21.656 µs | 21.707 µs | 21.729 µs | 0 | | 1,000 keyframe samples, 8 frames | 25.197 µs | 25.251 µs | 25.269 µs | 0 | | 1,000 keyframe samples, 32 frames | 27.932 µs | 27.969 µs | 27.971 µs | 0 | | 1,000 analytic spring integration samples | 6.042 µs | 6.106 µs | 6.216 µs | 0 | | 1,000 stagger delay calculations | 0.574 µs | 0.583 µs | 0.587 µs | 0 | The scalar timing/easing workload remains below its 100 µs median budget. These figures are a reproducible development baseline rather than a cross-platform guarantee; run the benchmark on each target platform when platform-specific performance matters. --- --- url: /base/primitives/nav-stack.md description: >- A navigation stack of views with push, pop, forward, and replace, and an animatable transition lifecycle. --- # Nav Stack A last-in-first-out stack of views, one visible at a time: push a view over the current one, pop back to the one below, or replace the top. It is SwiftUI's `NavigationStack`, Qt's `StackView`, and WinUI's `Frame`. Underneath it is a [History](../history.md) whose active entries run from the root through the current page. A popped page becomes a forward entry until the next push discards that forward branch, so `forward` brings it back the way WinUI's `GoForward` does. Like every `gpui-base` primitive, Nav Stack supplies behavior and semantic structure without imposing a product visual language. The pages are views you create, and how a change between them moves is decided by your item renderer. ## Example The [single native Cargo entrypoint](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/components.rs) selects this primitive from the [shared showcase implementation](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/mod.rs). The same showcase is compiled once for the WASM preview above. ```bash cargo run -p gpui-base --example components -- nav-stack ``` ## Import ```rust use gpui_kit::base::{NavMotion, NavOperation, NavPage, NavStack, NavStackState}; use gpui_kit::base::motion::{PresencePhase, Transition}; ``` ## Anatomy and API `NavStackState` is the stack. It lives in a GPUI entity, holds `AnyView`s root first, and emits `NavStackEvent` after every change. | Method | Does | | ------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- | | `push(view, motion, cx)` | Pushes over the current top. Into an empty stack it is immediate, like Qt's `initialItem`. | | `pop(motion, cx)` | Pops the top and returns it. The root is never popped, so this returns `None` at a depth of one. | | `pop_to_root(motion, cx)` | Pops everything above the root in one transition and returns those views. | | `forward(motion, cx)` | Brings back the most recently popped view over the current top and returns it. `None` when nothing has been popped since the last push. | | `replace(view, motion, cx)` | Swaps the top for `view` and returns the one replaced, keeping the forward views. On an empty stack it pushes. | | `clear(cx)` | Empties the stack and the forward views immediately. | | `depth()`, `is_empty()`, `current()`, `views()`, `forward_views()` | Read the stack. Show a back button when `depth() > 1`, a forward button when `forward_views()` is not empty. | `NavStack` is the element. It holds the entity, takes a `transition` to run each change under, and hands every mounted view to the `item` renderer as a `NavPage`. Style the element for size, background and clipping; it is positioned so that the two pages of a change can overlap. `NavPage` is what the renderer receives. It already fills the container. Read `phase()` (`Entering`, `Present` or `Exiting`), `operation()` (`Push`, `Pop` or `Replace`, or `None` once settled) and `progress()` (eased, `0.0` to `1.0`, shared by both pages of one change), refine the page with GPUI styles, and return it. The authoritative module is [`components/nav_stack.rs`](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/components/nav_stack.rs). Native and browser previews compile this same file. ## Animation Animation is decided at two levels, and both default to none: * **The stack.** `NavStack` without a `transition` never animates; every change switches on the spot. Give it a `Transition` to animate changes, and an `item` renderer to say how. * **The change.** Each `push`, `pop`, `pop_to_root` and `replace` takes a `NavMotion`, as UIKit's `animated:` and Qt's `StackView.Immediate` do per call. `NavMotion::Animated` runs the stack's transition; `NavMotion::Immediate` switches on the spot even on an animated stack, which is what restoring a stack at launch or jumping to a page from a command wants. ```rust stack.update(cx, |stack, cx| stack.push(detail, NavMotion::Animated, cx)); stack.update(cx, |stack, cx| stack.push(restored, NavMotion::Immediate, cx)); ``` ## Transitions After a push, pop or replace, the outgoing view stays mounted until the element's `Transition` finishes. Paint order follows the operation: a pushed or replacing page paints over the page it covers, and a popped page paints over the page it reveals, so a slide reads correctly in both directions. ```rust NavStack::new(&self.stack) .size_full() .overflow_hidden() .transition(Transition::new(Duration::from_millis(220))) .item(|page, _, _| { let offset = match (page.phase(), page.operation()) { (PresencePhase::Entering, Some(NavOperation::Push)) => 1.0 - page.progress(), (PresencePhase::Exiting, Some(NavOperation::Pop)) => page.progress(), _ => 0.0, }; page.left(relative(offset)).into_any_element() }) ``` The stack also switches immediately when the platform asks for reduced motion, whatever the renderer would have drawn. A new operation while a transition is running supersedes it, and the pages reverse from where they are rather than jumping. While a change runs, neither page takes pointer input. ## State and events Keep the `NavStackState` entity on the view that renders the stack and observe it, so a push from anywhere re-renders the host. A page that needs to navigate holds a `WeakEntity` of the stack, as the showcase page does. `views()` and `forward_views()` are enough for a history menu: list both, and pop or forward until the chosen page is current. The showcase page draws that list as a trail of page numbers, the pages ahead greyed out. Focus is not moved by the stack. `AnyView` carries no focus handle; a page that wants focus takes it when it is pushed, as it would anywhere else. ## Complete Rust example The complete implementation used by the runnable showcase is embedded directly from Rust source: The command above supplies application initialization, window creation, and shared `BaseShowcase` state. ## Accessibility Announce the page change in the page itself: a heading at the top of each page gives assistive technology a landmark to land on after a push. The stack keeps only the current page interactive once a transition has finished. ## Notes Pages are entities. The stack retains the ones on it and the ones popped since the last push, which `forward` can bring back, so a page's own subscriptions and timers live until a push discards it or the stack is cleared. Verify reduced-motion behavior in the consuming design system. --- --- url: /zh-CN/base/primitives/nav-stack.md description: 支持 push、pop、forward 与 replace 的视图导航栈,过渡生命周期可动画。 --- # Nav Stack 后进先出的视图栈,同一时刻只显示一个:把新视图 push 到当前视图之上,pop 回到下面那个,或 replace 掉栈顶。它对应 SwiftUI 的 `NavigationStack`、Qt 的 `StackView` 和 WinUI 的 `Frame`。底层是一份视图的 [History](../history.md),活动条目从根页面排列到当前页面。pop 掉的页面会成为前进条目,直到下一次 push 丢弃这条前进分支,所以 `forward` 能像 WinUI 的 `GoForward` 一样把它带回来。 和所有 GPUI Base 原语一样,Nav Stack 只提供行为和语义结构,不规定产品视觉语言。页面是你创建的视图,页面之间怎么切换由你的 item renderer 决定。 ## 示例 原生示例和页面上方的 WASM 预览共用同一份实现: ```bash cargo run -p gpui-base --example components -- nav-stack ``` ## 导入 ```rust use gpui_kit::base::{NavMotion, NavOperation, NavPage, NavStack, NavStackState}; use gpui_kit::base::motion::{PresencePhase, Transition}; ``` ## 结构与 API `NavStackState` 就是栈。它放在 GPUI entity 里,按根在前的顺序持有 `AnyView`,每次变化后 emit `NavStackEvent`。 | 方法 | 作用 | | ------------------------------------------------------------------ | ------------------------------------------------------------------------------------- | | `push(view, motion, cx)` | 压到当前栈顶之上。压入空栈时立即生效,与 Qt 的 `initialItem` 一致。 | | `pop(motion, cx)` | 弹出栈顶并返回它。根页面永远不会被弹出,深度为 1 时返回 `None`。 | | `pop_to_root(motion, cx)` | 用一次过渡弹掉根以上的全部页面,并返回它们。 | | `forward(motion, cx)` | 把最近弹掉的页面带回到当前栈顶之上并返回它。上次 push 之后没有弹过页面时返回 `None`。 | | `replace(view, motion, cx)` | 用 `view` 换掉栈顶并返回被换掉的页面,前进页保留。空栈时等于 push。 | | `clear(cx)` | 立即清空栈和前进页。 | | `depth()`、`is_empty()`、`current()`、`views()`、`forward_views()` | 读取栈。`depth() > 1` 时显示返回按钮,`forward_views()` 非空时显示前进按钮。 | `NavStack` 是元素。它持有 entity,用 `transition` 指定每次变化的时长,把每个已挂载的视图作为 `NavPage` 交给 `item` renderer。元素本身负责尺寸、背景和裁剪;它已经设置了定位,让一次变化中的两个页面可以重叠。 `NavPage` 是 renderer 收到的东西,已经铺满容器。读取 `phase()`(`Entering`、`Present` 或 `Exiting`)、`operation()`(`Push`、`Pop`、`Replace`,稳定后为 `None`)和 `progress()`(已缓动,`0.0` 到 `1.0`,一次变化中两个页面共用),用 GPUI 样式修饰后返回。 权威实现位于 [`components/nav_stack.rs`](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/components/nav_stack.rs),原生与浏览器预览编译的是同一文件。 ## 动画 动画在两个层面决定,默认都没有: * **整个栈。** 不带 `transition` 的 `NavStack` 永远不动画,每次变化立即切换。给它一个 `Transition` 就会动画,再用 `item` renderer 决定怎么动。 * **单次变化。** `push`、`pop`、`pop_to_root` 和 `replace` 都接收一个 `NavMotion`,对应 UIKit 的 `animated:` 和 Qt 的 `StackView.Immediate`。`NavMotion::Animated` 走栈的 transition;`NavMotion::Immediate` 即便栈配了动画也立即切换,启动时恢复栈、从命令直接跳到某页时用它。 ```rust stack.update(cx, |stack, cx| stack.push(detail, NavMotion::Animated, cx)); stack.update(cx, |stack, cx| stack.push(restored, NavMotion::Immediate, cx)); ``` ## 过渡 push、pop 或 replace 之后,出场的视图会一直挂载到元素的 `Transition` 结束。绘制顺序跟随操作:push 或 replace 进来的页面盖在被它覆盖的页面上,pop 出去的页面盖在被它露出的页面上,所以滑动两个方向都正确。 ```rust NavStack::new(&self.stack) .size_full() .overflow_hidden() .transition(Transition::new(Duration::from_millis(220))) .item(|page, _, _| { let offset = match (page.phase(), page.operation()) { (PresencePhase::Entering, Some(NavOperation::Push)) => 1.0 - page.progress(), (PresencePhase::Exiting, Some(NavOperation::Pop)) => page.progress(), _ => 0.0, }; page.left(relative(offset)).into_any_element() }) ``` 系统要求减少动态效果时,无论 renderer 想画什么,栈都立即切换。过渡进行中来了新操作,新操作接管,页面从当前位置反向,不会跳变。过渡进行期间两个页面都不接收指针输入。 ## 状态与事件 把 `NavStackState` entity 放在渲染栈的视图上并 observe 它,这样从任何地方 push 都会让宿主重绘。需要导航的页面持有栈的 `WeakEntity`,示例页面就是这么做的。 `views()` 和 `forward_views()` 足够做一个历史菜单:把两者列出来,选中后连续 pop 或 forward 到那一页。示例页面把这个列表画成一行页码,前方的页面灰显。 栈不会移动焦点。`AnyView` 不带 focus handle;需要焦点的页面在被 push 时自己拿,和在其他地方一样。 ## 完整 Rust 示例 ## 可访问性 页面切换由页面自己宣告:每页顶部放一个标题,辅助技术在 push 之后有地方落脚。过渡结束后栈只保留当前页面可交互。 ## 注意事项 页面是 entity。栈会保留在栈上的页面,以及上次 push 之后弹掉、`forward` 能带回来的页面,所以页面自己的订阅和定时器会一直活到某次 push 丢弃它或栈被清空。请在消费端设计系统中验证减少动态效果时的表现。 --- --- url: /docs/components/notification.md description: >- Display toast notifications that appear at the top right of the window with auto-dismiss functionality. --- # Notification A toast notification system for displaying temporary messages to users. Notifications appear at the top right of the window and can auto-dismiss after a timeout. Supports multiple variants (info, success, warning, error), custom content, titles, and action buttons. Perfect for status updates, confirmations, and user feedback. ## Import ```rust use gpui_kit::component::{ notification::{Notification, NotificationType}, WindowExt }; ``` ## Usage ### Setup application root view for display of notifications You need to set up your application's root view to render the notification layer. This is typically done in your main application struct's render method. The [Root::render\_notification\_layer](https://docs.rs/gpui-component/latest/gpui_component/struct.Root.html#method.render_notification_layer) function handles rendering any active modals on top of your app content. ```rust use gpui_kit::component::{TitleBar, Root}; struct Example {} impl Render for Example { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let notification_layer = Root::render_notification_layer(window, cx); div() .size_full() .child( v_flex() .size_full() .child(TitleBar::new()) .child(div().flex_1().child("Hello world!")), ) // Render the notification layer on top of the app content .children(notification_layer) } } ``` ### Basic Notification ```rust // Simple string notification window.push_notification("This is a notification.", cx); // Using Notification builder Notification::new() .message("Your changes have been saved.") ``` ### Notification Types ```rust // Info notification (blue) window.push_notification( (NotificationType::Info, "File saved successfully."), cx, ); // Success notification (green) window.push_notification( (NotificationType::Success, "Payment processed successfully."), cx, ); // Warning notification (yellow/orange) window.push_notification( (NotificationType::Warning, "Network connection is unstable."), cx, ); // Error notification (red) window.push_notification( (NotificationType::Error, "Failed to save file. Please try again."), cx, ); ``` ### Notification with Title ```rust Notification::new() .title("Update Available") .message("A new version of the application is ready to install.") .with_type(NotificationType::Info) ``` ### Auto-hide Control ```rust // Disable auto-hide (manual dismiss only) Notification::new() .message("This notification stays until manually closed.") .autohide(false) // Default auto-hide after 5 seconds Notification::new() .message("This will disappear automatically.") .autohide(true) // default ``` The countdown pauses while the pointer is over the notifications or one of them has keyboard focus, and resumes when the pointer leaves or focus moves on. It keeps running while the window is inactive, so a message that must not be missed should disable auto-hide or use system delivery. ### With Action Button ```rust Notification::new() .title("Connection Lost") .message("Unable to connect to server.") .with_type(NotificationType::Error) .autohide(false) .action(|_, cx| { Button::new("retry") .primary() .label("Retry") .on_click(cx.listener(|this, _, window, cx| { // Perform retry action println!("Retrying connection..."); this.dismiss(window, cx); })) }) ``` ### Clickable Notifications ```rust Notification::new() .message("Click to view details") .on_click(cx.listener(|_, _, _, cx| { println!("Notification clicked"); // Handle notification click cx.notify(); })) ``` ### Custom Content ```rust use gpui_kit::component::text::markdown; let markdown_content = r#" ## Custom Notification - **Feature**: New dashboard available - **Status**: Ready to use - [Learn more](https://example.com) "#; Notification::new() .content(|_, window, cx| { markdown(markdown_content).into_any_element() }) ``` ### Unique Notifications When you need to manage notifications manually, such as for long-running processes or persistent alerts, you can use unique IDs to push and remove notifications as needed. In this case, you can create a special `struct` in local scope, and use `id` methods with this struct to identify the notification. Then you can push the notification when needed, and later remove it using the same ID. Like this: ```rust // Using type-based ID for uniqueness struct UpdateNotification; Notification::new() .id::() .message("System update available") .autohide(false) // Using type + element ID for multiple unique notifications struct TaskNotification; Notification::warning("Task failed to complete") .id1::("task-123") .title("Task Failed") ``` Then remove the notification with `window.remove_notification::`, like this: ```rust // Later, dismiss the notification window.remove_notification::(cx); ``` ### System Notification A notification can also be delivered to the operating system's notification center. Use `NotificationDelivery` to choose where a notification goes: as an in-app toast (`InApp`, the default), in the OS notification center (`System`), or both (`InAppAndSystem`). ```rust use gpui_kit::component::notification::{Notification, NotificationDelivery}; // Per-notification override; `.system()` and `.in_app_and_system()` are // shorthands for `.delivery(NotificationDelivery::...)`. Notification::info("Your download is ready.") .title("Download complete") .system() // Or set a global default for all notifications Theme::global_mut(cx).notification.delivery = NotificationDelivery::InAppAndSystem; ``` The notification's title and message become the system notification's title and body; a notification with neither is not posted. Pushing again with the same `.id::()` replaces the previous system notification, and `window.remove_notification::(cx)` / `window.clear_notifications(cx)` retract it. When the toast auto-hides, the system notification stays in the notification center. Clicking the system notification activates the application and its window, closes the in-app toast (if any), and fires `on_click` with a default `ClickEvent`. With `NotificationDelivery::System` no toast exists, so `on_close` is never called. `gpui_kit::component::init` registers the app-global `on_system_notification_response` handler, so do not register your own after it — gpui keeps only one. Notifications your application posts directly via `cx.show_system_notification` are left untouched. Platform requirements: | Platform | Requirement | Retraction | | --- | --- | --- | | macOS | Must run from a bundled `.app` in a trusted location (e.g. `/Applications`); silently disabled under plain `cargo run`. The first post triggers the system authorization prompt; a denial is remembered and later posts fail silently | Supported | | Windows | Call `cx.set_app_identity(identifier, name)` early in startup | Supported | | Linux | An XDG notification daemon must be present | Unsupported (ages out) | ## Examples ### Form Validation Error ```rust Notification::error("Please correct the following errors before submitting.") .title("Validation Failed") .autohide(false) .action(|_, _, cx| { Button::new("review") .outline() .label("Review Form") .on_click(cx.listener(|this, _, window, cx| { // Navigate to form this.dismiss(window, cx); })) }) ``` ### File Upload Progress ```rust struct UploadNotification; // Start upload notification window.push_notification( Notification::info("Uploading file...") .id::() .title("File Upload") .autohide(false), cx, ); // Update to success when complete window.push_notification( Notification::success("File uploaded successfully!") .id::() .title("Upload Complete"), cx, ); ``` ### System Status Updates ```rust // Warning about maintenance Notification::warning("System maintenance will begin in 30 minutes.") .title("Scheduled Maintenance") .autohide(false) .action(|_, cx| { Button::new("details") .link() .label("View Details") .on_click(cx.listener(|this, _, window, cx| { // Show maintenance details this.dismiss(window, cx); })) }) ``` ### Batch Operation Results ```rust use gpui_kit::component::text::markdown; let results_content = r#" ## Batch Operation Complete **Processed**: 150 items **Success**: 147 items **Failed**: 3 items [View failed items](/) "#; Notification::success("Batch operation completed with some failures.") .title("Operation Results") .content(|window, cx| { markdown(results_content).into_any_element() }) .autohide(false) ``` ### Interactive Confirmation ```rust struct SaveConfirmation; Notification::new() .id::() .title("Unsaved Changes") .message("You have unsaved changes. Save before leaving?") .autohide(false) .action(|_, cx| { Button::new("save") .primary() .label("Save") .on_click(cx.listener(|this, _, window, cx| { // Perform save println!("Saving changes..."); this.dismiss(window, cx); })) }) .on_click(cx.listener(|_, _, _, cx| { println!("Save reminder clicked"); cx.notify(); })) ``` --- --- url: /zh-CN/docs/components/notification.md description: 在窗口右上角显示支持自动消失的 toast 通知。 --- # Notification Notification 是一个 toast 通知系统,用于向用户显示短暂消息。通知会出现在窗口右上角,并可在超时后自动消失。它支持多种类型、标题、自定义内容和操作按钮,适合状态反馈、确认信息和异步操作提示。 ## 导入 ```rust use gpui_kit::component::{ notification::{Notification, NotificationType}, WindowExt }; ``` ## 用法 ### 在根视图中渲染通知层 如果你想显示通知,需要在应用根视图中渲染 notification layer。 [Root::render\_notification\_layer](https://docs.rs/gpui-component/latest/gpui_component/struct.Root.html#method.render_notification_layer) 会将当前激活的通知渲染在应用内容之上。 ```rust use gpui_kit::component::{TitleBar, Root}; struct Example {} impl Render for Example { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let notification_layer = Root::render_notification_layer(window, cx); div() .size_full() .child( v_flex() .size_full() .child(TitleBar::new()) .child(div().flex_1().child("Hello world!")), ) .children(notification_layer) } } ``` ### 基础通知 ```rust window.push_notification("This is a notification.", cx); Notification::new() .message("Your changes have been saved.") ``` ### 通知类型 ```rust window.push_notification( (NotificationType::Info, "File saved successfully."), cx, ); window.push_notification( (NotificationType::Success, "Payment processed successfully."), cx, ); window.push_notification( (NotificationType::Warning, "Network connection is unstable."), cx, ); window.push_notification( (NotificationType::Error, "Failed to save file. Please try again."), cx, ); ``` ### 带标题 ```rust Notification::new() .title("Update Available") .message("A new version of the application is ready to install.") .with_type(NotificationType::Info) ``` ### 自动隐藏 ```rust Notification::new() .message("This notification stays until manually closed.") .autohide(false) Notification::new() .message("This will disappear automatically.") .autohide(true) ``` 指针悬停在通知上或某条通知获得键盘焦点时倒计时暂停,指针移开或焦点离开后继续。窗口未激活时倒计时照常进行,不能错过的消息应关闭自动隐藏或使用系统通知投递。 ### 操作按钮 ```rust Notification::new() .title("Connection Lost") .message("Unable to connect to server.") .with_type(NotificationType::Error) .autohide(false) .action(|_, cx| { Button::new("retry") .primary() .label("Retry") .on_click(cx.listener(|this, _, window, cx| { println!("Retrying connection..."); this.dismiss(window, cx); })) }) ``` ### 可点击通知 ```rust Notification::new() .message("Click to view details") .on_click(cx.listener(|_, _, _, cx| { println!("Notification clicked"); cx.notify(); })) ``` ### 自定义内容 ```rust use gpui_kit::component::text::markdown; let markdown_content = r#" ## Custom Notification - **Feature**: New dashboard available - **Status**: Ready to use - [Learn more](https://example.com) "#; Notification::new() .content(|_, window, cx| { markdown(markdown_content).into_any_element() }) ``` ### 唯一通知 ID 如果你要手动管理通知,例如长任务状态或持久警告,可以为通知分配唯一 ID。 ```rust struct UpdateNotification; Notification::new() .id::() .message("System update available") .autohide(false) struct TaskNotification; Notification::warning("Task failed to complete") .id1::("task-123") .title("Task Failed") ``` 后续可以通过: ```rust window.remove_notification::(cx); ``` 来移除对应通知。 ### 系统通知 通知也可以投递到操作系统的通知中心。使用 `NotificationDelivery` 选择通知的去向:应用内 toast(`InApp`,默认)、系统通知中心(`System`)、或两者都发(`InAppAndSystem`)。 ```rust use gpui_kit::component::notification::{Notification, NotificationDelivery}; // 单条通知覆盖;`.system()` 和 `.in_app_and_system()` 是 // `.delivery(NotificationDelivery::...)` 的简写。 Notification::info("Your download is ready.") .title("Download complete") .system() // 或为所有通知设置全局默认值 Theme::global_mut(cx).notification.delivery = NotificationDelivery::InAppAndSystem; ``` 通知的标题和消息分别成为系统通知的标题和正文;两者都缺失时不会投递。用相同的 `.id::()` 再次推送会替换之前的系统通知,`window.remove_notification::(cx)` / `window.clear_notifications(cx)` 会将其撤回。toast 自动隐藏时,系统通知会保留在通知中心。 点击系统通知会激活应用及其窗口、关闭对应的应用内 toast(如有)、并以默认的 `ClickEvent` 触发 `on_click`。`NotificationDelivery::System` 模式下没有 toast,因此 `on_close` 不会被调用。 `gpui_kit::component::init` 会注册应用级的 `on_system_notification_response` 处理器,之后请勿再自行注册——gpui 只保留一个。应用通过 `cx.show_system_notification` 直接发送的系统通知不受影响。 平台要求: | 平台 | 要求 | 撤回 | | --- | --- | --- | | macOS | 必须从可信位置(如 `/Applications`)的打包 `.app` 运行;`cargo run` 裸跑时静默禁用。首次投递会触发系统授权弹窗,拒绝后系统会记住该选择,后续投递静默失败 | 支持 | | Windows | 启动早期调用 `cx.set_app_identity(identifier, name)` | 支持 | | Linux | 需要 XDG 通知守护进程 | 不支持(自然过期) | ## 示例 ### 表单校验失败 ```rust Notification::error("Please correct the following errors before submitting.") .title("Validation Failed") .autohide(false) ``` ### 文件上传进度 ```rust struct UploadNotification; window.push_notification( Notification::info("Uploading file...") .id::() .title("File Upload") .autohide(false), cx, ); ``` --- --- url: /base/primitives/number-input.md description: 'A numeric input with reusable increment, decrement, and step behavior.' --- # Number Input A numeric input with reusable increment, decrement, and step behavior. Like every `gpui-base` primitive, Number Input supplies behavior and semantic structure without imposing a product visual language. Apply GPUI styles and compose the exported parts to match your design system. ## Example The [single native Cargo entrypoint](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/components.rs) selects this primitive from the [shared showcase implementation](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/mod.rs). The same showcase is compiled once for the WASM preview above. ```bash cargo run -p gpui-base --example components -- number-input ``` ## Import ```rust use gpui_kit::base::{Decrement, Increment, NumberInput, NumberInputText}; ``` ## Anatomy and API The example composes `Decrement`, `Increment`, `NumberInput`, `NumberInputText`. GPUI's standard styling and event traits provide presentation; these base types provide the interaction structure. The authoritative module is [`components/number_input.rs`](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/components/number_input.rs). Native and browser previews compile this same file. ## State and events The backing input state owns numeric text/value; step actions apply the configured limits. Keep controlled state on the parent render type or in a GPUI entity. Update it in callbacks and call `cx.notify()`; do not recreate persistent entities during every render. ## Complete Rust example The complete implementation used by the runnable showcase is embedded directly from Rust source: The command above supplies application initialization, window creation, and shared `BaseShowcase` state. ## Accessibility Expose label, value, bounds, and keyboard-accessible step actions. ## Notes Use stable element IDs where accepted. Verify focus, hover, active, selected, disabled, reduced-motion, and high-contrast appearances in the consuming design system. --- --- url: /zh-CN/base/primitives/number-input.md description: 带可复用递增、递减和步进行为的数字输入。 --- # Number Input 带可复用递增、递减和步进行为的数字输入。 和所有 GPUI Base 原语一样,Number Input 只提供行为和语义结构,不规定产品视觉语言。请使用 GPUI 样式并组合导出的部件,使其符合你的设计系统。 ## 示例 原生示例和页面上方的 WASM 预览共用同一份实现: ```bash cargo run -p gpui-base --example components -- number-input ``` ## 导入 ```rust use gpui_kit::base::{Decrement, Increment, NumberInput, NumberInputText}; ``` ## 结构与 API 示例组合上述公开类型。GPUI 的标准样式和事件 trait 负责表现,Base 类型负责交互结构。权威实现位于 [`components/number-input.rs`](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/components/number-input.rs),原生与浏览器预览编译的是同一文件。 ## 状态与事件 父级或持久 entity 保存数值;按钮和文本输入共同更新同一状态。 受控状态应保存在父渲染类型或 GPUI entity 中;在回调中更新并调用 `cx.notify()`,不要在每次渲染时重建持久 entity。 ## 完整 Rust 示例 ## 可访问性 提供标签、范围与当前值语义,并让增减操作可通过键盘完成。 ## 注意事项 在支持的位置使用稳定元素 ID,并在消费端设计系统中验证焦点、悬停、按下、选中、禁用、减少动态效果和高对比度状态。 --- --- url: /docs/components/number-input.md description: >- Number input component with increment/decrement controls and numeric formatting. --- # NumberInput A specialized input component for numeric values with built-in increment/decrement buttons and support for min/max values, step values, and number formatting with thousands separators. ## Import ```rust use gpui_kit::component::input::{InputState, NumberInput, NumberInputEvent, StepAction}; ``` ## Usage ### Basic Number Input ```rust let number_input = cx.new(|cx| InputState::new(window, cx) .placeholder("Enter number") .default_value("1") ); NumberInput::new(&number_input) ``` ### Input Restriction and Normalization By default, the NumberInput only accepts a valid number: an optional leading `+`/`-` sign, digits and a single decimal point (e.g. `-1.5`), other characters are rejected on typing and pasting. Full-width number characters are normalized into their ASCII equivalents automatically, for CJK IME users: * Full-width digits: `123` → `123` * Full-width signs: `+` → `+`, `-` → `-` * Full-width dot and ideographic full stop: `.`, `。` → `.` A bare leading decimal point is kept as-is (e.g. `.5`, parsed as `0.5`), matching the web behavior, so deleting the integer part of `1.2` keeps `.2` and stays editable. To opt out of the default restriction, set an explicit mask: `state.set_mask_pattern(MaskPattern::None, window, cx)`. To further restrict the input (e.g. positive integers only), use `pattern`: ```rust // Integer input with validation let integer_input = cx.new(|cx| InputState::new(window, cx) .placeholder("Integer value") .pattern(Regex::new(r"^\d+$").unwrap()) // Only positive integers ); NumberInput::new(&integer_input) ``` ### With Min/Max/Step By default, the NumberInput updates the value internally with `step(1.)`: the `↑`/`↓` keys and the `+`/`-` buttons step the value by 1 and emit `InputEvent::Change`. Set `min`/`max` to clamp the range, or set a custom step. To fall back to emitting `NumberInputEvent::Step` only (the subscriber is responsible for updating the value), call `state.set_step(None, window, cx)`. A typed out-of-range value is kept while typing, and clamped on blur. Stepping follows the web behavior: a step that cannot move the value in the pressed direction (e.g. `↓` on a value at or below the `min`) does nothing. ```rust let stepper_input = cx.new(|cx| InputState::new(window, cx) .default_value("50") .step(5.) .min(0.) .max(100.) ); NumberInput::new(&stepper_input) ``` ### Dynamic Step Use `step_by` to calculate the step value from the current value and the step direction, e.g. a step size that varies by range. Because the step can differ by direction at a boundary, the closure receives the `StepAction`; here `1.0` steps by `0.1` going down and `0.5` going up. The closure also receives a `Context` for reading or updating other entities: ```rust let price_input = cx.new(|cx| InputState::new(window, cx) .step_by(|value, action, _cx| match action { StepAction::Increment => if value < 1.0 { 0.1 } else { 0.5 }, StepAction::Decrement => if value <= 1.0 { 0.1 } else { 0.5 }, }) .min(0.) ); NumberInput::new(&price_input) ``` The step strategy can also be updated at runtime via `set_step`: ```rust use gpui_kit::component::input::NumberStep; state.set_step(NumberStep::Fixed(0.01), window, cx); state.set_step(NumberStep::by_value(|v, _, _cx| if v < 1. { 0.01 } else { 0.1 }), window, cx); state.set_step(None, window, cx); // Fall back to NumberInputEvent::Step ``` ### With Number Formatting ```rust use gpui_kit::component::input::MaskPattern; // Currency input with thousands separator let currency_input = cx.new(|cx| InputState::new(window, cx) .placeholder("Amount") .mask_pattern(MaskPattern::Number { separator: Some(','), fraction: Some(2), // 2 decimal places }) ); NumberInput::new(¤cy_input) ``` ### Different Sizes ```rust // Large size NumberInput::new(&input).large() // Medium size (default) NumberInput::new(&input) // Small size NumberInput::new(&input).small() ``` ### With Prefix and Suffix ```rust use gpui_kit::component::{button::{Button, ButtonVariants}, IconName}; // With currency prefix NumberInput::new(&input) .prefix(div().child("$")) // With info button suffix NumberInput::new(&input) .suffix( Button::new("info") .ghost() .icon(IconName::Info) .xsmall() ) ``` ### Disabled State ```rust NumberInput::new(&input).disabled(true) ``` ### Without Default Styling ```rust // For custom container styling div() .w_full() .bg(cx.theme().secondary) .rounded(cx.theme().radius) .child(NumberInput::new(&input).appearance(false)) ``` ### Handle Number Input Events By default, the NumberInput updates the value internally. To fall back to `NumberInputEvent::Step` (the subscriber is responsible for updating the value), call `state.set_step(None, window, cx)`: ```rust let number_input = cx.new(|cx| InputState::new(window, cx)); let mut value: i64 = 0; // Subscribe to input changes cx.subscribe_in(&number_input, window, |view, state, event, window, cx| { match event { InputEvent::Change => { let text = state.read(cx).value(); if let Ok(new_value) = text.parse::() { view.value = new_value; } } _ => {} } }); // Subscribe to increment/decrement actions cx.subscribe_in(&number_input, window, |view, state, event, window, cx| { match event { NumberInputEvent::Step(step_action) => { match step_action { StepAction::Increment => { view.value += 1; state.update(cx, |input, cx| { input.set_value(view.value.to_string(), window, cx); }); } StepAction::Decrement => { view.value -= 1; state.update(cx, |input, cx| { input.set_value(view.value.to_string(), window, cx); }); } } } } }); ``` ### Programmatic Control ```rust // Increment programmatically NumberInput::increment(&number_input, window, cx); // Decrement programmatically NumberInput::decrement(&number_input, window, cx); ``` ## API Reference ### NumberInput | Method | Description | | ------------------------------ | ------------------------------------------ | | `new(state)` | Create number input with InputState entity | | `placeholder(str)` | Set placeholder text | | `size(size)` | Set input size (small, medium, large) | | `prefix(el)` | Add prefix element | | `suffix(el)` | Add suffix element | | `appearance(bool)` | Enable/disable default styling | | `disabled(bool)` | Set disabled state | | `increment(state, window, cx)` | Increment value programmatically | | `decrement(state, window, cx)` | Decrement value programmatically | ### NumberInputEvent | Event | Description | | ------------------ | ---------------------------------- | | `Step(StepAction)` | Increment/decrement pressed. Only emitted when `step` is `None` (opt out via `set_step(None, ...)`). | ### StepAction | Action | Description | | ----------- | ------------------------- | | `Increment` | Value should be increased | | `Decrement` | Value should be decreased | ### InputState (Number-specific methods) | Method | Description | | ----------------------------------- | ------------------------------------------------------- | | `step(impl Into)` | Set step value for built-in increment/decrement (default: 1) | | `step_by(fn(f64, StepAction, &mut Context) -> f64)` | Calculate step value based on the current value and direction | | `min(f64)` | Set minimum value, clamped on stepping and blur | | `max(f64)` | Set maximum value, clamped on stepping and blur | | `set_step(Option, ...)` | Update step strategy after construction | | `set_min(Option, ...)` | Update minimum value after construction | | `set_max(Option, ...)` | Update maximum value after construction | | `pattern(regex)` | Set regex pattern for validation (e.g., digits only) | | `mask_pattern(MaskPattern::Number)` | Set number formatting with separator and decimal places | | `value()` | Get current display value (formatted) | | `unmask_value()` | Get actual numeric value (unformatted) | ### MaskPattern::Number | Field | Type | Description | | ----------- | --------------- | -------------------------------------- | | `separator` | `Option` | Thousands separator (e.g., ',' or ' ') | | `fraction` | `Option` | Number of decimal places | ## Keyboard Navigation | Key | Action | | ----------- | -------------------------- | | `↑` | Increment value | | `↓` | Decrement value | | `Tab` | Navigate to next field | | `Shift+Tab` | Navigate to previous field | | `Enter` | Submit/confirm value | | `Escape` | Clear input (if enabled) | ## Examples ### Integer Counter ```rust struct CounterView { counter_input: Entity, counter_value: i32, } impl CounterView { fn new(window: &mut Window, cx: &mut Context) -> Self { let counter_input = cx.new(|cx| InputState::new(window, cx) .placeholder("Count") .default_value("0") .pattern(Regex::new(r"^-?\d+$").unwrap()) // Allow negative integers ); let _subscription = cx.subscribe_in(&counter_input, window, Self::on_number_event); Self { counter_input, counter_value: 0, } } fn on_number_event( &mut self, state: &Entity, event: &NumberInputEvent, window: &mut Window, cx: &mut Context, ) { match event { NumberInputEvent::Step(StepAction::Increment) => { self.counter_value += 1; state.update(cx, |input, cx| { input.set_value(self.counter_value.to_string(), window, cx); }); } NumberInputEvent::Step(StepAction::Decrement) => { self.counter_value -= 1; state.update(cx, |input, cx| { input.set_value(self.counter_value.to_string(), window, cx); }); } } } } // Usage NumberInput::new(&self.counter_input) ``` ### Currency Input ```rust struct PriceInput { price_input: Entity, price_value: f64, } impl PriceInput { fn new(window: &mut Window, cx: &mut Context) -> Self { let price_input = cx.new(|cx| InputState::new(window, cx) .placeholder("0.00") .mask_pattern(MaskPattern::Number { separator: Some(','), fraction: Some(2), }) ); Self { price_input, price_value: 0.0, } } } // Usage with currency prefix h_flex() .gap_2() .child(div().child("$")) .child(NumberInput::new(&self.price_input)) ``` ### Quantity Selector with Limits ```rust struct QuantitySelector { quantity_input: Entity, } impl QuantitySelector { fn new(window: &mut Window, cx: &mut Context) -> Self { // Step by 1 and clamp to 1..=99, no event handling needed. let quantity_input = cx.new(|cx| InputState::new(window, cx) .default_value("1") .min(1.) .max(99.) ); Self { quantity_input } } } // Usage NumberInput::new(&self.quantity_input).small() ``` ### Floating Point Input ```rust // Step by 0.1, the fraction digits of the value are kept on stepping, // e.g. 0.2 -> 0.3 (not 0.30000000000000004). let float_input = cx.new(|cx| InputState::new(window, cx) .placeholder("0.0") .step(0.1) ); NumberInput::new(&float_input) ``` ## Best Practices 1. **Validation**: Always validate numeric input on both client and server side 2. **Range Limits**: Use `min`/`max` to clamp values for user safety 3. **Step Size**: Choose appropriate `step` values for your use case 4. **Error Handling**: Provide clear feedback for invalid input 5. **Formatting**: Use consistent number formatting across your application 6. **Performance**: Debounce rapid increment/decrement actions if needed 7. **Accessibility**: Always provide proper labels and descriptions --- --- url: /zh-CN/docs/components/number-input.md description: 带增减按钮与数字格式化能力的数值输入组件。 --- # NumberInput NumberInput 是针对数值输入场景设计的组件,内置递增/递减按钮,并支持最小值、最大值、步进值以及千分位格式化等能力。 ## 导入 ```rust use gpui_kit::component::input::{InputState, NumberInput, NumberInputEvent, StepAction}; ``` ## 用法 ### 基础数值输入 ```rust let number_input = cx.new(|cx| InputState::new(window, cx) .placeholder("Enter number") .default_value("1") ); NumberInput::new(&number_input) ``` ### 输入限制与字符归一化 默认情况下,NumberInput 只接受合法数字:可选的开头 `+`/`-` 符号、数字和一个小数点(例如 `-1.5`),其他字符在输入和粘贴时会被拒绝。 针对中文等输入法用户,全角数字字符会自动转换为对应的半角字符: * 全角数字:`123` → `123` * 全角符号:`+` → `+`、`-` → `-` * 全角小数点与中文句号:`.`、`。` → `.` 以小数点开头的输入保持原样(例如 `.5`,解析为 `0.5`),与 Web 行为一致——删除 `1.2` 的整数部分会保留 `.2`,便于继续编辑。 如需关闭默认限制,可显式设置 mask:`state.set_mask_pattern(MaskPattern::None, window, cx)`。 如需进一步限制输入(例如仅允许正整数),可使用 `pattern`: ```rust let integer_input = cx.new(|cx| InputState::new(window, cx) .placeholder("Integer value") .pattern(Regex::new(r"^\d+$").unwrap()) ); NumberInput::new(&integer_input) ``` ### 最小值 / 最大值 / 步进值 默认情况下,NumberInput 以 `step(1.)` 在内部更新数值:`↑`/`↓` 键和 `+`/`-` 按钮按 1 步进并发出 `InputEvent::Change` 事件。可通过 `min`/`max` 限制范围,或设置自定义步进值。 如需仅发出 `NumberInputEvent::Step` 事件(由订阅方负责更新数值),可调用 `state.set_step(None, window, cx)`。 手动输入的越界值在输入过程中会被保留,失焦时自动收敛到范围内。步进遵循 Web 行为:无法朝按键方向移动数值的步进不会生效(例如数值已等于或低于 `min` 时按 `↓` 不会变化)。 ```rust let stepper_input = cx.new(|cx| InputState::new(window, cx) .default_value("50") .step(5.) .min(0.) .max(100.) ); NumberInput::new(&stepper_input) ``` ### 动态步长 使用 `step_by` 可以根据当前值和步进方向实时计算步长,例如步长随取值区间变化。步长可在边界处随方向不同,因此闭包会收到 `StepAction`;下例中以 `1.0` 为界,向下步进 `0.1`,向上步进 `0.5`。闭包还会收到一个 `Context`,可用于读取或更新其他 entity: ```rust let price_input = cx.new(|cx| InputState::new(window, cx) .step_by(|value, action, _cx| match action { StepAction::Increment => if value < 1.0 { 0.1 } else { 0.5 }, StepAction::Decrement => if value <= 1.0 { 0.1 } else { 0.5 }, }) .min(0.) ); NumberInput::new(&price_input) ``` 步进策略也可以在运行时通过 `set_step` 更新: ```rust use gpui_kit::component::input::NumberStep; state.set_step(NumberStep::Fixed(0.01), window, cx); state.set_step(NumberStep::by_value(|v, _, _cx| if v < 1. { 0.01 } else { 0.1 }), window, cx); state.set_step(None, window, cx); // 回退到 NumberInputEvent::Step 事件模式 ``` ### 数字格式化 ```rust use gpui_kit::component::input::MaskPattern; let currency_input = cx.new(|cx| InputState::new(window, cx) .placeholder("Amount") .mask_pattern(MaskPattern::Number { separator: Some(','), fraction: Some(2), }) ); NumberInput::new(¤cy_input) ``` ### 不同尺寸 ```rust NumberInput::new(&input).large() NumberInput::new(&input) NumberInput::new(&input).small() ``` ### 前缀与后缀 ```rust use gpui_kit::component::{button::{Button, ButtonVariants}, IconName}; NumberInput::new(&input) .prefix(div().child("$")) NumberInput::new(&input) .suffix( Button::new("info") .ghost() .icon(IconName::Info) .xsmall() ) ``` ### 禁用状态 ```rust NumberInput::new(&input).disabled(true) ``` ### 关闭默认外观 ```rust div() .w_full() .bg(cx.theme().secondary) .rounded(cx.theme().radius) .child(NumberInput::new(&input).appearance(false)) ``` ### 处理 NumberInput 事件 默认情况下 NumberInput 在内部更新数值。如需回退到 `NumberInputEvent::Step` 模式(由订阅方负责更新数值),可调用 `state.set_step(None, window, cx)`: ```rust let number_input = cx.new(|cx| InputState::new(window, cx)); let mut value: i64 = 0; cx.subscribe_in(&number_input, window, |view, state, event, window, cx| { match event { InputEvent::Change => { let text = state.read(cx).value(); if let Ok(new_value) = text.parse::() { view.value = new_value; } } _ => {} } }); cx.subscribe_in(&number_input, window, |view, state, event, window, cx| { match event { NumberInputEvent::Step(step_action) => { match step_action { StepAction::Increment => { view.value += 1; state.update(cx, |input, cx| { input.set_value(view.value.to_string(), window, cx); }); } StepAction::Decrement => { view.value -= 1; state.update(cx, |input, cx| { input.set_value(view.value.to_string(), window, cx); }); } } } } }); ``` ### 程序化控制 ```rust NumberInput::increment(&number_input, window, cx); NumberInput::decrement(&number_input, window, cx); ``` ## API 参考 ### NumberInput | 方法 | 说明 | | ------------------------------ | ------------------------------------------ | | `new(state)` | 使用 `InputState` 创建数值输入组件 | | `placeholder(str)` | 设置占位文案 | | `size(size)` | 设置尺寸 | | `prefix(el)` | 添加前缀元素 | | `suffix(el)` | 添加后缀元素 | | `appearance(bool)` | 开启或关闭默认样式 | | `disabled(bool)` | 设置禁用状态 | | `increment(state, window, cx)` | 以代码方式递增 | | `decrement(state, window, cx)` | 以代码方式递减 | ### NumberInputEvent | 事件 | 说明 | | ------------------ | ---------------------------------- | | `Step(StepAction)` | 点击增减按钮时触发,仅当 `step` 为 `None` 时发出(通过 `set_step(None, ...)` 选择该模式) | ### StepAction | 动作 | 说明 | | ----------- | ------------------------- | | `Increment` | 数值增加 | | `Decrement` | 数值减少 | ### InputState(数值相关方法) | 方法 | 说明 | | ----------------------------------- | ------------------------------------------------------- | | `step(impl Into)` | 设置内置递增/递减的步进值(默认为 1) | | `step_by(fn(f64, StepAction, &mut Context) -> f64)` | 步进时根据当前值和方向实时计算步长 | | `min(f64)` | 设置最小值,步进与失焦时收敛到该值 | | `max(f64)` | 设置最大值,步进与失焦时收敛到该值 | | `set_step(Option, ...)` | 构造后更新步进策略 | | `set_min(Option, ...)` | 构造后更新最小值 | | `set_max(Option, ...)` | 构造后更新最大值 | | `pattern(regex)` | 设置校验正则,例如只允许数字 | | `mask_pattern(MaskPattern::Number)` | 设置数字格式化规则 | | `value()` | 获取当前展示值 | | `unmask_value()` | 获取未格式化的真实数值 | ### MaskPattern::Number | 字段 | 类型 | 说明 | | ----------- | --------------- | -------------------------------------- | | `separator` | `Option` | 千分位分隔符 | | `fraction` | `Option` | 小数位数 | ## 键盘导航 | 按键 | 行为 | | ----------- | -------------------------- | | `↑` | 增加数值 | | `↓` | 减少数值 | | `Tab` | 切换到下一个字段 | | `Shift+Tab` | 切换到上一个字段 | | `Enter` | 提交或确认当前值 | | `Escape` | 清空输入(若启用) | ## 示例 ### 整数计数器 ```rust struct CounterView { counter_input: Entity, counter_value: i32, } impl CounterView { fn new(window: &mut Window, cx: &mut Context) -> Self { let counter_input = cx.new(|cx| InputState::new(window, cx) .placeholder("Count") .default_value("0") .pattern(Regex::new(r"^-?\d+$").unwrap()) ); let _subscription = cx.subscribe_in(&counter_input, window, Self::on_number_event); Self { counter_input, counter_value: 0, } } fn on_number_event( &mut self, state: &Entity, event: &NumberInputEvent, window: &mut Window, cx: &mut Context, ) { match event { NumberInputEvent::Step(StepAction::Increment) => { self.counter_value += 1; state.update(cx, |input, cx| { input.set_value(self.counter_value.to_string(), window, cx); }); } NumberInputEvent::Step(StepAction::Decrement) => { self.counter_value -= 1; state.update(cx, |input, cx| { input.set_value(self.counter_value.to_string(), window, cx); }); } } } } NumberInput::new(&self.counter_input) ``` ### 货币输入 ```rust struct PriceInput { price_input: Entity, price_value: f64, } impl PriceInput { fn new(window: &mut Window, cx: &mut Context) -> Self { let price_input = cx.new(|cx| InputState::new(window, cx) .placeholder("0.00") .mask_pattern(MaskPattern::Number { separator: Some(','), fraction: Some(2), }) ); Self { price_input, price_value: 0.0, } } } h_flex() .gap_2() .child(div().child("$")) .child(NumberInput::new(&self.price_input)) ``` ### 带上下限的数量选择器 ```rust struct QuantitySelector { quantity_input: Entity, } impl QuantitySelector { fn new(window: &mut Window, cx: &mut Context) -> Self { // 按 1 步进并限制在 1..=99 范围内,无需处理事件。 let quantity_input = cx.new(|cx| InputState::new(window, cx) .default_value("1") .min(1.) .max(99.) ); Self { quantity_input } } } NumberInput::new(&self.quantity_input).small() ``` ### 浮点数输入 ```rust // 按 0.1 步进,步进时保留当前值的小数位数, // 例如 0.2 -> 0.3(而不是 0.30000000000000004)。 let float_input = cx.new(|cx| InputState::new(window, cx) .placeholder("0.0") .step(0.1) ); NumberInput::new(&float_input) ``` ## 最佳实践 1. 客户端和服务端都应校验数值输入。 2. 使用 `min`/`max` 对用户可操作范围设置明确上下限。 3. 根据业务场景选择合适的 `step` 步进值。 4. 对非法输入提供清晰反馈。 5. 在整个应用中保持统一的数字格式。 6. 高频点击增减时,必要时做节流或去抖。 7. 始终为输入提供清晰的标签与描述。 --- --- url: /base/primitives/otp-input.md description: A multi-cell one-time-code input driven by a shared text state. --- # OTP Input A multi-cell one-time-code input driven by a shared text state. Like every `gpui-base` primitive, OTP Input supplies behavior and semantic structure without imposing a product visual language. Apply GPUI styles and compose the exported parts to match your design system. ## Example The [single native Cargo entrypoint](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/components.rs) selects this primitive from the [shared showcase implementation](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/mod.rs). The same showcase is compiled once for the WASM preview above. ```bash cargo run -p gpui-base --example components -- otp-input ``` ## Import ```rust use gpui_kit::base::{OtpInput, OtpState}; ``` ## Anatomy and API The example composes `OtpInput`, `OtpState`. GPUI's standard styling and event traits provide presentation; these base types provide the interaction structure. The authoritative module is [`components/otp_input.rs`](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/components/otp_input.rs). Native and browser previews compile this same file. ## State and events `OtpState` owns the complete code and active cell; visual cells share that state. Keep controlled state on the parent render type or in a GPUI entity. Update it in callbacks and call `cx.notify()`; do not recreate persistent entities during every render. ## Complete Rust example The complete implementation used by the runnable showcase is embedded directly from Rust source: The command above supplies application initialization, window creation, and shared `BaseShowcase` state. ## Accessibility Label the whole code, announce length/errors, and support paste. ## Notes Use stable element IDs where accepted. Verify focus, hover, active, selected, disabled, reduced-motion, and high-contrast appearances in the consuming design system. --- --- url: /zh-CN/base/primitives/otp-input.md description: 由共享文本状态驱动的多单元格一次性验证码输入。 --- # OTP Input 由共享文本状态驱动的多单元格一次性验证码输入。 和所有 GPUI Base 原语一样,OTP Input 只提供行为和语义结构,不规定产品视觉语言。请使用 GPUI 样式并组合导出的部件,使其符合你的设计系统。 ## 示例 原生示例和页面上方的 WASM 预览共用同一份实现: ```bash cargo run -p gpui-base --example components -- otp-input ``` ## 导入 ```rust use gpui_kit::base::{OtpInput, OtpState}; ``` ## 结构与 API 示例组合上述公开类型。GPUI 的标准样式和事件 trait 负责表现,Base 类型负责交互结构。权威实现位于 [`components/otp-input.rs`](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/components/otp-input.rs),原生与浏览器预览编译的是同一文件。 ## 状态与事件 OtpState 保存完整验证码,各视觉单元格只是同一输入状态的投影。 受控状态应保存在父渲染类型或 GPUI entity 中;在回调中更新并调用 `cx.notify()`,不要在每次渲染时重建持久 entity。 ## 完整 Rust 示例 ## 可访问性 提供整体标签,不要让辅助技术把每个视觉格误读为互不相关的输入。 ## 注意事项 在支持的位置使用稳定元素 ID,并在消费端设计系统中验证焦点、悬停、按下、选中、禁用、减少动态效果和高对比度状态。 --- --- url: /docs/components/otp-input.md description: >- One-time password input component with multiple fields, auto-focus, and paste handling. --- # OtpInput A specialized input component for one-time passwords (OTP) that displays multiple input fields in a grid layout. Perfect for SMS verification codes, authenticator app codes, and other numeric verification scenarios. ## Import ```rust use gpui_kit::component::input::{OtpInput, OtpState}; ``` ## Usage ### Basic OTP Input ```rust let otp_state = cx.new(|cx| OtpState::new(6, window, cx)); OtpInput::new(&otp_state) ``` ### With Default Value ```rust let otp_state = cx.new(|cx| OtpState::new(6, window, cx) .default_value("123456") ); OtpInput::new(&otp_state) ``` ### Masked OTP Input ```rust let otp_state = cx.new(|cx| OtpState::new(6, window, cx) .masked(true) .default_value("123456") ); OtpInput::new(&otp_state) ``` ### Different Sizes ```rust // Small size OtpInput::new(&otp_state).small() // Medium size (default) OtpInput::new(&otp_state) // Large size OtpInput::new(&otp_state).large() // Custom size OtpInput::new(&otp_state).with_size(px(55.)) ``` ### Grouped Layout ```rust // Single group (all fields together) OtpInput::new(&otp_state).groups(1) // Two groups (default) - splits fields in half OtpInput::new(&otp_state).groups(2) // Three groups - splits fields into thirds OtpInput::new(&otp_state).groups(3) ``` ### Disabled State ```rust OtpInput::new(&otp_state).disabled(true) ``` ### Different Length Codes ```rust // 4-digit PIN let pin_state = cx.new(|cx| OtpState::new(4, window, cx)); OtpInput::new(&pin_state).groups(1) // 6-digit SMS code (most common) let sms_state = cx.new(|cx| OtpState::new(6, window, cx)); OtpInput::new(&sms_state) // 8-digit authenticator code let auth_state = cx.new(|cx| OtpState::new(8, window, cx)); OtpInput::new(&auth_state).groups(2) ``` ### Handle OTP Events ```rust let otp_state = cx.new(|cx| OtpState::new(6, window, cx)); cx.subscribe(&otp_state, |this, state, event: &InputEvent, cx| { match event { InputEvent::Change => { let code = state.read(cx).value(); if code.len() == 6 { println!("Complete OTP: {}", code); // Automatically submit when complete this.verify_otp(&code, cx); } } InputEvent::Focus => println!("OTP input focused"), InputEvent::Blur => println!("OTP input lost focus"), _ => {} } }); ``` ### Programmatic Control ```rust // Set value programmatically otp_state.update(cx, |state, cx| { state.set_value("123456", window, cx); }); // Toggle masking otp_state.update(cx, |state, cx| { state.set_masked(true, window, cx); }); // Focus the input otp_state.update(cx, |state, cx| { state.focus(window, cx); }); // Get current value let current_value = otp_state.read(cx).value(); ``` ## API Reference ### OtpState | Method | Description | | ------------------------------ | -------------------------------------------- | | `new(length, window, cx)` | Create a new OTP state with specified length | | `default_value(str)` | Set initial value | | `masked(bool)` | Enable masked display (shows asterisks) | | `set_value(str, window, cx)` | Set OTP value programmatically | | `value()` | Get current OTP value | | `set_masked(bool, window, cx)` | Toggle masked display | | `focus(window, cx)` | Focus the OTP input | | `focus_handle(cx)` | Get focus handle | ### OtpInput | Method | Description | | ---------------- | ---------------------------------------- | | `new(state)` | Create OTP input with state entity | | `groups(n)` | Set number of visual groups (default: 2) | | `disabled(bool)` | Set disabled state | | `small()` | Small size (6x6 px fields) | | `large()` | Large size (11x11 px fields) | | `with_size(px)` | Custom field size | ### InputEvent | Event | Description | | -------- | ------------------------------------------------- | | `Change` | Emitted when OTP is complete (all digits entered) | | `Focus` | Input received focus | | `Blur` | Input lost focus | ## Examples ### SMS Verification ```rust struct SmsVerification { otp_state: Entity, phone_number: String, is_verifying: bool, } impl SmsVerification { fn new(window: &mut Window, cx: &mut Context) -> Self { let otp_state = cx.new(|cx| OtpState::new(6, window, cx)); cx.subscribe(&otp_state, |this, state, event: &InputEvent, cx| { if let InputEvent::Change = event { let code = state.read(cx).value(); this.verify_sms_code(&code, cx); } }); Self { otp_state, phone_number: "+1234567890".to_string(), is_verifying: false, } } fn verify_sms_code(&mut self, code: &str, cx: &mut Context) { self.is_verifying = true; // API call to verify SMS code println!("Verifying SMS code: {}", code); cx.notify(); } } impl Render for SmsVerification { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .gap_4() .child(format!("Enter the 6-digit code sent to {}", self.phone_number)) .child(OtpInput::new(&self.otp_state)) .when(self.is_verifying, |this| { this.child("Verifying...") }) } } ``` ### Two-Factor Authentication ```rust struct TwoFactorAuth { otp_state: Entity, is_masked: bool, } impl TwoFactorAuth { fn new(window: &mut Window, cx: &mut Context) -> Self { let otp_state = cx.new(|cx| OtpState::new(6, window, cx) .masked(true) ); Self { otp_state, is_masked: true, } } fn toggle_visibility(&mut self, window: &mut Window, cx: &mut Context) { self.is_masked = !self.is_masked; self.otp_state.update(cx, |state, cx| { state.set_masked(self.is_masked, window, cx); }); } } impl Render for TwoFactorAuth { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .gap_4() .child("Enter your authenticator code") .child(OtpInput::new(&self.otp_state)) .child( Button::new("toggle-visibility") .label(if self.is_masked { "Show" } else { "Hide" }) .on_click(cx.listener(Self::toggle_visibility)) ) } } ``` ### PIN Entry ```rust struct PinEntry { pin_state: Entity, attempts: usize, max_attempts: usize, } impl PinEntry { fn new(window: &mut Window, cx: &mut Context) -> Self { let pin_state = cx.new(|cx| OtpState::new(4, window, cx) .masked(true) ); cx.subscribe(&pin_state, |this, state, event: &InputEvent, cx| { if let InputEvent::Change = event { let pin = state.read(cx).value(); this.verify_pin(&pin, cx); } }); Self { pin_state, attempts: 0, max_attempts: 3, } } fn verify_pin(&mut self, pin: &str, cx: &mut Context) { self.attempts += 1; // Simulate PIN verification if pin == "1234" { println!("PIN verified successfully!"); } else { println!("Incorrect PIN. Attempts: {}/{}", self.attempts, self.max_attempts); // Clear PIN on incorrect attempt self.pin_state.update(cx, |state, cx| { state.set_value("", window, cx); }); } cx.notify(); } } impl Render for PinEntry { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let is_locked = self.attempts >= self.max_attempts; v_flex() .gap_4() .child("Enter your 4-digit PIN") .child( OtpInput::new(&self.pin_state) .groups(1) .disabled(is_locked) ) .when(is_locked, |this| { this.child("Too many attempts. Please try again later.") }) .when(self.attempts > 0 && !is_locked, |this| { this.child(format!( "Incorrect PIN. {} attempts remaining.", self.max_attempts - self.attempts )) }) } } ``` ## Behavior ### Input Handling * **Numeric Only**: Accepts only digits (0-9) * **Auto-Focus**: Automatically moves to next field when digit is entered * **Backspace**: Removes current digit and moves to previous field * **Length Limit**: Prevents input beyond specified length * **Auto-Complete**: Emits `Change` event when all fields are filled ### Visual Feedback * **Focus Indicator**: Blue border and blinking cursor on active field * **Masking**: Shows asterisk icons instead of numbers when enabled * **Grouping**: Visual separation of fields into groups for better readability * **Disabled State**: Grayed out appearance when disabled ### Keyboard Navigation * **Arrow Keys**: Navigate between fields * **Tab**: Move to next focusable element * **Shift+Tab**: Move to previous focusable element * **Backspace**: Delete current digit and move backward * **Delete**: Clear current field ## Common Patterns ### Auto-Submit on Complete ```rust cx.subscribe(&otp_state, |this, state, event: &InputEvent, cx| { if let InputEvent::Change = event { let code = state.read(cx).value(); if code.len() == 6 { // Auto-submit when complete this.submit_verification_code(&code, cx); } } }); ``` ### Clear on Focus ```rust cx.subscribe(&otp_state, |this, state, event: &InputEvent, cx| { if let InputEvent::Focus = event { // Clear previous value when user starts entering new code state.update(cx, |state, cx| { state.set_value("", window, cx); }); } }); ``` ### Resend Code Timer ```rust struct OtpWithResend { otp_state: Entity, resend_timer: Option, can_resend: bool, } // Implementation would include timer logic for resend functionality ``` --- --- url: /zh-CN/docs/components/otp-input.md description: 带多输入框、自动聚焦和粘贴处理的一次性验证码输入组件。 --- # OtpInput OtpInput 是为一次性验证码(OTP)设计的输入组件,会以网格方式显示多个输入框,适合短信验证码、验证器 App 动态码以及 PIN 码输入场景。 ## 导入 ```rust use gpui_kit::component::input::{OtpInput, OtpState}; ``` ## 用法 ### 基础 OTP 输入 ```rust let otp_state = cx.new(|cx| OtpState::new(6, window, cx)); OtpInput::new(&otp_state) ``` ### 默认值 ```rust let otp_state = cx.new(|cx| OtpState::new(6, window, cx) .default_value("123456") ); OtpInput::new(&otp_state) ``` ### 掩码输入 ```rust let otp_state = cx.new(|cx| OtpState::new(6, window, cx) .masked(true) .default_value("123456") ); OtpInput::new(&otp_state) ``` ### 不同尺寸 ```rust OtpInput::new(&otp_state).small() OtpInput::new(&otp_state) OtpInput::new(&otp_state).large() OtpInput::new(&otp_state).with_size(px(55.)) ``` ### 分组布局 ```rust OtpInput::new(&otp_state).groups(1) OtpInput::new(&otp_state).groups(2) OtpInput::new(&otp_state).groups(3) ``` ### 禁用状态 ```rust OtpInput::new(&otp_state).disabled(true) ``` ### 不同长度的验证码 ```rust let pin_state = cx.new(|cx| OtpState::new(4, window, cx)); OtpInput::new(&pin_state).groups(1) let sms_state = cx.new(|cx| OtpState::new(6, window, cx)); OtpInput::new(&sms_state) let auth_state = cx.new(|cx| OtpState::new(8, window, cx)); OtpInput::new(&auth_state).groups(2) ``` ### 处理 OTP 事件 ```rust let otp_state = cx.new(|cx| OtpState::new(6, window, cx)); cx.subscribe(&otp_state, |this, state, event: &InputEvent, cx| { match event { InputEvent::Change => { let code = state.read(cx).value(); if code.len() == 6 { println!("Complete OTP: {}", code); this.verify_otp(&code, cx); } } InputEvent::Focus => println!("OTP input focused"), InputEvent::Blur => println!("OTP input lost focus"), _ => {} } }); ``` ### 程序化控制 ```rust otp_state.update(cx, |state, cx| { state.set_value("123456", window, cx); }); otp_state.update(cx, |state, cx| { state.set_masked(true, window, cx); }); otp_state.update(cx, |state, cx| { state.focus(window, cx); }); let current_value = otp_state.read(cx).value(); ``` ## API 参考 ### OtpState | 方法 | 说明 | | ------------------------------ | -------------------------------------------- | | `new(length, window, cx)` | 创建指定长度的 OTP 状态 | | `default_value(str)` | 设置初始值 | | `masked(bool)` | 开启掩码显示 | | `set_value(str, window, cx)` | 以代码方式设置值 | | `value()` | 获取当前值 | | `set_masked(bool, window, cx)` | 切换掩码状态 | | `focus(window, cx)` | 聚焦输入框 | | `focus_handle(cx)` | 获取焦点句柄 | ### OtpInput | 方法 | 说明 | | ---------------- | ---------------------------------------- | | `new(state)` | 使用状态实体创建 OTP 输入组件 | | `groups(n)` | 设置可视分组数量,默认值为 2 | | `disabled(bool)` | 设置禁用状态 | | `small()` | 小尺寸 | | `large()` | 大尺寸 | | `with_size(px)` | 自定义单格尺寸 | ### InputEvent | 事件 | 说明 | | -------- | ------------------------------------------------- | | `Change` | 所有数字输入完毕后触发 | | `Focus` | 输入框获得焦点 | | `Blur` | 输入框失去焦点 | ## 示例 ### 短信验证码 ```rust struct SmsVerification { otp_state: Entity, phone_number: String, is_verifying: bool, } impl SmsVerification { fn new(window: &mut Window, cx: &mut Context) -> Self { let otp_state = cx.new(|cx| OtpState::new(6, window, cx)); cx.subscribe(&otp_state, |this, state, event: &InputEvent, cx| { if let InputEvent::Change = event { let code = state.read(cx).value(); this.verify_sms_code(&code, cx); } }); Self { otp_state, phone_number: "+1234567890".to_string(), is_verifying: false, } } fn verify_sms_code(&mut self, code: &str, cx: &mut Context) { self.is_verifying = true; println!("Verifying SMS code: {}", code); cx.notify(); } } impl Render for SmsVerification { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .gap_4() .child(format!("Enter the 6-digit code sent to {}", self.phone_number)) .child(OtpInput::new(&self.otp_state)) .when(self.is_verifying, |this| { this.child("Verifying...") }) } } ``` ### 双因素认证 ```rust struct TwoFactorAuth { otp_state: Entity, is_masked: bool, } impl TwoFactorAuth { fn new(window: &mut Window, cx: &mut Context) -> Self { let otp_state = cx.new(|cx| OtpState::new(6, window, cx) .masked(true) ); Self { otp_state, is_masked: true, } } fn toggle_visibility(&mut self, window: &mut Window, cx: &mut Context) { self.is_masked = !self.is_masked; self.otp_state.update(cx, |state, cx| { state.set_masked(self.is_masked, window, cx); }); } } impl Render for TwoFactorAuth { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .gap_4() .child("Enter your authenticator code") .child(OtpInput::new(&self.otp_state)) .child( Button::new("toggle-visibility") .label(if self.is_masked { "Show" } else { "Hide" }) .on_click(cx.listener(Self::toggle_visibility)) ) } } ``` ### PIN 码输入 ```rust struct PinEntry { pin_state: Entity, attempts: usize, max_attempts: usize, } impl PinEntry { fn new(window: &mut Window, cx: &mut Context) -> Self { let pin_state = cx.new(|cx| OtpState::new(4, window, cx) .masked(true) ); cx.subscribe(&pin_state, |this, state, event: &InputEvent, cx| { if let InputEvent::Change = event { let pin = state.read(cx).value(); this.verify_pin(&pin, cx); } }); Self { pin_state, attempts: 0, max_attempts: 3, } } fn verify_pin(&mut self, pin: &str, cx: &mut Context) { self.attempts += 1; if pin == "1234" { println!("PIN verified successfully!"); } else { println!("Incorrect PIN. Attempts: {}/{}", self.attempts, self.max_attempts); self.pin_state.update(cx, |state, cx| { state.set_value("", window, cx); }); } cx.notify(); } } impl Render for PinEntry { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let is_locked = self.attempts >= self.max_attempts; v_flex() .gap_4() .child("Enter your 4-digit PIN") .child( OtpInput::new(&self.pin_state) .groups(1) .disabled(is_locked) ) .when(is_locked, |this| { this.child("Too many attempts. Please try again later.") }) .when(self.attempts > 0 && !is_locked, |this| { this.child(format!( "Incorrect PIN. {} attempts remaining.", self.max_attempts - self.attempts )) }) } } ``` ## 行为说明 ### 输入处理 * **仅数字**:只接受 `0-9`。 * **自动聚焦**:输入数字后自动跳到下一个输入框。 * **退格**:删除当前数字并回到前一个输入框。 * **长度限制**:不会超过设定长度。 * **自动完成**:所有输入框填满后触发 `Change` 事件。 ### 视觉反馈 * **焦点态**:当前输入框显示高亮边框与闪烁光标。 * **掩码**:启用后显示星号而不是数字。 * **分组**:可将输入框按组分隔,提升可读性。 * **禁用态**:禁用后显示灰化样式。 ### 键盘导航 * **方向键**:在输入框之间移动。 * **Tab**:切换到下一个可聚焦元素。 * **Shift+Tab**:切换到上一个可聚焦元素。 * **Backspace**:删除当前数字并向前移动。 * **Delete**:清空当前输入框。 ## 常见模式 ### 输入完成后自动提交 ```rust cx.subscribe(&otp_state, |this, state, event: &InputEvent, cx| { if let InputEvent::Change = event { let code = state.read(cx).value(); if code.len() == 6 { this.submit_verification_code(&code, cx); } } }); ``` ### 聚焦时清空旧值 ```rust cx.subscribe(&otp_state, |this, state, event: &InputEvent, cx| { if let InputEvent::Focus = event { state.update(cx, |state, cx| { state.set_value("", window, cx); }); } }); ``` ### 重发验证码计时器 ```rust struct OtpWithResend { otp_state: Entity, resend_timer: Option, can_resend: bool, } // Implementation would include timer logic for resend functionality ``` --- --- url: /shell/overlays.md description: >- Dialogs, the sheet and toasts, their stacking and dismissal order, and why they may only be opened from an event. --- # Overlays Dialogs, the sheet and toasts are **host** capabilities, reached through the global `window`. They are not something a script draws. A dialog is not a floating `div`. It is a place in the window's stacking order, a focus trap, an Escape target, and a promise about what pressing the backdrop means — all of which the window's root has to decide, because only something that sees every overlay at once can order them. A script drawing its own dialog would own none of that, and two scripts drawing two dialogs would own even less. So the script says **what** to put in front of the user, and the root says where it goes and how it leaves. What crosses the boundary is small: a function returning an element, a side to anchor to, a sentence to show. They are on `window` rather than on `cx` because a dialog belongs to the window, not to the View that opened it: `cx.notify()` re-renders one View, `window.open_dialog()` changes what the user is looking at. `gpui-component` draws the same line, so the two halves of an application read as one vocabulary — and `window` is somewhere to grow. Overlays are what it carries today; `Window` in Rust also answers focus, size and appearance, and those land in a namespace that already exists. ## The surface `window` is a **global**. There is nothing to import — and unlike `cx`, which every host call hands you as an argument, nothing hands you `window` either. It is simply in scope. A callback parameter named `window` shadows it, which is ordinary scoping and not an error — and if a future callback ever hands one in, it would be this same object, because `window` is ambient: it reads the call that is running. That is also why it is not a parameter today. In Rust it has to be one, since Rust has no ambient state to read; here the read is available, which is the same reason `fs` and `store` are not parameters either. ::: warning Do not copy Rust's `|event, window, cx|` A script handler takes `(event, cx)`. A three-parameter version binds `window` to the context and leaves `cx` undefined, and the failure reads as `close_dialog is not a function`. With `// @ts-check` the generated declarations catch it at the line where you wrote it. ::: ```js const depth = window.open_dialog(() => confirmClear(count), { escape_dismissable: false, backdrop_dismissable: false, }); window.close_dialog(); // -> did anything close? window.close_all_dialogs(); // -> how many closed window.has_active_dialog(); window.open_sheet(() => filters()); // right, the default side window.open_sheet_at("left", () => nav()); window.close_sheet(); // -> did anything close? window.has_active_sheet(); window.push_toast({ title: "Saved", description: "3 files", level: "success", timeout: 4000, id: "save" }); window.remove_toast("save"); window.clear_toasts(); ``` ## Dialogs `window.open_dialog(content, options?)` takes a **function returning an element**, not an element: ```text expected a function returning an element; open_dialog and open_sheet take a function, not an element and not a View class ``` The reason is lifetime, not taste. An element belongs to the arena of the render pass that built it, and a dialog outlives the call that opened it — so an element built at open time would belong to the wrong pass. The function runs when the dialog draws, and again whenever it redraws, which is the same contract `render` has. **Whatever it closes over is the dialog's state.** There is no `props`: a dialog receives what it shows the way every other value in the script arrives, by being in scope. ```js // confirm.js import { v_flex, h_flex } from "gpui-base"; export default (count, onConfirm) => () => v_flex() .w(360) .p(24) .gap(12) .child(`Delete ${count} completed items?`) .child("This cannot be undone.") .child( h_flex() .justify_end() .gap(8) .child(cancelButton(() => window.close_dialog())) .child(deleteButton((_event, cx) => { onConfirm(cx); window.close_dialog(); })), ); ``` ```js // main.js window.open_dialog(confirmClear(this.completed, (cx) => this.deleteCompleted(cx))); ``` Note what the root supplies and what it does not. It supplies the backdrop, the position, the layering, the focus trap and the surface it sits on; the width, the padding, the border, the type and the buttons are the script's, like everything else in this runtime. | Option | Default | Effect | | --- | --- | --- | | `escape_dismissable` | `true` | Whether Escape closes it | | `backdrop_dismissable` | `true` | Whether pressing the backdrop closes it | An unknown option is refused rather than ignored, which is the point: ```text unknown option `escapeDismissable` for window.open_dialog(content, options); expected escape_dismissable or backdrop_dismissable ``` A silently ignored `escapeDismissable` would look like it worked, and the dialog would be dismissable anyway. `open_dialog` returns the **new depth of the stack**, not a handle. The root addresses dialogs by position and never by identity, so a handle would have to promise "close *this* dialog", which is not an operation that exists. The depth is what a script can use — to assert one opened, or to unwind to a known level. `close_dialog` returns whether it found one to close; `close_all_dialogs` returns how many it closed. ::: warning Do not carry `cx` into the dialog The `cx` in the handler that opened the dialog belongs to that handler. By the time the dialog's own button is pressed, it is stale, and using it reports a stale-context error. Close over **data**, and take `cx` from the dialog's own callback arguments — which is why the example above passes `onConfirm` a `cx` rather than capturing one. The overlay calls themselves have no such hazard: they are ambient, like `fs` and `store`, so there is no handle to hold past its call. ::: ## The sheet ```js window.open_sheet(() => filtersPanel(filters)); window.open_sheet_at("left", () => navigation()); ``` At most one sheet is open at a time. `window.open_sheet` anchors it to the right; `window.open_sheet_at` takes `"left"`, `"right"`, `"top"` or `"bottom"`. It has no options at all, because there is only ever one and it is dismissed by Escape or by its overlay whenever no dialog is above it. ```text unknown sheet placement `middle`; expected left, right, top or bottom ``` ## Toasts A toast is the one overlay that is **data rather than a View** — no function, no instance, nothing for the script to render — which is why its whole content crosses the boundary as an options object. | Field | Default | Meaning | | --- | --- | --- | | `title` | required | The sentence the user reads | | `description` | — | A second line | | `level` | `info` | `info`, `success`, `warning` or `error` | | `timeout` | 5 s | Milliseconds, or `null` to stay until dismissed | | `id` | generated | Identity, for replacing and dismissing | An omitted `timeout` keeps the default and an explicit `null` makes the toast sticky, so the two cannot be collapsed into one option. The `id` is what turns a repeated failure into one standing message instead of a pile. The `--watch` loop uses exactly this: a failed reload posts a sticky error toast with a fixed id, so saving a broken file five times replaces the message rather than stacking five of them, and the next successful reload retracts it with `remove_toast`. ```text unknown toast level `fatal`; expected info, success, warning or error ``` Three toasts are mounted at once. Older ones stay in the manager and reappear as newer ones leave, so a burst is throttled rather than lost. ## The window itself The same `window` global answers questions about the window, not only about what is floating over it. ```js render(cx) { const { width, height } = window.viewport_size(); return v_flex() .when(width < 600, (el) => el.flex_col()) .text_size(window.rem_size() * 0.875); } ``` **Measurements are legal from `render`**, and that is the point of them: a View that lays itself out from the window's size has to ask during the pass that draws it. | Member | What it answers | | --- | --- | | `rem_size()` / `line_height()` | The window's type metrics, in pixels | | `viewport_size()` | The drawable area | | `bounds()` | Where the window sits on screen and how big it is — larger than the viewport by its title bar | | `mouse_position()` | Where the pointer is, in window coordinates | | `appearance()` | `"light"` or `"dark"` | | `is_window_active()` / `is_fullscreen()` / `is_maximized()` | The platform window's state | **Calls that change the window are refused from `render`**, for the reason `cx.notify()` is: a frame that changes the window it is drawing into is a frame arguing with itself. | Member | What it does | | --- | --- | | `set_rem_size(size)` | Rescales everything expressed in rems | | `refresh()` | Redraws every View in the window | | `focus_next()` / `focus_prev()` | Moves the keyboard one tab stop | | `dispatch_action(action)` | Dispatches an action down this window's focus path | | `activate_window()` / `minimize_window()` / `zoom_window()` / `toggle_fullscreen()` | Platform window controls | `zoom_window()` is the platform's own zoom, not a scale factor — `set_rem_size` is the one that rescales. ## Stacking and dismissal Painted back to front: 1. **Content** — the script's root View. 2. **Sheet** — at most one, anchored to an edge. A sheet is a *place* in the window, so it sits below the dialog stack: a dialog raised from inside a sheet must be readable, and a sheet raised under a dialog must not cover it. 3. **Dialog stack** — in open order, oldest at the bottom. 4. **Toasts** — above everything. A toast reports the outcome of the action the user just took, and an open dialog is exactly the situation where that outcome matters most, so it is the one layer that is never occluded. Only the topmost dialog draws a backdrop: a stack of three dims the window once, not three times, and that single backdrop is what separates the live dialog from the inert ones behind it. Dismissal is always **one layer, never a cascade**: * **Escape** closes the topmost dialog only. Lower dialogs render with keyboard handling disabled, so repeated Escapes unwind the stack one dialog per press and never reach the sheet while a dialog is open. * `escape_dismissable: false` withdraws the **key binding**, not the underlying cancel action. A close control the script puts inside the dialog still works — which is what makes an undismissable dialog one the user must answer rather than one they cannot leave. * **Backdrop press** closes the topmost dialog, and only if it was opened with `backdrop_dismissable`. * **Enter does nothing** at this layer. Base's dialog host treats Enter as "confirm and close"; that belongs to the dialog's own primary button, which the script owns, so the root vetoes the built-in confirmation rather than guessing which content wanted it. * A **sheet** is dismissed by Escape or by its overlay only when no dialog is open, because a dialog above it holds focus. * `close_all_dialogs` is the one operation that unwinds the whole stack, and it leaves the sheet alone. **Focus** is restored through the stack's own history. Opening an overlay records what was focused and focuses the overlay; closing it restores that handle. Closing the second dialog returns focus to the first, and closing the first returns it to whatever the window was on before either opened. Tab and Shift-Tab honour the focus trap, so tabbing inside an overlay cycles within it rather than walking into the content behind it. ## The `ScopePhase` rule **An overlay may only be opened or closed from an event handler or a task.** ```text window.open_dialog(content, options) is not allowed during the `render` phase; overlays may only be opened or closed while handling an event or a task ``` Opening or closing an overlay mutates the window, and the render pass is reading it. GPUI's borrow model has no way to express "the script may notify from here but not from there", so the runtime carries the [`ScopePhase`](./state.md#scope-phases) explicitly and every overlay entry point refuses `render`, `layout`, and being called from outside any host call at all — in the last case there is no window to reach either. The refusal names the phase it came from, because that is the only clue the author has. `window.has_active_dialog()` and `window.has_active_sheet()` are the exception, and read the same rule: they ask a question rather than change anything, and a View that draws itself differently while a dialog is up has to ask during the pass that draws it. ## Overlays need a `ShellRoot` Every one of these calls reaches the window's root View. A window whose first View is not a `ShellRoot` refuses them, and says which mistake it was — a host wiring problem, not a script one: ```text window.open_dialog(content, options) needs a ShellRoot as the window's first View; this window was opened with another View ``` See [Getting started](./getting-started.md#add-the-runtime-to-a-rust-application). ## Not there yet * **A result from a dialog.** `open_dialog` returns a depth, not a promise that settles when the dialog closes. Close over a callback, as the example above does, or have the dialog write back to state the opener reads. * **Tooltips and context menus.** Popover and HoverCard are available as anchored surfaces; dedicated tooltip and context-menu APIs are not yet exposed. * **Positioning options.** A dialog is centred and a sheet is edge-anchored; neither can be placed. --- --- url: /base/primitives/pagination.md description: A controlled page navigator with explicit current and total page state. --- # Pagination A controlled page navigator with explicit current and total page state. Like every `gpui-base` primitive, Pagination supplies behavior and semantic structure without imposing a product visual language. Apply GPUI styles and compose the exported parts to match your design system. ## Example The [single native Cargo entrypoint](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/components.rs) selects this primitive from the [shared showcase implementation](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/mod.rs). The same showcase is compiled once for the WASM preview above. ```bash cargo run -p gpui-base --example components -- pagination ``` ## Import ```rust use gpui_kit::base::{Pagination, PaginationState}; ``` ## Anatomy and API The example composes `Pagination`, `PaginationState`. GPUI's standard styling and event traits provide presentation; these base types provide the interaction structure. The authoritative module is [`components/pagination.rs`](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/components/pagination.rs). Native and browser previews compile this same file. ## State and events `PaginationState` owns current and total pages; `on_change` reports valid requested pages. Keep controlled state on the parent render type or in a GPUI entity. Update it in callbacks and call `cx.notify()`; do not recreate persistent entities during every render. ## Complete Rust example The complete implementation used by the runnable showcase is embedded directly from Rust source: The command above supplies application initialization, window creation, and shared `BaseShowcase` state. ## Accessibility Identify current page, label previous/next, and disable unavailable boundary actions. ## Notes Use stable element IDs where accepted. Verify focus, hover, active, selected, disabled, reduced-motion, and high-contrast appearances in the consuming design system. --- --- url: /docs/components/pagination.md description: 'Pagination with page navigation, next and previous links.' --- # Pagination The [Pagination] component provides page navigation with next and previous links. It displays page numbers and allows users to navigate through multiple pages of content. ## Import ```rust use gpui_kit::component::pagination::Pagination; ``` ## Usage ### Basic Pagination ```rust Pagination::new("my-pagination") .current_page(5) .total_pages(10) .on_click(|page, _, cx| { println!("Navigated to page: {}", page); }) ``` ### With Visible Pages By default, the pagination shows up to 5 visible page buttons. You can customize this with `visible_pages()`: ```rust Pagination::new("my-pagination") .current_page(1) .total_pages(50) .visible_pages(10) .on_click(|page, _, cx| { // Handle page change }) ``` ### Compact Style The compact style only shows the previous and next buttons with icons, without displaying page numbers. Use `compact` method to enable compact style: ```rust Pagination::new("my-pagination") .compact() .current_page(3) .total_pages(10) .on_click(|page, _, cx| { // Handle page change }) ``` ### Different Sizes The Pagination supports the [Sizable] trait for different sizes: ```rust use gpui_kit::component::{Sizable as _, Size}; Pagination::new("my-pagination") .xsmall() .current_page(1) .total_pages(10) Pagination::new("my-pagination") .small() .current_page(1) .total_pages(10) Pagination::new("my-pagination") .current_page(1) .total_pages(10) // Medium (default) Pagination::new("my-pagination") .large() .current_page(1) .total_pages(10) ``` ### Disabled State ```rust Pagination::new("my-pagination") .current_page(4) .total_pages(10) .disabled(true) .on_click(|_, _, _| {}) ``` ### Handle Page Change Events The `on_click` callback receives the new page number when users click on page numbers, previous, or next buttons: ```rust Pagination::new("my-pagination") .current_page(current_page) .total_pages(total_pages) .on_click(|page, _, cx| { // Update your state with the new page // The page number is 1-based }) ``` ## API Reference * [Pagination] ### Sizing Implements [Sizable] trait: * `xsmall()` - Extra small size * `small()` - Small size * `medium()` - Medium size (default) * `large()` - Large size * `with_size(size)` - Set custom size ### Methods * `current_page(page: usize)` - Set the current page number (1-based). The value will be clamped between 1 and total\_pages. * `total_pages(pages: usize)` - Set the total number of pages. * `visible_pages(max: usize)` - Set the maximum number of visible page buttons (default: 5). * `compact()` - Enable compact style (only shows prev/next buttons with icons). * `disabled(bool)` - Set the disabled state. * `on_click(handler)` - Set the handler for page change events. ## Examples ### With State Management ```rust let mut current_page = 1; let total_pages = 20; Pagination::new("pagination") .current_page(current_page) .total_pages(total_pages) .on_click({ let entity = entity.clone(); move |page, _, cx| { entity.update(cx, |this, cx| { this.current_page = *page; cx.notify(); }); } }) ``` ### Large Dataset Pagination For large datasets, use `visible_pages()` to show more page options: ```rust Pagination::new("large-pagination") .current_page(25) .total_pages(100) .visible_pages(10) .on_click(|page, _, cx| { // Load data for the new page }) ``` [Pagination]: https://docs.rs/gpui-component/latest/gpui_component/pagination/struct.Pagination.html [Sizable]: https://docs.rs/gpui-component/latest/gpui_component/trait.Sizable.html --- --- url: /zh-CN/base/primitives/pagination.md description: 显式管理当前页和总页数的受控分页导航。 --- # Pagination 显式管理当前页和总页数的受控分页导航。 和所有 GPUI Base 原语一样,Pagination 只提供行为和语义结构,不规定产品视觉语言。请使用 GPUI 样式并组合导出的部件,使其符合你的设计系统。 ## 示例 原生示例和页面上方的 WASM 预览共用同一份实现: ```bash cargo run -p gpui-base --example components -- pagination ``` ## 导入 ```rust use gpui_kit::base::{Pagination, PaginationState}; ``` ## 结构与 API 示例组合上述公开类型。GPUI 的标准样式和事件 trait 负责表现,Base 类型负责交互结构。权威实现位于 [`components/pagination.rs`](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/components/pagination.rs),原生与浏览器预览编译的是同一文件。 ## 状态与事件 PaginationState 保存页数边界与当前页,应用处理页码变化。 受控状态应保存在父渲染类型或 GPUI entity 中;在回调中更新并调用 `cx.notify()`,不要在每次渲染时重建持久 entity。 ## 完整 Rust 示例 ## 可访问性 标记当前页,为上一页、下一页和具体页码提供可访问名称。 ## 注意事项 在支持的位置使用稳定元素 ID,并在消费端设计系统中验证焦点、悬停、按下、选中、禁用、减少动态效果和高对比度状态。 --- --- url: /zh-CN/docs/components/pagination.md description: 提供页码、上一页和下一页导航的分页组件。 --- # Pagination [Pagination] 组件用于在多页内容之间切换,支持显示页码、上一页和下一页操作,适合表格、列表和搜索结果等需要分页浏览的场景。 ## 导入 ```rust use gpui_kit::component::pagination::Pagination; ``` ## 用法 ### 基础分页 ```rust Pagination::new("my-pagination") .current_page(5) .total_pages(10) .on_click(|page, _, cx| { println!("Navigated to page: {}", page); }) ``` ### 自定义可见页数 默认最多显示 5 个页码按钮,可以通过 `visible_pages()` 调整: ```rust Pagination::new("my-pagination") .current_page(1) .total_pages(50) .visible_pages(10) .on_click(|page, _, cx| { // 处理页码切换 }) ``` ### 紧凑模式 紧凑模式只显示上一页和下一页按钮,不显示具体页码: ```rust Pagination::new("my-pagination") .compact() .current_page(3) .total_pages(10) .on_click(|page, _, cx| { // 处理页码切换 }) ``` ### 不同尺寸 ```rust use gpui_kit::component::{Sizable as _, Size}; Pagination::new("my-pagination") .xsmall() .current_page(1) .total_pages(10) Pagination::new("my-pagination") .small() .current_page(1) .total_pages(10) Pagination::new("my-pagination") .current_page(1) .total_pages(10) Pagination::new("my-pagination") .large() .current_page(1) .total_pages(10) ``` ### 禁用状态 ```rust Pagination::new("my-pagination") .current_page(4) .total_pages(10) .disabled(true) .on_click(|_, _, _| {}) ``` ### 处理页码变化 `on_click` 会在用户点击页码、上一页或下一页时返回新的页码: ```rust Pagination::new("my-pagination") .current_page(current_page) .total_pages(total_pages) .on_click(|page, _, cx| { // 用新的页码更新状态 // 页码从 1 开始 }) ``` ## API 参考 ### 尺寸 实现了 [Sizable] trait: * `xsmall()`:超小尺寸 * `small()`:小尺寸 * `medium()`:中尺寸,默认值 * `large()`:大尺寸 * `with_size(size)`:设置自定义尺寸 ### 方法 * `current_page(page: usize)`:设置当前页,页码从 1 开始,超出范围时会自动限制到 `1..=total_pages` * `total_pages(pages: usize)`:设置总页数 * `visible_pages(max: usize)`:设置最多显示多少个页码按钮,默认 `5` * `compact()`:启用紧凑模式,仅显示前后翻页按钮 * `disabled(bool)`:设置禁用状态 * `on_click(handler)`:设置页码切换回调 ## 示例 ### 结合状态管理 ```rust let mut current_page = 1; let total_pages = 20; Pagination::new("pagination") .current_page(current_page) .total_pages(total_pages) .on_click({ let entity = entity.clone(); move |page, _, cx| { entity.update(cx, |this, cx| { this.current_page = *page; cx.notify(); }); } }) ``` ### 大数据集分页 ```rust Pagination::new("large-pagination") .current_page(25) .total_pages(100) .visible_pages(10) .on_click(|page, _, cx| { // 加载新页的数据 }) ``` [Pagination]: https://docs.rs/gpui-component/latest/gpui_component/pagination/struct.Pagination.html [Sizable]: https://docs.rs/gpui-component/latest/gpui_component/trait.Sizable.html --- --- url: /shell/performance.md description: >- What a script costs once frame rate stops being the variable — invalidation against description size, the View as the boundary that bounds both, and the two failures FPS cannot tell apart. --- # Performance [The script is not in the frame](./index.md#performance-the-script-is-not-in-the-frame) is the claim the runtime is built on. This page is what follows from it: once a repaint no longer runs JavaScript, the cost that is left has a shape small enough to write down. ```text script cost = how often a View is invalidated × what describing that View costs ``` Neither factor is the frame rate. A window repainting at 120 Hz runs no more JavaScript than one repainting at 30 Hz, and a View nobody has invalidated runs none at all. Both factors are yours: the left one is where you call `cx.notify()`, the right one is how much interface sits behind a single call. Everything below is one of those two, or a way of telling which is the problem. ## Every View has its own Snapshot GPUI Shell gives each JavaScript View a Snapshot of its own: the description that View's `render` produced, kept in Rust. **A View's Snapshot is reused until that View changes.** Every frame in between is drawn from it — turned into GPUI elements, laid out, painted — entirely in Rust. No JavaScript runs. ```text the View changed ──▶ render() ──▶ a new Snapshot ──▶ frame the View did not ─────────────────▶ the Snapshot it has ──▶ frame ``` Snapshots are per View, not per window. A window holding a hundred Views holds a hundred Snapshots, and each one is invalidated on its own: | What happens | What runs | | --- | --- | | `Watchlist` calls `cx.notify()` | `Watchlist.render`, and nothing else | | The parent calls `cx.notify()` | The parent's `render`. Each child answers the frame from its own Snapshot | | `this.chart.set_props({ symbol })` | That child's `update` and `render`. The parent is not rebuilt | | A child of a child calls `cx.notify()` | That child's `render`. Invalidation does not travel upward | | The theme changes | Every View, because a Snapshot bakes in the colours it was built with | ## Split a large View into small ones A View is rebuilt whole. There is no partial rebuild inside one: if a View's description is four hundred nodes, any change rebuilds all four hundred, however small the change was. That is what makes a large View expensive. Everything it draws shares one Snapshot, so the data that changes most often invalidates the parts that never change along with it. In a market terminal, one price moving re-describes the chart, the sidebar and the order book too — not because they changed, but because they sit inside the same View. Splitting is the fix. Give each part that changes on its own a View of its own with `cx.new`, and a change reaches one Snapshot instead of all of them: ```js import { View } from "gpui-kit"; export default class Terminal extends View { init(props, cx) { this.sidebar = cx.new(Sidebar); this.watchlist = cx.new(Watchlist, { symbols: props.symbols }); this.chart = cx.new(PriceChart, { symbol: props.symbols[0] }); this.detail = cx.new(Detail, { symbol: props.symbols[0] }); } render() { return h_flex() .child(this.sidebar) .child(this.watchlist) .child(v_flex().child(this.chart).child(this.detail)); } } ``` On the 40-row watchlist this page measures, describing the whole panel costs **0.315 ms** and describing one row costs **0.012 ms** — 361 nodes against 9. Nesting itself costs almost nothing to weigh against that: a parent records a handle per child, not the child's description. So a complex interface is not, by itself, a performance problem. A large View is. And splitting for performance means splitting into **Views** — not into plugins, applications or processes. Reach for a second application when you want a second *authority*, which is [Capabilities](./capabilities.md), not when you want a second cache. ## Notify what a reader can see `cx.notify()` is the whole dependency system, and it means one specific thing: **my description is out of date.** It is not an event notification, and using it as one is the most common way to make a script expensive. A feed handler is the usual case: ```js onQuote(quote, cx) { this.quotes.set(quote.symbol, quote); cx.notify(); // every tick, including the ones nobody is looking at } ``` If the View draws twenty symbols out of a subscription of two thousand, that `notify` pays for a full description of the panel on every tick of every symbol it does not draw. The fix is a condition, not a faster render: ```js onQuote(quote, cx) { this.quotes.set(quote.symbol, quote); if (this.visible.has(quote.symbol)) cx.notify(); } ``` Three rules follow from the same idea: * **Invalidate the View that changed.** State that belongs to one child should live on that child and be notified there, rather than on the parent that mounts it. * **Notifying more often than the frame rate costs nothing extra.** See below — batching by hand buys nothing, conditioning does. * **From the host, `cx.notify()` and `ScriptView::refresh` are different requests.** A bare `notify` repaints the description that already exists. If Rust changed state the script reads through a [HostModule](./host-module.md), the description is stale and only `refresh` says so. See [Hosting](./hosting.md#refreshing-a-view-from-host-state). ### What `notify` does, and what coalesces it `cx.notify()` rebuilds nothing. It sets a flag on the View saying its description may be stale, and asks GPUI to draw. The rebuild happens later, inside the frame, and only if the flag is still set. So every notify between two frames collapses into one `render` — whether they came from three event handlers, from a task in a loop, or from the host: ```text notify notify notify ──▶ one frame ──▶ one render() ``` Setting a flag three times is setting it once. Nothing is dropped: all three handlers ran and all three changed state; what they share is the single rebuild that follows. **That puts a ceiling on what invalidation can cost: at most one script render per View per frame.** A feed ticking a thousand times a second costs at most 120 renders a second on a 120 Hz display, not a thousand. It is why an over-eager `notify` shows up as wasted work rather than as a runaway. The runtime adds no throttle of its own on top of that, and there is none to tune. The coalescing is GPUI's own scheduling, and it never defers a rebuild past the next frame — so it costs no latency, which is the other half of the pair below. ### What the cache costs in memory A View holds **two** descriptions: the one it published, and the one it replaced. The second is kept a moment longer because an event can still be dispatched against a frame that has already been superseded, and the handlers that frame needs belong to that older description. There is no third. Publishing a new description drops the oldest, and dropping it retires the callbacks registered with it. So the ceiling is two descriptions per live View, and nothing accumulates with time: a View that has re-rendered a million times holds exactly what a View that rendered twice holds. Closing a panel drops its View, and both of its descriptions go with it. This is the other reason to split a large View rather than fear splitting: a hundred small Views hold a hundred small pairs, which together are the same interface described twice — not a hundred times. ## Frame rate and presentation latency are different failures Two things can be wrong with a running interface, and only one of them shows up as FPS: ```text Rendering FPS is the frame smooth? State → presentation how long after state changes does the reader see it? ``` Missing a `cx.notify()` costs no frames at all. GPUI keeps replaying the last good description at full rate, so the HUD reads a steady 120 FPS while the interface is showing something that stopped being true — and then jumps a quarter of a second later, when something unrelated invalidates the View. Every rendering measurement calls this healthy. | Symptom | Which number is wrong | Usual cause | | --- | --- | --- | | The window stutters while nothing in the application is changing | FPS | Description too large per frame, or a virtual list doing per-row work; see [the measurement](./engine.md#the-measurement) | | The window stutters while a feed is running | FPS *and* invalidation | One boundary being rebuilt too often, too large, or both | | The window is smooth and the data is late | Presentation latency | A `notify` that was skipped, deferred behind an `await`, or issued as a host `cx.notify()` where `refresh` was meant | Diagnose them separately. An FPS reading that never dropped is not evidence that invalidation is correct. ## Reading the counters The runtime counts the two events apart, and the host can read them with `runtime.read_metrics()` — see [Watching what it costs](./hosting.md#watching-what-it-costs) for the API and the delta-against-a-baseline pattern that turns them into per-second rates. | Reading | The question it answers | | --- | --- | | `script_renders()` | How often JavaScript ran. Follows `cx.notify()`, reloads and theme changes — never frames | | `materializations()` | How often a dirty Snapshot became elements. Clean window frames reuse the cached GPUI subtree | | `mean_script_render()` | What one description costs, host calls included | | `mean_native()` | How much of that was inside HostModule functions rather than describing | | `slowest_script_render()` | The worst single build in the run | | `frame_script_calls()` | Entries into the VM from the frame path — [virtual list](./elements.md) item renderers and [dock](./dock.md) chrome handlers, which are the only two | | `structure_repeat_rate()` | Of the rebuilds that had a predecessor, what fraction described the same *shape* — see below | What the shape of a reading says: * **`script_renders` per second far above the rate the data actually changes** — a `notify` is firing on things the reader cannot see. Condition it. * **`script_renders` reasonable, `mean_script_render` high** — the boundary is too large. Split the View. * **`mean_native` most of `mean_script_render`** — the cost is in the host functions the description calls, not in the description. Read them once into fields before `render`, not per node. * **`slowest_script_render` far above the mean** — one build is paying for something the rest are not: a collection materialized on first render, or a rarely-taken branch that describes far more than the common one. A mean that drifts as a whole is system load instead. Repeated `cx.theme()` calls do not repeatedly cross the native snapshot boundary. The runtime synchronizes a lightweight theme revision before a description starts; components then share one frozen JavaScript object until the semantic tokens or appearance change. Reading the theme in each component is therefore a cache lookup, not a reason to serialize the palette again. ## Where the Snapshot cache stops The Snapshot removes the cost of **no change**. It does not remove the cost of a **small change**. A Snapshot holds structure and values together: ```text StockRow ├── Symbol("AAPL") ├── Price("230.42") └── Change("+1.42%") ``` When the price becomes `230.51`, the structure is identical and only one leaf differs — but a new description is the only way to say so, so the whole View is described again: every `div()`, every `.gap()`, every `.bg()`, every crossing into Rust. That is the dirty-render path, and on a fast feed it is the one that runs. The lever is the one this page opens with: **shrink the boundary that has to be rebuilt.** On the watchlist above, describing the whole panel costs 0.315 ms and describing one row costs 0.012 ms — 361 nodes against 9. Putting the row behind a View of its own is what turns the first number into the second, and it is available today. `structure_repeats()` and `structure_changes()` are how you check that the boundary is doing what you think. They count how often a rebuild produced the same *shape* as the description it replaced, differing only in the values inside it. A panel reporting a low rate is worth knowing about on its own: something in it is changing structure when you thought only a number was. --- --- url: /docs/components/plot.md description: >- A low-level plotting library for creating custom charts and data visualizations. --- # Plot The `plot` module provides low-level building blocks for creating custom charts. It includes scales, shapes, and utilities that power the high-level `Chart` components. ## Import ```rust use gpui_kit::component::plot::{ scale::{Scale, ScaleLinear, ScaleBand, ScalePoint, ScaleOrdinal}, shape::{Bar, Stack, Line, Area, Pie, Arc}, PlotAxis, AxisText }; ``` ## Scales Scales map a dimension of abstract data to a visual representation. ### ScaleLinear Maps a continuous quantitative domain to a continuous range. ```rust let scale = ScaleLinear::new( vec![0., 100.], // Domain (data values) vec![0., 500.] // Range (pixel coordinates) ); scale.tick(&50.); // Returns pixel position ``` ### ScaleBand Maps a discrete domain to a continuous range, useful for bar charts. ```rust let scale = ScaleBand::new( vec!["A", "B", "C"], // Domain vec![0., 300.] // Range ) .padding_inner(0.1) .padding_outer(0.1); scale.band_width(); // Returns width of each band scale.tick(&"A"); // Returns start position of band "A" ``` ### ScalePoint Maps a discrete domain to a set of points in a continuous range, useful for scatter plots or line charts with categorical axes. ```rust let scale = ScalePoint::new( vec!["A", "B", "C"], // Domain vec![0., 300.] // Range ); scale.tick(&"A"); // Returns position of point "A" ``` ### ScaleOrdinal Maps a discrete domain to a discrete range. ```rust let scale = ScaleOrdinal::new( vec!["A", "B", "C"], // Domain vec![color1, color2, color3] // Range ); scale.map(&"A"); // Returns color1 ``` ## Shapes ### Bar Renders a bar shape, commonly used in bar charts. ```rust Bar::new() .data(data) .band_width(30.) .x(|d| x_scale.tick(&d.category)) .y0(|d| y_scale.tick(&0.).unwrap()) .y1(|d| y_scale.tick(&d.value)) .fill(|d| color_scale.map(&d.category)) .paint(&bounds, window, cx); ``` ### Line Renders a line shape, commonly used in line charts. ```rust Line::new() .data(data) .x(|d| x_scale.tick(&d.date)) .y(|d| y_scale.tick(&d.value)) .stroke(cx.theme().chart_1) .stroke_width(px(2.)) .paint(&bounds, window); ``` #### Line with Dots Supports rendering dots at data points. ```rust Line::new() .data(data) .x(|d| x_scale.tick(&d.date)) .y(|d| y_scale.tick(&d.value)) .dot() .dot_size(px(4.)) .paint(&bounds, window); ``` ### Area Renders an area shape, commonly used in area charts. ```rust Area::new() .data(data) .x(|d| x_scale.tick(&d.date)) .y0(height) // Baseline .y1(|d| y_scale.tick(&d.value)) .fill(cx.theme().chart_1.opacity(0.5)) .stroke(cx.theme().chart_1) .paint(&bounds, window); ``` ### Pie & Arc Renders pie charts and donut charts using `Pie` layout and `Arc` shape. ```rust // 1. Compute pie layout let pie = Pie::new() .value(|d| Some(d.value)) .pad_angle(0.05); let arcs = pie.arcs(&data); // 2. Render arcs let arc_shape = Arc::new() .inner_radius(0.) .outer_radius(100.); for arc_data in arcs { arc_shape.paint( &arc_data, color_scale.map(&arc_data.data.category), // Color None, // Override inner radius None, // Override outer radius &bounds, window ); } ``` ### Stack Computes stacked layout for data series. ```rust let stack = Stack::new() .data(data) .keys(vec!["series1", "series2"]) .value(|d, key| match key { "series1" => Some(d.val1), "series2" => Some(d.val2), _ => None }); let series = stack.series(); // Returns Vec> ``` ## Components ### PlotAxis Renders chart axes with labels and ticks. ```rust PlotAxis::new() .x(height) // Y position for X axis .x_label(labels) // Iterator of AxisText .stroke(cx.theme().border) .paint(&bounds, window, cx); ``` ## Examples ### Custom Bar Chart Implementation Here's how to implement a custom stacked bar chart using low-level plot primitives: ```rust struct StackedBarChart { data: Vec, series: Vec>, } impl StackedBarChart { pub fn new(data: Vec) -> Self { let series = Stack::new() .data(data.clone()) .keys(vec!["desktop", "mobile"]) .value(|d, key| match key { "desktop" => Some(d.desktop), "mobile" => Some(d.mobile), _ => None, }) .series(); Self { data, series } } } impl Plot for StackedBarChart { fn paint(&mut self, bounds: Bounds, window: &mut Window, cx: &mut App) { // 1. Setup Scales let x = ScaleBand::new( self.data.iter().map(|v| v.date.clone()).collect(), vec![0., width], ); let y = ScaleLinear::new(vec![0., max_value], vec![height, 0.]); // 2. Draw Axis // ... (axis rendering logic) // 3. Draw Stacked Bars let bar = Bar::new() .stack_data(&self.series) .band_width(x.band_width()) .x(move |d| x.tick(&d.data.date)) .fill(move |_| cx.theme().chart_1); bar.paint(&bounds, window, cx); } } ``` --- --- url: /zh-CN/docs/components/plot.md description: 用于构建自定义图表和数据可视化的底层绘图库。 --- # Plot `plot` 模块提供了构建自定义图表所需的底层能力,包括比例尺、图形和辅助工具。高层 `Chart` 组件也是基于这些原语实现的,适合需要完全控制图表绘制逻辑的场景。 ## 导入 ```rust use gpui_kit::component::plot::{ scale::{Scale, ScaleLinear, ScaleBand, ScalePoint, ScaleOrdinal}, shape::{Bar, Stack, Line, Area, Pie, Arc}, PlotAxis, AxisText }; ``` ## 比例尺 比例尺用于把抽象数据映射成可视化坐标或样式。 ### ScaleLinear ```rust let scale = ScaleLinear::new( vec![0., 100.], vec![0., 500.] ); scale.tick(&50.); ``` ### ScaleBand ```rust let scale = ScaleBand::new( vec!["A", "B", "C"], vec![0., 300.] ) .padding_inner(0.1) .padding_outer(0.1); scale.band_width(); scale.tick(&"A"); ``` ### ScalePoint ```rust let scale = ScalePoint::new( vec!["A", "B", "C"], vec![0., 300.] ); scale.tick(&"A"); ``` ### ScaleOrdinal ```rust let scale = ScaleOrdinal::new( vec!["A", "B", "C"], vec![color1, color2, color3] ); scale.map(&"A"); ``` ## 图形 ### Bar ```rust Bar::new() .data(data) .band_width(30.) .x(|d| x_scale.tick(&d.category)) .y0(|d| y_scale.tick(&0.).unwrap()) .y1(|d| y_scale.tick(&d.value)) .fill(|d| color_scale.map(&d.category)) .paint(&bounds, window, cx); ``` ### Line ```rust Line::new() .data(data) .x(|d| x_scale.tick(&d.date)) .y(|d| y_scale.tick(&d.value)) .stroke(cx.theme().chart_1) .stroke_width(px(2.)) .paint(&bounds, window); ``` #### 带节点的折线 ```rust Line::new() .data(data) .x(|d| x_scale.tick(&d.date)) .y(|d| y_scale.tick(&d.value)) .dot() .dot_size(px(4.)) .paint(&bounds, window); ``` ### Area ```rust Area::new() .data(data) .x(|d| x_scale.tick(&d.date)) .y0(height) .y1(|d| y_scale.tick(&d.value)) .fill(cx.theme().chart_1.opacity(0.5)) .stroke(cx.theme().chart_1) .paint(&bounds, window); ``` ### Pie 与 Arc ```rust let pie = Pie::new() .value(|d| Some(d.value)) .pad_angle(0.05); let arcs = pie.arcs(&data); let arc_shape = Arc::new() .inner_radius(0.) .outer_radius(100.); for arc_data in arcs { arc_shape.paint( &arc_data, color_scale.map(&arc_data.data.category), None, None, &bounds, window ); } ``` ### Stack ```rust let stack = Stack::new() .data(data) .keys(vec!["series1", "series2"]) .value(|d, key| match key { "series1" => Some(d.val1), "series2" => Some(d.val2), _ => None }); let series = stack.series(); ``` ## 组件 ### PlotAxis ```rust PlotAxis::new() .x(height) .x_label(labels) .stroke(cx.theme().border) .paint(&bounds, window, cx); ``` ## 示例 ### 自定义堆叠柱状图 ```rust struct StackedBarChart { data: Vec, series: Vec>, } impl StackedBarChart { pub fn new(data: Vec) -> Self { let series = Stack::new() .data(data.clone()) .keys(vec!["desktop", "mobile"]) .value(|d, key| match key { "desktop" => Some(d.desktop), "mobile" => Some(d.mobile), _ => None, }) .series(); Self { data, series } } } impl Plot for StackedBarChart { fn paint(&mut self, bounds: Bounds, window: &mut Window, cx: &mut App) { // 1. 准备比例尺 let x = ScaleBand::new( self.data.iter().map(|v| v.date.clone()).collect(), vec![0., width], ); let y = ScaleLinear::new(vec![0., max_value], vec![height, 0.]); // 2. 绘制坐标轴 // ...(坐标轴绘制逻辑) // 3. 绘制堆叠柱 let bar = Bar::new() .stack_data(&self.series) .band_width(x.band_width()) .x(move |d| x.tick(&d.data.date)) .fill(move |_| cx.theme().chart_1); bar.paint(&bounds, window, cx); } } ``` --- --- url: /base/primitives/popover.md description: An anchored floating surface with controlled or internally managed open state. --- # Popover An anchored floating surface with controlled or internally managed open state. Like every `gpui-base` primitive, Popover supplies behavior and semantic structure without imposing a product visual language. Apply GPUI styles and compose the exported parts to match your design system. ## Example The [single native Cargo entrypoint](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/components.rs) selects this primitive from the [shared showcase implementation](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/mod.rs). The same showcase is compiled once for the WASM preview above. ```bash cargo run -p gpui-base --example components -- popover ``` ## Import ```rust use gpui_kit::base::{Popover}; ``` ## Anatomy and API The example composes `Popover`. GPUI's standard styling and event traits provide presentation; these base types provide the interaction structure. The authoritative module is [`components/popover.rs`](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/components/popover.rs). Native and browser previews compile this same file. ## State and events Open state can be parent-controlled; activation, outside click, and Escape request lifecycle changes. Keep controlled state on the parent render type or in a GPUI entity. Update it in callbacks and call `cx.notify()`; do not recreate persistent entities during every render. ## Complete Rust example The complete implementation used by the runnable showcase is embedded directly from Rust source: The command above supplies application initialization, window creation, and shared `BaseShowcase` state. ## Accessibility Support Escape/outside dismissal and return focus; move focus only when its content requires it. ## Notes Use stable element IDs where accepted. Verify focus, hover, active, selected, disabled, reduced-motion, and high-contrast appearances in the consuming design system. --- --- url: /docs/components/popover.md description: A floating overlay that displays rich content relative to a trigger element. --- # Popover Popover component for displaying floating content that appears when interacting with a trigger element. Supports multiple positioning options, custom content, different trigger methods, and automatic dismissal behaviors. Perfect for tooltips, menus, forms, and other contextual information. ## Import ```rust use gpui_kit::component::popover::{Popover}; ``` ## Usage ### Basic Popover :::info Any element that implements [Selectable] can be used as a trigger, for example, a [Button]. Any element that implements [RenderOnce] or [Render] can be used as popover content, use `.child(...)` to add children directly. ::: ```rust use gpui_kit::ParentElement as _; use gpui_kit::component::{button::Button, popover::Popover}; Popover::new("basic-popover") .trigger(Button::new("trigger").label("Click me").outline()) .child("Hello, this is a popover!") .child("It appears when you click the button.") ``` ### Popover with Custom Positioning The `anchor` method controls how the popover attaches to the trigger, using the [`Anchor`] type. Imagine the popover has a pointer tip (like a speech bubble's tail). The anchor is where that tip sits relative to the trigger — `Anchor::TopLeft` places it at the trigger's top-left corner, `Anchor::BottomRight` at the bottom-right, and so on. The popover then hangs off that point. For example, `Anchor::TopLeft` places the popover just below the trigger, left-aligned to it: ```text [ Trigger ] ┌──────────────┐ │ Popover │ └──────────────┘ ``` ```rust use gpui_kit::component::Anchor; // Anchored to the trigger's top corners Popover::new("top-left") .anchor(Anchor::TopLeft) .trigger(Button::new("btn").label("Top Left").outline()) .child("Anchored to the trigger's top-left") Popover::new("top-center") .anchor(Anchor::TopCenter) .trigger(Button::new("btn").label("Top Center").outline()) .child("Anchored to the trigger's top-center") Popover::new("top-right") .anchor(Anchor::TopRight) .trigger(Button::new("btn").label("Top Right").outline()) .child("Anchored to the trigger's top-right") // Anchored to the trigger's bottom corners Popover::new("bottom-left") .anchor(Anchor::BottomLeft) .trigger(Button::new("btn").label("Bottom Left").outline()) .child("Anchored to the trigger's bottom-left") Popover::new("bottom-center") .anchor(Anchor::BottomCenter) .trigger(Button::new("btn").label("Bottom Center").outline()) .child("Anchored to the trigger's bottom-center") Popover::new("bottom-right") .anchor(Anchor::BottomRight) .trigger(Button::new("btn").label("Bottom Right").outline()) .child("Anchored to the trigger's bottom-right") ``` ### View in Popover You can add any `Entity` that implemented [Render] as the popover content. ```rust let view = cx.new(|_| MyView::new()); Popover::new("form-popover") .anchor(Anchor::BottomLeft) .trigger(Button::new("show-form").label("Open Form").outline()) .child(view.clone()) ``` ### Add content by `content` method The `content` method allows you to create more complex popover content using a closure. This is useful when you need to build dynamic content or need access to the popover's context. This method will let us to have `&mut PopoverState`, `&mut Window` and `&mut Context` parameters in the closure is to allow you to interact with the popover's state and the overall application context if needed. :::warning This `content` callback will called every time on render the popover. So, you should avoid creating new elements or entities in the content closure or other heavy operations that may impact performance. ::: And `content` will works with `child`, `children` methods together. ```rust use gpui_kit::ParentElement as _; use gpui_kit::component::popover::Popover; Popover::new("complex-popover") .anchor(Anchor::BottomLeft) .trigger(Button::new("complex").label("Complex Content").outline()) .content(|_, _, _| { div() .child("This popover has complex content.") .child( Button::new("action-btn") .label("Perform Action") .outline() ) }) ``` ### Right-Click Popover Sometimes you may want to show a popover on right-click, for example, to create a special your ownen context menu. The `mouse_button` method allows you to specify which mouse button triggers the popover. ```rust use gpui_kit::MouseButton; Popover::new("context-menu") .anchor(Anchor::BottomRight) .mouse_button(MouseButton::Right) .trigger(Button::new("right-click").label("Right Click Me").outline()) .child("Context Menu") .child(Separator::horizontal()) .child("This is a custom context menu.") ``` ### Dismiss Popover manually If you want to dismiss the popover programmatically from within the content, you can emit a `DismissEvent`. In this case, you should use `content` method to create the popover content so you have access to the `cx: &mut Context`. ```rust use gpui_kit::component::{DismissEvent, popover::Popover}; Popover::new("dismiss-popover") .trigger(Button::new("dismiss").label("Dismiss Popover").outline()) .content(|_, cx| { div() .child("Click the button below to dismiss this popover.") .child( Button::new("close-btn") .label("Close Popover") .on_click(cx.listener(|_, _, _, cx| { // NOTE: Here `cx` is `&mut Context` type, so we can emit DismissEvent. cx.emit(DismissEvent); })) ) }) ``` ### Styling Popover Like the others components in GPUI Component, the `appearance(false)` method can be used to disable the default styling of the popover, allowing you to fully customize its appearance. And the `Popover` has implemented the [Styled] trait, so you can use all the styling methods provided by GPUI to style the popover content as you like. ```rust // For custom styled popovers or when you want full control Popover::new("custom-popover") .appearance(false) .trigger(Button::new("custom").label("Custom Style")) .bg(cx.theme().accent) .text_color(cx.theme().accent_foreground) .p_6() .rounded_xl() .shadow_2xl() .child("Fully custom styled popover") ``` ### Control Open State There have `open` and `on_open_change` methods to control the open state of the popover programmatically. This is useful when you want to synchronize the popover's open state with other UI elements or application state. :::tip When you use `open` to control the popover's open state, that means you have take full control of it, so you need to update the state in `on_open_change` callback to keep the popover working correctly. ::: ```rust use gpui_kit::component::popover::Popover; struct MyView { popover_open: bool, } Popover::new("controlled-popover") .open(self.open) .on_open_change(cx.listener(|this, open: &bool, _, cx| { this.popover_open = *open; cx.notify(); })) .trigger(Button::new("control-btn").label("Control Popover").outline()) .child("This popover's open state is controlled programmatically.") ``` ### Default Open The `default_open` method allows you to set the initial open state of the popover when it is first rendered. Please note that if you use the `open` method to control the popover's open state, the `default_open` setting will be ignored. ```rust use gpui_kit::component::popover::Popover; Popover::new("default-open-popover") .default_open(true) .trigger(Button::new("default-open-btn").label("Default Open").outline()) .child("This popover is open by default when first rendered.") ``` [Button]: https://docs.rs/gpui-component/latest/gpui_component/button/struct.Button.html [Selectable]: https://docs.rs/gpui-component/latest/gpui_component/trait.Selectable.html [Render]: https://docs.rs/gpui/latest/gpui/trait.Render.html [RenderOnce]: https://docs.rs/gpui/latest/gpui/trait.RenderOnce.html [Styled]: https://docs.rs/gpui/latest/gpui/trait.Styled.html [`Anchor`]: https://docs.rs/gpui-component/latest/gpui_component/enum.Anchor.html --- --- url: /zh-CN/base/primitives/popover.md description: 支持受控或内部开关状态的锚定浮层。 --- # Popover 支持受控或内部开关状态的锚定浮层。 和所有 GPUI Base 原语一样,Popover 只提供行为和语义结构,不规定产品视觉语言。请使用 GPUI 样式并组合导出的部件,使其符合你的设计系统。 ## 示例 原生示例和页面上方的 WASM 预览共用同一份实现: ```bash cargo run -p gpui-base --example components -- popover ``` ## 导入 ```rust use gpui_kit::base::{Popover}; ``` ## 结构与 API 示例组合上述公开类型。GPUI 的标准样式和事件 trait 负责表现,Base 类型负责交互结构。权威实现位于 [`components/popover.rs`](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/components/popover.rs),原生与浏览器预览编译的是同一文件。 ## 状态与事件 触发器切换打开状态;点击外部或 Escape 可按配置关闭。 受控状态应保存在父渲染类型或 GPUI entity 中;在回调中更新并调用 `cx.notify()`,不要在每次渲染时重建持久 entity。 ## 完整 Rust 示例 ## 可访问性 管理触发器与内容的关系和焦点,不要让关闭后焦点丢失。 ## 注意事项 在支持的位置使用稳定元素 ID,并在消费端设计系统中验证焦点、悬停、按下、选中、禁用、减少动态效果和高对比度状态。 --- --- url: /zh-CN/docs/components/popover.md description: 相对于触发元素显示富内容的浮动层组件。 --- # Popover Popover 用于在触发元素附近展示浮动内容。它支持多种定位方式、自定义内容、不同触发方式以及自动关闭行为,适合用来实现提示卡片、上下文菜单、小表单和局部操作面板。 ## 导入 ```rust use gpui_kit::component::popover::{Popover}; ``` ## 用法 ### 基础 Popover :::info 任何实现了 [Selectable] 的元素都可以作为触发器,例如 [Button]。 任何实现了 [RenderOnce] 或 [Render] 的元素都可以作为 Popover 内容,可以直接通过 `.child(...)` 添加。 ::: ```rust use gpui_kit::ParentElement as _; use gpui_kit::component::{button::Button, popover::Popover}; Popover::new("basic-popover") .trigger(Button::new("trigger").label("Click me").outline()) .child("Hello, this is a popover!") .child("It appears when you click the button.") ``` ### 自定义定位 `anchor` 方法用于控制 Popover 如何贴合触发器,使用 [`Anchor`] 类型。 可以把 Popover 想象成有一个箭头尖角(像对话气泡的小三角)。anchor 指的就是这个尖角相对于触发器落在哪个点——`Anchor::TopLeft` 把它放在触发器的左上角,`Anchor::BottomRight` 放在右下角,以此类推。Popover 就从这个点挂出来。 例如 `Anchor::TopLeft` 会让 Popover 出现在触发器正下方,并与其左对齐: ```text [ Trigger ] ┌──────────────┐ │ Popover │ └──────────────┘ ``` ```rust use gpui_kit::component::Anchor; Popover::new("top-center") .anchor(Anchor::TopCenter) .trigger(Button::new("btn").label("Top Center").outline()) .child("Anchored to the trigger's top-center") ``` ### 在 Popover 中渲染 View 你也可以把实现了 [Render] 的 `Entity` 作为 Popover 内容: ```rust let view = cx.new(|_| MyView::new()); Popover::new("form-popover") .anchor(Anchor::BottomLeft) .trigger(Button::new("show-form").label("Open Form").outline()) .child(view.clone()) ``` ### 使用 `content` 构造动态内容 如果你需要根据状态动态构造内容,或者希望在闭包中拿到 Popover 的上下文,可以使用 `content`: ```rust use gpui_kit::ParentElement as _; use gpui_kit::component::popover::Popover; Popover::new("complex-popover") .anchor(Anchor::BottomLeft) .trigger(Button::new("complex").label("Complex Content").outline()) .content(|_, _, _| { div() .child("This popover has complex content.") .child( Button::new("action-btn") .label("Perform Action") .outline() ) }) ``` :::warning `content` 回调会在每次渲染 Popover 时执行,因此不要在闭包里频繁创建重量级对象或进行高成本计算。 ::: ### 右键触发 如果你想把 Popover 当作自定义上下文菜单来用,可以指定鼠标按键: ```rust use gpui_kit::MouseButton; Popover::new("context-menu") .anchor(Anchor::BottomRight) .mouse_button(MouseButton::Right) .trigger(Button::new("right-click").label("Right Click Me").outline()) .child("Context Menu") .child(Separator::horizontal()) .child("This is a custom context menu.") ``` ### 手动关闭 如果你希望在内容内部主动关闭 Popover,可以发出 `DismissEvent`: ```rust use gpui_kit::component::{DismissEvent, popover::Popover}; Popover::new("dismiss-popover") .trigger(Button::new("dismiss").label("Dismiss Popover").outline()) .content(|_, cx| { div() .child("Click the button below to dismiss this popover.") .child( Button::new("close-btn") .label("Close Popover") .on_click(cx.listener(|_, _, _, cx| { cx.emit(DismissEvent); })) ) }) ``` ### 自定义样式 Popover 同样支持 `appearance(false)` 来关闭默认样式,并通过 [Styled] trait 完整自定义外观: ```rust Popover::new("custom-popover") .appearance(false) .trigger(Button::new("custom").label("Custom Style")) .bg(cx.theme().accent) .text_color(cx.theme().accent_foreground) .p_6() .rounded_xl() .shadow_2xl() .child("Fully custom styled popover") ``` ### 受控打开状态 通过 `open` 和 `on_open_change`,你可以把 Popover 的开关状态交给外部状态管理: ```rust use gpui_kit::component::popover::Popover; struct MyView { popover_open: bool, } Popover::new("controlled-popover") .open(self.open) .on_open_change(cx.listener(|this, open: &bool, _, cx| { this.popover_open = *open; cx.notify(); })) .trigger(Button::new("control-btn").label("Control Popover").outline()) .child("This popover's open state is controlled programmatically.") ``` ### 默认打开 如果只想设置首次渲染时默认打开,可以使用 `default_open(true)`: ```rust use gpui_kit::component::popover::Popover; Popover::new("default-open-popover") .default_open(true) .trigger(Button::new("default-open-btn").label("Default Open").outline()) .child("This popover is open by default when first rendered.") ``` [Button]: https://docs.rs/gpui-component/latest/gpui_component/button/struct.Button.html [Selectable]: https://docs.rs/gpui-component/latest/gpui_component/trait.Selectable.html [Render]: https://docs.rs/gpui/latest/gpui/trait.Render.html [RenderOnce]: https://docs.rs/gpui/latest/gpui/trait.RenderOnce.html [Styled]: https://docs.rs/gpui/latest/gpui/trait.Styled.html [`Anchor`]: https://docs.rs/gpui-component/latest/gpui_component/enum.Anchor.html --- --- url: /base/primitives/popup.md description: A low-level trigger and anchored floating-content host. --- # Popup `Popup` owns trigger measurement, anchor positioning, deferred rendering, and window-edge snapping. The application owns open state, content, appearance, and motion. Higher-level primitives such as Popover build on the same floating-surface ideas. ## Example The [single native Cargo entrypoint](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/components.rs) selects this primitive from the [shared showcase implementation](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/mod.rs). The same showcase is compiled once for the WASM preview above. ```bash cargo run -p gpui-base --example components -- popup ``` ## Import ```rust use gpui_kit::base::Popup; ``` ## Anatomy and API The example composes `Popup`. GPUI's standard styling and event traits provide presentation; these base types provide the interaction structure. The authoritative module is [`components/popup.rs`](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/components/popup.rs). Native and browser previews compile this same file. ## State and events The caller owns trigger, anchor, open state, content, and dismissal policy. Keep controlled state on the parent render type or in a GPUI entity. Update it in callbacks and call `cx.notify()`; do not recreate persistent entities during every render. ## Complete Rust example The complete implementation used by the runnable showcase is embedded directly from Rust source: The command above supplies application initialization, window creation, and shared `BaseShowcase` state. ## Accessibility The caller must supply suitable menu, listbox, or dialog semantics and focus policy. ## Notes Use stable element IDs where accepted. Verify focus, hover, active, selected, disabled, reduced-motion, and high-contrast appearances in the consuming design system. --- --- url: /zh-CN/base/primitives/popup.md description: 底层触发器与锚定浮动内容宿主。 --- # Popup 底层触发器与锚定浮动内容宿主。 和所有 GPUI Base 原语一样,Popup 只提供行为和语义结构,不规定产品视觉语言。请使用 GPUI 样式并组合导出的部件,使其符合你的设计系统。 ## 示例 原生示例和页面上方的 WASM 预览共用同一份实现: ```bash cargo run -p gpui-base --example components -- popup ``` ## 导入 ```rust use gpui_kit::base::{Popup}; ``` ## 结构与 API 示例组合上述公开类型。GPUI 的标准样式和事件 trait 负责表现,Base 类型负责交互结构。权威实现位于 [`components/popup.rs`](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/components/popup.rs),原生与浏览器预览编译的是同一文件。 ## 状态与事件 应用负责打开状态和关闭策略;Popup 负责锚定及浮层结构。 受控状态应保存在父渲染类型或 GPUI entity 中;在回调中更新并调用 `cx.notify()`,不要在每次渲染时重建持久 entity。 ## 完整 Rust 示例 ## 可访问性 根据所组合控件补充正确角色、名称、焦点管理与 Escape 关闭行为。 ## 注意事项 在支持的位置使用稳定元素 ID,并在消费端设计系统中验证焦点、悬停、按下、选中、禁用、减少动态效果和高对比度状态。 --- --- url: /base/primitives.md description: The complete catalog of user-facing gpui-base primitives. --- # Primitives GPUI Base primitives provide behavior without prescribing presentation. Each page documents the public import and the smallest useful composition. The live example above the page is built from `crates/base/examples` and can also run as a native GPUI application. ## Primitive catalog * [Accordion](./accordion.md) — A disclosure group composed from independently styleable header, trigger, and panel parts. * [Alert Dialog](./alert-dialog.md) — A modal confirmation surface for actions that need an explicit decision. * [Avatar](./avatar.md) — An image with composable fallback content for a person or entity. * [Button](./button.md) — An unstyled, accessible pressable with semantic state and keyboard activation. * [Calendar](./calendar.md) — A state-driven date grid with selection matchers and custom item rendering. * [Checkbox](./checkbox.md) — A controlled tri-state check control with a separately styled indicator. * [Collapsible](./collapsible.md) — A composable region that shows or hides content without prescribing its trigger styling. * [Color Picker](./color-picker.md) — State and interaction foundations for selecting colors in a custom picker UI. * [Combobox](./combobox.md) — A text input paired with keyboard-navigable suggestions and selection behavior. * [Date Picker](./date-picker.md) — A focus-aware date input that composes calendar behavior with a popup. * [Dialog](./dialog.md) — A composable modal surface with focus management, backdrop, title, and close parts. * [Hover Card](./hover-card.md) — A delayed floating card associated with a pointer or keyboard trigger. * [Input](./input.md) — A single-line text input with selection, masking, validation, and number stepping. * [Textarea](./textarea.md) — A multi-line text field with fixed rows, wrapping, and auto-grow behavior. * [Editor](./editor.md) — A source-code editor foundation with highlighting, gutter, folding, decorations, and LSP hooks. * [Link](./link.md) — An accessible link-like control with application-defined styling. * [Nav Stack](./nav-stack.md) — A navigation stack of views with push, pop, forward, and replace, and an animatable transition lifecycle. * [Number Input](./number-input.md) — A numeric input with reusable increment, decrement, and step behavior. * [OTP Input](./otp-input.md) — A multi-cell one-time-code input driven by a shared text state. * [Pagination](./pagination.md) — A controlled page navigator with explicit current and total page state. * [Popover](./popover.md) — An anchored floating surface with controlled or internally managed open state. * [Popup](./popup.md) — A low-level trigger and anchored floating-content host. * [Progress](./progress.md) — Composable track and indicator parts for reporting task completion. * [Radio](./radio.md) — A controlled single-choice item with selectable and disabled semantics. * [Radio Group](./radio-group.md) — Groups radio items and provides keyboard navigation for a single selection. * [Resizable](./resizable.md) — Panel groups and resize handles for user-adjustable split layouts. * [Scrollbar](./scrollbar.md) — An unstyled scrollbar connected to GPUI scroll or uniform-list handles. * [Select](./select.md) — A button-like selection control backed by an anchored, keyboard-navigable popup. * [Sheet](./sheet.md) — A modal surface that enters from an edge while managing dismissal and focus. * [Slider](./slider.md) — A state-driven range input with independently styleable track, indicator, and thumb. * [Switch](./switch.md) — A controlled on/off control with separately styleable track and thumb. * [Table](./table.md) — Semantic table primitives for composing headers, bodies, rows, and cells. * [Tabs](./tabs.md) — A tab list and accessible tab controls with controlled selection. * [Toast](./toast.md) — A managed, animated stack of temporary status messages. * [Toggle](./toggle.md) — A controlled two-state pressable for persistent choices such as formatting. * [Toggle Group](./toggle-group.md) — Coordinates a set of toggle controls as a single- or multiple-selection group. * [Tooltip](./tooltip.md) — A delayed, positioned description associated with a trigger element. * [Tree](./tree.md) — A virtualized hierarchical list with explicit expansion and selection state. --- --- url: /base/primitives/progress.md description: Composable track and indicator parts for reporting task completion. --- # Progress Composable track and indicator parts for reporting task completion. Like every `gpui-base` primitive, Progress supplies behavior and semantic structure without imposing a product visual language. Apply GPUI styles and compose the exported parts to match your design system. ## Example The [single native Cargo entrypoint](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/components.rs) selects this primitive from the [shared showcase implementation](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/mod.rs). The same showcase is compiled once for the WASM preview above. ```bash cargo run -p gpui-base --example components -- progress ``` ## Import ```rust use gpui_kit::base::{Progress, ProgressIndicator, ProgressTrack}; ``` ## Anatomy and API The example composes `Progress`, `ProgressIndicator`, `ProgressTrack`. GPUI's standard styling and event traits provide presentation; these base types provide the interaction structure. The authoritative module is [`components/progress.rs`](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/components/progress.rs). Native and browser previews compile this same file. ## State and events Set the value on `Progress`; size and position `ProgressIndicator` inside `ProgressTrack`. Keep controlled state on the parent render type or in a GPUI entity. Update it in callbacks and call `cx.notify()`; do not recreate persistent entities during every render. ## Complete Rust example The complete implementation used by the runnable showcase is embedded directly from Rust source: The command above supplies application initialization, window creation, and shared `BaseShowcase` state. ## Accessibility Expose task label and numeric value; use indeterminate state only when progress is unknown. ## Notes Use stable element IDs where accepted. Verify focus, hover, active, selected, disabled, reduced-motion, and high-contrast appearances in the consuming design system. --- --- url: /zh-CN/base/primitives/progress.md description: 用于报告任务完成度的可组合轨道与指示器。 --- # Progress 用于报告任务完成度的可组合轨道与指示器。 和所有 GPUI Base 原语一样,Progress 只提供行为和语义结构,不规定产品视觉语言。请使用 GPUI 样式并组合导出的部件,使其符合你的设计系统。 ## 示例 原生示例和页面上方的 WASM 预览共用同一份实现: ```bash cargo run -p gpui-base --example components -- progress ``` ## 导入 ```rust use gpui_kit::base::{Progress, ProgressIndicator, ProgressTrack}; ``` ## 结构与 API 示例组合上述公开类型。GPUI 的标准样式和事件 trait 负责表现,Base 类型负责交互结构。权威实现位于 [`components/progress.rs`](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/components/progress.rs),原生与浏览器预览编译的是同一文件。 ## 状态与事件 应用提供规范化进度值;视觉宽度由该值派生。 受控状态应保存在父渲染类型或 GPUI entity 中;在回调中更新并调用 `cx.notify()`,不要在每次渲染时重建持久 entity。 ## 完整 Rust 示例 ## 可访问性 暴露进度角色、当前值、最小值、最大值及有意义的文本标签。 ## 注意事项 在支持的位置使用稳定元素 ID,并在消费端设计系统中验证焦点、悬停、按下、选中、禁用、减少动态效果和高对比度状态。 --- --- url: /docs/components/progress.md description: >- Displays an indicator showing the completion progress of a task, typically displayed as a progress bar or circular indicator. --- # Progress Progress components visually represent the completion percentage of a task. The library provides two variants: * **[Progress](#progress)** - A linear horizontal progress bar * **[ProgressCircle](#progresscircle)** - A circular progress indicator Both components feature smooth transition animations when the value changes, a loading (indeterminate) animation mode, customizable colors, and automatic styling that adapts to the current theme. ## Progress ```rust use gpui_kit::component::progress::Progress; ``` ### Usage ```rust Progress::new("my-progress") .value(75.0) // 75% complete ``` ### Different Progress Values ```rust Progress::new("progress-0").value(0.0) Progress::new("progress-25").value(25.0) Progress::new("progress-75").value(75.0) Progress::new("progress-100").value(100.0) ``` ### Loading State Use `.loading(true)` to show an indeterminate animation when the actual progress is unknown. The `value` is ignored while loading is active. ```rust // Indeterminate loading animation Progress::new("loading").loading(true) // Toggle between loading and determinate Progress::new("my-progress") .loading(self.is_loading) .value(self.progress) ``` ### Sizes `Progress` implements the `Sizable` trait: ```rust Progress::new("xs").value(50.0).xsmall() // 4px height Progress::new("sm").value(50.0).small() // 6px height Progress::new("md").value(50.0) // 8px height (default) Progress::new("lg").value(50.0).large() // 10px height ``` ### Custom Style The component implements the `Styled` trait, allowing custom height, border radius, color, and border: ```rust Progress::new("custom") .value(32.0) .h(px(16.)) .rounded(px(2.)) .color(cx.theme().green_light) .border_2() .border_color(cx.theme().green) ``` ### Dynamic Progress Updates ```rust struct MyView { value: f32, is_loading: bool, } impl Render for MyView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .gap_3() .child( h_flex() .gap_2() .child( Button::new("toggle-loading") .label("Loading") .selected(self.is_loading) .on_click(cx.listener(|this, _, _, cx| { this.is_loading = !this.is_loading; cx.notify(); })), ) .child(Button::new("inc").icon(IconName::Plus).on_click( cx.listener(|this, _, _, _| { this.value = (this.value + 10.).min(100.); }), )), ) .child( Progress::new("progress") .value(self.value) .loading(self.is_loading), ) } } ``` ### API Reference | Method | Type | Description | |---|---|---| | `new(id)` | `ElementId` | Create a new progress bar | | `value(v)` | `f32` | Set progress value (0–100), clamped automatically | | `loading(v)` | `bool` | Enable indeterminate loading animation; ignores `value` when `true` | | `color(c)` | `impl Into` | Override the fill color (defaults to `theme.progress_bar`) | | `xsmall()` / `small()` / `large()` | — | Set predefined height via `Sizable` | | `Styled` trait methods | — | Custom height, border radius, border, etc. | ## ProgressCircle A circular progress indicator that displays progress as an arc. Ideal for compact spaces, inline labels, or as a download/upload indicator. ```rust use gpui_kit::component::progress::ProgressCircle; ``` ### Usage ```rust ProgressCircle::new("circle").value(50.0) ``` ### Loading State ```rust // Indeterminate rotating arc animation ProgressCircle::new("loading").loading(true) // Toggle between loading and determinate ProgressCircle::new("circle") .loading(self.is_loading) .value(self.progress) ``` ### Sizes `ProgressCircle` implements the `Sizable` trait. Named sizes map to fixed pixel dimensions; use `.size(px(n))` for custom sizes: ```rust ProgressCircle::new("xs").value(50.0).xsmall() // size_2 ProgressCircle::new("sm").value(50.0).small() // size_3 ProgressCircle::new("md").value(50.0) // size_4 (default) ProgressCircle::new("lg").value(50.0).large() // size_5 ProgressCircle::new("xl").value(50.0).size_20() // 80px ``` ### Custom Color ```rust ProgressCircle::new("green").value(75.0).color(cx.theme().green) ProgressCircle::new("yellow").value(40.0).color(cx.theme().yellow) ProgressCircle::new("primary").value(60.0).color(cx.theme().primary) ``` ### With Inner Content `ProgressCircle` implements `ParentElement`, so you can place content inside the circle: ```rust ProgressCircle::new("circle-with-label") .value(self.value) .size_20() .child( v_flex() .size_full() .items_center() .justify_center() .gap_1() .child( div() .child(format!("{}%", self.value as i32)) .text_color(cx.theme().progress_bar), ) .child(div().child("Loading").text_xs()), ) ``` ### Inline with Label ```rust h_flex() .gap_2() .items_center() .child( ProgressCircle::new("download") .color(cx.theme().primary) .value(self.progress) .size_4(), ) .child("Downloading...") ``` ### API Reference | Method | Type | Description | |---|---|---| | `new(id)` | `ElementId` | Create a new circular progress indicator | | `value(v)` | `f32` | Set progress value (0–100), clamped automatically | | `loading(v)` | `bool` | Enable indeterminate loading animation; ignores `value` when `true` | | `color(c)` | `impl Into` | Override the arc color (defaults to `theme.progress_bar`) | | `xsmall()` / `small()` / `large()` | — | Set predefined size via `Sizable` | | `size(px(n))` | `Pixels` | Set custom size | | `ParentElement` | — | Place content inside the circle | ## Examples ### File Upload ```rust struct FileUpload { uploaded: u64, total: u64, } impl FileUpload { fn progress(&self) -> f32 { if self.total == 0 { return 0.0; } (self.uploaded as f32 / self.total as f32) * 100.0 } fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { v_flex() .gap_2() .child( h_flex() .justify_between() .child("Uploading...") .child(format!("{:.0}%", self.progress())), ) .child(Progress::new("upload").value(self.progress())) } } ``` ### Initialization with Loading State ```rust struct AppInit { loading: bool, progress: f32, } impl Render for AppInit { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .gap_3() .child( h_flex() .gap_2() .items_center() .child( ProgressCircle::new("init-circle") .loading(self.loading) .value(self.progress) .size_4(), ) .child(if self.loading { "Initializing..." } else { "Ready" }), ) .child( Progress::new("init-bar") .loading(self.loading) .value(self.progress), ) } } ``` ### Multi-Step Process ```rust struct Install { step: usize, // current package index total: usize, // total packages step_progress: f32, } impl Install { fn overall(&self) -> f32 { if self.total == 0 { return 0.0; } (self.step as f32 + self.step_progress / 100.0) / self.total as f32 * 100.0 } fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { v_flex() .gap_2() .child( h_flex() .justify_between() .child(format!("Package {}/{}", self.step + 1, self.total)) .child(format!("{:.0}%", self.overall())), ) .child(Progress::new("overall").value(self.overall())) .child( h_flex() .gap_2() .items_center() .child(Progress::new("package").value(self.step_progress).small()) .child("Current package"), ) } } ``` --- --- url: /zh-CN/docs/components/progress.md description: 用于显示任务完成进度的线性或环形指示器。 --- # Progress Progress 组件用于直观展示任务完成百分比。库中提供两种形式: * **[Progress](#progress)**:线性水平进度条 * **[ProgressCircle](#progresscircle)**:环形进度指示器 这两个组件都支持数值变化动画、加载中(不确定进度)状态、自定义颜色以及自动适配当前主题。 ## Progress ```rust use gpui_kit::component::progress::Progress; ``` ### 用法 ```rust Progress::new("my-progress") .value(75.0) ``` ### 不同进度值 ```rust Progress::new("progress-0").value(0.0) Progress::new("progress-25").value(25.0) Progress::new("progress-75").value(75.0) Progress::new("progress-100").value(100.0) ``` ### 加载状态 当实际进度未知时,可通过 `.loading(true)` 显示不确定进度动画。启用后会忽略 `value`。 ```rust Progress::new("loading").loading(true) Progress::new("my-progress") .loading(self.is_loading) .value(self.progress) ``` ### 尺寸 `Progress` 实现了 `Sizable` trait: ```rust Progress::new("xs").value(50.0).xsmall() Progress::new("sm").value(50.0).small() Progress::new("md").value(50.0) Progress::new("lg").value(50.0).large() ``` ### 自定义样式 组件实现了 `Styled` trait,可自定义高度、圆角、颜色和边框: ```rust Progress::new("custom") .value(32.0) .h(px(16.)) .rounded(px(2.)) .color(cx.theme().green_light) .border_2() .border_color(cx.theme().green) ``` ### 动态更新进度 ```rust struct MyView { value: f32, is_loading: bool, } impl Render for MyView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .gap_3() .child( h_flex() .gap_2() .child( Button::new("toggle-loading") .label("Loading") .selected(self.is_loading) .on_click(cx.listener(|this, _, _, cx| { this.is_loading = !this.is_loading; cx.notify(); })), ) .child(Button::new("inc").icon(IconName::Plus).on_click( cx.listener(|this, _, _, _| { this.value = (this.value + 10.).min(100.); }), )), ) .child( Progress::new("progress") .value(self.value) .loading(self.is_loading), ) } } ``` ### API 参考 | 方法 | 类型 | 说明 | |---|---|---| | `new(id)` | `ElementId` | 创建新的进度条 | | `value(v)` | `f32` | 设置进度值,范围 0 到 100,超出时会自动裁剪 | | `loading(v)` | `bool` | 开启不确定进度动画;为 `true` 时忽略 `value` | | `color(c)` | `impl Into` | 覆盖填充颜色,默认使用 `theme.progress_bar` | | `xsmall()` / `small()` / `large()` | — | 通过 `Sizable` 设置预定义高度 | | `Styled` trait methods | — | 自定义高度、圆角、边框等 | ## ProgressCircle 环形进度指示器适合用于紧凑场景、内联状态或下载上传进度。 ```rust use gpui_kit::component::progress::ProgressCircle; ``` ### 用法 ```rust ProgressCircle::new("circle").value(50.0) ``` ### 加载状态 ```rust ProgressCircle::new("loading").loading(true) ProgressCircle::new("circle") .loading(self.is_loading) .value(self.progress) ``` ### 尺寸 `ProgressCircle` 也实现了 `Sizable` trait。命名尺寸映射到固定像素值,也可以通过 `.size(px(n))` 设置自定义尺寸: ```rust ProgressCircle::new("xs").value(50.0).xsmall() ProgressCircle::new("sm").value(50.0).small() ProgressCircle::new("md").value(50.0) ProgressCircle::new("lg").value(50.0).large() ProgressCircle::new("xl").value(50.0).size_20() ``` ### 自定义颜色 ```rust ProgressCircle::new("green").value(75.0).color(cx.theme().green) ProgressCircle::new("yellow").value(40.0).color(cx.theme().yellow) ProgressCircle::new("primary").value(60.0).color(cx.theme().primary) ``` ### 内部内容 `ProgressCircle` 实现了 `ParentElement`,所以你可以在环形内部放置内容: ```rust ProgressCircle::new("circle-with-label") .value(self.value) .size_20() .child( v_flex() .size_full() .items_center() .justify_center() .gap_1() .child( div() .child(format!("{}%", self.value as i32)) .text_color(cx.theme().progress_bar), ) .child(div().child("Loading").text_xs()), ) ``` ### 和文本一起内联显示 ```rust h_flex() .gap_2() .items_center() .child( ProgressCircle::new("download") .color(cx.theme().primary) .value(self.progress) .size_4(), ) .child("Downloading...") ``` ### API 参考 | 方法 | 类型 | 说明 | |---|---|---| | `new(id)` | `ElementId` | 创建新的环形进度组件 | | `value(v)` | `f32` | 设置进度值,范围 0 到 100,超出时会自动裁剪 | | `loading(v)` | `bool` | 开启不确定进度动画;为 `true` 时忽略 `value` | | `color(c)` | `impl Into` | 覆盖弧线颜色,默认使用 `theme.progress_bar` | | `xsmall()` / `small()` / `large()` | — | 通过 `Sizable` 设置预定义尺寸 | | `size(px(n))` | `Pixels` | 设置自定义尺寸 | | `ParentElement` | — | 允许在环形内部放置内容 | ## 示例 ### 文件上传 ```rust struct FileUpload { uploaded: u64, total: u64, } impl FileUpload { fn progress(&self) -> f32 { if self.total == 0 { return 0.0; } (self.uploaded as f32 / self.total as f32) * 100.0 } fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { v_flex() .gap_2() .child( h_flex() .justify_between() .child("Uploading...") .child(format!("{:.0}%", self.progress())), ) .child(Progress::new("upload").value(self.progress())) } } ``` ### 初始化加载状态 ```rust struct AppInit { loading: bool, progress: f32, } impl Render for AppInit { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .gap_3() .child( h_flex() .gap_2() .items_center() .child( ProgressCircle::new("init-circle") .loading(self.loading) .value(self.progress) .size_4(), ) .child(if self.loading { "Initializing..." } else { "Ready" }), ) .child( Progress::new("init-bar") .loading(self.loading) .value(self.progress), ) } } ``` ### 多步骤流程 ```rust struct Install { step: usize, total: usize, step_progress: f32, } impl Install { fn overall(&self) -> f32 { if self.total == 0 { return 0.0; } (self.step as f32 + self.step_progress / 100.0) / self.total as f32 * 100.0 } fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { v_flex() .gap_2() .child( h_flex() .justify_between() .child(format!("Package {}/{}", self.step + 1, self.total)) .child(format!("{:.0}%", self.overall())), ) .child(Progress::new("overall").value(self.overall())) .child( h_flex() .gap_2() .items_center() .child(Progress::new("package").value(self.step_progress).small()) .child("Current package"), ) } } ``` --- --- url: /base/primitives/radio.md description: A controlled single-choice item with selectable and disabled semantics. --- # Radio A controlled single-choice item with selectable and disabled semantics. Like every `gpui-base` primitive, Radio supplies behavior and semantic structure without imposing a product visual language. Apply GPUI styles and compose the exported parts to match your design system. ## Example The [single native Cargo entrypoint](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/components.rs) selects this primitive from the [shared showcase implementation](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/mod.rs). The same showcase is compiled once for the WASM preview above. ```bash cargo run -p gpui-base --example components -- radio ``` ## Import ```rust use gpui_kit::base::{Radio}; ``` ## Anatomy and API The example composes `Radio`. GPUI's standard styling and event traits provide presentation; these base types provide the interaction structure. The authoritative module is [`components/radio.rs`](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/components/radio.rs). Native and browser previews compile this same file. ## State and events Pass a controlled checked value; `on_change` reports selection and the parent clears peers. Keep controlled state on the parent render type or in a GPUI entity. Update it in callbacks and call `cx.notify()`; do not recreate persistent entities during every render. ## Complete Rust example The complete implementation used by the runnable showcase is embedded directly from Rust source: The command above supplies application initialization, window creation, and shared `BaseShowcase` state. ## Accessibility Give each option a label and place mutually exclusive options in a named group. ## Notes Use stable element IDs where accepted. Verify focus, hover, active, selected, disabled, reduced-motion, and high-contrast appearances in the consuming design system. --- --- url: /zh-CN/base/primitives/radio.md description: 具有选中和禁用语义的受控单选项。 --- # Radio 具有选中和禁用语义的受控单选项。 和所有 GPUI Base 原语一样,Radio 只提供行为和语义结构,不规定产品视觉语言。请使用 GPUI 样式并组合导出的部件,使其符合你的设计系统。 ## 示例 原生示例和页面上方的 WASM 预览共用同一份实现: ```bash cargo run -p gpui-base --example components -- radio ``` ## 导入 ```rust use gpui_kit::base::{Radio}; ``` ## 结构与 API 示例组合上述公开类型。GPUI 的标准样式和事件 trait 负责表现,Base 类型负责交互结构。权威实现位于 [`components/radio.rs`](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/components/radio.rs),原生与浏览器预览编译的是同一文件。 ## 状态与事件 父级保存选中值,激活某一项时替换当前选择。 受控状态应保存在父渲染类型或 GPUI entity 中;在回调中更新并调用 `cx.notify()`,不要在每次渲染时重建持久 entity。 ## 完整 Rust 示例 ## 可访问性 提供标签,并暴露单选角色、选中和禁用状态。 ## 注意事项 在支持的位置使用稳定元素 ID,并在消费端设计系统中验证焦点、悬停、按下、选中、禁用、减少动态效果和高对比度状态。 --- --- url: /docs/components/radio.md description: >- A set of checkable buttons—known as radio buttons—where no more than one of the buttons can be checked at a time. --- # Radio Radio buttons allow users to select a single option from a set of mutually exclusive choices. Use radio buttons when you want to give users a choice between multiple options and only one selection is allowed. ## Import ```rust use gpui_kit::component::radio::{Radio, RadioGroup}; ``` ## Usage ### Basic Radio Button ```rust Radio::new("radio-option-1") .label("Option 1") .checked(false) .on_click(|checked, _, _| { println!("Radio is now: {}", checked); }) ``` ### Controlled Radio Button ```rust struct MyView { radio_checked: bool, } impl Render for MyView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { Radio::new("radio") .label("Select this option") .checked(self.radio_checked) .on_click(cx.listener(|view, checked, _, cx| { view.radio_checked = *checked; cx.notify(); })) } } ``` ### Radio Group (Recommended) ```rust struct MyView { selected_option: Option, } impl Render for MyView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { RadioGroup::horizontal("options") .children(["Option 1", "Option 2", "Option 3"]) .selected_index(self.selected_option) .on_change(cx.listener(|view, selected_index: &usize, _, cx| { view.selected_option = Some(*selected_index); cx.notify(); })) } } ``` ### Different Sizes ```rust Radio::new("small").label("Small").xsmall() Radio::new("medium").label("Medium") // default Radio::new("large").label("Large").large() ``` ### Disabled State ```rust Radio::new("disabled") .label("Disabled option") .disabled(true) .checked(false) Radio::new("disabled-checked") .label("Disabled and checked") .checked(true) .disabled(true) ``` ### Multi-line Label with Custom Content ```rust Radio::new("custom") .label("Primary option") .child( div() .text_color(cx.theme().muted_foreground) .child("This is additional descriptive text that provides more context.") ) .w(px(300.)) ``` ### Custom Tab Order ```rust Radio::new("radio") .label("Custom tab order") .tab_index(2) .tab_stop(true) ``` ## Radio Group Usage ### Horizontal Layout ```rust RadioGroup::horizontal("horizontal-group") .children(["First", "Second", "Third"]) .selected_index(Some(0)) .on_change(cx.listener(|view, index, _, cx| { println!("Selected index: {}", index); cx.notify(); })) ``` ### Vertical Layout ```rust RadioGroup::vertical("vertical-group") .child(Radio::new("option1").label("United States")) .child(Radio::new("option2").label("Canada")) .child(Radio::new("option3").label("Mexico")) .selected_index(Some(1)) .disabled(false) ``` ### Styled Radio Group ```rust RadioGroup::vertical("styled-group") .w(px(220.)) .p_2() .border_1() .border_color(cx.theme().border) .rounded(cx.theme().radius) .child(Radio::new("option1").label("Option 1")) .child(Radio::new("option2").label("Option 2")) .child(Radio::new("option3").label("Option 3")) .selected_index(Some(0)) ``` ### Disabled Radio Group ```rust RadioGroup::vertical("disabled-group") .children(["Option A", "Option B", "Option C"]) .selected_index(Some(1)) .disabled(true) // Disables all radio buttons in the group ``` ## API Reference ### Radio | Method | Description | | ------------------ | ----------------------------------------------------------- | | `new(id)` | Create a new radio button with the given ID | | `label(text)` | Set label text | | `checked(bool)` | Set checked state | | `disabled(bool)` | Set disabled state | | `on_click(fn)` | Callback when clicked, receives `&bool` (new checked state) | | `tab_stop(bool)` | Enable/disable tab navigation (default: true) | | `tab_index(isize)` | Set tab order index (default: 0) | ### RadioGroup | Method | Description | | ------------------------------- | ------------------------------------------------------------------- | | `horizontal(id)` | Create a new horizontal radio group | | `vertical(id)` | Create a new vertical radio group | | `layout(Axis)` | Set layout direction (Vertical or Horizontal) | | `child(Radio)` | Add a single radio button to the group | | `children(items)` | Add multiple radio buttons from an iterator | | `selected_index(Option)` | Set the selected option by index | | `disabled(bool)` | Disable all radio buttons in the group | | `on_change(fn)` | Callback when selection changes, receives `&usize` (selected index) | ### Styling Both Radio and RadioGroup implement `Styled` trait for custom styling: Radio also implements `Sizable` trait: * `xsmall()` - Extra small size * `small()` - Small size * `medium()` - Medium size (default) * `large()` - Large size ## Examples ### Settings Panel ```rust struct SettingsView { theme: Option, // 0: Light, 1: Dark, 2: Auto language: Option, // 0: English, 1: Spanish, 2: French } impl Render for SettingsView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .gap_6() .child( v_flex() .gap_2() .child(div().text_sm().font_semibold().child("Theme")) .child( RadioGroup::vertical("theme") .child(Radio::new("light").label("Light")) .child(Radio::new("dark").label("Dark")) .child(Radio::new("auto").label("Auto")) .selected_index(self.theme) .on_change(cx.listener(|view, index, _, cx| { view.theme = Some(*index); cx.notify(); })) ) ) .child( v_flex() .gap_2() .child(div().text_sm().font_semibold().child("Language")) .child( RadioGroup::horizontal("language") .children(["English", "Español", "Français"]) .selected_index(self.language) .on_change(cx.listener(|view, index, _, cx| { view.language = Some(*index); cx.notify(); })) ) ) } } ``` ### Survey Form ```rust struct SurveyView { satisfaction: Option, recommendation: Option, } impl Render for SurveyView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .gap_8() .child( v_flex() .gap_3() .child( div() .text_base() .font_medium() .child("How satisfied are you with our service?") ) .child( RadioGroup::vertical("satisfaction") .child(Radio::new("very-satisfied").label("Very satisfied")) .child(Radio::new("satisfied").label("Satisfied")) .child(Radio::new("neutral").label("Neutral")) .child(Radio::new("dissatisfied").label("Dissatisfied")) .child(Radio::new("very-dissatisfied").label("Very dissatisfied")) .selected_index(self.satisfaction) .on_change(cx.listener(|view, index, _, cx| { view.satisfaction = Some(*index); cx.notify(); })) ) ) .child( v_flex() .gap_3() .child( div() .text_base() .font_medium() .child("How likely are you to recommend us?") ) .child( RadioGroup::horizontal("recommendation") .children((0..=10).map(|i| i.to_string())) .selected_index(self.recommendation) .on_change(cx.listener(|view, index, _, cx| { view.recommendation = Some(*index); cx.notify(); })) ) ) } } ``` ### Payment Method Selection ```rust struct PaymentView { payment_method: Option, } impl Render for PaymentView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .gap_4() .child( div() .text_lg() .font_semibold() .child("Select Payment Method") ) .child( RadioGroup::vertical("payment") .child( Radio::new("credit-card") .label("Credit Card") .child( div() .text_color(cx.theme().muted_foreground) .child("Visa, MasterCard, American Express") ) ) .child( Radio::new("paypal") .label("PayPal") .child( div() .text_color(cx.theme().muted_foreground) .child("Pay with your PayPal account") ) ) .child( Radio::new("bank-transfer") .label("Bank Transfer") .child( div() .text_color(cx.theme().muted_foreground) .child("Direct bank account transfer") ) ) .selected_index(self.payment_method) .on_change(cx.listener(|view, index, _, cx| { view.payment_method = Some(*index); cx.notify(); })) ) } } ``` ## Best Practices 1. **Use RadioGroup**: Always prefer `RadioGroup` over individual `Radio` components for mutually exclusive choices 2. **Clear Labels**: Provide descriptive labels that clearly indicate what each option represents 3. **Default Selection**: Consider providing a sensible default selection, especially for required fields 4. **Logical Order**: Arrange options in a logical order (alphabetical, frequency of use, or importance) 5. **Limit Options**: Keep the number of radio options reasonable (typically 2-7 options) 6. **Group Related Options**: Use visual grouping and clear headings for multiple radio groups 7. **Responsive Design**: Consider using horizontal layout for fewer options and vertical for more options --- --- url: /zh-CN/docs/components/radio.md description: 一组互斥的单选按钮,同一时间只能选中一个选项。 --- # Radio Radio 用于在一组选项中选择唯一结果。适合“多选一”的场景,例如设置项、问卷和支付方式选择等。 ## 导入 ```rust use gpui_kit::component::radio::{Radio, RadioGroup}; ``` ## 用法 ### 基础单选按钮 ```rust Radio::new("radio-option-1") .label("Option 1") .checked(false) .on_click(|checked, _, _| { println!("Radio is now: {}", checked); }) ``` ### 受控单选按钮 ```rust struct MyView { radio_checked: bool, } impl Render for MyView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { Radio::new("radio") .label("Select this option") .checked(self.radio_checked) .on_click(cx.listener(|view, checked, _, cx| { view.radio_checked = *checked; cx.notify(); })) } } ``` ### RadioGroup(推荐) ```rust struct MyView { selected_option: Option, } impl Render for MyView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { RadioGroup::horizontal("options") .children(["Option 1", "Option 2", "Option 3"]) .selected_index(self.selected_option) .on_change(cx.listener(|view, selected_index: &usize, _, cx| { view.selected_option = Some(*selected_index); cx.notify(); })) } } ``` ### 不同尺寸 ```rust Radio::new("small").label("Small").xsmall() Radio::new("medium").label("Medium") Radio::new("large").label("Large").large() ``` ### 禁用状态 ```rust Radio::new("disabled") .label("Disabled option") .disabled(true) .checked(false) Radio::new("disabled-checked") .label("Disabled and checked") .checked(true) .disabled(true) ``` ### 多行标签与自定义内容 ```rust Radio::new("custom") .label("Primary option") .child( div() .text_color(cx.theme().muted_foreground) .child("This is additional descriptive text that provides more context.") ) .w(px(300.)) ``` ### 自定义 Tab 顺序 ```rust Radio::new("radio") .label("Custom tab order") .tab_index(2) .tab_stop(true) ``` ## Radio Group 用法 ### 横向布局 ```rust RadioGroup::horizontal("horizontal-group") .children(["First", "Second", "Third"]) .selected_index(Some(0)) .on_change(cx.listener(|view, index, _, cx| { println!("Selected index: {}", index); cx.notify(); })) ``` ### 纵向布局 ```rust RadioGroup::vertical("vertical-group") .child(Radio::new("option1").label("United States")) .child(Radio::new("option2").label("Canada")) .child(Radio::new("option3").label("Mexico")) .selected_index(Some(1)) .disabled(false) ``` ### 带样式的分组 ```rust RadioGroup::vertical("styled-group") .w(px(220.)) .p_2() .border_1() .border_color(cx.theme().border) .rounded(cx.theme().radius) .child(Radio::new("option1").label("Option 1")) .child(Radio::new("option2").label("Option 2")) .child(Radio::new("option3").label("Option 3")) .selected_index(Some(0)) ``` ### 禁用整个分组 ```rust RadioGroup::vertical("disabled-group") .children(["Option A", "Option B", "Option C"]) .selected_index(Some(1)) .disabled(true) ``` ## API 参考 ### Radio | 方法 | 说明 | | --- | --- | | `new(id)` | 使用给定 ID 创建单选按钮 | | `label(text)` | 设置标签文本 | | `checked(bool)` | 设置选中状态 | | `disabled(bool)` | 设置禁用状态 | | `on_click(fn)` | 点击回调,参数为新的 `&bool` 选中状态 | | `tab_stop(bool)` | 是否允许通过 Tab 聚焦,默认 `true` | | `tab_index(isize)` | 设置 Tab 顺序,默认 `0` | ### RadioGroup | 方法 | 说明 | | --- | --- | | `horizontal(id)` | 创建横向分组 | | `vertical(id)` | 创建纵向分组 | | `layout(Axis)` | 设置布局方向 | | `child(Radio)` | 添加单个 Radio | | `children(items)` | 通过迭代器批量添加 Radio | | `selected_index(Option)` | 设置选中项索引 | | `disabled(bool)` | 禁用分组内所有 Radio | | `on_change(fn)` | 选择变化回调,参数为选中的 `&usize` 索引 | ### 样式 Radio 和 RadioGroup 都实现了 `Styled` trait。 Radio 还实现了 `Sizable` trait: * `xsmall()`:超小尺寸 * `small()`:小尺寸 * `medium()`:中尺寸,默认值 * `large()`:大尺寸 ## 最佳实践 1. 互斥选项优先使用 `RadioGroup`,不要手动管理一组独立的 `Radio`。 2. 标签要明确,用户应当一眼看懂每个选项的含义。 3. 对必填项可以提供合理的默认选中项。 4. 选项顺序应符合业务逻辑,例如频率、重要性或字母顺序。 5. 单选项数量应保持适中,通常建议 2 到 7 个。 6. 多组单选项应配合清晰标题和视觉分组。 7. 选项较少时可横向排列,较多时更适合纵向排列。 --- --- url: /base/primitives/radio-group.md description: Groups radio items and provides keyboard navigation for a single selection. --- # Radio Group Groups radio items and provides keyboard navigation for a single selection. Like every `gpui-base` primitive, Radio Group supplies behavior and semantic structure without imposing a product visual language. Apply GPUI styles and compose the exported parts to match your design system. ## Example The [single native Cargo entrypoint](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/components.rs) selects this primitive from the [shared showcase implementation](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/mod.rs). The same showcase is compiled once for the WASM preview above. ```bash cargo run -p gpui-base --example components -- radio-group ``` ## Import ```rust use gpui_kit::base::{Radio, RadioGroup}; ``` ## Anatomy and API The example composes `Radio`, `RadioGroup`. GPUI's standard styling and event traits provide presentation; these base types provide the interaction structure. The authoritative module is [`components/radio_group.rs`](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/components/radio_group.rs). Native and browser previews compile this same file. ## State and events The group coordinates one selected value and keyboard movement; `Radio` renders each option. Keep controlled state on the parent render type or in a GPUI entity. Update it in callbacks and call `cx.notify()`; do not recreate persistent entities during every render. ## Complete Rust example The complete implementation used by the runnable showcase is embedded directly from Rust source: The command above supplies application initialization, window creation, and shared `BaseShowcase` state. ## Accessibility Label the group, expose one checked item, and support arrow keys among enabled choices. ## Notes Use stable element IDs where accepted. Verify focus, hover, active, selected, disabled, reduced-motion, and high-contrast appearances in the consuming design system. --- --- url: /zh-CN/base/primitives/radio-group.md description: 将单选项分组,并为单项选择提供键盘导航。 --- # Radio Group 将单选项分组,并为单项选择提供键盘导航。 和所有 GPUI Base 原语一样,Radio Group 只提供行为和语义结构,不规定产品视觉语言。请使用 GPUI 样式并组合导出的部件,使其符合你的设计系统。 ## 示例 原生示例和页面上方的 WASM 预览共用同一份实现: ```bash cargo run -p gpui-base --example components -- radio-group ``` ## 导入 ```rust use gpui_kit::base::{Radio, RadioGroup}; ``` ## 结构与 API 示例组合上述公开类型。GPUI 的标准样式和事件 trait 负责表现,Base 类型负责交互结构。权威实现位于 [`components/radio-group.rs`](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/components/radio-group.rs),原生与浏览器预览编译的是同一文件。 ## 状态与事件 组持有唯一选中值,并协调各项的焦点与选择。 受控状态应保存在父渲染类型或 GPUI entity 中;在回调中更新并调用 `cx.notify()`,不要在每次渲染时重建持久 entity。 ## 完整 Rust 示例 ## 可访问性 提供组标签,保留方向键导航以及各项的选中、禁用语义。 ## 注意事项 在支持的位置使用稳定元素 ID,并在消费端设计系统中验证焦点、悬停、按下、选中、禁用、减少动态效果和高对比度状态。 --- --- url: /docs/components/rating.md description: A simple interactive star rating component. --- # Rating A star rating component that allows users to select a rating value. Supports different sizes, custom colors, disabled state, and click handlers. ## Import ```rust use gpui_kit::component::rating::Rating; ``` ## Usage ### Basic Rating ```rust Rating::new("my-rating") .value(3) .max(5) .on_click(|value, _, _| { println!("Rating changed to: {}", value); }) ``` ### Controlled Rating ```rust struct MyView { rating: usize, } impl Render for MyView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { Rating::new("rating") .value(self.rating) .max(5) .on_click(cx.listener(|view, value: &usize, _, cx| { view.rating = *value; cx.notify(); })) } } ``` ### Different Sizes The Rating component supports the [Sizable] trait for different sizes. ```rust Rating::new("rating").xsmall().value(3).max(5) Rating::new("rating").small().value(3).max(5) Rating::new("rating").value(3).max(5) // default (Medium) Rating::new("rating").large().value(3).max(5) ``` ### Custom Color By default, the rating uses the theme's `yellow` color. You can customize it with the `color` method. ```rust Rating::new("rating") .value(4) .max(5) .color(cx.theme().green) ``` ### Disabled State ```rust Rating::new("rating") .value(2) .max(5) .disabled(true) ``` ### Custom Maximum The default maximum is 5 stars, but you can set a different maximum value. ```rust Rating::new("rating") .value(7) .max(10) ``` ### Click Behavior The rating component has special click behavior: * Clicking on a star that's already filled will reduce the rating by 1 * Clicking on an unfilled star will set the rating to that star's value The `on_click` callback receives the new rating value as `&usize`. ```rust Rating::new("rating") .value(3) .max(5) .on_click(|new_value, _, _| { println!("New rating: {}", new_value); }) ``` ## API Reference * [Rating] ### Methods * `new(id: impl Into)` - Create a new Rating component * `with_size(size: impl Into)` - Set the star size (implements [Sizable]) * `value(value: usize)` - Set the initial rating value (0..=max) * `max(max: usize)` - Set the maximum number of stars (default: 5) * `color(color: impl Into)` - Set the active color (default: theme yellow) * `disabled(disabled: bool)` - Disable interaction (implements [Disableable]) * `on_click(handler: Fn(&usize, &mut Window, &mut App))` - Set click handler ## Examples ### Read-only Display ```rust Rating::new("rating") .value(4) .max(5) .disabled(true) ``` ### Interactive Rating with State ```rust struct ProductView { user_rating: usize, } impl Render for ProductView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .gap_3() .child( Rating::new("product-rating") .value(self.user_rating) .max(5) .on_click(cx.listener(|view, value: &usize, _, cx| { view.user_rating = *value; // Save rating to backend, etc. cx.notify(); })) ) .child(format!("Your rating: {}/5", self.user_rating)) } } ``` ### Large Rating with Custom Color ```rust Rating::new("rating") .large() .value(5) .max(5) .color(cx.theme().orange) ``` [Rating]: https://docs.rs/gpui-component/latest/gpui_component/rating/struct.Rating.html [Sizable]: https://docs.rs/gpui-component/latest/gpui_component/trait.Sizable.html [Disableable]: https://docs.rs/gpui-component/latest/gpui_component/trait.Disableable.html --- --- url: /zh-CN/docs/components/rating.md description: 简单的交互式星级评分组件。 --- # Rating Rating 是一个星级评分组件,允许用户选择评分值。它支持不同尺寸、自定义颜色、禁用状态以及点击事件处理。 ## 导入 ```rust use gpui_kit::component::rating::Rating; ``` ## 用法 ### 基础评分 ```rust Rating::new("my-rating") .value(3) .max(5) .on_click(|value, _, _| { println!("Rating changed to: {}", value); }) ``` ### 受控评分 ```rust struct MyView { rating: usize, } impl Render for MyView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { Rating::new("rating") .value(self.rating) .max(5) .on_click(cx.listener(|view, value: &usize, _, cx| { view.rating = *value; cx.notify(); })) } } ``` ### 不同尺寸 Rating 实现了 [Sizable] trait: ```rust Rating::new("rating").xsmall().value(3).max(5) Rating::new("rating").small().value(3).max(5) Rating::new("rating").value(3).max(5) Rating::new("rating").large().value(3).max(5) ``` ### 自定义颜色 默认使用主题中的 `yellow` 颜色。你也可以通过 `color` 方法覆盖: ```rust Rating::new("rating") .value(4) .max(5) .color(cx.theme().green) ``` ### 禁用状态 ```rust Rating::new("rating") .value(2) .max(5) .disabled(true) ``` ### 自定义最大值 默认最大值为 5 星,也可以设置为任意数量: ```rust Rating::new("rating") .value(7) .max(10) ``` ### 点击行为 Rating 的点击行为有两个规则: * 点击已点亮的星星,会将评分减少 1。 * 点击未点亮的星星,会将评分设置为该星星对应的值。 `on_click` 回调接收到的新值类型为 `&usize`。 ```rust Rating::new("rating") .value(3) .max(5) .on_click(|new_value, _, _| { println!("New rating: {}", new_value); }) ``` ## API 参考 * [Rating] ### 方法 * `new(id: impl Into)`:创建新的 Rating 组件。 * `with_size(size: impl Into)`:设置星星尺寸,支持 [Sizable]。 * `value(value: usize)`:设置当前评分值,范围 `0..=max`。 * `max(max: usize)`:设置最大星数,默认值为 5。 * `color(color: impl Into)`:设置激活颜色,默认使用主题黄色。 * `disabled(disabled: bool)`:禁用交互,支持 [Disableable]。 * `on_click(handler: Fn(&usize, &mut Window, &mut App))`:设置点击处理函数。 ## 示例 ### 只读展示 ```rust Rating::new("rating") .value(4) .max(5) .disabled(true) ``` ### 带状态的交互评分 ```rust struct ProductView { user_rating: usize, } impl Render for ProductView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .gap_3() .child( Rating::new("product-rating") .value(self.user_rating) .max(5) .on_click(cx.listener(|view, value: &usize, _, cx| { view.user_rating = *value; cx.notify(); })) ) .child(format!("Your rating: {}/5", self.user_rating)) } } ``` ### 自定义颜色的大尺寸评分 ```rust Rating::new("rating") .large() .value(5) .max(5) .color(cx.theme().orange) ``` [Rating]: https://docs.rs/gpui-component/latest/gpui_component/rating/struct.Rating.html [Sizable]: https://docs.rs/gpui-component/latest/gpui_component/trait.Sizable.html [Disableable]: https://docs.rs/gpui-component/latest/gpui_component/trait.Disableable.html --- --- url: /releases.md description: 'Every published version of GPUI Kit, with the notes from its GitHub release.' --- --- --- url: /base/primitives/resizable.md description: Panel groups and resize handles for user-adjustable split layouts. --- # Resizable Panel groups and resize handles for user-adjustable split layouts. Like every `gpui-base` primitive, Resizable supplies behavior and semantic structure without imposing a product visual language. Apply GPUI styles and compose the exported parts to match your design system. ## Example The [single native Cargo entrypoint](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/components.rs) selects this primitive from the [shared showcase implementation](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/mod.rs). The same showcase is compiled once for the WASM preview above. ```bash cargo run -p gpui-base --example components -- resizable ``` ## Import ```rust use gpui_kit::base::{ResizablePanel, ResizablePanelGroup, ResizableState, h_resizable, resizable_panel}; ``` ## Anatomy and API The example composes `ResizablePanel`, `ResizablePanelGroup`, `ResizableState`. GPUI's standard styling and event traits provide presentation; these base types provide the interaction structure. The authoritative module is [`components/resizable.rs`](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/components/resizable.rs). Native and browser previews compile this same file. ## State and events Panel sizes live in resizable state; dragging handles updates adjacent panels subject to minimums. Keep controlled state on the parent render type or in a GPUI entity. Update it in callbacks and call `cx.notify()`; do not recreate persistent entities during every render. ## Complete Rust example The complete implementation used by the runnable showcase is embedded directly from Rust source: The command above supplies application initialization, window creation, and shared `BaseShowcase` state. ## Accessibility Provide keyboard alternatives for handles and preserve usable minimum panel sizes. ## Notes Use stable element IDs where accepted. Verify focus, hover, active, selected, disabled, reduced-motion, and high-contrast appearances in the consuming design system. --- --- url: /zh-CN/base/primitives/resizable.md description: 用于用户可调分栏布局的面板组和调整手柄。 --- # Resizable 用于用户可调分栏布局的面板组和调整手柄。 和所有 GPUI Base 原语一样,Resizable 只提供行为和语义结构,不规定产品视觉语言。请使用 GPUI 样式并组合导出的部件,使其符合你的设计系统。 ## 示例 原生示例和页面上方的 WASM 预览共用同一份实现: ```bash cargo run -p gpui-base --example components -- resizable ``` ## 导入 ```rust use gpui_kit::base::{ResizablePanel, ResizablePanelGroup, ResizableState, h_resizable, resizable_panel}; ``` ## 结构与 API 示例组合上述公开类型。GPUI 的标准样式和事件 trait 负责表现,Base 类型负责交互结构。权威实现位于 [`components/resizable.rs`](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/components/resizable.rs),原生与浏览器预览编译的是同一文件。 ## 状态与事件 ResizableState 持久保存面板尺寸;拖动手柄时更新约束内的比例。 受控状态应保存在父渲染类型或 GPUI entity 中;在回调中更新并调用 `cx.notify()`,不要在每次渲染时重建持久 entity。 ## 完整 Rust 示例 ## 可访问性 手柄应可聚焦、可由键盘调整,并提供方向和当前尺寸信息。 ## 注意事项 在支持的位置使用稳定元素 ID,并在消费端设计系统中验证焦点、悬停、按下、选中、禁用、减少动态效果和高对比度状态。 --- --- url: /docs/components/resizable.md description: >- A flexible panel layout system with draggable resize handles and adjustable panels. --- # Resizable The resizable component system provides a flexible way to create layouts with resizable panels. It supports both horizontal and vertical resizing, nested layouts, size constraints, and drag handles. Perfect for creating paned interfaces, split views, and adjustable dashboards. ## Import ```rust use gpui_kit::component::resizable::{ h_resizable, v_resizable, resizable_panel, ResizablePanelGroup, ResizablePanel, ResizableState, ResizablePanelEvent }; ``` ## Usage Use `h_resizable` to create a horizontal layout, `v_resizable` to create a vertical layout. The first argument is the `id` for this \[ResizablePanelGroup]. :::tip In GPUI, the `id` must be unique within the layout scope (The nearest parent has presents `id`). ::: ```rust h_resizable("my-layout") .on_resize(|state, window, cx| { // Handle resize event // You can read the panel sizes from the state. let state = state.read(cx); let sizes = state.sizes(); }) .child( // Use resizable_panel() to create a sized panel. resizable_panel() .size(px(200.)) .child("Left Panel") ) .child( // Or you can just add AnyElement without a size. div() .child("Right Panel") .into_any_element() ) ``` The `v_resizable` component is used to create a vertical layout. ```rust v_resizable("vertical-layout") .child( resizable_panel() .size(px(100.)) .child("Top Panel") ) .child( div() .child("Bottom Panel") .into_any_element() ) ``` ### Panel Size Constraints ```rust resizable_panel() .size(px(200.)) // Initial size .size_range(px(150.)..px(400.)) // Min and max size .child("Constrained Panel") ``` ### Multiple Panels ```rust h_resizable("multi-panel", state) .child( resizable_panel() .size(px(200.)) .size_range(px(150.)..px(300.)) .child("Left Panel") ) .child( resizable_panel() .child("Center Panel") ) .child( resizable_panel() .size(px(250.)) .child("Right Panel") ) ``` ### Nested Layouts ```rust v_resizable("main-layout", window, cx) .child( resizable_panel() .size(px(300.)) .child( h_resizable("nested-layout", window, cx) .child( resizable_panel() .size(px(200.)) .child("Top Left") ) .child( resizable_panel() .child("Top Right") ) ) ) .child( resizable_panel() .child("Bottom Panel") ) ``` ### Nested Panel Groups ```rust h_resizable("outer", window, cx) .child( resizable_panel() .size(px(200.)) .child("Left Panel") ) .group( v_resizable("inner", window, cx) .child( resizable_panel() .size(px(150.)) .child("Top Right") ) .child( resizable_panel() .child("Bottom Right") ) ) ``` ### Conditional Panel Visibility ```rust resizable_panel() .visible(self.show_sidebar) .size(px(250.)) .child("Sidebar Content") ``` ### Panel with Size Limits ```rust // Panel with minimum size only resizable_panel() .size_range(px(100.)..Pixels::MAX) .child("Flexible Panel") // Panel with both min and max resizable_panel() .size_range(px(200.)..px(500.)) .child("Constrained Panel") // Panel with exact constraints resizable_panel() .size(px(300.)) .size_range(px(300.)..px(300.)) // Fixed size .child("Fixed Panel") ``` ## Examples ### File Explorer Layout ```rust struct FileExplorer { show_sidebar: bool, } impl Render for FileExplorer { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { h_resizable("file-explorer", window, cx) .child( resizable_panel() .visible(self.show_sidebar) .size(px(250.)) .size_range(px(200.)..px(400.)) .child( v_flex() .p_4() .child("📁 Folders") .child("• Documents") .child("• Pictures") .child("• Downloads") ) ) .child( v_flex() .p_4() .child("📄 Files") .child("file1.txt") .child("file2.pdf") .child("image.png") .into_any_element() ) } } ``` ### IDE Layout ```rust struct IDELayout { main_state: Entity, sidebar_state: Entity, bottom_state: Entity, } impl Render for IDELayout { fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { h_resizable("ide-main", self.main_state.clone()) .child( resizable_panel() .size(px(300.)) .size_range(px(200.)..px(500.)) .child( v_resizable("sidebar", self.sidebar_state.clone()) .child( resizable_panel() .size(px(200.)) .child("File Explorer") ) .child( resizable_panel() .child("Outline") ) ) ) .child( resizable_panel() .child( v_resizable("editor-area", self.bottom_state.clone()) .child( resizable_panel() .child("Code Editor") ) .child( resizable_panel() .size(px(150.)) .size_range(px(100.)..px(300.)) .child("Terminal / Output") ) ) ) } } ``` ### Dashboard with Widgets ```rust struct Dashboard { layout_state: Entity, widget_state: Entity, } impl Render for Dashboard { fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { v_resizable("dashboard", self.layout_state.clone()) .child( resizable_panel() .size(px(120.)) .child("Header / Navigation") ) .child( resizable_panel() .child( h_resizable("widgets", self.widget_state.clone()) .child( resizable_panel() .size(px(300.)) .child("Chart Widget") ) .child( resizable_panel() .child("Data Table") ) .child( resizable_panel() .size(px(250.)) .child("Stats Panel") ) ) ) .child( resizable_panel() .size(px(60.)) .child("Footer") ) } } ``` ### Settings Panel ```rust struct SettingsPanel { settings_state: Entity, } impl SettingsPanel { fn new(cx: &mut Context) -> Self { let settings_state = ResizableState::new(cx); // Listen for resize events to save layout preferences cx.subscribe(&settings_state, |this, _, event: &ResizablePanelEvent, cx| { match event { ResizablePanelEvent::Resized => { this.save_layout_preferences(cx); } } }); Self { settings_state } } fn save_layout_preferences(&self, cx: &mut Context) { let sizes = self.settings_state.read(cx).sizes(); // Save to preferences println!("Saving layout: {:?}", sizes); } } impl Render for SettingsPanel { fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { h_resizable("settings", self.settings_state.clone()) .child( resizable_panel() .size(px(200.)) .size_range(px(150.)..px(300.)) .child( v_flex() .gap_2() .p_4() .child("Categories") .child("• General") .child("• Appearance") .child("• Advanced") ) ) .child( resizable_panel() .child( div() .p_6() .child("Settings Content Area") ) ) } } ``` ## Best Practices 1. **State Management**: Use separate ResizableState for independent layouts 2. **Size Constraints**: Always set reasonable min/max sizes for panels 3. **Event Handling**: Subscribe to ResizablePanelEvent for layout persistence 4. **Nested Layouts**: Use `.group()` method for clean nested structures 5. **Performance**: Avoid excessive nesting for better performance 6. **User Experience**: Provide adequate handle padding for easier interaction --- --- url: /zh-CN/docs/components/resizable.md description: 具有拖拽分隔条与尺寸约束的可调整面板布局系统。 --- # Resizable Resizable 组件系统用于构建可拖拽调整大小的面板布局,支持横向与纵向分割、嵌套布局、尺寸限制和拖拽句柄,适合实现 IDE、仪表盘或分栏视图。 ## 导入 ```rust use gpui_kit::component::resizable::{ h_resizable, v_resizable, resizable_panel, ResizablePanelGroup, ResizablePanel, ResizableState, ResizablePanelEvent }; ``` ## 用法 使用 `h_resizable` 创建横向布局,使用 `v_resizable` 创建纵向布局。 第一个参数是 \[ResizablePanelGroup] 的 `id`。 :::tip 在 GPUI 中,`id` 在当前布局作用域内必须唯一。 ::: ```rust h_resizable("my-layout") .on_resize(|state, window, cx| { let state = state.read(cx); let sizes = state.sizes(); }) .child( resizable_panel() .size(px(200.)) .child("Left Panel") ) .child( div() .child("Right Panel") .into_any_element() ) ``` 纵向布局写法类似: ```rust v_resizable("vertical-layout") .child( resizable_panel() .size(px(100.)) .child("Top Panel") ) .child( div() .child("Bottom Panel") .into_any_element() ) ``` ### 面板尺寸约束 ```rust resizable_panel() .size(px(200.)) .size_range(px(150.)..px(400.)) .child("Constrained Panel") ``` ### 多面板布局 ```rust h_resizable("multi-panel", state) .child( resizable_panel() .size(px(200.)) .size_range(px(150.)..px(300.)) .child("Left Panel") ) .child( resizable_panel() .child("Center Panel") ) .child( resizable_panel() .size(px(250.)) .child("Right Panel") ) ``` ### 嵌套布局 ```rust v_resizable("main-layout", window, cx) .child( resizable_panel() .size(px(300.)) .child( h_resizable("nested-layout", window, cx) .child( resizable_panel() .size(px(200.)) .child("Top Left") ) .child( resizable_panel() .child("Top Right") ) ) ) .child( resizable_panel() .child("Bottom Panel") ) ``` ### 嵌套面板组 ```rust h_resizable("outer", window, cx) .child( resizable_panel() .size(px(200.)) .child("Left Panel") ) .group( v_resizable("inner", window, cx) .child( resizable_panel() .size(px(150.)) .child("Top Right") ) .child( resizable_panel() .child("Bottom Right") ) ) ``` ### 条件显示面板 ```rust resizable_panel() .visible(self.show_sidebar) .size(px(250.)) .child("Sidebar Content") ``` ### 带尺寸上下限的面板 ```rust resizable_panel() .size_range(px(100.)..Pixels::MAX) .child("Flexible Panel") resizable_panel() .size_range(px(200.)..px(500.)) .child("Constrained Panel") resizable_panel() .size(px(300.)) .size_range(px(300.)..px(300.)) .child("Fixed Panel") ``` ## 示例 ### 文件浏览器布局 ```rust struct FileExplorer { show_sidebar: bool, } impl Render for FileExplorer { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { h_resizable("file-explorer", window, cx) .child( resizable_panel() .visible(self.show_sidebar) .size(px(250.)) .size_range(px(200.)..px(400.)) .child( v_flex() .p_4() .child("📁 Folders") .child("• Documents") .child("• Pictures") .child("• Downloads") ) ) .child( v_flex() .p_4() .child("📄 Files") .child("file1.txt") .child("file2.pdf") .child("image.png") .into_any_element() ) } } ``` ### IDE 布局 ```rust struct IDELayout { main_state: Entity, sidebar_state: Entity, bottom_state: Entity, } impl Render for IDELayout { fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { h_resizable("ide-main", self.main_state.clone()) .child( resizable_panel() .size(px(300.)) .size_range(px(200.)..px(500.)) .child( v_resizable("sidebar", self.sidebar_state.clone()) .child( resizable_panel() .size(px(200.)) .child("File Explorer") ) .child( resizable_panel() .child("Outline") ) ) ) .child( resizable_panel() .child( v_resizable("editor-area", self.bottom_state.clone()) .child( resizable_panel() .child("Code Editor") ) .child( resizable_panel() .size(px(150.)) .size_range(px(100.)..px(300.)) .child("Terminal / Output") ) ) ) } } ``` ### 仪表盘布局 ```rust struct Dashboard { layout_state: Entity, widget_state: Entity, } impl Render for Dashboard { fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { v_resizable("dashboard", self.layout_state.clone()) .child( resizable_panel() .size(px(120.)) .child("Header / Navigation") ) .child( resizable_panel() .child( h_resizable("widgets", self.widget_state.clone()) .child( resizable_panel() .size(px(300.)) .child("Chart Widget") ) .child( resizable_panel() .child("Data Table") ) .child( resizable_panel() .size(px(250.)) .child("Stats Panel") ) ) ) .child( resizable_panel() .size(px(60.)) .child("Footer") ) } } ``` ### 设置面板 ```rust struct SettingsPanel { settings_state: Entity, } impl SettingsPanel { fn new(cx: &mut Context) -> Self { let settings_state = ResizableState::new(cx); cx.subscribe(&settings_state, |this, _, event: &ResizablePanelEvent, cx| { match event { ResizablePanelEvent::Resized => { this.save_layout_preferences(cx); } } }); Self { settings_state } } fn save_layout_preferences(&self, cx: &mut Context) { let sizes = self.settings_state.read(cx).sizes(); println!("Saving layout: {:?}", sizes); } } impl Render for SettingsPanel { fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { h_resizable("settings", self.settings_state.clone()) .child( resizable_panel() .size(px(200.)) .size_range(px(150.)..px(300.)) .child( v_flex() .gap_2() .p_4() .child("Categories") .child("• General") .child("• Appearance") .child("• Advanced") ) ) .child( resizable_panel() .child( div() .p_6() .child("Settings Content Area") ) ) } } ``` ## 最佳实践 1. 为独立布局使用各自的 `ResizableState`。 2. 始终为面板设置合理的最小和最大尺寸。 3. 通过订阅 `ResizablePanelEvent` 持久化用户布局。 4. 使用 `.group()` 构建清晰的嵌套结构。 5. 避免无意义的深层嵌套以减少复杂度。 6. 为拖拽句柄保留足够交互空间,提升体验。 --- --- url: /docs/root.md --- # Root View The [Root] component for as the root provider of GPUI Component features in a window. We must to use [Root] as the **first level child** of a window to enable GPUI Component features. This is important, if we don't use [Root] as the first level child of a window, there will have some unexpected behaviors. ```rs fn main() { gpui_kit::application().run(move |cx| { // This must be called before using any GPUI Component features. gpui_kit::init(cx); cx.spawn(async move |cx| { cx.open_window(WindowOptions::default(), |window, cx| { let view = cx.new(|_| Example); // This first level on the window, should be a Root. cx.new(|cx| Root::new(view, window, cx)) }) .expect("Failed to open window"); }) .detach(); }); } ``` ## Window Border By default, [Root] renders GPUI Component's client-side window border wrapper. For layer-shell fullscreen windows or other surfaces that should not render this wrapper, disable it with `bordered(false)`: ```rs cx.new(|cx| Root::new(view, window, cx).bordered(false)) ``` ## Overlays We have dialogs, sheets, notifications, we need placement for them to show, so [Root] provides methods to render these overlays: * [Root::render\_dialog\_layer](https://docs.rs/gpui-component/latest/gpui_component/struct.Root.html#method.render_dialog_layer) - Render the current opened modals. * [Root::render\_sheet\_layer](https://docs.rs/gpui-component/latest/gpui_component/struct.Root.html#method.render_sheet_layer) - Render the current opened drawers. * [Root::render\_notification\_layer](https://docs.rs/gpui-component/latest/gpui_component/struct.Root.html#method.render_notification_layer) - Render the notification list. We can put these layers in the `render` method your first level view (Root > YourFirstView): ```rs struct MyApp; impl Render for MyApp { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { div() .size_full() .child("My App Content") .children(Root::render_dialog_layer(cx)) .children(Root::render_sheet_layer(cx)) .children(Root::render_notification_layer(cx)) } } ``` :::tip Here the example we used `children` method, it because if there is no opened dialogs, sheets, notifications, these methods will return `None`, so GPUI will not render anything. ::: [Root]: https://docs.rs/gpui-component/latest/gpui_component/root/struct.Root.html --- --- url: /zh-CN/docs/root.md --- # Root View [Root] 组件是 GPUI Component 在窗口中的根提供者。要启用 GPUI Component 的功能,必须把 [Root] 作为窗口中的 **第一层子节点**。 这一点很重要。如果不把 [Root] 放在窗口的第一层,许多行为都会出现异常或不符合预期。 ```rs fn main() { gpui_kit::application().run(move |cx| { // This must be called before using any GPUI Component features. gpui_kit::init(cx); cx.spawn(async move |cx| { cx.open_window(WindowOptions::default(), |window, cx| { let view = cx.new(|_| Example); // This first level on the window, should be a Root. cx.new(|cx| Root::new(view, window, cx)) }) .expect("Failed to open window"); }) .detach(); }); } ``` ## 窗口边框 默认情况下,[Root] 会渲染 GPUI Component 的客户端窗口边框包装层。`layer-shell` 全屏窗口等场景不应渲染这层边框,可以使用 `bordered(false)` 关闭: ```rs cx.new(|cx| Root::new(view, window, cx).bordered(false)) ``` ## 浮层 对话框、抽屉、通知等 UI 都需要一个统一的展示层,[Root] 提供了这些浮层的渲染入口: * [Root::render\_dialog\_layer](https://docs.rs/gpui-component/latest/gpui_component/struct.Root.html#method.render_dialog_layer) - 渲染当前打开的对话框 * [Root::render\_sheet\_layer](https://docs.rs/gpui-component/latest/gpui_component/struct.Root.html#method.render_sheet_layer) - 渲染当前打开的抽屉 * [Root::render\_notification\_layer](https://docs.rs/gpui-component/latest/gpui_component/struct.Root.html#method.render_notification_layer) - 渲染通知列表 可以在你的第一层视图中这样放置这些图层(Root > YourFirstView): ```rs struct MyApp; impl Render for MyApp { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { div() .size_full() .child("My App Content") .children(Root::render_dialog_layer(cx)) .children(Root::render_sheet_layer(cx)) .children(Root::render_notification_layer(cx)) } } ``` :::tip 这里使用的是 `children` 而不是 `child`,因为当没有打开的 dialog、sheet 或 notification 时,这些方法会返回 `None`,GPUI 就不会渲染任何内容。 ::: [Root]: https://docs.rs/gpui-component/latest/gpui_component/root/struct.Root.html --- --- url: /docs/components/scrollable.md description: >- Scrollable container with custom scrollbars, scroll tracking, and virtualization support. --- # Scrollable A comprehensive scrollable container component that provides custom scrollbars, scroll tracking, and virtualization capabilities. Supports both vertical and horizontal scrolling with customizable appearance and behavior. ## Import ```rust use gpui_kit::component::{ scroll::{ScrollableElement, ScrollbarAxis, ScrollbarMode}, StyledExt as _, }; ``` ## Usage ### Basic Scrollable Container The simplest way to make any element scrollable is using the `overflow_scrollbar()` method from `ScrollableElement` trait. This method is almost like the `overflow_scroll()` method, but it adds scrollbars. * `overflow_scrollbar()` - Adds scrollbars for both axes as needed. * `overflow_x_scrollbar()` - Adds horizontal scrollbar as needed. * `overflow_y_scrollbar()` - Adds vertical scrollbar as needed. ```rust use gpui_kit::{div, Axis}; use gpui_kit::component::ScrollableElement; div() .id("scrollable-container") .size_full() .child("Your content here") .overflow_scrollbar() ``` ### Vertical Scrolling ```rust v_flex() .id("scrollable-container") .overflow_y_scrollbar() .gap_2() .p_4() .child("Scrollable Content") .children((0..100).map(|i| { div() .h(px(40.)) .w_full() .bg(cx.theme().secondary) .child(format!("Item {}", i)) })) ``` ### Horizontal Scrolling ```rust h_flex() .id("scrollable-container") .overflow_x_scrollbar() .gap_2() .p_4() .children((0..50).map(|i| { div() .min_w(px(120.)) .h(px(80.)) .bg(cx.theme().accent) .child(format!("Card {}", i)) })) ``` ### Both Directions ```rust div() .id("scrollable-container") .size_full() .overflow_scrollbar() .child( div() .w(px(2000.)) // Wide content .h(px(2000.)) // Tall content .bg(cx.theme().background) .child("Large content area") ) ``` ## Custom Scrollbars ### Manual Scrollbar Creation For more control, you can create scrollbars manually: ```rust use gpui_kit::component::scroll::{ScrollableElement}; pub struct ScrollableView { scroll_handle: ScrollHandle, } impl Render for ScrollableView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { div() .relative() .size_full() .child( div() .id("content") .track_scroll(&self.scroll_handle) .overflow_scroll() .size_full() .child("Your scrollable content") ) .vertical_scrollbar(&self.scroll_handle) } } ``` ## Virtualization ### VirtualList for Large Datasets For rendering large lists efficiently, use `VirtualList`: ```rust use gpui_kit::component::{VirtualList, VirtualListScrollHandle}; pub struct LargeListView { items: Vec, scroll_handle: VirtualListScrollHandle, } impl Render for LargeListView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let item_count = self.items.len(); VirtualList::new( self.scroll_handle.clone(), item_count, |ix, window, cx| { // Item sizes - can be different for each item size(px(300.), px(40.)) }, |ix, bounds, selected, window, cx| { // Render each item div() .size(bounds.size) .bg(if selected { cx.theme().accent } else { cx.theme().background }) .child(format!("Item {}: {}", ix, self.items[ix])) .into_any_element() }, ) } } ``` ### Scrolling to Specific Items ```rust impl LargeListView { fn scroll_to_item(&mut self, index: usize) { self.scroll_handle.scroll_to_item(index, ScrollStrategy::Top); } fn scroll_to_item_centered(&mut self, index: usize) { self.scroll_handle.scroll_to_item(index, ScrollStrategy::Center); } } ``` ### Variable Item Sizes ```rust VirtualList::new( scroll_handle, items.len(), |ix, window, cx| { // Different heights based on content let height = if items[ix].len() > 50 { px(80.) // Tall items for long content } else { px(40.) // Normal height }; size(px(300.), height) }, |ix, bounds, selected, window, cx| { // Render logic }, ) ``` ## Theme Customization ### Scrollbar Appearance Customize scrollbar appearance through theme configuration: ```rust // In your theme JSON { "scrollbar.background": "#ffffff20", "scrollbar.thumb.background": "#00000060", "scrollbar.thumb.hover.background": "#000000" } ``` ### Scrollbar Show Modes Control when scrollbars are visible: ```rust use gpui_kit::component::{Theme, scroll::ScrollbarMode}; // In theme initialization Theme::set_scrollbar_mode(ScrollbarMode::Scrolling, cx); // Only while scrolling Theme::set_scrollbar_mode(ScrollbarMode::Hover, cx); // On hover Theme::set_scrollbar_mode(ScrollbarMode::Always, cx); // Always visible ``` ### System Integration Sync scrollbar behavior with system preferences: ```rust // Automatically sync with system settings Theme::sync_scrollbar_appearance(cx); ``` ## Examples ### File Browser with Scrolling ```rust pub struct FileBrowser { files: Vec, } impl Render for FileBrowser { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { div() .border_1() .border_color(cx.theme().border) .size_full() .child( v_flex() .gap_1() .p_2() .overflow_y_scrollbar() .children(self.files.iter().map(|file| { div() .h(px(32.)) .w_full() .px_2() .flex() .items_center() .hover(|style| style.bg(cx.theme().secondary_hover)) .child(file.clone()) })) ) } } ``` ### Chat Messages with Auto-scroll ```rust pub struct ChatView { messages: Vec, scroll_handle: ScrollHandle, should_auto_scroll: bool, } impl ChatView { fn add_message(&mut self, message: String) { self.messages.push(message); if self.should_auto_scroll { // Scroll to bottom for new messages let max_offset = self.scroll_handle.max_offset(); self.scroll_handle.set_offset(point(px(0.), max_offset.y)); } } } ``` ### Data Table with Virtual Scrolling ```rust pub struct DataTable { data: Vec>, scroll_handle: VirtualListScrollHandle, } impl Render for DataTable { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { VirtualList::new( self.scroll_handle.clone(), self.data.len(), |_ix, _window, _cx| size(px(800.), px(32.)), // Fixed row height |ix, bounds, _selected, _window, cx| { h_flex() .size(bounds.size) .border_b_1() .border_color(cx.theme().border) .children(self.data[ix].iter().map(|cell| { div() .flex_1() .px_2() .flex() .items_center() .child(cell.clone()) })) .into_any_element() }, ) } } ``` --- --- url: /zh-CN/docs/components/scrollable.md description: 支持自定义滚动条、滚动跟踪与虚拟列表的可滚动容器。 --- # Scrollable Scrollable 是一个功能完整的可滚动容器组件,支持自定义滚动条、滚动位置跟踪以及虚拟化渲染。它同时支持纵向和横向滚动,并可按需定制显示行为。 ## 导入 ```rust use gpui_kit::component::{ scroll::{ScrollableElement, ScrollbarAxis, ScrollbarMode}, StyledExt as _, }; ``` ## 用法 ### 基础可滚动容器 让任意元素具备滚动能力的最简单方式,是使用 `ScrollableElement` trait 提供的 `overflow_scrollbar()`: * `overflow_scrollbar()`:按需为两个方向都添加滚动条。 * `overflow_x_scrollbar()`:按需添加横向滚动条。 * `overflow_y_scrollbar()`:按需添加纵向滚动条。 ```rust use gpui_kit::{div, Axis}; use gpui_kit::component::ScrollableElement; div() .id("scrollable-container") .size_full() .child("Your content here") .overflow_scrollbar() ``` ### 纵向滚动 ```rust v_flex() .id("scrollable-container") .overflow_y_scrollbar() .gap_2() .p_4() .child("Scrollable Content") .children((0..100).map(|i| { div() .h(px(40.)) .w_full() .bg(cx.theme().secondary) .child(format!("Item {}", i)) })) ``` ### 横向滚动 ```rust h_flex() .id("scrollable-container") .overflow_x_scrollbar() .gap_2() .p_4() .children((0..50).map(|i| { div() .min_w(px(120.)) .h(px(80.)) .bg(cx.theme().accent) .child(format!("Card {}", i)) })) ``` ### 双向滚动 ```rust div() .id("scrollable-container") .size_full() .overflow_scrollbar() .child( div() .w(px(2000.)) .h(px(2000.)) .bg(cx.theme().background) .child("Large content area") ) ``` ## 自定义滚动条 ### 手动创建滚动条 如果你需要更高的控制粒度,可以手动创建滚动条: ```rust use gpui_kit::component::scroll::{ScrollableElement}; pub struct ScrollableView { scroll_handle: ScrollHandle, } impl Render for ScrollableView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { div() .relative() .size_full() .child( div() .id("content") .track_scroll(&self.scroll_handle) .overflow_scroll() .size_full() .child("Your scrollable content") ) .vertical_scrollbar(&self.scroll_handle) } } ``` ## 虚拟化 ### 使用 VirtualList 处理大数据集 渲染超长列表时,推荐使用 `VirtualList`: ```rust use gpui_kit::component::{VirtualList, VirtualListScrollHandle}; pub struct LargeListView { items: Vec, scroll_handle: VirtualListScrollHandle, } impl Render for LargeListView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let item_count = self.items.len(); VirtualList::new( self.scroll_handle.clone(), item_count, |ix, window, cx| { size(px(300.), px(40.)) }, |ix, bounds, selected, window, cx| { div() .size(bounds.size) .bg(if selected { cx.theme().accent } else { cx.theme().background }) .child(format!("Item {}: {}", ix, self.items[ix])) .into_any_element() }, ) } } ``` ### 滚动到指定项 ```rust impl LargeListView { fn scroll_to_item(&mut self, index: usize) { self.scroll_handle.scroll_to_item(index, ScrollStrategy::Top); } fn scroll_to_item_centered(&mut self, index: usize) { self.scroll_handle.scroll_to_item(index, ScrollStrategy::Center); } } ``` ### 可变高度项 ```rust VirtualList::new( scroll_handle, items.len(), |ix, window, cx| { let height = if items[ix].len() > 50 { px(80.) } else { px(40.) }; size(px(300.), height) }, |ix, bounds, selected, window, cx| { // Render logic }, ) ``` ## 主题定制 ### 滚动条外观 可以通过主题配置自定义滚动条样式: ```rust // In your theme JSON { "scrollbar.background": "#ffffff20", "scrollbar.thumb.background": "#00000060", "scrollbar.thumb.hover.background": "#000000" } ``` ### 滚动条显示模式 控制滚动条何时显示: ```rust use gpui_kit::component::{Theme, scroll::ScrollbarMode}; Theme::set_scrollbar_mode(ScrollbarMode::Scrolling, cx); Theme::set_scrollbar_mode(ScrollbarMode::Hover, cx); Theme::set_scrollbar_mode(ScrollbarMode::Always, cx); ``` ### 跟随系统设置 ```rust Theme::sync_scrollbar_appearance(cx); ``` ## 示例 ### 带滚动的文件浏览器 ```rust pub struct FileBrowser { files: Vec, } impl Render for FileBrowser { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { div() .border_1() .border_color(cx.theme().border) .size_full() .child( v_flex() .gap_1() .p_2() .overflow_y_scrollbar() .children(self.files.iter().map(|file| { div() .h(px(32.)) .w_full() .px_2() .flex() .items_center() .hover(|style| style.bg(cx.theme().secondary_hover)) .child(file.clone()) })) ) } } ``` ### 自动滚动到底部的聊天列表 ```rust pub struct ChatView { messages: Vec, scroll_handle: ScrollHandle, should_auto_scroll: bool, } impl ChatView { fn add_message(&mut self, message: String) { self.messages.push(message); if self.should_auto_scroll { let max_offset = self.scroll_handle.max_offset(); self.scroll_handle.set_offset(point(px(0.), max_offset.y)); } } } ``` ### 带虚拟滚动的数据表格 ```rust pub struct DataTable { data: Vec>, scroll_handle: VirtualListScrollHandle, } impl Render for DataTable { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { VirtualList::new( self.scroll_handle.clone(), self.data.len(), |_ix, _window, _cx| size(px(800.), px(32.)), |ix, bounds, _selected, _window, cx| { h_flex() .size(bounds.size) .border_b_1() .border_color(cx.theme().border) .children(self.data[ix].iter().map(|cell| { div() .flex_1() .px_2() .flex() .items_center() .child(cell.clone()) })) .into_any_element() }, ) } } ``` --- --- url: /base/primitives/scrollbar.md description: >- Add a styled, animated scrollbar to GPUI scroll views, lists, and custom viewports. --- # Scrollbar `Scrollbar` is a custom-painted scrollbar connected to a GPUI scroll handle. It supports vertical, horizontal, and two-axis viewports; track clicks; thumb dragging; configurable visibility modes; typed paint styles; reduced motion; and reversible visibility and width transitions. `gpui-base` owns the interaction and transition lifecycle. Your application or design-system layer owns colors, geometry, timing, and entrance choreography. ## Run the example The native showcase and WASM preview use the same implementation: ```bash cargo run -p gpui-base --example components -- scrollbar ``` The source is available in [`components/scrollbar.rs`](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/components/scrollbar.rs). ## Imports ```rust use std::time::Duration; use gpui_kit::{div, px, rgb, ScrollHandle, Styled as _}; use gpui_kit::base::{ Scrollbar, ScrollbarAxis, ScrollbarEntrance, ScrollbarMode, ScrollbarMotion, ScrollbarStyles, ScrollbarTheme, Theme, }; ``` ## Basic usage Keep the `ScrollHandle` on persistent view state. Attach it to the scrollable content with `track_scroll`, then overlay a `Scrollbar` in the same relative container. ```rust pub struct ActivityList { scroll_handle: ScrollHandle, } impl ActivityList { pub fn new() -> Self { Self { scroll_handle: ScrollHandle::new(), } } fn render_list(&self) -> impl gpui_kit::IntoElement { div() .relative() .size_full() .overflow_scroll() .track_scroll(&self.scroll_handle) .child(div().children((1..=100).map(|row| { div().h_8().px_2().child(format!("Activity {row}")) }))) .child(Scrollbar::new(&self.scroll_handle)) } } ``` `Scrollbar::new` enables both axes. Use an axis-specific constructor when the container scrolls in only one direction: ```rust Scrollbar::vertical(&scroll_handle); Scrollbar::horizontal(&scroll_handle); Scrollbar::new(&scroll_handle).axis(ScrollbarAxis::Vertical); ``` The scrollbar is an absolute overlay. Its layout and hitboxes stay fixed while the painted track and thumb animate, so entrance motion does not move content or change the interaction geometry. ## Visibility modes Set a mode on one scrollbar, or omit `.mode(...)` to use the global `ScrollbarTheme` mode. ```rust Scrollbar::vertical(&scroll_handle).mode(ScrollbarMode::Scrolling); Scrollbar::vertical(&scroll_handle).mode(ScrollbarMode::Hover); Scrollbar::vertical(&scroll_handle).mode(ScrollbarMode::Always); ``` | Mode | Behavior | | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `Scrolling` | Appears after scrolling or dragging. A visible scrollbar stays visible while hovered; leaving starts a fresh idle hold. Hover cannot reveal a fully hidden scrollbar. | | `Hover` | Appears when the pointer enters the scrollbar track. | | `Always` | Remains visible and skips visibility transitions. | All modes use a 6 px resting thumb by default. Track hover keeps that width. Thumb hover and active dragging target the 8 px active width. Width changes use the configured `expand` duration. Hidden track and thumb clicks are ignored. In `Scrolling` mode, a hidden thumb also does not retain a latent hover state that could expand it on the next scroll. ## Configure the global theme `ScrollbarTheme` uses private fields with consuming builders and readers. Set it during application initialization or when your design-system theme changes. ```rust fn install_scrollbar_theme(cx: &mut gpui_kit::App) { let styles = ScrollbarStyles::default() .track(|style| { style .width(px(16.)) .bg(rgb(0x000000).alpha(0.08)) }) .track_hover(|style| { style.bg(rgb(0x000000).alpha(0.12)) }) .track_active(|style| { style.bg(rgb(0x000000).alpha(0.16)) }) .thumb(|style| { style .width(px(6.)) .inset(px(4.)) .radius(px(3.)) .min_length(px(48.)) .bg(rgb(0x737373)) }) .thumb_hover(|style| { style.width(px(8.)).bg(rgb(0x525252)) }) .thumb_active(|style| { style.width(px(8.)).bg(rgb(0x404040)) }); let motion = ScrollbarMotion::default() .with_idle(Duration::from_secs(2)) .with_enter(Duration::from_millis(300)) .with_exit(Duration::from_millis(500)) .with_expand(Duration::from_millis(300)) .with_entrance(ScrollbarEntrance::Fade) .with_thumb_hover_entrance(ScrollbarEntrance::SlideAndFade); Theme::global_mut(cx).scrollbar = ScrollbarTheme::new() .with_mode(ScrollbarMode::Scrolling) .with_motion(motion) .with_styles(styles); } ``` The same values can be inspected without exposing the theme's fields: ```rust let scrollbar = &Theme::global(cx).scrollbar; let mode = scrollbar.mode(); let motion = scrollbar.motion(); let styles = scrollbar.styles(); ``` ## Motion behavior Base ships without product motion. `ScrollbarMotion::default()` uses a 2-second behavioral idle hold, but its `enter`, `exit`, and `expand` durations are zero. An application that does not install motion therefore gets immediate visibility and width changes. The example theme above produces this choreography: | Trigger | Entrance | | ------------------------------------- | ---------------------------------------------------------------- | | Scroll in `Scrolling` or `Hover` mode | `entrance`: fade in place | | Track hover in `Hover` mode | `entrance`: fade in place | | Thumb hover in `Hover` mode | `thumb_hover_entrance`: slide from the nearest edge while fading | | `Always` mode | Immediate; visibility motion is skipped | For `SlideAndFade`, a vertical scrollbar enters from the right and a horizontal scrollbar enters from the bottom. Opacity uses linear entrance progress; position uses cubic ease-out. Exit opacity and position use cubic ease-in. An interrupted transition samples its current opacity and position before changing direction. A zero duration adopts the target immediately, including when a transition is already running. GPUI's reduced-motion preference also sets visibility and width durations to zero. You do not need a separate reduced-motion theme. ## Per-instance styles Use `.styles(...)` to override the global styles for one scrollbar. Instance styles take precedence over theme defaults. ```rust Scrollbar::vertical(&scroll_handle).styles(|styles| { styles .track(|style| style.width(px(14.)).bg(rgb(0xf5f5f5))) .track_hover(|style| style.bg(rgb(0xe5e5e5))) .thumb(|style| { style .width(px(6.)) .inset(px(3.)) .radius(px(3.)) .min_length(px(40.)) .bg(rgb(0x737373)) }) .thumb_hover(|style| style.width(px(8.)).bg(rgb(0x525252))) .thumb_active(|style| style.width(px(8.)).bg(rgb(0x404040))) }) ``` `ScrollbarTrackStyle` supports `bg`, `border_color`, and `width`. `ScrollbarThumbStyle` supports `bg`, `width`, `inset`, `radius`, and `min_length`. ## Custom viewport geometry The viewport normally comes from `ScrollbarHandle::viewport_bounds`. Two overrides support composite or custom-painted controls: ```rust Scrollbar::vertical(&scroll_handle) .viewport_bounds(editor_content_bounds); Scrollbar::vertical(&scroll_handle) .viewport_from_layout(); ``` Use `viewport_bounds` when your painted viewport differs from the handle's layout bounds. Use `viewport_from_layout` when a positioned overlay container already represents the exact viewport, such as a table body below a fixed header. Override the content size only when the handle cannot report the complete scrollable extent: ```rust Scrollbar::vertical(&scroll_handle) .scroll_size(gpui_kit::size(px(800.), px(4_000.))); ``` ## Custom scroll handles `ScrollHandle`, `UniformListScrollHandle`, and `ListState` implement `ScrollbarHandle`. Custom scroll containers can implement the same trait: ```rust use gpui_kit::{Bounds, Pixels, Point, Size}; use gpui_kit::base::ScrollbarHandle; impl ScrollbarHandle for MyScrollState { fn viewport_bounds(&self) -> Bounds { self.viewport_bounds() } fn offset(&self) -> Point { self.offset() } fn set_offset(&self, offset: Point) { self.set_offset(offset); } fn content_size(&self) -> Size { self.content_size() } fn start_drag(&self) { self.set_scrollbar_dragging(true); } fn end_drag(&self) { self.set_scrollbar_dragging(false); } } ``` `start_drag` and `end_drag` are optional. Use them when the scroll container needs to suspend snapping, selection, or another behavior during thumb drag. Only the actively dragged axis receives `end_drag` on mouse-up. ## Stable identity `Scrollbar::new`, `vertical`, and `horizontal` derive an element ID from their call site. Set an explicit stable ID when the same call site produces multiple independent scrollbars: ```rust Scrollbar::vertical(&scroll_handle).id(("activity-list", panel_id)); ``` A stable identity preserves retained visibility and width animation state across renders. ## Complete showcase source The runnable example is embedded directly from the shared Rust source: ## Accessibility and interaction checklist * Keep wheel, trackpad, and keyboard scrolling available on the underlying viewport. * Preserve the default full-track interaction hitbox even when the painted thumb is narrow. * Give the thumb adequate contrast in normal, hover, and active states. * Do not move layout or hitboxes to implement entrance animation. * Test `Scrolling`, `Hover`, and `Always` with reduced motion enabled. * Test vertical, horizontal, and two-axis overflow independently. --- --- url: /zh-CN/base/primitives/scrollbar.md description: 为 GPUI 滚动视图、列表和自定义视口添加带动画的可定制滚动条。 --- # Scrollbar `Scrollbar` 是连接 GPUI 滚动句柄的自绘滚动条,支持纵向、横向和双轴视口、轨道点击、滑块拖动、可配置可见模式、类型化绘制样式、减少动态效果,以及可反向的可见性和宽度过渡。`gpui-base` 负责交互与过渡生命周期,应用或设计系统负责颜色、几何、时序和进入编排。 ## 运行示例 ```bash cargo run -p gpui-base --example components -- scrollbar ``` 原生与 WASM 共用 [`scrollbar.rs`](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/components/scrollbar.rs)。 ## 基本用法 把 `ScrollHandle` 保存在持久视图状态中,通过 `track_scroll` 连接可滚动内容,并在同一个 `relative()` 容器中叠加 `Scrollbar`。`Scrollbar::new` 启用双轴;单轴使用 `vertical`、`horizontal` 或 `.axis(...)`。滚动条是绝对定位覆盖层,进入动画不会移动内容或命中区域。 ## 可见模式 * `Scrolling`:滚动或拖动后显示,离开悬停区域后重新计算空闲等待。 * `Hover`:指针进入轨道后显示。 * `Always`:始终可见并跳过可见性过渡。 默认静止滑块宽 6 px,滑块悬停或拖动目标宽度为 8 px。隐藏的轨道与滑块不响应点击。 ## 全局主题与实例覆盖 使用 `ScrollbarStyles` 的 `track`、`track_hover`、`track_active`、`thumb`、`thumb_hover` 和 `thumb_active` builder 设置外观;使用 `ScrollbarMotion` 配置 `idle`、`enter`、`exit`、`expand` 以及 `ScrollbarEntrance`。再把它们装入 `Theme::global_mut(cx).scrollbar = ScrollbarTheme::new().with_mode(...).with_motion(...).with_styles(...)`。字段保持私有,可通过 reader 查询。单个实例的 `.styles(...)` 优先于全局主题。 ## 动画行为 Base 不附带产品动画。默认仅有 2 秒行为性空闲等待,进入、退出和展开时长均为零。`Fade` 原地淡入;`SlideAndFade` 让纵向滚动条从右侧、横向滚动条从底部进入。被中断的过渡从当前视觉值反向;零时长立即采用目标值。GPUI 的减少动态效果偏好也会把可见性和宽度时长降为零。 ## 自定义视口与句柄 默认视口来自 `ScrollbarHandle::viewport_bounds`。组合控件可用 `.viewport_bounds(bounds)` 指定绘制视口,或 `.viewport_from_layout()` 使用覆盖层布局;只有句柄无法报告完整范围时才用 `.scroll_size(...)` 覆盖内容尺寸。`ScrollHandle`、`UniformListScrollHandle` 和 `ListState` 已实现 `ScrollbarHandle`;自定义容器需实现视口、偏移、设置偏移和内容尺寸,必要时实现 `start_drag` / `end_drag`。 ## 稳定身份 构造器默认从调用位置派生 ID。同一调用位置生成多个独立滚动条时,应使用 `.id(("activity-list", panel_id))` 提供稳定 ID,以跨渲染保留可见性和宽度动画状态。 ## 完整示例 ## 可访问性与交互检查 * 保留底层视口的滚轮、触控板和键盘滚动。 * 即使绘制的滑块很窄,也保留完整轨道命中区域。 * 验证普通、悬停和活动状态对比度。 * 动画不要移动布局或命中区域。 * 在减少动态效果下分别测试三种模式以及纵向、横向和双轴溢出。 --- --- url: /base/primitives/select.md description: >- A button-like selection control backed by an anchored, keyboard-navigable popup. --- # Select A button-like selection control backed by an anchored, keyboard-navigable popup. Like every `gpui-base` primitive, Select supplies behavior and semantic structure without imposing a product visual language. Apply GPUI styles and compose the exported parts to match your design system. ## Example The [single native Cargo entrypoint](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/components.rs) selects this primitive from the [shared showcase implementation](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/mod.rs). The same showcase is compiled once for the WASM preview above. ```bash cargo run -p gpui-base --example components -- select ``` ## Import ```rust use gpui_kit::base::{Select}; ``` ## Anatomy and API The example composes `Select`. GPUI's standard styling and event traits provide presentation; these base types provide the interaction structure. The authoritative module is [`components/select.rs`](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/components/select.rs). Native and browser previews compile this same file. ## State and events The delegate/state owns items and selection; activation opens the list and selection closes it. Keep controlled state on the parent render type or in a GPUI entity. Update it in callbacks and call `cx.notify()`; do not recreate persistent entities during every render. ## Complete Rust example The complete implementation used by the runnable showcase is embedded directly from Rust source: The command above supplies application initialization, window creation, and shared `BaseShowcase` state. ## Accessibility Label the trigger, expose expanded/selected state, and support traversal, selection, Escape, and focus return. ## Notes Use stable element IDs where accepted. Verify focus, hover, active, selected, disabled, reduced-motion, and high-contrast appearances in the consuming design system. --- --- url: /zh-CN/base/primitives/select.md description: 由锚定、支持键盘导航的弹层驱动的选择控件。 --- # Select 由锚定、支持键盘导航的弹层驱动的选择控件。 和所有 GPUI Base 原语一样,Select 只提供行为和语义结构,不规定产品视觉语言。请使用 GPUI 样式并组合导出的部件,使其符合你的设计系统。 ## 示例 原生示例和页面上方的 WASM 预览共用同一份实现: ```bash cargo run -p gpui-base --example components -- select ``` ## 导入 ```rust use gpui_kit::base::{Select}; ``` ## 结构与 API 示例组合上述公开类型。GPUI 的标准样式和事件 trait 负责表现,Base 类型负责交互结构。权威实现位于 [`components/select.rs`](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/components/select.rs),原生与浏览器预览编译的是同一文件。 ## 状态与事件 应用保存当前值;打开状态、焦点项与选择行为由控件协调。 受控状态应保存在父渲染类型或 GPUI entity 中;在回调中更新并调用 `cx.notify()`,不要在每次渲染时重建持久 entity。 ## 完整 Rust 示例 ## 可访问性 提供标签,暴露当前值,并保留上下键、Enter、Escape 与类型检索。 ## 注意事项 在支持的位置使用稳定元素 ID,并在消费端设计系统中验证焦点、悬停、按下、选中、禁用、减少动态效果和高对比度状态。 --- --- url: /docs/components/select.md description: Displays a list of options for the user to pick from—triggered by a button. --- # Select :::info This component was named `Dropdown` in `<= 0.3.x`. It has been renamed to `Select` to better reflect its purpose. ::: A select component that allows users to choose from a list of options. Supports search functionality, grouped items, custom rendering, and various states. Built with keyboard navigation and accessibility in mind. :::tip For richer selection UIs with custom trigger rendering or multi-select, see [Combobox](combobox). ::: ## Import ```rust use gpui_kit::component::select::{ Select, SelectState, SelectItem, SelectDelegate, SelectEvent, SearchableVec, SelectGroup }; ``` ## Usage ### Basic Select You can create a basic select dropdown by initializing a `SelectState` with a list of items. The first type parameter of `SelectState` is the items for the state, which must implement the [SelectItem] trait. The built-in implementations of `SelectItem` include common types like `String`, `SharedString`, and `&'static str`. ```rust let state = cx.new(|cx| { SelectState::new( vec!["Apple", "Orange", "Banana"], Some(IndexPath::default()), // Select first item window, cx, ) }); Select::new(&state) ``` ### Placeholder ```rust let state = cx.new(|cx| { SelectState::new( vec!["Rust", "Go", "JavaScript"], None, // No initial selection window, cx, ) }); Select::new(&state) .placeholder("Select a language...") ``` ### Searchable Use `searchable(true)` to enable search functionality within the dropdown. ```rust let fruits = SearchableVec::new(vec![ "Apple", "Orange", "Banana", "Grape", "Pineapple", ]); let state = cx.new(|cx| { SelectState::new(fruits, None, window, cx).searchable(true) }); Select::new(&state) .icon(IconName::Search) // Shows search icon ``` ### Impl SelectItem By default, we have implmemented `SelectItem` for common types like `String`, `SharedString` and `&'static str`. You can also create your own item types by implementing the `SelectItem` trait. This is useful when you want to display complex data structures, and also want get that data type from `select_value` method. You can also customize the search logic by overriding the `matches` method. ```rust #[derive(Debug, Clone)] struct Country { name: SharedString, code: SharedString, } impl SelectItem for Country { type Value = SharedString; fn title(&self) -> SharedString { self.name.clone() } fn display_title(&self) -> Option { // Custom display for selected item Some(format!("{} ({})", self.name, self.code).into_any_element()) } fn value(&self) -> &Self::Value { &self.code } fn matches(&self, query: &str) -> bool { // Custom search logic self.name.to_lowercase().contains(&query.to_lowercase()) || self.code.to_lowercase().contains(&query.to_lowercase()) } } ``` ### Group Items ```rust let mut grouped_items = SearchableVec::new(vec![]); // Group countries by first letter grouped_items.push( SelectGroup::new("A") .items(vec![ Country { name: "Australia".into(), code: "AU".into() }, Country { name: "Austria".into(), code: "AT".into() }, ]) ); grouped_items.push( SelectGroup::new("B") .items(vec![ Country { name: "Brazil".into(), code: "BR".into() }, Country { name: "Belgium".into(), code: "BE".into() }, ]) ); let state = cx.new(|cx| { SelectState::new(grouped_items, None, window, cx) }); Select::new(&state) ``` ### Sizes ```rust Select::new(&state).large() Select::new(&state) // medium (default) Select::new(&state).small() ``` ### Disabled State ```rust Select::new(&state).disabled(true) ``` ### Cleanable ```rust Select::new(&state) .cleanable(true) // Show clear button when item is selected ``` ### Custom Appearance ```rust Select::new(&state) .w(px(320.)) // Set dropdown width .menu_width(px(400.)) // Set menu popup width .menu_max_h(rems(10.)) // Set menu max height (default: 20rem) .appearance(false) // Remove default styling .title_prefix("Country: ") // Add prefix to selected title ``` ### Empty State ```rust let state = cx.new(|cx| { SelectState::new(Vec::::new(), None, window, cx) }); Select::new(&state) .empty( h_flex() .h_24() .justify_center() .text_color(cx.theme().muted_foreground) .child("No options available") ) ``` ### Events ```rust cx.subscribe_in(&state, window, |view, state, event, window, cx| { match event { SelectEvent::Confirm(value) => { if let Some(selected_value) = value { println!("Selected: {:?}", selected_value); } else { println!("Selection cleared"); } } } }); ``` ### Mutating ```rust // Set by index state.update(cx, |state, cx| { state.set_selected_index(Some(IndexPath::default().row(2)), window, cx); }); // Set by value (requires PartialEq on Value type) state.update(cx, |state, cx| { state.set_selected_value(&"US".into(), window, cx); }); // Get current selection let current_value = state.read(cx).selected_value(); ``` Update items: ```rust state.update(cx, |state, cx| { let new_items = vec!["New Option 1".into(), "New Option 2".into()]; state.set_items(new_items, window, cx); }); ``` ## Examples ### Language Selector ```rust let languages = SearchableVec::new(vec![ "Rust".into(), "TypeScript".into(), "Go".into(), "Python".into(), "JavaScript".into(), ]); let state = cx.new(|cx| { SelectState::new(languages, None, window, cx) }); Select::new(&state) .placeholder("Select language...") .title_prefix("Language: ") ``` ### Country/Region Selector ```rust #[derive(Debug, Clone)] struct Region { name: SharedString, code: SharedString, flag: SharedString, } impl SelectItem for Region { type Value = SharedString; fn title(&self) -> SharedString { self.name.clone() } fn display_title(&self) -> Option { Some( h_flex() .items_center() .gap_2() .child(self.flag.clone()) .child(format!("{} ({})", self.name, self.code)) .into_any_element() ) } fn value(&self) -> &Self::Value { &self.code } } let regions = vec![ Region { name: "United States".into(), code: "US".into(), flag: "🇺🇸".into() }, Region { name: "Canada".into(), code: "CA".into(), flag: "🇨🇦".into() }, ]; let state = cx.new(|cx| { SelectState::new(regions, None, window, cx) }); Select::new(&state) .placeholder("Select country...") ``` ### Integrated with Input Field ```rust // Combined country code + phone input h_flex() .border_1() .border_color(cx.theme().input) .rounded(cx.theme().radius_lg) .w_full() .gap_1() .child( div().w(px(140.)).child( Select::new(&country_state) .appearance(false) // No border/background .py_2() .pl_3() ) ) .child(Separator::vertical()) .child( div().flex_1().child( Input::new(&phone_input) .appearance(false) .placeholder("Phone number") .pr_3() .py_2() ) ) ``` ### Multi-level Grouped Select ```rust let mut grouped_countries = SearchableVec::new(vec![]); for (continent, countries) in countries_by_continent { grouped_countries.push( SelectGroup::new(continent) .items(countries) ); } let state = cx.new(|cx| { SelectState::new(grouped_countries, None, window, cx) }); Select::new(&state) .menu_width(px(350.)) .placeholder("Select country...") ``` ## Keyboard Shortcuts | Key | Action | | --------- | --------------------------------------- | | `Tab` | Focus dropdown | | `Enter` | Open menu or select current item | | `Up/Down` | Navigate options (opens menu if closed) | | `Escape` | Close menu | | `Space` | Open menu | ## Theming The dropdown respects the current theme and uses the following theme tokens: * `background` - Dropdown input background * `input` - Border color * `foreground` - Text color * `muted_foreground` - Placeholder and disabled text * `accent` - Selected item background * `accent_foreground` - Placeholder text color * `border` - Menu border * `radius` - Border radius [SelectItem]: https://docs.rs/gpui-component/latest/gpui_component/select/trait.SelectItem.html --- --- url: /zh-CN/docs/components/select.md description: 通过按钮触发的选项选择组件。 --- # Select :::info 在 `<= 0.3.x` 中,这个组件的名字是 `Dropdown`。 现在已经改名为 `Select`,以便更准确地表达它的用途。 ::: Select 允许用户从一组选项中选择一个值。 它支持搜索、分组、自定义渲染和多种状态,并内建键盘导航和可访问性支持。 :::tip 如需自定义触发器渲染或多选功能,请参阅 [Combobox](combobox)。 ::: ## 导入 ```rust use gpui_kit::component::select::{ Select, SelectState, SelectItem, SelectDelegate, SelectEvent, SearchableVec, SelectGroup }; ``` ## 用法 ### 基础用法 `SelectState` 的第一个类型参数表示状态中的选项集合,这些选项需要实现 [SelectItem] trait。 框架已经为 `String`、`SharedString` 和 `&'static str` 提供了默认实现。 ```rust let state = cx.new(|cx| { SelectState::new( vec!["Apple", "Orange", "Banana"], Some(IndexPath::default()), window, cx, ) }); Select::new(&state) ``` ### Placeholder ```rust let state = cx.new(|cx| { SelectState::new( vec!["Rust", "Go", "JavaScript"], None, window, cx, ) }); Select::new(&state) .placeholder("Select a language...") ``` ### 可搜索 启用 `searchable(true)` 后,下拉菜单中会出现搜索能力: ```rust let fruits = SearchableVec::new(vec![ "Apple", "Orange", "Banana", "Grape", "Pineapple", ]); let state = cx.new(|cx| { SelectState::new(fruits, None, window, cx).searchable(true) }); Select::new(&state) .icon(IconName::Search) ``` ### 自定义 SelectItem 如果你希望选项携带更复杂的数据结构,或者希望 `selected_value` 返回自定义类型,可以自己实现 `SelectItem`。 同时也可以通过重写 `matches` 定制搜索逻辑。 ```rust #[derive(Debug, Clone)] struct Country { name: SharedString, code: SharedString, } impl SelectItem for Country { type Value = SharedString; fn title(&self) -> SharedString { self.name.clone() } fn display_title(&self) -> Option { Some(format!("{} ({})", self.name, self.code).into_any_element()) } fn value(&self) -> &Self::Value { &self.code } fn matches(&self, query: &str) -> bool { self.name.to_lowercase().contains(&query.to_lowercase()) || self.code.to_lowercase().contains(&query.to_lowercase()) } } ``` ### 分组 ```rust let mut grouped_items = SearchableVec::new(vec![]); grouped_items.push( SelectGroup::new("A") .items(vec![ Country { name: "Australia".into(), code: "AU".into() }, Country { name: "Austria".into(), code: "AT".into() }, ]) ); grouped_items.push( SelectGroup::new("B") .items(vec![ Country { name: "Brazil".into(), code: "BR".into() }, Country { name: "Belgium".into(), code: "BE".into() }, ]) ); let state = cx.new(|cx| { SelectState::new(grouped_items, None, window, cx) }); Select::new(&state) ``` ### 尺寸 ```rust Select::new(&state).large() Select::new(&state) Select::new(&state).small() ``` ### 禁用态 ```rust Select::new(&state).disabled(true) ``` ### 可清空 ```rust Select::new(&state) .cleanable(true) ``` ### 自定义外观 ```rust Select::new(&state) .w(px(320.)) .menu_width(px(400.)) .menu_max_h(rems(10.)) .appearance(false) .title_prefix("Country: ") ``` ### 空状态 ```rust let state = cx.new(|cx| { SelectState::new(Vec::::new(), None, window, cx) }); Select::new(&state) .empty( h_flex() .h_24() .justify_center() .text_color(cx.theme().muted_foreground) .child("No options available") ) ``` ### 事件 ```rust cx.subscribe_in(&state, window, |view, state, event, window, cx| { match event { SelectEvent::Confirm(value) => { if let Some(selected_value) = value { println!("Selected: {:?}", selected_value); } else { println!("Selection cleared"); } } } }); ``` ### 更新选中项和数据 ```rust state.update(cx, |state, cx| { state.set_selected_index(Some(IndexPath::default().row(2)), window, cx); }); state.update(cx, |state, cx| { state.set_selected_value(&"US".into(), window, cx); }); let current_value = state.read(cx).selected_value(); ``` 更新选项列表: ```rust state.update(cx, |state, cx| { let new_items = vec!["New Option 1".into(), "New Option 2".into()]; state.set_items(new_items, window, cx); }); ``` ## 示例 ### 语言选择器 ```rust let languages = SearchableVec::new(vec![ "Rust".into(), "TypeScript".into(), "Go".into(), "Python".into(), "JavaScript".into(), ]); let state = cx.new(|cx| { SelectState::new(languages, None, window, cx) }); Select::new(&state) .placeholder("Select language...") .title_prefix("Language: ") ``` ### 与 Input 组合 ```rust h_flex() .border_1() .border_color(cx.theme().input) .rounded(cx.theme().radius_lg) .w_full() .gap_1() .child( div().w(px(140.)).child( Select::new(&country_state) .appearance(false) .py_2() .pl_3() ) ) .child(Separator::vertical()) .child( div().flex_1().child( Input::new(&phone_input) .appearance(false) .placeholder("Phone number") .pr_3() .py_2() ) ) ``` ## 键盘快捷键 | 按键 | 行为 | | --- | --- | | `Tab` | 聚焦到 Select | | `Enter` | 打开菜单或确认当前项 | | `Up/Down` | 在选项间移动 | | `Escape` | 关闭菜单 | | `Space` | 打开菜单 | ## 主题 Select 会使用当前主题中的这些 token: * `background` - 输入区域背景 * `input` - 边框颜色 * `foreground` - 文本颜色 * `muted_foreground` - placeholder 与禁用态文字 * `accent` - 当前项背景 * `accent_foreground` - 当前项文字 * `border` - 菜单边框 * `radius` - 圆角 [SelectItem]: https://docs.rs/gpui-component/latest/gpui_component/select/trait.SelectItem.html --- --- url: /docs/components/settings.md description: A settings UI with grouped setting items and pages. --- # Settings > Since: v0.5.0 The Settings component provides a UI for managing application settings. It includes grouped setting items and pages. We can search by title, description, and custom keywords to filter the settings to display only relevant settings (Like this macOS, iOS Settings). ## Import ```rust use gpui_kit::component::setting::{Settings, SettingPage, SettingGroup, SettingItem, SettingField}; ``` ## Usage ### Build a settings Here we have components that can be used to build a settings page. * [Settings] - The main settings component that holds multiple setting pages. * [SettingPage] - A page of related setting groups. * [SettingGroup] - A group of related setting items based on [GroupBox] style. * [SettingItem] - A single setting item with title, description, and field. * [SettingField] - Provide different field types like Input, Dropdown, Switch, etc. The layout of the settings is like this: ``` Settings SettingPage SettingGroup SettingItem Title Description (optional) SettingField ``` ### Basic Settings ```rust use gpui_kit::component::setting::{Settings, SettingPage, SettingGroup, SettingItem, SettingField}; Settings::new("my-settings") .pages(vec![ SettingPage::new("General") .group( SettingGroup::new() .title("Basic Options") .item( SettingItem::new( "Enable Feature", SettingField::switch( |cx: &App| true, |val: bool, cx: &mut App| { println!("Feature enabled: {}", val); }, ) ) ) ) ]) ``` ### With Multiple Pages :::info When you want default expland a page, you can use `default_open(true)` on the [SettingPage]. ::: ```rust Settings::new("app-settings") .pages(vec![ SettingPage::new("General") .default_open(true) .group(SettingGroup::new().title("Appearance").items(vec![...])), SettingPage::new("Software Update") .group(SettingGroup::new().title("Updates").items(vec![...])), SettingPage::new("About") .group(SettingGroup::new().items(vec![...])), ]) ``` ### Group Variants ```rust use gpui_kit::component::group_box::GroupBoxVariant; Settings::new("my-settings") .with_group_variant(GroupBoxVariant::Outline) .pages(vec![...]) Settings::new("my-settings") .with_group_variant(GroupBoxVariant::Fill) .pages(vec![...]) ``` ## Setting Page ### Basic Page ```rust SettingPage::new("General") .group(SettingGroup::new().title("Options").items(vec![...])) ``` ### Multiple Groups ```rust SettingPage::new("General") .groups(vec![ SettingGroup::new().title("Appearance").items(vec![...]), SettingGroup::new().title("Font").items(vec![...]), SettingGroup::new().title("Other").items(vec![...]), ]) ``` ### Icon ```rust SettingPage::new("General") .icon(IconName::Settings) .groups(vec![...]) ``` ### Title Suffix Use `title_suffix` to render a custom element after the title in the page header, for example an info icon button that opens the help documentation: ```rust SettingPage::new("General") .title_suffix(|_, _| { Button::new("help") .icon(IconName::Info) .ghost() .xsmall() .on_click(|_, _, cx| cx.open_url("https://example.com/help")) }) .groups(vec![...]) ``` ### Default Open ```rust SettingPage::new("General") .default_open(true) .groups(vec![...]) ``` ### resettable Enable reset functionality for a page: ```rust SettingPage::new("General") .resettable(true) .groups(vec![...]) ``` ## Setting Group ### Basic Group ```rust SettingGroup::new() .title("Appearance") .items(vec![ SettingItem::new(...), SettingItem::new(...), ]) ``` ### Single Item ```rust SettingGroup::new() .title("Font") .item(SettingItem::new(...)) ``` ### Without Title ```rust SettingGroup::new() .items(vec![...]) ``` ## Setting Item ### Basic Item ```rust SettingItem::new("Title", SettingField::switch(...)) .description("Description text") ``` ### Custom Item with a render closure You can create a fully custom setting item using `SettingItem::render`: ```rust SettingItem::render(|options, _, _| { h_flex() .w_full() .justify_between() .child("Custom content") .child( Button::new("action") .label("Action") .with_size(options.size) ) .into_any_element() }) ``` ### Vertical Layout By default, setting items use horizontal layout. Use `layout(Axis::Vertical)` for vertical layout: ```rust SettingItem::new( "CLI Path", SettingField::input(...) ) .layout(Axis::Vertical) .description("This item uses vertical layout.") ``` ### With Markdown Description ```rust use gpui_kit::component::text::markdown; SettingItem::new( "Documentation", SettingField::element(...) ) .description(markdown("Rust doc for the `gpui-component` crate.")) ``` ### Disabled Use `disabled(true)` to render a setting item in a non-interactive state. The whole row is dimmed and the built-in field (Switch, Checkbox, Input, Dropdown, NumberInput) is automatically disabled. ```rust SettingItem::new( "Dark Mode", SettingField::switch(...) ) .description("Switch between light and dark themes.") .disabled(true) ``` For \[SettingItem::render] custom items, the row is still dimmed automatically, but the renderer is responsible for honoring the disabled state on any interactive controls inside it via `options.disabled`: ```rust SettingItem::render(|options, _, _| { h_flex() .child("Custom content") .child( Button::new("action") .label("Action") .with_size(options.size) .disabled(options.disabled) ) .into_any_element() }) .disabled(true) ``` ### Search Keywords Use `keywords` to attach additional search terms to an item. They are only used for search matching and are never rendered. For example, an item titled "Enable Two-factor auth" can be made searchable via "MFA": ```rust SettingItem::new( "Enable Two-factor auth", SettingField::switch(...) ) .keywords(["MFA", "2FA"]) ``` This is also useful for \[SettingItem::render] custom items that have no title or description but should still appear in search results: ```rust SettingItem::render(|options, _, _| { h_flex().child("Custom content").into_any_element() }) .keywords(["Advanced", "Network"]) ``` ## Setting Fields The [SettingField] enum provides different field types for various input needs. ### Switch The switch field represents a `boolean` on/off state. ```rust SettingItem::new( "Dark Mode", SettingField::switch( |cx: &App| cx.theme().mode.is_dark(), |val: bool, cx: &mut App| { // Handle value change }, ) .default_value(false) ) ``` ### Checkbox Like the switch, but uses a checkbox UI. ```rust SettingItem::new( "Auto Switch Theme", SettingField::checkbox( |cx: &App| AppSettings::global(cx).auto_switch_theme, |val: bool, cx: &mut App| { AppSettings::global_mut(cx).auto_switch_theme = val; }, ) .default_value(false) ) ``` ### Input Display a single line text input. ```rust SettingItem::new( "CLI Path", SettingField::input( |cx: &App| AppSettings::global(cx).cli_path.clone(), |val: SharedString, cx: &mut App| { AppSettings::global_mut(cx).cli_path = val; }, ) .default_value("/usr/local/bin/bash".into()) ) .layout(Axis::Vertical) .description("Path to the CLI executable.") ``` ### Dropdown A dropdown with a list of options. ```rust SettingItem::new( "Font Family", SettingField::dropdown( vec![ ("Arial".into(), "Arial".into()), ("Helvetica".into(), "Helvetica".into()), ("Times New Roman".into(), "Times New Roman".into()), ], |cx: &App| AppSettings::global(cx).font_family.clone(), |val: SharedString, cx: &mut App| { AppSettings::global_mut(cx).font_family = val; }, ) .default_value("Arial".into()) ) ``` ### NumberInput ```rust use gpui_kit::component::setting::NumberFieldOptions; SettingItem::new( "Font Size", SettingField::number_input( NumberFieldOptions { min: 8.0, max: 72.0, ..Default::default() }, |cx: &App| AppSettings::global(cx).font_size, |val: f64, cx: &mut App| { AppSettings::global_mut(cx).font_size = val; }, ) .default_value(14.0) ) ``` ### Custom Field by Render Closure The `SettingField::render` method allows you to create a custom field using a closure that returns an element. ```rust SettingItem::new( "GitHub Repository", SettingField::render(|options, _window, _cx| { Button::new("open-url") .outline() .label("Repository...") .with_size(options.size) .on_click(|_, _window, cx| { cx.open_url("https://github.com/example/repo"); }) }) ) ``` ### Custom Field Element You may have a complex field that you want to reuse, you may want split the element into a separate struct to do the complex logic. In this case, the [SettingFieldElement] trait can help you to create a custom field element. ```rust use gpui_kit::component::setting::{SettingFieldElement, RenderOptions}; struct OpenURLSettingField { label: SharedString, url: SharedString, } impl SettingFieldElement for OpenURLSettingField { type Element = Button; fn render_field(&self, options: &RenderOptions, _: &mut Window, _: &mut App) -> Self::Element { let url = self.url.clone(); Button::new("open-url") .outline() .label(self.label.clone()) .with_size(options.size) .on_click(move |_, _window, cx| { cx.open_url(url.as_str()); }) } } ``` Then use it in the setting item: ```rust SettingItem::new( "GitHub Repository", SettingField::element(OpenURLSettingField { label: "Repository...".into(), url: "https://github.com/longbridge/gpui-kit".into(), }) ) ``` ## API Reference * [Settings] * [SettingPage] * [SettingGroup] * [SettingItem] * [SettingField] * [NumberFieldOptions] ### Sizing Implements [Sizable] trait: * `xsmall()` - Extra small size * `small()` - Small size * `medium()` - Medium size (default) * `large()` - Large size * `with_size(Size)` - Set specific size ## Examples ### Complete Settings Example ```rust use gpui_kit::{App, SharedString}; use gpui_kit::component::{ Settings, SettingPage, SettingGroup, SettingItem, SettingField, setting::NumberFieldOptions, group_box::GroupBoxVariant, Size, }; Settings::new("app-settings") .with_size(Size::Medium) .with_group_variant(GroupBoxVariant::Outline) .pages(vec![ SettingPage::new("General") .resettable(true) .default_open(true) .groups(vec![ SettingGroup::new() .title("Appearance") .items(vec![ SettingItem::new( "Dark Mode", SettingField::switch( |cx: &App| cx.theme().mode.is_dark(), |val: bool, cx: &mut App| { // Handle theme change }, ) ) .description("Switch between light and dark themes."), ]), SettingGroup::new() .title("Font") .items(vec![ SettingItem::new( "Font Family", SettingField::dropdown( vec![ ("Arial".into(), "Arial".into()), ("Helvetica".into(), "Helvetica".into()), ], |cx: &App| "Arial".into(), |val: SharedString, cx: &mut App| { // Handle font change }, ) ), SettingItem::new( "Font Size", SettingField::number_input( NumberFieldOptions { min: 8.0, max: 72.0, ..Default::default() }, |cx: &App| 14.0, |val: f64, cx: &mut App| { // Handle size change }, ) ), ]), ]), SettingPage::new("Software Update") .resettable(true) .group( SettingGroup::new() .title("Updates") .items(vec![ SettingItem::new( "Auto Update", SettingField::switch( |cx: &App| true, |val: bool, cx: &mut App| { // Handle auto update }, ) ) .description("Automatically download and install updates."), ]) ), ]) ``` [Settings]: https://docs.rs/gpui-component/latest/gpui_component/setting/struct.Settings.html [SettingPage]: https://docs.rs/gpui-component/latest/gpui_component/setting/struct.SettingPage.html [SettingGroup]: https://docs.rs/gpui-component/latest/gpui_component/setting/struct.SettingGroup.html [SettingItem]: https://docs.rs/gpui-component/latest/gpui_component/setting/struct.SettingItem.html [SettingField]: https://docs.rs/gpui-component/latest/gpui_component/setting/enum.SettingField.html [SettingFieldElement]: https://docs.rs/gpui-component/latest/gpui_component/setting/trait.SettingFieldElement.html [NumberFieldOptions]: https://docs.rs/gpui-component/latest/gpui_component/setting/struct.NumberFieldOptions.html [GroupBox]: ./group-box.md [Sizable]: https://docs.rs/gpui-component/latest/gpui_component/trait.Sizable.html --- --- url: /zh-CN/docs/components/settings.md description: 用于构建设置页、设置分组和设置项的界面组件。 --- # Settings > Since: v0.5.0 Settings 组件用于构建应用设置界面,支持页面分组、按标题/描述/自定义关键词搜索过滤以及多种字段类型,适合实现类似 macOS 或 iOS 设置页的结构。 ## 导入 ```rust use gpui_kit::component::setting::{Settings, SettingPage, SettingGroup, SettingItem, SettingField}; ``` ## 用法 ### 构建设置界面 可组合以下组件来组织设置页: * [Settings]:顶层设置容器,持有多个设置页面。 * [SettingPage]:一组相关设置的页面。 * [SettingGroup]:基于 [GroupBox] 风格的设置分组。 * [SettingItem]:单个设置项,包含标题、描述和字段。 * [SettingField]:具体字段,如 Input、Dropdown、Switch 等。 整体层级如下: ``` Settings SettingPage SettingGroup SettingItem Title Description (optional) SettingField ``` ### 基础示例 ```rust use gpui_kit::component::setting::{Settings, SettingPage, SettingGroup, SettingItem, SettingField}; Settings::new("my-settings") .pages(vec![ SettingPage::new("General") .group( SettingGroup::new() .title("Basic Options") .item( SettingItem::new( "Enable Feature", SettingField::switch( |cx: &App| true, |val: bool, cx: &mut App| { println!("Feature enabled: {}", val); }, ) ) ) ) ]) ``` ### 多页面 :::info 如果你希望某个页面默认展开,可在 [SettingPage] 上使用 `default_open(true)`。 ::: ```rust Settings::new("app-settings") .pages(vec![ SettingPage::new("General") .default_open(true) .group(SettingGroup::new().title("Appearance").items(vec![...])), SettingPage::new("Software Update") .group(SettingGroup::new().title("Updates").items(vec![...])), SettingPage::new("About") .group(SettingGroup::new().items(vec![...])), ]) ``` ### 分组样式 ```rust use gpui_kit::component::group_box::GroupBoxVariant; Settings::new("my-settings") .with_group_variant(GroupBoxVariant::Outline) .pages(vec![...]) Settings::new("my-settings") .with_group_variant(GroupBoxVariant::Fill) .pages(vec![...]) ``` ## Setting Page ### 基础页面 ```rust SettingPage::new("General") .group(SettingGroup::new().title("Options").items(vec![...])) ``` ### 多分组 ```rust SettingPage::new("General") .groups(vec![ SettingGroup::new().title("Appearance").items(vec![...]), SettingGroup::new().title("Font").items(vec![...]), SettingGroup::new().title("Other").items(vec![...]), ]) ``` ### 图标 ```rust SettingPage::new("General") .icon(IconName::Settings) .groups(vec![...]) ``` ### 标题后缀 使用 `title_suffix` 在页头标题后渲染自定义元素,例如一个点击后打开帮助文档的 info 图标按钮: ```rust SettingPage::new("General") .title_suffix(|_, _| { Button::new("help") .icon(IconName::Info) .ghost() .xsmall() .on_click(|_, _, cx| cx.open_url("https://example.com/help")) }) .groups(vec![...]) ``` ### 默认展开 ```rust SettingPage::new("General") .default_open(true) .groups(vec![...]) ``` ### 支持重置 ```rust SettingPage::new("General") .resettable(true) .groups(vec![...]) ``` ## Setting Group ### 基础分组 ```rust SettingGroup::new() .title("Appearance") .items(vec![ SettingItem::new(...), SettingItem::new(...), ]) ``` ### 单项分组 ```rust SettingGroup::new() .title("Font") .item(SettingItem::new(...)) ``` ### 无标题分组 ```rust SettingGroup::new() .items(vec![...]) ``` ## Setting Item ### 基础设置项 ```rust SettingItem::new("Title", SettingField::switch(...)) .description("Description text") ``` ### 使用 render closure 的自定义项 ```rust SettingItem::render(|options, _, _| { h_flex() .w_full() .justify_between() .child("Custom content") .child( Button::new("action") .label("Action") .with_size(options.size) ) .into_any_element() }) ``` ### 纵向布局 设置项默认是横向布局;可通过 `layout(Axis::Vertical)` 切换为纵向布局: ```rust SettingItem::new( "CLI Path", SettingField::input(...) ) .layout(Axis::Vertical) .description("This item uses vertical layout.") ``` ### Markdown 描述 ```rust use gpui_kit::component::text::markdown; SettingItem::new( "Documentation", SettingField::element(...) ) .description(markdown("Rust doc for the `gpui-component` crate.")) ``` ### 禁用状态 通过 `disabled(true)` 可以将设置项置为不可交互状态:整行会变暗,内置字段 (Switch、Checkbox、Input、Dropdown、NumberInput)也会被自动禁用。 ```rust SettingItem::new( "Dark Mode", SettingField::switch(...) ) .description("Switch between light and dark themes.") .disabled(true) ``` 对于 \[SettingItem::render] 自定义项,整行同样会自动变暗,但其中可交互的控件 需要渲染闭包通过 `options.disabled` 自行处理: ```rust SettingItem::render(|options, _, _| { h_flex() .child("Custom content") .child( Button::new("action") .label("Action") .with_size(options.size) .disabled(options.disabled) ) .into_any_element() }) .disabled(true) ``` ### 搜索关键词 通过 `keywords` 可以为设置项附加额外的搜索关键词。这些关键词仅用于搜索匹配, 不会被渲染出来。例如,标题为 "Enable Two-factor auth" 的设置项可以通过 "MFA" 搜索到: ```rust SettingItem::new( "Enable Two-factor auth", SettingField::switch(...) ) .keywords(["MFA", "2FA"]) ``` 这对于没有标题和描述、但仍希望能被搜索到的 \[SettingItem::render] 自定义项同样 有用: ```rust SettingItem::render(|options, _, _| { h_flex().child("Custom content").into_any_element() }) .keywords(["Advanced", "Network"]) ``` ## Setting Fields [SettingField] 枚举提供了多种常见字段类型。 ### Switch ```rust SettingItem::new( "Dark Mode", SettingField::switch( |cx: &App| cx.theme().mode.is_dark(), |val: bool, cx: &mut App| { // Handle value change }, ) .default_value(false) ) ``` ### Checkbox ```rust SettingItem::new( "Auto Switch Theme", SettingField::checkbox( |cx: &App| AppSettings::global(cx).auto_switch_theme, |val: bool, cx: &mut App| { AppSettings::global_mut(cx).auto_switch_theme = val; }, ) .default_value(false) ) ``` ### Input ```rust SettingItem::new( "CLI Path", SettingField::input( |cx: &App| AppSettings::global(cx).cli_path.clone(), |val: SharedString, cx: &mut App| { AppSettings::global_mut(cx).cli_path = val; }, ) .default_value("/usr/local/bin/bash".into()) ) .layout(Axis::Vertical) .description("Path to the CLI executable.") ``` ### Dropdown ```rust SettingItem::new( "Font Family", SettingField::dropdown( vec![ ("Arial".into(), "Arial".into()), ("Helvetica".into(), "Helvetica".into()), ("Times New Roman".into(), "Times New Roman".into()), ], |cx: &App| AppSettings::global(cx).font_family.clone(), |val: SharedString, cx: &mut App| { AppSettings::global_mut(cx).font_family = val; }, ) .default_value("Arial".into()) ) ``` ### NumberInput ```rust use gpui_kit::component::setting::NumberFieldOptions; SettingItem::new( "Font Size", SettingField::number_input( NumberFieldOptions { min: 8.0, max: 72.0, ..Default::default() }, |cx: &App| AppSettings::global(cx).font_size, |val: f64, cx: &mut App| { AppSettings::global_mut(cx).font_size = val; }, ) .default_value(14.0) ) ``` ### 使用 render closure 创建自定义字段 ```rust SettingItem::new( "GitHub Repository", SettingField::render(|options, _window, _cx| { Button::new("open-url") .outline() .label("Repository...") .with_size(options.size) .on_click(|_, _window, cx| { cx.open_url("https://github.com/example/repo"); }) }) ) ``` ### 自定义字段元素 如果某个字段逻辑较复杂并且需要复用,可以实现 [SettingFieldElement] trait: ```rust use gpui_kit::component::setting::{SettingFieldElement, RenderOptions}; struct OpenURLSettingField { label: SharedString, url: SharedString, } impl SettingFieldElement for OpenURLSettingField { type Element = Button; fn render_field(&self, options: &RenderOptions, _: &mut Window, _: &mut App) -> Self::Element { let url = self.url.clone(); Button::new("open-url") .outline() .label(self.label.clone()) .with_size(options.size) .on_click(move |_, _window, cx| { cx.open_url(url.as_str()); }) } } ``` 然后在设置项中这样使用: ```rust SettingItem::new( "GitHub Repository", SettingField::element(OpenURLSettingField { label: "Repository...".into(), url: "https://github.com/longbridge/gpui-kit".into(), }) ) ``` ## API 参考 * [Settings] * [SettingPage] * [SettingGroup] * [SettingItem] * [SettingField] * [NumberFieldOptions] ### 尺寸 实现了 [Sizable] trait: * `xsmall()`:超小尺寸 * `small()`:小尺寸 * `medium()`:中尺寸,默认值 * `large()`:大尺寸 * `with_size(Size)`:指定具体尺寸 ## 示例 ### 完整设置页示例 ```rust use gpui_kit::{App, SharedString}; use gpui_kit::component::{ Settings, SettingPage, SettingGroup, SettingItem, SettingField, setting::NumberFieldOptions, group_box::GroupBoxVariant, Size, }; Settings::new("app-settings") .with_size(Size::Medium) .with_group_variant(GroupBoxVariant::Outline) .pages(vec![ SettingPage::new("General") .resettable(true) .default_open(true) .groups(vec![ SettingGroup::new() .title("Appearance") .items(vec![ SettingItem::new( "Dark Mode", SettingField::switch( |cx: &App| cx.theme().mode.is_dark(), |val: bool, cx: &mut App| { // Handle theme change }, ) ) .description("Switch between light and dark themes."), ]), SettingGroup::new() .title("Font") .items(vec![ SettingItem::new( "Font Family", SettingField::dropdown( vec![ ("Arial".into(), "Arial".into()), ("Helvetica".into(), "Helvetica".into()), ], |cx: &App| "Arial".into(), |val: SharedString, cx: &mut App| { // Handle font change }, ) ), SettingItem::new( "Font Size", SettingField::number_input( NumberFieldOptions { min: 8.0, max: 72.0, ..Default::default() }, |cx: &App| 14.0, |val: f64, cx: &mut App| { // Handle size change }, ) ), ]), ]), SettingPage::new("Software Update") .resettable(true) .group( SettingGroup::new() .title("Updates") .items(vec![ SettingItem::new( "Auto Update", SettingField::switch( |cx: &App| true, |val: bool, cx: &mut App| { // Handle auto update }, ) ) .description("Automatically download and install updates."), ]) ), ]) ``` [Settings]: https://docs.rs/gpui-component/latest/gpui_component/setting/struct.Settings.html [SettingPage]: https://docs.rs/gpui-component/latest/gpui_component/setting/struct.SettingPage.html [SettingGroup]: https://docs.rs/gpui-component/latest/gpui_component/setting/struct.SettingGroup.html [SettingItem]: https://docs.rs/gpui-component/latest/gpui_component/setting/struct.SettingItem.html [SettingField]: https://docs.rs/gpui-component/latest/gpui_component/setting/enum.SettingField.html [SettingFieldElement]: https://docs.rs/gpui-component/latest/gpui_component/setting/trait.SettingFieldElement.html [NumberFieldOptions]: https://docs.rs/gpui-component/latest/gpui_component/setting/struct.NumberFieldOptions.html [GroupBox]: ./group-box.md [Sizable]: https://docs.rs/gpui-component/latest/gpui_component/trait.Sizable.html --- --- url: /base/primitives/sheet.md description: A modal surface that enters from an edge while managing dismissal and focus. --- # Sheet A modal surface that enters from an edge while managing dismissal and focus. Like every `gpui-base` primitive, Sheet supplies behavior and semantic structure without imposing a product visual language. Apply GPUI styles and compose the exported parts to match your design system. ## Example The [single native Cargo entrypoint](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/components.rs) selects this primitive from the [shared showcase implementation](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/mod.rs). The same showcase is compiled once for the WASM preview above. ```bash cargo run -p gpui-base --example components -- sheet ``` ## Import ```rust use gpui_kit::base::{Sheet}; ``` ## Anatomy and API The example composes `Sheet`. GPUI's standard styling and event traits provide presentation; these base types provide the interaction structure. The authoritative module is [`components/sheet.rs`](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/components/sheet.rs). Native and browser previews compile this same file. ## State and events Open and dismissal mirror a dialog while placement chooses the entering edge. Keep controlled state on the parent render type or in a GPUI entity. Update it in callbacks and call `cx.notify()`; do not recreate persistent entities during every render. ## Complete Rust example The complete implementation used by the runnable showcase is embedded directly from Rust source: The command above supplies application initialization, window creation, and shared `BaseShowcase` state. ## Accessibility Apply dialog semantics: title it, trap and restore focus, and provide close. ## Notes Use stable element IDs where accepted. Verify focus, hover, active, selected, disabled, reduced-motion, and high-contrast appearances in the consuming design system. --- --- url: /zh-CN/base/primitives/sheet.md description: 从边缘进入并管理关闭与焦点的模态界面。 --- # Sheet 从边缘进入并管理关闭与焦点的模态界面。 和所有 GPUI Base 原语一样,Sheet 只提供行为和语义结构,不规定产品视觉语言。请使用 GPUI 样式并组合导出的部件,使其符合你的设计系统。 ## 示例 原生示例和页面上方的 WASM 预览共用同一份实现: ```bash cargo run -p gpui-base --example components -- sheet ``` ## 导入 ```rust use gpui_kit::base::{Sheet}; ``` ## 结构与 API 示例组合上述公开类型。GPUI 的标准样式和事件 trait 负责表现,Base 类型负责交互结构。权威实现位于 [`components/sheet.rs`](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/components/sheet.rs),原生与浏览器预览编译的是同一文件。 ## 状态与事件 打开状态由应用或触发器控制;退出结束前内容可继续挂载。 受控状态应保存在父渲染类型或 GPUI entity 中;在回调中更新并调用 `cx.notify()`,不要在每次渲染时重建持久 entity。 ## 完整 Rust 示例 ## 可访问性 按模态层处理焦点与背景交互,提供标题和明确关闭方式。 ## 注意事项 在支持的位置使用稳定元素 ID,并在消费端设计系统中验证焦点、悬停、按下、选中、禁用、减少动态效果和高对比度状态。 --- --- url: /docs/components/sheet.md description: >- A sliding panel that appears from the edges of the screen for displaying content. --- # Sheet A Sheet (also known as a sidebar or slide-out panel) is a navigation component that slides in from the edges of the screen. It provides additional space for content without taking up the main view, and can be used for navigation menus, forms, or any supplementary content. ## Import ```rust use gpui_kit::component::WindowExt; use gpui_kit::component::Placement; ``` ## Usage ### Setup application root view for display of sheets You need to set up your application's root view to render the sheet layer. This is typically done in your main application struct's render method. The [Root::render\_sheet\_layer](https://docs.rs/gpui-component/latest/gpui_component/struct.Root.html#method.render_sheet_layer) function handles rendering any active modals on top of your app content. ```rust use gpui_kit::component::TitleBar; struct MyApp { view: AnyView, } impl Render for MyApp { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let sheet_layer = Root::render_sheet_layer(window, cx); div() .size_full() .child( v_flex() .size_full() .child(TitleBar::new()) .child(div().flex_1().overflow_hidden().child(self.view.clone())), ) // Render the sheet layer on top of the app content .children(sheet_layer) } } ``` ### Basic Sheet ```rust window.open_sheet(cx, |sheet, _, _| { sheet .title("Navigation") .child("Sheet content goes here") }) ``` ### Sheet with Placement ```rust // Left sheet (default) window.open_sheet_at(Placement::Left, cx, |sheet, _, _| { sheet.title("Left Sheet") }) // Right sheet window.open_sheet_at(Placement::Right, cx, |sheet, _, _| { sheet.title("Right Sheet") }) // Top sheet window.open_sheet_at(Placement::Top, cx, |sheet, _, _| { sheet.title("Top Sheet") }) // Bottom sheet window.open_sheet_at(Placement::Bottom, cx, |sheet, _, _| { sheet.title("Bottom Sheet") }) ``` ### Sheet with Custom Size ```rust window.open_sheet(cx, |sheet, _, _| { sheet .title("Wide Sheet") .size(px(500.)) // Custom width for left/right, height for top/bottom .child("This sheet is 500px wide") }) ``` ### Sheet with Form Content ```rust let input = cx.new(|cx| InputState::new(window, cx)); let date = cx.new(|cx| DatePickerState::new(window, cx)); window.open_sheet(cx, |sheet, _, _| { sheet .title("User Profile") .child( v_flex() .gap_4() .child("Enter your information:") .child(Input::new(&input).placeholder("Full Name")) .child(DatePicker::new(&date).placeholder("Date of Birth")) ) .footer( h_flex() .gap_3() .child(Button::new("save").primary().label("Save")) .child(Button::new("cancel").label("Cancel")) ) }) ``` ### Overlay Options ```rust window.open_sheet(cx, |sheet, _, _| { sheet .title("Settings") .overlay(true) // Show overlay background (default: true) .overlay_closable(true) // Click overlay to close (default: true) .child("Sheet settings content") }) // No overlay window.open_sheet(cx, |sheet, _, _| { sheet .title("Side Panel") .overlay(false) // No overlay background .child("This sheet has no overlay") }) ``` ### Resizable Sheet ```rust window.open_sheet(cx, |sheet, _, _| { sheet .title("Resizable Panel") .resizable(true) // Allow user to resize (default: true) .size(px(300.)) .child("You can resize this sheet by dragging the edge") }) ``` ### Custom Margin and Positioning ```rust window.open_sheet(cx, |sheet, _, _| { sheet .title("Below Title Bar") .margin_top(px(32.)) // Space for window title bar .child("This sheet appears below the title bar") }) ``` ### Sheet with List ```rust let delegate = ListDelegate::new(items); let list = cx.new(|cx| List::new(delegate, window, cx)); window.open_sheet_at(Placement::Left, cx, |sheet, _, _| { sheet .title("File Explorer") .size(px(400.)) .child( div() .border_1() .border_color(cx.theme().border) .rounded(cx.theme().radius) .size_full() .child(list.clone()) ) }) ``` ### Close Event Handling ```rust window.open_sheet(cx, |sheet, _, _| { sheet .title("Sheet with Handler") .child("This sheet has a custom close handler") .on_close(|_, window, cx| { window.push_notification("Sheet was closed", cx); }) }) ``` ### Navigation Sheet ```rust window.open_sheet_at(Placement::Left, cx, |sheet, _, _| { sheet .title("Navigation") .size(px(280.)) .child( v_flex() .gap_2() .child(Button::new("home").ghost().label("Home").w_full()) .child(Button::new("profile").ghost().label("Profile").w_full()) .child(Button::new("settings").ghost().label("Settings").w_full()) .child(Button::new("logout").ghost().label("Logout").w_full()) ) }) ``` ### Custom Styling ```rust window.open_sheet(cx, |sheet, _, cx| { sheet .title("Styled Sheet") .bg(cx.theme().accent) .text_color(cx.theme().accent_foreground) .border_color(cx.theme().primary) .child("Custom styled sheet content") }) ``` ### Programmatic Close ```rust // Close sheet from inside Button::new("close") .label("Close Sheet") .on_click(|_, window, cx| { window.close_sheet(cx); }) // Close sheet from outside window.close_sheet(cx); ``` ## API Reference ### Window Extensions | Method | Description | | ---------------------------------- | ----------------------------------------- | | `open_sheet(cx, fn)` | Open sheet with default placement (Right) | | `open_sheet_at(placement, cx, fn)` | Open sheet at specific placement | | `close_sheet(cx)` | Close current sheet | ### Sheet Builder | Method | Description | | ------------------------ | --------------------------------------- | | `title(str)` | Set sheet title | | `child(el)` | Add content to sheet body | | `footer(el)` | Set footer content | | `size(px)` | Set sheet size (width or height) | | `margin_top(px)` | Set top margin (for title bars) | | `resizable(bool)` | Allow resizing (default: true) | | `overlay(bool)` | Show overlay background (default: true) | | `overlay_closable(bool)` | Click overlay to close (default: true) | | `on_close(fn)` | Close event callback | ### Placement Options | Value | Description | | ------------------- | ----------------------------------- | | `Placement::Left` | Slides in from left edge | | `Placement::Right` | Slides in from right edge (default) | | `Placement::Top` | Slides in from top edge | | `Placement::Bottom` | Slides in from bottom edge | ### Styling Methods | Method | Description | | --------------------- | ------------------------ | | `bg(color)` | Set background color | | `text_color(color)` | Set text color | | `border_color(color)` | Set border color | | `px_*()/py_*()` | Custom padding | | `gap_*()` | Spacing between children | ## Examples ### Settings Panel ```rust window.open_sheet_at(Placement::Right, cx, |sheet, _, _| { sheet .title("Settings") .size(px(350.)) .child( v_flex() .gap_4() .child("Appearance") .child(Checkbox::new("dark-mode").label("Dark Mode")) .child(Checkbox::new("animations").label("Enable Animations")) .child("Notifications") .child(Checkbox::new("push-notifications").label("Push Notifications")) ) .footer( h_flex() .justify_end() .gap_2() .child(Button::new("apply").primary().label("Apply")) .child(Button::new("cancel").label("Cancel")) ) }) ``` ### File Browser ```rust window.open_sheet_at(Placement::Left, cx, |sheet, _, _| { sheet .title("Files") .size(px(300.)) .child( v_flex() .size_full() .child( h_flex() .gap_2() .p_2() .child(Button::new("new-folder").small().icon(IconName::FolderPlus)) .child(Button::new("upload").small().icon(IconName::Upload)) ) .child( div() .flex_1() .overflow_hidden() .child(file_tree_list) ) ) }) ``` ### Help Panel ```rust window.open_sheet_at(Placement::Bottom, cx, |sheet, _, _| { sheet .title("Help & Documentation") .size(px(200.)) .child( h_flex() .gap_4() .child("Keyboard Shortcuts") .child(Kbd::new("⌘").child("K")) .child("Search") .child(Kbd::new("⌘").child("P")) .child("Command Palette") ) }) ``` ## Best Practices 1. **Appropriate Placement**: Use left/right for navigation, top/bottom for temporary content 2. **Consistent Sizing**: Maintain consistent sheet sizes across your application 3. **Clear Headers**: Always provide descriptive titles 4. **Close Options**: Provide multiple ways to close (ESC, overlay click, close button) 5. **Content Organization**: Use proper spacing and grouping for sheet content 6. **Responsive Design**: Consider sheet behavior on smaller screens 7. **Performance**: Lazy load sheet content when possible for better performance --- --- url: /zh-CN/docs/components/sheet.md description: 从屏幕边缘滑出的内容面板组件。 --- # Sheet Sheet 是一种从屏幕边缘滑出的面板组件,也常被用作侧栏、抽屉或临时内容面板。它适合承载导航菜单、表单、设置项和辅助信息,而不会直接占用主视图空间。 ## 导入 ```rust use gpui_kit::component::WindowExt; use gpui_kit::component::Placement; ``` ## 用法 ### 在根视图中渲染 Sheet 图层 如果应用要支持 Sheet,需要在根视图中渲染 sheet layer。 [Root::render\_sheet\_layer](https://docs.rs/gpui-component/latest/gpui_component/struct.Root.html#method.render_sheet_layer) 会把当前激活的 Sheet 渲染到应用内容之上。 ```rust use gpui_kit::component::TitleBar; struct MyApp { view: AnyView, } impl Render for MyApp { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let sheet_layer = Root::render_sheet_layer(window, cx); div() .size_full() .child( v_flex() .size_full() .child(TitleBar::new()) .child(div().flex_1().overflow_hidden().child(self.view.clone())), ) .children(sheet_layer) } } ``` ### 基础 Sheet ```rust window.open_sheet(cx, |sheet, _, _| { sheet .title("Navigation") .child("Sheet content goes here") }) ``` ### 不同方向 ```rust window.open_sheet_at(Placement::Left, cx, |sheet, _, _| { sheet.title("Left Sheet") }) window.open_sheet_at(Placement::Right, cx, |sheet, _, _| { sheet.title("Right Sheet") }) ``` ### 自定义尺寸 ```rust window.open_sheet(cx, |sheet, _, _| { sheet .title("Wide Sheet") .size(px(500.)) .child("This sheet is 500px wide") }) ``` ### 表单内容 ```rust let input = cx.new(|cx| InputState::new(window, cx)); let date = cx.new(|cx| DatePickerState::new(window, cx)); window.open_sheet(cx, |sheet, _, _| { sheet .title("User Profile") .child( v_flex() .gap_4() .child("Enter your information:") .child(Input::new(&input).placeholder("Full Name")) .child(DatePicker::new(&date).placeholder("Date of Birth")) ) .footer( h_flex() .gap_3() .child(Button::new("save").primary().label("Save")) .child(Button::new("cancel").label("Cancel")) ) }) ``` ### Overlay 与关闭行为 ```rust window.open_sheet(cx, |sheet, _, _| { sheet .title("Settings") .overlay(true) .overlay_closable(true) .child("Sheet settings content") }) ``` ### 可调整大小 ```rust window.open_sheet(cx, |sheet, _, _| { sheet .title("Resizable Panel") .resizable(true) .size(px(300.)) .child("You can resize this sheet by dragging the edge") }) ``` ### 自定义位置偏移 ```rust window.open_sheet(cx, |sheet, _, _| { sheet .title("Below Title Bar") .margin_top(px(32.)) .child("This sheet appears below the title bar") }) ``` ### 自定义样式 ```rust window.open_sheet(cx, |sheet, _, cx| { sheet .title("Styled Sheet") .bg(cx.theme().accent) .text_color(cx.theme().accent_foreground) .border_color(cx.theme().primary) .child("Custom styled sheet content") }) ``` ### 主动关闭 ```rust Button::new("close") .label("Close Sheet") .on_click(|_, window, cx| { window.close_sheet(cx); }) window.close_sheet(cx); ``` ## API 参考 ### Window 扩展 * `open_sheet(cx, fn)` 默认在右侧打开 * `open_sheet_at(placement, cx, fn)` 在指定方向打开 * `close_sheet(cx)` 关闭当前 Sheet ### 常用 Builder 方法 * `title(str)` * `child(el)` * `footer(el)` * `size(px)` * `margin_top(px)` * `resizable(bool)` * `overlay(bool)` * `overlay_closable(bool)` * `on_close(fn)` ## 最佳实践 1. 左右方向更适合导航和设置面板 2. 上下方向更适合临时辅助内容 3. 尽量提供清晰标题和明显关闭路径 4. 长内容建议分组排版 5. 对于复杂内容,尽量延迟加载内部数据 --- --- url: /docs/components/shimmer.md description: >- Theme-aware loading text with configurable sweep timing, spread, direction, and reduced-motion behavior. --- # Shimmer `ShimmerText` renders readable text with a moving highlight for short-lived loading or generated-content states. `ShimmerStyle` is the reusable appearance and timing value shared by `ShimmerText`, `Marker`, and attachment titles. The utility keeps text as the layout owner, so typography, wrapping, and truncation remain ordinary GPUI text behavior. It does not replace text with a skeleton block, own loading state, or announce progress to assistive technology. Keep a meaningful label in the text and let the surrounding application own the operation state. ## When to use * “Thinking…” or “Generating…” while an AI response is being produced. * File titles in an `Uploading` or `Processing` state. * Lightweight text placeholders for short background work. Use `Skeleton` for placeholder layout blocks, `Spinner` for a rotating indeterminate control, and plain text when the state does not need motion. ## Import ```rust use std::time::Duration; use gpui_kit::{ParentElement as _, Styled as _}; use gpui_kit::component::{ shimmer::{ShimmerStyle, ShimmerText}, ActiveTheme as _, }; ``` ## Basic usage Use the default theme-aware shimmer for a loading label: ```rust ShimmerText::new("Thinking…") ``` `ShimmerText` implements `Styled`, so it inherits the surrounding text style and can be refined like other GPUI elements: ```rust ShimmerText::new("Generating a response…") .text_sm() .text_color(cx.theme().muted_foreground) .max_w_full() ``` The default configuration is: | Property | Default | Meaning | | --- | --- | --- | | Duration | 2 seconds | One complete sweep. | | Highlight color | theme-aware | Derived from the active text/background/theme. | | Spread | relative `0.3` | Highlight half-width as a fraction of text width; a fixed `Pixels` width is also accepted. | | Direction | left to right | `reverse(false)`. | | Repetition | looping | `once(false)`. | | Reduced motion | static text | Animation frames are skipped while text remains visible. | The highlight follows the active theme rather than assuming a white highlight. This keeps the effect legible in both light and dark themes. An explicit color is available when a product's semantic accent requires it. ## Configure `ShimmerStyle` Create a reusable style when multiple labels should share the same motion: ```rust let loading_style = ShimmerStyle::new() .duration(Duration::from_secs(3)) .highlight_color(cx.theme().primary) .spread(0.45) .reverse(true) .once(false); ShimmerText::new("Indexing files…").with_shimmer_style(loading_style); ShimmerText::new("Building response…").with_shimmer_style(loading_style); ``` The individual configuration methods are also available directly on `ShimmerText`: ```rust ShimmerText::new("Uploading…") .duration(Duration::from_secs(4)) .spread(0.5) .reverse(true) .once(true) ``` Use `with_shimmer_style(...)` when the style is shared or built conditionally; use the direct methods when a one-off label is clearer. ### Duration `duration(...)` sets one complete sweep. Values below one millisecond are clamped to one millisecond, so a zero duration does not disable animation. Use `once(true)` for a one-shot effect or render ordinary text when the operation is not loading. ```rust ShimmerText::new("Preparing preview…") .duration(Duration::from_millis(900)) ``` ### Highlight color Leave `highlight_color` unset to use the theme-aware default. Use a semantic theme color when the loading state belongs to a product accent: ```rust ShimmerText::new("Syncing…") .highlight_color(cx.theme().primary) ``` An explicit color is painted over the text and must have enough contrast in both themes. Avoid raw palette values in component call sites; use `cx.theme().primary`, `muted_foreground`, or another semantic token. ### Spread `spread(...)` controls the highlight half-width. A bare `f32` is relative to the text width: finite values are clamped to the inclusive `0.05..=1.0` range. A `Pixels` value is an absolute half-width with a one-pixel minimum, keeping the band the same physical width across labels of different lengths. Non-finite values leave the current spread unchanged: ```rust ShimmerText::new("Loading a narrow label…").spread(0.15); ShimmerText::new("Loading a broad label…").spread(0.7); ShimmerText::new("Fixed-width highlight…").spread(px(48.)); ``` Use a smaller spread for dense status rows and a broader spread for a short assistant label. Prefer the relative form so the text's width remains the scale; use an absolute spread when aligned labels should share one band width. ### Direction and play-once `reverse(true)` sweeps from right to left. `once(true)` completes one sweep and does not loop: ```rust ShimmerText::new("Finalizing…") .reverse(true) .once(true) ``` There is no public angle, pause, progress, or RTL-specific builder. If a product needs those behaviors, keep the loading text static or own a separate animation component until the API is intentionally extended. ## Compose with Marker `Marker` uses `ShimmerStyle` only when it is loading with the `Shimmer` style. Use `MarkerContent::text(...)` to give the component a text run that can receive the highlight: ```rust use gpui_kit::component::marker::{Marker, MarkerContent, MarkerLoadingStyle}; Marker::new() .loading(true) .with_loading_style(MarkerLoadingStyle::Shimmer) .with_shimmer_style( ShimmerStyle::new() .duration(Duration::from_secs(3)) .spread(0.4) .reverse(true), ) .content(MarkerContent::new().text("Searching conversation history…")) ``` If `MarkerContent` contains only arbitrary elements, Marker uses a gentle opacity animation for the content slot instead of trying to repaint those elements as text. Icons and separator lines remain static. The spinner loading style does not use shimmer. ## Compose with Attachment An attachment title automatically shimmers while its inherited or explicit status is `Uploading` or `Processing`. Customize that title without replacing the attachment composition: ```rust use gpui_kit::component::attachment::{ Attachment, AttachmentContent, AttachmentDescription, AttachmentStatus, AttachmentTitle, }; Attachment::new() .status(AttachmentStatus::Processing) .content( AttachmentContent::new() .title( AttachmentTitle::new("transcript.pdf").with_shimmer_style( ShimmerStyle::new() .highlight_color(cx.theme().primary) .spread(0.45), ), ) .description(AttachmentDescription::new("Processing document…")), ) ``` The title's explicit status overrides the parent status. Generic children added with `AttachmentContent::child(...)` do not inherit attachment state because their concrete type is erased; use the typed title builder when the loading behavior matters. ## Use with messages and bubbles `ShimmerText` is an ordinary element and can be placed anywhere a text child is accepted: ```rust use gpui_kit::component::{ bubble::{Bubble, BubbleContent, BubbleVariant}, message::{Message, MessageContent}, }; Message::new() .content( MessageContent::new().bubble( Bubble::new() .with_variant(BubbleVariant::Ghost) .content(BubbleContent::new().child( ShimmerText::new("The assistant is thinking…"), )), ), ) ``` The application should switch from shimmer text to the final message content when generation completes. Do not leave an animated label running after the operation has ended. ## Styling, theme, and reduced motion `ShimmerText` implements `Styled`; style its font, size, color, wrapping, and layout at the call site: ```rust ShimmerText::new("Loading project data…") .text_base() .font_medium() .text_color(cx.theme().foreground) .max_w_full() ``` The animation reads the active theme's foreground, background, and dark/light mode when no explicit highlight color is provided. A custom theme therefore changes the default shimmer without requiring per-label overrides. Explicit colors remain the caller's responsibility for contrast. When `cx.reduce_motion()` is true, `ShimmerText` renders `StyledText` without requesting animation frames. Marker follows the same rule for typed text and keeps arbitrary content static. This is a rendering behavior, not a separate builder option; applications should keep the label meaningful in both modes. ## Accessibility guidance * Keep a meaningful text label visible to assistive technology. “Thinking…” or “Uploading report.pdf…” is more useful than an unlabeled animated band. * Do not rely on the highlight color, direction, or motion to communicate success, failure, or percentage. * Stop or replace the shimmer when the operation completes, fails, or is cancelled. * Respect reduced-motion preferences. The utility leaves static text in place, so no separate motion-only fallback is required. * Use semantic `Button` or `Link` controls for cancel, retry, and navigation; shimmer itself is not interactive. * Verify an explicit highlight color in both light and dark themes and avoid low-contrast combinations. ## When not to use Shimmer Shimmer communicates activity, not progress. * Use `Progress` for a known percentage. * Use `Spinner` for a compact rotating indicator. * Use `Skeleton` for multi-line placeholder layout. * Render ordinary text once a stable, completed, or failed state exists; do not leave the animation running. ## API reference ### `ShimmerStyle` | Method | Default | Purpose | | --- | --- | --- | | `new()` | same as `Default` | Create a theme-aware two-second looping style. | | `duration(Duration)` | 2 seconds | Set one sweep duration; clamps below 1 ms. | | `highlight_color(Hsla)` | theme-aware | Override the highlight color. | | `spread(f32 \| Pixels)` | relative `0.3` | Set half-width: `f32` is relative and clamps to `0.05..=1.0`; `Pixels` is absolute with a 1px minimum. | | `reverse(bool)` | `false` | Reverse the sweep direction. | | `once(bool)` | `false` | Play one sweep instead of looping. | ### `ShimmerText` | Method | Default | Purpose | | --- | --- | --- | | `new(text)` | default style, generated identity | Create loading text. | | `id(ElementId)` | text-based identity | Distinguish identical sibling labels. | | `with_shimmer_style(ShimmerStyle)` | default style | Apply a reusable configuration. | | `duration(Duration)` | 2 seconds | Set duration directly. | | `highlight_color(Hsla)` | theme-aware | Set color directly. | | `spread(f32 \| Pixels)` | relative `0.3` | Set spread directly. | | `reverse(bool)` | `false` | Set direction directly. | | `once(bool)` | `false` | Set repetition directly. | | `Styled` methods | inherited text style | Refine typography, color, wrapping, and layout. | ### Related components * \[`Marker`] — status rows with spinner or shimmer loading styles. * \[`AttachmentTitle`] — status-aware file title with shimmer customization. * \[`Progress`] — determinate progress. * \[`Spinner`] — compact indeterminate progress. [ShimmerStyle]: https://docs.rs/gpui-component/latest/gpui_component/shimmer/struct.ShimmerStyle.html [ShimmerText]: https://docs.rs/gpui-component/latest/gpui_component/shimmer/struct.ShimmerText.html [Marker]: https://docs.rs/gpui-component/latest/gpui_component/marker/struct.Marker.html [AttachmentTitle]: https://docs.rs/gpui-component/latest/gpui_component/attachment/struct.AttachmentTitle.html [Progress]: https://docs.rs/gpui-component/latest/gpui_component/progress/struct.Progress.html [Spinner]: https://docs.rs/gpui-component/latest/gpui_component/spinner/struct.Spinner.html --- --- url: /zh-CN/docs/components/shimmer.md description: 可复用、遵循主题和 reduced motion 的文字 loading 高光。 --- # Shimmer `ShimmerText` 为文字提供连续的 loading 高光,适合 thinking、上传中、处理中的短状态。`ShimmerStyle` 保存动画的时长、颜色、宽度、方向和是否只播放一次;它可以独立使用,也可以传给 `Marker` 或 `AttachmentTitle`。 Shimmer 只负责文字表现,不保存 loading 状态。是否显示 shimmer、什么时候改为完成文本,仍由应用状态决定。 ## 适用场景 * AI 回复生成中的 “正在思考…” 或 “正在生成…”。 * 文件标题处于 `Uploading` / `Processing` 状态。 * 轻量的文本占位或后台任务状态。 如果需要骨架布局或占位矩形,使用 `Skeleton`;如果需要旋转进度指示,使用 `Spinner`;如果状态只需要静态文字,不要增加动画。 ## 导入 ```rust use std::time::Duration; use gpui_kit::Styled as _; use gpui_kit::component::{ ActiveTheme as _, StyledExt as _, attachment::{ Attachment, AttachmentContent, AttachmentDescription, AttachmentStatus, AttachmentTitle, }, marker::{Marker, MarkerContent, MarkerLoadingStyle}, shimmer::{ShimmerStyle, ShimmerText}, }; ``` ## 基础用法 ```rust ShimmerText::new("正在思考…") ``` `ShimmerText` 默认使用当前文字上下文的字号、字体、颜色、换行和截断规则。它的默认配置为: | 配置 | 默认值 | 说明 | | --- | --- | --- | | `duration` | 两秒 | 完成一次从左到右的高光扫过。 | | `highlight_color` | 自动计算 | 根据文字颜色和当前主题生成明亮但可读的高光。 | | `spread` | 相对 `0.3` | 高光半宽占文字宽度的比例,也可以传 `Pixels` 设置固定宽度。 | | `reverse` | `false` | 从左向右移动。 | | `once` | `false` | 默认循环播放。 | 需要给同一文本的多个 sibling 设置独立动画身份时,可以设置 `.id(...)`: ```rust ShimmerText::new("正在生成…") .id("assistant-status") ``` ## ShimmerStyle 可以创建一个可复用配置,再传给多个文字: ```rust let processing_shimmer = ShimmerStyle::new() .duration(Duration::from_secs(3)) .spread(0.4) .reverse(true); ShimmerText::new("正在处理文件…") .with_shimmer_style(processing_shimmer); ``` `ShimmerText` 也提供同名的快捷 builder: ```rust ShimmerText::new("正在处理文件…") .duration(Duration::from_secs(3)) .spread(0.4) .reverse(true) ``` ### 时长 `duration(...)` 设置一次完整 sweep 的时长: ```rust ShimmerText::new("正在连接…") .duration(Duration::from_secs(4)) ``` 零时长会被限制为至少一毫秒,避免动画时钟失效。加载状态通常使用较慢的周期;短周期会增加注意力和运动感,应只用于确实需要强调的状态。 ### 颜色 默认高光会跟随文字颜色和亮/暗主题。产品有明确强调色时,可以使用主题中的语义 token: ```rust ShimmerText::new("正在同步…") .highlight_color(cx.theme().primary) ``` 也可以在共享样式中设置: ```rust let shimmer = ShimmerStyle::new() .highlight_color(cx.theme().info) .spread(0.35); ``` 不要在组件调用点写固定 hex 颜色;自定义颜色应来自当前主题或应用自己的 token 层,并在亮色和暗色主题中检查对比度。 ### Spread `spread(...)` 设置高光半宽。传 `f32` 表示相对文字宽度的比例,有限值会被限制在 `0.05..=1.0`;传 `Pixels` 表示固定的绝对半宽(最小一像素),适合让长短不一的对齐 label 共享同一条高光宽度: ```rust ShimmerText::new("正在上传…") .spread(0.15); // 窄高光 ShimmerText::new("正在上传…") .spread(0.75); // 宽高光 ShimmerText::new("正在上传…") .spread(px(48.)); // 固定宽度高光 ``` 非有限值会保留原配置。较窄的 spread 更克制,较宽的 spread 更容易被注意到。 ### 方向与单次播放 需要从右向左移动时使用 `reverse(true)`;`once(true)` 让高光完成一次 sweep 后停止: ```rust ShimmerText::new("正在准备结果…") .reverse(true) .once(true) ``` 反向只改变动画方向,不改变布局、文字颜色或可访问文本。`once` 适合把一次视觉提示与状态迁移联系起来,但它不会自动改变文字,也不会通知应用;状态完成后仍应由应用重新渲染普通文字。 ## 与 Marker 组合 Marker 的 Shimmer loading 会把 `MarkerContent::text(...)` 中的文字替换为 `ShimmerText`: ```rust Marker::new() .loading(true) .with_loading_style(MarkerLoadingStyle::Shimmer) .content(MarkerContent::new().text("正在思考…")) ``` 可以把统一配置传给 Marker: ```rust Marker::new() .loading(true) .with_loading_style(MarkerLoadingStyle::Shimmer) .with_shimmer_style( ShimmerStyle::new() .duration(Duration::from_secs(3)) .highlight_color(cx.theme().primary) .spread(0.4), ) .content(MarkerContent::new().text("正在生成回复…")) ``` 显式组合的 `MarkerIcon` 和 Separator 装饰线保持静止;如果需要 Spinner,应选择 `MarkerLoadingStyle::Spinner`。 ## 与 Attachment 组合 `AttachmentTitle` 在父级状态为 `Uploading` 或 `Processing` 时自动使用 shimmer: ```rust Attachment::new() .status(AttachmentStatus::Processing) .content( AttachmentContent::new() .title(AttachmentTitle::new("report.pdf")) .description(AttachmentDescription::new("正在生成预览")), ) ``` 标题可以覆盖自己的动画配置,父级 status 仍负责决定是否处于 loading: ```rust Attachment::new() .status(AttachmentStatus::Uploading) .content( AttachmentContent::new().title( AttachmentTitle::new("large-export.zip") .with_shimmer_style( ShimmerStyle::new() .duration(Duration::from_secs(3)) .spread(0.25) .once(true), ), ), ) ``` 如果调用方给 `AttachmentTitle` 设置了显式 `AttachmentStatus::Complete`,标题不会播放 shimmer;`.with_shimmer_style(...)` 只配置动画,不会开启 loading。 ## 在消息与气泡中使用 `ShimmerText` 是普通元素,任何接受文字 child 的位置都可以使用: ```rust use gpui_kit::component::{ bubble::{Bubble, BubbleContent, BubbleVariant}, message::{Message, MessageContent}, }; Message::new() .content( MessageContent::new().bubble( Bubble::new() .with_variant(BubbleVariant::Ghost) .content(BubbleContent::new().child( ShimmerText::new("助手正在思考…"), )), ), ) ``` 生成完成后应用应切换到最终消息内容,不要让动画在操作结束后继续运行。 ## 样式、主题与 reduced motion `ShimmerText` 实现 `Styled`,可以像普通文字一样调整字号、颜色、最大宽度和布局: ```rust ShimmerText::new("正在生成…") .text_sm() .font_medium() .text_color(cx.theme().muted_foreground) .max_w_full() ``` 动画文本保持 `StyledText` 的布局,因此会继承换行和截断规则。高光的背景与 foreground 由当前主题计算;应用应避免在父级同时设置难以读取的背景和文字颜色。 系统启用 reduced motion 时,`ShimmerText` 会直接渲染静态 `StyledText`,不会请求动画帧。应用无需额外写一个重复的动画分支,但仍应让文字本身完整表达状态。 ## 可访问性指引 * shimmer 是视觉提示,状态文字必须本身有意义,不能只显示无文本的高光。“正在思考…”或“正在上传 report.pdf…”比无标签的动画条更有用。 * 颜色和移动方向不能承担唯一语义;完成、失败、暂停等状态应由应用文字或控件表达。 * 操作完成、失败或取消时,停止或替换 shimmer。 * 高光不提供角度、RTL 自动适配或单独的 disable builder。需要关闭时,不渲染 `ShimmerText`,直接渲染普通文字;需要 RTL 语义时由应用布局和文本方向处理,也可以用 `.reverse(true)` 手动控制移动方向。 * 如果应用在 ShimmerText 周围增加 Button、Link 或 overlay,交互控件仍需要自己的可读 label 和键盘路径。 * 显式高光颜色应在亮色和暗色主题中都验证过,避免低对比组合。 ## 何时不使用 Shimmer * 需要表示确定百分比时使用 `Progress`。 * 需要持续旋转指示器时使用 `Spinner`。 * 需要多行占位布局时使用 `Skeleton`。 * 已经有稳定结果时直接渲染普通 `StyledText`,不要让动画继续运行。 ## API 参考 ### `ShimmerStyle` | 方法 | 默认值 | 说明 | | --- | --- | --- | | `new()` | 同 `Default` | 创建主题高光、两秒周期的循环配置。 | | `duration(Duration)` | 两秒 | 设置完整 sweep 时长;最小为一毫秒。 | | `highlight_color(Hsla)` | 主题计算 | 设置显式高光颜色,覆盖主题计算。 | | `spread(f32 \| Pixels)` | 相对 `0.3` | 设置高光半宽:`f32` 为相对比例(限制在 `0.05..=1.0`),`Pixels` 为绝对宽度(最小 1px)。 | | `reverse(bool)` | `false` | 设置是否从右向左移动。 | | `once(bool)` | `false` | 设置是否只完成一次 sweep。 | ### `ShimmerText` | 方法 | 默认值 | 说明 | | --- | --- | --- | | `new(text)` | 默认样式、按文字生成身份 | 创建 loading 文字。 | | `id(ElementId)` | 基于文字的身份 | 区分文字相同的 sibling。 | | `with_shimmer_style(ShimmerStyle)` | 默认样式 | 应用完整的可复用配置。 | | `duration(Duration)` | 两秒 | 直接设置时长。 | | `highlight_color(Hsla)` | 主题计算 | 直接设置颜色。 | | `spread(f32 \| Pixels)` | 相对 `0.3` | 直接设置半宽。 | | `reverse(bool)` | `false` | 直接设置方向。 | | `once(bool)` | `false` | 直接设置播放次数。 | | `Styled` 方法 | 继承文字样式 | 调整字号、颜色、宽度、字体和布局。 | ### 相关组件 * \[`Marker`] — 支持 spinner 或 shimmer loading 的状态行。 * \[`AttachmentTitle`] — 感知状态、可自定义 shimmer 的文件标题。 * \[`Progress`] — 确定进度。 * \[`Spinner`] — 紧凑的不确定进度指示。 [ShimmerStyle]: https://docs.rs/gpui-component/latest/gpui_component/shimmer/struct.ShimmerStyle.html [ShimmerText]: https://docs.rs/gpui-component/latest/gpui_component/shimmer/struct.ShimmerText.html [Marker]: https://docs.rs/gpui-component/latest/gpui_component/marker/struct.Marker.html [AttachmentTitle]: https://docs.rs/gpui-component/latest/gpui_component/attachment/struct.AttachmentTitle.html [Progress]: https://docs.rs/gpui-component/latest/gpui_component/progress/struct.Progress.html [Spinner]: https://docs.rs/gpui-component/latest/gpui_component/spinner/struct.Spinner.html --- --- url: /docs/components/sidebar.md description: >- A composable, themeable and customizable sidebar component for navigation and content organization. --- # Sidebar A flexible sidebar component that provides navigation structure for applications. Features collapsible states, nested menu items, header and footer sections, and responsive design. Perfect for creating application navigation panels, admin dashboards, and complex hierarchical interfaces. ## Import ```rust use gpui_kit::component::sidebar::{ Sidebar, SidebarHeader, SidebarFooter, SidebarGroup, SidebarMenu, SidebarMenuItem, SidebarToggleButton }; ``` ## Usage ### Basic Sidebar ```rust use gpui_kit::component::{sidebar::*, Side}; Sidebar::new() .header( SidebarHeader::new() .child("My Application") ) .child( SidebarGroup::new("Navigation") .child( SidebarMenu::new() .child( SidebarMenuItem::new("Dashboard") .icon(IconName::LayoutDashboard) .on_click(|_, _, _| println!("Dashboard clicked")) ) .child( SidebarMenuItem::new("Settings") .icon(IconName::Settings) .on_click(|_, _, _| println!("Settings clicked")) ) ) ) .footer( SidebarFooter::new() .child("User Profile") ) ``` ### Collapsible Sidebar ```rust let mut collapsed = false; Sidebar::new() .collapsed(collapsed) .collapsible(true) .header( SidebarHeader::new() .child( h_flex() .child(Icon::new(IconName::Home)) .when(!collapsed, |this| this.child("Home")) ) ) .child( SidebarGroup::new("Menu") .child( SidebarMenu::new() .child( SidebarMenuItem::new("Files") .icon(IconName::Folder) ) ) ) // Toggle button SidebarToggleButton::new() .collapsed(collapsed) .on_click(|_, _, _| { collapsed = !collapsed; }) ``` ### Nested Menu Items ```rust SidebarMenuItem::new("Projects") .icon(IconName::FolderOpen) .active(true) .children([ SidebarMenuItem::new("Web App") .active(false) .on_click(|_, _, _| println!("Web App selected")), SidebarMenuItem::new("Mobile App") .active(true) .on_click(|_, _, _| println!("Mobile App selected")), SidebarMenuItem::new("Desktop App") .on_click(|_, _, _| println!("Desktop App selected")), ]) .on_click(|_, _, _| { // Toggle project group }) ``` ### Multiple Groups ```rust Sidebar::new() .child( SidebarGroup::new("Main") .child( SidebarMenu::new() .child(SidebarMenuItem::new("Dashboard").icon(IconName::Home)) .child(SidebarMenuItem::new("Analytics").icon(IconName::BarChart)) ) ) .child( SidebarGroup::new("Content") .child( SidebarMenu::new() .child(SidebarMenuItem::new("Posts").icon(IconName::FileText)) .child(SidebarMenuItem::new("Media").icon(IconName::Image)) .child(SidebarMenuItem::new("Comments").icon(IconName::MessageCircle)) ) ) .child( SidebarGroup::new("Settings") .child( SidebarMenu::new() .child(SidebarMenuItem::new("General").icon(IconName::Settings)) .child(SidebarMenuItem::new("Users").icon(IconName::Users)) ) ) ``` ### With Badges and Suffixes ```rust use gpui_kit::component::{Badge, Switch}; SidebarMenuItem::new("Notifications") .icon(IconName::Bell) .suffix( Badge::new() .count(5) .child("5") ) SidebarMenuItem::new("Dark Mode") .icon(IconName::Moon) .suffix( Switch::new("dark-mode") .checked(true) .xsmall() ) SidebarMenuItem::new("Settings") .icon(IconName::Settings) .suffix(IconName::ChevronRight) ``` ### Right-Side Placement ```rust Sidebar::new() .side(Side::Right) .width(300) .header( SidebarHeader::new() .child("Right Panel") ) .child( SidebarGroup::new("Tools") .child( SidebarMenu::new() .child(SidebarMenuItem::new("Inspector").icon(IconName::Search)) .child(SidebarMenuItem::new("Console").icon(IconName::Terminal)) ) ) ``` ### Context Menus Add right-click context menus to sidebar menu items for additional actions: ```rust use gpui_kit::component::menu::PopupMenu; SidebarMenuItem::new("Project Files") .icon(IconName::Folder) .context_menu(|menu, _, _| { menu.link("Open in Editor", "https://editor.example.com") .separator() .menu_with_description("Rename", "Rename this project", Box::new(RenameAction)) .menu_with_description("Delete", "Delete this project", Box::new(DeleteAction)) .separator() .submenu("Share", |submenu| { submenu.menu("Copy Link", Box::new(CopyLinkAction)) .menu("Send via Email", Box::new(EmailAction)) }) }) // Multiple items with context menus SidebarMenu::new() .child( SidebarMenuItem::new("Documentation") .icon(IconName::BookOpen) .context_menu(|menu, _, _| { menu.menu("View Online", Box::new(ViewOnlineAction)) .menu("Download PDF", Box::new(DownloadPdfAction)) }) ) .child( SidebarMenuItem::new("Settings") .icon(IconName::Settings) .children([ SidebarMenuItem::new("General") .context_menu(|menu, _, _| { menu.menu("Reset to Defaults", Box::new(ResetAction)) }), SidebarMenuItem::new("Advanced") .context_menu(|menu, _, _| { menu.menu("Export Settings", Box::new(ExportAction)) .menu("Import Settings", Box::new(ImportAction)) }) ]) ) ``` ### Custom Width and Styling ```rust Sidebar::new() .width(280) // Custom width in pixels .border_width(2) // Custom border width .header( SidebarHeader::new() .p_4() // Custom padding .rounded(cx.theme().radius) .child("Custom Styled Sidebar") ) ``` ### Interactive Header with Popup Menu ```rust use gpui_kit::component::menu::DropdownMenu; SidebarHeader::new() .child( h_flex() .gap_2() .child(Icon::new(IconName::Building)) .child("Company Name") .child(Icon::new(IconName::ChevronsUpDown)) ) .dropdown_menu(|menu, _, _| { menu.menu("Acme Corp", Box::new(SelectCompany("acme"))) .menu("Tech Solutions", Box::new(SelectCompany("tech"))) .separator() .menu("Switch Organization", Box::new(SwitchOrg)) }) ``` ### Footer with User Information ```rust SidebarFooter::new() .justify_between() .child( h_flex() .gap_2() .child(Icon::new(IconName::User)) .when(!collapsed, |this| { this.child( v_flex() .child("John Doe") .child(div().text_xs().text_color(cx.theme().muted_foreground).child("john@example.com")) ) }) ) .when(!collapsed, |this| { this.child(Icon::new(IconName::MoreHorizontal)) }) ``` ### Responsive Sidebar ```rust let is_mobile = window_width < 768; Sidebar::new() .collapsed(is_mobile || manually_collapsed) .width(if is_mobile { 60 } else { 240 }) .header( SidebarHeader::new() .child( div() .when(!is_mobile, |this| this.child("Full App Name")) .when(is_mobile, |this| this.child(Icon::new(IconName::Menu))) ) ) ``` ## Theming The sidebar uses dedicated theme colors: ```rust // Theme colors used by sidebar cx.theme().sidebar // Background cx.theme().sidebar_foreground // Text color cx.theme().sidebar_border // Border color cx.theme().sidebar_accent // Hover/active background cx.theme().sidebar_accent_foreground // Hover/active text cx.theme().sidebar_primary // Primary elements cx.theme().sidebar_primary_foreground // Primary text ``` ## Examples ### File Explorer Sidebar ```rust Sidebar::new() .header( SidebarHeader::new() .child( h_flex() .gap_2() .child(IconName::Folder) .child("Explorer") ) ) .child( SidebarGroup::new("Folders") .child( SidebarMenu::new() .child( SidebarMenuItem::new("src") .icon(IconName::FolderOpen) .active(true) .children([ SidebarMenuItem::new("components") .icon(IconName::Folder), SidebarMenuItem::new("utils") .icon(IconName::Folder), SidebarMenuItem::new("main.rs") .icon(IconName::FileCode) .active(true), ]) ) .child( SidebarMenuItem::new("tests") .icon(IconName::Folder) ) .child( SidebarMenuItem::new("Cargo.toml") .icon(IconName::FileText) ) ) ) ``` ### Admin Dashboard Sidebar ```rust Sidebar::new() .header( SidebarHeader::new() .child( h_flex() .gap_2() .child( div() .size_8() .rounded_full() .bg(cx.theme().primary) .child(Icon::new(IconName::Crown)) ) .child("Admin Panel") ) ) .child( SidebarGroup::new("Overview") .child( SidebarMenu::new() .child( SidebarMenuItem::new("Dashboard") .icon(IconName::LayoutDashboard) .active(true) ) .child( SidebarMenuItem::new("Analytics") .icon(IconName::TrendingUp) .suffix(Badge::new().count(2)) ) ) ) .child( SidebarGroup::new("Management") .child( SidebarMenu::new() .child( SidebarMenuItem::new("Users") .icon(IconName::Users) .suffix("1,234") ) .child( SidebarMenuItem::new("Orders") .icon(IconName::ShoppingCart) .suffix(Badge::new().dot().variant_destructive()) ) .child( SidebarMenuItem::new("Products") .icon(IconName::Package) ) ) ) .footer( SidebarFooter::new() .child( h_flex() .gap_2() .child(IconName::User) .child("Administrator") ) .child(IconName::LogOut) ) ``` ### Settings Sidebar ```rust Sidebar::new() .width(300) .header( SidebarHeader::new() .child("Settings") ) .child( SidebarGroup::new("General") .child( SidebarMenu::new() .child( SidebarMenuItem::new("Appearance") .icon(IconName::Palette) .active(true) ) .child( SidebarMenuItem::new("Notifications") .icon(IconName::Bell) .suffix( Switch::new("notifications") .checked(true) .xsmall() ) ) .child( SidebarMenuItem::new("Privacy") .icon(IconName::Shield) ) ) ) .child( SidebarGroup::new("Advanced") .child( SidebarMenu::new() .child( SidebarMenuItem::new("Developer") .icon(IconName::Code) .children([ SidebarMenuItem::new("Debug Mode") .suffix( Switch::new("debug") .checked(false) .xsmall() ), SidebarMenuItem::new("Console") .on_click(|_, _, _| println!("Open console")), ]) ) .child( SidebarMenuItem::new("Performance") .icon(IconName::Zap) ) ) ) ``` --- --- url: /zh-CN/docs/components/sidebar.md description: 一个可组合、可主题化、可自定义的侧边栏组件。 --- # Sidebar Sidebar 是一个灵活的应用导航组件,支持折叠状态、嵌套菜单项、头部与底部区域以及响应式布局。它非常适合文件浏览器、后台管理面板和多层级导航界面。 ## 导入 ```rust use gpui_kit::component::sidebar::{ Sidebar, SidebarHeader, SidebarFooter, SidebarGroup, SidebarMenu, SidebarMenuItem, SidebarToggleButton }; ``` ## 用法 ### 基础 Sidebar ```rust use gpui_kit::component::{sidebar::*, Side}; Sidebar::new() .header( SidebarHeader::new() .child("My Application") ) .child( SidebarGroup::new("Navigation") .child( SidebarMenu::new() .child( SidebarMenuItem::new("Dashboard") .icon(IconName::LayoutDashboard) .on_click(|_, _, _| println!("Dashboard clicked")) ) .child( SidebarMenuItem::new("Settings") .icon(IconName::Settings) .on_click(|_, _, _| println!("Settings clicked")) ) ) ) .footer( SidebarFooter::new() .child("User Profile") ) ``` ### 可折叠 Sidebar ```rust let mut collapsed = false; Sidebar::new() .collapsed(collapsed) .collapsible(true) .header( SidebarHeader::new() .child( h_flex() .child(Icon::new(IconName::Home)) .when(!collapsed, |this| this.child("Home")) ) ) ``` 配合切换按钮: ```rust SidebarToggleButton::new() .collapsed(collapsed) .on_click(|_, _, _| { collapsed = !collapsed; }) ``` ### 嵌套菜单 ```rust SidebarMenuItem::new("Projects") .icon(IconName::FolderOpen) .active(true) .children([ SidebarMenuItem::new("Web App").active(false), SidebarMenuItem::new("Mobile App").active(true), SidebarMenuItem::new("Desktop App"), ]) ``` ### 多分组 ```rust Sidebar::new() .child( SidebarGroup::new("Main") .child( SidebarMenu::new() .child(SidebarMenuItem::new("Dashboard").icon(IconName::Home)) .child(SidebarMenuItem::new("Analytics").icon(IconName::BarChart)) ) ) ``` ### Badge 与后缀 ```rust use gpui_kit::component::{Badge, Switch}; SidebarMenuItem::new("Notifications") .icon(IconName::Bell) .suffix( Badge::new() .count(5) .child("5") ) ``` ### 右侧放置 ```rust Sidebar::new() .side(Side::Right) .width(300) .header( SidebarHeader::new() .child("Right Panel") ) ``` ### 右键菜单 ```rust use gpui_kit::component::menu::PopupMenu; SidebarMenuItem::new("Project Files") .icon(IconName::Folder) .context_menu(|menu, _, _| { menu.link("Open in Editor", "https://editor.example.com") .separator() .menu_with_description("Rename", "Rename this project", Box::new(RenameAction)) .menu_with_description("Delete", "Delete this project", Box::new(DeleteAction)) }) ``` ### 自定义宽度与样式 ```rust Sidebar::new() .width(280) .border_width(2) .header( SidebarHeader::new() .p_4() .rounded(cx.theme().radius) .child("Custom Styled Sidebar") ) ``` ## 主题 Sidebar 使用一组独立的主题颜色: ```rust cx.theme().sidebar cx.theme().sidebar_foreground cx.theme().sidebar_border cx.theme().sidebar_accent cx.theme().sidebar_accent_foreground cx.theme().sidebar_primary cx.theme().sidebar_primary_foreground ``` ## 示例 ### 文件浏览器 ```rust Sidebar::new() .header( SidebarHeader::new() .child( h_flex() .gap_2() .child(IconName::Folder) .child("Explorer") ) ) ``` ### 管理后台 ```rust Sidebar::new() .header( SidebarHeader::new() .child( h_flex() .gap_2() .child("Admin Panel") ) ) ``` ### 设置侧栏 ```rust Sidebar::new() .width(300) .header( SidebarHeader::new() .child("Settings") ) ``` --- --- url: /docs/components/skeleton.md description: Use to show a placeholder while content is loading. --- # Skeleton The Skeleton component displays animated placeholder content while actual content is loading. It provides visual feedback to users that content is being loaded and helps maintain layout structure during loading states. ## Import ```rust use gpui_kit::component::skeleton::Skeleton; ``` ## Usage ### Basic Skeleton ```rust Skeleton::new() ``` ### Text Line Skeleton ```rust // Single line of text Skeleton::new() .w(px(250.)) .h_4() .rounded_md() // Multiple text lines v_flex() .gap_2() .child(Skeleton::new().w(px(250.)).h_4().rounded_md()) .child(Skeleton::new().w(px(200.)).h_4().rounded_md()) .child(Skeleton::new().w(px(180.)).h_4().rounded_md()) ``` ### Circle Skeleton ```rust // Avatar placeholder Skeleton::new() .size_12() .rounded_full() // Profile picture placeholder Skeleton::new() .w(px(64.)) .h(px(64.)) .rounded_full() ``` ### Rectangle Skeleton ```rust // Card image placeholder Skeleton::new() .w(px(250.)) .h(px(125.)) .rounded_md() // Button placeholder Skeleton::new() .w(px(120.)) .h(px(40.)) .rounded_md() ``` ### Different Shapes ```rust // Text content Skeleton::new().w(px(200.)).h_4().rounded_sm() // Square image Skeleton::new().size_20().rounded_md() // Wide banner Skeleton::new().w_full().h(px(200.)).rounded_lg() // Small icon Skeleton::new().size_6().rounded_md() ``` ### Secondary Variant ```rust // Use secondary color (more subtle) Skeleton::new() .secondary() .w(px(200.)) .h_4() .rounded_md() ``` ## Animation The Skeleton component includes a built-in pulse animation that: * Runs continuously with a 2-second duration * Uses a bounce easing function with ease-in-out * Animates opacity from 100% to 50% and back * Automatically repeats to indicate loading state The animation cannot be disabled as it's essential for indicating loading state. ## Sizes The Skeleton component doesn't have predefined size variants. Instead, use gpui's sizing utilities: ```rust // Height utilities Skeleton::new().h_3() // 12px height Skeleton::new().h_4() // 16px height Skeleton::new().h_5() // 20px height Skeleton::new().h_6() // 24px height // Width utilities Skeleton::new().w(px(100.)) // 100px width Skeleton::new().w(px(200.)) // 200px width Skeleton::new().w_full() // Full width Skeleton::new().w_1_2() // 50% width // Square sizes Skeleton::new().size_4() // 16x16px Skeleton::new().size_8() // 32x32px Skeleton::new().size_12() // 48x48px Skeleton::new().size_16() // 64x64px ``` ## Examples ### Loading Profile Card ```rust v_flex() .gap_4() .p_4() .border_1() .border_color(cx.theme().border) .rounded(cx.theme().radius_lg) .child( h_flex() .gap_3() .items_center() .child(Skeleton::new().size_12().rounded_full()) // Avatar .child( v_flex() .gap_2() .child(Skeleton::new().w(px(120.)).h_4().rounded_md()) // Name .child(Skeleton::new().w(px(100.)).h_3().rounded_md()) // Email ) ) .child( v_flex() .gap_2() .child(Skeleton::new().w_full().h_4().rounded_md()) // Bio line 1 .child(Skeleton::new().w(px(200.)).h_4().rounded_md()) // Bio line 2 ) ``` ### Loading Article List ```rust v_flex() .gap_6() .children((0..3).map(|_| { h_flex() .gap_4() .child(Skeleton::new().w(px(120.)).h(px(80.)).rounded_md()) // Thumbnail .child( v_flex() .gap_2() .flex_1() .child(Skeleton::new().w_full().h_5().rounded_md()) // Title .child(Skeleton::new().w(px(300.)).h_4().rounded_md()) // Excerpt line 1 .child(Skeleton::new().w(px(250.)).h_4().rounded_md()) // Excerpt line 2 .child(Skeleton::new().w(px(100.)).h_3().rounded_md()) // Date ) })) ``` ### Loading Table Rows ```rust v_flex() .gap_2() .children((0..5).map(|_| { h_flex() .gap_4() .p_3() .border_b_1() .border_color(cx.theme().border) .child(Skeleton::new().size_8().rounded_full()) // Status indicator .child(Skeleton::new().w(px(150.)).h_4().rounded_md()) // Name .child(Skeleton::new().w(px(200.)).h_4().rounded_md()) // Email .child(Skeleton::new().w(px(80.)).h_4().rounded_md()) // Role .child(Skeleton::new().w(px(60.)).h_4().rounded_md()) // Actions })) ``` ### Loading Button States ```rust h_flex() .gap_3() .child(Skeleton::new().w(px(80.)).h(px(36.)).rounded_md()) // Primary button .child(Skeleton::new().w(px(70.)).h(px(36.)).rounded_md()) // Secondary button .child(Skeleton::new().size_9().rounded_md()) // Icon button ``` ### Loading Form Fields ```rust v_flex() .gap_4() .child( v_flex() .gap_1() .child(Skeleton::new().w(px(60.)).h_4().rounded_md()) // Label .child(Skeleton::new().w_full().h(px(40.)).rounded_md()) // Input ) .child( v_flex() .gap_1() .child(Skeleton::new().w(px(80.)).h_4().rounded_md()) // Label .child(Skeleton::new().w_full().h(px(120.)).rounded_md()) // Textarea ) ``` ### Conditional Loading ```rust if loading { Skeleton::new().w(px(200.)).h_4().rounded_md() } else { div().child("Actual content here") } ``` ## Theming The Skeleton component uses the theme's `skeleton` color, which defaults to the `secondary` color if not specified. You can customize it in your theme: ```json { "skeleton.background": "#e2e8f0" } ``` The `secondary(true)` variant applies 50% opacity to the skeleton color for more subtle loading indicators. --- --- url: /zh-CN/docs/components/skeleton.md description: 在内容加载时显示占位骨架。 --- # Skeleton Skeleton 会在真实内容尚未加载完成时显示带动画的占位块,为用户提供加载反馈,并尽量保持界面布局稳定。 ## 导入 ```rust use gpui_kit::component::skeleton::Skeleton; ``` ## 用法 ### 基础 Skeleton ```rust Skeleton::new() ``` ### 文本行占位 ```rust Skeleton::new() .w(px(250.)) .h_4() .rounded_md() v_flex() .gap_2() .child(Skeleton::new().w(px(250.)).h_4().rounded_md()) .child(Skeleton::new().w(px(200.)).h_4().rounded_md()) .child(Skeleton::new().w(px(180.)).h_4().rounded_md()) ``` ### 圆形占位 ```rust Skeleton::new() .size_12() .rounded_full() Skeleton::new() .w(px(64.)) .h(px(64.)) .rounded_full() ``` ### 矩形占位 ```rust Skeleton::new() .w(px(250.)) .h(px(125.)) .rounded_md() Skeleton::new() .w(px(120.)) .h(px(40.)) .rounded_md() ``` ### 不同形状 ```rust Skeleton::new().w(px(200.)).h_4().rounded_sm() Skeleton::new().size_20().rounded_md() Skeleton::new().w_full().h(px(200.)).rounded_lg() Skeleton::new().size_6().rounded_md() ``` ### Secondary 变体 ```rust Skeleton::new() .secondary() .w(px(200.)) .h_4() .rounded_md() ``` ## 动画 Skeleton 内置脉冲动画,行为如下: * 持续循环播放,周期为 2 秒 * 使用 bounce easing,并带有 ease-in-out 变化 * 透明度会在 100% 和 50% 之间往返变化 * 自动重复,以持续表达“内容正在加载” 该动画不可关闭,因为它本身就是加载状态的重要视觉提示。 ## 尺寸 Skeleton 没有预设的尺寸枚举,通常配合 gpui 的尺寸工具使用: ```rust Skeleton::new().h_3() Skeleton::new().h_4() Skeleton::new().h_5() Skeleton::new().h_6() Skeleton::new().w(px(100.)) Skeleton::new().w(px(200.)) Skeleton::new().w_full() Skeleton::new().w_1_2() Skeleton::new().size_4() Skeleton::new().size_8() Skeleton::new().size_12() Skeleton::new().size_16() ``` ## 示例 ### 资料卡片加载中 ```rust v_flex() .gap_4() .p_4() .border_1() .border_color(cx.theme().border) .rounded(cx.theme().radius_lg) .child( h_flex() .gap_3() .items_center() .child(Skeleton::new().size_12().rounded_full()) .child( v_flex() .gap_2() .child(Skeleton::new().w(px(120.)).h_4().rounded_md()) .child(Skeleton::new().w(px(100.)).h_3().rounded_md()) ) ) .child( v_flex() .gap_2() .child(Skeleton::new().w_full().h_4().rounded_md()) .child(Skeleton::new().w(px(200.)).h_4().rounded_md()) ) ``` ### 文章列表加载中 ```rust v_flex() .gap_6() .children((0..3).map(|_| { h_flex() .gap_4() .child(Skeleton::new().w(px(120.)).h(px(80.)).rounded_md()) .child( v_flex() .gap_2() .flex_1() .child(Skeleton::new().w_full().h_5().rounded_md()) .child(Skeleton::new().w(px(300.)).h_4().rounded_md()) .child(Skeleton::new().w(px(250.)).h_4().rounded_md()) .child(Skeleton::new().w(px(100.)).h_3().rounded_md()) ) })) ``` ### 表格行加载中 ```rust v_flex() .gap_2() .children((0..5).map(|_| { h_flex() .gap_4() .p_3() .border_b_1() .border_color(cx.theme().border) .child(Skeleton::new().size_8().rounded_full()) .child(Skeleton::new().w(px(150.)).h_4().rounded_md()) .child(Skeleton::new().w(px(200.)).h_4().rounded_md()) .child(Skeleton::new().w(px(80.)).h_4().rounded_md()) .child(Skeleton::new().w(px(60.)).h_4().rounded_md()) })) ``` ### 按钮加载态 ```rust h_flex() .gap_3() .child(Skeleton::new().w(px(80.)).h(px(36.)).rounded_md()) .child(Skeleton::new().w(px(70.)).h(px(36.)).rounded_md()) .child(Skeleton::new().size_9().rounded_md()) ``` ### 表单字段加载态 ```rust v_flex() .gap_4() .child( v_flex() .gap_1() .child(Skeleton::new().w(px(60.)).h_4().rounded_md()) .child(Skeleton::new().w_full().h(px(40.)).rounded_md()) ) .child( v_flex() .gap_1() .child(Skeleton::new().w(px(80.)).h_4().rounded_md()) .child(Skeleton::new().w_full().h(px(120.)).rounded_md()) ) ``` ### 条件加载 ```rust if loading { Skeleton::new().w(px(200.)).h_4().rounded_md() } else { div().child("Actual content here") } ``` ## 主题 Skeleton 默认使用主题中的 `skeleton` 颜色;如果未配置,则回退到 `secondary`。你可以在主题中这样覆盖: ```json { "skeleton.background": "#e2e8f0" } ``` `secondary(true)` 变体会对骨架颜色应用 50% 透明度,让占位效果更柔和。 --- --- url: /skills.md description: GPUI Kit Skills - Available skills for working with GPUI Kit --- --- --- url: /zh-CN/skills.md description: 适用于 GPUI Kit 的开发技能与实践指南。 --- --- --- url: /base/primitives/slider.md description: >- A state-driven range input with independently styleable track, indicator, and thumb. --- # Slider A state-driven range input with independently styleable track, indicator, and thumb. Like every `gpui-base` primitive, Slider supplies behavior and semantic structure without imposing a product visual language. Apply GPUI styles and compose the exported parts to match your design system. ## Example The [single native Cargo entrypoint](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/components.rs) selects this primitive from the [shared showcase implementation](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/mod.rs). The same showcase is compiled once for the WASM preview above. ```bash cargo run -p gpui-base --example components -- slider ``` ## Import ```rust use gpui_kit::base::{Slider, SliderState}; ``` ## Anatomy and API The example composes `Slider`, `SliderState`. GPUI's standard styling and event traits provide presentation; these base types provide the interaction structure. The authoritative module is [`components/slider.rs`](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/components/slider.rs). Native and browser previews compile this same file. ## State and events `SliderState` owns bounds and value; track, indicator, and thumb are separate visual parts. Keep controlled state on the parent render type or in a GPUI entity. Update it in callbacks and call `cx.notify()`; do not recreate persistent entities during every render. ## Complete Rust example The complete implementation used by the runnable showcase is embedded directly from Rust source: The command above supplies application initialization, window creation, and shared `BaseShowcase` state. ## Accessibility Expose label, current value, and bounds; support keyboard increments. ## Notes Use stable element IDs where accepted. Verify focus, hover, active, selected, disabled, reduced-motion, and high-contrast appearances in the consuming design system. --- --- url: /zh-CN/base/primitives/slider.md description: 轨道、已选区和滑块可独立设置样式的状态驱动范围输入。 --- # Slider 轨道、已选区和滑块可独立设置样式的状态驱动范围输入。 和所有 GPUI Base 原语一样,Slider 只提供行为和语义结构,不规定产品视觉语言。请使用 GPUI 样式并组合导出的部件,使其符合你的设计系统。 ## 示例 原生示例和页面上方的 WASM 预览共用同一份实现: ```bash cargo run -p gpui-base --example components -- slider ``` ## 导入 ```rust use gpui_kit::base::{Slider, SliderState}; ``` ## 结构与 API 示例组合上述公开类型。GPUI 的标准样式和事件 trait 负责表现,Base 类型负责交互结构。权威实现位于 [`components/slider.rs`](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/components/slider.rs),原生与浏览器预览编译的是同一文件。 ## 状态与事件 SliderState 持久保存值和范围,拖动及键盘操作更新它。 受控状态应保存在父渲染类型或 GPUI entity 中;在回调中更新并调用 `cx.notify()`,不要在每次渲染时重建持久 entity。 ## 完整 Rust 示例 ## 可访问性 暴露范围、当前值和方向,并保留方向键、Page Up/Down 等键盘操作。 ## 注意事项 在支持的位置使用稳定元素 ID,并在消费端设计系统中验证焦点、悬停、按下、选中、禁用、减少动态效果和高对比度状态。 --- --- url: /docs/components/slider.md description: >- A control that allows the user to select values from a range using a draggable thumb. --- # Slider A slider component for selecting numeric values within a specified range. Supports both single value and range selection modes, horizontal and vertical orientations, custom styling, and step intervals. ## Import ```rust use gpui_kit::component::slider::{Slider, SliderState, SliderEvent, SliderValue}; ``` ## Usage ### Basic Slider ```rust let slider_state = cx.new(|_| { SliderState::new() .min(0.0) .max(100.0) .default_value(50.0) .step(1.0) }); Slider::new(&slider_state) ``` ### Slider with Event Handling ```rust struct MyView { slider_state: Entity, current_value: f32, } impl MyView { fn new(cx: &mut Context) -> Self { let slider_state = cx.new(|_| { SliderState::new() .min(0.0) .max(100.0) .default_value(25.0) .step(5.0) }); let subscription = cx.subscribe(&slider_state, |this, _, event: &SliderEvent, cx| { match event { SliderEvent::Change(value) => { this.current_value = value.start(); cx.notify(); } } }); Self { slider_state, current_value: 25.0, } } } impl Render for MyView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .gap_2() .child(Slider::new(&self.slider_state)) .child(format!("Value: {}", self.current_value)) } } ``` ### Range Slider ```rust let range_slider = cx.new(|_| { SliderState::new() .min(0.0) .max(100.0) .default_value(20.0..80.0) // Range from 20 to 80 .step(1.0) }); Slider::new(&range_slider) ``` ### Vertical Slider ```rust Slider::new(&slider_state) .vertical() .h(px(200.)) ``` ### Custom Step Intervals ```rust // Integer steps let integer_slider = cx.new(|_| { SliderState::new() .min(0.0) .max(10.0) .step(1.0) .default_value(5.0) }); // Decimal steps let decimal_slider = cx.new(|_| { SliderState::new() .min(0.0) .max(1.0) .step(0.01) .default_value(0.5) }); ``` ### Min/Max Configuration ```rust // Temperature slider let temp_slider = cx.new(|_| { SliderState::new() .min(-10.0) .max(40.0) .default_value(20.0) .step(0.5) }); // Percentage slider let percent_slider = cx.new(|_| { SliderState::new() .min(0.0) .max(100.0) .default_value(75.0) .step(5.0) }); ``` ### Disabled State ```rust Slider::new(&slider_state) .disabled(true) ``` ### Custom Styling ```rust Slider::new(&slider_state) .bg(cx.theme().success) .text_color(cx.theme().success_foreground) .rounded(px(4.)) ``` ### Scale There have 2 types of scale for the slider: * `Linear` (default) * `Logarithmic` The logarithmic scale is useful when the range of values is large and you want to give more precision to smaller values. ```rust let log_slider = cx.new(|_| { SliderState::new() .min(1.0) // min must be greater than 0 for log scale .max(1000.0) .default_value(10.0) .step(1.0) .scale(SliderScale::Logarithmic) }); ``` In this case: :::info $$ v = min \times (max/min)^p $$ The value `v` is calculated using the formula above, where `p` is the slider percentage (0 to 1). ::: * If slider at 25%, value will be approximately `5.62`. * If slider at 50%, value will be approximately `31.62`. * If slider at 75%, value will be approximately `177.83`. * If slider at 100%, value will be `1000.0`. #### Conversions ```rust // From f32 let single_value: SliderValue = 42.0.into(); // From tuple let range_value: SliderValue = (10.0, 90.0).into(); // From Range let range_value: SliderValue = (10.0..90.0).into(); ``` ### SliderEvent | Event | Description | | ---------------------- | --------------------------------------------------------------- | | `Change(SliderValue)` | Emitted continuously while the slider value is being changed | | `Release(SliderValue)` | Emitted once when the user releases the slider after interaction | ### Styling The slider component implements `Styled` trait and supports: * Background color for track and thumb * Text color for thumb * Border radius * Size customization ## Examples ### Color Picker ```rust struct ColorPicker { hue_slider: Entity, saturation_slider: Entity, lightness_slider: Entity, alpha_slider: Entity, current_color: Hsla, } impl ColorPicker { fn new(cx: &mut Context) -> Self { let hue_slider = cx.new(|_| { SliderState::new() .min(0.0) .max(1.0) .step(0.01) .default_value(0.5) }); let saturation_slider = cx.new(|_| { SliderState::new() .min(0.0) .max(1.0) .step(0.01) .default_value(1.0) }); // Subscribe to all sliders to update color let subscriptions = [&hue_slider, &saturation_slider /* ... */] .iter() .map(|slider| { cx.subscribe(slider, |this, _, event: &SliderEvent, cx| { match event { SliderEvent::Change(_) => { this.update_color(cx); } } }) }) .collect::>(); Self { hue_slider, saturation_slider, // ... other fields } } fn update_color(&mut self, cx: &mut Context) { let h = self.hue_slider.read(cx).value().start(); let s = self.saturation_slider.read(cx).value().start(); // ... calculate color self.current_color = hsla(h, s, l, a); cx.notify(); } } impl Render for ColorPicker { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { h_flex() .gap_4() .child( v_flex() .gap_2() .child("Hue") .child(Slider::new(&self.hue_slider).vertical().h(px(120.))) ) .child( v_flex() .gap_2() .child("Saturation") .child(Slider::new(&self.saturation_slider).vertical().h(px(120.))) ) // ... other sliders } } ``` ### Volume Control ```rust struct VolumeControl { volume_slider: Entity, volume: f32, } impl VolumeControl { fn new(cx: &mut Context) -> Self { let volume_slider = cx.new(|_| { SliderState::new() .min(0.0) .max(100.0) .step(1.0) .default_value(50.0) }); let subscription = cx.subscribe(&volume_slider, |this, _, event: &SliderEvent, cx| { match event { SliderEvent::Change(value) => { this.volume = value.start(); this.apply_volume_change(); cx.notify(); } } }); Self { volume_slider, volume: 50.0, } } fn apply_volume_change(&self) { // Apply volume change to audio system println!("Volume changed to: {}%", self.volume); } } impl Render for VolumeControl { fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { h_flex() .items_center() .gap_3() .child("🔊") .child(Slider::new(&self.volume_slider).flex_1()) .child(format!("{}%", self.volume as i32)) } } ``` ### Price Range Filter ```rust struct PriceFilter { price_range: Entity, min_price: f32, max_price: f32, } impl PriceFilter { fn new(cx: &mut Context) -> Self { let price_range = cx.new(|_| { SliderState::new() .min(0.0) .max(1000.0) .step(10.0) .default_value(100.0..500.0) // Range slider }); let subscription = cx.subscribe(&price_range, |this, _, event: &SliderEvent, cx| { match event { SliderEvent::Change(value) => { this.min_price = value.start(); this.max_price = value.end(); this.filter_products(); cx.notify(); } } }); Self { price_range, min_price: 100.0, max_price: 500.0, } } fn filter_products(&self) { println!("Filtering products: ${} - ${}", self.min_price, self.max_price); } } impl Render for PriceFilter { fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { v_flex() .gap_2() .child("Price Range") .child(Slider::new(&self.price_range)) .child(format!("${} - ${}", self.min_price as i32, self.max_price as i32)) } } ``` ### Temperature Slider with Custom Styling ```rust struct TemperatureControl { temp_slider: Entity, temperature: f32, } impl Render for TemperatureControl { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let temp_color = if self.temperature < 10.0 { cx.theme().info // Cold - blue } else if self.temperature > 25.0 { cx.theme().destructive // Hot - red } else { cx.theme().success // Comfortable - green }; v_flex() .gap_3() .child("Temperature Control") .child( Slider::new(&self.temp_slider) .bg(temp_color) .text_color(cx.theme().background) .rounded(px(8.)) ) .child(format!("{}°C", self.temperature as i32)) } } ``` ## Keyboard Shortcuts | Key | Action | | ------------- | ------------------------------ | | `←` / `↓` | Decrease value by step | | `→` / `↑` | Increase value by step | | `Page Down` | Decrease by larger amount | | `Page Up` | Increase by larger amount | | `Home` | Set to minimum value | | `End` | Set to maximum value | | `Tab` | Move focus to next element | | `Shift + Tab` | Move focus to previous element | --- --- url: /zh-CN/docs/components/slider.md description: 通过拖动滑块在区间内选择数值的控件。 --- # Slider Slider 用于在给定范围内选择数值,支持单值和区间选择、横向和纵向布局、自定义样式以及步进控制。 ## 导入 ```rust use gpui_kit::component::slider::{Slider, SliderState, SliderEvent, SliderValue}; ``` ## 用法 ### 基础 Slider ```rust let slider_state = cx.new(|_| { SliderState::new() .min(0.0) .max(100.0) .default_value(50.0) .step(1.0) }); Slider::new(&slider_state) ``` ### 处理事件 ```rust struct MyView { slider_state: Entity, current_value: f32, } impl MyView { fn new(cx: &mut Context) -> Self { let slider_state = cx.new(|_| { SliderState::new() .min(0.0) .max(100.0) .default_value(25.0) .step(5.0) }); let subscription = cx.subscribe(&slider_state, |this, _, event: &SliderEvent, cx| { match event { SliderEvent::Change(value) => { this.current_value = value.start(); cx.notify(); } } }); Self { slider_state, current_value: 25.0, } } } impl Render for MyView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .gap_2() .child(Slider::new(&self.slider_state)) .child(format!("Value: {}", self.current_value)) } } ``` ### 区间 Slider ```rust let range_slider = cx.new(|_| { SliderState::new() .min(0.0) .max(100.0) .default_value(20.0..80.0) .step(1.0) }); Slider::new(&range_slider) ``` ### 纵向 Slider ```rust Slider::new(&slider_state) .vertical() .h(px(200.)) ``` ### 自定义步进 ```rust let integer_slider = cx.new(|_| { SliderState::new() .min(0.0) .max(10.0) .step(1.0) .default_value(5.0) }); let decimal_slider = cx.new(|_| { SliderState::new() .min(0.0) .max(1.0) .step(0.01) .default_value(0.5) }); ``` ### 最小值和最大值 ```rust let temp_slider = cx.new(|_| { SliderState::new() .min(-10.0) .max(40.0) .default_value(20.0) .step(0.5) }); let percent_slider = cx.new(|_| { SliderState::new() .min(0.0) .max(100.0) .default_value(75.0) .step(5.0) }); ``` ### 禁用状态 ```rust Slider::new(&slider_state) .disabled(true) ``` ### 自定义样式 ```rust Slider::new(&slider_state) .bg(cx.theme().success) .text_color(cx.theme().success_foreground) .rounded(px(4.)) ``` ### Scale Slider 支持两种比例模式: * `Linear` * `Logarithmic` 对数比例适合跨度很大的取值范围,可以让较小数值获得更细的控制精度。 ```rust let log_slider = cx.new(|_| { SliderState::new() .min(1.0) .max(1000.0) .default_value(10.0) .step(1.0) .scale(SliderScale::Logarithmic) }); ``` 在这种模式下: :::info $$ v = min \times (max/min)^p $$ 其中 `p` 是滑块位置对应的百分比,范围为 0 到 1。 ::: * 当滑块在 25% 时,值约为 `5.62` * 当滑块在 50% 时,值约为 `31.62` * 当滑块在 75% 时,值约为 `177.83` * 当滑块在 100% 时,值为 `1000.0` #### 类型转换 ```rust let single_value: SliderValue = 42.0.into(); let range_value: SliderValue = (10.0, 90.0).into(); let range_value: SliderValue = (10.0..90.0).into(); ``` ### SliderEvent | 事件 | 说明 | | --- | --- | | `Change(SliderValue)` | 滑块值变化过程中持续触发 | | `Release(SliderValue)` | 拖拽结束(松开鼠标)时触发一次 | ### 样式 Slider 实现了 `Styled` trait,支持: * 轨道和滑块背景色 * 滑块文本颜色 * 圆角 * 尺寸定制 ## 示例 ### 颜色选择器 ```rust struct ColorPicker { hue_slider: Entity, saturation_slider: Entity, lightness_slider: Entity, alpha_slider: Entity, current_color: Hsla, } impl ColorPicker { fn new(cx: &mut Context) -> Self { let hue_slider = cx.new(|_| { SliderState::new() .min(0.0) .max(1.0) .step(0.01) .default_value(0.5) }); let saturation_slider = cx.new(|_| { SliderState::new() .min(0.0) .max(1.0) .step(0.01) .default_value(1.0) }); let subscriptions = [&hue_slider, &saturation_slider /* ... */] .iter() .map(|slider| { cx.subscribe(slider, |this, _, event: &SliderEvent, cx| { match event { SliderEvent::Change(_) => { this.update_color(cx); } } }) }) .collect::>(); Self { hue_slider, saturation_slider, // ... other fields } } fn update_color(&mut self, cx: &mut Context) { let h = self.hue_slider.read(cx).value().start(); let s = self.saturation_slider.read(cx).value().start(); // ... 计算颜色 self.current_color = hsla(h, s, l, a); cx.notify(); } } ``` ### 音量控制 ```rust struct VolumeControl { volume_slider: Entity, volume: f32, } impl VolumeControl { fn new(cx: &mut Context) -> Self { let volume_slider = cx.new(|_| { SliderState::new() .min(0.0) .max(100.0) .step(1.0) .default_value(50.0) }); let subscription = cx.subscribe(&volume_slider, |this, _, event: &SliderEvent, cx| { match event { SliderEvent::Change(value) => { this.volume = value.start(); this.apply_volume_change(); cx.notify(); } } }); Self { volume_slider, volume: 50.0, } } fn apply_volume_change(&self) { println!("Volume changed to: {}%", self.volume); } } ``` ### 价格区间筛选 ```rust struct PriceFilter { price_range: Entity, min_price: f32, max_price: f32, } impl PriceFilter { fn new(cx: &mut Context) -> Self { let price_range = cx.new(|_| { SliderState::new() .min(0.0) .max(1000.0) .step(10.0) .default_value(100.0..500.0) }); let subscription = cx.subscribe(&price_range, |this, _, event: &SliderEvent, cx| { match event { SliderEvent::Change(value) => { this.min_price = value.start(); this.max_price = value.end(); this.filter_products(); cx.notify(); } } }); Self { price_range, min_price: 100.0, max_price: 500.0, } } fn filter_products(&self) { println!("Filtering products: ${} - ${}", self.min_price, self.max_price); } } ``` ## 键盘快捷键 | 按键 | 操作 | | --- | --- | | `←` / `↓` | 按步进减小数值 | | `→` / `↑` | 按步进增大数值 | | `Page Down` | 较大幅度减小数值 | | `Page Up` | 较大幅度增大数值 | | `Home` | 设置为最小值 | | `End` | 设置为最大值 | | `Tab` | 焦点移动到下一个元素 | | `Shift + Tab` | 焦点移动到上一个元素 | --- --- url: /docs/components/spinner.md description: Displays an animated loading showing the completion progress of a task. --- # Spinner Spinner element displays an animated loading. Perfect for showing loading states, progress spinners, and other visual feedback during asynchronous operations. Features customizable icons, colors, sizes, and rotation animations. ## Import ```rust use gpui_kit::component::spinner::Spinner; ``` ## Usage ### Basic ```rust // Default loader icon Spinner::new() ``` ### Spinner with Custom Color ```rust use gpui_kit::component::ActiveTheme; // Blue spinner Spinner::new() .color(cx.theme().blue) // Green spinner for success states Spinner::new() .color(cx.theme().green) // Custom color Spinner::new() .color(cx.theme().cyan) ``` ### Spinner Sizes ```rust // Extra small spinner Spinner::new().xsmall() // Small spinner Spinner::new().small() // Medium spinner (default) Spinner::new() // Large spinner Spinner::new().large() // Custom size Spinner::new().with_size(px(64.)) ``` ### Spinner with Custom Icon ```rust use gpui_kit::component::IconName; // Loading circle icon Spinner::new() .icon(IconName::LoaderCircle) // Large loading circle with custom color Spinner::new() .icon(IconName::LoaderCircle) .large() .color(cx.theme().cyan) // Different loading icons Spinner::new() .icon(IconName::Loader) .color(cx.theme().primary) ``` ## Available Icons The Spinner component supports various loading and progress icons: ### Loading Icons * `Loader` (default) - Rotating line spinner * `LoaderCircle` - Circular loading spinner ### Other Compatible Icons * Any icon from the `IconName` enum can be used, though loading-specific icons work best with the rotation animation ## Animation The Spinner component features a built-in rotation animation: * **Duration**: 0.8 seconds (configurable via speed parameter) * **Easing**: Ease-in-out transition * **Repeat**: Infinite loop * **Transform**: 360-degree rotation ## Size Reference | Size | Method | Approximate Pixels | | ----------- | ------------------- | ------------------ | | Extra Small | `.xsmall()` | ~12px | | Small | `.small()` | ~14px | | Medium | (default) | ~16px | | Large | `.large()` | ~24px | | Custom | `.with_size(px(n))` | n px | ## Examples ### Loading States ```rust // Simple loading spinner Spinner::new() // Loading with custom color Spinner::new() .color(cx.theme().blue) // Large loading spinner Spinner::new() .large() .color(cx.theme().primary) ``` ### Different Loading Icons ```rust // Default loader (line spinner) Spinner::new() .color(cx.theme().muted_foreground) // Circle loader Spinner::new() .icon(IconName::LoaderCircle) .color(cx.theme().blue) // Large circle loader with custom color Spinner::new() .icon(IconName::LoaderCircle) .large() .color(cx.theme().green) ``` ### Status Spinners ```rust // Loading state Spinner::new() .small() .color(cx.theme().muted_foreground) // Processing state Spinner::new() .icon(IconName::LoaderCircle) .color(cx.theme().blue) // Success processing (still animating) Spinner::new() .icon(IconName::LoaderCircle) .color(cx.theme().green) ``` ### Size Variations ```rust // Extra small for inline text Spinner::new() .xsmall() .color(cx.theme().muted_foreground) // Small for buttons Spinner::new() .small() .color(cx.theme().primary_foreground) // Medium for general use (default) Spinner::new() .color(cx.theme().primary) // Large for prominent loading states Spinner::new() .large() .color(cx.theme().blue) // Custom size for specific requirements Spinner::new() .with_size(px(32.)) .color(cx.theme().orange) ``` ### In UI Components ```rust // In a button Button::new("submit-btn") .loading(true) .icon( Spinner::new() .small() .color(cx.theme().primary_foreground) ) .label("Loading...") // In a card header div() .flex() .items_center() .gap_2() .child("Processing...") .child( Spinner::new() .small() .color(cx.theme().muted_foreground) ) // Full-screen loading div() .flex() .items_center() .justify_center() .h_full() .w_full() .child( Spinner::new() .large() .color(cx.theme().primary) ) ``` ## Performance Considerations * The animation uses CSS transforms for optimal performance * Multiple spinners on the same page share the same animation timing * The component is lightweight and suitable for frequent updates * Consider using smaller sizes for better performance with many spinners ## Common Patterns ### Conditional Loading ```rust // Show spinner only when loading .when(is_loading, |this| { this.child( Spinner::new() .small() .color(cx.theme().muted_foreground) ) }) ``` ### Loading with Text ```rust // Loading text with spinner h_flex() .items_center() .gap_2() .child( Spinner::new() .small() .color(cx.theme().primary) ) .child("Loading data...") ``` ### Overlay Loading ```rust // Full overlay with spinner div() .absolute() .inset_0() .flex() .items_center() .justify_center() .bg(cx.theme().background.alpha(0.8)) .child( v_flex() .items_center() .gap_3() .child( Spinner::new() .large() .color(cx.theme().primary) ) .child("Loading...") ) ``` --- --- url: /zh-CN/docs/components/spinner.md description: 显示旋转加载动画,用于反馈任务或异步操作的进行中状态。 --- # Spinner Spinner 用于显示旋转中的加载动画,适合异步请求、处理中状态和其他需要即时反馈的场景。它支持自定义图标、颜色、尺寸以及内置旋转动画。 ## 导入 ```rust use gpui_kit::component::spinner::Spinner; ``` ## 用法 ### 基础用法 ```rust Spinner::new() ``` ### 自定义颜色 ```rust use gpui_kit::component::ActiveTheme; Spinner::new() .color(cx.theme().blue) Spinner::new() .color(cx.theme().green) Spinner::new() .color(cx.theme().cyan) ``` ### 不同尺寸 ```rust Spinner::new().xsmall() Spinner::new().small() Spinner::new() Spinner::new().large() Spinner::new().with_size(px(64.)) ``` ### 自定义图标 ```rust use gpui_kit::component::IconName; Spinner::new() .icon(IconName::LoaderCircle) Spinner::new() .icon(IconName::LoaderCircle) .large() .color(cx.theme().cyan) Spinner::new() .icon(IconName::Loader) .color(cx.theme().primary) ``` ## 可用图标 ### 加载图标 * `Loader`,默认的线形旋转图标 * `LoaderCircle`,圆形加载图标 ### 其他兼容图标 * 理论上可使用 `IconName` 中任意图标,但带旋转语义的图标效果最好 ## 动画 Spinner 内置旋转动画: * 时长:`0.8` 秒 * 缓动:ease-in-out * 循环:无限重复 * 变换:360 度旋转 ## 尺寸参考 | 尺寸 | 方法 | 近似像素 | | --- | --- | --- | | 超小 | `.xsmall()` | ~12px | | 小 | `.small()` | ~14px | | 中 | 默认 | ~16px | | 大 | `.large()` | ~24px | | 自定义 | `.with_size(px(n))` | `n` px | ## 示例 ### 加载状态 ```rust Spinner::new() Spinner::new() .color(cx.theme().blue) Spinner::new() .large() .color(cx.theme().primary) ``` ### 不同加载图标 ```rust Spinner::new() .color(cx.theme().muted_foreground) Spinner::new() .icon(IconName::LoaderCircle) .color(cx.theme().blue) Spinner::new() .icon(IconName::LoaderCircle) .large() .color(cx.theme().green) ``` ### 状态型 Spinner ```rust Spinner::new() .small() .color(cx.theme().muted_foreground) Spinner::new() .icon(IconName::LoaderCircle) .color(cx.theme().blue) Spinner::new() .icon(IconName::LoaderCircle) .color(cx.theme().green) ``` ### 在界面组件中使用 ```rust Button::new("submit-btn") .loading(true) .icon( Spinner::new() .small() .color(cx.theme().primary_foreground) ) .label("Loading...") // 在卡片头部 div() .flex() .items_center() .gap_2() .child("Processing...") .child( Spinner::new() .small() .color(cx.theme().muted_foreground) ) ``` ## 性能说明 * 动画基于 transform,性能开销较低 * 多个 Spinner 可共享相同动画节奏 * 组件本身较轻,适合频繁更新的界面 * 大量同时显示时,优先使用更小尺寸 ## 常见模式 ### 条件加载 ```rust .when(is_loading, |this| { this.child( Spinner::new() .small() .color(cx.theme().muted_foreground) ) }) ``` ### 文字配合加载图标 ```rust h_flex() .items_center() .gap_2() .child( Spinner::new() .small() .color(cx.theme().primary) ) .child("Loading data...") ``` --- --- url: /shell/state.md description: >- Views, init and render, cx.notify(), retained input state, and asynchronous work. --- # State and Views A View is the one thing in this runtime that has an identity, survives a frame, and is owned by GPUI. Everything else — elements, callbacks, the `cx` handed to a call — belongs to the pass that created it. ## Defining a View ```js import { View } from "gpui-kit"; export default class Counter extends View { init(props) { this.count = props?.start ?? 0; } render(cx) { return v_flex().child(`${this.count}`); } } ``` `init` runs once, when the View is created. It is where state that survives frames is set up — plain fields, and any [retained entity](#retained-state) the View needs. `render` **returns one element, retained `Entity` or string**, and runs when the View has been invalidated rather than on every frame — see [When `render` runs](#when-render-runs). Returning anything else fails immediately: ```text render(cx) must return an element, an Entity, or a string ``` `main.js` must `export default` a View class. The host constructs one instance and mounts it as the window's root View; a module whose default export is not a class is refused with a message saying so. Never store an element on the instance. See [Elements](./elements.md#elements-are-single-use). ## `cx.notify()` Nothing repaints on its own. There are no signals, no observables and no automatic dependency tracking. Change state, then ask for a re-render: ```js add(cx) { this.items = [...this.items, { id: this.nextId, caption, done: false }]; this.nextId += 1; cx.notify(); } ``` This runs against the whole default assumption of the front-end ecosystem, so it is worth stating flatly: **there is no `useState` here, and no dependency array.** Three reasons the runtime does not add one. GPUI is itself an explicit-`notify` model, and two reactive mental models inside one application interfere with each other rather than compose. Automatic tracking would mean wrapping every View instance in a `Proxy`, which is a permanent cost on the render path — and QuickJS has no JIT to amortize it. And a missing `notify` has a determinate symptom: the interface does not update. That is far cheaper to find than an automatic system that fires too often. Several `notify` calls inside one event handler collapse into a single repaint — and into a single `render`. ## When `render` runs `render` does **not** run once per frame. GPUI repaints for reasons your application never hears about — a pointer moving over a button, a text cursor blinking, a list scrolling, an animation advancing — and none of those are a reason to run JavaScript. So a `render` call does not describe *this frame*. It describes the interface once, into a Snapshot the runtime keeps: ```text cx.notify() ──▶ render() ──▶ Snapshot ──┬──▶ frame ├──▶ frame └──▶ frame … ``` The Snapshot is rebuilt when, and only when, something invalidates it: * `cx.notify()` from an event handler or an async task * a [hot-reload](./getting-started.md) replacing the script * a theme change, because `bg(cx.theme().colors.surface)` records a real colour while `render` runs and bakes it into the Snapshot * the Host calling `ScriptView::refresh`, which is how Rust says it changed state your script reads through a [HostModule](./host-module.md). A plain `cx.notify()` from the Host is a repaint and runs no script — the two are different requests Everything else replays the description you already produced, in Rust, without running any JavaScript. Three consequences worth holding on to: **Your `render` cost follows your users, not your frame rate.** A View that changes ten times a second costs ten renders a second, whether the window is repainting at 60 FPS or 120. Describing a large panel is affordable precisely because it is not being redescribed sixty times for no reason. **Hover, focus and active styles never call back into script.** `.hover(s => s.opacity(0.8))` is resolved into a native style description while the Snapshot is built, and GPUI applies it from there. A pointer moving across your interface runs no JavaScript at all. The same is true of an [`Input`](#retained-state)'s cursor and selection. **A failed `render` does not destroy the interface.** A Snapshot is published only after `render` returns successfully, so a script that throws leaves the previous description — and the handlers registered with it — exactly as they were. The failure appears as a banner **over** the interface that still works, saying it is one version behind and offering the detail for pasting somewhere; you keep your scroll position and your focus. A View whose very first render failed has nothing to keep, and gets the full error surface instead. Either way the failing `render` is not re-run until something invalidates the View again. ## Scope phases Every call from Rust into the script opens a scope carrying a **phase**, and the phase decides what the `cx` for that call may do. | `ScopePhase` | When | May | May not | | --- | --- | --- | --- | | `render` | Building an element tree | Read state, build elements, register callbacks | `notify`, open overlays, create retained state | | `event` | Handling a click or a change | Everything | Block | | `task` | Resuming asynchronous work | Everything | Block | | `layout` | Rendering one virtualized item inside GPUI's layout pass | Read state, build elements | `notify`, open overlays, create retained state | `cx.phase()` reports the current one, and `"none"` outside any host call. `cx.theme()` returns a deeply read-only Snapshot of gpui-base's current semantic theme for this call: direct color roles as well as `colors`, `spacing`, `radius`, `appearance`, and `is_dark`. Each refusal is a specific message, not undefined behaviour: ```text cx.notify() is not allowed during the `render` phase; request a re-render from an event handler instead ``` Notifying yourself while rendering is a loop, which is why it is refused rather than deferred. ## Two kinds of `cx` `&mut Window` and `&mut App` are borrows in GPUI: they live exactly as long as one call. A script object outlives any borrow, so the script-side `cx` cannot hold them. GPUI has a second flavour for the code that needs one anyway — `AsyncApp`, which `cx.spawn` hands its closure — and so does this. **`Context`** is what `render` and every event handler receive. It holds a **generation number**, checked against the live scope stack on every use, so keeping one past its call is an error rather than a corrupted frame: ```text cx is no longer valid: it was captured during an earlier call and used later. Use cx.spawn or take cx from the callback arguments instead. ``` **`AsyncContext`** is what `init` receives, and what `cx.spawn` and `cx.timer` hand their callbacks. It names no call at all — it resolves whichever one is running when you use it — so an `await` does not take it away: ```js async save(cx) { await cx.sleep(100); cx.notify(); // the same cx, still the right one } ``` Those three are exactly the places whose job is to set up or continue work that outlives the call they started in. Everywhere else the strict flavour is what you want, and being told you kept it too long is the point. `cx` exposes nothing but functions — `Object.keys(cx)` shows the methods and no generation — so a script cannot forge one. There is no third way to get one. A module's top level and a bare `constructor` are handed no context and cannot ask for one — which is the point rather than a gap: GPUI has no module top level either, and work started there would belong to no View, so nothing would own it and nothing would cancel it. Start it in `init`, which is where a View is handed its context. ## Retained state A View's own fields hold plain data. Anything with cross-frame machinery of its own — a text field's content, cursor position and undo history — lives in a GPUI entity, and the script holds a **handle** to it. ```js import { InputState, Input } from "gpui-base"; init() { this.draft = InputState.new({ placeholder: "What needs doing?" }); this.draft.on("submit", (_event, cx) => this.add(cx)); } render(cx) { return Input.new(this.draft) .flex_1() .h(28) .px(8) .border(1) .border_color(cx.theme().colors.input) .bg(cx.theme().colors.surface) .text_size(12); } ``` | Call | Effect | | --- | --- | | `InputState.new({ placeholder, value })` | Creates the state; both options are optional | | `state.value()` | The current text | | `state.set_value(text)` | Replaces it | | `state.on(event, handler)` | Subscribes; see below | | `state.release()` | Drops the handle | | `Input.new(state)` | The element that renders it | **Create it in `init` or an event handler, never in `render`.** Creating an entity needs a live window, and the render pass is the one place where doing so would be wrong anyway: ```text InputState.new(...) cannot run during render; create state in init() or in an event handler and keep it on the View ``` The script holds a handle, not the entity — GPUI owns that. Using a released handle throws rather than returning `undefined`, because an `undefined` in JavaScript travels a long way before it fails and by then the origin is gone: ```text this input state has been released ``` `Input` is the one element the runtime gives defaults to, and only three: a centred row, full width, and a click anywhere in the frame focuses it. Each is a default a script can override but should not have to remember — without the first, text sits at the top of whatever height the frame was given, which on screen looks like a bug rather than a missing style. ### Input events ```js this.draft.on("submit", (event, cx) => this.add(cx)); ``` | Event | Fires on | | --- | --- | | `change` | The text changed | | `submit` | Enter was pressed; `event.secondary` and `event.shift` say how | | `focus` | The field gained focus | | `blur` | It lost focus | Unlike a rendered `on_click`, this subscription **outlives the render that created it**. The subscription is owned by the runtime's handle store rather than by the script, because a script has nowhere to keep it and a handler that stops firing because a value was garbage collected is the kind of bug nobody finds. It is released when the handle is. A misspelled event name lists the valid ones: ```text unknown input event `changed`; expected one of: change, submit, focus, blur ``` ### Calendar state `CalendarState` is the same pattern holding something different: which month is being looked at, which date is chosen, and the day grid that follows from both. ```js init(_props, cx) { this.calendar = CalendarState.new(); this.calendar.on("change", (date, cx) => this.pick(date, cx)); } render(cx) { const grid = this.calendar.month_days()[0]; return v_flex().children( grid.map((week) => h_flex().gap(4).children( week.map((day) => Button.new(day) .selected(day === this.calendar.value()) .on_click((_e, cx) => { this.calendar.set_value(day); cx.notify(); }) .child(String(Number(day.slice(8)))), ), ), ), ); } ``` `month_days()` is why it exists: which dates fall in which week, where the neighbouring months' days go, and how many weeks this month needs. You draw the cells — base's `Calendar` element is **not** bound, because it walks the same grid calling a renderer once per cell, up to forty-two crossings into JavaScript per frame from inside GPUI's layout pass, for cells that carry no behavior. Dates are `"YYYY-MM-DD"`, a range is `[start, end]`, and nothing selected is `null`. A range stays a pair even before its end is chosen — `["2026-08-03", null]` does not collapse to its start — because "one day is selected" and "a range has been started" are different states to base, and its own logic branches on the difference. | Call | Effect | | --- | --- | | `CalendarState.new()` | Creates the state; like every retained handle, only in `init` or an event handler | | `month_days()` | The grid, as months of weeks of days; every week is seven days | | `year()` / `month()` / `today()` | The year and month the grid is for, and today as it was read at creation | | `value()` / `set_value(next)` | The selection | | `next_month()` / `prev_month()` | Moves the grid a month either way; illegal from `render` | | `on("change", handler)` | The only event, reporting a date being selected | | `release()` | Drops the handle | ## Asynchronous work Script code is asynchronous in the ordinary JavaScript way — `async` functions and native promises. The runtime supplies the parts a bare QuickJS does not have: a clock, an owner for pending work, and something to pump the job queue. | Call | Effect | | --- | --- | | `cx.sleep(ms)` | A promise resolved after `ms` on GPUI's foreground executor | | `cx.spawn(body, opts?)` | Calls `body(cx)` and adopts the promise it returns | | `cx.timer.after(ms, handler, opts?)` | Calls `handler(cx)` once | | `cx.timer.every(ms, handler, opts?)` | Calls `handler(cx)` repeatedly | Scheduling is on `cx` because that is where GPUI keeps it — `App::spawn`, and a timer from the executor a context hands out. There is nothing to import. All of them return work that runs on the main thread. Nothing script-visible ever leaves it: there is no `Worker`, and the VM and GPUI's `App` are both main-thread only. ```js flash(cx) { this.saved = true; cx.notify(); cx.spawn(async (cx) => { await cx.sleep(1500); this.saved = false; cx.notify(); }); } ``` Nothing is imported for that: `cx` arrives as the handler's second argument, and the `cx` its body receives is the async flavour that survives the `await`. **`spawn` adopts the promise, and that is the point.** An unhandled rejection is JavaScript's most common silent failure: the work stops, the interface keeps the state it had, and nothing is written anywhere. Here it reaches `tracing::error!` with the script's own stack. ### Ownership and cancellation Every task belongs to a View — `opts.owner`, or the View that is running when it is created. The task holds a weak reference, so when the panel that started the work goes away the callback is skipped rather than writing into state nothing will render again. ```js const handle = cx.timer.every(1000, (cx) => this.tick(cx)); handle.cancel(); handle.is_done(); ``` `owner: null` opts out and outlives every View; it is the only value other than the current View the runtime accepts today. Cancelling a `cx.sleep` leaves its promise **pending for ever**. That is what cancellation means for a promise: the continuation does not run, and no error is invented for code that asked to stop. `cx.timer.every` measures its interval from the end of one call, so a slow handler delays the next tick rather than stacking ticks behind it. ### Timers and standard host APIs ```text setTimeout -> cx.timer.after(ms, callback) setInterval -> cx.timer.every(ms, callback) clearTimeout / clearInterval -> cancel() the Task returned by after / every ``` `setTimeout`, `setInterval`, `clearTimeout` and `clearInterval` are throwing stubs. Use `cx.timer.after` for one-shot work, `cx.timer.every` for repeated work, and call `cancel()` on the returned `Task` to stop either one. Global `fetch` and the safe standard modules documented under [Capabilities](./capabilities.md), including `websocket`, are real asynchronous host APIs. CommonJS `require` remains unavailable; use ES modules. Browser DOM and storage are absent: there is no `document` or `localStorage`. The global `window` is gpui-shell's overlay host for dialogs, sheets and toasts; it is not a browser `Window` and exposes no DOM. ## Not there yet * **Global and cross-view state.** There is no store beyond the persistence layer in [Capabilities](./capabilities.md) and ordinary module scope. * **Actions and key bindings.** `gpui.action` and `gpui.keymap` are designed but not bound; the only key handling today is what `ShellRoot` installs (Tab, Shift-Tab, Escape). * **Multiple windows.** The host opens the window; there is no `gpui.open_window`. * **`gpui.gc_stats()`**, and the debug panel that would read it. --- --- url: /docs/components/status-bar.md description: >- A horizontal status bar with left, center, and right regions, usually placed at the bottom of a window or pane. --- # StatusBar StatusBar is a horizontal bar split into three regions — `left`, `center`, and `right`. It is usually placed at the bottom of a window or pane to show contextual information and quick actions. The design mirrors the status bars found in native UI frameworks: Windows `StatusStrip`, WPF `StatusBar`, and macOS `NSStatusBar`. ## Import ```rust use gpui_kit::component::status_bar::StatusBar; ``` ## Regions Pass any `impl IntoElement` — a string, an `Icon`, a `Button`, a custom layout, etc. — to a region. `left` and `right` pin items to each end; `child` / `children` add to the center, whose alignment follows the pinned ends — centered with both `left` and `right`, end-aligned with only `left`, start-aligned otherwise (only `right`, or neither, like a plain container). Call a method multiple times to add more. * For a **non-interactive label**, pass a plain string — it inherits the bar's text style and has no hover. * For a **clickable button**, pass a ghost, xsmall `Button` — `Button::new(id).ghost().xsmall()` — so buttons stay a consistent size. Chain `label`, `icon`, `tooltip`, `on_click`, etc. * For a **separator**, pass `Separator::vertical()`. * For anything else, pass the element directly. ## Usage ### Labels ```rust StatusBar::new() .left("Ready") .child("README.md") .right("UTF-8") ``` ### Buttons ```rust StatusBar::new() .left( Button::new("branch").ghost().xsmall() .icon(IconName::Github) .label("main") .on_click(|_, window, cx| { /* ... */ }), ) .right( Button::new("go-to-line").ghost().xsmall() .label("Ln 1, Col 1") .tooltip("Go to Line/Column") .on_click(cx.listener(|this, _, window, cx| { /* ... */ })), ) ``` ### Separators and custom elements ```rust StatusBar::new() .left(Button::new("branch").ghost().xsmall().icon(IconName::Github).label("main")) .left(Separator::vertical()) .left( // Any custom element works. h_flex() .items_center() .gap_1() .child(Icon::new(IconName::CircleCheck).xsmall()) .child("0 problems"), ) .child(Progress::new("indexing").value(60.).w_24()) ``` ### Custom styling `StatusBar` implements `Styled`, so any style method overrides the defaults. ```rust StatusBar::new() .bg(cx.theme().secondary) .border_color(cx.theme().border) .left("Ready") ``` ## API Reference ### StatusBar | Method | Description | | ----------------- | ---------------------------------------------------- | | `new()` | Create a new, empty status bar | | `left(child)` | Append an element to the left region (call to add more) | | `right(child)` | Append an element to the right region | | `child(c)` / `children(cs)` | Add element(s) to the center region | Each region method takes `impl IntoElement`. `StatusBar` also implements `Styled`, so style methods (`bg`, `border_color`, `py`, etc.) can override the defaults. ## Notes * The center (via `child` / `children`) is centered with both `left` and `right`, end-aligned with only `left`, and start-aligned otherwise (only `right`, or neither — like a plain container). * Use a plain string (or any non-interactive element) for read-only items to avoid the button hover effect; use a ghost xsmall `Button` only for clickable items. * Colors come from the `status_bar` (background) and `status_bar_border` theme tokens, which fall back to `background` / `border`. --- --- url: /zh-CN/docs/components/status-bar.md description: 一个分为左、中、右三个区域的水平状态栏,通常放置在窗口或面板底部。 --- # StatusBar StatusBar 是一个水平栏,分为 `left`、`center`、`right` 三个区域。它通常放置在窗口或面板底部,用于显示上下文信息和快捷操作。 其设计参考了原生 UI 框架中的状态栏:Windows 的 `StatusStrip`、WPF 的 `StatusBar` 以及 macOS 的 `NSStatusBar`。 ## 引入 ```rust use gpui_kit::component::status_bar::StatusBar; ``` ## 区域 向区域传入任意 `impl IntoElement` —— 字符串、`Icon`、`Button`、自定义布局等。`left` 和 `right` 把项固定在两端;`child` / `children` 添加到中间区域,其对齐方式取决于固定了哪一端 —— 同时有 `left` 和 `right` 时居中,只有 `left` 时右对齐,否则左对齐(只有 `right`,或两者都没有时,像普通容器一样)。多次调用即可追加更多。 * **不可交互的标签**:直接传字符串 —— 它会继承状态栏的文字样式,且没有 hover。 * **可点击的按钮**:传入一个 ghost、xsmall 的 `Button` —— `Button::new(id).ghost().xsmall()` —— 保证按钮尺寸一致。可链式调用 `label`、`icon`、`tooltip`、`on_click` 等。 * **分隔线**:传入 `Separator::vertical()`。 * **其他任意内容**:直接传该元素。 ## 用法 ### 标签 ```rust StatusBar::new() .left("Ready") .child("README.md") .right("UTF-8") ``` ### 按钮 ```rust StatusBar::new() .left( Button::new("branch").ghost().xsmall() .icon(IconName::Github) .label("main") .on_click(|_, window, cx| { /* ... */ }), ) .right( Button::new("go-to-line").ghost().xsmall() .label("Ln 1, Col 1") .tooltip("Go to Line/Column") .on_click(cx.listener(|this, _, window, cx| { /* ... */ })), ) ``` ### 分割线与自定义元素 ```rust StatusBar::new() .left(Button::new("branch").ghost().xsmall().icon(IconName::Github).label("main")) .left(Separator::vertical()) .left( // 任意自定义元素都可以。 h_flex() .items_center() .gap_1() .child(Icon::new(IconName::CircleCheck).xsmall()) .child("0 problems"), ) .child(Progress::new("indexing").value(60.).w_24()) ``` ### 自定义样式 `StatusBar` 实现了 `Styled`,因此任意样式方法都会覆盖默认值。 ```rust StatusBar::new() .bg(cx.theme().secondary) .border_color(cx.theme().border) .left("Ready") ``` ## API 参考 ### StatusBar | 方法 | 说明 | | ---------------- | ------------------------------------------ | | `new()` | 创建一个空的状态栏 | | `left(child)` | 向左侧区域追加一个元素(可多次调用) | | `right(child)` | 向右侧区域追加一个元素 | | `child(c)` / `children(cs)` | 向中间区域添加元素 | 每个区域方法接受 `impl IntoElement`。`StatusBar` 同时实现了 `Styled`,样式方法(`bg`、`border_color`、`py` 等)可以覆盖默认值。 ## 注意事项 * 中间区域(通过 `child` / `children`)在同时有 `left` 和 `right` 时居中,只有 `left` 时右对齐,否则左对齐(只有 `right`,或两者都没有时,像普通容器一样)。 * 只读项请用纯字符串(或任意不可交互元素),以避免按钮的 hover 效果;只有可点击项才用 ghost、xsmall 的 `Button`。 * 颜色取自 `status_bar`(背景)和 `status_bar_border`(边框)主题变量,缺省回退到 `background` / `border`。 --- --- url: /docs/components/stepper.md description: >- A step-by-step progress for users to navigate through a series of steps or stages. --- # Stepper A step-by-step progress component that guides users through a series of steps or stages. Supports horizontal and vertical layouts, custom icons, and different sizes. ## Import ```rust use gpui_kit::component::stepper::{Stepper, StepperItem}; ``` ## Usage ### Basic Stepper Use `selected_index` method to set current active step by index (0-based), default is `0`. ```rust Stepper::new("my-stepper") .selected_index(0) .items([ StepperItem::new().child("Step 1"), StepperItem::new().child("Step 2"), StepperItem::new().child("Step 3"), ]) .on_click(|step, _, _| { println!("Clicked step: {}", step); }) ``` ### With Icons ```rust use gpui_kit::component::IconName; Stepper::new("icon-stepper") .selected_index(0) .items([ StepperItem::new() .icon(IconName::Calendar) .child("Order Details"), StepperItem::new() .icon(IconName::Inbox) .child("Shipping"), StepperItem::new() .icon(IconName::Frame) .child("Preview"), StepperItem::new() .icon(IconName::Info) .child("Finish"), ]) ``` ### Vertical Layout ```rust Stepper::new("vertical-stepper") .vertical() .selected_index(2) .items_center() .items([ StepperItem::new() .pb_8() .icon(IconName::Building2) .child(v_flex().child("Step 1").child("Description for step 1.")), StepperItem::new() .pb_8() .icon(IconName::Asterisk) .child(v_flex().child("Step 2").child("Description for step 2.")), StepperItem::new() .pb_8() .icon(IconName::Folder) .child(v_flex().child("Step 3").child("Description for step 3.")), StepperItem::new() .icon(IconName::CircleCheck) .child(v_flex().child("Step 4").child("Description for step 4.")), ]) ``` ### Text Center The `text_center` method centers the text within each step item. ```rust Stepper::new("center-stepper") .selected_index(0) .text_center(true) .items([ StepperItem::new().child( v_flex() .items_center() .child("Step 1") .child("Desc for step 1."), ), StepperItem::new().child( v_flex() .items_center() .child("Step 2") .child("Desc for step 2."), ), StepperItem::new().child( v_flex() .items_center() .child("Step 3") .child("Desc for step 3."), ), ]) ``` ### Different Sizes ```rust use gpui_kit::component::{Sizable as _, Size}; Stepper::new("stepper") .xsmall() .items([...]) Stepper::new("stepper") .small() .items([...]) Stepper::new("stepper") .large() .items([...]) ``` ### Disabled State ```rust Stepper::new("disabled-stepper") .disabled(true) .items([ StepperItem::new().child("Step 1"), StepperItem::new().child("Step 2"), ]) ``` ### Handle Click Events ```rust Stepper::new("my-stepper") .selected_index(current_step) .items([ StepperItem::new().child("Step 1"), StepperItem::new().child("Step 2"), StepperItem::new().child("Step 3"), ]) .on_click(cx.listener(|this, step, _, cx| { this.current_step = *step; cx.notify(); })) ``` ## API Reference * [Stepper] * [StepperItem] ### Sizing Implements [Sizable] trait: * `xsmall()` - Extra small size * `small()` - Small size * `medium()` - Medium size (default) * `large()` - Large size ## Examples ### Multi-step Form ```rust Stepper::new("form-stepper") .w_full() .selected_index(form_step) .items([ StepperItem::new() .icon(IconName::User) .child("Personal Info"), StepperItem::new() .icon(IconName::CreditCard) .child("Payment"), StepperItem::new() .icon(IconName::CircleCheck) .child("Confirmation"), ]) .on_click(cx.listener(|this, step, _, cx| { this.form_step = *step; cx.notify(); })) ``` ### Disabled Individual Steps ```rust Stepper::new("stepper") .selected_index(0) .items([ StepperItem::new().child("Available"), StepperItem::new().disabled(true).child("Locked"), StepperItem::new().child("Available"), ]) ``` [Stepper]: https://docs.rs/gpui-component/latest/gpui_component/stepper/struct.Stepper.html [StepperItem]: https://docs.rs/gpui-component/latest/gpui_component/stepper/struct.StepperItem.html [Sizable]: https://docs.rs/gpui-component/latest/gpui_component/trait.Sizable.html --- --- url: /zh-CN/docs/components/stepper.md description: 用于引导用户按步骤完成流程的进度组件。 --- # Stepper Stepper 用于按步骤展示流程进度,适合表单向导、订单流程和安装步骤等场景。支持横向和纵向布局、自定义图标以及不同尺寸。 ## 导入 ```rust use gpui_kit::component::stepper::{Stepper, StepperItem}; ``` ## 用法 ### 基础 Stepper 使用 `selected_index` 设置当前步骤,索引从 `0` 开始,默认值也是 `0`。 ```rust Stepper::new("my-stepper") .selected_index(0) .items([ StepperItem::new().child("Step 1"), StepperItem::new().child("Step 2"), StepperItem::new().child("Step 3"), ]) .on_click(|step, _, _| { println!("Clicked step: {}", step); }) ``` ### 带图标的 Stepper ```rust use gpui_kit::component::IconName; Stepper::new("icon-stepper") .selected_index(0) .items([ StepperItem::new() .icon(IconName::Calendar) .child("Order Details"), StepperItem::new() .icon(IconName::Inbox) .child("Shipping"), StepperItem::new() .icon(IconName::Frame) .child("Preview"), StepperItem::new() .icon(IconName::Info) .child("Finish"), ]) ``` ### 纵向布局 ```rust Stepper::new("vertical-stepper") .vertical() .selected_index(2) .items_center() .items([ StepperItem::new() .pb_8() .icon(IconName::Building2) .child(v_flex().child("Step 1").child("Description for step 1.")), StepperItem::new() .pb_8() .icon(IconName::Asterisk) .child(v_flex().child("Step 2").child("Description for step 2.")), StepperItem::new() .pb_8() .icon(IconName::Folder) .child(v_flex().child("Step 3").child("Description for step 3.")), StepperItem::new() .icon(IconName::CircleCheck) .child(v_flex().child("Step 4").child("Description for step 4.")), ]) ``` ### 文本居中 ```rust Stepper::new("center-stepper") .selected_index(0) .text_center(true) .items([ StepperItem::new().child( v_flex() .items_center() .child("Step 1") .child("Desc for step 1."), ), StepperItem::new().child( v_flex() .items_center() .child("Step 2") .child("Desc for step 2."), ), StepperItem::new().child( v_flex() .items_center() .child("Step 3") .child("Desc for step 3."), ), ]) ``` ### 不同尺寸 ```rust use gpui_kit::component::{Sizable as _, Size}; Stepper::new("stepper") .xsmall() .items([...]) Stepper::new("stepper") .small() .items([...]) Stepper::new("stepper") .large() .items([...]) ``` ### 禁用状态 ```rust Stepper::new("disabled-stepper") .disabled(true) .items([ StepperItem::new().child("Step 1"), StepperItem::new().child("Step 2"), ]) ``` ## API 参考 * [Stepper] * [StepperItem] ### 尺寸 实现了 [Sizable] trait: * `xsmall()`:超小尺寸 * `small()`:小尺寸 * `medium()`:中尺寸,默认值 * `large()`:大尺寸 ## 示例 ### 多步骤表单 ```rust Stepper::new("form-stepper") .w_full() .selected_index(form_step) .items([ StepperItem::new() .icon(IconName::User) .child("Personal Info"), StepperItem::new() .icon(IconName::CreditCard) .child("Payment"), StepperItem::new() .icon(IconName::CircleCheck) .child("Confirmation"), ]) .on_click(cx.listener(|this, step, _, cx| { this.form_step = *step; cx.notify(); })) ``` ### 禁用单个步骤 ```rust Stepper::new("stepper") .selected_index(0) .items([ StepperItem::new().child("Available"), StepperItem::new().disabled(true).child("Locked"), StepperItem::new().child("Available"), ]) ``` [Stepper]: https://docs.rs/gpui-component/latest/gpui_component/stepper/struct.Stepper.html [StepperItem]: https://docs.rs/gpui-component/latest/gpui_component/stepper/struct.StepperItem.html [Sizable]: https://docs.rs/gpui-component/latest/gpui_component/trait.Sizable.html --- --- url: /shell/styling.md description: >- The fluent style surface, length and colour grammars, semantic theme tokens, and hover / active / focus styles. --- # Styling The script owns presentation, so this is where most of an application's code goes. Every element accepts the same style surface, written as one fluent chain — exactly what the Rust side writes: ```js render(cx) { return v_flex().size_full().bg(cx.theme().colors.surface).p(12).gap(8).rounded(6); } ``` ```rust // The same thing in Rust, on gpui-base. v_flex().size_full().bg(surface).p(px(12.)).gap(px(8.)).rounded(px(6.)) ``` ## Two halves, one surface The style surface has two halves, and they exist for different reasons. **No-argument methods come from GPUI's reflection table.** `flex_col`, `items_center`, `gap_2`, `rounded_md`, `text_sm`, `size_full`, `font_semibold`, `truncate`, `cursor_pointer` — the whole family, obtained from `gpui_kit::base::styled_ext_reflection_methods` and `gpui_kit::styled_reflection::methods` with no maintenance at all. Not one of these names is written down anywhere in the runtime. When upstream GPUI adds a style method, the script surface has it, and so does the generated `gpui-kit.d.ts`. The build these pages were written against exposes **3,148** of them. It is however many `fn(self) -> Self` style methods GPUI currently has, and it moves when GPUI moves. `gpui-shell types` prints the exact figure for your build. **Methods that take arguments cannot be reflected**, so there are **57** of them bound by hand. That list is the one hand-maintained table in the styling layer, and it is deliberately small. The two halves never overlap: a name is in one or the other, and a test fails the build if a name ever lands in both. ## Lengths A bare number is pixels. A string carries its unit. ```js .p(12) // 12px .w("50%") // half the parent .h("auto") .gap("0.5rem") ``` Which of those a given method accepts follows **its Rust signature**, because that signature is what rejects the bad ones. GPUI has three length types nested inside each other, and the runtime keeps the distinction rather than flattening it: | Type | Accepts | Rejects | | ---------------- | ------------------------------------------------- | --------------------- | | `Length` | a number, `"12px"`, `"1.5rem"`, `"50%"`, `"auto"` | — | | `DefiniteLength` | a number, `"12px"`, `"1.5rem"`, `"50%"` | `"auto"` | | `AbsoluteLength` | a number, `"12px"`, `"1.5rem"` | percentages, `"auto"` | ```text `p` cannot be "auto"; it expects a definite length such as 12 or "50%" ``` ```text `rounded` expects an absolute length such as 8 or "0.5rem"; percentages and "auto" are not allowed here ``` `"auto"` padding and a percentage radius have no meaning in the layout engine underneath, and a runtime that accepted them would have to invent one. ### The parametric methods | Family | Methods | Argument | | ---------- | -------------------------------------------------------------------------- | ---------------- | | Size | `w` `h` `size` `min_w` `min_h` `min_size` `max_w` `max_h` `max_size` | `Length` | | Padding | `p` `px` `py` `pt` `pb` `pl` `pr` | `DefiniteLength` | | Margin | `m` `mx` `my` `mt` `mb` `ml` `mr` | `Length` | | Position | `inset` `top` `bottom` `left` `right` | `Length` | | Flex | `gap` `gap_x` `gap_y` | `DefiniteLength` | | Flex | `flex_basis` | `Length` | | Flex | `flex_grow` `flex_shrink` | number | | Border | `border` `border_t` `border_b` `border_l` `border_r` `border_x` `border_y` | `AbsoluteLength` | | Radius | `rounded` and the `_t` `_b` `_l` `_r` `_tl` `_tr` `_bl` `_br` forms | `AbsoluteLength` | | Paint | `bg` `text_color` `text_bg` `border_color` | colour | | Paint | `text_size` | `AbsoluteLength` | | Paint | `line_height` | `DefiniteLength` | | Typography | `font_family` | string | | Paint | `opacity` | number | `line_height` is the one exception worth memorizing: a **bare number is a multiplier**, not pixels. `line_height(1.45)` means 1.45× the font size, because that is what it means everywhere else in the industry and 1.45px is never what anyone meant. A string still follows the ordinary grammar. ### What is deliberately not bound `shadow`, `cursor`, `text_align`, `text_overflow`, `font_weight` and `scrollbar_width` take Rust structs or enums rather than scalars, and are not exposed as parametric methods. Every one of them has a no-argument form that is reflected and works today: `shadow_sm`, `cursor_pointer`, `text_center`, `truncate`, `font_bold`. A real shadow API belongs with the token work, not as a positional argument list. ## Colours A colour is normally read from the call-scoped theme. Semantic token name strings remain accepted for compatibility, and hex literals are available for fixed colours: ```js render(cx) { return element .bg(cx.theme().colors.surface) // follows the theme .text_color("#1e88e5"); // does not } ``` The palette defines seventeen tokens: | | | | --------- | -------------------------------------------------------------------- | | Ground | `background`, `foreground` | | Surfaces | `surface`, `surface_foreground` | | Emphasis | `primary`, `primary_foreground`, `secondary`, `secondary_foreground` | | Recessive | `muted`, `muted_foreground` | | Highlight | `accent`, `accent_foreground`, `selection` | | Danger | `destructive`, `destructive_foreground` | | Chrome | `border`, `input`, `ring` | Hex literals accept `#rgb`, `#rrggbb` and `#rrggbbaa`. **Prefer a value from `cx.theme().colors`.** A literal bypasses the theme, and a theme switch will not reach it. The example application makes exactly this point: it follows the visual language of `crates/base/examples/showcase`, which has to write literal colours because Base ships no palette, and reads semantic tokens instead — so the same code follows a theme that the Rust showcase cannot. A mistyped token names the whole set rather than failing vaguely: ```text unknown color token `surfacee`; expected one of: background, foreground, surface, … — or a #rrggbb literal ``` ### Where the active tokens come from gpui-shell does not own a palette or theme file format. It reads the active `gpui_kit::base::Theme` supplied by the host. A JavaScript application may replace that same Base Snapshot with `set_theme({ appearance, tokens })`; theme names and any registry remain application state. ## State styles `hover`, `active` and `focus` take a function, which receives a detached element that collects the declarations: ```js renderSave(cx) { return Button.new("save") .bg(cx.theme().colors.surface) .border(1) .border_color(cx.theme().colors.border) .hover((style) => style.bg(cx.theme().colors.muted).border_color(cx.theme().colors.foreground)) .active((style) => style.bg(cx.theme().colors.border)) .focus((style) => style.border_color(cx.theme().colors.ring)) .child("Save"); } ``` The function's return value is ignored, so a chain and a block body both work. The declarations inside are the **ordinary style methods** — there is no second grammar for "what a style is", and every length and colour rule above applies unchanged. Two implementation facts leak far enough to be worth knowing: * **`active` and `focus` need a stable element identity.** A plain `div` acquires one lazily, derived from its position in the description, which is stable across renders for a stable tree. `Button`, `Checkbox` and `Input` already have one. * **A `Switch` ignores state styles.** The switch root is not the interactive element — its track is — so a state style on it has nowhere to land. The runtime logs a warning saying to style the row around it instead, rather than dropping the declaration silently. ## Scrolling overflow Scrolling is element behavior rather than a style declaration. Give the viewport a bounded width or height, then choose the axes it owns: ```js v_flex() .id("activity") .h(240) .overflow_y_scroll() .children(this.rows.map((row) => row)); ``` Use `.overflow_scroll()` for both axes, `.overflow_x_scroll()` for horizontal scrolling, or `.overflow_y_scroll()` for vertical scrolling. A stable `.id(...)` keeps the native scroll position attached to the same viewport across script renders. The corresponding `.overflow_scrollbar()`, `.overflow_x_scrollbar()` and `.overflow_y_scrollbar()` methods keep the same scrolling behavior and also paint gpui-component's native scrollbars. They require a stable `.id(...)` so each viewport keeps independent scrollbar and scroll-position state. ## Theme values Read semantic values from the context that is rendering or handling the event: ```js render(cx) { return v_flex() .gap(cx.theme().spacing.md) .rounded(cx.theme().radius.lg) .bg(cx.theme().colors.surface) .child(`${cx.theme().appearance}: ${cx.theme().is_dark ? "dark" : "light"}`); } ``` The Snapshot is deeply read-only. `theme()` remains as a compatibility accessor, but `cx.theme()` is preferred. An application may call `set_theme({ appearance, tokens })` from an event or task with its own complete color, spacing, and radius token Snapshot. gpui-shell writes that Snapshot into gpui-base and rebuilds token-backed script Views; it does not own theme names, palettes, or a file format. ## Native motion `.transition(property, policy)` and `.spring(property, policy?)` animate later target changes for `opacity`, `width`, `height`, `left`, and `top`. Motion is retained and advanced by native GPUI frames: after the script changes the target and calls `cx.notify()`, animation frames do **not** re-enter JavaScript. ```js div() .id("drawer") .left(this.open ? 320 : 16) .opacity(this.open ? 1 : 0.5) .transition("left", { duration: 220, easing: "ease-out" }) .spring("opacity", { response: 260, damping: 0.85 }); ``` Animated length targets are **numeric pixels only**. Relative values such as `"50%"`, `"1rem"`, and `"auto"` cannot be sampled into a stable native channel and are rejected. Give the element a stable `.id(...)` (controls already use their constructor id), otherwise a changing tree position changes the motion identity. ## Unknown methods ```text unknown style method `text_colour` (did you mean: text_color?) ``` The suggestion is a Levenshtein match against the full name list, with a tight budget — two edits, relaxed to a third of the name for longer identifiers. A wrong suggestion is worse than none. There is a nice piece of machinery behind that message, and it explains a number in the source. QuickJS reports a missing method as a bare `TypeError: not a function` **without naming the property**, so a mistyped style name would otherwise arrive with no clue at all. Wrapping the element prototype in a `Proxy` fixes that — and measured at roughly 30% of the entire description pass (1.09 ms → 1.42 ms for 443 nodes). So the runtime keeps a fast plain prototype as the default, and when a render fails with "not a function" it **re-runs that render once** against a diagnostic `Proxy` prototype, purely to produce the message. Errors are rare; a 30% tax on every render is not. ## Not there yet * **Semantic state styles.** `gpui-base` has a `state_style` layer with a defined priority order for checked, selected and disabled. It is not bound; use `.when(condition, …)` for those states today. * **Keyframe animation.** Target-value transitions and springs exist; arbitrary keyframes and per-frame JavaScript callbacks do not. * **Spacing and radius tokens in styles.** The palette carries spacing and radius scales, but style methods take lengths, not token names — only colours resolve a token. Applications define their own scale as a constant, the way the example's `SPACE` object does. --- --- url: /base/primitives/switch.md description: A controlled on/off control with separately styleable track and thumb. --- # Switch A controlled on/off control with separately styleable track and thumb. Like every `gpui-base` primitive, Switch supplies behavior and semantic structure without imposing a product visual language. Apply GPUI styles and compose the exported parts to match your design system. ## Example The [single native Cargo entrypoint](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/components.rs) selects this primitive from the [shared showcase implementation](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/mod.rs). The same showcase is compiled once for the WASM preview above. ```bash cargo run -p gpui-base --example components -- switch ``` ## Import ```rust use gpui_kit::base::{Switch, SwitchThumb, SwitchTrack}; ``` ## Anatomy and API The example composes `Switch`, `SwitchThumb`, `SwitchTrack`. GPUI's standard styling and event traits provide presentation; these base types provide the interaction structure. The authoritative module is [`components/switch.rs`](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/components/switch.rs). Native and browser previews compile this same file. ## State and events Pass the controlled boolean to `checked`; `on_change` emits the requested next value. Keep controlled state on the parent render type or in a GPUI entity. Update it in callbacks and call `cx.notify()`; do not recreate persistent entities during every render. ## Complete Rust example The complete implementation used by the runnable showcase is embedded directly from Rust source: The command above supplies application initialization, window creation, and shared `BaseShowcase` state. ## Accessibility Label the setting, expose checked state, and keep the accessible name stable. ## Notes Use stable element IDs where accepted. Verify focus, hover, active, selected, disabled, reduced-motion, and high-contrast appearances in the consuming design system. --- --- url: /zh-CN/base/primitives/switch.md description: 轨道和滑块可分别设置样式的受控开关。 --- # Switch 轨道和滑块可分别设置样式的受控开关。 和所有 GPUI Base 原语一样,Switch 只提供行为和语义结构,不规定产品视觉语言。请使用 GPUI 样式并组合导出的部件,使其符合你的设计系统。 ## 示例 原生示例和页面上方的 WASM 预览共用同一份实现: ```bash cargo run -p gpui-base --example components -- switch ``` ## 导入 ```rust use gpui_kit::base::{Switch, SwitchThumb, SwitchTrack}; ``` ## 结构与 API 示例组合上述公开类型。GPUI 的标准样式和事件 trait 负责表现,Base 类型负责交互结构。权威实现位于 [`components/switch.rs`](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/components/switch.rs),原生与浏览器预览编译的是同一文件。 ## 状态与事件 父级保存开关值,激活后切换并重新渲染。 受控状态应保存在父渲染类型或 GPUI entity 中;在回调中更新并调用 `cx.notify()`,不要在每次渲染时重建持久 entity。 ## 完整 Rust 示例 ## 可访问性 提供标签,使用开关语义并暴露当前开启和禁用状态。 ## 注意事项 在支持的位置使用稳定元素 ID,并在消费端设计系统中验证焦点、悬停、按下、选中、禁用、减少动态效果和高对比度状态。 --- --- url: /docs/components/switch.md description: A control that allows the user to toggle between checked and not checked. --- # Switch A toggle switch component for binary on/off states. Features smooth animations, different sizes, labels, disabled state, and customizable positioning. ## Import ```rust use gpui_kit::component::switch::Switch; ``` ## Usage ### Basic Switch ```rust Switch::new("my-switch") .checked(false) .on_click(|checked, _, _| { println!("Switch is now: {}", checked); }) ``` ### Controlled Switch ```rust struct MyView { is_enabled: bool, } impl Render for MyView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { Switch::new("switch") .checked(self.is_enabled) .on_click(cx.listener(|view, checked, _, cx| { view.is_enabled = *checked; cx.notify(); })) } } ``` ### With Label ```rust Switch::new("notifications") .label("Enable notifications") .checked(true) .on_click(|checked, _, _| { println!("Notifications: {}", if *checked { "enabled" } else { "disabled" }); }) ``` ### Different Sizes ```rust // Small switch Switch::new("small-switch") .small() .label("Small switch") // Medium switch (default) Switch::new("medium-switch") .label("Medium switch") // Using explicit size Switch::new("custom-switch") .with_size(Size::Small) .label("Custom size") ``` ### Disabled State ```rust // Disabled unchecked Switch::new("disabled-off") .label("Disabled (off)") .disabled(true) .checked(false) // Disabled checked Switch::new("disabled-on") .label("Disabled (on)") .disabled(true) .checked(true) ``` ### Custom Color Use `.color()` to override the checked-state background color. The disabled alpha is applied automatically on top of the custom color. ```rust // Success color when checked Switch::new("switch") .label("Success") .checked(true) .color(cx.theme().success) // Danger color when checked Switch::new("switch") .label("Danger") .checked(true) .color(cx.theme().danger) // Custom color + disabled: color is shown at 50% opacity Switch::new("switch") .label("Disabled") .checked(true) .color(cx.theme().success) .disabled(true) ``` ### With Tooltip ```rust Switch::new("switch") .label("Airplane mode") .tooltip("Enable airplane mode to disable all wireless connections") .checked(false) ``` ## API Reference ### Switch | Method | Description | | ------------------ | ----------------------------------------------------------- | | `new(id)` | Create a new switch with the given ID | | `checked(bool)` | Set the checked/toggled state | | `label(text)` | Set label text for the switch | | `label_side(side)` | Position label (Side::Left or Side::Right) | | `disabled(bool)` | Set disabled state | | `tooltip(text)` | Add tooltip text | | `color(color)` | Set background color when checked (default: `theme.primary`) | | `on_click(fn)` | Callback when clicked, receives `&bool` (new checked state) | ### Styling Implements `Sizable` and `Disableable` traits: * `small()` - Small switch size (28x16px toggle area) * `medium()` - Medium switch size (36x20px toggle area, default) * `with_size(size)` - Set explicit size * `disabled(bool)` - Disabled state ### Styling Properties The switch can also be styled using GPUI's styling methods: * `w(width)` - Custom width * `h(height)` - Custom height * Standard margin, padding, and positioning methods ## Examples ### Settings Panel ```rust struct SettingsView { marketing_emails: bool, security_emails: bool, push_notifications: bool, } v_flex() .gap_4() .child( // Setting with description v_flex() .gap_2() .child( h_flex() .items_center() .justify_between() .child( v_flex() .child(Label::new("Marketing emails").text_lg()) .child( Label::new("Receive emails about new products and features") .text_color(theme.muted_foreground) ) ) .child( Switch::new("marketing") .checked(self.marketing_emails) .on_click(cx.listener(|view, checked, _, cx| { view.marketing_emails = *checked; cx.notify(); })) ) ) ) .child( // Simple setting h_flex() .items_center() .justify_between() .child(Label::new("Push notifications")) .child( Switch::new("push") .checked(self.push_notifications) .on_click(cx.listener(|view, checked, _, cx| { view.push_notifications = *checked; cx.notify(); })) ) ) ``` ### Compact Settings List ```rust v_flex() .gap_3() .child( Switch::new("wifi") .label("Wi-Fi") .label_side(Side::Left) .checked(true) .small() ) .child( Switch::new("bluetooth") .label("Bluetooth") .label_side(Side::Left) .checked(false) .small() ) .child( Switch::new("airplane") .label("Airplane Mode") .label_side(Side::Left) .checked(false) .disabled(true) .small() ) ``` ### Form Integration ```rust struct FormData { subscribe_newsletter: bool, enable_notifications: bool, remember_me: bool, } v_flex() .gap_4() .p_4() .border_1() .border_color(theme.border) .rounded(theme.radius) .child( Switch::new("newsletter") .label("Subscribe to newsletter") .checked(self.subscribe_newsletter) .tooltip("Receive monthly updates about new features") .on_click(cx.listener(|view, checked, _, cx| { view.subscribe_newsletter = *checked; cx.notify(); })) ) .child( Switch::new("notifications") .label("Enable notifications") .checked(self.enable_notifications) .on_click(cx.listener(|view, checked, _, cx| { view.enable_notifications = *checked; cx.notify(); })) ) .child( Switch::new("remember") .label("Remember me") .checked(self.remember_me) .small() .on_click(cx.listener(|view, checked, _, cx| { view.remember_me = *checked; cx.notify(); })) ) ``` ### Custom Styling ```rust Switch::new("custom") .label("Custom styled switch") .w(px(200.)) .checked(true) .on_click(|checked, _, _| { println!("Custom switch: {}", checked); }) ``` ## Animation The switch features smooth animations: * **Toggle animation**: 150ms duration when switching states * **Background color transition**: Changes from switch color to primary color * **Position animation**: Smooth movement of the toggle indicator * **Disabled state**: Animations are disabled when the switch is disabled --- --- url: /zh-CN/docs/components/switch.md description: 用于在选中和未选中之间切换的开关控件。 --- # Switch Switch 是一个二元开关组件,适合表示开启 / 关闭状态。它支持平滑动画、不同尺寸、标签、禁用状态和自定义颜色。 ## 导入 ```rust use gpui_kit::component::switch::Switch; ``` ## 用法 ### 基础 Switch ```rust Switch::new("my-switch") .checked(false) .on_click(|checked, _, _| { println!("Switch is now: {}", checked); }) ``` ### 受控 Switch ```rust struct MyView { is_enabled: bool, } impl Render for MyView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { Switch::new("switch") .checked(self.is_enabled) .on_click(cx.listener(|view, checked, _, cx| { view.is_enabled = *checked; cx.notify(); })) } } ``` ### 带标签 ```rust Switch::new("notifications") .label("Enable notifications") .checked(true) .on_click(|checked, _, _| { println!("Notifications: {}", if *checked { "enabled" } else { "disabled" }); }) ``` ### 不同尺寸 ```rust Switch::new("small-switch") .small() .label("Small switch") Switch::new("medium-switch") .label("Medium switch") Switch::new("custom-switch") .with_size(Size::Small) .label("Custom size") ``` ### 禁用状态 ```rust Switch::new("disabled-off") .label("Disabled (off)") .disabled(true) .checked(false) Switch::new("disabled-on") .label("Disabled (on)") .disabled(true) .checked(true) ``` ### 自定义颜色 `color()` 用于覆盖选中状态下的背景色;禁用态透明度会自动叠加: ```rust Switch::new("switch") .label("Success") .checked(true) .color(cx.theme().success) Switch::new("switch") .label("Danger") .checked(true) .color(cx.theme().danger) Switch::new("switch") .label("Disabled") .checked(true) .color(cx.theme().success) .disabled(true) ``` ### 带 Tooltip ```rust Switch::new("switch") .label("Airplane mode") .tooltip("Enable airplane mode to disable all wireless connections") .checked(false) ``` ## API 参考 ### Switch | 方法 | 说明 | | --- | --- | | `new(id)` | 使用给定 ID 创建开关 | | `checked(bool)` | 设置当前选中状态 | | `label(text)` | 设置标签文本 | | `label_side(side)` | 设置标签位置,`Side::Left` 或 `Side::Right` | | `disabled(bool)` | 设置禁用状态 | | `tooltip(text)` | 添加提示文本 | | `color(color)` | 设置选中时的背景色,默认 `theme.primary` | | `on_click(fn)` | 点击回调,参数为新的 `&bool` 状态 | ### 样式 实现了 `Sizable` 和 `Disableable` trait: * `small()`:小尺寸,开关区域约 `28x16px` * `medium()`:中尺寸,默认,开关区域约 `36x20px` * `with_size(size)`:显式设置尺寸 * `disabled(bool)`:禁用状态 ## 示例 ### 设置面板 ```rust struct SettingsView { marketing_emails: bool, security_emails: bool, push_notifications: bool, } v_flex() .gap_4() .child( v_flex() .gap_2() .child( h_flex() .items_center() .justify_between() .child( v_flex() .child(Label::new("Marketing emails").text_lg()) .child( Label::new("Receive emails about new products and features") .text_color(theme.muted_foreground) ) ) .child( Switch::new("marketing") .checked(self.marketing_emails) .on_click(cx.listener(|view, checked, _, cx| { view.marketing_emails = *checked; cx.notify(); })) ) ) ) ``` ### 紧凑设置列表 ```rust v_flex() .gap_3() .child( Switch::new("wifi") .label("Wi-Fi") .label_side(Side::Left) .checked(true) .small() ) .child( Switch::new("bluetooth") .label("Bluetooth") .label_side(Side::Left) .checked(false) .small() ) ``` ## 动画 Switch 包含平滑切换动画: * 切换动画时长约 150ms * 背景色会在关闭色与激活色之间过渡 * 圆点位置会平滑移动 * 禁用状态下不会触发交互动效 --- --- url: /base/primitives/table.md description: 'Semantic table primitives for composing headers, bodies, rows, and cells.' --- # Table Semantic table primitives for composing headers, bodies, rows, and cells. Like every `gpui-base` primitive, Table supplies behavior and semantic structure without imposing a product visual language. Apply GPUI styles and compose the exported parts to match your design system. ## Example The [single native Cargo entrypoint](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/components.rs) selects this primitive from the [shared showcase implementation](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/mod.rs). The same showcase is compiled once for the WASM preview above. ```bash cargo run -p gpui-base --example components -- table ``` ## Import ```rust use gpui_kit::base::{Table, TableBody, TableCell, TableHead, TableHeader, TableRow}; ``` ## Anatomy and API The example composes `Table`, `TableBody`, `TableCell`, `TableHead`, `TableHeader`, `TableRow`. GPUI's standard styling and event traits provide presentation; these base types provide the interaction structure. The authoritative module is [`components/table.rs`](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/components/table.rs). Native and browser previews compile this same file. ## State and events Rows and cells are stateless composition; sorting, selection, and mutations remain in the parent. Keep controlled state on the parent render type or in a GPUI entity. Update it in callbacks and call `cx.notify()`; do not recreate persistent entities during every render. ## Complete Rust example The complete implementation used by the runnable showcase is embedded directly from Rust source: The command above supplies application initialization, window creation, and shared `BaseShowcase` state. ## Accessibility Use headers, preserve reading order, and separately expose sort and selection controls. ## Notes Use stable element IDs where accepted. Verify focus, hover, active, selected, disabled, reduced-motion, and high-contrast appearances in the consuming design system. --- --- url: /zh-CN/base/primitives/table.md description: 用于组合表头、表体、行和单元格的语义表格原语。 --- # Table 用于组合表头、表体、行和单元格的语义表格原语。 和所有 GPUI Base 原语一样,Table 只提供行为和语义结构,不规定产品视觉语言。请使用 GPUI 样式并组合导出的部件,使其符合你的设计系统。 ## 示例 原生示例和页面上方的 WASM 预览共用同一份实现: ```bash cargo run -p gpui-base --example components -- table ``` ## 导入 ```rust use gpui_kit::base::{Table, TableBody, TableCell, TableHead, TableHeader, TableRow}; ``` ## 结构与 API 示例组合上述公开类型。GPUI 的标准样式和事件 trait 负责表现,Base 类型负责交互结构。权威实现位于 [`components/table.rs`](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/components/table.rs),原生与浏览器预览编译的是同一文件。 ## 状态与事件 排序、选择和数据状态由应用持有,表格原语只表达结构。 受控状态应保存在父渲染类型或 GPUI entity 中;在回调中更新并调用 `cx.notify()`,不要在每次渲染时重建持久 entity。 ## 完整 Rust 示例 ## 可访问性 保留表头与单元格关系;交互式行或单元格必须有清晰焦点和名称。 ## 注意事项 在支持的位置使用稳定元素 ID,并在消费端设计系统中验证焦点、悬停、按下、选中、禁用、减少动态效果和高对比度状态。 --- --- url: /docs/components/table.md description: A basic table component for directly rendering tabular data. --- # Table A simple, stateless, composable table component for rendering tabular data. Unlike [DataTable], this component does not include virtual scrolling, sorting, or column management — it is designed for straightforward data display using a declarative API. ## Import ```rust use gpui_kit::component::table::{ Table, TableHeader, TableBody, TableFooter, TableRow, TableHead, TableCell, TableCaption, }; ``` ## Usage ### Basic Table ```rust Table::new() .child(TableHeader::new().child( TableRow::new() .child(TableHead::new().child("Name")) .child(TableHead::new().child("Email")) .child(TableHead::new().text_right().child("Amount")) )) .child(TableBody::new() .child(TableRow::new() .child(TableCell::new().child("John")) .child(TableCell::new().child("john@example.com")) .child(TableCell::new().text_right().child("$100.00"))) .child(TableRow::new() .child(TableCell::new().child("Jane")) .child(TableCell::new().child("jane@example.com")) .child(TableCell::new().text_right().child("$200.00"))) ) .child(TableCaption::new().child("A list of recent invoices.")) ``` ### With Footer ```rust Table::new() .child(TableHeader::new().child( TableRow::new() .child(TableHead::new().child("Invoice")) .child(TableHead::new().child("Status")) .child(TableHead::new().text_right().child("Amount")) )) .child(TableBody::new() .child(TableRow::new() .child(TableCell::new().child("INV001")) .child(TableCell::new().child("Paid")) .child(TableCell::new().text_right().child("$250.00"))) ) .child(TableFooter::new().child( TableRow::new() .child(TableCell::new().child("Total")) .child(TableCell::new().child("")) .child(TableCell::new().text_right().child("$250.00")) )) ``` ### Column Widths Use `.w()` on `TableHead` and `TableCell` to set fixed column widths: ```rust TableRow::new() .child(TableHead::new().w(px(80.)).child("ID")) .child(TableHead::new().child("Name")) // flex-1 .child(TableHead::new().w(px(120.)).child("Date")) ``` ### Text Alignment ```rust // Center-aligned header TableHead::new().text_center().child("Status") // Right-aligned cell (e.g., for numbers) TableCell::new().text_right().child("$1,000.00") ``` ### Without Border (via Styled) All table sub-components implement the `Styled` trait, so you can customize styles directly: ```rust // Remove border and rounded corners Table::new() .border_0() .rounded_none() .child(/* ... */) ``` ### Custom Styling Since all components implement `Styled`, you can apply any GPUI style: ```rust // Custom row hover TableRow::new() .bg(cx.theme().table_even) .child(/* ... */) // Custom cell padding TableCell::new() .px_4() .child("Custom padded content") ``` ## Sub-components | Component | Description | |-----------|-------------| | `Table` | Root container with border, rounded corners, and background | | `TableHeader` | Header section with distinct background and font weight | | `TableBody` | Body section wrapping data rows | | `TableFooter` | Footer section with top border | | `TableRow` | A flex row with bottom border | | `TableHead` | Header cell with alignment and width options | | `TableCell` | Data cell with alignment and width options | | `TableCaption` | Caption text below the table | ## API Reference ### Table * `new()` - Create a new table * Implements `Styled`, `ParentElement`, `Sizable`, `RenderOnce` ### TableHead / TableCell * `new()` - Create a new head/cell * `w(width)` - Set fixed width (otherwise flex-1) * `text_center()` - Center-align content * `text_right()` - Right-align content * Implements `Styled`, `ParentElement`, `RenderOnce` ### TableHeader / TableBody / TableFooter / TableRow / TableCaption * `new()` - Create a new instance * Implements `Styled`, `ParentElement`, `RenderOnce` ## Table vs DataTable | Feature | Table | DataTable | |---------|-------|-----------| | Virtual scrolling | No | Yes | | Column sorting | No | Yes | | Column resizing | No | Yes | | Column moving | No | Yes | | Cell selection | No | Yes | | Row selection | No | Yes | | Infinite loading | No | Yes | | Keyboard navigation | No | Yes | | State management | Stateless | TableState | | Best for | Small, static data | Large, interactive datasets | [DataTable]: ./data-table.md --- --- url: /zh-CN/docs/components/table.md description: 一个用于直接渲染表格数据的基础表格组件。 --- # Table Table 是一个简单、无状态、可组合的表格组件,用于渲染表格型数据。与 [DataTable] 不同,它不包含虚拟滚动、排序或列管理能力,更适合直接用声明式 API 展示较小且静态的数据。 ## 导入 ```rust use gpui_kit::component::table::{ Table, TableHeader, TableBody, TableFooter, TableRow, TableHead, TableCell, TableCaption, }; ``` ## 用法 ### 基础表格 ```rust Table::new() .child(TableHeader::new().child( TableRow::new() .child(TableHead::new().child("Name")) .child(TableHead::new().child("Email")) .child(TableHead::new().text_right().child("Amount")) )) .child(TableBody::new() .child(TableRow::new() .child(TableCell::new().child("John")) .child(TableCell::new().child("john@example.com")) .child(TableCell::new().text_right().child("$100.00"))) .child(TableRow::new() .child(TableCell::new().child("Jane")) .child(TableCell::new().child("jane@example.com")) .child(TableCell::new().text_right().child("$200.00"))) ) .child(TableCaption::new().child("A list of recent invoices.")) ``` ### 带 Footer ```rust Table::new() .child(TableHeader::new().child( TableRow::new() .child(TableHead::new().child("Invoice")) .child(TableHead::new().child("Status")) .child(TableHead::new().text_right().child("Amount")) )) .child(TableBody::new() .child(TableRow::new() .child(TableCell::new().child("INV001")) .child(TableCell::new().child("Paid")) .child(TableCell::new().text_right().child("$250.00"))) ) .child(TableFooter::new().child( TableRow::new() .child(TableCell::new().child("Total")) .child(TableCell::new().child("")) .child(TableCell::new().text_right().child("$250.00")) )) ``` ### 列宽 可以在 `TableHead` 和 `TableCell` 上使用 `.w()` 设置固定列宽: ```rust TableRow::new() .child(TableHead::new().w(px(80.)).child("ID")) .child(TableHead::new().child("Name")) .child(TableHead::new().w(px(120.)).child("Date")) ``` ### 文本对齐 ```rust TableHead::new().text_center().child("Status") TableCell::new().text_right().child("$1,000.00") ``` ### 去掉边框 所有表格子组件都实现了 `Styled`,可以直接自定义样式: ```rust Table::new() .border_0() .rounded_none() .child(/* ... */) ``` ### 自定义样式 ```rust TableRow::new() .bg(cx.theme().table_even) .child(/* ... */) TableCell::new() .px_4() .child("Custom padded content") ``` ## 子组件 | 组件 | 说明 | | --- | --- | | `Table` | 根容器,带边框、圆角和背景 | | `TableHeader` | 表头区域 | | `TableBody` | 表体区域 | | `TableFooter` | 表尾区域 | | `TableRow` | 一行数据 | | `TableHead` | 表头单元格 | | `TableCell` | 数据单元格 | | `TableCaption` | 表格下方说明文字 | ## API 摘要 ### Table * `new()` - 创建新表格 * 实现了 `Styled`、`ParentElement`、`Sizable`、`RenderOnce` ### TableHead / TableCell * `new()` - 创建单元格 * `w(width)` - 设置固定宽度 * `text_center()` - 居中对齐 * `text_right()` - 右对齐 ### TableHeader / TableBody / TableFooter / TableRow / TableCaption * `new()` - 创建实例 * 实现了 `Styled`、`ParentElement`、`RenderOnce` ## Table 和 DataTable 的区别 | 特性 | Table | DataTable | | --- | --- | --- | | 虚拟滚动 | No | Yes | | 列排序 | No | Yes | | 列宽调整 | No | Yes | | 列拖动 | No | Yes | | 单元格选择 | No | Yes | | 行选择 | No | Yes | | 无限加载 | No | Yes | | 键盘导航 | No | Yes | | 状态管理 | Stateless | TableState | | 适用场景 | 小型静态数据 | 大型交互式数据集 | [DataTable]: ./data-table.md --- --- url: /base/primitives/tabs.md description: A tab list and accessible tab controls with controlled selection. --- # Tabs A tab list and accessible tab controls with controlled selection. Like every `gpui-base` primitive, Tabs supplies behavior and semantic structure without imposing a product visual language. Apply GPUI styles and compose the exported parts to match your design system. ## Example The [single native Cargo entrypoint](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/components.rs) selects this primitive from the [shared showcase implementation](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/mod.rs). The same showcase is compiled once for the WASM preview above. ```bash cargo run -p gpui-base --example components -- tabs ``` ## Import ```rust use gpui_kit::base::{Tab, Tabs}; ``` ## Anatomy and API The example composes `Tab`, `Tabs`. GPUI's standard styling and event traits provide presentation; these base types provide the interaction structure. The authoritative module is [`components/tabs.rs`](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/components/tabs.rs). Native and browser previews compile this same file. ## State and events The parent owns selected index/value; each tab reflects it and click handlers update the parent. Keep controlled state on the parent render type or in a GPUI entity. Update it in callbacks and call `cx.notify()`; do not recreate persistent entities during every render. ## Complete Rust example The complete implementation used by the runnable showcase is embedded directly from Rust source: The command above supplies application initialization, window creation, and shared `BaseShowcase` state. ## Accessibility Associate tabs with panels, expose selection, and support keyboard traversal. ## Notes Use stable element IDs where accepted. Verify focus, hover, active, selected, disabled, reduced-motion, and high-contrast appearances in the consuming design system. --- --- url: /zh-CN/base/primitives/tabs.md description: 带受控选择的标签列表和可访问标签控件。 --- # Tabs 带受控选择的标签列表和可访问标签控件。 和所有 GPUI Base 原语一样,Tabs 只提供行为和语义结构,不规定产品视觉语言。请使用 GPUI 样式并组合导出的部件,使其符合你的设计系统。 ## 示例 原生示例和页面上方的 WASM 预览共用同一份实现: ```bash cargo run -p gpui-base --example components -- tabs ``` ## 导入 ```rust use gpui_kit::base::{Tab, Tabs}; ``` ## 结构与 API 示例组合上述公开类型。GPUI 的标准样式和事件 trait 负责表现,Base 类型负责交互结构。权威实现位于 [`components/tabs.rs`](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/components/tabs.rs),原生与浏览器预览编译的是同一文件。 ## 状态与事件 父级保存活动标签,激活标签后更新对应面板。 受控状态应保存在父渲染类型或 GPUI entity 中;在回调中更新并调用 `cx.notify()`,不要在每次渲染时重建持久 entity。 ## 完整 Rust 示例 ## 可访问性 保留标签列表、标签与面板关系,以及方向键和焦点行为。 ## 注意事项 在支持的位置使用稳定元素 ID,并在消费端设计系统中验证焦点、悬停、按下、选中、禁用、减少动态效果和高对比度状态。 --- --- url: /docs/components/tabs.md description: >- A set of layered sections of content—known as tab panels—that are displayed one at a time. --- # Tabs A tabbed interface component for organizing content into separate sections. Supports multiple variants, sizes, navigation controls, and interactive features like reordering and prefix/suffix elements. ## Import ```rust use gpui_kit::component::tab::{Tab, TabBar}; ``` ## Usage ### Basic Tabs ```rust TabBar::new("tabs") .selected_index(0) .on_click(|selected_index, _, _| { println!("Tab {} selected", selected_index); }) .child(Tab::new().label("Account")) .child(Tab::new().label("Profile")) .child(Tab::new().label("Settings")) ``` ### Tab Variants #### Default Tabs ```rust TabBar::new("default-tabs") .selected_index(0) .child(Tab::new().label("Account")) .child(Tab::new().label("Profile")) .child(Tab::new().label("Documents")) ``` #### Underline Tabs ```rust TabBar::new("underline-tabs") .underline() .selected_index(0) .child(Tab::new().label("Account")) .child(Tab::new().label("Profile")) .child(Tab::new().label("Documents")) ``` #### Pill Tabs ```rust TabBar::new("pill-tabs") .pill() .selected_index(0) .child(Tab::new().label("Account")) .child(Tab::new().label("Profile")) .child(Tab::new().label("Documents")) ``` #### Outline Tabs ```rust TabBar::new("outline-tabs") .outline() .selected_index(0) .child(Tab::new().label("Account")) .child(Tab::new().label("Profile")) .child(Tab::new().label("Documents")) ``` #### Segmented Tabs ```rust use gpui_kit::component::IconName; TabBar::new("segmented-tabs") .segmented() .selected_index(0) .child(IconName::Bot) .child(IconName::Calendar) .child(IconName::Map) .children(vec!["Settings", "About"]) ``` ### Tab Sizes ```rust // Extra Small TabBar::new("tabs").xsmall() .child(Tab::new().label("Small")) // Small TabBar::new("tabs").small() .child(Tab::new().label("Small")) // Medium (default) TabBar::new("tabs") .child(Tab::new().label("Medium")) // Large TabBar::new("tabs").large() .child(Tab::new().label("Large")) ``` ### Tabs with Icons ```rust use gpui_kit::component::{Icon, IconName}; TabBar::new("icon-tabs") .child(Tab::default().icon(IconName::User).with_variant(TabVariant::Tab)) .child(Tab::default().icon(IconName::Settings).with_variant(TabVariant::Tab)) .child(Tab::default().icon(IconName::Mail).with_variant(TabVariant::Tab)) ``` ### Tabs with Prefix and Suffix ```rust use gpui_kit::component::button::Button; use gpui_kit::component::{h_flex, IconName}; TabBar::new("tabs-with-controls") .prefix( h_flex() .gap_1() .child(Button::new("back").ghost().xsmall().icon(IconName::ArrowLeft)) .child(Button::new("forward").ghost().xsmall().icon(IconName::ArrowRight)) ) .suffix( h_flex() .gap_1() .child(Button::new("inbox").ghost().xsmall().icon(IconName::Inbox)) .child(Button::new("more").ghost().xsmall().icon(IconName::Ellipsis)) ) .child(Tab::new().label("Account")) .child(Tab::new().label("Profile")) .child(Tab::new().label("Settings")) ``` ### Disabled Tabs ```rust TabBar::new("tabs-with-disabled") .child(Tab::new().label("Account")) .child(Tab::new().label("Profile").disabled(true)) .child(Tab::new().label("Settings")) ``` ### Dynamic Tabs ```rust struct TabsView { active_tab: usize, tabs: Vec, } impl Render for TabsView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { TabBar::new("dynamic-tabs") .selected_index(self.active_tab) .on_click(cx.listener(|view, index, _, cx| { view.active_tab = *index; cx.notify(); })) .children( self.tabs .iter() .map(|tab_name| Tab::new().label(tab_name.clone())) ) } } ``` ### Tabs with Menu Use `menu` option to enable a dropdown menu for tab selection when there are many tabs, this is default `false`. If enable, the will have a dropdown button at the end of the tab bar to show all tabs in a menu. ```rust TabBar::new("tabs-with-menu") .menu(true) .selected_index(0) .child(Tab::new().label("Account")) .child(Tab::new().label("Profile")) .child(Tab::new().label("Documents")) .child(Tab::new().label("Mail")) .child(Tab::new().label("Settings")) ``` ### Maximum Tab Width Use `max_width` to cap the width of each tab. For tabs created with `.label()`, text longer than the limit is truncated with an ellipsis automatically. Icon-only tabs (`.icon()`) are not affected. Prefix and suffix elements (e.g. a close button) are never truncated — the label yields space first. The *more* menu (when enabled) still shows the full label text. Tabs built from custom children (via `.child()`) are only given the width limit; add `truncate()` yourself to the part that should shrink. ```rust use gpui_kit::{div, px}; TabBar::new("tabs-with-max-width") .max_width(px(100.)) .menu(true) .selected_index(0) .child(Tab::new().label("Account Settings & Preferences")) .child(Tab::new().label("Documents & Files")) .child(Tab::new().label("Appearance & Themes")) .child( Tab::new().child( h_flex() .gap_1() .child(Icon::new(IconName::Bot)) .child(div().truncate().child("Custom Child Tab")), ), ) ``` ### Scrollable Tabs ```rust use gpui_kit::ScrollHandle; struct ScrollableTabsView { scroll_handle: ScrollHandle, } impl Render for ScrollableTabsView { fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { TabBar::new("scrollable-tabs") .track_scroll(&self.scroll_handle) .child(Tab::new().label("Very Long Tab Name 1")) .child(Tab::new().label("Very Long Tab Name 2")) .child(Tab::new().label("Very Long Tab Name 3")) .child(Tab::new().label("Very Long Tab Name 4")) .child(Tab::new().label("Very Long Tab Name 5")) } } ``` ### Individual Tab Configuration ```rust TabBar::new("custom-tabs") .child( Tab::new().label("Custom Tab") .id("custom-id") .prefix(IconName::Star) .suffix(IconName::X) .on_click(|_, _, _| { println!("Custom tab clicked"); }) ) ``` ## API Reference ### TabBar | Method | Description | | --------------------------- | -------------------------------------------------- | | `new(id)` | Create a new tab bar with the given ID | | `child(tab)` | Add a tab to the bar | | `children(tabs)` | Add multiple tabs to the bar | | `selected_index(index)` | Set the active tab index | | `on_click(fn)` | Callback when a tab is clicked, receives tab index | | `prefix(element)` | Add element before the tabs | | `suffix(element)` | Add element after the tabs | | `last_empty_space(element)` | Custom element for empty space at the end | | `track_scroll(handle)` | Enable scrolling with a scroll handle | | `with_menu(bool)` | Enable dropdown menu for tab selection | | `max_width(width)` | Set maximum width of each tab; truncates | ### TabBar Variants | Method | Description | | ----------------------- | ------------------------------------ | | `with_variant(variant)` | Set the tab variant for all children | | `underline()` | Use underline variant | | `pill()` | Use pill variant | | `outline()` | Use outline variant | | `segmented()` | Use segmented variant | ### Tab | Method | Description | | ----------------------- | ---------------------------------------------- | | `new(label)` | Create a new tab with a label | | `empty()` | Create an empty tab | | `icon(icon)` | Create a tab with only an icon | | `id(id)` | Set custom ID for the tab | | `with_variant(variant)` | Set the tab variant | | `pill()` | Use pill variant | | `outline()` | Use outline variant | | `segmented()` | Use segmented variant | | `underline()` | Use underline variant | | `prefix(element)` | Add element before tab content | | `suffix(element)` | Add element after tab content | | `disabled(bool)` | Set disabled state | | `selected(bool)` | Set selected state (usually handled by TabBar) | | `on_click(fn)` | Custom click handler for individual tab | ### TabVariant ```rust pub enum TabVariant { Tab, // Default bordered tabs Outline, // Rounded outline tabs Pill, // Rounded pill-shaped tabs Segmented, // Segmented control style Underline, // Underline indicator tabs } ``` ### Styling Both `TabBar` and `Tab` implement `Sizable` trait: * `xsmall()` - Extra small size * `small()` - Small size * `medium()` - Medium size (default) * `large()` - Large size ## Advanced Examples ### Custom Tab Content ```rust Tab::empty() .child( h_flex() .items_center() .gap_2() .child(IconName::Folder) .child("Documents") .child( div() .px_1() .py_0p5() .text_xs() .bg(cx.theme().accent) .text_color(cx.theme().accent_foreground) .rounded(cx.theme().radius.half()) .child("12") ) ) ``` ### Tabs with State Management ```rust struct TabsWithContent { active_tab: usize, tab_contents: Vec, } impl TabsWithContent { fn render_tab_content(&self, cx: &mut Context) -> impl IntoElement { match self.active_tab { 0 => div().child("Account content"), 1 => div().child("Profile content"), 2 => div().child("Settings content"), _ => div().child("Unknown content"), } } } impl Render for TabsWithContent { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .child( TabBar::new("content-tabs") .selected_index(self.active_tab) .on_click(cx.listener(|view, index, _, cx| { view.active_tab = *index; cx.notify(); })) .child(Tab::new().label("Account")) .child(Tab::new().label("Profile")) .child(Tab::new().label("Settings")) ) .child( div() .flex_1() .p_4() .child(self.render_tab_content(cx)) ) } } ``` ### Tabs with Close Buttons While the basic Tab component doesn't include closeable functionality, you can create closeable tabs using suffix elements: ```rust struct CloseableTabsView { tabs: Vec, active_tab: usize, } impl CloseableTabsView { fn close_tab(&mut self, index: usize, cx: &mut Context) { if self.tabs.len() > 1 { self.tabs.remove(index); if self.active_tab >= index && self.active_tab > 0 { self.active_tab -= 1; } cx.notify(); } } } impl Render for CloseableTabsView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { TabBar::new("closeable-tabs") .selected_index(self.active_tab) .on_click(cx.listener(|view, index, _, cx| { view.active_tab = *index; cx.notify(); })) .children( self.tabs .iter() .enumerate() .map(|(index, tab_name)| { Tab::new().label(tab_name.clone()) .suffix( Button::new(format!("close-{}", index)) .icon(IconName::X) .ghost() .xsmall() .on_click(cx.listener(move |view, _, _, cx| { view.close_tab(index, cx); })) ) }) ) } } ``` ## Notes * The `TabBar` manages the selection state of all child tabs * Individual tab `on_click` handlers are ignored when `TabBar.on_click` is set * Tabs automatically inherit the variant and size from their parent `TabBar` * The `with_menu` option adds a dropdown for tab selection when there are many tabs * Scrolling is automatically enabled when tabs overflow the container width * The dock system provides advanced closeable tab functionality for complex layouts --- --- url: /zh-CN/docs/components/tabs.md description: 将内容拆分为多个标签面板并逐个切换显示的组件。 --- # Tabs Tabs 用于把内容组织成多个独立分区,一次只显示一个标签面板。它支持多种外观、尺寸、导航控制,以及前后缀元素、滚动和菜单等交互能力。 ## 导入 ```rust use gpui_kit::component::tab::{Tab, TabBar}; ``` ## 用法 ### 基础 Tabs ```rust TabBar::new("tabs") .selected_index(0) .on_click(|selected_index, _, _| { println!("Tab {} selected", selected_index); }) .child(Tab::new().label("Account")) .child(Tab::new().label("Profile")) .child(Tab::new().label("Settings")) ``` ### 标签样式 #### 默认样式 ```rust TabBar::new("default-tabs") .selected_index(0) .child(Tab::new().label("Account")) .child(Tab::new().label("Profile")) .child(Tab::new().label("Documents")) ``` #### Underline ```rust TabBar::new("underline-tabs") .underline() .selected_index(0) .child(Tab::new().label("Account")) .child(Tab::new().label("Profile")) .child(Tab::new().label("Documents")) ``` #### Pill ```rust TabBar::new("pill-tabs") .pill() .selected_index(0) .child(Tab::new().label("Account")) .child(Tab::new().label("Profile")) .child(Tab::new().label("Documents")) ``` #### Outline ```rust TabBar::new("outline-tabs") .outline() .selected_index(0) .child(Tab::new().label("Account")) .child(Tab::new().label("Profile")) .child(Tab::new().label("Documents")) ``` #### Segmented ```rust use gpui_kit::component::IconName; TabBar::new("segmented-tabs") .segmented() .selected_index(0) .child(IconName::Bot) .child(IconName::Calendar) .child(IconName::Map) .children(vec!["Settings", "About"]) ``` ### 不同尺寸 ```rust TabBar::new("tabs").xsmall() .child(Tab::new().label("Small")) TabBar::new("tabs").small() .child(Tab::new().label("Small")) TabBar::new("tabs") .child(Tab::new().label("Medium")) TabBar::new("tabs").large() .child(Tab::new().label("Large")) ``` ### 带图标的标签 ```rust use gpui_kit::component::{Icon, IconName}; TabBar::new("icon-tabs") .child(Tab::default().icon(IconName::User).with_variant(TabVariant::Tab)) .child(Tab::default().icon(IconName::Settings).with_variant(TabVariant::Tab)) .child(Tab::default().icon(IconName::Mail).with_variant(TabVariant::Tab)) ``` ### 前缀和后缀 ```rust use gpui_kit::component::button::Button; use gpui_kit::component::{h_flex, IconName}; TabBar::new("tabs-with-controls") .prefix( h_flex() .gap_1() .child(Button::new("back").ghost().xsmall().icon(IconName::ArrowLeft)) .child(Button::new("forward").ghost().xsmall().icon(IconName::ArrowRight)) ) .suffix( h_flex() .gap_1() .child(Button::new("inbox").ghost().xsmall().icon(IconName::Inbox)) .child(Button::new("more").ghost().xsmall().icon(IconName::Ellipsis)) ) .child(Tab::new().label("Account")) .child(Tab::new().label("Profile")) .child(Tab::new().label("Settings")) ``` ### 禁用标签 ```rust TabBar::new("tabs-with-disabled") .child(Tab::new().label("Account")) .child(Tab::new().label("Profile").disabled(true)) .child(Tab::new().label("Settings")) ``` ### 动态标签 ```rust struct TabsView { active_tab: usize, tabs: Vec, } impl Render for TabsView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { TabBar::new("dynamic-tabs") .selected_index(self.active_tab) .on_click(cx.listener(|view, index, _, cx| { view.active_tab = *index; cx.notify(); })) .children( self.tabs .iter() .map(|tab_name| Tab::new().label(tab_name.clone())) ) } } ``` ### 菜单模式 当标签很多时,可以开启 `menu(true)`,在标签栏末尾显示下拉菜单按钮: ```rust TabBar::new("tabs-with-menu") .menu(true) .selected_index(0) .child(Tab::new().label("Account")) .child(Tab::new().label("Profile")) .child(Tab::new().label("Documents")) .child(Tab::new().label("Mail")) .child(Tab::new().label("Settings")) ``` ### 最大标签宽度 使用 `max_width` 限制每个标签的最大宽度。通过 `.label()` 创建的标签会自动截断超长文本并显示省略号。仅图标的标签(`.icon()`)不受此限制。前缀和后缀元素(例如关闭按钮)不会被截断——标签文本会优先让出空间。如果启用了溢出菜单,下拉项仍会显示完整标签文本。 通过 `.child()` 传入自定义内容的标签只会应用宽度限制,需要自行在应当收缩的部分调用 `truncate()`。 ```rust use gpui_kit::{div, px}; TabBar::new("tabs-with-max-width") .max_width(px(100.)) .menu(true) .selected_index(0) .child(Tab::new().label("Account Settings & Preferences")) .child(Tab::new().label("Documents & Files")) .child(Tab::new().label("Appearance & Themes")) .child( Tab::new().child( h_flex() .gap_1() .child(Icon::new(IconName::Bot)) .child(div().truncate().child("Custom Child Tab")), ), ) ``` ## API 参考 ### TabBar | 方法 | 说明 | | --- | --- | | `new(id)` | 创建新的标签栏 | | `child(tab)` | 添加单个标签 | | `children(tabs)` | 批量添加标签 | | `selected_index(index)` | 设置当前选中索引 | | `on_click(fn)` | 点击标签时触发,返回标签索引 | | `prefix(element)` | 在标签前添加元素 | | `suffix(element)` | 在标签后添加元素 | | `last_empty_space(element)` | 自定义尾部空白区域 | | `track_scroll(handle)` | 配合滚动句柄启用可滚动标签栏 | | `with_menu(bool)` | 启用下拉菜单选择 | | `max_width(width)` | 设置每个标签的最大宽度;超长文本自动截断 | ### TabBar 变体 | 方法 | 说明 | | --- | --- | | `with_variant(variant)` | 为所有子标签设置统一样式 | | `underline()` | 下划线样式 | | `pill()` | 胶囊样式 | | `outline()` | 描边样式 | | `segmented()` | 分段控制样式 | ### Tab | 方法 | 说明 | | --- | --- | | `new(label)` | 创建带标签文本的 Tab | | `empty()` | 创建空 Tab | | `icon(icon)` | 创建仅图标的 Tab | | `id(id)` | 设置自定义 ID | | `with_variant(variant)` | 设置当前 Tab 的样式 | | `prefix(element)` | 在标签内容前添加元素 | | `suffix(element)` | 在标签内容后添加元素 | | `disabled(bool)` | 设置禁用状态 | | `selected(bool)` | 设置选中状态,通常由 `TabBar` 统一管理 | | `on_click(fn)` | 为单个标签设置点击回调 | ### 样式 `TabBar` 和 `Tab` 都实现了 `Sizable` trait: * `xsmall()`:超小尺寸 * `small()`:小尺寸 * `medium()`:中尺寸,默认值 * `large()`:大尺寸 ## 说明 * `TabBar` 负责统一管理所有子标签的选中状态 * 当设置了 `TabBar.on_click` 时,单个 `Tab.on_click` 通常不会生效 * 子标签会自动继承父级 `TabBar` 的样式和尺寸 * 标签过多时可通过 `with_menu` 或滚动支持提升可用性 --- --- url: /docs/components/tag.md description: A short item that can be used to categorize or label content. --- # Tag A versatile tag component for categorizing and labeling content. Tags are compact visual indicators that help organize information and display metadata like categories, status, or properties. ## Import ```rust use gpui_kit::component::tag::Tag; ``` ## Usage ### Basic Tags ```rust // Primary tag (default filled style) Tag::primary().child("Primary") // Secondary tag Tag::secondary().child("Secondary") // Status tags Tag::danger().child("Danger") Tag::success().child("Success") Tag::warning().child("Warning") Tag::info().child("Info") ``` ### Tag Variants ```rust // Semantic variants Tag::primary().child("Featured") Tag::secondary().child("Category") Tag::danger().child("Critical") Tag::success().child("Completed") Tag::warning().child("Pending") Tag::info().child("Information") ``` ### Outline Tags ```rust // Outline style variants Tag::primary().outline().child("Primary Outline") Tag::secondary().outline().child("Secondary Outline") Tag::danger().outline().child("Error Outline") Tag::success().outline().child("Success Outline") Tag::warning().outline().child("Warning Outline") Tag::info().outline().child("Info Outline") ``` ### Tag Sizes ```rust // Small size Tag::primary().small().child("Small Tag") // Medium size (default) Tag::primary().child("Medium Tag") ``` ### Custom Colors ```rust use gpui_kit::component::ColorName; // Using predefined color names Tag::color(ColorName::Blue).child("Blue Tag") Tag::color(ColorName::Green).child("Green Tag") Tag::color(ColorName::Purple).child("Purple Tag") Tag::color(ColorName::Pink).child("Pink Tag") Tag::color(ColorName::Indigo).child("Indigo Tag") Tag::color(ColorName::Yellow).child("Yellow Tag") Tag::color(ColorName::Red).child("Red Tag") ``` ### Custom HSLA Colors ```rust use gpui_kit::{hsla, Hsla}; // Custom colors with HSLA values let color = hsla(220.0 / 360.0, 0.8, 0.5, 1.0); let foreground = hsla(0.0, 0.0, 1.0, 1.0); let border = hsla(220.0 / 360.0, 0.8, 0.4, 1.0); Tag::custom(color, foreground, border).child("Custom Color") ``` ### Rounded Corners ```rust use gpui_kit::px; // Fully rounded tags Tag::primary().rounded_full().child("Rounded Full") // Custom border radius Tag::primary().rounded(px(4.0)).child("Custom Radius") // Square corners Tag::primary().rounded(px(0.0)).child("Square Tag") ``` ### Combined Styles ```rust // Small tags with full rounding Tag::primary().small().rounded_full().child("Small Pill") Tag::success().small().rounded_full().child("Success Pill") // Outline tags with custom rounding Tag::warning().outline().rounded(px(2.0)).child("Custom Outline") // Color tags with outline style Tag::color(ColorName::Purple).outline().child("Purple Outline") ``` ## Tag Categories and Use Cases ### Status Tags ```rust // Task or item status Tag::success().child("Completed") Tag::warning().child("In Progress") Tag::danger().child("Failed") Tag::info().child("Pending Review") ``` ### Category Labels ```rust // Content categorization Tag::secondary().child("Technology") Tag::color(ColorName::Blue).child("Design") Tag::color(ColorName::Green).child("Development") Tag::color(ColorName::Purple).child("Marketing") ``` ### Priority Indicators ```rust // Priority levels Tag::danger().child("High Priority") Tag::warning().child("Medium Priority") Tag::secondary().child("Low Priority") ``` ### Feature Tags ```rust // Feature flags or attributes Tag::primary().small().child("New") Tag::success().small().child("Popular") Tag::info().small().child("Beta") Tag::warning().small().child("Limited") ``` ## API Reference ### Tag Creation Methods | Method | Description | | --------------------------- | ------------------------------------------ | | `primary()` | Create a primary tag (blue theme) | | `secondary()` | Create a secondary tag (gray theme) | | `danger()` | Create a danger tag (red theme) | | `success()` | Create a success tag (green theme) | | `warning()` | Create a warning tag (yellow/orange theme) | | `info()` | Create an info tag (blue theme) | | `color(ColorName)` | Create a tag with predefined color | | `custom(color, fg, border)` | Create a tag with custom HSLA colors | ### Style Methods | Method | Description | | ----------------- | -------------------------------------------- | | `outline()` | Apply outline style (transparent background) | | `rounded(radius)` | Set custom border radius | | `rounded_full()` | Apply full rounding (pill shape) | ### Size Methods (from Sizable trait) | Method | Description | | ----------------- | -------------------------------- | | `small()` | Small tag size (reduced padding) | | `with_size(size)` | Set custom size | ### Content Methods (from ParentElement trait) | Method | Description | | ---------------- | ---------------------------- | | `child(element)` | Add child content to the tag | ## Examples ### Tag Collections ```rust use gpui_kit::component::{h_flex, v_flex}; // Horizontal tag group h_flex() .gap_2() .child(Tag::primary().child("React")) .child(Tag::success().child("TypeScript")) .child(Tag::info().child("Next.js")) .child(Tag::warning().child("Beta")) // Vertical tag stack v_flex() .gap_1() .child(Tag::danger().small().child("Critical")) .child(Tag::warning().small().child("Important")) .child(Tag::secondary().small().child("Normal")) ``` ### Status Dashboard Tags ```rust // System status indicators h_flex() .gap_3() .child( v_flex() .child("API Status:") .child(Tag::success().child("Operational")) ) .child( v_flex() .child("Database:") .child(Tag::warning().child("Maintenance")) ) .child( v_flex() .child("Cache:") .child(Tag::danger().child("Down")) ) ``` ### Interactive Tag Lists ```rust // Note: Event handling would require additional state management // Tags themselves are display components // Filter tags (would need click handlers) h_flex() .gap_2() .child(Tag::primary().small().child("All")) .child(Tag::secondary().outline().small().child("Active")) .child(Tag::secondary().outline().small().child("Completed")) .child(Tag::secondary().outline().small().child("Archived")) ``` ### Color-Coded Categories ```rust use gpui_kit::component::ColorName; // Content type tags h_flex() .gap_2() .flex_wrap() .child(Tag::color(ColorName::Red).child("Bug")) .child(Tag::color(ColorName::Blue).child("Feature")) .child(Tag::color(ColorName::Green).child("Enhancement")) .child(Tag::color(ColorName::Purple).child("Documentation")) .child(Tag::color(ColorName::Yellow).child("Question")) .child(Tag::color(ColorName::Pink).child("Discussion")) ``` ### Pill-Style Tags ```rust // Skill tags with pill styling h_flex() .gap_2() .flex_wrap() .child(Tag::color(ColorName::Blue).rounded_full().small().child("Rust")) .child(Tag::color(ColorName::Green).rounded_full().small().child("JavaScript")) .child(Tag::color(ColorName::Purple).rounded_full().small().child("Python")) .child(Tag::color(ColorName::Red).rounded_full().small().child("Go")) ``` ## Behavior Notes * Tags automatically adjust their appearance based on the current theme * Outline tags maintain border visibility across different backgrounds * Small tags use reduced padding and border radius for compact layouts * Custom colors support both light and dark theme adaptations * Tags are display components and don't include built-in interaction handlers * Multiple tags can be combined in flex layouts for tag clouds or lists * Border radius automatically scales based on tag size unless explicitly overridden ## Design Guidelines ### When to Use Tags * **Categorization**: Group content by type, topic, or theme * **Status Indication**: Show state, progress, or health status * **Metadata Display**: Present attributes, properties, or classifications * **Filtering**: Visual indicators for active filters or selections * **Feature Flags**: Highlight new, beta, or special features ### Color Usage * **Semantic Colors**: Use danger (red) for errors, success (green) for completion, warning (yellow) for caution, info (blue) for information * **Category Colors**: Use the ColorName variants for content categorization where color coding helps with recognition * **Custom Colors**: Reserve for brand colors or specific design system requirements ### Size Guidelines * **Small Tags**: Use for compact layouts, metadata, or when space is limited * **Medium Tags**: Default size for most use cases, provides good readability and click targets * **Rounding**: Use `rounded_full()` for pill-style tags, custom `rounded()` for specific design requirements --- --- url: /zh-CN/docs/components/tag.md description: 用于分类、标记和显示元数据的紧凑标签组件。 --- # Tag Tag 是一个轻量但灵活的标签组件,适合展示分类、状态、优先级和其他元数据。它体积紧凑,适合在列表、卡片和详情页中重复使用。 ## 导入 ```rust use gpui_kit::component::tag::Tag; ``` ## 用法 ### 基础标签 ```rust Tag::primary().child("Primary") Tag::secondary().child("Secondary") Tag::danger().child("Danger") Tag::success().child("Success") Tag::warning().child("Warning") Tag::info().child("Info") ``` ### 语义变体 ```rust Tag::primary().child("Featured") Tag::secondary().child("Category") Tag::danger().child("Critical") Tag::success().child("Completed") Tag::warning().child("Pending") Tag::info().child("Information") ``` ### Outline 风格 ```rust Tag::primary().outline().child("Primary Outline") Tag::secondary().outline().child("Secondary Outline") Tag::danger().outline().child("Error Outline") Tag::success().outline().child("Success Outline") ``` ### 尺寸 ```rust Tag::primary().small().child("Small Tag") Tag::primary().child("Medium Tag") ``` ### 预设颜色 ```rust use gpui_kit::component::ColorName; Tag::color(ColorName::Blue).child("Blue Tag") Tag::color(ColorName::Green).child("Green Tag") Tag::color(ColorName::Purple).child("Purple Tag") Tag::color(ColorName::Pink).child("Pink Tag") ``` ### 自定义 HSLA 颜色 ```rust use gpui_kit::{hsla, Hsla}; let color = hsla(220.0 / 360.0, 0.8, 0.5, 1.0); let foreground = hsla(0.0, 0.0, 1.0, 1.0); let border = hsla(220.0 / 360.0, 0.8, 0.4, 1.0); Tag::custom(color, foreground, border).child("Custom Color") ``` ### 圆角 ```rust use gpui_kit::px; Tag::primary().rounded_full().child("Rounded Full") Tag::primary().rounded(px(4.0)).child("Custom Radius") Tag::primary().rounded(px(0.0)).child("Square Tag") ``` ## 常见场景 ### 状态标签 ```rust Tag::success().child("Completed") Tag::warning().child("In Progress") Tag::danger().child("Failed") Tag::info().child("Pending Review") ``` ### 分类标签 ```rust Tag::secondary().child("Technology") Tag::color(ColorName::Blue).child("Design") Tag::color(ColorName::Green).child("Development") Tag::color(ColorName::Purple).child("Marketing") ``` ### 优先级标签 ```rust Tag::danger().child("High Priority") Tag::warning().child("Medium Priority") Tag::secondary().child("Low Priority") ``` ## API 参考 ### 创建方法 | 方法 | 说明 | | --- | --- | | `primary()` | 主色标签 | | `secondary()` | 次级标签 | | `danger()` | 危险状态标签 | | `success()` | 成功状态标签 | | `warning()` | 警告状态标签 | | `info()` | 信息标签 | | `color(ColorName)` | 使用预设颜色创建标签 | | `custom(color, fg, border)` | 使用自定义 HSLA 颜色创建标签 | ### 样式方法 | 方法 | 说明 | | --- | --- | | `outline()` | 使用描边风格 | | `rounded(radius)` | 自定义圆角 | | `rounded_full()` | 完整圆角,胶囊样式 | ### 尺寸方法 | 方法 | 说明 | | --- | --- | | `small()` | 小尺寸标签 | | `with_size(size)` | 设置自定义尺寸 | ## 设计建议 * 状态类信息优先使用语义颜色,如 success、warning、danger * 分类标签可结合 `ColorName` 做稳定的颜色映射 * 空间有限时优先使用 `small()` * 纯展示标签不应默认承担交互职责 --- --- url: /base/text-selection.md description: >- Add native window-level text selection to plain text and custom GPUI participants. --- # Text Selection `gpui-base` provides window-level text selection for ordinary GPUI participants. It coordinates pointer gestures, Shift-click extension, selection across multiple text elements, copying, scrolling, scopes, and multi-window lifetime without prescribing how text is laid out or highlighted. Use it when you render text with `StyledText`, `TextLayout`, a virtualized document, or another custom GPUI `Element`. ## Get started To add text selection to a custom GPUI participant, connect its layout and paint lifecycle to the window selection state as shown below. A selectable window has three roles: 1. One `TextSelectionLayer` element owns the selection state and window pointer handlers. 2. Each independently selectable text participant owns a stable `TextSelectionHandle`. 3. During rendering, the participant registers current geometry and projects the resulting snapshot onto laid-out `TextSelectionRun`s. ### Key parts | API | Lifetime | Purpose | | --------------------------- | ------------------------------- | ----------------------------------------------------------------------------- | | `TextSelectionLayer` | Once per window | Installs window-level pointer handling and selection state. | | `TextSelection` | Static API | Queries and controls the window selection. | | `TextSelectionHandle` | Once per selectable participant | Identifies the participant and stores its callbacks and projected selection. | | `TextSelectionRegistration` | Recreated each rendered frame | Reports the current hitbox, bounds, scroll offset, scope, and document order. | | `TextSelectionRun` | Recreated during paint | Describes laid-out text for projection to a UTF-8 byte range. | | `TextSelectionProjection` | Returned by `update_runs` | Pairs each submitted run with its selected byte range. | | `TextSelectionSnapshot` | Produced when selection changes | Describes the participant's endpoints and coverage. | | `TextSelectionEvent` | Emitted to subscribers | Reports selection changes, clearing, and auto-scroll requests. | | `TextSelectionContentKey` | Stable content identity | Identifies virtualized content at a selection endpoint. | The complete flow is: 1. Retain one `TextSelectionLayer` element at the window root. 2. Create one `TextSelectionHandle` for each independently selectable participant. 3. During prepaint, call `TextSelectionHandle::register` with a `TextSelectionRegistration`. 4. During paint, pass laid-out `TextSelectionRun`s to `TextSelectionHandle::update_runs`. 5. Paint each returned byte range behind its glyphs. 6. Read or clear the window selection through `TextSelection`. The installed layer also provides familiar multi-click behavior: double-click selects a word using the same boundary rules as `Input`, while triple-click and later clicks select the newline-delimited logical line. The window state belongs to the retained `TextSelectionLayer` element. Handles and callbacks never receive or own that internal state. ## How it works `gpui-base` owns gesture coordination and range projection. The application remains responsible for layout and painting across the participant seam: ## Install the window element Add one `TextSelectionLayer` as the first child of the window root: ```rust use gpui_kit::prelude::*; use gpui_kit::{Context, Render, Window}; use gpui_kit::base::TextSelectionLayer; impl Render for AppView { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { div() .size_full() .child(TextSelectionLayer) .child(self.content.clone()) } } ``` `TextSelectionLayer` is a zero-sized element. Keep it first and mount only one per window. Calling `TextSelection::activate_scope` before the first prepaint stores the scope until the layer binds its window state. ## Create a stable handle Create one handle for the semantic lifetime of the participant. Do not create a new handle every frame. ```rust use gpui_kit::{Context, Subscription, Window}; use gpui_kit::base::TextSelectionHandle; struct DocumentView { selection: TextSelectionHandle, _selection_refresh: Subscription, } impl DocumentView { fn new(window: &Window, cx: &mut Context) -> Self { let selection = TextSelectionHandle::new("", cx); let selection_refresh = selection.refresh_window_on_change(window, cx); Self { selection, _selection_refresh: selection_refresh, } } } ``` `refresh_window_on_change` redraws only the owning window when this handle's selection changes. Retain the returned subscription for as long as the participant is rendered, or explicitly call `.detach()` when the subscription should live for the rest of the participant entity's lifetime. Use `subscribe` instead when the participant needs events or more targeted invalidation. The `fallback_copy_text` passed to `TextSelectionHandle::new` is used until the participant projects laid-out runs or supplies custom copy behavior. Use `set_fallback_copy_text` to replace it. ## Register geometry during prepaint Call `TextSelectionHandle::register(registration, window, cx)` once per rendered frame, after the handle's bounds and hitbox are known: ```rust use gpui_kit::{Bounds, Hitbox, Pixels, Window}; use gpui_kit::base::TextSelectionRegistration; fn register_selection( handle: &TextSelectionHandle, hitbox: Hitbox, bounds: Bounds, window: &mut Window, cx: &mut gpui_kit::App, ) { handle.register( TextSelectionRegistration::new(hitbox, bounds) .with_document_order(0) .with_text_bounds(vec![bounds]), window, cx, ); } ``` * `bounds` is the participant's content viewport in window coordinates. * `text_bounds` contains the visible glyph-bearing areas. Blank-only drags do not start a text selection. * `document_order` provides stable ordering between participants for cross-participant selection and copy. Do not derive semantic order from a `HashMap` or accidental paint order. * `with_scroll_offset` maps window points into scrolled content coordinates. * `with_scope` assigns an explicit opaque scope. A surrounding `.text_selection_scope(scope)` builder overrides it while that subtree renders. Handles not registered in the current frame stop participating automatically. ## Project selection onto text runs In paint, call `TextSelectionHandle::update_runs` with laid-out runs containing the exact text used to create each `TextLayout`. It returns a `TextSelectionProjection` containing UTF-8-safe byte ranges: ```rust use gpui_kit::{Bounds, Pixels, SharedString, TextLayout}; use gpui_kit::base::TextSelectionRun; fn selected_range( handle: &TextSelectionHandle, text: SharedString, layout: TextLayout, bounds: Bounds, cx: &mut gpui_kit::App, ) -> Option> { handle .update_runs( &[TextSelectionRun::new(text, layout, bounds) .with_document_order(0)], cx, ) .ranges() .iter() .next() .and_then(|range| range.clone()) } ``` Paint the returned range behind the glyphs, then paint the text normally. Wrapped selections need three kinds of highlight geometry: the remainder of the first line, full-width middle lines, and the prefix of the last line. For multiple runs, give each run a stable `document_order`. Input order is preserved in `projection.ranges()` so each range can be paired with its original layout; document order is used when composing copied text. The [shared Text Selection showcase](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/components/text_selection.rs) is the complete runnable example used by both the native command and the live Rust/WASM preview above: ```bash cargo run -p gpui-base --example components -- text-selection ``` ## Complete Rust example ## Query and control the window selection Use `TextSelection` associated functions to read or mutate the window selection. No extension trait import is required: ```rust use gpui_kit::base::TextSelection; let has_selection = TextSelection::has_selection(window, cx); let text = TextSelection::selected_text(window, cx); TextSelection::end(window, cx); // End a drag, preserving its range. TextSelection::clear(window, cx); // Clear window and participant-local ranges. ``` `selected_text` invokes participant copy callbacks only after the window and handle state leases have been released, so a callback may safely read or update selection state. ## Advanced participant adapters Plain text usually needs only `refresh_window_on_change` and `update_runs`. Rich or virtualized participants can configure additional behavior directly on the handle: | Method | Use | | -------------------------- | ------------------------------------------------------------------------------------- | | `refresh_window_on_change` | Redraw only the owning window when this handle's selection changes. | | `subscribe` | Receive `TextSelectionEvent` values for selection changes, clearing, and auto-scroll. | | `copy_with` | Export source text or include virtualized content that is not currently painted. | | `set_fallback_copy_text` | Replace the participant's fallback copy text. | | `resolve_content_key_with` | Attach a stable `TextSelectionContentKey` to an endpoint. | | `focus_with` | Focus the participant when a drag begins inside it. | | `clear_with` | Synchronously clear participant-local state when the window selection clears. | | `set_local_selection` | Report participant-local selection such as select-all. | Callbacks are invoked outside selection-state leases. They may update the participant or query `TextSelection` without causing a reentrant entity borrow. When `subscribe` receives `TextSelectionEvent::AutoScroll(Some(delta))`, feed that delta into the participant's scrolling loop; `None` stops it. Positive deltas move toward the bottom. The shared showcase demonstrates this with content taller than its viewport. For a virtualized document, inspect `TextSelectionEvent::SelectionChanged` and use `TextSelectionSnapshot::coverage()`, `window_points()`, and each endpoint's `content_point()` and `content_key()`. Coverage distinguishes a bounded participant from one selected from its start, to its end, or in full, allowing `copy_with` to include unpainted content. ## Isolate modal content with scopes Only handles in the active `TextSelectionScopeId` participate. Set the active window scope, then mark the corresponding rendered subtree: ```rust use gpui_kit::base::{ElementExt as _, TextSelection, TextSelectionScopeId}; let dialog_scope = TextSelectionScopeId::new(); TextSelection::activate_scope(dialog_scope, window, cx); let dialog = dialog_content.text_selection_scope(dialog_scope); ``` Scope stacks are isolated per window and are cleaned up even if a scoped subtree panics while rendering. Changing the active scope clears the previous selection atomically. ## Integration checklist * Retain one `TextSelectionLayer` element as the first child of each custom window root. * Keep each `TextSelectionHandle` stable across renders. * Register current geometry every rendered frame. * Use explicit document order and window-local scopes. * Pass the exact UTF-8 text used by each `TextLayout`. * Paint highlights before glyphs. * Keep parser, source export, and virtual-document knowledge in the participant. --- --- url: /base/primitives/textarea.md description: An unstyled multi-line text field with fixed rows or auto-grow behavior. --- # Textarea `Textarea` is for ordinary multi-line text. Its interface stays focused on text entry: rows, wrapping, auto-grow, value updates, insertion, replacement, and cursor position. Code-editor concepts are intentionally kept on [`Editor`](./editor.md). ## Import ```rust use gpui_kit::base::input::{InputEvent, Textarea, TextareaState}; ``` ## Fixed rows ```rust let notes = cx.new(|cx| { TextareaState::new(window, cx) .rows(5) .placeholder("Notes") .default_value("First line\nSecond line") }); Textarea::new(¬es) ``` ## Auto-grow The textarea grows between the supplied minimum and maximum row counts. Once it reaches the maximum, its content scrolls. ```rust let message = cx.new(|cx| { TextareaState::new(window, cx) .auto_grow(2, 8) .placeholder("Write a message") }); Textarea::new(&message) ``` ## Editing the value ```rust notes.update(cx, |state, cx| { state.insert("Appended text", window, cx); }); let cursor = notes.read(cx).cursor_position(cx); let value = notes.read(cx).value(); ``` Use `soft_wrap(false)` when visual wrapping is undesirable. Set `submit_on_enter(true)` only when Enter should submit instead of inserting a line break. `TextareaState` emits the same `InputEvent` variants as `InputState`. ## Presentation The control is unstyled. Your design system supplies the frame, height, colors, padding, and `InputEditorStyle`. For a styled control, see the [`gpui-component` Textarea](../../docs/components/textarea.md). ## Runnable example ```bash cargo run -p gpui-base --example components -- textarea ``` --- --- url: /zh-CN/base/primitives/textarea.md description: 支持固定行数或自动增高的无样式多行文本框。 --- # Textarea `Textarea` 用于普通多行文本,接口聚焦于行数、换行、自动增高、值更新、插入、替换和光标位置。代码编辑器概念由 [`Editor`](./editor.md) 提供。 ## 导入 ```rust use gpui_kit::base::input::{InputEvent, Textarea, TextareaState}; ``` ## 固定行数 ```rust let notes = cx.new(|cx| { TextareaState::new(window, cx) .rows(5) .placeholder("Notes") .default_value("First line\nSecond line") }); Textarea::new(¬es) ``` ## 自动增高 文本框会在指定的最小与最大行数之间增长;达到最大值后内容改为滚动。 ```rust let message = cx.new(|cx| { TextareaState::new(window, cx) .auto_grow(2, 8) .placeholder("Write a message") }); Textarea::new(&message) ``` ## 编辑值 ```rust notes.update(cx, |state, cx| state.insert("Appended text", window, cx)); let cursor = notes.read(cx).cursor_position(cx); let value = notes.read(cx).value(); ``` 不希望视觉换行时使用 `soft_wrap(false)`。只有 Enter 应提交而非换行时才设置 `submit_on_enter(true)`。`TextareaState` 发出与 `InputState` 相同的 `InputEvent`。 ## 表现 该控件没有产品样式;边框、高度、颜色、内边距和 `InputEditorStyle` 由设计系统提供。现成样式控件参见 [`gpui-component` Textarea](../../docs/components/textarea.md)。 ## 可运行示例 ```bash cargo run -p gpui-base --example components -- textarea ``` --- --- url: /docs/components/textarea.md description: 'Multi-line text input with fixed rows, soft wrapping, and auto-grow.' --- # Textarea `Textarea` is the styled control for ordinary multi-line text. Use [`Input`](./input.md) for a single line and [`Editor`](./editor.md) for source code. ## Import ```rust use gpui_kit::component::input::{Textarea, TextareaState}; ``` ## Basic usage ```rust let notes = cx.new(|cx| { TextareaState::new(window, cx) .rows(5) .placeholder("Notes") }); Textarea::new(¬es) ``` ## Auto-grow ```rust let message = cx.new(|cx| { TextareaState::new(window, cx) .auto_grow(2, 8) .placeholder("Write a message") }); Textarea::new(&message) ``` The control grows until `max_rows`; overflowing content then scrolls. ## Value and events ```rust let value = notes.read(cx).value(); notes.update(cx, |state, cx| { state.set_value("Updated notes", window, cx); }); cx.subscribe(¬es, |this, state, event: &InputEvent, cx| { if matches!(event, InputEvent::Change) { this.notes = state.read(cx).value(); cx.notify(); } }); ``` `insert`, `replace`, `cursor_position`, `soft_wrap`, `searchable`, and `submit_on_enter` are available on `TextareaState`. ## Appearance ```rust Textarea::new(¬es) .h(px(160.)) .bordered(true) .disabled(false) .readonly(false) .aria_label("Notes") ``` Unlike `disabled`, a read-only textarea keeps the normal appearance and still can be focused, selected and copied, it only rejects the changes made by the user. `Textarea` deliberately does not expose Input-only adornments such as `prefix`, `suffix`, mask toggle, or the clear button. Compose related actions beside the textarea. --- --- url: /zh-CN/docs/components/textarea.md description: 支持固定行数、软换行和自动增高的多行文本输入组件。 --- # Textarea `Textarea` 用于普通多行文本。单行输入请使用 [Input](./input.md),源代码编辑请使用 [Editor](./editor.md)。 ## 导入 ```rust use gpui_kit::component::input::{Textarea, TextareaState}; ``` ## 基础用法 ```rust let notes = cx.new(|cx| { TextareaState::new(window, cx) .rows(5) .placeholder("备注") }); Textarea::new(¬es) ``` ## 自动增高 ```rust let message = cx.new(|cx| { TextareaState::new(window, cx) .auto_grow(2, 8) .placeholder("输入消息") }); Textarea::new(&message) ``` 组件最多增长到 `max_rows`,之后内容在内部滚动。 ## 值与事件 ```rust let value = notes.read(cx).value(); notes.update(cx, |state, cx| { state.set_value("更新后的备注", window, cx); }); cx.subscribe(¬es, |this, state, event: &InputEvent, cx| { if matches!(event, InputEvent::Change) { this.notes = state.read(cx).value(); cx.notify(); } }); ``` `TextareaState` 还提供 `insert`、`replace`、`cursor_position`、 `soft_wrap`、`searchable` 和 `submit_on_enter`。 ## 外观 ```rust Textarea::new(¬es) .h(px(160.)) .bordered(true) .disabled(false) .readonly(false) .aria_label("备注") ``` 与 `disabled` 不同,只读 Textarea 保持正常外观,仍然可以聚焦、选中和复制,只是拒绝用户对内容的修改。 `Textarea` 不提供只适用于单行 Input 的前后缀、密码显示切换和清除按钮;相关操作应组合在 Textarea 外部。 --- --- url: /base/text-view.md description: Render selectable Markdown and HTML directly with gpui-base. --- # TextView `gpui-base` owns the complete `TextView` implementation for rendering Markdown and common HTML. It includes document parsing, links, images, lists, tables, code blocks, scrolling, line clamping, plugins, selection, and copying without depending on `gpui-component`. The live example above uses only `gpui-base`. Its fenced Rust block is intentionally unhighlighted: syntax highlighting is opt-in. ## Set up the window Call `gpui_kit::base::init` once during application startup and render one `TextSelectionLayer` per window. The layer coordinates selection across `TextView`, [`SelectableText`](./text-selection.md), and custom text renderers. ```rust use gpui_kit::prelude::*; use gpui_kit::{Context, Render, Window}; use gpui_kit::base::{TextSelectionLayer, TextView}; impl Render for AppView { fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { div() .size_full() .child(TextSelectionLayer) .child(TextView::markdown( "readme", "# Hello\n\nSelect and copy this **Markdown**.", )) } } ``` If the application already calls `gpui_kit::component::init`, Base initialization is included. `gpui-component::Root` also installs the window selection layer. TextView is selectable by default. While dragging a selection near a viewport edge, the shared selection layer scrolls the related `overflow_*_scroll` region automatically; no TextView scroll or selection parameter is required. Use `.selectable(false)` only to disable selection explicitly. ## Markdown and HTML Use the helpers for call-site-derived IDs, or constructors when an explicit stable ID is useful: ```rust use gpui_kit::base::{html, markdown, TextView}; let short_markdown = markdown("A **short** message."); let short_html = html("

A short message.

"); let preview = TextView::markdown("document-preview", markdown_source).scrollable(true); let article = TextView::html("article", html_source); ``` `scrollable(true)` makes the view fill its container and scroll vertically. Without it, the view grows to fit its content. `max_lines(n)` clamps a non-scrollable preview to at most `n` body-text lines. ## Complete default styling Every constructor starts with `TextViewStyle::default()`. The default contains readable neutral foreground, muted, link, selection, code-background, border, heading, paragraph, inline-code, and table styles. A Base-only application does not need to construct a style before rendering text. Override only the values owned by your design system: ```rust use gpui_kit::base::TextViewStyle; let style = TextViewStyle::default() .with_foreground(app_colors.foreground) .with_muted_foreground(app_colors.muted_foreground) .with_link(app_colors.link) .with_selection(app_colors.selection); TextView::markdown("themed", source).style(style) ``` `TextViewStyle::from_theme(&theme)` maps the semantic colors from a `gpui_kit::base::Theme`. Applications using the higher-level component theme can use `gpui_kit::component::text::text_view_style(cx.theme())`. ## Syntax highlighting is opt-in `gpui-base` does not enable syntax highlighting and has no tree-sitter language dependency. Fenced code blocks use the neutral code surface and plain foreground until the application supplies `code_block_highlighter`. The callback receives a `CodeBlock` and returns byte ranges paired with GPUI `HighlightStyle` values: ```rust use gpui_kit::HighlightStyle; use gpui_kit::base::TextView; TextView::markdown("highlighted", source).code_block_highlighter(|block| { my_highlighter(block.lang(), block.code()) .into_iter() .map(|(range, color)| { ( range, HighlightStyle { color: Some(color), ..Default::default() }, ) }) .collect() }) ``` Ranges are UTF-8 byte ranges relative to `CodeBlock::code()`. Invalid ranges are discarded. The highlighter implementation and its language registrations remain entirely application-owned. ## Retained state and streaming updates Use `TextViewState` when content changes without replacing the view: ```rust use gpui_kit::base::{TextView, TextViewState}; let document = cx.new(|cx| TextViewState::markdown(initial_source, cx)); // Render TextView::new(&document) // Later document.update(cx, |state, cx| state.set_text(updated_source, cx)); ``` Selection can copy rendered text or Markdown source through `SelectionFormat`. Link routing, code-block actions, table actions, images, and custom Markdown plugins use the same builders as the compatibility API documented on the [gpui-component TextView page](../docs/components/text-view.md). ## Runnable source The live preview and native command use the same Base-only source: ```bash cargo run -p gpui-base --example components -- text-view ``` --- --- url: /zh-CN/base/text-view.md description: 直接使用 gpui-base 渲染可选择的 Markdown 与 HTML。 --- # TextView `gpui-base` 现在拥有完整的 `TextView` 实现,可渲染 Markdown 和常用 HTML。解析、链接、图片、列表、表格、代码块、滚动、行数限制、插件、文本选择和复制都不依赖 `gpui-component`。 上方可运行示例只依赖 `gpui-base`。其中 Rust 代码块特意没有着色,因为语法高亮默认不开启。 ## 设置窗口 应用启动时调用一次 `gpui_kit::base::init`,并在每个窗口渲染一个 `TextSelectionLayer`。它统一协调 `TextView`、[`SelectableText`](./text-selection.md) 和自定义文本 renderer 的选择行为。 ```rust use gpui_kit::prelude::*; use gpui_kit::{Context, Render, Window}; use gpui_kit::base::{TextSelectionLayer, TextView}; impl Render for AppView { fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { div() .size_full() .child(TextSelectionLayer) .child(TextView::markdown( "readme", "# Hello\n\n选择并复制这段 **Markdown**。", )) } } ``` 如果应用已经调用 `gpui_kit::component::init`,其中已包含 Base 初始化;`gpui-component::Root` 也会安装窗口选择层。 TextView 默认支持选择。拖动选区靠近视口边缘时,共享选择层会自动滚动相关的 `overflow_*_scroll` 区域,不需要额外设置 TextView 的滚动或选择参数。只有明确需要禁用选择时才使用 `.selectable(false)`。 ## Markdown 与 HTML 短内容可以使用自动生成调用点 ID 的 helper,需要明确稳定 ID 时使用构造器: ```rust use gpui_kit::base::{html, markdown, TextView}; let short_markdown = markdown("一段 **Markdown**。"); let short_html = html("

一段 HTML

"); let preview = TextView::markdown("document-preview", markdown_source).scrollable(true); let article = TextView::html("article", html_source); ``` `scrollable(true)` 让视图填满容器并垂直滚动;未设置时视图随内容增长。`max_lines(n)` 可把非滚动预览限制在最多 `n` 行正文高度。 ## 可直接使用的默认样式 所有构造方式都会使用 `TextViewStyle::default()`。默认值已经包含可读的正文、次要文字、链接、选择色、代码背景、边框、标题、段落、行内代码和表格样式。只使用 Base 的项目不需要先定义一套样式才能显示文本。 应用可以只覆盖自己设计系统负责的颜色: ```rust use gpui_kit::base::TextViewStyle; let style = TextViewStyle::default() .with_foreground(app_colors.foreground) .with_muted_foreground(app_colors.muted_foreground) .with_link(app_colors.link) .with_selection(app_colors.selection); TextView::markdown("themed", source).style(style) ``` `TextViewStyle::from_theme(&theme)` 可读取 `gpui_kit::base::Theme` 的语义颜色。使用上层组件主题时,可调用 `gpui_kit::component::text::text_view_style(cx.theme())`。 ## 语法高亮由使用者开启 `gpui-base` 默认不启用语法高亮,也不包含 tree-sitter 语言依赖。应用未提供 `code_block_highlighter` 时,围栏代码块只使用中性的代码背景和普通前景色。 回调接收 `CodeBlock`,并返回 UTF-8 字节范围及对应的 GPUI `HighlightStyle`: ```rust use gpui_kit::HighlightStyle; use gpui_kit::base::TextView; TextView::markdown("highlighted", source).code_block_highlighter(|block| { my_highlighter(block.lang(), block.code()) .into_iter() .map(|(range, color)| { ( range, HighlightStyle { color: Some(color), ..Default::default() }, ) }) .collect() }) ``` 范围相对于 `CodeBlock::code()`;无效范围会被丢弃。高亮器实现和语言注册完全由应用管理。 ## 保留状态与动态更新 内容需要持续更新时使用 `TextViewState`: ```rust use gpui_kit::base::{TextView, TextViewState}; let document = cx.new(|cx| TextViewState::markdown(initial_source, cx)); TextView::new(&document) document.update(cx, |state, cx| state.set_text(updated_source, cx)); ``` 通过 `SelectionFormat` 可以选择复制渲染文本或 Markdown 源码。链接路由、代码块操作、表格操作、图片和 Markdown 插件继续使用与兼容 API 相同的 builder,详见 [gpui-component TextView 文档](../docs/components/text-view.md)。 ## 可运行源码 网页预览和本地命令使用同一份 Base-only 源码: ```bash cargo run -p gpui-base --example components -- text-view ``` --- --- url: /docs/components/text-view.md description: Renders Markdown and HTML text with optional custom Markdown plugins. --- # TextView `TextView` renders formatted text in GPUI. It supports Markdown and simple HTML, text selection, code block actions, and custom Markdown plugins for project-specific syntax. The canonical implementation now lives in `gpui-base`; this module remains a compatibility re-export and provides component-theme adaptation. Base-only setup, complete default styling, and opt-in syntax highlighting are documented on [GPUI Base TextView](/base/text-view.md). TextView is selectable by default and uses the shared window selection engine from `gpui-base`. Use `.selectable(false)` only when selection must be disabled. See [GPUI Base Text Selection](/base/text-selection.md) when integrating plain text or a custom renderer with the same selection. ## Import ```rust use gpui_kit::component::text::{markdown, TextView}; ``` ## Usage ### Markdown Use the `markdown` helper when you only need to render Markdown text: ```rust use gpui_kit::component::text::markdown; markdown("# Hello\n\nThis is **Markdown**.") .scrollable(true) ``` You can also construct a `TextView` directly when you need a stable id: ```rust use gpui_kit::component::text::TextView; TextView::markdown("preview", markdown_source) ``` ### HTML ```rust TextView::html("html-preview", "Hello") ``` ### Clamp to a number of lines Use `max_lines` to render a bounded preview of rich content — for example a collapsed "show more" section. The view's height is capped at `n` × the base line height, and a line of glyphs is never cut in half: a line that would straddle the bottom of the box is left out whole, across paragraphs, lists, headings, code blocks and tables: ```rust TextView::markdown("preview", markdown_source).max_lines(5) ``` Nothing is shown with less than a line of itself to show, so the border and padding a table row leads with never strands at the bottom. Whatever has more than that is cut on the box edge and keeps the part that fits, so an image crossing the edge shows instead of disappearing and leaving blank space behind. `TextViewState::is_clamped()` reports whether the previous painted frame actually clipped content, so the caller can decide whether to render an "expand" affordance. `n` counts lines of body text, so paragraph spacing and taller lines mean fewer of them fit inside the capped height, and a line taller than the whole budget keeps the part that fits rather than emptying the box. `max_lines` only applies to the fit-content mode and is ignored when `scrollable` is set. ## Link Click Handling Use `on_link_click` when links should be routed by the application instead of being opened directly by `App::open_url`. The callback receives the resolved URL and the original GPUI `ClickEvent`, so it can distinguish mouse buttons, keyboard activation, touch, and modifier keys: ```rust use gpui_kit::ClickEvent; use gpui_kit::component::text::markdown; markdown("[Open the project](https://github.com/longbridge/gpui-kit)") .on_link_click(|url, event, _window, cx| { if event.is_right_click() { println!("Show a context menu for {url}"); return; } match event { ClickEvent::Mouse(click) if click.up.modifiers.control => { println!("Open {url} in an internal view"); } _ => cx.open_url(url), } }) ``` Installing a handler consumes the link event and disables the default URL opening behavior. If no handler is installed, links continue to use `App::open_url` as usual. The callback is used for both text links and linked images. ## Markdown Plugins Use `.plugin(...)` to support custom Markdown formats. A plugin owns both parsing and rendering, so callers only need to attach it to the `TextView`: ```rust markdown(source) .plugin(TickerPlugin::new()) ``` A Markdown plugin implements `MarkdownPlugin`: ```rust use gpui_kit::{App, IntoElement, ParentElement as _, Window}; use gpui_kit::component::text::{ markdown_ast, MarkdownNode, MarkdownParseContext, MarkdownPlugin, }; struct TickerNode { symbol: String, } struct TickerPlugin; impl TickerPlugin { fn new() -> Self { Self } } impl MarkdownPlugin for TickerPlugin { fn is_block(&self) -> bool { true } fn name(&self) -> &str { "ticker" } fn parse( &self, node: &markdown_ast::Node, cx: &MarkdownParseContext<'_>, ) -> Option { let markdown_ast::Node::Paragraph(paragraph) = node else { return None; }; let [markdown_ast::Node::Text(text)] = paragraph.children.as_slice() else { return None; }; let symbol = text.value.strip_prefix('$')?; Some( MarkdownNode::new( "ticker", TickerNode { symbol: symbol.to_string(), }, ) .text(format!("${symbol}")) .markdown(cx.node_source(node).unwrap_or(text.value.as_str())), ) } fn render( &self, node: &MarkdownNode, _window: &mut Window, _cx: &mut App, ) -> impl IntoElement { let ticker = node.data::().expect("ticker node data"); gpui_kit::div().child(format!("${}", ticker.symbol)) } } ``` Then attach it to a Markdown `TextView`: ```rust markdown("$AAPL.US") .plugin(TickerPlugin::new()) ``` ## MarkdownNode `MarkdownNode` is the neutral data passed between `parse` and `render`. ```rust MarkdownNode::new("ticker", TickerNode { symbol }) .text("$AAPL.US") .markdown("$AAPL.US") ``` * `name` is the stable node name used to match the renderer. * `data` is typed parser output read with `node.data::()`. * `text` is the plain text representation used by selection and fallback rendering. * `markdown` is the Markdown representation used when the document is serialized back to Markdown. ## Block Plugins Custom Markdown rendering currently supports block plugins. Return `true` from `is_block()` for plugins that should be registered today: ```rust fn is_block(&self) -> bool { true } ``` Inline plugins are reserved for future `TextView` support. ## Code Block Actions You can render controls for Markdown code blocks: ```rust markdown(source) .code_block_actions(|code_block, _window, _cx| { gpui_kit::div().child(format!("Run {}", code_block.lang().unwrap_or_default())) }) ``` --- --- url: /zh-CN/docs/components/text-view.md description: 渲染 Markdown 与 HTML 文本,并支持自定义 Markdown 插件。 --- # TextView `TextView` 用于在 GPUI 中渲染格式化文本。它支持 Markdown、简单 HTML、文本选择、代码块操作,以及通过 Markdown 插件解析和渲染项目自定义语法。 标准实现现在位于 `gpui-base`;本模块保留兼容重导出和组件主题适配。仅使用 Base 时的设置、完整默认样式及可选语法高亮请参阅 [GPUI Base TextView](/zh-CN/base/text-view.md)。 `TextView::selectable(true)` 使用 `gpui-base` 提供的窗口级文本选择引擎。如果要让普通文本或自定义 renderer 参与同一选择,请参阅 [GPUI Base Text Selection](/base/text-selection.md)(英文)。 ## 导入 ```rust use gpui_kit::component::text::{markdown, TextView}; ``` ## 用法 ### Markdown 只需要渲染 Markdown 时,可以使用 `markdown` helper: ```rust use gpui_kit::component::text::markdown; markdown("# Hello\n\nThis is **Markdown**.") .selectable(true) .scrollable(true) ``` 如果需要稳定 id,也可以直接构造 `TextView`: ```rust use gpui_kit::component::text::TextView; TextView::markdown("preview", markdown_source) .selectable(true) ``` ### HTML ```rust TextView::html("html-preview", "Hello") ``` ## Markdown 插件 使用 `.plugin(...)` 支持自定义 Markdown 格式。插件同时拥有解析和渲染逻辑,调用方只需要把它挂到 `TextView` 上: ```rust markdown(source) .plugin(TickerPlugin::new()) ``` Markdown 插件实现 `MarkdownPlugin`: ```rust use gpui_kit::{App, IntoElement, ParentElement as _, Window}; use gpui_kit::component::text::{ markdown_ast, MarkdownNode, MarkdownParseContext, MarkdownPlugin, }; struct TickerNode { symbol: String, } struct TickerPlugin; impl TickerPlugin { fn new() -> Self { Self } } impl MarkdownPlugin for TickerPlugin { fn is_block(&self) -> bool { true } fn name(&self) -> &str { "ticker" } fn parse( &self, node: &markdown_ast::Node, cx: &MarkdownParseContext<'_>, ) -> Option { let markdown_ast::Node::Paragraph(paragraph) = node else { return None; }; let [markdown_ast::Node::Text(text)] = paragraph.children.as_slice() else { return None; }; let symbol = text.value.strip_prefix('$')?; Some( MarkdownNode::new( "ticker", TickerNode { symbol: symbol.to_string(), }, ) .text(format!("${symbol}")) .markdown(cx.node_source(node).unwrap_or(text.value.as_str())), ) } fn render( &self, node: &MarkdownNode, _window: &mut Window, _cx: &mut App, ) -> impl IntoElement { let ticker = node.data::().expect("ticker node data"); gpui_kit::div().child(format!("${}", ticker.symbol)) } } ``` 然后挂到 Markdown `TextView`: ```rust markdown("$AAPL.US") .plugin(TickerPlugin::new()) ``` ## MarkdownNode `MarkdownNode` 是 `parse` 和 `render` 之间传递的中性数据结构。 ```rust MarkdownNode::new("ticker", TickerNode { symbol }) .text("$AAPL.US") .markdown("$AAPL.US") ``` * `name` 是稳定的节点名称,用于匹配 renderer。 * `data` 是 parser 产生的类型化数据,通过 `node.data::()` 读取。 * `text` 是纯文本表示,用于选择和未注册 renderer 时的回退渲染。 * `markdown` 是 Markdown 表示,用于将文档重新序列化为 Markdown。 ## Block 插件 当前自定义 Markdown 渲染支持 block 插件。现在可注册的插件需要在 `is_block()` 中返回 `true`: ```rust fn is_block(&self) -> bool { true } ``` Inline 插件保留给未来的 `TextView` 支持。 ## 代码块操作 可以为 Markdown 代码块渲染操作控件: ```rust markdown(source) .code_block_actions(|code_block, _window, _cx| { gpui_kit::div().child(format!("Run {}", code_block.lang().unwrap_or_default())) }) ``` --- --- url: /shell/engine.md description: >- QuickJS behind one internal interface, why the seam exists, and the three measurements that tell script cost apart from frame cost. --- # The Engine Seam The scripting engine sits behind one internal interface. Everything above it — the element description arena, the materializer, the call scope, the style table, the theme, the capability model, the overlay host, hot-reload — is engine independent, and only the engine module knows what a script value is. ```bash cargo run -p gpui-shell -- examples/js_todolist ``` [QuickJS](https://github.com/quickjs-ng/quickjs), via [`rquickjs`](https://github.com/DelSkayn/rquickjs) — which vendors the `quickjs-ng` fork — is the engine that ships and the only one today. It sits behind a `quickjs` cargo feature all the same, and building with no engine is a **compile error** rather than a crate that exports nothing. ## Why there is a seam at all The engine choice is the one decision in this runtime that could not be settled on paper. Everything else in the design follows from GPUI's element model and can be argued about with a whiteboard. The engine cannot, because the whole approach stands or falls on a single number: **how long it takes script code to describe a realistic interface.** Every method call in a builder chain is one crossing of the language boundary, and if that per-call cost is too high, no amount of design fixes it. What the number is *compared against* changed once a script `render` stopped being a frame render. A description is built when application state moves and [replayed by every frame until it moves again](./state.md#when-render-runs), so the cost below is paid per user action rather than per repaint. That makes the boundary cost matter less than it did — but it does not make it free, and it is still the number that would decide a second engine. So the seam is a way of not having to be right in advance. The decision is made by measurement, and a second engine would be a new module rather than a rewrite. JavaScript is the default for one reason, and it is a product reason rather than a technical one: **application code reads better in it.** With presentation owned by the script, the vast majority of an application is composing elements, writing styles and handling events — and the readability of that code decides whether the runtime is worth using. Classes, arrow functions, template literals and destructuring land squarely on that kind of code. The secondary benefit is that JavaScript is the best-covered language in model training data, which matters for one of the [three settings](./index.md#where-it-fits). The cost is stated rather than glossed over. QuickJS **has no JIT** — it is a bytecode interpreter, so hot loops and per-call costs will not beat a JIT-compiled engine on principle. That is a real trade, and the benchmark below is where it would show up if it mattered. ## The measurement There are three costs here, and treating them as one was the original mistake. The benchmark describes a 40 × 5 grid of styled cells — 443 description nodes, roughly ten recorded operations each — and reports each cost separately: ```bash cargo test -p gpui-shell --release --lib benchmark -- --nocapture ``` | | What it measures | 443 nodes | Paid | | --- | --- | --- | --- | | **A** | script → Snapshot | **1.4 ms** | once per application change | | **B** | Snapshot → GPUI elements | **0.7 ms** | every frame | | **C** | a full cached repaint | **1.8 ms**, **no JavaScript at all** | every frame | Run it in release or the figures mean nothing. Every absolute number on this page comes from a release build on a MacBook Pro (M3, 8 cores, 24 GB), and moves with the machine. **C is the one that is an assertion rather than a timing.** Fifty repaints of an unchanged View run no JavaScript at all. If a single one of them ever does, the runtime has regressed to charging script cost per frame, and the benchmark fails rather than merely getting slower. One size cannot show which of the three costs scale, so a fourth test walks the same panel up to 8,403 nodes. It sits behind `--ignored` because the largest size takes seconds: ```bash cargo test -p gpui-shell --release --lib benchmark -- --ignored --nocapture ``` Describing costs 1.1 ms at 443 nodes, then 5.1, 10.3 and 20.5 ms as the panel grows to 2,103, 4,203 and 8,403. A whole frame — B plus GPUI's layout and paint, which is what C measures — costs 1.3, 5.9, 12.0 and 27.0 ms. Both scale close to linearly with the node count. What does not scale is the JavaScript: no frame at any size runs a line of it. Three things that settles: * **4,203 nodes is where the Snapshot decides the outcome.** 12 ms a frame holds 60 FPS; rebuilding the description for every frame would cost 22 ms and drop them. Below that size both models have room to spare, which is worth knowing before reading too much into the ratio. * **The description cost did not vanish, it moved.** 20 ms for 8,403 nodes is paid when the user acts rather than sixty times a second, but it is still 20 ms — which is why the per-call cost remains the number a second engine would be judged on. * **Past a few thousand nodes the bill is not script at all.** 27 ms a frame at that size, with the VM untouched, is materialization, layout and paint. A View that large wants virtualizing; a faster engine would not move it. Read A against the design's own budget — 1.5 ms for one script `render` — and it clears it, but with less room than hoped: the budget was derived from roughly 150 ns per recorded operation across 800 nodes, and the measurement reports about 320 ns across 443. A panel three times this size would not fit in one pass. What changed is how often that matters. At 120 FPS the old model would have spent 168 ms of every second describing an interface nobody had changed; the same panel now costs 1.4 ms when the user actually changes something, and 0.7 ms to repaint. The levers the design names for genuinely enormous panels — driving the per-call cost down, memoizing unchanged subtrees, virtualizing long lists — are still [not implemented](./elements.md#not-there-yet), and are now optimizations rather than prerequisites. Two implementation choices came out of the same measurement and are visible in the runtime today: * **Elements are plain objects sharing one prototype**, with the style methods installed on that prototype by a JavaScript prelude that loops over the name list. Not one class per element, not a fresh closure per property access, and not 3,000 Rust closures. * **The diagnostic `Proxy` prototype is not the default.** Wrapping the prototype in a `Proxy` so a mistyped method can be named costs about 30% of the whole description pass, so the runtime keeps a plain prototype and re-runs a failed render once against the diagnostic one purely to produce the message. See [Styling](./styling.md#unknown-methods). ### A live market-data workload The synthetic benchmark isolates costs; a Longbridge market terminal exercises them together. The following sample used a release build in the active window on a 3,840 × 2,160 display running at 144 Hz. Its watchlist received live quote updates while the selected instrument's details and five-day price chart were visible. The target was 120 FPS, which gives each frame **8.33 ms**. Opt-in runtime counters sampled one-second intervals and separated script description work from native materialization: | Measurement | Observed range | | --- | --- | | Full JavaScript `render` plus Spec recording | **12.0–13.5 ms** per dirty render | | Snapshot materialization | **0.93–1.08 ms** per materialization | | Script renders caused by quote updates | **8–20 per second** | | Materializations while the window was active | **59–78 per second** | One active-window FPS HUD sample reported **69 FPS**, **10.9 ms** frame time and **18.3%** dropped frames. That HUD measurement includes GPUI layout and paint and therefore is not directly interchangeable with either runtime counter, but it confirms the end-to-end workload was missing the 8.33 ms target. The useful conclusion is narrower than “JavaScript is slow.” A clean Snapshot can be materialized in about 1 ms, comfortably inside the frame budget. A quote-driven dirty update, however, spends roughly 12–13.5 ms before that materialization is complete because the application rebuilds and records its full description. Repeatedly invalidating the root script View therefore dominates this workload; optimizing only the native materializer would not recover 120 FPS. These figures deliberately exclude debug builds and samples taken after the window lost active status. Both change scheduling and frame presentation enough to make their FPS readings unsuitable for an architectural comparison. They are also a workload measurement, not a replacement for the reproducible crate benchmark above: quote frequency, visible content, hardware and display timing all affect the absolute result. ## Threads and memory The VM and GPUI's `App` share one thread — the main one — inside one process. `ShellRuntime` is an `Rc` with `RefCell` interiors, so it is neither `Send` nor `Sync`. There is no worker and no second VM. Background work never touches the VM. Timers (`cx.sleep`, `cx.timer`) count down there, and filesystem, process, fetch, TCP and WebSocket operations hand off their blocking work there. Results settle on the foreground executor, so JavaScript continuations still run on the main thread in a `Task` scope. GPUI also does its own work on its own threads once the elements exist. Three consequences matter when profiling: * **A builder call is a function call.** It crosses the FFI boundary and nothing else — no serialization, no IPC round trip, no copy beyond the conversion of the argument itself. The benchmark reports that cost per recorded operation, and across the four panel sizes it lands at **240–340 ns**. * **Script work still shares the UI thread.** Filesystem, process, fetch, TCP and WebSocket operations hand blocking work to background workers and settle on the foreground executor, but JavaScript computation and HostModule calls run beside GPUI and must stay bounded. * **A runaway script cannot be preempted from another thread.** What cuts it off is the interpreter's own interrupt — 50 ms inside `render`, 500 ms inside an event handler — and a `catch` block cannot swallow it. Memory splits four ways, each with a different owner and a different moment of release: | What | Where it lives | Released when | | --- | --- | --- | | Objects, closures, module scope | The QuickJS heap, capped at 256 MiB | Its GC runs, or the runtime drops | | The element description arena | Rust; moved into the Snapshot it produced | That Snapshot drops | | Registered callbacks | A Rust arena keyed by Snapshot generation | That Snapshot drops and retires its generation | | GPUI elements | GPUI's own frame arena | The draw that built them ends | A View holds **two** Snapshots rather than one: the live description, and the one it replaced. The previous is kept a generation longer because a frame already in flight may still be reading it, and releasing it early would retire callbacks that frame still needs. Nothing that crosses the boundary is an object. An element handle is an integer index into the arena, retained host state — an `InputState`'s rope, cursor and selection — lives in a GPUI entity the script addresses through a handle, and every argument and result is plain data. ## What linking it costs Two numbers a host has to know before it takes the dependency: how much bigger the binary gets, and how much more memory it holds. Measured on the two smallest real programs in this repository, so the figures are the cost of this crate rather than the cost of whatever else an application happens to contain: | | `hello_world` | `gpui-shell` running `js_todolist` | Added | | --- | --- | --- | --- | | Binary, stripped | 12.6 MiB | 26.1 MiB | **+13.5 MiB** | | Binary, unstripped | 16.5 MiB | 33.8 MiB | +17.3 MiB | | Resident memory | 67 MiB | 81 MiB | **+14 MiB** | `hello_world` is 41 lines of Rust over `gpui` and `gpui-component` — a window and a counter. The `gpui-shell` CLI is the smallest host that can run a script application; here it is running `examples/js_todolist`, 519 lines of JavaScript across four modules, with a live QuickJS runtime behind it. Memory is the median of four runs, discarding a first run that reads high while caches are cold; the binaries are `--release` with the workspace's default profile, stripped with `strip(1)`. **The +13.5 MiB is a constant, and that is the most useful thing here.** The same pair measured on the component gallery — a program five times the size — adds the same 13.5 MiB stripped, where it is +19.8% rather than +107%. Two independent measurements agreeing to three significant figures is what makes this a fact about `gpui-shell` rather than a reading of one application. The memory rows look like they disagree and do not. On the gallery the difference is *within measurement noise*: that build reads 194–208 MiB across runs, and 14 MiB is simply below its own spread. The minimal program can resolve it because 67 MiB has less to hide it in. ### Where the binary goes Not mostly QuickJS. The interpreter is one to two megabytes; the rest is the Standard Runtime it arrives with. `fetch`, `websocket` and `crypto` bring `hyper`, `rustls`, `ring`, `h2`, a `webpki` root store and the compression crates, and `gpui-component` alone brings none of them — `hello_world` links no HTTP, no TLS and no `tokio`. The whole stack enters through this crate. That also explains why an older measurement of this table read +4.7 MiB: it predates the Standard Runtime. `fs`, `net`, `crypto`, `fetch`, `websocket` and `zlib` all arrived after it. There is no configuration that takes the element surface without them. `quickjs` is the only engine feature and it is `default`; building with `--no-default-features` is a `compile_error!`, and the Standard Runtime is inside that same feature. Splitting the two would be new work rather than exposing a switch that already exists. Two savings that look available and are not, both measured rather than reasoned about: * **Dropping the five upstream crates Shell does not register** — `llrt_fetch`, `llrt_fs`, `llrt_net`, `llrt_os` and `llrt_console`, which were dependencies only for a compile-time assertion — changes the binary by **zero bytes**. Their heavy features resolve to crates other dependencies already pull. They are gone anyway, because 14 crates that cost nothing in bytes still cost compile time and supply-chain surface, and depending on an upstream `fetch` that Shell deliberately does not use reads as though it does. * **Narrowing `reqwest` to what `fetch.rs` actually uses** — dropping `charset`, `multipart`, `socks`, `stream` and `macos-system-configuration`, none of which that file can reach — saves **0.1 MiB**. So the 13.5 MiB is not slack. It is `hyper`, `rustls`, `ring` and the interpreter, and a host that wants any of `fetch`, `websocket` or `crypto` links all of it. ### Per runtime The figures above are for one runtime. A host that mounts several — a plugin host with one per plugin — pays the engine's construction each time: a QuickJS runtime and context, the module registry, the globals, the host installers, and a 43 KB prelude that is parsed on every construction. The 256 MiB heap cap from [Capabilities](./capabilities.md#the-sandbox) is a ceiling, not a reservation; nothing is committed until a script allocates it. ## What is on each side The proportion is itself the argument for the seam: above it is the actual design, below it is "what does a script value look like". | Above the seam — engine independent | Below the seam — what an engine implements | | --- | --- | | The render Snapshot: what one script `render` produces and what frames replay | Converting an engine value to the runtime's neutral value type | | The element description arena, single-use checking, and the debug tree | The module system's shape — ES modules and a resolver, versus `require` and a path list | | Materialization: descriptions into real GPUI elements, pure Rust | Method dispatch — functions on a shared prototype, versus an `__index` metamethod | | The call scope: phases, generations, and the crate's only `unsafe` | The callback handle type | | The style table, parametric styles and spelling suggestions | Converting the neutral error type into the language's own exception | | The default token palette and colour token resolution | How a View is defined — `class extends View`, versus a metatable | | The capability model and path resolution | The language-specific part of the sandbox | | Length and colour coercion | | | The neutral error type, the callback arena, the error overlay | | | `ScriptView`, `ShellRoot`, hot-reload | | None of the modules on the left names a VM anywhere in its source. That is what makes the seam real: it is not a trait, it is the fact that the rest of the crate reaches the engine through about a dozen entry points and nothing else. A trait would actually be worse here. The two handle types — a View class and a View instance — carry lifetimes of their own on the QuickJS side, and forcing them through a trait would move that complexity into the type system without removing any of it. The contract's load-bearing rule is about *when*, not what: **the engine's `build_snapshot` is the only entry into script `render`, and nothing calls it per frame.** An engine that rendered opportunistically — on a repaint, on a hover, on a timer — would put script cost back on the frame budget, which is the coupling the seam exists to prevent. Benchmark C is what would catch it. ## Portability If a second engine is ever added, **scripts will not be portable between them.** They would be different languages: a View is `class Counter extends View` in JavaScript and would be something else anywhere else. What has to be the same is everything around them — the binding surface, the render protocol, the phase rules, the capability model, the error messages. The requirement the design imposes is behavioural: the same use case must produce the **same description tree** under either engine, and the same application activity must trigger the **same number of script `render` calls**. That is what would keep the seam from rotting into two divergent runtimes. ## Known gap: async is not fully behind the seam The seam's contract does not yet cover asynchronous work. QuickJS requires the host to drain its job queue itself — nothing after an `await` runs until somebody asks — and that is not a shape every engine shares. So the scheduler cannot sit entirely above the seam. It needs two operations from an engine: turning a host task into something the script can await, and running the pending jobs. Promise jobs are drained at host-call boundaries, and a render that merely notices pending jobs queues a foreground drain instead of executing arbitrary continuations on the paint path. That preserves the central invariant: an async continuation may invalidate a View, but a frame never re-enters JavaScript just because it is a frame. Until both are addressed, the scheduler is QuickJS-specific. The rule it will be held to is the one that applies to any new capability: it goes above the seam unless it genuinely cannot be expressed there. ## Why not WebAssembly, or a separate process Two questions the seam invites. `gpui-shell` runs the VM **in the host process, on the main thread**, alongside GPUI's `App`. That is what makes the 240–340 ns per recorded call possible at all. A separate process would put an IPC round trip on every recorded builder call, and there is no budget for one even at the reduced frequency Snapshots buy. For the same reason there is no `Worker`: the VM and the `App` are both main-thread only. The wasm target is the other reason the seam is drawn where it is. QuickJS is plain C and compiles to WebAssembly; not every candidate engine does, and some generate machine code, which is a constraint on platforms that forbid writable-executable memory. Neither fact decides today's engine, but they are why "the engine is a parameter, not a part of the architecture" is written down at all. --- --- url: /docs/theme.md --- # Theme All components support theming through the built-in Theme system, the [ActiveTheme] trait provides access to the current theme colors: ```rs use gpui_kit::component::{ActiveTheme as _}; // Access theme colors in your components cx.theme().primary cx.theme().background cx.theme().foreground ``` So if you want use the colors from the current theme, you should keep your component or view have [App] context. ## Gradient Backgrounds Theme color values remain backward compatible with the existing string format: ```json { "colors": { "button.primary.background": "#4F46E5" } } ``` Background tokens that opt in to gradient rendering can also use CSS-style two-stop linear gradients: ```json { "colors": { "button.primary.background": "linear-gradient(135deg, #4F46E5, #06B6D4)", "button.primary.hover.background": "linear-gradient(to right, red-500 25%, blue-600 75%)" } } ``` Top-level theme fields, such as `cx.theme().button_primary`, remain solid `Hsla` values for compatibility. Code that needs the full resolved token can use `cx.theme().tokens.button_primary`; its `.color` field is the solid representative color, and its `.background` field contains the configured `Background`, including gradients. ## Theme Registry There have more than 20 built-in themes available in [themes](https://github.com/longbridge/gpui-kit/tree/main/themes) folder. https://github.com/longbridge/gpui-kit/tree/main/themes And we have a [ThemeRegistry] to help us to load themes. Use the `name` of an entry in the `themes` array, such as `Ayu Light`, when looking up a theme from the registry. ```rs use std::path::PathBuf; use gpui_kit::{App, SharedString}; use gpui_kit::component::{Theme, ThemeRegistry}; pub fn init(cx: &mut App) { let theme_name = SharedString::from("Ayu Light"); // Load and watch themes from ./themes directory if let Err(err) = ThemeRegistry::watch_dir(PathBuf::from("./themes"), cx, move |cx| { if let Some(theme) = ThemeRegistry::global(cx) .themes() .get(&theme_name) .cloned() { Theme::global_mut(cx).apply_config(&theme); } }) { tracing::error!("Failed to watch themes directory: {}", err); } } ``` [ActiveTheme]: https://docs.rs/gpui-component/latest/gpui_component/theme/trait.ActiveTheme.html [ThemeRegistry]: https://docs.rs/gpui-component/latest/gpui_component/theme/struct.ThemeRegistry.html [App]: https://docs.rs/gpui/latest/gpui/struct.App.html --- --- url: /zh-CN/docs/theme.md --- # Theme 所有组件都支持内置主题系统。[ActiveTheme] trait 用于访问当前主题中的颜色值: ```rs use gpui_kit::component::{ActiveTheme as _}; // Access theme colors in your components cx.theme().primary cx.theme().background cx.theme().foreground ``` 因此,如果你希望组件使用当前主题的颜色,组件或视图就需要运行在带有 [App] 上下文的环境中。 ## 渐变背景 主题颜色值继续兼容既有的字符串格式: ```json { "colors": { "button.primary.background": "#4F46E5" } } ``` 支持渐变渲染的背景 token 也可以使用 CSS 风格的两段线性渐变: ```json { "colors": { "button.primary.background": "linear-gradient(135deg, #4F46E5, #06B6D4)", "button.primary.hover.background": "linear-gradient(to right, red-500 25%, blue-600 75%)" } } ``` `cx.theme().button_primary` 等顶层字段仍然是纯色 `Hsla`,保持兼容。需要完整 resolved token 时使用 `cx.theme().tokens.button_primary`;其中 `.color` 是纯色代表色,`.background` 是实际配置的 `Background`,包含渐变。 ## Theme Registry 仓库在 [themes](https://github.com/longbridge/gpui-kit/tree/main/themes) 目录下内置了 20+ 主题。 你可以通过 [ThemeRegistry] 来加载和监听这些主题文件: 从 registry 查找主题时使用 `themes` 数组中条目的 `name`,例如 `Ayu Light`。 ```rs use std::path::PathBuf; use gpui_kit::{App, SharedString}; use gpui_kit::component::{Theme, ThemeRegistry}; pub fn init(cx: &mut App) { let theme_name = SharedString::from("Ayu Light"); // Load and watch themes from ./themes directory if let Err(err) = ThemeRegistry::watch_dir(PathBuf::from("./themes"), cx, move |cx| { if let Some(theme) = ThemeRegistry::global(cx) .themes() .get(&theme_name) .cloned() { Theme::global_mut(cx).apply_config(&theme); } }) { tracing::error!("Failed to watch themes directory: {}", err); } } ``` [ActiveTheme]: https://docs.rs/gpui-component/latest/gpui_component/theme/trait.ActiveTheme.html [ThemeRegistry]: https://docs.rs/gpui-component/latest/gpui_component/theme/struct.ThemeRegistry.html [App]: https://docs.rs/gpui/latest/gpui/struct.App.html --- --- url: /docs/components/title-bar.md description: >- A custom window title bar component with window controls and custom content support. --- # TitleBar TitleBar provides a customizable window title bar that can replace the default OS title bar. It includes platform-specific window controls (minimize, maximize, close) and supports custom content and styling. The component automatically adapts to different operating systems (macOS, Windows, Linux) with appropriate behaviors and visual styles. ## Import ```rust use gpui_kit::component::TitleBar; ``` ## Usage ### Basic Title Bar ```rust TitleBar::new() .child(div().child("My Application")) ``` ### Title Bar with Custom Content ```rust TitleBar::new() .child( div() .flex() .items_center() .gap_3() .child("App Name") .child(Badge::new().count(5)) ) .child( div() .flex() .items_center() .gap_2() .child(Button::new("settings").icon(IconName::Settings)) .child(Button::new("profile").icon(IconName::User)) ) ``` ### Title Bar with Menu Bar ```rust TitleBar::new() .child( div() .flex() .items_center() .child(AppMenuBar::new(window, cx)) ) .child( div() .flex() .items_center() .justify_end() .gap_2() .child(Button::new("github").icon(IconName::GitHub)) .child(Button::new("notifications").icon(IconName::Bell)) ) ``` ### Title Bar with Window Controls (Linux only) ```rust TitleBar::new() .on_close_window(|_, window, cx| { // Custom close behavior window.push_notification("Saving before close...", cx); // Perform cleanup window.remove_window(); }) .child(div().child("Custom Close Behavior")) ``` ### Styled Title Bar ```rust TitleBar::new() .bg(cx.theme().primary) .border_color(cx.theme().primary_border) .child( div() .text_color(cx.theme().primary_foreground) .child("Styled Title Bar") ) ``` ### Title Bar Options for Window Use `TitleBar::window_options()` as the base of the window options, it sets up everything the title bar needs, including letting the title bar own dragging and double clicking instead of the system. ```rust use gpui_kit::WindowOptions; WindowOptions { window_bounds: Some(window_bounds), ..TitleBar::window_options() } ``` If you build the \[`WindowOptions`] yourself, set both fields: ```rust use gpui_kit::WindowOptions; WindowOptions { titlebar: Some(TitleBar::title_bar_options()), // Required on macOS, otherwise the system also handles title bar double // clicks and delays title bar clicks to disambiguate double clicks. app_owns_titlebar_drag: true, ..Default::default() } ``` ## Platform Differences ### macOS * Uses native traffic light buttons (minimize, maximize, close) * Traffic light position is automatically set to `(9px, 9px)` * Double-click behavior calls `window.titlebar_double_click()` * Left padding accounts for traffic light buttons (80px) * Appears transparent by default ### Windows * Custom window control buttons with system integration * Uses `WindowControlArea` for proper window management * Control buttons have hover and active states * Fixed button width of 34px each * Left padding is 12px ### Linux * Custom window control buttons with manual event handling * Supports custom close window callback via `on_close_window()` * Double-click to maximize/restore window * Right-click shows window context menu * Window dragging supported in title bar area ## API Reference ### TitleBar | Method | Description | | --------------------- | ---------------------------------------- | | `new()` | Create a new title bar | | `child(element)` | Add child element to the title bar | | `on_close_window(fn)` | Custom close window handler (Linux only) | | `title_bar_options()` | Get default titlebar options for window | | `window_options()` | Get default window options for the title bar | ### Window Configuration | Property | Description | | ------------------------ | -------------------------------------------------------------- | | `appears_transparent` | Make title bar transparent (default: true) | | `traffic_light_position` | Position of macOS traffic lights | | `title` | Window title (optional when using custom title bar) | | `app_owns_titlebar_drag` | Let the title bar own dragging and double clicking (macOS only) | ### Title Bar Element (Internal) The `TitleBarElement` provides window dragging functionality on Linux platforms. ### Constants | Constant | Value | Description | | ------------------------ | ------------------------------- | ------------------------- | | `TITLE_BAR_HEIGHT` | `34px` | Standard title bar height | | `TITLE_BAR_LEFT_PADDING` | `80px` (macOS), `12px` (others) | Left padding for content | ## Examples ### Application Title Bar ```rust use gpui_kit::component::{TitleBar, button::Button, menu::AppMenuBar}; struct AppTitleBar { app_menu_bar: Entity, } impl Render for AppTitleBar { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { TitleBar::new() .child( div() .flex() .items_center() .child(self.app_menu_bar.clone()) ) .child( div() .flex() .items_center() .justify_end() .gap_2() .child( Button::new("settings") .ghost() .icon(IconName::Settings) ) .child( Button::new("help") .ghost() .icon(IconName::HelpCircle) ) ) } } ``` ### Title Bar with Breadcrumbs ```rust TitleBar::new() .child( div() .flex() .items_center() .gap_2() .child("Home") .child(IconName::ChevronRight) .child("Documents") .child(IconName::ChevronRight) .child("Project") ) .child( div() .flex() .items_center() .gap_1() .child(Button::new("search").icon(IconName::Search).ghost()) .child(Button::new("more").icon(IconName::MoreHorizontal).ghost()) ) ``` ### Custom Themed Title Bar ```rust TitleBar::new() .h(px(40.)) // Custom height .bg(cx.theme().accent) .border_b_2() .border_color(cx.theme().accent_border) .child( div() .flex() .items_center() .text_color(cx.theme().accent_foreground) .font_weight_semibold() .child("Custom Theme App") ) ``` ### Title Bar with Status ```rust TitleBar::new() .child( div() .flex() .items_center() .gap_3() .child("My Editor") .child( div() .text_xs() .text_color(cx.theme().muted_foreground) .child("● Unsaved changes") ) ) .child( div() .flex() .items_center() .gap_2() .child( div() .text_xs() .text_color(cx.theme().muted_foreground) .child("Line 42, Col 12") ) .child( Button::new("sync") .small() .ghost() .icon(IconName::RotateCcw) .tooltip("Sync changes") ) ) ``` ### Minimal Title Bar ```rust TitleBar::new() .child( div() .text_center() .flex_1() .child("Document.txt") ) ``` ### Title Bar with Search ```rust TitleBar::new() .child( div() .flex() .items_center() .gap_3() .child("File Explorer") .child( Input::new("search") .placeholder("Search files...") .w(px(200.)) .small() ) ) ``` ## Notes * The title bar automatically handles platform-specific styling and behavior * Window controls are only rendered on Windows and Linux platforms * The component integrates with GPUI's window management system * Custom styling should consider platform conventions * Window dragging is handled automatically in appropriate areas --- --- url: /zh-CN/docs/components/title-bar.md description: 支持窗口控制和自定义内容的自定义标题栏组件。 --- # TitleBar TitleBar 用于替换系统默认标题栏,提供可定制的窗口标题区域。它内置平台相关的窗口控制按钮,并支持插入菜单栏、状态信息和自定义操作区。组件会根据 macOS、Windows 和 Linux 自动调整行为和视觉样式。 ## 导入 ```rust use gpui_kit::component::TitleBar; ``` ## 用法 ### 基础标题栏 ```rust TitleBar::new() .child(div().child("My Application")) ``` ### 带自定义内容的标题栏 ```rust TitleBar::new() .child( div() .flex() .items_center() .gap_3() .child("App Name") .child(Badge::new().count(5)) ) .child( div() .flex() .items_center() .gap_2() .child(Button::new("settings").icon(IconName::Settings)) .child(Button::new("profile").icon(IconName::User)) ) ``` ### 带菜单栏 ```rust TitleBar::new() .child( div() .flex() .items_center() .child(AppMenuBar::new(window, cx)) ) .child( div() .flex() .items_center() .justify_end() .gap_2() .child(Button::new("github").icon(IconName::GitHub)) .child(Button::new("notifications").icon(IconName::Bell)) ) ``` ### Linux 自定义关闭行为 ```rust TitleBar::new() .on_close_window(|_, window, cx| { window.push_notification("Saving before close...", cx); window.remove_window(); }) .child(div().child("Custom Close Behavior")) ``` ### 自定义样式 ```rust TitleBar::new() .bg(cx.theme().primary) .border_color(cx.theme().primary_border) .child( div() .text_color(cx.theme().primary_foreground) .child("Styled Title Bar") ) ``` ### 窗口配置 推荐以 `TitleBar::window_options()` 作为窗口配置的基础,它会配置好标题栏所需的 全部选项,其中包括让标题栏(而不是系统)来处理拖动和双击。 ```rust use gpui_kit::WindowOptions; WindowOptions { window_bounds: Some(window_bounds), ..TitleBar::window_options() } ``` 如果自行构造 \[`WindowOptions`],需要同时设置这两个字段: ```rust use gpui_kit::WindowOptions; WindowOptions { titlebar: Some(TitleBar::title_bar_options()), // macOS 上必须设置,否则系统也会处理标题栏双击, // 并且会为了判定双击而延迟投递标题栏点击。 app_owns_titlebar_drag: true, ..Default::default() } ``` ## 平台差异 ### macOS * 使用原生红黄绿窗口按钮 * traffic light 默认位置为 `(9px, 9px)` * 双击标题栏会调用 `window.titlebar_double_click()` * 左侧默认预留 `80px` * 默认表现为透明标题栏 ### Windows * 使用自定义窗口控制按钮并接入系统窗口管理 * 通过 `WindowControlArea` 处理交互 * 支持 hover 和 active 状态 * 每个控制按钮宽度固定为 `34px` * 左侧默认内边距为 `12px` ### Linux * 使用手动事件处理的自定义窗口控制按钮 * 支持通过 `on_close_window()` 覆盖关闭逻辑 * 支持双击最大化 / 还原 * 支持右键弹出窗口菜单 * 支持在标题栏区域拖动窗口 ## API 参考 ### TitleBar | 方法 | 说明 | | --- | --- | | `new()` | 创建标题栏 | | `child(element)` | 向标题栏中添加子元素 | | `on_close_window(fn)` | 自定义关闭行为,仅 Linux 有效 | | `title_bar_options()` | 获取窗口可用的默认标题栏配置 | | `window_options()` | 获取标题栏配套的默认窗口配置 | ### 窗口配置项 | 属性 | 说明 | | --- | --- | | `appears_transparent` | 标题栏透明(默认 true) | | `traffic_light_position` | macOS 红黄绿按钮位置 | | `title` | 窗口标题(使用自定义标题栏时可选) | | `app_owns_titlebar_drag` | 由标题栏自行处理拖动与双击(仅 macOS) | ### 常量 | 常量 | 值 | 说明 | | --- | --- | --- | | `TITLE_BAR_HEIGHT` | `34px` | 标准标题栏高度 | | `TITLE_BAR_LEFT_PADDING` | `80px`(macOS),`12px`(其他) | 内容区域左侧留白 | ## 说明 * 组件会自动处理平台相关的标题栏行为 * Windows 和 Linux 才会渲染自定义窗口控制按钮 * 拖拽窗口的逻辑已内置在合适区域中 * 自定义样式时应考虑各平台对标题栏的交互习惯 --- --- url: /base/primitives/toast.md description: 'A managed, animated stack of temporary status messages.' --- # Toast A managed, animated stack of temporary status messages. Like every `gpui-base` primitive, Toast supplies behavior and semantic structure without imposing a product visual language. Apply GPUI styles and compose the exported parts to match your design system. ## Example The [single native Cargo entrypoint](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/components.rs) selects this primitive from the [shared showcase implementation](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/mod.rs). The same showcase is compiled once for the WASM preview above. ```bash cargo run -p gpui-base --example components -- toast ``` ## Import ```rust use gpui_kit::base::{Toast, ToastManager, ToastOptions, ToastStack}; ``` ## Anatomy and API The example composes `Toast`, `ToastManager`, `ToastOptions`, `ToastStack`. GPUI's standard styling and event traits provide presentation; these base types provide the interaction structure. The authoritative module is [`components/toast.rs`](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/components/toast.rs). Native and browser previews compile this same file. ## State and events Push messages through toast state; transition status retains an item during entry and exit. Keep controlled state on the parent render type or in a GPUI entity. Update it in callbacks and call `cx.notify()`; do not recreate persistent entities during every render. ## Complete Rust example The complete implementation used by the runnable showcase is embedded directly from Rust source: The command above supplies application initialization, window creation, and shared `BaseShowcase` state. ## Accessibility Choose live-region priority carefully and avoid essential actions only in expiring content. ## Notes Use stable element IDs where accepted. Verify focus, hover, active, selected, disabled, reduced-motion, and high-contrast appearances in the consuming design system. --- --- url: /zh-CN/base/primitives/toast.md description: 受管理、带动画的临时状态消息栈。 --- # Toast 受管理、带动画的临时状态消息栈。 和所有 GPUI Base 原语一样,Toast 只提供行为和语义结构,不规定产品视觉语言。请使用 GPUI 样式并组合导出的部件,使其符合你的设计系统。 ## 示例 原生示例和页面上方的 WASM 预览共用同一份实现: ```bash cargo run -p gpui-base --example components -- toast ``` ## 导入 ```rust use gpui_kit::base::{Toast, ToastManager, ToastOptions, ToastStack}; ``` ## 结构与 API 示例组合上述公开类型。GPUI 的标准样式和事件 trait 负责表现,Base 类型负责交互结构。权威实现位于 [`components/toast.rs`](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/components/toast.rs),原生与浏览器预览编译的是同一文件。 ## 状态与事件 ToastManager 管理消息的加入、超时和移除;应用选择持续时间与操作。 受控状态应保存在父渲染类型或 GPUI entity 中;在回调中更新并调用 `cx.notify()`,不要在每次渲染时重建持久 entity。 ## 完整 Rust 示例 ## 可访问性 根据紧急程度使用合适的实时区域;重要内容不能只依赖自动消失的消息。 ## 注意事项 在支持的位置使用稳定元素 ID,并在消费端设计系统中验证焦点、悬停、按下、选中、禁用、减少动态效果和高对比度状态。 --- --- url: /base/primitives/toggle.md description: A controlled two-state pressable for persistent choices such as formatting. --- # Toggle A controlled two-state pressable for persistent choices such as formatting. Like every `gpui-base` primitive, Toggle supplies behavior and semantic structure without imposing a product visual language. Apply GPUI styles and compose the exported parts to match your design system. ## Example The [single native Cargo entrypoint](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/components.rs) selects this primitive from the [shared showcase implementation](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/mod.rs). The same showcase is compiled once for the WASM preview above. ```bash cargo run -p gpui-base --example components -- toggle ``` ## Import ```rust use gpui_kit::base::{Toggle}; ``` ## Anatomy and API The example composes `Toggle`. GPUI's standard styling and event traits provide presentation; these base types provide the interaction structure. The authoritative module is [`components/toggle.rs`](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/components/toggle.rs). Native and browser previews compile this same file. ## State and events Pass the controlled pressed value; `on_change` emits the requested next value. Keep controlled state on the parent render type or in a GPUI entity. Update it in callbacks and call `cx.notify()`; do not recreate persistent entities during every render. ## Complete Rust example The complete implementation used by the runnable showcase is embedded directly from Rust source: The command above supplies application initialization, window creation, and shared `BaseShowcase` state. ## Accessibility Expose pressed state and keep a stable accessible name. ## Notes Use stable element IDs where accepted. Verify focus, hover, active, selected, disabled, reduced-motion, and high-contrast appearances in the consuming design system. --- --- url: /zh-CN/base/primitives/toggle.md description: 用于格式等持久选择的受控双态按钮。 --- # Toggle 用于格式等持久选择的受控双态按钮。 和所有 GPUI Base 原语一样,Toggle 只提供行为和语义结构,不规定产品视觉语言。请使用 GPUI 样式并组合导出的部件,使其符合你的设计系统。 ## 示例 原生示例和页面上方的 WASM 预览共用同一份实现: ```bash cargo run -p gpui-base --example components -- toggle ``` ## 导入 ```rust use gpui_kit::base::{Toggle}; ``` ## 结构与 API 示例组合上述公开类型。GPUI 的标准样式和事件 trait 负责表现,Base 类型负责交互结构。权威实现位于 [`components/toggle.rs`](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/components/toggle.rs),原生与浏览器预览编译的是同一文件。 ## 状态与事件 父级保存按下状态,激活时切换。 受控状态应保存在父渲染类型或 GPUI entity 中;在回调中更新并调用 `cx.notify()`,不要在每次渲染时重建持久 entity。 ## 完整 Rust 示例 ## 可访问性 暴露按钮名称、按下和禁用状态,并保留键盘激活。 ## 注意事项 在支持的位置使用稳定元素 ID,并在消费端设计系统中验证焦点、悬停、按下、选中、禁用、减少动态效果和高对比度状态。 --- --- url: /docs/components/toggle.md description: A button-style toggle component for binary on/off or selected states. --- # Toggle A button-style toggle component that represents on/off or selected states. Unlike a traditional switch, toggles appear as buttons that can be pressed in or out. They're perfect for toolbar buttons, filter options, or any binary choice that benefits from a button-like appearance. ## Import ```rust use gpui_kit::component::button::{Toggle, ToggleGroup}; ``` ## Usage ### Basic Toggle ```rust Toggle::new("toggle1"). .label("Toggle me") .checked(false) .on_click(|checked, _, _| { println!("Toggle is now: {}", checked); }) ``` Here, we can use `on_click` to handle toggle state changes. The callback receives the **new checked state** as a `bool`. ### Icon Toggle ```rust use gpui_kit::component::IconName; Toggle::new("toggle2") .icon(IconName::Eye) .checked(true) .on_click(|checked, _, _| { println!("Visibility: {}", if *checked { "shown" } else { "hidden" }); }) ``` ### Controlled Toggle ```rust struct MyView { is_active: bool, } impl Render for MyView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { Toggle::new("active") .label("Active") .checked(self.is_active) .on_click(cx.listener(|view, checked, _, cx| { view.is_active = *checked; cx.notify(); })) } } ``` ### Toggle Variants ```rust // Ghost toggle (default) Toggle::new("ghost-toggle") .ghost() .label("Ghost") // Outline toggle Toggle::new("outline-toggle") .outline() .label("Outline") ``` ### Different Sizes ```rust // Extra small Toggle::new("xs-toggle") .icon(IconName::Star) .xsmall() // Small Toggle::new("small-toggle") .label("Small") .small() // Medium (default) Toggle::new("medium-toggle") .label("Medium") // Large Toggle::new("large-toggle") .label("Large") .large() ``` ### Disabled State ```rust // Disabled unchecked Toggle::new("disabled-toggle") .label("Disabled") .disabled(true) .checked(false) // Disabled checked Toggle::new("disabled-checked-toggle") .label("Selected (Disabled)") .disabled(true) .checked(true) ``` ## Toggle vs Switch | Feature | Toggle | Switch | | ---------------------- | ------------------------------------------- | ----------------------------------------- | | **Appearance** | Button-like, can be pressed in/out | Traditional switch with sliding indicator | | **Use Cases** | Toolbar buttons, filters, binary options | Settings, preferences, on/off states | | **Visual Style** | Rectangular button shape | Rounded switch track with thumb | | **State Indication** | Background color change, pressed appearance | Position of sliding thumb | | **Multiple Selection** | Supports groups with multiple selection | Individual switches only | **Use Toggle when you want:** * Button-like appearance for binary states * Grouping multiple related options * Toolbar or filter interfaces * Options that feel like "selections" rather than "settings" **Use Switch when you want:** * Traditional on/off control appearance * Settings or preferences interface * Clear visual indication of state with sliding animation * Individual boolean controls ## Integration with ToggleGroup Toggle buttons can be grouped together using `ToggleGroup` for related options: ### Basic Toggle Group ```rust ToggleGroup::new("filter-group") .child(Toggle::new(0).icon(IconName::Bell)) .child(Toggle::new(1).icon(IconName::Bot)) .child(Toggle::new(2).icon(IconName::Inbox)) .child(Toggle::new(3).label("Other")) .on_click(|checkeds, _, _| { println!("Selected toggles: {:?}", checkeds); }) ``` The `on_click` callback receives a `Vec` representing the **new checked state** of each toggle in the group. ### Toggle Group with Controlled State ```rust struct FilterView { notifications: bool, bots: bool, inbox: bool, other: bool, } impl Render for FilterView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { ToggleGroup::new("filters") .child(Toggle::new(0).icon(IconName::Bell).checked(self.notifications)) .child(Toggle::new(1).icon(IconName::Bot).checked(self.bots)) .child(Toggle::new(2).icon(IconName::Inbox).checked(self.inbox)) .child(Toggle::new(3).label("Other").checked(self.other)) .on_click(cx.listener(|view, checkeds, _, cx| { view.notifications = checkeds[0]; view.bots = checkeds[1]; view.inbox = checkeds[2]; view.other = checkeds[3]; cx.notify(); })) } } ``` ### Toggle Group Variants and Sizes ```rust // Outline variant, small size ToggleGroup::new("compact-filters") .outline() .small() .child(Toggle::new(0).icon(IconName::Filter)) .child(Toggle::new(1).icon(IconName::Sort)) .child(Toggle::new(2).icon(IconName::Search)) // Ghost variant (default), extra small ToggleGroup::new("mini-toolbar") .xsmall() .child(Toggle::new(0).icon(IconName::Bold)) .child(Toggle::new(1).icon(IconName::Italic)) .child(Toggle::new(2).icon(IconName::Underline)) ``` ### Segmented Toggle Group Use `segmented()` when a group should render as a connected segmented control. The group still uses the same multi-toggle behavior: `on_click` receives a `Vec` with the new checked state for each item. ```rust ToggleGroup::new("formatting") .segmented() .outline() .child(Toggle::new(0).label("Bold").checked(self.bold)) .child(Toggle::new(1).label("Italic").checked(self.italic)) .child(Toggle::new(2).label("Code").checked(self.code)) .on_click(cx.listener(|view, states, _, cx| { view.bold = states[0]; view.italic = states[1]; view.code = states[2]; cx.notify(); })) ``` By default, segmented groups use a zero gap so adjacent items share one outline. Pass a non-zero gap when you want the segmented sizing and variants but separated items: ```rust use gpui_kit::px; ToggleGroup::new("quick-actions") .segmented() .outline() .gap(px(8.)) .small() .child(Toggle::new(0).label("Star")) .child(Toggle::new(1).label("Watch")) .child(Toggle::new(2).label("Pin")) ``` If you need mutually exclusive behavior, keep that state in your view model and set only one child to `checked(true)` until a dedicated single-selection API is available. ## Event Handling ### Individual Toggle Events ```rust Toggle::new("subscribe-toggle") .label("Subscribe") .on_click(|checked, window, cx| { if *checked { // Handle subscription logic println!("Subscribed!"); } else { // Handle unsubscription logic println!("Unsubscribed!"); } }) ``` ## Examples ### Toolbar with Toggle Buttons ```rust struct EditorToolbar { bold: bool, italic: bool, underline: bool, strikethrough: bool, } h_flex() .gap_1() .p_2() .bg(cx.theme().background) .border_1() .border_color(cx.theme().border) .child( ToggleGroup::new("formatting") .small() .child(Toggle::new(0).icon(IconName::Bold).checked(self.bold)) .child(Toggle::new(1).icon(IconName::Italic).checked(self.italic)) .child(Toggle::new(2).icon(IconName::Underline).checked(self.underline)) .child(Toggle::new(3).icon(IconName::Strikethrough).checked(self.strikethrough)) .on_click(cx.listener(|view, states, _, cx| { view.bold = states[0]; view.italic = states[1]; view.underline = states[2]; view.strikethrough = states[3]; cx.notify(); })) ) ``` ### Filter Interface ```rust struct FilterPanel { show_completed: bool, show_pending: bool, show_cancelled: bool, show_urgent: bool, } v_flex() .gap_3() .p_4() .child(Label::new("Filter by status")) .child( ToggleGroup::new("status-filters") .outline() .child(Toggle::new(0).label("Completed").checked(self.show_completed)) .child(Toggle::new(1).label("Pending").checked(self.show_pending)) .child(Toggle::new(2).label("Cancelled").checked(self.show_cancelled)) .on_click(cx.listener(|view, states, _, cx| { view.show_completed = states[0]; view.show_pending = states[1]; view.show_cancelled = states[2]; cx.notify(); })) ) .child( Toggle::new("urgent-filter") .label("Show urgent only") .checked(self.show_urgent) .on_click(cx.listener(|view, checked, _, cx| { view.show_urgent = *checked; cx.notify(); })) ) ``` ### Settings with Individual Toggles ```rust struct NotificationSettings { email_notifications: bool, push_notifications: bool, marketing_emails: bool, } v_flex() .gap_4() .child( h_flex() .items_center() .justify_between() .child( v_flex() .child(Label::new("Email notifications")) .child( Label::new("Receive notifications via email") .text_color(cx.theme().muted_foreground) .text_sm() ) ) .child( Toggle::new("email-notifications") .icon(IconName::Mail) .checked(self.email_notifications) .on_click(cx.listener(|view, checked, _, cx| { view.email_notifications = *checked; cx.notify(); })) ) ) .child( h_flex() .items_center() .justify_between() .child(Label::new("Push notifications")) .child( Toggle::new("push-notifications") .icon(IconName::Bell) .checked(self.push_notifications) .on_click(cx.listener(|view, checked, _, cx| { view.push_notifications = *checked; cx.notify(); })) ) ) ``` ### Multi-select Options ```rust struct SelectionView { selected_categories: Vec, } impl SelectionView { fn categories() -> Vec<&'static str> { vec!["Technology", "Design", "Business", "Science", "Art"] } } v_flex() .gap_3() .child(Label::new("Select categories of interest")) .child( ToggleGroup::new("categories") .children( Self::categories() .into_iter() .enumerate() .map(|(i, category)| { Toggle::new(i) .label(category) .checked(self.selected_categories.get(i).copied().unwrap_or(false)) }) ) .on_click(cx.listener(|view, states, _, cx| { view.selected_categories = states.clone(); cx.notify(); })) ) ``` ## Best Practices 1. **Use meaningful labels**: Choose clear, descriptive text for toggle labels 2. **Group related options**: Use ToggleGroup for logically related binary choices 3. **Provide visual feedback**: The checked state should be clearly distinguishable 4. **Consider context**: Use toggles for options that feel like "selections" rather than "settings" 5. **Maintain state consistency**: Ensure toggle state reflects the actual application state 6. **Accessible labels**: Provide tooltips or ARIA labels for icon-only toggles --- --- url: /zh-CN/docs/components/toggle.md description: 以按钮形态表示开关或选中状态的切换组件。 --- # Toggle Toggle 是一种按钮式的二元切换组件,用于表示选中 / 未选中、开启 / 关闭等状态。与传统 Switch 不同,Toggle 更像可按下或弹起的按钮,适合工具栏、筛选器和多选项场景。 ## 导入 ```rust use gpui_kit::component::button::{Toggle, ToggleGroup}; ``` ## 用法 ### 基础 Toggle ```rust Toggle::new("toggle1") .label("Toggle me") .checked(false) .on_click(|checked, _, _| { println!("Toggle is now: {}", checked); }) ``` `on_click` 回调接收的是切换后的新状态。 ### 图标 Toggle ```rust use gpui_kit::component::IconName; Toggle::new("toggle2") .icon(IconName::Eye) .checked(true) .on_click(|checked, _, _| { println!("Visibility: {}", if *checked { "shown" } else { "hidden" }); }) ``` ### 受控 Toggle ```rust struct MyView { is_active: bool, } impl Render for MyView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { Toggle::new("active") .label("Active") .checked(self.is_active) .on_click(cx.listener(|view, checked, _, cx| { view.is_active = *checked; cx.notify(); })) } } ``` ### 样式变体 ```rust Toggle::new("ghost-toggle") .ghost() .label("Ghost") Toggle::new("outline-toggle") .outline() .label("Outline") ``` ### 不同尺寸 ```rust Toggle::new("xs-toggle") .icon(IconName::Star) .xsmall() Toggle::new("small-toggle") .label("Small") .small() Toggle::new("medium-toggle") .label("Medium") Toggle::new("large-toggle") .label("Large") .large() ``` ### 禁用状态 ```rust Toggle::new("disabled-toggle") .label("Disabled") .disabled(true) .checked(false) Toggle::new("disabled-checked-toggle") .label("Selected (Disabled)") .disabled(true) .checked(true) ``` ## Toggle 与 Switch 的区别 | 特性 | Toggle | Switch | | --- | --- | --- | | 外观 | 按钮式,可按下 / 弹起 | 传统滑块式开关 | | 场景 | 工具栏、筛选器、二元选择 | 设置项、偏好项、开关状态 | | 状态表达 | 背景和按压感变化 | 滑块位置变化 | | 分组能力 | 支持 `ToggleGroup` | 主要单独使用 | ## 与 ToggleGroup 配合使用 ### 基础分组 ```rust ToggleGroup::new("filter-group") .child(Toggle::new(0).icon(IconName::Bell)) .child(Toggle::new(1).icon(IconName::Bot)) .child(Toggle::new(2).icon(IconName::Inbox)) .child(Toggle::new(3).label("Other")) .on_click(|checkeds, _, _| { println!("Selected toggles: {:?}", checkeds); }) ``` ### 受控分组 ```rust struct FilterView { notifications: bool, bots: bool, inbox: bool, other: bool, } impl Render for FilterView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { ToggleGroup::new("filters") .child(Toggle::new(0).icon(IconName::Bell).checked(self.notifications)) .child(Toggle::new(1).icon(IconName::Bot).checked(self.bots)) .child(Toggle::new(2).icon(IconName::Inbox).checked(self.inbox)) .child(Toggle::new(3).label("Other").checked(self.other)) .on_click(cx.listener(|view, checkeds, _, cx| { view.notifications = checkeds[0]; view.bots = checkeds[1]; view.inbox = checkeds[2]; view.other = checkeds[3]; cx.notify(); })) } } ``` ### 分段样式 ToggleGroup 使用 `segmented()` 可以把一组 Toggle 渲染成贴合的分段控件。这个样式只影响外观, 交互语义仍然是当前的多选模型:`on_click` 会收到每一项最新状态组成的 `Vec`。 ```rust ToggleGroup::new("formatting") .segmented() .outline() .child(Toggle::new(0).label("Bold").checked(self.bold)) .child(Toggle::new(1).label("Italic").checked(self.italic)) .child(Toggle::new(2).label("Code").checked(self.code)) .on_click(cx.listener(|view, states, _, cx| { view.bold = states[0]; view.italic = states[1]; view.code = states[2]; cx.notify(); })) ``` 分段样式默认使用 `0px` 间距,相邻项会共享一个连续边框。需要保留间距时可以传入 非零 `gap`: ```rust use gpui_kit::px; ToggleGroup::new("quick-actions") .segmented() .outline() .gap(px(8.)) .small() .child(Toggle::new(0).label("Star")) .child(Toggle::new(1).label("Watch")) .child(Toggle::new(2).label("Pin")) ``` 如果业务需要互斥选择,请继续在视图状态中自行保证只有一项 `checked(true)`, 直到后续提供专门的单选 API。 ## 最佳实践 1. 需要按钮式反馈时优先使用 Toggle,而不是 Switch。 2. 一组相关选项应使用 `ToggleGroup` 统一管理。 3. 图标型 Toggle 最好补充 tooltip 或可访问标签。 4. Toggle 状态应与实际业务状态保持同步,避免视觉与数据不一致。 --- --- url: /base/primitives/toggle-group.md description: Coordinates a set of toggle controls as a single- or multiple-selection group. --- # Toggle Group Coordinates a set of toggle controls as a single- or multiple-selection group. Like every `gpui-base` primitive, Toggle Group supplies behavior and semantic structure without imposing a product visual language. Apply GPUI styles and compose the exported parts to match your design system. ## Example The [single native Cargo entrypoint](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/components.rs) selects this primitive from the [shared showcase implementation](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/mod.rs). The same showcase is compiled once for the WASM preview above. ```bash cargo run -p gpui-base --example components -- toggle-group ``` ## Import ```rust use gpui_kit::base::{Toggle, ToggleGroup}; ``` ## Anatomy and API The example composes `Toggle`, `ToggleGroup`. GPUI's standard styling and event traits provide presentation; these base types provide the interaction structure. The authoritative module is [`components/toggle_group.rs`](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/components/toggle_group.rs). Native and browser previews compile this same file. ## State and events The group coordinates single or multiple selection while children reflect group state. Keep controlled state on the parent render type or in a GPUI entity. Update it in callbacks and call `cx.notify()`; do not recreate persistent entities during every render. ## Complete Rust example The complete implementation used by the runnable showcase is embedded directly from Rust source: The command above supplies application initialization, window creation, and shared `BaseShowcase` state. ## Accessibility Label the group, expose each selection state, and keep focus order predictable. ## Notes Use stable element IDs where accepted. Verify focus, hover, active, selected, disabled, reduced-motion, and high-contrast appearances in the consuming design system. --- --- url: /zh-CN/base/primitives/toggle-group.md description: 将多个 Toggle 协调为单选或多选组。 --- # Toggle Group 将多个 Toggle 协调为单选或多选组。 和所有 GPUI Base 原语一样,Toggle Group 只提供行为和语义结构,不规定产品视觉语言。请使用 GPUI 样式并组合导出的部件,使其符合你的设计系统。 ## 示例 原生示例和页面上方的 WASM 预览共用同一份实现: ```bash cargo run -p gpui-base --example components -- toggle-group ``` ## 导入 ```rust use gpui_kit::base::{Toggle, ToggleGroup}; ``` ## 结构与 API 示例组合上述公开类型。GPUI 的标准样式和事件 trait 负责表现,Base 类型负责交互结构。权威实现位于 [`components/toggle-group.rs`](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/components/toggle-group.rs),原生与浏览器预览编译的是同一文件。 ## 状态与事件 组模式决定只保留一个值还是一组值;变更由父级持久化。 受控状态应保存在父渲染类型或 GPUI entity 中;在回调中更新并调用 `cx.notify()`,不要在每次渲染时重建持久 entity。 ## 完整 Rust 示例 ## 可访问性 提供组名称,清楚表达每项的按下状态,并支持一致的键盘导航。 ## 注意事项 在支持的位置使用稳定元素 ID,并在消费端设计系统中验证焦点、悬停、按下、选中、禁用、减少动态效果和高对比度状态。 --- --- url: /base/primitives/tooltip.md description: 'A delayed, positioned description associated with a trigger element.' --- # Tooltip A delayed, positioned description associated with a trigger element. Like every `gpui-base` primitive, Tooltip supplies behavior and semantic structure without imposing a product visual language. Apply GPUI styles and compose the exported parts to match your design system. ## Example The [single native Cargo entrypoint](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/components.rs) selects this primitive from the [shared showcase implementation](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/mod.rs). The same showcase is compiled once for the WASM preview above. ```bash cargo run -p gpui-base --example components -- tooltip ``` ## Import ```rust use gpui_kit::base::{Tooltip}; ``` ## Anatomy and API The example composes `Tooltip`. GPUI's standard styling and event traits provide presentation; these base types provide the interaction structure. The authoritative module is [`components/tooltip.rs`](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/components/tooltip.rs). Native and browser previews compile this same file. ## State and events Hover or focus schedules it and exit or blur dismisses it; content is descriptive. Keep controlled state on the parent render type or in a GPUI entity. Update it in callbacks and call `cx.notify()`; do not recreate persistent entities during every render. ## Complete Rust example The complete implementation used by the runnable showcase is embedded directly from Rust source: The command above supplies application initialization, window creation, and shared `BaseShowcase` state. ## Accessibility Show on focus as well as hover; tooltips supplement names and contain no required controls. ## Notes Use stable element IDs where accepted. Verify focus, hover, active, selected, disabled, reduced-motion, and high-contrast appearances in the consuming design system. --- --- url: /zh-CN/base/primitives/tooltip.md description: 与触发元素关联、延迟显示且可定位的说明。 --- # Tooltip 与触发元素关联、延迟显示且可定位的说明。 和所有 GPUI Base 原语一样,Tooltip 只提供行为和语义结构,不规定产品视觉语言。请使用 GPUI 样式并组合导出的部件,使其符合你的设计系统。 ## 示例 原生示例和页面上方的 WASM 预览共用同一份实现: ```bash cargo run -p gpui-base --example components -- tooltip ``` ## 导入 ```rust use gpui_kit::base::{Tooltip}; ``` ## 结构与 API 示例组合上述公开类型。GPUI 的标准样式和事件 trait 负责表现,Base 类型负责交互结构。权威实现位于 [`components/tooltip.rs`](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/components/tooltip.rs),原生与浏览器预览编译的是同一文件。 ## 状态与事件 指针悬停或键盘聚焦后延迟显示,离开或失焦后关闭。 受控状态应保存在父渲染类型或 GPUI entity 中;在回调中更新并调用 `cx.notify()`,不要在每次渲染时重建持久 entity。 ## 完整 Rust 示例 ## 可访问性 Tooltip 只补充说明,不能承载完成任务所必需的信息;触发器必须可聚焦。 ## 注意事项 在支持的位置使用稳定元素 ID,并在消费端设计系统中验证焦点、悬停、按下、选中、禁用、减少动态效果和高对比度状态。 --- --- url: /docs/components/tooltip.md description: >- Display helpful information on hover or focus, with support for keyboard shortcuts and custom content. --- # Tooltip A versatile tooltip component that displays helpful information when hovering over or focusing on elements. Supports text content, custom elements, keyboard shortcuts, different trigger methods, and positioning options. ## Import ```rust use gpui_kit::component::tooltip::Tooltip; ``` ## Usage ### Basic Tooltip with Text ```rust // Simple text tooltip div() .child("Hover me") .id("basic-tooltip") .tooltip(|window, cx| { Tooltip::new("This is a helpful tooltip").build(window, cx) }) ``` ### Button with Tooltip ```rust Button::new("save-btn") .label("Save") .tooltip("Save the current document") ``` ### Tooltip with Action/Keybinding ```rust actions!(my_actions, [SaveDocument]); Button::new("save-btn") .label("Save") .tooltip_with_action( "Save the current document", &SaveDocument, Some("MyContext") ) ``` ### Custom Element Tooltip ```rust div() .child("Hover for rich content") .id("rich-tooltip") .tooltip(|window, cx| { Tooltip::element(|_, cx| { h_flex() .gap_x_1() .child(IconName::Info) .child( div() .child("Muted Text") .text_color(cx.theme().muted_foreground) ) .child( div() .child("Danger Text") .text_color(cx.theme().danger) ) .child(IconName::ArrowUp) }) .build(window, cx) }) ``` ### Tooltip with Manual Keybinding ```rust div() .child("Custom keybinding") .id("custom-kb") .tooltip(|window, cx| { Tooltip::new("Delete item") .key_binding(Some(Kbd::new("Delete"))) .build(window, cx) }) ``` ## Advanced Usage ### Components with Built-in Tooltip Support Many components have built-in tooltip methods: ```rust // Button Button::new("btn") .label("Click me") .tooltip("This button performs an action") // Switch Switch::new("toggle") .label("Enable notifications") .tooltip("Toggle push notifications on/off") // Checkbox Checkbox::new("check") .label("Remember me") .tooltip("Keep me logged in for 30 days") // Radio Radio::new("option") .label("Option 1") .tooltip("Select this option to enable feature X") ``` ### Complex Tooltip Content ```rust div() .child("Hover for details") .id("complex-tooltip") .tooltip(|window, cx| { Tooltip::element(|_, cx| { v_flex() .gap_2() .child( h_flex() .gap_1() .child(IconName::User) .child("User Information") .text_sm() .font_semibold() ) .child( div() .child("Last login: 2 hours ago") .text_xs() .text_color(cx.theme().muted_foreground) ) .child( div() .child("Status: Active") .text_xs() .text_color(cx.theme().success) ) }) .build(window, cx) }) ``` ### Tooltip in Form Elements ```rust v_flex() .gap_4() .child( Input::new("email") .placeholder("Enter your email") .tooltip("We'll never share your email address") ) .child( Input::new("password") .input_type(InputType::Password) .placeholder("Password") .tooltip("Must be at least 8 characters with special characters") ) ``` ## API Reference ### Tooltip | Method | Description | | ------------------------- | -------------------------------------------- | | `new(text)` | Create a tooltip with text content | | `element(builder)` | Create a tooltip with custom element content | | `action(action, context)` | Set action to display keybinding information | | `key_binding(kbd)` | Set manual keybinding information | | `build(window, cx)` | Build and return the tooltip as AnyView | ### Built-in Tooltip Methods Components with tooltip support typically provide these methods: | Method | Description | | -------------------------------------------- | --------------------------------------- | | `tooltip(text)` | Add simple text tooltip | | `tooltip_with_action(text, action, context)` | Add tooltip with action keybinding | | `tooltip(closure)` | Add custom tooltip with builder closure | ### Tooltip Styling The tooltip automatically applies theme-appropriate styling: * Background: `theme.popover` * Text color: `theme.popover_foreground` * Border: `theme.border` * Shadow: Medium drop shadow * Border radius: 6px * Font: System UI font You can apply additional styling using the `Styled` trait: ```rust Tooltip::new("Custom styled tooltip") .bg(cx.theme().accent) .text_color(cx.theme().accent_foreground) .build(window, cx) ``` ## Examples ### Toolbar with Tooltips ```rust h_flex() .gap_1() .child( Button::new("new") .icon(IconName::Plus) .tooltip_with_action("Create new file", &NewFile, Some("Editor")) ) .child( Button::new("open") .icon(IconName::FolderOpen) .tooltip_with_action("Open file", &OpenFile, Some("Editor")) ) .child( Button::new("save") .icon(IconName::Save) .tooltip_with_action("Save file", &SaveFile, Some("Editor")) ) ``` ### Status Indicators with Tooltips ```rust h_flex() .gap_2() .child( div() .size_3() .rounded_full() .bg(cx.theme().success) .tooltip(|window, cx| { Tooltip::new("Connected to server").build(window, cx) }) ) .child( div() .size_3() .rounded_full() .bg(cx.theme().warning) .tooltip(|window, cx| { Tooltip::new("Limited connectivity").build(window, cx) }) ) ``` ### Interactive Elements with Rich Tooltips ```rust v_flex() .gap_3() .child( div() .p_2() .border_1() .border_color(cx.theme().border) .rounded(cx.theme().radius) .child("File: document.txt") .id("file-item") .tooltip(|window, cx| { Tooltip::element(|_, cx| { v_flex() .gap_1() .child( h_flex() .gap_2() .child(IconName::File) .child("document.txt") .text_sm() .font_medium() ) .child( div() .child("Size: 2.4 KB") .text_xs() .text_color(cx.theme().muted_foreground) ) .child( div() .child("Modified: 2 hours ago") .text_xs() .text_color(cx.theme().muted_foreground) ) .child( h_flex() .gap_1() .child(Kbd::new("Enter")) .child("to open") .text_xs() .text_color(cx.theme().muted_foreground) ) }) .build(window, cx) }) ) ``` ### Form Validation with Tooltips ```rust struct FormView { email_error: Option, password_error: Option, } v_flex() .gap_4() .child( Input::new("email") .placeholder("Email address") .when_some(self.email_error.clone(), |this, error| { this.tooltip(move |window, cx| { Tooltip::element(|_, cx| { h_flex() .gap_1() .child(IconName::AlertCircle) .child(error.clone()) .text_color(cx.theme().destructive) }) .build(window, cx) }) }) ) ``` ## Best Practices ### Content Guidelines * **Be concise**: Keep tooltip text short and to the point * **Be helpful**: Provide additional context, not redundant information * **Use proper tone**: Match your application's voice and tone * **Avoid critical info**: Don't put essential information only in tooltips ### Usage Guidelines * **Progressive disclosure**: Use tooltips for additional context, not primary information * **Consistency**: Use consistent tooltip patterns throughout your application * **Performance**: Avoid complex content in frequently triggered tooltips * **Testing**: Test tooltips with both mouse and keyboard interaction ### Examples of Good Tooltip Content ```rust // Good: Provides helpful context Button::new("delete") .icon(IconName::Trash) .tooltip("Delete this item permanently") // Good: Explains abbreviation div() .child("CPU: 45%") .tooltip("Central Processing Unit usage") // Good: Describes action with keybinding Button::new("undo") .icon(IconName::Undo) .tooltip_with_action("Undo last action", &Undo, Some("Editor")) ``` ### Examples to Avoid ```rust // Avoid: Redundant information Button::new("save") .label("Save") .tooltip("Save") // Doesn't add value // Avoid: Critical information Button::new("delete") .tooltip("This will permanently delete all your files") // Too important for tooltip only ``` --- --- url: /zh-CN/docs/components/tooltip.md description: 在悬停或聚焦时显示提示信息,支持快捷键和自定义内容。 --- # Tooltip Tooltip 用于在鼠标悬停或元素获得焦点时显示补充信息。它支持纯文本、自定义内容、快捷键信息以及多种触发方式,适合做解释说明、状态提示和操作说明。 ## 导入 ```rust use gpui_kit::component::tooltip::Tooltip; ``` ## 用法 ### 纯文本 Tooltip ```rust div() .child("Hover me") .id("basic-tooltip") .tooltip(|window, cx| { Tooltip::new("This is a helpful tooltip").build(window, cx) }) ``` ### 按钮 Tooltip ```rust Button::new("save-btn") .label("Save") .tooltip("Save the current document") ``` ### 携带快捷键信息 ```rust actions!(my_actions, [SaveDocument]); Button::new("save-btn") .label("Save") .tooltip_with_action( "Save the current document", &SaveDocument, Some("MyContext") ) ``` ### 自定义内容 Tooltip ```rust div() .child("Hover for rich content") .id("rich-tooltip") .tooltip(|window, cx| { Tooltip::element(|_, cx| { h_flex() .gap_x_1() .child(IconName::Info) .child( div() .child("Muted Text") .text_color(cx.theme().muted_foreground) ) .child( div() .child("Danger Text") .text_color(cx.theme().danger) ) .child(IconName::ArrowUp) }) .build(window, cx) }) ``` ### 手动指定快捷键 ```rust div() .child("Custom keybinding") .id("custom-kb") .tooltip(|window, cx| { Tooltip::new("Delete item") .key_binding(Some(Kbd::new("Delete"))) .build(window, cx) }) ``` ## API 参考 ### Tooltip | 方法 | 说明 | | --- | --- | | `new(text)` | 创建文本型 Tooltip | | `element(builder)` | 创建自定义内容 Tooltip | | `action(action, context)` | 关联 action,显示对应快捷键信息 | | `key_binding(kbd)` | 手动设置快捷键展示 | | `build(window, cx)` | 构建并返回 Tooltip 视图 | ### 内置 Tooltip 方法 很多组件内置了 Tooltip 支持,常见形式包括: | 方法 | 说明 | | --- | --- | | `tooltip(text)` | 添加简单文本提示 | | `tooltip_with_action(text, action, context)` | 添加带快捷键的提示 | | `tooltip(closure)` | 使用构建器生成自定义提示 | ## 样式 Tooltip 默认会自动应用与主题匹配的样式: * 背景:`theme.popover` * 文字:`theme.popover_foreground` * 边框:`theme.border` * 阴影:中等投影 * 圆角:约 `6px` 也可以继续通过 `Styled` trait 自定义: ```rust Tooltip::new("Custom styled tooltip") .bg(cx.theme().accent) .text_color(cx.theme().accent_foreground) .build(window, cx) ``` ## 示例 ### 工具栏提示 ```rust h_flex() .gap_1() .child( Button::new("new") .icon(IconName::Plus) .tooltip_with_action("Create new file", &NewFile, Some("Editor")) ) .child( Button::new("save") .icon(IconName::Save) .tooltip_with_action("Save file", &SaveFile, Some("Editor")) ) ``` ### 表单说明 ```rust v_flex() .gap_4() .child( Input::new("email") .placeholder("Enter your email") .tooltip("We'll never share your email address") ) .child( Input::new("password") .input_type(InputType::Password) .placeholder("Password") .tooltip("Must be at least 8 characters with special characters") ) ``` ## 最佳实践 * 提示文案应简短直接,不重复界面上已经明显存在的信息。 * Tooltip 适合做补充说明,不应用来承载关键流程信息。 * 图标按钮、缩写和危险操作尤其适合配套 Tooltip。 * 需要高频触发的 Tooltip 应尽量避免复杂内容,以减少渲染开销。 --- --- url: /base/primitives/tree.md description: A virtualized hierarchical list with explicit expansion and selection state. --- # Tree A virtualized hierarchical list with explicit expansion and selection state. Like every `gpui-base` primitive, Tree supplies behavior and semantic structure without imposing a product visual language. Apply GPUI styles and compose the exported parts to match your design system. ## Example The [single native Cargo entrypoint](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/components.rs) selects this primitive from the [shared showcase implementation](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/mod.rs). The same showcase is compiled once for the WASM preview above. ```bash cargo run -p gpui-base --example components -- tree ``` ## Import ```rust use gpui_kit::base::{Tree, TreeItem, TreeState}; ``` ## Anatomy and API The example composes `Tree`, `TreeItem`, `TreeState`. GPUI's standard styling and event traits provide presentation; these base types provide the interaction structure. The authoritative module is [`components/tree.rs`](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/components/tree.rs). Native and browser previews compile this same file. ## State and events `TreeState` owns items, expansion, and selection; tree actions update that entity. Keep controlled state on the parent render type or in a GPUI entity. Update it in callbacks and call `cx.notify()`; do not recreate persistent entities during every render. ## Complete Rust example The complete implementation used by the runnable showcase is embedded directly from Rust source: The command above supplies application initialization, window creation, and shared `BaseShowcase` state. ## Accessibility Expose hierarchy, level, expansion, and selection; preserve keyboard movement and visible focus. ## Notes Use stable element IDs where accepted. Verify focus, hover, active, selected, disabled, reduced-motion, and high-contrast appearances in the consuming design system. --- --- url: /zh-CN/base/primitives/tree.md description: 显式管理展开与选择状态的虚拟化层级列表。 --- # Tree 显式管理展开与选择状态的虚拟化层级列表。 和所有 GPUI Base 原语一样,Tree 只提供行为和语义结构,不规定产品视觉语言。请使用 GPUI 样式并组合导出的部件,使其符合你的设计系统。 ## 示例 原生示例和页面上方的 WASM 预览共用同一份实现: ```bash cargo run -p gpui-base --example components -- tree ``` ## 导入 ```rust use gpui_kit::base::{Tree, TreeItem, TreeState}; ``` ## 结构与 API 示例组合上述公开类型。GPUI 的标准样式和事件 trait 负责表现,Base 类型负责交互结构。权威实现位于 [`components/tree.rs`](https://github.com/longbridge/gpui-kit/blob/main/crates/base/examples/showcase/components/tree.rs),原生与浏览器预览编译的是同一文件。 ## 状态与事件 TreeState 保存展开、选择和虚拟滚动状态;稳定节点 ID 用于跨渲染保留身份。 受控状态应保存在父渲染类型或 GPUI entity 中;在回调中更新并调用 `cx.notify()`,不要在每次渲染时重建持久 entity。 ## 完整 Rust 示例 ## 可访问性 暴露层级、展开与选中状态,并保留方向键、Home/End 和类型导航。 ## 注意事项 在支持的位置使用稳定元素 ID,并在消费端设计系统中验证焦点、悬停、按下、选中、禁用、减少动态效果和高对比度状态。 --- --- url: /docs/components/tree.md description: >- A hierarchical tree view component for displaying and navigating tree-structured data. --- # Tree A versatile tree component for displaying hierarchical data with expand/collapse functionality, keyboard navigation, and custom item rendering. Perfect for file explorers, navigation menus, or any nested data structure. ## Import ```rust use gpui_kit::component::tree::{tree, TreeState, TreeItem, TreeEntry}; ``` ## Usage ### Basic Tree ```rust // Create tree state let tree_state = cx.new(|cx| { TreeState::new(cx).items(vec![ TreeItem::new("src", "src") .expanded(true) .child(TreeItem::new("src/lib.rs", "lib.rs")) .child(TreeItem::new("src/main.rs", "main.rs")), TreeItem::new("Cargo.toml", "Cargo.toml"), TreeItem::new("README.md", "README.md"), ]) }); // Render tree tree(&tree_state, |ix, entry, selected, window, cx| { ListItem::new(ix) .child( h_flex() .gap_2() .child(entry.item().label.clone()) ) }) ``` ### File Tree with Icons ```rust use gpui_kit::component::{ListItem, IconName, h_flex}; tree(&tree_state, |ix, entry, selected, window, cx| { let item = entry.item(); let icon = if !entry.is_folder() { IconName::File } else if entry.is_expanded() { IconName::FolderOpen } else { IconName::Folder }; ListItem::new(ix) .selected(selected) .pl(px(16.) * entry.depth() + px(12.)) // Indent based on depth .child( h_flex() .gap_2() .child(icon) .child(item.label.clone()) ) .on_click(cx.listener(move |_, _, _, _| { // Handle item click })) }) ``` ### Dynamic Tree Loading ```rust impl MyView { fn load_files(&mut self, path: PathBuf, cx: &mut Context) { let tree_state = self.tree_state.clone(); cx.spawn(async move |cx| { let items = build_file_items(&path).await; tree_state.update(cx, |state, cx| { state.set_items(items, cx); }) }).detach(); } } fn build_file_items(path: &Path) -> Vec { let mut items = Vec::new(); if let Ok(entries) = std::fs::read_dir(path) { for entry in entries.flatten() { let path = entry.path(); let name = path.file_name() .and_then(|n| n.to_str()) .unwrap_or("Unknown") .to_string(); if path.is_dir() { let children = build_file_items(&path); items.push(TreeItem::new(path.to_string_lossy(), name) .children(children)); } else { items.push(TreeItem::new(path.to_string_lossy(), name)); } } } items } ``` ### Tree with Selection Handling ```rust struct MyTreeView { tree_state: Entity, selected_item: Option, } impl MyTreeView { fn handle_selection(&mut self, item: TreeItem, cx: &mut Context) { self.selected_item = Some(item.clone()); println!("Selected: {} ({})", item.label, item.id); cx.notify(); } } // In render method tree(&self.tree_state, { let view = cx.entity(); move |ix, entry, selected, window, cx| { view.update(cx, |this, cx| { ListItem::new(ix) .selected(selected) .child(entry.item().label.clone()) .on_click(cx.listener({ let item = entry.item().clone(); move |this, _, _, cx| { this.handle_selection(item.clone(), cx); } })) }) } }) ``` ### Disabled Items ```rust TreeItem::new("protected", "Protected Folder") .disabled(true) .child(TreeItem::new("secret.txt", "secret.txt")) ``` ### Programmatic Tree Control ```rust // Get current selection if let Some(entry) = tree_state.read(cx).selected_entry() { println!("Current selection: {}", entry.item().label); } // Set selection programmatically (by selected_index) tree_state.update(cx, |state, cx| { state.set_selected_index(Some(2), cx); // Select third item }); // Set selection programmatically (by tree item) tree_state.update(cx, |state, cx| { state.set_selected_item(Some(item), cx); // Select third item }); // Scroll to specific item tree_state.update(cx, |state, _| { state.scroll_to_item(5, gpui_kit::ScrollStrategy::Center); }); // Clear selection (by selected_index) tree_state.update(cx, |state, cx| { state.set_selected_index(None, cx); }); // Clear selection (by tree item) tree_state.update(cx, |state, cx| { state.set_selected_item(None, cx); }); ``` ## API Reference ### TreeState | Method | Description | |--------------------------------|----------------------------------| | `new(cx)` | Create a new tree state | | `items(items)` | Set initial tree items | | `set_items(items, cx)` | Update tree items and notify | | `selected_index()` | Get currently selected index | | `set_selected_index(ix, cx)` | Set selected index | | `set_selected_item(item, cx)` | Set selected by tree item | | `selected_item(item, cx)` | Get currently selected tree item | | `selected_entry()` | Get currently selected entry | | `scroll_to_item(ix, strategy)` | Scroll to specific item | ### TreeItem | Method | Description | | ----------------- | -------------------------------------- | | `new(id, label)` | Create new tree item with ID and label | | `child(item)` | Add single child item | | `children(items)` | Add multiple child items | | `expanded(bool)` | Set expanded state | | `disabled(bool)` | Set disabled state | | `is_folder()` | Check if item has children | | `is_expanded()` | Check if item is expanded | | `is_disabled()` | Check if item is disabled | ### TreeEntry | Method | Description | | --------------- | --------------------------- | | `item()` | Get the source TreeItem | | `depth()` | Get item depth in tree | | `is_folder()` | Check if entry has children | | `is_expanded()` | Check if entry is expanded | | `is_disabled()` | Check if entry is disabled | ### tree() Function | Parameter | Description | | ------------- | ------------------------------------- | | `state` | `Entity` for managing tree | | `render_item` | Closure for rendering each item | #### Render Item Closure ```rust Fn(usize, &TreeEntry, bool, &mut Window, &mut App) -> ListItem ``` * `usize`: Item index in flattened tree * `&TreeEntry`: Tree entry with item and metadata * `bool`: Whether item is currently selected * `&mut Window`: Current window context * `&mut App`: Application context * Returns: `ListItem` for rendering ## Examples ### Lazy Loading Tree ```rust struct LazyTreeView { tree_state: Entity, loaded_paths: HashSet, } impl LazyTreeView { fn load_children(&mut self, item_id: &str, cx: &mut Context) { if self.loaded_paths.contains(item_id) { return; } let path = PathBuf::from(item_id); if path.is_dir() { let tree_state = self.tree_state.clone(); let item_id = item_id.to_string(); cx.spawn(async move |cx| { let children = load_directory_children(&path).await; tree_state.update(cx, |state, cx| { // Update specific item with loaded children state.update_item_children(&item_id, children, cx); }) }).detach(); self.loaded_paths.insert(item_id.to_string()); } } } ``` ### Search and Filter ```rust struct SearchableTree { tree_state: Entity, original_items: Vec, search_query: String, } impl SearchableTree { fn filter_tree(&mut self, query: &str, cx: &mut Context) { self.search_query = query.to_string(); let filtered_items = if query.is_empty() { self.original_items.clone() } else { filter_tree_items(&self.original_items, query) }; self.tree_state.update(cx, |state, cx| { state.set_items(filtered_items, cx); }); } } fn filter_tree_items(items: &[TreeItem], query: &str) -> Vec { items.iter() .filter_map(|item| { if item.label.to_lowercase().contains(&query.to_lowercase()) { Some(item.clone().expanded(true)) // Auto-expand matches } else { // Check if any children match let filtered_children = filter_tree_items(&item.children, query); if !filtered_children.is_empty() { Some(item.clone() .children(filtered_children) .expanded(true)) } else { None } } }) .collect() } ``` ### Multi-Select Tree ```rust struct MultiSelectTree { tree_state: Entity, selected_items: HashSet, } impl MultiSelectTree { fn toggle_selection(&mut self, item_id: &str, cx: &mut Context) { if self.selected_items.contains(item_id) { self.selected_items.remove(item_id); } else { self.selected_items.insert(item_id.to_string()); } cx.notify(); } fn is_selected(&self, item_id: &str) -> bool { self.selected_items.contains(item_id) } } // In render method tree(&self.tree_state, { let view = cx.entity(); move |ix, entry, _selected, window, cx| { view.update(cx, |this, cx| { let item = entry.item(); let is_multi_selected = this.is_selected(&item.id); ListItem::new(ix) .selected(is_multi_selected) .child( h_flex() .gap_2() .child(checkbox().checked(is_multi_selected)) .child(item.label.clone()) ) .on_click(cx.listener({ let item_id = item.id.clone(); move |this, _, _, cx| { this.toggle_selection(&item_id, cx); } })) }) } }) ``` ## Keyboard Navigation The Tree component supports comprehensive keyboard navigation: | Key | Action | | ------- | ----------------------------------------- | | `↑` | Select previous item | | `↓` | Select next item | | `←` | Collapse current folder or move to parent | | `→` | Expand current folder | | `Enter` | Toggle expand/collapse for folders | | `Space` | Custom action (configurable) | ```rust // Custom keyboard handling tree(&tree_state) .key_context("MyTree") .on_action(cx.listener(|this, action: &MyCustomAction, _, cx| { // Handle custom actions })) ``` --- --- url: /zh-CN/docs/components/tree.md description: 用于显示和导航树形结构数据的层级树组件。 --- # Tree Tree 是一个用于展示层级数据的通用组件,支持展开/折叠、键盘导航、自定义项渲染以及编程式选中控制。它非常适合文件浏览器、菜单树和嵌套数据结构。 ## 导入 ```rust use gpui_kit::component::tree::{tree, TreeState, TreeItem, TreeEntry}; ``` ## 用法 ### 基础树 ```rust let tree_state = cx.new(|cx| { TreeState::new(cx).items(vec![ TreeItem::new("src", "src") .expanded(true) .child(TreeItem::new("src/lib.rs", "lib.rs")) .child(TreeItem::new("src/main.rs", "main.rs")), TreeItem::new("Cargo.toml", "Cargo.toml"), TreeItem::new("README.md", "README.md"), ]) }); tree(&tree_state, |ix, entry, selected, window, cx| { ListItem::new(ix) .child( h_flex() .gap_2() .child(entry.item().label.clone()) ) }) ``` ### 文件树与图标 ```rust tree(&tree_state, |ix, entry, selected, window, cx| { let item = entry.item(); let icon = if !entry.is_folder() { IconName::File } else if entry.is_expanded() { IconName::FolderOpen } else { IconName::Folder }; ListItem::new(ix) .selected(selected) .pl(px(16.) * entry.depth() + px(12.)) .child( h_flex() .gap_2() .child(icon) .child(item.label.clone()) ) }) ``` ### 动态加载 ```rust impl MyView { fn load_files(&mut self, path: PathBuf, cx: &mut Context) { let tree_state = self.tree_state.clone(); cx.spawn(async move |cx| { let items = build_file_items(&path).await; tree_state.update(cx, |state, cx| { state.set_items(items, cx); }) }).detach(); } } ``` ### 选择处理 ```rust struct MyTreeView { tree_state: Entity, selected_item: Option, } impl MyTreeView { fn handle_selection(&mut self, item: TreeItem, cx: &mut Context) { self.selected_item = Some(item.clone()); println!("Selected: {} ({})", item.label, item.id); cx.notify(); } } ``` ### 禁用项 ```rust TreeItem::new("protected", "Protected Folder") .disabled(true) .child(TreeItem::new("secret.txt", "secret.txt")) ``` ### 编程式控制 ```rust if let Some(entry) = tree_state.read(cx).selected_entry() { println!("Current selection: {}", entry.item().label); } tree_state.update(cx, |state, cx| { state.set_selected_index(Some(2), cx); }); ``` ## API 参考 ### TreeState * `new(cx)` * `items(items)` * `set_items(items, cx)` * `selected_index()` * `set_selected_index(ix, cx)` * `set_selected_item(item, cx)` * `selected_entry()` * `scroll_to_item(ix, strategy)` ### TreeItem * `new(id, label)` * `child(item)` * `children(items)` * `expanded(bool)` * `disabled(bool)` ### TreeEntry * `item()` * `depth()` * `is_folder()` * `is_expanded()` * `is_disabled()` ## 键盘导航 | 按键 | 行为 | | --- | --- | | `↑` | 选中上一个节点 | | `↓` | 选中下一个节点 | | `←` | 折叠当前节点或移动到父级 | | `→` | 展开当前节点 | | `Enter` | 切换展开/折叠 | | `Space` | 自定义动作 | --- --- url: /contributors.md --- --- --- url: /base/virtual-list.md description: >- Render a hundred thousand differently sized rows by drawing only the ones on screen. --- # Virtual List Render a list of any length by drawing only the items currently on screen. Unlike `gpui_kit::uniform_list`, **each item may have a different size** — which is what makes it usable for tables with variable row heights, chat transcripts, and outline trees. Virtual List is infrastructure rather than a component: it has no appearance of its own, contributes no chrome, and imposes nothing on the items you return. You give it the sizes up front and a closure that renders a range. ## Why sizes up front Virtualization needs to know the total extent of the list and which items intersect the viewport **without rendering anything**. Two designs solve this: | Approach | How | Cost | | ------------------------ | ---------------------------------------------------------- | ----------------------------------------------------------------------------- | | `gpui_kit::uniform_list` | Every item is the same size, so offsets are multiplication | No per-item data, but no variable sizes | | Measure as you scroll | Render, measure, correct | Scrollbar jumps; scroll position drifts | | **`VirtualList`** | You supply every item's size | Exact offsets and a stable scrollbar, at the cost of knowing sizes in advance | The third is why `item_sizes` is a required argument rather than a callback. If your items are genuinely unmeasurable until drawn, compute a good estimate, or use a fixed row height and let content clip. ## Get started ```rust use std::rc::Rc; use gpui_kit::base::{v_virtual_list, VirtualListScrollHandle}; use gpui_kit::{px, size}; let sizes = Rc::new(vec![size(px(280.), px(32.)); 100_000]); v_virtual_list( cx.entity(), "customers", sizes, |_this, range, _window, _cx| { range .map(|ix| div().h_8().px_2().child(format!("Customer {ix}"))) .collect() }, ) .track_scroll(&self.scroll_handle) .size_full() ``` The closure is handed a `Range` — only the visible slice, plus a small overdraw — and returns one element per index in that range. It receives `&mut V` for the entity you passed, so it can read your data without cloning it into the closure. Use `h_virtual_list` for a horizontal list; everything else is identical. ## The size contract Three rules, and breaking any of them shows up as misplaced items rather than a panic. **Only one dimension is read.** A vertical list uses each `Size`'s `height` and ignores its `width`; a horizontal list uses `width` and ignores `height`. The value you pass for the unused axis is free. **The cross axis is measured, not declared.** A vertical list gets its width by laying out one item and measuring it — by default item 0. If your first item is not representative (an unusually short label, say), point it at one that is: ```rust v_virtual_list(entity, "rows", sizes, render) .with_item_to_measure_index(3) ``` **`item_sizes.len()` is the item count.** The list renders exactly that many items; there is no separate count argument. If the vector disagrees with your data, the extra indices are still requested from your closure. `Rc>>` is shared rather than owned so that rebuilding the element every frame does not reallocate the size table. Keep the `Rc` in your entity and clone the handle, rather than constructing the vector inside `render`. ## Scrolling `VirtualListScrollHandle` owns the scroll position and survives re-renders, so it belongs in your entity, not in `render`. ```rust struct CustomerList { scroll: VirtualListScrollHandle, } // Jump to an item self.scroll.scroll_to_item(4_200, ScrollStrategy::Top); // Follow a growing list self.scroll.scroll_to_bottom(); ``` `scroll_to_item` takes a `ScrollStrategy` — `Top`, `Center`, or `Bottom` — and works on indices that have never been rendered, because the offset comes from the size table rather than from measurement. `base_handle()` exposes the underlying GPUI `ScrollHandle` when you need it. Attach the handle with `.track_scroll(&handle)`. ## With a scrollbar `VirtualListScrollHandle` implements `ScrollbarHandle`, so the base `Scrollbar` reads it directly. The list draws no scrollbar of its own. ```rust div() .relative() .child( v_virtual_list(cx.entity(), "rows", sizes, render) .track_scroll(&self.scroll) .size_full(), ) .child(Scrollbar::vertical(&self.scroll)) ``` The container needs `relative()` because the scrollbar positions itself against it. ## Sizing behavior `with_sizing_behavior` controls whether the list computes a size of its own: * `ListSizingBehavior::Auto` (default) — the list does not calculate a fixed size, and takes the space its parent gives it. * `ListSizingBehavior::Infer` — the list calculates its size from its items. The default plus a bounded parent is what almost every layout wants. A virtual list inside an unbounded parent has nothing to virtualize against, so it would try to lay out every item. ## The render closure The closure runs on **every frame that the visible range changes**, so treat it as a hot path: * Do no I/O, sorting, or filtering inside it. Keep the prepared data in your entity and index into it. * Return elements, not entities. Creating a GPUI entity per row defeats virtualization — the entity outlives the frame, so a hundred thousand rows would create a hundred thousand entities. * Element ids, if you set them, should derive from the item index or a stable key, not from position within the returned vector. State lives outside the closure. Update it in your own callbacks and call `cx.notify()`; do not mutate it during render. ## What it costs Work per frame is proportional to the number of **visible** items, not total items — the example on this page holds 100,000 rows and draws about a dozen. The size table is the one part that scales with the total: it is one `Size` per item, held once behind an `Rc`. The tradeoff is that the size table must exist before the first frame. For a million rows of uniform height that is a megabyte of sizes for information a single number could carry — that is the case `gpui_kit::uniform_list` exists for, and it is the better choice there. ## Complete Rust example ```bash cargo run -p gpui-base --example components -- virtual-list ``` ## Checklist * Hold `item_sizes` and the scroll handle in your entity; rebuild neither during render. * Keep `item_sizes.len()` equal to your data length. * Point `with_item_to_measure_index` at a representative item if item 0 is not one. * Give the list a bounded parent, and `relative()` on that parent if you add a scrollbar. * Preserve logical order, item counts, and stable identity so assistive technology sees a coherent list across virtualization. --- --- url: /docs/components/virtual-list.md description: >- High-performance virtualized list component for rendering large datasets with variable item sizes. --- # VirtualList VirtualList is a high-performance component designed for efficiently rendering large datasets by only rendering visible items. Unlike uniform lists, VirtualList supports variable item sizes, making it perfect for complex layouts like tables with different row heights or dynamic content. ## Import ```rust use gpui_kit::component::{ v_virtual_list, h_virtual_list, VirtualListScrollHandle, scroll::{Scrollbar, ScrollbarState, ScrollbarAxis}, }; use std::rc::Rc; use gpui_kit::{px, size, ScrollStrategy, Size, Pixels}; ``` ## Usage ### Basic Vertical Virtual List ```rust use std::rc::Rc; use gpui_kit::{px, size, Size, Pixels}; pub struct ListViewExample { items: Vec, item_sizes: Rc>>, scroll_handle: VirtualListScrollHandle, } impl ListViewExample { fn new(cx: &mut Context) -> Self { let items = (0..5000).map(|i| format!("Item {}", i)).collect::>(); let item_sizes = Rc::new(items.iter().map(|_| size(px(200.), px(30.))).collect()); Self { items, item_sizes, scroll_handle: VirtualListScrollHandle::new(), } } } impl Render for ListViewExample { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_virtual_list( cx.entity().clone(), "my-list", self.item_sizes.clone(), |view, visible_range, _, cx| { visible_range .map(|ix| { div() .h(px(30.)) .w_full() .bg(cx.theme().secondary) .child(format!("Item {}", ix)) }) .collect() }, ) .track_scroll(&self.scroll_handle) } } ``` ### Horizontal Virtual List ```rust h_virtual_list( cx.entity().clone(), "horizontal-list", item_sizes.clone(), |view, visible_range, _, cx| { visible_range .map(|ix| { div() .w(px(120.)) // Width is used for horizontal lists .h_full() .bg(cx.theme().accent) .child(format!("Card {}", ix)) }) .collect() }, ) .track_scroll(&scroll_handle) ``` ### Variable Item Sizes VirtualList excels at handling items with different sizes: ```rust let item_sizes = Rc::new( (0..1000) .map(|i| { // Different heights based on index let height = if i % 5 == 0 { px(60.) // Header items are taller } else if i % 3 == 0 { px(45.) // Some items are medium } else { px(30.) // Regular items }; size(px(300.), height) }) .collect::>() ); v_virtual_list( cx.entity().clone(), "variable-list", item_sizes.clone(), |view, visible_range, _, cx| { visible_range .map(|ix| { let content = if ix % 5 == 0 { format!("Header {}", ix / 5) } else { format!("Item {}", ix) }; let bg_color = if ix % 5 == 0 { cx.theme().accent } else { cx.theme().secondary }; div() .w_full() .h(item_sizes[ix].height) .bg(bg_color) .flex() .items_center() .px_4() .child(content) }) .collect() }, ) ``` ### Table-like Layout with Multiple Columns VirtualList can render complex layouts like tables: ```rust v_virtual_list( cx.entity().clone(), "table-list", item_sizes.clone(), |view, visible_range, _, cx| { visible_range .map(|row_ix| { h_flex() .w_full() .h(px(40.)) .border_b_1() .border_color(cx.theme().border) .children( // Multiple columns per row (0..5).map(|col_ix| { div() .flex_1() .h_full() .px_3() .flex() .items_center() .child(format!("R{}C{}", row_ix, col_ix)) }) ) }) .collect() }, ) ``` ## Scroll Handling ### Basic Scroll Control ```rust pub struct ScrollableList { scroll_handle: VirtualListScrollHandle, scroll_state: ScrollbarState, } impl Render for ScrollableList { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { div() .relative() .size_full() .child( v_virtual_list(/* ... */) .track_scroll(&self.scroll_handle) .p_4() .border_1() .border_color(cx.theme().border) ) .child( // Add scrollbars div() .absolute() .top_0() .left_0() .right_0() .bottom_0() .child( Scrollbar::both(&self.scroll_state, &self.scroll_handle) .axis(ScrollbarAxis::Vertical) ) ) } } ``` ### Programmatic Scrolling ```rust impl ScrollableList { // Scroll to specific item fn scroll_to_item(&self, index: usize) { self.scroll_handle.scroll_to_item(index, ScrollStrategy::Top); } // Center item in view fn center_item(&self, index: usize) { self.scroll_handle.scroll_to_item(index, ScrollStrategy::Center); } // Scroll to bottom fn scroll_to_bottom(&self) { self.scroll_handle.scroll_to_bottom(); } // Get current scroll position fn get_scroll_offset(&self) -> Point { self.scroll_handle.offset() } // Set scroll position manually fn set_scroll_position(&self, offset: Point) { self.scroll_handle.set_offset(offset); } } ``` ### Both Axis Scrolling For content that scrolls in both directions: ```rust v_virtual_list( cx.entity().clone(), "both-axis", item_sizes.clone(), |view, visible_range, _, cx| { visible_range .map(|ix| { // Wide content that requires horizontal scrolling h_flex() .gap_2() .children((0..20).map(|col| { div() .min_w(px(100.)) .h(px(30.)) .bg(cx.theme().secondary) .child(format!("R{}C{}", ix, col)) })) }) .collect() }, ) .track_scroll(&scroll_handle) .child( Scrollbar::both(&scroll_state, &scroll_handle) .axis(ScrollbarAxis::Both) ) ``` ## Performance Optimization ### Efficient Item Rendering Only visible items are rendered, making VirtualList highly performant: ```rust // The render function is only called for visible items v_virtual_list( cx.entity().clone(), "efficient-list", item_sizes.clone(), |view, visible_range, _, cx| { // visible_range contains only the items currently visible // This typically contains 10-20 items, not all 10,000 println!("Rendering {} items out of {}", visible_range.len(), view.total_items); visible_range .map(|ix| { // Complex rendering logic here // Only executed for visible items expensive_item_renderer(ix, cx) }) .collect() }, ) ``` ### Memory Management VirtualList automatically manages memory by: * Only rendering visible items * Reusing rendered elements when scrolling * Calculating precise visible ranges ```rust // Large dataset - only visible items use memory let large_dataset = (0..1_000_000).map(|i| format!("Item {}", i)).collect(); // Memory usage remains constant regardless of dataset size v_virtual_list(/* render only visible items */) ``` ### Variable Heights with Caching For dynamic content with calculated heights: ```rust struct DynamicItem { content: String, calculated_height: Option, } impl MyView { fn calculate_item_size(&mut self, ix: usize) -> Size { if let Some(height) = self.items[ix].calculated_height { return size(px(300.), height); } // Calculate height based on content let content_lines = self.items[ix].content.lines().count(); let height = px(20. + content_lines as f32 * 16.); // Cache the calculated height self.items[ix].calculated_height = Some(height); size(px(300.), height) } } ``` ## Examples ### File Explorer with Virtual Scrolling ```rust pub struct FileExplorer { files: Vec, item_sizes: Rc>>, scroll_handle: VirtualListScrollHandle, selected_index: Option, } impl FileExplorer { fn calculate_item_heights(&mut self) { let sizes = self.files.iter().map(|file| { // Different heights for different file types let height = match file.file_type { FileType::Directory => px(40.), FileType::Image => px(60.), // Larger for thumbnails FileType::Document => px(35.), _ => px(30.), }; size(px(400.), height) }).collect(); self.item_sizes = Rc::new(sizes); } } impl Render for FileExplorer { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_virtual_list( cx.entity().clone(), "file-list", self.item_sizes.clone(), |view, visible_range, _, cx| { visible_range .map(|ix| { let file = &view.files[ix]; let is_selected = view.selected_index == Some(ix); div() .w_full() .h(view.item_sizes[ix].height) .px_3() .py_1() .flex() .items_center() .gap_2() .bg(if is_selected { cx.theme().accent } else { Color::transparent() }) .hover(|style| style.bg(cx.theme().secondary_hover)) .child(file_icon(&file.file_type)) .child(file.name.clone()) .child( div() .flex_1() .text_right() .text_xs() .text_color(cx.theme().muted_foreground) .child(format_file_size(file.size)) ) .on_click(cx.listener(move |view, _, _, cx| { view.selected_index = Some(ix); cx.notify(); })) }) .collect() }, ) .track_scroll(&self.scroll_handle) } } ``` ### Chat Messages with Auto-scroll ```rust pub struct ChatWindow { messages: Vec, scroll_handle: VirtualListScrollHandle, auto_scroll: bool, } impl ChatWindow { fn add_message(&mut self, message: ChatMessage, cx: &mut Context) { self.messages.push(message); // Recalculate item sizes self.update_item_sizes(); if self.auto_scroll { // Scroll to bottom for new messages self.scroll_handle.scroll_to_bottom(); } cx.notify(); } fn update_item_sizes(&mut self) { let sizes = self.messages.iter().map(|msg| { // Calculate height based on message content let lines = msg.content.lines().count().max(1); let height = px(40. + (lines.saturating_sub(1)) as f32 * 16.); size(px(350.), height) }).collect(); self.item_sizes = Rc::new(sizes); } } impl Render for ChatWindow { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .size_full() .child( v_virtual_list( cx.entity().clone(), "chat-messages", self.item_sizes.clone(), |view, visible_range, _, cx| { visible_range .map(|ix| { let msg = &view.messages[ix]; div() .w_full() .px_4() .py_2() .child( v_flex() .gap_1() .child( h_flex() .justify_between() .child( div() .text_sm() .font_weight(FontWeight::SEMIBOLD) .child(msg.author.clone()) ) .child( div() .text_xs() .text_color(cx.theme().muted_foreground) .child(format_timestamp(msg.timestamp)) ) ) .child( div() .text_sm() .child(msg.content.clone()) ) ) }) .collect() }, ) .track_scroll(&self.scroll_handle) .flex_1() ) .child( // Chat input at bottom div() .w_full() .h(px(60.)) .border_t_1() .border_color(cx.theme().border) .child("Chat input here...") ) } } ``` ### Data Grid with Fixed Headers ```rust pub struct DataGrid { headers: Vec, data: Vec>, column_widths: Vec, scroll_handle: VirtualListScrollHandle, } impl Render for DataGrid { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .size_full() .child( // Fixed header h_flex() .w_full() .h(px(40.)) .bg(cx.theme().secondary) .border_b_1() .border_color(cx.theme().border) .children( self.headers.iter().zip(&self.column_widths).map(|(header, &width)| { div() .w(width) .h_full() .px_3() .flex() .items_center() .font_weight(FontWeight::SEMIBOLD) .child(header.clone()) }) ) ) .child( // Virtual list for data rows v_virtual_list( cx.entity().clone(), "data-rows", Rc::new(vec![size(px(800.), px(32.)); self.data.len()]), |view, visible_range, _, cx| { visible_range .map(|row_ix| { h_flex() .w_full() .h(px(32.)) .border_b_1() .border_color(cx.theme().border.opacity(0.5)) .children( view.data[row_ix].iter().zip(&view.column_widths).map(|(cell, &width)| { div() .w(width) .h_full() .px_3() .flex() .items_center() .child(cell.clone()) }) ) }) .collect() }, ) .track_scroll(&self.scroll_handle) .flex_1() ) } } ``` ## Best Practices 1. **Item Sizing**: Pre-calculate item sizes when possible for best performance 2. **Memory Management**: Use VirtualList for any list with >50 items 3. **Scroll Performance**: Avoid heavy computations in render functions 4. **State Management**: Keep item state separate from rendering logic 5. **Error Handling**: Handle edge cases like empty lists gracefully 6. **Testing**: Test with various data sizes and scroll positions ## Performance Tips 1. **Pre-calculate Sizes**: Calculate item sizes upfront rather than during render 2. **Minimize Re-renders**: Use stable item keys and avoid recreating render functions 3. **Batch Updates**: Group multiple data changes together 4. **Efficient Rendering**: Keep item render functions lightweight 5. **Memory Monitoring**: Monitor memory usage with very large datasets --- --- url: /zh-CN/docs/components/virtual-list.md description: 用于大数据集渲染的高性能虚拟列表组件,支持可变尺寸项。 --- # VirtualList VirtualList 是一个面向大规模数据集的高性能列表组件。它只渲染当前可见区域的项目,因此非常适合长列表、动态高度内容以及类似表格的复杂布局。 与普通均匀列表不同,VirtualList 支持每一项拥有不同尺寸。 ## 导入 ```rust use gpui_kit::component::{ v_virtual_list, h_virtual_list, VirtualListScrollHandle, scroll::{Scrollbar, ScrollbarState, ScrollbarAxis}, }; use std::rc::Rc; use gpui_kit::{px, size, ScrollStrategy, Size, Pixels}; ``` ## 用法 ### 基础纵向列表 ```rust pub struct ListViewExample { items: Vec, item_sizes: Rc>>, scroll_handle: VirtualListScrollHandle, } ``` ```rust v_virtual_list( cx.entity().clone(), "my-list", self.item_sizes.clone(), |view, visible_range, _, cx| { visible_range .map(|ix| { div() .h(px(30.)) .w_full() .bg(cx.theme().secondary) .child(format!("Item {}", ix)) }) .collect() }, ) .track_scroll(&self.scroll_handle) ``` ### 横向列表 ```rust h_virtual_list( cx.entity().clone(), "horizontal-list", item_sizes.clone(), |view, visible_range, _, cx| { visible_range .map(|ix| { div() .w(px(120.)) .h_full() .bg(cx.theme().accent) .child(format!("Card {}", ix)) }) .collect() }, ) ``` ### 可变尺寸项 ```rust let item_sizes = Rc::new( (0..1000) .map(|i| { let height = if i % 5 == 0 { px(60.) } else if i % 3 == 0 { px(45.) } else { px(30.) }; size(px(300.), height) }) .collect::>() ); ``` ## 滚动控制 ### 基础滚动 ```rust pub struct ScrollableList { scroll_handle: VirtualListScrollHandle, scroll_state: ScrollbarState, } ``` ### 编程式滚动 ```rust impl ScrollableList { fn scroll_to_item(&self, index: usize) { self.scroll_handle.scroll_to_item(index, ScrollStrategy::Top); } fn center_item(&self, index: usize) { self.scroll_handle.scroll_to_item(index, ScrollStrategy::Center); } fn scroll_to_bottom(&self) { self.scroll_handle.scroll_to_bottom(); } } ``` ### 双轴滚动 ```rust Scrollbar::both(&scroll_state, &scroll_handle) .axis(ScrollbarAxis::Both) ``` ## 性能说明 VirtualList 的核心优势在于: * 只渲染可见范围内的项 * 滚动时复用已渲染元素 * 数据量很大时内存占用仍然稳定 因此只要列表项超过几十个,尤其是达到上百上千个时,就值得优先考虑 VirtualList。 ## 示例 ### 文件浏览器 ```rust pub struct FileExplorer { files: Vec, item_sizes: Rc>>, scroll_handle: VirtualListScrollHandle, selected_index: Option, } ``` ### 聊天窗口 ```rust pub struct ChatWindow { messages: Vec, scroll_handle: VirtualListScrollHandle, auto_scroll: bool, } ``` ### 固定表头的数据网格 ```rust pub struct DataGrid { headers: Vec, data: Vec>, column_widths: Vec, scroll_handle: VirtualListScrollHandle, } ``` ## 最佳实践 1. 尽量预先计算 item 尺寸 2. 渲染函数里避免重计算 3. 列表项数量超过 50 时优先考虑 VirtualList 4. 将项状态与渲染逻辑拆开管理 5. 测试不同数据量和滚动位置下的表现 --- --- url: /DESIGN.md --- # Website Design Design rules for the documentation site in `website/`. It exists so later changes stay coherent instead of drifting back into a generic template. The site is the first thing a visitor sees of a library that renders native desktop UI. So it has one job before anything else: make it obvious, in a single screen, what this project is — and then prove it is real. ## Principles 1. **Say what it is, immediately.** The headline states the product in plain language ("Build fantastic, high-performance desktop apps."). Positioning that only makes sense to someone who already knows the project — architecture layers, "ship fast or own everything" — belongs further down the page, not in the hero. 2. **Show the real thing, never a mock.** Component documentation embeds live WebAssembly examples compiled from the same Rust sources as the native examples. Homepage capability previews are deliberately diagrams, not fake product screenshots. 3. **Verifiable facts over adjectives.** Star count, licence, platforms, real type names (`DockArea`, `Rope`, `Tiles`), real numbers from the README (120 FPS, 200K lines). A developer judges credibility from specifics. 4. **Same palette and typeface as the library.** Colours come from `crates/component/src/theme/default-theme.json`; code colours come from the same shiki theme the docs use; the type is the platform font, as in a real app. The site must not invent a look the components cannot produce. 5. **Restraint carries the design.** Hierarchy comes from type scale, weight and hairlines — not from colour fills or decorative gradients. ## Colour Defined in `.vitepress/theme/style.css`, mapped from the default theme so the site and the documented components share one palette. | Token | Light | Dark | Theme source | | -------------------- | --------------------- | ------------------------------ | ----------------------------------------------- | | `--background` | `#ffffff` | `#0a0a0a` | `background` | | `--foreground` | `#0a0a0a` | `#fafafa` | `foreground` | | `--border` | `#e5e5e5` | `#262626` | `border` | | `--secondary` | `#f5f5f5` | `#262626` | `secondary.background` | | `--muted-foreground` | `#737373` | `#a3a3a3` | `muted.foreground` | | `--sidebar` | `#fafafa` | `#0f0f0f` | `sidebar.background` | | `--titlebar` | `#f8f8f8` | `#171717` | `title_bar.background` | | `--brand` | `#171717` | `#fafafa` | `primary.background` | | `--data-1…5` | `#93c5fd` → `#1e40af` | blue scale, keyed by `#419cff` | `chart_1…chart_5` | | logo accent | `#3b82f6` | `#419cff` | light `chart_2` / dark syntax link and tag blue | | `--selection` | `#55a0fc` | same | `selection.background` | | `--success` | `#22c55e` | same | `success.background` | | `--code-*` | macos-classic-light | macos-classic-dark | `src/*.theme.json` | Rules that follow from this: * **The brand colour is near-black (near-white in dark mode).** It is used for primary buttons, focus rings and the active sidebar indicator — never as an "accent" to add interest, because it is the same value as body text. Section kickers and captions use `--muted-foreground` instead. * **Never use `--brand` as a background behind text you did not also invert.** Text selection in particular uses `--selection`: black text on a near-black selection is unreadable. * **Saturated colour is reserved for data**, exactly as the theme reserves `chart_*`. The capability diagrams and charts may use `--data-*`; marketing surfaces may not. * **The logo accent is per-theme**: `#3b82f6` (`chart_2`) on light and `#419cff` (the dark syntax theme’s link and tag blue) on dark, so the mark follows the code palette shown on the same background. The mark is split into two paths — an open `C`, and the bar-and-stem that turns it into a `G` — so that stroke can carry the accent while the rest stays neutral. The values are baked into `public/logo.svg` and `logo-dark.svg`; making them follow a token would mean rendering the mark inline instead of as an image. In-page diagrams keep using `--data-2` and are unaffected. * **`--success` marks "running"** — the live WASM indicator and the example badge — where near-black would not read as a live signal. * **Hand-written code snippets use the `--code-*` tokens**, which are lifted from the same macos-classic theme shiki applies in the docs. Never invent highlighting for a snippet. * Links are distinguished by a rule, not a hue, since the brand colour equals the text colour. See `.vp-doc a`. ## Typography The platform font, because that is what the library itself renders with: SF Pro on macOS, Segoe UI Variable on Windows, with `Noto Sans SC` / `PingFang SC` for Chinese. Monospace prefers `ui-monospace` / SF Mono and falls back to `JetBrains Mono`. No webfont is downloaded for body text. Base size 15px. * **Display** — `clamp(2.2rem, 4.3vw, 3.6rem)`, weight 660, tracking `-0.042em`. * **Section heading** — `clamp(2rem, 3.6vw, 3rem)`, weight 660, tracking `-0.045em`. * **Body** — 1rem, line-height 1.7; docs prose is capped at `46rem`. * **Kicker / label** — 0.66–0.68rem mono, uppercase, wide tracking, muted. Small mono labels, not colour, mark structure. Two constraints that are easy to get wrong: * **Negative tracking and wide tracking are for Latin only.** CJK glyphs sit on a fixed em grid, so `html[lang^="zh"]` resets body tracking and reduces the kicker's letter-spacing. Do not apply Latin tracking globally. * **Numerals use `tabular-nums`** in tables, keys and counters so they do not reflow when values change. ## Layout * Page width `1280px`, gutter `1.5rem` (`1rem` under 640px). * **One container for every band.** Each `
` under `.home` is full-bleed with a `border-top` hairline, and its content sits in `.band__inner`, which owns the width and the `--section-gap` vertical padding. Sections must butt directly against each other — the hairline is the only separator. A stray margin between two sections is a bug; verify the gap is `0`. * Nav, hero, every band and the footer must resolve to the **same left edge and width** (x=80, w=1280 at 1440px). Check this after any layout change. * Navigation is a **toolbar**: brand, a hairline divider, then the sections on the left; search, stars, language and appearance collected on the right. Height 3.5rem/56px. The docs navbar is the same toolbar — VitePress's own navbar is reordered by CSS, and the language and star controls are injected through the `nav-bar-content-after` slot rather than living in `nav` items, so the real search keeps working. The controls are that one set at every width: VitePress would otherwise move the appearance switch into a `…` flyout on mid-size screens and into the hamburger screen on phones. * The docs navbar's blur belongs on `.VPNavBar`, never on `.VPNav`. `.VPNav` wraps both the bar and the phone menu screen, and a `backdrop-filter` makes its element the containing block for fixed descendants — on `.VPNav` it collapses the screen, which is anchored `top: nav-height; bottom: 0`, to zero height, and the hamburger opens onto nothing. * The hero is two columns: copy, and a macOS window holding a real snippet from the Quick Start guide. Its vertical rhythm is 20 / 20 / 24 / 24 / 20 px. * Pages that are the site's own surfaces — App Stories, Skills, Contributors, Releases — use `layout: home` to leave the docs shell, and a shared rule in `style.css` gives their content the same top offset as a docs page. VitePress pads only the hero it would render there, so without that rule a title sits against the toolbar. A page must not add its own offset on top. * Live WASM examples belong to their component documentation, next to the API and guidance they demonstrate. The homepage links into that documentation instead of maintaining a separate gallery surface. * Cards in a row must align internally, not just at their outer edges: absorb the slack after the description (`margin-bottom: auto`, or `min-height` when two cards must match) so chips, snippets and previews line up. Equal-length code snippets are part of that contract. * Grid backgrounds are hairline blueprint grids masked to fade at the edges. No colour wash behind headlines. * Check for horizontal overflow at phone widths. Grid columns must be `minmax(0, 1fr)` — a bare `1fr` takes its minimum from content, and a `nowrap` snippet will push the page wider than the viewport. * Under 640px the section links are hidden, so they move into a drawer behind a burger button in the header. Anything removed at a breakpoint needs somewhere else to live — hiding it outright is a dead end for phone users. The window title is dropped there too, since the segmented control already names the current view. * The docs navbar collapses on VitePress's own 768px breakpoint, and the same rule applies: the sections and the language menu move into the hamburger screen (`nav-screen-content-after`), while the star count is only a label beside an icon that stays. A 360px phone leaves about 180px next to the title, which is four 2rem controls — measure before adding a fifth. ## Surfaces and the window language Anything showing the library running is framed as a **macOS window** — the `.mac-window` class in `style.css`. It is the closest visual analogue to what the library actually produces, so it reads as a native application rather than a screenshot card. The frame is: a hairline outer stroke, an inner top highlight, layered soft shadows, and real traffic lights (`#ff5f57`, `#febc2e`, `#28c840`). The title is centred and independent of the lights, as macOS does. Used by the hero snippet and `ComponentExample.vue` on every component page, so the two never diverge. **Do not put document tabs inside the window chrome.** A tab strip in the titlebar fights the traffic lights, and a browser-style tab row below it is not how gpui-component presents views. View switching uses the library's own **segmented control** (`.segmented`, mapped from `tab_bar.segmented.background` and `tab.active.background`), placed in the section heading — outside the window, which stays pure chrome. Radii: `--radius-control` 0.375rem for controls, `--radius-card` 0.625rem for cards, `--radius-surface` 0.875rem for large surfaces, 0.75rem for windows. ## Social card `public/og.png` is the Open Graph / Twitter card for every page. It is drawn at 1200×630 and stored at 2400×1260, because Slack and the other unfurlers re-encode the image at their own preview size and a 1× source arrives soft on a high-density screen. `og:image:width` and `og:image:height` state the stored size. `public/og-dark.png` is the same card on the dark palette — social platforms cannot choose by theme, so it is not referenced from any meta tag; it exists for surfaces that can, such as a `` in the GitHub README. Both are screenshots of `public/og-template.html`, which holds both palettes in one file behind `prefers-color-scheme`, so the two renders cannot drift. To regenerate: serve the site, open the template at a 1200×630 viewport with `deviceScaleFactor: 2`, and capture once per colour scheme. The template pins its own layout viewport to 1200px, so the capture frames the card exactly. The card carries **identity only** — mark, product name, tagline, and three facts that do not expire. It must not carry a headline: the platform draws the page's own `og:title` and description beside the image, and a baked-in headline competes with them. For the same reason the facts avoid the star count, which a static image cannot keep current. Its content is shifted toward the top of the frame, leaving the bottom clear for source labels and other overlays added by social clients. Per-page `og:title`, `og:description`, `og:url` and the canonical link come from `transformPageData` in `config.mts`. Note it uses `||`, not `??`: the home page supplies empty strings rather than undefined. A per-page image would need a server to render it — shadcn and reui use a dynamic `/og?title=` route — and GitHub Pages has none, so one shared image is the right trade. ## Motion Entrances only, and short: `rise` at 620ms on `cubic-bezier(.16, 1, .3, 1)`, staggered 70ms. Live indicators use a slow 2.4s pulse. Everything sits inside `@media (prefers-reduced-motion: no-preference)`; nothing conveys meaning through motion alone. ## Content rules * The crate is **not published**. Installation must show the git dependency, never `cargo add`, and the UI must not display a version number. * Code samples must be real API, verified against `crates/component` and `crates/base`. * The release notes page is rendered at build time from GitHub Releases (`data/releases.data.js`) by the docs markdown pipeline; it holds no copy of its own, so a release is written once, on GitHub. * Capability copy tracks the README's feature list — 120 FPS rendering, complex data tables, virtualized lists, the 200K-line editor, freeform docking, multi-theme support. Update it when the README's features change. * Capability previews are **diagrams**, not product mocks: they share one padding box and one gap, and they may use `--data-*` to read as UI. A diagram that needs a scrollbar needs its track too, or it looks like a glitch. * Landing-page copy lives in one bilingual `copy` object in `index.vue`. Both locales must be updated together, matching the site-wide rule that `website/docs/` and `website/zh-CN/docs/` stay in sync. * Live examples should demonstrate the documented component's real behavior and use the same source as its native example. ## App Stories `apps.vue` lists the applications people have shipped with the library — the strongest available answer to "is this real?", and the reason it sits in the navbar rather than inside the Resources menu. Submissions come from [discussion #989](https://github.com/longbridge/gpui-kit/discussions/989). * **The screenshots are the authors' own published GitHub URLs**, used as submitted. They must not be wrapped in `.mac-window`: most already contain a real titlebar, and a second set of traffic lights around one turns a real screenshot into a mock. A hairline frame and one shared 16:10 crop, anchored to the top, is what makes a row of them align. * **Order is the only ranking.** It reads as editorial judgement — how complete and shipped an app is first, GitHub traction second — and is maintained by hand when an entry is added. * **No star counts.** A hard-coded number goes stale, and resolving one repository per app at build time would exhaust the unauthenticated GitHub rate limit that `repo.data.js` already draws on. The facts a card does carry — platforms, open source or commercial, whether it is still in development — do not expire. * Copy for both locales lives in one `copy` object plus a per-app `blurb: { en, zh }`, so an entry cannot be added in one language only. ## Files | File | Role | | -------------------------------------------------- | ----------------------------------------------------------------------- | | `.vitepress/theme/style.css` | Tokens, `.mac-window`, VitePress overrides, doc typography | | `index.vue` | Landing page: markup, bilingual copy, page-scoped styles | | `apps.vue` | App Stories page: the showcase list, bilingual copy, page-scoped styles | | `.vitepress/theme/index.ts` | Theme entry; injects nav controls and the example window | | `.vitepress/theme/components/ComponentExample.vue` | Windowed live example on component pages | | `.vitepress/config.mts` | Navigation, sidebar generation, locales | | `src/*.theme.json` | shiki syntax themes; the source of `--code-*` | ## Attribution The footer is the one place the site states GPUI's origin, in one plain line: built on GPUI, from Zed Industries, also Apache-2.0. The landing page footer and the docs footer say the same thing. Do not repeat it in the hero, tutorials or API pages, where the product is GPUI Kit, and do not grow it into a disclaimer. --- --- url: /zh-CN/docs.md description: 基于 GPUI 构建出色高性能桌面应用的综合性 Rust 开发框架。 --- # GPUI Kit 简介 GPUI Kit 是一个基于 GPUI 的综合性 Rust 桌面应用开发框架。 它将完整 UI 系统与应用级数据、布局、内容和编辑能力整合在一起,并以三个层层递进的 crate 交付,只需依赖 `gpui-kit` 一个包即可全部获得: * **`gpui-base`**:无样式的行为、受控状态、焦点、浮层、虚拟列表、Dock 基础设施与语义化设计 token。 * **`gpui-component`**:即 GPUI Component,完整的带样式组件库,提供 60+ 控件、主题、数据表格、Dock 布局和代码编辑器。 * **`gpui-shell`**:让 Rust 宿主可以被 JavaScript 扩展,能力逐项授予。 使用 `gpui-component` 可以获得统一、成熟的视觉风格;基于 `gpui-base` 则可以复用可靠的行为与基础设施,同时创建并拥有自己的设计系统。本节文档面向 GPUI Component;另外两层请参阅 [GPUI Base](/zh-CN/base/) 与 [GPUI Shell](/zh-CN/shell/)。 ## 特性 * **60+ 组件**:覆盖表单、导航、浮层、反馈和布局等场景 * **生产就绪**:从第一天起用于构建 Longbridge Pro,并在公开发布的商业桌面应用中持续打磨 * **原生体验**:设计灵感来自 macOS 与 Windows 的现代桌面控件 * **120 FPS**:GPU 加速界面,在高负载下依然保持流畅 * **数据表格**:虚拟滚动、固定列、列宽调整、排序与单元格选择,可承载数十万行数据 * **虚拟列表**:只渲染可见区域,并支持不同尺寸的列表项 * **代码编辑器**:20 万行、Tree-sitter 高亮、诊断、补全和悬浮提示 * **Dock 布局**:可调整面板、可拖拽标签、嵌套分割、边缘停靠和 Tiles 自由布局 * **丰富内容**:原生 Markdown 与 HTML、语法高亮和图表 * **设计自由**:使用完整视觉系统,或基于 `gpui-base` 构建自己的系统 * **类型化动效**:CSS 对齐的 easing、timing、keyframes、spring、presence 与测量式展开,稳定采样路径零分配 * **跨平台**:通过一份 Rust 代码交付 macOS、Windows 和 Linux ## 下一步 * 阅读 [开始使用](./getting-started) * 浏览 [组件文档](./components/index) * 阅读 [GPUI Base 动画与动效](/zh-CN/base/motion) --- --- url: /zh-CN/shell/dependencies.md description: shell package——什么样的 Git 仓库算一个,以及 manifest 如何命名、选择版本、抓取与导入它,直到编辑器看见它。 --- # 依赖 应用用相对路径 import 自己的文件。除此之外,它写下的每一条 import 只有两个来源:运行时提供的**内建模块**——`gpui-kit`、`gpui-base`、`gpui-shell`、`gpui-fps`,以及标准运行时的 `fs/promises`、`path`、`crypto`、`net`、`websocket`——或者一个**依赖**:manifest 声明、gpui-shell 在 entry module 求值之前从 Git 抓取的 JavaScript package。 这里没有 registry,没有包管理器,也没有安装步骤。一个依赖就是一个 Git remote、一个 ref,加上脚本 import 它时用的名字。 ## Shell package 依赖可以是 manifest 指向的任何一个 Git 仓库。`omarchy-ui` 属于其中一类,而这一类有自己的名字:**shell package**——为 gpui-shell 而写、而不是为 Node 或浏览器而写的 JavaScript package,就像 crate 是为 Cargo 而写的一样。五件事决定一个仓库是不是 shell package: | shell package | 原因 | | ----------------------------------------------------------- | ------------------------------------------------------- | | 发布 ES module,且不需要构建步骤 | 运行时直接求值 checkout 里的文件,而且 `require` 不存在 | | 根目录 `package.json` 带 `"type": "module"` 与 `main` | 它让声明只需一行,并同时向运行时和编辑器指明 entry | | 只 import 内建模块与自己的文件 | 其余的都解析不了——它无法反向伸回 import 它的应用 | | 把 `gpui-kit` 与 `gpui-base` 当作由环境提供,而非自己的依赖 | 它们来自加载它的运行时,版本由 Host 决定 | | 不声明任何属于自己的 capability | 它的 `fs` 与 `fetch` 调用都跑在使用方应用的授权之下 | 这个名字不会被任何代码读取:依赖是靠被声明来识别的,不是靠被贴标签。它的作用是让一个作者写下、另一个作者找到;写给搜索引擎的那一份,是仓库上的 `gpui-shell` topic。 [`omarchy-ui`](https://github.com/huacnlee/omarchy-ui) 就是一个 shell package,本页余下部分都以它为例。 ## 声明一个依赖 `omarchy-ui` 是一个提供展示组件与主题工具的 shell package。在 `gpui-shell.json` 里加一行就够了: ```json { "id": "com.example.projects", "name": "Projects", "entry": "main.js", "dependencies": { "omarchy-ui": "huacnlee/omarchy-ui" } } ``` map key 就是脚本使用的裸模块名——package 内部并不参与决定它。是 manifest 给 package 起名,就像 import 的 `as` 子句那样:两个应用可以用不同名字引用同一个 remote,而仓库改名也不会改变 import: ```js import { AppShell, Button, CenteredWorkspace, MutedText, PageColumn, Surface, Title, } from "omarchy-ui"; export function render(cx) { const card = new Surface() .children([ new Title("Projects").build(cx), new MutedText("Choose a project to continue").build(cx), new Button("project-create") .label("Create project…") .onClick((_event, context) => context.notify()) .build(cx), ]) .build(cx); const page = new PageColumn("projects-page").child(card).build(cx); return new AppShell() .content( new CenteredWorkspace("projects-workspace").content(page).build(cx), ) .build(cx); } ``` 其他什么都不用改。脚本还是[快速开始](./getting-started.md)里的那个脚本,依赖只是拓宽了它能 import 的范围。 ## 什么能解析,解析到哪里 | 写法 | 解析结果 | | ------------------------------------- | ---------------------------------------------------- | | `"omarchy-ui"` | package entry——见 [package entry](#package-entry) | | `"omarchy-ui/src/style"` | checkout 内的该文件,`.js` 后缀可省略 | | package 内部的 `"./theme.js"` | 该 package 自己 checkout 内的文件 | | package 内部的 `"gpui-kit"` | 内建模块,与应用代码中完全一致 | | 另一个已声明的依赖名 | 那个 package 的 entry——已声明的 package 之间互相可见 | | 从 package 内部用裸名 import 应用文件 | 拒绝:package 不能反向伸回 import 它的应用 | 解析结果一旦离开起点所在的 checkout,就会在模块加载前被拒绝,因此 `../` 无法走出一个 package、进入旁边的缓存。在应用目录内,同一条边界就是应用根目录,也正是[沙箱](./capabilities.md#沙箱)对相对 import 已有的规则。 **依赖不是第二层沙箱。** 它在应用自己的上下文里求值,持有的授权与 manifest 完全相同:package 读文件,用的就是你的 `fs.read` 范围。声明一个依赖,等于像引入一个 Rust crate 那样信任它的代码,而你钉住的 ref 决定了那究竟是哪一份代码。 ## 选择版本 字符串形式是严格的 GitHub 简写或完整 Git URL,两者都可带可选的 `#ref`: ```json { "dependencies": { "default-main": "huacnlee/omarchy-ui", "named-ref": "huacnlee/omarchy-ui#v1.2.0", "commit": "https://github.com/huacnlee/omarchy-ui#0123456789abcdef0123456789abcdef01234567", "remote-head": "https://github.com/huacnlee/omarchy-ui" } } ``` | 形式 | 选中 | | -------------------------- | ---------------------------- | | `owner/repository` | `main` | | `owner/repository#ref` | 该 branch、tag 或 commit-ish | | `https://…/repository` | remote 的 `HEAD` | | `https://…/repository#ref` | 该 branch、tag 或 commit-ish | 简写形式故意收得很紧:恰好一组 `owner/repository`,字符限于字母数字与 `.`、`-`、`_`,最多一个 `#`,前后不能有空白,fragment 必须是合法的 Git ref 名。其余一律作为 manifest 错误,而不是从一个笔误里猜出 URL。完整 URL 可以是任意 Git 传输方式,包括 `ssh://` 与 `git@host:owner/repo`。 **branch、tag 或 remote `HEAD` 在每次加载应用时都会重新 fetch 并解析;commit ID 永远选中同一个 commit。** 依赖一个 branch,意味着下次开窗时代码会在你脚下变化——这在你自己开发 package 时很方便,而在你发布之后就是一个供应链决定。凡是不由你掌控的东西,请钉住 tag 或 commit。 抓取需要 Host 的 `PATH` 上有 `git`,并且发生在脚本 capability 存在之前——这是 gpui-shell 代表应用运行 Git,不是脚本在访问网络,因此它不受 `capabilities.network` 管辖,也不需要 `fs.execute`。当没有东西要抓(缓存里已有该 commit)时,这次加载完全不产生网络访问;而当依赖是移动 ref 且当前没有网络时,fetch 失败,应用不会加载。 ## Package entry checkout 就绪后,gpui-shell 读取 package 根目录的 `package.json`,把字符串类型的 `main` 作为 entry。`omarchy-ui` 发布的是: ```json { "type": "module", "main": "src/index.js", "types": "src/index.d.ts" } ``` 于是 `import { Title } from "omarchy-ui"` 求值的是 `src/index.js`。没有 `package.json`,或者其中没有 `main` 时,entry 是根目录的 `index.js`。 运行时读 `main`,编辑器读 `types`。两者出自同一个文件——这正是为什么一个附带 `.d.ts` 的 package 不需要应用做任何事,就能在调用处拥有完整类型。 JSON 格式错误、非字符串 `main`,以及缺失、不是文件或逃出 checkout 的 entry,都会在应用 JavaScript 执行前令加载失败。 ## Object 形式 最初的 object 形式保持完全兼容。它必须显式且只指定一个 `branch` 或 `tag`,其 repository 相对的 `entry` 默认是 `index.js`: ```json { "dependencies": { "omarchy-ui": { "git": "https://github.com/huacnlee/omarchy-ui", "tag": "v1.2.0", "entry": "src/index.js" } } } ``` 现有 manifest 无需迁移。改用字符串形式,意味着由 package 通过 `package.json` 的 `main`(或根目录 `index.js`)自己发布 entry,而不是让每个使用方各写一遍。 ## 缓存 ```text ~/.gpui-shell/cache/dependencies/ ├── locks/.lock ├── mirrors/.git └── checkouts/// ``` `` 是去掉 fragment 后完整 URL 的 SHA-256,它既是 remote 的身份,也是缓存的身份。每个 remote 一把锁,串行化 mirror 更新。checkout 按 commit 寻址且从不改写,因此并发启动与旧的 hot-reload generation 各自读到自己开始时的那棵树。 每次使用都会拿 mirror 的 configured origin 与 manifest 校验,且比较的是原始配置值——Git 的 `url.*.insteadOf` 仍可以选择另一个实际 fetch URL,镜像站或企业内网替换因此照常可用。Git 以非交互方式运行,禁用凭据提示,每条命令限时 30 秒,于是一个要求输入密码的仓库会给出错误信息,而不是把一个正等着打开的窗口挂在那里。 这份缓存不会被自动清理。它是内容寻址的,所以直接删掉是安全的:下次加载会重新抓取需要的部分。 ## 编辑器看见的东西 运行时靠 manifest 回答 `import { Title } from "omarchy-ui"`。编辑器则是从 import 所在文件向上查找 `node_modules`,它从来没听说过 `gpui-shell.json`。放着不管,一条正确的 import 会被标红成找不到的模块,它背后的每个名字也随之失去类型、参数提示和文档。 因此每次加载——以及 `gpui-shell types`——都会把 materialize 出来的 checkout 按 manifest 给的名字链接进应用的 `node_modules`: ```text projects/ ├── gpui-shell.json ├── main.js ├── gpui-kit.d.ts 运行时生成——请忽略 ├── jsconfig.json 只生成一次,之后归你 └── node_modules/ └── omarchy-ui → ~/.gpui-shell/cache/dependencies/checkouts// ``` 这样编辑器读到的,就是运行时即将执行的同一批文件,它显示的签名与 JSDoc 都来自 package 自身,不会与实际运行的代码脱节。 只有 gpui-shell 自己写下的条目会被替换或删除——指向自身依赖缓存的 symlink,或带有它标记文件的目录。同名的已安装 package 不会被动到;manifest 中已移除的依赖,其链接也会一并清除。若平台拒绝创建 symlink(例如未开启开发者模式、权限不足的 Windows 进程),gpui-shell 改为写入一个转发该 checkout 的小 package:裸 import 的类型效果相同,只有 package subpath import 无法解析。 当目录里既没有 `jsconfig.json` 也没有 `tsconfig.json` 时会生成一份 `jsconfig.json`,且只生成一次——已有的配置永远不会被替换。它不是装饰:靠推断得到的 `moduleResolution` 可能落到那种从不查看 `node_modules` 的解析方式,把运行时明明能解析的依赖标红;而默认的 `lib` 会把浏览器的全局对象塞给脚本,它们的声明与 `gpui-kit.d.ts` 产生冲突,于是描述 API 的那个文件本身反倒被报成错误。 `node_modules` 和 `gpui-kit.d.ts` 一样属于生成物,两者都应加入忽略列表: ```text gpui-kit.d.ts node_modules/ ``` 这个目录之所以叫 `node_modules`,是因为所有编辑器只认这一个位置;这里没有包管理器参与,也没有任何东西来自 registry。这个名字还换来了安静:TypeScript 会把从这里解析到的内容视为 external library,于是依赖自身的 implicit-`any` 之类诊断不会混进你自己的诊断里。 ## 抓取与链接何时发生 | 调用 | 抓取并链接 | 失败时 | | ----------------------------------------------- | ---------- | ------------------------------------ | | `gpui-shell ` | 是 | 加载失败;仅链接这一步是 best-effort | | `gpui-shell check ` | 是 | 作为 check 失败报告 | | `gpui-shell types ` | 是 | 报告错误,并带非零退出码 | | 嵌入式 Host 的 `ShellRuntime::load` | 是 | 加载失败;仅链接这一步是 best-effort | | `gpui_kit::shell::write_dependency_links(root)` | 是 | 以 error 返回给调用方 | 加载依赖抓取,所以无法 materialize 的依赖会让加载失败。写编辑器链接则不然:只读的应用目录是失去编辑器类型的理由,不是拒绝运行的理由。`gpui-shell types` 存在的意义正是这个差别——它做同样的事,并把没能完成的部分报出来。 hot-reload 对待 package 的方式与对待应用文件一致:每次加载都是一个新的 module generation,所以重启应用就足以让一个 branch 依赖前进到新的 commit。 ## 可能的失败 以下每一种都在应用 JavaScript 求值之前报告: | 信息 | 原因 | | ---------------------------------------------------------- | -------------------------------------------------- | | `GitHub shorthand must contain exactly owner/repository …` | 简写里带了路径、scheme 或非法字符 | | `a string dependency #Git ref must not be empty` | 末尾多了一个 `#` | | `could not clone Git dependency …` | Git 失败:remote 不存在、没有凭据、没有网络 | | `git timed out after 30 seconds …` | fetch 卡住,通常是弹出了交互式凭据提示 | | `Git dependency … cache origin is …, expected …` | 两个 manifest 对同一条缓存记录不一致;删掉它再试 | | `Git dependency … package.json main must be a string` | `main` 是对象,或是逃出 checkout 的路径 | | `Git dependency … has no entry …` | `main` 或 object 形式的 `entry` 指向不存在的东西 | | `cannot resolve module … from …` | subpath import 指向不存在的文件,或离开了 checkout | ## 发布一个 shell package shell package 就是一个普通的 Git 仓库:`omarchy-ui` 没有构建产物、没有 lockfile,也没有发布步骤。在决定它是不是 shell package 的那五件事之外,让它用起来舒服的是这些: * **根目录 `package.json` 里的 `main`**,使用方只写一行、不用写 `entry`。旁边的 `"type": "module"` 让编辑器和运行时对「源码是 ES module」保持一致。 * **一个统一 re-export 公开面的 entry。** 由 `src/index.js` 决定使用方能叫出哪些名字;checkout 里的其余文件仍可通过 subpath 触达——那是有用的应急出口,却是糟糕的公开 API。 * **类型放在源码旁边**,通过 `package.json` 的 `types` 指出。生成的 `.d.ts` 与 JSDoc 都可以,两者都会顺着链接抵达调用处——使用方的 `jsconfig.json` 不需要任何 `paths` 配置。 * **为发布打 tag**,让使用方可以钉住 `#v1.2.0`,而不是跟着 `main` 走。 * **在仓库上加 `gpui-shell` topic**,让想找 shell package 的人能找到它。 ## 继续阅读 | 页面 | 内容 | | -------------------------------- | --------------------------------------------------- | | [能力](./capabilities.md) | manifest 的其余部分:身份、版本,以及脚本能触达什么 | | [快速开始](./getting-started.md) | `gpui-shell types`、`check`,以及依赖加入的那份声明 | | [API 参考](./api.md) | package 与你的代码一同 import 的内建模块 | --- --- url: /zh-CN/shell/elements.md description: 构造器、用 child / children / when 组合,以及元素描述为什么只能使用一次。 --- # Elements `gpui-shell` 里的元素是一段**描述**,不是一个对象。它只在一次渲染中存在,被使用时即被消费。本页讲能构建什么、怎么组合,以及一段描述被用了两次时运行时会做什么。 ## 构造器 每个模块只装它自己那个包提供的东西: ```js import { div, svg, image } from "gpui-kit"; import { h_flex, v_flex, Button, Link, Checkbox, Switch, Input, InputState, } from "gpui-base"; import { fps_monitor } from "gpui-fps"; ``` 函数是小写的,组件类型首字母大写并通过 `.new` 构造。这与 Rust 侧一一对应:那边 `div()` 同样是自由函数,`Button::new(id)` 同样是类型上的关联函数。 | 构造器 | 来自 | 产出 | | ------------------ | ----------- | --------------------------------------------------------------- | | `div()` | `gpui-kit` | 自身不带布局的元素 | | `value` | `gpui-kit` | 文本元素,参数会被转成字符串 | | `svg(path)` | `gpui-kit` | 来自应用自身目录、跟随主题着色的矢量图标 | | `image(path)` | `gpui-kit` | 来自应用自身目录的全彩图片 | | `h_flex()` | `gpui-base` | 一行 | | `v_flex()` | `gpui-base` | 一列 | | `Button.new(id)` | `gpui-base` | base 的 `Button`:激活、焦点、disabled 与 selected 状态,无样式 | | `Link.new(id)` | `gpui-base` | 可聚焦的外部 HTTP(S) 链接;用 `.href(url)` 设置目标 | | `Checkbox.new(id)` | `gpui-base` | base 的受控 checkbox,无样式也无勾选标记 | | `Switch.new(id)` | `gpui-base` | base 的受控 switch,无样式 | | `Input.new(state)` | `gpui-base` | 由 [`InputState`](./state.md#留存状态) 支撑的文本框 | | `fps_monitor()` | `gpui-fps` | 原生 `gpui-fps` 性能 HUD,每个窗口共享一个 monitor | 这是入门够用的一组,不是全部。base 绑定的组件——`Select`、`Combobox`、`Tabs`、`Table`、`VirtualList`、`Slider`、`Popover`、`Avatar`、`Accordion`、`Pagination`、`CalendarState` 等等——完整清单在 [API 参考](./api.md#gpui-base-模块)里。 ### 性能监视器 `fps_monitor()` 直接公开原生 `gpui-fps` HUD,不会把采样或绘制搬进 JavaScript。monitor 在首次使用时创建,并按窗口复用。一个窗口最多渲染一次,并把它放在设置了 `relative()` 的父元素中: ```js div().relative().size_full().child(content).child(fps_monitor()); ``` 默认固定在右上角。可以沿用已有的 anchor 取值调整位置,例如 `fps_monitor().anchor("bottom_left")`。HUD 自己拥有完整外观,普通元素样式、children 和交互状态不会作用于它。 ### 为什么是 `.new(id)` 而不是 `new Button(id)` JavaScript 的习惯写法是 `new Button(id)`。运行时不提供它,理由正是本页的主题:`new` 承诺的是一个有身份的对象——可以保存、可以挂在实例上、可以再次使用。而描述恰恰不是这种东西。`Button.new(id)` 读起来是“构造一段描述”,它做的也正是这件事,并且与 Rust 侧一字不差。 View 是相反的情形,用的就是标准写法:`class Counter extends View`。 View 确实有身份、有跨帧状态,并且由 GPUI 拥有。同一份文件里出现两种构造形态,是因为这两类东西的生命周期本来就不同。 ### id `Button`、`Link`、`Checkbox` 与 `Switch` 的 `id` 用于跨渲染标识元素,GPUI 据此保留焦点与元素状态。请保持它稳定,并在兄弟节点之间唯一——用 `` `item-${item.id}` ``,而不是一个会在列表被筛选时移位的数组下标。 其余元素——`div`、`h_flex`——的身份是**它在这次渲染所构建的树里所处的位置**。只要树的形状不变,这就够用;而一旦上方多出一个条件子节点,它下面的每个元素都会移位,按下状态、焦点以及其他按身份记录的东西都跟着移位。 `.id(name)` 用来说明“这是哪个元素”,而不是“它落在了哪里”: ```js div() .id("toolbar") .active((el) => el.opacity(0.7)); ``` 凡是身份必须扛得住邻居变化的元素,都给它取个名字。`Button`、`Link`、`Checkbox` 与 `Switch` 已经从 `new(id)` 拿到了身份,会忽略这里的名字——并且是给出警告,而不是默不作声。 ### 文本 **字符串本身就是元素。** GPUI 为 `&str`、`String` 与 `SharedString` 实现了 `IntoElement`,所以文本的写法就是把字符串交给承载它的元素,没有 `text()` 可调: ```js v_flex().child(`${this.remaining} of ${this.items.length} remaining`).child(42); ``` 样式由承载它的元素带,和 Rust 那边完全一致: ```js div().text_size(12).font_semibold().child("AAPL"); ``` 字符串子元素最终变成一个包含它的 `div`——这正是 `div().child(s)` 已经说明的事。 ### 图片 ```js svg("icons/check.svg").w(14).h(14).flex_none(); image("images/brand.png").w(120).h(40); ``` 两种路径都相对于**应用根目录**——也就是交给 `gpui-shell` 的那个目录——而不是相对于调用构造器的文件。这个不对称常常让人意外,所以值得直说:`import "./ui.js"` 相对于发起 import 的文件解析,和所有 JavaScript 模块系统一样;而 `svg("icons/check.svg")` 与 `image("images/brand.png")` 相对于应用根目录解析,和 Web 应用的 public 目录一样。运行时无法知道是哪个模块调用了 asset 构造器,因此按文件解析的资源路径对它并不可得。 越出应用目录的路径会被拒绝。缺失的文件会按路径去重报告一次,并附上查找位置,而不是安静地什么都不画。 单个 asset 最多 16 MiB。列举 asset tree 时最多接受 10,000 个 entry 与累计 1 MiB 的 UTF-8 文件名,避免 asset discovery 无界增长内存。 单色图标应使用 `svg()`:它会继承周围的文字颜色,所以深色按钮里的图标不用脚本说第二遍就是浅色的。Logo、照片或插画等需要保留源文件颜色的内容应使用 `image()`。 ```js renderIcon(cx) { return div() .bg(cx.theme().colors.foreground) .text_color(cx.theme().colors.surface) .child(svg("icons/check.svg").w(11).h(11)); // 以 surface 绘制 } ``` ## 组合 | 方法 | 作用 | | -------------------------- | ------------------------------------ | | `.child(element)` | 添加一个子元素,该子元素随即被消费 | | `.children(iterable)` | 按顺序添加多个 | | `.when(condition, branch)` | 仅当 `condition` 为真时应用 `branch` | ```js v_flex() .gap(8) .child(this.header()) .children(this.visible().map((item) => this.row(item))) .when(this.items.length === 0, (el) => el.child("Nothing yet")); ``` `.when` 的存在是为了不让一个条件把链断成两截。`branch` **必须返回该元素**——不返回的分支会立刻抛异常,而不是悄悄丢掉它构建的一切: ```text when(...) must return the element ``` 这与 GPUI 自己的 `FluentBuilder`,以及本仓库 Rust 侧“元素构建保持一条流式链”的风格规则同源。 如果条件是在两个元素之间二选一,普通三元表达式比 `when` 更清楚: ```js .child( visible.length === 0 ? emptyState("No items yet", "Type above and press Add.") : v_flex().children(visible.map((item) => this.row(item))), ) ``` ## 行为方法 这些不是样式。它们把状态报告给基础层,由基础层处理交互,外观仍然交给你。 | 方法 | 用于 | 作用 | | --------------------------------- | ----------------------------------- | -------------------------------------------------- | | `.on_click(handler)` | `Button` | `handler(event, cx)`,点击**以及**键盘激活都会触发 | | `.on_change(handler)` | `Checkbox`、`Switch` | `handler(checked, cx)`,由脚本保存新值 | | `.disabled(value)` | `Button`、`Checkbox`、`Switch` | 阻止激活并报告该状态 | | `.selected(value)` | `Button` | 报告 selected 状态 | | `.checked(value)` | `Checkbox`、`Switch` | 受控值 | | `.accessibility_label(text)` | `Button`、`Checkbox` | 屏幕阅读器读出的内容 | | `.tooltip(text)` | `div`、`h_flex`、`v_flex`、`Button` | 指针停留后显示的说明文字 | | `.id(name)` | `div`、`h_flex`、`v_flex` | 一个稳定的身份,取代“在树中的位置” | | `.overflow_scrollbar()` | `div`、`h_flex`、`v_flex` | 双轴滚动并绘制原生 scrollbar | | `.overflow_x_scrollbar()` | `div`、`h_flex`、`v_flex` | 水平滚动并绘制原生 scrollbar | | `.overflow_y_scrollbar()` | `div`、`h_flex`、`v_flex` | 垂直滚动并绘制原生 scrollbar | | `.on_key_down(handler)` | [可接输入的元素](#哪些元素接了输入) | 该元素持有键盘时的 `handler(event, cx)` | | `.on_key_up(handler)` | [可接输入的元素](#哪些元素接了输入) | 松开时同上 | | `.on_mouse_down(button, handler)` | [可接输入的元素](#哪些元素接了输入) | 按下 `"left"`、`"right"` 或 `"middle"` | | `.on_mouse_up(button, handler)` | [可接输入的元素](#哪些元素接了输入) | 松开 | | `.on_mouse_down_out(handler)` | [可接输入的元素](#哪些元素接了输入) | 在该元素**之外**任意位置按下 | | `.on_scroll_wheel(handler)` | [可接输入的元素](#哪些元素接了输入) | 滚轮与触控板滚动 | | `.on_action(action, handler)` | [可接输入的元素](#哪些元素接了输入) | 命名 action 被派发到它或它内部 | | `.key_context(name)` | [可接输入的元素](#哪些元素接了输入) | 该元素与其子树所处的按键绑定上下文 | disabled、selected 与 checked 的**外观**由你来画。基础层只报告状态,脚本不说就什么都不会变: ```js Button.new("clear") .disabled(this.completed === 0) .when(this.completed === 0, (el) => el.opacity(0.4)) .child("Clear completed"); ``` `.accessibility_label` 对纯图标控件最重要——没有它,这类控件什么都不会被读出来: ```js Button.new(`remove-${item.id}`) .accessibility_label(`Remove “${item.caption}”`) .child(svg("icons/trash.svg").w(14).h(14)); ``` ### 受控值只报告意图 base 的 checkbox 不会自己改状态。它只报告用户的请求,由脚本决定: ```js Checkbox.new(`item-${item.id}`) .checked(item.done) // 值来自脚本状态 .on_change((done, cx) => { // 回调只是一个请求 this.toggle(item.id, done, cx); }) .child(indicator(item.done)) .child(label(item.caption)); ``` 运行时绝不会替脚本悄悄维护一个 checked 标志。如果它这么做,脚本作者与 Rust 作者会对同一个控件持有不同的心智模型,而这两类作者共存于同一个应用里。 ### 事件对象 `on_click` 的处理函数收到的是一个普通对象,字段名与 Rust 结构一致: ```js .on_click((event, cx) => { // event.click_count === 1 // event.modifiers === { shift, control, alt, platform } }); ``` `platform` 在 macOS 上是 Command,其他平台是 Windows 键。这里只暴露基础层已经归一化过的语义——Base 把“回车激活按钮”与“点击按钮”归为同一个回调,脚本看不到这个差别。 按键处理器拿到的组合键有两种形态。`keystroke` 是整串,拼法和写绑定时一致;`key` 与 `modifiers` 是同一个组合键拆开的样子,只关心其中一半时用它: ```js .on_key_down((event, cx) => { if (event.keystroke === "cmd-s") { this.save(); cx.stop_propagation(); } }); ``` **平台修饰键在所有平台上都拼作 `cmd`**,Linux 与 Windows 也一样。GPUI 会按编译目标平台来拼——`cmd-`、`super-`、`win-`——这对给人读的 keymap 是对的,对给程序比较的字符串是错的:同一份脚本要在三个平台上跑,`event.keystroke === "cmd-s"` 必须在三个平台上是同一件事。 指针处理器拿到按钮、当前连击次数以及落点。`local_position` 与 `bounds` 在元素第一次绘制之前是没有的: ```js .on_mouse_down("right", (event, cx) => { // event.button === "right" // event.click_count === 1 // event.local_position?.x —— 相对这个元素 this.openMenuAt(event.position, cx); }); ``` 滚动处理器拿到的一律是像素;设备按行上报时,原始行数也在: ```js .on_scroll_wheel((event, cx) => { this.offset += event.delta.y; // 一律是像素 // event.delta_lines?.y —— 只有设备按行上报时才有 cx.notify(); }); ``` ### 哪些元素接了输入 上面这八个方法是 GPUI 自己的 `InteractiveElement` 构建器,shell 把它们装在 `div`、`h_flex`、`v_flex`、`Button`、`Link`、`Checkbox`、`Switch`、`Radio`、`Toggle`、`Tabs` 与 `Tab` 上。 其余组件各自构建自己的 base 类型、挂自己的监听器,所以写在它们上面的处理器会被记进描述、但永远到不了 GPUI。日志里会说明,而不是留给你自己去发现: ```text `on_key_down` is not wired on a Select: the shell installs GPUI's input listeners on the element it owns outright, which is a plain `div`, `h_flex` or `v_flex`. Wrap it and write `on_key_down` on the wrapper ``` **接线了不等于收得到。** 按键沿焦点路径传递,指针沿 hitbox 传递,所以一个不接受焦点句柄的组件——`Tab` 就是——听得到按下、永远听不到按键,无论两者接线得多好。哪些组件接受焦点句柄,见[焦点与无障碍](#焦点与无障碍)。 ### Actions 与按键绑定 action 是比按键高一层的东西。`cx.bind_keys` 说哪个组合键在什么上下文里意味着 `"save"`;`on_action` 说 `"save"` 做什么。菜单项或工具栏按钮派发同一个名字就会走到同一个处理器,而两边都不必知道对方存在: ```js init(_props, cx) { cx.bind_keys([ { keystroke: "cmd-s", action: "save", context: "Editor" }, { keystroke: "ctrl-k ctrl-c", action: "comment", context: "Editor" }, ]); } render(_cx) { return div() .key_context("Editor") .track_focus(this.handle) .on_action("save", (_event, cx) => this.save(cx)) .child( Button.new("save") .on_click(() => window.dispatch_action("save")) .child("Save"), ); } ``` `context` 是一个匹配元素 `key_context(...)` 的谓词,所以同一个组合键可以在列表里是一个意思、在编辑器里是另一个意思。keymap 属于应用而不属于某个窗口,所以在一个 View 里绑的组合键,在它的谓词匹配的任何地方都生效。 同一个元素上注册多个 `on_action` 是可以的,彼此独立。一个它们都没认领的 action 会继续往外层传——这正是内层面板处理 Save、外层窗口处理 Quit 的做法。 整份绑定列表会在安装任何一条之前先校验完:因为第四条有拼写错误而只装了一半的 keymap,比一条都没装更糟,而脚本没有办法知道装进去的是哪一半。 ::: tip 事件处理器请用箭头函数 箭头函数不绑定自己的 `this`,所以处理函数里的 `this` 仍然是 View 实例。用 `function () {}` 写会拿到错误的 `this`。这是为本运行时写脚本时最常见的一处错误,人和模型都一样。 ::: ## 焦点与无障碍 焦点目标由脚本自己持有。`cx.focus_handle()` 创建一个——对应 GPUI 的 `App::focus_handle`,那边并没有 `FocusHandle::new` 可供镜像——它像 [`InputState`](./state.md#留存状态) 一样挂在 View 上,再用 `.track_focus(handle)` 交给某个元素: ```js init(props, cx) { this.search = cx.focus_handle(); } render() { return Button.new("search") .tab_index(1) .track_focus(this.search) .child("Search"); } ``` `cx.focus_handle()` 需要一次活的 Host 调用;而在 `render` 里创建的 handle 每一帧都是新的,它所跟踪的焦点会被下一次重绘丢掉。所以它属于 `init` 或事件处理器,在 `render` 里调用会抛错。 | handle 上的方法 | 回答什么 | | --------------------- | ---------------------------- | | `handle.focus()` | 把键盘移到跟踪它的那个元素上 | | `handle.is_focused()` | 那个元素此刻是否持有键盘 | | `handle.release()` | 释放这个 handle | `Tab` 与 `Shift-Tab` 由窗口根 View 处理:它按下表的顺序双向行走,并遵守已打开的 dialog 或 sheet 的 focus trap。 | 方法 | 作用于 | 效果 | | --------------------------- | ------------------------------------------------------------------ | ------------------------------------------------------------------------------------------ | | `.track_focus(handle)` | `div`、`h_flex`、`v_flex`、`Button`、`Checkbox`、`Radio`、`Toggle` | 把元素绑定到脚本持有的 handle | | `.tab_index(n)` | 上述这些,外加 `Link`、`Switch` | 元素在窗口 Tab 顺序中的位置;同时也把它变成一个 tab stop | | `.tab_stop(value)` | 与 `tab_index` 相同 | Tab 是否能落到它上面。`false` 保留它在顺序中的位置但不可达 | | `.role(name)` | `div`、`h_flex`、`v_flex`、`Button`、`Checkbox` | 屏幕阅读器把这个元素读作什么 | | `.aria_selected(value)` | `div`、`h_flex`、`v_flex` | 脚本自己搭的列表里某一项的选中状态 | | `.aria_active_descendant()` | `div`、`h_flex`、`v_flex` | 在祖先持有键盘时,把本元素报读为当前焦点项——比如输入框保持焦点的 combobox 中被高亮的那一项 | 三张表的范围不同,是因为组件本身不同。`Button`、`Checkbox`、`Radio`、`Toggle` 的焦点 handle 由一个你可以替换的值构建;`Link` 与 `Switch` 自己构建 handle,且没有可替换它的 builder。除 `Button` 与 `Checkbox` 之外的每个组件都自带 role——`Tab` 就是 tab,`Radio` 就是 radio——只有这两个把 role 当作可覆盖项,这正是「让一个按钮被读作菜单项」得以成立的原因。组件无法承接的调用会**写进日志**,而不是被悄悄丢掉: ```text `role` is not wired on a Tab: base's Tab owns this part of its own focus and accessibility. Put it on an element around it ``` 朴素元素六个方法全都接受,脚本正是靠它们搭出 base 没有对应组件的 listbox、toolbar 或 dialog: ```js div() .id(`cadence-${index}`) .role("list_box_option") .aria_selected(index === this.chosen) .when(index === this.chosen, (el) => el.aria_active_descendant()) .child(name); ``` role 的取值逐字镜像 `gpui_kit::Role` 的 snake\_case 拼写——`list_box`、`list_box_option`、`combo_box`、`menu_item`——整套取值以 `Role` 联合类型写在 `gpui-kit.d.ts` 里,编辑器能补全;不在其中的名字会在调用处失败: ```text unknown accessibility role `listbox`; the names mirror gpui_kit::Role in snake_case — see the Role type in gpui-kit.d.ts ``` ## 元素是一次性的 这条规则最容易让新读者意外,所以下面写清它长什么样、以及为什么成立。 ```js const row = h_flex().child("hello"); v_flex().child(row).child(row); // 抛异常 ``` ```text element `h_flex` was already added to a parent; elements are single-use values ``` 跨帧保存也是同样的失败: ```js init() { this.header = h_flex().child("Todo"); // 错误 } render() { return v_flex().child("Todo list").child(this.header); } ``` ```text this element belongs to a previous render pass; elements are single-use values and must be rebuilt each time render runs ``` 有一处毛刺值得知道:arena 每一趟都会清空并复用下标,所以一个过期元素偶尔会正好持有运行时刚分配给“它要挂上去的那个节点”的下标。误用仍然会被抓到,但信息变成 `an element cannot be added to itself`。两者含义相同——这个元素属于一趟已经结束的渲染。 ### 为什么 这条限制来自 GPUI 本身:`RenderOnce::render` **按值**取走 `self`,`.child()` 也按值取走子元素。Rust 里编译器用移动语义强制这一点:使用已移动的值是编译错误。JavaScript 既没有移动语义也没有编译器,于是运行时在运行期强制同一条规则——而描述 arena 本来就有做这件事所需的记录,因为节点被挂载的那一刻就会被标记为已有父节点。 另一种做法是在重复使用时复制描述。这一条被否决了:它会让同一段脚本在 Rust 与 JavaScript 里含义不同,而重复使用几乎总是错误而非本意。 ### 可行的写法 在 `render` 里构建,把重复部分抽成**每次返回新元素的函数**: ```js const label = (value, cx) => div().text_size(12).text_color(cx.theme().colors.foreground).child(value); render(cx) { return v_flex() .child(label("first", cx)) .child(label("second", cx)); } ``` [示例应用](https://github.com/longbridge/gpui-kit/tree/main/examples/js_todolist)就是这样写的:`ui.js` 把 `button`、`label`、`icon`、`checkbox` 等导出为函数,`main.js` 调用它们。读起来像一个组件库,而且不花什么代价——一次函数调用就是一段新描述的来源。 ## 回调属于它所在的那次渲染 传给 `.on_click` 的处理函数属于那次渲染产出的那份描述——而不是属于某一帧。那份描述会[被之后的每一帧复用,直到有东西让它失效](./state.md#render-什么时候执行),处理函数在这期间一直可调用。描述里只记录一个 id;Rust 装配的闭包持有对运行时的弱引用加上这个 id。 被替换掉的那份描述会多保留一代,因为事件可能针对一个已经被取代的帧派发。再晚到达的事件会被丢弃并记一条 `debug` 日志,而不是报错——作者没有做错什么,也没有什么可修。 实际后果是:渲染期注册的回调不是订阅。需要活得比本次渲染更久的东西——比如响应输入框的 `change` 事件——见 [State and Views](./state.md#输入事件)。 ## 未知方法是错误 既不是样式也不属于上面那批行为方法的调用,会在调用点失败;如果有相近的名字,会给出建议: ```text unknown element method `items_centre` (did you mean `items_center`?) ``` ```text unknown element method `on_clicked`; it is neither a style method nor one of child, children, when, on_click, on_change, disabled, selected, checked, id ``` 这件事比看上去重要。拼错的样式名不会改变画面——它只是没起作用——没有诊断的话完全不可见。运行时如何在不给每次渲染加负担的前提下产生这条信息,见 [Styling](./styling.md#未知方法)。 ## 还没有的东西 元素接口现在已经包含 Tabs、Table、Progress、表单控件、Popover/HoverCard 锚定浮层、Textarea、Scrollbar、PathBuilder、VirtualList,以及一个由脚本绘制 chrome 的 [dock area](./dock.md)。仍刻意缺少: * 更高层的 List、Tree 系统,以及尚未接入的其他 `gpui-base` 组件; * `gpui.memo`——它能让未变化的子树跳过重建描述的那部分脚本工作。 焦点现在归脚本所有,但还不完整。仍然缺少的部分: * **复合控件内部的键盘导航,需要自己写。** Tab 与 Shift-Tab 能在控件之间移动;在 listbox、菜单或 tab list *内部*移动的方向键不会自动出现。零件现在都有了——`on_key_down`、`cx.bind_keys` 与 `key_context`——但把 ↑ / ↓ 变成高亮移动这件事仍然是脚本的活。 * **窗口尚无焦点时的第一次 Tab。** 只要还没有任何元素持有焦点,根 View 的 Tab 绑定就没有可达的分发路径;焦点必须先以别的方式进入——点击,或者 `handle.focus()`。 * **`Tab`、`Tabs`,以及 table、group、progress 的各个部件**不在 Tab 顺序里。base 本身就把它们排除在键盘焦点之外,对它们调用 `tab_index` 会被记录而不是被承接。 * **`Link` 与 `Switch` 上的 `track_focus`**,原因相同:它们自己构建 handle,且不暴露替换它的 builder。 --- --- url: /zh-CN/base/motion.md description: >- gpui-base 的类型化 transition、spring、keyframes、presence、stagger 与 reduced-motion 行为。 --- # 动画与动效 `gpui-base` 负责确定性的动效采样与生命周期,并把视觉选择留给应用。它提供稳定 keyed state、中断与反向、animation frame 请求和 reduced-motion 处理,不强加产品级时长或样式。 运行本文配套的交互示例: ```bash cargo run -p gpui-base --example motion ``` 示例包含五个相互独立的页面,可通过顶部标签逐个查看。 ## 能力一览 | 示例 | API | 演示内容 | | --- | --- | --- | | Sliding time | `transition` | 08:00–20:00 的四位独立滚动数字,目标会在前一次过渡完成前继续变化 | | Spring | `spring` | 快速切换目标时仍保持速度连续的分段选择器指示块 | | Keyframes | `Keyframes`、`Timing`、`animate_keyframes` | 持续循环的多段活动信号 | | Stagger | `Stagger` | 无分配地为列表计算错峰时间 | | Presence | `Presence` | 退出动画完成前继续挂载内容 | 此外还提供 `Easing`、`Discrete`、`MotionTransform` 和 `MotionReveal`,它们与同一套 primitive 组合,不需要额外动画 runtime。 ## Transition 已知时长、向目标值变化时使用 `transition`。每个独立运动值都要有稳定 ID: ```rust let opacity = transition( ("save-dialog", "opacity"), if open { 1.0 } else { 0.0 }, Transition::new(Duration::from_millis(180)).easing(Easing::EaseOut), window, cx, ); ``` 运动中改变目标会从当前采样值继续;直接反向还会缩短返回时长。`transition_with_status` 额外返回 `Idle`、`Delayed`、`Running` 或 `Finished`。 `Easing` 支持 CSS 关键字曲线、cubic Bézier、全部 step position 和分段 `linear()` stops,无效参数会返回类型化错误。 ## Spring 目标可能在运动中变化时使用 `spring`。它同时保留位置与速度,适合选择指示器和空间值回落。 ```rust let x = spring( "selected-indicator", selected_x, Spring::new(Duration::from_millis(420)).with_damping(0.72), window, cx, ); ``` 指针直接控制数值时,不要让 spring 追赶指针;拖动中使用 `with_travel(false)`,释放后再恢复。 `with_damping` 要求有限且非负的 ratio;`with_epsilon` 要求有限且大于零,并以目标值自身的单位解释。builder 会在无效的可信常量上 panic;配置值或用户输入应使用 `try_with_damping` 和 `try_with_epsilon`。归一化值通常保留默认的 `0.001`,像素移动可以使用 `0.1` 等较粗容差。 ## Keyframes 与 Timing `Keyframes` 定义经过校验的值序列;`Timing` 按绝对 elapsed time 采样,支持正负 delay、有限或无限迭代,以及 normal、reverse 和 alternate 播放方向。 offset 必须从 `0` 开始、以 `1` 结束并保持单调。不可插值属性使用 `Discrete`。 `animate_keyframes` 会在传入的稳定 ID 下保留播放起始时间。使用相同 ID 重新渲染只会继续当前序列,不会重新开始。需要重播时,把应用持有的 generation 放进 ID,例如 `("notification-enter", generation)`,并在每次重播时递增它。 ## Presence 与 Stagger `Presence` 将逻辑可见性与实际挂载分开,阶段包括 entering、present、exiting 和 absent。`should_render()` 为 true 时继续渲染,并把 `progress` 应用到所选视觉属性。退出中重新打开会从当前进度反向。 `Stagger` 可以从首项、末项、中心或指定位置开始,为每个 index 计算 delay;它不分配时间表,也不接管列表 identity。 ## 测量式展开 `MotionReveal` 按 child 的自然尺寸测量,再根据 progress 裁剪可见高度。`Collapsible::motion_id(...)` 是控件层的便捷入口;没有 motion ID 时仍保持即时挂载/卸载。 ## Reduced motion 与性能 Transition、spring、keyframes、presence 和 reveal 控件都遵守 GPUI 的 reduced-motion 偏好。有限动画会直接同步目标、更新 retained state,并且不留下待处理 frame。动画不能成为表达状态的唯一方式。 benchmark 覆盖的纯稳定采样路径——timing/easing、关键帧查找、解析式 spring 积分和 stagger delay 计算——均为零分配。Keyed transition、spring、presence 和 reveal 生命周期由 GPUI retained state 与 frame-request 测试覆盖,因为这些更新属于框架生命周期,而不是纯采样器。采样使用绝对时间,关键帧查找使用二分搜索。运行 release benchmark: ```bash cargo bench -p gpui-base --bench motion ``` 选择最小且合适的 primitive:固定时长目标使用 `transition`,频繁变化的空间目标使用 `spring`,编排序列使用 keyframes,卸载前退出使用 `Presence`,列表错峰使用 `Stagger`。 ## Benchmark 结果 以下数据来自 Linux x86\_64 release 构建,每项运行 31 个 batch、每个 batch 迭代 200 次: | 工作负载 | Median | P95 | Worst | 内存分配 | | --- | ---: | ---: | ---: | ---: | | 1,000 次 scalar timing + easing 采样 | 26.490 µs | 26.567 µs | 27.290 µs | 0 | | 1,000 次 keyframe 采样,2 frames | 21.656 µs | 21.707 µs | 21.729 µs | 0 | | 1,000 次 keyframe 采样,8 frames | 25.197 µs | 25.251 µs | 25.269 µs | 0 | | 1,000 次 keyframe 采样,32 frames | 27.932 µs | 27.969 µs | 27.971 µs | 0 | | 1,000 次解析式 spring 积分采样 | 6.042 µs | 6.106 µs | 6.216 µs | 0 | | 1,000 次 stagger delay 计算 | 0.574 µs | 0.583 µs | 0.587 µs | 0 | Scalar timing/easing 工作负载低于 100 µs median 预算。这些数值是可复现的开发基线,并非跨平台性能保证;对特定平台性能有要求时,应在对应目标平台重新运行 benchmark。 --- --- url: /zh-CN/base/primitives.md description: gpui-base 面向用户的完整原语目录。 --- # 原语 GPUI Base 原语只提供行为,不规定视觉表现。每个页面都会说明公开导入路径和最小可用组合。页面上方的在线示例由 `crates/base/examples` 构建,也可以作为原生 GPUI 应用运行。 ## 原语目录 * [Accordion](./accordion.md) — 由可独立设置样式的标题、触发器和面板组成的折叠组。 * [Alert Dialog](./alert-dialog.md) — 用于需要明确确认之操作的模态对话框。 * [Avatar](./avatar.md) — 带可组合后备内容的人物或实体图像。 * [Button](./button.md) — 无样式、可访问且支持键盘激活的按钮。 * [Calendar](./calendar.md) — 支持选择匹配器和自定义日期渲染的状态驱动日历。 * [Checkbox](./checkbox.md) — 指示器可单独设置样式的受控三态复选框。 * [Collapsible](./collapsible.md) — 不限定触发器样式的可折叠内容区域。 * [Color Picker](./color-picker.md) — 构建自定义颜色选择器所需的状态和交互基础。 * [Combobox](./combobox.md) — 结合文本输入、键盘导航建议和选择行为的组合框。 * [Date Picker](./date-picker.md) — 将日历与弹出层组合起来、可感知焦点的日期输入。 * [Dialog](./dialog.md) — 带焦点管理、遮罩、标题和关闭部件的可组合模态层。 * [Hover Card](./hover-card.md) — 与指针或键盘触发器关联的延迟浮动卡片。 * [Input](./input.md) — 支持选择、掩码、验证和数值步进的单行输入。 * [Textarea](./textarea.md) — 支持固定行数、换行和自动增高的多行文本框。 * [Editor](./editor.md) — 支持高亮、行号槽、折叠、装饰和 LSP 扩展的代码编辑器基础。 * [Link](./link.md) — 样式由应用定义的可访问链接控件。 * [Nav Stack](./nav-stack.md) — 支持 push、pop、forward 与 replace 的视图导航栈,过渡生命周期可动画。 * [Number Input](./number-input.md) — 带递增、递减和步进行为的数字输入。 * [OTP Input](./otp-input.md) — 由共享文本状态驱动的多单元格验证码输入。 * [Pagination](./pagination.md) — 显式管理当前页与总页数的受控分页器。 * [Popover](./popover.md) — 支持受控或内部开关状态的锚定浮层。 * [Popup](./popup.md) — 底层触发器与锚定浮动内容宿主。 * [Progress](./progress.md) — 用轨道和指示器报告任务完成度。 * [Radio](./radio.md) — 具有选中与禁用语义的受控单选项。 * [Radio Group](./radio-group.md) — 为单项选择提供分组与键盘导航。 * [Resizable](./resizable.md) — 用于可调整分栏布局的面板组和拖拽手柄。 * [Scrollbar](./scrollbar.md) — 连接 GPUI 滚动句柄或统一列表句柄的滚动条。 * [Select](./select.md) — 由锚定且支持键盘导航的弹层驱动的选择控件。 * [Sheet](./sheet.md) — 从边缘进入并管理关闭和焦点的模态层。 * [Slider](./slider.md) — 轨道、指示区和滑块可独立设置样式的范围输入。 * [Switch](./switch.md) — 轨道与滑块可分别设置样式的受控开关。 * [Table](./table.md) — 用于组合表头、表体、行和单元格的语义化表格原语。 * [Tabs](./tabs.md) — 带受控选择的标签列表和可访问标签控件。 * [Toast](./toast.md) — 受管理、带动画的临时状态消息栈。 * [Toggle](./toggle.md) — 用于格式等持久选择的受控双态按钮。 * [Toggle Group](./toggle-group.md) — 将多个 Toggle 协调为单选或多选组。 * [Tooltip](./tooltip.md) — 与触发元素关联、延迟显示且可定位的说明。 * [Tree](./tree.md) — 显式管理展开与选择状态的虚拟化层级列表。 --- --- url: /zh-CN/releases.md description: GPUI Kit 每个已发布版本及其 GitHub Release 说明。 --- --- --- url: /zh-CN/docs/i18n.md description: 在应用中新增或覆盖 GPUI Component 的翻译。 --- # 国际化 GPUI Component 为组件提供了内置翻译,目前包含 `en`、`zh-CN`、`zh-HK`。应用可以新增语言或覆盖个别翻译,而不需要复制完整的内置 locale 文件。 此功能需要 `rust-i18n` 4.2 或更高版本。 ## 添加依赖 在持有 locale 文件的应用 crate 中添加 `rust-i18n`: ```toml [dependencies] gpui-component = "0.6" rust-i18n = "4.2" ``` ## 创建应用 locale 文件 在应用 crate 中创建 `locales/ui.yml`,并将组件翻译放在 `gpui_component` namespace 下: ```yaml _version: 2 gpui_component: Calendar: week.0: fr: Di month.January: fr: Janvier DatePicker: placeholder: fr: Sélectionner une date ``` namespace 必须是 `gpui_component`,它对应 Rust crate 名称 `gpui-component`,其中连字符转换为下划线。可在 [GPUI Component 内置 locale 文件](https://github.com/longbridge/gpui-kit/blob/main/crates/component/locales/ui.yml)中查看可用的翻译 key。 ## 注册扩展 首先在应用的 crate root 初始化 locales: ```rust rust_i18n::i18n!("locales", fallback = "en"); ``` 然后在初始化 GPUI Component 之前注册扩展: ```rust app.run(move |cx| { rust_i18n::extend!(gpui_component); gpui_kit::init(cx); // 打开窗口并初始化应用的其他部分。 }); ``` 应用启动期间只需调用一次 `extend!`。 ## 查找优先级 应用提供的翻译优先于组件的内置翻译: ```text 应用 locales(locales/ui.yml) │ │ 未找到 key ▼ GPUI Component 内置 locales ``` 这种 deep merge 行为意味着: * 新语言(例如 `fr`)只需提供应用需要的 key。 * locale 和 key 都相同时,应用提供的翻译会覆盖内置值。 * 应用未提供的 key 会继续使用组件内置值。 * GPUI Component 后续新增的翻译无需复制,也会自动可用。 例如,只定义 `gpui_component.Calendar.month.January.en` 会修改一月份的英文标签,其他英文日历标签仍然来自 GPUI Component。 ### 命名空间只对组件内部的查找生效 `extend!` 改变的是 GPUI Component 查找**自身** key 的方式,它不会让应用自己的 `t!` 调用能访问到内置翻译: ```rust // GPUI Component 组件内部:先查应用,再查内置。 t!("Calendar.month.February") // -> "February" // 应用代码中:只会读取应用自己的 locale 文件。 t!("gpui_component.Calendar.month.February") // -> 未定义时返回 key 本身 ``` 组件文案应当通过渲染组件来呈现,不要自己去查这些 key。 ## 切换语言 `gpui-component` 已经再导出 locale 相关方法,应用无需直接依赖 `rust-i18n` 即可切换语言: ```rust gpui_kit::component::set_locale("fr"); let current = gpui_kit::component::locale(); ``` 组件随后会按照上述优先级,使用当前 locale 查找翻译文本。 当前 locale 属于 GPUI 不感知的全局状态,修改它本身不会触发重绘。展示翻译文本的 view 需要自行通知: ```rust gpui_kit::component::set_locale("fr"); cx.notify(); ``` --- --- url: /zh-CN/docs/installation.md --- # 安装 在开始使用 `gpui-component` 构建应用之前,需要先准备对应的开发环境并安装依赖。 ## 系统要求 目前可以在 macOS、Windows 和 Linux 上进行开发。 ### macOS * macOS 15 或更高版本 * Xcode Command Line Tools ## Windows * Windows 10 或更高版本 仓库提供了一个脚本用于安装所需工具链和依赖。可以在 PowerShell 中运行: ```ps .\script\install-window.ps1 ``` ## Linux 在 Linux 上,可以运行下面的脚本安装系统依赖: ```bash ./script/bootstrap ``` ## Rust 和 Cargo `gpui-component` 使用 Rust 构建,因此请确保系统已经安装 Rust 和 Cargo。 * Rust 1.90 或更高版本 * Cargo(通常随 Rust 一起安装) 安装库时,只需要在 `Cargo.toml` 的 `[dependencies]` 中加入: ```toml gpui-kit = "0.6" ``` `gpui-kit` 会替你引入配套的 GPUI crate,应用无需再单独声明 GPUI。`use gpui_kit::*;` 就是 GPUI 本身,各层按名访问:`gpui_kit::component`(带样式的组件)、`gpui_kit::base`、`gpui_kit::assets`、`gpui_kit::platform`。 ## 加快开发构建 Debug 构建下 GPUI、组件库和文字排版相关的 crate 都不做优化,`cargo run` 出来的应用渲染会明显比 release 慢。可以只对这些依赖开启优化,你自己的代码仍然保持快速、可调试的 debug 构建。Profile 只在应用(或 workspace)根目录的 `Cargo.toml` 中生效: ```toml [profile.dev.package] gpui-pre = { opt-level = 3 } gpui-component = { opt-level = 3 } gpui-kit = { opt-level = 3 } gpui-kit-assets = { opt-level = 3 } gpui-pre-macros = { opt-level = 3 } gpui-pre-platform = { opt-level = 3 } rustybuzz = { opt-level = 3 } taffy = { opt-level = 3 } ttf-parser = { opt-level = 3 } ``` --- --- url: /zh-CN/apps.md description: 使用 GPUI Kit 构建的桌面应用——从生产环境的交易终端,到数据库客户端、终端与系统工具。 --- --- --- url: /zh-CN/shell/getting-started.md description: 把运行时接进一个 Rust 应用、写它要加载的脚本,并在不开窗口的情况下检查这个脚本。 --- # Getting Started `gpui-shell` 首先是给一个 Rust GPUI 应用加上 JavaScript 扩展点的办法:由 Host 构建运行时、决定脚本能碰到什么,并把脚本 View 挂在它想挂的位置。直接运行一个脚本目录——也就是下面那个 `gpui-shell` 二进制——是随之而来的开发便利,而不是它的定位。 ## 把运行时接进 Rust 应用 `gpui-shell` 二进制本身是一个很薄的 Host:解析命令行、装上日志 sink、建一个运行时、开一个窗口。任何嵌入这个库的 Host 做的也是同样四件事。 ```rust use gpui_kit::shell::{Capabilities, ShellRuntime}; gpui_platform::application() .with_assets(gpui_kit::shell::AppAssets::new(root.clone())) .run(move |cx| { // 初始化 gpui-base、shell 的默认 token 调色板,以及样式反射表。 gpui_kit::shell::init(cx); let runtime = ShellRuntime::new(cx).expect("script runtime"); // 在 Host 开口之前,什么都不允许。 gpui_kit::shell::set_store_path(store_directory.join("store.json")); gpui_kit::shell::set_capabilities( Capabilities::new() .read_roots([root.clone()]) .write_roots([store_directory.clone()]) .store(true), ); cx.open_window(Default::default(), move |window, cx| { runtime.load(&root, window, cx) }) .expect("window"); }); ``` 其中两行承载的是规则而不是机制。 **`runtime.load(...)` 返回窗口的 `ShellRoot`**,作用与 `gpui-component` 窗口中的 `Root` 相同。它持有 dialog 栈、sheet、toast 栈、焦点恢复与 Tab 导航。manifest 负责选择应用入口并记录能力请求,但不会自行批准这些请求。带 manifest 与不带 manifest 的目录都使用 Host 当前的默认 policy;后者采用 `main.js`。 **能力默认为空。** `Capabilities::default()` 什么都不授予——没有文件、没有存储、没有剪贴板、没有进程。由 Host 决定,因为只有 Host 知道它对即将运行的这段代码信任到什么程度。见 [Capabilities](./capabilities.md)。 同时也要装上 `tracing` subscriber。运行时通过 `tracing` 报告脚本错误、未处理的 promise rejection 以及 phase 非法的调用;没有 subscriber 时,这些全部被丢弃,症状是一个安静地不再响应的界面。 ## 它加载的那个脚本 一个文件就够了。新建一个目录,放一个 `main.js`: ```js // hello/main.js import { View } from "gpui-kit"; import { v_flex, Button } from "gpui-base"; export default class Hello extends View { init() { this.clicks = 0; } render(cx) { return v_flex() .size_full() .items_center() .justify_center() .gap(12) .bg(cx.theme().colors.background) .child( div() .text_color(cx.theme().colors.foreground) .child(`Clicked ${this.clicks} times`), ) .child( Button.new("click") .h(28) .px(12) .items_center() .justify_center() .border(1) .border_color(cx.theme().colors.border) .bg(cx.theme().colors.surface) .text_color(cx.theme().colors.foreground) .on_click((_event, cx) => { this.clicks += 1; cx.notify(); }) .child("Click me"), ); } } ``` ```bash cargo run -p gpui-shell -- hello ``` 这个文件里有四件事值得现在就点明,因为后面所有内容都建立在它们之上。 **能力由哪个包提供,就从哪个模块导入。** `"gpui-kit"` 是 GPUI 自身的元素和运行时补上的部分——`View`、`div`、`text`、存储、调度。`"gpui-base"` 是 gpui-base 的布局辅助、组件和主题——`v_flex`、`Button`、`InputState`。`"gpui-fps"` 是它的性能浮层。一个名字只属于其中一个模块,所以一行 import 就说清了脚本依赖的是哪一层。运行时还提供一层刻意收窄的 JavaScript 标准能力:`buffer`、`path`、`url`、`crypto`、`zlib`、`console`、`process`、`os`、`fs/promises`、`net`、`websocket`,以及全局 `fetch`。应用相对导入仍被限制在应用目录内。`node:fs` 这类 `node:` 别名、包查找和 CommonJS `require` 不属于契约。 **`main.js` 必须 `export default` 一个继承 `View` 的类。** `init` 在 View 创建时只执行一次;`render` 返回一个元素、留存的 `Entity` 或字符串,并且是在 View 失效时执行,而不是每帧执行——见 [`render` 什么时候执行](./state.md#render-什么时候执行)。 **样式方法是 snake\_case,你自己写的代码是 camelCase。** `items_center`、`on_click`、`text_color`、`gap_2` 保留了 Rust 的拼写,因为无参样式接口是从 GPUI 的反射表生成的,而不是手写的。应用自己声明的一切——变量、方法、对象的键——都是普通的 JavaScript camelCase。这个对比是刻意的:snake\_case 的调用是 Host 接口,camelCase 的是你的代码。 **没有任何东西会自动重绘。** 没有 signal,没有 `useState`,也没有依赖数组。改完状态,自己调用 `cx.notify()`。 ## 单独运行一个脚本 一个脚本目录也可以直接跑起来,不必先写 Host 。自带的示例就是这么运行的;一段脚本在被它将来所属的那个应用加载之前,通常也是这样开发的。`gpui-shell` 没有发布到 crates.io,所以先克隆仓库,再在仓库根目录运行: ```bash cargo run -p gpui-shell -- examples/js_todolist ``` 窗口里会出现一个可用的 todo list:带留存状态的输入框、受控 checkbox、一个确认 dialog、一个 toast、从应用自身目录加载的图标,以及在未获授权时退化为内存存储的持久化。它的目的是把整个运行时都跑一遍,而不是做到最小——哪里坏了,通常先在这里露出来。 参数是一个**目录**,不是文件。运行时解析该目录,默认读取其中的 `main.js`,取出该模块 default 导出的类、构造一个实例,并把它挂载为窗口的根 View。如果目录中存在 `gpui-shell.json`,二进制会先验证它,并采用其中声明的 `entry` 与 capabilities。 ## 不运行也能检查脚本 JavaScript 没有编译器,这个运行时也不打算造一个。它补上的是编译器本该替你做的那件事: ```bash cargo run -p gpui-shell -- check hello ``` `check` 会加载应用,并向一个永远不显示的窗口渲染一帧,成功退出 `0`,失败退出 `1`。因为脚本接口是动态的——未知的样式方法、类型不对的参数、被重复使用的元素,都是运行期事实——所以“构建并渲染一次”是唯一诚实的检查方式。它能报出: * 语法错误,并带上脚本自身的调用栈; * 无法解析的 import,以及越出应用目录的 import; * 缺失或形态不对的 default 导出; * 未知的样式方法,并给出 `did you mean` 建议; * 类型不对的样式参数,例如 `.p("auto")`; * 被使用了两次的元素。 它不开窗口,因此可以放进编辑器、CI,或者一个 agent 的循环里。 加上 `--print-spec` 可以顺带打印构建出的元素描述: ```bash cargo run -p gpui-shell -- check hello --print-spec ``` 这份输出是 arena 自己的调试输出——在它变成真实元素之前,由组件与记录操作构成的那棵树。当问题是“我这条链到底记录了什么”时,它很有用。 ## 生成 TypeScript 声明 ```bash cargo run -p gpui-shell -- types hello ``` 它会在应用旁边写出 `gpui-kit.d.ts`。在脚本顶部加上 `// @ts-check`,编辑器就会补全整套 API,并在运行之前、在调用点上直接拒绝拼错的样式方法、不存在的颜色 token,或者 `.p("auto")`。 它同时会把编辑器需要的其余部分一并配好:manifest 声明的每个 Git 依赖都会被抓取并按声明的名字链接进 `node_modules`,于是 `import { style } from "omarchy-ui"` 解析到的正是运行时将要执行的那批文件,连同该 package 自己的类型、参数与 JSDoc;若目录里既没有 `jsconfig.json` 也没有 `tsconfig.json`,还会生成一份 `jsconfig.json`。详见[依赖](./dependencies.md)。 这份声明可信,是因为它**从运行时实际派发所依据的那几张表生成**,而不是照着文档抄的: * 样式方法名来自 JavaScript prelude 构建元素原型时遍历的同一份列表; * 每个有参方法的参数类型是**探测**出来的——生成器逐一询问运行时该方法接受哪些字面量,所以 length、definite length、absolute length、颜色与裸数字之间的区别,由真正做校验的那段代码决定; * 颜色的联合类型来自已安装调色板的 token 名。 有三件事声明刻意不表达,因为没有类型能表达:能力是否被**授权**(被拒绝的 `fs.readFile` 一样能通过类型检查);元素与 `cx` 的**生命周期**(TypeScript 没有仿射类型,重复使用元素照样能通过类型检查,也照样会抛异常);以及**某个方法适用于哪个组件**(所有元素共用一个原型,所以 `.checked(true)` 声明在全部元素上,在 `div` 上只是不起作用)。 升级运行时之后重新生成即可;输出是确定性的,所以 diff 是可以审阅的。 ## Hot-reload ```bash cargo run -p gpui-shell -- hello --watch cargo run -p gpui-shell -- hello --dev # 隐含 --watch ``` `--watch` 每秒轮询应用目录四次,对一串连续写入去抖 200 ms,然后重载。一次重载会重新读取**每一个**模块,入口也在内——一个悄悄用了旧 import 的 hot-reload 比没有更糟,因为它看起来是成功的。 重载会在碰到实时 View 之前,先把所有可能失败的工作做完。如果新代码加载失败,之前的 View 继续运行,错误输出到 stderr,同时窗口里出现一个带固定 id 的 toast;下一次成功重载会把它撤回。存了一份坏代码,不会因此丢掉窗口。 `--dev` 隐含 `--watch`,并在构造运行时之前开启 development mode。它恢复动态代码构造器并让内建原型保持可写,但 capability 检查完全不变。见 [Capabilities](./capabilities.md#沙箱)。 ## 命令一览 ```text gpui-shell [--watch] [--dev] gpui-shell check [--print-spec] gpui-shell types gpui-shell --help | --version ``` | 参数 | 含义 | | -------------- | -------------------------------------------------- | | `` | 应用根目录,或其中的 `main.js` | | `check` | 不开窗口地加载并渲染一次,退出码 `0` 或 `1` | | `types` | 写出 `gpui-kit.d.ts`、链接 manifest 依赖、生成配置 | | `--watch` | 源码变化时重载 | | `--dev` | 开发模式,隐含 `--watch` | | `--print-spec` | 配合 `check`,额外打印构建出的元素描述 | --- --- url: /zh-CN/docs/getting-started.md description: 学习如何在项目中安装并使用 GPUI Component。 --- # 开始使用 ## 安装 在 `Cargo.toml` 中添加依赖: ```toml [dependencies] gpui-kit = "0.6" anyhow = "1.0" ``` :::tip `gpui-kit` 始终引入 GPUI 和 `gpui-base`,并默认带上 `gpui-component` 和默认图标集。如果你希望自行管理图标与资源文件,只保留需要的 feature 即可: ```toml gpui-kit = { version = "0.6", default-features = false, features = ["component"] } ``` 更多说明见 [资源与图标](./assets.md)。 ::: ## 快速开始 下面是一个最小可运行示例: ```rust use gpui_kit::component::button::*; use gpui_kit::component::*; use gpui_kit::*; pub struct HelloWorld; impl Render for HelloWorld { fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { div() .v_flex() .gap_2() .size_full() .items_center() .justify_center() .child("Hello, World!") .child( Button::new("ok") .primary() .label("Let's Go!") .on_click(|_, _, _| println!("Clicked!")), ) } } fn main() { let app = gpui_kit::application().with_assets(gpui_kit::assets::Assets); app.run(move |cx| { gpui_kit::init(cx); cx.spawn(async move |cx| { cx.open_window(WindowOptions::default(), |window, cx| { let view = cx.new(|_| HelloWorld); cx.new(|cx| Root::new(view, window, cx)) }) .expect("Failed to open window"); }) .detach(); }); } ``` :::info 请确保在 `app.run` 闭包中尽早调用 `gpui_kit::init(cx);`。它会初始化主题和全局配置。 ::: ## 后续阅读 * [组件总览](./components/index) * [资源与图标](./assets.md) --- --- url: /zh-CN/shell/engine.md description: QuickJS 位于一条内部接口之后、这条分界线存在的理由,以及把脚本成本与帧成本分开的三项实测。 --- # The Engine Seam 脚本引擎位于一条内部接口之后。这条分界线(seam)之上的一切——元素描述 arena、把描述变成真实元素的 `materialize`、CallScope、样式表、主题、能力模型、浮层 Host 、hot-reload——都与引擎无关,只有引擎模块知道脚本值长什么样。 ```bash cargo run -p gpui-shell -- examples/js_todolist ``` [QuickJS](https://github.com/quickjs-ng/quickjs) 经由 [`rquickjs`](https://github.com/DelSkayn/rquickjs) 随发行版一起提供——后者 vendored 的是 `quickjs-ng` 这个分支——它也是今天唯一的引擎。它仍然放在 `quickjs` 这个 cargo feature 之后,不启用任何引擎构建是**编译错误**,而不是编出一个什么都不导出的 crate。 ## 为什么会有这条分界线 引擎选择是这个运行时里唯一无法在纸面上判定的决定。 其余的设计都可以从 GPUI 的元素模型推出来,在白板上就能争清楚。引擎不行,因为整套方案的成立与否取决于一个数字:**脚本代码描述一个真实界面需要多久。** builder 链上的每一次方法调用都是一次跨语言边界,如果这笔单次调用成本过高,任何设计都救不回来。 变的是这个数字*拿来跟什么比*。脚本的一次 `render` 不再等于一帧渲染之后,一次描述在应用状态变动时构建,然后[被之后的每一帧复用,直到状态再次变动](./state.md#render-什么时候执行)——所以下面这笔成本是按用户操作付,不是按重绘付。这让边界成本不像从前那么关键,但它并没有变成免费,而且仍然是决定要不要引入第二个引擎的那个数字。 所以这条分界线是一种“不必提前判断正确”的做法。决定交给实测,而第二个引擎会是一个新模块,不是一次重写。 JavaScript 是默认,理由只有一条,而且是产品理由不是技术理由:**应用代码用它写出来更好读。** 呈现权在脚本手上,应用的绝大部分就是组合元素、写样式、处理事件——这类代码的可读性直接决定这个运行时值不值得用。类、箭头函数、模板字符串与解构,恰好落在这类代码上。附带收益是 JavaScript 在模型训练语料里覆盖最好,这对[三种场景](./index.md#适用场景)之一是决定性的。 代价也如实写出来:QuickJS **没有 JIT**——它是字节码解释器,热点循环与单次调用成本原则上都赢不了带 JIT 的引擎。这是一笔真实的取舍,而下面的基准正是它一旦要紧就会显形的地方。 ## 那次实测 这里有三笔不同的成本,把它们当成一笔正是最初的错误。基准测试描述一个 40 × 5 的样式化单元格网格——443 个描述节点,每个约十次记录操作——并把每笔成本分开报告: ```bash cargo test -p gpui-shell --release --lib benchmark -- --nocapture ``` | | 测的是什么 | 443 个节点 | 什么时候付 | | --- | --- | --- | --- | | **A** | 脚本 → Snapshot | **1.4 ms** | 应用状态每变动一次 | | **B** | Snapshot → GPUI 元素 | **0.7 ms** | 每一帧 | | **C** | 一整帧的缓存重绘 | **1.8 ms**,**一行 JavaScript 都没跑** | 每一帧 | 要用 release 跑,否则数字没有意义。本页所有绝对数值都来自一台 MacBook Pro(M3,8 核,24 GB)上的 release 构建,会随机器变化。 **C 是断言,不只是计时。** 对一个未变化的 View 重绘五十次,一行 JavaScript 也没有跑过。哪怕有一帧跑了,就说明运行时退回到了按帧收取脚本成本的老路上——那时基准是直接失败,而不是只变慢一点。 只测一个规模,看不出这三笔成本里哪几笔会随规模增长,所以第四个测试把同一个面板一路放大到 8,403 个节点。它挂在 `--ignored` 之后,因为最大的那一档要跑好几秒: ```bash cargo test -p gpui-shell --release --lib benchmark -- --ignored --nocapture ``` 描述一次,443 个节点是 1.1 ms;面板放大到 2,103、4,203、8,403 个节点,则依次是 5.1、10.3、20.5 ms。而一整帧——也就是 B 加上 GPUI 的布局与绘制,即 C 量的那件事——对应是 1.3、5.9、12.0、27.0 ms。两笔都随节点数接近线性增长;不增长的是 JavaScript——每一档的每一帧都是零行。这三件事因此说清了: * **4,203 个节点是 Snapshot 决定结果的那个规模。** 12 ms 一帧稳在 60 FPS;若每帧都重建描述则要 22 ms,直接掉帧。比这更小的规模,两种做法都有余量——在过度解读那个倍数之前,这一点值得先知道。 * **描述这笔成本没有消失,只是挪了位置。** 8,403 个节点的 20 ms 是用户操作时才付,不是每秒付六十次;但它仍然是 20 ms,所以单次调用成本依旧是评判第二个引擎的那个数字。 * **超过几千个节点之后,账单根本不在脚本一侧。** 那个规模上 27 ms 一帧、其间一行 JavaScript 都没跑,花的是 `materialize`、布局与绘制。这么大的 View 该做的是虚拟化,换个更快的引擎也推不动它。 把 A 对照设计给出的预算,答案仍是“在预算之内,但余量比预期少”:目标是一次脚本 `render` 花 1.5 ms,实测达标;但那份预算是按 800 个节点、每次记录操作约 150 ns 推出来的,而实测是 443 个节点上约 320 ns,三倍规模的面板放不进一次描述。变的是这件事有多要紧。按老模型,120 FPS 下每一秒会有 168 ms 花在重复描述一个没人改过的界面上;现在同一个面板在用户真正改动时花 1.4 ms,重绘则是 0.7 ms。设计为超大面板列出的三条手段——压低单次调用成本、缓存未变化的子树、对长列表做虚拟化——依然[还没有实现](./elements.md#还没有的东西),但它们现在是优化项,不再是前提条件。 同一次实测还带来了两个实现选择,今天在运行时里看得到: * **元素是共用同一个原型的普通对象**,样式方法由一段 JavaScript prelude 遍历名字列表装到该原型上。不是每个元素一个 class,不是每次属性访问新建闭包,也不是 3000 个 Rust 闭包。 * **带诊断的 `Proxy` 原型不是默认。** 用 `Proxy` 包住原型以便说出拼错的方法名,代价是整个描述过程的约 30%;所以运行时保留普通原型,只在一次渲染失败后用诊断原型重跑一遍,纯粹为了产出那条信息。见 [Styling](./styling.md#未知方法)。 ### 实时行情负载实测 合成基准把各项成本拆开测;Longbridge 行情终端则让它们在同一个真实负载中同时发生。下面的样本来自 release 构建,窗口处于活动状态,显示器分辨率为 3,840 × 2,160、刷新率为 144 Hz。Watchlist 持续接收实时行情,同时显示选中标的的详情与五日价格图。目标是 120 FPS,即每帧预算 **8.33 ms**。 通过可选的运行时计数器按一秒区间采样,并将脚本描述成本与 native materialize 分开: | 测量项 | 实测范围 | | --- | --- | | 完整 JavaScript `render` 加 Spec recording | 每次脏渲染 **12.0–13.5 ms** | | Snapshot materialize | 每次 **0.93–1.08 ms** | | 行情更新触发的脚本渲染 | 每秒 **8–20 次** | | 窗口活动时的 materialize | 每秒 **59–78 次** | 一次活动窗口下的 FPS HUD 样本为 **69 FPS**、帧时间 **10.9 ms**、掉帧率 **18.3%**。HUD 的测量包含 GPUI layout 与 paint,因此不能与前两项运行时计数直接互换,但它确认了端到端负载没有达到 8.33 ms 的目标。 有效结论比“JavaScript 很慢”更具体:未变化的 Snapshot 能在约 1 ms 内 materialize,明显低于一帧预算;但行情造成一次脏更新时,应用会重建并记录完整描述,完成 materialize 之前总计要花约 12–13.5 ms。因此,这个负载的主要成本是反复使根 Script View 失效;只优化 native materializer 无法恢复 120 FPS。 这些数据有意排除了 debug 构建,也排除了窗口失去活动状态后的样本。两者都会显著改变调度与帧呈现,FPS 不适合用于架构比较。这组数据也是工作负载实测,不替代上面可复现的 crate 基准:行情频率、可见内容、硬件与显示时序都会改变绝对值。 ## 线程与内存 VM 与 GPUI 的 `App` 共用一个线程——主线程——在同一个进程里。`ShellRuntime` 是一个内部用 `RefCell` 的 `Rc`,既不是 `Send` 也不是 `Sync`。这里没有 worker,也没有第二个 VM。 后台工作从不接触 VM。计时器(`cx.sleep`、`cx.timer`)在那里倒计时,文件、进程、fetch、TCP 与 WebSocket 也把阻塞工作交给那里。结果回到前台执行器 settle,所以 JavaScript 续体仍在主线程、在一个 `Task` scope 里执行。元素产出之后,GPUI 也会用自己的线程完成自身工作。 做性能分析时,有三条推论要紧: * **一次 builder 调用就是一次函数调用。** 它只跨过 FFI 边界,此外别无其他——没有序列化,没有 IPC 往返,除了参数本身的转换之外也没有拷贝。基准测试按记录下来的操作数报告这笔成本,四档面板测下来落在 **240–340 ns**。 * **脚本工作仍与 UI 共用线程。** 文件、进程、fetch、TCP 与 WebSocket 会把阻塞工作交给后台 worker,再回到 foreground executor settle;但 JavaScript 计算与 HostModule 调用仍和 GPUI 在同一线程上,必须保持有界。 * **失控的脚本无法从另一个线程抢占。** 切断它的是解释器自己的中断——`render` 里 50 ms、事件处理器里 500 ms——而且 `catch` 吞不掉它。 内存分成四块,各有各的归属,也各有各的释放时机: | 是什么 | 待在哪里 | 什么时候释放 | | --- | --- | --- | | 对象、闭包、模块作用域 | QuickJS 堆,上限 256 MiB | 它自己的 GC 跑过,或者运行时销毁 | | 元素描述 arena | Rust 侧;移交给它产出的那份 Snapshot | 那份 Snapshot 销毁时 | | 已注册的回调 | Rust 侧的 arena,按 Snapshot 的 generation 索引 | 那份 Snapshot 销毁并退役它那一代时 | | GPUI 元素 | GPUI 自己的帧 arena | 构建它们的那次绘制结束时 | 一个 View 持有的是**两份** Snapshot 而不是一份:当前生效的那份描述,以及被它替换掉的上一份。上一份要多留一代,因为一个已经在途的帧可能仍在读它,提前释放会把那一帧还需要的回调一并退役掉。 跨过边界的东西没有一个是对象。元素句柄是指向 arena 的一个整数下标;由 Host 留存的状态——`InputState` 的 rope、光标与选区——待在一个 GPUI 实体里,脚本通过句柄寻址它;每一个参数与返回值都是纯数据。 ## 链接它要付多少 Host 在取这个依赖之前必须知道两个数:二进制大多少,内存多占多少。 测的是本仓库里最小的两个真实程序,这样得到的是这个 crate 的代价,而不是某个应用碰巧还装了什么的代价: | | `hello_world` | `gpui-shell` 跑 `js_todolist` | 增加 | | --- | --- | --- | --- | | 二进制,stripped | 12.6 MiB | 26.1 MiB | **+13.5 MiB** | | 二进制,unstripped | 16.5 MiB | 33.8 MiB | +17.3 MiB | | 常驻内存 | 67 MiB | 81 MiB | **+14 MiB** | `hello_world` 是 41 行 Rust,跑在 `gpui` 和 `gpui-component` 上——一个窗口和一个计数器。`gpui-shell` CLI 是能跑起一个脚本应用的最小 Host;这里它跑的是 `examples/js_todolist`,四个模块共 519 行 JavaScript,背后是一个活的 QuickJS 运行时。内存取四次运行的中位数,丢弃缓存还冷时读数偏高的第一次;二进制是 `--release`、workspace 默认 profile,用 `strip(1)` 处理。 **+13.5 MiB 是个常数,这是这里最有用的一点。** 同一对测量放到组件 gallery 上——一个体量五倍于此的程序——stripped 增加的还是 13.5 MiB,只是占比从 +107% 变成 +19.8%。两次独立测量在三位有效数字上一致,这才让它成为关于 `gpui-shell` 的事实,而不是对某一个应用的一次读数。 内存那两行看起来矛盾,其实不矛盾。在 gallery 上这个差异**落在测量噪音里**:那个构建多次运行读数在 194–208 MiB 之间,而 14 MiB 本来就小于它自身的波动范围。最小程序能分辨出来,是因为 67 MiB 里没有那么多东西可以把它藏起来。 ### 二进制大在哪 主要不是 QuickJS。解释器本身一到两 MB,剩下的是随它一起来的标准运行时。`fetch`、`websocket` 和 `crypto` 带进了 `hyper`、`rustls`、`ring`、`h2`、一份 `webpki` 根证书库和压缩相关的 crate,而 `gpui-component` 一个都不带——`hello_world` 里没有 HTTP、没有 TLS、也没有 `tokio`。整个这套栈都是从这个 crate 进来的。 这也解释了这张表更早的一版为什么写的是 +4.7 MiB:那是标准运行时之前的数字。`fs`、`net`、`crypto`、`fetch`、`websocket`、`zlib` 都是之后才加进来的。 而且今天没有"只要元素表面、不要这些"的配置。`quickjs` 是唯一的引擎 feature 而且是 `default`,用 `--no-default-features` 构建会直接 `compile_error!`,标准运行时也在这同一个 feature 里面。把两者拆开是新工作,不是把一个已经存在的开关暴露出来。 有两处看起来能省、实际不能的地方,都是实测出来的,不是推出来的: * **去掉那五个 Shell 并不注册的上游 crate**——`llrt_fetch`、`llrt_fs`、`llrt_net`、`llrt_os`、`llrt_console`,它们此前只为一处编译期断言而成为依赖——二进制**一个字节都不变**。它们那些重量级 feature 解析到的 crate,其它依赖本来就会拉进来。不过它们还是被移除了:14 个不占字节的 crate 仍然要付编译时间和供应链审计面,而且依赖一个 Shell 刻意不用的上游 `fetch`,读起来就像它在用。 * **把 `reqwest` 收窄到 `fetch.rs` 真正用到的范围**——去掉 `charset`、`multipart`、`socks`、`stream` 和 `macos-system-configuration`,这几个那个文件一个都够不着——省下 **0.1 MiB**。 所以这 13.5 MiB 里没有水分。它就是 `hyper`、`rustls`、`ring` 和解释器本身;Host 只要想要 `fetch`、`websocket`、`crypto` 里的任何一个,就得把它们全部链接进来。 ### 每个运行时 上面这些数字对应一个运行时。挂多个的 Host——比如每个插件一个的插件 Host——每次都要付一遍引擎的构造成本:一个 QuickJS runtime 和 context、模块注册表、全局对象、 Host 安装器,以及一段 43 KB 的 prelude,每次构造都会解析一遍。[Capabilities](./capabilities.md#沙箱) 里那个 256 MiB 的堆上限是天花板,不是预留;脚本不分配就不会占用。 ## 分界线两侧各有什么 这个比例本身就是这条分界线存在的论据:上面是真正的设计,下面是“脚本值长什么样”。 | 分界线之上——与引擎无关 | 分界线之下——由引擎实现 | | --- | --- | | 渲染 Snapshot:脚本 `render` 一次产出什么、之后的帧复用什么 | 把引擎值转换成运行时里与引擎无关的值类型 | | 元素描述 arena、一次性检查与调试树 | 模块系统的形态——ES module 加解析器,还是 `require` 加路径表 | | `materialize`:把描述变成真实 GPUI 元素,纯 Rust | 方法派发——共享原型上的函数,还是 `__index` 元方法 | | CallScope:phase、generation,以及整个 crate 唯一的 `unsafe` | 回调句柄类型 | | 样式表、有参样式与拼写建议 | 把与引擎无关的错误类型转成该语言自己的异常 | | 默认 token 调色板与颜色 token 解析 | View 如何定义——`class extends View`,还是元表 | | 能力模型与路径解析 | 沙箱中与语言相关的部分 | | 长度与颜色的转换 | | | 与引擎无关的错误类型、回调 arena、错误浮层 | | | `ScriptView`、`ShellRoot`、hot-reload | | 左侧的模块,源码里没有一处出现 VM 的名字。这才是这条分界线为真的证据:它不是一个 trait,而是“crate 的其余部分只通过十来个入口触达引擎、此外别无他途”这一事实。 在这里用 trait 反而更糟。两个句柄类型——View 类与 View 实例——在 QuickJS 一侧各自带着生命周期标注,硬套一层 trait 只会把这份复杂度搬进类型系统,而不会消掉它。 契约里最吃重的一条规则关于*什么时候*而不是做什么:**引擎的 `build_snapshot` 是进入脚本 `render` 的唯一入口,而且没有任何东西按帧调用它。** 一个会见缝插针地渲染的引擎——重绘时、悬停时、定时器到点时——会把脚本成本重新压回帧预算,而这正是这条分界线要防的耦合。基准 C 就是抓这件事的。 ## 可移植性 如果将来真的增加第二个引擎,**脚本在两者之间不可移植。** 它们会是不同的语言: View 在 JavaScript 里是 `class Counter extends View`,换一门语言就是别的写法。 必须相同的是围绕它的其余一切——绑定接口、渲染协议、phase 规则、能力模型、错误信息。设计提出的要求是行为层面的:同一个用例在两个引擎下必须产出**同一棵描述树**,同样的应用活动必须触发**同样次数的脚本 `render`**。这正是防止这条分界线腐烂成两个各行其是的运行时的东西。 ## 已知缺口:异步还没有完全落在分界线之上 这条分界线的契约目前不覆盖异步。 QuickJS 要求 Host 自己去清空 job 队列——没人来问,`await` 之后的代码就永远不执行——而这不是每个引擎都有的形态。所以调度器无法整体落在分界线之上。它需要引擎再提供两个操作:把一个 Host 任务变成脚本可等待的值,以及跑完待执行的 job。 Promise job 会在 Host 调用边界被 drain;render 如果只是发现还有 pending job,会排入一次 foreground drain,而不是在 paint 路径上执行任意 continuation。这保住了核心不变量:异步 continuation 可以让 View 失效,但一帧绝不会仅仅因为自己是一帧就重新进入 JavaScript。 在这两条被处理之前,调度器是 QuickJS 专属的。它将来要遵守的规则,与任何新增能力的规则一样:除非确实无法表达,否则加在分界线之上。 ## 为什么不用 WebAssembly,也不用独立进程 这条分界线会引出的两个问题。 `gpui-shell` 把 VM 跑在**Host 进程内、主线程上**,与 GPUI 的 `App` 在一起。正是这一点,才让单次记录调用停在 240–340 ns。独立进程会在每一次记录 builder 调用上加一次 IPC 往返;即便有了 Snapshot 把频率降下来,这份预算依然没有。同样的理由也解释了为什么没有 `Worker`:VM 与 `App` 都是主线程独占的。 wasm 目标是分界线画在这个位置的另一个理由。QuickJS 是纯 C,能编到 WebAssembly;不是每个候选引擎都能,有些还会生成机器码,这在禁止可写可执行内存的平台上是一项约束。这些事实都不决定今天的引擎,但它们是“引擎是架构的一个参数,而不是架构的一部分”这句话被写下来的原因。 --- --- url: /zh-CN/base/getting-started.md description: 安装、初始化并渲染第一个 gpui-base 控件。 --- # 快速开始 ## 安装 使用与 `gpui-base` 匹配的 GPUI 仓库版本: ```toml [dependencies] gpui-base = { git = "https://github.com/longbridge/gpui-kit" } gpui = { git = "https://github.com/zed-industries/zed" } gpui_platform = { git = "https://github.com/zed-industries/zed", features = ["font-kit"] } ``` ## 初始化 打开窗口前调用一次 `gpui_kit::base::init`。若应用已调用 `gpui_kit::component::init`,其中已经包含 Base 初始化。 ```rust use gpui_kit::AppContext as _; fn main() { gpui_platform::application().run(|cx| { gpui_kit::base::init(cx); // 在这里打开应用窗口。 }); } ``` ## 渲染控件并设置样式 Base 控件刻意不提供产品专属的内边距、颜色或圆角,请用普通 GPUI 方法设置样式: ```rust use gpui_kit::prelude::*; use gpui_kit::{px, rgb}; use gpui_kit::base::Button; Button::new("save") .px_3().py_2().rounded(px(6.)) .bg(rgb(0x2563eb)).text_color(rgb(0xffffff)) .on_click(|_, _, _| println!("save")) .child("Save") ``` 跨渲染保持每个 `ElementId` 稳定,GPUI 才能保留元素和焦点状态。Checkbox、Switch、Radio、Toggle 等受控组件会通过回调报告下一个值;把它存进视图,并在下一次渲染时传回。 ## 默认颜色 Token `gpui-base` 通过 `ColorTokens::light()` 和 `ColorTokens::dark()` 提供可直接使用的浅色、深色语义调色板。`ColorTokens::default()` 使用浅色调色板。两套颜色均使用 `Hsla`,并与 `gpui-component` 的默认浅色、深色主题保持相同的语义角色。 ```rust use gpui_kit::base::{ColorTokens, SemanticThemeTokens, Theme}; // 根据应用当前外观选择对应调色板。 let colors = if is_dark { ColorTokens::dark() } else { ColorTokens::light() }; Theme::global_mut(cx).tokens = SemanticThemeTokens { colors, ..Default::default() }; ``` 调色板描述的是语义角色,而不是某个组件的专用颜色:`background` 与 `foreground`、`surface` 与 `surface_foreground`、`primary`、`secondary`、`muted`、`accent`、`destructive`、`border`、`input`、`ring` 和 `selection`,以及对应的前景色。能从既有角色推导的细节,Base 组件就直接推导,例如链接颜色取自 `primary`,不再为它单独加 token。`selection` 之所以自成一个角色,是因为没有别的角色能替代:选区绘制在文字下方,必须保证文字依然清晰,而 `accent` 和 `ring` 都无法保证这一点。 调用 `gpui_kit::component::init` 时,当前浅色或深色主题会自动映射到同一套 Base token。只使用 `gpui-base` 的应用应在外观模式变化时安装对应的调色板。 ## 运行共享示例 ```sh cargo run -p gpui-base --example components -- button ``` 将 `button` 替换为[原语目录](./primitives/index.md)中的 slug。网站会把同一份展示代码编译为 `wasm32-unknown-unknown` 并加载到各原语页面。 --- --- url: /zh-CN/shell/performance.md description: >- 当帧率不再是变量之后,JavaScript 真正的开销——失效频率乘以描述规模、每个 View 各自的 Snapshot,以及 FPS 分辨不出来的那两类问题。 --- # Performance 一个 View 的 `render()` 不是每帧都跑。它产出一份描述并存成 Snapshot,之后的每一帧都由 Rust 从这份 Snapshot 画出来,不再进入 JavaScript——[首页那一节](./index.md#性能-脚本不在每一帧里)讲的就是这件事。 一旦重绘不进入 JavaScript,剩下要算的就只有两样: ```text JavaScript 的开销 = 一个 View 多久失效一次 × 描述这个 View 要花多少 ``` 两样都不是帧率。窗口以 120 Hz 还是 30 Hz 重绘,JavaScript 执行的次数完全一样;没有人让它失效的 View,一次都不执行。 两样也都在你手里:左边是你在哪里调用 `cx.notify()`,右边是一次 `notify` 背后压了多少界面。这一页剩下的内容讲的就是这两件事,以及出问题时怎么分辨是哪一个。 ## 每个 View 都有自己的 Snapshot GPUI Shell 给每一个 JavaScript View 一份属于它自己的 Snapshot:这个 View 的 `render` 产出的那份描述,保存在 Rust 一侧。 **只要 View 本身没有变化,它的 Snapshot 就一直被复用。** 中间的每一帧都从这份 Snapshot 画出来——转成 GPUI 元素、布局、绘制——全部在 Rust 里完成,不执行任何 JavaScript。 ```text View 变了 ──▶ render() ──▶ 新的 Snapshot ──▶ 帧 View 没变 ─────────────────▶ 已有的那份 Snapshot ──▶ 帧 ``` Snapshot 是按 View 存的,不是按窗口存的。一个窗口里有一百个 View,就有一百份 Snapshot,各自独立失效: | 发生了什么 | 会执行什么 | | --- | --- | | `Watchlist` 调用 `cx.notify()` | `Watchlist.render`,其余什么都不跑 | | 父 View 调用 `cx.notify()` | 父 View 的 `render`。每个子 View 用自己的 Snapshot 回答这一帧 | | `this.chart.set_props({ symbol })` | 那个子 View 的 `update` 与 `render`。父 View 不重建 | | 子 View 的子 View 调用 `cx.notify()` | 那个子 View 的 `render`。失效不会向上传播 | | 主题切换 | 每一个 View——因为 Snapshot 里烘进了它构建时的颜色 | ## 把大 View 拆成小 View View 是整体重建的,内部没有局部重建:如果一个 View 的描述有四百个节点,那么任何一点变化都会把这四百个节点全部重建一遍,无论变化多小。 这就是大 View 贵的原因。它画的所有东西共用一份 Snapshot,于是变化最频繁的那部分数据,会连带让那些从不变化的部分一起失效。在一个行情终端里,一个价格动一下,图表、侧栏、盘口也会被重新描述一遍——不是因为它们变了,而是因为它们和价格在同一个 View 里。 拆分就是解法。把各自独立变化的部分用 `cx.new` 拆成各自的 View,一次变化就只会落到一份 Snapshot 上,而不是全部: ```js import { View } from "gpui-kit"; export default class Terminal extends View { init(props, cx) { this.sidebar = cx.new(Sidebar); this.watchlist = cx.new(Watchlist, { symbols: props.symbols }); this.chart = cx.new(PriceChart, { symbol: props.symbols[0] }); this.detail = cx.new(Detail, { symbol: props.symbols[0] }); } render() { return h_flex() .child(this.sidebar) .child(this.watchlist) .child(v_flex().child(this.chart).child(this.detail)); } } ``` 在本页测量的那块 40 行看板上,描述整块面板要 **0.315 ms**,描述其中一行只要 **0.012 ms**——361 个节点对 9 个。 嵌套本身的开销和这个差距比几乎可以忽略:父 View 为每个子 View 记录的是一个句柄,不是子 View 的描述。所以界面复杂本身不是性能问题, View 太大才是。 而且「为了性能而拆」指的是拆成 **View**,不是拆成多个插件、多个应用或多个进程。需要第二个应用,是因为你想要第二份**授权**,那是 [Capabilities](./capabilities.md) 的事,而不是因为你想要第二份缓存。 ## 只为用户看得见的变化 notify `cx.notify()` 就是这里全部的依赖系统,而它只表达一件事:**我的描述过期了。** 它不是事件通知,把它当事件通知用,是让 JavaScript 变贵的最常见方式。 行情回调是典型场景: ```js onQuote(quote, cx) { this.quotes.set(quote.symbol, quote); cx.notify(); // 每一跳都通知,包括没人在看的那些 } ``` 如果这个 View 从两千只订阅里只画二十只,这句 `notify` 会为它根本没画的标的的每一跳,付一次完整的面板描述。解法是一个条件,不是更快的 render: ```js onQuote(quote, cx) { this.quotes.set(quote.symbol, quote); if (this.visible.has(quote.symbol)) cx.notify(); } ``` 同一个想法推出三条规则: * **让变化的那个 View 失效。** 只属于某个子 View 的状态,就应该放在那个子 View 上、在那里 notify,而不是放在挂载它的父 View 上。 * **notify 得比帧率还密,也不会更贵。** 见下——手动攒批换不来什么,加条件才有用。 * **在 Host 一侧,`cx.notify()` 与 `ScriptView::refresh` 是两个不同的请求。** 单纯的 `notify` 只是重绘已有的描述。如果 Rust 改的是脚本通过 [HostModule](./host-module.md) 读到的状态,那描述已经陈旧,只有 `refresh` 能说明这一点。见 [Hosting](./hosting.md#host-状态变了-怎么刷新-view)。 ### notify 到底做了什么,谁在合并它 `cx.notify()` 不重建任何东西。它只是在这个 View 上置一个标志,表示「我的描述可能过期了」,然后请求 GPUI 绘制。重建发生在之后的那一帧里,而且只在标志仍然置位时才发生。 所以两帧之间的所有 notify 都会合并成一次 `render`——无论它们来自三个事件回调、一个循环里的 task,还是 Host: ```text notify notify notify ──▶ 一帧 ──▶ 一次 render() ``` 一个标志置三次等于置一次。什么都没有被丢掉:三个回调都执行了,状态也都改了;它们共享的只是随后那一次重建。 **这就给失效的开销划了一个上限:每个 View 每帧最多一次脚本 render。** 一秒跳一千次的行情,在 120 Hz 的屏幕上最多也只有每秒 120 次 render,而不是一千次。这也是为什么滥用 `notify` 表现为白做功,而不是失控。 运行时不在这之上再加任何自己的节流,也没有可调的参数。合并来自 GPUI 自己的调度,而且它绝不会把重建推迟到下一帧之后——所以它不带来延迟,而延迟正是下面那一对里的另一半。 ### 这份缓存占多少内存 一个 View 持有**两份**描述:它已发布的那份,以及被它替换掉的那份。留着第二份,是因为事件仍可能派发到一个已经被取代的帧上,而那一帧需要的回调属于那份较旧的描述。 没有第三份。发布新描述时最旧的那份会被丢弃,丢弃它同时会退役随它注册的那些回调。所以上限就是每个存活的 View 两份描述,而且不随时间累积:一个重渲染过一百万次的 View,持有的东西和一个只渲染过两次的 View 完全一样。关掉一个面板,它的 View 就没了,两份描述一起走。 这也是「该拆大 View 而不必怕拆」的另一个理由:一百个小 View 持有的是一百对小描述,加起来仍然只是把这个界面描述了两遍,而不是一百遍。 ## 帧率与呈现延迟是两类问题 一个运行中的界面可能出两种问题,而只有一种会体现在 FPS 上: ```text 渲染帧率 画面流畅吗? 状态 → 呈现 状态变了以后,多久用户才看得到? ``` 漏掉一次 `cx.notify()` 一帧都不会掉。GPUI 会继续以满帧率重放上一份完好的描述,于是 HUD 稳稳地读出 120 FPS,而界面显示的东西早就不成立了——然后在四分之一秒后,因为某件不相干的事让这个 View 失效,画面突然跳一下。所有渲染指标都会把这种情况判为健康。 | 症状 | 哪个数字不对 | 常见原因 | | --- | --- | --- | | 应用里什么都没变,窗口却卡 | 帧率 | 每帧要物化的描述过大,或虚拟列表在按行做额外工作;见[那次实测](./engine.md#那次实测) | | 行情在跑的时候窗口卡 | 帧率**和**失效频率 | 某个 View 重建得太频繁、太大,或两者都有 | | 画面很流畅,但数据慢半拍 | 呈现延迟 | 某次 `notify` 被漏掉、被压在 `await` 之后,或该用 `refresh` 的地方用了 Host 侧的 `cx.notify()` | 这两件事要分开诊断。FPS 从没掉过,并不能证明失效逻辑是对的。 ## 怎么读那几个计数器 运行时把这两类事件分开计数,Host 用 `runtime.read_metrics()` 读取——接口本身以及“留一个基线再相减”得到每秒速率的用法,见[观察它花了多少](./hosting.md#观察它花了多少)。 | 读数 | 它回答什么 | | --- | --- | | `script_renders()` | JavaScript 执行了多少次。跟着 `cx.notify()`、hot-reload 与主题切换走,永远不跟帧走 | | `materializations()` | Snapshot 变成元素多少次。跟着帧走 | | `mean_script_render()` | 一次描述要花多少,包含其中的 Host 调用 | | `mean_native()` | 其中有多少是在 HostModule 函数里,而不是在描述界面 | | `slowest_script_render()` | 这一段里最慢的那一次构建 | | `frame_script_calls()` | 从帧路径进入 VM 的次数——只有[虚拟列表](./elements.md)的 item 渲染器与 [Dock](./dock.md) 的 chrome 回调会计入 | | `structure_repeat_rate()` | 在有上一份描述可比的重建里,有多大比例产出了相同的**结构**——见下 | 一份读数的形状说明什么: * **每秒 `script_renders` 远高于数据实际变化的频率**——`notify` 正在为用户看不见的东西触发。加条件。 * **`script_renders` 正常,但 `mean_script_render` 高**——View 太大。把它拆开。 * **`mean_native` 占了 `mean_script_render` 的大部分**——成本在描述过程中调用的那些 Host 函数上,而不在描述本身。在 `render` 之前把它们一次性读进字段,不要按节点调用。 * **`slowest_script_render` 远高于均值**——某一次构建付了其余各次没付的东西:首次渲染物化的一份集合,或一个很少走到、却描述得多得多的分支。如果是均值整体在漂,那是系统负载,不是这个。 ## Snapshot 缓存止步于哪里 Snapshot 消除的是**没有变化**的成本,它不消除**变化很小**的成本。 一份 Snapshot 把结构和取值一起存着: ```text StockRow ├── Symbol("AAPL") ├── Price("230.42") └── Change("+1.42%") ``` 当价格变成 `230.51`,结构完全一样,只有一个叶子不同——但要表达这一点,唯一的办法就是产出一份新的描述,于是整个 View 被重新描述一遍:每个 `div()`、每个 `.gap()`、每个 `.bg()`、每一次进入 Rust 的跨越。这就是 dirty render 那条路径,行情一快,跑的就是它。 可用的杠杆就是本页开头那一个:**把必须重建的 View 缩小。** 在上面那块看板上,描述整块面板 0.315 ms,描述其中一行 0.012 ms——361 个节点对 9 个。把这一行放进它自己的 View,就是把前一个数字变成后一个,而这是今天就能做的。 `structure_repeats()` 与 `structure_changes()` 是用来核对这条线划得对不对的。它们统计一次重建产出的**结构**与被替换那份是否相同——只有其中的取值不同。如果某块面板报出来的比例很低,这件事本身就值得知道:你以为只有一个数字在变,实际上有东西在改变结构。 --- --- url: /zh-CN/base/text-selection.md description: 在多个自绘文本参与者之间实现窗口级文本选择。 --- # 文本选择 `gpui-base` 的文本选择基础设施允许一次拖拽跨越多个独立绘制的文本块,同时保留每个参与者自己的布局与绘制逻辑。它适合文档、消息流和其他由多个自定义元素组成、但用户期望像连续文本一样选择的界面。 ## 开始使用 核心由窗口级 `TextSelectionState`、稳定的 `SelectableTextHandle`、prepaint 阶段注册的几何信息,以及把全局选择投影到各文本 run 的辅助 API 组成。 ## 工作方式 每个参与者在 prepaint 时报告文本、屏幕几何和逻辑顺序。窗口状态根据指针锚点与当前点计算跨参与者的选择区间;绘制时,各参与者只查询落在自身范围内的切片并绘制选中背景。状态更新发生在事件回调中,不在 `render` 中写入。 ## 安装窗口元素 在窗口内容的稳定外层安装选择宿主,使它能够接收拖拽、释放和复制操作。宿主应覆盖所有需要共同选择的参与者,但不应改变它们的布局或样式。 ## 创建稳定句柄 每个参与者的 `SelectableTextHandle` 应保存在 entity 中或由稳定数据键派生。不要在每次渲染时创建新身份,否则拖拽途中重绘会丢失锚点、逻辑顺序或选择投影。 ## 在 prepaint 注册几何 文本最终布局完成后注册边界、行与字符位置。注册顺序必须与用户看到和辅助技术读取的逻辑顺序一致。动态插入、删除或移动参与者时,用稳定身份更新对应记录。 ## 把选择投影到文本 run 绘制各 run 前查询当前窗口选择,取得与本地文本相交的字节或字符范围,再把范围转换为文本系统需要的高亮几何。注意 UTF-8 边界,不要把字节偏移当作字符索引。 ## 完整 Rust 示例 ```bash cargo run -p gpui-base --example text-selection ``` ## 查询与控制窗口选择 应用可以读取当前选中文本、主动清除选择,并把复制命令连接到窗口状态。程序化更新后调用 `cx.notify()`,让所有受影响参与者重绘。 ## 高级参与者适配器 自定义文本布局可以实现参与者接口,提供命中测试、范围投影和文本提取。适配器应只桥接已有布局数据,避免在指针移动热路径中重新排版或分配大型缓冲区。 ## 用 scope 隔离模态内容 Dialog、Sheet 等模态内容应使用独立 scope,避免一次选择跨越背景与前景。模态关闭后恢复原 scope,不要让已卸载参与者留在窗口注册表中。 ## 集成检查清单 * 窗口只安装一个相应 scope 的选择宿主。 * 参与者身份和逻辑顺序跨渲染稳定。 * 只在 prepaint 注册最终几何,并正确处理 UTF-8。 * 拖拽更新有界,不在 render 中修改状态。 * 验证跨块拖拽、反向选择、复制、动态内容、滚动和模态隔离。 --- --- url: /zh-CN/shell/styling.md description: 流式样式接口、长度与颜色语法、语义主题 token,以及 hover / active / focus 状态样式。 --- # Styling 呈现权在脚本,所以一个应用的大部分代码都写在这里。所有元素接受同一套样式接口,写成一条流式链——与 Rust 侧写的一模一样: ```js render(cx) { return v_flex().size_full().bg(cx.theme().colors.surface).p(12).gap(8).rounded(6); } ``` ```rust // 同一件事,Rust 侧、基于 gpui-base。 v_flex().size_full().bg(surface).p(px(12.)).gap(px(8.)).rounded(px(6.)) ``` ## 统一的样式 API 所有样式方法都通过同一套链式 API 调用。根据 GPUI 是否能够自动导出方法信息,实现分为以下两类。 **无参方法来自 GPUI 的反射表。** `flex_col`、`items_center`、`gap_2`、`rounded_md`、`text_sm`、`size_full`、`font_semibold`、`truncate`、`cursor_pointer`——整个家族都取自 `gpui_kit::base::styled_ext_reflection_methods` 与 `gpui_kit::styled_reflection::methods`,零维护成本。这些名字没有一个写在运行时的任何地方。上游 GPUI 新增一个样式方法,脚本接口就有了,生成的 `gpui-kit.d.ts` 也有了。 本文写作时的这次构建里有 **3,148** 个。这个数字就是 GPUI 当前有多少个 `fn(self) -> Self` 形态的样式方法,GPUI 变它就变。`gpui-shell types` 会打印你这次构建的准确数字。 **有参方法无法被反射**,所以有 **57** 个是手工绑定的。这份列表是样式层里唯一手工维护的表,而且刻意保持很小。 两类方法不会重名。测试会检查每个名字只出现一次,发现冲突时直接让构建失败。 ## 长度 裸数字是像素。字符串自带单位。 ```js .p(12) // 12px .w("50%") // 父容器的一半 .h("auto") .gap("0.5rem") ``` 某个方法接受其中哪些,取决于**它的 Rust 签名**——因为正是那个签名在拒绝不合法的形式。GPUI 有三种互相嵌套的长度类型,运行时保留了这个区分而没有把它拍平: | 类型 | 接受 | 拒绝 | | ---------------- | --------------------------------------------- | ---------------- | | `Length` | 数字、`"12px"`、`"1.5rem"`、`"50%"`、`"auto"` | — | | `DefiniteLength` | 数字、`"12px"`、`"1.5rem"`、`"50%"` | `"auto"` | | `AbsoluteLength` | 数字、`"12px"`、`"1.5rem"` | 百分比、`"auto"` | ```text `p` cannot be "auto"; it expects a definite length such as 12 or "50%" ``` ```text `rounded` expects an absolute length such as 8 or "0.5rem"; percentages and "auto" are not allowed here ``` `"auto"` 的内边距和百分比的圆角,在底层布局引擎里没有含义;接受它们的运行时就必须自己发明一个含义。 ### 有参方法一览 | 家族 | 方法 | 参数 | | ------ | -------------------------------------------------------------------------- | ---------------- | | 尺寸 | `w` `h` `size` `min_w` `min_h` `min_size` `max_w` `max_h` `max_size` | `Length` | | 内边距 | `p` `px` `py` `pt` `pb` `pl` `pr` | `DefiniteLength` | | 外边距 | `m` `mx` `my` `mt` `mb` `ml` `mr` | `Length` | | 定位 | `inset` `top` `bottom` `left` `right` | `Length` | | Flex | `gap` `gap_x` `gap_y` | `DefiniteLength` | | Flex | `flex_basis` | `Length` | | Flex | `flex_grow` `flex_shrink` | 数字 | | 边框 | `border` `border_t` `border_b` `border_l` `border_r` `border_x` `border_y` | `AbsoluteLength` | | 圆角 | `rounded` 及 `_t` `_b` `_l` `_r` `_tl` `_tr` `_bl` `_br` 各形式 | `AbsoluteLength` | | 绘制 | `bg` `text_color` `text_bg` `border_color` | 颜色 | | 绘制 | `text_size` | `AbsoluteLength` | | 绘制 | `line_height` | `DefiniteLength` | | 字体 | `font_family` | 字符串 | | 绘制 | `opacity` | 数字 | `line_height` 是唯一值得专门记住的例外:**裸数字是倍数,不是像素**。`line_height(1.45)` 表示字号的 1.45 倍,因为业界其他地方都是这个含义,而 1.45px 从来不是任何人的意思。字符串仍然走普通的长度语法。 ### 刻意没有绑定的 `shadow`、`cursor`、`text_align`、`text_overflow`、`font_weight` 与 `scrollbar_width` 接受的是 Rust 结构体或枚举而不是标量,因此没有作为有参方法暴露。它们每一个都有一个被反射到、今天就能用的无参形式:`shadow_sm`、`cursor_pointer`、`text_center`、`truncate`、`font_bold`。真正的 shadow API 应当与 token 工作一起做,而不是做成一串位置参数。 ## 颜色 颜色通常从调用期主题读取。语义 token 名字符串仍为兼容性保留,固定颜色也可以使用十六进制字面量: ```js render(cx) { return element .bg(cx.theme().colors.surface) // 跟随主题 .text_color("#1e88e5"); // 不跟随 } ``` 调色板定义了十七个 token: | | | | ---- | -------------------------------------------------------------------- | | 基底 | `background`、`foreground` | | 表面 | `surface`、`surface_foreground` | | 强调 | `primary`、`primary_foreground`、`secondary`、`secondary_foreground` | | 弱化 | `muted`、`muted_foreground` | | 高亮 | `accent`、`accent_foreground`、`selection` | | 危险 | `destructive`、`destructive_foreground` | | 框架 | `border`、`input`、`ring` | 十六进制字面量接受 `#rgb`、`#rrggbb` 与 `#rrggbbaa`。 **优先使用 `cx.theme().colors` 中的值。** 字面量绕开了主题,切换主题时不会波及到它。示例应用恰好说明了这一点:它沿用 `crates/base/examples/showcase` 的视觉语言——那份 Rust 示例只能写死颜色,因为 Base 不带调色板——而示例应用读的是语义 token,因此同一份代码能跟随主题,Rust 版的 showcase 做不到。 拼错 token 会列出整个集合,而不是含糊地失败: ```text unknown color token `surfacee`; expected one of: background, foreground, surface, … — or a #rrggbb literal ``` ### 当前 token 来自哪里 gpui-shell 不拥有调色板或主题文件格式,而是读取 Host 提供的 `gpui_kit::base::Theme`。JavaScript 应用也可以通过 `set_theme({ appearance, tokens })` 替换同一份 Base Snapshot;主题名称和 registry 始终属于应用状态。 ## 状态样式 `hover`、`active` 与 `focus` 接受一个函数,函数收到一个用于收集声明的游离元素: ```js renderSave(cx) { return Button.new("save") .bg(cx.theme().colors.surface) .border(1) .border_color(cx.theme().colors.border) .hover((style) => style.bg(cx.theme().colors.muted).border_color(cx.theme().colors.foreground)) .active((style) => style.bg(cx.theme().colors.border)) .focus((style) => style.border_color(cx.theme().colors.ring)) .child("Save"); } ``` 函数的返回值会被忽略,所以链式写法和块状写法都能用。里面写的就是**普通的样式方法**——“什么是样式”没有第二套语法,上面所有长度与颜色规则原样适用。 有两处实现细节会泄漏到使用者这边,值得知道: * **`active` 与 `focus` 需要稳定的元素身份。** 普通 `div` 会按需获得一个,由它在描述中的位置推出;只要树是稳定的,这个身份跨渲染就是稳定的。`Button`、`Checkbox` 与 `Input` 本来就有。 * **`Switch` 会忽略状态样式。** switch 的根节点不是可交互元素——它的 track 才是——所以挂在根上的状态样式无处落地。运行时会记一条警告,提示改为给它外面那一行加样式,而不是不声不响地丢掉这条声明。 ## 滚动溢出内容 滚动属于元素行为,不是样式声明。先为 viewport 设置有限的宽度或高度,再指定它负责的滚动方向: ```js v_flex() .id("activity") .h(240) .overflow_y_scroll() .children(this.rows.map((row) => row)); ``` `.overflow_scroll()` 同时启用两个方向,`.overflow_x_scroll()` 只启用横向滚动,`.overflow_y_scroll()` 只启用纵向滚动。稳定的 `.id(...)` 会让原生滚动位置在多次脚本 render 之间始终归属于同一个 viewport。 对应的 `.overflow_scrollbar()`、`.overflow_x_scrollbar()` 与 `.overflow_y_scrollbar()` 保持相同的滚动行为,同时绘制 gpui-component 的原生 scrollbar。它们需要稳定的 `.id(...)`,确保每个 viewport 分别保留自己的 scrollbar 与滚动位置状态。 ## 主题值 从正在 render 或处理事件的 context 读取语义值: ```js render(cx) { return v_flex() .gap(cx.theme().spacing.md) .rounded(cx.theme().radius.lg) .bg(cx.theme().colors.surface) .child(`${cx.theme().appearance}: ${cx.theme().is_dark ? "dark" : "light"}`); } ``` 这个 Snapshot 是深度只读的。`theme()` 仍作为兼容入口保留,但优先使用 `cx.theme()`。应用可以从 event 或 task 调用 `set_theme({ appearance, tokens })`,传入自己管理的完整颜色、间距与圆角 token Snapshot。gpui-shell 只把它写入 gpui-base 并重建使用 token 的脚本 View,不拥有主题名称、palette 或文件格式。 ## 原生动画 `.transition(property, policy)` 与 `.spring(property, policy?)` 会为 `opacity`、`width`、`height`、`left` 和 `top` 的后续目标变化制作动画。动画由 GPUI 原生保留并逐帧推进:脚本改变目标并调用 `cx.notify()` 后,动画帧**不会重新进入 JavaScript**。 ```js div() .id("drawer") .left(this.open ? 320 : 16) .opacity(this.open ? 1 : 0.5) .transition("left", { duration: 220, easing: "ease-out" }) .spring("opacity", { response: 260, damping: 0.85 }); ``` 参与动画的长度目标**只能是数值像素**。`"50%"`、`"1rem"` 与 `"auto"` 之类相对值无法采样成稳定的原生通道,因此会被拒绝。请给元素稳定的 `.id(...)`(控件已使用构造器 id),否则树位置变化会改变动画 identity。 ## 未知方法 ```text unknown style method `text_colour` (did you mean: text_color?) ``` 建议来自对完整名字表的 Levenshtein 匹配,阈值卡得很紧——两次编辑,对较长标识符放宽到名字长度的三分之一。给错的建议比不给更糟。 这条信息背后有一处漂亮的机制,也解释了源码里的一个数字。QuickJS 报告缺失方法时只给一句 `TypeError: not a function`,**不带属性名**,所以拼错的样式名本来会毫无线索地到达使用者。用 `Proxy` 包住元素原型可以解决这一点——代价是实测占整个描述过程的约 30%(443 个节点从 1.09 ms 涨到 1.42 ms)。 于是运行时默认使用快速的普通原型,只有当一次渲染以 “not a function” 失败时,才**用带诊断 `Proxy` 的原型把这次渲染重跑一遍**,纯粹为了产出那条信息。出错是罕见的,每次渲染多付 30% 不是。 ## 还没有的东西 * **语义状态样式。** `gpui-base` 有一层 `state_style`,为 checked、selected、disabled 定义了优先级顺序。它还没有被绑定;今天请用 `.when(condition, …)` 表达这些状态。 * **Keyframe 动画。** 已有目标值 transition 与 spring;任意 keyframe 和逐帧 JavaScript callback 仍不存在。 * **样式中的 spacing 与 radius token。** 调色板带有 spacing 与 radius 标尺,但样式方法接受的是长度而不是 token 名——只有颜色会去查 token。应用自己定义一份标尺常量即可,示例里的 `SPACE` 对象就是这么做的。 --- --- url: /zh-CN/docs/comparison.md description: GPUI Kit 与 Iced、egui、Qt 6 的对比。 --- # 框架对比 GPUI Kit 与其他桌面 UI 框架的对比。表格由人工维护,如发现任何错误或过时信息,请提交 issue 或 PR。 | 特性 | GPUI Kit | [Iced] | [egui] | [Qt 6] | | ------------------- | -------------------- | ------------------ | --------------------- | ------------------------------------------------- | | 语言 | Rust | Rust | Rust | C++/QML | | 核心 | GPUI | wgpu | wgpu | QT | | 许可证 | Apache 2.0 | MIT | MIT/Apache 2.0 | [Commercial/LGPL](https://www.qt.io/qt-licensing) | | 最小二进制大小 \[^1] | 12MB | 11MB | 5M | 20MB \[^2] | | 跨平台 | 是 | 是 | 是 | 是 | | 文档 | 一般 | 一般 | 一般 | 良好 | | Web 支持 | 是(WASM) | 是 | 是 | 是 | | UI 风格 | 现代 | 基础 | 基础 | 基础 | | CJK 支持 | 是 | 是 | 差 | 是 | | Chart | 是 | 否 | 否 | 是 | | Table(大数据集) | 是(虚拟行、列) | 否 | 是(虚拟行) | 是(虚拟行、列) | | Table 列宽调整 | 是 | 否 | 是 | 是 | | 文本基础 | Rope | [COSMIC Text] \[^3] | trait TextBuffer \[^4] | [QTextDocument] | | Code Editor | 简单 | 简单 | 简单 | 基础 API | | Dock 布局 | 是 | 是 | 是 | 是 | | 语法高亮 | [Tree Sitter] | [Syntect] | [Syntect] | [QSyntaxHighlighter] | | Markdown 渲染 | 是 | 是 | 基础 | 否 | | Markdown 混合 HTML | 是 | 否 | 否 | 否 | | HTML 渲染 | 基础 | 否 | 否 | 基础 | | 文本选择 | TextView | 否 | 任意 Label | 是 | | 自定义主题 | 是 | 是 | 是 | 是 | | 内置主题 | 是 | 否 | 否 | 否 | | 国际化 | 是 | 是 | 是 | 是 | [Iced]: https://github.com/iced-rs/iced [egui]: https://github.com/emilk/egui [QT 6]: https://www.qt.io/product/qt6 [Tree Sitter]: https://tree-sitter.github.io/tree-sitter/ [Syntect]: https://github.com/trishume/syntect [QSyntaxHighlighter]: https://doc.qt.io/qt-6/qsyntaxhighlighter.html [QTextDocument]: https://doc.qt.io/qt-6/qtextdocument.html [COSMIC Text]: https://github.com/pop-os/cosmic-text \[^1]: 使用简单 Hello World 示例的 Release 构建。 \[^2]: [减小 Qt 应用程序的二进制大小](https://www.qt.io/blog/reducing-binary-size-of-qt-applications-part-3-more-platforms) \[^3]: Iced Editor: \[^4]: egui TextBuffer: --- --- url: /zh-CN/shell/overlays.md description: dialog、sheet 与 toast,它们的层叠与关闭顺序,以及为什么只能从事件中打开。 --- # Overlays Dialog、sheet 与 toast 是**Host**能力,通过全局的 `window` 访问。它们不是脚本画出来的东西。 Dialog 不是一个浮动的 `div`。它是窗口层叠顺序中的一个位置、一个焦点陷阱、一个 Escape 目标,以及一个关于“按下遮罩意味着什么”的承诺——而这些都必须由窗口的根 View 决定,因为只有能同时看到所有浮层的东西才能给它们排序。脚本自己画的 dialog 一样都拥有不了;两个脚本各画一个 dialog,拥有得更少。 所以脚本说的是**放什么**到用户面前,根 View 说的是它放在哪里、以及怎么离开。跨越这条边界的东西很少:一个返回元素的函数、一个贴靠的边、一句要显示的话。 这些 API 放在 `window` 而不是 `cx` 上,是因为 dialog 属于窗口,不属于打开它的 view:`cx.notify()` 重新渲染一个 view,`window.open_dialog()` 则改变窗口当前显示的内容。`gpui-component` 也采用相同的职责划分,因此 Rust 与 JavaScript 的 API 保持一致。以后若要暴露焦点、尺寸或窗口外观等能力,也可以继续放在 `window` 上。 ## 接口 `window` 是**全局的**。不需要 import——而且和 `cx` 不同:`cx` 是每次 Host 调用作为参数交给你的,`window` 则是本来就在作用域里。 回调参数如果叫 `window`,会遮蔽这个全局——这是普通的作用域规则,不是错误;而且将来即使某个回调真的传入一个 `window`,那也是同一个对象,因为 `window` 是 ambient 的:它读的是当前正在跑的那次调用。这也正是它今天不是参数的原因。Rust 里它必须是参数,因为 Rust 没有可读的 ambient 状态;这里可以读,`fs` 和 `store` 不是参数也是同一个道理。 ::: warning 不要照抄 Rust 的 `|event, window, cx|` 脚本的处理函数签名是 `(event, cx)`。写成三个参数会把 `window` 绑到 context 上,而 `cx` 是 undefined,报错读起来是 `close_dialog is not a function`。加上 `// @ts-check`,生成的声明会在你写下那一行就报出来。 ::: ```js const depth = window.open_dialog(() => confirmClear(count), { escape_dismissable: false, backdrop_dismissable: false, }); window.close_dialog(); // -> 有没有关掉东西? window.close_all_dialogs(); // -> 关掉了几个 window.has_active_dialog(); window.open_sheet(() => filters()); // 默认贴右边 window.open_sheet_at("left", () => nav()); window.close_sheet(); // -> 有没有关掉东西? window.has_active_sheet(); window.push_toast({ title: "Saved", description: "3 files", level: "success", timeout: 4000, id: "save" }); window.remove_toast("save"); window.clear_toasts(); ``` ## Dialog `window.open_dialog(content, options?)` 接受的是**一个返回元素的函数**,不是元素: ```text expected a function returning an element; open_dialog and open_sheet take a function, not an element and not a view class ``` 理由是生命周期,不是口味。元素属于创建它的那次 render pass 的 arena,而 dialog 活得比打开它的那次调用更久——在 open 时建出来的元素会属于错的那一趟。这个函数在 dialog 绘制时运行,此后每次重绘再运行一次,和 `render` 的契约完全一样。 **它闭包捕获的东西就是 dialog 的状态。** 没有 `props`:dialog 拿到要显示的内容的方式,和脚本里其他任何值一样——它就在作用域里。 ```js // confirm.js import { v_flex, h_flex } from "gpui-base"; export default (count, onConfirm) => () => v_flex() .w(360) .p(24) .gap(12) .child(`Delete ${count} completed items?`) .child("This cannot be undone.") .child( h_flex() .justify_end() .gap(8) .child(cancelButton(() => window.close_dialog())) .child(deleteButton((_event, cx) => { onConfirm(cx); window.close_dialog(); })), ); ``` ```js // main.js window.open_dialog(confirmClear(this.completed, (cx) => this.deleteCompleted(cx))); ``` 注意根 View 提供了什么、又没有提供什么。它提供遮罩、位置、层叠、焦点陷阱,以及承载内容的表面;宽度、内边距、边框、文字与按钮和这个运行时里的其他一切一样,都是脚本的。 | 选项 | 默认 | 作用 | | --- | --- | --- | | `escape_dismissable` | `true` | Escape 是否关闭它 | | `backdrop_dismissable` | `true` | 按下遮罩是否关闭它 | 未知选项会被拒绝而不是被忽略,这正是重点: ```text unknown option `escapeDismissable` for window.open_dialog(content, options); expected escape_dismissable or backdrop_dismissable ``` 一个被悄悄忽略的 `escapeDismissable` 看起来像是生效了,而那个 dialog 照样可以被 Escape 关掉。 `open_dialog` 返回的是**栈的新深度**,不是句柄。根 View 按位置而不是按身份寻址 dialog,所以句柄就必须承诺“关掉**这个** dialog”,而那不是一个存在的操作。深度才是脚本用得上的东西——用来断言确实打开了一个,或者退回到某个已知层级。`close_dialog` 返回它有没有找到可关的;`close_all_dialogs` 返回关掉了几个。 ::: warning 不要把 `cx` 带进 dialog 打开 dialog 的那个回调里的 `cx` 属于那个回调。等到 dialog 自己的按钮被按下时,它已经过期,使用它会报出 stale context 错误。请闭包捕获**数据**,并从 dialog 自身回调的参数里取 `cx`——上面的例子给 `onConfirm` 传的是一个 `cx`,而不是捕获一个,正是这个原因。 overlay 调用本身没有这个隐患:它们是 ambient 的,和 `fs`、`store` 一样,没有句柄可以留到调用之后。 ::: ## Sheet ```js window.open_sheet(() => filtersPanel(filters)); window.open_sheet_at("left", () => navigation()); ``` 同时最多打开一个 sheet。`window.open_sheet` 贴靠右边;`window.open_sheet_at` 接受 `"left"`、`"right"`、`"top"` 或 `"bottom"`。它没有任何选项,因为总共只有一个,并且在没有 dialog 压在上面时由 Escape 或它的遮罩关闭。 ```text unknown sheet placement `middle`; expected left, right, top or bottom ``` ## Toast Toast 是唯一**是数据而不是 View**的浮层——没有类、没有实例,也没有什么要脚本去渲染——所以它的全部内容以一个选项对象的形式跨越边界。 | 字段 | 默认 | 含义 | | --- | --- | --- | | `title` | 必填 | 用户读到的那句话 | | `description` | — | 第二行 | | `level` | `info` | `info`、`success`、`warning` 或 `error` | | `timeout` | 5 秒 | 毫秒数,或 `null` 表示一直留到被关闭 | | `id` | 自动生成 | 身份,用于替换与关闭 | 省略 `timeout` 使用默认值,显式写 `null` 让 toast 常驻,所以这两者不能合并成一个选项。 `id` 是把“反复失败”变成“一条长期存在的信息”而不是一堆通知的关键。`--watch` 的循环用的正是这个:一次失败的重载会用固定 id 发一条常驻的错误 toast,于是把一份坏文件存五次是替换而不是叠出五条;下一次成功重载再用 `remove_toast` 撤回它。 ```text unknown toast level `fatal`; expected info, success, warning or error ``` 同时挂载三条 toast。更早的留在管理器里,随着较新的离开再出现,所以一次爆发是被节流而不是被丢弃。 ## 窗口本身 同一个 `window` 全局也回答窗口自身的问题,而不只是它上面浮着什么。 ```js render(cx) { const { width, height } = window.viewport_size(); return v_flex() .when(width < 600, (el) => el.flex_col()) .text_size(window.rem_size() * 0.875); } ``` **度量在 `render` 中是合法的**,而且这正是它们的用处:一个要按窗口尺寸决定自身布局的 View,只能在绘制它的那一趟里问。 | 成员 | 说明 | | --- | --- | | `rem_size()` / `line_height()` | 窗口的排版度量,单位是像素 | | `viewport_size()` | 可绘制区域 | | `bounds()` | 窗口在屏幕上的位置与大小;比 viewport 大出标题栏那部分 | | `mouse_position()` | 指针位置,窗口坐标 | | `appearance()` | `"light"` 或 `"dark"` | | `is_window_active()` / `is_fullscreen()` / `is_maximized()` | 平台窗口的状态 | **改变窗口的调用在 `render` 中会被拒绝**,理由和 `cx.notify()` 一样:一帧去改自己正在绘制的窗口,就是这一帧在和自己较劲。 | 成员 | 说明 | | --- | --- | | `set_rem_size(size)` | 重新缩放所有以 rem 表达的尺寸 | | `refresh()` | 重绘窗口里的每一个 View | | `focus_next()` / `focus_prev()` | 把键盘移到相邻的一个 tab stop | | `dispatch_action(action)` | 沿本窗口的焦点路径派发一个 action | | `activate_window()` / `minimize_window()` / `zoom_window()` / `toggle_fullscreen()` | 平台窗口控制 | `zoom_window()` 是平台自己的“缩放”,不是缩放系数——要改的是后者的话,用 `set_rem_size`。 ## 层叠与关闭 从后往前绘制: 1. **内容**——脚本的根 View。 2. **Sheet**——最多一个,贴靠某条边。Sheet 是窗口里的一个*位置*,所以它位于 dialog 栈之下:从 sheet 里唤起的 dialog 必须可读,而在 dialog 之下唤起的 sheet 不能盖住它。 3. **Dialog 栈**——按打开顺序,最早的在最下面。 4. **Toast**——在所有东西之上。Toast 报告的是用户刚做的那个操作的结果,而“正开着一个 dialog”恰恰是这个结果最重要的时刻,所以它是唯一永不被遮挡的一层。 只有最上层的 dialog 画遮罩:三层 dialog 让窗口变暗一次而不是三次,而那唯一一层遮罩正是把活跃的 dialog 与它背后失效的那些区分开的东西。 关闭永远是**一层,绝不连锁**: * **Escape** 只关闭最上层的 dialog。下层 dialog 渲染时禁用键盘处理,所以连按 Escape 会一层一层退栈,并且在还有 dialog 打开时永远不会波及 sheet。 * `escape_dismissable: false` 撤掉的是**按键绑定**,不是底层的取消动作。脚本放在 dialog 里的关闭控件照样有效——这正是“不可关闭的 dialog”意味着用户必须回答它,而不是被困在里面。 * **按下遮罩**关闭最上层的 dialog,且仅当它是以 `backdrop_dismissable` 打开的。 * **回车在这一层什么都不做。** Base 的 dialog host 把回车视为“确认并关闭”;那属于 dialog 自己的主按钮,而主按钮归脚本所有,所以根 View 否决了内建的确认行为,而不是去猜哪份内容需要它。 * **Sheet** 只在没有 dialog 打开时由 Escape 或它的遮罩关闭,因为压在上面的 dialog 持有焦点。 * `close_all_dialogs` 是唯一会整体退栈的操作,而且它不动 sheet。 **焦点**沿着栈自身的历史恢复。打开浮层时记录当前焦点并把焦点交给浮层,关闭时恢复。关掉第二个 dialog 会把焦点还给第一个,关掉第一个则还给二者打开之前窗口所在的位置。Tab 与 Shift-Tab 遵守焦点陷阱,所以在浮层里按 Tab 是在浮层内循环,而不是走进它背后的内容。 ## `ScopePhase` 规则 **浮层只能从事件回调或任务中打开与关闭。** ```text window.open_dialog(content, options) is not allowed during the `render` phase; overlays may only be opened or closed while handling an event or a task ``` 打开或关闭浮层会修改窗口,而 `render` phase 正在读它。GPUI 的借用模型无法表达“脚本在这里可以 notify、在那里不行”,所以运行时显式携带 [`ScopePhase`](./state.md#scopephase),每一个浮层入口都拒绝 `render`、`layout`,以及根本不在任何 Host 调用中的情形——最后这种情况下也没有窗口可以触达。 拒绝信息会写明它是从哪个 phase 发出的,因为那是作者唯一的线索。 ## 浮层需要 `ShellRoot` 上述每一个调用最终都会到达窗口的根 View。第一层 View 不是 `ShellRoot` 的窗口会拒绝它们,并指明这是哪一类错误——Host 接线问题,不是脚本问题: ```text window.open_dialog(content, options) needs a ShellRoot as the window's first view; this window was opened with another view ``` 见 [Getting Started](./getting-started.md#把运行时接进-rust-应用)。 ## 还没有的东西 * **Dialog 的返回值。** `open_dialog` 返回的是深度,不是一个在 dialog 关闭时 settle 的 promise。请像上面的例子那样闭包捕获一个回调,或者让 dialog 写回打开方会读取的状态。 * **Tooltip 与右键菜单。** Popover 和 HoverCard 已可作为锚定浮层使用;专用的 tooltip 与右键菜单 API 尚未公开。 * **定位选项。** Dialog 居中,sheet 贴边,两者都不能指定位置。 --- --- url: /zh-CN/shell/state.md description: View、init 与 render、cx.notify()、留存的输入状态,以及异步工作。 --- # State and Views View 是这个运行时里唯一有身份、能跨帧存活、并且由 GPUI 拥有的东西。其余一切——元素、回调、传给某次调用的 `cx`——都属于产生它的那一次调用。 ## 定义 View ```js import { View } from "gpui-kit"; export default class Counter extends View { init(props) { this.count = props?.start ?? 0; } render(cx) { return v_flex().child(`${this.count}`); } } ``` `init` 在 View 创建时执行一次。跨帧存活的状态在这里建立——普通字段,以及 View 需要的任何[留存实体](#留存状态)。 `render` **返回一个元素、留存的 `Entity` 或字符串**,并且是在 View 被置为失效时执行,而不是每帧执行——见 [`render` 什么时候执行](#render-什么时候执行)。返回其它东西会立刻失败: ```text render(cx) must return an element, an Entity, or a string ``` `main.js` 必须 `export default` 一个 View 类。 Host 构造一个实例并把它挂载为窗口的根 View;default 导出不是类的模块会被拒绝,并说明原因。 永远不要把元素存在实例上。见 [Elements](./elements.md#元素是一次性的)。 ## `cx.notify()` 没有任何东西会自动重绘。这里没有 signal、没有 observable,也没有自动依赖追踪。改完状态,然后请求重新渲染: ```js add(cx) { this.items = [...this.items, { id: this.nextId, caption, done: false }]; this.nextId += 1; cx.notify(); } ``` 这与整个前端生态的默认假设正好相反,所以有必要直说:**这里没有 `useState`,也没有依赖数组。** 运行时不加自动追踪,有三个理由。 GPUI 本身就是显式 `notify` 的模型,两套响应式心智模型放进同一个应用会互相干扰而不是彼此配合。自动追踪意味着要把每个 View 实例包进 `Proxy`,这是渲染路径上一笔长期开销——而 QuickJS 没有 JIT 来摊薄它。而漏写 `notify` 的症状是确定的:界面不更新。找出这种问题,远比排查一个触发过多的自动系统便宜。 一次事件回调内的多次 `notify` 会合并为一次重绘——也合并为一次 `render`。 ## `render` 什么时候执行 `render` **不是**每帧执行一次。GPUI 会因为你的应用完全不知情的原因重绘——指针划过一个按钮、文本光标闪烁、列表滚动、动画推进——这些都不构成执行 JavaScript 的理由。 所以一次 `render` 调用描述的不是*这一帧*。它把界面描述一次,写进运行时保留的一份 Snapshot: ```text cx.notify() ──▶ render() ──▶ Snapshot ──┬──▶ 帧 ├──▶ 帧 └──▶ 帧 … ``` Snapshot 只在有东西让它失效时才重建: * 事件回调或异步任务里的 `cx.notify()` * [hot-reload](./getting-started.md) 替换了脚本 * 主题切换——因为 `bg(cx.theme().colors.surface)` 在 `render` 执行时记录真实颜色,已经烘进了 Snapshot * Host 调用 `ScriptView::refresh`——Rust 用它表示“我改了脚本会读到的状态”(通过 [HostModule](./host-module.md))。Host 侧单纯的 `cx.notify()` 只是重绘,不会跑脚本:这是两个不同的请求 其余情况都在 Rust 里复用你已经产出的那份描述,不执行任何 JavaScript。 三条值得记住的推论: **你的 `render` 成本跟着用户走,不跟着帧率走。** 一个每秒变化十次的 View,成本就是每秒十次渲染,无论窗口是 60 FPS 还是 120 FPS 在重绘。描述一个大面板之所以负担得起,正是因为它不会为了没有变化的内容被重复描述六十次。 **hover、focus 与 active 样式永远不回调脚本。** `.hover(s => s.opacity(0.8))` 在构建 Snapshot 时就被解析成原生样式描述,之后由 GPUI 自己套用。指针在界面上移动不会执行任何 JavaScript。[`Input`](#留存状态) 的光标与选区同理。 **一次失败的 `render` 不会毁掉界面。** Snapshot 只在 `render` 成功返回后才发布,所以抛异常的脚本会让上一份描述——以及随它注册的那些回调——原封不动地留着。失败以横幅的形式**盖在**仍然可用的界面之上,说明当前画面比最新版本旧了一版,并把详情交出去供粘贴;你的滚动位置和焦点都还在。首次渲染就失败的 View 没有可保留的东西,会拿到整屏的错误界面。两种情况下,在有东西再次让 View 失效之前,运行时都不会重跑那次失败的 `render`。 ## ScopePhase 每一次从 Rust 进入脚本的调用都会开启一个带 **phase** 的作用域,phase 决定这次调用的 `cx` 能做什么。 | `ScopePhase` | 时机 | 允许 | 不允许 | | --- | --- | --- | --- | | `render` | 构建元素树 | 读状态、构建元素、注册回调 | `notify`、打开浮层、创建留存状态 | | `event` | 处理点击或变更 | 全部 | 阻塞 | | `task` | 恢复异步工作 | 全部 | 阻塞 | | `layout` | 在 GPUI 布局过程中渲染一个虚拟化项 | 读状态、构建元素 | `notify`、打开浮层、创建留存状态 | `cx.phase()` 返回当前 phase,不在任何 Host 调用中时返回 `"none"`。 `cx.theme()` 返回这次调用中 gpui-base 当前语义主题的深度只读 Snapshot:既包含直接颜色角色,也包含 `colors`、`spacing`、`radius`、`appearance` 与 `is_dark`。优先使用它,而不是兼容用的 `theme()` 导出,因为 context 写法明确表达了调用生命周期与当前 Host 主题。 每一条拒绝都是一条具体信息,而不是未定义行为: ```text cx.notify() is not allowed during the `render` phase; request a re-render from an event handler instead ``` 渲染中通知自己是一个死循环,所以它被拒绝而不是被延后。 ## 两种 `cx` 在 GPUI 里 `&mut Window` 与 `&mut App` 是借用:它们的存活期恰好是一次调用。脚本对象比任何借用都活得久,所以脚本侧的 `cx` 不能持有它们。GPUI 为确实需要跨调用持有的代码准备了第二种——`AsyncApp`,由 `cx.spawn` 交给它的闭包——这里也一样。 **`Context`** 是 `render` 和每个事件处理器收到的那种。它持有一个 **generation 编号**,每次使用都与实时的作用域栈比对,所以把它留到调用之外得到的是一条错误,而不是一帧被破坏的画面: ```text cx is no longer valid: it was captured during an earlier call and used later. Use cx.spawn or take cx from the callback arguments instead. ``` **`AsyncContext`** 是 `init` 收到的那种,也是 `cx.spawn` 与 `cx.timer` 交给回调的那种。它不指名任何一次调用——用到它时才解析当时正在执行的那一次——所以 `await` 不会把它带走: ```js async save(cx) { await cx.sleep(100); cx.notify(); // 同一个 cx,仍然是对的那个 } ``` 这三处正是职责为「安排或延续比启动它的那次调用活得更久的工作」的地方。其余场合要的就是严格的那种,被告知「你留得太久了」正是它的价值所在。 `cx` 上除了函数什么都没有——`Object.keys(cx)` 只看得到方法,看不到 generation——所以脚本无法伪造一个。 没有第三种拿到它的办法。模块顶层和裸 `constructor` 不会被交给 context,也无从索取——这是设计而非缺口:GPUI 根本没有模块顶层,在那里启动的工作不属于任何 View,没有东西拥有它,也没有东西取消它。把它放进 `init`,那正是 View 被交给 context 的地方。 ## 留存状态 View 自己的字段放普通数据。带有跨帧机制的东西——文本框的内容、光标位置与撤销历史——存放在 GPUI 实体里,脚本持有一个**句柄**。 ```js import { InputState, Input } from "gpui-base"; init() { this.draft = InputState.new({ placeholder: "What needs doing?" }); this.draft.on("submit", (_event, cx) => this.add(cx)); } render(cx) { return Input.new(this.draft) .flex_1() .h(28) .px(8) .border(1) .border_color(cx.theme().colors.input) .bg(cx.theme().colors.surface) .text_size(12); } ``` | 调用 | 作用 | | --- | --- | | `InputState.new({ placeholder, value })` | 创建状态,两个选项都可省略 | | `state.value()` | 当前文本 | | `state.set_value(text)` | 替换文本 | | `state.on(event, handler)` | 订阅,见下 | | `state.release()` | 释放句柄 | | `Input.new(state)` | 渲染它的元素 | **在 `init` 或事件回调里创建,绝不要在 `render` 里创建。** 创建实体需要一个实时窗口,而 `render` 本来也是最不该做这件事的地方: ```text InputState.new(...) cannot run during render; create state in init() or in an event handler and keep it on the view ``` 脚本持有的是句柄而不是实体——实体归 GPUI 所有。使用已释放的句柄会抛异常,而不是返回 `undefined`;因为 `undefined` 在 JavaScript 里往往飘出很远才炸,那时源头已经找不到了: ```text this input state has been released ``` `Input` 是唯一由运行时给出默认值的元素,而且只有三条:垂直居中的一行、占满宽度、点击框内任意位置获得焦点。每一条都是脚本可以覆盖、但不该被迫记住的默认——没有第一条,文本会贴在给定高度的顶部,在屏幕上看起来像 bug 而不是缺一条样式。 ### 输入事件 ```js this.draft.on("submit", (event, cx) => this.add(cx)); ``` | 事件 | 触发于 | | --- | --- | | `change` | 文本发生变化 | | `submit` | 按下回车;`event.secondary` 与 `event.shift` 说明按法 | | `focus` | 获得焦点 | | `blur` | 失去焦点 | 与渲染期注册的 `on_click` 不同,这个订阅**活得比创建它的那次渲染更久**。订阅由运行时的句柄存储持有而不是由脚本持有,因为脚本没有地方放它,而“因为某个值被回收所以处理函数不再触发”是那种没人找得到的 bug。它随句柄一起释放。 事件名拼错会列出合法值: ```text unknown input event `changed`; expected one of: change, submit, focus, blur ``` ### 日历状态 `CalendarState` 是同样的模式,留存的东西不同:正在看的是哪个月、选中的是哪一天,以及由此推出的那张日期网格。 ```js init(_props, cx) { this.calendar = CalendarState.new(); this.calendar.on("change", (date, cx) => this.pick(date, cx)); } render(cx) { const grid = this.calendar.month_days()[0]; return v_flex().children( grid.map((week) => h_flex().gap(4).children( week.map((day) => Button.new(day) .selected(day === this.calendar.value()) .on_click((_e, cx) => { this.calendar.set_value(day); cx.notify(); }) .child(String(Number(day.slice(8)))), ), ), ), ); } ``` `month_days()` 是它存在的理由:哪些日期落在哪一周、相邻月份的日子补在哪里、这个月需要几行。格子是你自己画的——base 的 `Calendar` 元素**没有**绑定,因为它遍历同一份网格、每个格子调用一次渲染回调,一帧最多四十二次跨语言调用,而且发生在 GPUI 的 layout 过程里,为的是一批本身不带行为的格子。 日期一律是 `"YYYY-MM-DD"`,区间是 `[start, end]`,没选是 `null`。区间即使终点还没定也保持成对——`["2026-08-03", null]` 不会塌成它的起点,因为“选了一天”和“区间开了个头”对 base 是两种状态,它自己的逻辑在这上面分支。 | 方法 | 说明 | | --- | --- | | `CalendarState.new()` | 创建状态;和其他留存状态一样,只能在 `init` 或事件处理器里 | | `month_days()` | 网格:按月分组的“周”,每周固定七天 | | `year()` / `month()` / `today()` | 网格对应的年月,以及创建时读到的今天 | | `value()` / `set_value(next)` | 选中的日期 | | `next_month()` / `prev_month()` | 前后移一个月;在 `render` 中不合法 | | `on("change", handler)` | 唯一的事件,报告一个日期被选中 | | `release()` | 丢弃句柄 | ## 异步工作 脚本代码用的是普通的 JavaScript 异步方式——`async` 函数与原生 promise。运行时补上的是裸 QuickJS 没有的那部分:一个时钟、待执行工作的 owner,以及负责推动 job 队列的人。 | 导出 | 作用 | | --- | --- | | `cx.sleep(ms)` | 在 GPUI 的 foreground executor 上,`ms` 之后 resolve 的 promise | | `cx.spawn(body, opts?)` | 调用 `body(cx)` 并接管它返回的 promise | | `cx.timer.after(ms, handler, opts?)` | 调用一次 `handler(cx)` | | `cx.timer.every(ms, handler, opts?)` | 反复调用 `handler(cx)` | 调度挂在 `cx` 上,因为 GPUI 就是这么放的——`App::spawn`,以及由 context 交出的 executor 上的 timer。不需要 import 任何东西。 它们产生的工作全部在主线程上运行。脚本可见的东西从不离开主线程:这里没有 `Worker`,VM 与 GPUI 的 `App` 都是主线程独占的。 ```js flash(cx) { this.saved = true; cx.notify(); cx.spawn(async (cx) => { await cx.sleep(1500); this.saved = false; cx.notify(); }); } ``` 这段不需要任何 import:`cx` 就是处理器的第二个参数,而它的 body 收到的 `cx` 是能挺过 `await` 的异步那种。 **`cx.spawn` 会接管 promise,这正是它的意义。** 未处理的 rejection 是 JavaScript 最常见的静默失败:工作停了,界面保持原状,什么都没写到任何地方。在这里它会带着脚本自己的调用栈进入 `tracing::error!`。 ### 归属与取消 每个任务都属于某个 View——`opts.owner`,或者创建它时正在运行的那个 View。任务持有弱引用,所以当发起这项工作的面板消失时,回调会被跳过,而不是写进一份再也不会被渲染的状态。 ```js const handle = cx.timer.every(1000, (cx) => this.tick(cx)); handle.cancel(); handle.is_done(); ``` `owner: null` 表示退出这套归属、比任何 View 都活得久;它是今天除了当前 View 之外运行时唯一接受的值。 取消一个 `sleep` 会让它的 promise **永远 pending**。这就是取消对 promise 的含义:后续代码不执行,也不为一段主动要求停止的代码凭空发明一个错误。 `timer.every` 的间隔从上一次调用结束开始计时,所以慢的处理函数会推迟下一次 tick,而不是把 tick 堆起来。 ### Timer 与标准 Host API ```text setTimeout -> cx.timer.after(ms, callback) setInterval -> cx.timer.every(ms, callback) clearTimeout / clearInterval -> 对 after / every 返回的 Task 调用 cancel() ``` `setTimeout`、`setInterval`、`clearTimeout` 与 `clearInterval` 都是会抛错的 stub。一次性工作使用 `cx.timer.after`,重复工作使用 `cx.timer.every`;要停止其中任意一种,都对返回的 `Task` 调用 `cancel()`。全局 `fetch`,以及 [Capabilities](./capabilities.md) 中记录的安全标准模块(包括 `websocket`),都是真实的异步 Host API。CommonJS `require` 仍不可用;请使用 ES module。 浏览器 DOM 与存储并不存在:没有 `document` 或 `localStorage`。全局 `window` 是 gpui-shell 用来承载 dialog、sheet 与 toast 的 overlay host,并不是浏览器 `Window`,也不提供 DOM。 ## 还没有的东西 * **全局与跨 View 状态。** 除了 [Capabilities](./capabilities.md) 里的持久化层和普通模块作用域,没有别的 store。 * **Action 与快捷键。** `gpui.action` 与 `gpui.keymap` 设计了但没有绑定;今天唯一的按键处理是 `ShellRoot` 安装的那几个(Tab、Shift-Tab、Escape)。 * **多窗口。** 窗口由 Host 打开,没有 `gpui.open_window`。 * **`gpui.gc_stats()`**,以及会读取它的调试面板。 --- --- url: /zh-CN/shell/examples.md description: 独立与嵌入式完整应用,包括留存状态、 HostModule 与原生动画。 --- # Examples 仓库自带四个示例,合起来覆盖独立应用、可停靠布局、由 Host 状态驱动的脚本,以及动画帧完全不进入 JavaScript 的原生 motion。 | | 怎么跑 | 展示了什么 | | --- | --- | --- | | [Todo list](#todo-list) | 一个独立应用 | 脚本这一侧的全部:留存输入、dialog、toast、受授权约束的存储、资源、类型 | | [工作区](#工作区) | 一个独立应用 | 可停靠布局:熬过重启的面板,以及全部由脚本绘制的 chrome | | [报价面板](#报价面板) | gallery 里的一块面板 | Host 那一半:HostModule 、一个实体被两种语言读取、实时的成本读数 | | [原生动画](#原生动画) | gallery 内独立的脚本 View | 由 GPUI 保留并采样的像素目标 transition 与 spring | ## 一个完整的应用 这里的示例每个都只讲一件事。想看一个完整产品放在一个仓库里——OAuth、实时 WebSocket 报价流、虚拟化自选列表、用留存嵌套 View 承载的价格图表,以及它自己的 Rust Host 二进制——见 [**longbridge/longbridge-lite**](https://github.com/longbridge/longbridge-lite)。 它是一个只读的 Longbridge 桌面客户端,几千行 JavaScript,也是目前基于这个运行时 写出来的最大的东西。 ## Todo list ```bash cargo run -p gpui-shell -- examples/js_todolist ``` `examples/js_todolist/` 的目的是把整个运行时都跑一遍,而不是做到最小——`gpui-shell` 哪里坏了,通常先在这里露出来。 ```text main.js View:状态、筛选、所有事件处理 ui.js 呈现层,以函数形式导出 storage.js 持久化,以及没拿到授权时怎么办 confirm.js 确认 dialog,它自己也是一个 View icons/ 四个 SVG,相对应用根目录解析 gpui-kit.d.ts 自动生成;jsconfig.json 与 types.d.ts 把类型接上 ``` 其中有四件事值得照抄。 **`ui.js` 是一个由函数构成的组件库。** 它导出 `label`、`muted`、`title`、`button`、`iconButton`、`checkbox`、`field`、`row`、`surface`、`rule` 与 `emptyState`,于是 `main.js` 读起来就像在用一个组件库: ```js export const label = (value, cx) => div().text_size(12).line_height(1).text_color(cx.theme().colors.foreground).child(value); export const surface = (cx) => v_flex().flex_1().bg(cx.theme().colors.surface).border(1).border_color(cx.theme().colors.border).overflow_hidden(); ``` `main.js` 把当前 `cx` 传给这些 helper,由 helper 直接通过 `cx.theme()` 读取 token。这样做没有额外代价,因为[一次函数调用产出的正是一份新的描述](./elements.md)。这也是对“基础层不提供任何带样式的控件”的回答——带样式的那一层你在自己的文件里写一次,从此不必重复。 **存储是把拒绝吸收掉,而不是先去问有没有权限。** Host 没授予存储时 `store` 会抛异常,而这是关于 Host 的一个事实,不是这个应用的错误: ```js export function load() { try { const saved = store.get(KEY); return Array.isArray(saved) ? saved : []; } catch (error) { console.warn(`todolist: storage unavailable, starting empty (${error.message})`); return []; } } ``` `save()` 会返回这次写入有没有落盘,页脚则把它显示在界面上——“Not saved — this host did not grant storage, so the list lasts for this run only”。在边界上把拒绝吸收掉,然后如实告诉用户。 **dialog 是一个函数,不是一个元素。** `confirm.js` default 导出一个返回内容函数的函数;`main.js` 用 `window.open_dialog(confirmClear(count, onConfirm))` 打开它。数量和回调是闭包捕获的,不是交接过去的。见 [Overlays](./overlays.md)。 **类型是配好的,一共三个文件。** `jsconfig.json` 打开 `checkJs`,`gpui-kit.d.ts` 由 `gpui-shell types` 生成,`types.d.ts` 放这个应用自己的形状——`Todo`、`Filter`。编辑器补全与 `checkJs` 报错从此就能用,不需要任何构建步骤。 ## 工作区 ```bash cargo run -p gpui-shell -- examples/js_dock ``` `examples/js_dock/` 是一个可停靠的工作区——左边是文件列表,中间是文档,布局会以你离开时的样子回来。 ```text main.js 工作区本体:面板、dock 与持久化 ui.js chrome:标签页、dock 外框、落点提示 ``` 其中三件事是重点。 **base 不画 chrome,所以这些全在 `ui.js` 里。** 标签栏、dock 的标题条、折叠控件、缩放把手与落点提示都是用普通样式接口写出来的普通元素。一个这些都没有的 area 照样能停靠、拖动、调整大小、持久化,只是除了面板本身之外什么都不画。 **标签页带的是命令,不是处理器。** chrome 描述会缓存到原生状态改变为止,所以其中的脚本事件处理器没有可靠的生命周期。`select_tab(group, tab.index)` 与 `close_panel(group, tab.id)` 完全不携带脚本值——它们只是指名某个容器、以及要请它做什么。 **面板就是多了两个方法的 View。** `Document.serialize()` 返回它的标题与编辑次数;重启之后 `deserialize(data)` 把它们收回来。关于这块面板的其他一切——它在哪、是否正在显示——都是布局的事,永远不会传到脚本。 完整接口见 [Dock 与面板](./dock.md)。 ## 报价面板 ```bash cargo run -- shell ``` gallery 的 Shell story 并排跑着两块面板:左边那块由 Rust 的 `shell_story.rs` 画,右边那块由 JavaScript 的 `crates/story/js/quotes/main.js` 画,两边读的是同一份数据。 脚本自己不持有任何状态。这块行情板是一个 Rust 的 `Entity`,从 story 在运行时启动前注册的 [HostModule](./host-module.md) import 进来: ```text import { quotes, ticks, watch, watch_all } from "market"; ``` 主题值来自调用作用域内的 `cx.theme()` Snapshot,而不是第二个 HostModule 。 因为两块面板读的是同一个实体,两边一旦对不上就会立刻看出来——这也是它算一个测试而不只是演示的原因。改 `main.js` 就能改变右边那块面板,中间不需要 `cargo build`;面板旁边有一个 “Reload script” 按钮。 底下就是这套文档反复引用的那组计数读数:每秒的脚本次数对每秒的帧数,还有一个 feed 选择器,可以让其中一个动而另一个不动。这就是[那条性能主张](./index.md#性能-脚本不在每一帧里)在一个运行着的窗口里的样子。 ## 原生动画 `crates/story/js/motion/main.js` 刻意使用与 quote benchmark 分离的 `ScriptView`,避免动画活动污染 render-frequency 测量。它可以在 `.transition(...)` 与 `.spring(...)` 之间切换,再重新设定 opacity 以及像素值的 width、height、left 与 top。 脚本只运行一次来发布新目标。后续每一个动画帧都由 GPUI 原生调度与采样,不会重新进入 JavaScript。示例只使用数值像素目标——没有 `rem`、百分比或 `auto`——并使用稳定 id,让留存通道能够跨 description 重建继续存在。 ## 从哪儿开始 把 `examples/js_todolist` 复制到你自己的目录里跑起来——它是一个类型都配好了的完整应用。把 `main.js` 削回到一个只有 `init` 与 `render` 的 `View`,留着 `ui.js`,再从那里往上加。 要写 Host 的话,`crates/story/src/stories/shell_story.rs` 是另一侧的可用参考:它构建运行时、注册 HostModule 、挂载一个 `ScriptView`,并按需重载它。[Hosting](./hosting.md) 走的是同样这几个调用。 --- --- url: /zh-CN/docs/components.md --- # 组件 ## 基础组件 * [Accordion](accordion) - 折叠内容面板 * [Alert](alert) - 多种变体的提示消息 * [Attachment](attachment) - 文件与媒体附件表面 * [Avatar](avatar) - 用户头像与回退文本 * [Badge](badge) - 徽标与数量指示器 * [Bubble](bubble) - 支持对齐与 reaction 的聊天消息表面 * [Button](button) - 支持多种样式的按钮 * [Checkbox](checkbox) - 二元选择控件 * [Icon](icon) - 图标展示组件 * [Image](image) - 带回退能力的图片展示 * [Marker](marker) - 会话状态与分隔标记 * [Message](message) - 可组合的聊天消息结构 * [MessageScroller](message-scroller) - 支持尾部跟随的虚拟消息列表 * [TextView](text-view) - Markdown 与 HTML 文本渲染 * [Tooltip](tooltip) - 悬浮提示 ## 表单组件 * [Input](input) - 单行文本输入与类输入控件 * [Textarea](textarea) - 支持固定行数或自动增高的多行文本输入 * [Editor](editor) - 支持语法高亮、行号和折叠的源代码编辑器 * [Select](select) - 选项选择器 * [Combobox](combobox) - 可搜索的单选或多选下拉组件 * [NumberInput](number-input) - 数字输入 * [DatePicker](date-picker) - 日期选择器 * [OtpInput](otp-input) - 一次性验证码输入 * [ColorPicker](color-picker) - 颜色选择器 * [Form](form) - 表单容器与布局 ## 布局与高级组件 * [Command](command) - 用于搜索与快捷操作的命令面板 * [Dialog](dialog) - 对话框与模态窗口 * [Popover](popover) - 浮层内容 * [Resizable](resizable) - 可调整大小的面板 * [Scrollable](scrollable) - 可滚动容器 * [Sidebar](sidebar) - 侧边栏导航 * [StatusBar](status-bar) - 底部状态栏,含左/中/右三区 * [Chart](chart) - 图表组件 * [DataTable](data-table) - 高性能数据表格 * [Dock](/zh-CN/docs/dock) - 支持标签、分割与状态持久化的生产级 Dock 布局 * [Tree](tree) - 树形结构组件 * [VirtualList](virtual-list) - 大数据量虚拟列表 ## 翻译说明 组件页已经预置中文路由结构,尚未完成的页面会先显示中文占位说明,并回链到英文原文: * [English version](/docs/components/index) --- --- url: /zh-CN/docs/coding-guides.md description: 构建可维护 GPUI Kit 应用的架构、代码风格与命名规范 --- # 编码指南 本指南总结 GPUI Kit 中经过长期实践验证的应用架构与代码模式,面向工程师和 coding agent。请先阅读[设计指南](./design-guides.md):代码结构的职责是保存产品意图,而不是替代产品设计。 本文是一份规范性指南:**必须**表示生命周期、正确性或生态约束;**应该**表示默认架构,偏离时需要有具体理由。精确方法签名以当前源码和 API 文档为准。 ## 架构总览 依赖只向下。上层负责领域语义与流程协调,下层负责可复用的表现和行为。通用 component 不能依赖具体应用页面;`gpui-base` 不能依赖 GPUI Component 主题。 边界定义如下: * **app shell:** 组合窗口与 Feature Crate,不承载具体 Feature 逻辑; * **feature crate:** 在一个公开边界内组织同一业务能力的 model、service、view、command、dialog 与 workflow; * **app component:** 跨 Feature 复用且带有领域语义的模式; * **gpui-component:** 带主题的通用 UI; * **gpui-base:** 不带产品表现的可复用行为与 geometry。 ### 大型应用按业务能力组织 crate 在大型 Rust 应用中,一个完整 Feature 通常应该成为独立 crate,而不是继续向全局`views`、`models` 或 `modals` 目录添加文件。同一能力的 model、view、command、dialog 与 workflow 应该放在一起。编辑 Workspace 的 dialog 属于 Workspace Feature;只有可复用的 Dialog 基础组件属于 UI library。 ```text crates/ ├── app/ │ └── src/main.rs # 组合窗口与 Feature ├── workspace/ │ └── src/ │ ├── lib.rs # Feature 的公开边界 │ ├── model.rs │ ├── commands.rs │ ├── workspace_view.rs │ └── rename_dialog.rs ├── search/ │ └── src/ │ ├── lib.rs │ ├── model.rs │ ├── commands.rs │ ├── search_view.rs │ └── filters.rs ├── settings/ │ └── src/ │ ├── lib.rs │ ├── model.rs │ ├── settings_view.rs │ └── account_dialog.rs └── shared/ └── src/ ├── lib.rs └── recent_items.rs # 多个 Feature 共同使用的稳定能力 ``` 不要反过来建立全局 `models/`、`views/`、`modals/` 与 `commands/` 目录。这种方式只是按实现角色给文件分类,却会把每个 Feature 拆散到整个应用中。 App Shell 只组合 Feature Crate,尽量不承载 Feature 逻辑。Feature 可以依赖稳定的共享能力与 UI 基础设施,但不能反向依赖 App Shell,也不能进入另一个 Feature 的内部实现。两个 Feature 需要协作时,优先使用明确的 command、event、数据类型或小型共享 service,而不是让彼此的 view 形成依赖。只有一项能力已有清晰名称和两个以上真实使用方时,才提取共享 crate。 crate 边界也是工程边界。它让 Cargo 只重编译和测试较小的依赖子图,让所有权直接体现在`Cargo.toml` 中,并收紧一次修改需要评审和回归验证的范围。它还让删除 Feature 成为真实的架构检验:如果删除一个 Feature 仍需在全局 view 与 modal 目录中到处搜索,它从未真正解耦。 不要为每个页面或 helper 创建 crate。只有当一项能力拥有独立状态与生命周期、稳定的公开边界,或已经大到值得独立编译和测试时才拆分。依赖必须无环,并始终指向更小、更稳定的 crate。 ## 初始化与 Root 所有权 在创建组件 view 前只初始化一次 GPUI Component,并让每个窗口的第一层是 `Root`: ```rust app.run(move |cx| { gpui_kit::init(cx); cx.spawn(async move |cx| { cx.open_window(WindowOptions::default(), |window, cx| { let workspace = cx.new(|cx| Workspace::new(window, cx)); cx.new(|cx| Root::new(workspace, window, cx)) }) .expect("failed to open window"); }) .detach(); }); ``` `Root` 不只是容器。它协调 dialog、sheet、notification、tooltip/menu layer、modal focus restore、focus trap 与窗口级文本选择。一个窗口只能有一个 `Root`;绕过它的 UI 可能静止时正常,却会在 overlay 嵌套或快速切换 focus 时失效。 ## 理解 GPUI 阶段与上下文 GPUI 使用 retained state 与 declarative rendering。`Entity` 跨 frame 存活;`render` 返回的 element tree 只描述当前 frame。必须始终区分持久状态与一次 render 的输出。 * `Context`:修改当前 entity,建立 listener,emit event,并通知 observer; * `App`:访问 global 以及读取或更新 entity,不表示当前 element 拥有这些状态; * `Window`:拥有窗口 focus、Action dispatch、input、element keyed state、measurement 与 animation-frame request; * layout、prepaint、paint 属于后续 phase,只有需要 resolved geometry 时才使用相应 hook。 不能把 `&mut Window`、`&mut App`、`&mut Context<_>` 保存到当前调用之外。应保存`Entity`、`WeakEntity`、`FocusHandle`、scroll handle 或领域 ID 等 typed handle。 ## 选择正确的组成单元 ### 值类型 UI 使用 `RenderOnce` 当所有输入都由 caller 提供,且 element 不需要跨 frame 保存 application state 时,使用`RenderOnce` 或 `IntoElement`。纯 presentation wrapper 和小控件通常属于此类。 ```rust #[derive(IntoElement)] struct EmptyState { title: SharedString, } impl RenderOnce for EmptyState { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { div() .v_flex() .gap_2() .items_center() .text_color(cx.theme().muted_foreground) .child(self.title) } } ``` ### 跨 frame 行为使用 `Entity` 行为需要 observation、subscription、focus、async、history、measurement 或增量更新时,使用 entity-backed `Render` view。Entity 存在 owning view 中,不能在每次 `render`里重建。 ```rust struct SearchView { query: Entity, } impl SearchView { fn new(window: &mut Window, cx: &mut Context) -> Self { let query = cx.new(|cx| InputState::new(window, cx).placeholder("Search…")); Self { query } } } ``` 不要把每个视觉 fragment 都做成 Entity;生命周期和协调有成本,只有 retained identity 确实重要时才建立 entity boundary。 ### 元素、视图与行为系统各有职责 生态中的 public unit 并非一种模板: * Button、Checkbox、Link、Tabs 等 semantic element; * Dialog、Popover、Select、Combobox 等 compound behavior root; * Input、Table、Tree、Dock、notification 等 entity-backed system; * positioning、virtualization、scroll、focus trap、motion、history、measurement 等 infrastructure。 Element 内部可以很复杂,但对 caller 仍是值。Stateful system 可以通过 render callback 让应用拥有表现,同时不重做行为。Public seam 应由行为决定,而不是 renderer 中有多少个 `div` 决定。 ## 状态所有权 状态应放在能够保持其正确性的最窄 owner 中: * domain state 属于 model 或 feature view; * transient view state 属于绘制它的 view; * reusable behavior state 属于为该行为设计的 component state; * 少量 element-local state 可以使用 GPUI keyed state; * 共享 application service 可以使用 GPUI global。 普通 selection/toggle 优先采用 controlled value:传入当前值,接收 requested change,由 owner 更新,再 render。Callback 表达 intent,不能建立第二份隐藏真相。 ```rust Checkbox::new("show-hidden") .checked(self.show_hidden) .label("Show hidden files") .on_click(cx.listener(|this, checked, _, cx| { this.show_hidden = *checked; cx.notify(); })) ``` 改变渲染结果后调用 `cx.notify()`;owner 需要处理语义事件时使用 `cx.emit(...)`;生命周期跟随 Entity 时使用 `cx.subscribe(...)` / `cx.observe(...)`。API 要求时必须保留返回的 Subscription。 读取或派生值不能触发 notify。禁止在 `render` 中无条件 notify,否则会永久重绘。形成同一 invariant 的字段应一次更新,只 notify 一次。无法获得 context 的 reusable state API 必须明确让 owner 负责 emit/notify。 ### 防止状态反馈环 文本输入、selection、filter 和 controlled popup 常有两条路径:外部 owner 设置值,以及用户请求新值。同步外部值时不能再次通过 user callback 回传。使用 origin/revision 或 coherent snapshot 比较,确保一次逻辑变化只报告一次。Callback 可能同步关闭、替换或更新当前组件时,调用路径必须可重入。 ## 稳定标识 `ElementId` 是行为契约的一部分。它为元素提供稳定标识,并作为元素局部状态或组件状态的键。组件也可以将它用于自身的焦点、测量或动画标识;焦点与滚动通常仍由各自的 handle 管理。 * row、tab、tree node、重复 control 使用稳定 domain ID; * 同一 control 重复出现时,以 owning object namespace child ID; * 可插入/重排的数据不能使用翻译 label 或 mutable index 作为 ID; * `render` 中不能生成新 random ID。 ```rust Button::new(("delete-project", project.id)) .danger() .label("Delete") ``` ID 改变表示 UI identity 改变,其状态 reset 必须是有意行为。Transition channel、overlay token、scroll handle 和 persistence ID 也遵循相同规则:共享 key 会相互覆盖,每 frame 换 key 则永远无法累积状态。 ## 渲染与组合 `render` 保持 declarative:读取当前 state、派生 presentation value、组合 element。Domain operation、parsing 与复杂 mutation 放入具名 method 或 service。 ```rust impl Render for ProjectView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { div() .v_flex() .size_full() .child(self.render_toolbar(cx)) .child(self.render_content(cx)) } } ``` 当 helper 能命名有意义区域并减少读者同时记忆的状态时,提取 `render_*`。只有该区域有可复用契约或 retained lifecycle 时才提取新 component;builder chain 很长本身不是理由。 一致使用 GPUI Component fluent trait(`Sizable`、`Disableable`、`Selectable` 和 component builder)。小的 conditional refinement 使用 `.when(...)` / `.when_some(...)`;分支代表不同 interface 时使用普通 Rust control flow。 构造 custom surface 前先使用标准 semantic component。不能为了匹配一张截图就用 generic `div` 重做 menu、select、dropdown 或 command palette。复用 component 才能保留 item geometry、focus transfer、keyboard navigation、selection、disabled state、dismissal 与 accessibility contract。如果标准 component 无法表达可重复的合理 pattern,应改进其 explicit API,而不是在每个 call site styling arbitrary descendant。 应用提供的 render callback 必须无副作用。List item renderer、menu builder、dock panel renderer 可能因 measurement 或 redraw 被多次调用,不能执行业务操作、追加数据或注册无界 subscription。 ## 行为与表现的边界 Base 的长期规则是: > Base 拥有可复用行为,以及实现行为所必需的 geometry;表现层拥有产品视觉语言。 “Headless”不表示“只有一个空 div”。Popup collision、keyboard navigation、editing、virtualization、resize arithmetic、focus trap 和 dock reconciliation 需要内部结构与状态。把它们推给 caller 不是灵活,而是复制脆弱行为。 反过来,Base 不能选择品牌色、字体、密度、最终图标、variant 或应用 composition。表现能力通过 `Styled`、typed semantic-state style、explicit part、child slot 和 item renderer 暴露。不能遍历任意后代去猜 title/description/close button,必须提供语义 part。 ## 主题与样式 从 active theme 读取 semantic value,使用 GPUI `Styled` method 布局: ```rust div() .bg(cx.theme().background) .text_color(cx.theme().foreground) .border_1() .border_color(cx.theme().border) .rounded(cx.theme().radius) ``` 规则: * 不写死 product color、radius、spacing 或 control geometry; * application code 不得引入 raw hex、`rgb`/`rgba` 或 `hsla`;颜色从 `cx.theme()`读取,缺少语义 role 时补入 product theme; * application layout 使用 GPUI rem-based helper(`p_2()`、`gap_3()`、`w_64()`、`text_sm()`),而不是直接 `px(...)`; * token 按语义而不是 palette 位置使用; * state-independent geometry 放普通 builder chain; * runtime hover/active/focus/focus-visible 使用 GPUI modifier; * checked/selected/pressed/disabled 使用组件 semantic state style; * popup ownership 使用 explicit state,使 trigger 在 dismiss 前持续渲染 open/pressed 外观; * disabled control 不应响应时,guard hover/active refinement; * variant 保持少而有意义,不为每个 call site 增加一个 variant。 * primary style 绑定决策区域真正的 default commit 与 Enter action,不能从 action 数量、频率或 toolbar 位置推导。 * Badge/Alert variant 保持 semantic 且稀缺。普通 metadata 使用 neutral;不能仅因为 variant 存在,就把每个 enum case 或 section 映射成不同颜色。 有效 precedence 为:instance style → active semantic state → disabled → GPUI runtime interaction refinement。后一层只替换自己设置的字段。 新的 application-owned presentation 优先使用 `Theme::semantic_tokens()`:它提供通用 color role、radius、spacing、typography、shadow scale,并有意避免 component-specific 名称。Legacy token 保留兼容性,但不应继续成为每个 application widget 的 extension point。 当前有一个必须明确的 ownership 边界:`Theme::spacing_tokens()` 投射固定默认 scale,`Theme::apply_semantic_tokens(...)` 不保存 custom spacing/elevation scale。应用如需自定义,必须自行持有 `SemanticThemeTokens`(或更窄的 design-system state)并提供给 component。不能把 custom spacing snapshot 写入 global theme 后,期待下次`cx.theme().semantic_tokens()` 仍返回它。 直接修改 GPUI Component global theme 后调用 `Theme::sync_base(cx)`,让 Base 拥有的 scrollbar 与 resize handle 获得新 projection;完整 `Theme::change(...)` 会自行同步。 向外绘制的 focus ring 需要空间,ancestor `overflow_hidden()` 会裁掉它。优先让布局留出空间;产品确实需要大量 clipping 时,通过 theme focus-ring policy 保留 focused border,不能悄悄消除键盘焦点。 ### 基础字号控制应用缩放 `Root::render` 会调用 `window.set_rem_size(cx.theme().font_size)`。因此 theme base font 不只是 body typography,也是 application rem-based design scale 的 reference length。这有意沿用 Tailwind 中有价值的模型:具名 type、spacing、size step 共享一个 relative base,而不是变成互不相关的 pixel constant。 通过更新 base font 并 refresh window 改变 zoom: ```rust Theme::global_mut(cx).font_size = px(18.); Theme::sync_base(cx); window.refresh(); ``` Base font 自身是 px,因为它负责锚定 scale。Descendant application UI 通常使用`text_sm()`、`gap_2()`、`px_3()`、`h_8()`、`size_4()` 等 relative helper,让 type、whitespace、control 与 icon 一起响应。Custom component 如果把 rem-based text 与 fixed-px padding/icon geometry 混合,必须记录为什么该部分不应 zoom。 Application UI 中每个直接 `px(...)` 和 raw color constructor 都应视为 review finding。只有 documented physical/platform boundary、measured runtime geometry、raster/data color 或 theme/token definition 本身可以例外。方便或“匹配截图”都不是有效理由。 从 resolved layout 得到的 cache 必须把 `window.rem_size()` 纳入 invalidation key,或者依赖随它变化的 revision。包括 wrapped row height、text shaping/layout、virtual-list measurement、popup/dialog geometry、由 text 推导的 icon size 和 custom canvas metric。Command variable-height row 是生态中的现有案例:较大 base font 会让同一 fixed width 产生不同 wrapping,因此 rem 变化时会重新 measure。 不要把 application zoom 与 Dock panel zoom 混淆。Dock zoom 是 stateful layout operation:让一个 tab group 或 tile 保留 container chrome 并填满 DockArea,同时保留退出路径;它不能修改 window rem size。 ## 事件、Action 与焦点 Pointer-specific 行为使用 pointer callback;需要 key binding、menu 或多输入来源的 command 使用 GPUI Action。Action handler 靠近拥有 command 的 view。 一个 logical desktop command 只建模一次。Toolbar Button、`DropdownMenu` item、`ContextMenu` item、menu-bar item 与 key binding 应 dispatch 同一个 Action 或调用同一 owner method,不能复制五份 mutation。条件允许时,label、icon、shortcut、enabled state 来自同一 command policy,避免不同入口互相矛盾。Menu 拥有 navigation 与 dismiss;feature owner 仍然拥有 command 是否允许以及实际执行内容。 控件选择必须符合语义。命令即使需要降低强调,也应使用 `Button` 的 `outline`、`ghost` 或图标形式,不能换成 `Link`。GPUI Kit 应用约定:`Link` 只用于交给浏览器或邮件客户端打开的 URL、网页文档和电子邮件地址;应用内目标使用相应的导航组件,命令使用 `Button` 或`Action`。这是产品设计约定,不是 `gpui_kit::base::Link` 的能力限制;后者可以通过 `open_with`把目标交给其他导航实现。 只有 nested interaction 确实必须阻止 parent 处理同一 event 时才 stop propagation。无差别阻止会破坏 menu、selection、drag 与 window command。 Focus owner 必须明确: * 拥有 keyboard interaction 的 Entity 保存 `FocusHandle`; * 在正确 focused region 注册 key context 与 Action; * overlay 打开时转移 focus,关闭后恢复; * 绘制清楚的 `focus_visible` state; * 禁止在 `render` 中无条件 request focus。 `key_context` 与 `on_action` 应附着于同一个 focused region。注册了 Action 但没有正确 focus path,不算实现键盘交互。Composite widget 应完成整个 navigation model:方向键、适用时的 Home/End/Page、confirm、cancel 与 Tab,而不是几个孤立 shortcut。 Modal 必须 trap focus,并在关闭后恢复到仍有效的原目标。Nested overlay 从最上层关闭;快速 close/open 不能把 focus 恢复到正在关闭的中间 surface。 ## 异步任务与副作用 Async work 从 event、lifecycle hook 或具名 method 启动,不能作为 `render` 的无条件副作用。不应让 task 延长已关闭 view 生命周期时 capture weak entity。完成后通过正确 GPUI context 更新,并处理 entity/window 已不存在的情况;完成 coherent update 后只 notify 一次。 用 idle/loading/loaded/failed 等明确状态表示 async operation。Refresh 时尽量保留可用旧数据;防止重复破坏性提交;可恢复错误要显示在 UI,而不是只写 log。 昂贵 parsing/computation 使用 background executor,Entity mutation 回到对应 application context。结果可能晚于 request、document、view 或 selection 的变化,必须绑定 revision/ID,拒绝 stale result。 ## 布局、测量与滚动 `h_flex` 会让子元素在交叉轴上居中;`v_flex` 保持 flexbox 的默认值 `stretch`。这与 Zed 的 `h_flex` 一致, 也是一排控件想要的效果,所以图标加文字的一行什么都不用写。但一排等高的列不是这样:直接放进 `h_flex` 的列不会 填满行的高度,比行更高的列会被居中,于是它的顶部(通常是 header)被裁到窗口之外,而列附近的代码看不出原因。 子元素是列的一行,请显式写 `items_stretch()`: ```rust h_flex() .items_stretch() .size_full() .child(sidebar) .child(content) ``` 大多数 UI 使用 GPUI layout,不应自行 measurement。Measurement 是 popup、virtualization、editor、resize handle、chart 等依赖 resolved geometry 行为的深层工具。 * measurement 与 geometry 放在拥有行为的层; * 只有普通 layout 无法表达关系时才在 prepaint 观察 bounds; * prepaint 不能每 frame 修改无关 application state; * measured data 带 frame/revision 范围;font、rem、width、theme、content 改变后可能 stale; * popup flip 与 viewport clamp 等共享 geometry 必须集中实现,不能每个 overlay 各写一次。 Alignment invariant 应通过构造保证,而不是事后校正:sibling region 消费同一个 spacing token 或 shared inset,不能重复看似等价的 literal。关键重复 edge、column 与 gap 应增加 geometry assertion 或 visual regression coverage。验证不能只有默认 window;rem zoom 与 display scaling 会把 fractional coordinate 变成一个 physical pixel 的漂移,即使默认截图看起来整齐。 精度评审要测量 resolved result,但不能把测得的差值写成 raw `px(...)` 微调。应继续追踪到重复 padding、nested inset、border ownership、font metric 或 rounding,并修复结构 owner。 `h_flex()` 与 `v_flex()` 并不对称:`h_flex()` 会把 child 在 cross axis 上居中,`v_flex()` 则保持拉伸。因此 row 里的 column 拿到的是自身内容高度,而不是 row 的高度;内容比 row 高时会被居中,header 被挤出上边缘并裁掉。只要 child 拥有 header、footer 或需要按 row 高度解析的 scroll region,就给这个 column 加 `h_full()`,或给 row 加 `items_start()` / `items_stretch()`。 每个 scrollable region 只有一个 owner。Flex layout 中,可收缩 child 使用 `min_w_0()` /`min_h_0()`。Flex item 只有在自身 overflow 不是 visible 时才会放弃基于内容的 automatic minimum size,所以普通的可伸缩 child 在被明确要求之前不会围绕长内容收缩。`Scrollable` 已经为自己的 wrapper 处理了这一点,但夹在它与 flex container 之间的普通 `div` 仍需自行释放该最小值。避免意外 nested scroll;wheel input 应进入目标 axis,并在 API 不可移植时保留 platform/wasm 差异。 `Scrollable` 应附着在拥有完整面板、编辑器或窗口 viewport 的 element 上,使滚动条解析到区域边缘。内容内边距放在 scroll owner 内部,不能用带内边距的容器包住 scroll owner。滚动条悬在内容与面板边界中间,通常说明 scroll owner 错误或内边距放错了层。 ## 列表、表格与大数据集 数据可能超过小型有界集合时使用 virtualization。Row identity 与 visible position 分开,不要每次 render clone 全量数据。Stateful list/table 拥有 navigation、selection、scroll coordination、visible range;item renderer 拥有 row presentation。 分离以下状态: * source data 与 domain ID; * filtering/sorting; * selection; * viewport/scroll; * row rendering。 这样更新保持局部,也不会把 view tree 变成 data model。 Virtualization 是 behavior contract,不只是性能开关。Width、typography、rem、row content 变化时要 invalidate item measurement。即使多数 element 当前不存在,keyboard selection 与 scroll-to-item 仍必须在 model coordinate 上工作。 ## 公共 API 设计 Reusable component 应遵守: * constructor 建立合法 default; * builder 消费并返回 `Self`,使用 domain 词汇; * callback 描述 requested change;只有 pointer modifier/detail 有意义时才带 pointer event; * 需要持续演进的行为接口使用私有字段、构造方法与读取方法; * boolean reader 在同名 builder 存在时使用 `is_` / `has_`; * reader 需要 plain field name 时,non-boolean setter 使用 `with_`; * explicit compound part 优于检查 arbitrary descendant; * reusable behavior 不能强制 product-level visual choice。 需要持续演进的行为状态默认使用私有字段。配置、主题令牌、几何数据和序列化结构如果本来就是记录类型,并且直接构造属于公开契约,可以有意暴露字段,同时接受相应的兼容成本。调用方可以读取、但不应依赖穷举构造或匹配时,使用 `#[non_exhaustive]`。 内部重组时保持 public module path:通过稳定 module seam 和明确 re-export,让 folder 变化不影响 downstream import。命名优先使用平台 control 术语和项目既有词汇,不使用偶然的 web-framework 词汇。 ## 平台与能力边界 不能假设 native 与 web target 支持相同能力。Window decoration、accessibility bridge、system notification、clipboard、scroll gesture、font、timing 都可能不同。Platform-specific 代码放在窄 capability seam 后,并定义 fallback。 Platform branch 即使 presentation 不同,也必须保留 semantic contract。例如 system notification 的 retract 能力不同,application delivery state 仍要 coherent。尽可能分别测试 shared state machine 与 platform adapter。 ## 文件、代码与命名风格 * View/entity 以产品概念命名:`ProjectList`、`ProjectEditor`、`SettingsState`; * handler 以 intent 命名:`confirm_delete`、`open_project`、`on_query_changed`; * 每个 module 一个主要职责;必须读无关行为才能理解 state/lifecycle 时就应拆分; * component module、state、event 与 focused test 在共同变化时放在一起; * comment 记录 invariant 和意外 lifecycle constraint,不复述显而易见 builder; * 使用 `rustfmt` 并满足 workspace Clippy,不能用宽泛 `allow` 隐藏无关 warning。 ### 词汇是 API 的一部分 同一概念在所有 component 中使用同一个词。命名新 method 前先搜索 GPUI、`gpui-base`与 GPUI Component;生态没有既有词时,参考 macOS/Windows control 术语。本地化文档保留准确的 API 标识符;稳定的 UI framework 术语在翻译会损失精度时也可保留英文。标识符使用代码格式,必要时在首次出现处解释。普通叙述不能为了显得专业而随意混用语言。 | 概念 | 命名模式 | 示例 | | --- | --- | --- | | 值类型 control | 名词 | `Button`, `Checkbox`, `Tab` | | Retained behavior model | `State` | `InputState`, `TableState` | | Imperative shared reference | `Handle` | `DialogHandle`, scroll handle | | Semantic notification | `Event` | `TableEvent`, `SelectEvent` | | Keyboard command | 动词或 intent noun | `Confirm`, `Cancel`, `SelectNext` | | Pluggable owner | `Delegate` / `Provider` | `TableDelegate`, `CompletionProvider` | | Caller 提供的表现 | `render_` / `_renderer` | `render_item` | | Construction | `new` 或语义 constructor | `new`, `horizontal`, `vertical` | | Fluent property | 名词或形容词 | `label`, `disabled`, `selected`, `placement` | | 通用 non-boolean builder | `with_` | `with_size`, `with_mode` | | In-place mutation | `set_` | `set_items`, `set_selected_index` | | Boolean reader | `is_<形容词>` / `has_<名词>` | `is_open`, `is_closable`, `has_selection` | | Plain value reader | field noun | `placement`, `selected_value` | | Callback registration | `on_` | `on_click`, `on_open_change` | | Named region renderer | `render_` | `render_toolbar`, `render_content` | 新 API 中,消费并返回 `Self` 的链式构造方法不加 `set_`;通过 `&mut self` 修改状态时使用`set_`。已经公开的名称应保持兼容,现有的 `set_position` 等链式方法属于兼容例外,不作为新 API 的命名范例。 Boolean reader 只有两种:值持有某物时用 `has_<名词>`,描述状态或许可时用 `is_<形容词>`。只要动作有对应的形容词形式就用形容词:`is_closable` 而非 `can_close`,`is_zoomable` 而非 `can_zoom`,`is_copyable` 而非 `can_copy`。动作是没有形容词形式的动词短语时,改为命名它所需要的东西:用 `has_definition`,不用 `can_go_to_definition`。不再新增 `can_` reader。 Boolean builder 可叫 `disabled(bool)`,reader 叫 `is_disabled()`。含 non-boolean field 的公开接口中的非布尔字段使用 `with_item_ix(...)` 构造、`item_ix()` 读取,避免冲突。新的局部或内部零起始索引优先使用 `_ix`,现有公开名称如 `selected_index` 保持不变,不再引入 `_idx`。调用方从不构造的快照,不要为了对称而发布构造方法。 ### 让外层名字承担上下文 名字总是在某个东西内部被阅读。字段在它的类型内部被读到,参数在它的方法签名内部 被读到,所以两者都不重复外层已经说过的话:`with_item_ix(ix)`,而不是 `with_item_ix(item_ix)`。 同一个类型的字段保持相同的缩写程度。其中若有一个写全了,它就成了异类,读者会去 找那个让它与众不同的区别。又因为 builder 按 `with_` 命名,缩短字段会同时 缩短它的 builder,两者始终配对。 只在外层名字确实能消除歧义时才缩短。当短形式在生态的别处同时是*另一个量*的既有 术语时,在 doc comment 里说明你指的是哪一个,而不是把标识符加长 —— doc 是在调用处 被读到的,它能解释清楚一个更长的名字只能暗示的东西。 ### 精确区分领域词汇 * **selected** 是持久 membership/active item;**focused** 是 keyboard target;**hovered** 是 pointer presence;**confirmed** 是 activation result,不能混用。 * **open/close** 描述 overlay/disclosure state;**show/hide** 表示 transient presentation request;**expand/collapse** 描述结构。 * **disabled** 禁止交互;**read-only** 允许导航/选择但禁止编辑;**loading** 表示操作中并应防止重复提交。 * **index** 是当前位置;**id** 是稳定 identity;`IndexPath` 是层级位置。重排数据不能用 index 持久化或作为 key。 * **value** 是 controlled domain data;**presentation** 是 render 用 read-only snapshot;**state** 是 retained behavior。 * **placement** 是 side/anchor policy;**position** 是 resolved geometry。 * **size** 是 semantic control tier;**width/height/bounds** 是 geometry。 * **child/children** 遵循 GPUI composition;`header`、`footer`、`trigger`、`content`等 named slot 具有额外语义。 避免 `data`、`item2`、`handle_action`、`update_ui`、`process`、`manager`、`config` 等模糊 public name。只有确实协调集合或生命周期时才使用 `Manager`,例如 `ToastManager`。 ### 类型与模块命名 * Rust type/Action 使用 `UpperCamelCase`;module/function/method/field/local 使用`snake_case`;constant 使用 `SCREAMING_SNAKE_CASE`; * component module 拥有 public seam;内部 folder 可拆 state/element/geometry/platform/test,但不能把这些实现路径泄漏到 import; * 单一 component concept 使用 singular module;family 使用生态既有名称(`input`、`table`、`dock`); * 只有真正擦除 type boundary 的 wrapper 才加 `Any`,如 `AnyInputState`、`AnyElement`; * identifier 后缀 `Id`,零基 index 后缀 `ix`,collection 使用有意义复数;同一 subsystem 不混用 `idx`、`index`、`ix`; * predicate 尽量正向命名;正向的 `enabled`、`visible` 比多重否定更容易组合,但已建立的 control semantic(如 `disabled`)应保持一致。 ### 回调与事件命名 只有真正 click-level contract 才用 `on_click`。Base controlled semantic primitive 应优先`on_change(next_value, ...)`;styled compatibility component 可以为现有 API 或 pointer detail 保留 `on_click`。Model-driven change 不能伪造 `ClickEvent`。 Lifecycle hook 名称必须准确:`on_will_change` 可 veto/prepare;`on_change` 观察请求或当前值;`on_confirm` 提交选择;`on_dismiss` 关闭 transient surface。文档必须说明 callback 发生在 internal state change 之前、之后还是代替它,以及是否可同步 re-enter。 ### 文档与界面文案 Public docs 先说明 type 做什么、谁拥有 state。Example 必须使用当前可编译 API 和稳定 ID。记录 default、platform limitation、focus behavior、callback ordering,以及何时需要 notify、emit 或 theme sync。 标签、命令、确认对话框、大小写和省略号遵循[界面用词规范](./design-guides.md#界面用词)。每个领域对象、命令和状态使用一个固定术语。翻译键描述稳定意图(`dialog.delete_project.title`),不照抄源语言句子,也不包含 screen coordinate。不要用翻译片段拼句子,也不能因为两个含义碰巧有相同英文就复用同一个 key。 本地化的是意图,不是 syntax。每个 locale 可以独立决定语序、pluralization、标点与所需上下文。String 必须放进 component 并结合真实数据评审。Test 或 lint 应发现缺失 key、英文资源中的意外 CJK、三个句点组成的省略号、未经设计的 ALL CAPS 和固定术语不一致;上下文中的重复是否必要仍需人工判断。所有字符串都要放回真实组件,在代表性内容、文本扩展和应用缩放下验证。 ## 测试策略 使用能够证明行为的最低层: 1. pure test:state transition、geometry、parsing、ordering; 2. GPUI context test:entity、event、subscription; 3. `VisualTestContext` interaction test:focus、keyboard、pointer、layout、rendered state; 4. example/application smoke test:完整 workflow。 Interactive component 测试 semantic contract,而非 implementation detail:pointer/keyboard activation、controlled value change、disabled、focus movement、event count/order、stable ID、关键 empty/failure state。Bug 可稳定复现时,修复前先加 regression test。 依赖真实 window system 的 UI 行为通过 accessibility tree 的 role、label、value、enabled、focus、selection 验证。每次改变 state 后重新读取 tree,因为 element index 只是 snapshot。Screenshot 只验证 semantic tree 无法表达的视觉事实;coordinate input 是最后手段。Automated 与 manual evidence 分开报告。 ## 性能规则 * `render` 中不能无条件 mutate 或 notify; * 不要每 frame 重建 Entity、Subscription、FocusHandle 和昂贵 data structure; * coherent state change 后只 notify 最窄 owner; * 长 collection virtualize,只 render visible range; * 不为满足 closure 而 clone 大 string/collection,capture stable handle/shared data; * 先 measure 再 cache,cache 必须有明确 invalidation owner; * animation work 有界并遵守 reduced motion。 ## 常见失败模式 避免以下模式: * 一个 Entity 保存整个应用互不相关的 state; * 长 `render` 中混入 business logic/network request; * reorderable content 使用 random/index `ElementId`; * literal color/radius 破坏 custom theme; * 已有 semantic component 时仍用 clickable `div` 重做 focus/keyboard/disabled/a11y; * duplicated local state 与 controlled model 漂移; * `render` mutation 引发 `cx.notify()` loop; * 没有 owner 的 nested scroll; * 为 one-off screen 新增 component variant; * 可逆低风险操作也弹 confirmation dialog; * test 只调用 internal method,从不执行 pointer/keyboard 行为。 ## 编码代理规则 修改前必须阅读与改动最相关的 implementation、test、re-export seam 和 component docs。必须在当前源码搜索 method signature,不能从 React、CSS 或旧 GPUI 示例类比翻译。 每项改动都应能回答: 1. behavior owner 与 presentation owner 是谁; 2. retained identity 与 state lifecycle 是什么; 3. pointer、keyboard、focus、accessibility contract 是什么; 4. layout 与 overflow owner 是谁; 5. 使用哪些 theme token,例外为何存在; 6. 哪个 test 会在行为退化时失败。 生成代码必须经过人工 review 与 test。“能编译”不是 UI quality bar;为了让生成代码看起来整洁而进行的大范围 refactor,也不能替代对仓库架构的匹配。 ## 实现检查表 提交评审前,确认: * state 与 side-effect ownership 是否明确; * `RenderOnce` / `Entity` 是否有意选择; * repeated element 是否使用稳定 domain ID; * theme token 与 component Size 是否替代孤立 visual literal; * keyboard Action、focus、disabled 与 overlay 是否共同工作; * loading、empty、error、cancellation path 是否存在; * 长数据是否使用适当 virtualized component; * public API 是否保持 dependency direction 与 encapsulation; * test 是否在适当层证明 behavior; * formatting、Clippy、targeted test 与相关 example 是否通过。 应用初始化请阅读[开始使用](./getting-started.md),具体 API 以各组件页面为准。 --- --- url: /zh-CN/shell/capabilities.md description: 默认全部拒绝的模型,fs / storage / clipboard / process 接口,存储位置,以及沙箱裁掉了什么。 --- # Capabilities 脚本默认**什么都拿不到**。没有文件访问、没有剪贴板、没有进程执行、没有网络。`Capabilities::default()` 就是空集,并有一条断言把它钉在那里。 唯一的例外是存储,而且只在 manifest 这一层:没有写 `storage` 的应用会拿到属于它自己的 `localStorage`,就像浏览器不问自答地给每个 origin 一个那样。这是关于作者**要写什么**的约定,不是模型上的口子——Rust 侧的 `Capabilities` 在 Host 开口之前照样拒绝,manifest 也照样可以写 `"storage": false`。见 [Storage](#storage)。 授权由 Host 决定,因为只有 Host 知道它对即将运行的这段代码信任到什么程度。至于它主动**递出去**的东西——它自己的、有意暴露的那部分 Rust——见 [HostModule](./host-module.md)。 View 在加载时冻结 capabilities;修改默认值只影响之后加载的应用,不会悄悄改变已经按某项授权运行的代码。 ```rust gpui_kit::shell::set_capabilities( Capabilities::new() .read_roots([application_root.clone()]) .write_roots([data_directory.clone()]) .storage(true) .exit(true), ); ``` ## 本地运行的应用被授予什么 从命令行运行一个目录,是一次明确的信任行为——与 `node app.js` 一样——所以 `gpui-shell ` 授予的是一组具体且很窄的能力: | | | | -------- | ------------------------------ | | 读 | 应用目录,以及它自己的存储目录 | | 写 | 它自己的存储目录 | | 存储 | 授予 | | 剪贴板 | **不**授予 | | 进程执行 | **不**授予 | | 退出请求 | 授予 | | 网络 | **不**授予 | 因此应用可以读自己的源码与资源、使用自己的存储,除此之外没有别的。它刻意比“全部放开”要窄,因为将来安装的插件会走同一条代码路径、由 manifest 来决定授权——而一个对本地运行足够宽松的默认,继承过去就是错的默认。 ## 拒绝信息会写明怎么修 每一条拒绝都以“要声明什么”结尾,而不只是说了句拒绝: ```text filesystem read is not granted; declare capabilities.fs.read in the manifest ``` ```text `/etc/passwd` is outside every granted read root; add its directory to capabilities.fs.read in the manifest ``` ```text storage is not granted; set capabilities.storage to true ``` ```text running `git` is not granted; add it to capabilities.fs.execute in the manifest ``` ```text process.exit() is not granted; set capabilities.process.exit to true in the manifest ``` ## Manifest 目录通过 **`gpui-shell.json`** 被识别。Manifest 是惰性数据——发现阶段只读取身份、可选版本元数据、Git 依赖与请求的权限,不执行 entry module。它识别 `id`、`name`、`version`、`shell-version`、`entry`、`dependencies` 与 `capabilities`;只有 `id`、`name` 和 `entry` 必填: ```json { "id": "com.example.quotes", "name": "Quotes", "version": "1.0.0", "shell-version": "0.6.0", "entry": "main.js", "dependencies": { "omarchy-ui": "huacnlee/omarchy-ui" }, "capabilities": { "fs": { "read": ["${pluginDir}"], "write": ["${dataDir}"] }, "network": { "hosts": ["stream.example.com"], "http": [ { "scheme": "https", "host": "api.example.com", "methods": ["GET"], "path_prefixes": ["/v1/"] } ] }, "storage": true, "clipboard": { "read": false, "write": true }, "process": { "exit": false } } } ``` `dependencies` 把裸模块名映射到一个 JavaScript package,gpui-shell 会在 entry module 运行之前从 Git 抓取它——`import { Title } from "omarchy-ui"`。字符串形式 接受严格的 GitHub 简写或完整 Git URL,可带可选的 `#ref`;显式指定 `branch` 或 `tag` 的 object 形式同样保持支持。每次加载还会把 package 链接到编辑器能找到的 位置,于是这条 import 会带上 package 自己的类型与文档。版本选择、package entry、 缓存,以及编辑器看见的东西,详见[依赖](./dependencies.md)。 这个块里的每项授权省略时都默认**拒绝**,只有 `storage` 默认给予——要拒绝它就写 `"storage": false`。 未知字段、非法 reverse-DNS id、显式填写但不合法的 SemVer、不兼容的 `shell-version`、逃出目录的 entry,以及未知 `${...}` placeholder 都会在代码执行前令 manifest 失效。省略 `version` 时显示为 `unknown`。省略 `shell-version` 时接受当前 runtime;显式填写时,它表示应用所需的最早兼容 gpui-shell 版本。兼容规则遵循 SemVer:`0.x` 应用保持相同 minor,稳定版本保持相同 major。独立 CLI 会拒绝非法 manifest,不会在假设已经不一致时继续执行 entry。 每条 scoped `network.http` 规则除了 host、method 与 path 外,还会绑定请求的 scheme 与有效端口。`scheme` 默认为 `https`;`port` 默认为该 scheme 的标准端口,仅非默认 endpoint 需要显式填写。 ## `fs` ```js import * as fs from "fs/promises"; ``` 每个调用都返回 promise。`await` 它们,或者接 `.then`——另见下面关于 `render` 的提示。 | 调用 | resolve 结果 | | ------------------------------------------- | -------------------------------- | | `fs.readFile(path)` | `Uint8Array` | | `fs.readFile(path, "utf8")` | UTF-8 文本 | | `fs.writeFile(path, contents)` | — | | `fs.readdir(path)` | 按名字排序的名称数组 | | `fs.readdir(path, { withFileTypes: true })` | 带 `isDirectory()` 的 `Dirent[]` | | `fs.exists(path)` | `true` / `false` | | `fs.unlink(path)` | — | | `fs.rmdir(path)` | — | | `fs.mkdir(path, options?)` | — | ```js const source = await fs.readFile("notes.md", "utf8"); await fs.writeFile("notes.md", source + "\n"); ``` 相对路径相对某个已授权的根解析;绝对路径必须本来就在某个根之内。这套接口里的每一条路径都经过**同一个解析器**,所以不存在第二处让穿越漏洞藏身的地方。它先做归一化——`../../etc/passwd` 在到达文件系统之前就被拒绝——然后把「是否在根之内」这件事交给文件系统判定,而不是判定字符串:授权承诺的是一个**目录**,而 `data/escape/passwd` 在字面上位于根之内,一旦 `escape` 是符号链接就读到了 `/etc/passwd`。路径中已经存在的最深一段会被连同链接一起解析,其结果必须仍在根之下;解析不到任何目标的符号链接会被直接拒绝,而不是猜它指向哪里。 **授权是一个句柄,不是一个字符串。** 解析器交回一个打开的目录,它无法被诱导指向自身之外的任何东西;读、写、列目录、删除、建目录全部对着**它**执行——于是一条路径永远不会被解析两次,「判定允许」与「实际使用」之间也就没有窗口。 这一点要紧,是因为显而易见的写法行不通。先检查路径再调 `std::fs`,路径被解析了两次:检查时就在的链接会被抓住,而在两次解析**之间**替换掉某个目录组件的,会被第二次解析跟出根目录。这里用的是 [`cap-std`](https://docs.rs/cap-std)——在 Linux 上是 `openat2(RESOLVE_BENEATH)`,其他平台是逐级 `openat` 遍历。 其中三项的行为值得说明,理由都是同一个: **被拒绝的路径抛异常,而不是返回 `false`。** “你不能看”和“它不存在”是两个不同的事实,把它们合并会让脚本能一次一个布尔值地探测自己根目录之外的文件系统。 **删文件和删目录是两个调用**,和 Rust 一样——单独一个 "remove" 说不清目录算不算在内。`remove_dir` 只收空目录:写权限是按根授予的,递归删除会把一次路径笔误变成整个应用数据目录的丢失。真要这么做的脚本可以自己遍历。 **`mkdir` 就是别处那个 `mkdir`。** 不带参数时只建一层,父目录不存在就报错;`{ recursive: true }` 才把父目录一起建出来。它原来叫 `create_dir_all`——那个名字确实说清了它做什么,代价是它不是每个脚本作者已经认识的那个名字。 **`read_dir` 已排序。** 渲染列表的脚本不该自己再排一遍,也不该继承文件系统的任意顺序。 **每个调用都返回 promise。** 系统调用在主线程之外执行——磁盘要花多久没有上界,而在这里阻塞会同时卡住帧和 VM,而且卡在中断预算看不见的地方,因为那段时间花在内核里。 **拒绝仍然在调用点抛出**,而不是变成 rejected promise。能力检查几乎不花时间,留在调用线程上;而没人 await 的 rejected promise,等于没人看得见的拒绝。 `readFile` 会拒绝超过 64 MiB 的文件,并指出文件名和上限。没有这个上限的话,替代方案是一个必须塞进 JavaScript 堆的字符串——而那个堆本身也有上限——于是失败会表现为 VM 内部的内存耗尽,而不是一句你能据以行动的话。 `writeFile` 每次最多接受 8 MiB。`readdir` 最多返回 10,000 个 entry 或累计 1 MiB 的 UTF-8 文件名(先触及哪个就按哪个停止),避免恶意目录让一个 promise 造成无界分配。 ::: tip 仍然不要在 `render` 里读文件 `render` 描述界面,它没法 await。在 `init` 或事件回调里读,把结果留在 View 上,拿到后 `cx.notify()`。 ::: ## Storage [Web Storage API](https://developer.mozilla.org/zh-CN/docs/Web/API/Web_Storage_API),和浏览器里的是同一个。不需要 import:`localStorage` 与 `sessionStorage` 是全局变量,同时也挂在 `window` 上。 ```js localStorage.setItem("todolist.items", JSON.stringify(items)); const saved = localStorage.getItem("todolist.items"); // 键不存在时为 null localStorage.removeItem("todolist.items"); localStorage.length; localStorage.key(0); localStorage.clear(); ``` | 成员 | 说明 | | ------------------- | --------------------------- | | `length` | 已存的键数量 | | `key(index)` | 该位置上的键,越界为 `null` | | `getItem(key)` | 值,键不存在时为 `null` | | `setItem(key, val)` | 存入,值会被转成字符串 | | `removeItem(key)` | 忘掉一个键 | | `clear()` | 全部忘掉 | | `flush()` | 写入落盘后 resolve | **两者只差在活多久。** `localStorage` 是 Host 放好的一个文件,跨重启存活;`sessionStorage` 是内存,随进程一起消失。这也是只有前者是一项 capability 的原因:`sessionStorage` 里的东西从不离开进程,没有什么可授权的,因此在一个什么都没授权的 Host 上它照样能用。 **值是字符串**,和 web 上完全一样——`setItem` 会把拿到的东西转成字符串。有结构的东西进出各走一趟 `JSON.stringify` 和 `JSON.parse`,这跟你在浏览器里会写的代码是同一段: ```js localStorage.setItem( "window", JSON.stringify({ title: "Notes", size: [640, 480] }), ); const window = JSON.parse(localStorage.getItem("window") ?? "{}"); ``` 每个成员都是同步的,这是刻意的:`getItem` 在 `render` 里也可达,所以值缓存在内存里,读取从缓存回答。每次渲染读一次文件是荒唐的。 **一次修改安排一次写入,而不是执行一次写入。** 文件在后台线程写出——先写临时文件再改名覆盖目标,所以写到一半崩溃留下的是之前完整的配置,而不是一个被截断的文件——并且同时只有一次写入在途,于是一连串 `setItem` 汇成一个文件,而不是一次一个文件。写入在途期间发生的改动,由下一次写入带上。 需要确认落盘时 `await localStorage.flush()`。这是相对浏览器接口唯一多出来的一个成员,它存在是因为浏览器根本不必回答这个问题——它的存储从头到尾都是同步的。它是**屏障,不是第二个写入者**:等待此前所有修改抵达磁盘,写入失败时用写入自己的错误 reject。若让它自己再写一次,就会与自动写入抢同一个临时文件,两者之间没有任何顺序保证——旧版本可能最后落盘,把新版本抹掉。 cache 与等待队列都有上限:单个存储文件序列化后最多 8 MiB,最多 4,096 个 key,单个 value 最多 1 MiB。同时最多允许 1,024 个尚未完成的 `flush()` barrier;更多调用会 reject,而不是无限增长 waiter 列表。 ### 存储在哪里 存储按应用划分,位置由 Host 选择——应用不能指定自己的存储位置,否则两个应用可以故意撞在一起。 **Host 给应用起名字,数据跟着这个名字走:** ```rust let data = gpui_kit::shell::set_bundle_id("com.example.notes")?; gpui_kit::shell::set_capabilities(Capabilities::new().write_roots([data])); ``` | 平台 | 位置 | | ----------------- | ----------------------------------------------------------------------- | | Linux 与其他 Unix | `$XDG_DATA_HOME/gpui-shell/apps//store.json`,默认 `~/.local/share` | | macOS | `~/Library/Application Support/gpui-shell/apps//store.json` | | Windows | `%APPDATA%\gpui-shell\apps\\store.json` | id 就是身份,所以目录被改名、被移动、被一次升级整个替换掉,数据都还在——这正是用户说"我的设置"时指的东西。改用路径作 key,一次升级就等于悄悄让用户从头开始。 **运行时不会去某个文件里找这个 id。** 只有安装了这个应用的那一层知道它叫什么;运行时自己挑一个 manifest 去读,等于对一件不属于它的事情宣称权威。 被"指向"某个目录的 Host——这个命令行、一个 dev server——没有这样一个名字,而在那种情况下路径确实就是身份。`gpui_kit::shell::bundle_id_for_path(root)` 用目录名加完整路径的摘要造一个,于是同一个目录总是访问到同一份数据,同一份源码的两个 checkout 也互不干扰。这在你正在编辑它时是对的,在它已经被安装之后是错的——而这正是声明一个真名字带来的区别。 id 允许 `a-z`、`0-9`、`.`、`-`、`_`,不允许 `..`。这不是整洁问题:它会被拼到用户数据目录后面,没检查的 id 能够到目录里的其他东西。数据放在那里而不是应用内部,因为应用目录可能只读、往往是一个 git checkout,也不是用户预期自己数据所在的地方。 ### 未被授权时的退化 未被授权的 `localStorage` 会抛异常,而写得好的应用会把它当作关于 Host 的一个事实,而不是一个错误: ```js // storage.js —— 取自示例应用 export function load() { try { const saved = localStorage.getItem(KEY); if (saved === null) return []; const items = JSON.parse(saved); return Array.isArray(items) ? items : []; } catch (error) { console.warn( `todolist: storage unavailable, starting empty (${error.message})`, ); return []; } } ``` 示例的页脚随后会在界面上说明这一点——“Not saved — this host did not grant storage, so the list lasts for this run only”——这才是对的形态:在边界处吸收拒绝,并对用户说实话。 ## 剪贴板 ```js cx.write_to_clipboard("copied"); const text = cx.read_from_clipboard(); // 剪贴板中没有文本时为 undefined ``` 名字取自 `App::write_to_clipboard` 与 `App::read_from_clipboard`,挂在 `cx` 上是因为 GPUI 就放在那里。不需要 import。 读与写是**两项独立授权**,拒绝信息会指出缺的是哪一半: ```text writing the clipboard is not granted; declare capabilities.clipboard.write in the manifest ``` 剪贴板需要一次实时的 Host 调用——GPUI 的 `App` 只在一次调用期间存在——所以一个没有活调用的 `cx` 会直说,而不是 panic: ```text cx.read_from_clipboard() needs a live host call; call it from render, an event handler or a task ``` ## `console` ```js console.info("loaded", count, { source: "disk" }); console.warn("could not save"); ``` `debug`、`log`、`info`、`warn` 与 `error`。它是全局的——与其他 JavaScript 运行时一样——不需要 import;shell 原本把同一个对象再以 `gpui.log` 导出了一遍,那只多了一个名字,别的什么也没多。 **不需要任何能力**:能跑起来的脚本本来就能说话,禁掉它只会让作者失去自己的诊断信息,别的什么都拦不住。 多余的参数会以空格分隔追加在后面,与 `console.log` 的行为一致。结构化的值以 JSON 打印,因为那是读日志的人想看到的形式。 输出通过 `tracing` 走,target 是 `gpui_kit::shell::script`,所以在日志过滤里脚本输出与 Host 输出是可分开的。**没有安装 `tracing` subscriber 的 Host 会把这些全部丢弃**——连同运行时自己报告的抛异常的处理函数、未处理的 rejection 与 phase 非法的调用。`gpui-shell` 二进制安装的是一个 `INFO` 级别的 stderr sink,`--dev` 下是 `DEBUG`。 ## `process` ```js import process from "process"; // 同时也是一个全局 const { code, stdout, stderr } = await process.run("git", ["status"]); process.exit(0); ``` `process.run` 返回 promise,理由是 `fs` 那条的加强版。文件读取没有时间上界;子进程连上界的影子都没有——它可以算上几分钟、等一个永远不来的输入,甚至活得比窗口还久。在这个线程上等它,会把帧和 VM 一起卡住,而且卡在内核里,interrupt budget 看不见。 输出是**捕获的,不是继承的**:跑一条命令的脚本几乎总是想要它说了什么,而在一个窗口程序里,子进程往 Host 的 stdout 写,是写到没人会看的地方。`code` 成功时是 `0`,被信号杀死时是 `-1`——那种情况本来就没有退出码。 执行是有界的:30 秒、stdout 8 MiB、stderr 8 MiB。触及任一上限都会终止并回收子进程,同时 reject promise。取消所属任务或销毁 runtime 也会终止子进程。子进程从清空的环境开始,不会继承 Host secret;shell 也不提供添加环境变量的选项。 它受执行授权约束,授权有三种形态:拒绝(默认)、命令名白名单,或不受限。被拒绝的命令**在调用处抛出**而不是 reject,和被拒绝的 `fs` 路径一样——没人 await 的 rejected promise,等于没人看见的拒绝。 `process.exit` 在运行时内部是**一个请求,绝不是 `exit(2)`**。它把退出码交给 Host 安装的处理函数,由后者决定怎么做——关闭插件的面板、关闭窗口、结束进程。一个插件不能把 Host 进程带走,而 Host 可能还有未保存的状态。 处理函数不是可选的:授予了这项能力却没有安装处理函数的 Host,会让这次调用**直接失败**并指明是 Host 漏了什么。没人应答的请求比拒绝更糟,因为脚本分辨不出这两者。`gpui-shell` 这个二进制安装的是「 Host 本身就是进程」时该有的策略——按脚本要求的退出码结束进程。 这个名字上的撞车是刻意的。`process` 正是 JavaScript 作者——或者生成 JavaScript 的模型——会去伸手拿的名字,所以运行时把自己受能力约束的接口放在那里,而不是把这个名字空着、任其看起来像 Node 的却行为不同。 `process.exit` 使用独立的 `capabilities.process.exit` 授权。文件系统访问权不会隐式获得关闭面板、窗口或进程的权限。 ## 沙箱 除了能力授权之外,运行时还会裁剪语言本身。以下全部在**未开启开发模式**时生效。 **没有动态代码。** `globalThis.eval` 被直接删除——`ReferenceError` 不会被特性探测误认为是一个可用的 `eval`,而一个抛异常的桩会。四个函数编译器全部被替换:`Function`,以及通过 `(async function(){}).constructor`、`(function*(){}).constructor` 和异步生成器等价物可达的那三个。`Function` 是被*替换*而不是删除,并保留了真正的 `Function.prototype`,所以 `x instanceof Function` 与 `.call` / `.apply` / `.bind` 继续可用,只有构造会抛异常。 **冻结内建原型。** `Object`、`Array`、`Function`、`String` 与 `Number` 的原型被冻结。一个 VM 将来会承载多个插件,这使得这些原型成为共享可变状态:一个插件给 `Object.prototype` 加一个可枚举属性,就改变了其他所有插件以及运行时自身 prelude 的 `for...in`。代价是真实的——一个给 `Array.prototype` 打补丁的库会在 import 时就停止工作——所以明知要运行这类库的 Host 可以关掉冻结,并保留沙箱的其余部分。 **模块解析被限制在应用根目录内。** `import "./ui.js"` 相对发起 import 的文件解析;任何解析到应用目录之外的结果都会被拒绝。动态 `import()` 刻意保持可用——延迟加载将来靠它——并且由同一个解析器约束。 **资源上限**,让失控的脚本报错而不是把窗口一起带走: | 上限 | 值 | | ------------------------------------------ | ---------------------------------------------------------------------------- | | 堆 | 256 MiB——泄漏表现为一个可捕获的 JavaScript 异常,而不是整个 Host 被 OOM kill | | 解释器栈 | 1 MiB——深递归表现为 `RangeError`,而不是原生栈溢出 | | 已加载的 JavaScript module | 每个源码文件 8 MiB | | 尚未完成的 host task | 每个 runtime 1,024 个 | | 单次调用耗时:render 与 layout | 50 ms | | 单次调用耗时:event 与 task | 500 ms | | 单次调用耗时:不在任何调用中,例如模块求值 | 5 秒 | 时钟在每一次 Host 调用时重置,这正是渲染路径能比事件回调有更紧预算的原因。**中断无法被 `catch` 吞掉**——这一点有测试来度量,因为如果能被吞掉,中断就根本不是一道防线。每个 WebSocket 另有一条由 `read`、`write` 与 `close` 共用的 8-command 队列;队列已满时新操作会 reject,并要求调用方等待 outstanding work。 这里没有 quickjs-libc 的 `std`:quickjs-libc 从一开始就没有被编进这个构建。运行时仍提供下文列出的、经过审计的小型 `os` 模块。 ::: tip 开发模式 `--dev` 会启用源码监听,并在构造运行时之前调用 `gpui_kit::shell::set_development_mode(true)`。它会恢复动态代码构造器并让内建原型保持可写。 开发模式从不放宽能力约束。它让语言更好摆弄,但不会发出任何人没有声明过的访问权限——因为一项作者从没写下来的授权,就是一项在生产环境里会缺失的授权。 ::: ## 网络与安全标准 API 全局 `fetch(url, options?)` 返回 promise,结果提供 `{ status, ok, url, , json() }`。它的授权比原始网络更窄:每次请求与 redirect 都必须匹配声明的 HTTP host、method,以及精确 path 或 path prefix;HTTPS 永不降级到 HTTP,authorization 与调用方 header 也不会跨 origin。 `net.connect(host, port)` 与 `websocket` 模块具名导出的 `WebSocket.connect(url, { headers? })` 使用 `capabilities.network.hosts`。`WebSocket` 不会安装成浏览器全局,也不是构造器。Raw TCP 的 `read()` 返回 `Uint8Array`,到达 EOF 时返回 `null`,因此传输分块不会经过有损文本解码。WebSocket 支持文本与 `Uint8Array` 消息,并通过单一 actor 串行化写入;它不会跟随 redirect。Connect、handshake 与 write 操作都有 30 秒 timeout。每个 socket 同一时间只允许一个 outstanding `read()`;第二个会立即 reject,而不是与第一个争抢下一条消息。凭证 header 与握手控制 header 会被拒绝。Raw TCP 与 WebSocket 权限有意比 HTTP request grant 更宽。 DNS 解析是有界的进程级共享服务:所有应用共用两个 resolver worker 和一个最多 64 个请求的队列。排队沿用每次连接已有的 deadline,所以饱和时会以 timeout 失败,不会无界增长内存或线程。这是资源收敛,不是每应用的服务质量保证;同一进程中运行互不信任应用的 Host,不会获得应用之间的 DNS 公平性。 运行时还提供 `buffer`、`path`、`url`、`crypto`、`zlib`、`console`、`process` 与 `os`。它们是生成的 `gpui-kit.d.ts` 所声明、经过审计的 LLRT/Host 子集;`node:` 别名和任意 Node 内建模块不属于 shell 契约。 ## 还没有的东西 * **向用户询问授权。** 授权在应用加载之前就已决定,不会在使用的那一刻弹出询问。 --- --- url: /zh-CN/base/virtual-list.md description: 只绘制屏幕内项目,流畅呈现十万条不同尺寸的行。 --- # 虚拟列表 Virtual List 只绘制当前屏幕内的项目,因此可处理任意长度的列表。不同于 `gpui_kit::uniform_list`,每一项可以有不同尺寸,适合可变行高表格、聊天记录和大纲树。它属于基础设施而不是带外观的组件:你预先提供尺寸,再提供渲染范围的闭包。 ## 为什么要预先提供尺寸 虚拟化必须在不渲染项目的情况下知道总范围和可见项。`uniform_list` 用统一尺寸换取零逐项数据;边滚动边测量会造成滚动条跳动;`VirtualList` 用预先提供的逐项尺寸换取精确偏移和稳定滚动条。无法预先精确测量时可使用合理估算,或固定行高并裁剪内容。 ## 开始使用 ```rust use std::rc::Rc; use gpui_kit::base::{v_virtual_list, VirtualListScrollHandle}; use gpui_kit::{px, size}; let sizes = Rc::new(vec![size(px(280.), px(32.)); 100_000]); v_virtual_list(cx.entity(), "customers", sizes, |_this, range, _window, _cx| { range.map(|ix| div().h_8().px_2().child(format!("Customer {ix}"))).collect() }) .track_scroll(&self.scroll_handle) .size_full() ``` 闭包只收到可见范围及少量超绘制,并为每个索引返回一个元素。横向列表使用 `h_virtual_list`。 ## 尺寸契约 * 纵向列表只读取 `height`,横向列表只读取 `width`。 * 交叉轴通过布局一个项目测量,默认使用第 0 项;不具代表性时调用 `.with_item_to_measure_index(3)`。 * `item_sizes.len()` 就是项目数,必须与数据一致。 尺寸表使用 `Rc>>`,应保存在 entity 中并只克隆句柄,不要在 `render` 中重建。 ## 滚动与滚动条 `VirtualListScrollHandle` 持有跨渲染的滚动位置,支持 `scroll_to_item(index, ScrollStrategy::Top)`、`scroll_to_bottom()` 和 `base_handle()`。它实现了 `ScrollbarHandle`,可直接传给 `Scrollbar::vertical(&self.scroll)`;外层容器需要 `relative()`。 ## 尺寸行为与热路径 `ListSizingBehavior::Auto`(默认)使用父级提供的空间;`Infer` 根据项目推导尺寸。虚拟列表必须放在有边界的父级中。可见范围变化的每一帧都会运行渲染闭包,因此其中不要做 I/O、排序或过滤,也不要为每行创建持久 GPUI entity。元素 ID 应来自稳定数据键或项目索引,状态更新放在回调中并调用 `cx.notify()`。 每帧工作量与可见项目数而非总数成正比;只有尺寸表随总数增长。大量统一高度项目更适合 `gpui_kit::uniform_list`。 ## 完整 Rust 示例 ```bash cargo run -p gpui-base --example components -- virtual-list ``` ## 检查清单 * 在 entity 中保存尺寸表和滚动句柄。 * 保持尺寸表与数据长度一致,并选择有代表性的测量项。 * 提供有边界的父级;添加滚动条时父级使用 `relative()`。 * 保持逻辑顺序、项目数和稳定身份,让辅助技术获得连贯列表。 --- --- url: /zh-CN/docs/design-guides.md description: 面向 GPUI Kit 客户端应用的产品、视觉与交互设计规范 --- # 设计指南 请在选择组件或编写布局代码之前阅读本指南。它记录 GPUI Kit 在多年桌面应用开发中形成的产品判断:界面应当原生、克制、精确,并让用户无需猜测就能完成任务。 本文是一份规范性指南:**必须**表示正确性或生态约束,**应该**表示默认选择,偏离时需要有明确理由;**可以**表示可选方法。具体方法签名仍以组件 API 文档和当前源码为准。 这些规则建立在 `gpui-base` 的行为能力、GPUI Component 的主题与组件体系,以及桌面平台共同的交互习惯之上。Shadcn 提供了开放代码、组合能力和可靠默认值等有益方法,但不决定 GPUI 应用的外观。发生冲突时,优先服从 GPUI 生命周期和用户熟悉的桌面交互。 ## 设计主张 界面应该原生、安静、准确。由内容、层级与交互承担体验,装饰只用于支持它们。 1. **先清晰,后个性:** 在加入品牌表达前,先让主要任务与下一步行动清楚。 2. **先组合,后发明:** 从已有组件出发组成产品工作流;只有行为确实不同,才创建新 primitive。 3. **先 token,后数值:** 颜色、圆角、字体与间距必须构成系统,避免无法响应主题的孤立字面量。 4. **桌面约定优先于网页惯例:** 保留键盘操作、窗口框架、菜单、密集数据视图、可调整区域和持久导航。 5. **状态必须可见:** hover、焦点、选中、禁用、加载、校验与破坏性状态必须清楚且一致。 ## 借鉴 Shadcn Shadcn 最有价值的不是某一种边框颜色,而是一套建立系统的方法: * 拥有界面最上层代码,不与封闭抽象反复对抗; * 用小而可预测的部件组合出产品组件; * 默认样式本身就属于同一种视觉语言; * 代码和组合结构同时对人和 AI 可读; * 把行为 primitive 与带视觉观点的样式层分开。 GPUI Kit 通过 Rust 库以及 `gpui-base` / `gpui-component` 分层来实现这些原则。应用通常组合或适度封装公开组件;贡献者把真正可复用的行为放入 Base,把视觉决策留在上层。 以下网页习惯不应直接复制: | 网页习惯 | GPUI 原生默认方式 | | --- | --- | | 所有按钮都显示手形光标 | 按钮使用箭头,只有链接使用手形 | | 以页面跳转为主要结构 | 使用持久窗口、pane、sidebar、tab 与 menu | | 依赖浏览器兜底焦点和滚动 | 明确焦点所有者与各区域的滚动所有者 | | 移动优先的单列布局 | 有最小窗口尺寸定义的可调整桌面 shell | | 关键操作只在 hover 时出现 | 键盘和指针都可达,不依赖 hover 才存在 | | 每行放一排 hover-only 图标按钮 | 一个可见主操作,次要命令使用 `DropdownMenu` / `ContextMenu` | | 用 Link 样式文字执行应用 command | 使用 Button、outline 或 ghost;Link 只用于 URL、网页资源或 Email 地址 | | 全局采用很大的触摸密度 | 默认 medium,专业数据区域才局部紧凑 | | 通过 CSS 穿透修改后代 | 使用 typed builder、语义部件和应用组合 | ## 从用户任务开始 画界面之前先写清楚: * 用户的主要任务; * 正在查看或修改的对象; * 必须立即可见的操作; * 做决定所需的信息; * 空、加载、错误、离线、只读和无权限状态; * 完整流程的键盘路径。 窗口结构应由这些答案决定,不要从 dashboard 卡片网格或组件目录开始。优秀的桌面应用呈现用户的心智模型——文档、账户、项目、消息、设置——而不是内部服务架构。 先确定主要任务,再选择控件。视觉权重、位置与信息深度必须匹配功能在产品中的重要性。不能把核心结果缩成角落里的数字、图标或弱化操作,却让次要内容占据页面。当结果集本身就是产品价值,应使用摘要区域或卡片展示数量、代表性结果、当前状态和清晰的下一步。 每个操作都要有明确的对象、当前状态、作用范围和结果。界面尚未展示或暗示这些信息时,操作就出现得太早。不能因为后端已有能力就直接暴露入口;先设计用户如何理解这个对象和结果。 ## 视觉语言 ### 层级 使用少量、清晰的层级: * **窗口或页面标题**标识当前对象或工作区; * **章节标题**分隔有意义的区域; * **正文**承载工作内容; * **弱化文字**提供次要元数据和帮助; * **标签**标识控件和值。 先用字号、字重、间距与分隔线建立层级,再考虑颜色和容器。避免卡片嵌套卡片;大多数桌面区域只需要背景、hairline 边界和有意图的留白。 层级必须按整个功能评审,不能只看单个组件。隐藏强调色和装饰后,主要任务、当前选择、结果摘要和下一步仍应能从结构中识别。每个控件单看都合理,如果没有形成清楚的阅读顺序与决策路径,整体设计仍然失败。 注意力是有限的。一个局部区域只需要一个清晰重点。如果所有内容都有颜色、`Badge`、粗体、容器或 `Alert`,就没有内容真正重要。先用结构和距离建立优先级;只有某个区别会改变用户的注意或行动时,才使用更强的颜色或组件。 ### 颜色与主题 从 `cx.theme()` 读取颜色,并按语义角色使用: * `background` / `foreground`:主表面和正文; * `group_box`、`popover`、`sidebar` 及对应 foreground:具名表面; * `muted` / `muted_foreground`:辅助信息; * `primary`:当前决策区域的主要操作或选中强调; * `danger`、`warning`、`success`、`info`:只表达对应语义; * `border`、`input` 和 ring token:结构与交互。 不要把状态色当装饰,也不要只依赖颜色表达含义。自定义表面必须在明暗主题和自定义主题中验证;不能假设 foreground 一定是黑色或 background 一定是白色。 Badge 只用于有助快速扫描的短 state、count 或 classification,不能套在每个 label、metadata value、filter 或 section title 上。大多数 Badge 保持 neutral;只有真实表达 success、warning、danger 或 info 时才使用 semantic variant。满屏多色 Badge 通常说明 hierarchy 或 grouping 尚未设计完成。 Application UI 不应出现 raw hex、`rgb`/`rgba` 或 `hsla` color。颜色必须按语义角色从`cx.theme()` 解析。需要的 role 不存在时,在产品 theme/token 层定义,而不是在 call site 嵌入 palette value。Raw color 只属于 theme definition,或颜色本身就是数据的、经过审查的 data/raster content。 ### 圆角、间距与密度 所有应用拥有的控件圆角都应来自主题。这样产品才能作为一个整体变得方正或圆润。圆形和胶囊使用 `radius_full()`,不要写死最大圆角。 采用紧凑、重复的间距 scale。label 与 control 的距离应小于两个 group 的距离,两个 group 的距离应小于两个 section 的距离。优先使用组件尺寸(`xsmall`、`small`、默认 `medium`、`large`),不要单独写高度。compact 用于 toolbar 和数据密集界面,不能用来把结构不清楚的布局硬塞进更小空间。 共享语义 scale 有意保持很小:间距大致为 2、4、8、12、16、24、32 px;字体大致为 12、14、16、18、20 px。这些表示关系,不是鼓励把当前数值散落在 feature 代码中。GPUI Component 的 global `Theme` 当前投射的是固定默认 `SpacingTokens`;与 color/radius 不同,`Theme::apply_semantic_tokens` 不会保存 custom spacing scale。需要不同 scale 的应用必须自行持有完整 token snapshot,并在 application component 中一致使用。 ### 空间语法 间距表达关系。根据相邻对象的语义关系选择 token: | 关系 | 常用 token | 当前 scale | 示例 | | --- | --- | --- | --- | | 光学校正 | `xxs` | 2 px | 图标基线、紧凑分隔 | | 同一控件的部件 | `xs` | 4 px | 菜单图标与标签、标题与说明 | | 紧密相关的控件 | `sm` | 8 px | 按钮图标与文字、对话框操作 | | 同一个内容 group | `md` | 12 px | 通知各列、紧凑表单行 | | 同一 section 的不同 group | `lg` | 16 px | panel 内边距、表单组 | | 不同 section | `xl` | 24 px | 页面或 inspector 的主要区块 | | 主要区域边界 | `xxl` | 32 px | 空状态留白、页面大段落 | 这些数值描述当前默认 scale;生态默认代码使用 `cx.theme().spacing_tokens()` 或对应 GPUI scale helper。产品自有 scale 应保持关系顺序,并通过 application design-system context 传递,不能假设它会持久保存在 GPUI Component global theme 中。 处理上下左右空间时遵守以下规则: 1. **先分清内部与外部。** 组件 padding 由组件拥有,组件之间的 gap 由父容器拥有。 2. **纵向节奏表达分组。** 标题到说明的距离小于说明到下一 section 的距离;相等间距意味着相等关系。 3. **横向空间服务扫描。** 重复行中的图标、label、value、badge、尾部 action 应落在稳定列上。 4. **leading/trailing 是语义。** 即使当前 API 使用 left/right,也应按阅读方向思考,为未来 RTL 留出可能。 5. **不要重复 padding。** 已有 panel inset 内的 card 不应再无条件增加一整层 panel padding。 6. **谨慎使用光学校正。** 图标或字形允许 1–2 px 修正,但必须能解释为何偏离 scale。 现有系统中的常见组合揭示了这些关系: * 小按钮内容间距为 4 px,常规尺寸为 8 px; * Dialog header 与 footer 内部间距为 8 px; * 紧凑 list/menu 行通常采用 4 px 纵向、8–12 px 横向 padding; * Sheet header 约为 leading 16 px、trailing 12 px,为关闭控件留出空间;footer 为横向 16 px、纵向 12 px; * Notification 使用 16 px 横向 padding 和 12 px 列间距,因为图标、消息和操作属于不同 group。 这些不是所有 surface 的复制模板,而是在说明:控件内部最紧,行针对扫描优化,容器边界比内容内部花费更多空间。 ### 比例与层次 先确定内容约束,再确定比例。角色不同的 pane 不要机械地各占一半。 * Sidebar 应足够容纳稳定标签,但视觉上从属于工作区;为它定义最小、首选与最大宽度,而不只是百分比。 * Master–detail 中,collection 必须仍可扫描,detail 消耗剩余空间。约 1/3 与 2/3 可作为起点,但内容约束优先。 * Inspector 与辅助 sheet 默认不能遮住主要对象;内容增长时应可调整或关闭。 * Dialog 宽度由决策复杂度决定:短确认、中等表单;复杂工作应进入独立页面或窗口。 * 最强 elevation 只给最上层决策面;同一层内使用背景和 hairline,而不是不断加重阴影。 每个主要区域要定义三件事:任务可用的最小尺寸、舒适默认尺寸、如何消费剩余空间。如果 split 表达用户工作习惯,应持久化;恢复后必须按当前窗口重新 clamp。 ### 对齐细节 对齐是一套结构系统,不是最后的润色。每个 surface 应先建立少量 alignment spine:共享的 leading/trailing edge、文字 baseline、center line 与固定功能 lane。同一层级的元素即使使用不同 component,也应从上到下或从左到右落在同一条 spine 上。 纵向红色虚线贴近共享边缘或控件中心,说明内容、状态、时间与尾端操作的列对齐;横向虚线分别贴近文字基线并穿过行中心,说明文字底边和不同尺寸控件的垂直居中。右侧紧凑对照单独展示必须从结构所有者修复的一个渲染像素漂移。 * Sibling region 使用相同 content inset。同层级的 heading、toolbar、list row、empty state 与 footer 不能各自发明略有差异的起始线。 * 整个区域重复同一套 column geometry。Header、row、summary、loading state 与 inline editor 为 identity、metadata、status、number 和 action 保留相同 lane。 * 跨 row 与 section 对齐相关 control。从上到下扫描时,form label、field、description 与 validation message 应显现稳定的纵向网格。 * 水平 band 保持一致。同一 toolbar、title bar、status bar 或 row 中的项目共享 baseline 或 center line,不能逐个用 offset 微调。 * 只有真实 hierarchy、containment 或 disclosure 才引入 indentation。装饰性缩进会让 sibling 看起来像 subordinate,并破坏阅读起始线。 * Nested level 结束后必须准确回到 parent spine,不能让多层 container 的 padding 逐层漂移。 * Optional content 出现或消失时仍保持 spine。缺少 icon、badge、description 或 trailing action 不能推动其余 label;需要跨行比较时使用明确 slot 或 lane。 * 层级相同时,major region 之间也应互相对齐。Sidebar header、content title、split pane、toolbar 与 bottom bar 不必共享所有坐标,但相同层级应形成可见的连续线。 不是每一条边都必须对齐。Child 可以缩进,primary value 可以领先 supporting metadata,destructive decision 也可以获得额外间隔。但例外必须表达层级或语义,不能只是各 component padding 未协调的结果。先建立 shared spine,再明确设计例外。 精确对齐与重复 gap 是质量 invariant。两条 edge 或两段 spacing 按设计应相等时,相差一个 rendered pixel 也是 defect,不能以“肉眼大致一致”通过。必须在代表性的 window size、zoom level 与 display scale factor 下用测量工具检查 resolved bounds,直接比较 coordinate 与 distance,不能只看一张截图凭感觉验收。 这里的 rendered-pixel 容差是验证规则,不是允许用 raw pixel offset 修补代码。相等关系应来自同一个 `rem` helper、spacing token、grid definition 或 shared component inset;发生偏差时修复共同 owner。还要考虑 fractional layout 与 device rounding,确保预期 spine 在不同 zoom 下落到同一 physical pixel,而不是只在某一档看似对齐。 * 混合字号同排时按文字 baseline,而不是按 bounding box 中心对齐。 * 图标放入固定槽位,避免不同固有宽度导致 label 抖动。 * 可比较数字右对齐,文本和标识符通常左对齐(locale 另有要求除外)。 * 行尾 action 和 disclosure indicator 使用固定宽度 lane。 * 表单控件按交互 frame 对齐,而不是按下面的 help text 对齐。 * 只有两侧确实分别拥有相反边缘时才用 `justify_between`,不要用它掩盖中间结构缺失。 * Hairline 由边界所有者绘制,相邻区域不能各画一次同一条分隔线。 * 滚动条属于实际滚动的区域,并贴住面板、编辑器或窗口的尾端边缘。内容内边距可以缩进文字与行,但不能把滚动条推到界面中间;内容需要避让时,应保留明确的滚动条槽位。 ### 密度层级 Medium 是生态默认值。密度应在局部上下文整体变化,而不是只改一个控件: * **comfortable / large:** onboarding、稀疏表单、重要决策; * **standard / medium:** 大多数应用 chrome 与工作流; * **compact / small:** toolbar、menu、table 和重复的专业数据; * **extra compact / xsmall:** 极少数高密度工具,不应成为全应用默认。 当前控件体现的是有限 scale:按钮 frame 大致为 20、24、32 px;input 与数据控件在 large 时可到约 44 px;table row 大致为 26、30、32、40 px。使用组件 `Size`API,让字体、图标、padding 和 hit target 一起变化。只改外框高度通常是不完整的。 ### 缩放、基础字号与 `rem` 良好的 `rem` 系统能在界面 zoom 时保持设计层次。成功的 zoom 不只是每个对象都变大,而是在每一档 scale 下,title/body、control/icon、inner/outer spacing、primary/secondary region 之间仍保持相同关系。 GPUI Component 采用了 Tailwind 中有价值的相对 scale 思想。Theme 的 base `font_size`通过 `Root` 成为 window `rem`;`text_sm()`、`gap_2()`、`p_4()`、`h_8()`、`size_4()`等 GPUI scale helper 都以它解析。Typography、spacing、control 与 icon 因而共享同一条 zoom axis。 设计时关注比例: * type step 围绕 body base size 保持相同 hierarchy; * spacing step 围绕 typography 保持相同 grouping; * control frame、icon、hit target 与 label 一起缩放; * pane minimum 与 comfortable width 要容纳缩放后的内容; * radius 与 focus treatment 相对 control frame 保持光学一致。 不能只改变文字实现 zoom。固定高度 Button 中放大 label、固定 pane minimum 中放大 document、或 virtual-list measurement 未失效时放大 row,都会破坏原有节奏并裁切内容。反过来,把包括 hairline 在内的每个 physical pixel 全部相乘,也会让界面变得沉重。 原则上 application layout 不得直接调用 `px(...)`。使用 GPUI rem-based scale helper(`p_2`、`gap_3`、`w_64`、`text_sm` 等 builder)或 semantic component Size。只有值代表 physical/raster boundary 时才使用 fixed px:one-device-pixel hairline、platform window inset、bitmap dimension、minimum hit-test tolerance,或必须匹配 external surface 的 geometry。这些必须是经过审查和记录的例外。Product spacing、typography、icon size 与普通 control geometry 保持在 relative scale 上。 不能只在默认值验证。至少使用多档 base font 检查 hierarchy、wrap、truncate、minimum window size、pane resize、focus-ring clearance、popup placement 与 virtualized row measurement。同时区分 interface zoom 与 Dock panel zoom:Dock zoom 让一个 container 保留 chrome 并填满区域,不改变 `rem` 或 application scale。 ### 表面与层级 Elevation 用于解释叠放关系,而不是表达重要程度。基础窗口保持平面;区域由背景差异和 separator 划分。Popover、menu、dialog、notification 因处于内容上方,可以逐级增强阴影。不要给每张 card 都加阴影。 同类 surface 必须采用同一种处理。GPUI Component 有意让 Popover、Select、Combobox、DatePicker 和 menu 共用 popover surface,避免它们逐渐漂移。应用新增 anchored surface 时应复用该语义处理,而不是用无关的 border/shadow 字面量近似。 ### 字体与图标 界面文字使用平台 UI 字体;代码、标识符、快捷键和对齐数字才使用等宽字体。正文必须易读,避免过度大写或字距,尤其不能把拉丁文字的 tracking 直接套给 CJK。 一个产品使用同一套图标。图标辅助 label,不能用猜谜替代陌生操作。仅图标按钮必须有 tooltip 与 accessible name。填充或彩色图标用于表达状态,不用于让 toolbar 显得热闹。 ## 布局模式 ### 选择稳定的应用框架 大多数应用适合以下一种: * **单工作区:** toolbar/title bar 加一个主要视图; * **Sidebar 工作区:** 持久导航与变化的 detail; * **Master–detail:** 可调整 collection 与 detail pane; * **文档工作区:** tab 或 DockArea 管理多个长期对象; * **Utility window:** 单一任务与短而固定的操作路径。 全局导航在内容变化时应保持稳定。主要工作区用 `flex_1()` 消费剩余空间;可收缩的 overflow child 需要 `min_w_0()` / `min_h_0()`。滚动、虚拟列表、表格和 Dock 应使用 `Scrollable`、`VirtualList`、`Table`、`DockArea`,不要用多层 `div` 重做行为。 ### 可调整的桌面窗口 桌面并不表示固定尺寸。窗口变窄时按以下优先级处理: 1. 保留主要任务; 2. 可调整区域达到有文档的最小值; 3. 折叠次要 label 或 inspector; 4. 把低频操作移入 menu; 5. 只滚动真正发生 overflow 的区域。 隐藏操作时必须提供另一条路径。不要让整个窗口滚动,而实际只有 list 或 document body 需要滚动。 GPUI flex child 即使设置 `flex_1()`,也可能因为长内容拒绝收缩。设计和代码必须约定哪些 pane 可以收缩、截断、换行或滚动。`overflow_hidden()` 也会裁掉向外绘制的 focus ring,不能为了简化 overflow 牺牲键盘焦点可见性。 ### 表单与设置 每个字段使用可见 label,help 与 validation 放在所描述字段附近。相关字段应对齐,但不要强迫长 label 进入过窄固定列。独立选择用 `Checkbox`,少量可见互斥项用`RadioGroup`,较长集合用 `Select`,立即生效的设置用 `Switch`。 操作进行中禁用重复提交,保留用户输入,并在操作附近显示结果。Dialog 只用于短而聚焦的决策;需要探索或大量字段的流程使用完整页面或 sheet。 ## 组件与组合 采用 Shadcn 的核心思想:组件是构建材料,不是封闭设计系统。GPUI Component 提供一致默认值,应用拥有组合和产品语义。 * 变体按语义使用。主按钮(`primary`)只留给决策区域中明确的默认提交,通常也是按 Enter 执行的操作。操作唯一、使用频繁或希望用户注意,都不会使它自动成为主按钮。管理工具栏中的“添加”通常使用默认按钮;表单中默认提交的“创建”可以使用主按钮。`danger` 表示破坏性提交,`ghost` 用于低强调的工具栏操作。 * 优先使用明确的复合部件与渲染回调,不要穿透任意后代设置样式。 * 跨产品重复且带有领域语言或规则的模式,应封装为应用组件。 * 按语义角色使用标准组件。菜单、下拉菜单、弹出层、选择器与命令面板不是可以互换的容器;它们分别拥有选择、焦点、键盘操作、关闭方式和布局契约。 * 保留同类组件的几何规则。菜单行的上下左右内边距、高度、图标槽、勾选槽、分隔线、圆角和状态样式必须统一;不能用自定义弹出层模仿一个间距仅仅接近标准组件的菜单。 * 不要仅为重命名每个方法或冻结所有能力而包装一层库组件。 * 无产品样式的复用行为放入 `gpui-base`;有主题观点的表现留在 Component 或应用。 ## 交互状态 ### 让结果先于点击被理解 控件应当让结果在操作前就可以预见。优先使用熟悉的桌面控件与布局,让用户不必先学习界面。文案说明动作与对象,状态说明当前是否可用,反馈则确认同一个结果。 不要把实际会打开设置流程的按钮写成“保存”,也不要用“删除”描述仅从分组移除的操作。上下文不能说明范围时,直接写出范围。按下后立即反馈;耗时操作防止重复提交,并在被改变的对象附近显示结果。只有结果本身不可见时,才补充成功提示。 每个交互控件都要设计以下状态: | 状态 | 设计要求 | | --- | --- | | 静止 | 操作提示清楚但不嘈杂 | | 悬停 | 提供轻微指针反馈,但不能成为唯一线索 | | 按下 | 立即响应按压 | | 打开 | 附属弹出层打开期间持续显示按下或打开状态 | | 焦点可见 | 显示高对比键盘焦点环 | | 选中 | 状态持久,并与悬停明确区分 | | 禁用 | 降低强调,且不产生误导性的悬停或按下反馈 | | 加载 | 保持上下文、防止重复操作,并解释较长等待 | | 错误 | 说明发生了什么以及如何恢复 | 需要键盘访问的命令使用 GPUI 焦点系统和 `Action`。遵循熟悉的平台快捷键,在菜单或工具提示中展示,并在浮层打开或关闭后把焦点放到合理位置。 选中状态是信息模型的一部分,不是可选润色。标签页、分段选择、可选行、筛选项与导航入口必须持续显示选中状态。拥有下拉菜单的按钮在弹出层关闭前必须保持按下或打开外观;悬停无法说明触发按钮与弹出层之间的关系。 破坏性操作要区分可逆与不可逆。可逆变更优先 undo 或临时 notification;严重且不可撤销时才使用 `AlertDialog`,确认文案必须写出具体对象和后果。 ### 指针约定 Button、Checkbox、MenuItem、Tab 等原生控件使用默认箭头;link 使用手形。文本、resize、grab、prohibited cursor 只在确实描述当前操作时使用。Cursor 只是强化 affordance,不能替代可见状态与 accessibility role。 Hover 应克制,因为键盘和 accessibility 操作没有 hover。破坏性或关键操作不能只有 hover 时才存在。行内 action 可以在 rest 时更安静,但 selection、键盘或 context menu 必须提供同一命令。 ### 优先使用桌面命令入口,而不是悬停工具栏 根据命令的频率和作用范围决定入口: * 主要或高频操作使用带文字的按钮或熟悉的工具栏控件,并保持可见; * 当前区域的次要操作放入具有可见触发按钮的 `DropdownMenu`; * 作用于指针下对象的命令放入 `ContextMenu`; * 有自然键盘形式的重要命令同时提供 `Action` 与快捷键; * 悬停时出现的图标只能是快捷入口,同一命令必须在其他位置仍可到达。 这不只是视觉偏好。GPUI Component 的 menu system 已经拥有方向键导航、confirm/cancel、disabled item、separator、submenu、shortcut 展示、focus transfer/restore 和 nested menu dismiss。自定义 hover button strip 必须重新实现这些行为,而且 keyboard-only 与许多 assistive technology workflow 根本看不到它。 用户需要明确知道“这里还有更多命令”时使用 `DropdownMenu`,例如 toolbar overflow、document action、account menu。命令只作用于当前 selection 或 pointer 下对象时使用`ContextMenu`,例如 rename、duplicate、reveal、remove。Context menu 不能是 essential command 的唯一入口;按场景同时提供 menu bar、toolbar、keyboard 或 detail view 路径。 不要为了视觉极简把所有 action 都藏入 menu。Discovery 与速度同样重要:主要 action 保持可见;danger item 有清楚 label 并与普通命令分隔;同一 command 在所有入口使用一致 verb、icon、shortcut、enabled state 与执行结果。 ### 按钮表示应用操作,链接只表示外部资源 改变应用状态、确认决策、打开工具、提交数据或执行命令时使用按钮,并根据局部层级选择表现: * 当前决策的默认提交使用主按钮(`primary`); * 普通可见操作使用默认按钮; * 需要清楚边界但强调较低时使用描边按钮(`outline`); * 工具栏或行内的熟悉低强调操作使用幽灵按钮(`ghost`); * 只有符号广为人知时才使用图标按钮,并提供无障碍名称与工具提示。 不能因为按钮是界面中唯一操作、位于右上角,或团队希望获得更多点击就使用主按钮。主按钮表达默认提交及其键盘行为。添加项目、打开工具、刷新视图等普通命令,应根据局部层级使用默认按钮、描边按钮或幽灵按钮。 带下划线的 Link 只用于外部 resource target:URL、网页、在线文档或 Email 地址。它使用手形 cursor,因为语义是离开当前 application context 并访问该 resource。不能为了让功能 command 看起来安静而套 Link 样式。Link 形态的 Delete、Save、Refresh、Add、Open menu 或应用内跳转会隐藏 control affordance,并向 accessibility 暴露错误 role。 “查看”不会让 app 内 destination 变成 Link。完整报告、分析、detail panel 或 local record 仍应通过 Button、row、card、tab 或 disclosure control 打开。当 card 已说明会打开什么时,可以使用“完整分析”这类依赖上下文的短 label;下划线只留给真正交给 browser 或 mail client 的 resource。 所有应用内导航——sidebar row、tab、breadcrumb、list item、打开本地 view、切换 workspace——必须使用对应原生 component 或 Button/Action。视觉强调通过 Button variant 或 navigation component 的 selected state 决定,不能通过伪装语义实现。 ## 反馈与浮层 选择足以承载当前决策的最小 surface: * tooltip:短解释或快捷键; * popover:不中断任务的上下文控件; * menu:紧凑 action 列表; * notification:无需用户决策的异步状态; * dialog:聚焦决策或短表单; * alert dialog:有后果操作的明确确认; * sheet:需要更多持续空间的辅助工作。 Alert 即使不是 modal,也会打断视觉 hierarchy。它只用于当前任务中需要立即注意的重要异常信息,不能作为普通 description、tip 或空白内容的装饰 container。不需要立即注意或 action 时,使用 inline help、muted text 或普通 section。 避免 overlay 叠 overlay。Escape 关闭最上层可关闭 surface,焦点回到 trigger 或下一个逻辑目标。 Overlay action 必须指向 overlay 实际展示的 object 或 state。例如只有独立 recent-history section 可见且存在 entry 时,才显示“清除历史”。Search result、recent item 与 favorite 是不同 collection,应明确 label 和分区,不能混成一个没有解释的 list。不适用的 action 应隐藏,或 disabled 并说明原因;不能在 footer 塞一个作用不明的 trash icon。 Footer 不是无处安放 capability 的收纳区。它可以显示适用于整个 surface 的 shortcut、status 或 action,但每项都必须回答:object 是什么、为什么现在可用、影响什么 scope、执行后哪个 visible state 会改变。 ## 动效 动效用于解释变化,不是环境装饰。出现、关闭、展开和空间连续性使用短 transition。如果 opacity 或 transform 已能表达关系,就不要动画大面积 layout。遵守 reduced motion,状态理解不能依赖动画,也不要给所有组件安装默认动画。 Motion policy 属于 styled/application 层。Base 可以拥有 transition 所需的生命周期机制和 geometry,但不决定所有产品都 fade 或 slide。独立动画值使用稳定 ID;被打断时从当前采样值平滑反向,而不是回到旧端点重新开始。 ## 数据密集界面 Dense 不等于拥挤。在 table、tree、command palette、editor 与 dock 中: * header 和行主要标识保持稳定; * 可比较值对齐,必要时使用 tabular number; * 区分 focus、hover、active row 与 multi-selection; * sorting/filtering 可见且可逆; * filter/reorder 后按 domain ID 保持 selection; * virtualize 大集合,但不改变键盘语义; * 次要 column 和 inspector 渐进披露; * empty state 解释下一步。 一致字段的比较使用 table;异质内容扫描使用 list;真实层级使用 tree;只有用户需要安排长期工具或文档时才使用 dock。不要把复杂数据组件当作一种视觉样式。 ## 界面用词 文字是界面架构的一部分。一个功能中的入口、对象、命令、状态和结果应作为整体设计,不能按照实现逐项翻译。默认使用在当前上下文中仍然准确的最短表达。 ### 让上下文承担上下文 不要重复界面已经表达的信息。侧栏入口通常只需写对象或领域:使用 `Users`,而不是`User Management`;使用“快捷键”,而不是“快捷键配置管理”。如果表格内容本身都是操作,可以省略泛化的“操作”列名。对话框标题已经是“删除‘路线图’?”时,正文不必再次提问。 这是上下文经济,不是为了短而删。文字能够改变决策时必须保留:受影响范围、不可逆后果、异常前提或恢复方式。每一个额外词都应回答当前布局尚未回答的问题。 入口与对象使用名词(“用户”“外观”“订单”),命令使用动词(“保存”“复制”“导出”),状态使用形容词或短语(“离线”“已是最新版本”“待审核”)。除非能够区分真实领域概念,否则避免“管理”“模块”“页面”“功能”“操作”“系统”等包装词。 ### 分别写作,而不是翻译句形 先统一意图、层级与术语,再按每种语言的自然表达分别写作。不要保留源语言的语序、词数、客套填充或词性。中文概念直译可能是 `User Management`,自然英文却是 `Users`;忠实是保留用途,不是保留字面形式。 信息架构已经表达的词应删除。在 `Settings` 中,入口通常只写 `Account`,不用重复`Account Settings`,更不能写不自然的单数 `Account Setting`。正确英文来自控件的角色和相邻文字,而不是脱离上下文的中文短语。 为重复出现的对象、命令与状态维护一份小型产品词表。工具栏、菜单、右键菜单、对话框、快捷键搜索与文档对同一概念使用同一个词,除非上下文确实改变了含义。文案必须放回真实界面评审;只看翻译文件,往往发现不了相邻文字的重复和作用范围不一致。 技术写作不追求纯中文。已经稳定的 UI framework 术语,如果翻译后不够准确,可以保留英文;API 标识符保持原名并使用代码格式。普通叙述不能仅为显得专业而夹杂英文。术语首次出现时按需说明含义,此后在界面、文档和 API 示例中保持同一种写法。 ### 按钮与确认对话框 按钮默认简短,通常一至两个词,并说明结果,而不是手势或控件。使用“保存”“移动”“删除”,不用“点击进行保存”“执行移动操作”“确认删除操作”。不提交并离开的操作统一使用“取消”。`OK` 只用于确认已经读到纯信息。 简短是默认值,不是机械字数限制。当额外文字能够揭示后果,或区分容易混淆的选择时,应有意使用更长但仍可扫描的文字,例如“仅从此分组删除”与“从所有位置删除”,或“不保存并重新启动”。长度必须换来决策所需的信息,不能重复标题或正文。 能够准确概括结果时,确认按钮使用最具体的短词: | 上下文 | 较弱 | 推荐 | | --- | --- | --- | | 删除对话框 | “是”“Sure”“确认删除” | “删除” | | 未保存修改 | “确认”“是” | “放弃修改” | | 纯信息确认 | “确认操作” | “知道了”或 `OK` | | 无法用一个准确动词概括的复杂承诺 | “是” | “确认” | 当对话框已完整说明复杂承诺,而不存在准确的结果动词时,“确认”是合理的后备用词;它不应替代本来清楚的命令。`Sure` 是口语回应,不是稳定的英文命令,含义也不足以进入标准词表。 确认对话框应组成一个紧凑决策: * 标题写决策或条件,例如“删除‘路线图’?”; * 正文只补充新的作用范围、后果或恢复方式; * 操作使用“取消”和结果词,例如“删除”; * 破坏性样式标记破坏性结果,但不能代替准确用词。 能够说明实际情况时,不使用“提示”“警告”“错误”“确认”等泛化标题。避免“您确定要……吗”“是否需要……”“请注意……”以及状态已经表达清楚时的“成功”等套话。礼貌来自冷静、尊重的语气,不来自重复的“请”。 ### 大小写、标点与符号 英文 UI 默认使用 sentence case:`Reset layout`,不用 `Reset Layout` 或 `RESET LAYOUT`。专有名词与约定俗成的缩写保持原样。只有原生命令菜单等确实受益于平台惯例时才使用 title case,并在同一类控件中保持一致。 全大写可以作为克制的排版强调,适合极短的分组标签、眉题、状态,以及既有缩写或代码。它通过紧凑的字形和适当字距形成接近加粗的层级,但不应用于按钮、长标题、完整句子或密集列表。同一区域不要同时用全大写、强色和粗体争夺注意。不要自动转换所有字符串;产品名、缩写与本地化内容需要保留正确大小写。 标签、按钮、菜单项、标签页、标题、占位文字与短状态末尾不加句号;完整的说明、警告和错误句子使用完整标点。日常成功或失败消息避免感叹号。中文句子使用全角标点,短控件文字同样按语义省略句末标点。 使用单个省略号字符(`…`),不用三个句点。凡按钮或菜单项会打开对话框、sheet、独立窗口,或命令完成前还需要用户输入或选择,文字末尾都加省略号,例如 `Settings…`、“导出…”。立即执行的命令不加。正在进行的任务使用不确定进度指示器,不用装饰性的点号表达。 错误说明发生了什么,并在有帮助时给出下一步恢复方式。成功反馈只在结果状态尚不可见时出现。使用“无法保存。请检查网络连接后重试。”,不要只显示技术代码或长篇道歉;文档已经明显进入保存状态时,不再弹出“保存成功”。 ## 国际化与平台适配 文案必须承受扩展、CJK 排版和不同快捷键写法。不要按一条英文 label 固定控件宽度;不要把文字放进 raster asset;不要拼接翻译片段;只有存在 tooltip 等恢复路径时才截断 label。 尊重有意义的平台差异:Command/Control、原生窗口装饰、系统 appearance、scrollbar、menu 和 notification 能力。各平台的信息架构应稳定,但不能为了表面像素一致而消除用户熟悉的平台行为。 ## AI 生成界面的规则 AI 修改 GPUI 界面前必须阅读相邻 feature、theme token 和组件文档,并先说清主要任务、state owner、component composition 和 keyboard path。不能从 React/Shadcn 示例推断 API,也不能因为方法名“听起来合理”就发明 GPUI 方法。 AI 输出只有在人能够解释 hierarchy、density、component choice 与所有例外字面量时才算完成。看起来合理的截图不是证据;键盘、焦点、动态内容、主题、resize 和失败状态都属于设计。 ## 无障碍检查表 界面完成前验证: * 所有 action 都能通过键盘到达和执行; * focus 顺序符合视觉与任务顺序; * focus 始终可见,overlay 关闭后正确恢复; * control 有名称,仅图标 control 有 tooltip; * 文字与有意义边界对比度充足; * 状态不只依赖颜色; * disabled 与 read-only 可区分; * label、error、description 靠近对应 control; * 长翻译和更大字体下仍可用; * 即使紧凑布局,pointer target 仍舒适。 ## 设计评审清单 评审不是清点部件,而是判断界面是否做出了正确取舍。依次回答: 1. **任务清楚吗?** 新用户能否直接看懂界面用途、主要操作和下一步,而不必学习、猜测或试错? 2. **操作兑现承诺吗?** 文案、控件、状态、范围、反馈与结果是否始终描述同一件事? 3. **层级明确而克制吗?** 核心功能是否获得应有的空间,同时主按钮、强色、粗体、徽标和警示保持稀缺? 4. **还能做得更少吗?** 能否删除、合并或推迟某个入口、选项或状态,同时完整保留主要任务? 5. **结构准确吗?** 同层内容是否共享对齐轴,相等间距是否精确到渲染像素,滚动条是否贴住实际滚动区域的边缘? 6. **遵守组件体系吗?** 标准控件是否保留统一的几何、状态、键盘与关闭行为,外观是否来自主题与尺度令牌? 7. **所有状态与约束下都可用吗?** 检查键盘与焦点、空白、加载、失败、无权限、长翻译、缩放、最小窗口和减少动态效果。 8. **在真实窗口中验证了吗?** 使用真实组件、文案和代表性内容亲手完成任务,而不只评审一张理想截图。 继续阅读[编码指南](./coding-guides.md),把这些设计决策落实为 GPUI 架构和代码。 --- --- url: /zh-CN/contributors.md description: 为 GPUI Kit 做出贡献的开发者。 ---