从零开发一个 Switch 控件
本章我们将从零开始,完整实现一个 Switch(开关)控件。通过这个过程,你会掌握控件开发的所有核心环节:定义样式枚举、实现 View trait、绘制、测量、支持原子类、响应鼠标事件。
Switch 是一个合适的学习案例:它有"开/关"两种状态,需要绘制轨道和滑块,需要响应点击,还能通过原子类定制颜色和尺寸。学完这一章,你就可以按照同样的模式开发自己的控件了。
最终效果预览
第一步:创建文件
在你的控件库 crate 中新建文件:
并在 lib.rs 中注册:
第二步:定义样式枚举
使用 #[derive(Resolver)] 定义 Switch 支持的样式属性。派生宏会自动生成 SwitchStyleKey、SwitchStyleResolver、SwitchStyleComputed 等辅助类型。
要点:
- 每个变体携带一个具体类型,
Resolver宏会为每个变体生成对应的链式方法。 - 变体名用
PascalCase,生成的方法名会自动转为snake_case(如TrackColor→.track_color(...))。 - 变体中的类型会被包装为
Option<T>放入Computed结构体。
第三步:定义控件结构体
关键设计:
enabled: bool是当前值的快照,由create_updater建立的响应式依赖自动保持同步。enabled_signal: RwSignal<bool>保留信号引用,用于在on_button_down中写入新值。- 这种拆分是 Flor 的推荐模式:读取用值,写入用信号;当外部修改信号时,
create_updater会自动触发on_update_state更新enabled字段。
第四步:实现 View trait
4.1 必须实现的方法
4.2 on_update_state — 响应式更新
当外部通过信号更新样式或状态时,框架会调用 on_update_state。你需要 downcast 并更新内部字段:
响应式原理:构造函数中使用
create_updater注册了bool的更新回调。当外部代码修改enabled信号(如enabled.set(true))时,create_updater会检测到变化,自动调用view_id.update_state(Box::new(new_value)),最终触发这里的on_update_state。不需要手动传递信号给控件。
4.3 on_measure — 测量
on_measure 告诉布局系统这个控件需要多大的空间。对于 Switch,尺寸主要由样式决定:
要点:
known_dimensions是父容器传下来的已知尺寸约束。如果父容器设了固定宽高,这里会收到Some(...)。- 优先使用
known_dimensions,没有时才用控件自己的默认值。 self.style.get_data_borrow(control_state)获取当前控件状态下的计算样式。
4.4 on_draw — 绘制
这是控件开发中最核心的方法。Switch 需要绘制轨道(背景)和滑块(圆形):
绘制要点:
- 使用传入的
abs_location(绝对坐标)和layout.size(布局尺寸),不要重新推导。 - 从
computed中读取样式值,用unwrap_or提供默认值。 - 根据
control_state绘制不同状态(常规 vs 悬浮 vs 禁用)。 - 圆角值
h / 2.0可以做出"胶囊"形状。 - 滑块用
thumb_size / 2.0作为圆角半径,做出圆形。
4.5 on_update_class — 原子类支持
让用户可以通过 .class("switch-track-blue") 设置样式:
原子类解析要点:
- 先用
switch_control_state(control_state)切换到当前状态层,这样后续的set_*调用会写入正确的状态。 - 用
strip_prefix匹配类名前缀,然后用parse_color等共享解析方法。 - 解析成功后直接
return Ok(()),不要让后续规则意外匹配。 - 不认识的类名直接忽略,
on_update_class会被多次调用,每次只处理一个类名。
4.6 on_button_down — 响应点击
Switch 被点击时切换状态:
这里也可以用
on_click。区别在于on_click要求 down 和 up 命中同一个控件,on_button_down在按下时立即触发。对于开关,on_button_down体验更即时。
第五步:构造函数与工厂函数
构造函数要点:
ViewId::new_with_layout为控件创建LayoutResolver。create_updater是关键:它接收两个闭包——第一个读取信号值(建立响应式追踪),第二个在值变化时通过view_id.update_state触发on_update_state。返回值是信号的当前值,存储为enabled: bool。enabled_signal保留信号引用,用于on_button_down中的写入操作。computed_switch_style函数由#[derive(Resolver)]宏自动生成,你不需要手动编写。
第六步:理解 compute 函数(宏自动生成)
#[derive(Resolver)] 宏默认会自动生成一个 computed_switch_style 函数,它负责将枚举的原始样式值映射到 Computed 结构体。你不需要手动编写它,宏已经为你生成了等效于下面的代码:
关键点:
- 宏生成的 compute 函数名规则是
computed_{EnumName的snake_case}(如LabelStyle→computed_label_style)。 - 它只做简单的
clone映射,不处理单位转换。单位的 px 转换由Resoled的get_data_borrow内部处理。 - 如果不需要这个函数,可以通过
#[resolver(computed_fn = false)]关闭。详见 Resolver 派生宏 文档。
完整文件一览
下面是 switch.rs 的完整代码:
使用示例
基础用法
配合原子类
配合信号联动
配合 style builder
开发控件 checklist
回顾整个流程,开发一个 Flor 控件需要完成以下步骤:
- 定义样式枚举 — 使用
#[derive(Resolver)]声明所有可配置的样式属性。 - 定义控件结构体 — 持有
view_id、状态字段和style resolver。 - 实现
Viewtrait:view_id()— 返回结构体中的view_id。tag()— 返回调试用的标签名。on_update_state()— 处理update_state传入的样式更新。on_measure()— 有自然尺寸的控件需要实现,返回控件需要的尺寸。on_draw()— 核心绘制方法,读取 computed 样式和控件状态来绘制。on_update_class()— 解析原子类字符串,转换为样式更新。- 按需覆盖鼠标/键盘事件处理(
on_button_down、on_click等)。
- 编写构造函数 — 创建
ViewId、LayoutResolver、StyleResolver,使用create_updater建立响应式依赖,computed_xxx函数由宏自动生成。 - 编写工厂函数 — 提供简洁的 API 入口。
进阶话题
添加动画
如果你想给滑块添加平移动画,可以在 on_frame 中实现:
支持禁用状态
在 on_draw 中检查 ControlState::Disabled,绘制灰色轨道和半透明滑块。在 on_button_down 中检查 self.view_id.control_state() == ControlState::Disabled 并忽略点击。
扩大点击热区
如果你的滑块太小不好点,覆盖 on_hit_test 来扩大命中区域:

