Skip to content

滑条、复选框

Slider:拖出来的数值

Sliderbevy_ui_widgets 提供的无样式滑条控件。它由三部分组成:

  • Slider 组件(标记)——定义在滑条实体上,track_click 字段控制点击轨道的行为(Drag / Step / Snap);
  • SliderValue / SliderRange / SliderStep——附属组件,分别存储当前值、范围和步长。都是不可变的(#[component(immutable)]),只能通过 Commands 插入新值来更新;
  • SliderThumb 标记——标记后代实体中的"滑块"元素,滑条核心逻辑用它来计算拖拽距离。

SliderRange 默认是 0.0..=1.0SliderStep 默认 1.0SliderValue 默认 0.0。对于音量控件,通常设 SliderRange::new(0.0, 100.0) 并给一个初始值。

滑条核心不负责移动滑块——那是你的工作。监听 ValueChange<f32> 事件,插入新的 SliderValue,再用一个系统根据值更新滑块的 left 百分比:

rust
fn setup(mut commands: Commands, assets: Res<AssetServer>) {
    commands.spawn(Camera2d);
    commands
        .spawn(Node {
            width: percent(100),
            height: percent(100),
            flex_direction: FlexDirection::Column,
            align_items: AlignItems::Center,
            justify_content: JustifyContent::Center,
            row_gap: px(20),
            ..default()
        })
        .observe(
            |value_change: On<ValueChange<f32>>, mut texts: Query<&mut Text, With<VolumeText>>| {
                if let Ok(mut text) = texts.single_mut() {
                    **text = format!("音量:{:.0}", value_change.value);
                }
            },
        )
        .with_children(|parent| {
            parent.spawn((
                Text::new("音量:50"),
                TextFont {
                    font: FontSource::Handle(assets.load("fonts/FiraSans-Bold.ttf")),
                    font_size: FontSize::Px(24.0),
                    ..default()
                },
                VolumeText,
            ));

            parent
                .spawn((
                    Node {
                        width: px(300),
                        height: px(12),
                        ..default()
                    },
                    Slider {
                        track_click: TrackClick::Snap,
                        orientation: SliderOrientation::Auto,
                    },
                    SliderValue(50.0),
                    SliderRange::new(0.0, 100.0),
                ))
                .with_children(|parent| {
                    parent.spawn((
                        Node {
                            height: px(6),
                            border_radius: BorderRadius::all(px(3)),
                            ..default()
                        },
                        BackgroundColor(TRACK),
                    ));
                    parent
                        .spawn(Node {
                            display: Display::Flex,
                            position_type: PositionType::Absolute,
                            left: px(0),
                            right: px(12),
                            top: px(0),
                            bottom: px(0),
                            ..default()
                        })
                        .with_children(|parent| {
                            parent.spawn((
                                SliderThumb,
                                Node {
                                    width: px(12),
                                    height: px(12),
                                    position_type: PositionType::Absolute,
                                    left: percent(0),
                                    border_radius: BorderRadius::MAX,
                                    ..default()
                                },
                                BackgroundColor(THUMB),
                            ));
                        });
                })
                .observe(
                    |value_change: On<ValueChange<f32>>, mut commands: Commands| {
                        commands
                            .entity(value_change.source)
                            .insert(SliderValue(value_change.value));
                    },
                );
        });
}

Listing 29-3(其一):音量滑条——Slider + Observer + 滑块定位

滑块的定位逻辑单独放在一个系统里,用 Changed<SliderValue> 过滤器避免每帧都算:

rust
fn update_slider_thumb(
    sliders: Query<(Entity, &SliderValue, &SliderRange), Changed<SliderValue>>,
    children_q: Query<&Children>,
    mut thumbs: Query<&mut Node, With<SliderThumb>>,
) {
    for (entity, value, range) in &sliders {
        for child in children_q.iter_descendants(entity) {
            if let Ok(mut thumb) = thumbs.get_mut(child) {
                thumb.left = percent(range.thumb_position(value.0) * 100.0);
            }
        }
    }
}

Listing 29-3(其二):update_slider_thumb——根据 SliderValue 更新滑块位置

SliderRange::thumb_position 返回 0.0 到 1.0 之间的比例值,乘以 100 就是 CSS 百分比。滑条的布局故意留出 12px 的余量(right: px(12)),让滑块在两端不会溢出。

TrackClick 三种模式

  • Drag(默认)——点击轨道等同于点击滑块,开始拖拽;
  • Step——点击轨道按 SliderStep 增减,点击左侧减、右侧加;
  • Snap——点击轨道直接跳到点击位置。

键盘支持:左右箭头按 SliderStep 增减,Home / End 跳到首尾。

外部状态管理

滑条默认不自动更新自己的值——它只触发 ValueChange<f32> 事件,由你决定是否插入新的 SliderValue。这就是"外部状态管理":数据流是单向的,滑条是数据的"显示器"而不是"编辑器"。在设置面板这种场景下,你可能希望在 ValueChange 回调里同时更新 Resource 中的设置,再由 Resource 驱动 UI 更新。

如果不需要外部状态管理,可以用内置的 slider_self_update observer:

rust
slider_entity.observe(bevy::ui_widgets::slider_self_update);

Checkbox:勾选与取消

Checkbox 是无样式复选框。状态由 Checked 组件表示(存在 = 勾选,不存在 = 未勾选)。点击或按 Enter / Space 时触发 ValueChange<bool> 事件。

与 Slider 类似,Checkbox 默认不自动修改自身状态。使用内置的 checkbox_self_update observer 可以启用自动更新:

rust
fn setup(mut commands: Commands, assets: Res<AssetServer>) {
    commands.spawn(Camera2d);
    commands.spawn((
        Node {
            width: percent(100),
            height: percent(100),
            flex_direction: FlexDirection::Column,
            align_items: AlignItems::Center,
            justify_content: JustifyContent::Center,
            row_gap: px(16),
            ..default()
        },
        children![
            (
                Node {
                    column_gap: px(8),
                    align_items: AlignItems::Center,
                    ..default()
                },
                Hovered::default(),
                Checkbox,
                children![
                    (
                        Node {
                            width: px(20),
                            height: px(20),
                            border: UiRect::all(px(2)),
                            border_radius: BorderRadius::all(px(4)),
                            ..default()
                        },
                        BorderColor::all(Color::srgb(0.4, 0.4, 0.4)),
                        children![(
                            Node {
                                width: px(12),
                                height: px(12),
                                position_type: PositionType::Absolute,
                                left: px(2),
                                top: px(2),
                                ..default()
                            },
                            BackgroundColor(Color::NONE),
                        )],
                    ),
                    (
                        Text::new("全屏模式"),
                        TextFont {
                            font: FontSource::Handle(assets.load("fonts/FiraSans-Bold.ttf")),
                            font_size: FontSize::Px(20.0),
                            ..default()
                        },
                    ),
                ],
                observe(checkbox_self_update),
                observe(on_check_change),
            ),
            (
                Text::new("状态:未勾选"),
                TextFont {
                    font: FontSource::Handle(assets.load("fonts/FiraSans-Bold.ttf")),
                    font_size: FontSize::Px(18.0),
                    ..default()
                },
                StatusLabel,
            ),
        ],
    ));
}

fn on_check_change(
    _value: On<ValueChange<bool>>,
    checkboxes: Query<Has<Checked>, With<Checkbox>>,
    mut labels: Query<&mut Text, With<StatusLabel>>,
) {
    for (checked, mut text) in checkboxes.iter().zip(labels.iter_mut()) {
        **text = if checked {
            "状态:已勾选".to_string()
        } else {
            "状态:未勾选".to_string()
        };
    }
}

Listing 29-4:复选框——Checkbox + checkbox_self_update + 状态显示

checkbox_self_update 会在 ValueChange<bool> 触发时插入或移除 Checked 组件。在它之后挂的第二个 observer on_check_change 能读到更新后的 Checked 状态,用来刷新文本标签。

注意 Checkbox 的布局:外层是行布局(column_gap: px(8)),包含一个方框和一段文字。方框内部用一个绝对定位的小方块表示勾选标记——它的 BackgroundColorChecked 存在时变绿,不存在时变透明。这种手动样式控制正是 headless 控件的设计哲学:引擎只管行为,外观全交给你。

Checkable 组件

Checkbox 自动 require Checkable 组件。Checkable 是一个标记,告诉引擎"这个实体可以被勾选"——无障碍系统(accesskit)用它来设置 ARIA 属性。