This commit is contained in:
Talin 2025-07-13 18:44:54 +01:00 committed by GitHub
commit a539f97252
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 224 additions and 34 deletions

View File

@ -2,18 +2,21 @@ use bevy_app::{Plugin, PreUpdate};
use bevy_core_widgets::{Callback, CoreButton}; use bevy_core_widgets::{Callback, CoreButton};
use bevy_ecs::{ use bevy_ecs::{
bundle::Bundle, bundle::Bundle,
change_detection::DetectChanges,
component::Component, component::Component,
entity::Entity, entity::Entity,
hierarchy::{ChildOf, Children}, hierarchy::{ChildOf, Children},
lifecycle::RemovedComponents, lifecycle::RemovedComponents,
query::{Added, Changed, Has, Or}, query::{Added, Changed, Has, Or, With},
schedule::IntoScheduleConfigs, schedule::IntoScheduleConfigs,
spawn::{SpawnRelated, SpawnableList}, spawn::{SpawnRelated, SpawnableList},
system::{Commands, Query}, system::{Commands, Query, Res},
}; };
use bevy_input_focus::tab_navigation::TabIndex; use bevy_input_focus::{tab_navigation::TabIndex, InputFocus};
use bevy_picking::{hover::Hovered, PickingSystems}; use bevy_picking::{hover::Hovered, PickingSystems};
use bevy_ui::{AlignItems, InteractionDisabled, JustifyContent, Node, Pressed, UiRect, Val}; use bevy_ui::{
AlignItems, InteractionDisabled, JustifyContent, Node, Outline, Pressed, UiRect, Val,
};
use bevy_winit::cursor::CursorIcon; use bevy_winit::cursor::CursorIcon;
use crate::{ use crate::{
@ -21,7 +24,7 @@ use crate::{
font_styles::InheritableFont, font_styles::InheritableFont,
handle_or_path::HandleOrPath, handle_or_path::HandleOrPath,
rounded_corners::RoundedCorners, rounded_corners::RoundedCorners,
theme::{ThemeBackgroundColor, ThemeFontColor}, theme::{ThemeBackgroundColor, ThemeFontColor, UiTheme},
tokens, tokens,
}; };
@ -195,6 +198,27 @@ fn set_button_colors(
} }
} }
fn update_button_focus(
mut commands: Commands,
focus: Res<InputFocus>,
theme: Res<UiTheme>,
query: Query<Entity, With<ButtonVariant>>,
) {
if focus.is_changed() {
for button in query.iter() {
if focus.0 == Some(button) {
commands.entity(button).insert(Outline {
color: theme.color(tokens::FOCUS_RING),
width: Val::Px(2.0),
offset: Val::Px(2.0),
});
} else {
commands.entity(button).remove::<Outline>();
}
}
}
}
/// Plugin which registers the systems for updating the button styles. /// Plugin which registers the systems for updating the button styles.
pub struct ButtonPlugin; pub struct ButtonPlugin;
@ -202,7 +226,12 @@ impl Plugin for ButtonPlugin {
fn build(&self, app: &mut bevy_app::App) { fn build(&self, app: &mut bevy_app::App) {
app.add_systems( app.add_systems(
PreUpdate, PreUpdate,
(update_button_styles, update_button_styles_remove).in_set(PickingSystems::Last), (
update_button_styles,
update_button_styles_remove,
update_button_focus,
)
.in_set(PickingSystems::Last),
); );
} }
} }

View File

