Bevy Feathers: an opinionated widget toolkit for building Bevy tooling (#19730)
# Objective This PR introduces Bevy Feathers, an opinionated widget toolkit and theming system intended for use by the Bevy Editor, World Inspector, and other tools. The `bevy_feathers` crate is incomplete and hidden behind an experimental feature flag. The API is going to change significantly before release. --------- Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
This commit is contained in:
parent
fa9e303e61
commit
65bddbd3e4
16
Cargo.toml
16
Cargo.toml
@ -306,6 +306,9 @@ bevy_input_focus = ["bevy_internal/bevy_input_focus"]
|
||||
# Headless widget collection for Bevy UI.
|
||||
bevy_core_widgets = ["bevy_internal/bevy_core_widgets"]
|
||||
|
||||
# Feathers widget collection.
|
||||
experimental_bevy_feathers = ["bevy_internal/bevy_feathers"]
|
||||
|
||||
# Enable passthrough loading for SPIR-V shaders (Only supported on Vulkan, shader capabilities and extensions must agree with the platform implementation)
|
||||
spirv_shader_passthrough = ["bevy_internal/spirv_shader_passthrough"]
|
||||
|
||||
@ -4526,3 +4529,16 @@ name = "Core Widgets (w/Observers)"
|
||||
description = "Demonstrates use of core (headless) widgets in Bevy UI, with Observers"
|
||||
category = "UI (User Interface)"
|
||||
wasm = true
|
||||
|
||||
[[example]]
|
||||
name = "feathers"
|
||||
path = "examples/ui/feathers.rs"
|
||||
doc-scrape-examples = true
|
||||
required-features = ["experimental_bevy_feathers"]
|
||||
|
||||
[package.metadata.example.feathers]
|
||||
name = "Feathers Widgets"
|
||||
description = "Gallery of Feathers Widgets"
|
||||
category = "UI (User Interface)"
|
||||
wasm = true
|
||||
hidden = true
|
||||
|
@ -18,7 +18,7 @@ use bevy_ui::{InteractionDisabled, Pressed};
|
||||
/// Headless button widget. This widget maintains a "pressed" state, which is used to
|
||||
/// indicate whether the button is currently being pressed by the user. It emits a `ButtonClicked`
|
||||
/// event when the button is un-pressed.
|
||||
#[derive(Component, Debug)]
|
||||
#[derive(Component, Default, Debug)]
|
||||
#[require(AccessibilityNode(accesskit::Node::new(Role::Button)))]
|
||||
pub struct CoreButton {
|
||||
/// Optional system to run when the button is clicked, or when the Enter or Space key
|
||||
|
@ -92,7 +92,9 @@ pub struct SliderValue(pub f32);
|
||||
#[derive(Component, Debug, PartialEq, Clone, Copy)]
|
||||
#[component(immutable)]
|
||||
pub struct SliderRange {
|
||||
/// The beginning of the allowed range for the slider value.
|
||||
start: f32,
|
||||
/// The end of the allowed range for the slider value.
|
||||
end: f32,
|
||||
}
|
||||
|
||||
|
40
crates/bevy_feathers/Cargo.toml
Normal file
40
crates/bevy_feathers/Cargo.toml
Normal file
@ -0,0 +1,40 @@
|
||||
[package]
|
||||
name = "bevy_feathers"
|
||||
version = "0.17.0-dev"
|
||||
edition = "2024"
|
||||
description = "A collection of UI widgets for building editors and utilities in Bevy"
|
||||
homepage = "https://bevyengine.org"
|
||||
repository = "https://github.com/bevyengine/bevy"
|
||||
license = "MIT OR Apache-2.0"
|
||||
keywords = ["bevy"]
|
||||
|
||||
[dependencies]
|
||||
# bevy
|
||||
bevy_app = { path = "../bevy_app", version = "0.17.0-dev" }
|
||||
bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev" }
|
||||
bevy_color = { path = "../bevy_color", version = "0.17.0-dev" }
|
||||
bevy_core_widgets = { path = "../bevy_core_widgets", version = "0.17.0-dev" }
|
||||
bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" }
|
||||
bevy_input_focus = { path = "../bevy_input_focus", version = "0.17.0-dev" }
|
||||
bevy_log = { path = "../bevy_log", version = "0.17.0-dev" }
|
||||
bevy_picking = { path = "../bevy_picking", version = "0.17.0-dev" }
|
||||
bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev" }
|
||||
bevy_text = { path = "../bevy_text", version = "0.17.0-dev" }
|
||||
bevy_ui = { path = "../bevy_ui", version = "0.17.0-dev", features = [
|
||||
"bevy_ui_picking_backend",
|
||||
] }
|
||||
bevy_window = { path = "../bevy_window", version = "0.17.0-dev" }
|
||||
bevy_winit = { path = "../bevy_winit", version = "0.17.0-dev" }
|
||||
|
||||
# other
|
||||
accesskit = "0.19"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"]
|
||||
all-features = true
|
93
crates/bevy_feathers/src/assets/fonts/FiraMono-LICENSE
Normal file
93
crates/bevy_feathers/src/assets/fonts/FiraMono-LICENSE
Normal file
@ -0,0 +1,93 @@
|
||||
Digitized data copyright (c) 2012-2015, The Mozilla Foundation and Telefonica S.A.
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
BIN
crates/bevy_feathers/src/assets/fonts/FiraMono-Medium.ttf
Executable file
BIN
crates/bevy_feathers/src/assets/fonts/FiraMono-Medium.ttf
Executable file
Binary file not shown.
BIN
crates/bevy_feathers/src/assets/fonts/FiraSans-Bold.ttf
Normal file
BIN
crates/bevy_feathers/src/assets/fonts/FiraSans-Bold.ttf
Normal file
Binary file not shown.
BIN
crates/bevy_feathers/src/assets/fonts/FiraSans-BoldItalic.ttf
Normal file
BIN
crates/bevy_feathers/src/assets/fonts/FiraSans-BoldItalic.ttf
Normal file
Binary file not shown.
BIN
crates/bevy_feathers/src/assets/fonts/FiraSans-Italic.ttf
Normal file
BIN
crates/bevy_feathers/src/assets/fonts/FiraSans-Italic.ttf
Normal file
Binary file not shown.
93
crates/bevy_feathers/src/assets/fonts/FiraSans-License.txt
Normal file
93
crates/bevy_feathers/src/assets/fonts/FiraSans-License.txt
Normal file
@ -0,0 +1,93 @@
|
||||
Copyright (c) 2012-2015, The Mozilla Foundation and Telefonica S.A.
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://openfontlicense.org
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
BIN
crates/bevy_feathers/src/assets/fonts/FiraSans-Regular.ttf
Normal file
BIN
crates/bevy_feathers/src/assets/fonts/FiraSans-Regular.ttf
Normal file
Binary file not shown.
23
crates/bevy_feathers/src/constants.rs
Normal file
23
crates/bevy_feathers/src/constants.rs
Normal file
@ -0,0 +1,23 @@
|
||||
//! Various non-themable constants for the Feathers look and feel.
|
||||
|
||||
/// Font asset paths
|
||||
pub mod fonts {
|
||||
/// Default regular font path
|
||||
pub const REGULAR: &str = "embedded://bevy_feathers/assets/fonts/FiraSans-Regular.ttf";
|
||||
/// Regular italic font path
|
||||
pub const ITALIC: &str = "embedded://bevy_feathers/assets/fonts/FiraSans-Italic.ttf";
|
||||
/// Bold font path
|
||||
pub const BOLD: &str = "embedded://bevy_feathers/assets/fonts/FiraSans-Bold.ttf";
|
||||
/// Bold italic font path
|
||||
pub const BOLD_ITALIC: &str = "embedded://bevy_feathers/assets/fonts/FiraSans-BoldItalic.ttf";
|
||||
/// Monospace font path
|
||||
pub const MONO: &str = "embedded://bevy_feathers/assets/fonts/FiraMono-Medium.ttf";
|
||||
}
|
||||
|
||||
/// Size constants
|
||||
pub mod size {
|
||||
use bevy_ui::Val;
|
||||
|
||||
/// Common row size for buttons, sliders, spinners, etc.
|
||||
pub const ROW_HEIGHT: Val = Val::Px(22.0);
|
||||
}
|
208
crates/bevy_feathers/src/controls/button.rs
Normal file
208
crates/bevy_feathers/src/controls/button.rs
Normal file
@ -0,0 +1,208 @@
|
||||
use bevy_app::{Plugin, PreUpdate};
|
||||
use bevy_core_widgets::CoreButton;
|
||||
use bevy_ecs::{
|
||||
bundle::Bundle,
|
||||
component::Component,
|
||||
entity::Entity,
|
||||
hierarchy::{ChildOf, Children},
|
||||
lifecycle::RemovedComponents,
|
||||
query::{Added, Changed, Has, Or},
|
||||
schedule::IntoScheduleConfigs,
|
||||
spawn::{SpawnRelated, SpawnableList},
|
||||
system::{Commands, Query, SystemId},
|
||||
};
|
||||
use bevy_input_focus::tab_navigation::TabIndex;
|
||||
use bevy_picking::{hover::Hovered, PickingSystems};
|
||||
use bevy_ui::{AlignItems, InteractionDisabled, JustifyContent, Node, Pressed, UiRect, Val};
|
||||
use bevy_winit::cursor::CursorIcon;
|
||||
|
||||
use crate::{
|
||||
constants::{fonts, size},
|
||||
font_styles::InheritableFont,
|
||||
handle_or_path::HandleOrPath,
|
||||
rounded_corners::RoundedCorners,
|
||||
theme::{ThemeBackgroundColor, ThemeFontColor},
|
||||
tokens,
|
||||
};
|
||||
|
||||
/// Color variants for buttons. This also functions as a component used by the dynamic styling
|
||||
/// system to identify which entities are buttons.
|
||||
#[derive(Component, Default, Clone)]
|
||||
pub enum ButtonVariant {
|
||||
/// The standard button appearance
|
||||
#[default]
|
||||
Normal,
|
||||
/// A button with a more prominent color, this is used for "call to action" buttons,
|
||||
/// default buttons for dialog boxes, and so on.
|
||||
Primary,
|
||||
}
|
||||
|
||||
/// Parameters for the button template, passed to [`button`] function.
|
||||
#[derive(Default, Clone)]
|
||||
pub struct ButtonProps {
|
||||
/// Color variant for the button.
|
||||
pub variant: ButtonVariant,
|
||||
/// Rounded corners options
|
||||
pub corners: RoundedCorners,
|
||||
/// Click handler
|
||||
pub on_click: Option<SystemId>,
|
||||
}
|
||||
|
||||
/// Template function to spawn a button.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `props` - construction properties for the button.
|
||||
/// * `overrides` - a bundle of components that are merged in with the normal button components.
|
||||
/// * `children` - a [`SpawnableList`] of child elements, such as a label or icon for the button.
|
||||
pub fn button<C: SpawnableList<ChildOf> + Send + Sync + 'static, B: Bundle>(
|
||||
props: ButtonProps,
|
||||
overrides: B,
|
||||
children: C,
|
||||
) -> impl Bundle {
|
||||
(
|
||||
Node {
|
||||
height: size::ROW_HEIGHT,
|
||||
justify_content: JustifyContent::Center,
|
||||
align_items: AlignItems::Center,
|
||||
padding: UiRect::axes(Val::Px(8.0), Val::Px(0.)),
|
||||
flex_grow: 1.0,
|
||||
..Default::default()
|
||||
},
|
||||
CoreButton {
|
||||
on_click: props.on_click,
|
||||
},
|
||||
props.variant,
|
||||
Hovered::default(),
|
||||
CursorIcon::System(bevy_window::SystemCursorIcon::Pointer),
|
||||
TabIndex(0),
|
||||
props.corners.to_border_radius(4.0),
|
||||
ThemeBackgroundColor(tokens::BUTTON_BG),
|
||||
ThemeFontColor(tokens::BUTTON_TEXT),
|
||||
InheritableFont {
|
||||
font: HandleOrPath::Path(fonts::REGULAR.to_owned()),
|
||||
font_size: 14.0,
|
||||
},
|
||||
overrides,
|
||||
Children::spawn(children),
|
||||
)
|
||||
}
|
||||
|
||||
fn update_button_styles(
|
||||
q_buttons: Query<
|
||||
(
|
||||
Entity,
|
||||
&ButtonVariant,
|
||||
Has<InteractionDisabled>,
|
||||
Has<Pressed>,
|
||||
&Hovered,
|
||||
&ThemeBackgroundColor,
|
||||
&ThemeFontColor,
|
||||
),
|
||||
Or<(Changed<Hovered>, Added<Pressed>, Added<InteractionDisabled>)>,
|
||||
>,
|
||||
mut commands: Commands,
|
||||
) {
|
||||
for (button_ent, variant, disabled, pressed, hovered, bg_color, font_color) in q_buttons.iter()
|
||||
{
|
||||
set_button_colors(
|
||||
button_ent,
|
||||
variant,
|
||||
disabled,
|
||||
pressed,
|
||||
hovered.0,
|
||||
bg_color,
|
||||
font_color,
|
||||
&mut commands,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn update_button_styles_remove(
|
||||
q_buttons: Query<(
|
||||
Entity,
|
||||
&ButtonVariant,
|
||||
Has<InteractionDisabled>,
|
||||
Has<Pressed>,
|
||||
&Hovered,
|
||||
&ThemeBackgroundColor,
|
||||
&ThemeFontColor,
|
||||
)>,
|
||||
mut removed_disabled: RemovedComponents<InteractionDisabled>,
|
||||
mut removed_pressed: RemovedComponents<Pressed>,
|
||||
mut commands: Commands,
|
||||
) {
|
||||
removed_disabled
|
||||
.read()
|
||||
.chain(removed_pressed.read())
|
||||
.for_each(|ent| {
|
||||
if let Ok((button_ent, variant, disabled, pressed, hovered, bg_color, font_color)) =
|
||||
q_buttons.get(ent)
|
||||
{
|
||||
set_button_colors(
|
||||
button_ent,
|
||||
variant,
|
||||
disabled,
|
||||
pressed,
|
||||
hovered.0,
|
||||
bg_color,
|
||||
font_color,
|
||||
&mut commands,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn set_button_colors(
|
||||
button_ent: Entity,
|
||||
variant: &ButtonVariant,
|
||||
disabled: bool,
|
||||
pressed: bool,
|
||||
hovered: bool,
|
||||
bg_color: &ThemeBackgroundColor,
|
||||
font_color: &ThemeFontColor,
|
||||
commands: &mut Commands,
|
||||
) {
|
||||
let bg_token = match (variant, disabled, pressed, hovered) {
|
||||
(ButtonVariant::Normal, true, _, _) => tokens::BUTTON_BG_DISABLED,
|
||||
(ButtonVariant::Normal, false, true, _) => tokens::BUTTON_BG_PRESSED,
|
||||
(ButtonVariant::Normal, false, false, true) => tokens::BUTTON_BG_HOVER,
|
||||
(ButtonVariant::Normal, false, false, false) => tokens::BUTTON_BG,
|
||||
(ButtonVariant::Primary, true, _, _) => tokens::BUTTON_PRIMARY_BG_DISABLED,
|
||||
(ButtonVariant::Primary, false, true, _) => tokens::BUTTON_PRIMARY_BG_PRESSED,
|
||||
(ButtonVariant::Primary, false, false, true) => tokens::BUTTON_PRIMARY_BG_HOVER,
|
||||
(ButtonVariant::Primary, false, false, false) => tokens::BUTTON_PRIMARY_BG,
|
||||
};
|
||||
|
||||
let font_color_token = match (variant, disabled) {
|
||||
(ButtonVariant::Normal, true) => tokens::BUTTON_TEXT_DISABLED,
|
||||
(ButtonVariant::Normal, false) => tokens::BUTTON_TEXT,
|
||||
(ButtonVariant::Primary, true) => tokens::BUTTON_PRIMARY_TEXT_DISABLED,
|
||||
(ButtonVariant::Primary, false) => tokens::BUTTON_PRIMARY_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 button styles.
|
||||
pub struct ButtonPlugin;
|
||||
|
||||
impl Plugin for ButtonPlugin {
|
||||
fn build(&self, app: &mut bevy_app::App) {
|
||||
app.add_systems(
|
||||
PreUpdate,
|
||||
(update_button_styles, update_button_styles_remove).in_set(PickingSystems::Last),
|
||||
);
|
||||
}
|
||||
}
|
17
crates/bevy_feathers/src/controls/mod.rs
Normal file
17
crates/bevy_feathers/src/controls/mod.rs
Normal file
@ -0,0 +1,17 @@
|
||||
//! Meta-module containing all feathers controls (widgets that are interactive).
|
||||
use bevy_app::Plugin;
|
||||
|
||||
mod button;
|
||||
mod slider;
|
||||
|
||||
pub use button::{button, ButtonPlugin, ButtonProps, ButtonVariant};
|
||||
pub use slider::{slider, SliderPlugin, SliderProps};
|
||||
|
||||
/// Plugin which registers all `bevy_feathers` controls.
|
||||
pub struct ControlsPlugin;
|
||||
|
||||
impl Plugin for ControlsPlugin {
|
||||
fn build(&self, app: &mut bevy_app::App) {
|
||||
app.add_plugins((ButtonPlugin, SliderPlugin));
|
||||
}
|
||||
}
|
209
crates/bevy_feathers/src/controls/slider.rs
Normal file
209
crates/bevy_feathers/src/controls/slider.rs
Normal file
@ -0,0 +1,209 @@
|
||||
use core::f32::consts::PI;
|
||||
|
||||
use bevy_app::{Plugin, PreUpdate};
|
||||
use bevy_color::Color;
|
||||
use bevy_core_widgets::{CoreSlider, SliderRange, SliderValue, TrackClick};
|
||||
use bevy_ecs::{
|
||||
bundle::Bundle,
|
||||
children,
|
||||
component::Component,
|
||||
entity::Entity,
|
||||
hierarchy::Children,
|
||||
lifecycle::RemovedComponents,
|
||||
query::{Added, Changed, Has, Or, Spawned, With},
|
||||
schedule::IntoScheduleConfigs,
|
||||
spawn::SpawnRelated,
|
||||
system::{In, Query, Res, SystemId},
|
||||
};
|
||||
use bevy_input_focus::tab_navigation::TabIndex;
|
||||
use bevy_picking::PickingSystems;
|
||||
use bevy_ui::{
|
||||
widget::Text, AlignItems, BackgroundGradient, ColorStop, Display, FlexDirection, Gradient,
|
||||
InteractionDisabled, InterpolationColorSpace, JustifyContent, LinearGradient, Node, UiRect,
|
||||
Val,
|
||||
};
|
||||
use bevy_winit::cursor::CursorIcon;
|
||||
|
||||
use crate::{
|
||||
constants::{fonts, size},
|
||||
font_styles::InheritableFont,
|
||||
handle_or_path::HandleOrPath,
|
||||
rounded_corners::RoundedCorners,
|
||||
theme::{ThemeFontColor, ThemedText, UiTheme},
|
||||
tokens,
|
||||
};
|
||||
|
||||
/// Slider template properties, passed to [`slider`] function.
|
||||
#[derive(Clone)]
|
||||
pub struct SliderProps {
|
||||
/// Slider current value
|
||||
pub value: f32,
|
||||
/// Slider minimum value
|
||||
pub min: f32,
|
||||
/// Slider maximum value
|
||||
pub max: f32,
|
||||
/// On-change handler
|
||||
pub on_change: Option<SystemId<In<f32>>>,
|
||||
}
|
||||
|
||||
impl Default for SliderProps {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
value: 0.0,
|
||||
min: 0.0,
|
||||
max: 1.0,
|
||||
on_change: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component, Default, Clone)]
|
||||
#[require(CoreSlider)]
|
||||
struct SliderStyle;
|
||||
|
||||
/// Marker for the text
|
||||
#[derive(Component, Default, Clone)]
|
||||
struct SliderValueText;
|
||||
|
||||
/// Spawn a new slider widget.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `props` - construction properties for the slider.
|
||||
/// * `overrides` - a bundle of components that are merged in with the normal slider components.
|
||||
pub fn slider<B: Bundle>(props: SliderProps, overrides: B) -> impl Bundle {
|
||||
(
|
||||
Node {
|
||||
height: size::ROW_HEIGHT,
|
||||
justify_content: JustifyContent::Center,
|
||||
align_items: AlignItems::Center,
|
||||
padding: UiRect::axes(Val::Px(8.0), Val::Px(0.)),
|
||||
flex_grow: 1.0,
|
||||
..Default::default()
|
||||
},
|
||||
CoreSlider {
|
||||
on_change: props.on_change,
|
||||
track_click: TrackClick::Drag,
|
||||
},
|
||||
SliderStyle,
|
||||
SliderValue(props.value),
|
||||
SliderRange::new(props.min, props.max),
|
||||
CursorIcon::System(bevy_window::SystemCursorIcon::EwResize),
|
||||
TabIndex(0),
|
||||
RoundedCorners::All.to_border_radius(6.0),
|
||||
// Use a gradient to draw the moving bar
|
||||
BackgroundGradient(vec![Gradient::Linear(LinearGradient {
|
||||
angle: PI * 0.5,
|
||||
stops: vec![
|
||||
ColorStop::new(Color::NONE, Val::Percent(0.)),
|
||||
ColorStop::new(Color::NONE, Val::Percent(50.)),
|
||||
ColorStop::new(Color::NONE, Val::Percent(50.)),
|
||||
ColorStop::new(Color::NONE, Val::Percent(100.)),
|
||||
],
|
||||
color_space: InterpolationColorSpace::Srgb,
|
||||
})]),
|
||||
overrides,
|
||||
children![(
|
||||
// Text container
|
||||
Node {
|
||||
display: Display::Flex,
|
||||
flex_direction: FlexDirection::Row,
|
||||
align_items: AlignItems::Center,
|
||||
justify_content: JustifyContent::Center,
|
||||
..Default::default()
|
||||
},
|
||||
ThemeFontColor(tokens::SLIDER_TEXT),
|
||||
InheritableFont {
|
||||
font: HandleOrPath::Path(fonts::MONO.to_owned()),
|
||||
font_size: 12.0,
|
||||
},
|
||||
children![(Text::new("10.0"), ThemedText, SliderValueText,)],
|
||||
)],
|
||||
)
|
||||
}
|
||||
|
||||
fn update_slider_colors(
|
||||
mut q_sliders: Query<
|
||||
(Has<InteractionDisabled>, &mut BackgroundGradient),
|
||||
(With<SliderStyle>, Or<(Spawned, Added<InteractionDisabled>)>),
|
||||
>,
|
||||
theme: Res<UiTheme>,
|
||||
) {
|
||||
for (disabled, mut gradient) in q_sliders.iter_mut() {
|
||||
set_slider_colors(&theme, disabled, gradient.as_mut());
|
||||
}
|
||||
}
|
||||
|
||||
fn update_slider_colors_remove(
|
||||
mut q_sliders: Query<(Has<InteractionDisabled>, &mut BackgroundGradient)>,
|
||||
mut removed_disabled: RemovedComponents<InteractionDisabled>,
|
||||
theme: Res<UiTheme>,
|
||||
) {
|
||||
removed_disabled.read().for_each(|ent| {
|
||||
if let Ok((disabled, mut gradient)) = q_sliders.get_mut(ent) {
|
||||
set_slider_colors(&theme, disabled, gradient.as_mut());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn set_slider_colors(theme: &Res<'_, UiTheme>, disabled: bool, gradient: &mut BackgroundGradient) {
|
||||
let bar_color = theme.color(match disabled {
|
||||
true => tokens::SLIDER_BAR_DISABLED,
|
||||
false => tokens::SLIDER_BAR,
|
||||
});
|
||||
let bg_color = theme.color(tokens::SLIDER_BG);
|
||||
if let [Gradient::Linear(linear_gradient)] = &mut gradient.0[..] {
|
||||
linear_gradient.stops[0].color = bar_color;
|
||||
linear_gradient.stops[1].color = bar_color;
|
||||
linear_gradient.stops[2].color = bg_color;
|
||||
linear_gradient.stops[3].color = bg_color;
|
||||
}
|
||||
}
|
||||
|
||||
fn update_slider_pos(
|
||||
mut q_sliders: Query<
|
||||
(Entity, &SliderValue, &SliderRange, &mut BackgroundGradient),
|
||||
(
|
||||
With<SliderStyle>,
|
||||
Or<(
|
||||
Changed<SliderValue>,
|
||||
Changed<SliderRange>,
|
||||
Changed<Children>,
|
||||
)>,
|
||||
),
|
||||
>,
|
||||
q_children: Query<&Children>,
|
||||
mut q_slider_text: Query<&mut Text, With<SliderValueText>>,
|
||||
) {
|
||||
for (slider_ent, value, range, mut gradient) in q_sliders.iter_mut() {
|
||||
if let [Gradient::Linear(linear_gradient)] = &mut gradient.0[..] {
|
||||
let percent_value = range.thumb_position(value.0) * 100.0;
|
||||
linear_gradient.stops[1].point = Val::Percent(percent_value);
|
||||
linear_gradient.stops[2].point = Val::Percent(percent_value);
|
||||
}
|
||||
|
||||
// Find slider text child entity and update its text with the formatted value
|
||||
q_children.iter_descendants(slider_ent).for_each(|child| {
|
||||
if let Ok(mut text) = q_slider_text.get_mut(child) {
|
||||
text.0 = format!("{}", value.0);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Plugin which registers the systems for updating the slider styles.
|
||||
pub struct SliderPlugin;
|
||||
|
||||
impl Plugin for SliderPlugin {
|
||||
fn build(&self, app: &mut bevy_app::App) {
|
||||
app.add_systems(
|
||||
PreUpdate,
|
||||
(
|
||||
update_slider_colors,
|
||||
update_slider_colors_remove,
|
||||
update_slider_pos,
|
||||
)
|
||||
.in_set(PickingSystems::Last),
|
||||
);
|
||||
}
|
||||
}
|
70
crates/bevy_feathers/src/cursor.rs
Normal file
70
crates/bevy_feathers/src/cursor.rs
Normal file
@ -0,0 +1,70 @@
|
||||
//! Provides a way to automatically set the mouse cursor based on hovered entity.
|
||||
use bevy_app::{App, Plugin, PreUpdate};
|
||||
use bevy_ecs::{
|
||||
entity::Entity,
|
||||
hierarchy::ChildOf,
|
||||
resource::Resource,
|
||||
schedule::IntoScheduleConfigs,
|
||||
system::{Commands, Query, Res},
|
||||
};
|
||||
use bevy_picking::{hover::HoverMap, pointer::PointerId, PickingSystems};
|
||||
use bevy_window::Window;
|
||||
use bevy_winit::cursor::CursorIcon;
|
||||
|
||||
/// A component that specifies the cursor icon to be used when the mouse is not hovering over
|
||||
/// any other entity. This is used to set the default cursor icon for the window.
|
||||
#[derive(Resource, Debug, Clone, Default)]
|
||||
pub struct DefaultCursorIcon(pub CursorIcon);
|
||||
|
||||
/// System which updates the window cursor icon whenever the mouse hovers over an entity with
|
||||
/// a [`CursorIcon`] component. If no entity is hovered, the cursor icon is set to
|
||||
/// the cursor in the [`DefaultCursorIcon`] resource.
|
||||
pub(crate) fn update_cursor(
|
||||
mut commands: Commands,
|
||||
hover_map: Option<Res<HoverMap>>,
|
||||
parent_query: Query<&ChildOf>,
|
||||
cursor_query: Query<&CursorIcon>,
|
||||
mut q_windows: Query<(Entity, &mut Window, Option<&CursorIcon>)>,
|
||||
r_default_cursor: Res<DefaultCursorIcon>,
|
||||
) {
|
||||
let cursor = hover_map.and_then(|hover_map| match hover_map.get(&PointerId::Mouse) {
|
||||
Some(hover_set) => hover_set.keys().find_map(|entity| {
|
||||
cursor_query.get(*entity).ok().or_else(|| {
|
||||
parent_query
|
||||
.iter_ancestors(*entity)
|
||||
.find_map(|e| cursor_query.get(e).ok())
|
||||
})
|
||||
}),
|
||||
None => None,
|
||||
});
|
||||
|
||||
let mut windows_to_change: Vec<Entity> = Vec::new();
|
||||
for (entity, _window, prev_cursor) in q_windows.iter_mut() {
|
||||
match (cursor, prev_cursor) {
|
||||
(Some(cursor), Some(prev_cursor)) if cursor == prev_cursor => continue,
|
||||
(None, None) => continue,
|
||||
_ => {
|
||||
windows_to_change.push(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
windows_to_change.iter().for_each(|entity| {
|
||||
if let Some(cursor) = cursor {
|
||||
commands.entity(*entity).insert(cursor.clone());
|
||||
} else {
|
||||
commands.entity(*entity).insert(r_default_cursor.0.clone());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Plugin that supports automatically changing the cursor based on the hovered entity.
|
||||
pub struct CursorIconPlugin;
|
||||
|
||||
impl Plugin for CursorIconPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
if app.world().get_resource::<DefaultCursorIcon>().is_none() {
|
||||
app.init_resource::<DefaultCursorIcon>();
|
||||
}
|
||||
app.add_systems(PreUpdate, update_cursor.in_set(PickingSystems::Last));
|
||||
}
|
||||
}
|
53
crates/bevy_feathers/src/dark_theme.rs
Normal file
53
crates/bevy_feathers/src/dark_theme.rs
Normal file
@ -0,0 +1,53 @@
|
||||
//! The standard `bevy_feathers` dark theme.
|
||||
use crate::{palette, tokens};
|
||||
use bevy_color::{Alpha, Luminance};
|
||||
use bevy_platform::collections::HashMap;
|
||||
|
||||
use crate::theme::ThemeProps;
|
||||
|
||||
/// Create a [`ThemeProps`] object and populate it with the colors for the default dark theme.
|
||||
pub fn create_dark_theme() -> ThemeProps {
|
||||
ThemeProps {
|
||||
color: HashMap::from([
|
||||
(tokens::WINDOW_BG.into(), palette::GRAY_0),
|
||||
(tokens::BUTTON_BG.into(), palette::GRAY_3),
|
||||
(
|
||||
tokens::BUTTON_BG_HOVER.into(),
|
||||
palette::GRAY_3.lighter(0.05),
|
||||
),
|
||||
(
|
||||
tokens::BUTTON_BG_PRESSED.into(),
|
||||
palette::GRAY_3.lighter(0.1),
|
||||
),
|
||||
(tokens::BUTTON_BG_DISABLED.into(), palette::GRAY_2),
|
||||
(tokens::BUTTON_PRIMARY_BG.into(), palette::ACCENT),
|
||||
(
|
||||
tokens::BUTTON_PRIMARY_BG_HOVER.into(),
|
||||
palette::ACCENT.lighter(0.05),
|
||||
),
|
||||
(
|
||||
tokens::BUTTON_PRIMARY_BG_PRESSED.into(),
|
||||
palette::ACCENT.lighter(0.1),
|
||||
),
|
||||
(tokens::BUTTON_PRIMARY_BG_DISABLED.into(), palette::GRAY_2),
|
||||
(tokens::BUTTON_TEXT.into(), palette::WHITE),
|
||||
(
|
||||
tokens::BUTTON_TEXT_DISABLED.into(),
|
||||
palette::WHITE.with_alpha(0.5),
|
||||
),
|
||||
(tokens::BUTTON_PRIMARY_TEXT.into(), palette::WHITE),
|
||||
(
|
||||
tokens::BUTTON_PRIMARY_TEXT_DISABLED.into(),
|
||||
palette::WHITE.with_alpha(0.5),
|
||||
),
|
||||
(tokens::SLIDER_BG.into(), palette::GRAY_1),
|
||||
(tokens::SLIDER_BAR.into(), palette::ACCENT),
|
||||
(tokens::SLIDER_BAR_DISABLED.into(), palette::GRAY_2),
|
||||
(tokens::SLIDER_TEXT.into(), palette::WHITE),
|
||||
(
|
||||
tokens::SLIDER_TEXT_DISABLED.into(),
|
||||
palette::WHITE.with_alpha(0.5),
|
||||
),
|
||||
]),
|
||||
}
|
||||
}
|
62
crates/bevy_feathers/src/font_styles.rs
Normal file
62
crates/bevy_feathers/src/font_styles.rs
Normal file
@ -0,0 +1,62 @@
|
||||
//! A framework for inheritable font styles.
|
||||
use bevy_app::Propagate;
|
||||
use bevy_asset::{AssetServer, Handle};
|
||||
use bevy_ecs::{
|
||||
component::Component,
|
||||
lifecycle::Insert,
|
||||
observer::On,
|
||||
system::{Commands, Query, Res},
|
||||
};
|
||||
use bevy_text::{Font, TextFont};
|
||||
|
||||
use crate::handle_or_path::HandleOrPath;
|
||||
|
||||
/// A component which, when inserted on an entity, will load the given font and propagate it
|
||||
/// downward to any child text entity that has the [`ThemedText`](crate::theme::ThemedText) marker.
|
||||
#[derive(Component, Default, Clone, Debug)]
|
||||
pub struct InheritableFont {
|
||||
/// The font handle or path.
|
||||
pub font: HandleOrPath<Font>,
|
||||
/// The desired font size.
|
||||
pub font_size: f32,
|
||||
}
|
||||
|
||||
impl InheritableFont {
|
||||
/// Create a new `InheritableFont` from a handle.
|
||||
pub fn from_handle(handle: Handle<Font>) -> Self {
|
||||
Self {
|
||||
font: HandleOrPath::Handle(handle),
|
||||
font_size: 16.0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new `InheritableFont` from a path.
|
||||
pub fn from_path(path: &str) -> Self {
|
||||
Self {
|
||||
font: HandleOrPath::Path(path.to_string()),
|
||||
font_size: 16.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An observer which looks for changes to the `InheritableFont` component on an entity, and
|
||||
/// propagates downward the font to all participating text entities.
|
||||
pub(crate) fn on_changed_font(
|
||||
ev: On<Insert, InheritableFont>,
|
||||
font_style: Query<&InheritableFont>,
|
||||
assets: Res<AssetServer>,
|
||||
mut commands: Commands,
|
||||
) {
|
||||
if let Ok(style) = font_style.get(ev.target()) {
|
||||
if let Some(font) = match style.font {
|
||||
HandleOrPath::Handle(ref h) => Some(h.clone()),
|
||||
HandleOrPath::Path(ref p) => Some(assets.load::<Font>(p)),
|
||||
} {
|
||||
commands.entity(ev.target()).insert(Propagate(TextFont {
|
||||
font,
|
||||
font_size: style.font_size,
|
||||
..Default::default()
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
61
crates/bevy_feathers/src/handle_or_path.rs
Normal file
61
crates/bevy_feathers/src/handle_or_path.rs
Normal file
@ -0,0 +1,61 @@
|
||||
//! Provides a way to specify assets either by handle or by path.
|
||||
use bevy_asset::{Asset, Handle};
|
||||
|
||||
/// Enum that represents a reference to an asset as either a [`Handle`] or a [`String`] path.
|
||||
///
|
||||
/// This is useful for when you want to specify an asset, but don't always have convenient
|
||||
/// access to an asset server reference.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum HandleOrPath<T: Asset> {
|
||||
/// Specify the asset reference as a handle.
|
||||
Handle(Handle<T>),
|
||||
/// Specify the asset reference as a [`String`].
|
||||
Path(String),
|
||||
}
|
||||
|
||||
impl<T: Asset> Default for HandleOrPath<T> {
|
||||
fn default() -> Self {
|
||||
Self::Path("".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
// Necessary because we don't want to require T: PartialEq
|
||||
impl<T: Asset> PartialEq for HandleOrPath<T> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
(HandleOrPath::Handle(h1), HandleOrPath::Handle(h2)) => h1 == h2,
|
||||
(HandleOrPath::Path(p1), HandleOrPath::Path(p2)) => p1 == p2,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Asset> From<Handle<T>> for HandleOrPath<T> {
|
||||
fn from(h: Handle<T>) -> Self {
|
||||
HandleOrPath::Handle(h)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Asset> From<&str> for HandleOrPath<T> {
|
||||
fn from(p: &str) -> Self {
|
||||
HandleOrPath::Path(p.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Asset> From<String> for HandleOrPath<T> {
|
||||
fn from(p: String) -> Self {
|
||||
HandleOrPath::Path(p.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Asset> From<&String> for HandleOrPath<T> {
|
||||
fn from(p: &String) -> Self {
|
||||
HandleOrPath::Path(p.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Asset + Clone> From<&HandleOrPath<T>> for HandleOrPath<T> {
|
||||
fn from(p: &HandleOrPath<T>) -> Self {
|
||||
p.to_owned()
|
||||
}
|
||||
}
|
73
crates/bevy_feathers/src/lib.rs
Normal file
73
crates/bevy_feathers/src/lib.rs
Normal file
@ -0,0 +1,73 @@
|
||||
//! `bevy_feathers` is a collection of styled and themed widgets for building editors and
|
||||
//! inspectors.
|
||||
//!
|
||||
//! The aesthetic choices made here are designed with a future Bevy Editor in mind,
|
||||
//! but this crate is deliberately exposed to the public to allow the broader ecosystem to easily create
|
||||
//! tooling for themselves and others that fits cohesively together.
|
||||
//!
|
||||
//! While it may be tempting to use this crate for your game's UI, it's deliberately not intended for that.
|
||||
//! We've opted for a clean, functional style, and prioritized consistency over customization.
|
||||
//! That said, if you like what you see, it can be a helpful learning tool.
|
||||
//! Consider copying this code into your own project,
|
||||
//! and refining the styles and abstractions provided to meet your needs.
|
||||
//!
|
||||
//! ## Warning: Experimental!
|
||||
//! All that said, this crate is still experimental and unfinished!
|
||||
//! It will change in breaking ways, and there will be both bugs and limitations.
|
||||
//!
|
||||
//! Please report issues, submit fixes and propose changes.
|
||||
//! Thanks for stress-testing; let's build something better together.
|
||||
|
||||
use bevy_app::{HierarchyPropagatePlugin, Plugin, PostUpdate};
|
||||
use bevy_asset::embedded_asset;
|
||||
use bevy_ecs::query::With;
|
||||
use bevy_text::{TextColor, TextFont};
|
||||
use bevy_winit::cursor::CursorIcon;
|
||||
|
||||
use crate::{
|
||||
controls::ControlsPlugin,
|
||||
cursor::{CursorIconPlugin, DefaultCursorIcon},
|
||||
theme::{ThemedText, UiTheme},
|
||||
};
|
||||
|
||||
pub mod constants;
|
||||
pub mod controls;
|
||||
pub mod cursor;
|
||||
pub mod dark_theme;
|
||||
pub mod font_styles;
|
||||
pub mod handle_or_path;
|
||||
pub mod palette;
|
||||
pub mod rounded_corners;
|
||||
pub mod theme;
|
||||
pub mod tokens;
|
||||
|
||||
/// Plugin which installs observers and systems for feathers themes, cursors, and all controls.
|
||||
pub struct FeathersPlugin;
|
||||
|
||||
impl Plugin for FeathersPlugin {
|
||||
fn build(&self, app: &mut bevy_app::App) {
|
||||
app.init_resource::<UiTheme>();
|
||||
|
||||
embedded_asset!(app, "assets/fonts/FiraSans-Bold.ttf");
|
||||
embedded_asset!(app, "assets/fonts/FiraSans-BoldItalic.ttf");
|
||||
embedded_asset!(app, "assets/fonts/FiraSans-Regular.ttf");
|
||||
embedded_asset!(app, "assets/fonts/FiraSans-Italic.ttf");
|
||||
embedded_asset!(app, "assets/fonts/FiraMono-Medium.ttf");
|
||||
|
||||
app.add_plugins((
|
||||
ControlsPlugin,
|
||||
CursorIconPlugin,
|
||||
HierarchyPropagatePlugin::<TextColor, With<ThemedText>>::default(),
|
||||
HierarchyPropagatePlugin::<TextFont, With<ThemedText>>::default(),
|
||||
));
|
||||
|
||||
app.insert_resource(DefaultCursorIcon(CursorIcon::System(
|
||||
bevy_window::SystemCursorIcon::Default,
|
||||
)));
|
||||
|
||||
app.add_systems(PostUpdate, theme::update_theme)
|
||||
.add_observer(theme::on_changed_background)
|
||||
.add_observer(theme::on_changed_font_color)
|
||||
.add_observer(font_styles::on_changed_font);
|
||||
}
|
||||
}
|
29
crates/bevy_feathers/src/palette.rs
Normal file
29
crates/bevy_feathers/src/palette.rs
Normal file
@ -0,0 +1,29 @@
|
||||
//! The Feathers standard color palette.
|
||||
use bevy_color::Color;
|
||||
|
||||
/// <div style="background-color: #000000; width: 10px; padding: 10px; border: 1px solid;"></div>
|
||||
pub const BLACK: Color = Color::oklcha(0.0, 0.0, 0.0, 1.0);
|
||||
/// <div style="background-color: #1F1F24; width: 10px; padding: 10px; border: 1px solid;"></div> - window background
|
||||
pub const GRAY_0: Color = Color::oklcha(0.2414, 0.0095, 285.67, 1.0);
|
||||
/// <div style="background-color: #2A2A2E; width: 10px; padding: 10px; border: 1px solid;"></div> - pane background
|
||||
pub const GRAY_1: Color = Color::oklcha(0.2866, 0.0072, 285.93, 1.0);
|
||||
/// <div style="background-color: #36373B; width: 10px; padding: 10px; border: 1px solid;"></div> - item background
|
||||
pub const GRAY_2: Color = Color::oklcha(0.3373, 0.0071, 274.77, 1.0);
|
||||
/// <div style="background-color: #46474D; width: 10px; padding: 10px; border: 1px solid;"></div> - item background (active)
|
||||
pub const GRAY_3: Color = Color::oklcha(0.3992, 0.0101, 278.38, 1.0);
|
||||
/// <div style="background-color: #414142; width: 10px; padding: 10px; border: 1px solid;"></div> - border
|
||||
pub const WARM_GRAY_1: Color = Color::oklcha(0.3757, 0.0017, 286.32, 1.0);
|
||||
/// <div style="background-color: #B1B1B2; width: 10px; padding: 10px; border: 1px solid;"></div> - bright label text
|
||||
pub const LIGHT_GRAY_1: Color = Color::oklcha(0.7607, 0.0014, 286.37, 1.0);
|
||||
/// <div style="background-color: #838385; width: 10px; padding: 10px; border: 1px solid;"></div> - dim label text
|
||||
pub const LIGHT_GRAY_2: Color = Color::oklcha(0.6106, 0.003, 286.31, 1.0);
|
||||
/// <div style="background-color: #FFFFFF; width: 10px; padding: 10px; border: 1px solid;"></div> - button label text
|
||||
pub const WHITE: Color = Color::oklcha(1.0, 0.000000059604645, 90.0, 1.0);
|
||||
/// <div style="background-color: #206EC9; width: 10px; padding: 10px; border: 1px solid;"></div> - call-to-action and selection color
|
||||
pub const ACCENT: Color = Color::oklcha(0.542, 0.1594, 255.4, 1.0);
|
||||
/// <div style="background-color: #AB4051; width: 10px; padding: 10px; border: 1px solid;"></div> - for X-axis inputs and drag handles
|
||||
pub const X_AXIS: Color = Color::oklcha(0.5232, 0.1404, 13.84, 1.0);
|
||||
/// <div style="background-color: #5D8D0A; width: 10px; padding: 10px; border: 1px solid;"></div> - for Y-axis inputs and drag handles
|
||||
pub const Y_AXIS: Color = Color::oklcha(0.5866, 0.1543, 129.84, 1.0);
|
||||
/// <div style="background-color: #2160A3; width: 10px; padding: 10px; border: 1px solid;"></div> - for Z-axis inputs and drag handles
|
||||
pub const Z_AXIS: Color = Color::oklcha(0.4847, 0.1249, 253.08, 1.0);
|
96
crates/bevy_feathers/src/rounded_corners.rs
Normal file
96
crates/bevy_feathers/src/rounded_corners.rs
Normal file
@ -0,0 +1,96 @@
|
||||
//! Mechanism for specifying which corners of a widget are rounded, used for segmented buttons
|
||||
//! and control groups.
|
||||
use bevy_ui::{BorderRadius, Val};
|
||||
|
||||
/// Allows specifying which corners are rounded and which are sharp. All rounded corners
|
||||
/// have the same radius. Not all combinations are supported, only the ones that make
|
||||
/// sense for a segmented buttons.
|
||||
///
|
||||
/// A typical use case would be a segmented button consisting of 3 individual buttons in a
|
||||
/// row. In that case, you would have the leftmost button have rounded corners on the left,
|
||||
/// the right-most button have rounded corners on the right, and the center button have
|
||||
/// only sharp corners.
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq)]
|
||||
pub enum RoundedCorners {
|
||||
/// No corners are rounded.
|
||||
None,
|
||||
#[default]
|
||||
/// All corners are rounded.
|
||||
All,
|
||||
/// Top-left corner is rounded.
|
||||
TopLeft,
|
||||
/// Top-right corner is rounded.
|
||||
TopRight,
|
||||
/// Bottom-right corner is rounded.
|
||||
BottomRight,
|
||||
/// Bottom-left corner is rounded.
|
||||
BottomLeft,
|
||||
/// Top corners are rounded.
|
||||
Top,
|
||||
/// Right corners are rounded.
|
||||
Right,
|
||||
/// Bottom corners are rounded.
|
||||
Bottom,
|
||||
/// Left corners are rounded.
|
||||
Left,
|
||||
}
|
||||
|
||||
impl RoundedCorners {
|
||||
/// Convert the `RoundedCorners` to a `BorderRadius` for use in a `Node`.
|
||||
pub fn to_border_radius(&self, radius: f32) -> BorderRadius {
|
||||
let radius = Val::Px(radius);
|
||||
let zero = Val::ZERO;
|
||||
match self {
|
||||
RoundedCorners::None => BorderRadius::all(zero),
|
||||
RoundedCorners::All => BorderRadius::all(radius),
|
||||
RoundedCorners::TopLeft => BorderRadius {
|
||||
top_left: radius,
|
||||
top_right: zero,
|
||||
bottom_right: zero,
|
||||
bottom_left: zero,
|
||||
},
|
||||
RoundedCorners::TopRight => BorderRadius {
|
||||
top_left: zero,
|
||||
top_right: radius,
|
||||
bottom_right: zero,
|
||||
bottom_left: zero,
|
||||
},
|
||||
RoundedCorners::BottomRight => BorderRadius {
|
||||
top_left: zero,
|
||||
top_right: zero,
|
||||
bottom_right: radius,
|
||||
bottom_left: zero,
|
||||
},
|
||||
RoundedCorners::BottomLeft => BorderRadius {
|
||||
top_left: zero,
|
||||
top_right: zero,
|
||||
bottom_right: zero,
|
||||
bottom_left: radius,
|
||||
},
|
||||
RoundedCorners::Top => BorderRadius {
|
||||
top_left: radius,
|
||||
top_right: radius,
|
||||
bottom_right: zero,
|
||||
bottom_left: zero,
|
||||
},
|
||||
RoundedCorners::Right => BorderRadius {
|
||||
top_left: zero,
|
||||
top_right: radius,
|
||||
bottom_right: radius,
|
||||
bottom_left: zero,
|
||||
},
|
||||
RoundedCorners::Bottom => BorderRadius {
|
||||
top_left: zero,
|
||||
top_right: zero,
|
||||
bottom_right: radius,
|
||||
bottom_left: radius,
|
||||
},
|
||||
RoundedCorners::Left => BorderRadius {
|
||||
top_left: radius,
|
||||
top_right: zero,
|
||||
bottom_right: zero,
|
||||
bottom_left: radius,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
114
crates/bevy_feathers/src/theme.rs
Normal file
114
crates/bevy_feathers/src/theme.rs
Normal file
@ -0,0 +1,114 @@
|
||||
//! A framework for theming.
|
||||
use bevy_app::Propagate;
|
||||
use bevy_color::{palettes, Color};
|
||||
use bevy_ecs::{
|
||||
change_detection::DetectChanges,
|
||||
component::Component,
|
||||
lifecycle::Insert,
|
||||
observer::On,
|
||||
query::Changed,
|
||||
resource::Resource,
|
||||
system::{Commands, Query, Res},
|
||||
};
|
||||
use bevy_log::warn_once;
|
||||
use bevy_platform::collections::HashMap;
|
||||
use bevy_text::TextColor;
|
||||
use bevy_ui::{BackgroundColor, BorderColor};
|
||||
|
||||
/// A collection of properties that make up a theme.
|
||||
#[derive(Default, Clone)]
|
||||
pub struct ThemeProps {
|
||||
/// Map of design tokens to colors.
|
||||
pub color: HashMap<String, Color>,
|
||||
// Other style property types to be added later.
|
||||
}
|
||||
|
||||
/// The currently selected user interface theme. Overwriting this resource changes the theme.
|
||||
#[derive(Resource, Default)]
|
||||
pub struct UiTheme(pub ThemeProps);
|
||||
|
||||
impl UiTheme {
|
||||
/// Lookup a color by design token. If the theme does not have an entry for that token,
|
||||
/// logs a warning and returns an error color.
|
||||
pub fn color<'a>(&self, token: &'a str) -> Color {
|
||||
let color = self.0.color.get(token);
|
||||
match color {
|
||||
Some(c) => *c,
|
||||
None => {
|
||||
warn_once!("Theme color {} not found.", token);
|
||||
// Return a bright obnoxious color to make the error obvious.
|
||||
palettes::basic::FUCHSIA.into()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Associate a design token with a given color.
|
||||
pub fn set_color(&mut self, token: impl Into<String>, color: Color) {
|
||||
self.0.color.insert(token.into(), color);
|
||||
}
|
||||
}
|
||||
|
||||
/// Component which causes the background color of an entity to be set based on a theme color.
|
||||
#[derive(Component, Clone, Copy)]
|
||||
#[require(BackgroundColor)]
|
||||
#[component(immutable)]
|
||||
pub struct ThemeBackgroundColor(pub &'static str);
|
||||
|
||||
/// Component which causes the border color of an entity to be set based on a theme color.
|
||||
/// Only supports setting all borders to the same color.
|
||||
#[derive(Component, Clone, Copy)]
|
||||
#[require(BorderColor)]
|
||||
#[component(immutable)]
|
||||
pub struct ThemeBorderColor(pub &'static str);
|
||||
|
||||
/// Component which causes the inherited text color of an entity to be set based on a theme color.
|
||||
#[derive(Component, Clone, Copy)]
|
||||
#[component(immutable)]
|
||||
pub struct ThemeFontColor(pub &'static str);
|
||||
|
||||
/// A marker component that is used to indicate that the text entity wants to opt-in to using
|
||||
/// inherited text styles.
|
||||
#[derive(Component)]
|
||||
pub struct ThemedText;
|
||||
|
||||
pub(crate) fn update_theme(
|
||||
mut q_background: Query<(&mut BackgroundColor, &ThemeBackgroundColor)>,
|
||||
theme: Res<UiTheme>,
|
||||
) {
|
||||
if theme.is_changed() {
|
||||
// Update all background colors
|
||||
for (mut bg, theme_bg) in q_background.iter_mut() {
|
||||
bg.0 = theme.color(theme_bg.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn on_changed_background(
|
||||
ev: On<Insert, ThemeBackgroundColor>,
|
||||
mut q_background: Query<
|
||||
(&mut BackgroundColor, &ThemeBackgroundColor),
|
||||
Changed<ThemeBackgroundColor>,
|
||||
>,
|
||||
theme: Res<UiTheme>,
|
||||
) {
|
||||
// Update background colors where the design token has changed.
|
||||
if let Ok((mut bg, theme_bg)) = q_background.get_mut(ev.target()) {
|
||||
bg.0 = theme.color(theme_bg.0);
|
||||
}
|
||||
}
|
||||
|
||||
/// An observer which looks for changes to the [`ThemeFontColor`] component on an entity, and
|
||||
/// propagates downward the text color to all participating text entities.
|
||||
pub(crate) fn on_changed_font_color(
|
||||
ev: On<Insert, ThemeFontColor>,
|
||||
font_color: Query<&ThemeFontColor>,
|
||||
theme: Res<UiTheme>,
|
||||
mut commands: Commands,
|
||||
) {
|
||||
if let Ok(token) = font_color.get(ev.target()) {
|
||||
let color = theme.color(token.0);
|
||||
commands
|
||||
.entity(ev.target())
|
||||
.insert(Propagate(TextColor(color)));
|
||||
}
|
||||
}
|
76
crates/bevy_feathers/src/tokens.rs
Normal file
76
crates/bevy_feathers/src/tokens.rs
Normal file
@ -0,0 +1,76 @@
|
||||
//! Design tokens used by Feathers themes.
|
||||
//!
|
||||
//! The term "design token" is commonly used in UX design to mean the smallest unit of a theme,
|
||||
//! similar in concept to a CSS variable. Each token represents an assignment of a color or
|
||||
//! value to a specific visual aspect of a widget, such as background or border.
|
||||
|
||||
/// Window background
|
||||
pub const WINDOW_BG: &str = "feathers.window.bg";
|
||||
|
||||
/// Focus ring
|
||||
pub const FOCUS_RING: &str = "feathers.focus";
|
||||
|
||||
/// Regular text
|
||||
pub const TEXT_MAIN: &str = "feathers.text.main";
|
||||
/// Dim text
|
||||
pub const TEXT_DIM: &str = "feathers.text.dim";
|
||||
|
||||
// Normal buttons
|
||||
|
||||
/// Regular button background
|
||||
pub const BUTTON_BG: &str = "feathers.button.bg";
|
||||
/// Regular button background (hovered)
|
||||
pub const BUTTON_BG_HOVER: &str = "feathers.button.bg.hover";
|
||||
/// Regular button background (disabled)
|
||||
pub const BUTTON_BG_DISABLED: &str = "feathers.button.bg.disabled";
|
||||
/// Regular button background (pressed)
|
||||
pub const BUTTON_BG_PRESSED: &str = "feathers.button.bg.pressed";
|
||||
/// Regular button text
|
||||
pub const BUTTON_TEXT: &str = "feathers.button.txt";
|
||||
/// Regular button text (disabled)
|
||||
pub const BUTTON_TEXT_DISABLED: &str = "feathers.button.txt.disabled";
|
||||
|
||||
// Primary ("default") buttons
|
||||
|
||||
/// Primary button background
|
||||
pub const BUTTON_PRIMARY_BG: &str = "feathers.button.primary.bg";
|
||||
/// Primary button background (hovered)
|
||||
pub const BUTTON_PRIMARY_BG_HOVER: &str = "feathers.button.primary.bg.hover";
|
||||
/// Primary button background (disabled)
|
||||
pub const BUTTON_PRIMARY_BG_DISABLED: &str = "feathers.button.primary.bg.disabled";
|
||||
/// Primary button background (pressed)
|
||||
pub const BUTTON_PRIMARY_BG_PRESSED: &str = "feathers.button.primary.bg.pressed";
|
||||
/// Primary button text
|
||||
pub const BUTTON_PRIMARY_TEXT: &str = "feathers.button.primary.txt";
|
||||
/// Primary button text (disabled)
|
||||
pub const BUTTON_PRIMARY_TEXT_DISABLED: &str = "feathers.button.primary.txt.disabled";
|
||||
|
||||
// Slider
|
||||
|
||||
/// Background for slider
|
||||
pub const SLIDER_BG: &str = "feathers.slider.bg";
|
||||
/// Background for slider moving bar
|
||||
pub const SLIDER_BAR: &str = "feathers.slider.bar";
|
||||
/// Background for slider moving bar (disabled)
|
||||
pub const SLIDER_BAR_DISABLED: &str = "feathers.slider.bar.disabled";
|
||||
/// Background for slider text
|
||||
pub const SLIDER_TEXT: &str = "feathers.slider.text";
|
||||
/// Background for slider text (disabled)
|
||||
pub const SLIDER_TEXT_DISABLED: &str = "feathers.slider.text.disabled";
|
||||
|
||||
// Checkbox
|
||||
|
||||
/// Checkbox border around the checkmark
|
||||
pub const CHECKBOX_BORDER: &str = "feathers.checkbox.border";
|
||||
/// Checkbox border around the checkmark (hovered)
|
||||
pub const CHECKBOX_BORDER_HOVER: &str = "feathers.checkbox.border.hover";
|
||||
/// Checkbox border around the checkmark (disabled)
|
||||
pub const CHECKBOX_BORDER_DISABLED: &str = "feathers.checkbox.border.disabled";
|
||||
/// Checkbox check mark
|
||||
pub const CHECKBOX_MARK: &str = "feathers.checkbox.mark";
|
||||
/// Checkbox check mark (disabled)
|
||||
pub const CHECKBOX_MARK_DISABLED: &str = "feathers.checkbox.mark.disabled";
|
||||
/// Checkbox label text
|
||||
pub const CHECKBOX_TEXT: &str = "feathers.checkbox.text";
|
||||
/// Checkbox label text (disabled)
|
||||
pub const CHECKBOX_TEXT_DISABLED: &str = "feathers.checkbox.text.disabled";
|
@ -416,6 +416,7 @@ bevy_dev_tools = { path = "../bevy_dev_tools", optional = true, version = "0.17.
|
||||
bevy_gilrs = { path = "../bevy_gilrs", optional = true, version = "0.17.0-dev" }
|
||||
bevy_gizmos = { path = "../bevy_gizmos", optional = true, version = "0.17.0-dev", default-features = false }
|
||||
bevy_gltf = { path = "../bevy_gltf", optional = true, version = "0.17.0-dev" }
|
||||
bevy_feathers = { path = "../bevy_feathers", optional = true, version = "0.17.0-dev" }
|
||||
bevy_image = { path = "../bevy_image", optional = true, version = "0.17.0-dev" }
|
||||
bevy_input_focus = { path = "../bevy_input_focus", optional = true, version = "0.17.0-dev", default-features = false, features = [
|
||||
"bevy_reflect",
|
||||
|
@ -35,6 +35,8 @@ pub use bevy_core_widgets as core_widgets;
|
||||
pub use bevy_dev_tools as dev_tools;
|
||||
pub use bevy_diagnostic as diagnostic;
|
||||
pub use bevy_ecs as ecs;
|
||||
#[cfg(feature = "bevy_feathers")]
|
||||
pub use bevy_feathers as feathers;
|
||||
#[cfg(feature = "bevy_gilrs")]
|
||||
pub use bevy_gilrs as gilrs;
|
||||
#[cfg(feature = "bevy_gizmos")]
|
||||
|
@ -276,7 +276,7 @@ impl From<Justify> for cosmic_text::Align {
|
||||
|
||||
/// `TextFont` determines the style of a text span within a [`ComputedTextBlock`], specifically
|
||||
/// the font face, the font size, and the color.
|
||||
#[derive(Component, Clone, Debug, Reflect)]
|
||||
#[derive(Component, Clone, Debug, Reflect, PartialEq)]
|
||||
#[reflect(Component, Default, Debug, Clone)]
|
||||
pub struct TextFont {
|
||||
/// The specific font face to use, as a `Handle` to a [`Font`] asset.
|
||||
|
@ -81,6 +81,7 @@ The default feature set enables most of the expected features of a game engine,
|
||||
|detailed_trace|Enable detailed trace event logging. These trace events are expensive even when off, thus they require compile time opt-in|
|
||||
|dynamic_linking|Force dynamic linking, which improves iterative compile times|
|
||||
|embedded_watcher|Enables watching in memory asset providers for Bevy Asset hot-reloading|
|
||||
|experimental_bevy_feathers|Feathers widget collection.|
|
||||
|experimental_pbr_pcss|Enable support for PCSS, at the risk of blowing past the global, per-shader sampler limit on older/lower-end GPUs|
|
||||
|exr|EXR image format support|
|
||||
|ff|Farbfeld image format support|
|
||||
|
180
examples/ui/feathers.rs
Normal file
180
examples/ui/feathers.rs
Normal file
@ -0,0 +1,180 @@
|
||||
//! This example shows off the various Bevy Feathers widgets.
|
||||
|
||||
use bevy::{
|
||||
core_widgets::{CoreWidgetsPlugin, SliderStep},
|
||||
feathers::{
|
||||
controls::{button, slider, ButtonProps, ButtonVariant, SliderProps},
|
||||
dark_theme::create_dark_theme,
|
||||
rounded_corners::RoundedCorners,
|
||||
theme::{ThemeBackgroundColor, ThemedText, UiTheme},
|
||||
tokens, FeathersPlugin,
|
||||
},
|
||||
input_focus::{
|
||||
tab_navigation::{TabGroup, TabNavigationPlugin},
|
||||
InputDispatchPlugin,
|
||||
},
|
||||
prelude::*,
|
||||
ui::InteractionDisabled,
|
||||
winit::WinitSettings,
|
||||
};
|
||||
|
||||
fn main() {
|
||||
App::new()
|
||||
.add_plugins((
|
||||
DefaultPlugins,
|
||||
CoreWidgetsPlugin,
|
||||
InputDispatchPlugin,
|
||||
TabNavigationPlugin,
|
||||
FeathersPlugin,
|
||||
))
|
||||
.insert_resource(UiTheme(create_dark_theme()))
|
||||
// Only run the app when there is user input. This will significantly reduce CPU/GPU use.
|
||||
.insert_resource(WinitSettings::desktop_app())
|
||||
.add_systems(Startup, setup)
|
||||
.run();
|
||||
}
|
||||
|
||||
fn setup(mut commands: Commands) {
|
||||
// ui camera
|
||||
commands.spawn(Camera2d);
|
||||
let root = demo_root(&mut commands);
|
||||
commands.spawn(root);
|
||||
}
|
||||
|
||||
fn demo_root(commands: &mut Commands) -> impl Bundle {
|
||||
(
|
||||
Node {
|
||||
width: Val::Percent(100.0),
|
||||
height: Val::Percent(100.0),
|
||||
align_items: AlignItems::Start,
|
||||
justify_content: JustifyContent::Start,
|
||||
display: Display::Flex,
|
||||
flex_direction: FlexDirection::Column,
|
||||
row_gap: Val::Px(10.0),
|
||||
..default()
|
||||
},
|
||||
TabGroup::default(),
|
||||
ThemeBackgroundColor(tokens::WINDOW_BG),
|
||||
children![(
|
||||
Node {
|
||||
display: Display::Flex,
|
||||
flex_direction: FlexDirection::Column,
|
||||
align_items: AlignItems::Stretch,
|
||||
justify_content: JustifyContent::Start,
|
||||
padding: UiRect::all(Val::Px(8.0)),
|
||||
row_gap: Val::Px(8.0),
|
||||
width: Val::Percent(30.),
|
||||
min_width: Val::Px(200.),
|
||||
..default()
|
||||
},
|
||||
children![
|
||||
(
|
||||
Node {
|
||||
display: Display::Flex,
|
||||
flex_direction: FlexDirection::Row,
|
||||
align_items: AlignItems::Center,
|
||||
justify_content: JustifyContent::Start,
|
||||
column_gap: Val::Px(8.0),
|
||||
..default()
|
||||
},
|
||||
children![
|
||||
button(
|
||||
ButtonProps {
|
||||
on_click: Some(commands.register_system(|| {
|
||||
info!("Normal button clicked!");
|
||||
})),
|
||||
..default()
|
||||
},
|
||||
(),
|
||||
Spawn((Text::new("Normal"), ThemedText))
|
||||
),
|
||||
button(
|
||||
ButtonProps {
|
||||
on_click: Some(commands.register_system(|| {
|
||||
info!("Disabled button clicked!");
|
||||
})),
|
||||
..default()
|
||||
},
|
||||
InteractionDisabled,
|
||||
Spawn((Text::new("Disabled"), ThemedText))
|
||||
),
|
||||
button(
|
||||
ButtonProps {
|
||||
on_click: Some(commands.register_system(|| {
|
||||
info!("Primary button clicked!");
|
||||
})),
|
||||
variant: ButtonVariant::Primary,
|
||||
..default()
|
||||
},
|
||||
(),
|
||||
Spawn((Text::new("Primary"), ThemedText))
|
||||
),
|
||||
]
|
||||
),
|
||||
(
|
||||
Node {
|
||||
display: Display::Flex,
|
||||
flex_direction: FlexDirection::Row,
|
||||
align_items: AlignItems::Center,
|
||||
justify_content: JustifyContent::Start,
|
||||
column_gap: Val::Px(1.0),
|
||||
..default()
|
||||
},
|
||||
children![
|
||||
button(
|
||||
ButtonProps {
|
||||
on_click: Some(commands.register_system(|| {
|
||||
info!("Left button clicked!");
|
||||
})),
|
||||
corners: RoundedCorners::Left,
|
||||
..default()
|
||||
},
|
||||
(),
|
||||
Spawn((Text::new("Left"), ThemedText))
|
||||
),
|
||||
button(
|
||||
ButtonProps {
|
||||
on_click: Some(commands.register_system(|| {
|
||||
info!("Center button clicked!");
|
||||
})),
|
||||
corners: RoundedCorners::None,
|
||||
..default()
|
||||
},
|
||||
(),
|
||||
Spawn((Text::new("Center"), ThemedText))
|
||||
),
|
||||
button(
|
||||
ButtonProps {
|
||||
on_click: Some(commands.register_system(|| {
|
||||
info!("Right button clicked!");
|
||||
})),
|
||||
variant: ButtonVariant::Primary,
|
||||
corners: RoundedCorners::Right,
|
||||
},
|
||||
(),
|
||||
Spawn((Text::new("Right"), ThemedText))
|
||||
),
|
||||
]
|
||||
),
|
||||
button(
|
||||
ButtonProps {
|
||||
on_click: Some(commands.register_system(|| {
|
||||
info!("Wide button clicked!");
|
||||
})),
|
||||
..default()
|
||||
},
|
||||
(),
|
||||
Spawn((Text::new("Button"), ThemedText))
|
||||
),
|
||||
slider(
|
||||
SliderProps {
|
||||
max: 100.0,
|
||||
value: 20.0,
|
||||
..default()
|
||||
},
|
||||
SliderStep(10.)
|
||||
),
|
||||
]
|
||||
),],
|
||||
)
|
||||
}
|
26
release-content/release-notes/feathers.md
Normal file
26
release-content/release-notes/feathers.md
Normal file
@ -0,0 +1,26 @@
|
||||
---
|
||||
title: Bevy Feathers
|
||||
authors: ["@viridia", "@Atlas16A"]
|
||||
pull_requests: [19730]
|
||||
---
|
||||
|
||||
To make it easier for Bevy engine developers and third-party tool creators to make comfortable, visually cohesive tooling,
|
||||
we're pleased to introduce "Feathers" - a comprehensive widget set that offers:
|
||||
|
||||
- Standard widgets designed to match the look and feel of the planned Bevy Editor
|
||||
- Components that can be leveraged to build custom editors, inspectors, and utility interfaces
|
||||
- Essential UI elements including buttons, sliders, checkboxes, menu buttons, and more
|
||||
- Layout containers for organizing and structuring UI elements
|
||||
- Decorative elements such as icons for visual enhancement
|
||||
- Robust theming support ensuring consistent visual styling across applications
|
||||
- Accessibility features with built-in screen reader and assistive technology support
|
||||
- Interactive cursor behavior that changes appropriately when hovering over widgets
|
||||
|
||||
Feathers isn't meant as a toolkit for building exciting and cool game UIs: it has a somewhat plain
|
||||
and utilitarian look and feel suitable for editors and graphical utilities. That being said, using
|
||||
the themeing framework, you can spice up the colors quite a bit.
|
||||
It can also serve as a helpful base to understand how to extend and style `bevy_ui` and our new core widgets;
|
||||
copy the code into your project and start hacking!
|
||||
|
||||
Feathers is still in development, and is currently hidden behind an experimental feature flag,
|
||||
`experimental_bevy_feathers`.
|
Loading…
Reference in New Issue
Block a user