diff --git a/crates/bevy_feathers/Cargo.toml b/crates/bevy_feathers/Cargo.toml index 07d883704ac73..5402b4f822768 100644 --- a/crates/bevy_feathers/Cargo.toml +++ b/crates/bevy_feathers/Cargo.toml @@ -21,11 +21,13 @@ bevy_log = { path = "../bevy_log", version = "0.17.0-dev" } bevy_math = { path = "../bevy_math", 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_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" } bevy_render = { path = "../bevy_render", 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_ui_render = { path = "../bevy_ui_render", version = "0.17.0-dev" } bevy_window = { path = "../bevy_window", version = "0.17.0-dev" } bevy_winit = { path = "../bevy_winit", version = "0.17.0-dev" } diff --git a/crates/bevy_feathers/src/alpha_pattern.rs b/crates/bevy_feathers/src/alpha_pattern.rs new file mode 100644 index 0000000000000..81fbea1e8ac30 --- /dev/null +++ b/crates/bevy_feathers/src/alpha_pattern.rs @@ -0,0 +1,59 @@ +use bevy_app::Plugin; +use bevy_asset::{Asset, Assets, Handle}; +use bevy_ecs::{ + component::Component, + lifecycle::Add, + observer::On, + resource::Resource, + system::{Query, Res}, + world::FromWorld, +}; +use bevy_reflect::TypePath; +use bevy_render::render_resource::{AsBindGroup, ShaderRef}; +use bevy_ui_render::ui_material::{MaterialNode, UiMaterial}; + +#[derive(AsBindGroup, Asset, TypePath, Default, Debug, Clone)] +pub(crate) struct AlphaPatternMaterial {} + +impl UiMaterial for AlphaPatternMaterial { + fn fragment_shader() -> ShaderRef { + "embedded://bevy_feathers/assets/shaders/alpha_pattern.wgsl".into() + } +} + +#[derive(Resource)] +pub(crate) struct AlphaPatternResource(pub(crate) Handle); + +impl FromWorld for AlphaPatternResource { + fn from_world(world: &mut bevy_ecs::world::World) -> Self { + let mut ui_materials = world + .get_resource_mut::>() + .unwrap(); + Self(ui_materials.add(AlphaPatternMaterial::default())) + } +} + +/// Marker that tells us we want to fill in the [`MaterialNode`] with the alpha material. +#[derive(Component, Default, Clone)] +pub(crate) struct AlphaPattern; + +/// Observer to fill in the material handle (since we don't have access to the materials asset +/// in the template) +fn on_add_color_swatch( + ev: On, + mut q_swatch: Query<&mut MaterialNode>, + r_material: Res, +) { + if let Ok(mut material) = q_swatch.get_mut(ev.target()) { + material.0 = r_material.0.clone(); + } +} + +/// Plugin which registers the systems for updating the button styles. +pub struct AlphaPatternPlugin; + +impl Plugin for AlphaPatternPlugin { + fn build(&self, app: &mut bevy_app::App) { + app.add_observer(on_add_color_swatch); + } +} diff --git a/crates/bevy_feathers/src/assets/shaders/alpha_pattern.wgsl b/crates/bevy_feathers/src/assets/shaders/alpha_pattern.wgsl new file mode 100644 index 0000000000000..053645ed5b89c --- /dev/null +++ b/crates/bevy_feathers/src/assets/shaders/alpha_pattern.wgsl @@ -0,0 +1,39 @@ +// This shader draws a checkerboard pattern +#import bevy_ui::ui_vertex_output::UiVertexOutput + +@fragment +fn fragment(in: UiVertexOutput) -> @location(0) vec4 { + let uv = (in.uv - vec2(0.5, 0.5)) * in.size / 16.; + let check = select(0.0, 1.0, (fract(uv.x) < 0.5) != (fract(uv.y) < 0.5)); + let bg = mix(vec3(0.2, 0.2, 0.2), vec3(0.6, 0.6, 0.6), check); + + let size = vec2(in.size.x, in.size.y); + let external_distance = sd_rounded_box((in.uv - 0.5) * size, size, in.border_radius); + let alpha = smoothstep(0.5, -0.5, external_distance); + + return vec4(bg, alpha); +} + +// From: https://github.com/bevyengine/bevy/pull/8973 +// The returned value is the shortest distance from the given point to the boundary of the rounded box. +// Negative values indicate that the point is inside the rounded box, positive values that the point is outside, and zero is exactly on the boundary. +// arguments +// point -> The function will return the distance from this point to the closest point on the boundary. +// size -> The maximum width and height of the box. +// corner_radii -> The radius of each rounded corner. Ordered counter clockwise starting top left: +// x = top left, y = top right, z = bottom right, w = bottom left. +fn sd_rounded_box(point: vec2, size: vec2, corner_radii: vec4) -> f32 { + // if 0.0 < y then select bottom left (w) and bottom right corner radius (z) + // else select top left (x) and top right corner radius (y) + let rs = select(corner_radii.xy, corner_radii.wz, 0.0 < point.y); + // w and z are swapped so that both pairs are in left to right order, otherwise this second select statement would return the incorrect value for the bottom pair. + let radius = select(rs.x, rs.y, 0.0 < point.x); + // Vector from the corner closest to the point, to the point + let corner_to_point = abs(point) - 0.5 * size; + // Vector from the center of the radius circle to the point + let q = corner_to_point + radius; + // length from center of the radius circle to the point, 0s a component if the point is not within the quadrant of the radius circle that is part of the curved corner. + let l = length(max(q, vec2(0.0))); + let m = min(max(q.x, q.y), 0.0); + return l + m - radius; +} diff --git a/crates/bevy_feathers/src/controls/color_slider.rs b/crates/bevy_feathers/src/controls/color_slider.rs new file mode 100644 index 0000000000000..e4828ef9c15ed --- /dev/null +++ b/crates/bevy_feathers/src/controls/color_slider.rs @@ -0,0 +1,378 @@ +use core::f32::consts::PI; + +use bevy_app::{Plugin, PreUpdate}; +use bevy_asset::Handle; +use bevy_color::{Alpha, Color, Hsla}; +use bevy_core_widgets::{ + Callback, CoreSlider, CoreSliderThumb, SliderRange, SliderValue, TrackClick, +}; +use bevy_ecs::{ + bundle::Bundle, + children, + component::Component, + entity::Entity, + hierarchy::Children, + query::{Changed, Or, With}, + schedule::IntoScheduleConfigs, + spawn::SpawnRelated, + system::{In, Query}, +}; +use bevy_input_focus::tab_navigation::TabIndex; +use bevy_log::warn_once; +use bevy_picking::PickingSystems; +use bevy_ui::{ + AlignItems, BackgroundColor, BackgroundGradient, BorderColor, BorderRadius, ColorStop, Display, + FlexDirection, Gradient, InterpolationColorSpace, LinearGradient, Node, Outline, PositionType, + UiRect, UiTransform, Val, Val2, ZIndex, +}; +use bevy_ui_render::ui_material::MaterialNode; +use bevy_winit::cursor::CursorIcon; + +use crate::{ + alpha_pattern::{AlphaPattern, AlphaPatternMaterial}, + palette, + rounded_corners::RoundedCorners, +}; + +const SLIDER_HEIGHT: f32 = 16.0; +const TRACK_PADDING: f32 = 3.0; +const TRACK_RADIUS: f32 = SLIDER_HEIGHT * 0.5 - TRACK_PADDING; +const THUMB_SIZE: f32 = SLIDER_HEIGHT - 2.0; + +/// Indicates which color channel we want to edit. +#[derive(Component, Default, Clone)] +pub enum ColorChannel { + /// Editing the RGB red channel (0..=1) + #[default] + Red, + /// Editing the RGB green channel (0..=1) + Green, + /// Editing the RGB blue channel (0..=1) + Blue, + /// Editing the hue channel (0..=360) + HslHue, + /// Editing the chroma / saturation channel (0..=1) + HslSaturation, + /// Editing the luminance channel (0..=1) + HslLightness, + /// Editing the alpha channel (0..=1) + Alpha, +} + +impl ColorChannel { + /// Return the range of this color channel. + pub fn range(&self) -> SliderRange { + match self { + ColorChannel::Red + | ColorChannel::Green + | ColorChannel::Blue + | ColorChannel::Alpha + | ColorChannel::HslSaturation + | ColorChannel::HslLightness => SliderRange::new(0., 1.), + ColorChannel::HslHue => SliderRange::new(0., 360.), + } + } + + /// Return the color endpoints and midpoint of the gradient. This is determined by both the + /// channel being edited and the base color. + pub fn gradient_ends(&self, base_color: Color) -> (Color, Color, Color) { + match self { + ColorChannel::Red => { + let base_rgb = base_color.to_srgba(); + ( + Color::srgb(0.0, base_rgb.green, base_rgb.blue), + Color::srgb(0.5, base_rgb.green, base_rgb.blue), + Color::srgb(1.0, base_rgb.green, base_rgb.blue), + ) + } + + ColorChannel::Green => { + let base_rgb = base_color.to_srgba(); + ( + Color::srgb(base_rgb.red, 0.0, base_rgb.blue), + Color::srgb(base_rgb.red, 0.5, base_rgb.blue), + Color::srgb(base_rgb.red, 1.0, base_rgb.blue), + ) + } + + ColorChannel::Blue => { + let base_rgb = base_color.to_srgba(); + ( + Color::srgb(base_rgb.red, base_rgb.green, 0.0), + Color::srgb(base_rgb.red, base_rgb.green, 0.5), + Color::srgb(base_rgb.red, base_rgb.green, 1.0), + ) + } + + ColorChannel::HslHue => ( + Color::hsl(0.0 + 0.0001, 1.0, 0.5), + Color::hsl(180.0, 1.0, 0.5), + Color::hsl(360.0 - 0.0001, 1.0, 0.5), + ), + + ColorChannel::HslSaturation => { + let base_hsla: Hsla = base_color.into(); + ( + Color::hsl(base_hsla.hue, 0.0, base_hsla.lightness), + Color::hsl(base_hsla.hue, 0.5, base_hsla.lightness), + Color::hsl(base_hsla.hue, 1.0, base_hsla.lightness), + ) + } + + ColorChannel::HslLightness => { + let base_hsla: Hsla = base_color.into(); + ( + Color::hsl(base_hsla.hue, base_hsla.saturation, 0.0), + Color::hsl(base_hsla.hue, base_hsla.saturation, 0.5), + Color::hsl(base_hsla.hue, base_hsla.saturation, 1.0), + ) + } + + ColorChannel::Alpha => ( + base_color.with_alpha(0.), + base_color.with_alpha(0.5), + base_color.with_alpha(1.), + ), + } + } +} + +/// Used to store the color channels that we are not editing: the components of the color +/// that are constant for this slider. +#[derive(Component, Default, Clone)] +pub struct SliderBaseColor(pub Color); + +/// Slider template properties, passed to [`color_slider`] function. +pub struct ColorSliderProps { + /// Slider current value + pub value: f32, + /// On-change handler + pub on_change: Callback>, + /// Which color component we're editing + pub channel: ColorChannel, +} + +impl Default for ColorSliderProps { + fn default() -> Self { + Self { + value: 0.0, + on_change: Callback::Ignore, + channel: ColorChannel::Alpha, + } + } +} + +/// A color slider widget. +#[derive(Component, Default, Clone)] +#[require(CoreSlider, SliderBaseColor(Color::WHITE))] +pub struct ColorSlider { + /// Which channel is being edited by this slider. + pub channel: ColorChannel, +} + +/// Marker for the track +#[derive(Component, Default, Clone)] +struct ColorSliderTrack; + +/// Marker for the thumb +#[derive(Component, Default, Clone)] +struct ColorSliderThumb; + +/// 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 color_slider(props: ColorSliderProps, overrides: B) -> impl Bundle { + ( + Node { + display: Display::Flex, + flex_direction: FlexDirection::Row, + height: Val::Px(SLIDER_HEIGHT), + align_items: AlignItems::Stretch, + flex_grow: 1.0, + ..Default::default() + }, + CoreSlider { + on_change: props.on_change, + track_click: TrackClick::Snap, + }, + ColorSlider { + channel: props.channel.clone(), + }, + SliderValue(props.value), + props.channel.range(), + CursorIcon::System(bevy_window::SystemCursorIcon::Pointer), + TabIndex(0), + overrides, + children![ + // track + ( + Node { + position_type: PositionType::Absolute, + left: Val::Px(0.), + right: Val::Px(0.), + top: Val::Px(TRACK_PADDING), + bottom: Val::Px(TRACK_PADDING), + ..Default::default() + }, + RoundedCorners::All.to_border_radius(TRACK_RADIUS), + ColorSliderTrack, + AlphaPattern, + MaterialNode::(Handle::default()), + children![ + // Left endcap + ( + Node { + width: Val::Px(THUMB_SIZE * 0.5), + ..Default::default() + }, + RoundedCorners::Left.to_border_radius(TRACK_RADIUS), + BackgroundColor(palette::X_AXIS), + ), + // Track with gradient + ( + Node { + flex_grow: 1.0, + ..Default::default() + }, + 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(100.)), + ], + color_space: InterpolationColorSpace::Srgb, + })]), + ZIndex(1), + children![( + Node { + position_type: PositionType::Absolute, + left: Val::Percent(0.), + top: Val::Percent(50.), + width: Val::Px(THUMB_SIZE), + height: Val::Px(THUMB_SIZE), + border: UiRect::all(Val::Px(2.0)), + ..Default::default() + }, + CoreSliderThumb, + ColorSliderThumb, + BorderRadius::MAX, + BorderColor::all(palette::WHITE), + Outline { + width: Val::Px(1.), + offset: Val::Px(0.), + color: palette::BLACK + }, + UiTransform::from_translation(Val2::new( + Val::Percent(-50.0), + Val::Percent(-50.0), + )) + )] + ), + // Right endcap + ( + Node { + width: Val::Px(THUMB_SIZE * 0.5), + ..Default::default() + }, + RoundedCorners::Right.to_border_radius(TRACK_RADIUS), + BackgroundColor(palette::Z_AXIS), + ), + ] + ), + ], + ) +} + +fn update_slider_pos( + mut q_sliders: Query< + (Entity, &SliderValue, &SliderRange), + ( + With, + Or<(Changed, Changed)>, + ), + >, + q_children: Query<&Children>, + mut q_slider_thumb: Query<&mut Node, With>, +) { + for (slider_ent, value, range) in q_sliders.iter_mut() { + for child in q_children.iter_descendants(slider_ent) { + if let Ok(mut thumb_node) = q_slider_thumb.get_mut(child) { + thumb_node.left = Val::Percent(range.thumb_position(value.0) * 100.0); + } + } + } +} + +fn update_track_color( + mut q_sliders: Query<(Entity, &ColorSlider, &SliderBaseColor), Changed>, + q_children: Query<&Children>, + q_track: Query<(), With>, + mut q_background: Query<&mut BackgroundColor>, + mut q_gradient: Query<&mut BackgroundGradient>, +) { + for (slider_ent, slider, SliderBaseColor(base_color)) in q_sliders.iter_mut() { + let (start, middle, end) = slider.channel.gradient_ends(*base_color); + if let Some(track_ent) = q_children + .iter_descendants(slider_ent) + .find(|ent| q_track.contains(*ent)) + { + let Ok(track_children) = q_children.get(track_ent) else { + continue; + }; + + if let Ok(mut cap_bg) = q_background.get_mut(track_children[0]) { + cap_bg.0 = start; + } + + if let Ok(mut gradient) = q_gradient.get_mut(track_children[1]) { + if let [Gradient::Linear(linear_gradient)] = &mut gradient.0[..] { + linear_gradient.stops[0].color = start; + linear_gradient.stops[1].color = middle; + linear_gradient.stops[2].color = end; + linear_gradient.color_space = match slider.channel { + ColorChannel::Red | ColorChannel::Green | ColorChannel::Blue => { + InterpolationColorSpace::Srgb + } + ColorChannel::HslHue + | ColorChannel::HslLightness + | ColorChannel::HslSaturation => InterpolationColorSpace::Hsl, + ColorChannel::Alpha => match base_color { + Color::Srgba(_) => InterpolationColorSpace::Srgb, + Color::LinearRgba(_) => InterpolationColorSpace::LinearRgb, + Color::Oklaba(_) => InterpolationColorSpace::OkLab, + Color::Oklcha(_) => InterpolationColorSpace::OkLchLong, + Color::Hsla(_) | Color::Hsva(_) => InterpolationColorSpace::Hsl, + _ => { + warn_once!( + "Unsupported color space for ColorSlider: {:?}", + base_color + ); + InterpolationColorSpace::Srgb + } + }, + }; + } + } + + if let Ok(mut cap_bg) = q_background.get_mut(track_children[2]) { + cap_bg.0 = end; + } + } + } +} + +/// Plugin which registers the systems for updating the slider styles. +pub struct ColorSliderPlugin; + +impl Plugin for ColorSliderPlugin { + fn build(&self, app: &mut bevy_app::App) { + app.add_systems( + PreUpdate, + (update_slider_pos, update_track_color).in_set(PickingSystems::Last), + ); + } +} diff --git a/crates/bevy_feathers/src/controls/color_swatch.rs b/crates/bevy_feathers/src/controls/color_swatch.rs new file mode 100644 index 0000000000000..b43497805f6cd --- /dev/null +++ b/crates/bevy_feathers/src/controls/color_swatch.rs @@ -0,0 +1,53 @@ +use bevy_asset::Handle; +use bevy_color::Alpha; +use bevy_ecs::{bundle::Bundle, children, component::Component, spawn::SpawnRelated}; +use bevy_ui::{BackgroundColor, BorderRadius, Node, PositionType, Val}; +use bevy_ui_render::ui_material::MaterialNode; + +use crate::{ + alpha_pattern::{AlphaPattern, AlphaPatternMaterial}, + constants::size, + palette, +}; + +/// Marker identifying a color swatch. +#[derive(Component, Default, Clone)] +pub struct ColorSwatch; + +/// Marker identifying the color swatch foreground, the piece that actually displays the color +/// in front of the alpha pattern. This exists so that users can reach in and change the color +/// dynamically. +#[derive(Component, Default, Clone)] +pub struct ColorSwatchFg; + +/// Template function to spawn a color swatch. +/// +/// # Arguments +/// * `overrides` - a bundle of components that are merged in with the normal swatch components. +pub fn color_swatch(overrides: B) -> impl Bundle { + ( + Node { + height: size::ROW_HEIGHT, + min_width: size::ROW_HEIGHT, + ..Default::default() + }, + ColorSwatch, + AlphaPattern, + MaterialNode::(Handle::default()), + BorderRadius::all(Val::Px(5.0)), + overrides, + children![( + Node { + position_type: PositionType::Absolute, + left: Val::Px(0.), + top: Val::Px(0.), + bottom: Val::Px(0.), + right: Val::Px(0.), + ..Default::default() + }, + ColorSwatchFg, + BackgroundColor(palette::ACCENT.with_alpha(0.5)), + BorderRadius::all(Val::Px(5.0)) + ),], + ) +} diff --git a/crates/bevy_feathers/src/controls/mod.rs b/crates/bevy_feathers/src/controls/mod.rs index ecad39707b925..209fd8e32dc0d 100644 --- a/crates/bevy_feathers/src/controls/mod.rs +++ b/crates/bevy_feathers/src/controls/mod.rs @@ -3,24 +3,34 @@ use bevy_app::Plugin; mod button; mod checkbox; +mod color_slider; +mod color_swatch; mod radio; mod slider; mod toggle_switch; pub use button::{button, ButtonPlugin, ButtonProps, ButtonVariant}; pub use checkbox::{checkbox, CheckboxPlugin, CheckboxProps}; +pub use color_slider::{ + color_slider, ColorChannel, ColorSlider, ColorSliderPlugin, ColorSliderProps, SliderBaseColor, +}; +pub use color_swatch::{color_swatch, ColorSwatch, ColorSwatchFg}; pub use radio::{radio, RadioPlugin}; pub use slider::{slider, SliderPlugin, SliderProps}; pub use toggle_switch::{toggle_switch, ToggleSwitchPlugin, ToggleSwitchProps}; +use crate::alpha_pattern::AlphaPatternPlugin; + /// 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(( + AlphaPatternPlugin, ButtonPlugin, CheckboxPlugin, + ColorSliderPlugin, RadioPlugin, SliderPlugin, ToggleSwitchPlugin, diff --git a/crates/bevy_feathers/src/lib.rs b/crates/bevy_feathers/src/lib.rs index ab02304a85b30..88ae6c03276e5 100644 --- a/crates/bevy_feathers/src/lib.rs +++ b/crates/bevy_feathers/src/lib.rs @@ -22,14 +22,17 @@ use bevy_app::{HierarchyPropagatePlugin, Plugin, PostUpdate}; use bevy_asset::embedded_asset; use bevy_ecs::query::With; use bevy_text::{TextColor, TextFont}; +use bevy_ui_render::UiMaterialPlugin; use bevy_winit::cursor::CursorIcon; use crate::{ + alpha_pattern::{AlphaPatternMaterial, AlphaPatternResource}, controls::ControlsPlugin, cursor::{CursorIconPlugin, DefaultCursorIcon}, theme::{ThemedText, UiTheme}, }; +mod alpha_pattern; pub mod constants; pub mod controls; pub mod cursor; @@ -48,17 +51,22 @@ impl Plugin for FeathersPlugin { fn build(&self, app: &mut bevy_app::App) { app.init_resource::(); + // Embedded font 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"); + // Embedded shader + embedded_asset!(app, "assets/shaders/alpha_pattern.wgsl"); + app.add_plugins(( ControlsPlugin, CursorIconPlugin, HierarchyPropagatePlugin::>::default(), HierarchyPropagatePlugin::>::default(), + UiMaterialPlugin::::default(), )); app.insert_resource(DefaultCursorIcon(CursorIcon::System( @@ -70,5 +78,7 @@ impl Plugin for FeathersPlugin { .add_observer(theme::on_changed_border) .add_observer(theme::on_changed_font_color) .add_observer(font_styles::on_changed_font); + + app.init_resource::(); } } diff --git a/examples/ui/feathers.rs b/examples/ui/feathers.rs index ae6ec31f4c009..6f57648457845 100644 --- a/examples/ui/feathers.rs +++ b/examples/ui/feathers.rs @@ -1,11 +1,15 @@ //! This example shows off the various Bevy Feathers widgets. use bevy::{ - core_widgets::{Callback, CoreRadio, CoreRadioGroup, CoreWidgetsPlugin, SliderStep}, + color::palettes, + core_widgets::{ + Callback, CoreRadio, CoreRadioGroup, CoreWidgetsPlugin, SliderStep, SliderValue, + }, feathers::{ controls::{ - button, checkbox, radio, slider, toggle_switch, ButtonProps, ButtonVariant, - CheckboxProps, SliderProps, ToggleSwitchProps, + button, checkbox, color_slider, color_swatch, radio, slider, toggle_switch, + ButtonProps, ButtonVariant, CheckboxProps, ColorChannel, ColorSlider, ColorSliderProps, + ColorSwatch, SliderBaseColor, SliderProps, ToggleSwitchProps, }, dark_theme::create_dark_theme, rounded_corners::RoundedCorners, @@ -21,6 +25,19 @@ use bevy::{ winit::WinitSettings, }; +/// 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, +} + fn main() { App::new() .add_plugins(( @@ -31,9 +48,14 @@ fn main() { FeathersPlugin, )) .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(), + }) // 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) + .add_systems(Update, update_colors) .run(); } @@ -58,6 +80,41 @@ fn demo_root(commands: &mut Commands) -> impl Bundle { }, ); + let change_red = + commands.register_system(|value: In, mut color: ResMut| { + color.rgb_color.red = *value; + }); + + let change_green = + commands.register_system(|value: In, mut color: ResMut| { + color.rgb_color.green = *value; + }); + + let change_blue = + commands.register_system(|value: In, mut color: ResMut| { + color.rgb_color.blue = *value; + }); + + let change_alpha = + commands.register_system(|value: In, mut color: ResMut| { + color.rgb_color.alpha = *value; + }); + + let change_hue = + commands.register_system(|value: In, mut color: ResMut| { + color.hsl_color.hue = *value; + }); + + let change_saturation = + commands.register_system(|value: In, mut color: ResMut| { + color.hsl_color.saturation = *value; + }); + + let change_lightness = + commands.register_system(|value: In, mut color: ResMut| { + color.hsl_color.lightness = *value; + }); + ( Node { width: Val::Percent(100.0), @@ -261,7 +318,146 @@ fn demo_root(commands: &mut Commands) -> impl Bundle { }, SliderStep(10.) ), + ( + 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, + on_change: Callback::System(change_red), + channel: ColorChannel::Red + }, + () + ), + color_slider( + ColorSliderProps { + value: 0.5, + on_change: Callback::System(change_green), + channel: ColorChannel::Green + }, + () + ), + color_slider( + ColorSliderProps { + value: 0.5, + on_change: Callback::System(change_blue), + channel: ColorChannel::Blue + }, + () + ), + color_slider( + ColorSliderProps { + value: 0.5, + on_change: Callback::System(change_alpha), + channel: ColorChannel::Alpha + }, + () + ), + ( + 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, + on_change: Callback::System(change_hue), + channel: ColorChannel::HslHue + }, + () + ), + color_slider( + ColorSliderProps { + value: 0.5, + on_change: Callback::System(change_saturation), + channel: ColorChannel::HslSaturation + }, + () + ), + color_slider( + ColorSliderProps { + value: 0.5, + on_change: Callback::System(change_lightness), + channel: ColorChannel::HslLightness + }, + () + ) ] ),], ) } + +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(), + })); + } + } +}