@ -2,6 +2,7 @@ use bevy_app::{Plugin, PreUpdate};
use bevy_core_widgets::{Callback, CoreCheckbox}; use bevy_core_widgets::{Callback, CoreCheckbox};
use bevy_ecs::{ use bevy_ecs::{
bundle::Bundle, bundle::Bundle,
change_detection::DetectChanges,
children, children,
component::Component, component::Component,
entity::Entity, entity::Entity,
@ -10,15 +11,15 @@ use bevy_ecs::{
query::{Added, Changed, Has, Or, With}, query::{Added, Changed, Has, Or, With},
schedule::IntoScheduleConfigs, schedule::IntoScheduleConfigs,
spawn::{Spawn, SpawnRelated, SpawnableList}, spawn::{Spawn, SpawnRelated, SpawnableList},
system::{Commands, In, Query}, system::{Commands, In, Query, Res},
}; };
use bevy_input_focus::tab_navigation::TabIndex; use bevy_input_focus::{tab_navigation::TabIndex, InputFocus};
use bevy_math::Rot2; use bevy_math::Rot2;
use bevy_picking::{hover::Hovered, PickingSystems}; use bevy_picking::{hover::Hovered, PickingSystems};
use bevy_render::view::Visibility; use bevy_render::view::Visibility;
use bevy_ui::{ use bevy_ui::{
AlignItems, BorderRadius, Checked, Display, FlexDirection, InteractionDisabled, JustifyContent, AlignItems, BorderRadius, Checked, Display, FlexDirection, InteractionDisabled, JustifyContent,
Node, PositionType, UiRect, UiTransform, Val, Node, Outline, PositionType, UiRect, UiTransform, Val,
}; };
use bevy_winit::cursor::CursorIcon; use bevy_winit::cursor::CursorIcon;
@ -26,7 +27,7 @@ use crate::{
constants::{fonts, size}, constants::{fonts, size},
font_styles::InheritableFont, font_styles::InheritableFont,
handle_or_path::HandleOrPath, handle_or_path::HandleOrPath,
theme::{ThemeBackgroundColor, ThemeBorderColor, ThemeFontColor}, theme::{ThemeBackgroundColor, ThemeBorderColor, ThemeFontColor, UiTheme},
tokens, tokens,
}; };
@ -296,6 +297,37 @@ fn set_checkbox_colors(
} }
} }
fn update_checkbox_focus(
mut commands: Commands,
focus: Res<InputFocus>,
theme: Res<UiTheme>,
q_checkbox: Query<Entity, With<CheckboxFrame>>,
q_children: Query<&Children>,
q_outline: Query<(), With<CheckboxOutline>>,
) {
if focus.is_changed() {
for checkbox_ent in q_checkbox.iter() {
// For a checkbox, we want to outline just the box, not the entire wiget with the label.
let Some(outline_ent) = q_children
.iter_descendants(checkbox_ent)
.find(|en| q_outline.contains(*en))
else {
return;
};
if focus.0 == Some(checkbox_ent) {
commands.entity(outline_ent).insert(Outline {
color: theme.color(tokens::FOCUS_RING),
width: Val::Px(2.0),
offset: Val::Px(2.0),
});
} else {
commands.entity(outline_ent).remove::<Outline>();
}
}
}
}
/// Plugin which registers the systems for updating the checkbox styles. /// Plugin which registers the systems for updating the checkbox styles.
pub struct CheckboxPlugin; pub struct CheckboxPlugin;
@ -303,7 +335,12 @@ impl Plugin for CheckboxPlugin {
fn build(&self, app: &mut bevy_app::App) { fn build(&self, app: &mut bevy_app::App) {
app.add_systems( app.add_systems(
PreUpdate, PreUpdate,
(update_checkbox_styles, update_checkbox_styles_remove).in_set(PickingSystems::Last), (
update_checkbox_styles,
update_checkbox_styles_remove,
update_checkbox_focus,
)
.in_set(PickingSystems::Last),
); );
} }
} }

View File

