mod materials;
mod presets;
use bevy::color::palettes::tailwind;
use bevy::input_focus::InputFocus;
use bevy::picking::events::{Press, Release};
use bevy::picking::hover::Hovered;
use bevy::picking::pointer::PointerButton;
use bevy::picking::prelude::Pickable;
use bevy::prelude::*;
use bevy::reflect::Typed;
use bevy::ui::UiGlobalTransform;
use bevy::window::SystemCursorIcon;
use bevy_sprinkles::prelude::{Curve, CurveEasing, CurveMode, CurvePoint, CurveTexture};
use inflector::Inflector;
use materials::{CurveMaterial, MAX_POINTS};
use presets::CURVE_PRESETS;
use crate::ui::icons::{ICON_ARROW_LEFT_RIGHT, ICON_FCURVE, ICON_MORE};
use crate::ui::tokens::{
BACKGROUND_COLOR, BORDER_COLOR, CORNER_RADIUS_LG, FONT_PATH, PRIMARY_COLOR, TEXT_MUTED_COLOR,
TEXT_SIZE_SM,
};
use crate::ui::widgets::button::{
ButtonClickEvent, ButtonProps, ButtonVariant, IconButtonProps, button, icon_button,
set_button_variant,
};
use crate::ui::widgets::combobox::{
ComboBoxChangeEvent, ComboBoxConfig, ComboBoxOptionData, combobox_with_label,
combobox_with_selected,
};
use crate::ui::widgets::cursor::{ActiveCursor, HoverCursor};
use crate::ui::widgets::popover::{
EditorPopover, PopoverHeaderProps, PopoverPlacement, PopoverProps, PopoverTracker,
activate_trigger, deactivate_trigger, popover, popover_content, popover_header,
};
use crate::ui::widgets::text_edit::EditorTextEdit;
use crate::ui::widgets::utils::is_descendant_of;
use crate::ui::widgets::vector_edit::{
EditorVectorEdit, VectorEditProps, VectorSize, VectorSuffixes, vector_edit,
};
use bevy_ui_text_input::TextInputQueue;
use bevy_ui_text_input::actions::{TextInputAction, TextInputEdit};
#[derive(Clone, Copy, PartialEq, Default)]
pub(crate) enum CurveAxis {
#[default]
X,
Y,
Z,
}
impl CurveAxis {
const ALL: [CurveAxis; 3] = [CurveAxis::X, CurveAxis::Y, CurveAxis::Z];
fn label(self) -> &'static str {
match self {
CurveAxis::X => "X",
CurveAxis::Y => "Y",
CurveAxis::Z => "Z",
}
}
fn channel_index(self) -> usize {
match self {
CurveAxis::X => 0,
CurveAxis::Y => 1,
CurveAxis::Z => 2,
}
}
fn color(self) -> Srgba {
match self {
CurveAxis::X => tailwind::RED_600,
CurveAxis::Y => tailwind::GREEN_600,
CurveAxis::Z => tailwind::SKY_600,
}
}
}
const CANVAS_SIZE: f32 = 232.0;
const CONTENT_PADDING: f32 = 12.0;
const POINT_HANDLE_SIZE: f32 = 12.0;
const TENSION_HANDLE_SIZE: f32 = 10.0;
const HANDLE_BORDER: f32 = 1.0;
const DRAG_SNAP_STEP: f64 = 0.01;
const CURVE_ALPHA: f32 = 0.8;
const FILL_ALPHA: f32 = 0.2;
pub fn plugin(app: &mut App) {
app.add_plugins(UiMaterialPlugin::<CurveMaterial>::default())
.add_observer(handle_trigger_click)
.add_observer(handle_preset_change)
.add_observer(handle_axes_mode_change)
.add_observer(handle_axis_tab_click)
.add_observer(handle_flip_click)
.add_observer(handle_point_mode_change)
.add_systems(
Update,
(
setup_curve_edit,
setup_curve_edit_content,
update_curve_visuals,
respawn_handles_on_point_change,
update_handle_colors,
sync_trigger_label,
sync_range_inputs_to_state,
handle_range_blur,
handle_canvas_right_click,
handle_point_right_click,
handle_tension_right_click,
sync_axis_tabs_visibility,
sync_axis_tab_styles,
sync_axis_tab_text_alignment,
sync_axes_combobox_to_state,
),
);
}
#[derive(Component)]
pub struct EditorCurveEdit;
#[derive(Component, Clone, Default)]
pub struct CurveEditState {
pub curve: CurveTexture,
pub(crate) active_axis: CurveAxis,
}
impl CurveEditState {
pub fn from_curve(curve: CurveTexture) -> Self {
Self {
curve,
active_axis: CurveAxis::default(),
}
}
pub fn set_curve(&mut self, curve: CurveTexture) {
self.curve = curve;
}
pub fn mark_custom(&mut self) {
self.curve.name = None;
}
pub fn label(&self) -> &str {
if self.is_per_axis() {
return "Per axis curves";
}
self.curve.name.as_deref().unwrap_or("Curve")
}
pub fn is_per_axis(&self) -> bool {
self.curve.y.is_some() || self.curve.z.is_some()
}
pub fn accent_color(&self) -> Srgba {
if self.is_per_axis() {
self.active_axis.color()
} else {
PRIMARY_COLOR
}
}
pub fn curve_color(&self) -> Srgba {
self.accent_color().with_alpha(CURVE_ALPHA)
}
pub fn fill_color(&self) -> Srgba {
self.accent_color().with_alpha(FILL_ALPHA)
}
pub fn active_curve(&self) -> &Curve {
match self.active_axis {
CurveAxis::Y => self.curve.y.as_ref().unwrap_or(&self.curve.x),
CurveAxis::Z => self.curve.z.as_ref().unwrap_or(&self.curve.x),
CurveAxis::X => &self.curve.x,
}
}
pub fn active_curve_mut(&mut self) -> &mut Curve {
match self.active_axis {
CurveAxis::Y if self.curve.y.is_some() => self.curve.y.as_mut().unwrap(),
CurveAxis::Z if self.curve.z.is_some() => self.curve.z.as_mut().unwrap(),
_ => &mut self.curve.x,
}
}
}
#[derive(EntityEvent)]
pub struct CurveEditChangeEvent {
pub entity: Entity,
}
#[derive(EntityEvent)]
pub struct CurveEditCommitEvent {
pub entity: Entity,
pub curve: CurveTexture,
}
fn trigger_curve_events(commands: &mut Commands, entity: Entity, curve: &CurveTexture) {
commands.trigger(CurveEditChangeEvent { entity });
commands.trigger(CurveEditCommitEvent {
entity,
curve: curve.clone(),
});
}
#[derive(Default)]
pub struct CurveEditProps {
pub curve: Option<CurveTexture>,
pub label: Option<String>,
}
impl CurveEditProps {
pub fn new() -> Self {
Self::default()
}
pub fn with_label(mut self, label: impl Into<String>) -> Self {
self.label = Some(label.into());
self
}
}
#[derive(Component, Default)]
pub struct CurveEditLabel(pub Option<String>);
pub fn curve_edit(props: CurveEditProps) -> impl Bundle {
let CurveEditProps { curve, label } = props;
let state = curve.map(CurveEditState::from_curve).unwrap_or_default();
(
EditorCurveEdit,
CurveEditLabel(label),
state,
PopoverTracker::default(),
Node {
flex_direction: FlexDirection::Column,
row_gap: px(3.0),
flex_grow: 1.0,
flex_shrink: 1.0,
flex_basis: px(0.0),
..default()
},
)
}
#[derive(Component)]
struct CurveEditTrigger(Entity);
#[derive(Component)]
struct CurveEditPopover(Entity);
#[derive(Component)]
struct CurveEditContent(Entity);
#[derive(Component)]
struct CurveCanvas {
curve_edit: Entity,
point_count: usize,
active_axis: CurveAxis,
is_per_axis: bool,
}
#[derive(Component)]
struct CurveMaterialNode(Entity);
#[derive(Component)]
struct PresetComboBox(Entity);
#[derive(Component)]
struct FlipButton(Entity);
#[derive(Component)]
struct AxesComboBox(Entity);
#[derive(Component)]
struct AxisTabs(Entity);
#[derive(Component)]
struct AxisTabButton {
curve_edit: Entity,
axis: CurveAxis,
}
#[derive(Component)]
struct RangeEdit(Entity);
#[derive(Component)]
struct PointHandle {
curve_edit: Entity,
canvas: Entity,
index: usize,
}
#[derive(Component)]
struct TensionHandle {
curve_edit: Entity,
canvas: Entity,
index: usize,
}
#[derive(Component)]
struct PointModeMenu;
#[derive(Component, Default)]
struct Dragging;
trait CurveControl: Component {
fn curve_edit_entity(&self) -> Entity;
fn canvas_entity(&self) -> Entity;
fn active_cursor(&self) -> SystemCursorIcon;
fn update_state(&self, state: &mut CurveEditState, normalized: Vec2, delta: Option<Vec2>);
}
impl CurveControl for CurveCanvas {
fn curve_edit_entity(&self) -> Entity {
self.curve_edit
}
fn canvas_entity(&self) -> Entity {
panic!("CurveCanvas should not be used as a control target")
}
fn active_cursor(&self) -> SystemCursorIcon {
SystemCursorIcon::Default
}
fn update_state(&self, _state: &mut CurveEditState, _normalized: Vec2, _delta: Option<Vec2>) {}
}
impl CurveControl for PointHandle {
fn curve_edit_entity(&self) -> Entity {
self.curve_edit
}
fn canvas_entity(&self) -> Entity {
self.canvas
}
fn active_cursor(&self) -> SystemCursorIcon {
SystemCursorIcon::Grabbing
}
fn update_state(&self, state: &mut CurveEditState, normalized: Vec2, _delta: Option<Vec2>) {
let curve = state.active_curve_mut();
if self.index >= curve.points.len() {
return;
}
let new_pos = (normalized.x + 0.5).clamp(0.0, 1.0);
let snapped_pos = (new_pos as f64 / DRAG_SNAP_STEP).round() * DRAG_SNAP_STEP;
let prev_pos = if self.index > 0 {
curve.points[self.index - 1].position + 0.001
} else {
0.0
};
let next_pos = if self.index < curve.points.len() - 1 {
curve.points[self.index + 1].position - 0.001
} else {
1.0
};
let clamped_pos = (snapped_pos as f32).clamp(prev_pos, next_pos);
let range_min = curve.range.min as f64;
let range_max = curve.range.max as f64;
let range_span = curve.range.span() as f64;
let normalized_value = 0.5 - normalized.y;
let raw_value =
(range_min + normalized_value as f64 * range_span).clamp(range_min, range_max);
let snapped_value = (raw_value / DRAG_SNAP_STEP).round() * DRAG_SNAP_STEP;
curve.points[self.index].position = clamped_pos;
curve.points[self.index].value = snapped_value;
state.mark_custom();
}
}
impl CurveControl for TensionHandle {
fn curve_edit_entity(&self) -> Entity {
self.curve_edit
}
fn canvas_entity(&self) -> Entity {
self.canvas
}
fn active_cursor(&self) -> SystemCursorIcon {
SystemCursorIcon::ColResize
}
fn update_state(&self, state: &mut CurveEditState, _normalized: Vec2, delta: Option<Vec2>) {
let curve = state.active_curve_mut();
if self.index == 0 || self.index >= curve.points.len() {
return;
}
let Some(delta) = delta else {
return;
};
let p1 = &curve.points[self.index];
let mode = p1.mode;
let current_tension = p1.tension;
const TENSION_SENSITIVITY: f64 = 0.005;
match mode {
CurveMode::SingleCurve | CurveMode::DoubleCurve => {
let tension_delta = -delta.y as f64 * TENSION_SENSITIVITY;
let raw_tension = (current_tension + tension_delta).clamp(-1.0, 1.0);
let snapped_tension = (raw_tension / DRAG_SNAP_STEP).round() * DRAG_SNAP_STEP;
curve.points[self.index].tension = snapped_tension;
}
CurveMode::Stairs | CurveMode::SmoothStairs => {
let tension_delta = -delta.y as f64 * TENSION_SENSITIVITY;
let raw_tension = (current_tension + tension_delta).clamp(0.0, 1.0);
let snapped_tension = (raw_tension / DRAG_SNAP_STEP).round() * DRAG_SNAP_STEP;
curve.points[self.index].tension = snapped_tension;
}
CurveMode::Hold => {}
}
state.mark_custom();
}
}
fn on_control_press<C: CurveControl>(
event: On<Pointer<Press>>,
mut commands: Commands,
controls: Query<&C>,
canvases: Query<(&ComputedNode, &UiGlobalTransform), With<CurveCanvas>>,
mut states: Query<&mut CurveEditState>,
) {
if event.button != PointerButton::Primary {
return;
}
let Ok(control) = controls.get(event.event_target()) else {
return;
};
let curve_edit_entity = control.curve_edit_entity();
let canvas_entity = control.canvas_entity();
let Ok((computed, ui_transform)) = canvases.get(canvas_entity) else {
return;
};
let cursor_pos = event.pointer_location.position / computed.inverse_scale_factor;
let Some(normalized) = computed.normalize_point(*ui_transform, cursor_pos) else {
return;
};
let Ok(mut state) = states.get_mut(curve_edit_entity) else {
return;
};
control.update_state(&mut state, normalized, None);
commands.trigger(CurveEditChangeEvent {
entity: curve_edit_entity,
});
}
fn on_control_release<C: CurveControl>(
event: On<Pointer<Release>>,
mut commands: Commands,
controls: Query<&C, Without<Dragging>>,
states: Query<&CurveEditState>,
) {
if event.button != PointerButton::Primary {
return;
}
let Ok(control) = controls.get(event.event_target()) else {
return;
};
let curve_edit_entity = control.curve_edit_entity();
if let Ok(state) = states.get(curve_edit_entity) {
commands.trigger(CurveEditCommitEvent {
entity: curve_edit_entity,
curve: state.curve.clone(),
});
}
}
fn on_control_drag_start<C: CurveControl>(
event: On<Pointer<DragStart>>,
mut commands: Commands,
controls: Query<&C>,
canvases: Query<(&ComputedNode, &UiGlobalTransform), With<CurveCanvas>>,
mut states: Query<&mut CurveEditState>,
) {
if event.button != PointerButton::Primary {
return;
}
let Ok(control) = controls.get(event.event_target()) else {
return;
};
let curve_edit_entity = control.curve_edit_entity();
let canvas_entity = control.canvas_entity();
commands
.entity(event.event_target())
.insert((Dragging, ActiveCursor(control.active_cursor())));
let Ok((computed, ui_transform)) = canvases.get(canvas_entity) else {
return;
};
let cursor_pos = event.pointer_location.position / computed.inverse_scale_factor;
let Some(normalized) = computed.normalize_point(*ui_transform, cursor_pos) else {
return;
};
let Ok(mut state) = states.get_mut(curve_edit_entity) else {
return;
};
control.update_state(&mut state, normalized, None);
commands.trigger(CurveEditChangeEvent {
entity: curve_edit_entity,
});
}
fn on_control_drag<C: CurveControl>(
event: On<Pointer<Drag>>,
mut commands: Commands,
controls: Query<&C, With<Dragging>>,
canvases: Query<(&ComputedNode, &UiGlobalTransform), With<CurveCanvas>>,
mut states: Query<&mut CurveEditState>,
) {
if event.button != PointerButton::Primary {
return;
}
let Ok(control) = controls.get(event.event_target()) else {
return;
};
let curve_edit_entity = control.curve_edit_entity();
let canvas_entity = control.canvas_entity();
let Ok((computed, ui_transform)) = canvases.get(canvas_entity) else {
return;
};
let cursor_pos = event.pointer_location.position / computed.inverse_scale_factor;
let Some(normalized) = computed.normalize_point(*ui_transform, cursor_pos) else {
return;
};
let Ok(mut state) = states.get_mut(curve_edit_entity) else {
return;
};
let delta = event.delta / computed.inverse_scale_factor;
control.update_state(&mut state, normalized, Some(delta));
commands.trigger(CurveEditChangeEvent {
entity: curve_edit_entity,
});
}
fn on_control_drag_end<C: CurveControl>(
event: On<Pointer<DragEnd>>,
mut commands: Commands,
controls: Query<&C>,
states: Query<&CurveEditState>,
) {
if event.button != PointerButton::Primary {
return;
}
let Ok(control) = controls.get(event.event_target()) else {
return;
};
let curve_edit_entity = control.curve_edit_entity();
commands
.entity(event.event_target())
.remove::<(Dragging, ActiveCursor)>();
if let Ok(state) = states.get(curve_edit_entity) {
commands.trigger(CurveEditCommitEvent {
entity: curve_edit_entity,
curve: state.curve.clone(),
});
}
}
fn setup_curve_edit(
mut commands: Commands,
asset_server: Res<AssetServer>,
curve_edits: Query<(Entity, &CurveEditState, Option<&CurveEditLabel>), Added<EditorCurveEdit>>,
) {
let font: Handle<Font> = asset_server.load(FONT_PATH);
for (entity, state, edit_label) in &curve_edits {
let label_text = edit_label.and_then(|l| l.0.as_deref()).unwrap_or("Curve");
let label_entity = commands
.spawn((
Text::new(label_text),
TextFont {
font: font.clone(),
font_size: TEXT_SIZE_SM,
weight: FontWeight::MEDIUM,
..default()
},
TextColor(TEXT_MUTED_COLOR.into()),
))
.id();
commands.entity(entity).add_child(label_entity);
let trigger_entity = commands
.spawn((
CurveEditTrigger(entity),
button(
ButtonProps::new(state.label())
.align_left()
.with_left_icon(ICON_FCURVE)
.with_right_icon(ICON_MORE),
),
))
.id();
commands.entity(entity).add_child(trigger_entity);
}
}
fn handle_trigger_click(
trigger: On<ButtonClickEvent>,
mut commands: Commands,
asset_server: Res<AssetServer>,
triggers: Query<&CurveEditTrigger>,
states: Query<&CurveEditState>,
mut trackers: Query<&mut PopoverTracker>,
existing_popovers: Query<(Entity, &CurveEditPopover)>,
all_popovers: Query<Entity, With<EditorPopover>>,
mut button_styles: Query<(&mut BackgroundColor, &mut BorderColor, &mut ButtonVariant)>,
parents: Query<&ChildOf>,
) {
let Ok(curve_trigger) = triggers.get(trigger.entity) else {
return;
};
let curve_edit_entity = curve_trigger.0;
let Ok(mut tracker) = trackers.get_mut(curve_edit_entity) else {
return;
};
for (popover_entity, popover_ref) in &existing_popovers {
if popover_ref.0 == curve_edit_entity {
commands.entity(popover_entity).try_despawn();
tracker.popover = None;
deactivate_trigger(trigger.entity, &mut button_styles);
return;
}
}
let any_popover_open = !all_popovers.is_empty();
if any_popover_open {
let is_nested = all_popovers
.iter()
.any(|popover| is_descendant_of(curve_edit_entity, popover, &parents));
if !is_nested {
return;
}
}
activate_trigger(trigger.entity, &mut button_styles);
let Ok(state) = states.get(curve_edit_entity) else {
return;
};
let presets: Vec<_> = CURVE_PRESETS
.iter()
.map(|p| ComboBoxOptionData::new(p.name))
.collect();
let axes_options = vec![
ComboBoxOptionData::new("All axes"),
ComboBoxOptionData::new("Per axis"),
];
let axes_selected = if state.is_per_axis() { 1 } else { 0 };
let popover_entity = commands
.spawn((
CurveEditPopover(curve_edit_entity),
popover(
PopoverProps::new(trigger.entity)
.with_placement(PopoverPlacement::Right)
.with_padding(0.0)
.with_node(Node {
width: px(256.0),
..default()
}),
),
))
.id();
tracker.open(popover_entity, trigger.entity);
let is_per_axis = state.is_per_axis();
let active_axis = state.active_axis;
commands
.entity(popover_entity)
.with_child(popover_header(
PopoverHeaderProps::new("Curve editor", popover_entity),
&asset_server,
))
.with_children(|parent| {
parent
.spawn((
Node {
width: percent(100),
padding: UiRect::all(px(CONTENT_PADDING)),
border: UiRect::bottom(px(1.0)),
column_gap: px(8.0),
align_items: AlignItems::Center,
..default()
},
BorderColor::all(BORDER_COLOR),
))
.with_children(|row| {
row.spawn((
PresetComboBox(curve_edit_entity),
combobox_with_label(presets, "Presets"),
));
row.spawn((
AxesComboBox(curve_edit_entity),
combobox_with_selected(axes_options, axes_selected),
));
row.spawn((Node {
flex_shrink: 0.0,
..default()
},))
.with_child((
FlipButton(curve_edit_entity),
icon_button(
IconButtonProps::new(ICON_ARROW_LEFT_RIGHT)
.variant(ButtonVariant::Default),
&asset_server,
),
));
});
parent
.spawn((
AxisTabs(curve_edit_entity),
Node {
display: if is_per_axis {
Display::Flex
} else {
Display::None
},
column_gap: px(3),
padding: UiRect::new(
px(CONTENT_PADDING),
px(CONTENT_PADDING),
px(CONTENT_PADDING),
px(0),
),
..default()
},
))
.with_children(|tabs| {
tabs.spawn((
Node {
width: percent(100.0),
column_gap: px(3),
padding: UiRect::all(px(3)),
border: UiRect::all(px(1)),
border_radius: BorderRadius::all(CORNER_RADIUS_LG),
..default()
},
BorderColor::all(BORDER_COLOR),
))
.with_children(|inner| {
for axis in CurveAxis::ALL {
let variant = if axis == active_axis {
ButtonVariant::Active
} else {
ButtonVariant::Ghost
};
let mut btn = inner.spawn((
AxisTabButton {
curve_edit: curve_edit_entity,
axis,
},
button(ButtonProps::new(axis.label()).with_variant(variant)),
));
btn.entry::<Node>().and_modify(|mut node| {
node.flex_grow = 1.0;
node.flex_basis = px(0.0);
});
}
});
});
parent.spawn((CurveEditContent(curve_edit_entity), popover_content()));
});
}
fn setup_curve_edit_content(
mut commands: Commands,
mut curve_materials: ResMut<Assets<CurveMaterial>>,
states: Query<&CurveEditState>,
contents: Query<(Entity, &CurveEditContent), Added<CurveEditContent>>,
) {
for (content_entity, content) in &contents {
let curve_edit_entity = content.0;
let Ok(state) = states.get(curve_edit_entity) else {
continue;
};
let channel = state.active_curve();
commands.entity(content_entity).with_children(|parent| {
let canvas_entity = parent
.spawn((
CurveCanvas {
curve_edit: curve_edit_entity,
point_count: channel.points.len(),
active_axis: state.active_axis,
is_per_axis: state.is_per_axis(),
},
Hovered::default(),
Node {
width: percent(100.0),
aspect_ratio: Some(1.0),
..default()
},
))
.id();
parent
.commands()
.entity(canvas_entity)
.with_children(|canvas_parent| {
canvas_parent.spawn((
CurveMaterialNode(curve_edit_entity),
Pickable::IGNORE,
MaterialNode(curve_materials.add(CurveMaterial::from_channel(
channel,
state.curve_color(),
state.fill_color(),
))),
Node {
position_type: PositionType::Absolute,
width: percent(100.0),
height: percent(100.0),
..default()
},
));
let handle_color = state.accent_color();
spawn_point_handles(
canvas_parent,
curve_edit_entity,
canvas_entity,
channel,
handle_color,
);
spawn_tension_handles(
canvas_parent,
curve_edit_entity,
canvas_entity,
channel,
&state.curve,
state.active_axis.channel_index(),
handle_color,
);
});
parent.spawn((
RangeEdit(curve_edit_entity),
vector_edit(
VectorEditProps::default()
.with_label("Range")
.with_size(VectorSize::Vec2)
.with_suffixes(VectorSuffixes::Range)
.with_default_values(vec![channel.range.min, channel.range.max]),
),
));
});
}
}
fn spawn_point_handles(
parent: &mut ChildSpawnerCommands,
curve_edit_entity: Entity,
canvas_entity: Entity,
channel: &Curve,
handle_color: Srgba,
) {
let range_span = channel.range.span();
for (i, point) in channel.points.iter().enumerate() {
let x = point.position;
let normalized_value = (point.value as f32 - channel.range.min) / range_span;
let y = 1.0 - normalized_value;
parent
.spawn((
PointHandle {
curve_edit: curve_edit_entity,
canvas: canvas_entity,
index: i,
},
HoverCursor(SystemCursorIcon::Grab),
handle_style(x, y, POINT_HANDLE_SIZE, handle_color),
))
.observe(on_control_press::<PointHandle>)
.observe(on_control_release::<PointHandle>)
.observe(on_control_drag_start::<PointHandle>)
.observe(on_control_drag::<PointHandle>)
.observe(on_control_drag_end::<PointHandle>);
}
}
fn spawn_tension_handles(
parent: &mut ChildSpawnerCommands,
curve_edit_entity: Entity,
canvas_entity: Entity,
channel: &Curve,
curve_texture: &CurveTexture,
channel_index: usize,
handle_color: Srgba,
) {
let range_span = channel.range.span();
for i in 1..channel.points.len() {
let p0 = &channel.points[i - 1];
let p1 = &channel.points[i];
if p1.mode == CurveMode::Hold {
continue;
}
let mid_x = (p0.position + p1.position) / 2.0;
let curve_value_at_mid = curve_texture.sample_channel(channel_index, mid_x);
let normalized_curve_value = (curve_value_at_mid - channel.range.min) / range_span;
let y = 1.0 - normalized_curve_value;
parent
.spawn((
TensionHandle {
curve_edit: curve_edit_entity,
canvas: canvas_entity,
index: i,
},
HoverCursor(SystemCursorIcon::ColResize),
handle_style(mid_x, y, TENSION_HANDLE_SIZE, handle_color),
))
.observe(on_control_press::<TensionHandle>)
.observe(on_control_release::<TensionHandle>)
.observe(on_control_drag_start::<TensionHandle>)
.observe(on_control_drag::<TensionHandle>)
.observe(on_control_drag_end::<TensionHandle>);
}
}
fn handle_style(x: f32, y: f32, size: f32, color: Srgba) -> impl Bundle {
(
Pickable::default(),
Hovered::default(),
Interaction::None,
Node {
position_type: PositionType::Absolute,
width: px(size),
height: px(size),
left: percent(x * 100.0 - size / CANVAS_SIZE * 50.0),
top: percent(y * 100.0 - size / CANVAS_SIZE * 50.0),
border: UiRect::all(px(HANDLE_BORDER)),
border_radius: BorderRadius::all(px(size / 2.0)),
..default()
},
BackgroundColor(BACKGROUND_COLOR.into()),
BorderColor::all(color),
)
}
fn update_curve_visuals(
states: Query<&CurveEditState, Changed<CurveEditState>>,
material_nodes: Query<(&CurveMaterialNode, &MaterialNode<CurveMaterial>)>,
mut curve_materials: ResMut<Assets<CurveMaterial>>,
mut point_handles: Query<(&PointHandle, &mut Node), Without<TensionHandle>>,
mut tension_handles: Query<(&TensionHandle, &mut Node), Without<PointHandle>>,
) {
for state in &states {
let curve_edit_entity = match material_nodes.iter().find(|(m, _)| states.get(m.0).is_ok()) {
Some((m, _)) => m.0,
None => continue,
};
if states.get(curve_edit_entity).is_err() {
continue;
}
let channel = state.active_curve();
for (mat_node, material_node) in &material_nodes {
if mat_node.0 != curve_edit_entity {
continue;
}
if let Some(material) = curve_materials.get_mut(&material_node.0) {
*material =
CurveMaterial::from_channel(channel, state.curve_color(), state.fill_color());
}
}
let range_span = channel.range.span();
for (handle, mut node) in &mut point_handles {
if handle.curve_edit != curve_edit_entity {
continue;
}
let Some(point) = channel.points.get(handle.index) else {
continue;
};
let x = point.position;
let normalized_value = (point.value as f32 - channel.range.min) / range_span;
let y = 1.0 - normalized_value;
node.left = percent(x * 100.0 - POINT_HANDLE_SIZE / CANVAS_SIZE * 50.0);
node.top = percent(y * 100.0 - POINT_HANDLE_SIZE / CANVAS_SIZE * 50.0);
}
for (handle, mut node) in &mut tension_handles {
if handle.curve_edit != curve_edit_entity {
continue;
}
if handle.index == 0 || handle.index >= channel.points.len() {
continue;
}
let p0 = &channel.points[handle.index - 1];
let p1 = &channel.points[handle.index];
let mid_x = (p0.position + p1.position) / 2.0;
let curve_value_at_mid = state
.curve
.sample_channel(state.active_axis.channel_index(), mid_x);
let normalized_curve_value = (curve_value_at_mid - channel.range.min) / range_span;
let y = 1.0 - normalized_curve_value;
node.left = percent(mid_x * 100.0 - TENSION_HANDLE_SIZE / CANVAS_SIZE * 50.0);
node.top = percent(y * 100.0 - TENSION_HANDLE_SIZE / CANVAS_SIZE * 50.0);
}
}
}
fn respawn_handles_on_point_change(
mut commands: Commands,
states: Query<(Entity, &CurveEditState), Changed<CurveEditState>>,
mut canvases: Query<(Entity, &mut CurveCanvas)>,
point_handles: Query<(Entity, &PointHandle)>,
tension_handles: Query<(Entity, &TensionHandle)>,
) {
for (curve_edit_entity, state) in &states {
for (canvas_entity, mut canvas) in &mut canvases {
if canvas.curve_edit != curve_edit_entity {
continue;
}
let channel = state.active_curve();
let current_point_count = channel.points.len();
let axis_changed = canvas.active_axis != state.active_axis;
let per_axis_changed = canvas.is_per_axis != state.is_per_axis();
if canvas.point_count == current_point_count && !axis_changed && !per_axis_changed {
continue;
}
canvas.point_count = current_point_count;
canvas.active_axis = state.active_axis;
canvas.is_per_axis = state.is_per_axis();
for (handle_entity, handle) in &point_handles {
if handle.curve_edit == canvas.curve_edit {
commands.entity(handle_entity).despawn();
}
}
for (handle_entity, handle) in &tension_handles {
if handle.curve_edit == canvas.curve_edit {
commands.entity(handle_entity).despawn();
}
}
let handle_color = state.accent_color();
let channel = state.active_curve();
commands.entity(canvas_entity).with_children(|parent| {
spawn_point_handles(
parent,
canvas.curve_edit,
canvas_entity,
channel,
handle_color,
);
spawn_tension_handles(
parent,
canvas.curve_edit,
canvas_entity,
channel,
&state.curve,
state.active_axis.channel_index(),
handle_color,
);
});
}
}
}
fn update_handle_colors(
mut removed_dragging: RemovedComponents<Dragging>,
states: Query<&CurveEditState>,
point_handles: Query<&PointHandle>,
tension_handles: Query<&TensionHandle>,
mut handles: ParamSet<(
Query<
(Entity, &Hovered, Has<Dragging>, &mut BackgroundColor),
(
Or<(With<PointHandle>, With<TensionHandle>)>,
Or<(Changed<Hovered>, Added<Dragging>)>,
),
>,
Query<(&Hovered, &mut BackgroundColor), Or<(With<PointHandle>, With<TensionHandle>)>>,
)>,
) {
let removed: Vec<Entity> = removed_dragging.read().collect();
for (entity, hovered, is_dragging, mut bg) in &mut handles.p0() {
if removed.contains(&entity) {
continue;
}
let accent = handle_accent(entity, &states, &point_handles, &tension_handles);
let hover_color = BACKGROUND_COLOR.mix(&accent, 0.8);
*bg = if is_dragging {
BackgroundColor(accent.into())
} else if hovered.get() {
BackgroundColor(hover_color.into())
} else {
BackgroundColor(BACKGROUND_COLOR.into())
};
}
for entity in removed {
if let Ok((hovered, mut bg)) = handles.p1().get_mut(entity) {
let accent = handle_accent(entity, &states, &point_handles, &tension_handles);
let hover_color = BACKGROUND_COLOR.mix(&accent, 0.8);
*bg = if hovered.get() {
BackgroundColor(hover_color.into())
} else {
BackgroundColor(BACKGROUND_COLOR.into())
};
}
}
}
fn handle_accent(
entity: Entity,
states: &Query<&CurveEditState>,
point_handles: &Query<&PointHandle>,
tension_handles: &Query<&TensionHandle>,
) -> Srgba {
let curve_edit = point_handles
.get(entity)
.map(|h| h.curve_edit)
.or_else(|_| tension_handles.get(entity).map(|h| h.curve_edit));
curve_edit
.ok()
.and_then(|e| states.get(e).ok())
.map(|s| s.accent_color())
.unwrap_or(PRIMARY_COLOR)
}
fn handle_preset_change(
trigger: On<ComboBoxChangeEvent>,
mut commands: Commands,
preset_boxes: Query<&PresetComboBox>,
mut states: Query<&mut CurveEditState>,
) {
let Ok(preset_box) = preset_boxes.get(trigger.entity) else {
return;
};
let curve_edit_entity = preset_box.0;
let Ok(mut state) = states.get_mut(curve_edit_entity) else {
return;
};
if let Some(preset) = CURVE_PRESETS.get(trigger.selected) {
let range = state.active_curve().range;
let preset_curve = preset.to_curve(range);
let channel = state.active_curve_mut();
*channel = preset_curve.x;
}
trigger_curve_events(&mut commands, curve_edit_entity, &state.curve);
}
fn handle_axes_mode_change(
trigger: On<ComboBoxChangeEvent>,
mut commands: Commands,
axes_boxes: Query<&AxesComboBox>,
mut states: Query<&mut CurveEditState>,
) {
let Ok(axes_box) = axes_boxes.get(trigger.entity) else {
return;
};
let curve_edit_entity = axes_box.0;
let Ok(mut state) = states.get_mut(curve_edit_entity) else {
return;
};
match trigger.selected {
0 => {
state.curve.y = None;
state.curve.z = None;
state.active_axis = CurveAxis::X;
}
1 => {
state.curve.y = Some(state.curve.x.clone());
state.curve.z = Some(state.curve.x.clone());
state.active_axis = CurveAxis::X;
}
_ => {}
}
state.mark_custom();
trigger_curve_events(&mut commands, curve_edit_entity, &state.curve);
}
fn handle_axis_tab_click(
trigger: On<ButtonClickEvent>,
tab_buttons: Query<&AxisTabButton>,
mut states: Query<&mut CurveEditState>,
) {
let Ok(tab) = tab_buttons.get(trigger.entity) else {
return;
};
let Ok(mut state) = states.get_mut(tab.curve_edit) else {
return;
};
if state.active_axis != tab.axis {
state.active_axis = tab.axis;
}
}
fn sync_axis_tabs_visibility(
mut states: Query<&mut CurveEditState, Changed<CurveEditState>>,
mut tabs: Query<(&AxisTabs, &mut Node)>,
) {
for (tab, mut node) in &mut tabs {
let Ok(mut state) = states.get_mut(tab.0) else {
continue;
};
let per_axis = state.is_per_axis();
node.display = if per_axis {
Display::Flex
} else {
Display::None
};
if !per_axis && state.active_axis != CurveAxis::X {
state.active_axis = CurveAxis::X;
}
}
}
fn sync_axis_tab_styles(
states: Query<&CurveEditState, Changed<CurveEditState>>,
tab_buttons: Query<(&AxisTabButton, Entity)>,
mut button_styles: Query<(&mut BackgroundColor, &mut BorderColor, &mut ButtonVariant)>,
children_query: Query<&Children>,
mut text_colors: Query<&mut TextColor>,
) {
for (tab, entity) in &tab_buttons {
let Ok(state) = states.get(tab.curve_edit) else {
continue;
};
let variant = if tab.axis == state.active_axis {
ButtonVariant::Active
} else {
ButtonVariant::Ghost
};
if let Ok((mut bg, mut border, mut current_variant)) = button_styles.get_mut(entity) {
*current_variant = variant;
set_button_variant(variant, &mut bg, &mut border);
}
if let Ok(children) = children_query.get(entity) {
for child in children.iter() {
if let Ok(mut text_color) = text_colors.get_mut(child) {
text_color.0 = variant.text_color().into();
}
}
}
}
}
fn sync_axis_tab_text_alignment(
tab_buttons: Query<Entity, With<AxisTabButton>>,
children_query: Query<&Children>,
mut text_layouts: Query<&mut TextLayout, With<Text>>,
) {
for entity in &tab_buttons {
let Ok(children) = children_query.get(entity) else {
continue;
};
for child in children.iter() {
if let Ok(mut layout) = text_layouts.get_mut(child) {
if layout.justify != Justify::Center {
layout.justify = Justify::Center;
}
}
}
}
}
fn sync_axes_combobox_to_state(
states: Query<&CurveEditState, Changed<CurveEditState>>,
axes_boxes: Query<(&AxesComboBox, Entity)>,
mut configs: Query<&mut ComboBoxConfig>,
) {
for (axes_box, entity) in &axes_boxes {
let Ok(state) = states.get(axes_box.0) else {
continue;
};
let expected = if state.is_per_axis() { 1 } else { 0 };
if let Ok(mut config) = configs.get_mut(entity) {
if config.selected != expected {
config.selected = expected;
}
}
}
}
fn handle_flip_click(
trigger: On<ButtonClickEvent>,
mut commands: Commands,
flip_buttons: Query<&FlipButton>,
mut states: Query<&mut CurveEditState>,
) {
let Ok(flip_button) = flip_buttons.get(trigger.entity) else {
return;
};
let curve_edit_entity = flip_button.0;
let Ok(mut state) = states.get_mut(curve_edit_entity) else {
return;
};
{
let channel = state.active_curve_mut();
let interp_props: Vec<_> = channel
.points
.iter()
.skip(1)
.map(|p| (p.mode, p.easing, p.tension))
.collect();
for point in &mut channel.points {
point.position = 1.0 - point.position;
}
channel.points.reverse();
if let Some(first) = channel.points.first_mut() {
first.mode = CurveMode::default();
first.easing = CurveEasing::default();
first.tension = 0.0;
}
for (i, (mode, easing, tension)) in interp_props.iter().rev().enumerate() {
if let Some(point) = channel.points.get_mut(i + 1) {
point.mode = *mode;
point.easing = *easing;
point.tension = *tension;
}
}
}
trigger_curve_events(&mut commands, curve_edit_entity, &state.curve);
}
fn sync_trigger_label(
states: Query<&CurveEditState>,
changed_states: Query<Entity, Changed<CurveEditState>>,
triggers: Query<(Entity, &CurveEditTrigger, &Children)>,
new_trigger_children: Query<Entity, (With<CurveEditTrigger>, Added<Children>)>,
mut texts: Query<&mut Text>,
) {
for curve_edit_entity in &changed_states {
let Ok(state) = states.get(curve_edit_entity) else {
continue;
};
for (_, trigger, children) in &triggers {
if trigger.0 != curve_edit_entity {
continue;
}
for child in children.iter() {
if let Ok(mut text) = texts.get_mut(child) {
**text = state.label().to_string();
break;
}
}
}
}
for trigger_entity in &new_trigger_children {
let Ok((_, trigger, children)) = triggers.get(trigger_entity) else {
continue;
};
let curve_edit_entity = trigger.0;
if changed_states.get(curve_edit_entity).is_ok() {
continue;
}
let Ok(state) = states.get(curve_edit_entity) else {
continue;
};
for child in children.iter() {
if let Ok(mut text) = texts.get_mut(child) {
**text = state.label().to_string();
break;
}
}
}
}
fn sync_range_inputs_to_state(
input_focus: Res<InputFocus>,
states: Query<(Entity, &CurveEditState), Changed<CurveEditState>>,
range_edits: Query<(Entity, &RangeEdit, &Children)>,
vector_edits: Query<&Children, With<EditorVectorEdit>>,
mut text_inputs: Query<(Entity, &mut TextInputQueue), With<EditorTextEdit>>,
parents: Query<&ChildOf>,
) {
for (curve_edit_entity, state) in &states {
for (_range_edit_entity, range_edit, range_children) in &range_edits {
if range_edit.0 != curve_edit_entity {
continue;
}
let channel = state.active_curve();
let values = [channel.range.min, channel.range.max];
for range_child in range_children.iter() {
let Ok(vector_children) = vector_edits.get(range_child) else {
continue;
};
for (i, vector_child) in vector_children.iter().enumerate() {
let Some(&value) = values.get(i) else {
continue;
};
let text = value.to_string();
for (text_input_entity, mut queue) in &mut text_inputs {
if input_focus.0 == Some(text_input_entity) {
continue;
}
if is_descendant_of(text_input_entity, vector_child, &parents) {
queue.add(TextInputAction::Edit(TextInputEdit::SelectAll));
queue.add(TextInputAction::Edit(TextInputEdit::Paste(text.clone())));
}
}
}
}
}
}
}
fn handle_range_blur(
input_focus: Res<InputFocus>,
mut last_focus: Local<Option<Entity>>,
mut commands: Commands,
mut states: Query<&mut CurveEditState>,
range_edits: Query<(Entity, &RangeEdit, &Children)>,
vector_edits: Query<&Children, With<EditorVectorEdit>>,
text_inputs: Query<&bevy_ui_text_input::TextInputBuffer, With<EditorTextEdit>>,
parents: Query<&ChildOf>,
) {
let current_focus = input_focus.0;
let previous_focus = *last_focus;
*last_focus = current_focus;
let Some(blurred_entity) = previous_focus else {
return;
};
if current_focus == Some(blurred_entity) {
return;
}
let Ok(buffer) = text_inputs.get(blurred_entity) else {
return;
};
for (_range_edit_entity, range_edit, range_children) in &range_edits {
let Ok(mut state) = states.get_mut(range_edit.0) else {
continue;
};
for range_child in range_children.iter() {
let Ok(vector_children) = vector_edits.get(range_child) else {
continue;
};
for (field_index, vector_child) in vector_children.iter().enumerate() {
let is_descendant = is_descendant_of(blurred_entity, vector_child, &parents);
if !is_descendant {
continue;
}
let text = buffer.get_text();
if text.is_empty() {
return;
}
let Ok(value) = text.parse::<f32>() else {
return;
};
let mut changed = false;
{
let channel = state.active_curve_mut();
if field_index == 0 {
if (channel.range.min - value).abs() > f32::EPSILON {
channel.range.min = value;
changed = true;
}
} else if (channel.range.max - value).abs() > f32::EPSILON {
channel.range.max = value;
changed = true;
}
if changed {
let range_min = channel.range.min as f64;
let range_max = channel.range.max as f64;
for point in &mut channel.points {
point.value = point.value.clamp(range_min, range_max);
}
}
}
if changed {
state.mark_custom();
trigger_curve_events(&mut commands, range_edit.0, &state.curve);
}
return;
}
}
}
}
fn handle_canvas_right_click(
mut commands: Commands,
mouse: Res<ButtonInput<MouseButton>>,
windows: Query<&Window>,
canvases: Query<(&CurveCanvas, &ComputedNode, &UiGlobalTransform, &Hovered)>,
mut states: Query<&mut CurveEditState>,
point_handles: Query<&Hovered, With<PointHandle>>,
tension_handles: Query<&Hovered, With<TensionHandle>>,
) {
if !mouse.just_pressed(MouseButton::Right) {
return;
}
let point_hovered = point_handles.iter().any(|h| h.get());
let tension_hovered = tension_handles.iter().any(|h| h.get());
if point_hovered || tension_hovered {
return;
}
let Ok(window) = windows.single() else {
return;
};
let Some(cursor_position) = window.cursor_position() else {
return;
};
for (canvas, computed, ui_transform, hovered) in &canvases {
if !hovered.get() {
continue;
}
let Ok(mut state) = states.get_mut(canvas.curve_edit) else {
continue;
};
{
let channel = state.active_curve();
if channel.points.len() >= MAX_POINTS {
continue;
}
}
let cursor_pos = cursor_position / computed.inverse_scale_factor;
let Some(normalized) = computed.normalize_point(*ui_transform, cursor_pos) else {
continue;
};
let normalized_x = (normalized.x + 0.5).clamp(0.0, 1.0);
let normalized_y = (0.5 - normalized.y).clamp(0.0, 1.0);
let new_point = CurvePoint::new(normalized_x, 0.0)
.with_mode(CurveMode::DoubleCurve)
.with_tension(0.0);
{
let channel = state.active_curve_mut();
let range_min = channel.range.min as f64;
let range_span = channel.range.span() as f64;
let value = range_min + normalized_y as f64 * range_span;
let mut new_point = new_point;
new_point.value = value;
let insert_idx = channel
.points
.iter()
.position(|p| p.position > normalized_x)
.unwrap_or(channel.points.len());
channel.points.insert(insert_idx, new_point);
}
state.mark_custom();
trigger_curve_events(&mut commands, canvas.curve_edit, &state.curve);
break;
}
}
fn menu_separator() -> impl Bundle {
(
Node {
width: percent(100.0),
height: px(1.0),
margin: UiRect::vertical(px(4.0)),
..default()
},
BackgroundColor(BORDER_COLOR.into()),
)
}
fn menu_button_variant(is_active: bool, is_disabled: bool) -> ButtonVariant {
if is_disabled {
ButtonVariant::Disabled
} else if is_active {
ButtonVariant::Active
} else {
ButtonVariant::Ghost
}
}
fn spawn_enum_options<T, C, F>(
parent: &mut ChildSpawnerCommands,
current: T,
is_disabled: bool,
make_component: F,
) where
T: Typed + PartialEq + std::str::FromStr + Copy,
C: Component,
F: Fn(T, bool) -> C,
{
let bevy::reflect::TypeInfo::Enum(info) = T::type_info() else {
return;
};
for variant_info in info.iter() {
let Ok(value) = variant_info.name().parse::<T>() else {
continue;
};
let name = variant_info.name().to_sentence_case();
let is_active = value == current && !is_disabled;
let variant = menu_button_variant(is_active, is_disabled);
parent.spawn((
make_component(value, is_disabled),
button(ButtonProps::new(&name).with_variant(variant).align_left()),
));
}
}
fn spawn_mode_options(
parent: &mut ChildSpawnerCommands,
curve_edit: Entity,
point_index: usize,
current_mode: CurveMode,
is_first: bool,
) {
spawn_enum_options(parent, current_mode, is_first, |mode, disabled| {
ModeOption {
curve_edit,
point_index,
mode,
disabled,
}
});
}
fn spawn_easing_options(
parent: &mut ChildSpawnerCommands,
curve_edit: Entity,
point_index: usize,
current_easing: CurveEasing,
is_first: bool,
) {
spawn_enum_options(parent, current_easing, is_first, |easing, disabled| {
EasingOption {
curve_edit,
point_index,
easing,
disabled,
}
});
}
fn spawn_delete_option(
parent: &mut ChildSpawnerCommands,
curve_edit: Entity,
point_index: usize,
can_delete: bool,
) {
let variant = menu_button_variant(false, !can_delete);
parent.spawn((
DeletePointOption {
curve_edit,
point_index,
disabled: !can_delete,
},
button(
ButtonProps::new("Delete")
.with_variant(variant)
.align_left(),
),
));
}
fn handle_point_right_click(
mut commands: Commands,
mouse: Res<ButtonInput<MouseButton>>,
point_handles: Query<(Entity, &PointHandle, &Hovered)>,
states: Query<&CurveEditState>,
existing_menus: Query<Entity, With<PointModeMenu>>,
) {
if !mouse.just_pressed(MouseButton::Right) {
return;
}
for menu_entity in &existing_menus {
commands.entity(menu_entity).try_despawn();
}
for (handle_entity, point_handle, hovered) in &point_handles {
if !hovered.get() {
continue;
}
let Ok(state) = states.get(point_handle.curve_edit) else {
continue;
};
let channel = state.active_curve();
let Some(point) = channel.points.get(point_handle.index) else {
continue;
};
let is_first = point_handle.index == 0;
let can_delete = channel.points.len() > 2;
let popover_entity = commands
.spawn((
PointModeMenu,
popover(
PopoverProps::new(handle_entity)
.with_placement(PopoverPlacement::BottomStart)
.with_padding(4.0)
.with_z_index(300),
),
))
.id();
commands.entity(popover_entity).with_children(|parent| {
spawn_mode_options(
parent,
point_handle.curve_edit,
point_handle.index,
point.mode,
is_first,
);
parent.spawn(menu_separator());
spawn_easing_options(
parent,
point_handle.curve_edit,
point_handle.index,
point.easing,
is_first,
);
parent.spawn(menu_separator());
spawn_delete_option(
parent,
point_handle.curve_edit,
point_handle.index,
can_delete,
);
});
break;
}
}
#[derive(Component)]
struct ModeOption {
curve_edit: Entity,
point_index: usize,
mode: CurveMode,
disabled: bool,
}
#[derive(Component)]
struct DeletePointOption {
curve_edit: Entity,
point_index: usize,
disabled: bool,
}
#[derive(Component)]
struct EasingOption {
curve_edit: Entity,
point_index: usize,
easing: CurveEasing,
disabled: bool,
}
fn handle_point_mode_change(
trigger: On<ButtonClickEvent>,
mut commands: Commands,
mode_options: Query<&ModeOption>,
easing_options: Query<&EasingOption>,
delete_options: Query<&DeletePointOption>,
mut states: Query<&mut CurveEditState>,
menus: Query<Entity, With<PointModeMenu>>,
) {
let mut handled = false;
if let Ok(mode_opt) = mode_options.get(trigger.entity) {
if !mode_opt.disabled {
if let Ok(mut state) = states.get_mut(mode_opt.curve_edit) {
if let Some(point) = state
.active_curve_mut()
.points
.get_mut(mode_opt.point_index)
{
point.mode = mode_opt.mode;
state.mark_custom();
trigger_curve_events(&mut commands, mode_opt.curve_edit, &state.curve);
handled = true;
}
}
}
} else if let Ok(easing_opt) = easing_options.get(trigger.entity) {
if !easing_opt.disabled {
if let Ok(mut state) = states.get_mut(easing_opt.curve_edit) {
if let Some(point) = state
.active_curve_mut()
.points
.get_mut(easing_opt.point_index)
{
point.easing = easing_opt.easing;
state.mark_custom();
trigger_curve_events(&mut commands, easing_opt.curve_edit, &state.curve);
handled = true;
}
}
}
} else if let Ok(delete_opt) = delete_options.get(trigger.entity) {
if !delete_opt.disabled {
if let Ok(mut state) = states.get_mut(delete_opt.curve_edit) {
if state.active_curve().points.len() > 2 {
state
.active_curve_mut()
.points
.remove(delete_opt.point_index);
state.mark_custom();
trigger_curve_events(&mut commands, delete_opt.curve_edit, &state.curve);
handled = true;
}
}
}
}
if handled {
for menu in &menus {
commands.entity(menu).try_despawn();
}
}
}
fn handle_tension_right_click(
mut commands: Commands,
mouse: Res<ButtonInput<MouseButton>>,
tension_handles: Query<(&TensionHandle, &Hovered)>,
mut states: Query<&mut CurveEditState>,
) {
if !mouse.just_pressed(MouseButton::Right) {
return;
}
for (tension_handle, hovered) in &tension_handles {
if !hovered.get() {
continue;
}
let Ok(mut state) = states.get_mut(tension_handle.curve_edit) else {
continue;
};
if let Some(point) = state
.active_curve_mut()
.points
.get_mut(tension_handle.index)
{
point.tension = 0.0;
state.mark_custom();
trigger_curve_events(&mut commands, tension_handle.curve_edit, &state.curve);
}
break;
}
}