Interaction 与按钮
Interaction 枚举:最简单的交互
给任何 UI Node 加上 Button 标记组件(来自 bevy::widget,不是 bevy_ui_widgets),Bevy 的 ui_focus_system 就会自动根据鼠标位置更新该实体上的 Interaction 枚举:
pub enum Interaction {
Pressed, // 鼠标按下
Hovered, // 鼠标悬停
None, // 无交互
}Interaction 是 bevy_ui 内置的,不需要额外插件。查询它的方式和其他组件一样——用 Changed<Interaction> 过滤器只在状态变化时才触发系统:
fn button_style(
mut q: Query<
(&Interaction, &mut BackgroundColor, &mut BorderColor, &Children),
Changed<Interaction>,
>,
mut text_q: Query<&mut Text>,
) {
for (interaction, mut bg, mut border, children) in &mut q {
let mut text = text_q.get_mut(children[0]).unwrap();
match *interaction {
Interaction::Pressed => {
**text = "按下".to_string();
*bg = PRESSED.into();
border.set_all(Color::srgb(1.0, 0.0, 0.0));
}
Interaction::Hovered => {
**text = "悬停".to_string();
*bg = HOVERED.into();
border.set_all(Color::WHITE);
}
Interaction::None => {
**text = "点击".to_string();
*bg = NORMAL.into();
border.set_all(Color::BLACK);
}
}
}
}Listing 29-1(其一):根据 Interaction 切换按钮样式(examples/listing-29-01.rs)
按钮的搭建方式和第 28 章的 Node 没有区别——唯一的要求是实体上有 Button 组件。按钮的外观(圆角、边框、背景色)全靠你自己用 Node 属性 + 样式系统去实现,Button 只负责标记"这个 Node 可以被点击"。
fn setup(mut commands: Commands, assets: Res<AssetServer>) {
commands.spawn(Camera2d);
commands.spawn((
Node {
width: percent(100),
height: percent(100),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
..default()
},
children![(
Button,
Node {
width: px(150),
height: px(65),
border: UiRect::all(px(4)),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
border_radius: BorderRadius::MAX,
..default()
},
BorderColor::all(Color::BLACK),
BackgroundColor(NORMAL),
children![(
Text::new("点击"),
TextFont {
font: FontSource::Handle(assets.load("fonts/FiraSans-Bold.ttf")),
font_size: FontSize::Px(30.0),
..default()
},
TextColor(Color::srgb(0.9, 0.9, 0.9)),
)],
)],
));
}Listing 29-1(其二):一个可点击的按钮——Button 组件 + 布局 + 样式
运行这个例子,你会看到一个圆角按钮:默认深灰色,悬停时变亮,按下时变绿,鼠标移开恢复原样。按钮文本随状态切换。
Interaction 路线的优点是零配置、零插件。缺点是它只告诉你三种状态,没有"激活"(按下后松开)的概念——要实现 click 行为,你得自己检测 Pressed → None 的转换。
Activate 事件:按下去再松手
bevy_ui_widgets 的 Button 组件提供了更精确的交互语义。它不维护 Interaction 枚举,而是通过 picking 系统的 Pressed / Hovered 标记组件追踪状态,并在按钮被"激活"(按下后松开,或键盘聚焦时按 Enter / Space)时触发 Activate 事件。
fn setup(mut commands: Commands, assets: Res<AssetServer>) {
commands.spawn(Camera2d);
commands.spawn((
Node {
width: percent(100),
height: percent(100),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
..default()
},
children![(
WidgetButton,
Node {
width: px(150),
height: px(65),
border: UiRect::all(px(4)),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
border_radius: BorderRadius::MAX,
..default()
},
Interaction::None,
BorderColor::all(Color::BLACK),
BackgroundColor(Color::srgb(0.15, 0.15, 0.15)),
children![(
Text::new("Activate 按钮"),
TextFont {
font: FontSource::Handle(assets.load("fonts/FiraSans-Bold.ttf")),
font_size: FontSize::Px(24.0),
..default()
},
TextColor(Color::srgb(0.9, 0.9, 0.9)),
)],
observe(|_activate: On<Activate>| {
info!("按钮激活!");
}),
)],
));
}Listing 29-2:用 bevy_ui_widgets 的 Button + Activate 事件响应点击
注意几个关键区别:
- 插件:需要注册
UiWidgetsPlugins(或单独的ButtonPlugin); - 组件:用
bevy_ui_widgets::Button(不是bevy::widget::Button),为了避免命名冲突,通常用use ... as WidgetButton重命名; - Hovered:来自
bevy::picking::hover::Hovered,不是自动的——需要手动添加Hovered::default()组件; - 事件:用
observe函数(来自bevy_ui_widgets)在实体上挂一个 Observer,监听Activate事件。
Activate 只在完整的"按下→松开"周期后触发,比 Interaction::Pressed 更符合按钮的语义。键盘的 Enter 和 Space 也能触发它——这对无障碍访问很重要。
什么时候用哪个
Interaction + bevy::widget::Button | bevy_ui_widgets::Button + Activate | |
|---|---|---|
| 配置 | 零插件 | 需要 UiWidgetsPlugins |
| 状态 | Interaction 枚举 | Pressed / Hovered 组件 |
| 事件 | 无(需自行检测) | Activate 事件 |
| 键盘支持 | 无 | Enter / Space |
| 适用 | 原型、简单场景 | 正式项目、需要精确语义 |
本章后续的控件(Slider、Checkbox)都走 bevy_ui_widgets 路线。