不依赖 "DSL" 使用 Ribir
或许是为了更直观的调试,或许是为了让代码更具 Rust 风格,有些人会更倾向于避免使用过多的宏和引入新的语法,因此也就不愿意使用 Ribir 的 "DSL"。
这并无问题,得益于 Ribir 在设计初期就将 "DSL" 定位为一个轻量级的语法转换层,你完全可以直接使用 Ribir 的 API 来构建 UI。甚至在一个代码片段中,你可以选择部分使用 API,部分使用宏,两者交织在一起使用。一切都将简单而自然。
核心概念
在 Ribir 中:
- 视图是由 widget 作为基本单位构建的。
- widget 之间通过纯组合方式组成新的 widget。
因此,通过 API 构建 UI 主要涉及两个关键点:
- 如何创建 widget
- 如何组合子 widget
通过 API 创建 widget
以 Radio
widget 为例,其定义如下:
use ribir::prelude::*;
pub struct Radio {
pub selected: bool,
pub value: Box<dyn Any>
}
这与常规的 Rust 结构体无异,你可以直接创建一个对象:
use ribir::prelude::*;
let radio = Radio { selected: true, value: Box::new(1.) };
这样,我们就得到了一个选中的 Radio
.
通过 FatObj
扩展 widget 的能力
我们已经创建了一个 Radio
,但是 它并没有提供任何响应事件的 API。
这是因为在 Ribir 中,事件响应是由独立的 widget 负责实现。任何 widget 都可以通过与它组合来获取事件响应的能力。
并且,对于内建 widget 如事件响应,我们可以无需通过组合方式即可获取。Ribir 提供了一个 FatObj<T>
的泛型,它提供了所有内建 widget 的初始化 API。只需用它包裹我们的 widget,即可让 widget 获得所有内建 widget 的能力。
use ribir::prelude::*;
let radio = Radio { selected: true, value: Box::new(1.) };
let radio = FatObj::new(radio)
.on_tap(|_| println!("Radio tapped"));
但在实际使用中,我们通常不直接这样写,而是通过 Declare
这个 trait 来创建 widget。
use ribir::prelude::*;
let btn: FatObj<State<Radio>> = Radio::declarer()
.selected(true)
.on_tap(|_| println!("Radio clicked"))
.finish();
为何我们应使用 Declare
创建 widget?
在上述示例中,我们通过类似 Builder 模式来创建 widget,这使得过程看起来更复杂。然而,这种方式实际上带来了更多的优势。
完整的初始化 API
要注意的是,我们最终创建的是 FatObj<State<Radio>>
,而不是 Radio
。这是因为通过 Declare
,我们不仅可以使用同名方法配置属性,还可以利用 FatObj
扩展内建 widget 的能力。至于为什么要使用 State
,这是因为 State
可以让你的 widget 状态被监听和修改。
use ribir::prelude::*;
let mut radio: FatObj<State<Radio>> = Radio::declarer()
// 我们可以使用内建能力
.on_tap(|_| println!("taped!"))
.finish();
watch!($radio.selected)
.subscribe(|selected| println!("The radio state change to {selected}"));
当然,无论是 FatObj
还是 State
,只有在你用到它们提供的能力时,才会影响到最终构建的视图的开销。
支持使用 pipe!
流进行初始化
使用 Declare
创建 widget 的另一个优点是,它支持通过 pipe!
流来初始化属性。通过 pipe!
流初始化的属性会随着流的变化而变化。例如,我们想要创建两个 Radio
,其中一个的状态会跟随着另一个的状态而变化。
use ribir::prelude::*;
let mut radio1: FatObj<State<Radio>> = Radio::declarer()
.selected(true)
.finish();
let radio2 = Radio::declarer()
.selected(pipe!($radio1.selected))
.finish();
let _row = Row::declarer()
.finish()
.with_child(radio1)
.with_child(radio2)
.into_widget();
支持访问内建 widget 属性
需要注意的是,虽然通过 Declare
创建的 widget 可以直接配置所有内建能力,但如果你需要在初始化后修改内建 widget 的属性,你需要先获取对应的内建 widget 再进行操作。这是因为这些内建 widget 是按需组合得到的。下面的例子中,我们创建一个按钮,并在点击时更改其边距:
use ribir::prelude::*;
fn radio_btn() -> Widget<'static> {
let mut btn = Radio::declarer().finish();
let m = btn.get_margin_widget().clone_writer();
btn
.on_tap(move |_| m.write().margin = EdgeInsets::all(10.0))
.into_widget()
}
子 widget 的组合
在 Ribir 中,我们使用 with_child
方法将子 widget 和父 widget 组合成新的 widget。@
语法主要也是利用 with_child
来实现的。实际上,你可能会比想象中更频繁地使用它。
例如,对于一个 Button
,它显示的文本甚至都是一个子 widget,而不是它的属性。这是因为它既可以是一个文本按钮,也可以是一个图标按钮。如果这些都是属性,那么无论你是使用文本按钮还是图标按钮,都会为你不需要的属性分配内存。但如果作为一个子 widget ,就可以根据使用情况来组合。
这是一个文本按钮和图标按钮的例子:
use ribir::prelude::*;
let text_btn = Button::declarer()
.finish()
.with_child("Text Button");
let icon_btn = Button::declarer()
.finish()
.with_child(Icon.with_child(named_svgs::get_or_default("search")));
API 和宏的混合使用
Ribir 的 "DSL" 并不是一种全新的语言,而只是一组宏。每个宏都可以作为一个独立的表达式使用,因此你可以自由地混合使用它们。下面我们将实现一个计数器的例子。我们将直接通过 API 创建按钮和计数的文本,并在初始化它们的属性时使用 $
来避免克隆 cnt
。最后,我们将使用 @
语法将它们组合成一个 Row
:
use ribir::prelude::*;
let counter = fn_widget! {
let cnt = Stateful::new(0);
let btn = Button::declarer()
.on_tap(move |_| *$cnt.write() += 1)
.finish()
.with_child("Inc");
let label = H1::declarer()
.text(pipe!($cnt.to_string()))
.finish();
@Row {
align_items: Align::Center,
@ { btn }
@ { label }
}
};
结语
我们希望每个使用 Ribir 的人都能根据自己的喜好选择使用方式,无论是通过 "DSL" 还是直接使用 API,都能获得最佳的体验。
但你需要明白的是,Ribir 的 "DSL" 并不是一种新的语言,我们甚至不把它称为 "DSL"。它完全是基于我们在上文中介绍的 API 构建的,只是一组宏,目的是让 UI 结构更清晰,更易读,并避免一些明显的重复代码,比如因为 move 语义而需要频繁克隆 State。