use crate::{
brush::Brush,
core::{
algebra::{Matrix3, Point2, SimdPartialOrd, Vector2, Vector3},
color::Color,
curve::{Curve, CurveKeyKind},
math::{cubicf, inf_sup_cubicf, lerpf, wrap_angle, Rect},
pool::Handle,
uuid::Uuid,
},
curve::key::{CurveKeyView, KeyContainer},
define_constructor,
draw::{CommandTexture, Draw, DrawingContext},
formatted_text::{FormattedText, FormattedTextBuilder},
grid::{Column, GridBuilder, Row},
menu::{MenuItemBuilder, MenuItemContent, MenuItemMessage},
message::{ButtonState, KeyCode, MessageDirection, MouseButton, UiMessage},
numeric::{NumericUpDownBuilder, NumericUpDownMessage},
popup::PopupBuilder,
stack_panel::StackPanelBuilder,
text::TextBuilder,
widget::{Widget, WidgetBuilder, WidgetMessage},
BuildContext, Control, Thickness, UiNode, UserInterface, VerticalAlignment,
};
use fxhash::FxHashSet;
use std::{
any::{Any, TypeId},
cell::{Cell, RefCell},
ops::{Deref, DerefMut},
};
pub mod key;
#[derive(Debug, Clone, PartialEq)]
pub enum CurveEditorMessage {
Sync(Curve),
ViewPosition(Vector2<f32>),
Zoom(Vector2<f32>),
ZoomToFit,
HighlightZones(Vec<HighlightZone>),
ChangeSelectedKeysKind(CurveKeyKind),
ChangeSelectedKeysValue(f32),
ChangeSelectedKeysLocation(f32),
RemoveSelection,
AddKey(Vector2<f32>),
}
impl CurveEditorMessage {
define_constructor!(CurveEditorMessage:Sync => fn sync(Curve), layout: false);
define_constructor!(CurveEditorMessage:ViewPosition => fn view_position(Vector2<f32>), layout: false);
define_constructor!(CurveEditorMessage:Zoom => fn zoom(Vector2<f32>), layout: false);
define_constructor!(CurveEditorMessage:ZoomToFit => fn zoom_to_fit(), layout: false);
define_constructor!(CurveEditorMessage:HighlightZones => fn hightlight_zones(Vec<HighlightZone>), layout: false);
define_constructor!(CurveEditorMessage:RemoveSelection => fn remove_selection(), layout: false);
define_constructor!(CurveEditorMessage:ChangeSelectedKeysKind => fn change_selected_keys_kind(CurveKeyKind), layout: false);
define_constructor!(CurveEditorMessage:ChangeSelectedKeysValue => fn change_selected_keys_value(f32), layout: false);
define_constructor!(CurveEditorMessage:ChangeSelectedKeysLocation => fn change_selected_keys_location(f32), layout: false);
define_constructor!(CurveEditorMessage:AddKey => fn add_key(Vector2<f32>), layout: false);
}
#[derive(Clone, Debug, PartialEq)]
pub struct HighlightZone {
pub rect: Rect<f32>,
pub brush: Brush,
}
#[derive(Clone)]
pub struct CurveEditor {
widget: Widget,
key_container: KeyContainer,
zoom: Vector2<f32>,
view_position: Vector2<f32>,
view_matrix: Cell<Matrix3<f32>>,
screen_matrix: Cell<Matrix3<f32>>,
inv_screen_matrix: Cell<Matrix3<f32>>,
key_brush: Brush,
selected_key_brush: Brush,
key_size: f32,
grid_brush: Brush,
operation_context: Option<OperationContext>,
selection: Option<Selection>,
handle_radius: f32,
context_menu: ContextMenu,
text: RefCell<FormattedText>,
view_bounds: Option<Rect<f32>>,
show_x_values: bool,
show_y_values: bool,
grid_size: Vector2<f32>,
min_zoom: Vector2<f32>,
max_zoom: Vector2<f32>,
highlight_zones: Vec<HighlightZone>,
}
crate::define_widget_deref!(CurveEditor);
#[derive(Clone)]
struct ContextMenu {
widget: Handle<UiNode>,
add_key: Handle<UiNode>,
remove: Handle<UiNode>,
key: Handle<UiNode>,
make_constant: Handle<UiNode>,
make_linear: Handle<UiNode>,
make_cubic: Handle<UiNode>,
zoom_to_fit: Handle<UiNode>,
key_properties: Handle<UiNode>,
key_value: Handle<UiNode>,
key_location: Handle<UiNode>,
}
#[derive(Clone)]
struct DragEntry {
key: Uuid,
initial_position: Vector2<f32>,
}
#[derive(Clone)]
enum OperationContext {
DragKeys {
initial_mouse_pos: Vector2<f32>,
entries: Vec<DragEntry>,
},
MoveView {
initial_mouse_pos: Vector2<f32>,
initial_view_pos: Vector2<f32>,
},
DragTangent {
key: usize,
left: bool,
},
BoxSelection {
initial_mouse_pos: Vector2<f32>,
min: Cell<Vector2<f32>>,
max: Cell<Vector2<f32>>,
},
}
#[derive(Clone)]
enum Selection {
Keys { keys: FxHashSet<Uuid> },
LeftTangent { key: usize },
RightTangent { key: usize },
}
#[derive(Copy, Clone)]
enum PickResult {
Key(usize),
LeftTangent(usize),
RightTangent(usize),
}
impl Selection {
fn single_key(key: Uuid) -> Self {
let mut keys = FxHashSet::default();
keys.insert(key);
Self::Keys { keys }
}
}
impl Control for CurveEditor {
fn query_component(&self, type_id: TypeId) -> Option<&dyn Any> {
if type_id == TypeId::of::<Self>() {
Some(self)
} else {
None
}
}
fn draw(&self, ctx: &mut DrawingContext) {
ctx.transform_stack.push(Matrix3::identity());
self.update_matrices();
self.draw_background(ctx);
self.draw_highlight_zones(ctx);
self.draw_grid(ctx);
self.draw_curve(ctx);
self.draw_keys(ctx);
self.draw_operation(ctx);
ctx.transform_stack.pop();
}
fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
self.widget.handle_routed_message(ui, message);
if message.destination() == self.handle {
if let Some(msg) = message.data::<WidgetMessage>() {
match msg {
WidgetMessage::KeyUp(KeyCode::Delete) => {
self.remove_selection(ui);
}
WidgetMessage::MouseMove { pos, state } => {
let local_mouse_pos = self.point_to_local_space(*pos);
if let Some(operation_context) = self.operation_context.as_ref() {
match operation_context {
OperationContext::DragKeys {
entries,
initial_mouse_pos,
} => {
let local_delta = local_mouse_pos - initial_mouse_pos;
for entry in entries {
let key = self.key_container.key_mut(entry.key).unwrap();
key.position = entry.initial_position + local_delta;
}
self.sort_keys();
}
OperationContext::MoveView {
initial_mouse_pos,
initial_view_pos,
} => {
let d = pos - initial_mouse_pos;
let delta = Vector2::new(d.x / self.zoom.x, d.y / self.zoom.y);
ui.send_message(CurveEditorMessage::view_position(
self.handle,
MessageDirection::ToWidget,
initial_view_pos + delta,
));
}
OperationContext::DragTangent { key, left } => {
let key_pos =
self.key_container.key_index_ref(*key).unwrap().position;
let screen_key_pos = self.point_to_screen_space(key_pos);
let key = self.key_container.key_index_mut(*key).unwrap();
if let CurveKeyKind::Cubic {
left_tangent,
right_tangent,
} = &mut key.kind
{
let mut local_delta = pos - screen_key_pos;
if *left {
local_delta.x = local_delta.x.min(f32::EPSILON);
} else {
local_delta.x = local_delta.x.max(f32::EPSILON);
}
let tangent =
(local_delta.y / local_delta.x).clamp(-10e6, 10e6);
if *left {
*left_tangent = tangent;
} else {
*right_tangent = tangent;
}
} else {
unreachable!(
"attempt to edit tangents of non-cubic curve key!"
)
}
}
OperationContext::BoxSelection {
initial_mouse_pos,
min,
max,
..
} => {
min.set(local_mouse_pos.inf(initial_mouse_pos));
max.set(local_mouse_pos.sup(initial_mouse_pos));
}
}
} else if state.left == ButtonState::Pressed {
if let Some(selection) = self.selection.as_ref() {
match selection {
Selection::Keys { keys } => {
self.operation_context = Some(OperationContext::DragKeys {
entries: keys
.iter()
.map(|k| DragEntry {
key: *k,
initial_position: self
.key_container
.key_ref(*k)
.unwrap()
.position,
})
.collect::<Vec<_>>(),
initial_mouse_pos: local_mouse_pos,
});
}
Selection::LeftTangent { key } => {
self.operation_context =
Some(OperationContext::DragTangent {
key: *key,
left: true,
})
}
Selection::RightTangent { key } => {
self.operation_context =
Some(OperationContext::DragTangent {
key: *key,
left: false,
})
}
}
if self.operation_context.is_some() {
ui.capture_mouse(self.handle);
}
} else {
self.operation_context = Some(OperationContext::BoxSelection {
initial_mouse_pos: local_mouse_pos,
min: Default::default(),
max: Default::default(),
})
}
}
}
WidgetMessage::MouseUp { .. } => {
if let Some(context) = self.operation_context.take() {
ui.release_mouse_capture();
match context {
OperationContext::DragKeys { .. }
| OperationContext::DragTangent { .. } => {
self.sort_keys();
self.send_curve(ui);
}
OperationContext::BoxSelection { min, max, .. } => {
let min = min.get();
let max = max.get();
let rect =
Rect::new(min.x, min.y, max.x - min.x, max.y - min.y);
let mut selection = FxHashSet::default();
for key in self.key_container.keys() {
if rect.contains(key.position) {
selection.insert(key.id);
}
}
if !selection.is_empty() {
self.set_selection(
Some(Selection::Keys { keys: selection }),
ui,
);
}
}
_ => {}
}
}
}
WidgetMessage::MouseDown { pos, button } => match button {
MouseButton::Left => {
let pick_result = self.pick(*pos);
if let Some(picked) = pick_result {
match picked {
PickResult::Key(picked_key) => {
let picked_key_id = self
.key_container
.key_index_ref(picked_key)
.unwrap()
.id;
if let Some(selection) = self.selection.as_mut() {
match selection {
Selection::Keys { keys } => {
if ui.keyboard_modifiers().control {
keys.insert(picked_key_id);
}
if !keys.contains(&picked_key_id) {
self.set_selection(
Some(Selection::single_key(
picked_key_id,
)),
ui,
);
}
}
Selection::LeftTangent { .. }
| Selection::RightTangent { .. } => self
.set_selection(
Some(Selection::single_key(picked_key_id)),
ui,
),
}
} else {
self.set_selection(
Some(Selection::single_key(picked_key_id)),
ui,
);
}
}
PickResult::LeftTangent(picked_key) => {
self.set_selection(
Some(Selection::LeftTangent { key: picked_key }),
ui,
);
}
PickResult::RightTangent(picked_key) => {
self.set_selection(
Some(Selection::RightTangent { key: picked_key }),
ui,
);
}
}
} else {
self.set_selection(None, ui);
}
}
MouseButton::Middle => {
ui.capture_mouse(self.handle);
self.operation_context = Some(OperationContext::MoveView {
initial_mouse_pos: *pos,
initial_view_pos: self.view_position,
});
}
_ => (),
},
WidgetMessage::MouseWheel { amount, .. } => {
let k = if *amount < 0.0 { 0.9 } else { 1.1 };
let new_zoom = if ui.keyboard_modifiers().shift {
Vector2::new(self.zoom.x * k, self.zoom.y)
} else if ui.keyboard_modifiers.control {
Vector2::new(self.zoom.x, self.zoom.y * k)
} else {
self.zoom * k
};
ui.send_message(CurveEditorMessage::zoom(
self.handle,
MessageDirection::ToWidget,
new_zoom,
));
}
_ => {}
}
} else if let Some(msg) = message.data::<CurveEditorMessage>() {
if message.destination() == self.handle
&& message.direction() == MessageDirection::ToWidget
{
match msg {
CurveEditorMessage::Sync(curve) => {
self.key_container = KeyContainer::from(curve);
}
CurveEditorMessage::ViewPosition(view_position) => {
self.set_view_position(*view_position);
ui.send_message(message.reverse());
}
CurveEditorMessage::Zoom(zoom) => {
self.zoom = zoom.simd_clamp(self.min_zoom, self.max_zoom);
ui.send_message(message.reverse());
}
CurveEditorMessage::RemoveSelection => {
self.remove_selection(ui);
}
CurveEditorMessage::ChangeSelectedKeysKind(kind) => {
self.change_selected_keys_kind(kind.clone(), ui);
}
CurveEditorMessage::AddKey(screen_pos) => {
let local_pos = self.point_to_local_space(*screen_pos);
self.key_container.add(CurveKeyView {
position: local_pos,
kind: CurveKeyKind::Linear,
id: Uuid::new_v4(),
});
self.set_selection(None, ui);
self.sort_keys();
self.send_curve(ui);
}
CurveEditorMessage::ZoomToFit => {
let mut max_y = -f32::MAX;
let mut min_y = f32::MAX;
let mut max_x = -f32::MAX;
let mut min_x = f32::MAX;
let mut push = |x: f32, y: f32| {
if x > max_x {
max_x = x;
}
if x < min_x {
min_x = x;
}
if y > max_y {
max_y = y;
}
if y < min_y {
min_y = y;
}
};
for keys in self.key_container.keys().windows(2) {
let left = &keys[0];
let right = &keys[1];
match (&left.kind, &right.kind) {
(
CurveKeyKind::Cubic {
right_tangent: left_tangent,
..
},
CurveKeyKind::Constant,
)
| (
CurveKeyKind::Cubic {
right_tangent: left_tangent,
..
},
CurveKeyKind::Linear,
) => {
let (y0, y1) = inf_sup_cubicf(
left.position.y,
right.position.y,
*left_tangent,
0.0,
);
push(left.position.x, y0);
push(right.position.x, y1);
}
(
CurveKeyKind::Cubic {
right_tangent: left_tangent,
..
},
CurveKeyKind::Cubic {
left_tangent: right_tangent,
..
},
) => {
let (y0, y1) = inf_sup_cubicf(
left.position.y,
right.position.y,
*left_tangent,
*right_tangent,
);
push(left.position.x, y0);
push(right.position.x, y1);
}
_ => {
push(left.position.x, left.position.y);
push(right.position.x, right.position.y);
}
}
}
let min = Vector2::new(min_x, min_y);
let max = Vector2::new(max_x, max_y);
let center = (min + max).scale(0.5);
ui.send_message(CurveEditorMessage::zoom(
self.handle,
MessageDirection::ToWidget,
Vector2::new(
self.actual_local_size().x
/ (max.x - min.x).max(5.0 * f32::EPSILON),
self.actual_local_size().y
/ (max.y - min.y).max(5.0 * f32::EPSILON),
),
));
ui.send_message(CurveEditorMessage::view_position(
self.handle,
MessageDirection::ToWidget,
Vector2::new(
self.actual_local_size().x * 0.5 - center.x,
-self.actual_local_size().y * 0.5 + center.y,
),
));
}
CurveEditorMessage::ChangeSelectedKeysValue(value) => {
self.change_selected_keys_value(*value, ui);
}
CurveEditorMessage::ChangeSelectedKeysLocation(location) => {
self.change_selected_keys_location(*location, ui);
}
CurveEditorMessage::HighlightZones(zones) => {
self.highlight_zones = zones.clone();
}
}
}
}
}
}
fn preview_message(&self, ui: &UserInterface, message: &mut UiMessage) {
if let Some(MenuItemMessage::Click) = message.data::<MenuItemMessage>() {
if message.destination() == self.context_menu.remove {
ui.send_message(CurveEditorMessage::remove_selection(
self.handle,
MessageDirection::ToWidget,
));
} else if message.destination() == self.context_menu.make_constant {
ui.send_message(CurveEditorMessage::change_selected_keys_kind(
self.handle,
MessageDirection::ToWidget,
CurveKeyKind::Constant,
));
} else if message.destination() == self.context_menu.make_linear {
ui.send_message(CurveEditorMessage::change_selected_keys_kind(
self.handle,
MessageDirection::ToWidget,
CurveKeyKind::Linear,
));
} else if message.destination() == self.context_menu.make_cubic {
ui.send_message(CurveEditorMessage::change_selected_keys_kind(
self.handle,
MessageDirection::ToWidget,
CurveKeyKind::Cubic {
left_tangent: 0.0,
right_tangent: 0.0,
},
));
} else if message.destination() == self.context_menu.add_key {
let screen_pos = ui.node(self.context_menu.widget).screen_position();
ui.send_message(CurveEditorMessage::add_key(
self.handle,
MessageDirection::ToWidget,
screen_pos,
));
} else if message.destination() == self.context_menu.zoom_to_fit {
ui.send_message(CurveEditorMessage::zoom_to_fit(
self.handle,
MessageDirection::ToWidget,
));
}
} else if let Some(NumericUpDownMessage::<f32>::Value(value)) = message.data() {
if message.direction() == MessageDirection::FromWidget {
if message.destination() == self.context_menu.key_value {
ui.send_message(CurveEditorMessage::change_selected_keys_value(
self.handle,
MessageDirection::ToWidget,
*value,
));
} else if message.destination() == self.context_menu.key_location {
ui.send_message(CurveEditorMessage::change_selected_keys_location(
self.handle,
MessageDirection::ToWidget,
*value,
));
}
}
}
}
}
fn draw_cubic(
left_pos: Vector2<f32>,
left_tangent: f32,
right_pos: Vector2<f32>,
right_tangent: f32,
steps: usize,
ctx: &mut DrawingContext,
) {
let mut prev = left_pos;
for i in 0..steps {
let t = i as f32 / (steps - 1) as f32;
let middle_x = lerpf(left_pos.x, right_pos.x, t);
let middle_y = cubicf(left_pos.y, right_pos.y, t, left_tangent, right_tangent);
let pt = Vector2::new(middle_x, middle_y);
ctx.push_line(prev, pt, 1.0);
prev = pt;
}
}
fn round_to_step(x: f32, step: f32) -> f32 {
x - x % step
}
impl CurveEditor {
#[allow(clippy::let_and_return)] fn set_view_position(&mut self, position: Vector2<f32>) {
self.view_position = self.view_bounds.map_or(position, |bounds| {
let local_space_position = -position;
let clamped_local_space_position = Vector2::new(
local_space_position
.x
.clamp(bounds.position.x, bounds.position.x + 2.0 * bounds.size.x),
local_space_position
.y
.clamp(bounds.position.y, bounds.position.y + 2.0 * bounds.size.y),
);
let clamped_view_space = -clamped_local_space_position;
clamped_view_space
});
}
fn update_matrices(&self) {
let vp = Vector2::new(self.view_position.x, -self.view_position.y);
self.view_matrix.set(
Matrix3::new_nonuniform_scaling_wrt_point(
&self.zoom,
&Point2::from(self.actual_local_size().scale(0.5)),
) * Matrix3::new_translation(&vp),
);
let screen_bounds = self.screen_bounds();
self.screen_matrix.set(
Matrix3::new_translation(&screen_bounds.position)
* Matrix3::new_translation(&Vector2::new(0.0, screen_bounds.h()))
* Matrix3::new_nonuniform_scaling(&Vector2::new(1.0, -1.0))
* self.view_matrix.get(),
);
self.inv_screen_matrix
.set(self.view_matrix.get().try_inverse().unwrap_or_default());
}
pub fn point_to_view_space(&self, point: Vector2<f32>) -> Vector2<f32> {
self.view_matrix
.get()
.transform_point(&Point2::from(point))
.coords
}
pub fn point_to_screen_space(&self, point: Vector2<f32>) -> Vector2<f32> {
self.screen_matrix
.get()
.transform_point(&Point2::from(point))
.coords
}
pub fn vector_to_screen_space(&self, vector: Vector2<f32>) -> Vector2<f32> {
(self.screen_matrix.get() * Vector3::new(vector.x, vector.y, 0.0)).xy()
}
pub fn point_to_local_space(&self, point: Vector2<f32>) -> Vector2<f32> {
let mut p = point - self.screen_position();
p.y = self.actual_local_size().y - p.y;
self.view_matrix
.get()
.try_inverse()
.unwrap_or_default()
.transform_point(&Point2::from(p))
.coords
}
fn sort_keys(&mut self) {
self.key_container.sort_keys();
}
fn set_selection(&mut self, selection: Option<Selection>, ui: &UserInterface) {
self.selection = selection;
ui.send_message(WidgetMessage::enabled(
self.context_menu.remove,
MessageDirection::ToWidget,
self.selection.is_some(),
));
ui.send_message(WidgetMessage::enabled(
self.context_menu.key,
MessageDirection::ToWidget,
self.selection.is_some(),
));
ui.send_message(WidgetMessage::enabled(
self.context_menu.key_properties,
MessageDirection::ToWidget,
self.selection.is_some(),
));
if let Some(Selection::Keys { keys }) = self.selection.as_ref() {
if let Some(first) = keys.iter().next() {
if let Some(key) = self.key_container.key_ref(*first) {
ui.send_message(NumericUpDownMessage::value(
self.context_menu.key_location,
MessageDirection::ToWidget,
key.position.x,
));
ui.send_message(NumericUpDownMessage::value(
self.context_menu.key_value,
MessageDirection::ToWidget,
key.position.y,
));
}
}
}
}
fn remove_selection(&mut self, ui: &mut UserInterface) {
if let Some(Selection::Keys { keys }) = self.selection.as_ref() {
for &id in keys {
self.key_container.remove(id);
}
self.set_selection(None, ui);
self.send_curve(ui);
}
}
fn change_selected_keys_kind(&mut self, kind: CurveKeyKind, ui: &mut UserInterface) {
if let Some(Selection::Keys { keys }) = self.selection.as_ref() {
for key in keys {
self.key_container.key_mut(*key).unwrap().kind = kind.clone();
}
self.send_curve(ui);
}
}
fn change_selected_keys_value(&mut self, value: f32, ui: &mut UserInterface) {
if let Some(Selection::Keys { keys }) = self.selection.as_ref() {
let mut modified = false;
for key in keys {
let key_value = &mut self.key_container.key_mut(*key).unwrap().position.y;
if (*key_value).ne(&value) {
*key_value = value;
modified = true;
}
}
if modified {
self.send_curve(ui);
}
}
}
fn change_selected_keys_location(&mut self, location: f32, ui: &mut UserInterface) {
if let Some(Selection::Keys { keys }) = self.selection.as_ref() {
let mut modified = false;
for key in keys {
let key_location = &mut self.key_container.key_mut(*key).unwrap().position.x;
if (*key_location).ne(&location) {
*key_location = location;
modified = true;
}
}
if modified {
self.send_curve(ui);
}
}
}
fn pick(&self, pos: Vector2<f32>) -> Option<PickResult> {
for (i, key) in self.key_container.keys().iter().enumerate() {
let screen_pos = self.point_to_screen_space(key.position);
let bounds = Rect::new(
screen_pos.x - self.key_size * 0.5,
screen_pos.y - self.key_size * 0.5,
self.key_size,
self.key_size,
);
if bounds.contains(pos) {
return Some(PickResult::Key(i));
}
if let CurveKeyKind::Cubic {
left_tangent,
right_tangent,
} = key.kind
{
let left_handle_pos = self.tangent_screen_position(
wrap_angle(left_tangent.atan()) + std::f32::consts::PI,
key.position,
);
if (left_handle_pos - pos).norm() <= self.key_size * 0.5 {
return Some(PickResult::LeftTangent(i));
}
let right_handle_pos =
self.tangent_screen_position(wrap_angle(right_tangent.atan()), key.position);
if (right_handle_pos - pos).norm() <= self.key_size * 0.5 {
return Some(PickResult::RightTangent(i));
}
}
}
None
}
fn tangent_screen_position(&self, angle: f32, key_position: Vector2<f32>) -> Vector2<f32> {
self.point_to_screen_space(key_position)
+ Vector2::new(angle.cos(), angle.sin()).scale(self.handle_radius)
}
fn send_curve(&self, ui: &UserInterface) {
ui.send_message(CurveEditorMessage::sync(
self.handle,
MessageDirection::FromWidget,
self.key_container.curve(),
));
}
fn draw_background(&self, ctx: &mut DrawingContext) {
let screen_bounds = self.screen_bounds();
ctx.push_rect_filled(&screen_bounds, None);
ctx.commit(screen_bounds, self.background(), CommandTexture::None, None);
}
fn draw_highlight_zones(&self, ctx: &mut DrawingContext) {
for zone in self.highlight_zones.iter() {
let left_top_corner = self.point_to_screen_space(zone.rect.left_top_corner());
let bottom_right_corner = self.point_to_screen_space(zone.rect.right_bottom_corner());
ctx.push_rect_filled(
&Rect::new(
left_top_corner.x,
left_top_corner.y,
bottom_right_corner.x - left_top_corner.x,
bottom_right_corner.y - left_top_corner.y,
),
None,
);
ctx.commit(
self.clip_bounds(),
zone.brush.clone(),
CommandTexture::None,
None,
);
}
}
fn draw_grid(&self, ctx: &mut DrawingContext) {
let screen_bounds = self.screen_bounds();
let step_size_x = self.grid_size.x / self.zoom.x;
let step_size_y = self.grid_size.y / self.zoom.y;
let mut local_left_bottom = self.point_to_local_space(screen_bounds.left_top_corner());
let local_left_bottom_n = local_left_bottom;
local_left_bottom.x = round_to_step(local_left_bottom.x, step_size_x);
local_left_bottom.y = round_to_step(local_left_bottom.y, step_size_y);
let mut local_right_top = self.point_to_local_space(screen_bounds.right_bottom_corner());
local_right_top.x = round_to_step(local_right_top.x, step_size_x);
local_right_top.y = round_to_step(local_right_top.y, step_size_y);
let w = (local_right_top.x - local_left_bottom.x).abs();
let h = (local_right_top.y - local_left_bottom.y).abs();
let nw = ((w / step_size_x).ceil()) as usize;
let nh = ((h / step_size_y).ceil()) as usize;
for ny in 0..=nh {
let k = ny as f32 / (nh) as f32;
let y = local_left_bottom.y - k * h;
ctx.push_line(
self.point_to_screen_space(Vector2::new(local_left_bottom.x - step_size_x, y)),
self.point_to_screen_space(Vector2::new(local_right_top.x + step_size_x, y)),
1.0,
);
}
for nx in 0..=nw {
let k = nx as f32 / (nw) as f32;
let x = local_left_bottom.x + k * w;
ctx.push_line(
self.point_to_screen_space(Vector2::new(x, local_left_bottom.y + step_size_y)),
self.point_to_screen_space(Vector2::new(x, local_right_top.y - step_size_y)),
1.0,
);
}
let vb = self.point_to_screen_space(Vector2::new(0.0, -10e6));
let ve = self.point_to_screen_space(Vector2::new(0.0, 10e6));
ctx.push_line(vb, ve, 2.0);
let hb = self.point_to_screen_space(Vector2::new(-10e6, 0.0));
let he = self.point_to_screen_space(Vector2::new(10e6, 0.0));
ctx.push_line(hb, he, 2.0);
ctx.commit(
screen_bounds,
self.grid_brush.clone(),
CommandTexture::None,
None,
);
let mut text = self.text.borrow_mut();
if self.show_y_values {
for ny in 0..=nh {
let k = ny as f32 / (nh) as f32;
let y = local_left_bottom.y - k * h;
text.set_text(format!("{:.1}", y)).build();
ctx.draw_text(
screen_bounds,
self.point_to_screen_space(Vector2::new(local_left_bottom_n.x, y)),
&text,
);
}
}
if self.show_x_values {
for nx in 0..=nw {
let k = nx as f32 / (nw) as f32;
let x = local_left_bottom.x + k * w;
text.set_text(format!("{:.1}", x)).build();
ctx.draw_text(
screen_bounds,
self.point_to_screen_space(Vector2::new(x, local_left_bottom_n.y)),
&text,
);
}
}
}
fn draw_curve(&self, ctx: &mut DrawingContext) {
let screen_bounds = self.screen_bounds();
let draw_keys = self.key_container.keys();
if let Some(first) = draw_keys.first() {
let screen_pos = self.point_to_screen_space(first.position);
ctx.push_line(Vector2::new(0.0, screen_pos.y), screen_pos, 1.0);
}
if let Some(last) = draw_keys.last() {
let screen_pos = self.point_to_screen_space(last.position);
ctx.push_line(
screen_pos,
Vector2::new(screen_bounds.x() + screen_bounds.w(), screen_pos.y),
1.0,
);
}
for pair in draw_keys.windows(2) {
let left = &pair[0];
let right = &pair[1];
let left_pos = self.point_to_screen_space(left.position);
let right_pos = self.point_to_screen_space(right.position);
let steps = ((right_pos.x - left_pos.x).abs() / 2.0) as usize;
match (&left.kind, &right.kind) {
(CurveKeyKind::Constant, CurveKeyKind::Constant)
| (CurveKeyKind::Constant, CurveKeyKind::Linear)
| (CurveKeyKind::Constant, CurveKeyKind::Cubic { .. }) => {
ctx.push_line(left_pos, Vector2::new(right_pos.x, left_pos.y), 1.0);
ctx.push_line(Vector2::new(right_pos.x, left_pos.y), right_pos, 1.0);
}
(CurveKeyKind::Linear, CurveKeyKind::Constant)
| (CurveKeyKind::Linear, CurveKeyKind::Linear)
| (CurveKeyKind::Linear, CurveKeyKind::Cubic { .. }) => {
ctx.push_line(left_pos, right_pos, 1.0)
}
(
CurveKeyKind::Cubic {
right_tangent: left_tangent,
..
},
CurveKeyKind::Constant,
)
| (
CurveKeyKind::Cubic {
right_tangent: left_tangent,
..
},
CurveKeyKind::Linear,
) => draw_cubic(left_pos, *left_tangent, right_pos, 0.0, steps, ctx),
(
CurveKeyKind::Cubic {
right_tangent: left_tangent,
..
},
CurveKeyKind::Cubic {
left_tangent: right_tangent,
..
},
) => draw_cubic(
left_pos,
*left_tangent,
right_pos,
*right_tangent,
steps,
ctx,
),
}
}
ctx.commit(screen_bounds, self.foreground(), CommandTexture::None, None);
}
fn draw_keys(&self, ctx: &mut DrawingContext) {
let screen_bounds = self.screen_bounds();
let keys_to_draw = self.key_container.keys();
for (i, key) in keys_to_draw.iter().enumerate() {
let origin = self.point_to_screen_space(key.position);
let size = Vector2::new(self.key_size, self.key_size);
let half_size = size.scale(0.5);
ctx.push_rect_filled(
&Rect::new(
origin.x - half_size.x,
origin.y - half_size.x,
size.x,
size.y,
),
None,
);
let mut selected = false;
if let Some(selection) = self.selection.as_ref() {
match selection {
Selection::Keys { keys } => {
selected = keys.contains(&key.id);
}
Selection::LeftTangent { key } | Selection::RightTangent { key } => {
selected = i == *key;
}
}
}
if selected {
let (show_left, show_right) = match keys_to_draw.get(i.wrapping_sub(1)) {
Some(left) => match (&left.kind, &key.kind) {
(CurveKeyKind::Cubic { .. }, CurveKeyKind::Cubic { .. }) => (true, true),
(CurveKeyKind::Linear, CurveKeyKind::Cubic { .. })
| (CurveKeyKind::Constant, CurveKeyKind::Cubic { .. }) => (false, true),
_ => (false, false),
},
None => match key.kind {
CurveKeyKind::Cubic { .. } => (false, true),
_ => (false, false),
},
};
if let CurveKeyKind::Cubic {
left_tangent,
right_tangent,
} = key.kind
{
if show_left {
let left_handle_pos = self.tangent_screen_position(
wrap_angle(left_tangent.atan()) + std::f32::consts::PI,
key.position,
);
ctx.push_line(origin, left_handle_pos, 1.0);
ctx.push_circle(
left_handle_pos,
self.key_size * 0.5,
6,
Default::default(),
);
}
if show_right {
let right_handle_pos = self.tangent_screen_position(
wrap_angle(right_tangent.atan()),
key.position,
);
ctx.push_line(origin, right_handle_pos, 1.0);
ctx.push_circle(
right_handle_pos,
self.key_size * 0.5,
6,
Default::default(),
);
}
}
}
ctx.commit(
screen_bounds,
if selected {
self.selected_key_brush.clone()
} else {
self.key_brush.clone()
},
CommandTexture::None,
None,
);
}
}
fn draw_operation(&self, ctx: &mut DrawingContext) {
if let Some(OperationContext::BoxSelection { min, max, .. }) =
self.operation_context.as_ref()
{
let min = self.point_to_screen_space(min.get());
let max = self.point_to_screen_space(max.get());
let rect = Rect::new(min.x, min.y, max.x - min.x, max.y - min.y);
ctx.push_rect(&rect, 1.0);
ctx.commit(
self.screen_bounds(),
Brush::Solid(Color::WHITE),
CommandTexture::None,
None,
);
}
}
}
pub struct CurveEditorBuilder {
widget_builder: WidgetBuilder,
curve: Curve,
view_position: Vector2<f32>,
zoom: f32,
view_bounds: Option<Rect<f32>>,
show_x_values: bool,
show_y_values: bool,
grid_size: Vector2<f32>,
min_zoom: Vector2<f32>,
max_zoom: Vector2<f32>,
highlight_zones: Vec<HighlightZone>,
}
impl CurveEditorBuilder {
pub fn new(widget_builder: WidgetBuilder) -> Self {
Self {
widget_builder,
curve: Default::default(),
view_position: Default::default(),
zoom: 1.0,
view_bounds: None,
show_x_values: true,
show_y_values: true,
grid_size: Vector2::new(50.0, 50.0),
min_zoom: Vector2::new(0.001, 0.001),
max_zoom: Vector2::new(1000.0, 1000.0),
highlight_zones: Default::default(),
}
}
pub fn with_curve(mut self, curve: Curve) -> Self {
self.curve = curve;
self
}
pub fn with_zoom(mut self, zoom: f32) -> Self {
self.zoom = zoom;
self
}
pub fn with_view_position(mut self, view_position: Vector2<f32>) -> Self {
self.view_position = view_position;
self
}
pub fn with_show_x_values(mut self, show_x_values: bool) -> Self {
self.show_x_values = show_x_values;
self
}
pub fn with_show_y_values(mut self, show_y_values: bool) -> Self {
self.show_y_values = show_y_values;
self
}
pub fn with_view_bounds(mut self, bounds: Rect<f32>) -> Self {
self.view_bounds = Some(bounds);
self
}
pub fn with_grid_size(mut self, size: Vector2<f32>) -> Self {
self.grid_size = size;
self
}
pub fn with_min_zoom(mut self, min_zoom: Vector2<f32>) -> Self {
self.min_zoom = min_zoom;
self
}
pub fn with_max_zoom(mut self, max_zoom: Vector2<f32>) -> Self {
self.max_zoom = max_zoom;
self
}
pub fn with_highlight_zone(mut self, zones: Vec<HighlightZone>) -> Self {
self.highlight_zones = zones;
self
}
pub fn build(mut self, ctx: &mut BuildContext) -> Handle<UiNode> {
let keys = KeyContainer::from(&self.curve);
let add_key;
let remove;
let make_constant;
let make_linear;
let make_cubic;
let key;
let zoom_to_fit;
let key_properties;
let key_value;
let key_location;
let context_menu = PopupBuilder::new(WidgetBuilder::new())
.with_content(
StackPanelBuilder::new(
WidgetBuilder::new()
.with_child({
key_properties = GridBuilder::new(
WidgetBuilder::new()
.with_enabled(false)
.with_child(
TextBuilder::new(
WidgetBuilder::new()
.with_vertical_alignment(VerticalAlignment::Center)
.with_margin(Thickness::uniform(1.0))
.on_row(0)
.on_column(0),
)
.with_text("Location")
.build(ctx),
)
.with_child({
key_location = NumericUpDownBuilder::<f32>::new(
WidgetBuilder::new()
.with_margin(Thickness::uniform(1.0))
.on_row(0)
.on_column(1),
)
.build(ctx);
key_location
})
.with_child(
TextBuilder::new(
WidgetBuilder::new()
.with_vertical_alignment(VerticalAlignment::Center)
.with_margin(Thickness::uniform(1.0))
.on_row(1)
.on_column(0),
)
.with_text("Value")
.build(ctx),
)
.with_child({
key_value = NumericUpDownBuilder::<f32>::new(
WidgetBuilder::new()
.with_margin(Thickness::uniform(1.0))
.on_row(1)
.on_column(1),
)
.build(ctx);
key_value
}),
)
.add_column(Column::auto())
.add_column(Column::stretch())
.add_row(Row::strict(22.0))
.add_row(Row::strict(22.0))
.build(ctx);
key_properties
})
.with_child({
add_key = MenuItemBuilder::new(WidgetBuilder::new())
.with_content(MenuItemContent::text("Add Key"))
.build(ctx);
add_key
})
.with_child({
remove = MenuItemBuilder::new(WidgetBuilder::new().with_enabled(false))
.with_content(MenuItemContent::text("Remove"))
.build(ctx);
remove
})
.with_child({
key = MenuItemBuilder::new(WidgetBuilder::new().with_enabled(false))
.with_content(MenuItemContent::text("Key..."))
.with_items(vec![
{
make_constant = MenuItemBuilder::new(WidgetBuilder::new())
.with_content(MenuItemContent::text("Constant"))
.build(ctx);
make_constant
},
{
make_linear = MenuItemBuilder::new(WidgetBuilder::new())
.with_content(MenuItemContent::text("Linear"))
.build(ctx);
make_linear
},
{
make_cubic = MenuItemBuilder::new(WidgetBuilder::new())
.with_content(MenuItemContent::text("Cubic"))
.build(ctx);
make_cubic
},
])
.build(ctx);
key
})
.with_child({
zoom_to_fit = MenuItemBuilder::new(WidgetBuilder::new())
.with_content(MenuItemContent::text("Zoom To Fit"))
.build(ctx);
zoom_to_fit
}),
)
.build(ctx),
)
.build(ctx);
if self.widget_builder.foreground.is_none() {
self.widget_builder.foreground = Some(Brush::Solid(Color::opaque(130, 130, 130)))
}
let editor = CurveEditor {
widget: self
.widget_builder
.with_context_menu(context_menu)
.with_preview_messages(true)
.build(),
key_container: keys,
zoom: Vector2::new(1.0, 1.0),
view_position: Default::default(),
view_matrix: Default::default(),
screen_matrix: Default::default(),
inv_screen_matrix: Default::default(),
key_brush: Brush::Solid(Color::opaque(140, 140, 140)),
selected_key_brush: Brush::Solid(Color::opaque(220, 220, 220)),
key_size: 8.0,
handle_radius: 36.0,
operation_context: None,
grid_brush: Brush::Solid(Color::from_rgba(110, 110, 110, 50)),
selection: None,
text: RefCell::new(
FormattedTextBuilder::new(ctx.default_font())
.with_brush(Brush::Solid(Color::opaque(100, 100, 100)))
.build(),
),
context_menu: ContextMenu {
widget: context_menu,
add_key,
remove,
make_constant,
make_linear,
make_cubic,
key,
zoom_to_fit,
key_properties,
key_value,
key_location,
},
view_bounds: self.view_bounds,
show_x_values: self.show_x_values,
show_y_values: self.show_y_values,
grid_size: self.grid_size,
min_zoom: self.min_zoom,
max_zoom: self.max_zoom,
highlight_zones: self.highlight_zones,
};
ctx.add_node(UiNode::new(editor))
}
}