Path: blob/main/examples/ui/standard_widgets_observers.rs
6849 views
//! This experimental example illustrates how to create widgets using the `bevy_ui_widgets` widget set.1//!2//! The patterns shown here are likely to change substantially as the `bevy_ui_widgets` crate3//! matures, so please exercise caution if you are using this as a reference for your own code,4//! and note that there are still "user experience" issues with this API.56use bevy::{7color::palettes::basic::*,8ecs::system::SystemId,9input_focus::{10tab_navigation::{TabGroup, TabIndex, TabNavigationPlugin},11InputDispatchPlugin,12},13picking::hover::Hovered,14prelude::*,15reflect::Is,16ui::{Checked, InteractionDisabled, Pressed},17ui_widgets::{18Activate, Button, Callback, Checkbox, Slider, SliderRange, SliderThumb, SliderValue,19UiWidgetsPlugins, ValueChange,20},21};2223fn main() {24App::new()25.add_plugins((26DefaultPlugins,27UiWidgetsPlugins,28InputDispatchPlugin,29TabNavigationPlugin,30))31.insert_resource(DemoWidgetStates { slider_value: 50.0 })32.add_systems(Startup, setup)33.add_observer(button_on_interaction::<Add, Pressed>)34.add_observer(button_on_interaction::<Remove, Pressed>)35.add_observer(button_on_interaction::<Add, InteractionDisabled>)36.add_observer(button_on_interaction::<Remove, InteractionDisabled>)37.add_observer(button_on_interaction::<Insert, Hovered>)38.add_observer(slider_on_interaction::<Add, InteractionDisabled>)39.add_observer(slider_on_interaction::<Remove, InteractionDisabled>)40.add_observer(slider_on_interaction::<Insert, Hovered>)41.add_observer(slider_on_change_value::<SliderValue>)42.add_observer(slider_on_change_value::<SliderRange>)43.add_observer(checkbox_on_interaction::<Add, InteractionDisabled>)44.add_observer(checkbox_on_interaction::<Remove, InteractionDisabled>)45.add_observer(checkbox_on_interaction::<Insert, Hovered>)46.add_observer(checkbox_on_interaction::<Add, Checked>)47.add_observer(checkbox_on_interaction::<Remove, Checked>)48.add_systems(Update, (update_widget_values, toggle_disabled))49.run();50}5152const NORMAL_BUTTON: Color = Color::srgb(0.15, 0.15, 0.15);53const HOVERED_BUTTON: Color = Color::srgb(0.25, 0.25, 0.25);54const PRESSED_BUTTON: Color = Color::srgb(0.35, 0.75, 0.35);55const SLIDER_TRACK: Color = Color::srgb(0.05, 0.05, 0.05);56const SLIDER_THUMB: Color = Color::srgb(0.35, 0.75, 0.35);57const CHECKBOX_OUTLINE: Color = Color::srgb(0.45, 0.45, 0.45);58const CHECKBOX_CHECK: Color = Color::srgb(0.35, 0.75, 0.35);5960/// Marker which identifies buttons with a particular style, in this case the "Demo style".61#[derive(Component)]62struct DemoButton;6364/// Marker which identifies sliders with a particular style.65#[derive(Component, Default)]66struct DemoSlider;6768/// Marker which identifies the slider's thumb element.69#[derive(Component, Default)]70struct DemoSliderThumb;7172/// Marker which identifies checkboxes with a particular style.73#[derive(Component, Default)]74struct DemoCheckbox;7576/// A struct to hold the state of various widgets shown in the demo.77///78/// While it is possible to use the widget's own state components as the source of truth,79/// in many cases widgets will be used to display dynamic data coming from deeper within the app,80/// using some kind of data-binding. This example shows how to maintain an external source of81/// truth for widget states.82#[derive(Resource)]83struct DemoWidgetStates {84slider_value: f32,85}8687fn setup(mut commands: Commands, assets: Res<AssetServer>) {88// System to print a value when the button is clicked.89let on_click = commands.register_system(|_: In<Activate>| {90info!("Button clicked!");91});9293// System to update a resource when the slider value changes. Note that we could have94// updated the slider value directly, but we want to demonstrate externalizing the state.95let on_change_value = commands.register_system(96|value: In<ValueChange<f32>>, mut widget_states: ResMut<DemoWidgetStates>| {97widget_states.slider_value = value.0.value;98},99);100101// ui camera102commands.spawn(Camera2d);103commands.spawn(demo_root(&assets, on_click, on_change_value));104}105106fn demo_root(107asset_server: &AssetServer,108on_click: SystemId<In<Activate>>,109on_change_value: SystemId<In<ValueChange<f32>>>,110) -> impl Bundle {111(112Node {113width: percent(100),114height: percent(100),115align_items: AlignItems::Center,116justify_content: JustifyContent::Center,117display: Display::Flex,118flex_direction: FlexDirection::Column,119row_gap: px(10),120..default()121},122TabGroup::default(),123children![124button(asset_server, Callback::System(on_click)),125slider(0.0, 100.0, 50.0, Callback::System(on_change_value)),126checkbox(asset_server, "Checkbox", Callback::Ignore),127Text::new("Press 'D' to toggle widget disabled states"),128],129)130}131132fn button(asset_server: &AssetServer, on_click: Callback<In<Activate>>) -> impl Bundle {133(134Node {135width: px(150),136height: px(65),137border: UiRect::all(px(5)),138justify_content: JustifyContent::Center,139align_items: AlignItems::Center,140..default()141},142DemoButton,143Button {144on_activate: on_click,145},146Hovered::default(),147TabIndex(0),148BorderColor::all(Color::BLACK),149BorderRadius::MAX,150BackgroundColor(NORMAL_BUTTON),151children![(152Text::new("Button"),153TextFont {154font: asset_server.load("fonts/FiraSans-Bold.ttf"),155font_size: 33.0,156..default()157},158TextColor(Color::srgb(0.9, 0.9, 0.9)),159TextShadow::default(),160)],161)162}163164fn button_on_interaction<E: EntityEvent, C: Component>(165event: On<E, C>,166mut buttons: Query<167(168&Hovered,169Has<InteractionDisabled>,170Has<Pressed>,171&mut BackgroundColor,172&mut BorderColor,173&Children,174),175With<DemoButton>,176>,177mut text_query: Query<&mut Text>,178) {179if let Ok((hovered, disabled, pressed, mut color, mut border_color, children)) =180buttons.get_mut(event.event_target())181{182if children.is_empty() {183return;184}185let Ok(mut text) = text_query.get_mut(children[0]) else {186return;187};188let hovered = hovered.get();189// These "removal event checks" exist because the `Remove` event is triggered _before_ the component is actually190// removed, meaning it still shows up in the query. We're investigating the best way to improve this scenario.191let pressed = pressed && !(E::is::<Remove>() && C::is::<Pressed>());192let disabled = disabled && !(E::is::<Remove>() && C::is::<InteractionDisabled>());193match (disabled, hovered, pressed) {194// Disabled button195(true, _, _) => {196**text = "Disabled".to_string();197*color = NORMAL_BUTTON.into();198border_color.set_all(GRAY);199}200201// Pressed and hovered button202(false, true, true) => {203**text = "Press".to_string();204*color = PRESSED_BUTTON.into();205border_color.set_all(RED);206}207208// Hovered, unpressed button209(false, true, false) => {210**text = "Hover".to_string();211*color = HOVERED_BUTTON.into();212border_color.set_all(WHITE);213}214215// Unhovered button (either pressed or not).216(false, false, _) => {217**text = "Button".to_string();218*color = NORMAL_BUTTON.into();219border_color.set_all(BLACK);220}221}222}223}224225/// Create a demo slider226fn slider(227min: f32,228max: f32,229value: f32,230on_change: Callback<In<ValueChange<f32>>>,231) -> impl Bundle {232(233Node {234display: Display::Flex,235flex_direction: FlexDirection::Column,236justify_content: JustifyContent::Center,237align_items: AlignItems::Stretch,238justify_items: JustifyItems::Center,239column_gap: px(4),240height: px(12),241width: percent(30),242..default()243},244Name::new("Slider"),245Hovered::default(),246DemoSlider,247Slider {248on_change,249..default()250},251SliderValue(value),252SliderRange::new(min, max),253TabIndex(0),254Children::spawn((255// Slider background rail256Spawn((257Node {258height: px(6),259..default()260},261BackgroundColor(SLIDER_TRACK), // Border color for the checkbox262BorderRadius::all(px(3)),263)),264// Invisible track to allow absolute placement of thumb entity. This is narrower than265// the actual slider, which allows us to position the thumb entity using simple266// percentages, without having to measure the actual width of the slider thumb.267Spawn((268Node {269display: Display::Flex,270position_type: PositionType::Absolute,271left: px(0),272// Track is short by 12px to accommodate the thumb.273right: px(12),274top: px(0),275bottom: px(0),276..default()277},278children![(279// Thumb280DemoSliderThumb,281SliderThumb,282Node {283display: Display::Flex,284width: px(12),285height: px(12),286position_type: PositionType::Absolute,287left: percent(0), // This will be updated by the slider's value288..default()289},290BorderRadius::MAX,291BackgroundColor(SLIDER_THUMB),292)],293)),294)),295)296}297298fn slider_on_interaction<E: EntityEvent, C: Component>(299event: On<E, C>,300sliders: Query<(Entity, &Hovered, Has<InteractionDisabled>), With<DemoSlider>>,301children: Query<&Children>,302mut thumbs: Query<(&mut BackgroundColor, Has<DemoSliderThumb>), Without<DemoSlider>>,303) {304if let Ok((slider_ent, hovered, disabled)) = sliders.get(event.event_target()) {305// These "removal event checks" exist because the `Remove` event is triggered _before_ the component is actually306// removed, meaning it still shows up in the query. We're investigating the best way to improve this scenario.307let disabled = disabled && !(E::is::<Remove>() && C::is::<InteractionDisabled>());308for child in children.iter_descendants(slider_ent) {309if let Ok((mut thumb_bg, is_thumb)) = thumbs.get_mut(child)310&& is_thumb311{312thumb_bg.0 = thumb_color(disabled, hovered.0);313}314}315}316}317318fn slider_on_change_value<C: Component>(319insert: On<Insert, C>,320sliders: Query<(Entity, &SliderValue, &SliderRange), With<DemoSlider>>,321children: Query<&Children>,322mut thumbs: Query<(&mut Node, Has<DemoSliderThumb>), Without<DemoSlider>>,323) {324if let Ok((slider_ent, value, range)) = sliders.get(insert.entity) {325for child in children.iter_descendants(slider_ent) {326if let Ok((mut thumb_node, is_thumb)) = thumbs.get_mut(child)327&& is_thumb328{329thumb_node.left = percent(range.thumb_position(value.0) * 100.0);330}331}332}333}334335fn thumb_color(disabled: bool, hovered: bool) -> Color {336match (disabled, hovered) {337(true, _) => GRAY.into(),338339(false, true) => SLIDER_THUMB.lighter(0.3),340341_ => SLIDER_THUMB,342}343}344345/// Create a demo checkbox346fn checkbox(347asset_server: &AssetServer,348caption: &str,349on_change: Callback<In<ValueChange<bool>>>,350) -> impl Bundle {351(352Node {353display: Display::Flex,354flex_direction: FlexDirection::Row,355justify_content: JustifyContent::FlexStart,356align_items: AlignItems::Center,357align_content: AlignContent::Center,358column_gap: px(4),359..default()360},361Name::new("Checkbox"),362Hovered::default(),363DemoCheckbox,364Checkbox { on_change },365TabIndex(0),366Children::spawn((367Spawn((368// Checkbox outer369Node {370display: Display::Flex,371width: px(16),372height: px(16),373border: UiRect::all(px(2)),374..default()375},376BorderColor::all(CHECKBOX_OUTLINE), // Border color for the checkbox377BorderRadius::all(px(3)),378children![379// Checkbox inner380(381Node {382display: Display::Flex,383width: px(8),384height: px(8),385position_type: PositionType::Absolute,386left: px(2),387top: px(2),388..default()389},390BackgroundColor(Srgba::NONE.into()),391),392],393)),394Spawn((395Text::new(caption),396TextFont {397font: asset_server.load("fonts/FiraSans-Bold.ttf"),398font_size: 20.0,399..default()400},401)),402)),403)404}405406fn checkbox_on_interaction<E: EntityEvent, C: Component>(407event: On<E, C>,408checkboxes: Query<409(&Hovered, Has<InteractionDisabled>, Has<Checked>, &Children),410With<DemoCheckbox>,411>,412mut borders: Query<(&mut BorderColor, &mut Children), Without<DemoCheckbox>>,413mut marks: Query<&mut BackgroundColor, (Without<DemoCheckbox>, Without<Children>)>,414) {415if let Ok((hovered, disabled, checked, children)) = checkboxes.get(event.event_target()) {416let hovered = hovered.get();417// These "removal event checks" exist because the `Remove` event is triggered _before_ the component is actually418// removed, meaning it still shows up in the query. We're investigating the best way to improve this scenario.419let checked = checked && !(E::is::<Remove>() && C::is::<Checked>());420let disabled = disabled && !(E::is::<Remove>() && C::is::<InteractionDisabled>());421422let Some(border_id) = children.first() else {423return;424};425426let Ok((mut border_color, border_children)) = borders.get_mut(*border_id) else {427return;428};429430let Some(mark_id) = border_children.first() else {431warn!("Checkbox does not have a mark entity.");432return;433};434435let Ok(mut mark_bg) = marks.get_mut(*mark_id) else {436warn!("Checkbox mark entity lacking a background color.");437return;438};439440let color: Color = if disabled {441// If the checkbox is disabled, use a lighter color442CHECKBOX_OUTLINE.with_alpha(0.2)443} else if hovered {444// If hovering, use a lighter color445CHECKBOX_OUTLINE.lighter(0.2)446} else {447// Default color for the checkbox448CHECKBOX_OUTLINE449};450451// Update the background color of the check mark452border_color.set_all(color);453454let mark_color: Color = match (disabled, checked) {455(true, true) => CHECKBOX_CHECK.with_alpha(0.5),456(false, true) => CHECKBOX_CHECK,457(_, false) => Srgba::NONE.into(),458};459460if mark_bg.0 != mark_color {461// Update the color of the check mark462mark_bg.0 = mark_color;463}464}465}466467/// Update the widget states based on the changing resource.468fn update_widget_values(469res: Res<DemoWidgetStates>,470mut sliders: Query<Entity, With<DemoSlider>>,471mut commands: Commands,472) {473if res.is_changed() {474for slider_ent in sliders.iter_mut() {475commands476.entity(slider_ent)477.insert(SliderValue(res.slider_value));478}479}480}481482fn toggle_disabled(483input: Res<ButtonInput<KeyCode>>,484mut interaction_query: Query<485(Entity, Has<InteractionDisabled>),486Or<(With<Button>, With<Slider>, With<Checkbox>)>,487>,488mut commands: Commands,489) {490if input.just_pressed(KeyCode::KeyD) {491for (entity, disabled) in &mut interaction_query {492if disabled {493info!("Widget enabled");494commands.entity(entity).remove::<InteractionDisabled>();495} else {496info!("Widget disabled");497commands.entity(entity).insert(InteractionDisabled);498}499}500}501}502503504