gpui-kit Slider 原语实战:状态驱动的范围输入组件架构与实现
【免费下载链接】gpui-kitRust GUI components for building fantastic cross-platform desktop application by using GPUI.项目地址: https://gitcode.com/GitHub_Trending/gp/gpui-kit
Slider 是 gpui-kit 中gpui-base提供的状态驱动范围输入原语:轨道(Track)、已选区(Indicator)和滑块(Thumb)可独立设置样式,行为与语义结构由基础类型负责。本文以 Slider 中文文档 为核心骨架,结合 源码 与 示例实现,讲解如何用它在 GPUI 中构建符合自己设计系统的滑杆控件。
核心设计思想:行为与表现分离
与gpui-base中的所有原语一致,Slider 只提供行为和语义结构,不规定产品视觉语言:
- 状态:
SliderState持久保存值、范围、步进与当前交互状态; - 结构:
Slider/SliderTrack/SliderIndicator/SliderThumb是无样式行为根节点,负责把指针位置映射为值; - 表现:由消费方用 GPUI 样式(
Styled)、事件 trait(InteractiveElement等)组合这些部件实现。
这种分层使同一份行为代码可以承载任意视觉主题,也保证了原生与 WASM 预览编译的是同一份实现。
运行示例
原生示例与页面上方的 WASM 预览共用同一份实现,运行命令:
cargo run -p gpui-base-examples -- slider该命令通过 crates/base/examples/showcase/mod.rs 中的共享 showcase 初始化应用、创建窗口并提供BaseShowcase状态(命令选择逻辑见 mod.rs)。
导入
use gpui_kit::base::{Slider, SliderState};gpui_kit::base是对gpui_basecrate 的重导出(见 crates/kit/src/lib.rs),SliderState、Slider、SliderTrack、SliderIndicator、SliderThumb均通过 crates/base/src/lib.rs 的pub use对外公开。权威示例实现位于 crates/base/examples/showcase/components/slider.rs。
结构与 API
示例组合以下公开类型,GPUI 的标准样式与事件 trait 负责表现,Base 类型负责交互结构:
| 类型 | 职责 |
|---|---|
Slider | 行为根节点,声明role: Slider并暴露 ARIA 数值属性,提供水平/垂直方向与禁用能力 |
SliderTrack | 无样式轨道,记录几何信息以将指针位置映射为值,处理轨道点击与整条拖动 |
SliderIndicator | 已选区指示器,通过on_prepaint记录用于值映射的 bounds |
SliderThumb | 可拖动的滑块,支持start标记以区分范围模式下的起始/结束把手 |
SliderState | 状态实体,持久保存值、范围、步进、百分比位置、bounds 与拖动标志 |
这些类型都在 crates/base/src/slider.rs 中定义。
状态与事件
SliderState是EventEmitter<SliderEvent>(见 slider.rs),持久的gpui::Entity<SliderState>在示例中通过cx.new(|_| SliderState::new().min(0.).max(100.).default_value(64.))创建(见 showcase/mod.rs),并通过cx.observe在状态变化时触发重绘。
SliderEvent提供两种事件(slider.rs):
Change(SliderValue):拖动或点击过程中连续发出;Release(SliderValue):用户松开滑块后发出一次。
受控状态应保存在父渲染类型或 GPUI entity 中;在回调中更新并调用cx.notify(),不要在每次渲染时重建持久 entity。
完整 Rust 示例
权威实现(原生与浏览器预览编译同一文件)位于 crates/base/examples/showcase/components/slider.rs,核心组合如下:
let percentage = self.slider.read(cx).percentage().end; let thumb_size = 14.; div() .w_56() .text_xs() .child(div().mb_2().flex().justify_between() .child("Volume").child("Drag to adjust")) .child( Slider::new(&self.slider).w_full().h_7().child( SliderTrack::new(&self.slider) .relative().w_full().h_full() .child(div().absolute().top(px(13.)).left_0().w_full() .h(px(2.)).bg(super::example_rgb(0xd4d4d4))) // 轨道 .child( SliderIndicator::new(&self.slider) .absolute().top(px(13.)).left_0().w_full().h(px(2.)) .child(div().absolute().top_0().bottom_0().left_0() .right(relative(1. - percentage)) .bg(super::example_rgb(0x171717))), // 已选区 ) .child( SliderThumb::new(&self.slider) .absolute().top(px(7.)).left(relative(percentage)) .ml(px(-thumb_size / 2.)) .size(px(thumb_size)) .bg(super::example_rgb(0xffffff)) .border_1().border_color(super::example_rgb(0x171717)), // 滑块 ), ), )状态构建与观察
showcase/mod.rs 演示了状态生命周期:
let slider = cx.new(|_| SliderState::new().min(0.).max(100.).default_value(64.)); cx.observe(&slider, |_, _, cx| cx.notify()).detach();状态模型与刻度:SliderValue与SliderScale
SliderValue支持单值与范围两种形态(slider.rs):
SliderValue::Single(f32):单值;SliderValue::Range(f32, f32):范围(双滑块)。
支持从f32、(f32, f32)元组和Range<f32>转换,默认值为SliderValue::Single(0.0)。提供start()/end()/clamp()/is_range()等方法,其中set_start保证start <= end,set_end保证end >= start(slider.rs)。
SliderState的默认配置:min = 0.0、max = 100.0、step = 1.0、刻度Linear(slider.rs)。
线性刻度
值均匀分布在范围上:value = min + (max - min) * percentage(slider.rs)。
对数刻度
SliderScale::Logarithmic适用于音量(人耳听觉近似对数)、频率(音符)、缩放级别等低值处需要更精细控制的参数(slider.rs):
let slider = SliderState::new() .min(1.0) // 对数刻度下必须 > 0 .max(1000.0) .scale(SliderScale::Logarithmic);映射公式为base.powf(percentage) * min(base = max / min)。在 1..1000 范围内,滑块移动 1/3 处约得 ~10,2/3 处约得 ~100,整个范围均匀覆盖 3 个数量级。
约束:对数模式下min > 0且min < max,否则在min()/max()/scale()中直接assert!失败(slider.rs)。
指针交互与事件流
轨道点击(含范围模式选边)
SliderTrack在鼠标按下时,若为范围模式,先根据点击位置与已选区中心的比较决定拖动起始把手还是结束把手(position < center则拖 start),再调用update_value_by_position(slider.rs)。
整条拖动
单值模式下SliderTrack注册on_drag+on_drag_move,把轨道本身作为拖动目标(DragSlider),拖动过程中持续更新值(slider.rs)。
滑块拖动
SliderThumb在on_mouse_down中stop_propagation阻止事件冒泡到轨道,然后用DragThumb((entity_id, start))启动自身拖动,on_drag_move中根据start标记更新起始或结束把手(slider.rs)。
值映射与步进
update_value_by_position(slider.rs):
- 依据方向取指针相对 bounds 的位置:水平用
position.x - bounds.left(),垂直用bounds.bottom() - position.y; - 归一化为 0..1 的百分比,范围模式下按把手边界 clamp;
- 经
percentage_to_value反算值并做步进取整(value / step).round() * step; - 更新内部百分比与值,
cx.emit(SliderEvent::Change(...))并cx.notify()。
释放事件
Slider在on_mouse_up与on_mouse_up_out都监听(slider.rs),handle_release仅在用户确实按过/拖过(dragging == true)时才发出一次SliderEvent::Release(slider.rs)。这保证 Release 只在真实交互后出现。
方向与禁用
Slider::new(&state)默认水平,可用.horizontal()/.vertical()/.axis(Axis)指定方向(slider.rs);Slider、SliderTrack、SliderThumb均提供.disabled(bool)。禁用后不注册鼠标事件,但 ARIA 属性与结构仍渲染(slider.rs)。
可访问性
Slider渲染时声明Role::Slider,并暴露aria_numeric_value(当前值取value().end())、aria_min_numeric_value、aria_max_numeric_value、aria_numeric_value_step与aria_orientation(slider.rs),同时注册AccessibleAction::Increment/Decrement无障碍动作,按步进在范围内增减(slider.rs),并保留方向键、Page Up/Down 等键盘操作能力。
测试验证
crates/base/src/slider.rs 中的单元测试覆盖了关键行为:
SliderValue的三种转换与clamp(如Range(-1., 12.).clamp(0., 10.) == Range(0., 10.));- 线性状态下的百分比映射与范围排序(
min 0 / max 200 / 默认 (50, 150)→ 百分比0.25..0.75); - 对数刻度映射(
min 1 / max 1000 / 值 10→ 百分比端值 ≈ 1/3); - 对数校验:
scale(SliderScale::Logarithmic)且min = 0时 panic。
注意事项
在支持的位置使用稳定元素 ID(示例中如slider-bar-container、slider-bar、slider-thumb,见 slider.rs),并在消费端设计系统中验证焦点、悬停、按下、选中、禁用、减少动态效果和高对比度状态。相关主题可进一步参考 基础原语文档 与 Slider 组件层文档。
【免费下载链接】gpui-kitRust GUI components for building fantastic cross-platform desktop application by using GPUI.项目地址: https://gitcode.com/GitHub_Trending/gp/gpui-kit
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考