@ -1,7 +1,8 @@
use bevy_app::{Plugin, PreUpdate}; use bevy_app::{Plugin, PreUpdate};
use bevy_core_widgets::CoreRadio; use bevy_core_widgets::{CoreRadio, CoreRadioGroup};
use bevy_ecs::{ use bevy_ecs::{
bundle::Bundle, bundle::Bundle,
change_detection::DetectChanges,
children, children,
component::Component, component::Component,
entity::Entity, entity::Entity,
@ -10,14 +11,14 @@ use bevy_ecs::{
query::{Added, Changed, Has, Or, With}, query::{Added, Changed, Has, Or, With},
schedule::IntoScheduleConfigs, schedule::IntoScheduleConfigs,
spawn::{Spawn, SpawnRelated, SpawnableList}, spawn::{Spawn, SpawnRelated, SpawnableList},
system::{Commands, Query}, system::{Commands, Query, Res},
}; };
use bevy_input_focus::tab_navigation::TabIndex; use bevy_input_focus::InputFocus;
use bevy_picking::{hover::Hovered, PickingSystems}; use bevy_picking::{hover::Hovered, PickingSystems};
use bevy_render::view::Visibility; use bevy_render::view::Visibility;
use bevy_ui::{ use bevy_ui::{
AlignItems, BorderRadius, Checked, Display, FlexDirection, InteractionDisabled, JustifyContent, AlignItems, BorderRadius, Checked, Display, FlexDirection, InteractionDisabled, JustifyContent,
Node, UiRect, Val, Node, Outline, UiRect, Val,
}; };
use bevy_winit::cursor::CursorIcon; use bevy_winit::cursor::CursorIcon;
@ -25,7 +26,7 @@ use crate::{
constants::{fonts, size}, constants::{fonts, size},
font_styles::InheritableFont, font_styles::InheritableFont,
handle_or_path::HandleOrPath, handle_or_path::HandleOrPath,
theme::{ThemeBackgroundColor, ThemeBorderColor, ThemeFontColor}, theme::{ThemeBackgroundColor, ThemeBorderColor, ThemeFontColor, UiTheme},
tokens, tokens,
}; };
@ -59,7 +60,6 @@ pub fn radio<C: SpawnableList<ChildOf> + Send + Sync + 'static, B: Bundle>(
CoreRadio, CoreRadio,
Hovered::default(), Hovered::default(),
CursorIcon::System(bevy_window::SystemCursorIcon::Pointer), CursorIcon::System(bevy_window::SystemCursorIcon::Pointer),
TabIndex(0),
ThemeFontColor(tokens::RADIO_TEXT), ThemeFontColor(tokens::RADIO_TEXT),
InheritableFont { InheritableFont {
font: HandleOrPath::Path(fonts::REGULAR.to_owned()), font: HandleOrPath::Path(fonts::REGULAR.to_owned()),
@ -255,6 +255,27 @@ fn set_radio_colors(
} }
} }
fn update_radio_group_focus(
mut commands: Commands,
focus: Res<InputFocus>,
theme: Res<UiTheme>,
q_groups: Query<Entity, With<CoreRadioGroup>>,
) {
if focus.is_changed() {
for group in q_groups.iter() {
if focus.0 == Some(group) {
commands.entity(group).insert(Outline {
color: theme.color(tokens::FOCUS_RING),
width: Val::Px(2.0),
offset: Val::Px(2.0),
});
} else {
commands.entity(group).remove::<Outline>();
}
}
}
}
/// Plugin which registers the systems for updating the radio styles. /// Plugin which registers the systems for updating the radio styles.
pub struct RadioPlugin; pub struct RadioPlugin;
@ -262,7 +283,12 @@ impl Plugin for RadioPlugin {
fn build(&self, app: &mut bevy_app::App) { fn build(&self, app: &mut bevy_app::App) {
app.add_systems( app.add_systems(
PreUpdate, PreUpdate,
(update_radio_styles, update_radio_styles_remove).in_set(PickingSystems::Last), (
update_radio_styles,
update_radio_styles_remove,
update_radio_group_focus,
)
.in_set(PickingSystems::Last),
); );
} }
} }

View File

