--- url: /website/zh/guide/book-index.md --- # 唠叨之书 ## 始依 在学了rust那会儿,我有很多想做的东西,可我总是个思想上的巨人,行动上的矮子。有一天,我心血来潮的开始了解rust的gui框架,以为我后续的开发做准备。 那会我感觉看起来好像没什么可用性很高的框架啊,我也试过其中一款框架,可是因为api的原因,竟然连个界面切换的功能都无法实现。 可那会儿却没什么令我满意的框架,原因也很简单,每个框架都有自己的问题,抛开生态不说,我认为现有的框架最大的问题是“设计"。 其中,我翻到了`miniquad`的源码,受其鼓舞,最终,我决定自己开发一款`rust` 的`gui`框架。 ## 思考与设计与理念与成果 开发这个框架,我是慢慢来的,从最初开始还不太熟练,总是被编译器卡住到现在成了我最熟练的语言。 `flor` 进入验证阶段的大版本,到现在是第五个,这个版本,与我最初的想法有很大的出入,像极了`rust`的创始人看到了现在的`rust`的样子,和自己想的完全不一样,但与之不同的是我很满意现在的设计。 当前的设计,融合了传统的控件式开发和现代的`web`开发的各种设计元素。其表现,完全符合我心目中的既要,又要,还要的`rust` `gui`框架。 ### 最开始的设计目标 - 高性能 - 低编译后体积 - 高api一致性 - 多窗口支持 - 不强制绑定上下文 > 我认为现有的`rustgui`框架的问题: > > - 不支持多窗口 > - 强制对上下文进行绑定 > - 数据跨范围访问 > - 控件/组件扩展 > > 其中,最大的问题就是,强制绑定上下文,所以我的每一版设计都在向着这个方向努力。只不过之前的版本更倾向与寻找一种类`c#`那种窗口类的思想靠拢。 但是随着一次又一次的设计验证失败,有一天,我看到了`floem`框架,深受其启发,以其声明式布局、信号系统、对控件的管理思想进行了借鉴,开始进入了第五版(目前版本)设计。第五版设计进入后,很自然而然的实现了我要的api一致性,而且可扩展性表现非常良好,就一步一步的走到了现在。 ### 目前设计的成果与表现: - 高性能 - 低编译后体积 - 高api一致性 - 自写平台支持 - 不强制绑定上下文 - 支持跨线程交互的,响应式`信号系统`\[1] - 句柄暴露,支持作为`native`使用 - 多窗口支持,可在任意线程、任意位置创建窗口 - 基于`即时模式`的`保留模式`\[2],天然支持高性能动画。各窗口可独立设置刷新模式。 - `声明式UI DSL` - `原子类样式解析`支持\[3] - 终端应用体验偏简单,复杂度由框架/控件作者吸收。 - 支持异步任务调度和 UI 更新,无需手动切换 UI 线程 \[1]: `信号`支持跨线程,实现了`Copy`特征,可以随处移动,随处赋值,不用像`c#` 那样因为等待`ui`线程而出现性能问题。\ \[2]: `保留模式`是基于`即时模式`实现的,对于需要高刷新的控件,`保留模式`依然具备`即时模式`的性能。\ \[3]: 构建时提供`原子类名`,`原子类名`会被框架的布局系统和对应的控件按照顺序分别解析,高效可控构建应用布局。 > 如果你也认可这些设计理念,那你一定会喜欢这个框架的。 这些设计特点的集于一身,还带来了一些别样的隐性好处,比如: - 天然支持协作 - 无论是传统开发过来的还是现在web开发过来的程序员都可以在其中找到自己熟悉的思想。 - 支持异步任务调度和UI更新,无需手动切换线程 ### 设计理念/思想指导总纲 - 以 `API` 的使用体验优先,先思考 `API` 设计成什么样最好用,再思考,如何实现。使用体验,上手难度,维护成本,是框架的核心诉求。 - 在 `布局 DSL` 中使用宏是 `gui` 的恶梦,限制大且不说,IDE 提示支持也不一定好,像个黑盒一样,写错后,报错信息也不一定清楚。 极大的提高了框架的上手难度和维护容易度,所以在本框架中,非必要不使用宏,宏不是不用,而是优先级很低。 - 性能和编译体积是原初需求,所以功能能力支持,会尽可能的拆为特性,以保证框架的最小化能力。 ### 关于未来 Q: 以后还会有下一个迭代版本,或者其他思路的gui框架吗? A: 在我看来,所有的解决方案都是为了回答某个问题出现的,这个框架的最想要解决的问题是,脱离上下文的强制绑定。所以,会不会有,取决于会不会有新的没有答案的问题出现。 其他框架大多是为了某个具体用途、某种范式、某类应用而生,而这个框架的出现,是因为没有任何一个框架不是为了一个专一的事情而出现的。 所以,这个框架的愿景是: 让 `Flor` 成为 `Rust` 生态中大家默认会想到、愿意尝试的 `GUI` 选项之一。当开发者需要原生桌面应用时,希望它能自然进入候选,就像`C++` 的 `Qt`、`C#` 的 `WPF` 那样。 正如是这个框架名字的寓意: > _繁花继开,各放异彩。_ --- url: /website/zh/guide/startup.md --- # ## Crate 结构 图形后端与平台层处于同一依赖层级,互不依赖,由核心框架按 feature 选择。 | Crate | 用途 | 依赖层级 | | ------------------------- | ------------------------- | ------- | | `flor-base` | 共享类型、平台抽象和图形抽象。 | L0 基础 | | `flor-macros` | 过程宏(`style!`、`color!` 等)。 | L0 基础 | | `flor-graphics-direct2d` | Direct2D 渲染后端。 | L1 渲染后端 | | `flor-graphics-opengl` | OpenGL 渲染后端。 | L1 渲染后端 | | `flor-graphics-tiny-skia` | Tiny Skia 渲染后端。 | L1 渲染后端 | | `flor-platform-windows` | Windows 平台实现。 | L1 平台层 | | `flor` | 核心框架:视图系统、信号、事件循环、窗口管理。 | L2 框架 | 层级数字越小越靠底层;同一层的 crate 互不依赖。 ## 框架 Feature `flor` 默认不启用任何 feature。应用侧通常需要至少选择一个渲染后端,然后按需要启用布局、原子类、平台能力等 feature。 ```toml [dependencies] flor = { version = "0.1.0", features = ["direct2d", "tiny-skia", "layout-flex", "class"] } ``` 下面列的是 `flor` crate 暴露给用户使用的 feature。控件库可能会转发其中一部分 feature,但框架侧的名字以这里为准。平台能力的详细用法见 [框架能力](/website/zh/guide/features/overview.md)。 ### 渲染后端 渲染后端负责把 Flor 的绘制命令提交到具体图形实现。应用至少启用一个后端;如果同时启用 GPU 后端和 `tiny-skia`,GPU 初始化失败后会继续尝试 CPU 后端。 | Feature | 后端类型 | 后端 crate | 支持平台 | 用途 | | ----------- | ---- | ------------------------- | ------------- | -------------------------------- | | `direct2d` | GPU | `flor-graphics-direct2d` | Windows | Windows 原生 Direct2D 后端。 | | `opengl` | GPU | `flor-graphics-opengl` | Windows(当前实现) | OpenGL 后端。 | | `tiny-skia` | CPU | `flor-graphics-tiny-skia` | Windows(当前实现) | 纯 CPU 光栅化后端,可作为 GPU 后端初始化失败时的回退。 | `direct2d` 和 `opengl` 都会启用 `gpu-render-backend` 标记;同一次构建中选择一个 GPU 后端即可。`tiny-skia` 会启用 `cpu-render-backend` 标记,可以和一个 GPU 后端一起启用。 ### 绘制能力 这些 feature 不是后端本身,而是在已启用的后端上增加绘制或资源能力。 | Feature | 支持后端 | 支持平台 | 用途 | | ------------- | ----------------------------------- | ----------------------- | -------------------- | | `svg` | `direct2d` / `opengl` / `tiny-skia` | 跟随启用的渲染后端;当前实现为 Windows | 启用 SVG 资源和 SVG 绘制支持。 | | `memory-font` | `direct2d` / `opengl` / `tiny-skia` | 跟随启用的渲染后端;当前实现为 Windows | 启用从内存加载字体的能力。 | 基础形状、文本、图片等常规绘制能力由渲染后端提供,不需要额外打开 `svg` 或 `memory-font`。 ### 布局与 class | Feature | 用途 | | -------------- | ----------------------- | | `layout-flex` | 启用 Flex 布局相关能力。 | | `layout-grid` | 启用 Grid 布局相关能力。 | | `layout-block` | 启用 Block 布局相关能力。 | | `class` | 启用 `.class(...)` 原子类能力。 | 布局 builder 的完整方法见 [布局 Builder](/website/zh/guide/use/builder/layout.md)。class 的用法见 [原子类](/website/zh/guide/use/builder/class.md),layout class 的语法清单见 [layout class 语法](/website/zh/api/layout-class.md)。 ### 平台能力 | Feature | 用途 | | ------------------------------ | ---------------- | | `clipboard` | 剪贴板能力。 | | `drag-drop` | 拖放事件与相关 handler。 | | `tray` | 系统托盘能力。 | | `theme-change` | 系统主题变化相关能力。 | | `monitor` | 显示器信息相关能力。 | | `hi-dpi` | 高 DPI 相关能力。 | | `cross-thread-window-creation` | 允许跨线程创建窗口。 | 事件 builder 和 handler 里受 feature 控制的部分,见 [事件 Builder](/website/zh/guide/use/builder/event.md) 和 [Handler API](/website/zh/api/handler.md)。 ### 开发与内部选择 | Feature | 用途 | | -------------------- | ----------------------------------------- | | `signal-tracing` | 在 release 模式下保留 signal 标签,便于后续追踪和调试。 | | `cpu-render-backend` | CPU 渲染链标记,通常由 `tiny-skia` 间接启用。 | | `gpu-render-backend` | GPU 渲染链标记,通常由 `direct2d` 或 `opengl` 间接启用。 | | `no-check-backend` | 跳过“必须启用至少一个渲染后端”的编译检查,通常只用于特殊中间层或测试场景。 | 应用侧通常直接启用 `direct2d`、`opengl` 或 `tiny-skia`,不要手动只启用 `cpu-render-backend` / `gpu-render-backend` 这类内部标记。 ## 各部分介绍 ### 单纯使用框架需要了解的 默认你会和控件库配合使用,优先看这些内容: - 初始化与消息循环 - [窗口创建与控制](/website/zh/guide/use/window.md) - 支持跨线程的响应式信号系统 - 使用 class 或声明式 builder 配置布局和样式 - 按需开启框架 feature ### 开发控件需要了解的 默认你已经会使用 Flor 编写应用,再继续看这些内容: - `View trait`:新建控件需要实现的 trait,框架会托管生命周期、事件、绘制等流程。 - `ViewId`:控件的唯一 ID,也提供父子关系、焦点、滚动、可视状态、重绘等运行时访问入口。 - `Resolver` 属性宏和结构体:通过枚举生成带控件状态的属性模板代码。 - `Render API`:框架提供大多数控件绘制够用的一套绘制 API;需要高级能力时,再按后端获取对应 handle 自定义处理。 --- url: /website/zh/guide/glossary.md --- # 术语表 这一页用于统一 Flor 文档里的称呼。遇到中英文或 API 命名不完全一致的地方,以这里为准。 ## Flor Flor 是一个控件式 GUI 框架。 中文文档中可以直接使用 `Flor` 这个名字。它的中文译名是“花”。 当描述 Flor 的类型时,称作“框架”。它不是只提供零散工具函数的库,而是一套包含控件系统、信号系统、事件循环、窗口管理、渲染入口等内容的完整的 GUI 框架本身。 ## 控件 / View 中文文档中,一律称作“控件”。 英文语境和 Rust API 中,统一写作 `View`。这里的 `View` 命名参考了 Floem 的命名习惯;名字写进 API 后就保留下来了。因此: - 中文说明里写“控件” - 类型、trait、方法名里保留 `View` - 不额外混用“组件”作为主称呼 例如: ```rust use flor::view::View; fn mount(view: impl View) { // view 在中文文档中通常称为“控件”。 } ``` ## 信号 / Signal 中文文档中称作“信号”,英文和 API 中写作 `Signal`。 信号是 Flor 的响应式状态容器。值变化后,依赖它的 effect、updater 或控件绑定会被重新调度。 ## 读取器 / 写入器 / 读写器 这三个称呼是对一类信号结构体的统称,依据是它们实现了哪些读写 trait。语义核心在 trait,而不是结构体名字本身。 - 实现 `Read` 的信号结构体,可以称作读取器 - 实现 `Write` 的信号结构体,可以称作写入器 - 同时实现 `Read` 和 `Write` 的信号结构体,可以称作读写器或读写信号 常见类型包括: - `ReadSignal`:读取器 - `WriteSignal`:写入器 - `RwSignal`:读写器或读写信号 读写拆分主要用于表达业务访问边界:只读的地方传读取器,只写的地方传写入器,需要两者时传读写器。 ## Builder API 中保留 `Builder`,中文文档中通常称作“链式构建 API”或“构建器”。 例如 `ClassBuilder`、`StyleBuilder`、`LayoutBuilder` 都属于给控件追加样式、布局或行为的链式 API。 ## 原子类 Flor 文档里的“原子类”专指 `.class(...)` builder 带来的能力,也就是通过空格分隔的 class 字符串快速配置控件。 原子类不是一个独立控件类型,也不是泛指所有样式系统。它是 `ClassBuilder` 提供的可选功能:启用 `class` feature 后,控件可以用 `.class("...")` 写 layout class 和控件样式 class。 ## Resolver API 中保留 `Resolver`,中文文档中通常称作“解析器”。 Resolver 负责把 class、样式、状态等描述转换成控件实际使用的样式或布局数据。 --- url: /website/zh/guide/ai.md --- # AI Flor 的 [框架 DSL](/website/zh/guide/use/framework-dsl.md) 与 React 系的函数式 UI 表达有异曲同工之处。 这意味着,在使用 AI 辅助编写 Flor 界面时,AI 并不一定需要提前接受过针对 Flor 的专门训练。由于大量大模型已经在 React、JSX、函数式组件、声明式 UI 等内容上形成了较强的理解能力,Flor 的 DSL 可以在一定程度上复用这种表达习惯。 换句话说,当你向 AI 描述一个界面结构时,它可以借助自己对 React 式 UI 组织方式的理解,较快地迁移到 Flor 的 DSL 上,帮助你完成界面的快速搭建、结构调整和样式组织。具体的控件封装写法见 [框架 DSL](/website/zh/guide/use/framework-dsl.md)。 这也是 Flor “既要,又要,还要”理念的又一次体现: 既要传统 GUI 框架中控件式开发的清晰结构, 也要现代前端声明式 UI 的表达效率, 还要尽可能利用 AI 已经熟悉的函数式 UI 思维,让布局设计变得更容易被生成、修改和迭代。 当然,说句悄悄话:Flor 最开始的设计并不是刻意为了贴近 AI 使用场景而生的。它只是沿着 DSL API 的设计自然演化,最后才发现,这套设计天然适合被 AI 理解和生成。 因此,Flor 的文档站点也选择了带有 AI 支持能力的文档框架来构建。除了面向开发者阅读的普通文档外,本站还提供了面向大语言模型的文档入口,方便你在使用 AI 辅助开发时,将更适合模型阅读的资料提供给它。 如果你想让 AI 帮你编写 Flor 界面、解释 Flor 的 DSL,或者根据描述快速生成一个布局,可以优先使用下面提供的 LLM 文档链接。 ## llms.txt [llms.txt](https://llmstxt.org/) 是一种帮助 LLM 发现和使用项目文档的标准规范。Rspress 遵循该规范,发布了以下两个可直接使用的文件: - [https://flor-rs.github.io/website/zh/llms.txt](https://flor-rs.github.io/website/zh/llms.txt):结构化索引文件,包含所有文档页面的标题、链接与简要描述。 - [https://flor-rs.github.io/website/zh/llms-full.txt](https://flor-rs.github.io/website/zh/llms-full.txt):完整内容文件,将所有文档页面的内容合并为单个文件。 你可以根据使用场景选择合适的文件: - `llms.txt` 体积较小、消耗 token 少,适合让 AI 按需获取具体页面。 - `llms-full.txt` 包含全量文档内容,无需 AI 逐一跟随链接,适合需要 AI 全面了解 Flor 的场景,但会消耗更多 token,建议在支持大上下文窗口的 AI 工具中使用。 --- url: /website/zh/guide/use/init.md --- # ## 初始化与消息循环 因为任何平台的`gui`都无法脱离消息循环和一些功能/API注册,所以初始化和事件循环方法不可少。 ```rust use flor::{FlorGui, views}; fn main() -> Result<(), Box> { // 初始化,以及注册一些东西 FlorGui.init()?; // 省略窗口创建及业务代码 // 进入事件循环,这个方法会阻塞 FlorGui.event_loop()?; Ok(()) } ``` --- url: /website/zh/guide/use/window.md --- # ## 窗口创建与控制 窗口由 `WindowOption` 创建,创建成功后返回当前窗口的 `WindowId`。 ```rust use flor::windows::WindowOption; use flor_lys::label::label; let window_id = WindowOption { title: "Hello Flor".to_string(), width: 800, height: 600, ..WindowOption::default() } .open(move |_window_id| { label("hello flor ~") })?; ``` `WindowOption` 负责窗口的初始配置: ```rust pub struct WindowOption { pub title: String, pub width: u32, pub height: u32, pub rem_px: f32, pub wait_v_sync: bool, pub show_fps: bool, pub continuous_rendering: bool, pub background_color: Color, pub tooltip_delay: Duration, } ``` 默认值如下: | 字段 | 默认值 | 说明 | | ---------------------- | ---------------------------- | -------------------- | | `title` | `"Window"` | 窗口标题。 | | `width` | `800` | 初始窗口宽度。 | | `height` | `600` | 初始窗口高度。 | | `rem_px` | `16.0` | 当前窗口中 `1rem` 对应的像素值。 | | `wait_v_sync` | `true` | 渲染后端是否等待垂直同步。 | | `show_fps` | `false` | 是否显示 FPS。 | | `continuous_rendering` | `false` | 是否持续请求重绘。 | | `background_color` | `Color::rgb(255, 255, 255)` | 窗口背景色。 | | `tooltip_delay` | `Duration::from_millis(500)` | tooltip 响应延迟。 | `continuous_rendering` 只控制事件循环是否持续触发重绘,不会改变 Flor 的界面模型。普通 GUI 应用保持默认值即可;动画、实时预览、游戏循环这类需要每帧刷新的场景再设为 `true`。 ## open 的视图函数 `open` 的签名是: ```rust pub fn open(self, view_fn: F) -> Result where F: Fn(WindowId) -> V + Send + Sync + 'static, V: IntoViewIter, ``` `view_fn` 是窗口根视图的构建函数。Flor 创建平台窗口、渲染器和窗口入口后,会把当前窗口的 `WindowId` 传给它,并把返回值转换为窗口根控件树。 ```rust use flor::platform::WindowId; use flor::view::View; use flor_lys::label::label; fn build_view(_window_id: WindowId) -> impl View { label("hello flor ~") } WindowOption::default().open(build_view)?; ``` 如果不需要使用窗口 ID,就写成 `_window_id`。 ## open 参数里的 WindowId 怎么用 `open(move |window_id| { ... })` 里的 `window_id` 不建议在根视图构建阶段直接调用窗口控制方法。此时窗口正在完成初始化,Flor 还在挂载根视图、注册渲染器、初始化焦点和刷新布局;在这个闭包里直接 `set_size`、`set_window_mode`、`request_redraw`、`destroy` 等,容易让初始化顺序超出预期,产生额外重绘、布局状态不同步或平台层行为问题。 这个 `WindowId` 更适合被捕获到控件事件里,用来在后续事件中操控当前窗口: ```rust use flor::platform::base::WindowApi; use flor::view::builder::EventBuilder; use flor_lys::button::button; WindowOption::default().open(move |window_id| { button("关闭窗口").on_click(move || { let _ = window_id.destroy(); }) })?; ``` 如果只是设置初始标题、尺寸、背景色、刷新模式,应优先写在 `WindowOption` 字段里,而不是在 `open` 的闭包里再改。 ## WindowId `WindowId` 是平台窗口的句柄封装。当前 Windows 平台实现中它是: ```rust #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] pub struct WindowId(pub isize); ``` 它实现了 `WindowApi` 和 `WindowOperations`。调用这些方法时需要导入 trait: ```rust use flor::platform::base::{WindowApi, WindowOperations}; use flor::platform::WindowId; ``` ### 创建与生命周期 | API | 作用 | 使用建议 | | ------------------------------------------------ | ----------------- | ------------------------------------- | | `WindowOption::open(view_fn)` | 创建 Flor 窗口并挂载根视图。 | 应用侧优先使用这个入口。 | | `WindowApi::create_window(title, width, height)` | 只创建平台窗口。 | 框架内部使用;普通应用不要绕过 `WindowOption::open`。 | | `update_window()` | 同步触发平台窗口更新。 | 初始化流程内部会调用;应用侧通常不用手动调用。 | | `destroy()` | 销毁窗口。 | 适合放在按钮点击、菜单命令等事件中调用。 | ### 显示与窗口模式 | API | 作用 | | ----------------------- | --------- | | `show()` | 显示窗口。 | | `hide()` | 隐藏窗口。 | | `set_window_mode(mode)` | 设置窗口模式。 | | `get_window_mode()` | 读取当前窗口模式。 | `WindowMode` 包含 `Normal`、`Minimized`、`Maximized`、`Fullscreen`。当前 Windows 实现里,`Fullscreen` 暂时按最大化处理。 ### 位置与尺寸 | API | 作用 | | ----------------------------------------- | -------------- | | `get_left()` / `get_top()` | 读取窗口左上角屏幕坐标。 | | `set_left(left)` / `set_top(top)` | 单独设置窗口横向或纵向位置。 | | `set_position((x, y))` | 同时设置窗口位置。 | | `get_width()` / `get_height()` | 读取整个窗口宽高。 | | `set_width(width)` / `set_height(height)` | 单独设置窗口宽度或高度。 | | `set_size((width, height))` | 同时设置窗口大小。 | | `get_client_size()` | 读取客户区大小。 | | `get_client_rect()` | 读取客户区在屏幕上的矩形。 | | `get_window_rect()` | 读取整个窗口在屏幕上的矩形。 | 位置使用 `i32`,允许多显示器环境里的负坐标;尺寸使用 `u32`。 ### DPI、输入法与鼠标 | API | 作用 | | ------------------------------- | ----------------------------------------------- | | `get_scale_factor()` | 读取 DPI 缩放因子;当前 Windows 实现还是 `todo!()`,不要在应用侧调用。 | | `get_dpi()` | 读取窗口 DPI。 | | `set_ime_window_location(rect)` | 设置 IME 候选窗口位置。 | | `set_ime_open_state(is_open)` | 打开或关闭输入法状态。 | | `set_ime_allowed(allow)` | 允许或禁用 IME。 | | `set_cursor(cursor)` | 设置当前光标。 | | `drag_window()` | 触发系统窗口拖动。 | | `capture_mouse()` | 捕获鼠标。 | | `release_mouse()` | 释放鼠标捕获。 | ### 重绘 | API | 作用 | | ------------------ | --------- | | `request_redraw()` | 异步请求窗口重绘。 | 普通控件状态变化会由 Flor 自动请求重绘。只有在你直接通过窗口或平台能力改了框架无法感知的外部状态时,才需要手动调用 `request_redraw()`。 --- url: /website/zh/guide/use/signal.md --- # Flor Signal 响应式系统 Flor 框架内置了一个轻量级的响应式信号系统。该信号系统受 [Floem](https://github.com/lapce/floem) 启发,参考了其 API 设计,但由我们完全独立实现。与 Floem 不同的是,因为 Flor 的设计目标,**Flor Signal 天生支持跨线程使用**,信号可以在多线程环境下安全地读写和传递。 本文主要说明如何使用 Signal。完整函数签名和 trait 列表见 [Signal API 参考](/website/zh/api/signal.md)。 ## 创建信号 Signal 的使用入口基本都在 `flor::signal` 下。先记住这些 `create_*` API,就能完成大多数场景: | API | 用途 | | ---------------------------------------------- | -------------------- | | `create_signal(value)` | 创建一个可读写的值信号 | | `create_rw_signal(value)` | 创建值信号,并直接拆成读取器和写入器 | | `create_signal_with_label(value, label)` | 创建带调试标签的值信号 | | `create_rw_signal_with_label(value, label)` | 创建带调试标签的值信号,并直接拆分读写 | | `create_list_signal(vec)` | 创建一个可读写的列表信号 | | `create_rw_list_signal(vec)` | 创建列表信号,并直接拆成读取器和写入器 | | `create_list_signal_with_label(vec, label)` | 创建带调试标签的列表信号 | | `create_rw_list_signal_with_label(vec, label)` | 创建带调试标签的列表信号,并直接拆分读写 | ### 值信号 最常用的是 `create_signal`。它返回一个 `RwSignal`,可以读也可以写。 ```rust use flor::signal::{create_signal, Read, Write}; // 创建一个保存 i32 的可读写信号。 let count = create_signal(0); // 读取需要 Read trait。 let value = count.get(); // 写入需要 Write trait。 count.set(value + 1); // 基于旧值原地修改。 count.update(|value| *value += 1); ``` 如果读取和写入会交给不同的代码,使用 `create_rw_signal` 可以在创建时直接拿到一对读写句柄: ```rust use flor::signal::{create_rw_signal, Read, Write}; // read 只能读,write 只能写。 let (read, write) = create_rw_signal(0); let value = read.get(); write.set(value + 1); ``` 也可以先创建 `RwSignal`,再在需要的地方拆分或派生: ```rust use flor::signal::{create_signal, Read, Write}; let count = create_signal(0); // 派生只读和只写句柄。 let read = count.as_read(); let write = count.as_write(); write.set(read.get() + 1); // 或者直接拆成一对读写句柄。 let (read, write) = count.split(); write.set(read.get() + 1); ``` ## 常量信号 常量信号是很实用的能力:它允许 API 同时接收固定值和响应式值。比如控件属性既可以传 `"Hello"`,也可以传一个读取 signal 的闭包。 `ConstSignal` 不进入全局运行时,不建立订阅,`destroy()` 也是空操作。它只是把一个普通值包装成实现 `Read` 的读取器。 多数时候不需要直接写 `ConstSignal::new`,使用 `IntoRead` 更自然: ```rust use flor::signal::{IntoRead, Read}; // &str 会被转换成 ConstSignal。 let title = "Hello".into_read(); assert_eq!(title.get(), "Hello".to_string()); ``` `IntoRead` 已经覆盖常用基础类型、`String`、`&str`、`&String`、`ConstSignal`、`ReadSignal` 和 `RwSignal`。这让 API 可以把“固定值”和“响应式值”统一成读取器处理。 ## 读取与写入 ### 克隆读取 `get()` 和 `try_get()` 会克隆值,因此要求 `T: Clone + 'static`。 ```rust use flor::signal::{create_signal, Read}; let name = create_signal("Flor".to_string()); // get 会克隆当前值。 let value = name.get(); // try_get 在信号已销毁时返回 None。 let maybe_value = name.try_get(); ``` `get()` 在信号不存在或类型不匹配时 panic;`try_get()` 在信号不存在时返回 `None`。 ### 引用读取 如果值很大,不想 clone,可以使用 `get_ref()` 或 `try_get_ref()`。它们返回 `SignalRef<'_, T>`,可以像 `&T` 一样使用。 ```rust use flor::signal::{create_signal, Read, Write}; let name = create_signal("Flor".to_string()); { // get_ref 避免 clone,但会在守卫存活期间持有读锁。 let name_ref = name.get_ref(); assert_eq!(name_ref.len(), 4); } // 守卫释放后再写入同一个信号。 name.set("Signal".to_string()); ``` `SignalRef` 持有底层读锁。守卫未释放前,不要在同一线程对同一个信号调用 `set()`、`update()` 等需要写锁的操作,否则可能死锁。 ### 写入 `set()` 会替换整个值,`update()` 会在原地修改值。 ```rust use flor::signal::{create_signal, Write}; let count = create_signal(0); // 替换整个值。 count.set(10); // 基于旧值原地修改。 count.update(|value| *value += 1); ``` 如果信号可能已经被销毁,使用 `try_set()` 和 `try_update()`: ```rust use flor::signal::{create_signal, Signal, Write}; let count = create_signal(0); // 这里模拟信号已经被动态窗口或临时页面销毁。 count.destroy(); // try_* 在信号不存在时返回 false,而不是 panic。 assert!(!count.try_set(1)); assert!(!count.try_update(|value| *value += 1)); ``` ## 响应式更新 ### `create_effect` `create_effect` 创建基础副作用。闭包会立即执行一次,并接收上一次执行返回的值。 ```rust use flor::signal::{create_effect, create_signal, Read, Write}; let count = create_signal(0); create_effect(move |previous: Option| { // 在 effect 中读取信号,会自动建立依赖。 let current = count.get(); println!("count: {current}, previous: {previous:?}"); // 返回值会成为下一次执行时的 previous。 current }); // 写入 count 后,依赖 count 的 effect 会被重新执行。 count.set(1); ``` 第一次执行时 `previous` 是 `None`。后续由信号更新触发时,`previous` 是上一次闭包返回的值。 ### `create_updater` `create_updater` 更适合 UI 和派生状态绑定。它先执行 `compute` 得到初始值并返回;后续依赖变化时重新执行 `compute`,再把新值交给 `on_change`。 ```rust use flor::signal::{create_signal, create_updater, Read, Write}; let count = create_signal(0); let initial = create_updater( // compute:读取信号并生成派生值。 move || format!("Value: {}", count.get()), // on_change:依赖更新后接收新的派生值。 |value| println!("updated: {value}"), ); assert_eq!(initial, "Value: 0"); // 触发依赖更新;on_change 会在后续更新中收到新值。 count.set(5); ``` 注意:`on_change` 不会在初始计算时调用,只会在依赖更新后调用。 ### `create_updater_with_id` 普通业务代码通常使用 `create_updater` 即可。`create_updater_with_id` 主要给框架内部使用,它返回 `(Id, R)`,其中 `Id` 是 effect id,`R` 是初始值。 框架内部会用它把某些 UI effect 延迟到控件激活后再入队,例如 class、layout、transform 等 builder。之所以需要延迟,是因为在布局组装期间,控件的 `ViewId` 可能还没有挂载到窗口和父级控件上。等控件激活后再把 effect id 放入更新队列,可以让这些 UI 更新在完整的控件关系中执行。 ## 列表信号 列表信号保存 `Vec`,并为每个列表元素分配独立的行级 `Id`。结构变化订阅列表 `Id`;读取或修改某个元素时,会订阅或触发该元素自己的行级 `Id`。 ### 创建和基础操作 ```rust use flor::signal::{create_list_signal, ListRead, ListWrite}; // 列表信号保存 Vec。 let items = create_list_signal(vec![1, 2]); // 结构写入。 items.push(3); items.insert(1, 10); // 元素写入。 items.set(0, 100); items.update(2, |value| *value += 1); // 读取结构和元素。 assert_eq!(items.len(), Some(4)); assert_eq!(items.get(0), 100); // 删除会返回被移除的元素。 let removed = items.remove(1); assert_eq!(removed, 10); ``` ### 读取列表 ```rust use flor::signal::{create_list_signal, ListRead}; let items = create_list_signal(vec![1, 2, 3]); // 读取结构信息。 assert_eq!(items.len(), Some(3)); assert_eq!(items.len_or_zero(), 3); assert!(!items.is_empty()); // 读取元素值。 assert_eq!(items.get(0), 1); assert_eq!(items.try_get(10), None); // 读取整个列表。 assert!(items.contains(&2)); assert_eq!(items.to_vec(), vec![1, 2, 3]); ``` `len()` 返回 `Option`,信号销毁后返回 `None`;`len_or_zero()` 会把不存在的列表当作空列表处理。`get(index)` 在列表不存在或越界时 panic;`try_get(index)` 在列表不存在或越界时返回 `None`。 ### 无克隆读取 需要避免 clone 时,可以使用 `for_each_ref()` 或 `try_borrow()`。 ```rust use flor::signal::{create_list_signal, ListRead}; let items = create_list_signal(vec!["a".to_string(), "b".to_string()]); // for_each_ref 可以避免把每个元素 clone 出来。 items.for_each_ref(|item| { println!("{item}"); }); // try_borrow 返回列表只读借用,适合自己控制遍历方式。 if let Some(list_ref) = items.try_borrow() { for item in list_ref.iter() { println!("{item}"); } } ``` `for_each_ref()` 会订阅列表结构和遍历到的行级信号。`try_borrow()` 当前只订阅列表结构;如果 effect 需要在元素值被 `set(index, ...)` 或 `update(index, ...)` 后重新执行,优先使用 `get()`、`to_vec()`、`for_each_ref()` 或 `contains()`。 ### 安全写入版本 所有列表写入方法都有对应的安全版本: | panic 版本 | 安全版本 | 失败时 | | ---------------------- | -------------------------- | ---------- | | `push(value)` | `try_push(value)` | 返回 `false` | | `insert(index, value)` | `try_insert(index, value)` | 返回 `false` | | `set(index, value)` | `try_set(index, value)` | 返回 `false` | | `update(index, f)` | `try_update(index, f)` | 返回 `false` | | `remove(index)` | `try_remove(index)` | 返回 `None` | | `clear()` | `try_clear()` | 返回 `false` | 结构变化会触发列表 `Id`;元素值变化只触发该元素的行级 `Id`。因此,只读取 `len()` 的 effect 不会因为 `set(index, ...)` 重新执行;读取某个元素、`to_vec()` 或 `for_each_ref()` 的 effect 会对对应元素值变化敏感。 ## 批量更新 `batch` 会在当前线程内开启批处理模式。闭包中的信号写入不会立即把每次变化都加入更新队列,而是收集变化过的信号 `Id`,闭包结束后统一入队并去重。 ```rust use flor::signal::{batch, create_signal, Write}; let a = create_signal(0); let b = create_signal(0); batch(|| { // 同一个信号在一次 batch 中多次写入,会按信号 Id 去重。 a.set(1); a.set(2); // 不同信号仍会分别触发自己的订阅更新。 b.set(3); }); ``` `batch` 的去重粒度是信号 `Id`。同一个信号在一次 `batch` 中被多次写入,只会在闭包结束后按这个信号入队一次;所以上面的 `a.set(1)` 和 `a.set(2)` 最终只会触发一次 `a` 的订阅更新,订阅者看到的是最后一次写入后的值。不同信号仍会分别入队,例如 `a` 和 `b` 会各自触发自己的订阅更新。 `batch` 是线程局部的,只影响当前线程。其他线程中的信号写入不会进入当前线程的批处理集合。如果闭包 panic,`batch` 会恢复批处理状态,然后继续向外传播 panic。 ## 与 UI 控件集成 > 这章节属于“控件开发”的内容。如果不需要开发控件,快速了解即可。 Flor UI 控件通常通过 `create_updater` 绑定响应式值。 以 `Label::new` 的模式为例: ```rust pub fn new(title: P) -> Self { let view_id = ViewId::new_with_layout(|view_id| LayoutResolver::new(view_id)); let title = create_updater( move || title.make(), move |value| view_id.update_state(Box::new(value)), ); Self { view_id, title, style: LabelStyleResolver::new_with_compute_func(view_id, computed_label_style), measure_cache: FxHashMap::default(), } } ``` 用户侧一般传入闭包读取信号: ```rust use flor::signal::{create_signal, Read, Write}; use flor_lys::label::label; let title = create_signal("Hello".to_string()); let view = label(move || title.get()); title.set("World".to_string()); ``` 工作流程: 1. `create_updater` 先执行一次 `compute`,得到控件初始值。 2. `compute` 中读取信号时,当前 effect id 记录在线程局部 `SCOPE` 中。 3. `Read::track()` 或 `ListRead::track()` 把当前 effect 订阅到被读取的信号。 4. 信号写入时,运行时把对应信号 id 加入更新队列。 5. Flor 事件循环调用 `RUNTIME.execute_update_queue()`,运行订阅的 effect。 6. `on_change` 调用 `ViewId::update_state` 或其他 UI 更新入口。 ## 使用建议 优先通过闭包传递响应式值: ```rust label(move || title.get()); ``` 不要把 `title.get()` 的结果直接传给需要响应式更新的 API。那只会读取一次普通值。 用 `ReadSignal`、`WriteSignal` 表达业务意图: ```rust let (read, write) = create_signal(0).split(); child(read); store_writer(write); ``` 避免在 effect 中写入自己依赖的信号: ```rust create_effect(move |_| { let value = count.get(); count.set(value + 1); value }); ``` 这类代码会在后续更新中反复触发自己,容易形成无限更新。 持有 `SignalRef` 或 `ListRef` 时,先释放引用守卫,再写入同一个信号。 完整函数签名和 trait 列表见 [Signal API 参考](/website/zh/api/signal.md)。 ## 设计说明 ### 生命周期 所有值信号和列表信号都是全局存活的。`RwSignal`、`ReadSignal`、`WriteSignal`、`RwListSignal`、`ReadListSignal`、`WriteListSignal` 都是 `Copy` 句柄,复制句柄不会复制底层值,也不会改变生命周期。 全局存活是 Flor 为了让信号句柄保持 `Copy` 可用而做的取舍:信号创建后会在全局运行时中存活,丢弃某个句柄不会释放底层数据。Flor 不只适合大型甚至超大型 GUI,也适合小工具类程序;在轻量程序中,Flor 的编译体积表现也是最核心的设计目标之一。对于小工具、简单窗口、界面结构稳定、窗口生命周期接近进程生命周期的程序,让信号随进程结束自然回收通常就是推荐写法之一,内存和性能影响很小。 `destroy()` 更适合用在生命周期明确会结束的动态资源上,比如可反复创建和销毁的窗口、页面、面板,或者重型、超重型 GUI 中数量很大的临时信号。使用 `destroy()` 后,再配合 `try_get()`、`try_set()`、`try_update()` 处理可能已经失效的句柄。 ```rust use flor::signal::{create_signal, Read, Signal, Write}; let count = create_signal(1); // 信号句柄是 Copy;another 指向同一个底层信号。 let another = count; assert!(count.exists()); assert_eq!(another.get(), 1); // 销毁底层信号后,所有指向它的句柄都会失效。 count.destroy(); assert!(!another.exists()); assert_eq!(another.try_get(), None); assert!(!another.try_set(2)); ``` 列表信号销毁时,会同时销毁它内部所有行级信号。 ### 读写语义 Flor 的读写能力是通过 trait 提供的。`ReadSignal` 只实现 `Read`,所以在普通类型约束下不能调用 `set()` 或 `update()`;`WriteSignal` 只实现 `Write`,所以不能调用 `get()`。 读写拆分表达的是业务语义,不是安全边界或权限隔离。这句话并不表示读取器拥有写入器的权限:`ReadSignal` 本身没有写入方法,按 `Read` 约束传递时就只能读取;`WriteSignal` 本身没有读取方法,按 `Write` 约束传递时就只能写入。读就是读,写就是写,在业务代码和源码搜索里都能保持明确。 因此,推荐放心地用对应的信号类型表达业务访问边界:只需要读的地方传 `ReadSignal`,只需要写的地方传 `WriteSignal`,需要两者时再传 `RwSignal`。这样的拆分能让 API、调用点和代码阅读都更清晰。 同时,Flor 不会让这种语义拆分拖住使用。确实需要写入时,代码里会出现 `as_write()`、`split()` 或写入器参数,读者能清楚看到这里拿到了新的访问能力;普通路径下,读取器仍然不会有写入权限。 ## 调试标签 可以为读写信号设置标签,方便调试和追踪。 ```rust use flor::signal::{create_list_signal_with_label, create_signal_with_label}; let count = create_signal_with_label(0, "counter"); let items = create_list_signal_with_label(vec![1, 2, 3], "items"); count.set_label("main_counter"); items.set_label("main_items"); ``` 标签只在 `debug_assertions` 或启用 `signal-tracing` feature 时写入运行时。当前这部分主要提供运行时侧的标签记录,完整的可视化调试体验还需要等待 `devtools` 相关能力完善;因此它更适合作为后续追踪和调试工具的基础能力,而不是已经完成的用户侧调试面板。 ## 深入:运行时机制 这部分用于理解 Signal 的内部行为;普通使用不需要先掌握这些细节。 `RUNTIME` 保存以下核心状态: | 字段 | 作用 | | ---------------------- | ------------------------------- | | `values` | 值信号和 effect 上一次返回值 | | `list_signal` | 列表信号及其行级元素 | | `subscribe` | `Signal Id -> Effect Id` 的订阅关系 | | `effects` | effect id 到 effect 实例的映射 | | `effect_subscriptions` | effect 被多少个信号订阅 | | `update_queue` | 等待执行的信号 id 或 effect id | | `labels` | 调试标签,仅在调试或 `signal-tracing` 下存在 | 依赖追踪依靠线程局部的 `SCOPE`: 1. effect 执行前,把当前 effect id 写入 `SCOPE`。 2. `get()`、`get_ref()`、`ListRead` 读取方法会调用 `track()`。 3. `track()` 读取 `SCOPE`,把当前 effect 订阅到对应信号 id。 4. 写入信号时,运行时把被写入的信号 id 加入更新队列。 5. 事件循环执行更新队列,找到订阅该信号的 effect 并运行。 `batch` 使用线程局部的 `BATCH` 收集信号 id。批处理结束后,这些 id 被统一加入全局更新队列。 当前实现不会在每次 effect 重新执行前主动清空旧依赖。对于依赖集合会频繁变化的动态分支,应尽量让读取范围稳定,避免长期积累不再需要的订阅。 --- url: /website/zh/guide/use/handler.md --- # 外置事件 外置事件是给已经存在的控件追加事件逻辑的方式。你不需要为了处理一次点击、一次鼠标移动、一个快捷键,就去实现一个新的 `View`。把控件创建出来以后,接着链式调用 `on_*` 方法,把闭包交给 Flor 就可以。 这些 `on_*` 方法来自 `EventBuilder`,使用前先导入它: ```rust use flor::view::builder::EventBuilder; ``` 完整签名见 [Handler API](/website/zh/api/handler.md)。这一页先按使用过程来学:从一次点击开始,再逐步用到鼠标位置、键盘、焦点、生命周期和拖放。 ## 第一个点击事件 最常见的用法,是在控件后面直接接 `.on_click(...)`: ```rust use flor::signal::{Read, Write, create_signal}; use flor::view::builder::EventBuilder; use flor_lys::button::button; use flor_lys::label::label; let count = create_signal(0); let click_count = count; let add = button("加一").on_click(move || { click_count.update(|value| *value += 1); }); let text = label(move || format!("count: {}", count.get())); ``` `on_click` 的完整闭包有三个参数: ```rust |view_id, key_state, mouse_position| { ... } ``` 刚开始可以像上面一样写成无参数闭包,只关注业务逻辑。需要知道事件来源、鼠标位置或修饰键状态时,再把参数名字补回来。鼠标事件也支持只接收 `ViewId`,或者省略 `ViewId` 只接收 `KeyState` 和 `MousePosition`。 如果你在不同示例里看到参数数量不一样,不是因为事件有多套派发规则,而是同一个 handler 转换机制在起作用。事件 Builder 会根据闭包、函数或方法项的签名,把它转换成目标 handler。带有额外事件参数的 handler 通常支持四种填写形态: | 形态 | 示例 | 适用场景 | | ------------ | ------------------------------------------------ | --------- | | 完整参数 | `\|view_id, key_state, mouse_position\| { ... }` | 需要全部事件上下文 | | 无参数 | `\|\| { ... }` | 只触发业务动作 | | 只接收 `ViewId` | `\|view_id\| { ... }` | 只关心哪个控件触发 | | 省略 `ViewId` | `\|key_state, mouse_position\| { ... }` | 只关心事件数据 | 只有 `ViewId` 一个完整参数的事件,例如 `on_mouse_enter`、`on_create`,只有“完整参数”和“无参数”两种有效形态。底层机制见本页的 [机制说明](#机制说明),完整签名见 [Handler API](/website/zh/api/handler.md#intoeventhandler)。 事件闭包通常会写 signal、发命令、打开窗口、请求重绘,或者把业务消息投递到自己的任务系统里。只要记住一件事:事件闭包会在 GUI 事件处理流程中执行,不要在里面做长时间阻塞的工作。 ## 任何 View 都可以挂事件 `EventBuilder` 是给所有实现了 `ViewIdentity` 的类型实现的,所以事件不是按钮专属。一个标签、一个容器、一个自定义控件,只要能提供 `ViewId`,都可以挂外置事件。函数返回的 `impl IntoView` 也可以继续链式绑定事件: ```rust use flor::view::builder::EventBuilder; use flor_lys::label::label; let title = label("点击标题").on_click(|view_id, _, _| { println!("clicked {view_id}"); }); ``` 完整参数里的第一个参数是触发事件的 `ViewId`。它可以用来访问控件状态、请求重绘、设置焦点、捕获鼠标等。普通业务代码不一定需要它,可以直接省略;当你要把事件和具体控件关联起来时,它就是最直接的入口。 ## 鼠标位置和按键状态 鼠标类事件大多使用同一组参数: ```rust |view_id, key_state, mouse_position| ``` `key_state` 记录事件发生时的鼠标按钮和修饰键状态;`mouse_position` 保存坐标。对 `on_mouse_move`、`on_button_down`、`on_button_up`、`on_click`、`on_double_click` 这类鼠标命中事件来说,坐标会被转换成目标控件的局部坐标,`(0, 0)` 表示控件左上角。 ```rust use flor::view::builder::EventBuilder; use flor_lys::label::label; let area = label("拖动这里").on_mouse_move(|view_id, key_state, pos| { if key_state.lbutton_is_down { println!("drag {view_id}: {}, {}", pos.x, pos.y); } }); ``` 点击事件是合成事件:左键按下和松开都落在同一个命中控件上时,Flor 才会触发 `on_click`。如果只是想知道按钮什么时候按下或松开,使用 `on_button_down` 和 `on_button_up`。 ```rust use flor::view::builder::EventBuilder; use flor_lys::label::label; let area = label("press area") .on_button_down(|_, _, pos| { println!("down at {}, {}", pos.x, pos.y); }) .on_button_up(|_, _, pos| { println!("up at {}, {}", pos.x, pos.y); }); ``` 右键也有对应事件: ```rust use flor::view::builder::EventBuilder; use flor_lys::label::label; let item = label("右键操作").on_right_button_click(|_, _, pos| { println!("right click: {}, {}", pos.x, pos.y); }); ``` 中键当前暴露了 `on_middle_button_down`、`on_middle_button_up` 和 `on_middle_button_double_click`。源码里有内部的 middle click 合成逻辑,但外置事件 API 当前没有 `on_middle_button_click`。 ## 键盘事件要先拿到焦点 键盘事件派发给当前获得焦点的控件。普通控件如果想参与 Tab 焦点顺序,需要配合 `focus_index`。更完整的焦点说明见 [焦点机制](/website/zh/guide/use/focus.md)。 ```rust use flor::base::platform::{HandleResult, KeyCode}; use flor::view::builder::{EventBuilder, FocusIndexBuilder}; use flor_lys::label::label; let shortcut_area = label("按 Ctrl+S") .focus_index(1) .on_key_down(|code, is_alt, is_ctrl, is_shift| { if !is_alt && is_ctrl && !is_shift && code == KeyCode::S { println!("save"); HandleResult::Handled } else { HandleResult::Default } }); ``` `on_key_down` 和 `on_key_up` 都要返回 `HandleResult`。键盘事件可以使用完整参数 `|view_id, code, is_alt, is_ctrl, is_shift|`,也可以像上面一样省略 `ViewId`: | 返回值 | 含义 | | ----------------------- | ------------- | | `HandleResult::Handled` | 这个按键已经由你的代码处理 | | `HandleResult::Default` | 交给默认流程继续处理 | Flor 内部控件逻辑和外置 handler 都可能返回 `Handled`。只要其中任意一边返回 `Handled`,最终结果就是 `Handled`。 ## 焦点变化 `on_focus` 和 `on_blur` 用于监听焦点进入和离开。闭包收到 `ViewId` 和一个 `u16` 类型的虚拟焦点序号。 大多数控件只有一个焦点点位,这个序号通常是 `0`。少数复合控件可以通过 `View::on_focus_count` 暴露多个虚拟焦点点位,这时同一个控件内部可以区分不同焦点位置。 ```rust use flor::view::builder::{EventBuilder, FocusIndexBuilder}; use flor_lys::label::label; let focusable = label("可聚焦区域") .focus_index(1) .on_focus(|virtual_index| { println!("focus virtual index: {virtual_index}"); }) .on_blur(|| { println!("blur"); }); ``` ## 创建事件 `on_create` 会在控件树挂载到窗口以后触发。它适合做依赖 `ViewId` 已经进入控件树的初始化工作。 ```rust use flor::view::builder::EventBuilder; use flor_lys::label::label; let view = label("created").on_create(|view_id| { println!("created {view_id}"); }); ``` 如果只是初始化普通 Rust 数据,通常直接在创建控件前完成即可。只有当逻辑依赖控件已经注册、已经有 `ViewId` 和窗口关系时,再考虑 `on_create`。 ## 滚轮事件 当前滚轮外置事件的 API 名称是 `on_wheel_settings_changed`。它在鼠标滚轮消息路径中触发,闭包收到滚动方向、滚动量、按键状态和鼠标位置。 ```rust use flor::base::platform::ScrollAxis; use flor::view::builder::EventBuilder; use flor_lys::label::label; let wheel_area = label("滚动这里").on_wheel_settings_changed( |axis, delta, key_state, pos| { match axis { ScrollAxis::Vertical => println!("vertical wheel: {delta}"), ScrollAxis::Horizontal => println!("horizontal wheel: {delta}"), } if key_state.control_is_down { println!("ctrl wheel: {}, {}", pos.x, pos.y); } }, ); ``` 滚轮派发会从命中的控件向上寻找可滚动祖先;如果没有找到,就投递给窗口根控件。当前这条路径传入的是窗口客户区坐标,而不是目标控件局部坐标。 ## 拖放事件 拖放事件需要启用 `drag-drop` feature。常见流程是: 1. `on_drag_enter` 判断拖入的数据格式,设置 `DropEffect`; 2. `on_drag_over` 在拖拽移动时持续更新效果; 3. `on_drop` 读取真正的数据并完成业务处理; 4. `on_drag_leave` 清理悬停状态。 ```rust use flor::base::platform::{DragData, DragFormat, DropEffect}; use flor::view::builder::EventBuilder; use flor_lys::label::label; let drop_area = label("拖文件到这里") .on_drag_enter(|_, _, formats, effect| { let accepts_files = formats .iter() .any(|format| matches!(format, DragFormat::Files(_))); if accepts_files { *effect = DropEffect::Copy; } }) .on_drop(|_, _, data, effect| { if let DragData::Files(files) = data { for file in files { println!("drop file: {}", file.display()); } *effect = DropEffect::Copy; } }); ``` `on_drag_enter` 和 `on_drag_over` 收到的是可用格式列表,还没有真正取出数据;`on_drop` 才会收到 `DragData`。和滚轮一样,当前拖放路径传入的是窗口客户区坐标。 ## 使用建议 外置事件适合应用层逻辑:点击按钮修改 signal、按快捷键触发命令、拖放文件到面板、在鼠标移动时更新预览状态。 如果你是在写控件库,并且事件逻辑是控件本身不可分割的一部分,优先实现 `View` 的内部 `on_*` 方法。比如一个输入框如何处理 IME、光标、选区和文本编辑,应该属于控件内部逻辑;应用侧只需要绑定更高层的外置事件或控件自己暴露的业务回调。 事件闭包需要满足 `Send + Sync + 'static`。常见做法是捕获 signal 句柄、`Arc`、通道发送端,或者其他能安全长期保存的数据。不要捕获临时引用。 ## 机制说明 这一节用于理解外置事件在 Flor 里的位置。普通使用不需要先读这里。 每个 `ViewId` 创建时,Flor 会在 `VIEW_STORAGE.handlers` 里放入一个默认的 `ViewHandler`。`ViewHandler` 是一组可选 handler 槽位,例如 `on_click_handler`、`on_key_down_handler`、`on_focus_handler`。 当你调用: ```rust view.on_click(|view_id, key_state, pos| { // ... }) ``` `EventBuilder` 会取出这个 view 的 handler 存储,把闭包通过 `IntoEventHandler` 转换成对应的 handler 包装类型,并写入 `ViewHandler` 的 `on_click_handler` 槽位。方法返回原来的 `self`,所以它可以自然地继续链式调用。 这个转换是 trait 驱动的:完整参数签名先走 handler 包装类型的 `From` 实现,简化签名再由 `IntoEventHandler` 的其它实现补齐被省略的参数。`Args` 是给 Rust 做 trait 推导的标记类型,调用事件方法时一般不需要手写;当你自己封装一个接收事件 handler 的函数时,才需要把这个泛型带出去,写法见 [框架 DSL 的方法组合控件](/website/zh/guide/use/framework-dsl.md#方法组合控件)。 事件从平台层进入 Flor 后,大致路径是: 1. 平台窗口过程把系统消息转换成 `Message`; 2. `WindowsProcHandler` 把消息交给 `windows::event`; 3. 事件总线按事件类型处理:鼠标事件先命中测试,键盘事件找当前焦点,拖放事件维护当前拖放目标; 4. 找到目标 `ViewId` 后调用对应的 `call_*` 方法; 5. `call_*` 先执行控件内部的 `View::on_*`,再执行外置 handler。 大多数事件都是“内部逻辑 + 外置逻辑”都会执行。键盘事件会把两边的 `HandleResult` 合并:任意一边返回 `Handled`,最终就是 `Handled`。拖放事件会先用内部 `on_drag_*` 的返回值初始化 `DropEffect`,再把 `&mut DropEffect` 交给外置 handler 修改。 Tooltip 是一个内部特例:它的 `call_tooltip_show` 和 `call_tooltip_hide` 使用覆盖模式,如果存在外置 handler,就只执行外置 handler;否则执行控件内部方法。不过当前 `EventBuilder` 没有公开 tooltip 对应的链式方法,普通用户暂时不需要依赖这个细节。 还有一些 handler 槽位已经在 API 中存在,但当前事件总线没有完整派发到外置 handler,例如 `on_context_menu`、`on_destroy`、`on_resize`、`on_close_requested`、`on_work_area_changed`、`on_dpi_change` 和 `on_theme_changed`。查询具体状态时,以 [Handler API](/website/zh/api/handler.md) 的“当前派发状态”为准。 --- url: /website/zh/guide/use/control-state.md --- # 控件状态 Flor 框架已经实现了完整的控件状态系统,用于管理控件在不同交互场景下的状态表现。 ## ControlState 枚举定义 ```rust #[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Default)] pub enum ControlState { #[default] Normal, Focus, Hover, Active, Disabled, } ``` ## 状态模型设计 Flor 框架定义了控件的五种基本状态: - **Normal**:正常状态,控件的默认状态 - **Focus**:焦点状态,控件获得键盘焦点时 - **Hover**:悬停状态,鼠标悬停在控件上时 - **Active**:激活状态,控件被按下或点击时 - **Disabled**:禁用状态,控件不可交互时 ## 状态分类 在这些状态中,可以分为两类: ### 增强态 增强态是在基态基础上增加的状态,不会阻止用户交互: - **Focus**:补充状态,表示控件获得焦点 - **Hover**:补充状态,表示鼠标悬停 - **Active**:补充状态,表示控件被激活 ### 终止态 终止态会阻止用户交互,控件进入不可用状态: - **Disabled**:独立状态,控件完全禁用 ## 状态继承关系 Flor 框架实现的状态继承关系如下: ``` Normal ├─ Focus ├─ Hover │ └─ Active └─ Disabled(独立) ``` ## 继承规则 | 状态 | 继承自 | 原因 | | -------- | ------------------------ | --------------------------- | | Normal | 无 | 基态,所有其他状态的基础 | | Focus | Normal | Focus 只是补充,不改变控件的基本行为 | | Hover | Normal | Hover 只是补充,提供视觉反馈 | | Active | Hover -> Focus -> Normal | Active 通常是 Hover 的延续,用户按下控件 | | Disabled | 不继承任何状态 | Disabled 应完全隔离,阻止所有交互 | ### 继承链说明 **Active 的继承链**: - 首先检查是否处于 Hover 状态 - 如果不是 Hover,检查是否处于 Focus 状态 - 如果都不是,继承自 Normal 状态 **Disabled 的特殊处理**: - Disabled 状态完全独立,不继承任何其他状态的样式 - 禁用状态下,控件显示为不可用的视觉效果 - 所有交互事件都被忽略 ## 状态转换 控件状态之间的转换遵循以下规则: 1. **Normal → Focus**:控件获得键盘焦点 2. **Normal → Hover**:鼠标进入控件区域 3. **Hover → Active**:用户按下鼠标按钮 4. **Active → Hover**:用户释放鼠标按钮(仍在控件区域内) 5. **任意状态 → Disabled**:控件被禁用 6. **Disabled → Normal**:控件被启用 > 框架内置系统的状态转换不代表最终控件的型为模式 ## 实际应用 在实际使用中,你可以使用状态前缀来应用不同的样式: ```rust let button = button("点击我") .class("px-4 py-2 rounded bg-blue-500 text-white") .class("hover:bg-blue-600") .class("active:bg-blue-700") .class("focus:ring-2 focus:ring-blue-300") .class("disabled:bg-gray-300 disabled:text-gray-500 disabled:cursor-not-allowed"); ``` > 这里只是语法概念展示,class 的支持情况请以实际具体文档为准。 ## 状态前缀 Flor 支持使用以下状态前缀来定义条件样式: - `normal:`:正常状态(通常不需要显式指定) - `hover:`:鼠标悬停状态 - `focus:`:获得焦点状态 - `active:`:激活状态 - `disabled:`:禁用状态 这种状态管理方式让你能够灵活地控制控件在不同交互状态下的表现,提供良好的用户体验。 --- url: /website/zh/guide/use/builder/focus.md --- # 焦点 Builder 这一页只介绍用于配置焦点表的 builder 方法。焦点机制的整体说明见 [焦点机制](/website/zh/guide/use/focus.md),运行时通过 `ViewId` 操控焦点的 API 见 [ViewId](/website/zh/guide/control/view-id.md)。 实际公开的 trait 是: ```rust pub trait FocusIndexBuilder { fn focus_scope(self, focus_scope: u32) -> Self; fn focus_index(self, focus_index: u32) -> Self; } ``` ## focus\_index `focus_index` 把控件加入焦点表,并设置它在 Tab 顺序里的排序值。 ```rust use flor::view::builder::{EventBuilder, FocusIndexBuilder}; use flor_lys::label::label; let name = label("名称") .focus_index(0) .on_focus(|_, virtual_index| { println!("focus virtual index: {virtual_index}"); }); let confirm = label("确认").focus_index(1); ``` `0` 是合法排序值,不能把它理解成取消焦点。数字越小,越先被 Tab 访问。同一个排序值也能工作,最终排序还会包含 `ViewId` 和虚拟焦点序号;不过给同一组控件使用清晰递增的值更容易维护。 不写 `focus_index` 就是不加入焦点表。这样的控件不会被 Tab 选中,也不会成为控件级 [`on_key_*`](/website/zh/api/handler.md#键盘事件) 的焦点目标。 ## focus\_scope `focus_scope` 给当前控件及其子树提供一个排序偏移。它适合把页面拆成几个区域,让每个区域内部从 `0` 开始编号。 ```rust use flor::view::builder::FocusIndexBuilder; use flor::views; use flor_lys::div::div; use flor_lys::label::label; let toolbar = div(views![ label("工具 1").focus_index(0), label("工具 2").focus_index(1), ]) .focus_scope(100); let content = div(views![ label("内容 1").focus_index(0), label("内容 2").focus_index(1), ]) .focus_scope(200); ``` 上面的最终排序值是: | 控件 | 局部写法 | 最终排序值 | | ---- | --------- | ----- | | 工具 1 | `100 + 0` | `100` | | 工具 2 | `100 + 1` | `101` | | 内容 1 | `200 + 0` | `200` | | 内容 2 | `200 + 1` | `201` | `focus_scope` 可以嵌套,父级偏移会继续累加给子树。 注意:builder 的 `focus_scope(u32)` 只影响排序,不限制 Tab 的运行时范围。弹出层、Modal 这类焦点隔离场景使用 `ViewId::push_focus_scope()` 和 `ViewId::pop_focus_scope()`,机制说明见 [焦点机制](/website/zh/guide/use/focus.md#运行时作用域)。 --- url: /website/zh/guide/use/builder/event.md --- # 事件 Builder 事件 Builder 用来给控件挂外置事件处理函数。它适合应用侧代码:你不需要写一个新的控件类型,也不需要实现 `View`,直接在创建控件时链式绑定事件。 完整方法列表、handler 类型和派发状态见 [Handler API](/website/zh/api/handler.md)。这一页只讲怎么用。 ## 基本写法 导入 `EventBuilder` 后,所有实现了 `ViewIdentity` 的值都可以链式绑定事件。普通控件、`ViewBox`,以及函数返回的 `impl IntoView` 都满足这个条件。 ```rust use flor::view::builder::EventBuilder; use flor_lys::label::label; let item = label("保存") .on_click(|view_id, key_state, mouse_position| { println!("{view_id} clicked at {mouse_position:?}"); }) .on_mouse_enter(|view_id| { println!("{view_id} entered"); }) .on_mouse_leave(|view_id| { println!("{view_id} left"); }); ``` 事件回调的完整参数不是每种都一样。比如鼠标点击的完整参数是 `ViewId`、`KeyState` 和 `MousePosition`;鼠标进入、离开的完整参数是 `ViewId`;键盘事件的完整参数是 `ViewId`、`KeyCode` 和修饰键状态。现在多数 handler 也支持省略不用的参数,具体签名查 [EventBuilder API](/website/zh/api/handler.md#eventbuilder) 和 [handler 包装类型](/website/zh/api/handler.md#handler-包装类型)。 看到示例里的参数数量不一样时,按“同一个 handler 的不同转换形态”理解。事件 Builder 会把你传入的闭包、函数或方法项转换成对应 handler;带有额外事件参数的 handler 通常支持完整参数、无参数、只接收 `ViewId`、省略 `ViewId` 四种形态。机制细节见 [外置事件的机制说明](/website/zh/guide/use/handler.md#机制说明) 和 [IntoEventHandler API](/website/zh/api/handler.md#intoeventhandler)。 ## 不只可以传闭包 事件 Builder 的参数不是“只能传闭包”。源码里的方法参数是 `impl IntoEventHandler`,handler 类型保留了完整签名的 `From` 转换,同时额外支持无参数、只接收 `ViewId`、或省略 `ViewId` 的常见写法。所以你可以传闭包,也可以传普通函数、关联函数或方法项。 ```rust use flor::base::platform::{KeyState, MousePosition}; use flor::view::builder::EventBuilder; use flor::view::ViewId; use flor_lys::label::label; fn save_clicked(view_id: ViewId, _key_state: KeyState, _pos: MousePosition) { println!("save from {view_id}"); } let save = label("保存") .on_click(save_clicked); ``` 关联函数也可以直接传: ```rust use flor::base::platform::{KeyState, MousePosition}; use flor::view::builder::EventBuilder; use flor::view::ViewId; use flor_lys::label::label; struct Actions; impl Actions { fn open(view_id: ViewId, _key_state: KeyState, _pos: MousePosition) { println!("open from {view_id}"); } } let open = label("打开") .on_click(Actions::open); ``` 泛型函数或泛型关联函数也可以传,但要让 Rust 得到一个已经确定类型的函数项: ```rust use flor::view::builder::EventBuilder; use flor::view::ViewId; use flor_lys::label::label; fn trace_click(view_id: ViewId) { println!("slot {SLOT}: {view_id}"); } let debug = label("调试") .on_click(trace_click::<1>); ``` 带实例状态的方法通常用闭包捕获实例,再在闭包里调用方法;这样被传给 builder 的仍然是一个满足目标签名的 `Fn`。 闭包适合就地写少量逻辑;函数、方法项和泛型函数适合复用逻辑,或者把页面代码拆得更清楚。只要最终签名匹配,传哪一种都可以。 ## `Args` 泛型怎么理解 事件方法长这样: ```rust fn on_click( self, handler: impl IntoEventHandler, ) -> Self; ``` 这里的 `Args` 不是事件运行时传入的参数,也不需要用户手写。它只是给 Rust 做 trait 推导的标记:完整参数、无参数、只接收 `ViewId`、省略 `ViewId` 会分别匹配到不同的 `IntoEventHandler` 实现。 只有在你把事件 handler 当成自己函数的参数继续转发时,才需要把这个泛型写出来: ```rust use flor::view::builder::EventBuilder; use flor::view::handler::{IntoEventHandler, OnClickHandler}; use flor::view::View; use flor_lys::button::button; fn action_button( text: &'static str, on_click: impl IntoEventHandler, ) -> impl View { button(text).on_click(on_click) } let save = action_button("保存", || { println!("save"); }); ``` ## 省略不用的参数 如果只是不使用某些参数,可以继续用 `_` 或 `_name` 忽略: ```rust use flor::view::builder::EventBuilder; use flor_lys::label::label; let item = label("删除") .on_click(|view_id, _, _| { println!("delete {view_id}"); }); ``` 现在也可以直接减少参数数量。常见支持形态有三类: ```rust use flor::view::builder::EventBuilder; use flor_lys::label::label; let save = label("保存").on_click(|| { println!("save"); }); let source = label("来源").on_click(|view_id| { println!("save from {view_id}"); }); let inspect = label("检查").on_click(|key_state, _pos| { println!("ctrl: {}", key_state.control_is_down); }); ``` 完整参数写法仍然可用;省略参数适合业务逻辑不关心控件 ID、鼠标位置或按键状态的场景。 ## 鼠标事件 鼠标事件通常用于点击、按下、松开、移动、进入和离开。 ```rust use flor::view::builder::EventBuilder; use flor_lys::label::label; let item = label("可点击") .on_button_down(|view_id, _, pos| { println!("down {view_id}: {pos:?}"); }) .on_button_up(|view_id, _, pos| { println!("up {view_id}: {pos:?}"); }) .on_click(|view_id, _, _| { println!("click {view_id}"); }); ``` 常规鼠标命中事件的 `MousePosition` 是目标控件局部坐标。完整鼠标事件列表看 [Handler API 的当前派发状态](/website/zh/api/handler.md#当前派发状态)。 ## 键盘事件 键盘事件派发给当前焦点控件。要让控件收到控件级 `on_key_down` / `on_key_up`,先让它进入焦点表;焦点机制见 [焦点机制](/website/zh/guide/use/focus.md)。 ```rust use flor::base::platform::{HandleResult, KeyCode}; use flor::view::builder::{EventBuilder, FocusIndexBuilder}; use flor_lys::label::label; let editor = label("按 Ctrl+S 保存") .focus_index(0) .on_key_down(|code, is_alt, is_ctrl, is_shift| { if !is_alt && is_ctrl && !is_shift && code == KeyCode::S { println!("save"); HandleResult::Handled } else { HandleResult::Default } }); ``` 键盘 handler 返回 `HandleResult`。返回 `Handled` 表示这个按键已经处理;返回 `Default` 表示交给默认处理。键盘 handler 可以使用完整参数,也可以省略 `ViewId`;完整签名见 [键盘事件 API](/website/zh/api/handler.md#键盘事件)。 ## 焦点和生命周期事件 `on_focus` / `on_blur` 由焦点管理器触发,第二个参数是虚拟焦点序号。 ```rust use flor::view::builder::{EventBuilder, FocusIndexBuilder}; use flor_lys::label::label; let input = label("名称") .focus_index(0) .on_focus(|virtual_index| { println!("focus index: {virtual_index}"); }) .on_blur(|| { println!("blur"); }); ``` `on_create` 适合在控件树创建后做一次运行时初始化,例如为弹出层推入焦点作用域: ```rust use flor::view::builder::EventBuilder; use flor_lys::label::label; let dialog = label("弹出层") .on_create(|view_id| { view_id.push_focus_scope(); }); ``` 生命周期类 handler 的别名和参数见 [View 与生命周期事件 API](/website/zh/api/handler.md#view-与生命周期事件)。 ## 功能特性事件 拖放和主题变化这类事件受 feature 控制。启用对应 feature 后,相关 builder 方法才可用。完整说明见 [Handler API](/website/zh/api/handler.md)。 ```rust use flor::view::builder::EventBuilder; use flor_lys::label::label; #[cfg(feature = "drag-drop")] let drop_area = label("拖放到这里") .on_drop(|_key_state, _mouse_position, data, _effect| { println!("drop: {data:?}"); }); ``` 如果你不确定一个事件当前是否已经接入派发路径,先看 [当前派发状态](/website/zh/api/handler.md#当前派发状态)。教程页不会重复维护每个方法的完整状态表。 --- url: /website/zh/guide/use/builder/layout.md --- # 布局 Builder 布局 Builder 用来在创建控件时配置显示方式、尺寸、间距、定位、Flex/Grid 等布局属性。基本写法是在控件后调用 `.layout(|layout| { ... })`,在闭包里连续调用布局方法,最后返回修改后的 `layout`。 ## 基本写法 导入 `LayoutBuilder` 后,所有控件都可以调用 `.layout(...)`。还要导入 `LayoutResolverExt`,这样闭包里的 `layout` 才能使用 `display`、`size`、`flex_grow` 等布局方法。 下面的示例使用了 flex 相关类型,默认你已经启用 `layout-flex` feature。 ```rust use flor::taffy::{Dimension, Display, FlexDirection, LengthPercentage, Size}; use flor::view::builder::LayoutBuilder; use flor::view::resolver::{LayoutResolverExt, Unit}; use flor::views; use flor_lys::div::div; use flor_lys::label::label; let panel = div(views![label("内容")]) .layout(|layout| { layout .display(Display::Flex) .flex_direction(FlexDirection::Column) .size( Size { width: Dimension::Percent(1.0), height: Dimension::Auto, }, Size { width: Unit::Px, height: Unit::Px, }, ) .gap( Size { width: LengthPercentage::Length(8.0), height: LengthPercentage::Length(8.0), }, Size { width: Unit::Px, height: Unit::Px, }, ) }); ``` 闭包必须返回修改后的 `layout`。如果闭包捕获外部 signal 或变量,通常需要写成 `move |layout| { ... }`。 Flor 当前没有直接的 `.width(100)`、`.height(40)`、`.x(10)`、`.y(20)` 这类布局 builder 方法。尺寸、位置、边距都通过 `size`、`inset`、`margin` 等布局参数表达。 ## 链式写法和 set 写法 大多数时候直接链式调用即可: ```rust layout .display(Display::Flex) .flex_grow(1.0) ``` 每个布局方法还有一个同名的 `set_` 版本,例如 `display(...)` 对应 `set_display(...)`,`flex_grow(...)` 对应 `set_flex_grow(...)`。这组 `set_` 方法适合在 `if`、`match` 里分支修改同一个 `layout`。 这些方法由 Flor 自动生成,所以命名保持统一:布局项名用 snake\_case 写成方法名,`set_` 版本在前面加 `set_`。你按下面表格里的方法名使用即可。 ```rust use flor::taffy::Display; use flor::view::builder::LayoutBuilder; use flor::view::resolver::LayoutResolverExt; use flor_lys::label::label; let item = label("可切换") .layout(move |mut layout| { if true { layout.set_display(Display::None); } else { layout.set_display(Display::Flex); } layout }); ``` > `set_` 方法主要用作控件开发时,解析原子类赋值时使用 ## 状态布局 布局也可以按控件状态分别设置。调用 `hover()`、`focus()`、`active()` 等状态方法后,后续布局设置会写入对应状态;调用 `normal()` 或 `base()` 可以切回普通状态。 | 方法 | 参数 | 作用 | | --------------------- | -- | ------------ | | `base()` / `normal()` | 无 | 切回普通状态。 | | `focus()` | 无 | 后续布局项用于焦点状态。 | | `hover()` | 无 | 后续布局项用于悬停状态。 | | `active()` | 无 | 后续布局项用于激活状态。 | | `disabled()` | 无 | 后续布局项用于禁用状态。 | | `clear()` | 无 | 清空当前已有布局项。 | ```rust use flor::taffy::{Display, FlexDirection}; use flor::view::builder::LayoutBuilder; use flor::view::resolver::LayoutResolverExt; use flor::views; use flor_lys::div::div; use flor_lys::label::label; let view = div(views![label("内容")]) .layout(|layout| { layout .display(Display::Flex) .flex_direction(FlexDirection::Column) .hover() .display(Display::None) }); ``` ## 单位参数怎么读 布局值主要来自 `flor::taffy`,单位类型来自 `flor::view::resolver::Unit`。 `Unit` 是长度单位。它只在布局值是 length 时参与换算,最终会解析成 px 交给布局系统。 | 单位 | 含义 | 换算基值 | | ----------- | ---------------- | ---------------------------------------------------------------------------------- | | `Unit::Px` | 像素。 | 不换算,传入多少就是多少 px。 | | `Unit::Pt` | point,常见字体/排版单位。 | 使用当前窗口 DPI 换算,公式是 `1pt = dpi / 72 px`。普通布局长度使用窗口的垂直 DPI;窗口创建时从平台读取 DPI,DPI 变化时会更新。 | | `Unit::Rem` | root em。 | 使用当前窗口的 `WindowOption::rem_px`,默认是 `16.0`,所以默认 `1rem = 16px`。 | | `Unit::Vw` | viewport width。 | `1vw = 当前窗口客户区宽度 / 100`。窗口 resize 后会更新。 | | `Unit::Vh` | viewport height。 | `1vh = 当前窗口客户区高度 / 100`。窗口 resize 后会更新。 | 凡是方法里同时出现布局值和 `Unit`,规则都是:布局值负责表达 length、percent 或 auto,`Unit` 只在值是 length 时决定如何解析这个数值。比如 `Dimension::Length(12.0)` 搭配 `Unit::Px` 表示 12px;`Dimension::Percent(0.5)` 和 `Dimension::Auto` 不依赖单位。 ## 核心布局参数 这些方法不受 `layout-flex`、`layout-grid`、`layout-block` feature 影响。 | 方法 | 参数 | 说明 | | ------------------------------ | -------------------------------------------------------- | --------------------------------------------------------------------------------------- | | `display(value)` | `value: Display` | 设置布局策略,例如显示、隐藏以及启用对应 feature 后的 flex/grid/block 布局模式。 | | `item_is_table(value)` | `value: bool` | 标记子项是否按 table item 处理。当前 table layout 没有实现,应用代码通常不需要设置它。 | | `box_sizing(value)` | `value: BoxSizing` | 控制 `size`、`min_size`、`max_size` 等尺寸样式作用在 content box 还是 border box。 | | `overflow(value)` | `value: Point` | 设置 x/y 两个方向的溢出行为。使用 `Point { x, y }` 分别传入横向和纵向 `Overflow`。 | | `scrollbar_width(width, unit)` | `width: f32`, `unit: Unit` | 为 `Overflow::Scroll` 或 `Overflow::Auto` 预留滚动条空间。`unit` 说明 `width` 的单位。 | | `position(value)` | `value: Position` | 设置元素定位方式,决定 `inset` 使用哪种定位基准。 | | `inset(value, units)` | `value: Rect`, `units: Rect` | 设置 left/right/top/bottom 偏移。每条边的 `Length` 用对应 `Unit` 解析,`Percent` 和 `Auto` 不依赖单位。 | | `size(value, units)` | `value: Size`, `units: Size` | 设置初始宽高。`value.width` 和 `value.height` 分别是宽高,`units.width` 和 `units.height` 是 length 单位。 | | `min_size(value, units)` | `value: Size`, `units: Size` | 设置最小宽高,参数结构与 `size` 相同。 | | `max_size(value, units)` | `value: Size`, `units: Size` | 设置最大宽高,参数结构与 `size` 相同。 | | `aspect_ratio(value)` | `value: f32` | 设置首选宽高比,计算方式是 width / height。 | | `margin(value, units)` | `value: Rect`, `units: Rect` | 设置外边距。四条边都可用 length、percent 或 auto。 | | `padding(value, units)` | `value: Rect`, `units: Rect` | 设置内边距。四条边可用 length 或 percent,不支持 auto。 | | `border(value, units)` | `value: Rect`, `units: Rect` | 设置布局层面的边框厚度。四条边可用 length 或 percent,不支持 auto。 | 示例: ```rust use flor::taffy::{Dimension, LengthPercentageAuto, Rect, Size}; use flor::view::builder::LayoutBuilder; use flor::view::resolver::{LayoutResolverExt, Unit}; use flor::views; use flor_lys::div::div; use flor_lys::label::label; let view = div(views![label("内容")]) .layout(|layout| { layout .size( Size { width: Dimension::Length(320.0), height: Dimension::Auto, }, Size { width: Unit::Px, height: Unit::Px, }, ) .margin( Rect { left: LengthPercentageAuto::Length(12.0), right: LengthPercentageAuto::Length(12.0), top: LengthPercentageAuto::Length(8.0), bottom: LengthPercentageAuto::Length(8.0), }, Rect { left: Unit::Px, right: Unit::Px, top: Unit::Px, bottom: Unit::Px, }, ) }); ``` ## Flex 和 Grid 共享参数 这些方法在启用 `layout-flex` 或 `layout-grid` 任意一个 feature 后可用。 | 方法 | 参数 | 说明 | | ------------------------ | ---------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | | `align_items(value)` | `value: AlignItems` | 设置子项在 cross/block axis 上如何对齐。 | | `align_self(value)` | `value: AlignSelf` | 设置当前节点自身在 cross/block axis 上如何对齐;未设置时回退到父级 `align_items`。 | | `align_content(value)` | `value: AlignContent` | 设置容器内容在 cross/block axis 上如何分布。 | | `justify_content(value)` | `value: JustifyContent` | 设置容器内容在 main/inline axis 上如何分布。 | | `gap(value, units)` | `value: Size`, `units: Size` | 设置两个方向的项目间距。字段名沿用 Taffy 的 `Size { width, height }`,对应 length 值分别用 `units.width` 和 `units.height` 解析。 | ## Block 参数 这些方法需要启用 `layout-block` feature。 | 方法 | 参数 | 说明 | | ------------------- | ------------------ | ---------------------- | | `text_align(value)` | `value: TextAlign` | 设置 block 布局里行内方向的对齐方式。 | ## Flex 参数 这些方法需要启用 `layout-flex` feature。 | 方法 | 参数 | 说明 | | ------------------------- | -------------------------------- | --------------------------------------------------------------------------- | | `flex_direction(value)` | `value: FlexDirection` | 设置 flex 主轴方向。 | | `flex_wrap(value)` | `value: FlexWrap` | 设置 flex 子项是否换行。 | | `flex_basis(value, unit)` | `value: Dimension`, `unit: Unit` | 设置 flex 子项初始主轴尺寸。`Dimension::Length` 使用 `unit` 解析,`Percent` 和 `Auto` 不依赖单位。 | | `flex_grow(value)` | `value: f32` | 设置剩余空间增长比例。默认值是 0.0,传正数表示参与增长。 | | `flex_shrink(value)` | `value: f32` | 设置空间不足时的收缩比例。默认值是 1.0,传正数表示参与收缩。 | 示例: ```rust use flor::taffy::{AlignItems, Display, FlexDirection, JustifyContent}; use flor::view::builder::LayoutBuilder; use flor::view::resolver::LayoutResolverExt; use flor::views; use flor_lys::div::div; use flor_lys::label::label; let toolbar = div(views![label("左侧"), label("右侧")]) .layout(|layout| { layout .display(Display::Flex) .flex_direction(FlexDirection::Row) .align_items(AlignItems::Center) .justify_content(JustifyContent::SpaceBetween) }); ``` ## Grid 参数 这些方法需要启用 `layout-grid` feature。 | 方法 | 参数 | 说明 | | ------------------------------ | ---------------------------------------------------- | ------------------------------------------------------------ | | `justify_items(value)` | `value: AlignItems` | 设置 grid 子项在 inline axis 上如何对齐。 | | `justify_self(value)` | `value: AlignSelf` | 设置当前 grid 子项自身在 inline axis 上如何对齐;未设置时回退到父级 `justify_items`。 | | `grid_template_rows(value)` | `value: Vec<(TrackSizingFunction, Unit)>` | 设置显式 grid 行轨道尺寸。每个轨道携带一个 `Unit`,用于解析该轨道里的 length 值。 | | `grid_template_columns(value)` | `value: Vec<(TrackSizingFunction, Unit)>` | 设置显式 grid 列轨道尺寸。每个轨道携带一个 `Unit`,用于解析该轨道里的 length 值。 | | `grid_auto_rows(value)` | `value: Vec<(NonRepeatedTrackSizingFunction, Unit)>` | 设置隐式创建的 grid 行尺寸。 | | `grid_auto_columns(value)` | `value: Vec<(NonRepeatedTrackSizingFunction, Unit)>` | 设置隐式创建的 grid 列尺寸。 | | `grid_auto_flow(value)` | `value: GridAutoFlow` | 设置自动放置 grid item 的流向。 | | `grid_row(value)` | `value: Line` | 设置当前子项在 grid 行方向的起止位置。 | | `grid_column(value)` | `value: Line` | 设置当前子项在 grid 列方向的起止位置。 | ## 可用性速查 | 方法 | feature | | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------- | | `display`、`item_is_table`、`box_sizing`、`overflow`、`scrollbar_width`、`position`、`inset`、`size`、`min_size`、`max_size`、`aspect_ratio`、`margin`、`padding`、`border` | 始终生成 | | `align_items`、`align_self`、`align_content`、`justify_content`、`gap` | `layout-flex` 或 `layout-grid` | | `text_align` | `layout-block` | | `flex_direction`、`flex_wrap`、`flex_basis`、`flex_grow`、`flex_shrink` | `layout-flex` | | `justify_items`、`justify_self`、`grid_template_rows`、`grid_template_columns`、`grid_auto_rows`、`grid_auto_columns`、`grid_auto_flow`、`grid_row`、`grid_column` | `layout-grid` | --- url: /website/zh/guide/use/builder/class.md --- # 原子类支持 原子类指 `.class(...)` builder 带来的能力。它适合在创建控件时用一串类名快速配置布局和样式。 ```rust use flor::view::builder::ClassBuilder; use flor::views; use flor_lys::button::button; use flor_lys::div::div; use flor_lys::label::label; let view = div(views![ label("标题").class("text-lg font-bold"), button("保存").class("btn-filled btn-md") ]) .class("flex flex-col gap-3 p-4"); ``` 使用 `.class(...)` 需要启用 `class` feature。具体类名语法见 [class 语法解析基础](/website/zh/api/class-syntax.md),内置 layout class 清单见 [layout class 语法](/website/zh/api/layout-class.md)。 ## 基本写法 导入 `ClassBuilder` 后,所有控件都可以调用 `.class(...)`。 ```rust use flor::view::builder::ClassBuilder; use flor_lys::label::label; let title = label("Flor") .class("text-xl font-bold"); ``` `.class(...)` 只有一个方法入口。当前没有 `classes(...)`、`add_class(...)`、`remove_class(...)` 这几个 builder 方法;要设置多个类,直接写在同一个字符串里,用空格分隔。 ```rust let item = label("项目") .class("px-3 py-2 hover:bg-slate-100"); ``` ## 可以传什么 `.class(...)` 的参数需要能生成 `String`。常用写法有三种: | 写法 | 示例 | 适合场景 | | | | --------------- | ------------------------------------ | ------------- | ---------------------------------------------------------------------- | -------------------- | | 字符串字面量 | `.class("flex gap-2")` | 固定类名。 | | | | `String` | `.class(format!("w-[{}px]", width))` | 创建控件时已经算好的类名。 | | | | 返回 `String` 的闭包 | \`.class(move | | if open.get() { "block".to_string() } else { "hidden".to_string() })\` | 类名依赖 signal 或其他动态状态。 | 闭包版本会随依赖变化重新生成类名,适合控制显示隐藏、尺寸、状态样式等。 ```rust use flor::signal::{create_signal, Read}; use flor::view::builder::ClassBuilder; use flor_lys::label::label; let open = create_signal(true); let panel = label("详情") .class(move || { if open.get() { "block p-4".to_string() } else { "hidden".to_string() } }); ``` ## 类名怎样生效 同一串类名里可以同时写 layout class 和控件样式 class: ```rust let title = label("欢迎") .class("w-full px-4 py-2 text-center text-blue-600 font-semibold"); ``` 其中 layout class 由 Flor 统一解析;控件样式 class 由具体控件自己解析。不同控件支持的样式类不完全一样,不认识的类会被忽略。 完整解析规则查 [class 语法解析基础](/website/zh/api/class-syntax.md)。layout class 支持列表查 [layout class 语法](/website/zh/api/layout-class.md)。 ## 状态写法 类名可以带 `hover:`、`focus:`、`active:`、`disabled:` 这类状态前缀。 ```rust let item = label("可选项") .class("px-3 py-2 hover:bg-slate-100 focus:bg-blue-50 disabled:opacity-50"); ``` 状态前缀的解析规则见 [class 语法解析基础](/website/zh/api/class-syntax.md#状态前缀)。 ## 和 layout / style 的关系 Flor 的设计里,class 是面向快速开发的字符串表达层。它可以描述 layout 的值,也可以描述控件 style 的值,所以适合写简短、可读、接近 Tailwind 的配置: ```rust .class("flex flex-col gap-2 p-4") ``` `.layout(...)` 是直接设置布局的强类型 builder。`.style(...)` 是直接设置控件样式的强类型 builder。`.class(...)` 主要是为了快速开发,所以作为可选 feature 提供。 三者可以同时使用;同一项配置以后写入的值为准。 --- url: /website/zh/guide/use/builder/style.md --- # 样式 Builder 样式 Builder 的入口是 `.style(|style| { ... })`。它用于手动编写控件样式的链式配置,适合你想直接调用控件提供的强类型样式方法,而不是把样式写成 class 字符串时使用。 ```rust use flor::macros::color; use flor::view::builder::StyleBuilder; use flor_lys::label::{label, LabelStyleResolverExt}; let title = label("标题") .style(|style| { style .font_size(20.0) .text_color(color!("#0f172a")) }); ``` ## 基本写法 使用 `.style(...)` 时通常需要两个导入: | 导入 | 作用 | | ------------------------- | ----------------------- | | `StyleBuilder` | 让控件拥有 `.style(...)` 方法。 | | 控件自己的 `*StyleResolverExt` | 让闭包里的 `style` 拥有具体链式方法。 | 例如 `Label` 的样式方法来自 `LabelStyleResolverExt`: ```rust use flor::macros::color; use flor::view::builder::StyleBuilder; use flor_lys::label::{label, LabelStyleResolverExt}; let label = label("状态") .style(|style| style.text_color(color!("#2563eb"))); ``` `.style(...)` 的闭包必须返回修改后的 `style`。如果要连续设置多个值,就直接链式调用。 ## 不是所有控件都有 `.style(...)` 是给控件手动暴露样式链式能力用的。是否支持 `.style(...)`、闭包里的 `style` 有哪些方法,都由具体控件决定。 比如 `Label` 可以有 `font_size(...)`、`text_color(...)` 这类方法;`Button` 可以有按钮变体、颜色、圆角等方法。具体方法应看控件库中该控件的文档。 ```rust use flor::macros::color; use flor::view::builder::StyleBuilder; use flor_lys::button::{button, ButtonStyleResolverExt, ButtonVariant}; let save = button("保存") .style(|style| { style .variant(ButtonVariant::Outline) .color(color!("#2563eb")) }); ``` 如果某个控件没有实现样式 builder,调用 `.style(...)` 时编译器会提示该方法不可用。 ## 状态样式 样式 builder 可以按控件状态设置样式。常用状态方法包括 `normal()`、`hover()`、`focus()`、`active()`、`disabled()`。 ```rust use flor::macros::color; use flor::view::builder::StyleBuilder; use flor_lys::label::{label, LabelStyleResolverExt}; let item = label("可选项") .style(|style| { style .text_color(color!("#334155")) .hover() .text_color(color!("#2563eb")) }); ``` 这段代码表示:普通状态使用深灰色,悬停状态使用蓝色。 ## 和 class / layout 的关系 `.style(...)` 是控件样式的强类型写法。它适合在你已经知道目标控件支持哪些样式方法,并希望获得编译期检查时使用。 `.layout(...)` 是布局的强类型写法,负责显示、尺寸、间距、Flex/Grid 等布局属性。 `.class(...)` 是快速开发用的字符串写法。Flor 的设计理念是:class 可以描述 layout 和 style 的任意值表达,所以能用一串 class 同时设置布局和控件样式。也正因为它是为了快速开发增加的能力,`class` 是可选 feature。 三者可以同时使用;同一项配置以后写入的值为准。 --- url: /website/zh/guide/use/builder/disable.md --- # 禁用 Builder 禁用 Builder 用来把控件切到 `ControlState::Disabled`。它是应用侧配置控件状态的 builder,不是自定义控件作者需要在 `View` trait 里重写的方法。 当前公开接口是: ```rust pub trait DisableBuilder { fn disable(self, f: F) -> Self where F: Fn() -> bool + 'static; } ``` ## 基本写法 导入 `DisableBuilder` 后,所有实现了 `View` 的控件都可以调用 `.disable(...)`。 ```rust use flor::view::builder::DisableBuilder; use flor_lys::button::button; let save = button("保存").disable(|| true); ``` 闭包返回 `true` 表示禁用,返回 `false` 表示正常。固定禁用可以直接写 `|| true`,但更常见的是从信号里读取状态。 ## 响应式禁用 `.disable(...)` 会创建一个响应式 updater。闭包里读取的信号变化后,框架会重新计算禁用状态。 ```rust use flor::signal::{create_signal, Read, Write}; use flor::view::builder::DisableBuilder; use flor_lys::button::button; let saving = create_signal(false); let save = button("保存") .disable(move || saving.get()); // 开始保存时禁用按钮。 saving.set(true); ``` 需要组合多个条件时,直接在闭包里写业务判断: ```rust use flor::signal::{create_signal, Read}; use flor::view::builder::DisableBuilder; use flor_lys::button::button; let saving = create_signal(false); let form_valid = create_signal(false); let submit = button("提交") .disable(move || saving.get() || !form_valid.get()); ``` ## 机制说明 `.disable(...)` 内部会: 1. 读取当前控件的 `ViewId`。 2. 用 `create_updater_with_id` 创建一个 updater。 3. 在 updater 变化时写入 `ViewState.disable`。 4. 把 effect id 挂到当前 `ViewId` 的 pending effect 列表里,控件创建流程会激活它。 禁用状态参与 `ControlState` 计算,且优先级最高:`Disabled > Active > Focus > Hover > Normal`。控件作者在 `on_draw`、`on_measure` 或事件处理中读取 `ControlState::Disabled`,决定禁用状态下的视觉和交互行为。 禁用不会自动移除焦点表条目,也不会自动取消当前焦点。需要运行时改变焦点能力时,使用 [ViewId](/website/zh/guide/control/view-id.md) 的焦点 API。 --- url: /website/zh/guide/use/builder/z-index.md --- # 层级 Builder 层级 Builder 用来给控件设置 `z_index`。它只影响同一个父控件下的兄弟节点排序,不是全局层叠上下文。 ## 基本写法 导入 `ZIndexBuilder` 后,所有实现了 `View` 的控件都可以调用 `.z_index(...)`。 ```rust use flor::view::builder::ZIndexBuilder; use flor::views; use flor_lys::div::div; use flor_lys::label::label; let panel = div(views![ label("普通内容").z_index(|| 0), label("浮层内容").z_index(|| 10), ]); ``` 当前签名是: ```rust pub trait ZIndexBuilder { fn z_index(self, z_index: impl Fn() -> i32 + 'static) -> Self; } ``` 所以固定值也要写成返回 `i32` 的闭包,例如 `.z_index(|| 10)`。如果层级依赖 signal,就在闭包里读取 signal: ```rust use flor::signal::{create_signal, Read}; use flor::view::builder::ZIndexBuilder; use flor_lys::label::label; let popup_open = create_signal(false); let popup = label("菜单") .z_index(move || if popup_open.get() { 100 } else { 0 }); ``` ## 和 class 的关系 启用 `class` feature 后,layout class 里的 `z-*` 会被解析成同一套运行时层级值。 ```rust use flor::view::builder::ClassBuilder; use flor_lys::label::label; let popup = label("菜单").class("z-100"); ``` `z-auto` 会写入 `0`,`z-10` 会写入 `10`。`z-*` 是 layout class 里的特殊项:它不会进入 Taffy 布局样式,而是直接更新 `ViewId` 的层级值。 如果你已经用 `.z_index(...)` 动态控制层级,不要再在同一个控件上用 `z-*` 表达另一个固定层级,避免后续更新互相覆盖。 ## 适合的场景 常见用途是把同一容器里的临时内容和普通内容区分开: | 场景 | 建议 | | ----- | ---------------------- | | 弹出菜单 | 给菜单设置高于触发按钮和普通内容的值。 | | 浮动工具条 | 给工具条设置稳定的高层级值。 | | 拖动预览 | 拖动期间把预览控件切到更高层级。 | | 普通列表项 | 保持默认 `0`,只在需要悬浮或拖动时提高。 | 层级只在兄弟节点之间比较。父控件不同的两个控件,不能只靠 `z_index` 做全局遮盖;需要把它们放进同一个可排序的父容器,或者调整控件树结构。 ## 机制说明 `.z_index(...)` 会创建一个响应式 updater。初始化时会立刻计算一次闭包结果,并调用当前控件的 `ViewId::set_z_index(value)`;之后闭包依赖的 signal 更新时,会再次写入新的层级值。 `ViewId::set_z_index` 会: 1. 把值写入 `VIEW_STORAGE.view_z_index`。 2. 找到当前控件的父控件。 3. 按兄弟控件各自的 `z_index()` 重新排序父控件的 child list。 没有设置过层级的控件,`ViewId::z_index()` 返回 `0`。 --- url: /website/zh/guide/use/builder/transform.md --- # 变换 Builder 变换 Builder 用来给控件设置声明式 `Transform2D`。它影响控件自身及其子控件的绘制和命中测试,但不改变 Taffy 布局计算出来的位置和尺寸。 ## 基本写法 导入 `TransformBuilder` 后,所有实现了 `View` 的控件都可以调用 `.transform(...)`。 ```rust use flor::types::Transform2D; use flor::view::builder::TransformBuilder; use flor_lys::label::label; let title = label("Flor") .transform(Transform2D::rotation_degrees(-3.0)); ``` 当前签名是: ```rust pub trait TransformBuilder { fn transform(self, transform: impl TransformProp) -> Self; } ``` `TransformProp` 支持两种写法: | 写法 | 示例 | 适合场景 | | | | -------------------- | ------------------------------------------------- | ----------------- | ---------------------------------------------- | ----------------- | | 固定 `Transform2D` | `.transform(Transform2D::translation(12.0, 0.0))` | 创建后不变的视觉偏移、旋转、缩放。 | | | | 返回 `Transform2D` 的闭包 | \`.transform(move | | Transform2D::rotation\_degrees(angle.get()))\` | 由 signal 驱动的动态变换。 | ## 动态变换 闭包版本会随依赖更新重新计算变换。 ```rust use flor::signal::{create_signal, Read}; use flor::types::Transform2D; use flor::view::builder::TransformBuilder; use flor_lys::label::label; let angle = create_signal(0.0f32); let spinner = label("loading") .transform(move || Transform2D::rotate_at_degrees(angle.get(), 40.0, 12.0)); ``` `rotate_at_degrees(degrees, cx, cy)` 和 `scale_at(sx, sy, cx, cy)` 可以表达类似 `transform-origin` 的效果。Flor 当前没有单独的 `.origin(...)` builder。 ## Transform2D 常用构造 | API | 作用 | | ------------------------------------------------- | ---------- | | `Transform2D::IDENTITY` | 单位矩阵。 | | `Transform2D::new(m11, m12, m21, m22, dx, dy)` | 直接构建矩阵。 | | `Transform2D::translation(x, y)` | 平移。 | | `Transform2D::scale(sx, sy)` | 缩放。 | | `Transform2D::rotation(radians)` | 按弧度旋转。 | | `Transform2D::rotation_degrees(degrees)` | 按角度旋转。 | | `Transform2D::skew(x_rad, y_rad)` | 按弧度倾斜。 | | `Transform2D::skew_degrees(x_deg, y_deg)` | 按角度倾斜。 | | `Transform2D::rotate_at(radians, cx, cy)` | 绕指定点旋转。 | | `Transform2D::rotate_at_degrees(degrees, cx, cy)` | 绕指定点按角度旋转。 | | `Transform2D::scale_at(sx, sy, cx, cy)` | 绕指定点缩放。 | 链式组合使用 `then_*` 方法: ```rust use flor::types::Transform2D; let transform = Transform2D::translation(16.0, 0.0) .then_rotate_degrees(8.0) .then_scale(1.05, 1.05); ``` 组合顺序是先应用当前矩阵,再应用新的矩阵。也就是说,上面的例子会先平移,再旋转,最后缩放。 ## 不是 CSS transform 字符串 `.transform(...)` 接收的是 `Transform2D`,不是字符串。当前没有这些 builder 方法: ```rust // 当前不存在 .transform("rotate(45deg)") .rotate(45.0) .scale(1.2) .translate(10.0, 20.0) .skew(10.0, 0.0) .origin("center") ``` 对应写法应改成: ```rust use flor::types::Transform2D; .transform(Transform2D::rotation_degrees(45.0)) .transform(Transform2D::scale(1.2, 1.2)) .transform(Transform2D::translation(10.0, 20.0)) .transform(Transform2D::skew_degrees(10.0, 0.0)) .transform(Transform2D::rotate_at_degrees(45.0, 50.0, 20.0)) ``` ## 机制说明 `.transform(...)` 会创建一个响应式 updater。初始化时会立刻计算一次 `Transform2D`,并调用当前控件的 `ViewId::set_transform(transform)`;之后闭包依赖的 signal 更新时,会再次写入新的变换并请求重绘。 运行时变换存放在 `VIEW_STORAGE.transform`。布局刷新阶段会计算累积变换,绘制阶段会 `push_transform`,命中测试会用逆矩阵把窗口坐标转换回控件局部坐标。 如果矩阵不可逆,例如缩放到 `0.0`,命中测试无法把窗口坐标转换回局部坐标,这个控件分支会被跳过。动画里不要把可交互控件缩放到不可逆矩阵。 --- url: /website/zh/guide/use/resolver-layer.md --- # Resolver Layer 机制 Resolver Layer 是 Flor 用来组织控件配置的机制。它让控件的布局和样式可以分段写、重复写、按需覆盖,而不是要求你一次性把所有配置拼成一整块。 在使用代码里,`.class(...)`、`.layout(...)`、`.style(...)` 都会使用这套 Layer 机制。每调用一次这些 builder,都可以理解成给控件追加一层配置。 Layer 的合并规则很简单: | 情况 | 结果 | | ---------- | ---------------------- | | 不同层写了不同属性 | 这些属性会合并保留。 | | 不同层写了同一个属性 | 无论值相同还是不同,都以后写入的那一层为准。 | 也就是说,你可以把 class、layout、style 拆开多次写,也可以混在一起写。后面的调用不会把前面整层配置清空;它只会覆盖自己重新写过的属性。 ## 混合调用 比如先用 class 写常用布局,再用 layout builder 精确覆盖其中一个属性: ```rust use flor::taffy::Display; use flor::view::builder::{ClassBuilder, LayoutBuilder}; use flor::view::resolver::LayoutResolverExt; use flor::views; use flor_lys::div::div; use flor_lys::label::label; let panel = div(views![label("内容")]) .class("hidden p-4") .layout(|layout| layout.display(Display::Flex)) .class("gap-3"); ``` 这段代码可以按三层理解: | 层 | 写入内容 | | | | ---------------------- | ------------------ | -------------------------------- | --------------- | | `.class("hidden p-4")` | 写入 `display` 和内边距。 | | | | \`.layout( | layout | layout.display(Display::Flex))\` | 再次写入 `display`。 | | `.class("gap-3")` | 写入间距。 | | | 最终结果是:`display` 使用后写入的 `Display::Flex`,内边距来自 `p-4`,间距来自 `gap-3`。 ## 同属性覆盖 同一个属性写多次时,最后一次写入生效。class 是这样: ```rust use flor::view::builder::ClassBuilder; use flor_lys::label::label; let item = label("项目") .class("p-2") .class("p-4"); ``` 最终内边距是 `p-4`。 layout builder 也是这样: ```rust use flor::taffy::Display; use flor::view::builder::LayoutBuilder; use flor::view::resolver::LayoutResolverExt; use flor_lys::label::label; let item = label("项目") .layout(|layout| layout.display(Display::None)) .layout(|layout| layout.display(Display::Flex)); ``` 最终 `display` 是 `Display::Flex`。 style builder 也是同一套规则: ```rust use flor::view::builder::{ClassBuilder, StyleBuilder}; use flor_lys::label::{label, LabelStyleResolverExt}; let title = label("标题") .class("text-lg") .style(|style| style.font_size(20.0)); ``` 如果 `text-lg` 和 `font_size(20.0)` 都在设置字号,那么最终字号使用后写入的 `20.0`。 ## 不同属性合并 后面的调用只覆盖它重新写过的属性。没有重新写的属性,会继续保留: ```rust use flor::macros::color; use flor::view::builder::{ClassBuilder, StyleBuilder}; use flor_lys::label::{label, LabelStyleResolverExt}; let title = label("标题") .class("text-lg font-bold") .style(|style| style.text_color(color!("#2563eb"))); ``` 这里 `.class(...)` 写了字号和字重,`.style(...)` 只写文本颜色,所以最终会同时保留字号、字重和文本颜色。 ## 状态也按层合并 状态前缀也是同样的规则。普通状态、hover、focus、active、disabled 都会分别合并自己的属性: ```rust use flor::view::builder::ClassBuilder; use flor_lys::label::label; let item = label("保存") .class("text-slate-700 hover:text-blue-600 hover:font-bold") .class("hover:text-red-600"); ``` 最终普通状态使用 `text-slate-700`。hover 状态里,`font-bold` 会保留;文本颜色被后面的 `hover:text-red-600` 覆盖。 ## 有什么用 Layer 机制的作用不是让写法变复杂,而是让你可以把配置拆到合适的位置。 你可以先写一组基础配置,再在后面重新指定某些属性: ```rust let item = label("项目") .class("p-2 text-slate-700") .class("p-4"); ``` 这里后面的 `p-4` 只覆盖内边距,`text-slate-700` 仍然保留。 你也可以把某个属性单独交给响应式信号控制,而不是把整串 class 或整组样式都塞进 `if else` 里拼接: ```rust use flor::signal::{create_signal, Read}; use flor::taffy::Display; use flor::view::builder::{ClassBuilder, LayoutBuilder}; use flor::view::resolver::LayoutResolverExt; use flor_lys::label::label; let open = create_signal(true); let panel = label("详情") .class("p-4 text-slate-700") .layout(move |layout| { if open.get() { layout.display(Display::Flex) } else { layout.display(Display::None) } }); ``` 这样,静态配置仍然写在 `.class(...)` 里,只有真正会变化的 `display` 放进响应式 `.layout(...)`。如果把这些内容全部写成一个动态字符串,通常会出现重复的 class、复杂的分支和难维护的文本拼接。 Layer 机制也支撑了更高一层的控件封装:handler 可以作为参数传入,链式配置后的控件可以作为返回值继续传递。具体怎么把基础控件封装成复合控件或业务模块,见 [框架 DSL](/website/zh/guide/use/framework-dsl.md)。 维护终端用户的使用体验是框架的核心目标之一。Layer 机制就是为了让应用代码能清楚表达“哪些配置是基础值,哪些配置会覆盖,哪些配置会响应变化”,从而减少为了实现交互状态而制造出来的代码噪音。 只要记住一条规则:不同属性会合并,同一个属性永远后写覆盖前写。 --- url: /website/zh/guide/use/framework-dsl.md --- # 框架 DSL Flor 的界面代码可以当作一套普通 Rust DSL 来写:函数负责组合控件,参数负责传入属性、信号和 handler,返回值就是已经配置好的控件。 这和 JSX 的使用感受有相似之处:页面不是一大段字符串模板,而是一组可以拆分、复用、传参的控件函数。区别是 Flor 不需要额外的模板语言,组合本身就是 Rust 的函数调用和链式方法。 如果还不熟悉 `.class(...)`、`.layout(...)`、`.style(...)` 多次调用如何合并,先看 [Resolver Layer 机制](/website/zh/guide/use/resolver-layer.md)。 ## 控件树怎么表达 Flor 里最常见的子控件列表写法是 `views![...]`: ```rust use flor::views; use flor_lys::button::button; use flor_lys::div::div; use flor_lys::label::label; let panel = div(views![ label("账号登录"), button("发送验证码"), button("登录"), ]); ``` `views![...]` 接受一组控件表达式,并把它们转换成控件列表。它适合在控件树现场展开子节点,尤其是容器里直接写多个子控件时。 如果只需要把单个控件转换成通用控件对象,可以使用 `view!(...)`: ```rust use flor_lys::label::label; let title = flor::view!(label("账号登录")); ``` 宏不是必须入口。它只是让“现场写控件树”更顺手;当你需要把单个控件按某个参数类型传出去时,直接调用 `into_view()`,或者把值交给接收 `IntoViewIter` 的 API,往往更清楚。 ## IntoView 和 IntoViewIter `IntoView` / `IntoViewIter` 这组能力用于把控件转换成框架更通用的控件形态。它不只用于方法返回值,也用于控件参数、模块参数、容器子节点等需要类型对齐的地方。 当前公开写法里,单控件转换是 `IntoView`,子控件序列转换是 `IntoViewIter`。单个控件本身也实现 `IntoViewIter`,所以只传一个子控件时不再需要先手动转成列表。在封装方法时,单个控件可以返回 `impl View` 或 `impl IntoView`;多个控件可以返回 `impl IntoViewIter`: ```rust use flor::view::builder::ClassBuilder; use flor::view::{IntoView, IntoViewIter, View}; use flor::views; use flor_lys::button::button; use flor_lys::label::label; fn title_view() -> impl View { label("账号登录") .class("text-xl font-bold") } fn title_child() -> impl IntoView { label("账号登录") .class("text-xl font-bold") } fn action_buttons() -> impl IntoViewIter { views![ button("取消").class("w-full"), button("确定").class("w-full"), ] } ``` `title_view()` 返回的是一个正常控件,调用方还能继续接 `.class(...)`、`.layout(...)` 等 builder。`title_child()` 更强调“这个返回值会被拿去当作单个子控件传入”。`action_buttons()` 则表示这个函数返回一组子控件。 比如 `div(...)` 这类容器接收的是 `impl IntoViewIter`。只有一个子控件时,可以直接传这个控件: ```rust use flor::view::builder::ClassBuilder; use flor_lys::div::div; use flor_lys::label::label; let empty = div( label("暂无数据") .class("text-sm") ); ``` 如果某个控件或模块参数要的是一个子控件,就标注 `impl IntoView`;如果要的是一组子控件或兼容单个子控件,就标注 `impl IntoViewIter`: ```rust use flor::view::builder::ClassBuilder; use flor::view::{IntoView, IntoViewIter, View}; use flor::views; use flor_lys::button::button; use flor_lys::div::div; use flor_lys::label::label; fn dialog(title: impl IntoView, actions: impl IntoViewIter) -> impl View { div(views![ div(title), div(actions), ]) } let confirm = dialog( label("删除文件").class("text-lg font-bold"), button("确认").class("w-full"), ); ``` 这样写的重点是让参数和返回值表达清楚:这里需要的是普通控件、通用控件对象,还是一组可以继续交给容器的子控件。 宏和转换能力可以搭配使用。`views![...]` 适合现场写多个子节点,`into_view()` 适合把一个已经存在的控件表达式对齐到通用控件对象: | 写法 | 用途 | | ----------------------------- | ------------------------------------ | | `fn f() -> impl View` | 封装一个完整控件,并保留继续链式配置的能力。 | | `fn f() -> impl IntoView` | 封装一个准备作为子控件传递的单控件,并且仍可继续接常用 builder。 | | `fn f() -> impl IntoViewIter` | 封装一组准备交给容器接收的子控件;单控件也满足这个约束。 | | `child.into_view()` | 把单个控件转成通用控件对象。 | | `child` 作为 `IntoViewIter` 参数 | 把单个控件当作只包含它的子控件序列。 | | `views![a, b, c]` | 在控件树现场写子控件列表。 | | `view!(child)` | 在需要宏写法时,把单个控件转成通用控件对象。 | ## 返回值继续链式配置 `IntoView` 继承了 `ViewIdentity`,常用 builder 现在基于 `ViewIdentity` 读取控件身份。这意味着函数返回 `impl IntoView` 后,调用方仍然可以继续接 `.class(...)`、`.layout(...)`、`.on_click(...)`、`.focus_index(...)` 等链式方法: ```rust use flor::view::builder::{ClassBuilder, EventBuilder}; use flor::view::IntoView; use flor_lys::button::button; fn toolbar_button(text: &'static str) -> impl IntoView { button(text).class("px-3 py-1") } let save = toolbar_button("保存") .class("font-bold") .on_click(|| { println!("save"); }); ``` 这类封装不再需要为了继续链式配置而强行返回 `impl View`。当函数语义是“返回一个可作为子控件传递的值”时,`impl IntoView` 更直接。 ## 为什么需要宏 Flor 提供 `views![...]`,是为了让现场编写子控件列表时更顺手,同时避开 Rust 元组在 UI 子节点表达上的长度问题。 Floem 这类框架可以用元组表达子控件,例如 `(a, b, c)`。这种写法很自然,但在稳定 Rust 里,框架通常需要为不同长度的元组分别实现 trait。也就是说,它不是无限长度的:支持到多少个子节点,取决于框架写了多少组元组实现。 `views![...]` 会直接展开成控件列表,不需要为每一种子节点数量单独实现一遍 trait,也不容易让用户在子控件数量变多时撞到元组长度上限。 后续加入元组支持是可行的,做法通常是为常见长度的元组补实现,让用户可以在简单场景写 `(a, b)` 或 `(a, b, c)`。但只要稳定 Rust 还没有真正的可变参数泛型,元组支持本身仍然会有一个框架定义的上限。所以即使以后支持元组,`views![...]` 也更适合作为没有固定子节点数量时的稳定写法。 ## 方法组合控件 最小的封装就是把一个控件的布局、样式和事件写进函数里,然后返回它: ```rust use flor::view::builder::{ClassBuilder, EventBuilder, StringProp}; use flor::view::handler::IntoEventHandler; use flor::view::handler::OnClickHandler; use flor::view::View; use flor_lys::button::button; fn primary_button( text: T, on_click: impl IntoEventHandler, ) -> impl View where T: StringProp, { button(text) .class("w-full") .on_click(on_click) } ``` 这里的 `Args` 是事件 handler 参数形态的推导标记。这样写以后,调用方既可以传完整点击参数,也可以传无参数、只接收 `ViewId`、或省略 `ViewId` 的 handler。如果这里写成旧式的 `impl Into`,就只保留完整参数签名的 `From` 转换,简化参数形态会丢失。 调用方只需要关心这个按钮对外暴露的参数: ```rust let save = primary_button("保存", |view_id| { println!("save from {view_id}"); }); ``` 这里 `primary_button` 就像一个小型控件。它内部怎么写 class、怎么绑定事件,对调用方来说都被收进了函数边界里。 ## 组合成模块 多个控件也可以封装成一个复合控件或业务模块。比如登录模块可以把标题、验证码按钮、登录按钮和布局一起封装起来: ```rust use flor::view::builder::{ClassBuilder, EventBuilder}; use flor::view::handler::{IntoEventHandler, OnClickHandler}; use flor::view::View; use flor::views; use flor_lys::button::button; use flor_lys::div::div; use flor_lys::label::label; fn login_module( on_login: impl IntoEventHandler, on_send_code: impl IntoEventHandler, ) -> impl View { div(views![ label("账号登录").class("text-xl font-bold"), button("发送验证码") .class("w-full") .on_click(on_send_code), button("登录") .class("w-full") .on_click(on_login), ]) .class("flex flex-col gap-3 p-4") } ``` 不同 handler 参数要使用不同的 `Args` 泛型,例如这里的 `LoginArgs` 和 `SendCodeArgs`。它们没有业务含义,只是让两个 handler 各自独立推导自己的参数形态。 这个函数返回的不是模板片段,而是一个完整控件。调用方可以把 handler 作为参数传进去: ```rust let login = login_module( || { println!("login"); }, || { println!("send code"); }, ); ``` 如果模块里需要响应式状态,也可以把 signal 作为参数传入,让模块内部只控制自己关心的属性。比如登录按钮是否可用、验证码倒计时、面板是否显示,都可以独立拆成自己的参数,而不是把整段布局和样式写成复杂的字符串拼接。 ## 返回后继续配置 函数返回的控件仍然可以继续使用 builder。也就是说,模块内部可以写默认布局和样式,调用方可以在外部继续补充或覆盖: ```rust use flor::view::builder::ClassBuilder; let login = login_module(on_login, on_send_code) .class("w-[360px]"); ``` 这里 `login_module` 内部已经写了 `flex flex-col gap-3 p-4`,调用方只补了宽度。因为 [Resolver Layer 机制](/website/zh/guide/use/resolver-layer.md) 会按层合并配置,所以外部补充不会清掉模块内部的配置;如果外部重新写了同一个属性,则以后写的为准。 ## 为什么适合 AI Flor 的这套 DSL 走的是常见的函数式 UI 表达路径:方法组合控件,属性、信号和 handler 作为变量传入,返回值继续参与组合。 大量 AI 模型已经熟悉 React、JSX、函数式组件和声明式 UI 的组织方式,所以它们更容易迁移到 Flor 的写法上:识别控件树、补全属性、拆分模块、调整布局和事件绑定。 这也是 [AI](/website/zh/guide/ai.md) 页面提到的原因:Flor 并不是为了 AI 才这样设计,但这种 DSL API 自然走到了 AI 已经训练过、也更容易理解的表达路径上。 --- url: /website/zh/guide/use/focus.md --- # 焦点机制 Flor 的焦点是显式机制。控件不会因为能点击、能显示文字、绑定了键盘事件,就自动进入焦点系统。一个控件只有进入焦点表,才会被 Tab 访问,才会成为控件级 [`on_key_*`](/website/zh/api/handler.md#键盘事件) 的派发目标,也才会由焦点管理器触发 `on_focus` 和 `on_blur`。 先看一个最小例子: ```rust use flor::view::builder::{EventBuilder, FocusIndexBuilder}; use flor_lys::label::label; let name = label("名称") // 加入焦点表。0 是排序值,表示这是当前范围内最先访问的焦点项。 .focus_index(0) .on_focus(|view_id, virtual_index| { println!("focus {view_id}, virtual index: {virtual_index}"); }); ``` 没有 `focus_index` 的控件不会进入焦点表。它不会被 Tab 选中;点击它时,框架无法把焦点设置到这个控件;它的控件级 `on_focus` / `on_blur` 不会由焦点管理器触发;它的控件级 [`on_key_*`](/website/zh/api/handler.md#键盘事件) 也不会因为“这个控件获得焦点”而触发。 具体的 builder 写法见 [焦点 Builder](/website/zh/guide/use/builder/focus.md)。运行时通过 `ViewId` 操控焦点的 API 见 [ViewId](/website/zh/guide/control/view-id.md)。 ## 键盘事件依赖焦点 键盘事件会派发给当前焦点控件。想让一个控件处理控件级键盘事件,需要让它进入焦点表,并让它成为当前焦点。 ```rust use flor::base::platform::{HandleResult, KeyCode}; use flor::view::builder::{EventBuilder, FocusIndexBuilder}; use flor_lys::label::label; let editor = label("按 Ctrl+S 保存") .focus_index(0) .on_key_down(|_, code, is_alt, is_ctrl, is_shift| { if !is_alt && is_ctrl && !is_shift && code == KeyCode::S { println!("save"); HandleResult::Handled } else { HandleResult::Default } }); ``` 当前没有焦点控件时,`key_down` / `key_up` 会走窗口级处理;某个没有获得焦点的普通控件不会收到自己的控件级 [`on_key_*`](/website/zh/api/handler.md#键盘事件)。文本输入和 IME 输入同样依赖当前焦点控件。 ## 点击与焦点 鼠标事件走命中测试,和焦点表不是同一条派发路径。所以没有 `focus_index` 的控件仍然可以收到 `on_click`、`on_button_down`、`on_mouse_move` 这类鼠标事件。 点击结束时,框架会尝试把焦点设置到被点击的控件上。这个动作仍然受焦点表限制:目标控件没有 `focus_index`,焦点管理器找不到对应条目,控件不会成为当前焦点。 ```rust use flor::view::builder::{EventBuilder, FocusIndexBuilder}; use flor_lys::label::label; let item = label("点击后获得焦点") .focus_index(0) .on_click(|| { println!("clicked"); }); ``` ## 虚拟焦点 焦点表里的条目不是单纯的 `ViewId`,而是: ```text (focus_index, ViewId, virtual_index) ``` 大多数控件只有一个虚拟焦点点位,序号是 `0`。所以 `on_focus` 和 `on_blur` 的第二个参数常见值就是 `0`。 ```rust use flor::view::builder::{EventBuilder, FocusIndexBuilder}; use flor_lys::label::label; let view = label("单焦点控件") .focus_index(0) .on_focus(|_, virtual_index| { assert_eq!(virtual_index, 0); }); ``` 复合控件可以暴露多个虚拟焦点点位。比如一个编辑器控件可以把文本区、补全面板、行号区设计成同一个控件内的不同焦点位置。终端用户只需要读取 `virtual_index`,不需要自己申请虚拟焦点数量;申请数量属于控件开发者的 `View::on_focus_count` 能力。 ## 排序分段 复杂页面经常需要把焦点顺序拆成几个区域。Flor 提供 `focus_scope(u32)` 作为排序偏移:它会影响当前控件子树里的 `focus_index` 最终排序值。 ```rust use flor::view::builder::FocusIndexBuilder; use flor::views; use flor_lys::div::div; use flor_lys::label::label; let toolbar = div(views![ label("工具 1").focus_index(0), label("工具 2").focus_index(1), ]) .focus_scope(100); let content = div(views![ label("内容 1").focus_index(0), label("内容 2").focus_index(1), ]) .focus_scope(200); ``` 这组焦点的最终排序值是: | 控件 | 局部写法 | 最终排序值 | | ---- | --------- | ----- | | 工具 1 | `100 + 0` | `100` | | 工具 2 | `100 + 1` | `101` | | 内容 1 | `200 + 0` | `200` | | 内容 2 | `200 + 1` | `201` | `focus_scope(u32)` 是排序分段,不是弹窗里的焦点隔离。它不会让 Tab 只能停留在这个区域内。 ## 运行时作用域 Modal、Popup、侧边栏这类界面需要另一种能力:打开时让 Tab 只在弹出层内部循环,关闭后恢复之前的焦点位置。 Flor 用运行时焦点作用域处理这个场景。弹出层打开时,把弹出层根控件压入焦点作用域;关闭时,把这个作用域弹出。 ```rust use flor::view::builder::{EventBuilder, FocusIndexBuilder}; use flor::views; use flor_lys::div::div; use flor_lys::label::label; let dialog = div(views![ label("名称").focus_index(0), label("确认").focus_index(1), ]) .on_create(|dialog_root_id| { dialog_root_id.push_focus_scope(); }); ``` 关闭弹出层时调用 `pop_focus_scope`。完整的 `ViewId` 方法说明见 [ViewId](/website/zh/guide/control/view-id.md)。 运行时作用域只筛选已经在焦点表里的控件。如果弹出层内部没有控件设置 `focus_index`,Tab 在这个作用域里没有目标。 ## 机制说明 窗口创建时,Flor 会初始化焦点表: 1. 从窗口根节点开始遍历控件树。 2. 遇到 `focus_scope(u32)` 时,把它加到当前累计偏移上。 3. 遇到 `focus_index(u32)` 时,用“累计偏移 + 局部 index”生成排序值。 4. 查询该控件的虚拟焦点数量,默认是 `1`。 5. 为每个虚拟焦点生成一条 `(focus_index, ViewId, virtual_index)`。 6. 把所有条目排序后交给 `FocusManager`。 Tab 键进入事件总线后,`FocusManager::next()` 会在当前焦点表里向后移动;Shift+Tab 会调用 `prev()` 向前移动。每次焦点切换时,旧条目触发 `blur`,新条目触发 `focus`。 如果存在运行时焦点作用域,`FocusManager` 会先把焦点表过滤到栈顶作用域根控件的子树内,再执行 next/prev。 键盘事件查当前焦点所在的 `ViewId`,然后派发给这个控件。没有当前焦点时,控件级键盘 handler 不会被调用。 --- url: /website/zh/guide/features/overview.md --- # 框架能力 框架能力这一栏专门介绍 `flor` 的可选能力。它们大多通过 Cargo feature 开启,启用后会暴露对应的 API、事件 builder 或平台行为。 如果只是想查所有 feature 的名字,先看 [快速了解](/website/zh/guide/startup.md#框架-feature)。这一栏会把平台能力拆开讲清楚:启用后能做什么、入口在哪里、和哪些教程/API 有关。 ```toml [dependencies] flor = { version = "0.1.0", features = ["direct2d", "clipboard", "drag-drop"] } ``` ## 平台能力 | Feature | 能力 | 入口 | | ------------------------------ | ------------------------------- | --------------------------------------------------------------------- | | `clipboard` | 读写系统剪贴板。 | [剪贴板](/website/zh/guide/features/clipboard.md) | | `drag-drop` | 接收系统拖放进入、悬停、离开、释放事件。 | [拖放](/website/zh/guide/features/drag-drop.md) | | `tray` | 添加、更新、删除系统托盘图标,并接收托盘鼠标事件。 | [系统托盘](/website/zh/guide/features/tray.md) | | `theme-change` | 暴露系统主题变化相关事件入口。 | [主题变化](/website/zh/guide/features/theme-change.md) | | `monitor` | 查询显示器列表、工作区、DPI、缩放等信息。 | [显示器信息](/website/zh/guide/features/monitor.md) | | `hi-dpi` | 启用进程级 DPI 感知,配合 DPI 变化事件更新布局单位。 | [高 DPI](/website/zh/guide/features/hi-dpi.md) | | `cross-thread-window-creation` | 允许在事件循环线程之外请求创建窗口。 | [跨线程窗口创建](/website/zh/guide/features/cross-thread-window-creation.md) | 事件类能力通常还需要配合 [事件 Builder](/website/zh/guide/use/builder/event.md) 使用;完整 handler 签名和当前派发状态见 [Handler API](/website/zh/api/handler.md)。 --- url: /website/zh/guide/features/clipboard.md --- # 剪贴板 `clipboard` feature 用来启用系统剪贴板读写能力。它适合实现复制、粘贴、导入导出临时文本等功能。 ```toml [dependencies] flor = { version = "0.1.0", features = ["direct2d", "clipboard"] } ``` ## 基本写法 剪贴板入口在平台层。使用时导入 `ClipboardApi` trait,然后创建 `Clipboard`。 ```rust use flor::base::platform::ClipboardApi; use flor::platform::clipboard::Clipboard; let clipboard = Clipboard(None); clipboard.set_clipboard_text("hello flor".to_string())?; let text = clipboard.get_clipboard_text()?; println!("{text}"); ``` `Clipboard(None)` 表示不绑定具体窗口。需要把剪贴板操作关联到某个窗口时,可以传入 `Some(window_id)`。 ```rust use flor::base::platform::ClipboardApi; use flor::platform::clipboard::Clipboard; let clipboard = Clipboard(Some(window_id)); clipboard.set_clipboard_text("from this window".to_string())?; ``` ## 支持的内容 `ClipboardApi` 提供两类入口: | 方法 | 用途 | | -------------------------------------------------- | ------------- | | `set_clipboard_text(...)` / `get_clipboard_text()` | 读写文本。 | | `set_clipboard(...)` / `get_clipboard(...)` | 按剪贴板格式读写原始字节。 | | `register_format(...)` | 注册自定义剪贴板格式。 | | `set_clipboard_muti_type(...)` | 一次写入多个格式。 | 常见剪贴板格式由 `ClipboardType` 表示,包括 `Text`、`Image`、`Rtf`、`Html`、`Files`。如果要和其它应用交换复杂数据,可以先注册自定义格式,再读写对应字节。 --- url: /website/zh/guide/features/drag-drop.md --- # 拖放 `drag-drop` feature 用来启用系统拖放事件。启用后,控件可以通过事件 builder 接收拖入、悬停、离开和释放。 ```toml [dependencies] flor = { version = "0.1.0", features = ["direct2d", "drag-drop"] } ``` ## 基本写法 拖放是事件能力,入口在 [事件 Builder](/website/zh/guide/use/builder/event.md)。 ```rust use flor::base::platform::DropEffect; use flor::view::builder::EventBuilder; use flor_lys::label::label; let drop_area = label("拖到这里") .on_drag_enter(|_view_id, _key_state, _pos, formats, effect| { if formats.iter().any(|format| matches!(format, flor::base::platform::DragFormat::Files(_))) { *effect = DropEffect::Copy; } }) .on_drop(|_view_id, _key_state, _pos, data, effect| { println!("{data:?}"); *effect = DropEffect::Copy; }); ``` 事件: | 方法 | 用途 | | -------------------- | ------------- | | `on_drag_enter(...)` | 拖放内容进入控件命中区域。 | | `on_drag_over(...)` | 拖放内容在控件上移动。 | | `on_drag_leave(...)` | 拖放内容离开控件。 | | `on_drop(...)` | 用户释放拖放内容。 | 完整签名和当前派发状态见 [Handler API 的拖放事件](/website/zh/api/handler.md#拖放事件)。 ## 相关类型 拖放事件会直接遇到这些类型: | 类型 | 用途 | | --------------- | ----------------------------------- | | `DropEffect` | 控件给系统的拖放反馈,表示当前是拒绝、复制、移动、链接等效果。 | | `DragFormat` | 拖入和悬停阶段可用的数据格式,用来判断当前拖进来的内容大概是什么类型。 | | `DragData` | `on_drop(...)` 阶段真正拿到的数据。 | | `KeyState` | 拖放发生时的鼠标按键和修饰键状态。 | | `MousePosition` | 拖放发生时的鼠标坐标。 | `on_drag_enter(...)` 和 `on_drag_over(...)` 拿到的是 `&[DragFormat]`,适合先判断能不能接收;`on_drop(...)` 拿到的是 `&DragData`,适合读取真正的数据。 ## DropEffect `DropEffect` 用来告诉系统当前控件准备如何处理拖放内容。取值包括: | 值 | 含义 | | -------------------- | -------------- | | `DropEffect::None` | 不接受。 | | `DropEffect::Copy` | 接受,并表示复制。 | | `DropEffect::Move` | 接受,并表示移动。 | | `DropEffect::Link` | 接受,并表示创建链接。 | | `DropEffect::Scroll` | 表示拖放过程中发生滚动反馈。 | 在 `on_drag_enter`、`on_drag_over` 和 `on_drop` 里可以通过 `&mut DropEffect` 修改反馈。 ## DragFormat 和 DragData `DragFormat` 用于进入和悬停阶段,表示当前拖放对象声明自己可以提供哪些格式: | 值 | 含义 | | ----------------------- | ----------- | | `DragFormat::Files(_)` | 文件拖放。 | | `DragFormat::Text(_)` | 文本拖放。 | | `DragFormat::Image(_)` | 图片拖放。 | | `DragFormat::Custom(_)` | 平台或应用自定义格式。 | `DragData` 用于最终释放阶段,表示真正交给控件的数据: | 值 | 含义 | | ------------------------ | -------------- | | `DragData::None` | 没有可用数据。 | | `DragData::Files(paths)` | 文件路径列表。 | | `DragData::Text(text)` | 文本内容。 | | `DragData::Image(bytes)` | 图片字节数据。 | | `DragData::Raw(value)` | 平台原始数据,用于扩展场景。 | --- url: /website/zh/guide/features/tray.md --- # 系统托盘 `tray` feature 用来启用系统托盘图标能力。启用后可以添加、更新、删除托盘图标,并接收托盘图标上的鼠标事件。 ```toml [dependencies] flor = { version = "0.1.0", features = ["direct2d", "tray"] } ``` ## 基本写法 托盘能力通过 `FlorGui` 暴露。`FlorGui.init()?` 会初始化托盘所需的内部窗口。 ```rust use flor::base::platform::{IconSource, TrayEvent, TrayOptions}; use flor::FlorGui; FlorGui.init()?; FlorGui.tray_on_callback(|tray_id, event| { println!("tray {tray_id:?}: {event:?}"); }); let tray_id = FlorGui.tray_add(&TrayOptions { icon_path: Some(IconSource::from_path("app.ico".into())), tooltip: "Flor".to_string(), })?; ``` ## 相关类型 托盘功能会用到这些类型: | 类型 | 用途 | | ------------- | ------------------------------------------------ | | `TrayOptions` | 创建或更新托盘图标时传入的配置。当前包含 `icon_path` 和 `tooltip`。 | | `IconSource` | 图标来源,可以来自磁盘文件,也可以来自 RGBA 像素数据。 | | `TrayId` | `tray_add(...)` 返回的托盘图标 ID,后续更新、删除和事件回调都会用到它。 | | `TrayEvent` | 托盘图标触发的鼠标事件。 | | `MouseButton` | `TrayEvent` 里携带的鼠标按键,包含 `Left`、`Right`、`Middle`。 | `TrayEvent` 当前包括: | 值 | 含义 | | ------------------------------------- | ----------- | | `TrayEvent::MouseDown(button)` | 鼠标按键按下。 | | `TrayEvent::MouseUp(button)` | 鼠标按键松开。 | | `TrayEvent::MouseDoubleClick(button)` | 鼠标双击。 | | `TrayEvent::MouseEnter` | 鼠标进入托盘图标区域。 | | `TrayEvent::MouseLeave` | 鼠标离开托盘图标区域。 | | `TrayEvent::MouseMove` | 鼠标在托盘图标上移动。 | ## 图标来源 托盘图标通过 `IconSource` 指定: | 写法 | 用途 | | ------------------------------------------------ | ------------------------------- | | `IconSource::from_path(path)` | 从磁盘加载图标文件。Windows 当前适合传 `.ico`。 | | `IconSource::from_raw(width, height, rgba_data)` | 从 RGBA 像素数据创建图标。 | 更新和删除: ```rust FlorGui.tray_update(tray_id, &TrayOptions { icon_path: None, tooltip: "新的提示文字".to_string(), })?; FlorGui.tray_remove(tray_id)?; ``` --- url: /website/zh/guide/features/theme-change.md --- # 主题变化 `theme-change` feature 用来启用系统主题变化相关类型和事件 builder。它适合在系统从浅色模式切换到深色模式时,更新应用主题或重建相关样式。 ```toml [dependencies] flor = { version = "0.1.0", features = ["direct2d", "theme-change"] } ``` ## 事件入口 启用 feature 后,可以使用 `on_theme_changed(...)`: ```rust use flor::base::platform::ThemeMode; use flor::view::builder::EventBuilder; use flor_lys::label::label; let root = label("主题") .on_theme_changed(|_view_id, mode| { match mode { ThemeMode::Light => println!("light"), ThemeMode::Dark => println!("dark"), } }); ``` `ThemeMode` 目前有两个值: | 值 | 含义 | | ------------------ | ----- | | `ThemeMode::Light` | 浅色模式。 | | `ThemeMode::Dark` | 深色模式。 | 完整签名和当前派发状态见 [Handler API 的主题变化事件](/website/zh/api/handler.md#主题变化事件)。如果你只需要普通控件状态样式,不需要启用这个 feature;它面向的是系统主题变化。 --- url: /website/zh/guide/features/monitor.md --- # 显示器信息 `monitor` feature 用来启用显示器查询能力。它适合做窗口初始定位、多屏适配、读取工作区大小、按显示器 DPI 调整自定义逻辑等。 ```toml [dependencies] flor = { version = "0.1.0", features = ["direct2d", "monitor"] } ``` ## 基本写法 显示器入口在平台层。使用时导入 `MonitorApi` trait。 ```rust use flor::base::platform::MonitorApi; use flor::platform::Monitor; let monitors = Monitor::enumerate_monitors()?; for monitor in monitors { println!( "{} primary={} scale={} dpi=({}, {})", monitor.name(), monitor.is_primary(), monitor.scale_factor(), monitor.dpi_x(), monitor.dpi_y(), ); } ``` ## 查询入口 | 方法 | 用途 | | -------------------------------------------- | -------------------- | | `Monitor::enumerate_monitors()` | 枚举所有显示器。 | | `Monitor::monitor_from_point(x, y)` | 查询某个屏幕坐标所在的显示器。 | | `Monitor::monitor_from_window_id(window_id)` | 查询窗口所在的显示器。 | | `monitor.rect()` | 显示器完整区域。 | | `monitor.work_area()` | 可用工作区,通常会排除任务栏等系统区域。 | | `monitor.scale_factor()` | 缩放因子,通常由 DPI 推导。 | | `monitor.dpi_x()` / `monitor.dpi_y()` | 当前显示器 DPI。 | 如果只是想让布局单位随窗口 DPI 更新,通常不需要手动查询显示器;看 [高 DPI](/website/zh/guide/features/hi-dpi.md)。 --- url: /website/zh/guide/features/hi-dpi.md --- # 高 DPI `hi-dpi` feature 用来启用进程级 DPI 感知。它会让应用接收更准确的 DPI 信息,并让 Flor 在窗口 DPI 变化时更新渲染缩放和布局单位。 ```toml [dependencies] flor = { version = "0.1.0", features = ["direct2d", "hi-dpi"] } ``` ## 会影响什么 启用后,平台初始化时会设置 DPI awareness。窗口收到 DPI 变化消息时,Flor 会更新: | 内容 | 结果 | | ----- | ------------------------------- | | 渲染器缩放 | 渲染后端会收到新的 `dpi_x` / `dpi_y`。 | | 布局单位 | `pt` 等依赖 DPI 的单位会使用新的 DPI 重新计算。 | | 布局缓存 | DPI 变化后会清理布局 resolver 缓存并重新布局。 | 如果需要在应用层感知 DPI 变化,可以使用事件 builder: ```rust use flor::view::builder::EventBuilder; use flor_lys::label::label; let root = label("DPI") .on_dpi_change(|_view_id, dpi_x, dpi_y| { println!("dpi changed: {dpi_x}, {dpi_y}"); }); ``` `on_dpi_change(...)` 的完整签名见 [Handler API 的窗口事件](/website/zh/api/handler.md#窗口事件)。 --- url: /website/zh/guide/features/cross-thread-window-creation.md --- # 跨线程窗口创建 `cross-thread-window-creation` feature 允许在事件循环线程之外调用 `WindowOption::open(...)` 创建窗口。 ```toml [dependencies] flor = { version = "0.1.0", features = ["direct2d", "cross-thread-window-creation"] } ``` ## 什么时候需要启用 判断规则很简单:如果应用需要“在任意地方创建窗口”的能力,就一定要启用 `cross-thread-window-creation` feature。 典型场景包括在业务线程、后台任务、异步回调或非事件循环线程中调用 `WindowOption::open(...)`。这种调用路径一旦存在,就不要依赖人工保证“刚好在正确线程创建窗口”。 未启用该 feature 时,`WindowOption::open(...)` 默认会在当前线程直接创建窗口。这适合所有窗口都由事件循环线程或主线程创建的简单程序。 但如果在没有启用该 feature 的情况下,从非事件循环线程创建窗口,框架可能会遇到平台 UI 线程限制、事件循环同步问题、消息队列等待问题,甚至出现卡死或死锁。因此,只要程序存在跨线程创建窗口的需求,就应该显式开启该 feature。 ## 基本行为 启用 `cross-thread-window-creation` 后,`WindowOption::open(...)` 仍然使用同一个 API。 当调用发生在事件循环线程之外时,Flor 会把窗口创建请求投递到事件循环线程处理,并让当前线程等待创建结果。这样可以确保窗口真正由正确的 UI 线程创建,同时让调用方仍然获得同步的创建结果。 ```rust use flor::windows::WindowOption; use flor_lys::label::label; std::thread::spawn(|| { WindowOption::default() .open(|_window| label("from worker thread")) .expect("create window"); }); ``` ## 使用条件 跨线程窗口创建依赖事件循环线程处理创建队列,因此需要满足以下条件之一: 1. 事件循环已经在运行; 2. 创建请求已经在事件循环进入前排队,之后会由事件循环线程处理。 常见用法是:主线程初始化 Flor,并进入 `FlorGui.event_loop()?`;业务线程在需要时调用 `WindowOption::open(...)` 请求创建新窗口。 ```rust use flor::FlorGui; FlorGui.init()?; // 业务线程中可以请求创建窗口 // ... FlorGui.event_loop()?; ``` ## 为什么这是一个 feature `cross-thread-window-creation` 会引入额外的跨线程请求队列、同步等待和相关调度逻辑,因此会带来一定的性能和体积开销。 对于稍微大一些的 GUI 程序来说,这部分开销通常可以忽略不计。但对于非常小的工具类程序,尤其是所有窗口都只在主线程创建的程序,这些额外逻辑并不是必需的,体积和启动路径上的额外代码也更值得控制。 因此 Flor 将它设计为可选 feature:需要跨线程创建窗口能力的程序可以显式开启;不需要该能力的小型程序则可以避免引入额外开销。 ## 平台设计原因 很多平台都对 UI 线程有要求,其中 macOS 明确要求 UI 相关操作运行在主线程。这也是 Flor 采用这套设计的重要原因:框架需要让应用代码可以从业务位置发起创建窗口的请求,同时仍然保证真正的窗口创建发生在正确的 UI / 事件循环线程。 为了兼容这类平台限制,并避免在不同线程直接创建窗口导致未定义行为、消息循环卡住或死锁问题,Flor 将跨线程窗口创建设计为“请求投递到事件循环线程,由事件循环线程实际创建窗口”的模式。 也就是说,`cross-thread-window-creation` 并不是让任意线程真正直接创建窗口,而是让任意线程都可以安全地请求创建窗口,最终窗口仍然由正确的 UI / 事件循环线程完成创建。 如果你的应用所有窗口都在主线程或事件循环线程创建,不需要启用这个 feature。 --- url: /website/zh/guide/control/view-id.md --- # ViewId `ViewId` 是控件在 Flor 运行时里的标识和操作句柄。控件树、布局状态、事件 handler、焦点表、滚动状态、鼠标捕获和渲染资源都通过它关联到同一个控件。 这一页面面向两类场景:在事件回调中操作当前控件,或在自定义控件实现里通过 `self.view_id` 访问运行时能力。只想配置焦点顺序时,优先看 [焦点 Builder](/website/zh/guide/use/builder/focus.md)。 ## 获取 ViewId 事件 builder 回调会把目标控件的 `ViewId` 作为第一个参数传入: ```rust use flor::view::builder::EventBuilder; use flor_lys::label::label; let item = label("点击") .on_click(|view_id, key_state, mouse_position| { println!("{view_id} clicked at {mouse_position:?}"); }); ``` 自定义控件通常把 `ViewId` 存成字段,并在构造函数里创建: ```rust use flor::view::{View, ViewId}; pub struct MyView { view_id: ViewId, } impl MyView { pub fn new() -> Self { Self { view_id: ViewId::new(), } } } impl View for MyView { fn view_id(&self) -> ViewId { self.view_id } } ``` `ViewId::new()` 会注册基础的 `ViewState` 和 `ViewHandler`。`ViewId::new_with_layout(...)` 允许创建时自定义 `LayoutResolver`,一般只在底层控件或布局系统需要接管初始 layout resolver 时使用。 ## 重绘请求 > **这是 Flor 控件开发的最高级别硬规范,请务必遵守。** 无论你的控件是跑在即时模式还是保留模式,只要内部影响视觉的属性发生变更,就必须调用 `view_id.request_redraw()`。 ```rust // 在 on_update_state、on_frame、事件回调或任何修改视觉属性的地方 self.color = new_color; self.view_id.request_redraw(); // 必须调用 ``` Flor 不会自动追踪控件内部字段的变化。如果你修改了颜色、文本、图片句柄、动画状态等任何会影响绘制结果的字段,却不调用 `request_redraw()`,窗口不会重绘,用户就看不到变化。 这条规范适用于: - `on_update_state` 里更新字段后 - `on_frame` 里推进动画状态后 - 事件回调里修改内部状态后 - 任何自定义方法里改变视觉属性后 如果你在实现自定义控件时发现视觉没有更新,首先检查是否遗漏了 `request_redraw()` 调用。控件开发的其他章节会反复强调这一点,例如 [View Trait 的生命周期钩子](/website/zh/guide/control/view-trait.md#生命周期与状态)。 ## 状态与布局 `ViewId` 可以读取当前控件的布局和状态: ```rust let layout = view_id.layout()?; let abs = view_id.abs_location()?; let style = view_id.get_current_style()?; let size = view_id.with_state(|state| state.layout.size)?; view_id.with_state_mut(|state| { state.disable = true; })?; ``` `layout()` 返回 Taffy 计算后的 `taffy::Layout`。`abs_location()` 返回相对窗口左上角的绝对位置,这个值在布局刷新时写入缓存。 `with_state` 和 `with_state_mut` 是闭包式访问接口:它们只在闭包执行期间持有内部状态锁。闭包里不要再做长时间工作,也不要把借用到的状态引用保存出去。 `get_current_style()` 和 `with_current_style(...)` 会按当前 `ControlState` 读取样式。当前状态会影响 resolver 选择 `normal`、`hover`、`focus`、`active` 或 `disabled` 变体。 ## 状态更新 `update_state` 会把任意 `Box` 交给控件实例的 `View::on_update_state`,然后请求重绘: ```rust view_id.update_state(Box::new(String::from("新的标题"))); ``` 控件需要在自己的 `on_update_state` 中 downcast 并更新内部字段。比如文本、图片句柄、样式更新对象这类控件私有状态都适合走这条路径。 如果启用了 `class` feature,`update_class(layer_id, class_str)` 会解析类名,更新 layout resolver,并调用控件的 `on_update_class(control_state, class)`。`z-*` 类会被识别为 z-index 并从 class 列表中移除。 ## 控件树与窗口 `ViewId` 记录控件在树中的关系和所属窗口: ```rust let parent = view_id.parent_view_id(); let window = view_id.window_id(); parent_view_id.push_view(Box::new(child)); ``` `push_view` 会把子控件加入当前控件,重建子树窗口归属,并触发父控件的 `on_child_push`。应用层通常通过 `ViewBuilder::views`、`ViewBuilder::push_view` 或 `views![]` 宏组织树;直接调用 `ViewId::push_view` 更适合运行时追加子控件。 ## 焦点操控 焦点机制的用户侧说明见 [焦点机制](/website/zh/guide/use/focus.md)。这里列出 `ViewId` 上和焦点相关的运行时方法。 ```rust impl ViewId { pub fn update_focus_index(self, focus_index: Option); pub fn set_focus(self, virtual_index: Option); pub fn is_focused(self) -> bool; pub fn push_focus_scope(self); pub fn pop_focus_scope(self); } ``` ### update\_focus\_index `update_focus_index` 在运行时更新控件是否参与焦点表。 ```rust view_id.update_focus_index(Some(0)); // 加入或更新焦点表,排序值为 0 view_id.update_focus_index(Some(10)); // 加入或更新焦点表,排序值为 10 view_id.update_focus_index(None); // 从焦点表移除 ``` `Some(0)` 是合法排序值。`None` 才表示退出焦点系统。 当前实现按单焦点条目更新:它会先移除这个 `ViewId` 已有的焦点条目,再在 `Some(index)` 时插入 `(index, view_id, 0)`。如果控件开发者实现了多个虚拟焦点,初始化焦点表会按 `on_focus_count()` 展开;运行时 `update_focus_index` 目前只插入虚拟焦点 `0`。 ### set\_focus `set_focus` 设置或取消当前焦点。 ```rust view_id.set_focus(Some(0)); // 聚焦到第一个虚拟焦点 view_id.set_focus(Some(1)); // 聚焦到第二个虚拟焦点 view_id.set_focus(None); // 如果当前焦点在这个 ViewId 上,就取消它 ``` `set_focus(Some(index))` 要求这个 `(ViewId, virtual_index)` 已经存在于焦点表。没有设置 `focus_index` 的控件不会被成功聚焦。传入不存在的虚拟焦点序号时,方法不会改变当前焦点。 `set_focus(None)` 只清除这个 `ViewId` 当前持有的焦点:如果当前焦点在它身上,会触发 `blur` 并让窗口进入无当前焦点状态;如果当前焦点在别的控件上,不会清掉别的控件。 ### is\_focused `is_focused` 判断当前焦点是否在这个 `ViewId` 上,不区分虚拟焦点序号。 ```rust if view_id.is_focused() { println!("focused"); } ``` ### push\_focus\_scope 与 pop\_focus\_scope 这两个方法用于运行时焦点作用域。打开 Modal、Popup、侧边栏时,把根控件压入作用域;关闭时弹出作用域。 ```rust dialog_root_id.push_focus_scope(); // 关闭弹出层时 dialog_root_id.pop_focus_scope(); ``` 压入作用域后,Tab 和 Shift+Tab 只在这个根控件的子树里循环。`pop_focus_scope` 会弹出当前作用域,并尝试恢复进入作用域之前的焦点。 ## 控件状态 `ViewId` 可以判断控件的运行时交互状态: ```rust let state = view_id.control_state(); let focused = view_id.is_focused(); let hovered = view_id.is_hover(); let active = view_id.is_active(); ``` `control_state()` 的优先级是 `Disabled > Active > Focus > Hover > Normal`。`control_state_with_pressed(pressed)` 允许控件用一个临时按下状态计算 `ControlState`,但它只考虑 `Disabled`、传入的 `pressed`、`Hover` 和 `Normal`。 `is_active()` 查询的是框架记录的 pressed 状态。`is_hover()` 查询的是所属窗口当前的 hover 目标。 ## 可见性与层级 `z_index` 控制同一父节点下的绘制和命中顺序: ```rust view_id.set_z_index(10); let z = view_id.z_index(); ``` `set_z_index()` 会更新存储里的 z-index,并在有父节点时重新排序同级子节点。 `visual()` 返回当前控件是否在最近一次绘制中被标记为可见。框架内部会用它跳过不可见控件的 `on_frame`,自定义控件一般只需要知道:这个值来自绘制阶段的可见性缓存,不是布局样式的完整替代。 ## 滚动 滚动能力由控件注册 `ScrollState` 后生效: ```rust if view_id.is_scroll_view() { let current = view_id.scroll_offset(); let max = view_id.max_scroll_offset(); } view_id.scroll_to(0.0, 120.0); view_id.scroll_by(0.0, 32.0); view_id.scroll_to_top(); view_id.scroll_to_bottom(); ``` `scroll_to` 和 `scroll_by` 会把目标值钳制到 `0.0..=max`。如果这个 `ViewId` 没有注册滚动状态,这些方法不会改变任何东西。滚动位置改变时会请求重绘。 ## 鼠标捕获 拖拽、滚动条拖动、按住鼠标后继续跟踪移动时,可以捕获鼠标: ```rust view_id.capture_mouse()?; view_id.release_mouse()?; ``` 捕获后,窗口会记录 `capture_view_id`,平台层也会进入鼠标捕获状态。释放后,事件回到普通命中测试路径。 ## Transform 与坐标转换 声明式 transform 会影响控件自身和子控件的绘制、布局后的累积变换以及命中测试: ```rust use flor::types::Transform2D; view_id.set_transform(Transform2D::translation(12.0, 0.0)); let transform = view_id.get_transform(); view_id.clear_transform(); ``` `set_transform` 和 `clear_transform` 会请求重绘。布局刷新后,框架会计算每个控件的 accumulated transform。需要把窗口坐标转换为控件局部坐标时,可以用: ```rust let local = view_id.window_to_local_position(mouse_position); ``` 如果没有累积变换,或变换不可逆,这个方法会返回原始坐标。 ## 渲染资源加载 `ViewId` 实现了 `LoadRenderResource`,可以用所属窗口的 renderer 创建图片资源: ```rust use flor::render::LoadRenderResource; let image = view_id.load_image(bytes)?; ``` `load_raw_image` 支持原始帧数据和帧延迟。启用 `svg` feature 后还可以调用 `load_svg`。如果当前 `ViewId` 找不到对应 renderer,这些方法会返回 `FlorRendererError::RenderNotFound`。 --- url: /website/zh/guide/control/view-trait.md --- # View Trait `View` 是 Flor 的控件实现接口。一个具体控件通过它提供自己的 `ViewId`,并按需覆盖测量、绘制、命中测试、焦点、输入事件、tooltip、拖放和状态更新钩子。 应用侧组合界面时通常使用独立控件库(例如 `flor_lys`)提供的控件,再配合 builder 做配置。只有在编写新控件、封装底层绘制逻辑或实现复杂交互时,才需要直接实现 `View`。 控件作者应该把注意力放在 `on_*` 方法上。除了必须实现的 `view_id()`,以及临时调试用的 `tag()`,不要重写 `call_*`、`bus_*`、`visual_rect()` 这类非 `on_*` 派发方法;它们是框架内部调度层。 ## 最小控件 每个控件必须保存自己的 `ViewId`。`view_id()` 只返回这个稳定字段,不要在 `view_id()` 里重新创建 ID。 ```rust use flor::view::{View, ViewId}; pub struct Spacer { view_id: ViewId, } impl Spacer { pub fn new() -> Self { Self { view_id: ViewId::new(), } } } impl View for Spacer { fn view_id(&self) -> ViewId { self.view_id } } ``` 默认控件不会绘制内容,测量结果是 `Size::ZERO`,命中测试按布局矩形判断。实际控件通常会覆盖 `on_draw`;是否覆盖 `on_measure` 取决于这个控件是否需要向布局系统声明自己的自然尺寸。 `tag()` 默认返回 `"View"`。它目前主要是调试时临时识别控件类型的入口,后续可能调整或删除,不要把业务逻辑建立在 `tag()` 上。 ## 绘制与测量 Flor 使用 Taffy 计算布局。布局阶段会调用 `on_measure`,绘制阶段会调用 `on_draw`。 > `on_measure` 不是所有控件的必需能力。如果你希望控件支持响应式布局、内容自适应、按文字或图片计算自然尺寸,就需要实现测量,让框架知道控件需要多大的空间。如果是类 MFC 的纯固定布局场景,由调用方手动指定控件宽高,也可以不实现测量,直接依赖布局样式里的固定尺寸。 ```rust use flor::base::graphics::RenderContext; use flor::error::Error; use flor::render::FlorRenderer; use flor::taffy::{AvailableSpace, Size, Style}; use flor::types::Color; use flor::view::resolver::ComputedLayout; use flor::view::{ControlState, View, ViewId}; pub struct ColorBlock { view_id: ViewId, color: Color, } impl ColorBlock { pub fn new(color: Color) -> Self { Self { view_id: ViewId::new(), color, } } } impl View for ColorBlock { fn view_id(&self) -> ViewId { self.view_id } fn on_measure( &mut self, known_dimensions: Size>, _available_space: Size, _style: &Style, _control_state: ControlState, _render: &mut FlorRenderer, ) -> Result, Error> { Ok(Size { width: known_dimensions.width.unwrap_or(80.0), height: known_dimensions.height.unwrap_or(32.0), }) } fn on_draw( &mut self, render: &mut FlorRenderer, _control_state: ControlState, abs_location: (f32, f32), layout: ComputedLayout, ) -> Result<(), Error> { let brush = render.create_solid_color_brush(self.color, None)?; render.fill_quad( abs_location.0, abs_location.1, layout.size.width, layout.size.height, &brush, None, None, )?; Ok(()) } } ``` `on_measure` 接收 Taffy 传入的已知尺寸、可用空间、当前 `Style`、当前 `ControlState` 和 renderer。文字控件通常会在这里创建 text layout 来测量内容;图片控件通常会根据图片尺寸、可用空间和缩放策略返回需要的尺寸。 `on_draw` 接收 renderer、当前 `ControlState`、窗口坐标系里的绝对位置和计算后的布局。绘制时使用传入的 `abs_location` 和 `layout.size`,不要重新推导窗口坐标。子控件会在当前控件之后绘制;`on_draw_overlay` 会在子控件之后调用,适合滚动条、浮层装饰等覆盖层。 ## 生命周期与状态 常用生命周期钩子包括: | 方法 | 调用时机 | | ------------------ | ------------------------------------------- | | `on_create` | 控件创建流程里调用,外置 `on_create` handler 也会在同一路径执行 | | `on_update_state` | `ViewId::update_state(Box)` 被调用时执行 | | `on_frame` | 帧调度时执行,可返回下一次需要唤醒的等待时间 | | `on_child_push` | 子控件加入后通知父控件 | | `on_child_dispose` | 子控件释放后通知父控件 | ### on\_create `on_create` 在窗口创建流程遍历控件树时调用。框架实际走的是 `call_create`:先激活挂在这个 `ViewId` 上的 pending effect,再调用控件内部 `on_create`,最后调用用户通过 builder 绑定的外置 `on_create` handler。 如果控件初始化依赖窗口、renderer 或已经进入控件树的上下文,可以放在 `on_create`。单纯字段初始化应该放在构造函数里,不要等到 `on_create`。 ### on\_update\_state `on_update_state` 是控件响应式更新的入口,通常和信号系统配合使用。控件库会创建 updater:在 compute 闭包里读取信号,在 on-change 闭包里调用 `ViewId::update_state(Box::new(...))`,最后由控件自己的 `on_update_state` downcast 并更新内部字段。 `flor_lys::label` 和 `flor_lys::button` 都采用这个模式:标题可以是固定值,也可以是读取信号的闭包;信号变化后更新标题并请求重绘。 ```rust use flor::signal::create_updater; use flor::view::builder::StringProp; use flor::view::{View, ViewId}; use std::any::Any; pub struct TitleView { view_id: ViewId, title: String, } impl TitleView { pub fn new(title: P) -> Self { let view_id = ViewId::new(); // 创建响应式更新器:读取信号值,变化时调用 update_state let title = create_updater( move || title.make(), move |value| view_id.update_state(Box::new(value)), ); Self { view_id, title } } } impl View for TitleView { fn view_id(&self) -> ViewId { self.view_id } fn on_update_state(&mut self, state: Box) { // downcast 并更新内部字段 if let Ok(title) = state.downcast::() { self.title = *title; // 注意:ViewId::update_state 已经会在调用后请求重绘, // 所以这里不需要再调用 request_redraw() } } } ``` 如果状态变化会影响自然尺寸,比如文字变化、图片句柄变化或字体样式变化,控件需要清理测量缓存,并在需要时标记所属窗口重新布局。只影响绘制的变化通常只需要请求重绘;`ViewId::update_state` 已经会在调用后请求重绘。 ### on\_frame `on_frame` 用于动画和周期性状态推进。默认返回 `Ok(None)`,表示这个控件没有下一次主动唤醒需求。 返回 `Ok(Some(duration))` 表示控件希望事件循环在不晚于这段时间后再次醒来。框架会遍历可见控件子树,把所有子控件返回的等待时间取最小值,再交给平台等待逻辑。也就是说,一个 GIF 控件返回下一帧剩余时间,输入控件返回光标闪烁间隔,最终会以最短的那个时间作为下一次唤醒目标。 ```rust use flor::error::Error; use flor::view::{View, ViewId}; use std::time::{Duration, Instant}; pub struct CursorView { view_id: ViewId, focused: bool, cursor_visible: bool, last_blink: Instant, } impl View for CursorView { fn view_id(&self) -> ViewId { self.view_id } fn on_frame(&mut self, now: Instant) -> Result, Error> { // 如果没有焦点,不需要动画,返回 None 表示不需要唤醒 if !self.focused { return Ok(None); } const BLINK: Duration = Duration::from_millis(530); // 检查是否到达闪烁时间 if now.duration_since(self.last_blink) >= BLINK { // 切换光标可见状态 self.cursor_visible = !self.cursor_visible; self.last_blink = now; // 重要:视觉状态变化后必须请求重绘 self.view_id.request_redraw(); } // 返回下一次需要唤醒的时间 Ok(Some(BLINK)) } } ``` `flor_lys::image` 的 GIF 帧推进也是同一类用法:根据当前时间算出目标帧,帧变化时请求重绘,并返回距离下一帧的剩余时间。需要注意的是,当前 `bus_frame` 会跳过 `display: none` 和最近一次绘制未标记为可见的控件。 #### on\_frame\_policy `on_frame_policy()` 默认返回 `FramePolicy::VisibleOnly`,当前公开枚举还有 `FramePolicy::Always`。当前调度路径还没有读取这个返回值;`bus_frame` 仍然按 `display != none` 和最近一次绘制得到的 `ViewId::visual()` 可见性缓存决定是否调用 `on_frame`。 ### on\_child\_push `on_child_push` 在子控件加入当前控件后调用。它主要给复合控件使用,例如记录当前有多少子控件、重建子控件索引、刷新内部缓存或同步辅助数据结构。 `div` 也是复合控件类型,只是它本身没有额外的控件逻辑和绘制逻辑;框架已经提供了基础布局能力,所以简单容器可以不覆盖这个方法。 ### on\_child\_dispose `on_child_dispose` 在子控件从当前控件下释放后调用。它和 `on_child_push` 对应,适合清理复合控件维护的子控件计数、缓存、选择状态或命中辅助结构。 这两个方法都发生在框架完成基础父子关系更新之后。控件作者不需要在这里手动写入 `VIEW_STORAGE.child_ids`。 ## class 更新 启用 `class` feature 后,`ViewId::update_class` 会先解析状态前缀,例如 `hover:`、`focus:`、`active:`、`disabled:`,再把控件样式类交给: ```rust fn on_update_class(&mut self, control_state: ControlState, class: &str) -> Result<(), Error>; ``` 控件可以在这里把类名转换成自己的样式更新。布局类仍然由 `LayoutResolver` 处理;`z-*` 会被解析成运行时 z-index,不会进入控件样式解析。 ## 焦点与键盘 焦点表按 `(focus_index, ViewId, virtual_index)` 管理。控件可以覆盖这些方法: | 方法 | 作用 | | ---------------------------------------------- | -------------------------- | | `on_focus_count` | 声明这个控件有多少个虚拟焦点点位,默认 `1` | | `on_virtual_focus_at` | 点击时按鼠标位置决定要聚焦哪个虚拟焦点,默认 `0` | | `on_focus` | 当前控件获得某个虚拟焦点时调用 | | `on_blur` | 当前控件失去某个虚拟焦点时调用 | | `on_key_down` / `on_key_up` | 当前控件持有焦点时接收键盘事件 | | `on_ime_start` / `on_ime_input` / `on_ime_end` | 当前焦点控件接收 IME 输入流程 | `on_focus_count` 的返回值决定初始化焦点表时生成多少条虚拟焦点记录。比如返回 `3` 且这个控件设置了 `focus_index`,焦点表会为它生成虚拟序号 `0`、`1`、`2`。`on_virtual_focus_at` 则用于把一次点击映射到其中某个虚拟焦点。 键盘方法返回 `HandleResult`。内部 `on_key_*` 和外置 handler 的返回值会合并:任意一方返回 `Handled`,最终就是 `Handled`。 完整焦点机制见 [焦点机制](/website/zh/guide/use/focus.md)。运行时焦点 API 见 [ViewId](/website/zh/guide/control/view-id.md)。 ## 鼠标与滚轮事件 鼠标事件通常在命中测试后派发给目标控件: | 方法 | 说明 | | ------------------------------------------------------------------ | ------------- | | `on_mouse_enter` / `on_mouse_move` / `on_mouse_leave` | 鼠标进入、移动、离开 | | `on_button_down` / `on_button_up` / `on_click` / `on_double_click` | 左键按下、抬起、点击、双击 | | `on_right_button_*` | 右键事件 | | `on_middle_button_*` | 中键事件 | | `on_wheel_scroll_lines_changed` | 滚轮行滚动事件 | 多数事件的 `call_*` 默认实现会先执行控件内部 `on_*`,再执行用户通过 builder 绑定的外置 handler。控件作者覆盖 `on_*`;`call_*` 是框架派发层,不要在控件实现里重写。 ## Tooltip 与拖放 Tooltip 有独立的覆盖模式: ```rust fn on_tooltip_show(&mut self, key_state: KeyState, mouse_position: MousePosition) -> Result<(), Error>; fn on_tooltip_hide(&mut self) -> Result<(), Error>; ``` 如果用户绑定了 tooltip handler,框架只执行外置 handler;没有绑定时才调用控件的 `on_tooltip_show` 或 `on_tooltip_hide`。 拖放方法受 `drag-drop` feature 控制: | 方法 | 返回 | | --------------- | --------------------------- | | `on_drag_enter` | `Result` | | `on_drag_over` | `Result` | | `on_drag_leave` | `Result<(), Error>` | | `on_drop` | `Result` | `drag_enter`、`drag_over` 和 `drop` 的外置 handler 可以通过 `&mut DropEffect` 修改最终效果。 ## 命中测试 命中测试是给异型视觉控件提供的能力,绝大多数控件都不需要重写。默认实现已经能处理矩形布局框的命中判断。 命中测试从窗口坐标开始,框架会用 accumulated transform 把鼠标位置转换为每个控件的局部坐标。传给下面两个方法的 `mouse_position` 都是控件局部坐标: ```rust fn on_hit_test_overlay(&self, mouse_position: MousePosition, key_state: KeyState) -> bool; fn on_hit_test(&self, mouse_position: MousePosition, key_state: KeyState) -> bool; ``` 每个节点的调用顺序是: 1. 如果控件未被最近一次绘制标记为可见、`display: none`,或鼠标不在父级裁剪区域内,跳过这个分支。 2. 先调用当前控件的 `on_hit_test_overlay`。它命中时直接返回当前控件,不再检查子节点。 3. 如果 overlay 没命中,再按反向绘制顺序检查子控件。后绘制的子控件先测试,符合上层元素优先命中的直觉。 4. 子控件都没命中时,最后调用当前控件的 `on_hit_test`。 `on_hit_test_overlay` 用于"覆盖在子控件之上"的区域,比如滚动条、拖拽调整手柄、浮动按钮。默认实现会根据布局里的 scrollbar size 判断滚动条区域。 `on_hit_test` 用于控件主体区域。默认实现判断点是否在 `(0, 0, width, height)` 布局矩形内。只有需要圆形按钮、不规则路径、透明区域穿透、扩大点击热区时,才需要覆盖这个方法。 命中测试只决定事件目标。控件的可见区域裁剪、绘制是否发生、焦点是否可获得,都由其他机制决定。 ## 控件状态 Flor 内部继承了控件状态机制,很多方法都会接收 `ControlState` 参数。绘制和测量都会收到这个状态,状态优先级是 `Disabled > Active > Focus > Hover > Normal`。 控件应该用这个状态读取自己的样式 resolver,或者选择不同绘制分支。禁用状态不是 `View` trait 上的控件开发方法。应用侧通过 [禁用 Builder](/website/zh/guide/use/builder/disable.md) 设置 `ViewState.disable`;控件作者只需要正确响应 `ControlState::Disabled`,例如绘制禁用样式、忽略输入或改变命中行为。 ## 可见区域 布局刷新完成后,框架会遍历控件树,调用每个控件的 `visual_rect()` 并缓存到 `VIEW_STORAGE.visual_rect`。默认 `visual_rect()` 使用布局边界;它内部会读取 `on_visual_overflow()` 来扩展这个边界。 绘制阶段会读取缓存的 `visual_rect`。如果一个控件的可见矩形和父级裁剪区域没有交集,框架会跳过这个控件,不调用 `on_draw`,也不会把它写入 `VIEW_STORAGE.visual`。如果没有被剔除,框架先写入可见标记,再派发 `on_draw`。 控件作者不要重写 `visual_rect()`,应该覆盖: ```rust fn on_visual_overflow(&self) -> VisualOverflow; ``` 这个方法有两个主要用途: - 控件会画到布局框之外时,扩大可见区域。比如阴影、外发光、focus ring、tooltip 箭头。 - 异形控件或不规则路径控件需要用更准确的 bounds 参与可见性判断,避免明明画面有内容,却因为默认布局框太小而被漏掉可见标记。 `VisualOverflow::Uniform` 适合四周等距扩散,`Custom` 适合带方向偏移的阴影,`Path` 会使用路径 bounds。它影响的是可见性剔除和可见标记,不等于像素级裁剪或命中测试;命中测试仍然应该由 `on_hit_test` / `on_hit_test_overlay` 自己决定。 ## 资源加载 所有 `View: LoadRenderResource` 都可以通过自己的 `view_id()` 找到所属窗口 renderer,并创建渲染资源: ```rust use flor::render::LoadRenderResource; let handle = self.load_image(bytes)?; ``` `load_raw_image` 支持原始帧数据,启用 `svg` feature 后支持 `load_svg`。如果控件还没有关联到 renderer,会返回 `FlorRendererError::RenderNotFound`。 ## VIEW\_STORAGE 如果需要更底层的数据能力访问,可以使用 `VIEW_STORAGE` 全局变量。它存储了控件树、布局状态、可见性缓存、焦点表等运行时数据。大多数控件开发场景不需要直接访问 `VIEW_STORAGE`,优先使用 `ViewId` 提供的方法。 --- url: /website/zh/guide/control/resolver/style-unit.md --- # 样式单位 Flor 的布局系统使用 `Unit` 和 `Length` 类型表示尺寸单位。所有单位在传递给布局引擎前都会转换为像素值。 ```rust use flor::view::resolver::{Unit, Length}; ``` ## Unit `Unit` 是单位类型标记,用于标记数值属于哪种单位系统。 ```rust pub enum Unit { /// 像素,不进行转换 #[default] Px, /// 点,使用窗口 DPI 转换:1pt = dpi / 72px Pt, /// Root em,使用窗口配置的 rem_px 转换 Rem, /// 视口宽度单位,1vw = 客户区宽度的 1% Vw, /// 视口高度单位,1vh = 客户区高度的 1% Vh, } ``` 默认值是 `Unit::Px`。 ### Px(像素) 像素是绝对单位,不进行任何转换。布局引擎直接使用像素值。 ```rust let unit = Unit::Px; ``` ### Pt(点) 点(Point)是印刷单位,使用窗口 DPI 进行转换: ``` 1pt = dpi / 72px ``` 在标准 96 DPI 下,`1pt = 1.333px`,`12pt = 16px`。 ```rust let unit = Unit::Pt; ``` ### Rem(Root em) Rem 是相对于根元素字体大小的单位。Flor 使用窗口配置的 `rem_px` 作为换算基准: ``` 1rem = rem_px px ``` 默认 `rem_px = 16.0`,即 `1rem = 16px`。 ```rust let unit = Unit::Rem; ``` ### Vw(视口宽度) Vw 是相对于视口宽度的单位: ``` 1vw = viewport_width / 100 px ``` 视口宽度等于窗口客户区宽度。`50vw` 表示视口宽度的一半。 ```rust let unit = Unit::Vw; ``` ### Vh(视口高度) Vh 是相对于视口高度的单位: ``` 1vh = viewport_height / 100 px ``` 视口高度等于窗口客户区高度。`50vh` 表示视口高度的一半。 ```rust let unit = Unit::Vh; ``` ## Length `Length` 是带数值的长度类型,将数值和单位绑定在一起。 ```rust pub enum Length { /// 像素值 Px(f32), /// 点值 Pt(f32), /// Root em 值 Rem(f32), /// 视口宽度值 Vw(f32), /// 视口高度值 Vh(f32), } ``` 示例: ```rust let length = Length::Px(16.0); // 16 像素 let length = Length::Rem(1.0); // 1 rem let length = Length::Vw(50.0); // 50% 视口宽度 ``` ## UnitMetrics `UnitMetrics` 存储窗口级别的单位换算参数,每个窗口维护一份实例。 ```rust pub struct UnitMetrics { /// 1rem 对应的像素值,默认 16.0 pub rem_px: AtomicF32, /// 水平 DPI,默认 96.0 pub dpi_x: AtomicF32, /// 垂直 DPI,默认 96.0 pub dpi_y: AtomicF32, /// 视口宽度(客户区宽度,像素) pub viewport_width: AtomicF32, /// 视口高度(客户区高度,像素) pub viewport_height: AtomicF32, } ``` ### 默认值 | 参数 | 默认值 | | ----------------- | ------ | | `rem_px` | 16.0 | | `dpi_x` | 96.0 | | `dpi_y` | 96.0 | | `viewport_width` | 1024.0 | | `viewport_height` | 768.0 | ### 更新时机 这些值在窗口创建时初始化,并在以下时机更新: - 窗口尺寸变化时,更新 `viewport_width` 和 `viewport_height` - DPI 变化时,更新 `dpi_x` 和 `dpi_y` - 用户修改 `WindowOption::rem_px` 时,更新 `rem_px` ## 单位换算公式 | 单位 | 公式 | 示例(默认参数) | | --- | ---------------------------- | ----------------------------- | | Px | 不转换 | `16px` → `16px` | | Pt | `pt * dpi_y / 72` | `12pt` (dpi=96) → `16px` | | Rem | `rem * rem_px` | `1rem` (rem\_px=16) → `16px` | | Vw | `vw * viewport_width / 100` | `50vw` (width=1024) → `512px` | | Vh | `vh * viewport_height / 100` | `50vh` (height=768) → `384px` | ## 使用建议 - **Px**:用于精确尺寸,如边框宽度、图标尺寸 - **Rem**:用于字体大小、间距,便于响应式设计 - **Pt**:用于印刷相关场景,或需要与 CSS pt 保持一致时 - **Vw / Vh**:用于视口相关布局,如全屏容器、居中定位 --- url: /website/zh/guide/control/resolver/resolver-struct.md --- # Resolver 结构体 > 这个页面不需要刻意学习,读过有个印象就够了。控件开发时直接使用 [Resolver 派生宏](/website/zh/guide/control/resolver/resolver-derive.md) 即可。 `Resolver` 是 Flor 框架内部的样式/布局解析器,用于管理状态变体、单位换算和计算缓存。控件作者通常不需要直接操作 `Resolver`,而是通过 [Resolver 派生宏](/website/zh/guide/control/resolver/resolver-derive.md) 自动生成实现。 ```rust use flor::view::resolver::Resolver; ``` ## 概述 `Resolver` 的核心职责: - 存储不同 `ControlState` 下的样式变体 - 通过 `UnitResolver` 进行单位换算 - 缓存计算结果,避免重复计算 - 支持多层级样式叠加(layer) 控件作者只需要知道:`Resolver` 会根据当前控件状态(Normal、Hover、Focus、Active、Disabled)自动选择对应的样式变体,并计算出最终结果。 ## new\_with\_compute\_func `new_with_compute_func` 是 `Resolver` 的构造入口,需要传入 `ViewId` 和计算函数: ```rust pub fn new_with_compute_func(view_id: ViewId, compute_func: F) -> Self ``` 计算函数签名: ```rust F: for<'a> Fn(&UnitResolver, &ResolverComputeMap) -> D ``` 它接收单位解析器和合并后的样式变体,返回计算后的数据。 ### 实例:LayoutResolver `LayoutResolver` 是 `Resolver` 的一个实例,用于计算 Taffy 布局样式: ```rust use flor::view::resolver::LayoutResolver; impl LayoutResolver { pub fn new(view_id: ViewId) -> Self { Self::new_with_compute_func(view_id, computed_layout) } } ``` `computed_layout` 函数和 `LayoutResolver` 类型都由 `#[derive(Resolver)]` 派生宏自动生成。它会遍历所有布局变体,调用 `UnitResolver` 进行单位换算,最终生成 `taffy::Style`。 ## 缓存机制 `Resolver` 内部维护缓存: - `cache_data`:按 `ControlState` 缓存计算结果 - `dirty`:脏标记,用于判断是否需要重新计算 当样式变体变化时,缓存会被清除。获取数据时会自动触发计算。 控件作者不需要关心缓存细节,只需要知道:多次调用 `get_data_clone` 或 `get_data_borrow` 不会重复计算。 ## 使用方式 控件作者不应该手动创建 `Resolver`。正确的方式是: 1. 定义样式枚举,例如 `Layout` 或自定义样式类型 2. 使用 `#[derive(Resolver)]` 派生宏自动生成 `Resolver` 实现 3. 在控件构造函数里调用生成的 `new` 方法 详见 [Resolver 派生宏](/website/zh/guide/control/resolver/resolver-derive.md)。 --- url: /website/zh/guide/control/resolver/resolver-derive.md --- # Resolver 派生宏 `#[derive(Resolver)]` 是 Flor 提供的样式解析派生宏。它为样式枚举自动生成 Resolver 类型、链式方法、状态变体支持和响应式更新能力。 这个宏内置控件状态机制,主要用于控件的样式系统。它可以与 [class 系统](/website/zh/guide/use/builder/class.md) 配合使用,在 `on_update_class` 中解析类名并更新样式。 控件作者只需要定义样式枚举,派生宏会自动生成所有 Resolver 相关代码。 ## 最简用法 定义一个样式枚举,添加 `#[derive(Resolver)]`: ```rust use flor::view::resolver::Resolver; use flor::types::Color; #[derive(Clone, Debug, Resolver)] pub enum LabelStyle { TextColor(Color), FontSize(f32), } ``` 派生宏会自动生成: - `LabelStyleKey` - 样式属性键枚举 - `LabelStyleResolver` - Resolver 类型别名 - `LabelStyleResolverExt` trait - 链式方法(`.text_color(...)`、`.font_size(...)`) - `LabelStyleComputed` - 计算后的样式结构体 - `LabelStyleUpdate` - 响应式更新枚举 - `computed_label_style` 函数 - 将原始样式值映射到 `Computed` 结构体 ## 生成的类型 ### Key 枚举 `{EnumName}Key` 是样式属性键,用于 Resolver 内部查找: ```rust pub enum LabelStyleKey { TextColor, FontSize, } ``` ### Resolver 类型别名 `{EnumName}Resolver` 是完整的 Resolver 类型别名: ```rust pub type LabelStyleResolver = Resolver< LabelStyleKey, LabelStyle, LabelStyleComputed, fn(&UnitResolver, &ResolverComputeMap) -> LabelStyleComputed >; ``` ### Computed 结构体 `{EnumName}Computed` 是计算后的样式结构体,所有字段都是 `Option`: ```rust #[derive(Clone, Debug, Default)] pub struct LabelStyleComputed { pub text_color: Option, pub font_size: Option, } ``` 绘制时读取 computed 结构体: ```rust fn on_draw(&mut self, ...) { let style = self.style_resolver.get_data_clone(ControlState::Normal); let color = style.text_color.unwrap_or(Color::BLACK); let size = style.font_size.unwrap_or(16.0); } ``` ## 链式方法 `{EnumName}ResolverExt` trait 为 Resolver 提供链式方法。方法名是变体名的 snake\_case 形式: ```rust pub trait LabelStyleResolverExt { fn text_color(self, value: Color) -> Self; fn font_size(self, value: f32) -> Self; fn set_text_color(&mut self, value: Color) -> &mut Self; fn set_font_size(&mut self, value: f32) -> &mut Self; } ``` 使用示例: ```rust let resolver = LabelStyleResolver::new(view_id) .text_color(Color::RED) .font_size(14.0); ``` ## 状态变体 Resolver 支持按 `ControlState` 存储不同样式变体。通过 `.normal()`、`.hover()`、`.focus()`、`.active()`、`.disabled()` 切换当前状态: ```rust let resolver = LabelStyleResolver::new(view_id) .normal() .text_color(Color::BLACK) .font_size(16.0) .hover() .text_color(Color::BLUE) .focus() .text_color(Color::RED); ``` 绘制时按当前控件状态读取: ```rust let style = resolver.get_data_clone(control_state); ``` 状态继承规则: - `Hover` 继承 `Normal` 的样式,再覆盖 Hover 变体 - `Focus` 继承 `Normal` 的样式,再覆盖 Focus 变体 - `Active` 继承 `Normal` 和 `Focus` 的样式,再覆盖 Active 变体 - `Disabled` 不继承,只使用 Disabled 变体 ## StyleBuilder 派生宏默认为控件生成 `StyleBuilder` trait 实现,允许应用侧通过 `.style(...)` 方法修改样式: ```rust impl StyleBuilder for Label { fn style(mut self, style_fn: impl Fn(LabelStyleResolver) -> LabelStyleResolver) -> Self { self.style = style_fn(self.style); self } } ``` 应用侧使用: ```rust label("标题") .style(|s| s.text_color(Color::RED).font_size(24.0)); ``` `.style(...)` 方法接收一个函数,函数里可以使用 ResolverExt 的链式方法修改样式。 ## Update 枚举 派生宏默认生成 `{EnumName}Update` 枚举和 `update_view` 方法: ```rust pub enum LabelStyleUpdate { TextColor(ControlState, Color), FontSize(ControlState, f32), } impl LabelStyle { pub fn update_view( resolver: &mut LabelStyleResolver, update: LabelStyleUpdate ) { ... } } ``` `Update` 枚举包含每个样式变体的更新类型,`update_view` 方法会更新 Resolver 中对应状态的样式值。 ## 配置参数 通过 `#[resolver(...)]` 属性配置生成内容: ### update\_view = false 跳过 Update 枚举和 update\_view 方法生成: ```rust #[derive(Clone, Debug, Resolver)] #[resolver(update_view = false)] pub enum Layout { Display(Display), } ``` 适用于不需要响应式更新的样式类型。 ### computed = false 跳过 Computed 结构体生成: ```rust #[derive(Clone, Debug, Resolver)] #[resolver(computed = false, data = taffy::Style)] pub enum Layout { Display(Display), } ``` 适用于直接使用外部类型(如 `taffy::Style`)作为计算结果的场景。 ### computed\_fn = false 跳过 `computed_xxx` 独立函数生成。默认情况下,派生宏会生成一个 `pub fn computed_{enum_snake_name}(...)` 函数,将枚举的原始样式值映射到 `Computed` 结构体: ```rust // 自动生成的函数(以 LabelStyle 为例) pub fn computed_label_style( _unit_resolver: &UnitResolver, variants: &ResolverComputeMap, ) -> LabelStyleComputed { let mut computed = LabelStyleComputed::default(); for (k, v) in variants.iter() { match k { LabelStyleKey::TextColor => { if let LabelStyle::TextColor(val) = v { computed.text_color = Some(val.clone()); } } LabelStyleKey::FontSize => { if let LabelStyle::FontSize(val) = v { computed.font_size = Some(val.clone()); } } _ => {} } } computed } ``` 这个函数会被传入 `new_with_compute_func`: ```rust LabelStyleResolver::new_with_compute_func(view_id, computed_label_style) ``` 当不需要自动生成 compute 函数时,可以关闭: ```rust #[derive(Clone, Debug, Resolver)] #[resolver(computed_fn = false)] pub enum MyStyle { Value(f32), } ``` > **注意**:`computed_fn = false` 通常搭配 `computed = false` 使用(如 Layout),此时既不生成 `Computed` 结构体,也不生成 compute 函数。如果只关闭 `computed_fn` 但保留 `computed`,则需要手动编写 compute 函数。 ### data = Type 指定 Resolver 的数据类型,生成类型别名: ```rust #[derive(Clone, Debug, Resolver)] #[resolver(data = taffy::Style)] pub enum Layout { Display(Display), } ``` 生成的 `LayoutResolver` 会使用 `taffy::Style` 作为 D 泛型。 ### builder = false 跳过 StyleBuilder 生成: ```rust #[derive(Clone, Debug, Resolver)] #[resolver(builder = false)] pub enum InternalStyle { Value(f32), } ``` 适用于内部样式类型,不需要暴露给应用侧 builder。 ### default = false 跳过 Resolver 的 Default 实现: ```rust #[derive(Clone, Debug, Resolver)] #[resolver(default = false)] pub enum CustomStyle { Value(f32), } ``` ## 变体属性 ### skip\_attr 跳过某个变体的所有生成: ```rust #[derive(Clone, Debug, Resolver)] pub enum Style { Color(Color), #[resolver(skip_attr)] InternalDebug(String), // 不生成 Key、方法等 } ``` ### skip\_linkfn 跳过链式方法生成,但保留 Key 和其他内容: ```rust #[derive(Clone, Debug, Resolver)] pub enum Layout { #[resolver(skip_linkfn)] Size(Size), // 不生成 .size() 方法 Display(Display), } ``` ## 控件中的实际使用 以下示例展示 Resolver 在控件中的完整使用流程。 ### 构造函数 控件构造时创建 Resolver,可以传入自定义计算函数: ```rust use flor::view::ViewId; use flor::view::resolver::LabelStyleResolver; pub struct Label { view_id: ViewId, title: String, style: LabelStyleResolver, } impl Label { pub fn new(title: String) -> Self { let view_id = ViewId::new(); Self { view_id, title, // 使用 new_with_compute_func 传入计算函数 style: LabelStyleResolver::new_with_compute_func(view_id, computed_label_style), } } } ``` `computed_label_style` 函数由派生宏自动生成,它会遍历所有样式变体并填充 `LabelStyleComputed`。 ### on\_measure 测量时按当前控件状态读取样式: ```rust fn on_measure( &mut self, known_dimensions: Size>, available_space: Size, _style: &Style, control_state: ControlState, render: &mut FlorRenderer, ) -> Result, Error> { // 获取当前状态的样式 let computed = self.style.get_data_borrow(control_state); // 使用样式值进行测量 let font_size = computed.font_size.unwrap_or(16.0); let font_family = computed.font_family.clone().unwrap_or_default(); // 创建文本格式并测量 let text_format = render.create_text_format(&font_family)?; text_format.set_font_size(font_size); let layout_text = render.create_text_layout(self.title.clone(), bounds, text_format)?; let (width, height) = layout_text.measure_text()?; Ok(Size { width, height }) } ``` `get_data_borrow` 返回 RwLock 的映射守卫,避免克隆开销。如果需要多次访问或跨方法传递,使用 `get_data_clone`。 ### on\_draw 绘制时同样按控件状态读取样式: ```rust fn on_draw( &mut self, render: &mut FlorRenderer, control_state: ControlState, abs_location: (f32, f32), layout: ComputedLayout, ) -> Result<(), Error> { let computed = self.style.get_data_borrow(control_state); // 读取样式值,使用 unwrap_or 提供默认值 let text_color = computed.text_color.unwrap_or(Color::BLACK); let font_size = computed.font_size.unwrap_or(16.0); // 使用样式绘制 let brush = render.create_solid_color_brush(text_color, None)?; // ... } ``` ### on\_update\_class `on_update_class` 是 class 系统的入口。控件在这里解析类名并更新样式: ```rust fn on_update_class(&mut self, control_state: ControlState, class: &str) -> Result<(), Error> { // 1. 切换到当前状态 self.style.switch_control_state(control_state); // 2. 解析类名并设置样式 let class = class.trim(); if let Some(rest) = class.strip_prefix("text-") { // text-red-500 -> 设置颜色 if let Some(color) = parse_color(rest) { self.style.set_text_color(color); } // text-lg -> 设置字号 if let Some(size) = self.style.unit_resolver.parse_tw_font_size(rest) { self.style.set_font_size(size); } } if let Some(rest) = class.strip_prefix("bg-") { // bg-blue-500 -> 设置背景 if let Some(color) = parse_color(rest) { self.style.set_background(Background::Color(color)); } } Ok(()) } ``` 关键步骤: 1. `switch_control_state(control_state)` - 切换 Resolver 到当前状态 2. `set_xxx(...)` - 使用生成的链式方法设置样式值 每个类名匹配成功后直接返回,不匹配则继续尝试其他规则。 ### on\_update\_state 响应式更新时处理 `LabelStyleUpdate`: ```rust fn on_update_state(&mut self, state: Box) { // 尝试处理标题更新 if let Ok(title) = state.downcast::() { self.title = *title; return; } // 尝试处理样式更新 if let Ok(update) = state.downcast::() { // 调用宏生成的 update_view 方法 LabelStyle::update_view(&mut self.style, *update); } } ``` `LabelStyleUpdate` 由派生宏自动生成,包含每个样式变体的更新枚举。`update_view` 方法会更新 Resolver 中对应状态的样式值。 --- url: /website/zh/guide/control/resolver/class-resolve.md --- # 类名解析辅助函数 `flor::view::resolver::shared` 提供类名解析的辅助函数,用于解析状态前缀、颜色、圆角、字重等常见样式类名。 ```rust use flor::view::resolver::shared::*; ``` ## parse\_state\_prefix 解析类名中的状态前缀,返回对应的 `ControlState` 和剩余类名。 ```rust pub fn parse_state_prefix(class: &str) -> (ControlState, &str) ``` ### 支持的前缀 | 前缀 | 返回状态 | | ----------- | ------------------------ | | `hover:` | `ControlState::Hover` | | `focus:` | `ControlState::Focus` | | `active:` | `ControlState::Active` | | `disabled:` | `ControlState::Disabled` | | 无前缀 | `ControlState::Normal` | ### 示例 ```rust let (state, rest) = parse_state_prefix("hover:bg-red-500"); // state = ControlState::Hover, rest = "bg-red-500" let (state, rest) = parse_state_prefix("bg-blue-200"); // state = ControlState::Normal, rest = "bg-blue-200" ``` ## extract\_bracket\_value 提取方括号内的值,用于解析 `[value]` 形式的任意值类名。 ```rust pub fn extract_bracket_value(s: &str) -> Option<&str> ``` ### 参数 | 参数 | 类型 | 说明 | | --- | ------ | ------- | | `s` | `&str` | 待解析的字符串 | ### 返回值 - 如果字符串以 `[` 开头并以 `]` 结尾,返回括号内的内容 - 否则返回 `None` ### 示例 ```rust let val = extract_bracket_value("[#fff]"); // val = Some("#fff") let val = extract_bracket_value("#fff"); // val = None ``` ## parse\_color 解析颜色值,支持关键字、Hex 和 Tailwind 颜色名。 ```rust pub fn parse_color(value: &str) -> Option ``` ### 支持的格式 | 格式 | 示例 | 说明 | | -------- | ------------------------------- | ------------ | | 关键字 | `transparent`, `black`, `white` | 预定义颜色名 | | Hex | `#fff`, `#ffffff` | 3 位或 6 位十六进制 | | 括号 Hex | `[#fff]`, `[#ffffff]` | 方括号包裹的 Hex | | Tailwind | `red-500`, `blue-100` | TW 调色板名 | ### Tailwind 调色板 支持以下调色板,每个调色板包含 50、100、200、300、400、500、600、700、800、900、950 共 11 个色阶: - 灰色系:`slate`, `gray`, `zinc`, `neutral` - 红色系:`red`, `orange`, `amber`, `yellow` - 绿色系:`lime`, `green`, `emerald`, `teal`, `cyan` - 蓝色系:`sky`, `blue`, `indigo` - 紫色系:`violet`, `purple`, `fuchsia`, `pink`, `rose` ### 示例 ```rust let color = parse_color("transparent"); // color = Some(Color::rgba(0, 0, 0, 0)) let color = parse_color("#fff"); // color = Some(Color::from_hex_str("#fff")) let color = parse_color("red-500"); // color = Some(Color::RED_500) let color = parse_color("slate-700"); // color = Some(Color::SLATE_700) ``` ## parse\_tw\_color 解析 Tailwind 颜色名,返回对应的 `Color`。 ```rust pub fn parse_tw_color(color_name: &str, shade: &str) -> Option ``` ### 参数 | 参数 | 类型 | 说明 | | ------------ | ------ | --------------------- | | `color_name` | `&str` | 调色板名称,如 `red`, `blue` | | `shade` | `&str` | 色阶数字,如 `500`, `100` | ### 返回值 - 如果调色板名和色阶都有效,返回对应的 `Color` - 否则返回 `None` ### 示例 ```rust let color = parse_tw_color("red", "500"); // color = Some(Color::RED_500) let color = parse_tw_color("emerald", "700"); // color = Some(Color::EMERALD_700) let color = parse_tw_color("unknown", "500"); // color = None ``` ## parse\_rounded 解析 `rounded-*` 类名,返回圆角值。 ```rust pub fn parse_rounded(class: &str) -> Option ``` ### 支持的类名 | 类名 | 返回值 | 说明 | | --------------- | -------- | ------ | | `rounded-none` | `0.0` | 无圆角 | | `rounded-sm` | `2.0` | 小圆角 | | `rounded` | `4.0` | 默认圆角 | | `rounded-md` | `6.0` | 中等圆角 | | `rounded-lg` | `8.0` | 大圆角 | | `rounded-xl` | `12.0` | 更大圆角 | | `rounded-2xl` | `16.0` | 超大圆角 | | `rounded-3xl` | `24.0` | 巨大圆角 | | `rounded-full` | `9999.0` | 完全圆形 | | `rounded-[Npx]` | `N.0` | 自定义像素值 | | `rounded-[N]` | `N.0` | 自定义数值 | ### 示例 ```rust let radius = parse_rounded("rounded-lg"); // radius = Some(8.0) let radius = parse_rounded("rounded-[12px]"); // radius = Some(12.0) let radius = parse_rounded("rounded-unknown"); // radius = None ``` ## parse\_font\_weight 解析 `font-*` 类名中的字重,返回 `FontWeight`。 ```rust pub fn parse_font_weight(name: &str) -> Option ``` ### 支持的字重名 | 类名 | 返回值 | CSS 字重 | | ----------------- | ------------------------ | ------ | | `font-thin` | `FontWeight::Thin` | 100 | | `font-extralight` | `FontWeight::ExtraLight` | 200 | | `font-light` | `FontWeight::Light` | 300 | | `font-normal` | `FontWeight::Normal` | 400 | | `font-medium` | `FontWeight::Medium` | 500 | | `font-semibold` | `FontWeight::SemiBold` | 600 | | `font-bold` | `FontWeight::Bold` | 700 | | `font-extrabold` | `FontWeight::ExtraBold` | 800 | | `font-black` | `FontWeight::Black` | 900 | ### 参数 | 参数 | 类型 | 说明 | | ------ | ------ | -------------------------------------- | | `name` | `&str` | 字重名称(不含 `font-` 前缀),如 `bold`, `medium` | ### 示例 ```rust let weight = parse_font_weight("bold"); // weight = Some(FontWeight::Bold) let weight = parse_font_weight("semibold"); // weight = Some(FontWeight::SemiBold) let weight = parse_font_weight("unknown"); // weight = None ``` --- url: /website/zh/guide/control/create-widget.md --- # 从零开发一个 Switch 控件 本章我们将从零开始,完整实现一个 `Switch`(开关)控件。通过这个过程,你会掌握控件开发的所有核心环节:定义样式枚举、实现 `View` trait、绘制、测量、支持原子类、响应鼠标事件。 `Switch` 是一个合适的学习案例:它有"开/关"两种状态,需要绘制轨道和滑块,需要响应点击,还能通过原子类定制颜色和尺寸。学完这一章,你就可以按照同样的模式开发自己的控件了。 ## 最终效果预览 ```rust use flor_lys::switch::switch; let enabled = create_signal(false); // 使用原子类定制样式 switch(enabled).class("switch-track-blue switch-size-md"); // 使用 style builder 定制样式 switch(enabled) .style(|s| s .track_color(Color::GREEN) .thumb_color(Color::WHITE) ); ``` *** ## 第一步:创建文件 在你的控件库 crate 中新建文件: ``` flor-lys/crates/flor-lys/src/switch.rs ``` 并在 `lib.rs` 中注册: ```rust pub mod switch; ``` *** ## 第二步:定义样式枚举 使用 `#[derive(Resolver)]` 定义 `Switch` 支持的样式属性。派生宏会自动生成 `SwitchStyleKey`、`SwitchStyleResolver`、`SwitchStyleComputed` 等辅助类型。 ```rust use flor::macros::Resolver; use flor::types::Color; /// Switch 样式枚举 /// /// 每个变体对应一个可配置的样式属性。 /// #[derive(Resolver)] 自动为你生成: /// - SwitchStyleKey — 属性键枚举 /// - SwitchStyleResolver — 解析器类型别名 /// - SwitchStyleComputed — 计算后的样式值结构体 /// - SwitchStyleResolverExt — 链式方法 trait #[derive(Clone, Debug, Resolver)] pub enum SwitchStyle { /// 轨道颜色(开启状态) TrackColor(Color), /// 轨道颜色(关闭状态) TrackOffColor(Color), /// 滑块颜色 ThumbColor(Color), /// 滑块颜色(悬浮状态) ThumbHoverColor(Color), /// 开关宽度 Width(f32), /// 开关高度 Height(f32), /// 圆角半径 CornerRadius(f32), /// 透明度 Opacity(f32), } ``` **要点**: - 每个变体携带一个具体类型,`Resolver` 宏会为每个变体生成对应的链式方法。 - 变体名用 `PascalCase`,生成的方法名会自动转为 `snake_case`(如 `TrackColor` → `.track_color(...)`)。 - 变体中的类型会被包装为 `Option` 放入 `Computed` 结构体。 *** ## 第三步:定义控件结构体 ```rust use flor::view::ViewId; use flor::signal::RwSignal; /// Switch 控件 /// /// 包含一个 bool 信号来控制开/关状态。 /// 所有开关都持有自己的 style resolver。 #[derive(Debug)] pub struct Switch { /// 控件唯一 ID,框架通过它管理控件树 view_id: ViewId, /// 当前开/关状态(由 create_updater 响应式更新) enabled: bool, /// 开/关状态的信号引用(用于写入) enabled_signal: RwSignal, /// 样式解析器 style: SwitchStyleResolver, } ``` **关键设计**: - `enabled: bool` 是当前值的快照,由 `create_updater` 建立的响应式依赖自动保持同步。 - `enabled_signal: RwSignal` 保留信号引用,用于在 `on_button_down` 中写入新值。 - 这种拆分是 Flor 的推荐模式:读取用值,写入用信号;当外部修改信号时,`create_updater` 会自动触发 `on_update_state` 更新 `enabled` 字段。 *** ## 第四步:实现 View trait ### 4.1 必须实现的方法 ```rust use flor::view::{View, ControlState}; impl View for Switch { fn view_id(&self) -> ViewId { // 直接返回结构体里的 view_id,不要在这里创建新 ID self.view_id } fn tag(&self) -> &str { // 调试标识,暂时按控件名返回即可 "Switch" } } ``` ### 4.2 on\_update\_state — 响应式更新 当外部通过信号更新样式或状态时,框架会调用 `on_update_state`。你需要 downcast 并更新内部字段: ```rust use std::any::Any; impl View for Switch { // ... view_id, tag ... fn on_update_state(&mut self, state: Box) { // 先处理 enabled 状态更新(由 create_updater 触发) if let Ok(value) = state.downcast::() { self.enabled = *value; return; } // 处理样式更新(SwitchStyleUpdate 由 Resolver 宏自动生成) if let Ok(update) = state.downcast::() { SwitchStyle::update_view(&mut self.style, *update); } } } ``` > **响应式原理**:构造函数中使用 `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`,尺寸主要由样式决定: ```rust use flor::error::Error; use flor::render::FlorRenderer; use flor::taffy::{AvailableSpace, Size, Style}; impl View for Switch { // ... fn on_measure( &mut self, known_dimensions: Size>, _available_space: Size, _style: &Style, control_state: ControlState, _render: &mut FlorRenderer, ) -> Result, Error> { let computed = self.style.get_data_borrow(control_state); // 默认尺寸 let width = computed.width.unwrap_or(44.0); let height = computed.height.unwrap_or(24.0); Ok(Size { width: known_dimensions.width.unwrap_or(width), height: known_dimensions.height.unwrap_or(height), }) } } ``` **要点**: - `known_dimensions` 是父容器传下来的已知尺寸约束。如果父容器设了固定宽高,这里会收到 `Some(...)`。 - 优先使用 `known_dimensions`,没有时才用控件自己的默认值。 - `self.style.get_data_borrow(control_state)` 获取当前控件状态下的计算样式。 ### 4.4 on\_draw — 绘制 这是控件开发中最核心的方法。`Switch` 需要绘制轨道(背景)和滑块(圆形): ```rust use flor::base::graphics::RenderContext; use flor::view::resolver::ComputedLayout; impl View for Switch { // ... fn on_draw( &mut self, render: &mut FlorRenderer, control_state: ControlState, abs_location: (f32, f32), layout: ComputedLayout, ) -> Result<(), Error> { let x = abs_location.0; let y = abs_location.1; let w = layout.size.width; let h = layout.size.height; let computed = self.style.get_data_borrow(control_state); let is_on = self.enabled; // 轨道颜色:根据开/关状态选择 let track_color = if is_on { computed.track_color.unwrap_or_else(|| { Color::from_hex_str("#3b82f6").unwrap_or_default() }) } else { computed.track_off_color.unwrap_or_else(|| { Color::from_hex_str("#d1d5db").unwrap_or_default() }) }; let corner_radius = computed.corner_radius.unwrap_or(h / 2.0); let opacity = computed.opacity.unwrap_or(1.0); // 绘制轨道 let track_brush = render.create_solid_color_brush( track_color.with_alpha((255.0 * opacity) as u8), None, )?; render.fill_quad(x, y, w, h, &track_brush, Some(corner_radius), None)?; // 计算滑块位置 let thumb_size = h - 4.0; // 上下各留 2px let thumb_x = if is_on { x + w - thumb_size - 3.0 } else { x + 3.0 }; let thumb_y = y + 2.0; // 滑块颜色:悬浮时使用 hover 颜色 let thumb_color = if control_state == ControlState::Hover { computed .thumb_hover_color .unwrap_or_else(|| Color::from_hex_str("#f3f4f6").unwrap_or_default()) } else { computed .thumb_color .unwrap_or_else(|| Color::WHITE) }; let thumb_brush = render.create_solid_color_brush( thumb_color.with_alpha((255.0 * opacity) as u8), None, )?; render.fill_quad( thumb_x, thumb_y, thumb_size, thumb_size, &thumb_brush, Some(thumb_size / 2.0), // 圆形 None, )?; Ok(()) } } ``` **绘制要点**: - 使用传入的 `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")` 设置样式: ```rust use flor::view::resolver::parse_color; impl View for Switch { // ... fn on_update_class(&mut self, control_state: ControlState, class: &str) -> Result<(), Error> { // 切换到对应状态的样式层 self.style.switch_control_state(control_state); let class = class.trim(); // 1. 轨道颜色(开启状态):switch-track-{color} if let Some(rest) = class.strip_prefix("switch-track-") { if let Some(color) = parse_color(rest) { self.style.set_track_color(color); return Ok(()); } } // 2. 轨道颜色(关闭状态):switch-track-off-{color} if let Some(rest) = class.strip_prefix("switch-track-off-") { if let Some(color) = parse_color(rest) { self.style.set_track_off_color(color); return Ok(()); } } // 3. 滑块颜色:switch-thumb-{color} if let Some(rest) = class.strip_prefix("switch-thumb-") { if let Some(color) = parse_color(rest) { self.style.set_thumb_color(color); return Ok(()); } } // 4. 尺寸:switch-size-{sm|md|lg} match class { "switch-size-sm" => { self.style.set_width(36.0); self.style.set_height(20.0); return Ok(()); } "switch-size-md" => { self.style.set_width(44.0); self.style.set_height(24.0); return Ok(()); } "switch-size-lg" => { self.style.set_width(52.0); self.style.set_height(28.0); return Ok(()); } _ => {} } Ok(()) } } ``` **原子类解析要点**: - 先用 `switch_control_state(control_state)` 切换到当前状态层,这样后续的 `set_*` 调用会写入正确的状态。 - 用 `strip_prefix` 匹配类名前缀,然后用 `parse_color` 等共享解析方法。 - 解析成功后直接 `return Ok(())`,不要让后续规则意外匹配。 - 不认识的类名直接忽略,`on_update_class` 会被多次调用,每次只处理一个类名。 ### 4.6 on\_button\_down — 响应点击 `Switch` 被点击时切换状态: ```rust use flor::base::platform::{HandleResult, KeyState, MousePosition}; impl View for Switch { // ... fn on_button_down( &mut self, key_state: KeyState, mouse_position: MousePosition, ) -> Result { // 切换开关状态:通过信号写入,会触发 create_updater 回调 self.enabled_signal.set(!self.enabled); Ok(HandleResult::Handled) } } ``` > 这里也可以用 `on_click`。区别在于 `on_click` 要求 down 和 up 命中同一个控件,`on_button_down` 在按下时立即触发。对于开关,`on_button_down` 体验更即时。 *** ## 第五步:构造函数与工厂函数 ```rust use flor::signal::{RwSignal, create_updater}; use flor::view::resolver::LayoutResolver; use flor::view::builder::ViewBuilder; impl Switch { /// 创建开关控件 pub fn new(enabled: RwSignal) -> Self { // 创建 ViewId,同时创建 LayoutResolver let view_id = ViewId::new_with_layout(|view_id| { LayoutResolver::new(view_id) }); // 建立响应式依赖:当 enabled 信号变化时,自动调用 on_update_state let enabled_value = create_updater( move || enabled.get(), move |v: bool| view_id.update_state(Box::new(v)), ); Self { view_id, enabled: enabled_value, enabled_signal: enabled, // computed_switch_style 由 #[derive(Resolver)] 宏自动生成 style: SwitchStyleResolver::new_with_compute_func(view_id, computed_switch_style), } } } /// 工厂函数:创建 Switch #[inline] pub fn switch(enabled: RwSignal) -> Switch { Switch::new(enabled) } ``` **构造函数要点**: - `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` 结构体。你不需要手动编写它,宏已经为你生成了等效于下面的代码: ```rust // 以下由 #[derive(Resolver)] 自动生成,这里仅展示效果 pub fn computed_switch_style( _unit_resolver: &UnitResolver, variants: &ResolverComputeMap, ) -> SwitchStyleComputed { let mut computed = SwitchStyleComputed::default(); for (k, v) in variants.iter() { match k { SwitchStyleKey::TrackColor => { if let SwitchStyle::TrackColor(val) = v { computed.track_color = Some(val.clone()); } } SwitchStyleKey::TrackOffColor => { if let SwitchStyle::TrackOffColor(val) = v { computed.track_off_color = Some(val.clone()); } } // ... 其他变体同理 _ => {} } } computed } ``` **关键点**: - 宏生成的 compute 函数名规则是 `computed_{EnumName的snake_case}`(如 `LabelStyle` → `computed_label_style`)。 - 它只做简单的 `clone` 映射,不处理单位转换。单位的 px 转换由 `Resoled` 的 `get_data_borrow` 内部处理。 - 如果不需要这个函数,可以通过 `#[resolver(computed_fn = false)]` 关闭。详见 [Resolver 派生宏](/website/zh/guide/control/resolver/resolver-derive.md#computed_fn--false) 文档。 *** ## 完整文件一览 下面是 `switch.rs` 的完整代码: ```rust use flor::base::graphics::RenderContext; use flor::base::platform::{HandleResult, KeyState, MousePosition}; use flor::error::Error; use flor::macros::Resolver; use flor::render::FlorRenderer; use flor::signal::{RwSignal, create_updater}; use flor::taffy::{AvailableSpace, Size, Style}; use flor::types::Color; use flor::view::builder::ViewBuilder; use flor::view::resolver::{ComputedLayout, LayoutResolver, parse_color}; use flor::view::{ControlState, View, ViewId}; use std::any::Any; // ============================================================================ // SwitchStyle 样式枚举 // ============================================================================ #[derive(Clone, Debug, Resolver)] pub enum SwitchStyle { TrackColor(Color), TrackOffColor(Color), ThumbColor(Color), ThumbHoverColor(Color), Width(f32), Height(f32), CornerRadius(f32), Opacity(f32), } // ============================================================================ // Switch 结构体 // ============================================================================ #[derive(Debug)] pub struct Switch { view_id: ViewId, /// 当前状态值(由 create_updater 响应式更新) enabled: bool, /// 信号引用(用于写入) enabled_signal: RwSignal, style: SwitchStyleResolver, } // ============================================================================ // View trait 实现 // ============================================================================ impl View for Switch { fn view_id(&self) -> ViewId { self.view_id } fn tag(&self) -> &str { "Switch" } fn on_update_state(&mut self, state: Box) { // 处理 enabled 状态更新(由 create_updater 触发) if let Ok(value) = state.downcast::() { self.enabled = *value; return; } // 处理样式更新 if let Ok(update) = state.downcast::() { SwitchStyle::update_view(&mut self.style, *update); } } fn on_measure( &mut self, known_dimensions: Size>, _available_space: Size, _style: &Style, control_state: ControlState, _render: &mut FlorRenderer, ) -> Result, Error> { let computed = self.style.get_data_borrow(control_state); let width = computed.width.unwrap_or(44.0); let height = computed.height.unwrap_or(24.0); Ok(Size { width: known_dimensions.width.unwrap_or(width), height: known_dimensions.height.unwrap_or(height), }) } fn on_draw( &mut self, render: &mut FlorRenderer, control_state: ControlState, abs_location: (f32, f32), layout: ComputedLayout, ) -> Result<(), Error> { let x = abs_location.0; let y = abs_location.1; let w = layout.size.width; let h = layout.size.height; let computed = self.style.get_data_borrow(control_state); let is_on = self.enabled; let track_color = if is_on { computed .track_color .unwrap_or_else(|| Color::from_hex_str("#3b82f6").unwrap_or_default()) } else { computed .track_off_color .unwrap_or_else(|| Color::from_hex_str("#d1d5db").unwrap_or_default()) }; let corner_radius = computed.corner_radius.unwrap_or(h / 2.0); let opacity = computed.opacity.unwrap_or(1.0); let track_brush = render.create_solid_color_brush( track_color.with_alpha((255.0 * opacity) as u8), None, )?; render.fill_quad(x, y, w, h, &track_brush, Some(corner_radius), None)?; let thumb_size = h - 4.0; let thumb_x = if is_on { x + w - thumb_size - 3.0 } else { x + 3.0 }; let thumb_y = y + 2.0; let thumb_color = if control_state == ControlState::Hover { computed .thumb_hover_color .unwrap_or_else(|| Color::from_hex_str("#f3f4f6").unwrap_or_default()) } else { computed.thumb_color.unwrap_or(Color::WHITE) }; let thumb_brush = render.create_solid_color_brush( thumb_color.with_alpha((255.0 * opacity) as u8), None, )?; render.fill_quad( thumb_x, thumb_y, thumb_size, thumb_size, &thumb_brush, Some(thumb_size / 2.0), None, )?; Ok(()) } fn on_update_class(&mut self, control_state: ControlState, class: &str) -> Result<(), Error> { self.style.switch_control_state(control_state); let class = class.trim(); if let Some(rest) = class.strip_prefix("switch-track-") { if let Some(color) = parse_color(rest) { self.style.set_track_color(color); return Ok(()); } } if let Some(rest) = class.strip_prefix("switch-track-off-") { if let Some(color) = parse_color(rest) { self.style.set_track_off_color(color); return Ok(()); } } if let Some(rest) = class.strip_prefix("switch-thumb-") { if let Some(color) = parse_color(rest) { self.style.set_thumb_color(color); return Ok(()); } } match class { "switch-size-sm" => { self.style.set_width(36.0); self.style.set_height(20.0); return Ok(()); } "switch-size-md" => { self.style.set_width(44.0); self.style.set_height(24.0); return Ok(()); } "switch-size-lg" => { self.style.set_width(52.0); self.style.set_height(28.0); return Ok(()); } _ => {} } Ok(()) } fn on_button_down( &mut self, _key_state: KeyState, _mouse_position: MousePosition, ) -> Result { self.enabled_signal.set(!self.enabled); Ok(HandleResult::Handled) } } // ============================================================================ // 构造函数与工厂函数 // ============================================================================ impl Switch { pub fn new(enabled: RwSignal) -> Self { let view_id = ViewId::new_with_layout(|view_id| LayoutResolver::new(view_id)); let enabled_value = create_updater( move || enabled.get(), move |v: bool| view_id.update_state(Box::new(v)), ); Self { view_id, enabled: enabled_value, enabled_signal: enabled, style: SwitchStyleResolver::new_with_compute_func(view_id, computed_switch_style), } } } #[inline] pub fn switch(enabled: RwSignal) -> Switch { Switch::new(enabled) } ``` *** ## 使用示例 ### 基础用法 ```rust use flor::signal::create_signal; use flor_lys::switch::switch; let notifications = create_signal(false); switch(notifications); ``` ### 配合原子类 ```rust switch(notifications) .class("switch-track-blue switch-size-lg"); ``` ### 配合信号联动 ```rust let dark_mode = create_signal(false); // 其他控件可以读取 dark_mode 信号来响应 let bg_color = move || { if dark_mode.get() { Color::from_hex_str("#1a1a2e").unwrap() } else { Color::WHITE } }; let panel = div(views![ label("深色模式"), switch(dark_mode).class("switch-track-purple"), ]) .class("flex items-center gap-3 p-4"); ``` ### 配合 style builder ```rust switch(dark_mode) .style(|s| s .track_color(Color::GREEN) .track_off_color(Color::GRAY) .thumb_color(Color::WHITE) ); ``` *** ## 开发控件 checklist 回顾整个流程,开发一个 Flor 控件需要完成以下步骤: 1. **定义样式枚举** — 使用 `#[derive(Resolver)]` 声明所有可配置的样式属性。 2. **定义控件结构体** — 持有 `view_id`、状态字段和 `style resolver`。 3. **实现 `View` trait**: - `view_id()` — 返回结构体中的 `view_id`。 - `tag()` — 返回调试用的标签名。 - `on_update_state()` — 处理 `update_state` 传入的样式更新。 - `on_measure()` — 有自然尺寸的控件需要实现,返回控件需要的尺寸。 - `on_draw()` — 核心绘制方法,读取 computed 样式和控件状态来绘制。 - `on_update_class()` — 解析原子类字符串,转换为样式更新。 - 按需覆盖鼠标/键盘事件处理(`on_button_down`、`on_click` 等)。 4. **编写构造函数** — 创建 `ViewId`、`LayoutResolver`、`StyleResolver`,使用 `create_updater` 建立响应式依赖,`computed_xxx` 函数由宏自动生成。 5. **编写工厂函数** — 提供简洁的 API 入口。 *** ## 进阶话题 ### 添加动画 如果你想给滑块添加平移动画,可以在 `on_frame` 中实现: ```rust fn on_frame(&mut self, now: Instant) -> Result, Error> { let target_x = if self.enabled { /* 开启位置 */ } else { /* 关闭位置 */ }; // 计算当前过渡位置 // 如果位置还在变化,返回 Some(Duration) 请求下一帧 // 如果已经到达目标,返回 None } ``` ### 支持禁用状态 在 `on_draw` 中检查 `ControlState::Disabled`,绘制灰色轨道和半透明滑块。在 `on_button_down` 中检查 `self.view_id.control_state() == ControlState::Disabled` 并忽略点击。 ### 扩大点击热区 如果你的滑块太小不好点,覆盖 `on_hit_test` 来扩大命中区域: ```rust fn on_hit_test(&self, mouse_position: MousePosition, _key_state: KeyState) -> bool { // 在布局矩形的基础上扩大 4px let padding = 4.0; mouse_position.x >= -padding && mouse_position.x <= self.layout_width() + padding && mouse_position.y >= -padding && mouse_position.y <= self.layout_height() + padding } ``` --- url: /website/zh/api/index.md --- # API 概览 ## Class 语法解析基础 ### [Class 语法解析基础](/website/zh/api/class-syntax.md) - [入口](/website/zh/api/class-syntax.md#入口) - [拆分规则](/website/zh/api/class-syntax.md#拆分规则) - [状态前缀](/website/zh/api/class-syntax.md#状态前缀) - [任意值语法](/website/zh/api/class-syntax.md#任意值语法) - [长度值](/website/zh/api/class-syntax.md#长度值) - [关键字](/website/zh/api/class-syntax.md#关键字) - [颜色值](/website/zh/api/class-syntax.md#颜色值) - [共享样式辅助语法](/website/zh/api/class-syntax.md#共享样式辅助语法) - [处理路径](/website/zh/api/class-syntax.md#处理路径) ## Layout class 语法 ### [Layout class 语法](/website/zh/api/layout-class.md) - [值语法](/website/zh/api/layout-class.md#值语法) - [Display](/website/zh/api/layout-class.md#display) - [Position](/website/zh/api/layout-class.md#position) - [Box Sizing](/website/zh/api/layout-class.md#box-sizing) - [Z Index](/website/zh/api/layout-class.md#z-index) - [Overflow](/website/zh/api/layout-class.md#overflow) - [Size](/website/zh/api/layout-class.md#size) - [Padding](/website/zh/api/layout-class.md#padding) - [Margin](/website/zh/api/layout-class.md#margin) - [Inset](/website/zh/api/layout-class.md#inset) - [Border Width](/website/zh/api/layout-class.md#border-width) - [Gap](/website/zh/api/layout-class.md#gap) - [Flex](/website/zh/api/layout-class.md#flex) - [Flex / Grid 共享对齐](/website/zh/api/layout-class.md#flex--grid-共享对齐) - [Grid](/website/zh/api/layout-class.md#grid) - [Block Text Align](/website/zh/api/layout-class.md#block-text-align) - [Aspect Ratio](/website/zh/api/layout-class.md#aspect-ratio) - [Scrollbar Width](/website/zh/api/layout-class.md#scrollbar-width) - [状态前缀](/website/zh/api/layout-class.md#状态前缀) - [合并行为](/website/zh/api/layout-class.md#合并行为) - [当前不支持](/website/zh/api/layout-class.md#当前不支持) ## 信号 API ### [信号 API](/website/zh/api/signal.md) - [`Signal`](/website/zh/api/signal.md#signal) - [`Read`](/website/zh/api/signal.md#readt) - [`Write`](/website/zh/api/signal.md#writet) - [`RwSignal`](/website/zh/api/signal.md#rwsignalt) - [`ListRead`](/website/zh/api/signal.md#listreadt) - [`ListWrite`](/website/zh/api/signal.md#listwritet) - [创建函数](/website/zh/api/signal.md#创建函数) ## View 转换 API ### [View 转换 API](/website/zh/api/view.md) - [导入](/website/zh/api/view.md#导入) - [`ViewBox`](/website/zh/api/view.md#viewbox) - [`ViewIdentity`](/website/zh/api/view.md#viewidentity) - [`IntoView`](/website/zh/api/view.md#intoview) - [`IntoViewIter`](/website/zh/api/view.md#intoviewiter) - [`ViewBuilder`](/website/zh/api/view.md#viewbuilder) - [宏](/website/zh/api/view.md#宏) ## 方法Handler API ### [方法Handler API](/website/zh/api/handler.md) - [导入](/website/zh/api/handler.md#导入) - [`EventBuilder`](/website/zh/api/handler.md#eventbuilder) - [`IntoEventHandler`](/website/zh/api/handler.md#intoeventhandler) - [当前派发状态](/website/zh/api/handler.md#当前派发状态) - [`ViewHandler`](/website/zh/api/handler.md#viewhandler) - [Handler 包装类型](/website/zh/api/handler.md#handler-包装类型) - [类型别名](/website/zh/api/handler.md#类型别名) - [参数说明](/website/zh/api/handler.md#参数说明) --- url: /website/zh/api/class-syntax.md --- # class 语法解析基础 本页是 `.class(...)` 字符串的通用解析规则。使用教程见 [原子类支持](/website/zh/guide/use/builder/class.md),layout class 的完整清单见 [layout class 语法](/website/zh/api/layout-class.md)。 ## 入口 ```rust use flor::view::builder::ClassBuilder; view.class("flex gap-2 p-4"); ``` `ClassBuilder` 需要启用 `class` feature。公开入口只有一个: ```rust pub trait ClassBuilder { fn class(self, class_str: M) -> Self; } ``` `M` 需要实现 `ClassProp`。当前常用输入包括: | 输入 | 示例 | | | | -------------------------- | ------------------------------------ | - | ------------------------------------------------------------ | | `&'static str` | `.class("flex gap-2")` | | | | `String` | `.class(format!("w-[{}px]", width))` | | | | `Fn() -> String + 'static` | \`.class(move | | if open.get() { "block".into() } else { "hidden".into() })\` | 闭包形式会被响应式更新器重新求值,适合 class 字符串依赖 signal 的场景。 ## 拆分规则 传入的字符串会按空白字符拆分: ```text "flex flex-col gap-2 p-4" ``` 会被拆成: ```text flex flex-col gap-2 p-4 ``` 不存在引号转义或保留空格的 class token。如果值里需要特殊字符,用方括号任意值语法。 ## 状态前缀 class token 可以带一个状态前缀: | 前缀 | 状态 | | ----------- | -------- | | 无前缀 | Normal | | `hover:` | Hover | | `focus:` | Focus | | `active:` | Active | | `disabled:` | Disabled | 示例: ```rust .class("p-2 hover:p-4 focus:bg-blue-50 disabled:opacity-50") ``` 状态前缀只解析一层。`md:hover:p-4`、`hover:focus:p-4` 这类组合前缀当前不会按多个状态解析。 ## 任意值语法 很多 class 支持方括号写法: ```text w-[320px] rounded-[10px] text-[#0f172a] aspect-[16/9] ``` 解析时会去掉最外层 `[` 和 `]`,再按对应属性的值语法解析。方括号不是所有 class 都支持,具体以对应 class 文档或控件实现为准。 ## 长度值 layout class 和部分控件样式 class 会用统一的长度解析。 | 写法 | 示例 | 说明 | | ----- | -------- | ----------------------------------------------------------------- | | 裸数字 | `4` | 按 Tailwind 间距习惯解析,`4` 等于 `1rem`。默认 `1rem = 16px` 时,`4` 等于 `16px`。 | | `px` | `12px` | 像素。 | | `rem` | `1.5rem` | 使用当前窗口的 `WindowOption::rem_px`,默认 `16.0`。 | | `pt` | `12pt` | 使用当前窗口 DPI,公式是 `1pt = dpi / 72 px`。 | | `vw` | `50vw` | 当前窗口客户区宽度的百分比。 | | `vh` | `50vh` | 当前窗口客户区高度的百分比。 | | 百分比 | `50%` | 按百分比解析。 | | 分数 | `1/2` | 转成百分比。 | 示例: ```text p-4 w-1/2 h-[240px] mt-[1rem] max-h-[80vh] ``` ## 关键字 不同属性支持的关键字不同。常见关键字包括: | 关键字 | 常见用途 | | --------------------- | -------------------------------- | | `auto` | `Dimension::Auto` 或 auto margin。 | | `full` | 100%。 | | `screen` | 当前窗口客户区宽度或高度。 | | `fit` / `min` / `max` | 当前尺寸解析中会作为 auto 处理。 | 布局 class 对这些关键字的支持范围,见 [layout class 语法](/website/zh/api/layout-class.md)。控件样式 class 对这些关键字的支持范围,见对应控件库对具体控件的文档。 ## 颜色值 控件样式类如果使用 Flor 的共享颜色解析,通常支持: | 写法 | 示例 | | ----------- | -------------------------------- | | 关键字 | `transparent`、`black`、`white` | | 十六进制 | `#fff`、`#ffffff` | | 方括号十六进制 | `[#0f172a]` | | Tailwind 色板 | `slate-900`、`blue-600`、`red-500` | 颜色解析是否用于某个 class,由具体控件决定。例如 `text-blue-600`、`bg-slate-100` 是否生效,要看当前控件是否实现了对应样式 class。 ## 共享样式辅助语法 部分控件会复用这些解析函数: | 语法 | 示例 | 说明 | | --------------- | ------------------------------------------------------------------------------------------------------------- | ------- | | `rounded` | `rounded` | 4px 圆角。 | | `rounded-*` | `rounded-none`、`rounded-sm`、`rounded-md`、`rounded-lg`、`rounded-xl`、`rounded-2xl`、`rounded-3xl`、`rounded-full` | 常用圆角档位。 | | `rounded-[Npx]` | `rounded-[10px]` | 自定义圆角。 | | `font-*` | `font-thin`、`font-light`、`font-normal`、`font-medium`、`font-semibold`、`font-bold`、`font-black` | 字重。 | 这些是共享解析能力,不代表所有控件都自动支持。控件必须在自己的 class 更新逻辑中使用这些解析结果。 ## 处理路径 `.class(...)` 传入的类名会经过这些用户可见的处理: 1. 按空白拆分成多个 class token。 2. `z-*` 会作为控件层级处理。 3. 其他 token 会按状态前缀拆成状态和实际 class 名。 4. 实际 class 名会交给控件的样式解析逻辑。 5. 同一批 class 也会交给 layout class 解析逻辑。 layout class 的具体支持范围见 [layout class 语法](/website/zh/api/layout-class.md)。控件样式 class 的具体支持范围由控件文档或控件源码决定。 --- url: /website/zh/api/layout-class.md --- # layout class 语法 本页列出 Flor 内置 layout class 的当前支持范围。使用 `.class(...)` 的教程见 [原子类支持](/website/zh/guide/use/builder/class.md),通用 class 字符串解析规则见 [class 语法解析基础](/website/zh/api/class-syntax.md)。 layout class 由 Flor 统一解析,对所有控件可用。Flex、Grid、Block 相关类受 feature 控制。 ## 值语法 下表里的 `*` 代表一个值。通用值语法见 [class 语法解析基础](/website/zh/api/class-syntax.md#长度值)。 | 写法 | 示例 | 说明 | | --- | -------------------------------------------- | -------------------------------- | | 裸数字 | `4` | 按 Tailwind 间距习惯解析,默认 `4 = 16px`。 | | 带单位 | `[12px]`、`[1rem]`、`[12pt]`、`[50vw]`、`[50vh]` | 支持 `px`、`rem`、`pt`、`vw`、`vh`。 | | 百分比 | `50%`、`[50%]` | 转为百分比。 | | 分数 | `1/2`、`1/3` | 转为百分比。 | | 关键字 | `auto`、`full`、`screen` | 按具体属性解释。 | ## Display | 类名 | 效果 | feature | | -------- | ---------------- | -------------- | | `hidden` | `Display::None` | 无 | | `flex` | `Display::Flex` | `layout-flex` | | `grid` | `Display::Grid` | `layout-grid` | | `block` | `Display::Block` | `layout-block` | ## Position | 类名 | 效果 | | ---------- | -------------------- | | `relative` | `Position::Relative` | | `absolute` | `Position::Absolute` | 当前不支持 `fixed`、`sticky`。 ## Box Sizing | 类名 | 效果 | | ------------- | ----------------------- | | `box-border` | `BoxSizing::BorderBox` | | `box-content` | `BoxSizing::ContentBox` | ## Z Index `z-*` 在 `.class(...)` 入口里作为层级处理。 | 类名 | 效果 | | -------- | ---------------- | | `z-auto` | z-index 设为 `0`。 | | `z-10` | z-index 设为 `10`。 | | `z--1` | z-index 设为 `-1`。 | `z-*` 当前不走状态前缀解析;例如 `hover:z-10` 不会作为 hover 层级处理。 ## Overflow | 类名 | 效果 | | ------------------------------------------------------------------------------------ | --------------------------- | | `overflow-visible` | x/y 都为 `Overflow::Visible`。 | | `overflow-hidden` | x/y 都为 `Overflow::Hidden`。 | | `overflow-clip` | x/y 都为 `Overflow::Clip`。 | | `overflow-scroll` | x/y 都为 `Overflow::Scroll`。 | | `overflow-x-visible` / `overflow-x-hidden` / `overflow-x-clip` / `overflow-x-scroll` | 只设置 x 方向。 | | `overflow-y-visible` / `overflow-y-hidden` / `overflow-y-clip` / `overflow-y-scroll` | 只设置 y 方向。 | ## Size | 类名模式 | 效果 | 支持值 | | --------------------------------------- | ----------- | ------------------------------------------------ | | `w-*` | 宽度 | `auto`、`full`、`fit`、`min`、`max`、数字、分数、百分比、任意长度值。 | | `h-*` | 高度 | 同 `w-*`。 | | `size-*` | 同时设置宽高 | 同 `w-*`。 | | `w-screen` / `h-screen` / `size-screen` | 使用当前窗口客户区尺寸 | `screen`。 | | `min-w-*` / `min-h-*` | 最小宽度 / 最小高度 | 同 `w-*`,并支持 `screen`。 | | `max-w-*` / `max-h-*` | 最大宽度 / 最大高度 | 同 `w-*`,并支持 `screen`。 | 示例: ```text w-full h-auto w-1/2 min-w-[240px] max-h-[80vh] size-10 ``` ## Padding | 类名模式 | 效果 | | ------ | ------ | | `p-*` | 四边内边距。 | | `px-*` | 左右内边距。 | | `py-*` | 上下内边距。 | | `pl-*` | 左内边距。 | | `pr-*` | 右内边距。 | | `pt-*` | 上内边距。 | | `pb-*` | 下内边距。 | 支持数字、百分比、分数、`full` 和任意长度值。 ```text p-4 px-2 pt-[10px] pl-1/2 ``` ## Margin | 类名模式 | 效果 | | ------ | ------ | | `m-*` | 四边外边距。 | | `mx-*` | 左右外边距。 | | `my-*` | 上下外边距。 | | `ml-*` | 左外边距。 | | `mr-*` | 右外边距。 | | `mt-*` | 上外边距。 | | `mb-*` | 下外边距。 | 支持 `auto`、数字、百分比、分数、`full` 和任意长度值。 ```text m-4 mx-auto mt-2 mb-[20px] ``` ## Inset Inset 类通常和 `absolute` 或 `relative` 一起使用。 | 类名模式 | 效果 | | ----------- | ----- | | `inset-*` | 四边偏移。 | | `inset-x-*` | 左右偏移。 | | `inset-y-*` | 上下偏移。 | | `left-*` | 左偏移。 | | `right-*` | 右偏移。 | | `top-*` | 上偏移。 | | `bottom-*` | 下偏移。 | 支持 `auto`、数字、百分比、分数、`full` 和任意长度值。 ```text inset-0 top-4 left-1/2 right-[10px] ``` ## Border Width 这里的 border class 只设置布局层面的边框厚度,不设置边框颜色或圆角。 | 类名模式 | 效果 | | --------------------------------------------------------- | ---------- | | `border` | 四边厚度为 1px。 | | `border-*` | 四边厚度。 | | `border-x-*` / `border-y-*` | 左右 / 上下厚度。 | | `border-l-*` / `border-r-*` / `border-t-*` / `border-b-*` | 单边厚度。 | ```text border border-2 border-b-[1px] ``` ## Gap 需要 `layout-flex` 或 `layout-grid` feature。 | 类名模式 | 效果 | | --------- | -------- | | `gap-*` | 两个方向的间距。 | | `gap-x-*` | x 方向间距。 | | `gap-y-*` | y 方向间距。 | ```text gap-4 gap-x-2 gap-y-[10px] ``` ## Flex 需要 `layout-flex` feature。 | 类名 | 效果 | | --------------------------- | ----------------------------------- | | `flex-row` | `FlexDirection::Row` | | `flex-row-reverse` | `FlexDirection::RowReverse` | | `flex-col` | `FlexDirection::Column` | | `flex-col-reverse` | `FlexDirection::ColumnReverse` | | `flex-wrap` | `FlexWrap::Wrap` | | `flex-wrap-reverse` | `FlexWrap::WrapReverse` | | `flex-nowrap` | `FlexWrap::NoWrap` | | `flex-1` | `grow: 1`、`shrink: 1`、`basis: 0%` | | `flex-auto` | `grow: 1`、`shrink: 1`、`basis: auto` | | `flex-initial` | `grow: 0`、`shrink: 1`、`basis: auto` | | `flex-none` | `grow: 0`、`shrink: 0`、`basis: auto` | | `grow` | `FlexGrow(1.0)` | | `grow-*` / `grow-[1.5]` | 自定义 grow 值。 | | `shrink` | `FlexShrink(1.0)` | | `shrink-*` / `shrink-[0.5]` | 自定义 shrink 值。 | | `basis-*` | `FlexBasis`,值按尺寸值解析。 | ## Flex / Grid 共享对齐 需要 `layout-flex` 或 `layout-grid` feature。 | 类名 | 效果 | | ----------------- | ------------------------------ | | `items-start` | `AlignItems::Start` | | `items-end` | `AlignItems::End` | | `items-center` | `AlignItems::Center` | | `items-baseline` | `AlignItems::Baseline` | | `items-stretch` | `AlignItems::Stretch` | | `self-start` | `AlignSelf::Start` | | `self-end` | `AlignSelf::End` | | `self-center` | `AlignSelf::Center` | | `self-baseline` | `AlignSelf::Baseline` | | `self-stretch` | `AlignSelf::Stretch` | | `justify-start` | `JustifyContent::Start` | | `justify-end` | `JustifyContent::End` | | `justify-center` | `JustifyContent::Center` | | `justify-between` | `JustifyContent::SpaceBetween` | | `justify-around` | `JustifyContent::SpaceAround` | | `justify-evenly` | `JustifyContent::SpaceEvenly` | | `justify-stretch` | `JustifyContent::Stretch` | | `content-start` | `AlignContent::Start` | | `content-end` | `AlignContent::End` | | `content-center` | `AlignContent::Center` | | `content-between` | `AlignContent::SpaceBetween` | | `content-around` | `AlignContent::SpaceAround` | | `content-evenly` | `AlignContent::SpaceEvenly` | | `content-stretch` | `AlignContent::Stretch` | ## Grid 需要 `layout-grid` feature。 | 类名 | 效果 | | --------------------------- | --------------------------- | | `grid-flow-row` | `GridAutoFlow::Row` | | `grid-flow-col` | `GridAutoFlow::Column` | | `grid-flow-row-dense` | `GridAutoFlow::RowDense` | | `grid-flow-col-dense` | `GridAutoFlow::ColumnDense` | | `row-start-*` / `row-end-*` | 设置 grid 行起止线,值为整数。 | | `col-start-*` / `col-end-*` | 设置 grid 列起止线,值为整数。 | | `row-span-*` | 设置行 span,值为正整数。 | | `col-span-*` | 设置列 span,值为正整数。 | | `justify-items-start` | `JustifyItems::Start` | | `justify-items-end` | `JustifyItems::End` | | `justify-items-center` | `JustifyItems::Center` | | `justify-items-stretch` | `JustifyItems::Stretch` | | `justify-self-start` | `JustifySelf::Start` | | `justify-self-end` | `JustifySelf::End` | | `justify-self-center` | `JustifySelf::Center` | | `justify-self-stretch` | `JustifySelf::Stretch` | 当前不支持 `grid-cols-*`、`grid-rows-*`、`auto-cols-*`、`auto-rows-*`。 ## Block Text Align 需要 `layout-block` feature。 | 类名 | 效果 | | ------------- | ------------------------- | | `text-left` | `TextAlign::LegacyLeft` | | `text-center` | `TextAlign::LegacyCenter` | | `text-right` | `TextAlign::LegacyRight` | 注意:这些是 block layout 的文本/行内对齐类。具体控件也可能把 `text-*` 解析为控件样式,例如文本颜色或字号。 ## Aspect Ratio | 类名 | 效果 | | --------------- | ------ | | `aspect-square` | 1:1。 | | `aspect-video` | 16:9。 | | `aspect-[N]` | 自定义比例。 | | `aspect-[N/M]` | 分数比例。 | ```text aspect-square aspect-video aspect-[4/3] ``` ## Scrollbar Width | 类名模式 | 效果 | | ------------- | --------------------------- | | `scrollbar-*` | 设置 `ScrollbarWidth`,值使用 px。 | ```text scrollbar-0 scrollbar-4 scrollbar-[10px] ``` ## 状态前缀 layout class 支持通用状态前缀: ```text p-4 hover:p-6 focus:p-8 ``` 状态前缀规则见 [class 语法解析基础](/website/zh/api/class-syntax.md#状态前缀)。 `z-*` 是例外:它在进入状态前缀解析前被作为层级处理,因此当前不支持 `hover:z-10` 这类写法。 ## 合并行为 同一批 class 会按声明顺序解析: ```text p-4 pl-2 pt-1 ``` 结果是 left 被 `pl-2` 覆盖,top 被 `pt-1` 覆盖,其他边保留 `p-4`。 ```text p-4 p-8 ``` 结果是 `p-8` 覆盖 `p-4`。 多次调用 `.class(...)` 会创建多层配置;后创建的层在同一布局属性上覆盖前面的层。 ## 当前不支持 以下是常见但当前 layout class 没有实现的语法: | 语法 | 状态 | | ------------------------------------------------ | ---------------------------- | | `fixed`、`sticky` | 不支持。 | | `grid-cols-*`、`grid-rows-*` | 不支持。 | | `auto-cols-*`、`auto-rows-*` | 不支持。 | | `order-*` | 不支持。 | | `place-content-*`、`place-items-*`、`place-self-*` | 不支持。 | | `space-x-*`、`space-y-*` | 不支持。 | | 响应式前缀 `sm:`、`md:`、`lg:` | 不支持。 | | 负值前缀 `-m-4`、`-top-2` | 不支持;需要负值时优先使用 builder 或补充实现。 | --- url: /website/zh/api/signal.md --- # Signal API 本页用于查询 `flor::signal` 的主要公开 API。使用教程见 [Signal 响应式系统](/website/zh/guide/use/signal.md)。 ## `Signal` ```rust trait Signal { fn id(&self) -> Id; fn exists(&self) -> bool; fn destroy(&self); } ``` ## `Read` ```rust trait Read: Signal { fn track(&self); fn try_get(&self) -> Option where T: Clone + 'static; fn get(&self) -> T where T: Clone + 'static; fn get_ref(&self) -> SignalRef<'_, T>; fn try_get_ref(&self) -> Option>; } ``` ## `Write` ```rust trait Write: Signal { fn set(&self, new_value: T) where T: 'static; fn try_set(&self, new_value: T) -> bool where T: 'static; fn update(&self, f: impl FnOnce(&mut T)) where T: 'static; fn try_update(&self, f: impl FnOnce(&mut T)) -> bool where T: 'static; } ``` ## `RwSignal` `RwSignal` 通常由 `create_signal` 或 `create_signal_with_label` 返回。业务代码一般不需要根据 `Id` 手动构造信号句柄。 ```rust impl RwSignal { fn split(self) -> (ReadSignal, WriteSignal); fn as_read(&self) -> ReadSignal; fn as_write(&self) -> WriteSignal; fn set_label(&self, label: &str); } ``` ## `ListRead` ```rust trait ListRead: Signal { fn track(&self); fn len(&self) -> Option; fn len_or_zero(&self) -> usize; fn is_empty(&self) -> bool; fn contains(&self, value: &T) -> bool where T: PartialEq + 'static; fn try_contains(&self, value: &T) -> Option where T: PartialEq + 'static; fn try_get(&self, index: usize) -> Option where T: Clone + 'static; fn get(&self, index: usize) -> T where T: Clone + 'static; fn to_vec(&self) -> Vec where T: Clone + 'static; fn try_to_vec(&self) -> Option> where T: Clone + 'static; fn for_each_ref(&self, f: F) -> Option<()> where F: FnMut(&T), T: 'static; fn try_borrow(&self) -> Option> where T: 'static; } ``` ## `ListWrite` ```rust trait ListWrite: Signal { fn track(&self); fn push(&self, value: T) where T: 'static; fn try_push(&self, value: T) -> bool where T: 'static; fn set(&self, index: usize, value: T) where T: 'static; fn try_set(&self, index: usize, value: T) -> bool where T: 'static; fn insert(&self, index: usize, value: T) where T: 'static; fn try_insert(&self, index: usize, value: T) -> bool where T: 'static; fn remove(&self, index: usize) -> T where T: 'static; fn try_remove(&self, index: usize) -> Option where T: 'static; fn clear(&self); fn try_clear(&self) -> bool; fn update(&self, index: usize, f: impl FnOnce(&mut T)) where T: 'static; fn try_update(&self, index: usize, f: impl FnOnce(&mut T)) -> bool where T: 'static; } ``` ## 创建函数 ```rust fn create_signal(value: T) -> RwSignal; fn create_rw_signal(value: T) -> (ReadSignal, WriteSignal); fn create_signal_with_label(value: T, label: &str) -> RwSignal; fn create_rw_signal_with_label( value: T, label: &str, ) -> (ReadSignal, WriteSignal); fn create_list_signal(value: Vec) -> RwListSignal; fn create_rw_list_signal( value: Vec, ) -> (ReadListSignal, WriteListSignal); fn create_list_signal_with_label( value: Vec, label: &str, ) -> RwListSignal; fn create_rw_list_signal_with_label( value: Vec, label: &str, ) -> (ReadListSignal, WriteListSignal); fn create_effect(f: impl Fn(Option) -> T + 'static) where T: Any + 'static; fn create_updater( compute: impl Fn() -> R + 'static, on_change: impl Fn(R) + 'static, ) -> R where R: 'static; fn create_updater_with_id( compute: impl Fn() -> R + 'static, on_change: impl Fn(R) + 'static, ) -> (Id, R) where R: 'static; fn batch(f: impl Fn()); ``` --- url: /website/zh/api/view.md --- # View 转换 API 本页用于查询 `flor::view` 中和控件身份、通用控件对象、子控件序列相关的公开 API。使用教程见 [框架 DSL](/website/zh/guide/use/framework-dsl.md)。 ## 导入 ```rust use flor::view::{ IntoView, IntoViewIter, View, ViewBox, ViewIdentity, }; use flor::view::builder::ViewBuilder; ``` ## `ViewBox` `ViewBox` 是框架内部和容器 API 常用的通用控件对象: ```rust pub type ViewBox = Box; ``` `view!(...)` 宏会把单个控件转换成 `ViewBox`,`views![...]` 宏会生成 `Vec`。 ## `ViewIdentity` `ViewIdentity` 是 builder 读取控件身份的抽象: ```rust pub trait ViewIdentity { fn identity(&self) -> ViewId; } ``` 当前 `View` 和 `ViewBox` 都实现了 `ViewIdentity`。因此基于 `ViewIdentity` 的 builder 不只可以用于具体控件,也可以用于通用控件对象,以及返回类型写成 `impl IntoView` 的控件封装。 常用的 `ClassBuilder`、`LayoutBuilder`、`EventBuilder`、`FocusIndexBuilder`、`DisableBuilder`、`TransformBuilder`、`ZIndexBuilder` 都通过 `ViewIdentity` 获取目标控件。 ## `IntoView` `IntoView` 用于把单个控件转换成 `ViewBox`: ```rust pub trait IntoView: ViewIdentity + Send + Sync + 'static { fn into_view(self) -> ViewBox; } ``` 实现情况: | 类型 | 行为 | | --------------------------------- | ------------------------------------------- | | `T: View + Send + Sync + 'static` | 包装成 `Box` | | `ViewBox` | 原样返回 | `IntoView` 继承 `ViewIdentity`。如果一个函数返回 `impl IntoView`,调用方仍然可以继续接常用 builder: ```rust use flor::view::IntoView; use flor::view::builder::{ClassBuilder, EventBuilder}; use flor_lys::button::button; fn action(text: &'static str) -> impl IntoView { button(text).class("px-2") } let save = action("保存") .class("font-bold") .on_click(|| { println!("save"); }); ``` ## `IntoViewIter` `IntoViewIter` 用于把一个值转换成 `ViewBox` 迭代器,主要给容器子节点、窗口根视图和 `ViewBuilder::views` 使用: ```rust pub trait IntoViewIter { type Iter: Iterator; fn into_view_iter(self) -> Self::Iter; } ``` 实现情况: | 类型 | 行为 | | -------------- | ---------------------- | | `T: IntoView` | 转成只包含一个 `ViewBox` 的迭代器 | | `Vec` | 直接消费这个列表 | 这意味着接收 `impl IntoViewIter` 的 API 可以同时接收单个控件和 `views![...]` 生成的控件列表: ```rust use flor::views; use flor_lys::button::button; use flor_lys::div::div; use flor_lys::label::label; let single = div(label("只有一个子控件")); let multiple = div(views![ label("标题"), button("确认"), ]); ``` ## `ViewBuilder` `ViewBuilder` 给已有控件追加子控件: ```rust pub trait ViewBuilder { fn views(self, views: impl IntoViewIter) -> Self; fn push_view(self, view: impl IntoView) -> Self; } ``` `views(...)` 接收子控件序列;因为单个控件也实现 `IntoViewIter`,所以它可以接收单个控件或 `views![...]` 列表。`push_view(...)` 用于追加一个子控件。 ```rust use flor::view::builder::ViewBuilder; use flor::views; use flor_lys::button::button; use flor_lys::div::div; use flor_lys::label::label; let panel = div(views![label("标题")]) .push_view(button("保存")) .views(label("补充说明")); ``` 如果是在创建容器时传入初始子控件,优先使用容器构造函数或 `views![...]`。`ViewBuilder` 更适合在已经拿到父控件值以后追加子控件。 ## 宏 | 宏 | 返回值 | 作用 | | ----------------- | -------------- | -------------- | | `view!(control)` | `ViewBox` | 把单个控件装箱成通用控件对象 | | `views![a, b, c]` | `Vec` | 构造多个子控件组成的列表 | 宏内部调用的是 `IntoView::into_view(...)`,因此每个元素都需要实现 `IntoView`。 --- url: /website/zh/api/handler.md --- # Handler API 本页用于查询 `flor::view::handler` 和 `flor::view::builder::EventBuilder` 的主要公开 API。使用教程见 [外置事件](/website/zh/guide/use/handler.md)。 ## 导入 ```rust use flor::view::builder::EventBuilder; use flor::view::handler::*; use flor::view::ViewId; use flor::base::platform::{ HandleResult, KeyCode, KeyState, MousePosition, ScrollAxis, }; ``` 启用拖放或主题 feature 后,还会用到这些类型: ```rust use flor::base::platform::{DragData, DragFormat, DropEffect}; use flor::base::platform::ThemeMode; ``` ## `EventBuilder` `EventBuilder` 对所有 `V: ViewIdentity` 实现。所有方法都会保存 handler 并返回 `Self`,用于链式调用。普通控件、`ViewBox`、以及函数返回的 `impl IntoView` 都可以继续使用这些方法。 ```rust pub trait EventBuilder { fn on_mouse_move(self, handler: impl IntoEventHandler) -> Self; fn on_double_click(self, handler: impl IntoEventHandler) -> Self; fn on_click(self, handler: impl IntoEventHandler) -> Self; fn on_button_down(self, handler: impl IntoEventHandler) -> Self; fn on_button_up(self, handler: impl IntoEventHandler) -> Self; fn on_right_button_double_click( self, handler: impl IntoEventHandler, ) -> Self; fn on_right_button_click(self, handler: impl IntoEventHandler) -> Self; fn on_right_button_down(self, handler: impl IntoEventHandler) -> Self; fn on_right_button_up(self, handler: impl IntoEventHandler) -> Self; fn on_middle_button_double_click( self, handler: impl IntoEventHandler, ) -> Self; fn on_middle_button_down(self, handler: impl IntoEventHandler) -> Self; fn on_middle_button_up(self, handler: impl IntoEventHandler) -> Self; fn on_context_menu(self, handler: impl IntoEventHandler) -> Self; fn on_key_down(self, handler: impl IntoEventHandler) -> Self; fn on_key_up(self, handler: impl IntoEventHandler) -> Self; fn on_mouse_enter(self, handler: impl IntoEventHandler) -> Self; fn on_mouse_leave(self, handler: impl IntoEventHandler) -> Self; fn on_focus(self, handler: impl IntoEventHandler) -> Self; fn on_blur(self, handler: impl IntoEventHandler) -> Self; fn on_create(self, handler: impl IntoEventHandler) -> Self; fn on_destroy(self, handler: impl IntoEventHandler) -> Self; fn on_resize(self, handler: impl IntoEventHandler) -> Self; fn on_close_requested(self, handler: impl IntoEventHandler) -> Self; fn on_work_area_changed(self, handler: impl IntoEventHandler) -> Self; fn on_wheel_settings_changed( self, handler: impl IntoEventHandler, ) -> Self; fn on_dpi_change(self, handler: impl IntoEventHandler) -> Self; #[cfg(feature = "theme-change")] fn on_theme_changed(self, handler: impl IntoEventHandler) -> Self; #[cfg(feature = "drag-drop")] fn on_drag_enter(self, handler: impl IntoEventHandler) -> Self; #[cfg(feature = "drag-drop")] fn on_drag_over(self, handler: impl IntoEventHandler) -> Self; #[cfg(feature = "drag-drop")] fn on_drag_leave(self, handler: impl IntoEventHandler) -> Self; #[cfg(feature = "drag-drop")] fn on_drop(self, handler: impl IntoEventHandler) -> Self; } ``` ## `IntoEventHandler` `IntoEventHandler` 是事件 builder 接收 handler 的转换入口。它把闭包、普通函数、关联函数、方法项或已经确定泛型参数的泛型函数,转换成目标 handler 包装类型。 ```rust pub trait IntoEventHandler { fn into_event_handler(self) -> T; } ``` 转换分两层: | 层级 | 机制 | | ---- | ---------------------------------------------------------------------------------------- | | 完整参数 | handler 包装类型实现 `From`,`IntoEventHandler` 再通过 `F: Into` 调用这个 `From` 转换 | | 简化参数 | `IntoEventHandler` 针对无参数、只接收 `ViewId`、省略 `ViewId` 等形态补齐被省略的参数 | 所以完整参数形态是 `From` 驱动的转换,事件 Builder 再用 `IntoEventHandler` 把不同参数形态统一到同一个入口。`Args` 是编译期标记,用来区分参数形态;调用 `.on_click(...)`、`.on_key_down(...)` 时通常由 Rust 自动推导,不需要手写。 例如 `on_click` 的底层类型是 `MouseHandler`,以下四种写法都可以转换成 `OnClickHandler`: ```rust view.on_click(|view_id, key_state, mouse_position| { /* 完整参数 */ }); view.on_click(|| { /* 不关心事件参数 */ }); view.on_click(|view_id| { /* 只关心控件 */ }); view.on_click(|key_state, mouse_position| { /* 省略 ViewId */ }); ``` 只有 `ViewId` 一个完整参数的 `Handler` 别名事件,例如 `on_mouse_enter`、`on_mouse_leave`、`on_create`、`on_destroy`、`on_resize`,没有“省略 `ViewId` 后继续接收事件数据”的形态,因此只支持完整参数和无参数。 函数和方法项只要最终满足对应的 `Fn(...) + Send + Sync + 'static` 签名,也会参与同一套转换: ```rust fn clicked(view_id: ViewId) { println!("{view_id}"); } struct Actions; impl Actions { fn open(view_id: ViewId) { println!("open {SLOT}: {view_id}"); } } view.on_click(clicked); view.on_click(Actions::open::<1>); ``` 如果你要在自己的控件封装里继续接收并转发 handler,不要退回到 `impl Into`。应保留 `Args` 泛型,这样调用方仍然可以使用完整参数、无参数、只接收 `ViewId`、省略 `ViewId` 这几种形态: ```rust fn accept_click(handler: impl IntoEventHandler) -> OnClickHandler { handler.into_event_handler() } ``` ## 当前派发状态 | API | 当前状态 | | -------------------------------------------------------------- | ------------------------------------------ | | `on_mouse_move` | 已派发。目标是捕获鼠标的控件或当前 hover 控件 | | `on_mouse_enter` / `on_mouse_leave` | 已派发。hover 目标变化时触发 | | `on_button_down` / `on_button_up` | 已派发。左键 down/up | | `on_click` | 已派发。左键 down/up 命中同一控件时合成 | | `on_double_click` | 已派发。左键双击 | | `on_right_button_down` / `on_right_button_up` | 已派发。右键 down/up | | `on_right_button_click` | 已派发。右键 down/up 命中同一控件时合成 | | `on_right_button_double_click` | 已派发。右键双击 | | `on_middle_button_down` / `on_middle_button_up` | 已派发。中键 down/up | | `on_middle_button_double_click` | 已派发。中键双击 | | `on_key_down` / `on_key_up` | 已派发。目标是当前焦点控件 | | `on_focus` / `on_blur` | 已派发。由焦点管理器触发 | | `on_create` | 已派发。窗口创建控件树后触发 | | `on_wheel_settings_changed` | 已派发。当前鼠标滚轮消息会走这个 handler | | `on_drag_enter` / `on_drag_over` / `on_drag_leave` / `on_drop` | 已派发,需要 `drag-drop` feature | | `on_context_menu` | 有绑定槽位,当前源码没有外置派发入口 | | `on_destroy` | 有绑定槽位,当前源码没有外置派发入口 | | `on_resize` | 有绑定槽位,当前 Resize 只更新布局和渲染尺寸,没有调用外置 handler | | `on_close_requested` | 有绑定槽位,当前关闭请求没有调用外置 handler | | `on_work_area_changed` | 有绑定槽位,当前窗口总线实现为空 | | `on_dpi_change` | 有绑定槽位,当前 DPI 消息只更新渲染器、单位和布局,没有调用外置 handler | | `on_theme_changed` | 有绑定槽位,需要 `theme-change` feature,当前窗口总线实现为空 | ## `ViewHandler` `ViewHandler` 是每个 `ViewId` 对应的一组 handler 槽位。应用代码通常通过 `EventBuilder` 写入这些槽位,不需要直接操作 `ViewHandler`。 ```rust #[derive(Default)] pub struct ViewHandler { pub on_mouse_move_handler: Option, pub on_double_click_handler: Option, pub on_click_handler: Option, pub on_button_down_handler: Option, pub on_button_up_handler: Option, pub on_right_button_double_click_handler: Option, pub on_right_button_click_handler: Option, pub on_right_button_down_handler: Option, pub on_right_button_up_handler: Option, pub on_middle_button_double_click_handler: Option, pub on_middle_button_down_handler: Option, pub on_middle_button_up_handler: Option, pub on_context_menu_handler: Option, pub on_key_down_handler: Option, pub on_key_up_handler: Option, pub on_mouse_enter_handler: Option, pub on_mouse_leave_handler: Option, pub on_focus_handler: Option, pub on_blur_handler: Option, pub on_create_handler: Option, pub on_destroy_handler: Option, pub on_tooltip_show_handler: Option, pub on_tooltip_hide_handler: Option, pub on_resize_handler: Option, pub on_close_requested_handler: Option, pub on_work_area_changed_handler: Option, pub on_wheel_settings_changed_handler: Option, pub on_dpi_change_handler: Option, #[cfg(feature = "theme-change")] pub on_theme_changed_handler: Option, #[cfg(feature = "drag-drop")] pub on_drag_enter_handler: Option, #[cfg(feature = "drag-drop")] pub on_drag_over_handler: Option, #[cfg(feature = "drag-drop")] pub on_drag_leave_handler: Option, #[cfg(feature = "drag-drop")] pub on_drop_handler: Option, } ``` ## Handler 包装类型 所有 handler 包装类型都实现了完整参数签名的 `From`,其中 `F` 是对应签名的闭包或函数,并满足 `Send + Sync + 'static`。事件 builder 还通过 `IntoEventHandler` 支持若干简化参数形态,因此用户侧通常直接传闭包,不需要手动构造包装类型。 ### `Handler` ```rust pub struct Handler(pub Arc); ``` 用于只需要 `ViewId` 的事件。 可接收: | 形态 | 示例 | | ---- | --------------------- | | 完整参数 | `\|view_id\| { ... }` | | 无参数 | `\|\| { ... }` | ### `MouseHandler` ```rust pub struct MouseHandler( pub Arc, ); ``` 用于鼠标移动、点击、按下、松开、双击等事件。 可接收: | 形态 | 示例 | | ------------ | ------------------------------------------------ | | 完整参数 | `\|view_id, key_state, mouse_position\| { ... }` | | 无参数 | `\|\| { ... }` | | 只接收 `ViewId` | `\|view_id\| { ... }` | | 省略 `ViewId` | `\|key_state, mouse_position\| { ... }` | ### `KeyHandler` ```rust pub struct KeyHandler( pub Arc< dyn Fn(ViewId, KeyCode, bool, bool, bool) -> HandleResult + Send + Sync + 'static, >, ); ``` 参数依次是 `ViewId`、`KeyCode`、`is_alt`、`is_ctrl`、`is_shift`。 可接收: | 形态 | 示例 | | ------------ | ---------------------------------------------------------------------- | | 完整参数 | `\|view_id, code, is_alt, is_ctrl, is_shift\| -> HandleResult { ... }` | | 无参数 | `\|\| -> HandleResult { ... }` | | 只接收 `ViewId` | `\|view_id\| -> HandleResult { ... }` | | 省略 `ViewId` | `\|code, is_alt, is_ctrl, is_shift\| -> HandleResult { ... }` | ### `FocusHandler` ```rust pub struct FocusHandler( pub Arc, ); ``` 第二个参数是虚拟焦点序号。 可接收: | 形态 | 示例 | | ------------ | ---------------------------------- | | 完整参数 | `\|view_id, focus_index\| { ... }` | | 无参数 | `\|\| { ... }` | | 只接收 `ViewId` | `\|view_id\| { ... }` | | 省略 `ViewId` | `\|focus_index\| { ... }` | ### `OnWheelSettingsChangedHandler` ```rust pub struct OnWheelSettingsChangedHandler( pub Arc< dyn Fn(ViewId, ScrollAxis, f32, KeyState, MousePosition) + Send + Sync + 'static, >, ); ``` 参数依次是 `ViewId`、滚动方向、滚动量、按键状态、鼠标位置。 可接收: | 形态 | 示例 | | ------------ | ------------------------------------------------------------- | | 完整参数 | `\|view_id, axis, delta, key_state, mouse_position\| { ... }` | | 无参数 | `\|\| { ... }` | | 只接收 `ViewId` | `\|view_id\| { ... }` | | 省略 `ViewId` | `\|axis, delta, key_state, mouse_position\| { ... }` | ### `OnDpiChangeHandler` ```rust pub struct OnDpiChangeHandler( pub Arc, ); ``` 第二、第三个参数分别是 `dpi_x` 和 `dpi_y`。 可接收: | 形态 | 示例 | | ------------ | ----------------------------------- | | 完整参数 | `\|view_id, dpi_x, dpi_y\| { ... }` | | 无参数 | `\|\| { ... }` | | 只接收 `ViewId` | `\|view_id\| { ... }` | | 省略 `ViewId` | `\|dpi_x, dpi_y\| { ... }` | ### `OnThemeChangedHandler` 需要 `theme-change` feature。 ```rust pub struct OnThemeChangedHandler( pub Arc, ); ``` 可接收: | 形态 | 示例 | | ------------ | --------------------------------- | | 完整参数 | `\|view_id, theme_mode\| { ... }` | | 无参数 | `\|\| { ... }` | | 只接收 `ViewId` | `\|view_id\| { ... }` | | 省略 `ViewId` | `\|theme_mode\| { ... }` | ### `DragEnterOverHandler` 需要 `drag-drop` feature。 ```rust pub struct DragEnterOverHandler( pub Arc< dyn Fn(ViewId, KeyState, MousePosition, &[DragFormat], &mut DropEffect) + Send + Sync + 'static, >, ); ``` 用于 `on_drag_enter` 和 `on_drag_over`。 可接收: | 形态 | 示例 | | ------------ | ----------------------------------------------------------------- | | 完整参数 | `\|view_id, key_state, mouse_position, formats, effect\| { ... }` | | 无参数 | `\|\| { ... }` | | 只接收 `ViewId` | `\|view_id\| { ... }` | | 省略 `ViewId` | `\|key_state, mouse_position, formats, effect\| { ... }` | ### `DropHandler` 需要 `drag-drop` feature。 ```rust pub struct DropHandler( pub Arc< dyn Fn(ViewId, KeyState, MousePosition, &DragData, &mut DropEffect) + Send + Sync + 'static, >, ); ``` 用于 `on_drop`。 可接收: | 形态 | 示例 | | ------------ | -------------------------------------------------------------- | | 完整参数 | `\|view_id, key_state, mouse_position, data, effect\| { ... }` | | 无参数 | `\|\| { ... }` | | 只接收 `ViewId` | `\|view_id\| { ... }` | | 省略 `ViewId` | `\|key_state, mouse_position, data, effect\| { ... }` | ## 类型别名 ### 鼠标事件 | 别名 | 底层类型 | | ---------------------------------- | -------------- | | `OnMouseMoveHandler` | `MouseHandler` | | `OnDoubleClickHandler` | `MouseHandler` | | `OnClickHandler` | `MouseHandler` | | `OnButtonDownHandler` | `MouseHandler` | | `OnButtonUpHandler` | `MouseHandler` | | `OnRightButtonDoubleClickHandler` | `MouseHandler` | | `OnRightButtonClickHandler` | `MouseHandler` | | `OnRightButtonDownHandler` | `MouseHandler` | | `OnRightButtonUpHandler` | `MouseHandler` | | `OnMiddleButtonDoubleClickHandler` | `MouseHandler` | | `OnMiddleButtonDownHandler` | `MouseHandler` | | `OnMiddleButtonUpHandler` | `MouseHandler` | | `OnContextMenuHandler` | `MouseHandler` | 当前没有 `OnMiddleButtonClickHandler`。 ### 键盘事件 | 别名 | 底层类型 | | ------------------ | ------------ | | `OnKeyDownHandler` | `KeyHandler` | | `OnKeyUpHandler` | `KeyHandler` | ### View 与生命周期事件 | 别名 | 底层类型 | | --------------------- | -------------- | | `OnMouseEnterHandler` | `Handler` | | `OnMouseLeaveHandler` | `Handler` | | `OnFocusHandler` | `FocusHandler` | | `OnBlurHandler` | `FocusHandler` | | `OnCreateHandler` | `Handler` | | `OnDestroyHandler` | `Handler` | ### Tooltip 事件 | 别名 | 底层类型 | | ---------------------- | -------------- | | `OnTooltipShowHandler` | `MouseHandler` | | `OnTooltipHideHandler` | `Handler` | 当前 `EventBuilder` 没有公开 tooltip 对应的链式绑定方法。 ### 窗口相关事件 | 别名 | 底层类型 | | ------------------------------- | -------------------------------- | | `OnResizeHandler` | `Handler` | | `OnCloseRequestedHandler` | `Handler` | | `OnWorkAreaChangedHandler` | `Handler` | | `OnWheelSettingsChangedHandler` | 独立包装类型 | | `OnDpiChangeHandler` | 独立包装类型 | | `OnThemeChangedHandler` | 独立包装类型,需要 `theme-change` feature | ### 拖放事件 需要 `drag-drop` feature。 | 别名 | 底层类型 | | -------------------- | ---------------------- | | `OnDragEnterHandler` | `DragEnterOverHandler` | | `OnDragOverHandler` | `DragEnterOverHandler` | | `OnDragLeaveHandler` | `Handler` | | `DropHandler` | 独立包装类型 | ## 参数说明 | 类型 | 说明 | | --------------- | --------------------------------------------------------------------------- | | `ViewId` | 触发事件的目标控件 ID | | `KeyState` | 鼠标按钮和修饰键状态 | | `MousePosition` | 鼠标坐标。常规鼠标命中事件是目标控件局部坐标;滚轮和拖放当前是窗口客户区坐标 | | `KeyCode` | 按键枚举。常见按键有字母、数字、方向键、功能键、`Enter`、`Escape`、`Tab`、`Backspace`、`Space`、`Delete` | | `HandleResult` | 键盘事件处理结果,`Handled` 表示已处理,`Default` 表示走默认处理 | | `ScrollAxis` | `Vertical` 或 `Horizontal` | | `DropEffect` | 拖放反馈效果,常用 `None`、`Copy`、`Move`、`Link` | | `DragFormat` | 拖放进入/悬停阶段可用的数据格式 | | `DragData` | drop 阶段真正传入的数据 | --- url: /website/zh/index.md --- # Flor 一个既要,又要,还要的 Rust GUI 框架 > 解决 Rust 生命周期痛点,重新定义 GUI 开发体验 [快速开始](guide/book-index) | [GitHub](https://github.com/flor-rs/flor) ## Features - [⚡ **极致性能**](/guide/book-index): 高性能渲染引擎,低编译后体积,基于即时模式的保留模式设计,天然支持高性能动画。 - [📡 **跨线程响应式信号系统**](/guide/use/signal): 支持跨线程使用的响应式信号系统,解决 Rust 生命周期带来的痛点,实现 UI 与数据的自动同步。 - [🔓 **无上下文绑定**](/guide/book-index): 不强制绑定上下文,支持在任意线程任意地方创建窗口,提供极大的开发灵活性。 - [🎨 **声明式 UI DSL**](/api/layout-class): 简洁直观的声明式 UI 语法,结合 Tailwind CSS 风格的布局类,快速构建复杂界面。 - [🖥️ **多窗口支持**](/guide/book-index): 原生支持多窗口,各窗口可独立设置刷新模式,满足复杂应用场景需求。 - [🔧 **原生句柄暴露**](/guide/book-index): 暴露底层句柄,支持作为 native 库使用,提供最大的灵活性和扩展性。 - [📦 **样式派生宏**](/guide/control/resolver/style-derive-macro): 通过 `#[derive(Resolver)]` 宏自动生成完整的样式管理系统,简化控件开发流程。 - [🌍 **跨平台兼容**](/guide/book-index): 自写平台支持,确保在 Windows、macOS、Linux 等多个平台上的一致体验。 - [🐛 **框架调试协议 (计划中)**](/guide/book-index): 计划中的调试协议和配套控制台,提供强大的开发和调试工具。