Path: blob/main/examples/ui/widgets/standard_widgets.rs
9331 views
//! This experimental example illustrates how to create widgets using the `bevy_ui_widgets` widget set.1//!2//! These widgets have no inherent styling, so this example also shows how to implement custom styles.3//!4//! The patterns shown here are likely to change substantially as the `bevy_ui_widgets` crate5//! matures, so please exercise caution if you are using this as a reference for your own code,6//! and note that there are still "user experience" issues with this API.78use bevy::{9color::palettes::basic::*,10input_focus::{11tab_navigation::{TabGroup, TabIndex, TabNavigationPlugin},12InputDispatchPlugin, InputFocus,13},14picking::hover::Hovered,15prelude::*,16ui::{Checked, InteractionDisabled, Pressed},17ui_widgets::{18checkbox_self_update, observe,19popover::{Popover, PopoverAlign, PopoverPlacement, PopoverSide},20Activate, Button, Checkbox, CoreSliderDragState, MenuAction, MenuButton, MenuEvent,21MenuItem, MenuPopup, RadioButton, RadioGroup, Slider, SliderRange, SliderThumb,22SliderValue, TrackClick, UiWidgetsPlugins, ValueChange,23},24};2526fn main() {27App::new()28.add_plugins((29DefaultPlugins,30UiWidgetsPlugins,31InputDispatchPlugin,32TabNavigationPlugin,33))34.insert_resource(DemoWidgetStates {35slider_value: 50.0,36slider_click: TrackClick::Snap,37})38.add_systems(Startup, setup)39.add_systems(40Update,41(42update_widget_values,43update_button_style,44update_button_style2,45update_slider_style.after(update_widget_values),46update_slider_style2.after(update_widget_values),47update_checkbox_or_radio_style.after(update_widget_values),48update_checkbox_or_radio_style2.after(update_widget_values),49update_menu_item_style,50update_menu_item_style2,51toggle_disabled,52),53)54.run();55}5657const NORMAL_BUTTON: Color = Color::srgb(0.15, 0.15, 0.15);58const HOVERED_BUTTON: Color = Color::srgb(0.25, 0.25, 0.25);59const PRESSED_BUTTON: Color = Color::srgb(0.35, 0.75, 0.35);60const SLIDER_TRACK: Color = Color::srgb(0.05, 0.05, 0.05);61const SLIDER_THUMB: Color = Color::srgb(0.35, 0.75, 0.35);62const ELEMENT_OUTLINE: Color = Color::srgb(0.45, 0.45, 0.45);63const ELEMENT_FILL: Color = Color::srgb(0.35, 0.75, 0.35);64const ELEMENT_FILL_DISABLED: Color = Color::srgb(0.5019608, 0.5019608, 0.5019608);6566/// Marker which identifies buttons with a particular style, in this case the "Demo style".67#[derive(Component)]68struct DemoButton;6970/// Marker which identifies sliders with a particular style.71#[derive(Component, Default)]72struct DemoSlider;7374/// Marker which identifies the slider's thumb element.75#[derive(Component, Default)]76struct DemoSliderThumb;7778/// Marker which identifies checkboxes with a particular style.79#[derive(Component, Default)]80struct DemoCheckbox;8182/// Marker which identifies a styled radio button. We'll use this to change the track click83/// behavior.84#[derive(Component, Default)]85struct DemoRadio(TrackClick);8687/// Menu anchor marker88#[derive(Component)]89struct DemoMenuAnchor;9091/// Menu button styling marker92#[derive(Component)]93struct DemoMenuButton;9495/// Menu item styling marker96#[derive(Component)]97struct DemoMenuItem;9899/// A struct to hold the state of various widgets shown in the demo.100///101/// While it is possible to use the widget's own state components as the source of truth,102/// in many cases widgets will be used to display dynamic data coming from deeper within the app,103/// using some kind of data-binding. This example shows how to maintain an external source of104/// truth for widget states.105#[derive(Resource)]106struct DemoWidgetStates {107slider_value: f32,108slider_click: TrackClick,109}110111/// Update the widget states based on the changing resource.112fn update_widget_values(113res: Res<DemoWidgetStates>,114mut sliders: Query<(Entity, &mut Slider), With<DemoSlider>>,115radios: Query<(Entity, &DemoRadio, Has<Checked>)>,116mut commands: Commands,117) {118if res.is_changed() {119for (slider_ent, mut slider) in sliders.iter_mut() {120commands121.entity(slider_ent)122.insert(SliderValue(res.slider_value));123slider.track_click = res.slider_click;124}125126for (radio_id, radio_value, checked) in radios.iter() {127let will_be_checked = radio_value.0 == res.slider_click;128if will_be_checked != checked {129if will_be_checked {130commands.entity(radio_id).insert(Checked);131} else {132commands.entity(radio_id).remove::<Checked>();133}134}135}136}137}138139fn setup(mut commands: Commands, assets: Res<AssetServer>) {140// ui camera141commands.spawn(Camera2d);142commands.spawn(demo_root(&assets));143}144145fn demo_root(asset_server: &AssetServer) -> impl Bundle {146(147Node {148width: percent(100),149height: percent(100),150align_items: AlignItems::Center,151justify_content: JustifyContent::Center,152display: Display::Flex,153flex_direction: FlexDirection::Column,154row_gap: px(10),155..default()156},157TabGroup::default(),158children![159(160button(asset_server),161observe(|_activate: On<Activate>| {162info!("Button clicked!");163}),164),165(166slider(0.0, 100.0, 50.0),167observe(168|value_change: On<ValueChange<f32>>,169mut widget_states: ResMut<DemoWidgetStates>| {170widget_states.slider_value = value_change.value;171},172)173),174(175checkbox(asset_server, "Checkbox"),176observe(checkbox_self_update)177),178(179radio_group(asset_server),180observe(181|value_change: On<ValueChange<Entity>>,182mut widget_states: ResMut<DemoWidgetStates>,183q_radios: Query<&DemoRadio>| {184if let Ok(radio) = q_radios.get(value_change.value) {185widget_states.slider_click = radio.0;186}187},188)189),190menu_button(asset_server),191Text::new("Press 'D' to toggle widget disabled states"),192],193)194}195196fn button(asset_server: &AssetServer) -> impl Bundle {197(198Node {199width: px(150),200height: px(65),201border: UiRect::all(px(5)),202border_radius: BorderRadius::MAX,203justify_content: JustifyContent::Center,204align_items: AlignItems::Center,205..default()206},207DemoButton,208Button,209Hovered::default(),210TabIndex(0),211BorderColor::all(Color::BLACK),212BackgroundColor(NORMAL_BUTTON),213children![(214Text::new("Button"),215TextFont {216font: asset_server.load("fonts/FiraSans-Bold.ttf").into(),217font_size: FontSize::Px(33.0),218..default()219},220TextColor(Color::srgb(0.9, 0.9, 0.9)),221TextShadow::default(),222)],223)224}225226fn menu_button(asset_server: &AssetServer) -> impl Bundle {227(228Node { ..default() },229DemoMenuAnchor,230observe(on_menu_event),231children![(232Node {233width: px(200),234height: px(65),235border: UiRect::all(px(5)),236box_sizing: BoxSizing::BorderBox,237justify_content: JustifyContent::SpaceBetween,238align_items: AlignItems::Center,239padding: UiRect::axes(px(16), px(0)),240border_radius: BorderRadius::all(px(5)),241..default()242},243DemoMenuButton,244MenuButton,245Hovered::default(),246TabIndex(0),247BorderColor::all(Color::BLACK),248BackgroundColor(NORMAL_BUTTON),249children![250(251Text::new("Menu"),252TextFont {253font: asset_server.load("fonts/FiraSans-Bold.ttf").into(),254font_size: FontSize::Px(33.0),255..default()256},257TextColor(Color::srgb(0.9, 0.9, 0.9)),258TextShadow::default(),259),260(261Node {262width: px(12),263height: px(12),264..default()265},266BackgroundColor(GRAY.into()),267)268],269)],270)271}272273fn update_button_style(274mut buttons: Query<275(276Has<Pressed>,277&Hovered,278Has<InteractionDisabled>,279&mut BackgroundColor,280&mut BorderColor,281&Children,282),283(284Or<(285Changed<Pressed>,286Changed<Hovered>,287Added<InteractionDisabled>,288)>,289With<DemoButton>,290),291>,292mut text_query: Query<&mut Text>,293) {294for (pressed, hovered, disabled, mut color, mut border_color, children) in &mut buttons {295let mut text = text_query.get_mut(children[0]).unwrap();296set_button_style(297disabled,298hovered.get(),299pressed,300&mut color,301&mut border_color,302&mut text,303);304}305}306307/// Supplementary system to detect removed marker components308fn update_button_style2(309mut buttons: Query<310(311Has<Pressed>,312&Hovered,313Has<InteractionDisabled>,314&mut BackgroundColor,315&mut BorderColor,316&Children,317),318With<DemoButton>,319>,320mut removed_depressed: RemovedComponents<Pressed>,321mut removed_disabled: RemovedComponents<InteractionDisabled>,322mut text_query: Query<&mut Text>,323) {324removed_depressed325.read()326.chain(removed_disabled.read())327.for_each(|entity| {328if let Ok((pressed, hovered, disabled, mut color, mut border_color, children)) =329buttons.get_mut(entity)330{331let mut text = text_query.get_mut(children[0]).unwrap();332set_button_style(333disabled,334hovered.get(),335pressed,336&mut color,337&mut border_color,338&mut text,339);340}341});342}343344fn set_button_style(345disabled: bool,346hovered: bool,347pressed: bool,348color: &mut BackgroundColor,349border_color: &mut BorderColor,350text: &mut Text,351) {352match (disabled, hovered, pressed) {353// Disabled button354(true, _, _) => {355**text = "Disabled".to_string();356*color = NORMAL_BUTTON.into();357border_color.set_all(GRAY);358}359360// Pressed and hovered button361(false, true, true) => {362**text = "Press".to_string();363*color = PRESSED_BUTTON.into();364border_color.set_all(RED);365}366367// Hovered, unpressed button368(false, true, false) => {369**text = "Hover".to_string();370*color = HOVERED_BUTTON.into();371border_color.set_all(WHITE);372}373374// Unhovered button (either pressed or not).375(false, false, _) => {376**text = "Button".to_string();377*color = NORMAL_BUTTON.into();378border_color.set_all(BLACK);379}380}381}382383/// Create a demo slider384fn slider(min: f32, max: f32, value: f32) -> impl Bundle {385(386Node {387display: Display::Flex,388flex_direction: FlexDirection::Column,389justify_content: JustifyContent::Center,390align_items: AlignItems::Stretch,391justify_items: JustifyItems::Center,392column_gap: px(4),393height: px(12),394width: percent(30),395..default()396},397Name::new("Slider"),398Hovered::default(),399DemoSlider,400Slider {401track_click: TrackClick::Snap,402},403SliderValue(value),404SliderRange::new(min, max),405TabIndex(0),406Children::spawn((407// Slider background rail408Spawn((409Node {410height: px(6),411border_radius: BorderRadius::all(px(3)),412..default()413},414BackgroundColor(SLIDER_TRACK), // Border color for the slider415)),416// Invisible track to allow absolute placement of thumb entity. This is narrower than417// the actual slider, which allows us to position the thumb entity using simple418// percentages, without having to measure the actual width of the slider thumb.419Spawn((420Node {421display: Display::Flex,422position_type: PositionType::Absolute,423left: px(0),424// Track is short by 12px to accommodate the thumb.425right: px(12),426top: px(0),427bottom: px(0),428..default()429},430children![(431// Thumb432DemoSliderThumb,433SliderThumb,434Node {435display: Display::Flex,436width: px(12),437height: px(12),438position_type: PositionType::Absolute,439left: percent(0), // This will be updated by the slider's value440border_radius: BorderRadius::MAX,441..default()442},443BackgroundColor(SLIDER_THUMB),444)],445)),446)),447)448}449450/// Update the visuals of the slider based on the slider state.451fn update_slider_style(452sliders: Query<453(454Entity,455&SliderValue,456&SliderRange,457&Hovered,458&CoreSliderDragState,459Has<InteractionDisabled>,460),461(462Or<(463Changed<SliderValue>,464Changed<SliderRange>,465Changed<Hovered>,466Changed<CoreSliderDragState>,467Added<InteractionDisabled>,468)>,469With<DemoSlider>,470),471>,472children: Query<&Children>,473mut thumbs: Query<(&mut Node, &mut BackgroundColor, Has<DemoSliderThumb>), Without<DemoSlider>>,474) {475for (slider_ent, value, range, hovered, drag_state, disabled) in sliders.iter() {476for child in children.iter_descendants(slider_ent) {477if let Ok((mut thumb_node, mut thumb_bg, is_thumb)) = thumbs.get_mut(child)478&& is_thumb479{480thumb_node.left = percent(range.thumb_position(value.0) * 100.0);481thumb_bg.0 = thumb_color(disabled, hovered.0 | drag_state.dragging);482}483}484}485}486487fn update_slider_style2(488sliders: Query<489(490Entity,491&Hovered,492&CoreSliderDragState,493Has<InteractionDisabled>,494),495With<DemoSlider>,496>,497children: Query<&Children>,498mut thumbs: Query<(&mut BackgroundColor, Has<DemoSliderThumb>), Without<DemoSlider>>,499mut removed_disabled: RemovedComponents<InteractionDisabled>,500) {501removed_disabled.read().for_each(|entity| {502if let Ok((slider_ent, hovered, drag_state, disabled)) = sliders.get(entity) {503for child in children.iter_descendants(slider_ent) {504if let Ok((mut thumb_bg, is_thumb)) = thumbs.get_mut(child)505&& is_thumb506{507thumb_bg.0 = thumb_color(disabled, hovered.0 | drag_state.dragging);508}509}510}511});512}513514fn thumb_color(disabled: bool, hovered: bool) -> Color {515match (disabled, hovered) {516(true, _) => ELEMENT_FILL_DISABLED,517518(false, true) => SLIDER_THUMB.lighter(0.3),519520_ => SLIDER_THUMB,521}522}523524/// Create a demo checkbox525fn checkbox(asset_server: &AssetServer, caption: &str) -> impl Bundle {526(527Node {528display: Display::Flex,529flex_direction: FlexDirection::Row,530justify_content: JustifyContent::FlexStart,531align_items: AlignItems::Center,532align_content: AlignContent::Center,533column_gap: px(4),534..default()535},536Name::new("Checkbox"),537Hovered::default(),538DemoCheckbox,539Checkbox,540TabIndex(0),541Children::spawn((542Spawn((543// Checkbox outer544Node {545display: Display::Flex,546width: px(16),547height: px(16),548border: UiRect::all(px(2)),549border_radius: BorderRadius::all(px(3)),550..default()551},552BorderColor::all(ELEMENT_OUTLINE), // Border color for the checkbox553children![554// Checkbox inner555(556Node {557display: Display::Flex,558width: px(8),559height: px(8),560position_type: PositionType::Absolute,561left: px(2),562top: px(2),563..default()564},565BackgroundColor(ELEMENT_FILL),566),567],568)),569Spawn((570Text::new(caption),571TextFont {572font: asset_server.load("fonts/FiraSans-Bold.ttf").into(),573font_size: FontSize::Px(20.0),574..default()575},576)),577)),578)579}580581// Update the element's styles.582fn update_checkbox_or_radio_style(583mut q_checkbox: Query<584(Has<Checked>, &Hovered, Has<InteractionDisabled>, &Children),585(586Or<(With<DemoCheckbox>, With<DemoRadio>)>,587Or<(588Added<DemoCheckbox>,589Changed<Hovered>,590Added<Checked>,591Added<InteractionDisabled>,592)>,593),594>,595mut q_border_color: Query<596(&mut BorderColor, &mut Children),597(Without<DemoCheckbox>, Without<DemoRadio>),598>,599mut q_bg_color: Query<&mut BackgroundColor, (Without<DemoCheckbox>, Without<Children>)>,600) {601for (checked, Hovered(is_hovering), is_disabled, children) in q_checkbox.iter_mut() {602let Some(border_id) = children.first() else {603continue;604};605606let Ok((mut border_color, border_children)) = q_border_color.get_mut(*border_id) else {607continue;608};609610let Some(mark_id) = border_children.first() else {611warn!("Checkbox does not have a mark entity.");612continue;613};614615let Ok(mut mark_bg) = q_bg_color.get_mut(*mark_id) else {616warn!("Checkbox mark entity lacking a background color.");617continue;618};619620set_checkbox_or_radio_style(621is_disabled,622*is_hovering,623checked,624&mut border_color,625&mut mark_bg,626);627}628}629630fn update_checkbox_or_radio_style2(631mut q_checkbox: Query<632(Has<Checked>, &Hovered, Has<InteractionDisabled>, &Children),633Or<(With<DemoCheckbox>, With<DemoRadio>)>,634>,635mut q_border_color: Query<636(&mut BorderColor, &mut Children),637(Without<DemoCheckbox>, Without<DemoRadio>),638>,639mut q_bg_color: Query<640&mut BackgroundColor,641(Without<DemoCheckbox>, Without<DemoRadio>, Without<Children>),642>,643mut removed_checked: RemovedComponents<Checked>,644mut removed_disabled: RemovedComponents<InteractionDisabled>,645) {646removed_checked647.read()648.chain(removed_disabled.read())649.for_each(|entity| {650if let Ok((checked, Hovered(is_hovering), is_disabled, children)) =651q_checkbox.get_mut(entity)652{653let Some(border_id) = children.first() else {654return;655};656657let Ok((mut border_color, border_children)) = q_border_color.get_mut(*border_id)658else {659return;660};661662let Some(mark_id) = border_children.first() else {663warn!("Checkbox does not have a mark entity.");664return;665};666667let Ok(mut mark_bg) = q_bg_color.get_mut(*mark_id) else {668warn!("Checkbox mark entity lacking a background color.");669return;670};671672set_checkbox_or_radio_style(673is_disabled,674*is_hovering,675checked,676&mut border_color,677&mut mark_bg,678);679}680});681}682683fn set_checkbox_or_radio_style(684disabled: bool,685hovering: bool,686checked: bool,687border_color: &mut BorderColor,688mark_bg: &mut BackgroundColor,689) {690let color: Color = if disabled {691// If the element is disabled, use a lighter color692ELEMENT_OUTLINE.with_alpha(0.2)693} else if hovering {694// If hovering, use a lighter color695ELEMENT_OUTLINE.lighter(0.2)696} else {697// Default color for the element698ELEMENT_OUTLINE699};700701// Update the background color of the element702border_color.set_all(color);703704let mark_color: Color = match (disabled, checked) {705(true, true) => ELEMENT_FILL_DISABLED,706(false, true) => ELEMENT_FILL,707(_, false) => Srgba::NONE.into(),708};709710if mark_bg.0 != mark_color {711// Update the color of the element712mark_bg.0 = mark_color;713}714}715716/// Create a demo radio group717fn radio_group(asset_server: &AssetServer) -> impl Bundle {718(719Node {720display: Display::Flex,721flex_direction: FlexDirection::Column,722align_items: AlignItems::Start,723column_gap: px(4),724..default()725},726Name::new("RadioGroup"),727RadioGroup,728TabIndex::default(),729children![730(radio(asset_server, TrackClick::Drag, "Slider Drag"),),731(radio(asset_server, TrackClick::Step, "Slider Step"),),732(radio(asset_server, TrackClick::Snap, "Slider Snap"),)733],734)735}736737/// Create a demo radio button738fn radio(asset_server: &AssetServer, value: TrackClick, caption: &str) -> impl Bundle {739(740Node {741display: Display::Flex,742flex_direction: FlexDirection::Row,743justify_content: JustifyContent::FlexStart,744align_items: AlignItems::Center,745align_content: AlignContent::Center,746column_gap: px(4),747..default()748},749Name::new("RadioButton"),750Hovered::default(),751DemoRadio(value),752RadioButton,753Children::spawn((754Spawn((755// Radio outer756Node {757display: Display::Flex,758width: px(16),759height: px(16),760border: UiRect::all(px(2)),761border_radius: BorderRadius::MAX,762..default()763},764BorderColor::all(ELEMENT_OUTLINE), // Border color for the radio button765children![766// Radio inner767(768Node {769display: Display::Flex,770width: px(8),771height: px(8),772position_type: PositionType::Absolute,773left: px(2),774top: px(2),775border_radius: BorderRadius::MAX,776..default()777},778BackgroundColor(ELEMENT_FILL),779),780],781)),782Spawn((783Text::new(caption),784TextFont {785font: asset_server.load("fonts/FiraSans-Bold.ttf").into(),786font_size: FontSize::Px(20.0),787..default()788},789)),790)),791)792}793794fn on_menu_event(795menu_event: On<MenuEvent>,796q_anchor: Single<(Entity, &Children), With<DemoMenuAnchor>>,797q_popup: Query<Entity, With<MenuPopup>>,798assets: Res<AssetServer>,799mut focus: ResMut<InputFocus>,800mut commands: Commands,801) {802let (anchor, children) = q_anchor.into_inner();803let popup = children.iter().find_map(|c| q_popup.get(c).ok());804info!("Menu action: {:?}", menu_event.action);805match menu_event.action {806MenuAction::Open => {807if popup.is_none() {808spawn_menu(anchor, assets, commands);809}810}811MenuAction::Toggle => match popup {812Some(popup) => commands.entity(popup).despawn(),813None => spawn_menu(anchor, assets, commands),814},815MenuAction::Close | MenuAction::CloseAll => {816if let Some(popup) = popup {817commands.entity(popup).despawn();818}819}820MenuAction::FocusRoot => {821focus.0 = Some(anchor);822}823}824}825826fn spawn_menu(anchor: Entity, assets: Res<AssetServer>, mut commands: Commands) {827let menu = commands828.spawn((829Node {830display: Display::Flex,831flex_direction: FlexDirection::Column,832min_height: px(10.),833min_width: percent(100),834border: UiRect::all(px(1)),835position_type: PositionType::Absolute,836..default()837},838MenuPopup::default(),839Visibility::Hidden, // Will be visible after positioning840BorderColor::all(GREEN),841BackgroundColor(GRAY.into()),842BoxShadow::new(843Srgba::BLACK.with_alpha(0.9).into(),844px(0),845px(0),846px(1),847px(4),848),849GlobalZIndex(100),850Popover {851positions: vec![852PopoverPlacement {853side: PopoverSide::Bottom,854align: PopoverAlign::Start,855gap: 2.0,856},857PopoverPlacement {858side: PopoverSide::Top,859align: PopoverAlign::Start,860gap: 2.0,861},862],863window_margin: 10.0,864},865OverrideClip,866children![867menu_item(&assets),868menu_item(&assets),869menu_item(&assets),870menu_item(&assets)871],872))873.id();874commands.entity(anchor).add_child(menu);875}876877fn menu_item(asset_server: &AssetServer) -> impl Bundle {878(879Node {880padding: UiRect::axes(px(8), px(2)),881justify_content: JustifyContent::Center,882align_items: AlignItems::Start,883..default()884},885DemoMenuItem,886MenuItem,887Hovered::default(),888TabIndex(0),889BackgroundColor(NORMAL_BUTTON),890children![(891Text::new("Menu Item"),892TextFont {893font: asset_server.load("fonts/FiraSans-Bold.ttf").into(),894font_size: FontSize::Px(33.0),895..default()896},897TextColor(Color::srgb(0.9, 0.9, 0.9)),898TextShadow::default(),899)],900)901}902903fn update_menu_item_style(904mut buttons: Query<905(906Has<Pressed>,907&Hovered,908Has<InteractionDisabled>,909&mut BackgroundColor,910),911(912Or<(913Changed<Pressed>,914Changed<Hovered>,915Added<InteractionDisabled>,916)>,917With<DemoMenuItem>,918),919>,920) {921for (pressed, hovered, disabled, mut color) in &mut buttons {922set_menu_item_style(disabled, hovered.get(), pressed, &mut color);923}924}925926/// Supplementary system to detect removed marker components927fn update_menu_item_style2(928mut buttons: Query<929(930Has<Pressed>,931&Hovered,932Has<InteractionDisabled>,933&mut BackgroundColor,934),935With<DemoMenuItem>,936>,937mut removed_depressed: RemovedComponents<Pressed>,938mut removed_disabled: RemovedComponents<InteractionDisabled>,939) {940removed_depressed941.read()942.chain(removed_disabled.read())943.for_each(|entity| {944if let Ok((pressed, hovered, disabled, mut color)) = buttons.get_mut(entity) {945set_menu_item_style(disabled, hovered.get(), pressed, &mut color);946}947});948}949950fn set_menu_item_style(disabled: bool, hovered: bool, pressed: bool, color: &mut BackgroundColor) {951match (disabled, hovered, pressed) {952// Pressed and hovered menu item953(false, true, true) => {954*color = PRESSED_BUTTON.into();955}956957// Hovered, unpressed menu item958(false, true, false) => {959*color = HOVERED_BUTTON.into();960}961962// Unhovered menu item (either pressed or not).963_ => {964*color = NORMAL_BUTTON.into();965}966}967}968969fn toggle_disabled(970input: Res<ButtonInput<KeyCode>>,971mut interaction_query: Query<972(Entity, Has<InteractionDisabled>),973Or<(974With<Button>,975With<MenuButton>,976With<Slider>,977With<Checkbox>,978With<RadioButton>,979)>,980>,981mut commands: Commands,982) {983if input.just_pressed(KeyCode::KeyD) {984for (entity, disabled) in &mut interaction_query {985if disabled {986info!("Widget enabled");987commands.entity(entity).remove::<InteractionDisabled>();988} else {989info!("Widget disabled");990commands.entity(entity).insert(InteractionDisabled);991}992}993}994}995996997