@ -5,6 +5,7 @@ use bevy_color::Color;
use bevy_core_widgets::{Callback, CoreSlider, SliderRange, SliderValue, TrackClick}; use bevy_core_widgets::{Callback, CoreSlider, SliderRange, SliderValue, TrackClick};
use bevy_ecs::{ use bevy_ecs::{
bundle::Bundle, bundle::Bundle,
change_detection::DetectChanges,
children, children,
component::Component, component::Component,
entity::Entity, entity::Entity,
@ -13,14 +14,14 @@ use bevy_ecs::{
query::{Added, Changed, Has, Or, Spawned, With}, query::{Added, Changed, Has, Or, Spawned, With},
schedule::IntoScheduleConfigs, schedule::IntoScheduleConfigs,
spawn::SpawnRelated, spawn::SpawnRelated,
system::{In, Query, Res}, system::{Commands, In, Query, Res},
}; };
use bevy_input_focus::tab_navigation::TabIndex; use bevy_input_focus::{tab_navigation::TabIndex, InputFocus};
use bevy_picking::PickingSystems; use bevy_picking::PickingSystems;
use bevy_ui::{ use bevy_ui::{
widget::Text, AlignItems, BackgroundGradient, ColorStop, Display, FlexDirection, Gradient, widget::Text, AlignItems, BackgroundGradient, ColorStop, Display, FlexDirection, Gradient,
InteractionDisabled, InterpolationColorSpace, JustifyContent, LinearGradient, Node, UiRect, InteractionDisabled, InterpolationColorSpace, JustifyContent, LinearGradient, Node, Outline,
Val, UiRect, Val,
}; };
use bevy_winit::cursor::CursorIcon; use bevy_winit::cursor::CursorIcon;
@ -190,6 +191,27 @@ fn update_slider_pos(
} }
} }
fn update_slider_focus(
mut commands: Commands,
focus: Res<InputFocus>,
theme: Res<UiTheme>,
q_sliders: Query<Entity, With<SliderStyle>>,
) {
if focus.is_changed() {
for slider in q_sliders.iter() {
if focus.0 == Some(slider) {
commands.entity(slider).insert(Outline {
color: theme.color(tokens::FOCUS_RING),
width: Val::Px(2.0),
offset: Val::Px(2.0),
});
} else {
commands.entity(slider).remove::<Outline>();
}
}
}
}
/// Plugin which registers the systems for updating the slider styles. /// Plugin which registers the systems for updating the slider styles.
pub struct SliderPlugin; pub struct SliderPlugin;
@ -201,6 +223,7 @@ impl Plugin for SliderPlugin {
update_slider_colors, update_slider_colors,
update_slider_colors_remove, update_slider_colors_remove,
update_slider_pos, update_slider_pos,
update_slider_focus,
) )
.in_set(PickingSystems::Last), .in_set(PickingSystems::Last),
); );

View File

@ -4,6 +4,7 @@ use bevy_app::{Plugin, PreUpdate};
use bevy_core_widgets::{Callback, CoreCheckbox}; use bevy_core_widgets::{Callback, CoreCheckbox};
use bevy_ecs::{ use bevy_ecs::{
bundle::Bundle, bundle::Bundle,
change_detection::DetectChanges,
children, children,
component::Component, component::Component,
entity::Entity, entity::Entity,
@ -12,17 +13,19 @@ use bevy_ecs::{
query::{Added, Changed, Has, Or, With}, query::{Added, Changed, Has, Or, With},
schedule::IntoScheduleConfigs, schedule::IntoScheduleConfigs,
spawn::SpawnRelated, spawn::SpawnRelated,
system::{Commands, In, Query}, system::{Commands, In, Query, Res},
world::Mut, world::Mut,
}; };
use bevy_input_focus::tab_navigation::TabIndex; use bevy_input_focus::{tab_navigation::TabIndex, InputFocus};
use bevy_picking::{hover::Hovered, PickingSystems}; use bevy_picking::{hover::Hovered, PickingSystems};
use bevy_ui::{BorderRadius, Checked, InteractionDisabled, Node, PositionType, UiRect, Val}; use bevy_ui::{
BorderRadius, Checked, InteractionDisabled, Node, Outline, PositionType, UiRect, Val,
};
use bevy_winit::cursor::CursorIcon; use bevy_winit::cursor::CursorIcon;
use crate::{ use crate::{
constants::size, constants::size,
theme::{ThemeBackgroundColor, ThemeBorderColor}, theme::{ThemeBackgroundColor, ThemeBorderColor, UiTheme},
tokens, tokens,
}; };
@ -236,6 +239,27 @@ fn set_switch_colors(
} }
} }
fn update_switch_focus(
mut commands: Commands,
focus: Res<InputFocus>,
theme: Res<UiTheme>,
q_switches: Query<Entity, With<ToggleSwitchOutline>>,
) {
if focus.is_changed() {
for switch in q_switches.iter() {
if focus.0 == Some(switch) {
commands.entity(switch).insert(Outline {
color: theme.color(tokens::FOCUS_RING),
width: Val::Px(2.0),
offset: Val::Px(2.0),
});
} else {
commands.entity(switch).remove::<Outline>();
}
}
}
}
/// Plugin which registers the systems for updating the toggle switch styles. /// Plugin which registers the systems for updating the toggle switch styles.
pub struct ToggleSwitchPlugin; pub struct ToggleSwitchPlugin;
@ -243,7 +267,12 @@ impl Plugin for ToggleSwitchPlugin {
fn build(&self, app: &mut bevy_app::App) { fn build(&self, app: &mut bevy_app::App) {
app.add_systems( app.add_systems(
PreUpdate, PreUpdate,
(update_switch_styles, update_switch_styles_remove).in_set(PickingSystems::Last), (
update_switch_styles,
update_switch_styles_remove,
update_switch_focus,
)
.in_set(PickingSystems::Last),
); );
} }
} }

