From 6b2370c6a5769122aff4d3c451ce12190b73a864 Mon Sep 17 00:00:00 2001 From: Elijah Voigt Date: Thu, 4 Dec 2025 22:12:18 -0800 Subject: [PATCH] Save feathers example, intend to redo ui at some point --- examples/feathers.rs | 480 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 480 insertions(+) create mode 100644 examples/feathers.rs diff --git a/examples/feathers.rs b/examples/feathers.rs new file mode 100644 index 0000000..13ea3c2 --- /dev/null +++ b/examples/feathers.rs @@ -0,0 +1,480 @@ +//! This example shows off the various Bevy Feathers widgets. + +use bevy::{ + color::palettes, + feathers::{ + controls::{ + button, checkbox, color_slider, color_swatch, radio, slider, toggle_switch, + ButtonProps, ButtonVariant, ColorChannel, ColorSlider, ColorSliderProps, ColorSwatch, + SliderBaseColor, SliderProps, + }, + dark_theme::create_dark_theme, + rounded_corners::RoundedCorners, + theme::{ThemeBackgroundColor, ThemedText, UiTheme}, + tokens, FeathersPlugins, + }, + input_focus::tab_navigation::TabGroup, + prelude::*, + ui::{Checked, InteractionDisabled}, + ui_widgets::{ + checkbox_self_update, observe, slider_self_update, Activate, RadioButton, RadioGroup, + SliderPrecision, SliderStep, SliderValue, ValueChange, + }, +}; + +/// A struct to hold the state of various widgets shown in the demo. +#[derive(Resource)] +struct DemoWidgetStates { + rgb_color: Srgba, + hsl_color: Hsla, +} + +#[derive(Component, Clone, Copy, PartialEq)] +enum SwatchType { + Rgb, + Hsl, +} + +#[derive(Component, Clone, Copy)] +struct DemoDisabledButton; + +fn main() { + App::new() + .add_plugins((DefaultPlugins, FeathersPlugins)) + .insert_resource(UiTheme(create_dark_theme())) + .insert_resource(DemoWidgetStates { + rgb_color: palettes::tailwind::EMERALD_800.with_alpha(0.7), + hsl_color: palettes::tailwind::AMBER_800.into(), + }) + .add_systems(Startup, setup) + .add_systems(Update, update_colors) + .run(); +} + +fn setup(mut commands: Commands) { + // ui camera + commands.spawn(Camera2d); + commands.spawn(demo_root()); +} + +fn demo_root() -> impl Bundle { + ( + Node { + width: percent(100), + height: percent(100), + align_items: AlignItems::Start, + justify_content: JustifyContent::Start, + display: Display::Flex, + flex_direction: FlexDirection::Column, + row_gap: px(10), + ..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(px(8)), + row_gap: px(8), + width: percent(30), + min_width: px(200), + ..default() + }, + children![ + ( + Node { + display: Display::Flex, + flex_direction: FlexDirection::Row, + align_items: AlignItems::Center, + justify_content: JustifyContent::Start, + column_gap: px(8), + ..default() + }, + children![ + ( + button( + ButtonProps::default(), + (), + Spawn((Text::new("Normal"), ThemedText)) + ), + observe(|_activate: On| { + info!("Normal button clicked!"); + }) + ), + ( + button( + ButtonProps::default(), + (InteractionDisabled, DemoDisabledButton), + Spawn((Text::new("Disabled"), ThemedText)) + ), + observe(|_activate: On| { + info!("Disabled button clicked!"); + }) + ), + ( + button( + ButtonProps { + variant: ButtonVariant::Primary, + ..default() + }, + (), + Spawn((Text::new("Primary"), ThemedText)) + ), + observe(|_activate: On| { + info!("Disabled button clicked!"); + }) + ), + ] + ), + ( + Node { + display: Display::Flex, + flex_direction: FlexDirection::Row, + align_items: AlignItems::Center, + justify_content: JustifyContent::Start, + column_gap: px(1), + ..default() + }, + children![ + ( + button( + ButtonProps { + corners: RoundedCorners::Left, + ..default() + }, + (), + Spawn((Text::new("Left"), ThemedText)) + ), + observe(|_activate: On| { + info!("Left button clicked!"); + }) + ), + ( + button( + ButtonProps { + corners: RoundedCorners::None, + ..default() + }, + (), + Spawn((Text::new("Center"), ThemedText)) + ), + observe(|_activate: On| { + info!("Center button clicked!"); + }) + ), + ( + button( + ButtonProps { + variant: ButtonVariant::Primary, + corners: RoundedCorners::Right, + }, + (), + Spawn((Text::new("Right"), ThemedText)) + ), + observe(|_activate: On| { + info!("Right button clicked!"); + }) + ), + ] + ), + ( + button( + ButtonProps::default(), + (), + Spawn((Text::new("Button"), ThemedText)) + ), + observe(|_activate: On| { + info!("Wide button clicked!"); + }) + ), + ( + checkbox(Checked, Spawn((Text::new("Checkbox"), ThemedText))), + observe( + |change: On>, + query: Query>, + mut commands: Commands| { + info!("Checkbox clicked!"); + let mut button = commands.entity(query.single().unwrap()); + if change.value { + button.insert(InteractionDisabled); + } else { + button.remove::(); + } + let mut checkbox = commands.entity(change.source); + if change.value { + checkbox.insert(Checked); + } else { + checkbox.remove::(); + } + } + ) + ), + ( + checkbox( + InteractionDisabled, + Spawn((Text::new("Disabled"), ThemedText)) + ), + observe(|_change: On>| { + warn!("Disabled checkbox clicked!"); + }) + ), + ( + checkbox( + (InteractionDisabled, Checked), + Spawn((Text::new("Disabled+Checked"), ThemedText)) + ), + observe(|_change: On>| { + warn!("Disabled checkbox clicked!"); + }) + ), + ( + Node { + display: Display::Flex, + flex_direction: FlexDirection::Column, + row_gap: px(4), + ..default() + }, + RadioGroup, + observe( + |value_change: On>, + q_radio: Query>, + mut commands: Commands| { + for radio in q_radio.iter() { + if radio == value_change.value { + commands.entity(radio).insert(Checked); + } else { + commands.entity(radio).remove::(); + } + } + } + ), + children![ + radio(Checked, Spawn((Text::new("One"), ThemedText))), + radio((), Spawn((Text::new("Two"), ThemedText))), + radio((), Spawn((Text::new("Three"), ThemedText))), + radio( + InteractionDisabled, + Spawn((Text::new("Disabled"), ThemedText)) + ), + ] + ), + ( + Node { + display: Display::Flex, + flex_direction: FlexDirection::Row, + align_items: AlignItems::Center, + justify_content: JustifyContent::Start, + column_gap: px(8), + ..default() + }, + children![ + (toggle_switch((),), observe(checkbox_self_update)), + ( + toggle_switch(InteractionDisabled,), + observe(checkbox_self_update) + ), + ( + toggle_switch((InteractionDisabled, Checked),), + observe(checkbox_self_update) + ), + ] + ), + ( + slider( + SliderProps { + max: 100.0, + value: 20.0, + ..default() + }, + (SliderStep(10.), SliderPrecision(2)), + ), + observe(slider_self_update) + ), + ( + Node { + display: Display::Flex, + flex_direction: FlexDirection::Row, + justify_content: JustifyContent::SpaceBetween, + ..default() + }, + children![Text("Srgba".to_owned()), color_swatch(SwatchType::Rgb),] + ), + ( + color_slider( + ColorSliderProps { + value: 0.5, + channel: ColorChannel::Red + }, + () + ), + observe( + |change: On>, mut color: ResMut| { + color.rgb_color.red = change.value; + } + ) + ), + ( + color_slider( + ColorSliderProps { + value: 0.5, + channel: ColorChannel::Green + }, + () + ), + observe( + |change: On>, mut color: ResMut| { + color.rgb_color.green = change.value; + }, + ) + ), + ( + color_slider( + ColorSliderProps { + value: 0.5, + channel: ColorChannel::Blue + }, + () + ), + observe( + |change: On>, mut color: ResMut| { + color.rgb_color.blue = change.value; + }, + ) + ), + ( + color_slider( + ColorSliderProps { + value: 0.5, + channel: ColorChannel::Alpha + }, + () + ), + observe( + |change: On>, mut color: ResMut| { + color.rgb_color.alpha = change.value; + }, + ) + ), + ( + Node { + display: Display::Flex, + flex_direction: FlexDirection::Row, + justify_content: JustifyContent::SpaceBetween, + ..default() + }, + children![Text("Hsl".to_owned()), color_swatch(SwatchType::Hsl),] + ), + ( + color_slider( + ColorSliderProps { + value: 0.5, + channel: ColorChannel::HslHue + }, + () + ), + observe( + |change: On>, mut color: ResMut| { + color.hsl_color.hue = change.value; + }, + ) + ), + ( + color_slider( + ColorSliderProps { + value: 0.5, + channel: ColorChannel::HslSaturation + }, + () + ), + observe( + |change: On>, mut color: ResMut| { + color.hsl_color.saturation = change.value; + }, + ) + ), + ( + color_slider( + ColorSliderProps { + value: 0.5, + channel: ColorChannel::HslLightness + }, + () + ), + observe( + |change: On>, mut color: ResMut| { + color.hsl_color.lightness = change.value; + }, + ) + ) + ] + ),], + ) +} + +fn update_colors( + colors: Res, + mut sliders: Query<(Entity, &ColorSlider, &mut SliderBaseColor)>, + swatches: Query<(&SwatchType, &Children), With>, + mut commands: Commands, +) { + if colors.is_changed() { + for (slider_ent, slider, mut base) in sliders.iter_mut() { + match slider.channel { + ColorChannel::Red => { + base.0 = colors.rgb_color.into(); + commands + .entity(slider_ent) + .insert(SliderValue(colors.rgb_color.red)); + } + ColorChannel::Green => { + base.0 = colors.rgb_color.into(); + commands + .entity(slider_ent) + .insert(SliderValue(colors.rgb_color.green)); + } + ColorChannel::Blue => { + base.0 = colors.rgb_color.into(); + commands + .entity(slider_ent) + .insert(SliderValue(colors.rgb_color.blue)); + } + ColorChannel::HslHue => { + base.0 = colors.hsl_color.into(); + commands + .entity(slider_ent) + .insert(SliderValue(colors.hsl_color.hue)); + } + ColorChannel::HslSaturation => { + base.0 = colors.hsl_color.into(); + commands + .entity(slider_ent) + .insert(SliderValue(colors.hsl_color.saturation)); + } + ColorChannel::HslLightness => { + base.0 = colors.hsl_color.into(); + commands + .entity(slider_ent) + .insert(SliderValue(colors.hsl_color.lightness)); + } + ColorChannel::Alpha => { + base.0 = colors.rgb_color.into(); + commands + .entity(slider_ent) + .insert(SliderValue(colors.rgb_color.alpha)); + } + } + } + + for (swatch_type, children) in swatches.iter() { + commands + .entity(children[0]) + .insert(BackgroundColor(match swatch_type { + SwatchType::Rgb => colors.rgb_color.into(), + SwatchType::Hsl => colors.hsl_color.into(), + })); + } + } +}