Path: blob/main/examples/ui/widgets/standard_widgets_observers.rs
9334 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::*,8input_focus::{9tab_navigation::{TabGroup, TabIndex, TabNavigationPlugin},10InputDispatchPlugin,11},12picking::hover::Hovered,13prelude::*,14reflect::Is,15ui::{Checked, InteractionDisabled, Pressed},16ui_widgets::{17checkbox_self_update, observe, Activate, Button, Checkbox, Slider, SliderRange,18SliderThumb, SliderValue, UiWidgetsPlugins, ValueChange,19},20};2122fn main() {23App::new()24.add_plugins((25DefaultPlugins,26UiWidgetsPlugins,27InputDispatchPlugin,28TabNavigationPlugin,29))30.insert_resource(DemoWidgetStates { slider_value: 50.0 })31.add_systems(Startup, setup)32.add_observer(button_on_interaction::<Add, Pressed>)33.add_observer(button_on_interaction::<Remove, Pressed>)34.add_observer(button_on_interaction::<Add, InteractionDisabled>)35.add_observer(button_on_interaction::<Remove, InteractionDisabled>)36.add_observer(button_on_interaction::<Insert, Hovered>)37.add_observer(slider_on_interaction::<Add, InteractionDisabled>)38.add_observer(slider_on_interaction::<Remove, InteractionDisabled>)39.add_observer(slider_on_interaction::<Insert, Hovered>)40.add_observer(slider_on_change_value::<SliderValue>)41.add_observer(slider_on_change_value::<SliderRange>)42.add_observer(checkbox_on_interaction::<Add, InteractionDisabled>)43.add_observer(checkbox_on_interaction::<Remove, InteractionDisabled>)44.add_observer(checkbox_on_interaction::<Insert, Hovered>)45.add_observer(checkbox_on_interaction::<Add, Checked>)46.add_observer(checkbox_on_interaction::<Remove, Checked>)47.add_systems(Update, (update_widget_values, toggle_disabled))48.run();49}5051const NORMAL_BUTTON: Color = Color::srgb(0.15, 0.15, 0.15);52const HOVERED_BUTTON: Color = Color::srgb(0.25, 0.25, 0.25);53const PRESSED_BUTTON: Color = Color::srgb(0.35, 0.75, 0.35);54const SLIDER_TRACK: Color = Color::srgb(0.05, 0.05, 0.05);55const SLIDER_THUMB: Color = Color::srgb(0.35, 0.75, 0.35);56const CHECKBOX_OUTLINE: Color = Color::srgb(0.45, 0.45, 0.45);57const CHECKBOX_CHECK: Color = Color::srgb(0.35, 0.75, 0.35);5859/// Marker which identifies buttons with a particular style, in this case the "Demo style".60#[derive(Component)]61struct DemoButton;6263/// Marker which identifies sliders with a particular style.64#[derive(Component, Default)]65struct DemoSlider;6667/// Marker which identifies the slider's thumb element.68#[derive(Component, Default)]69struct DemoSliderThumb;7071/// Marker which identifies checkboxes with a particular style.72#[derive(Component, Default)]73struct DemoCheckbox;7475/// A struct to hold the state of various widgets shown in the demo.76///77/// While it is possible to use the widget's own state components as the source of truth,78/// in many cases widgets will be used to display dynamic data coming from deeper within the app,79/// using some kind of data-binding. This example shows how to maintain an external source of80/// truth for widget states.81#[derive(Resource)]82struct DemoWidgetStates {83slider_value: f32,84}8586fn setup(mut commands: Commands, assets: Res<AssetServer>) {87// ui camera88commands.spawn(Camera2d);89commands.spawn(demo_root(&assets));90}9192fn demo_root(asset_server: &AssetServer) -> impl Bundle {93(94Node {95width: percent(100),96height: percent(100),97align_items: AlignItems::Center,98justify_content: JustifyContent::Center,99display: Display::Flex,100flex_direction: FlexDirection::Column,101row_gap: px(10),102..default()103},104TabGroup::default(),105children![106(107button(asset_server),108observe(|_activate: On<Activate>| {109info!("Button clicked!");110}),111),112(113slider(0.0, 100.0, 50.0),114observe(115|value_change: On<ValueChange<f32>>,116mut widget_states: ResMut<DemoWidgetStates>| {117widget_states.slider_value = value_change.value;118},119)120),121(122checkbox(asset_server, "Checkbox"),123observe(checkbox_self_update),124),125Text::new("Press 'D' to toggle widget disabled states"),126],127)128}129130fn button(asset_server: &AssetServer) -> impl Bundle {131(132Node {133width: px(150),134height: px(65),135border: UiRect::all(px(5)),136justify_content: JustifyContent::Center,137align_items: AlignItems::Center,138border_radius: BorderRadius::MAX,139..default()140},141DemoButton,142Button,143Hovered::default(),144TabIndex(0),145BorderColor::all(Color::BLACK),146BackgroundColor(NORMAL_BUTTON),147children![(148Text::new("Button"),149TextFont {150font: asset_server.load("fonts/FiraSans-Bold.ttf").into(),151font_size: FontSize::Px(33.0),152..default()153},154TextColor(Color::srgb(0.9, 0.9, 0.9)),155TextShadow::default(),156)],157)158}159160fn button_on_interaction<E: EntityEvent, C: Component>(161event: On<E, C>,162mut buttons: Query<163(164&Hovered,165Has<InteractionDisabled>,166Has<Pressed>,167&mut BackgroundColor,168&mut BorderColor,169&Children,170),171With<DemoButton>,172>,173mut text_query: Query<&mut Text>,174) {175if let Ok((hovered, disabled, pressed, mut color, mut border_color, children)) =176buttons.get_mut(event.event_target())177{178if children.is_empty() {179return;180}181let Ok(mut text) = text_query.get_mut(children[0]) else {182return;183};184let hovered = hovered.get();185// These "removal event checks" exist because the `Remove` event is triggered _before_ the component is actually186// removed, meaning it still shows up in the query. We're investigating the best way to improve this scenario.187let pressed = pressed && !(E::is::<Remove>() && C::is::<Pressed>());188let disabled = disabled && !(E::is::<Remove>() && C::is::<InteractionDisabled>());189match (disabled, hovered, pressed) {190// Disabled button191(true, _, _) => {192**text = "Disabled".to_string();193*color = NORMAL_BUTTON.into();194border_color.set_all(GRAY);195}196197// Pressed and hovered button198(false, true, true) => {199**text = "Press".to_string();200*color = PRESSED_BUTTON.into();201border_color.set_all(RED);202}203204// Hovered, unpressed button205(false, true, false) => {206**text = "Hover".to_string();207*color = HOVERED_BUTTON.into();208border_color.set_all(WHITE);209}210211// Unhovered button (either pressed or not).212(false, false, _) => {213**text = "Button".to_string();214*color = NORMAL_BUTTON.into();215border_color.set_all(BLACK);216}217}218}219}220221/// Create a demo slider222fn slider(min: f32, max: f32, value: f32) -> impl Bundle {223(224Node {225display: Display::Flex,226flex_direction: FlexDirection::Column,227justify_content: JustifyContent::Center,228align_items: AlignItems::Stretch,229justify_items: JustifyItems::Center,230column_gap: px(4),231height: px(12),232width: percent(30),233..default()234},235Name::new("Slider"),236Hovered::default(),237DemoSlider,238Slider::default(),239SliderValue(value),240SliderRange::new(min, max),241TabIndex(0),242Children::spawn((243// Slider background rail244Spawn((245Node {246height: px(6),247border_radius: BorderRadius::all(px(3)),248..default()249},250BackgroundColor(SLIDER_TRACK), // Border color for the checkbox251)),252// Invisible track to allow absolute placement of thumb entity. This is narrower than253// the actual slider, which allows us to position the thumb entity using simple254// percentages, without having to measure the actual width of the slider thumb.255Spawn((256Node {257display: Display::Flex,258position_type: PositionType::Absolute,259left: px(0),260// Track is short by 12px to accommodate the thumb.261right: px(12),262top: px(0),263bottom: px(0),264..default()265},266children![(267// Thumb268DemoSliderThumb,269SliderThumb,270Node {271display: Display::Flex,272width: px(12),273height: px(12),274position_type: PositionType::Absolute,275left: percent(0), // This will be updated by the slider's value276border_radius: BorderRadius::MAX,277..default()278},279BackgroundColor(SLIDER_THUMB),280)],281)),282)),283)284}285286fn slider_on_interaction<E: EntityEvent, C: Component>(287event: On<E, C>,288sliders: Query<(Entity, &Hovered, Has<InteractionDisabled>), With<DemoSlider>>,289children: Query<&Children>,290mut thumbs: Query<(&mut BackgroundColor, Has<DemoSliderThumb>), Without<DemoSlider>>,291) {292if let Ok((slider_ent, hovered, disabled)) = sliders.get(event.event_target()) {293// These "removal event checks" exist because the `Remove` event is triggered _before_ the component is actually294// removed, meaning it still shows up in the query. We're investigating the best way to improve this scenario.295let disabled = disabled && !(E::is::<Remove>() && C::is::<InteractionDisabled>());296for child in children.iter_descendants(slider_ent) {297if let Ok((mut thumb_bg, is_thumb)) = thumbs.get_mut(child)298&& is_thumb299{300thumb_bg.0 = thumb_color(disabled, hovered.0);301}302}303}304}305306fn slider_on_change_value<C: Component>(307insert: On<Insert, C>,308sliders: Query<(Entity, &SliderValue, &SliderRange), With<DemoSlider>>,309children: Query<&Children>,310mut thumbs: Query<(&mut Node, Has<DemoSliderThumb>), Without<DemoSlider>>,311) {312if let Ok((slider_ent, value, range)) = sliders.get(insert.entity) {313for child in children.iter_descendants(slider_ent) {314if let Ok((mut thumb_node, is_thumb)) = thumbs.get_mut(child)315&& is_thumb316{317thumb_node.left = percent(range.thumb_position(value.0) * 100.0);318}319}320}321}322323fn thumb_color(disabled: bool, hovered: bool) -> Color {324match (disabled, hovered) {325(true, _) => GRAY.into(),326327(false, true) => SLIDER_THUMB.lighter(0.3),328329_ => SLIDER_THUMB,330}331}332333/// Create a demo checkbox334fn checkbox(asset_server: &AssetServer, caption: &str) -> impl Bundle {335(336Node {337display: Display::Flex,338flex_direction: FlexDirection::Row,339justify_content: JustifyContent::FlexStart,340align_items: AlignItems::Center,341align_content: AlignContent::Center,342column_gap: px(4),343..default()344},345Name::new("Checkbox"),346Hovered::default(),347DemoCheckbox,348Checkbox,349TabIndex(0),350Children::spawn((351Spawn((352// Checkbox outer353Node {354display: Display::Flex,355width: px(16),356height: px(16),357border: UiRect::all(px(2)),358border_radius: BorderRadius::all(px(3)),359..default()360},361BorderColor::all(CHECKBOX_OUTLINE), // Border color for the checkbox362children![363// Checkbox inner364(365Node {366display: Display::Flex,367width: px(8),368height: px(8),369position_type: PositionType::Absolute,370left: px(2),371top: px(2),372..default()373},374BackgroundColor(Srgba::NONE.into()),375),376],377)),378Spawn((379Text::new(caption),380TextFont {381font: asset_server.load("fonts/FiraSans-Bold.ttf").into(),382font_size: FontSize::Px(20.0),383..default()384},385)),386)),387)388}389390fn checkbox_on_interaction<E: EntityEvent, C: Component>(391event: On<E, C>,392checkboxes: Query<393(&Hovered, Has<InteractionDisabled>, Has<Checked>, &Children),394With<DemoCheckbox>,395>,396mut borders: Query<(&mut BorderColor, &mut Children), Without<DemoCheckbox>>,397mut marks: Query<&mut BackgroundColor, (Without<DemoCheckbox>, Without<Children>)>,398) {399if let Ok((hovered, disabled, checked, children)) = checkboxes.get(event.event_target()) {400let hovered = hovered.get();401// These "removal event checks" exist because the `Remove` event is triggered _before_ the component is actually402// removed, meaning it still shows up in the query. We're investigating the best way to improve this scenario.403let checked = checked && !(E::is::<Remove>() && C::is::<Checked>());404let disabled = disabled && !(E::is::<Remove>() && C::is::<InteractionDisabled>());405406let Some(border_id) = children.first() else {407return;408};409410let Ok((mut border_color, border_children)) = borders.get_mut(*border_id) else {411return;412};413414let Some(mark_id) = border_children.first() else {415warn!("Checkbox does not have a mark entity.");416return;417};418419let Ok(mut mark_bg) = marks.get_mut(*mark_id) else {420warn!("Checkbox mark entity lacking a background color.");421return;422};423424let color: Color = if disabled {425// If the checkbox is disabled, use a lighter color426CHECKBOX_OUTLINE.with_alpha(0.2)427} else if hovered {428// If hovering, use a lighter color429CHECKBOX_OUTLINE.lighter(0.2)430} else {431// Default color for the checkbox432CHECKBOX_OUTLINE433};434435// Update the background color of the check mark436border_color.set_all(color);437438let mark_color: Color = match (disabled, checked) {439(true, true) => CHECKBOX_CHECK.with_alpha(0.5),440(false, true) => CHECKBOX_CHECK,441(_, false) => Srgba::NONE.into(),442};443444if mark_bg.0 != mark_color {445// Update the color of the check mark446mark_bg.0 = mark_color;447}448}449}450451/// Update the widget states based on the changing resource.452fn update_widget_values(453res: Res<DemoWidgetStates>,454mut sliders: Query<Entity, With<DemoSlider>>,455mut commands: Commands,456) {457if res.is_changed() {458for slider_ent in sliders.iter_mut() {459commands460.entity(slider_ent)461.insert(SliderValue(res.slider_value));462}463}464}465466fn toggle_disabled(467input: Res<ButtonInput<KeyCode>>,468mut interaction_query: Query<469(Entity, Has<InteractionDisabled>),470Or<(With<Button>, With<Slider>, With<Checkbox>)>,471>,472mut commands: Commands,473) {474if input.just_pressed(KeyCode::KeyD) {475for (entity, disabled) in &mut interaction_query {476if disabled {477info!("Widget enabled");478commands.entity(entity).remove::<InteractionDisabled>();479} else {480info!("Widget disabled");481commands.entity(entity).insert(InteractionDisabled);482}483}484}485}486487488