Widget trait 表示 UI 的组件。Druid 包含一组内置的小部件,您也可以编写自己的小部件。将内置小部件和自定义小部件组合起来创建一个小部件树; 您将从一些单根小部件开始,这些小部件(通常)有子部件,它们本身可能有子部件,等等。Widget 有一个通用参数 T,表示该小部件处理的数据。有些小部件(比如布局小部件)可能完全不知道它们遇到的数据类型,而其他小部件(比如滑块)可能期望只有一种类型(比如 f64)。
widgets
由所有小部件实现的 trait。
小部件的所有外观和行为都封装在实现此特性的对象中。
Trait 由关联数据的类型(T)参数化。所有 trait 方法都可以访问这些数据,并且在事件的情况下,引用是可变的,因此事件可以直接更新数据。
只要应用程序数据发生变化,框架就会使用 update 方法遍历小部件层次结构。框架需要知道数据实际上是否已经更改,这就是为什么 T 有一个 Data 绑定。
所有的特征方法都提供了相应的上下文。小部件可以通过调用上下文中的方法来请求事物并导致操作。
此外,所有 trait 方法都提供了一个环境(Env)。
容器小部件通常不会直接在它们的子小部件上调用 Widget 方法,而是拥有封装在 WidgetPod 中的小部件,并在其上调用相应的方法。WidgetPod 包含这些遍历的状态和逻辑。另一方面,特别是轻量级容器可能直接包含它们的子 Widget (当不需要布局或事件流逻辑时) ,并且在这些情况下将调用这些方法。
作为一般模式,容器小部件将在其所有子级上调用相应的 WidgetPod 方法。WidgetPod 根据需要应用逻辑来确定是否递归。
pub fn event(
&mut self,
ctx: &mut EventCtx<'_, '_>,
event: &Event,
data: &mut T,
env: &Env
)
处理事件
此方法调用处理许多不同的事件(在 Event 枚举中)。小部件可以通过多种方式处理这些事件: 从 EventCtx 请求事件、变更数据或提交命令
pub fn lifecycle(
&mut self,
ctx: &mut LifeCycleCtx<'_, '_>,
event: &LifeCycle,
data: &T,
env: &Env
)
处理生命周期通知。 调用此方法是为了通知小部件某些特殊事件(可在 LifeCycle 枚举中获得) ,这些事件通常与小部件图中的变化或特定小部件的状态有关。 小部件不会根据这些事件变更应用程序状态,而只是根据需要更新它自己的内部状态; 如果小部件需要变更数据,它可以提交一个将在下一次机会执行的 Command。
pub fn update(
&mut self,
ctx: &mut UpdateCtx<'_, '_>,
old_data: &T,
data: &T,
env: &Env
)
更新小部件的外观以响应应用程序的 Data 或 Env 的更改。
每当数据或环境发生更改时调用此方法。当需要更新小部件的外观以响应这些更改时,您可以在提供的 UpdateCtx 上调用 request _ aint 或 request _ layup,以根据需要安排绘制和布局的调用。 如果小部件希望计算细粒度的增量,则提供数据的前一个值; 如果实际需要,则应尝试仅请求新的布局或绘制通过。 要确定 Env 是否已经更改,可以在提供的 UpdateCtx 上调用 Env _ change; 然后可以使用小部件中使用的任何键调用 Env _ key _ change,查看它们是否已经更改; 然后可以根据需要请求布局或绘制。
pub fn layout(
&mut self,
ctx: &mut LayoutCtx<'_, '_>,
bc: &BoxConstraints,
data: &T,
env: &Env
) -> Size
计算布局
叶子小部件应该确定它的大小(受提供的约束的限制)并返回它。 容器小部件将在其子小部件上递归地调用 WidgetPod: : 布局,为每个小部件提供适当的框约束、计算布局,然后在其每个子小部件上调用 set _ source。最后,它应该返回容器的大小。容器可以以任何顺序递归,这对于首先计算非 flex 小部件的大小来确定可用于 flex 小部件的空间量是很有帮助的。 为了提高效率,容器应该只调用一次子窗口小部件的布局,尽管没有任何强制措施。 布局策略受到 Flutter 的强烈启发。
pub fn paint(&mut self, ctx: &mut PaintCtx<'_, '_, '_>, data: &T, env: &Env)
绘制小部件外观。 PaintCtx 对实现 RenderContext 特性的内容进行了定义,该特性公开了小部件可以用来绘制其外观的各种方法。 容器小部件可以在递归到它们的子元素之前绘制背景,或者在之后绘制注释(例如,滚动条)。此外,它们还可以对呈现上下文应用掩码和转换,这对滚动特别有用。
下面让我们手动实现一个自己的小部件
首先创建我们的rust项目
cargo new druid-widget
创建完成项目后添加包
druid = { git = "https://github.com/linebender/druid.git"}
在main.rs引入我们需要用到的引用
use druid::{widget::{Label, LabelText, ControllerHost, Click, Flex},RoundedRectRadii,Affine,UnitPoint,LinearGradient,Insets,theme, Data, Size, Env, WindowDesc, AppLauncher, Color, Lens};
use druid::widget::prelude::*;
我们先创建我们的小部件结构体
struct MyButton<T>{
//按钮上面的文字使用Label
label:Label<T>,
//尺寸
label_size:Size
}
给MyButton实现new 方法和form_label方法,on_click点击事件
impl<T: Data> MyButton<T> {
pub fn new(text: impl Into<LabelText<T>>) -> MyButton<T>{
//根据传入文字参数,创建按钮
MyButton::form_label(Label::new(text))
}
pub fn form_label(label:Label<T>) -> MyButton<T>{
//根据传入label部件创建按钮
MyButton
{
label,
label_size: Size::ZERO,
}
}
//按钮点击事件(也可以实现其余的事件,鼠标悬浮,鼠标移除等)
pub fn on_click(
self,
f: impl Fn(&mut EventCtx,&mut T,&Env)+ 'static,
) -> ControllerHost<Self,Click<T>> {
//controller事件处理,后面会详细讲解,Click是druid里面的部件可以直接使用,点击事件
ControllerHost::new(self, Click::new(f))
}
}
下面让我们的MyButton实现Widget,实现部件显示(里边的代码和方法我都添加了注释)
impl<T:Data> Widget<T> for MyButton<T> {
fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut T, env: &Env) {
match event {
Event::MouseDown(_) => {
if !ctx.is_disabled() {
ctx.set_active(true);
//请求重画
ctx.request_paint();
}
},
Event::MouseUp(_) => {
if ctx.is_active() && ctx.is_disabled() {
//请求重画
ctx.request_paint();
//trace!("Button {:?} released",ctx.widget_id());
}
ctx.set_active(false);
},
_ => {}
}
}
fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data: &T, env: &Env) {
if let LifeCycle::HotChanged(_) | LifeCycle::DisabledChanged(_) = event {
ctx.request_paint();
}
self.label.lifecycle(ctx, event, data, env)
}
fn update(&mut self, ctx: &mut UpdateCtx, old_data: &T, data: &T, env: &Env) {
//调用label的update方法
self.label.update(ctx, old_data, data, env)
}
fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, data: &T, env: &Env) -> Size {
//设置padding的大小
let padding = Size::new(LABEL_INSETS.x_value(), LABEL_INSETS.y_value());
//shrink 按照传入的padding设置label的边距,(给定的大小也是取整数,四舍五入)
//loosen 复制一份相同大小的尺寸
let label_bc = bc.shrink(padding).loosen();
//调用label的layout方法
self.label_size = self.label.layout(ctx, &label_bc, data, env);
//获取部件的最小宽度
let min_height = env.get(theme::BORDERED_WIDGET_HEIGHT);
//返回偏移量
let baseline = self.label.baseline_offset();
//设置偏移量
ctx.set_baseline_offset(baseline + LABEL_INSETS.y1);
//按钮的大小
let button_size = bc.constrain(Size::new(
self.label_size.width + padding.width,
(self.label_size.height + padding.height).max(min_height),
));
println!("{:?}",button_size);
button_size
}
fn paint(&mut self, ctx: &mut PaintCtx, data: &T, env: &Env) {
//active 表示是否鼠标按下
//disabled 表示是否禁用
let is_active = ctx.is_active() && !ctx.is_disabled();
//hot 鼠标是否悬停在按钮上面
let is_hot = ctx.is_hot();
//size 布局大小
let size = ctx.size();
//获取按钮边框的宽度
let stroke_width = env.get(theme::BUTTON_BORDER_WIDTH);
let rounded_rect = size
//转换成原点格式 Rect::new(0., 0., self.width, self.height)
.to_rect()
//计算尺寸
.inset(-stroke_width / 2.0)
//拐角的半径 env.get(theme::BUTTON_BORDER_RADIUS)
.to_rounded_rect(RoundedRectRadii {
top_left:size.width/2.0,
top_right:size.width/2.0,
bottom_right:size.width/2.0,
bottom_left:size.width/2.0,
});
//
let bg_gradient = if ctx.is_disabled() {
//禁用的效果
LinearGradient::new(
UnitPoint::TOP,
UnitPoint::BOTTOM,
(
env.get(theme::DISABLED_BUTTON_LIGHT),
env.get(theme::DISABLED_BUTTON_DARK),
),
)
//鼠标按下的效果
} else if is_active {
LinearGradient::new(
UnitPoint::TOP,
UnitPoint::BOTTOM,
(Color::YELLOW,Color::WHITE)
//(env.get(theme::BUTTON_DARK), env.get(theme::BUTTON_LIGHT)),
)
} else {
//从某个位置到某个位置,可以渐变
LinearGradient::new(
//从顶部到底部 由红色渐变到白色
UnitPoint::TOP,
UnitPoint::BOTTOM,
(Color::PURPLE,Color::WHITE)
//(env.get(theme::BUTTON_LIGHT), env.get(theme::BUTTON_DARK)),
)
};
//边框的颜色
let border_color = if is_hot && !ctx.is_disabled() {
//env.get(theme::BORDER_LIGHT)
Color::RED
} else {
//env.get(theme::BORDER_DARK)
Color::WHITE
};
//
ctx.stroke(rounded_rect, &border_color, stroke_width);
ctx.fill(rounded_rect, &bg_gradient);
let label_offset = (size.to_vec2() - self.label_size.to_vec2()) / 2.0;
ctx.with_save(|ctx| {
ctx.transform(Affine::translate(label_offset));
//调用label的paint
self.label.paint(ctx, data, env);
});
}
}
下面我们创建我们的Data数据结构体
#[derive(Clone,Data,Lens)]
struct State{
//label显示的文字
text:String,
}
编写窗体部分
fn builder_ui() -> impl Widget<State> {
//MyButton创建,这里的Label使用了Data模型,MyButton添加了点击事件
let my_button = MyButton::form_label(Label::new(|state: &State,_env: &Env|{ format!("{}",state.text)}))
.on_click(|_etc,state,_env|{
println!("点击了按钮");
state.text = "你点击了".to_string();
});
//Flex布局,这个后面也会讲解一下
Flex::row()
//把我们的组件添加到布局
.with_child(my_button)
}
编写启动方法
fn main() {
let win = WindowDesc::new(builder_ui()).title("自定义按钮").window_size((300.0,300.0));
let _app = AppLauncher::with_window(win)
.log_to_console()
.launch(State { text: "按钮".to_string() });
}
运行实现的样式