diff --git a/_release-content/release-notes/more_widgets.md b/_release-content/release-notes/more_widgets.md new file mode 100644 index 0000000000000..d34e325a3951c --- /dev/null +++ b/_release-content/release-notes/more_widgets.md @@ -0,0 +1,9 @@ +--- +title: "Moar widgets!" +authors: ["@viridia"] +pull_requests: [23645, 23707] +--- + +Bevy Feathers, the opinionated UI widget collection, has added two new widgets: text input and +dropdown menu buttons. Note that unlike the older widgets, these are _only_ available through +BSN (which will be the primary access to feathers going forward). diff --git a/crates/bevy_feathers/src/assets/icons/_NAMING.md b/crates/bevy_feathers/src/assets/icons/_NAMING.md new file mode 100644 index 0000000000000..023666292019d --- /dev/null +++ b/crates/bevy_feathers/src/assets/icons/_NAMING.md @@ -0,0 +1,4 @@ +# Guidelines for Naming Files In This Directory + +The names of files in this directory should refer to what the icons look like ("x", "chevron", etc.) +rather than their assigned meanings ("close", "expand") because the latter can change. diff --git a/crates/bevy_feathers/src/assets/icons/chevron-down.png b/crates/bevy_feathers/src/assets/icons/chevron-down.png new file mode 100644 index 0000000000000..ba8a4c42104e0 Binary files /dev/null and b/crates/bevy_feathers/src/assets/icons/chevron-down.png differ diff --git a/crates/bevy_feathers/src/assets/icons/chevron-right.png b/crates/bevy_feathers/src/assets/icons/chevron-right.png new file mode 100644 index 0000000000000..681c167ac0152 Binary files /dev/null and b/crates/bevy_feathers/src/assets/icons/chevron-right.png differ diff --git a/crates/bevy_feathers/src/assets/icons/x.png b/crates/bevy_feathers/src/assets/icons/x.png new file mode 100644 index 0000000000000..23cff4ac589c8 Binary files /dev/null and b/crates/bevy_feathers/src/assets/icons/x.png differ diff --git a/crates/bevy_feathers/src/constants.rs b/crates/bevy_feathers/src/constants.rs index 359e5a4935b0c..4a59c3da22b1a 100644 --- a/crates/bevy_feathers/src/constants.rs +++ b/crates/bevy_feathers/src/constants.rs @@ -14,6 +14,16 @@ pub mod fonts { pub const MONO: &str = "embedded://bevy_feathers/assets/fonts/FiraMono-Medium.ttf"; } +/// Icon paths +pub mod icons { + /// Downward-pointing chevron + pub const CHEVRON_DOWN: &str = "embedded://bevy_feathers/assets/icons/chevron-down.png"; + /// Right-pointing chevron + pub const CHEVRON_RIGHT: &str = "embedded://bevy_feathers/assets/icons/chevron-right.png"; + /// Diagonal Cross + pub const X: &str = "embedded://bevy_feathers/assets/icons/x.png"; +} + /// Size constants pub mod size { use bevy_ui::Val; @@ -24,6 +34,9 @@ pub mod size { /// Width and height of a checkbox pub const CHECKBOX_SIZE: Val = Val::Px(18.0); + /// Height for pane headers + pub const HEADER_HEIGHT: Val = Val::Px(30.0); + /// Width and height of a radio button pub const RADIO_SIZE: Val = Val::Px(18.0); diff --git a/crates/bevy_feathers/src/controls/menu.rs b/crates/bevy_feathers/src/controls/menu.rs new file mode 100644 index 0000000000000..e79d545cb85c8 --- /dev/null +++ b/crates/bevy_feathers/src/controls/menu.rs @@ -0,0 +1,435 @@ +use bevy_app::{Plugin, PreUpdate}; +use bevy_camera::visibility::Visibility; +use bevy_color::{Alpha, Srgba}; +use bevy_ecs::{ + change_detection::DetectChanges, + component::Component, + entity::Entity, + hierarchy::Children, + lifecycle::RemovedComponents, + observer::On, + query::{Added, Changed, Has, Or, With}, + schedule::IntoScheduleConfigs, + system::{Commands, Query, Res, ResMut}, +}; +use bevy_log::warn; +use bevy_picking::{hover::Hovered, PickingSystems}; +use bevy_scene::{prelude::*, template_value}; +use bevy_text::{FontSize, FontWeight}; +use bevy_ui::{ + AlignItems, BoxShadow, Display, FlexDirection, GlobalZIndex, InteractionDisabled, + JustifyContent, Node, OverrideClip, PositionType, Pressed, UiRect, Val, +}; +use bevy_ui_widgets::{ + popover::{Popover, PopoverAlign, PopoverPlacement, PopoverSide}, + ActivateOnPress, MenuAction, MenuButton, MenuEvent, MenuFocusState, MenuItem, MenuPopup, +}; + +use crate::{ + constants::{fonts, icons, size}, + controls::{button, ButtonProps, ButtonVariant}, + cursor::EntityCursor, + font_styles::InheritableFont, + icon, + rounded_corners::RoundedCorners, + theme::{ThemeBackgroundColor, ThemeBorderColor, ThemeFontColor}, + tokens, +}; +use bevy_input_focus::{ + tab_navigation::{NavAction, TabIndex}, + InputFocus, +}; + +/// Parameters for the menu button template, passed to [`menu_button`] function. +pub struct MenuButtonProps { + /// Label for this menu button + pub label: Box, + /// Rounded corners options + pub corners: RoundedCorners, + /// Include the standard downward-pointing chevron (default true). + pub arrow: bool, +} + +impl Default for MenuButtonProps { + fn default() -> Self { + Self { + label: Box::new(bsn_list!()), + corners: Default::default(), + arrow: true, + } + } +} + +/// Marker for menu button +#[derive(Component, Default, Clone)] +struct FeathersMenuButton; + +/// Marker for menu items +#[derive(Component, Default, Clone)] +struct FeathersMenuItem; + +/// Marker for menu popup +#[derive(Component, Default, Clone)] +struct FeathersMenuPopup; + +/// Component that contains the popup content generator. +#[derive(Component, Clone, Default)] +struct FeathersMenuContainer; + +/// Menu scene function. This wraps the menu button and provides an anchor for the popopver. +pub fn menu() -> impl Scene { + bsn! { + Node { + height: size::ROW_HEIGHT, + justify_content: JustifyContent::Stretch, + align_items: AlignItems::Stretch, + } + FeathersMenuContainer + on(on_menu_event) + } +} + +fn on_menu_event( + mut ev: On, + q_menu_children: Query<&Children>, + q_popovers: Query<&mut Visibility, With>, + q_buttons: Query<(), With>, + mut commands: Commands, + mut focus: ResMut, +) { + match ev.event().action { + MenuAction::Open(nav) => { + let Ok(children) = q_menu_children.get(ev.source) else { + return; + }; + ev.propagate(false); + for child in children.iter() { + if q_popovers.contains(*child) { + commands + .entity(*child) + .insert((Visibility::Visible, MenuFocusState::Opening(nav))); + return; + } + } + warn!("Menu popup not found"); + } + MenuAction::Toggle => { + let Ok(children) = q_menu_children.get(ev.source) else { + return; + }; + for child in children.iter() { + if let Ok(visibility) = q_popovers.get(*child) { + ev.propagate(false); + if visibility == Visibility::Visible { + commands.entity(*child).insert(Visibility::Hidden); + } else { + commands.entity(*child).insert(( + Visibility::Visible, + MenuFocusState::Opening(NavAction::First), + )); + } + return; + } + } + warn!("Menu popup not found"); + } + MenuAction::CloseAll => { + let Ok(children) = q_menu_children.get(ev.source) else { + return; + }; + for child in children.iter() { + if q_popovers.contains(*child) { + ev.propagate(false); + commands.entity(*child).insert(Visibility::Hidden); + } + } + } + MenuAction::FocusRoot => { + let Ok(children) = q_menu_children.get(ev.source) else { + return; + }; + for child in children.iter() { + if q_buttons.contains(*child) { + ev.propagate(false); + focus.0 = Some(*child); + break; + } + } + } + } +} + +/// Menu button scene function. This produces a button that has a dropdown arrow. +/// +/// # Arguments +/// * `props` - construction properties for the button. +pub fn menu_button(props: MenuButtonProps) -> impl Scene { + bsn! { + :button(ButtonProps { + variant: ButtonVariant::Normal, + corners: props.corners, + }) + ActivateOnPress + MenuButton + FeathersMenuButton + Children [ + {props.label}, + { + if props.arrow { + Box::new(bsn_list!( + Node { + flex_grow: 1.0, + }, + :icon(icons::CHEVRON_DOWN), + )) as Box + } else { + Box::new(bsn_list!()) as Box + } + } + ] + } +} + +/// Menu Popup scene function +pub fn menu_popup() -> impl Scene { + bsn! { + Node { + position_type: PositionType::Absolute, + display: Display::Flex, + flex_direction: FlexDirection::Column, + justify_content: JustifyContent::Stretch, + align_items: AlignItems::Stretch, + border: UiRect::all(Val::Px(1.0)), + padding: UiRect::axes(Val::Px(0.0), Val::Px(4.0)), + border_radius: {RoundedCorners::All.to_border_radius(4.0)}, + } + FeathersMenuPopup + MenuPopup + template_value(Visibility::Hidden) + ThemeBackgroundColor(tokens::MENU_BG) + ThemeBorderColor(tokens::MENU_BORDER) + BoxShadow::new( + Srgba::BLACK.with_alpha(0.9).into(), + Val::Px(0.0), + Val::Px(0.0), + Val::Px(1.0), + Val::Px(4.0), + ) + GlobalZIndex(100) + template_value( + Popover { + positions: vec![ + PopoverPlacement { + side: PopoverSide::Bottom, + align: PopoverAlign::Start, + gap: 2.0, + }, + PopoverPlacement { + side: PopoverSide::Top, + align: PopoverAlign::Start, + gap: 2.0, + }, + ], + window_margin: 10.0, + } + ) + OverrideClip + } +} + +/// Parameters for the menu button template, passed to [`menu_button`] function. +pub struct MenuItemProps { + /// Label for this menu item + pub label: Box, +} + +impl Default for MenuItemProps { + fn default() -> Self { + Self { + label: Box::new(bsn_list!()), + } + } +} + +/// Menu item scene function +pub fn menu_item(props: MenuItemProps) -> impl Scene { + bsn! { + Node { + height: size::ROW_HEIGHT, + min_width: size::ROW_HEIGHT, + justify_content: JustifyContent::Start, + align_items: AlignItems::Center, + padding: UiRect::axes(Val::Px(8.0), Val::Px(0.)), + } + FeathersMenuItem + MenuItem + Hovered + EntityCursor::System(bevy_window::SystemCursorIcon::Pointer) + TabIndex(0) + ThemeBackgroundColor(tokens::MENU_BG) // Same as menu + ThemeFontColor(tokens::MENUITEM_TEXT) + InheritableFont { + font: fonts::REGULAR, + font_size: FontSize::Px(14.0), + weight: FontWeight::NORMAL, + } + Children [ + {props.label} + ] + } +} + +fn update_menuitem_styles( + q_menuitems: Query< + ( + Entity, + Has, + Has, + &Hovered, + &ThemeBackgroundColor, + &ThemeFontColor, + ), + ( + With, + Or<(Changed, Added, Added)>, + ), + >, + mut commands: Commands, + focus: Res, +) { + for (item_ent, disabled, pressed, hovered, bg_color, font_color) in q_menuitems.iter() { + set_menuitem_colors( + item_ent, + disabled, + pressed, + hovered.0, + Some(item_ent) == focus.0, + bg_color, + font_color, + &mut commands, + ); + } +} + +fn update_menuitem_styles_remove( + q_menuitems: Query< + ( + Entity, + Has, + Has, + &Hovered, + &ThemeBackgroundColor, + &ThemeFontColor, + ), + With, + >, + mut removed_disabled: RemovedComponents, + mut removed_pressed: RemovedComponents, + focus: Res, + mut commands: Commands, +) { + removed_disabled + .read() + .chain(removed_pressed.read()) + .for_each(|ent| { + if let Ok((item_ent, disabled, pressed, hovered, bg_color, font_color)) = + q_menuitems.get(ent) + { + set_menuitem_colors( + item_ent, + disabled, + pressed, + hovered.0, + Some(item_ent) == focus.0, + bg_color, + font_color, + &mut commands, + ); + } + }); +} + +fn update_menuitem_styles_focus_changed( + q_menuitems: Query< + ( + Entity, + Has, + Has, + &Hovered, + &ThemeBackgroundColor, + &ThemeFontColor, + ), + With, + >, + focus: Res, + mut commands: Commands, +) { + if focus.is_changed() { + for (item_ent, disabled, pressed, hovered, bg_color, font_color) in q_menuitems.iter() { + set_menuitem_colors( + item_ent, + disabled, + pressed, + hovered.0, + Some(item_ent) == focus.0, + bg_color, + font_color, + &mut commands, + ); + } + } +} + +fn set_menuitem_colors( + button_ent: Entity, + disabled: bool, + pressed: bool, + hovered: bool, + focused: bool, + bg_color: &ThemeBackgroundColor, + font_color: &ThemeFontColor, + commands: &mut Commands, +) { + let bg_token = match (focused, pressed, hovered) { + (true, _, _) => tokens::MENUITEM_BG_FOCUSED, + (false, true, _) => tokens::MENUITEM_BG_PRESSED, + (false, false, true) => tokens::MENUITEM_BG_HOVER, + (false, false, false) => tokens::MENU_BG, + }; + + let font_color_token = match disabled { + true => tokens::MENUITEM_TEXT_DISABLED, + false => tokens::MENUITEM_TEXT, + }; + + // Change background color + if bg_color.0 != bg_token { + commands + .entity(button_ent) + .insert(ThemeBackgroundColor(bg_token)); + } + + // Change font color + if font_color.0 != font_color_token { + commands + .entity(button_ent) + .insert(ThemeFontColor(font_color_token)); + } +} + +/// Plugin which registers the systems for updating the menu and menu button styles. +pub struct MenuPlugin; + +impl Plugin for MenuPlugin { + fn build(&self, app: &mut bevy_app::App) { + app.add_systems( + PreUpdate, + ( + update_menuitem_styles, + update_menuitem_styles_remove, + update_menuitem_styles_focus_changed, + ) + .in_set(PickingSystems::Last), + ); + } +} diff --git a/crates/bevy_feathers/src/controls/mod.rs b/crates/bevy_feathers/src/controls/mod.rs index 6de9952ea9b1f..25f190e9eafe1 100644 --- a/crates/bevy_feathers/src/controls/mod.rs +++ b/crates/bevy_feathers/src/controls/mod.rs @@ -6,32 +6,26 @@ mod checkbox; mod color_plane; mod color_slider; mod color_swatch; +mod menu; mod radio; mod slider; mod text_input; mod toggle_switch; mod virtual_keyboard; -pub use button::{button, button_bundle, ButtonPlugin, ButtonProps, ButtonVariant}; -pub use checkbox::{checkbox, checkbox_bundle, CheckboxPlugin}; -pub use color_plane::{color_plane, color_plane_bundle, ColorPlane, ColorPlaneValue}; -pub use color_slider::{ - color_slider, color_slider_bundle, ColorChannel, ColorSlider, ColorSliderPlugin, - ColorSliderProps, SliderBaseColor, -}; -pub use color_swatch::{ - color_swatch, color_swatch_bundle, ColorSwatch, ColorSwatchFg, ColorSwatchValue, -}; -pub use radio::{radio, radio_bundle, RadioPlugin}; -pub use slider::{slider, slider_bundle, SliderPlugin, SliderProps}; -pub use text_input::{text_input, text_input_container, TextInputPlugin, TextInputProps}; -pub use toggle_switch::{toggle_switch, toggle_switch_bundle, ToggleSwitchPlugin}; -pub use virtual_keyboard::{virtual_keyboard, virtual_keyboard_bundle, VirtualKeyPressed}; +pub use button::*; +pub use checkbox::*; +pub use color_plane::*; +pub use color_slider::*; +pub use color_swatch::*; +pub use menu::*; +pub use radio::*; +pub use slider::*; +pub use text_input::*; +pub use toggle_switch::*; +pub use virtual_keyboard::*; -use crate::{ - alpha_pattern::AlphaPatternPlugin, - controls::{color_plane::ColorPlanePlugin, color_swatch::ColorSwatchPlugin}, -}; +use crate::alpha_pattern::AlphaPatternPlugin; use bevy_app::Plugin; /// Plugin which registers all `bevy_feathers` controls. @@ -46,6 +40,7 @@ impl Plugin for ControlsPlugin { ColorPlanePlugin, ColorSliderPlugin, ColorSwatchPlugin, + MenuPlugin, RadioPlugin, SliderPlugin, TextInputPlugin, diff --git a/crates/bevy_feathers/src/dark_theme.rs b/crates/bevy_feathers/src/dark_theme.rs index f5db276eeca4c..f70cdf27070a2 100644 --- a/crates/bevy_feathers/src/dark_theme.rs +++ b/crates/bevy_feathers/src/dark_theme.rs @@ -1,6 +1,6 @@ //! The standard `bevy_feathers` dark theme. use crate::{palette, tokens}; -use bevy_color::{Alpha, Luminance}; +use bevy_color::{Alpha, Color, Luminance}; use bevy_platform::collections::HashMap; use crate::theme::ThemeProps; @@ -11,11 +11,12 @@ pub fn create_dark_theme() -> ThemeProps { color: HashMap::from([ (tokens::WINDOW_BG, palette::GRAY_0), (tokens::FOCUS_RING, palette::ACCENT.with_alpha(0.5)), - // Button + // Button (normal) (tokens::BUTTON_BG, palette::GRAY_3), (tokens::BUTTON_BG_HOVER, palette::GRAY_3.lighter(0.05)), (tokens::BUTTON_BG_PRESSED, palette::GRAY_3.lighter(0.1)), (tokens::BUTTON_BG_DISABLED, palette::GRAY_2), + // Button (primary) (tokens::BUTTON_PRIMARY_BG, palette::ACCENT), ( tokens::BUTTON_PRIMARY_BG_HOVER, @@ -26,6 +27,12 @@ pub fn create_dark_theme() -> ThemeProps { palette::ACCENT.lighter(0.1), ), (tokens::BUTTON_PRIMARY_BG_DISABLED, palette::GRAY_2), + // Button (plain) + (tokens::BUTTON_PLAIN_BG, Color::NONE), + (tokens::BUTTON_PLAIN_BG_HOVER, palette::GRAY_2), + (tokens::BUTTON_PLAIN_BG_PRESSED, palette::GRAY_3), + (tokens::BUTTON_PLAIN_BG_DISABLED, Color::NONE), + // Button text (tokens::BUTTON_TEXT, palette::WHITE), (tokens::BUTTON_TEXT_DISABLED, palette::WHITE.with_alpha(0.5)), (tokens::BUTTON_PRIMARY_TEXT, palette::WHITE), @@ -39,6 +46,10 @@ pub fn create_dark_theme() -> ThemeProps { (tokens::SLIDER_BAR_DISABLED, palette::GRAY_2), (tokens::SLIDER_TEXT, palette::WHITE), (tokens::SLIDER_TEXT_DISABLED, palette::WHITE.with_alpha(0.5)), + // Scrollbar + (tokens::SCROLLBAR_BG, palette::GRAY_2), + (tokens::SCROLLBAR_THUMB, palette::ACCENT), + (tokens::SCROLLBAR_THUMB_HOVER, palette::ACCENT.lighter(0.1)), // Checkbox (tokens::CHECKBOX_BG, palette::GRAY_3), (tokens::CHECKBOX_BG_CHECKED, palette::ACCENT), @@ -97,6 +108,17 @@ pub fn create_dark_theme() -> ThemeProps { palette::LIGHT_GRAY_2.with_alpha(0.3), ), (tokens::COLOR_PLANE_BG, palette::GRAY_1), + // Menus + (tokens::MENU_BG, palette::GRAY_1), + (tokens::MENU_BORDER, palette::WARM_GRAY_1), + (tokens::MENUITEM_BG_HOVER, palette::GRAY_1.lighter(0.05)), + (tokens::MENUITEM_BG_PRESSED, palette::GRAY_1.lighter(0.1)), + (tokens::MENUITEM_BG_FOCUSED, palette::GRAY_1.lighter(0.1)), + (tokens::MENUITEM_TEXT, palette::WHITE), + ( + tokens::MENUITEM_TEXT_DISABLED, + palette::WHITE.with_alpha(0.5), + ), // Text Input (tokens::TEXT_INPUT_BG, palette::GRAY_1), (tokens::TEXT_INPUT_TEXT, palette::LIGHT_GRAY_1), diff --git a/crates/bevy_feathers/src/icon.rs b/crates/bevy_feathers/src/icon.rs new file mode 100644 index 0000000000000..0bbde78666118 --- /dev/null +++ b/crates/bevy_feathers/src/icon.rs @@ -0,0 +1,18 @@ +//! BSN Template for loading images and displaying them as [`ImageNode`]s. +use bevy_asset::AssetServer; +use bevy_ecs::template::template; +use bevy_scene::{bsn, Scene}; +use bevy_ui::{widget::ImageNode, Node, Val}; + +/// Template which displays an icon. +pub fn icon(image: &'static str) -> impl Scene { + bsn! { + Node { + height: Val::Px(14.0), + } + template(move |entity| { + let handle = entity.resource::().load(image); + Ok(ImageNode::new(handle)) + }) + } +} diff --git a/crates/bevy_feathers/src/lib.rs b/crates/bevy_feathers/src/lib.rs index 3bb3e1b76e5c9..b38331206b7d1 100644 --- a/crates/bevy_feathers/src/lib.rs +++ b/crates/bevy_feathers/src/lib.rs @@ -18,6 +18,8 @@ //! Please report issues, submit fixes and propose changes. //! Thanks for stress-testing; let's build something better together. +extern crate alloc; + use bevy_app::{ HierarchyPropagatePlugin, Plugin, PluginGroup, PluginGroupBuilder, PostUpdate, PropagateSet, }; @@ -42,11 +44,14 @@ pub mod cursor; pub mod dark_theme; pub mod focus; pub mod font_styles; +mod icon; pub mod palette; pub mod rounded_corners; pub mod theme; pub mod tokens; +pub use icon::icon; + /// Plugin which installs observers and systems for feathers themes, cursors, and all controls. pub struct FeathersPlugin; @@ -61,6 +66,11 @@ impl Plugin for FeathersPlugin { embedded_asset!(app, "assets/fonts/FiraSans-Italic.ttf"); embedded_asset!(app, "assets/fonts/FiraMono-Medium.ttf"); + // Embedded icons + embedded_asset!(app, "assets/icons/chevron-down.png"); + embedded_asset!(app, "assets/icons/chevron-right.png"); + embedded_asset!(app, "assets/icons/x.png"); + // Embedded shader embedded_asset!(app, "assets/shaders/alpha_pattern.wgsl"); embedded_asset!(app, "assets/shaders/color_plane.wgsl"); diff --git a/crates/bevy_feathers/src/tokens.rs b/crates/bevy_feathers/src/tokens.rs index 4b3959c091f32..6c27bdc25dbe4 100644 --- a/crates/bevy_feathers/src/tokens.rs +++ b/crates/bevy_feathers/src/tokens.rs @@ -51,6 +51,20 @@ pub const BUTTON_PRIMARY_TEXT: ThemeToken = ThemeToken::new_static("feathers.but pub const BUTTON_PRIMARY_TEXT_DISABLED: ThemeToken = ThemeToken::new_static("feathers.button.primary.txt.disabled"); +// Plain buttons (transparent background) + +/// Plain button background +pub const BUTTON_PLAIN_BG: ThemeToken = ThemeToken::new_static("feathers.button.plain.bg"); +/// Plain button background (hovered) +pub const BUTTON_PLAIN_BG_HOVER: ThemeToken = + ThemeToken::new_static("feathers.button.plain.bg.hover"); +/// Plain button background (disabled) +pub const BUTTON_PLAIN_BG_DISABLED: ThemeToken = + ThemeToken::new_static("feathers.button.plain.bg.disabled"); +/// Plain button background (pressed) +pub const BUTTON_PLAIN_BG_PRESSED: ThemeToken = + ThemeToken::new_static("feathers.button.plain.bg.pressed"); + // Slider /// Background for slider @@ -65,6 +79,16 @@ pub const SLIDER_TEXT: ThemeToken = ThemeToken::new_static("feathers.slider.text pub const SLIDER_TEXT_DISABLED: ThemeToken = ThemeToken::new_static("feathers.slider.text.disabled"); +// Scrollbar + +/// Background for scrollbar +pub const SCROLLBAR_BG: ThemeToken = ThemeToken::new_static("feathers.scrollbar.bg"); +/// Background for scrollbar moving bar +pub const SCROLLBAR_THUMB: ThemeToken = ThemeToken::new_static("feathers.scrollbar.thumb"); +/// Background for scrollbar moving bar (hovered) +pub const SCROLLBAR_THUMB_HOVER: ThemeToken = + ThemeToken::new_static("feathers.scrollbar.thumb.hover"); + // Checkbox /// Checkbox background around the checkmark @@ -143,6 +167,24 @@ pub const SWITCH_SLIDE_DISABLED: ThemeToken = /// Color plane frame background pub const COLOR_PLANE_BG: ThemeToken = ThemeToken::new_static("feathers.colorplane.bg"); +// Menus + +/// Menu background +pub const MENU_BG: ThemeToken = ThemeToken::new_static("feathers.menu.bg"); +/// Menu border +pub const MENU_BORDER: ThemeToken = ThemeToken::new_static("feathers.menu.border"); +/// Menu item hovered +pub const MENUITEM_BG_HOVER: ThemeToken = ThemeToken::new_static("feathers.menuitem.bg.hover"); +/// Menu item pressed +pub const MENUITEM_BG_PRESSED: ThemeToken = ThemeToken::new_static("feathers.menuitem.bg.pressed"); +/// Menu item focused +pub const MENUITEM_BG_FOCUSED: ThemeToken = ThemeToken::new_static("feathers.menuitem.bg.focused"); +/// Menu item text +pub const MENUITEM_TEXT: ThemeToken = ThemeToken::new_static("feathers.menuitem.text"); +/// Menu item text (disabled) +pub const MENUITEM_TEXT_DISABLED: ThemeToken = + ThemeToken::new_static("feathers.menuitem.text.disabled"); + // Text Input /// Background for text input diff --git a/crates/bevy_input_focus/src/tab_navigation.rs b/crates/bevy_input_focus/src/tab_navigation.rs index 4ee868efdda38..3f1b64640df67 100644 --- a/crates/bevy_input_focus/src/tab_navigation.rs +++ b/crates/bevy_input_focus/src/tab_navigation.rs @@ -101,7 +101,7 @@ impl TabGroup { /// A navigation action that users might take to navigate your user interface in a cyclic fashion. /// /// These values are consumed by the [`TabNavigation`] system param. -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Debug, PartialEq)] pub enum NavAction { /// Navigate to the next focusable entity, wrapping around to the beginning if at the end. /// diff --git a/crates/bevy_ui/src/ui_node.rs b/crates/bevy_ui/src/ui_node.rs index 0867ab144aced..e7f9080240164 100644 --- a/crates/bevy_ui/src/ui_node.rs +++ b/crates/bevy_ui/src/ui_node.rs @@ -2412,7 +2412,7 @@ pub struct CalculatedClip { /// UI node entities with this component will ignore any clipping rect they inherit, /// the node will not be clipped regardless of its ancestors' `Overflow` setting. -#[derive(Component)] +#[derive(Component, Clone, Default)] pub struct OverrideClip; #[expect( diff --git a/crates/bevy_ui_widgets/src/button.rs b/crates/bevy_ui_widgets/src/button.rs index 30b2e60735616..ead745e09a307 100644 --- a/crates/bevy_ui_widgets/src/button.rs +++ b/crates/bevy_ui_widgets/src/button.rs @@ -24,6 +24,11 @@ use crate::Activate; #[require(AccessibilityNode(accesskit::Node::new(Role::Button)))] pub struct Button; +/// Optional marker component that indicates we want the button to activate on the pointer down +/// event, this is used for menu buttons. +#[derive(Component, Default, Debug, Clone)] +pub struct ActivateOnPress; + fn button_on_key_event( mut event: On>, q_state: Query, With