借鉴Flutter,不使用编程语言之外的标记语言而是使用编程语言本身来实现UI描述。这就要求编程语言支持泛型和反射特性。目前支持这两个特性又可以用于MCU平台的语言就只有Rust跟Zig了,但Rust太复杂了用在MCU平台大材小用,故选择Zig。Zig惟一的问题是目前还远没达到1.0版本,但当前v0.11版本的实用性也不错了。
项目命名为"zdec",“z”取自Zig,“dec”取自英文“声明”的前三个字母;而在汇编语言上一般用“dec”指令表示减法,所以"dec"也有在UI实现上做减法从而方便使用的意思。
目前已初步实现大体框架,实现了控件的创建/属性初始化和命令/属性的绑定。
UI构建方式如下:
const main_ui = .{
.{
d.Id.Button,
d.Size{ .width = 160, .height = 48 },
d.Align{ .lv_align = .Center, .y_ofs = -100 },
d.Text{ .text = "button" },
struct {
user_data: *Model,
pub fn onClicked(event: anytype) void {
const the_model = event.userData();
const step = 10;
std.debug.print("{s}: add Model.count by {d}\n", .{ @typeName(@TypeOf(event.target())), step });
the_model.add(step);
}
}{ .user_data = &_model },
},
.{
d.Id.Slider,
d.Size{ .width = 240, .height = 16 },
d.Align{ .lv_align = .Center, .y_ofs = 100 },
d.Range{ .min = 0, .max = 200 },
d.Bind(d.BindType.Value, @TypeOf(_model.count)){ .property = &_model.count },
},
};
var widget = try d.buildUI(lv.Screen.active(), main_ui);
效果是这样的:点击button,Slider的游标就会变化
上述例子的完整代码在这里:https://gitee.com/ufbycd/zdec/blob/dev/zdec/example.zig
项目主页在这里:https://gitee.com/ufbycd/zdec
目前只支持Linux,并使用SDL2作为后端来显示窗口。
最近编辑记录 海石生风 (2024-01-18 16:43:36)
离线
6啊,push到github和zig社区,很快就有人会参与
离线
compose是靠kotlin编译器插件实现声明式UI的,slint是靠rust的宏实现的,zig的comptime可以看做是编译器插件,也是可以实现声明式UI的
离线
这三个项目整理好了:将lvgl的C源码直接放入到zlvgl项目内,zlvgl和zdec两个项目分开管理而不使用git submodule。工程已经分别上传到gitee和github:
https://gitee.com/ufbycd/zlvgl
https://gitee.com/ufbycd/zdec
https://github.com/ufbycd/zlvgl
https://github.com/ufbycd/zdec
离线
@海石生风
元组是在编译时确定的,那怎么实现的状态diff和更新呢?还有UI元素的增减怎么实现的?比如根据条件显示一个label与否
离线
@海石生风
元组是在编译时确定的,那怎么实现的状态diff和更新呢?还有UI元素的增减怎么实现的?比如根据条件显示一个label与否
zig只在编译时支持泛型和反射,所以只能在编译时确定声明。控件一般来说对用户是隔离的,用户通常只操作模型;控件状态的更新来源于与其绑定的模型。
需要UI元素增减的场景是List和TableView控件吧, 这种控件会绑定一个模型数组,用户对表内的UI元素进行声明,UI框架会在编译时依据声明来生成一个用于构建UI元素的函数,模型有变化时就调用这个构建函数。
目前这还是一个构思,这种操作能否实现,还有待研究;毕竟zig这个编译时特性在众多编程言语中是绝无仅有的。
离线
@海石生风
“zig这个编译时特性在众多编程言语中是绝无仅有的” 这个特性确实是非常适合搞声明UI的,你这个选择非常正确。
看来你走的路线是利用binding来更新属性而不是对vdom进行diff再更新
我个人更喜欢reactjs这种vdom diff的方式,不用显式声明属性绑定,每次model变化,把整个UI 的builder重新运行一遍,再根据vdom实例化组件
坏处是开销大了不少;好处是可以任意方式写UI,每一帧的UI都可以是完全和上一帧不一样,是动态决定的。比如
if (fail_happen)
Text(‘error is {erro_code}')
else
Button(text=stat?'Yes':'No')
end
binding除了写起来有点繁琐以外,如果model值不能直接用于UI显示的话,还需要写转化函数。比如:
erro_code -> ‘error is {erro_code}'
stat -> stat?'Yes':'No'
以前玩微软XAML写UI就把我烦死了,写个binding还得写个converter
离线
vdom diff方式我之前也有考虑过,但觉得可能需要运行时反射,zig实现不了,就没有深究了。不过这种方式写起来确实要方便很多,后面有空再研究研究。
converter我目前的构思是在binding时像print函数那样指定格式化,如:
Bind(.Text, ErrorCode){ .property = &_model.error_code, .fmt = "error is {d}" }
“text=stat?'Yes':'No'”这种确实有点麻烦,我还在构思。
最近编辑记录 海石生风 (2024-01-29 18:49:21)
离线
很赞,声明式UI运动的又一种探索
离线
最近几天在思考热加载时突然醒悟,@达克罗德所提的“vdom diff”方式非常有助于实现热加载功能。
因为这种模式下的UI声明跟UI状态是分开的,像flutter分成了三颗树,分别对应UI声明、UI状态、UI渲染。热加载时不能影响UI状态,即要将UI状态独立出来,去加载没有状态的部分,这个部分其实就是UI声明。
所以反过来,flutter为什么要这样做,很程度是因为它要实现热加载功能。
最近编辑记录 海石生风 (2024-02-05 16:20:02)
离线
今年年初起的这个项目至今都有任何进展,是因为还有很多细节一直没有搞定。并且LVGL的API一直都不稳定,单单是搞其API绑定层都要不少精力。
因为Zig基本上可以直接调用C,故放弃API绑定层的思路,直接在框架内调用UI库的C接口,并将框架转为MVVM。
MVVM框架本身就规定好了View跟Model的绑定规则,非常适合声明式UI框架。
目前已经基本在awtk-mvvm上添加实现了zig语言支持:https://gitee.com/ufbycd/awtk-mvvm-zig-example,在zig上实现Model要比C/C++方便很多,便捷性几乎跟javascript的差不多。
下一步,将用zig元组替换xml来实现View声明,即实现一个zig的MVVM框架:zig-mvvm
再下一步,将zig-mvvm框架推广到其它UI库,如LVGL等。
因为业余自由时间不是很多,一天至多只能抽出两三个小时,也是随缘更新。
最近编辑记录 海石生风 (昨天 13:36:57)
离线