//! This example illustrates how to create widgets using the `bevy_core_widgets` widget set. use bevy::{ color::palettes::basic::*, core_widgets::{ Callback, CoreButton, CoreCheckbox, CoreRadio, CoreRadioGroup, CoreSlider, CoreSliderDragState, CoreSliderThumb, CoreWidgetsPlugin, SliderRange, SliderValue, TrackClick, }, input_focus::{ tab_navigation::{TabGroup, TabIndex, TabNavigationPlugin}, InputDispatchPlugin, }, picking::hover::Hovered, prelude::*, ui::{Checked, InteractionDisabled, Pressed}, winit::WinitSettings, }; fn main() { App::new() .add_plugins(( DefaultPlugins, CoreWidgetsPlugin, InputDispatchPlugin, TabNavigationPlugin, )) // Only run the app when there is user input. This will significantly reduce CPU/GPU use. .insert_resource(WinitSettings::desktop_app()) .insert_resource(DemoWidgetStates { slider_value: 50.0, slider_click: TrackClick::Snap, }) .add_systems(Startup, setup) .add_systems( Update, ( update_widget_values, update_button_style, update_button_style2, update_slider_style.after(update_widget_values), update_slider_style2.after(update_widget_values), update_checkbox_or_radio_style.after(update_widget_values), update_checkbox_or_radio_style2.after(update_widget_values), toggle_disabled, ), ) .run(); } const NORMAL_BUTTON: Color = Color::srgb(0.15, 0.15, 0.15); const HOVERED_BUTTON: Color = Color::srgb(0.25, 0.25, 0.25); const PRESSED_BUTTON: Color = Color::srgb(0.35, 0.75, 0.35); const SLIDER_TRACK: Color = Color::srgb(0.05, 0.05, 0.05); const SLIDER_THUMB: Color = Color::srgb(0.35, 0.75, 0.35); const CHECKBOX_OUTLINE: Color = Color::srgb(0.45, 0.45, 0.45); const CHECKBOX_CHECK: Color = Color::srgb(0.35, 0.75, 0.35); /// Marker which identifies buttons with a particular style, in this case the "Demo style". #[derive(Component)] struct DemoButton; /// Marker which identifies sliders with a particular style. #[derive(Component, Default)] struct DemoSlider; /// Marker which identifies the slider's thumb element. #[derive(Component, Default)] struct DemoSliderThumb; /// Marker which identifies checkboxes with a particular style. #[derive(Component, Default)] struct DemoCheckbox; /// Marker which identifies a styled radio button. We'll use this to change the track click /// behavior. #[derive(Component, Default)] struct DemoRadio(TrackClick); /// A struct to hold the state of various widgets shown in the demo. /// /// While it is possible to use the widget's own state components as the source of truth, /// in many cases widgets will be used to display dynamic data coming from deeper within the app, /// using some kind of data-binding. This example shows how to maintain an external source of /// truth for widget states. #[derive(Resource)] struct DemoWidgetStates { slider_value: f32, slider_click: TrackClick, } /// Update the widget states based on the changing resource. fn update_widget_values( res: Res, mut sliders: Query<(Entity, &mut CoreSlider), With>, radios: Query<(Entity, &DemoRadio, Has)>, mut commands: Commands, ) { if res.is_changed() { for (slider_ent, mut slider) in sliders.iter_mut() { commands .entity(slider_ent) .insert(SliderValue(res.slider_value)); slider.track_click = res.slider_click; } for (radio_id, radio_value, checked) in radios.iter() { let will_be_checked = radio_value.0 == res.slider_click; if will_be_checked != checked { if will_be_checked { commands.entity(radio_id).insert(Checked); } else { commands.entity(radio_id).remove::(); } } } } } fn setup(mut commands: Commands, assets: Res) { // System to print a value when the button is clicked. let on_click = commands.register_system(|| { info!("Button clicked!"); }); // System to update a resource when the slider value changes. Note that we could have // updated the slider value directly, but we want to demonstrate externalizing the state. let on_change_value = commands.register_system( |value: In, mut widget_states: ResMut| { widget_states.slider_value = *value; }, ); // System to update a resource when the radio group changes. let on_change_radio = commands.register_system( |value: In, mut widget_states: ResMut, q_radios: Query<&DemoRadio>| { if let Ok(radio) = q_radios.get(*value) { widget_states.slider_click = radio.0; } }, ); // ui camera commands.spawn(Camera2d); commands.spawn(demo_root( &assets, Callback::System(on_click), Callback::System(on_change_value), Callback::System(on_change_radio), )); } fn demo_root( asset_server: &AssetServer, on_click: Callback, on_change_value: Callback>, on_change_radio: Callback>, ) -> impl Bundle { ( Node { width: Val::Percent(100.0), height: Val::Percent(100.0), align_items: AlignItems::Center, justify_content: JustifyContent::Center, display: Display::Flex, flex_direction: FlexDirection::Column, row_gap: Val::Px(10.0), ..default() }, TabGroup::default(), children![ button(asset_server, on_click), slider(0.0, 100.0, 50.0, on_change_value), checkbox(asset_server, "Checkbox", Callback::Ignore), radio_group(asset_server, on_change_radio), Text::new("Press 'D' to toggle widget disabled states"), ], ) } fn button(asset_server: &AssetServer, on_click: Callback) -> impl Bundle { ( Node { width: Val::Px(150.0), height: Val::Px(65.0), border: UiRect::all(Val::Px(5.0)), justify_content: JustifyContent::Center, align_items: AlignItems::Center, ..default() }, DemoButton, CoreButton { on_activate: on_click, }, Hovered::default(), TabIndex(0), BorderColor::all(Color::BLACK), BorderRadius::MAX, BackgroundColor(NORMAL_BUTTON), children![( Text::new("Button"), TextFont { font: asset_server.load("fonts/FiraSans-Bold.ttf"), font_size: 33.0, ..default() }, TextColor(Color::srgb(0.9, 0.9, 0.9)), TextShadow::default(), )], ) } fn update_button_style( mut buttons: Query< ( Has, &Hovered, Has, &mut BackgroundColor, &mut BorderColor, &Children, ), ( Or<( Changed, Changed, Added, )>, With, ), >, mut text_query: Query<&mut Text>, ) { for (pressed, hovered, disabled, mut color, mut border_color, children) in &mut buttons { let mut text = text_query.get_mut(children[0]).unwrap(); set_button_style( disabled, hovered.get(), pressed, &mut color, &mut border_color, &mut text, ); } } /// Supplementary system to detect removed marker components fn update_button_style2( mut buttons: Query< ( Has, &Hovered, Has, &mut BackgroundColor, &mut BorderColor, &Children, ), With, >, mut removed_depressed: RemovedComponents, mut removed_disabled: RemovedComponents, mut text_query: Query<&mut Text>, ) { removed_depressed .read() .chain(removed_disabled.read()) .for_each(|entity| { if let Ok((pressed, hovered, disabled, mut color, mut border_color, children)) = buttons.get_mut(entity) { let mut text = text_query.get_mut(children[0]).unwrap(); set_button_style( disabled, hovered.get(), pressed, &mut color, &mut border_color, &mut text, ); } }); } fn set_button_style( disabled: bool, hovered: bool, pressed: bool, color: &mut BackgroundColor, border_color: &mut BorderColor, text: &mut Text, ) { match (disabled, hovered, pressed) { // Disabled button (true, _, _) => { **text = "Disabled".to_string(); *color = NORMAL_BUTTON.into(); border_color.set_all(GRAY); } // Pressed and hovered button (false, true, true) => { **text = "Press".to_string(); *color = PRESSED_BUTTON.into(); border_color.set_all(RED); } // Hovered, unpressed button (false, true, false) => { **text = "Hover".to_string(); *color = HOVERED_BUTTON.into(); border_color.set_all(WHITE); } // Unhovered button (either pressed or not). (false, false, _) => { **text = "Button".to_string(); *color = NORMAL_BUTTON.into(); border_color.set_all(BLACK); } } } /// Create a demo slider fn slider(min: f32, max: f32, value: f32, on_change: Callback>) -> impl Bundle { ( Node { display: Display::Flex, flex_direction: FlexDirection::Column, justify_content: JustifyContent::Center, align_items: AlignItems::Stretch, justify_items: JustifyItems::Center, column_gap: Val::Px(4.0), height: Val::Px(12.0), width: Val::Percent(30.0), ..default() }, Name::new("Slider"), Hovered::default(), DemoSlider, CoreSlider { on_change, track_click: TrackClick::Snap, }, SliderValue(value), SliderRange::new(min, max), TabIndex(0), Children::spawn(( // Slider background rail Spawn(( Node { height: Val::Px(6.0), ..default() }, BackgroundColor(SLIDER_TRACK), // Border color for the checkbox BorderRadius::all(Val::Px(3.0)), )), // Invisible track to allow absolute placement of thumb entity. This is narrower than // the actual slider, which allows us to position the thumb entity using simple // percentages, without having to measure the actual width of the slider thumb. Spawn(( Node { display: Display::Flex, position_type: PositionType::Absolute, left: Val::Px(0.0), // Track is short by 12px to accommodate the thumb. right: Val::Px(12.0), top: Val::Px(0.0), bottom: Val::Px(0.0), ..default() }, children![( // Thumb DemoSliderThumb, CoreSliderThumb, Node { display: Display::Flex, width: Val::Px(12.0), height: Val::Px(12.0), position_type: PositionType::Absolute, left: Val::Percent(0.0), // This will be updated by the slider's value ..default() }, BorderRadius::MAX, BackgroundColor(SLIDER_THUMB), )], )), )), ) } /// Update the visuals of the slider based on the slider state. fn update_slider_style( sliders: Query< ( Entity, &SliderValue, &SliderRange, &Hovered, &CoreSliderDragState, Has, ), ( Or<( Changed, Changed, Changed, Changed, Added, )>, With, ), >, children: Query<&Children>, mut thumbs: Query<(&mut Node, &mut BackgroundColor, Has), Without>, ) { for (slider_ent, value, range, hovered, drag_state, disabled) in sliders.iter() { for child in children.iter_descendants(slider_ent) { if let Ok((mut thumb_node, mut thumb_bg, is_thumb)) = thumbs.get_mut(child) { if is_thumb { thumb_node.left = Val::Percent(range.thumb_position(value.0) * 100.0); thumb_bg.0 = thumb_color(disabled, hovered.0 | drag_state.dragging); } } } } } fn update_slider_style2( sliders: Query< ( Entity, &Hovered, &CoreSliderDragState, Has, ), With, >, children: Query<&Children>, mut thumbs: Query<(&mut BackgroundColor, Has), Without>, mut removed_disabled: RemovedComponents, ) { removed_disabled.read().for_each(|entity| { if let Ok((slider_ent, hovered, drag_state, disabled)) = sliders.get(entity) { for child in children.iter_descendants(slider_ent) { if let Ok((mut thumb_bg, is_thumb)) = thumbs.get_mut(child) { if is_thumb { thumb_bg.0 = thumb_color(disabled, hovered.0 | drag_state.dragging); } } } } }); } fn thumb_color(disabled: bool, hovered: bool) -> Color { match (disabled, hovered) { (true, _) => GRAY.into(), (false, true) => SLIDER_THUMB.lighter(0.3), _ => SLIDER_THUMB, } } /// Create a demo checkbox fn checkbox( asset_server: &AssetServer, caption: &str, on_change: Callback>, ) -> impl Bundle { ( Node { display: Display::Flex, flex_direction: FlexDirection::Row, justify_content: JustifyContent::FlexStart, align_items: AlignItems::Center, align_content: AlignContent::Center, column_gap: Val::Px(4.0), ..default() }, Name::new("Checkbox"), Hovered::default(), DemoCheckbox, CoreCheckbox { on_change }, TabIndex(0), Children::spawn(( Spawn(( // Checkbox outer Node { display: Display::Flex, width: Val::Px(16.0), height: Val::Px(16.0), border: UiRect::all(Val::Px(2.0)), ..default() }, BorderColor::all(CHECKBOX_OUTLINE), // Border color for the checkbox BorderRadius::all(Val::Px(3.0)), children![ // Checkbox inner ( Node { display: Display::Flex, width: Val::Px(8.0), height: Val::Px(8.0), position_type: PositionType::Absolute, left: Val::Px(2.0), top: Val::Px(2.0), ..default() }, BackgroundColor(CHECKBOX_CHECK), ), ], )), Spawn(( Text::new(caption), TextFont { font: asset_server.load("fonts/FiraSans-Bold.ttf"), font_size: 20.0, ..default() }, )), )), ) } // Update the checkbox's styles. fn update_checkbox_or_radio_style( mut q_checkbox: Query< (Has, &Hovered, Has, &Children), ( Or<(With, With)>, Or<( Added, Changed, Added, Added, )>, ), >, mut q_border_color: Query< (&mut BorderColor, &mut Children), (Without, Without), >, mut q_bg_color: Query<&mut BackgroundColor, (Without, Without)>, ) { for (checked, Hovered(is_hovering), is_disabled, children) in q_checkbox.iter_mut() { let Some(border_id) = children.first() else { continue; }; let Ok((mut border_color, border_children)) = q_border_color.get_mut(*border_id) else { continue; }; let Some(mark_id) = border_children.first() else { warn!("Checkbox does not have a mark entity."); continue; }; let Ok(mut mark_bg) = q_bg_color.get_mut(*mark_id) else { warn!("Checkbox mark entity lacking a background color."); continue; }; set_checkbox_or_radio_style( is_disabled, *is_hovering, checked, &mut border_color, &mut mark_bg, ); } } fn update_checkbox_or_radio_style2( mut q_checkbox: Query< (Has, &Hovered, Has, &Children), Or<(With, With)>, >, mut q_border_color: Query< (&mut BorderColor, &mut Children), (Without, Without), >, mut q_bg_color: Query< &mut BackgroundColor, (Without, Without, Without), >, mut removed_checked: RemovedComponents, mut removed_disabled: RemovedComponents, ) { removed_checked .read() .chain(removed_disabled.read()) .for_each(|entity| { if let Ok((checked, Hovered(is_hovering), is_disabled, children)) = q_checkbox.get_mut(entity) { let Some(border_id) = children.first() else { return; }; let Ok((mut border_color, border_children)) = q_border_color.get_mut(*border_id) else { return; }; let Some(mark_id) = border_children.first() else { warn!("Checkbox does not have a mark entity."); return; }; let Ok(mut mark_bg) = q_bg_color.get_mut(*mark_id) else { warn!("Checkbox mark entity lacking a background color."); return; }; set_checkbox_or_radio_style( is_disabled, *is_hovering, checked, &mut border_color, &mut mark_bg, ); } }); } fn set_checkbox_or_radio_style( disabled: bool, hovering: bool, checked: bool, border_color: &mut BorderColor, mark_bg: &mut BackgroundColor, ) { let color: Color = if disabled { // If the checkbox is disabled, use a lighter color CHECKBOX_OUTLINE.with_alpha(0.2) } else if hovering { // If hovering, use a lighter color CHECKBOX_OUTLINE.lighter(0.2) } else { // Default color for the checkbox CHECKBOX_OUTLINE }; // Update the background color of the check mark border_color.set_all(color); let mark_color: Color = match (disabled, checked) { (true, true) => CHECKBOX_CHECK.with_alpha(0.5), (false, true) => CHECKBOX_CHECK, (_, false) => Srgba::NONE.into(), }; if mark_bg.0 != mark_color { // Update the color of the check mark mark_bg.0 = mark_color; } } /// Create a demo radio group fn radio_group(asset_server: &AssetServer, on_change: Callback>) -> impl Bundle { ( Node { display: Display::Flex, flex_direction: FlexDirection::Column, align_items: AlignItems::Start, column_gap: Val::Px(4.0), ..default() }, Name::new("RadioGroup"), CoreRadioGroup { on_change }, TabIndex::default(), children![ (radio(asset_server, TrackClick::Drag, "Slider Drag"),), (radio(asset_server, TrackClick::Step, "Slider Step"),), (radio(asset_server, TrackClick::Snap, "Slider Snap"),) ], ) } /// Create a demo radio button fn radio(asset_server: &AssetServer, value: TrackClick, caption: &str) -> impl Bundle { ( Node { display: Display::Flex, flex_direction: FlexDirection::Row, justify_content: JustifyContent::FlexStart, align_items: AlignItems::Center, align_content: AlignContent::Center, column_gap: Val::Px(4.0), ..default() }, Name::new("RadioButton"), Hovered::default(), DemoRadio(value), CoreRadio, Children::spawn(( Spawn(( // Radio outer Node { display: Display::Flex, width: Val::Px(16.0), height: Val::Px(16.0), border: UiRect::all(Val::Px(2.0)), ..default() }, BorderColor::all(CHECKBOX_OUTLINE), // Border color for the checkbox BorderRadius::MAX, children![ // Radio inner ( Node { display: Display::Flex, width: Val::Px(8.0), height: Val::Px(8.0), position_type: PositionType::Absolute, left: Val::Px(2.0), top: Val::Px(2.0), ..default() }, BorderRadius::MAX, BackgroundColor(CHECKBOX_CHECK), ), ], )), Spawn(( Text::new(caption), TextFont { font: asset_server.load("fonts/FiraSans-Bold.ttf"), font_size: 20.0, ..default() }, )), )), ) } fn toggle_disabled( input: Res>, mut interaction_query: Query< (Entity, Has), Or<( With, With, With, With, )>, >, mut commands: Commands, ) { if input.just_pressed(KeyCode::KeyD) { for (entity, disabled) in &mut interaction_query { if disabled { info!("Widget enabled"); commands.entity(entity).remove::(); } else { info!("Widget disabled"); commands.entity(entity).insert(InteractionDisabled); } } } }