框架 DSL
Flor 的界面代码可以当作一套普通 Rust DSL 来写:函数负责组合控件,参数负责传入属性、信号和 handler,返回值就是已经配置好的控件。
这和 JSX 的使用感受有相似之处:页面不是一大段字符串模板,而是一组可以拆分、复用、传参的控件函数。区别是 Flor 不需要额外的模板语言,组合本身就是 Rust 的函数调用和链式方法。
如果还不熟悉 .class(...)、.layout(...)、.style(...) 多次调用如何合并,先看 Resolver Layer 机制。
控件树怎么表达
Flor 里最常见的子控件列表写法是 views![...]:
views![...] 接受一组控件表达式,并把它们转换成控件列表。它适合在控件树现场展开子节点,尤其是容器里直接写多个子控件时。
如果只需要把单个控件转换成通用控件对象,可以使用 view!(...):
宏不是必须入口。它只是让“现场写控件树”更顺手;当你需要把单个控件按某个参数类型传出去时,直接调用 into_view(),或者把值交给接收 IntoViewIter 的 API,往往更清楚。
IntoView 和 IntoViewIter
IntoView / IntoViewIter 这组能力用于把控件转换成框架更通用的控件形态。它不只用于方法返回值,也用于控件参数、模块参数、容器子节点等需要类型对齐的地方。
当前公开写法里,单控件转换是 IntoView,子控件序列转换是 IntoViewIter。单个控件本身也实现 IntoViewIter,所以只传一个子控件时不再需要先手动转成列表。在封装方法时,单个控件可以返回 impl View 或 impl IntoView;多个控件可以返回 impl IntoViewIter:
title_view() 返回的是一个正常控件,调用方还能继续接 .class(...)、.layout(...) 等 builder。title_child() 更强调“这个返回值会被拿去当作单个子控件传入”。action_buttons() 则表示这个函数返回一组子控件。
比如 div(...) 这类容器接收的是 impl IntoViewIter。只有一个子控件时,可以直接传这个控件:
如果某个控件或模块参数要的是一个子控件,就标注 impl IntoView;如果要的是一组子控件或兼容单个子控件,就标注 impl IntoViewIter:
这样写的重点是让参数和返回值表达清楚:这里需要的是普通控件、通用控件对象,还是一组可以继续交给容器的子控件。
宏和转换能力可以搭配使用。views![...] 适合现场写多个子节点,into_view() 适合把一个已经存在的控件表达式对齐到通用控件对象:
返回值继续链式配置
IntoView 继承了 ViewIdentity,常用 builder 现在基于 ViewIdentity 读取控件身份。这意味着函数返回 impl IntoView 后,调用方仍然可以继续接 .class(...)、.layout(...)、.on_click(...)、.focus_index(...) 等链式方法:
这类封装不再需要为了继续链式配置而强行返回 impl View。当函数语义是“返回一个可作为子控件传递的值”时,impl IntoView 更直接。
为什么需要宏
Flor 提供 views![...],是为了让现场编写子控件列表时更顺手,同时避开 Rust 元组在 UI 子节点表达上的长度问题。
Floem 这类框架可以用元组表达子控件,例如 (a, b, c)。这种写法很自然,但在稳定 Rust 里,框架通常需要为不同长度的元组分别实现 trait。也就是说,它不是无限长度的:支持到多少个子节点,取决于框架写了多少组元组实现。
views![...] 会直接展开成控件列表,不需要为每一种子节点数量单独实现一遍 trait,也不容易让用户在子控件数量变多时撞到元组长度上限。
后续加入元组支持是可行的,做法通常是为常见长度的元组补实现,让用户可以在简单场景写 (a, b) 或 (a, b, c)。但只要稳定 Rust 还没有真正的可变参数泛型,元组支持本身仍然会有一个框架定义的上限。所以即使以后支持元组,views![...] 也更适合作为没有固定子节点数量时的稳定写法。
方法组合控件
最小的封装就是把一个控件的布局、样式和事件写进函数里,然后返回它:
这里的 Args 是事件 handler 参数形态的推导标记。这样写以后,调用方既可以传完整点击参数,也可以传无参数、只接收 ViewId、或省略 ViewId 的 handler。如果这里写成旧式的 impl Into<OnClickHandler>,就只保留完整参数签名的 From 转换,简化参数形态会丢失。
调用方只需要关心这个按钮对外暴露的参数:
这里 primary_button 就像一个小型控件。它内部怎么写 class、怎么绑定事件,对调用方来说都被收进了函数边界里。
组合成模块
多个控件也可以封装成一个复合控件或业务模块。比如登录模块可以把标题、验证码按钮、登录按钮和布局一起封装起来:
不同 handler 参数要使用不同的 Args 泛型,例如这里的 LoginArgs 和 SendCodeArgs。它们没有业务含义,只是让两个 handler 各自独立推导自己的参数形态。
这个函数返回的不是模板片段,而是一个完整控件。调用方可以把 handler 作为参数传进去:
如果模块里需要响应式状态,也可以把 signal 作为参数传入,让模块内部只控制自己关心的属性。比如登录按钮是否可用、验证码倒计时、面板是否显示,都可以独立拆成自己的参数,而不是把整段布局和样式写成复杂的字符串拼接。
返回后继续配置
函数返回的控件仍然可以继续使用 builder。也就是说,模块内部可以写默认布局和样式,调用方可以在外部继续补充或覆盖:
这里 login_module 内部已经写了 flex flex-col gap-3 p-4,调用方只补了宽度。因为 Resolver Layer 机制 会按层合并配置,所以外部补充不会清掉模块内部的配置;如果外部重新写了同一个属性,则以后写的为准。
为什么适合 AI
Flor 的这套 DSL 走的是常见的函数式 UI 表达路径:方法组合控件,属性、信号和 handler 作为变量传入,返回值继续参与组合。
大量 AI 模型已经熟悉 React、JSX、函数式组件和声明式 UI 的组织方式,所以它们更容易迁移到 Flor 的写法上:识别控件树、补全属性、拆分模块、调整布局和事件绑定。
这也是 AI 页面提到的原因:Flor 并不是为了 AI 才这样设计,但这种 DSL API 自然走到了 AI 已经训练过、也更容易理解的表达路径上。

