滑条、复选框
Slider:拖出来的数值
Slider 是 bevy_ui_widgets 提供的无样式滑条控件。它由三部分组成:
- Slider 组件(标记)——定义在滑条实体上,
track_click字段控制点击轨道的行为(Drag/Step/Snap); - SliderValue / SliderRange / SliderStep——附属组件,分别存储当前值、范围和步长。都是不可变的(
#[component(immutable)]),只能通过Commands插入新值来更新; - SliderThumb 标记——标记后代实体中的"滑块"元素,滑条核心逻辑用它来计算拖拽距离。
SliderRange 默认是 0.0..=1.0,SliderStep 默认 1.0,SliderValue 默认 0.0。对于音量控件,通常设 SliderRange::new(0.0, 100.0) 并给一个初始值。
滑条核心不负责移动滑块——那是你的工作。监听 ValueChange<f32> 事件,插入新的 SliderValue,再用一个系统根据值更新滑块的 left 百分比:
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> 过滤器避免每帧都算:
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:
slider_entity.observe(bevy::ui_widgets::slider_self_update);Checkbox:勾选与取消
Checkbox 是无样式复选框。状态由 Checked 组件表示(存在 = 勾选,不存在 = 未勾选)。点击或按 Enter / Space 时触发 ValueChange<bool> 事件。
与 Slider 类似,Checkbox 默认不自动修改自身状态。使用内置的 checkbox_self_update observer 可以启用自动更新:
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)),包含一个方框和一段文字。方框内部用一个绝对定位的小方块表示勾选标记——它的 BackgroundColor 在 Checked 存在时变绿,不存在时变透明。这种手动样式控制正是 headless 控件的设计哲学:引擎只管行为,外观全交给你。
Checkable 组件
Checkbox 自动 require Checkable 组件。Checkable 是一个标记,告诉引擎"这个实体可以被勾选"——无障碍系统(accesskit)用它来设置 ARIA 属性。