Decorative widgets (#23804)

# Objective

Part of #19236 

## Solution

A bunch of new, decorative widgets - see release note.

## Testing

Manual testing

## Showcase

<img width="384" height="207" alt="panes"
src="https://github.com/user-attachments/assets/0b0bb2ee-d520-4280-938e-e08d5c4e49d1"
/>

---------

Co-authored-by: Kevin Chen <chen.kevin.f@gmail.com>
This commit is contained in:
Talin
2026-04-15 08:42:55 -07:00
committed by GitHub
parent c0e8e2998f
commit 574e9356cb
22 changed files with 986 additions and 422 deletions
@@ -0,0 +1,15 @@
---
title: "Feathers widgets moving to BSN"
pull_requests: [23804]
---
Going forward, BSN will be the primary means to create Feathers widgets. The old spawning
functions have been renamed (`button` is now `button_bundle`), and will be removed in a future
release.
Some of the BSN widgets are slightly different than before:
- `button` no longer automatically includes `flex_grow`. This was originally added due to the
difficulty of overriding node styles when spawning, but in BSN that's no longer a problem.
- `button`, `checkbox` and `radio` now accept a `caption` parameter which lets you specify
the label directly instead of appending them via `Children`.
+10 -3
View File
@@ -1,9 +1,16 @@
---
title: "Moar widgets!"
authors: ["@viridia"]
pull_requests: [23645, 23707]
pull_requests: [23645, 23707, 23788, 23787, 23804]
---
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
Bevy Feathers, the opinionated UI widget collection, has added several new widgets:
- text input
- dropdown menu buttons
- icon (displays an image)
- label (displays a text string in the standard font)
- pane, subpane, and group (decorative frames which can be used in editors)
Note that unlike the older widgets, these are _only_ available through
BSN (which will be the primary access to feathers going forward).
+10
View File
@@ -26,6 +26,7 @@ pub mod icons {
/// Size constants
pub mod size {
use bevy_text::FontSize;
use bevy_ui::Val;
/// Common row size for buttons, sliders, spinners, etc.
@@ -45,4 +46,13 @@ pub mod size {
/// Height of a toggle switch
pub const TOGGLE_HEIGHT: Val = Val::Px(18.0);
/// Regular font size, used for most widget captions
pub const MEDIUM_FONT: FontSize = FontSize::Px(14.0);
/// Slightly smaller font size, used for text inputs
pub const COMPACT_FONT: FontSize = FontSize::Px(13.0);
/// Small font size
pub const SMALL_FONT: FontSize = FontSize::Px(12.0);
}
@@ -0,0 +1,12 @@
use bevy_scene::{bsn, Scene};
use bevy_ui::Node;
/// An invisible UI node that takes up space, and which has a positive `flex_grow` setting.
/// This is normally used within containers to provide a gap.
pub fn flex_spacer() -> impl Scene {
bsn! {
Node {
flex_grow: 1.0,
}
}
}
@@ -0,0 +1,78 @@
use bevy_scene::{bsn, Scene};
use bevy_text::FontWeight;
use bevy_ui::{px, AlignItems, Display, FlexDirection, JustifyContent, Node, UiRect, Val};
use crate::{
constants::{fonts, size},
font_styles::InheritableFont,
rounded_corners::RoundedCorners,
theme::{ThemeBackgroundColor, ThemeBorderColor, ThemeFontColor},
tokens,
};
/// Group
pub fn group() -> impl Scene {
bsn! {
Node {
display: Display::Flex,
flex_direction: FlexDirection::Column,
align_items: AlignItems::Stretch,
}
}
}
/// Group header
pub fn group_header() -> impl Scene {
bsn! {
Node {
display: Display::Flex,
flex_direction: FlexDirection::Row,
align_items: AlignItems::Center,
justify_content: JustifyContent::SpaceBetween,
border: UiRect {
left: Val::Px(1.0),
top: Val::Px(1.0),
right: Val::Px(1.0),
bottom: Val::Px(0.0),
},
padding: UiRect::axes(Val::Px(10.0), Val::Px(0.0)),
min_height: size::HEADER_HEIGHT,
column_gap: Val::Px(4.0),
border_radius: {RoundedCorners::Top.to_border_radius(4.0)}
}
ThemeBackgroundColor(tokens::GROUP_HEADER_BG)
ThemeBorderColor(tokens::GROUP_HEADER_BORDER)
ThemeFontColor(tokens::GROUP_HEADER_TEXT)
InheritableFont {
font: fonts::REGULAR,
font_size: size::MEDIUM_FONT,
weight: FontWeight::NORMAL,
}
}
}
/// Group body
pub fn group_body() -> impl Scene {
bsn! {
Node {
display: Display::Flex,
flex_direction: FlexDirection::Column,
border: UiRect {
left: Val::Px(1.0),
top: Val::Px(0.0),
right: Val::Px(1.0),
bottom: Val::Px(1.0),
},
row_gap: px(4.0),
padding: UiRect::axes(Val::Px(6.0), Val::Px(6.0)),
border_radius: {RoundedCorners::Bottom.to_border_radius(4.0)}
}
ThemeBackgroundColor(tokens::GROUP_BODY_BG)
ThemeBorderColor(tokens::GROUP_BODY_BORDER)
InheritableFont {
font: fonts::REGULAR,
font_size: size::MEDIUM_FONT,
weight: FontWeight::NORMAL,
}
}
}
@@ -0,0 +1,10 @@
//! Meta-module containing all feathers containers: passive widgets that hold other widgets.
mod flex_spacer;
mod group;
mod pane;
mod subpane;
pub use flex_spacer::*;
pub use group::*;
pub use pane::*;
pub use subpane::*;
@@ -0,0 +1,92 @@
use bevy_ecs::hierarchy::Children;
use bevy_scene::{bsn, Scene};
use bevy_text::FontWeight;
use bevy_ui::{
px, AlignItems, AlignSelf, Display, FlexDirection, JustifyContent, Node, PositionType, UiRect,
Val,
};
use crate::{
constants::{fonts, size},
font_styles::InheritableFont,
rounded_corners::RoundedCorners,
theme::{ThemeBackgroundColor, ThemeBorderColor, ThemeFontColor},
tokens,
};
/// A standard pane
pub fn pane() -> impl Scene {
bsn! {
Node {
display: Display::Flex,
flex_direction: FlexDirection::Column,
align_items: AlignItems::Stretch,
}
}
}
/// Pane header
pub fn pane_header() -> impl Scene {
bsn! {
Node {
display: Display::Flex,
flex_direction: FlexDirection::Row,
align_items: AlignItems::Center,
justify_content: JustifyContent::SpaceBetween,
padding: UiRect::axes(Val::Px(6.0), Val::Px(6.0)),
border: UiRect {
left: Val::Px(1.0),
top: Val::Px(1.0),
right: Val::Px(1.0),
bottom: Val::Px(0.0),
},
min_height: size::HEADER_HEIGHT,
column_gap: Val::Px(6.0),
border_radius: {RoundedCorners::Top.to_border_radius(4.0)},
}
ThemeBackgroundColor(tokens::PANE_HEADER_BG)
ThemeBorderColor(tokens::PANE_HEADER_BORDER)
ThemeFontColor(tokens::PANE_HEADER_TEXT)
InheritableFont {
font: fonts::REGULAR,
font_size: size::MEDIUM_FONT,
weight: FontWeight::NORMAL,
}
}
}
/// Vertical divider between groups of widgets in pane headers
pub fn pane_header_divider() -> impl Scene {
bsn! {
Node {
width: Val::Px(1.0),
align_self: AlignSelf::Stretch,
}
Children [(
// Because we want to extend the divider into the header padding area, we'll use
// an absolutely-positioned child.
Node {
position_type: PositionType::Absolute,
left: px(0),
right: px(0),
top: {px(-6)},
bottom: {px(-6)},
}
ThemeBackgroundColor(tokens::PANE_HEADER_DIVIDER)
)]
}
}
/// Pane body
pub fn pane_body() -> impl Scene {
bsn! {
Node {
display: Display::Flex,
flex_direction: FlexDirection::Column,
row_gap: px(4.0),
padding: UiRect::axes(Val::Px(6.0), Val::Px(6.0)),
border_radius: {RoundedCorners::Bottom.to_border_radius(4.0)}
}
ThemeBackgroundColor(tokens::PANE_BODY_BG)
}
}
@@ -0,0 +1,78 @@
use bevy_scene::{bsn, Scene};
use bevy_text::FontWeight;
use bevy_ui::{px, AlignItems, Display, FlexDirection, JustifyContent, Node, UiRect, Val};
use crate::{
constants::{fonts, size},
font_styles::InheritableFont,
rounded_corners::RoundedCorners,
theme::{ThemeBackgroundColor, ThemeBorderColor, ThemeFontColor},
tokens,
};
/// Sub-pane
pub fn subpane() -> impl Scene {
bsn! {
Node {
display: Display::Flex,
flex_direction: FlexDirection::Column,
align_items: AlignItems::Stretch,
}
}
}
/// Sub-pane header
pub fn subpane_header() -> impl Scene {
bsn! {
Node {
display: Display::Flex,
flex_direction: FlexDirection::Row,
align_items: AlignItems::Center,
justify_content: JustifyContent::SpaceBetween,
border: UiRect {
left: Val::Px(1.0),
top: Val::Px(1.0),
right: Val::Px(1.0),
bottom: Val::Px(0.0),
},
padding: UiRect::axes(Val::Px(10.0), Val::Px(0.0)),
min_height: size::HEADER_HEIGHT,
column_gap: Val::Px(4.0),
border_radius: {RoundedCorners::Top.to_border_radius(4.0)}
}
ThemeBackgroundColor(tokens::SUBPANE_HEADER_BG)
ThemeBorderColor(tokens::SUBPANE_HEADER_BORDER)
ThemeFontColor(tokens::SUBPANE_HEADER_TEXT)
InheritableFont {
font: fonts::REGULAR,
font_size: size::MEDIUM_FONT,
weight: FontWeight::NORMAL,
}
}
}
/// Sub-pane body
pub fn subpane_body() -> impl Scene {
bsn! {
Node {
display: Display::Flex,
flex_direction: FlexDirection::Column,
border: UiRect {
left: Val::Px(1.0),
top: Val::Px(0.0),
right: Val::Px(1.0),
bottom: Val::Px(1.0),
},
row_gap: px(4.0),
padding: UiRect::axes(Val::Px(6.0), Val::Px(6.0)),
border_radius: {RoundedCorners::Bottom.to_border_radius(4.0)}
}
ThemeBackgroundColor(tokens::SUBPANE_BODY_BG)
ThemeBorderColor(tokens::SUBPANE_BODY_BORDER)
InheritableFont {
font: fonts::REGULAR,
font_size: size::MEDIUM_FONT,
weight: FontWeight::NORMAL,
}
}
}
+32 -6
View File
@@ -15,7 +15,7 @@ use bevy_input_focus::tab_navigation::TabIndex;
use bevy_picking::{hover::Hovered, PickingSystems};
use bevy_reflect::{prelude::ReflectDefault, Reflect};
use bevy_scene::{prelude::*, template_value};
use bevy_text::{FontSize, FontWeight};
use bevy_text::FontWeight;
use bevy_ui::{AlignItems, InteractionDisabled, JustifyContent, Node, Pressed, UiRect, Val};
use bevy_ui_widgets::Button;
@@ -40,6 +40,8 @@ pub enum ButtonVariant {
/// A button with a more prominent color, this is used for "call to action" buttons,
/// default buttons for dialog boxes, and so on.
Primary,
/// Don't display the button background unless hovering or pressed.
Plain,
}
/// Parameters for the button template, passed to [`button`] function.
@@ -81,7 +83,6 @@ pub fn button(props: ButtonProps) -> impl Scene {
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
padding: UiRect::axes(Val::Px(8.0), Val::Px(0.)),
flex_grow: 1.0,
border_radius: {props.corners.to_border_radius(4.0)},
}
Button
@@ -94,7 +95,7 @@ pub fn button(props: ButtonProps) -> impl Scene {
ThemeFontColor(tokens::BUTTON_TEXT)
InheritableFont {
font: fonts::REGULAR,
font_size: FontSize::Px(14.0),
font_size: size::MEDIUM_FONT,
weight: FontWeight::NORMAL,
}
Children [
@@ -103,6 +104,27 @@ pub fn button(props: ButtonProps) -> impl Scene {
}
}
/// Tool button scene function: a smaller button for embedding in panel headers.
///
/// # Arguments
/// * `props` - construction properties for the button.
///
/// # Emitted events
/// * [`bevy_ui_widgets::Activate`] when any of the following happens:
/// * the pointer is released while hovering over the button.
/// * the ENTER or SPACE key is pressed while the button has keyboard focus.
///
/// These events can be disabled by adding an [`bevy_ui::InteractionDisabled`] component to the entity
pub fn tool_button(props: ButtonProps) -> impl Scene {
bsn! {
:button(props)
Node {
padding: UiRect::axes(Val::Px(4.0), Val::Px(0.)),
min_width: size::ROW_HEIGHT,
}
}
}
/// Parameters for the [`button_bundle`] template.
#[derive(Default)]
pub struct ButtonBundleProps {
@@ -148,7 +170,7 @@ pub fn button_bundle<C: SpawnableList<ChildOf> + Send + Sync + 'static, B: Bundl
ThemeBackgroundColor(tokens::BUTTON_BG),
ThemeFontColor(tokens::BUTTON_TEXT),
InheritableFont {
font_size: FontSize::Px(14.0),
font_size: size::MEDIUM_FONT,
weight: FontWeight::NORMAL,
..Default::default()
},
@@ -245,13 +267,17 @@ fn set_button_styles(
(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,
(ButtonVariant::Plain, true, _, _) => tokens::BUTTON_PLAIN_BG_DISABLED,
(ButtonVariant::Plain, false, true, _) => tokens::BUTTON_PLAIN_BG_PRESSED,
(ButtonVariant::Plain, false, false, true) => tokens::BUTTON_PLAIN_BG_HOVER,
(ButtonVariant::Plain, false, false, false) => tokens::BUTTON_PLAIN_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,
(ButtonVariant::Normal | ButtonVariant::Plain, true) => tokens::BUTTON_TEXT_DISABLED,
(ButtonVariant::Normal | ButtonVariant::Plain, false) => tokens::BUTTON_TEXT,
};
let cursor_shape = match disabled {
@@ -18,7 +18,7 @@ use bevy_math::Rot2;
use bevy_picking::{hover::Hovered, PickingSystems};
use bevy_reflect::{prelude::ReflectDefault, Reflect};
use bevy_scene::prelude::*;
use bevy_text::{FontSize, FontWeight};
use bevy_text::FontWeight;
use bevy_ui::{
AlignItems, BorderRadius, Checked, Display, FlexDirection, InteractionDisabled, JustifyContent,
Node, PositionType, UiRect, UiTransform, Val,
@@ -87,7 +87,7 @@ pub fn checkbox(props: CheckboxProps) -> impl Scene {
ThemeFontColor(tokens::CHECKBOX_TEXT)
InheritableFont {
font: fonts::REGULAR,
font_size: FontSize::Px(14.0),
font_size: size::MEDIUM_FONT,
weight: FontWeight::NORMAL,
}
Children [(
@@ -152,7 +152,7 @@ pub fn checkbox_bundle<C: SpawnableList<ChildOf> + Send + Sync + 'static, B: Bun
TabIndex(0),
ThemeFontColor(tokens::CHECKBOX_TEXT),
InheritableFont {
font_size: FontSize::Px(14.0),
font_size: size::MEDIUM_FONT,
weight: FontWeight::NORMAL,
..Default::default()
},
+3 -3
View File
@@ -15,7 +15,7 @@ use bevy_ecs::{
use bevy_log::warn;
use bevy_picking::{hover::Hovered, PickingSystems};
use bevy_scene::{prelude::*, template_value};
use bevy_text::{FontSize, FontWeight};
use bevy_text::FontWeight;
use bevy_ui::{
px, AlignItems, AlignSelf, BoxShadow, Display, FlexDirection, GlobalZIndex,
InteractionDisabled, JustifyContent, Node, OverrideClip, PositionType, Pressed, UiRect, Val,
@@ -29,8 +29,8 @@ use crate::{
constants::{fonts, icons, size},
controls::{button, ButtonProps, ButtonVariant},
cursor::EntityCursor,
display::icon,
font_styles::InheritableFont,
icon,
rounded_corners::RoundedCorners,
theme::{ThemeBackgroundColor, ThemeBorderColor, ThemeFontColor},
tokens,
@@ -271,7 +271,7 @@ pub fn menu_item(props: MenuItemProps) -> impl Scene {
ThemeFontColor(tokens::MENUITEM_TEXT)
InheritableFont {
font: fonts::REGULAR,
font_size: FontSize::Px(14.0),
font_size: size::MEDIUM_FONT,
weight: FontWeight::NORMAL,
}
Children [
+3 -3
View File
@@ -17,7 +17,7 @@ use bevy_input_focus::tab_navigation::TabIndex;
use bevy_picking::{hover::Hovered, PickingSystems};
use bevy_reflect::{prelude::ReflectDefault, Reflect};
use bevy_scene::prelude::*;
use bevy_text::{FontSize, FontWeight};
use bevy_text::FontWeight;
use bevy_ui::{
AlignItems, BorderRadius, Checked, Display, FlexDirection, InteractionDisabled, JustifyContent,
Node, UiRect, Val,
@@ -81,7 +81,7 @@ pub fn radio(props: RadioProps) -> impl Scene {
ThemeFontColor(tokens::RADIO_TEXT)
InheritableFont {
font: fonts::REGULAR,
font_size: FontSize::Px(14.0),
font_size: size::MEDIUM_FONT,
weight: FontWeight::NORMAL,
}
Children [(
@@ -141,7 +141,7 @@ pub fn radio_bundle<C: SpawnableList<ChildOf> + Send + Sync + 'static, B: Bundle
TabIndex(0),
ThemeFontColor(tokens::RADIO_TEXT),
InheritableFont {
font_size: FontSize::Px(14.0),
font_size: size::MEDIUM_FONT,
weight: FontWeight::NORMAL,
..Default::default()
},
+3 -3
View File
@@ -18,7 +18,7 @@ use bevy_input_focus::tab_navigation::TabIndex;
use bevy_picking::PickingSystems;
use bevy_reflect::{prelude::ReflectDefault, Reflect};
use bevy_scene::prelude::*;
use bevy_text::{FontSize, FontWeight};
use bevy_text::FontWeight;
use bevy_ui::{
widget::Text, AlignItems, BackgroundGradient, ColorStop, Display, FlexDirection, Gradient,
InteractionDisabled, InterpolationColorSpace, JustifyContent, LinearGradient, Node,
@@ -123,7 +123,7 @@ pub fn slider(props: SliderProps) -> impl Scene {
ThemeFontColor(tokens::SLIDER_TEXT)
InheritableFont {
font: fonts::MONO,
font_size: FontSize::Px(12.0),
font_size: size::SMALL_FONT,
weight: FontWeight::NORMAL,
}
Children [(Text("10.0") ThemedText SliderValueText)]
@@ -189,7 +189,7 @@ pub fn slider_bundle<B: Bundle>(props: SliderProps, overrides: B) -> impl Bundle
},
ThemeFontColor(tokens::SLIDER_TEXT),
InheritableFont {
font_size: FontSize::Px(12.0),
font_size: size::SMALL_FONT,
weight: FontWeight::NORMAL,
..Default::default()
},
@@ -12,7 +12,7 @@ use bevy_ecs::{
use bevy_input_focus::{tab_navigation::TabIndex, InputFocus, InputFocusVisible};
use bevy_picking::PickingSystems;
use bevy_scene::prelude::*;
use bevy_text::{EditableText, FontSize, FontWeight, LineBreak, TextCursorStyle, TextLayout};
use bevy_text::{EditableText, FontWeight, LineBreak, TextCursorStyle, TextLayout};
use bevy_ui::{
px, AlignItems, BorderColor, BorderRadius, Display, InteractionDisabled, JustifyContent, Node,
UiRect, Val,
@@ -61,7 +61,7 @@ pub fn text_input_container() -> impl Scene {
ThemeFontColor(tokens::TEXT_INPUT_TEXT)
InheritableFont {
font: fonts::REGULAR,
font_size: FontSize::Px(13.0),
font_size: size::COMPACT_FONT,
weight: FontWeight::NORMAL,
}
}
@@ -33,6 +33,9 @@ where
let key_clone = key.clone();
bsn! {
button(ButtonProps::default())
Node {
flex_grow: 1.0,
}
on(
move |activate: On<Activate>,
mut commands: Commands,
+20
View File
@@ -11,6 +11,8 @@ pub fn create_dark_theme() -> ThemeProps {
color: HashMap::from([
(tokens::WINDOW_BG, palette::GRAY_0),
(tokens::FOCUS_RING, palette::ACCENT.with_alpha(0.5)),
(tokens::TEXT_MAIN, palette::LIGHT_GRAY_1),
(tokens::TEXT_DIM, palette::LIGHT_GRAY_2),
// Button (normal)
(tokens::BUTTON_BG, palette::GRAY_3),
(tokens::BUTTON_BG_HOVER, palette::GRAY_3.lighter(0.05)),
@@ -128,6 +130,24 @@ pub fn create_dark_theme() -> ThemeProps {
),
(tokens::TEXT_INPUT_CURSOR, palette::ACCENT.lighter(0.2)),
(tokens::TEXT_INPUT_SELECTION, palette::ACCENT),
// Pane
(tokens::PANE_HEADER_BG, palette::GRAY_0),
(tokens::PANE_HEADER_BORDER, palette::WARM_GRAY_1),
(tokens::PANE_HEADER_TEXT, palette::LIGHT_GRAY_1),
(tokens::PANE_HEADER_DIVIDER, palette::WARM_GRAY_1),
(tokens::PANE_BODY_BG, palette::GRAY_1),
// Subpane
(tokens::SUBPANE_HEADER_BG, palette::GRAY_2),
(tokens::SUBPANE_HEADER_BORDER, palette::GRAY_3),
(tokens::SUBPANE_HEADER_TEXT, palette::LIGHT_GRAY_1),
(tokens::SUBPANE_BODY_BG, palette::GRAY_1),
(tokens::SUBPANE_BODY_BORDER, palette::GRAY_2),
// Group
(tokens::GROUP_HEADER_BG, palette::GRAY_2),
(tokens::GROUP_HEADER_BORDER, palette::GRAY_3),
(tokens::GROUP_HEADER_TEXT, palette::LIGHT_GRAY_1),
(tokens::GROUP_BODY_BG, palette::GRAY_2),
(tokens::GROUP_BODY_BORDER, palette::GRAY_3),
]),
}
}
@@ -1,4 +1,4 @@
//! BSN Template for loading images and displaying them as [`ImageNode`]s.
//! BSN Scene for loading images and displaying them as [`ImageNode`]s.
use bevy_asset::AssetServer;
use bevy_ecs::template::template;
use bevy_scene::{bsn, Scene};
+48
View File
@@ -0,0 +1,48 @@
//! BSN scene function for displaying a plain text string in the correct font.
use bevy_ecs::hierarchy::Children;
use bevy_scene::{bsn, template_value, Scene};
use bevy_text::FontWeight;
use bevy_ui::{widget::Text, Node};
use crate::{
constants::{fonts, size},
font_styles::InheritableFont,
theme::{ThemeFontColor, ThemedText},
tokens,
};
/// A text label.
pub fn label(text: impl Into<String>) -> impl Scene {
let text = Text::new(text.into());
bsn! {
Node
ThemeFontColor(tokens::TEXT_MAIN)
InheritableFont {
font: fonts::REGULAR,
font_size: size::MEDIUM_FONT,
weight: FontWeight::NORMAL,
}
Children [
template_value(text)
ThemedText
]
}
}
/// A text label with a dimmed color.
pub fn label_dim(text: impl Into<String>) -> impl Scene {
let text = Text::new(text.into());
bsn! {
Node
ThemeFontColor(tokens::TEXT_DIM)
InheritableFont {
font: fonts::REGULAR,
font_size: size::MEDIUM_FONT,
weight: FontWeight::NORMAL,
}
Children [
template_value(text)
ThemedText
]
}
}
+7
View File
@@ -0,0 +1,7 @@
//! Static widgets that only display data and are not interactive.
mod icon;
mod label;
pub use icon::*;
pub use label::*;
+2 -3
View File
@@ -47,19 +47,18 @@ use crate::{
mod alpha_pattern;
pub mod constants;
pub mod containers;
pub mod controls;
pub mod cursor;
pub mod dark_theme;
pub mod display;
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 FeathersCorePlugin;
+40
View File
@@ -198,3 +198,43 @@ pub const TEXT_INPUT_TEXT_DISABLED: ThemeToken =
pub const TEXT_INPUT_CURSOR: ThemeToken = ThemeToken::new_static("feathers.textinput.cursor");
/// Selection color for text input
pub const TEXT_INPUT_SELECTION: ThemeToken = ThemeToken::new_static("feathers.textinput.selection");
// Pane
/// Pane header background
pub const PANE_HEADER_BG: ThemeToken = ThemeToken::new_static("feathers.pane.header.bg");
/// Pane header border
pub const PANE_HEADER_BORDER: ThemeToken = ThemeToken::new_static("feathers.pane.header.border");
/// Pane header text color
pub const PANE_HEADER_TEXT: ThemeToken = ThemeToken::new_static("feathers.pane.header.text");
/// Pane header divider color
pub const PANE_HEADER_DIVIDER: ThemeToken = ThemeToken::new_static("feathers.pane.header.divider");
/// Pane body background
pub const PANE_BODY_BG: ThemeToken = ThemeToken::new_static("feathers.pane.body.bg");
// Subpane
/// Subpane background
pub const SUBPANE_HEADER_BG: ThemeToken = ThemeToken::new_static("feathers.subpane.header.bg");
/// Subpane header border
pub const SUBPANE_HEADER_BORDER: ThemeToken =
ThemeToken::new_static("feathers.subpane.header.border");
/// Subpane header text color
pub const SUBPANE_HEADER_TEXT: ThemeToken = ThemeToken::new_static("feathers.subpane.header.text");
/// Subpane body background
pub const SUBPANE_BODY_BG: ThemeToken = ThemeToken::new_static("feathers.subpane.body.bg");
/// Subpane body border
pub const SUBPANE_BODY_BORDER: ThemeToken = ThemeToken::new_static("feathers.subpane.body.border");
// Group
/// Group background
pub const GROUP_HEADER_BG: ThemeToken = ThemeToken::new_static("feathers.group.header.bg");
/// Group header border
pub const GROUP_HEADER_BORDER: ThemeToken = ThemeToken::new_static("feathers.group.header.border");
/// Group header text color
pub const GROUP_HEADER_TEXT: ThemeToken = ThemeToken::new_static("feathers.group.header.text");
/// Group body background
pub const GROUP_BODY_BG: ThemeToken = ThemeToken::new_static("feathers.group.body.bg");
/// Group body border
pub const GROUP_BODY_BORDER: ThemeToken = ThemeToken::new_static("feathers.group.body.border");
+514 -395
View File
@@ -3,16 +3,22 @@
use bevy::{
color::palettes,
feathers::{
constants::icons,
containers::{
flex_spacer, group, group_body, group_header, pane, pane_body, pane_header,
pane_header_divider, subpane, subpane_body, subpane_header,
},
controls::{
button, checkbox, color_plane, color_slider, color_swatch, menu, menu_button,
menu_divider, menu_item, menu_popup, radio, slider, text_input, text_input_container,
toggle_switch, ButtonProps, ButtonVariant, CheckboxProps, ColorChannel, ColorPlane,
ColorPlaneValue, ColorSlider, ColorSliderProps, ColorSwatch, ColorSwatchValue,
MenuButtonProps, MenuItemProps, RadioProps, SliderBaseColor, SliderProps,
TextInputProps,
toggle_switch, tool_button, ButtonProps, ButtonVariant, CheckboxProps, ColorChannel,
ColorPlane, ColorPlaneValue, ColorSlider, ColorSliderProps, ColorSwatch,
ColorSwatchValue, MenuButtonProps, MenuItemProps, RadioProps, SliderBaseColor,
SliderProps, TextInputProps,
},
cursor::{EntityCursor, OverrideCursor},
dark_theme::create_dark_theme,
display::{icon, label, label_dim},
rounded_corners::RoundedCorners,
theme::{ThemeBackgroundColor, ThemedText, UiTheme},
tokens, FeathersPlugins,
@@ -75,408 +81,521 @@ fn demo_root() -> impl Scene {
align_items: AlignItems::Start,
justify_content: JustifyContent::Start,
display: Display::Flex,
flex_direction: FlexDirection::Column,
row_gap: px(10),
flex_direction: FlexDirection::Row,
column_gap: px(8),
}
TabGroup
ThemeBackgroundColor(tokens::WINDOW_BG)
Children[(
Node {
display: Display::Flex,
flex_direction: FlexDirection::Column,
align_items: AlignItems::Stretch,
justify_content: JustifyContent::Start,
padding: UiRect::all(px(8)),
row_gap: px(8),
width: percent(30),
min_width: px(200),
}
Children [
(
Node {
display: Display::Flex,
flex_direction: FlexDirection::Row,
align_items: AlignItems::Center,
justify_content: JustifyContent::Start,
column_gap: px(8),
}
Children [
(
button(ButtonProps {
caption: Box::new(bsn_list!(
(Text("Normal") ThemedText),
)),
..default()
})
on(|_activate: On<Activate>| {
info!("Normal button clicked!");
})
AutoFocus
),
(
button(ButtonProps {
caption: Box::new(bsn_list!(
(Text("Disabled") ThemedText),
)),
..default()
})
InteractionDisabled
DemoDisabledButton
on(|_activate: On<Activate>| {
info!("Disabled button clicked!");
})
),
(
button(ButtonProps {
caption: Box::new(bsn_list!(
(Text("Primary") ThemedText),
)),
variant: ButtonVariant::Primary,
..default()
})
on(|_activate: On<Activate>| {
info!("Disabled button clicked!");
})
),
(
:menu
Children [
(
:menu_button(MenuButtonProps {
caption: Box::new(bsn_list!(
(Text("Menu") ThemedText),
)),
..default()
})
),
(
:menu_popup
Children [
(
menu_item(MenuItemProps {
caption: Box::new(bsn_list!(
(Text("MenuItem 1") ThemedText)))
})
on(|_: On<Activate>| {
info!("Menu item 1 clicked!");
})
),
(
menu_item(MenuItemProps {
caption: Box::new(bsn_list!(
(Text("MenuItem 2") ThemedText)))
})
on(|_: On<Activate>| {
info!("Menu item 2 clicked!");
})
),
:menu_divider,
(
menu_item(MenuItemProps {
caption: Box::new(bsn_list!(
(Text("MenuItem 3") ThemedText)))
})
on(|_: On<Activate>| {
info!("Menu item 3 clicked!");
})
)
]
)
]
)
]
),
(
Node {
display: Display::Flex,
flex_direction: FlexDirection::Row,
align_items: AlignItems::Center,
justify_content: JustifyContent::Start,
column_gap: px(1),
}
Children [
(
button(ButtonProps {
caption: Box::new(bsn_list!(
(Text("Left") ThemedText),
)),
corners: RoundedCorners::Left,
..default()
})
on(|_activate: On<Activate>| {
info!("Left button clicked!");
})
),
(
button(ButtonProps {
caption: Box::new(bsn_list!(
(Text("Center") ThemedText),
)),
corners: RoundedCorners::None,
..default()
})
on(|_activate: On<Activate>| {
info!("Center button clicked!");
})
),
(
button(ButtonProps {
caption: Box::new(bsn_list!(
(Text("Right") ThemedText),
)),
variant: ButtonVariant::Primary,
corners: RoundedCorners::Right,
})
on(|_activate: On<Activate>| {
info!("Right button clicked!");
})
),
]
),
(
button(ButtonProps::default())
on(|_activate: On<Activate>, mut ovr: ResMut<OverrideCursor>| {
ovr.0 = if ovr.0.is_some() {
None
} else {
Some(EntityCursor::System(SystemCursorIcon::Wait))
};
info!("Override cursor button clicked!");
})
Children [ (Text::new("Toggle override") ThemedText) ]
),
(
checkbox(CheckboxProps {
caption: Box::new(bsn_list!(
(Text("Checkbox") ThemedText),
)),
})
Checked
on(
|change: On<ValueChange<bool>>,
query: Query<Entity, With<DemoDisabledButton>>,
mut commands: Commands| {
info!("Checkbox clicked!");
let mut button = commands.entity(query.single().unwrap());
if change.value {
button.insert(InteractionDisabled);
} else {
button.remove::<InteractionDisabled>();
}
let mut checkbox = commands.entity(change.source);
if change.value {
checkbox.insert(Checked);
} else {
checkbox.remove::<Checked>();
}
Children[
:demo_column_1,
:demo_column_2,
]
}
}
fn demo_column_1() -> impl Scene {
bsn! {
Node {
display: Display::Flex,
flex_direction: FlexDirection::Column,
align_items: AlignItems::Stretch,
justify_content: JustifyContent::Start,
padding: UiRect::all(px(8)),
row_gap: px(8),
width: percent(30),
min_width: px(200),
}
Children [
(
Node {
display: Display::Flex,
flex_direction: FlexDirection::Row,
align_items: AlignItems::Center,
justify_content: JustifyContent::Start,
column_gap: px(8),
}
Children [
(
button(ButtonProps {
caption: Box::new(bsn_list!(
(Text("Normal") ThemedText),
)),
..default()
})
Node {
flex_grow: 1.0,
}
)
),
(
checkbox(CheckboxProps {
caption: Box::new(bsn_list!(
(Text("Disabled") ThemedText),
)),
})
InteractionDisabled
on(|_change: On<ValueChange<bool>>| {
warn!("Disabled checkbox clicked!");
})
),
(
checkbox(CheckboxProps {
caption: Box::new(bsn_list!(
(Text("Disabled+Checked") ThemedText),
)),
})
InteractionDisabled
Checked
on(|_change: On<ValueChange<bool>>| {
warn!("Disabled checkbox clicked!");
})
),
(
Node {
display: Display::Flex,
flex_direction: FlexDirection::Column,
row_gap: px(4),
}
RadioGroup
on(
|value_change: On<ValueChange<Entity>>,
q_radio: Query<Entity, With<RadioButton>>,
mut commands: Commands| {
for radio in q_radio.iter() {
if radio == value_change.value {
commands.entity(radio).insert(Checked);
} else {
commands.entity(radio).remove::<Checked>();
}
}
}
)
Children [
(radio(RadioProps {
caption: Box::new(bsn_list!(
(Text("One") ThemedText),
)),
}) Checked),
(radio(RadioProps {
caption: Box::new(bsn_list!(
(Text("Two") ThemedText),
)),
})),
(radio(RadioProps {
caption: Box::new(bsn_list!(
(Text("Three") ThemedText),
)),
})),
(radio(RadioProps {
on(|_activate: On<Activate>| {
info!("Normal button clicked!");
})
AutoFocus
),
(
button(ButtonProps {
caption: Box::new(bsn_list!(
(Text("Disabled") ThemedText),
)),
}) InteractionDisabled),
]
),
(
Node {
display: Display::Flex,
flex_direction: FlexDirection::Row,
align_items: AlignItems::Center,
justify_content: JustifyContent::Start,
column_gap: px(8),
}
Children [
(toggle_switch() on(checkbox_self_update)),
(toggle_switch() InteractionDisabled on(checkbox_self_update)),
(toggle_switch() InteractionDisabled Checked on(checkbox_self_update)),
]
),
(
slider(SliderProps {
max: 100.0,
value: 20.0,
..default()
})
SliderStep(10.)
SliderPrecision(2)
on(slider_self_update)
),
(
Node {
display: Display::Flex,
flex_direction: FlexDirection::Row,
justify_content: JustifyContent::SpaceBetween,
column_gap: px(4.0),
}
Children [
Text("Srgba"),
// Spacer
..default()
})
Node {
flex_grow: 1.0,
},
// Text input
(
:text_input_container
Node {
flex_grow: 1.0,
}
Children [
(
text_input(TextInputProps {
max_characters: Some(9),
})
HexColorInput
on(handle_hex_color_change)
)
]
)
(color_swatch() SwatchType::Rgb),
]
),
(
color_plane(ColorPlane::RedBlue)
on(|change: On<ValueChange<Vec2>>, mut color: ResMut<DemoWidgetStates>| {
color.rgb_color.red = change.value.x;
color.rgb_color.blue = change.value.y;
})
),
(
color_slider(ColorSliderProps {
value: 0.5,
channel: ColorChannel::Red
})
on(|change: On<ValueChange<f32>>, mut color: ResMut<DemoWidgetStates>| {
color.rgb_color.red = change.value;
})
),
(
color_slider(ColorSliderProps {
value: 0.5,
channel: ColorChannel::Green
})
on(|change: On<ValueChange<f32>>, mut color: ResMut<DemoWidgetStates>| {
color.rgb_color.green = change.value;
})
),
(
color_slider(ColorSliderProps {
value: 0.5,
channel: ColorChannel::Blue
})
on(|change: On<ValueChange<f32>>, mut color: ResMut<DemoWidgetStates>| {
color.rgb_color.blue = change.value;
})
),
(
color_slider(ColorSliderProps {
value: 0.5,
channel: ColorChannel::Alpha
})
on(|change: On<ValueChange<f32>>, mut color: ResMut<DemoWidgetStates>| {
color.rgb_color.alpha = change.value;
})
),
(
Node {
display: Display::Flex,
flex_direction: FlexDirection::Row,
justify_content: JustifyContent::SpaceBetween,
}
InteractionDisabled
DemoDisabledButton
on(|_activate: On<Activate>| {
info!("Disabled button clicked!");
})
),
(
button(ButtonProps {
caption: Box::new(bsn_list!(
(Text("Primary") ThemedText),
)),
variant: ButtonVariant::Primary,
..default()
})
Node {
flex_grow: 1.0,
}
on(|_activate: On<Activate>| {
info!("Disabled button clicked!");
})
),
(
:menu
Children [
(
:menu_button(MenuButtonProps {
caption: Box::new(bsn_list!(
(Text("Menu") ThemedText),
)),
..default()
})
Node {
flex_grow: 1.0,
}
),
(
:menu_popup
Children [
(
menu_item(MenuItemProps {
caption: Box::new(bsn_list!(
(Text("MenuItem 1") ThemedText)))
})
on(|_: On<Activate>| {
info!("Menu item 1 clicked!");
})
),
(
menu_item(MenuItemProps {
caption: Box::new(bsn_list!(
(Text("MenuItem 2") ThemedText)))
})
on(|_: On<Activate>| {
info!("Menu item 2 clicked!");
})
),
:menu_divider,
(
menu_item(MenuItemProps {
caption: Box::new(bsn_list!(
(Text("MenuItem 3") ThemedText)))
})
on(|_: On<Activate>| {
info!("Menu item 3 clicked!");
})
)
]
)
]
)
]
),
(
Node {
display: Display::Flex,
flex_direction: FlexDirection::Row,
align_items: AlignItems::Center,
justify_content: JustifyContent::Start,
column_gap: px(1),
}
Children [
(
button(ButtonProps {
caption: Box::new(bsn_list!(
(Text("Left") ThemedText),
)),
corners: RoundedCorners::Left,
..default()
})
Node {
flex_grow: 1.0,
}
on(|_activate: On<Activate>| {
info!("Left button clicked!");
})
),
(
button(ButtonProps {
caption: Box::new(bsn_list!(
(Text("Center") ThemedText),
)),
corners: RoundedCorners::None,
..default()
})
Node {
flex_grow: 1.0,
}
on(|_activate: On<Activate>| {
info!("Center button clicked!");
})
),
(
button(ButtonProps {
caption: Box::new(bsn_list!(
(Text("Right") ThemedText),
)),
variant: ButtonVariant::Primary,
corners: RoundedCorners::Right,
})
Node {
flex_grow: 1.0,
}
on(|_activate: On<Activate>| {
info!("Right button clicked!");
})
),
]
),
(
button(ButtonProps::default())
on(|_activate: On<Activate>, mut ovr: ResMut<OverrideCursor>| {
ovr.0 = if ovr.0.is_some() {
None
} else {
Some(EntityCursor::System(SystemCursorIcon::Wait))
};
info!("Override cursor button clicked!");
})
Children [ (Text::new("Toggle override") ThemedText) ]
),
(
checkbox(CheckboxProps {
caption: Box::new(bsn_list!(
(Text("Checkbox") ThemedText),
)),
})
Checked
on(
|change: On<ValueChange<bool>>,
query: Query<Entity, With<DemoDisabledButton>>,
mut commands: Commands| {
info!("Checkbox clicked!");
let mut button = commands.entity(query.single().unwrap());
if change.value {
button.insert(InteractionDisabled);
} else {
button.remove::<InteractionDisabled>();
}
let mut checkbox = commands.entity(change.source);
if change.value {
checkbox.insert(Checked);
} else {
checkbox.remove::<Checked>();
}
}
Children [
Text("Hsl"),
(color_swatch() SwatchType::Hsl)
]
),
(
color_slider(ColorSliderProps {
value: 0.5,
channel: ColorChannel::HslHue
})
on(|change: On<ValueChange<f32>>, mut color: ResMut<DemoWidgetStates>| {
color.hsl_color.hue = change.value;
})
),
(
color_slider(ColorSliderProps {
value: 0.5,
channel: ColorChannel::HslSaturation
})
on(|change: On<ValueChange<f32>>, mut color: ResMut<DemoWidgetStates>| {
color.hsl_color.saturation = change.value;
})
),
(
color_slider(ColorSliderProps {
value: 0.5,
channel: ColorChannel::HslLightness
})
on(|change: On<ValueChange<f32>>, mut color: ResMut<DemoWidgetStates>| {
color.hsl_color.lightness = change.value;
})
)
]
)]
),
(
checkbox(CheckboxProps {
caption: Box::new(bsn_list!(
(Text("Disabled") ThemedText),
)),
})
InteractionDisabled
on(|_change: On<ValueChange<bool>>| {
warn!("Disabled checkbox clicked!");
})
),
(
checkbox(CheckboxProps {
caption: Box::new(bsn_list!(
(Text("Disabled+Checked") ThemedText),
)),
})
InteractionDisabled
Checked
on(|_change: On<ValueChange<bool>>| {
warn!("Disabled checkbox clicked!");
})
),
(
Node {
display: Display::Flex,
flex_direction: FlexDirection::Column,
row_gap: px(4),
}
RadioGroup
on(
|value_change: On<ValueChange<Entity>>,
q_radio: Query<Entity, With<RadioButton>>,
mut commands: Commands| {
for radio in q_radio.iter() {
if radio == value_change.value {
commands.entity(radio).insert(Checked);
} else {
commands.entity(radio).remove::<Checked>();
}
}
}
)
Children [
(radio(RadioProps {
caption: Box::new(bsn_list!(
(Text("One") ThemedText),
)),
}) Checked),
(radio(RadioProps {
caption: Box::new(bsn_list!(
(Text("Two") ThemedText),
)),
})),
(radio(RadioProps {
caption: Box::new(bsn_list!(
(Text("Three") ThemedText),
)),
})),
(radio(RadioProps {
caption: Box::new(bsn_list!(
(Text("Disabled") ThemedText),
)),
}) InteractionDisabled),
]
),
(
Node {
display: Display::Flex,
flex_direction: FlexDirection::Row,
align_items: AlignItems::Center,
justify_content: JustifyContent::Start,
column_gap: px(8),
}
Children [
(toggle_switch() on(checkbox_self_update)),
(toggle_switch() InteractionDisabled on(checkbox_self_update)),
(toggle_switch() InteractionDisabled Checked on(checkbox_self_update)),
]
),
(
slider(SliderProps {
max: 100.0,
value: 20.0,
..default()
})
SliderStep(10.)
SliderPrecision(2)
on(slider_self_update)
),
(
Node {
display: Display::Flex,
flex_direction: FlexDirection::Row,
align_items: AlignItems::Center,
justify_content: JustifyContent::SpaceBetween,
column_gap: px(4.0),
}
Children [
:label("Srgba"),
// Spacer
:flex_spacer,
// Text input
(
:text_input_container
Node {
flex_grow: 1.0,
}
Children [
(
text_input(TextInputProps {
max_characters: Some(9),
})
HexColorInput
on(handle_hex_color_change)
)
]
)
(color_swatch() SwatchType::Rgb),
]
),
(
color_plane(ColorPlane::RedBlue)
on(|change: On<ValueChange<Vec2>>, mut color: ResMut<DemoWidgetStates>| {
color.rgb_color.red = change.value.x;
color.rgb_color.blue = change.value.y;
})
),
(
color_slider(ColorSliderProps {
value: 0.5,
channel: ColorChannel::Red
})
on(|change: On<ValueChange<f32>>, mut color: ResMut<DemoWidgetStates>| {
color.rgb_color.red = change.value;
})
),
(
color_slider(ColorSliderProps {
value: 0.5,
channel: ColorChannel::Green
})
on(|change: On<ValueChange<f32>>, mut color: ResMut<DemoWidgetStates>| {
color.rgb_color.green = change.value;
})
),
(
color_slider(ColorSliderProps {
value: 0.5,
channel: ColorChannel::Blue
})
on(|change: On<ValueChange<f32>>, mut color: ResMut<DemoWidgetStates>| {
color.rgb_color.blue = change.value;
})
),
(
color_slider(ColorSliderProps {
value: 0.5,
channel: ColorChannel::Alpha
})
on(|change: On<ValueChange<f32>>, mut color: ResMut<DemoWidgetStates>| {
color.rgb_color.alpha = change.value;
})
),
(
Node {
display: Display::Flex,
align_items: AlignItems::Center,
flex_direction: FlexDirection::Row,
justify_content: JustifyContent::SpaceBetween,
}
Children [
:label("Hsl"),
(color_swatch() SwatchType::Hsl)
]
),
(
color_slider(ColorSliderProps {
value: 0.5,
channel: ColorChannel::HslHue
})
on(|change: On<ValueChange<f32>>, mut color: ResMut<DemoWidgetStates>| {
color.hsl_color.hue = change.value;
})
),
(
color_slider(ColorSliderProps {
value: 0.5,
channel: ColorChannel::HslSaturation
})
on(|change: On<ValueChange<f32>>, mut color: ResMut<DemoWidgetStates>| {
color.hsl_color.saturation = change.value;
})
),
(
color_slider(ColorSliderProps {
value: 0.5,
channel: ColorChannel::HslLightness
})
on(|change: On<ValueChange<f32>>, mut color: ResMut<DemoWidgetStates>| {
color.hsl_color.lightness = change.value;
})
)
]
}
}
fn demo_column_2() -> impl Scene {
bsn! {
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.),
}
Children [
(
:pane Children [
:pane_header Children [
:tool_button(ButtonProps {
variant: ButtonVariant::Primary,
..default()
}) Children [
(Text("\u{0398}") ThemedText)
],
:pane_header_divider,
:tool_button(ButtonProps{
variant: ButtonVariant::Plain,
..default()
}) Children [
(Text("\u{00BC}") ThemedText)
],
:tool_button(ButtonProps{
variant: ButtonVariant::Plain,
..default()
}) Children [
(Text("\u{00BD}") ThemedText)
],
:tool_button(ButtonProps{
variant: ButtonVariant::Plain,
..default()
}) Children [
(Text("\u{00BE}") ThemedText)
],
:pane_header_divider,
:tool_button(ButtonProps{
variant: ButtonVariant::Plain,
..default()
}) Children [
:icon(icons::CHEVRON_DOWN)
],
:flex_spacer,
:tool_button(ButtonProps{
variant: ButtonVariant::Plain,
..default()
}) Children [
:icon(icons::X)
],
],
(
:pane_body Children [
:label_dim("A standard editor pane"),
:subpane Children [
:subpane_header Children [
(Text("Left") ThemedText),
(Text("Center") ThemedText),
(Text("Right") ThemedText)
],
:subpane_body Children [
:label_dim("A standard sub-pane"),
:group Children [
:group_header Children [
(Text("Group") ThemedText),
],
:group_body Children [
:label_dim("A standard group"),
],
]
],
]
]
),
]
),
]
}
}