From 5b3471ea35bce7d4fb89f49a3722d428db353d77 Mon Sep 17 00:00:00 2001 From: John Payne <20407779+johngpayne@users.noreply.github.com> Date: Mon, 22 Jun 2026 22:37:10 +0100 Subject: [PATCH 1/4] Theme awareness fixes for disclosure toggle, number input slider, slider itself, icon (uses text color). Added an icon_untinted, but its unused as everything I could see wanted the tint. --- .../src/controls/disclosure_toggle.rs | 65 +++++++------------ crates/bevy_feathers/src/controls/mod.rs | 1 + .../src/controls/number_input.rs | 49 +++++++++++++- crates/bevy_feathers/src/controls/slider.rs | 33 ++++++++++ crates/bevy_feathers/src/display/icon.rs | 36 ++++++++++ crates/bevy_feathers/src/lib.rs | 18 +++-- 6 files changed, 155 insertions(+), 47 deletions(-) diff --git a/crates/bevy_feathers/src/controls/disclosure_toggle.rs b/crates/bevy_feathers/src/controls/disclosure_toggle.rs index d7ab8468b4cdb..10b1333bd963d 100644 --- a/crates/bevy_feathers/src/controls/disclosure_toggle.rs +++ b/crates/bevy_feathers/src/controls/disclosure_toggle.rs @@ -1,11 +1,6 @@ use bevy_app::{App, Plugin, PreUpdate}; use bevy_ecs::{ - hierarchy::Children, - lifecycle::RemovedComponents, - query::{Added, Has, Or, With}, - reflect::ReflectComponent, - schedule::IntoScheduleConfigs, - system::{Query, Res}, + entity::Entity, hierarchy::Children, lifecycle::RemovedComponents, query::{Added, Has, Or, With}, reflect::ReflectComponent, schedule::IntoScheduleConfigs, system::{Commands, Query}, }; use bevy_input_focus::tab_navigation::TabIndex; use bevy_math::Rot2; @@ -14,15 +9,14 @@ use bevy_reflect::std_traits::ReflectDefault; use bevy_reflect::Reflect; use bevy_scene::{bsn, Scene, SceneComponent}; use bevy_ui::{ - px, widget::ImageNode, AlignItems, Checked, Display, InteractionDisabled, JustifyContent, Node, + px, AlignItems, Checked, Display, InteractionDisabled, JustifyContent, Node, UiTransform, }; use bevy_ui_widgets::Checkbox; use bevy_window::SystemCursorIcon; use crate::{ - constants::icons, cursor::EntityCursor, display::icon, focus::FocusIndicator, theme::UiTheme, - tokens, + constants::icons, cursor::EntityCursor, display::icon, focus::FocusIndicator, theme::InheritableThemeTextColor, tokens, }; /// A toggle button which shows a chevron that points either right or down, used to expand or @@ -47,6 +41,7 @@ impl FeathersDisclosureToggle { Checkbox EntityCursor::System(SystemCursorIcon::Pointer) FocusIndicator + InheritableThemeTextColor(tokens::BUTTON_TEXT) TabIndex(0) Children [ icon(icons::CHEVRON_RIGHT) @@ -58,32 +53,27 @@ impl FeathersDisclosureToggle { fn update_toggle_styles( mut q_toggle: Query< ( + Entity, Has, Has, &mut UiTransform, - &Children, + &InheritableThemeTextColor, ), ( With, Or<(Added, Added, Added)>, ), >, - mut q_icon: Query<&mut ImageNode>, - theme: Res, + mut commands: Commands, ) { - for (disabled, checked, mut transform, children) in q_toggle.iter_mut() { - let Some(child_id) = children.first() else { - continue; - }; - let Ok(mut icon_child) = q_icon.get_mut(*child_id) else { - continue; - }; + for (ent, disabled, checked, mut transform, text_color) in q_toggle.iter_mut() { set_toggle_styles( + ent, disabled, checked, transform.as_mut(), - &mut icon_child, - &theme, + text_color, + &mut commands, ); } } @@ -94,53 +84,48 @@ fn update_toggle_styles_remove( Has, Has, &mut UiTransform, - &Children, + &InheritableThemeTextColor, ), With, >, - mut q_icon: Query<&mut ImageNode>, mut removed_disabled: RemovedComponents, mut removed_checked: RemovedComponents, - theme: Res, + mut commands: Commands, ) { removed_disabled .read() .chain(removed_checked.read()) .for_each(|ent| { - if let Ok((disabled, checked, mut transform, children)) = q_toggle.get_mut(ent) { - let Some(child_id) = children.first() else { - return; - }; - let Ok(mut icon_child) = q_icon.get_mut(*child_id) else { - return; - }; + if let Ok((disabled, checked, mut transform, text_color)) = q_toggle.get_mut(ent) { set_toggle_styles( + ent, disabled, checked, transform.as_mut(), - &mut icon_child, - &theme, + text_color, + &mut commands, ); } }); } fn set_toggle_styles( + entity: Entity, disabled: bool, checked: bool, transform: &mut UiTransform, - image_node: &mut ImageNode, - theme: &Res<'_, UiTheme>, + text_color: &InheritableThemeTextColor, + commands: &mut Commands, ) { // It's effectively the same color as the caption of a "plain" variant tool button with an icon. - let icon_color = match disabled { - true => theme.color(&tokens::BUTTON_TEXT_DISABLED), - false => theme.color(&tokens::BUTTON_TEXT), + let new_text_color_token = match disabled { + true => tokens::BUTTON_TEXT_DISABLED, + false => tokens::BUTTON_TEXT, }; // Change icon color - if image_node.color != icon_color { - image_node.color = icon_color; + if new_text_color_token != text_color.0 { + commands.entity(entity).insert(InheritableThemeTextColor(new_text_color_token)); } match checked { diff --git a/crates/bevy_feathers/src/controls/mod.rs b/crates/bevy_feathers/src/controls/mod.rs index 0af6b51068d04..1ed35ecaf7b2f 100644 --- a/crates/bevy_feathers/src/controls/mod.rs +++ b/crates/bevy_feathers/src/controls/mod.rs @@ -53,6 +53,7 @@ impl Plugin for ControlsPlugin { DisclosureTogglePlugin, ListViewPlugin, MenuPlugin, + NumberInputPlugin, RadioPlugin, ScrollbarPlugin, SliderPlugin, diff --git a/crates/bevy_feathers/src/controls/number_input.rs b/crates/bevy_feathers/src/controls/number_input.rs index eebcb86d986e2..0f385a87e4f58 100644 --- a/crates/bevy_feathers/src/controls/number_input.rs +++ b/crates/bevy_feathers/src/controls/number_input.rs @@ -1,8 +1,9 @@ use std::{f32::consts::PI, ops::Range}; -use bevy_app::PropagateOver; +use bevy_app::{Plugin, PreUpdate, PropagateOver}; use bevy_color::Color; use bevy_ecs::{ + change_detection::DetectChanges, component::Component, entity::Entity, event::EntityEvent, @@ -11,6 +12,7 @@ use bevy_ecs::{ observer::On, query::{Has, With}, reflect::ReflectComponent, + schedule::IntoScheduleConfigs, system::{Commands, Query, Res, ResMut}, }; use bevy_input::{ @@ -26,6 +28,7 @@ use bevy_picking::{ events::{Cancel, Drag, DragEnd, DragStart, Pointer, Press, Release}, hover::Hovered, pointer::PointerButton, + PickingSystems, }; use bevy_reflect::std_traits::ReflectDefault; use bevy_reflect::Reflect; @@ -1190,3 +1193,47 @@ fn trigger_value_change( }), } } + +/// Re-apply the slidebar gradient colors for every number input when the theme changes. +fn update_slidebar_styles_theme( + q_children: Query<&Children>, + q_number_input: Query<(Entity, Has), With>, + mut q_text_input: Query<(&Hovered, &mut BackgroundGradient)>, + theme: Res, + mut commands: Commands, +) { + if !theme.is_changed() { + return; + } + for (root_entity, is_disabled) in q_number_input.iter() { + let Some(text_entity) = q_children + .iter_descendants(root_entity) + .find(|e| q_text_input.contains(*e)) + else { + continue; + }; + if let Ok((&Hovered(hovered), mut gradient)) = q_text_input.get_mut(text_entity) { + set_slidebar_styles( + text_entity, + &theme, + is_disabled, + false, + hovered, + &mut gradient, + &mut commands, + ); + } + } +} + +/// Plugin which keeps number-input slidebar colors in sync with the theme. +pub struct NumberInputPlugin; + +impl Plugin for NumberInputPlugin { + fn build(&self, app: &mut bevy_app::App) { + app.add_systems( + PreUpdate, + update_slidebar_styles_theme.in_set(PickingSystems::Last), + ); + } +} diff --git a/crates/bevy_feathers/src/controls/slider.rs b/crates/bevy_feathers/src/controls/slider.rs index 1600214320073..68c7ae32b2087 100644 --- a/crates/bevy_feathers/src/controls/slider.rs +++ b/crates/bevy_feathers/src/controls/slider.rs @@ -4,6 +4,7 @@ use bevy_app::{Plugin, PreUpdate}; use bevy_color::Color; use bevy_ecs::{ bundle::Bundle, + change_detection::DetectChanges, children, component::Component, entity::Entity, @@ -271,6 +272,37 @@ fn update_slider_styles_remove( }); } +/// Re-apply slider styles to every slider when the theme changes. +fn update_slider_styles_theme( + mut q_sliders: Query< + ( + Entity, + Has, + Has, + &Hovered, + &mut BackgroundGradient, + ), + With, + >, + theme: Res, + mut commands: Commands, +) { + if !theme.is_changed() { + return; + } + for (slider_ent, disabled, pressed, hovered, mut gradient) in q_sliders.iter_mut() { + set_slider_styles( + slider_ent, + &theme, + disabled, + pressed, + hovered.0, + gradient.as_mut(), + &mut commands, + ); + } +} + fn set_slider_styles( slider_ent: Entity, theme: &Res<'_, UiTheme>, @@ -376,6 +408,7 @@ impl Plugin for SliderPlugin { ( update_slider_styles, update_slider_styles_remove, + update_slider_styles_theme, update_slider_pos, ) .in_set(PickingSystems::Last), diff --git a/crates/bevy_feathers/src/display/icon.rs b/crates/bevy_feathers/src/display/icon.rs index c3c2ec0527044..b2914b2313a83 100644 --- a/crates/bevy_feathers/src/display/icon.rs +++ b/crates/bevy_feathers/src/display/icon.rs @@ -1,7 +1,20 @@ //! BSN Scene for loading images and displaying them as [`ImageNode`]s. +use bevy_ecs::query::{Changed, With}; +use bevy_ecs::reflect::ReflectComponent; +use bevy_ecs::{component::Component, system::Query}; +use bevy_reflect::Reflect; use bevy_scene::{bsn, Scene}; +use bevy_text::TextColor; use bevy_ui::{px, widget::ImageNode, Node}; +use crate::theme::ThemedText; + +/// Marker to tint an icon's ImageNode by the text color. +#[derive(Component, Default, Clone, Reflect)] +#[reflect(Component)] +#[require(ThemedText)] +pub struct ThemedIcon; + /// Template which displays an icon. pub fn icon(image: &'static str) -> impl Scene { bsn! { @@ -11,5 +24,28 @@ pub fn icon(image: &'static str) -> impl Scene { ImageNode { image: image } + ThemedIcon + } +} + +/// Template which displays an icon and doesn't tint it by the text color. +pub fn icon_untinted(image: &'static str) -> impl Scene { + bsn! { + Node { + height: px(14), + } + ImageNode { + image: image + } + } +} + +pub(crate) fn update_themed_icons( + mut q_icons: Query<(&mut ImageNode, &TextColor), (With, Changed)>, +) { + for (mut image, color) in &mut q_icons { + if image.color != color.0 { + image.color = color.0; + } } } diff --git a/crates/bevy_feathers/src/lib.rs b/crates/bevy_feathers/src/lib.rs index 9410534a23ae1..63067a7253737 100644 --- a/crates/bevy_feathers/src/lib.rs +++ b/crates/bevy_feathers/src/lib.rs @@ -102,12 +102,18 @@ impl Plugin for FeathersCorePlugin { bevy_window::SystemCursorIcon::Default, ))); - app.add_systems(PostUpdate, theme::update_theme) - .add_observer(theme::on_changed_background) - .add_observer(theme::on_changed_border) - .add_observer(theme::on_changed_font_color) - .add_observer(theme::on_changed_text_color) - .add_observer(font_styles::on_changed_font); + app.add_systems( + PostUpdate, + ( + theme::update_theme, + display::update_themed_icons.after(PropagateSet::::default()), + ), + ) + .add_observer(theme::on_changed_background) + .add_observer(theme::on_changed_border) + .add_observer(theme::on_changed_font_color) + .add_observer(theme::on_changed_text_color) + .add_observer(font_styles::on_changed_font); app.init_resource::(); } From ec9d0994aa3f9ebb02b33121ecd23e92e1c31514 Mon Sep 17 00:00:00 2001 From: John Payne <20407779+johngpayne@users.noreply.github.com> Date: Mon, 22 Jun 2026 22:40:19 +0100 Subject: [PATCH 2/4] fmt --- .../src/controls/disclosure_toggle.rs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/crates/bevy_feathers/src/controls/disclosure_toggle.rs b/crates/bevy_feathers/src/controls/disclosure_toggle.rs index 10b1333bd963d..2823da49b930a 100644 --- a/crates/bevy_feathers/src/controls/disclosure_toggle.rs +++ b/crates/bevy_feathers/src/controls/disclosure_toggle.rs @@ -1,6 +1,12 @@ use bevy_app::{App, Plugin, PreUpdate}; use bevy_ecs::{ - entity::Entity, hierarchy::Children, lifecycle::RemovedComponents, query::{Added, Has, Or, With}, reflect::ReflectComponent, schedule::IntoScheduleConfigs, system::{Commands, Query}, + entity::Entity, + hierarchy::Children, + lifecycle::RemovedComponents, + query::{Added, Has, Or, With}, + reflect::ReflectComponent, + schedule::IntoScheduleConfigs, + system::{Commands, Query}, }; use bevy_input_focus::tab_navigation::TabIndex; use bevy_math::Rot2; @@ -9,14 +15,14 @@ use bevy_reflect::std_traits::ReflectDefault; use bevy_reflect::Reflect; use bevy_scene::{bsn, Scene, SceneComponent}; use bevy_ui::{ - px, AlignItems, Checked, Display, InteractionDisabled, JustifyContent, Node, - UiTransform, + px, AlignItems, Checked, Display, InteractionDisabled, JustifyContent, Node, UiTransform, }; use bevy_ui_widgets::Checkbox; use bevy_window::SystemCursorIcon; use crate::{ - constants::icons, cursor::EntityCursor, display::icon, focus::FocusIndicator, theme::InheritableThemeTextColor, tokens, + constants::icons, cursor::EntityCursor, display::icon, focus::FocusIndicator, + theme::InheritableThemeTextColor, tokens, }; /// A toggle button which shows a chevron that points either right or down, used to expand or @@ -125,7 +131,9 @@ fn set_toggle_styles( // Change icon color if new_text_color_token != text_color.0 { - commands.entity(entity).insert(InheritableThemeTextColor(new_text_color_token)); + commands + .entity(entity) + .insert(InheritableThemeTextColor(new_text_color_token)); } match checked { From 0dfb0d3c5a754cc1f7439575481e18d5c67424fa Mon Sep 17 00:00:00 2001 From: John Payne <20407779+johngpayne@users.noreply.github.com> Date: Tue, 23 Jun 2026 00:17:58 +0100 Subject: [PATCH 3/4] clippy fix --- crates/bevy_feathers/src/display/icon.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_feathers/src/display/icon.rs b/crates/bevy_feathers/src/display/icon.rs index b2914b2313a83..d28a007159b99 100644 --- a/crates/bevy_feathers/src/display/icon.rs +++ b/crates/bevy_feathers/src/display/icon.rs @@ -9,7 +9,7 @@ use bevy_ui::{px, widget::ImageNode, Node}; use crate::theme::ThemedText; -/// Marker to tint an icon's ImageNode by the text color. +/// Marker to tint an icon's `ImageNode` by the text color. #[derive(Component, Default, Clone, Reflect)] #[reflect(Component)] #[require(ThemedText)] From a466c15ec661e5d84acfeab4e5c63f39a9ee9248 Mon Sep 17 00:00:00 2001 From: John Payne <20407779+johngpayne@users.noreply.github.com> Date: Tue, 23 Jun 2026 12:53:37 +0100 Subject: [PATCH 4/4] used InputFocus in update_slidebar_styles_theme to match newly merged signature of set_slidebar_styles --- crates/bevy_feathers/src/controls/number_input.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/bevy_feathers/src/controls/number_input.rs b/crates/bevy_feathers/src/controls/number_input.rs index 4674fa6685fb2..2eb2302809805 100644 --- a/crates/bevy_feathers/src/controls/number_input.rs +++ b/crates/bevy_feathers/src/controls/number_input.rs @@ -1214,6 +1214,7 @@ fn update_slidebar_styles_theme( q_number_input: Query<(Entity, Has), With>, mut q_text_input: Query<(&Hovered, &mut BackgroundGradient)>, theme: Res, + input_focus: Res, mut commands: Commands, ) { if !theme.is_changed() { @@ -1233,6 +1234,7 @@ fn update_slidebar_styles_theme( is_disabled, false, hovered, + input_focus.get() == Some(text_entity), &mut gradient, &mut commands, );