Skip to content

Interaction 与按钮

Interaction 枚举:最简单的交互

给任何 UI Node 加上 Button 标记组件(来自 bevy::widget,不是 bevy_ui_widgets),Bevy 的 ui_focus_system 就会自动根据鼠标位置更新该实体上的 Interaction 枚举:

rust
pub enum Interaction {
    Pressed,  // 鼠标按下
    Hovered,  // 鼠标悬停
    None,     // 无交互
}

Interactionbevy_ui 内置的,不需要额外插件。查询它的方式和其他组件一样——用 Changed<Interaction> 过滤器只在状态变化时才触发系统:

rust
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 可以被点击"。

rust
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_widgetsButton 组件提供了更精确的交互语义。它不维护 Interaction 枚举,而是通过 picking 系统的 Pressed / Hovered 标记组件追踪状态,并在按钮被"激活"(按下后松开,或键盘聚焦时按 Enter / Space)时触发 Activate 事件。

rust
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 事件响应点击

注意几个关键区别:

  1. 插件:需要注册 UiWidgetsPlugins(或单独的 ButtonPlugin);
  2. 组件:用 bevy_ui_widgets::Button(不是 bevy::widget::Button),为了避免命名冲突,通常用 use ... as WidgetButton 重命名;
  3. Hovered:来自 bevy::picking::hover::Hovered,不是自动的——需要手动添加 Hovered::default() 组件;
  4. 事件:用 observe 函数(来自 bevy_ui_widgets)在实体上挂一个 Observer,监听 Activate 事件。

Activate 只在完整的"按下→松开"周期后触发,比 Interaction::Pressed 更符合按钮的语义。键盘的 Enter 和 Space 也能触发它——这对无障碍访问很重要。

什么时候用哪个

Interaction + bevy::widget::Buttonbevy_ui_widgets::Button + Activate
配置零插件需要 UiWidgetsPlugins
状态Interaction 枚举Pressed / Hovered 组件
事件无(需自行检测)Activate 事件
键盘支持Enter / Space
适用原型、简单场景正式项目、需要精确语义

本章后续的控件(Slider、Checkbox)都走 bevy_ui_widgets 路线。