View File

@ -10,6 +10,7 @@ pub fn create_dark_theme() -> ThemeProps {
ThemeProps { ThemeProps {
color: HashMap::from([ color: HashMap::from([
(tokens::WINDOW_BG.into(), palette::GRAY_0), (tokens::WINDOW_BG.into(), palette::GRAY_0),
(tokens::FOCUS_RING.into(), palette::ACCENT.with_alpha(0.5)),
// Button // Button
(tokens::BUTTON_BG.into(), palette::GRAY_3), (tokens::BUTTON_BG.into(), palette::GRAY_3),
( (

View File

@ -1,7 +1,7 @@
//! This example illustrates how to create widgets using the `bevy_core_widgets` widget set. //! This example illustrates how to create widgets using the `bevy_core_widgets` widget set.
use bevy::{ use bevy::{
color::palettes::basic::*, color::palettes::{self, basic::*},
core_widgets::{ core_widgets::{
Callback, CoreButton, CoreCheckbox, CoreRadio, CoreRadioGroup, CoreSlider, Callback, CoreButton, CoreCheckbox, CoreRadio, CoreRadioGroup, CoreSlider,
CoreSliderDragState, CoreSliderThumb, CoreWidgetsPlugins, SliderRange, SliderValue, CoreSliderDragState, CoreSliderThumb, CoreWidgetsPlugins, SliderRange, SliderValue,
@ -9,7 +9,7 @@ use bevy::{
}, },
input_focus::{ input_focus::{
tab_navigation::{TabGroup, TabIndex, TabNavigationPlugin}, tab_navigation::{TabGroup, TabIndex, TabNavigationPlugin},
InputDispatchPlugin, InputDispatchPlugin, InputFocus,
}, },
picking::hover::Hovered, picking::hover::Hovered,
prelude::*, prelude::*,
@ -43,6 +43,7 @@ fn main() {
update_checkbox_or_radio_style.after(update_widget_values), update_checkbox_or_radio_style.after(update_widget_values),
update_checkbox_or_radio_style2.after(update_widget_values), update_checkbox_or_radio_style2.after(update_widget_values),
toggle_disabled, toggle_disabled,
focus_system,
), ),
) )
.run(); .run();
@ -739,6 +740,26 @@ fn radio(asset_server: &AssetServer, value: TrackClick, caption: &str) -> impl B
) )
} }
fn focus_system(
mut commands: Commands,
focus: Res<InputFocus>,
mut query: Query<Entity, With<TabIndex>>,
) {
if focus.is_changed() {
for button in query.iter_mut() {
if focus.0 == Some(button) {
commands.entity(button).insert(Outline {
color: palettes::tailwind::BLUE_700.into(),
width: Val::Px(2.0),
offset: Val::Px(2.0),
});
} else {
commands.entity(button).remove::<Outline>();
}
}
}
}
fn toggle_disabled( fn toggle_disabled(
input: Res<ButtonInput<KeyCode>>, input: Res<ButtonInput<KeyCode>>,
mut interaction_query: Query< mut interaction_query: Query<

View File

@ -1,7 +1,7 @@
//! This example illustrates how to create widgets using the `bevy_core_widgets` widget set. //! This example illustrates how to create widgets using the `bevy_core_widgets` widget set.
use bevy::{ use bevy::{
color::palettes::basic::*, color::palettes::{self, basic::*},
core_widgets::{ core_widgets::{
Callback, CoreButton, CoreCheckbox, CoreSlider, CoreSliderThumb, CoreWidgetsPlugins, Callback, CoreButton, CoreCheckbox, CoreSlider, CoreSliderThumb, CoreWidgetsPlugins,
SliderRange, SliderValue, SliderRange, SliderValue,
@ -9,7 +9,7 @@ use bevy::{
ecs::system::SystemId, ecs::system::SystemId,
input_focus::{ input_focus::{
tab_navigation::{TabGroup, TabIndex, TabNavigationPlugin}, tab_navigation::{TabGroup, TabIndex, TabNavigationPlugin},
InputDispatchPlugin, InputDispatchPlugin, InputFocus,
}, },
picking::hover::Hovered, picking::hover::Hovered,
prelude::*, prelude::*,
@ -44,7 +44,10 @@ fn main() {
.add_observer(checkbox_on_change_hover) .add_observer(checkbox_on_change_hover)
.add_observer(checkbox_on_add_checked) .add_observer(checkbox_on_add_checked)
.add_observer(checkbox_on_remove_checked) .add_observer(checkbox_on_remove_checked)
.add_systems(Update, (update_widget_values, toggle_disabled)) .add_systems(
Update,
(update_widget_values, toggle_disabled, focus_system),
)
.run(); .run();
} }
@ -730,6 +733,26 @@ fn update_widget_values(
} }
} }
fn focus_system(
mut commands: Commands,
focus: Res<InputFocus>,
mut query: Query<Entity, With<TabIndex>>,
) {
if focus.is_changed() {
for button in query.iter_mut() {
if focus.0 == Some(button) {
commands.entity(button).insert(Outline {
color: palettes::tailwind::BLUE_700.into(),
width: Val::Px(2.0),
offset: Val::Px(2.0),
});
} else {
commands.entity(button).remove::<Outline>();
}
}
}
}
fn toggle_disabled( fn toggle_disabled(
input: Res<ButtonInput<KeyCode>>, input: Res<ButtonInput<KeyCode>>,
mut interaction_query: Query< mut interaction_query: Query<

View File

@ -15,7 +15,7 @@ use bevy::{
tokens, FeathersPlugin, tokens, FeathersPlugin,
}, },
input_focus::{ input_focus::{
tab_navigation::{TabGroup, TabNavigationPlugin}, tab_navigation::{TabGroup, TabIndex, TabNavigationPlugin},
InputDispatchPlugin, InputDispatchPlugin,
}, },
prelude::*, prelude::*,
@ -215,6 +215,7 @@ fn demo_root(commands: &mut Commands) -> impl Bundle {
CoreRadioGroup { CoreRadioGroup {
on_change: Callback::System(radio_exclusion), on_change: Callback::System(radio_exclusion),
}, },
TabIndex(0),
children![ children![
radio(Checked, Spawn((Text::new("One"), ThemedText))), radio(Checked, Spawn((Text::new("One"), ThemedText))),
radio((), Spawn((Text::new("Two"), ThemedText))), radio((), Spawn((Text::new("Two"), ThemedText))),

View File

@ -1,7 +1,7 @@
--- ---
title: Bevy Feathers title: Bevy Feathers
authors: ["@viridia", "@Atlas16A"] authors: ["@viridia", "@Atlas16A"]
pull_requests: [19730, 19900, 19928] pull_requests: [19730, 19900, 19928, 20047]
--- ---
To make it easier for Bevy engine developers and third-party tool creators to make comfortable, visually cohesive tooling, To make it easier for Bevy engine developers and third-party tool creators to make comfortable, visually cohesive tooling,