Skip to content

Bevy Feathers: an opinionated widget toolkit for building Bevy tooling #19730

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 22 commits into from
Jun 28, 2025

Conversation

viridia
Copy link
Contributor

@viridia viridia commented Jun 19, 2025

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.

Copy link
Contributor

The generated examples/README.md is out of sync with the example metadata in Cargo.toml or the example readme template. Please run cargo run -p build-templated-pages -- update examples to update it, and commit the file change.

@alice-i-cecile alice-i-cecile added A-UI Graphical user interfaces, styles, layouts, and widgets M-Needs-Release-Note Work that should be called out in the blog due to impact X-Blessed Has a large architectural impact or tradeoffs, but the design has been endorsed by decision makers S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Jun 19, 2025
/// 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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up: we should probably move this out into bevy_window or something.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a number of things in here that I expect will eventually "graduate". This includes the theming framework.

@alice-i-cecile alice-i-cecile changed the title Feathers Bevy Feathers: an opinionated widget toolkit for building Bevy tooling Jun 19, 2025
/// Rounded corners options
pub corners: RoundedCorners,
/// Click handler
pub on_click: Option<SystemId>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the use case of a button without any click handler? It seems to me like using an Option here is adding boilerplate to the most common case. It doesn't really matter to be clear, I'm just curious of the reason why.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, the main reason is that during prototyping you may want to create your layout and don't have your handlers defined yet. I admit that this is a somewhat trivial use case.

Eventually I suspect that Option<SystemId> will get replaced with an enum allowing a system id, a cached system, "Ignore", or other options.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, the SystemId based design is the thing I'm most nervous about in this whole design. I'd like to revisit it, but not in this PR.

@alice-i-cecile alice-i-cecile requested a review from MalekiRe June 19, 2025 19:20
Comment on lines +7 to +14
#[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),
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i wonder if this should be in bevy_asset

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do something similar with Shaders. It would be nice for this pattern to be more generalized, but it's a bit out of scope here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i had a suspicion that there was somewhere that already had this, i had in my head that it was in bevy_gltf but the type that appears there has more information

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, spin this out in follow-up.

Comment on lines 1 to 29
//! The Feathers standard color palette.
use bevy_color::Color;

/// Black
pub const BLACK: Color = Color::oklcha(0.0, 0.0, 0.0, 1.0);
/// Gray 0 - window background
pub const GRAY_0: Color = Color::oklcha(0.2414, 0.0095, 285.67, 1.0);
/// Gray 1 - pane background
pub const GRAY_1: Color = Color::oklcha(0.2866, 0.0072, 285.93, 1.0);
/// Gray 2 - item background
pub const GRAY_2: Color = Color::oklcha(0.3373, 0.0071, 274.77, 1.0);
/// Gray 3 - item background (active)
pub const GRAY_3: Color = Color::oklcha(0.3992, 0.0101, 278.38, 1.0);
/// Warm Gray 3 - border
pub const WARM_GRAY_1: Color = Color::oklcha(0.3757, 0.0017, 286.32, 1.0);
/// Light Gray 1 - bright label text
pub const LIGHT_GRAY_1: Color = Color::oklcha(0.7607, 0.0014, 286.37, 1.0);
/// Light Gray 2 - dim label text
pub const LIGHT_GRAY_2: Color = Color::oklcha(0.6106, 0.003, 286.31, 1.0);
/// White - button label text
pub const WHITE: Color = Color::oklcha(1.0, 0.000000059604645, 90.0, 1.0);
/// Accent - call-to-action and selection color
pub const ACCENT: Color = Color::oklcha(0.542, 0.1594, 255.4, 1.0);
/// Dark Coral - for X-axis inputs and drag handles
pub const X_AXIS: Color = Color::oklcha(0.5232, 0.1404, 13.84, 1.0);
/// Olive - for Y-axis inputs and drag handles
pub const Y_AXIS: Color = Color::oklcha(0.5866, 0.1543, 129.84, 1.0);
/// Steel Blue - for Z-axis inputs and drag handles
pub const Z_AXIS: Color = Color::oklcha(0.4847, 0.1249, 253.08, 1.0);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These colors are, most of them, related to the colors used by the dark theme, right? other themes would not use most of these, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That decision is above my pay grade...or maybe below it. I don't know the answer :)

Right now I am making as few assumptions as I can.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're going to focus on a dark theme only for now.

Comment on lines +18 to +24
#[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.
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this should be an asset, and have an asset loader, and have the dark theme already as an asset so that people creating custom themes already have an starting point

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's designed so that it could potentially be an asset - just slap some serde derives on there and you are good to go. But I don't want to do that now. And particularly, for small things like the world inspector, any kind of filesystem dependencies should be avoided. So being able to define themes in code is also a feature.

