Merge d5b2e59d33
into 0ee937784e
This commit is contained in:
commit
4901ec5346
@ -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" }
|
||||
|
||||
|
59
crates/bevy_feathers/src/alpha_pattern.rs
Normal file
59
crates/bevy_feathers/src/alpha_pattern.rs
Normal file
@ -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<AlphaPatternMaterial>);
|
||||
|
||||
impl FromWorld for AlphaPatternResource {
|
||||
fn from_world(world: &mut bevy_ecs::world::World) -> Self {
|
||||
let mut ui_materials = world
|
||||
.get_resource_mut::<Assets<AlphaPatternMaterial>>()
|
||||
.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<Add, AlphaPattern>,
|
||||
mut q_swatch: Query<&mut MaterialNode<AlphaPatternMaterial>>,
|
||||
r_material: Res<AlphaPatternResource>,
|
||||
) {
|
||||
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);
|
||||
}
|
||||
}
|
39
crates/bevy_feathers/src/assets/shaders/alpha_pattern.wgsl
Normal file
39
crates/bevy_feathers/src/assets/shaders/alpha_pattern.wgsl
Normal file
@ -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<f32> {
|
||||
let uv = (in.uv - vec2<f32>(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<f32>(0.2, 0.2, 0.2), vec3<f32>(0.6, 0.6, 0.6), check);
|
||||
|
||||
let size = vec2<f32>(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<f32>(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<f32>, size: vec2<f32>, corner_radii: vec4<f32>) -> 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;
|
||||
}
|
378
crates/bevy_feathers/src/controls/color_slider.rs
Normal file
378
crates/bevy_feathers/src/controls/color_slider.rs
Normal file
@ -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<In<f32>>,
|
||||
/// 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<B: Bundle>(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::<AlphaPatternMaterial>(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<ColorSlider>,
|
||||
Or<(Changed<SliderValue>, Changed<SliderRange>)>,
|
||||
),
|
||||
>,
|
||||
q_children: Query<&Children>,
|
||||
mut q_slider_thumb: Query<&mut Node, With<ColorSliderThumb>>,
|
||||
) {
|
||||
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<SliderBaseColor>>,
|
||||
q_children: Query<&Children>,
|
||||
q_track: Query<(), With<ColorSliderTrack>>,
|
||||
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),
|
||||
);
|
||||
}
|
||||
}
|
53
crates/bevy_feathers/src/controls/color_swatch.rs
Normal file
53
crates/bevy_feathers/src/controls/color_swatch.rs
Normal file
@ -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<B: Bundle>(overrides: B) -> impl Bundle {
|
||||
(
|
||||
Node {
|
||||
height: size::ROW_HEIGHT,
|
||||
min_width: size::ROW_HEIGHT,
|
||||
..Default::default()
|
||||
},
|
||||
ColorSwatch,
|
||||
AlphaPattern,
|
||||
MaterialNode::<AlphaPatternMaterial>(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))
|
||||
),],
|
||||
)
|
||||
}
|
@ -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,
|
||||
|
@ -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::<UiTheme>();
|
||||
|
||||
// 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::<TextColor, With<ThemedText>>::default(),
|
||||
HierarchyPropagatePlugin::<TextFont, With<ThemedText>>::default(),
|
||||
UiMaterialPlugin::<AlphaPatternMaterial>::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::<AlphaPatternResource>();
|
||||
}
|
||||
}
|
||||
|
@ -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<f32>, mut color: ResMut<DemoWidgetStates>| {
|
||||
color.rgb_color.red = *value;
|
||||
});
|
||||
|
||||
let change_green =
|
||||
commands.register_system(|value: In<f32>, mut color: ResMut<DemoWidgetStates>| {
|
||||
color.rgb_color.green = *value;
|
||||
});
|
||||
|
||||
let change_blue =
|
||||
commands.register_system(|value: In<f32>, mut color: ResMut<DemoWidgetStates>| {
|
||||
color.rgb_color.blue = *value;
|
||||
});
|
||||
|
||||
let change_alpha =
|
||||
commands.register_system(|value: In<f32>, mut color: ResMut<DemoWidgetStates>| {
|
||||
color.rgb_color.alpha = *value;
|
||||
});
|
||||
|
||||
let change_hue =
|
||||
commands.register_system(|value: In<f32>, mut color: ResMut<DemoWidgetStates>| {
|
||||
color.hsl_color.hue = *value;
|
||||
});
|
||||
|
||||
let change_saturation =
|
||||
commands.register_system(|value: In<f32>, mut color: ResMut<DemoWidgetStates>| {
|
||||
color.hsl_color.saturation = *value;
|
||||
});
|
||||
|
||||
let change_lightness =
|
||||
commands.register_system(|value: In<f32>, mut color: ResMut<DemoWidgetStates>| {
|
||||
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<DemoWidgetStates>,
|
||||
mut sliders: Query<(Entity, &ColorSlider, &mut SliderBaseColor)>,
|
||||
swatches: Query<(&SwatchType, &Children), With<ColorSwatch>>,
|
||||
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(),
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user