Skip to content

Color Picker

为自定义颜色选择器提供状态与交互基础。

和所有 GPUI Base 原语一样,Color Picker 只提供行为和语义结构,不规定产品视觉语言。请使用 GPUI 样式并组合导出的部件,使其符合你的设计系统。

示例

原生示例和页面上方的 WASM 预览共用同一份实现:

bash
cargo run -p gpui-base --example components -- color-picker

导入

rust
use gpui_base::{ColorPicker, ColorPickerEvent, ColorPickerState, ColorSwatch};

结构与 API

示例组合上述公开类型。GPUI 的标准样式和事件 trait 负责表现,Base 类型负责交互结构。权威实现位于 components/color-picker.rs,原生与浏览器预览编译的是同一文件。

状态与事件

ColorPickerState 保存颜色,ColorPickerEvent 报告用户变更。

受控状态应保存在父渲染类型或 GPUI entity 中;在回调中更新并调用 cx.notify(),不要在每次渲染时重建持久 entity。

完整 Rust 示例

rust
use super::*;
use gpui::{Focusable as _, Hsla, MouseButton};

impl BaseShowcase {
    pub(in super::super) fn color_picker(
        &self,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) -> impl IntoElement {
        // A builder-supplied default cannot reach the hex field and the sliders
        // without a window, so flush it on the first render.
        self.color_picker
            .update(cx, |state, cx| state.sync_pending_value(window, cx));

        let picker = self.color_picker.read(cx);
        let open = picker.is_open();
        let selected = picker.value();
        let displayed = picker
            .displayed_color()
            .unwrap_or(super::example_rgb(0x171717).into());
        let hex = picker.hex_input().read(cx).value();
        let focus_handle = picker.focus_handle(cx);
        let hex_input = picker.hex_input().clone();
        let state = self.color_picker.clone();

        let trigger_state = state.clone();
        let trigger = div()
            .id("color-trigger")
            .w_full()
            .h_7()
            .px_2()
            .flex()
            .items_center()
            .gap_2()
            .border_1()
            .border_color(super::example_rgb(0x171717))
            .bg(super::example_rgb(0xffffff))
            .on_click(move |_, _, cx| {
                trigger_state.update(cx, |state, cx| state.toggle_open(cx));
            })
            .child(
                div()
                    .size(px(14.))
                    .bg(displayed)
                    .border_1()
                    .border_color(super::example_rgb(0x171717)),
            )
            .child(hex)
            .child(div().flex_1())
            .child(if open { "⌃" } else { "⌄" });

        let swatches = div().flex().gap_1().children(
            [0xdc2626u32, 0xd97706, 0x16a34a, 0x2563eb, 0x7c3aed]
                .into_iter()
                .enumerate()
                .map(|(index, value)| {
                    let color: Hsla = super::example_rgb(value).into();
                    let hover_state = state.clone();
                    let click_state = state.clone();
                    ColorSwatch::new(("swatch", index), color)
                        .selected(selected == Some(color))
                        .size(px(24.))
                        .bg(color)
                        .border_1()
                        .border_color(if selected == Some(color) {
                            super::example_rgb(0x171717)
                        } else {
                            super::example_rgb(0xffffff)
                        })
                        // Hovering previews without committing; leaving restores
                        // the committed color.
                        .on_hover(move |color, entered, window, cx| {
                            hover_state.update(cx, |state, cx| {
                                if entered {
                                    state.preview_color(color, window, cx);
                                } else {
                                    state.clear_preview(window, cx);
                                }
                            });
                        })
                        .on_click(move |color, _, window, cx| {
                            click_state
                                .update(cx, |state, cx| state.select_color(color, window, cx));
                        })
                }),
        );

        let content = div()
            .w(px(220.))
            .mt_1()
            .p_2()
            .flex()
            .flex_col()
            .gap_2()
            .border_1()
            .border_color(super::example_rgb(0x171717))
            .bg(super::example_rgb(0xffffff))
            .child(swatches)
            .child(
                InputBase::new("color-hex-input")
                    .w_full()
                    .h_7()
                    .px_2()
                    .flex()
                    .items_center()
                    .border_1()
                    .border_color(super::example_rgb(0xd4d4d4))
                    .styles(|styles| {
                        styles.focused(|style| style.border_color(super::example_rgb(0x171717)))
                    })
                    .on_mouse_down(MouseButton::Left, move |_, window, cx| {
                        hex_input.update(cx, |input, cx| input.focus(window, cx));
                    })
                    .child(picker.hex_input().clone()),
            );

        let open_state = state.clone();
        let root = ColorPicker::new("example-color-picker")
            .open(open)
            .track_focus(&focus_handle)
            .accessibility_label("Brand color")
            .on_open_change(move |open, _, cx| {
                open_state.update(cx, |state, cx| state.set_open(open, cx));
            })
            .w(px(220.))
            .text_xs()
            .child(trigger);

        Popup::new("example-color-picker-popup", root).when(open, |this| this.content(content))
    }
}

可访问性

为色样提供文本名称或数值,并确保键盘用户可以完成选择。

注意事项

在支持的位置使用稳定元素 ID,并在消费端设计系统中验证焦点、悬停、按下、选中、禁用、减少动态效果和高对比度状态。