Comment on lines 74 to 145
pub mod tokens {
/// Window background
pub const WINDOW_BG: &str = "window.bg";

/// Focus ring
pub const FOCUS_RING: &str = "focus";

/// Regular text
pub const TEXT_MAIN: &str = "text.main";
/// Dim text
pub const TEXT_DIM: &str = "text.dim";

// Normal buttons

/// Regular button background
pub const BUTTON_BG: &str = "button.bg";
/// Regular button background (hovered)
pub const BUTTON_BG_HOVER: &str = "button.bg.hover";
/// Regular button background (disabled)
pub const BUTTON_BG_DISABLED: &str = "button.bg.disabled";
/// Regular button background (pressed)
pub const BUTTON_BG_PRESSED: &str = "button.bg.pressed";
/// Regular button text
pub const BUTTON_TEXT: &str = "button.txt";
/// Regular button text (disabled)
pub const BUTTON_TEXT_DISABLED: &str = "button.txt.disabled";

// Primary ("default") buttons

/// Primary button background
pub const BUTTON_PRIMARY_BG: &str = "button.primary.bg";
/// Primary button background (hovered)
pub const BUTTON_PRIMARY_BG_HOVER: &str = "button.primary.bg.hover";
/// Primary button background (disabled)
pub const BUTTON_PRIMARY_BG_DISABLED: &str = "button.primary.bg.disabled";
/// Primary button background (pressed)
pub const BUTTON_PRIMARY_BG_PRESSED: &str = "button.primary.bg.pressed";
/// Primary button text
pub const BUTTON_PRIMARY_TEXT: &str = "button.primary.txt";
/// Primary button text (disabled)
pub const BUTTON_PRIMARY_TEXT_DISABLED: &str = "button.primary.txt.disabled";

// Slider

/// Background for slider
pub const SLIDER_BG: &str = "slider.bg";
/// Background for slider moving bar
pub const SLIDER_BAR: &str = "slider.bar";
/// Background for slider moving bar (disabled)
pub const SLIDER_BAR_DISABLED: &str = "slider.bar.disabled";
/// Background for slider text
pub const SLIDER_TEXT: &str = "slider.text";
/// Background for slider text (disabled)
pub const SLIDER_TEXT_DISABLED: &str = "slider.text.disabled";

// Checkbox

/// Checkbox border around the checkmark
pub const CHECKBOX_BORDER: &str = "checkbox.border";
/// Checkbox border around the checkmark (hovered)
pub const CHECKBOX_BORDER_HOVER: &str = "checkbox.border.hover";
/// Checkbox border around the checkmark (disabled)
pub const CHECKBOX_BORDER_DISABLED: &str = "checkbox.border.disabled";
/// Checkbox check mark
pub const CHECKBOX_MARK: &str = "checkbox.mark";
/// Checkbox check mark (disabled)
pub const CHECKBOX_MARK_DISABLED: &str = "checkbox.mark.disabled";
/// Checkbox label text
pub const CHECKBOX_TEXT: &str = "checkbox.text";
/// Checkbox label text (disabled)
pub const CHECKBOX_TEXT_DISABLED: &str = "checkbox.text.disabled";
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are these generic enough that any app would use them or are these more related to the editor?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I am going for here is any kind of app that is "editor-like" - that includes the Bevy editor, the world inspector, a substance builder, tree-growth editor and so on.

Eventually the parts that aren't editor specific can be extracted and put into their own crates. But I'd like things to mature before that.

children![
button(
ButtonProps {
on_click: Some(commands.register_system(|| {
Copy link
Contributor

@IceSentry IceSentry Jun 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't kept up with all the various ecs and bsn work. So out of curisoity, is there a future where we'll be able to do something like this:

ButtonProps {
	on_click: || { 
		info!("clicked!");
	},
	..default()
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I would like to be able to do that. I need to take a proper crack at the code here though.

}

/// An observer which looks for changes to the `InheritableFont` component on an entity, and
/// propagates downward the text color to all participating text entities.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This propagates the font, not the text color?

Comment on lines +104 to +101
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.)),
Copy link
Contributor

@ickshonpe ickshonpe Jun 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The zero and one hundred percent stops can be elided here. If there is space before the first stop or after the last stop it fills with the respective stop's color.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like that this is more explicit.

Copy link
Contributor

@ickshonpe ickshonpe Jun 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess so, I mean I'm just fussing as usual. But it's also a little more complicated to update the colors as there are four values to change instead of two.

Copy link
Contributor

@IceSentry IceSentry left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know how I feel about the embedded fonts, but since it's behind a feature it doesn't matter that much.

Other than that LGTM

@alice-i-cecile alice-i-cecile enabled auto-merge June 28, 2025 19:42
@alice-i-cecile
Copy link
Member

I'm happy with this as a base. There's lots to explore and follow-up on, but let's get this merged and let people start experimenting :)

@alice-i-cecile alice-i-cecile added this pull request to the merge queue Jun 28, 2025
Merged via the queue into bevyengine:main with commit 65bddbd Jun 28, 2025
36 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-UI Graphical user interfaces, styles, layouts, and widgets M-Needs-Release-Note Work that should be called out in the blog due to impact S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged X-Blessed Has a large architectural impact or tradeoffs, but the design has been endorsed by decision makers
Projects
Status: Respond (With Priority)
Development

Successfully merging this pull request may close these issues.

6 participants