-
-
Notifications
You must be signed in to change notification settings - Fork 4.2k
Color plane widget. #21743
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
viridia
wants to merge
6
commits into
bevyengine:main
Choose a base branch
from
viridia:color_plane
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+470
−16
Open
Color plane widget. #21743
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
6429878
Color plane widget.
viridia 497caac
Typos.
viridia 321128d
Release note.
viridia 84384a5
Update crates/bevy_feathers/src/controls/color_plane.rs
viridia 803cd31
Fix incorrect comment.
viridia b369525
Update crates/bevy_feathers/src/assets/shaders/color_plane.wgsl
viridia File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| // This shader draws the color plane in various color spaces. | ||
| #import bevy_ui::ui_vertex_output::UiVertexOutput | ||
| #import bevy_ui_render::color_space::{ | ||
| srgb_to_linear_rgb, | ||
| hsl_to_linear_rgb, | ||
| } | ||
|
|
||
| @group(1) @binding(0) var<uniform> fixed_channel: f32; | ||
|
|
||
| @fragment | ||
| fn fragment(in: UiVertexOutput) -> @location(0) vec4<f32> { | ||
| let uv = in.uv; | ||
| #ifdef PLANE_RG | ||
| return vec4(srgb_to_linear_rgb(vec3(uv.x, uv.y, fixed_channel)), 1.0); | ||
| #else ifdef PLANE_RB | ||
| return vec4(srgb_to_linear_rgb(vec3(uv.x, fixed_channel, uv.y)), 1.0); | ||
| #else ifdef PLANE_GB | ||
| return vec4(srgb_to_linear_rgb(vec3(fixed_channel, uv.x, uv.y)), 1.0); | ||
| #else ifdef PLANE_HS | ||
| return vec4(hsl_to_linear_rgb(vec3(uv.x, 1.0 - uv.y, fixed_channel)), 1.0); | ||
| #else ifdef PLANE_HL | ||
| return vec4(hsl_to_linear_rgb(vec3(uv.x, fixed_channel, 1.0 - uv.y)), 1.0); | ||
| #else | ||
| // Error color | ||
| return vec4(1.0, 0.0, 1.0, 1.0); | ||
| #endif | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,354 @@ | ||
| use bevy_app::{Plugin, PostUpdate}; | ||
| use bevy_asset::{Asset, Assets}; | ||
| use bevy_ecs::{ | ||
| bundle::Bundle, | ||
| children, | ||
| component::Component, | ||
| entity::Entity, | ||
| hierarchy::{ChildOf, Children}, | ||
| observer::On, | ||
| query::{Changed, Has, Or, With}, | ||
| reflect::ReflectComponent, | ||
| spawn::SpawnRelated, | ||
| system::{Commands, Query, Res, ResMut}, | ||
| }; | ||
| use bevy_math::{Vec2, Vec3}; | ||
| use bevy_picking::{ | ||
| events::{Cancel, Drag, DragEnd, DragStart, Pointer, Press}, | ||
| Pickable, | ||
| }; | ||
| use bevy_reflect::{prelude::ReflectDefault, Reflect, TypePath}; | ||
| use bevy_render::render_resource::AsBindGroup; | ||
| use bevy_shader::{ShaderDefVal, ShaderRef}; | ||
| use bevy_ui::{ | ||
| px, AlignSelf, BorderColor, BorderRadius, ComputedNode, ComputedUiRenderTargetInfo, Display, | ||
| InteractionDisabled, Node, Outline, PositionType, UiGlobalTransform, UiRect, UiScale, | ||
| UiTransform, Val, Val2, | ||
| }; | ||
| use bevy_ui_render::{prelude::UiMaterial, ui_material::MaterialNode, UiMaterialPlugin}; | ||
| use bevy_ui_widgets::ValueChange; | ||
|
|
||
| use crate::{cursor::EntityCursor, palette, theme::ThemeBackgroundColor, tokens}; | ||
|
|
||
| /// Marker identifying a color plane widget. | ||
| /// | ||
| /// The variant selects which view of the color pane is shown. | ||
| #[derive(Component, Default, Debug, Clone, Reflect, Copy, PartialEq, Eq, Hash)] | ||
| #[reflect(Component, Clone, Default)] | ||
| #[require(ColorPlaneDragState)] | ||
| pub enum ColorPlane { | ||
| /// Show red on the horizontal axis and green on the vertical. | ||
| RedGreen, | ||
| /// Show red on the horizontal axis and blue on the vertical. | ||
| RedBlue, | ||
| /// Show green on the horizontal axis and blue on the vertical. | ||
| GreenBlue, | ||
| /// Show hue on the horizontal axis and saturation on the vertical. | ||
| HueSaturation, | ||
| /// Show hue on the horizontal axis and lightness on the vertical. | ||
| #[default] | ||
| HueLightness, | ||
| } | ||
|
|
||
| /// Component that contains the two components of the selected color, as well as the "z" value. | ||
| /// The x and y values determine the placement of the thumb element, while the z value controls | ||
| /// the background gradient. | ||
| #[derive(Component, Default, Clone, Reflect)] | ||
| #[reflect(Component, Clone, Default)] | ||
| pub struct ColorPlaneValue(pub Vec3); | ||
|
|
||
| /// Marker identifying the inner element of the color plane. | ||
| #[derive(Component, Default, Clone, Reflect)] | ||
| #[reflect(Component, Clone, Default)] | ||
| struct ColorPlaneInner; | ||
|
|
||
| /// Marker identifying the thumb element of the color plane. | ||
| #[derive(Component, Default, Clone, Reflect)] | ||
| #[reflect(Component, Clone, Default)] | ||
| struct ColorPlaneThumb; | ||
|
|
||
| /// Component used to manage the state of a slider during dragging. | ||
| #[derive(Component, Default, Reflect)] | ||
| #[reflect(Component)] | ||
| struct ColorPlaneDragState(bool); | ||
|
|
||
| #[repr(C)] | ||
| #[derive(Eq, PartialEq, Hash, Copy, Clone)] | ||
| struct ColorPlaneMaterialKey { | ||
| plane: ColorPlane, | ||
| } | ||
|
|
||
| #[derive(AsBindGroup, Asset, TypePath, Default, Debug, Clone)] | ||
| #[bind_group_data(ColorPlaneMaterialKey)] | ||
| struct ColorPlaneMaterial { | ||
| plane: ColorPlane, | ||
|
|
||
| #[uniform(0)] | ||
| fixed_channel: f32, | ||
| } | ||
|
|
||
| impl From<&ColorPlaneMaterial> for ColorPlaneMaterialKey { | ||
| fn from(material: &ColorPlaneMaterial) -> Self { | ||
| Self { | ||
| plane: material.plane, | ||
| } | ||
| } | ||
| } | ||
|
|
||
| impl UiMaterial for ColorPlaneMaterial { | ||
| fn fragment_shader() -> ShaderRef { | ||
| "embedded://bevy_feathers/assets/shaders/color_plane.wgsl".into() | ||
| } | ||
|
|
||
| fn specialize( | ||
| descriptor: &mut bevy_render::render_resource::RenderPipelineDescriptor, | ||
| key: bevy_ui_render::prelude::UiMaterialKey<Self>, | ||
| ) { | ||
| let plane_def = match key.bind_group_data.plane { | ||
| ColorPlane::RedGreen => "PLANE_RG", | ||
| ColorPlane::RedBlue => "PLANE_RB", | ||
| ColorPlane::GreenBlue => "PLANE_GB", | ||
| ColorPlane::HueSaturation => "PLANE_HS", | ||
| ColorPlane::HueLightness => "PLANE_HL", | ||
| }; | ||
| descriptor.fragment.as_mut().unwrap().shader_defs = | ||
| vec![ShaderDefVal::Bool(plane_def.into(), true)]; | ||
| } | ||
| } | ||
|
|
||
| /// Template function to spawn a "color plane", which is a 2d picker that allows selecting two | ||
| /// components of a color space. | ||
| /// | ||
| /// The control emits a [`ValueChange<Vec2>`] representing the current x and y values, ranging | ||
| /// from 0 to 1. The control accepts a [`Vec3`] input value, where the third component ('z') | ||
| /// is used to provide the fixed constant channel for the background gradient. | ||
| /// | ||
| /// The control does not do any color space conversions internally, other than the shader code | ||
| /// for displaying gradients. Avoiding excess conversions helps avoid gimble-lock problems when | ||
| /// implementing a color picker for cylindrical color spaces such as HSL. | ||
| /// | ||
| /// # Arguments | ||
| /// * `overrides` - a bundle of components that are merged in with the normal swatch components. | ||
| pub fn color_plane<B: Bundle>(plane: ColorPlane, overrides: B) -> impl Bundle { | ||
| ( | ||
| Node { | ||
| display: Display::Flex, | ||
| min_height: px(100.0), | ||
| align_self: AlignSelf::Stretch, | ||
| padding: UiRect::all(px(4)), | ||
| ..Default::default() | ||
| }, | ||
| plane, | ||
| ColorPlaneValue::default(), | ||
| ThemeBackgroundColor(tokens::COLOR_PLANE_BG), | ||
| BorderRadius::all(px(5)), | ||
| EntityCursor::System(bevy_window::SystemCursorIcon::Crosshair), | ||
| overrides, | ||
| children![( | ||
| Node { | ||
| align_self: AlignSelf::Stretch, | ||
| flex_grow: 1.0, | ||
| ..Default::default() | ||
| }, | ||
| ColorPlaneInner, | ||
| children![( | ||
| Node { | ||
| position_type: PositionType::Absolute, | ||
| left: Val::Percent(0.), | ||
| top: Val::Percent(0.), | ||
| width: px(10), | ||
| height: px(10), | ||
| border: UiRect::all(Val::Px(1.0)), | ||
| ..Default::default() | ||
| }, | ||
| ColorPlaneThumb, | ||
| BorderRadius::MAX, | ||
| BorderColor::all(palette::WHITE), | ||
| Outline { | ||
| width: Val::Px(1.), | ||
| offset: Val::Px(0.), | ||
| color: palette::BLACK | ||
| }, | ||
| Pickable::IGNORE, | ||
| UiTransform::from_translation(Val2::new(Val::Percent(-50.0), Val::Percent(-50.0),)) | ||
| )], | ||
| ),], | ||
| ) | ||
| } | ||
|
|
||
| fn update_plane_color( | ||
| q_color_plane: Query< | ||
| (Entity, &ColorPlane, &ColorPlaneValue), | ||
| Or<(Changed<ColorPlane>, Changed<ColorPlaneValue>)>, | ||
| >, | ||
| q_children: Query<&Children>, | ||
| q_material_node: Query<&MaterialNode<ColorPlaneMaterial>>, | ||
| mut q_node: Query<&mut Node>, | ||
| mut r_materials: ResMut<Assets<ColorPlaneMaterial>>, | ||
| mut commands: Commands, | ||
| ) { | ||
| for (plane_ent, plane, plane_value) in q_color_plane.iter() { | ||
| // Find the inner entity | ||
| let Ok(children) = q_children.get(plane_ent) else { | ||
| continue; | ||
| }; | ||
| let Some(inner_ent) = children.first() else { | ||
| continue; | ||
| }; | ||
|
|
||
| if let Ok(material_node) = q_material_node.get(*inner_ent) { | ||
| // Node component exists, update it | ||
| if let Some(material) = r_materials.get_mut(material_node.id()) { | ||
| // Update properties | ||
| material.plane = *plane; | ||
| material.fixed_channel = plane_value.0.z; | ||
| } | ||
| } else { | ||
| // Insert new node component | ||
| let material = r_materials.add(ColorPlaneMaterial { | ||
| plane: *plane, | ||
| fixed_channel: plane_value.0.z, | ||
| }); | ||
| commands.entity(*inner_ent).insert(MaterialNode(material)); | ||
| } | ||
|
|
||
| // Find the thumb. | ||
| let Ok(children_inner) = q_children.get(*inner_ent) else { | ||
| continue; | ||
| }; | ||
| let Some(thumb_ent) = children_inner.first() else { | ||
| continue; | ||
| }; | ||
|
|
||
| let Ok(mut thumb_node) = q_node.get_mut(*thumb_ent) else { | ||
| continue; | ||
| }; | ||
|
|
||
| thumb_node.left = Val::Percent(plane_value.0.x * 100.0); | ||
| thumb_node.top = Val::Percent(plane_value.0.y * 100.0); | ||
| } | ||
| } | ||
|
|
||
| fn on_pointer_press( | ||
| mut press: On<Pointer<Press>>, | ||
| q_color_planes: Query<Has<InteractionDisabled>, With<ColorPlane>>, | ||
| q_color_plane_inner: Query< | ||
| ( | ||
| &ComputedNode, | ||
| &ComputedUiRenderTargetInfo, | ||
| &UiGlobalTransform, | ||
| &ChildOf, | ||
| ), | ||
| With<ColorPlaneInner>, | ||
| >, | ||
| ui_scale: Res<UiScale>, | ||
| mut commands: Commands, | ||
| ) { | ||
| if let Ok((node, node_target, transform, parent)) = q_color_plane_inner.get(press.entity) | ||
| && let Ok(disabled) = q_color_planes.get(parent.0) | ||
| { | ||
| press.propagate(false); | ||
| if !disabled { | ||
| let local_pos = transform.try_inverse().unwrap().transform_point2( | ||
| press.pointer_location.position * node_target.scale_factor() / ui_scale.0, | ||
| ); | ||
| let pos = local_pos / node.size() + Vec2::splat(0.5); | ||
| let new_value = pos.clamp(Vec2::ZERO, Vec2::ONE); | ||
| commands.trigger(ValueChange { | ||
| source: parent.0, | ||
| value: new_value, | ||
| }); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| fn on_drag_start( | ||
| mut drag_start: On<Pointer<DragStart>>, | ||
| mut q_color_planes: Query< | ||
| (&mut ColorPlaneDragState, Has<InteractionDisabled>), | ||
| With<ColorPlane>, | ||
| >, | ||
| q_color_plane_inner: Query<&ChildOf, With<ColorPlaneInner>>, | ||
| ) { | ||
| if let Ok(parent) = q_color_plane_inner.get(drag_start.entity) | ||
| && let Ok((mut state, disabled)) = q_color_planes.get_mut(parent.0) | ||
| { | ||
| drag_start.propagate(false); | ||
| if !disabled { | ||
| state.0 = true; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| fn on_drag( | ||
| mut drag: On<Pointer<Drag>>, | ||
| q_color_planes: Query<(&ColorPlaneDragState, Has<InteractionDisabled>), With<ColorPlane>>, | ||
| q_color_plane_inner: Query< | ||
| ( | ||
| &ComputedNode, | ||
| &ComputedUiRenderTargetInfo, | ||
| &UiGlobalTransform, | ||
| &ChildOf, | ||
| ), | ||
| With<ColorPlaneInner>, | ||
| >, | ||
| ui_scale: Res<UiScale>, | ||
| mut commands: Commands, | ||
| ) { | ||
| if let Ok((node, node_target, transform, parent)) = q_color_plane_inner.get(drag.entity) | ||
| && let Ok((state, disabled)) = q_color_planes.get(parent.0) | ||
| { | ||
| drag.propagate(false); | ||
| if state.0 && !disabled { | ||
| let local_pos = transform.try_inverse().unwrap().transform_point2( | ||
| drag.pointer_location.position * node_target.scale_factor() / ui_scale.0, | ||
| ); | ||
| let pos = local_pos / node.size() + Vec2::splat(0.5); | ||
| let new_value = pos.clamp(Vec2::ZERO, Vec2::ONE); | ||
| commands.trigger(ValueChange { | ||
| source: parent.0, | ||
| value: new_value, | ||
| }); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| fn on_drag_end( | ||
| mut drag_end: On<Pointer<DragEnd>>, | ||
| mut q_color_planes: Query<&mut ColorPlaneDragState, With<ColorPlane>>, | ||
| q_color_plane_inner: Query<&ChildOf, With<ColorPlaneInner>>, | ||
| ) { | ||
| if let Ok(parent) = q_color_plane_inner.get(drag_end.entity) | ||
| && let Ok(mut state) = q_color_planes.get_mut(parent.0) | ||
| { | ||
| drag_end.propagate(false); | ||
| state.0 = false; | ||
| } | ||
| } | ||
|
|
||
| fn on_drag_cancel( | ||
| drag_cancel: On<Pointer<Cancel>>, | ||
| mut q_color_planes: Query<&mut ColorPlaneDragState, With<ColorPlane>>, | ||
| q_color_plane_inner: Query<&ChildOf, With<ColorPlaneInner>>, | ||
| ) { | ||
| if let Ok(parent) = q_color_plane_inner.get(drag_cancel.entity) | ||
| && let Ok(mut state) = q_color_planes.get_mut(parent.0) | ||
| { | ||
| state.0 = false; | ||
| } | ||
| } | ||
|
|
||
| /// Plugin which registers the observers for updating the swatch color. | ||
| pub struct ColorPlanePlugin; | ||
|
|
||
| impl Plugin for ColorPlanePlugin { | ||
| fn build(&self, app: &mut bevy_app::App) { | ||
| app.add_plugins(UiMaterialPlugin::<ColorPlaneMaterial>::default()); | ||
| app.add_systems(PostUpdate, update_plane_color); | ||
| app.add_observer(on_pointer_press) | ||
| .add_observer(on_drag_start) | ||
| .add_observer(on_drag) | ||
| .add_observer(on_drag_end) | ||
| .add_observer(on_drag_cancel); | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.