1use crate::{
2 brush::Brush,
3 core::{
4 algebra::{Matrix3, Point2, Vector2, Vector3},
5 color::Color,
6 curve::{Curve, CurveKeyKind},
7 math::{cubicf, inf_sup_cubicf, lerpf, wrap_angle, Rect},
8 pool::Handle,
9 uuid::Uuid,
10 },
11 curve::key::{CurveKeyView, KeyContainer},
12 define_constructor,
13 draw::{CommandTexture, Draw, DrawingContext},
14 formatted_text::{FormattedText, FormattedTextBuilder},
15 menu::{MenuItemBuilder, MenuItemContent, MenuItemMessage},
16 message::{ButtonState, KeyCode, MessageDirection, MouseButton, UiMessage},
17 popup::PopupBuilder,
18 stack_panel::StackPanelBuilder,
19 widget::{Widget, WidgetBuilder, WidgetMessage},
20 BuildContext, Control, UiNode, UserInterface,
21};
22use fxhash::FxHashSet;
23use std::{
24 any::{Any, TypeId},
25 cell::{Cell, RefCell},
26 ops::{Deref, DerefMut},
27};
28
29pub mod key;
30
31#[derive(Debug, Clone, PartialEq)]
32pub enum CurveEditorMessage {
33 Sync(Curve),
34 ViewPosition(Vector2<f32>),
35 Zoom(f32),
36 ZoomToFit,
37
38 ChangeSelectedKeysKind(CurveKeyKind),
42 RemoveSelection,
43 AddKey(Vector2<f32>),
45}
46
47impl CurveEditorMessage {
48 define_constructor!(CurveEditorMessage:Sync => fn sync(Curve), layout: false);
49 define_constructor!(CurveEditorMessage:ViewPosition => fn view_position(Vector2<f32>), layout: false);
50 define_constructor!(CurveEditorMessage:Zoom => fn zoom(f32), layout: false);
51 define_constructor!(CurveEditorMessage:ZoomToFit => fn zoom_to_fit(), layout: false);
52 define_constructor!(CurveEditorMessage:RemoveSelection => fn remove_selection(), layout: false);
54 define_constructor!(CurveEditorMessage:ChangeSelectedKeysKind => fn change_selected_keys_kind(CurveKeyKind), layout: false);
55 define_constructor!(CurveEditorMessage:AddKey => fn add_key(Vector2<f32>), layout: false);
56}
57
58#[derive(Clone)]
59pub struct CurveEditor {
60 widget: Widget,
61 key_container: KeyContainer,
62 zoom: f32,
63 view_position: Vector2<f32>,
64 view_matrix: Cell<Matrix3<f32>>,
66 screen_matrix: Cell<Matrix3<f32>>,
71 inv_screen_matrix: Cell<Matrix3<f32>>,
74 key_brush: Brush,
75 selected_key_brush: Brush,
76 key_size: f32,
77 grid_brush: Brush,
78 operation_context: Option<OperationContext>,
79 selection: Option<Selection>,
80 handle_radius: f32,
81 context_menu: ContextMenu,
82 text: RefCell<FormattedText>,
83}
84
85crate::define_widget_deref!(CurveEditor);
86
87#[derive(Clone)]
88struct ContextMenu {
89 widget: Handle<UiNode>,
90 add_key: Handle<UiNode>,
91 remove: Handle<UiNode>,
92 key: Handle<UiNode>,
93 make_constant: Handle<UiNode>,
94 make_linear: Handle<UiNode>,
95 make_cubic: Handle<UiNode>,
96 zoom_to_fit: Handle<UiNode>,
97}
98
99#[derive(Clone)]
100struct DragEntry {
101 key: Uuid,
102 initial_position: Vector2<f32>,
103}
104
105#[derive(Clone)]
106enum OperationContext {
107 DragKeys {
108 initial_mouse_pos: Vector2<f32>,
110 entries: Vec<DragEntry>,
111 },
112 MoveView {
113 initial_mouse_pos: Vector2<f32>,
114 initial_view_pos: Vector2<f32>,
115 },
116 DragTangent {
117 key: usize,
118 left: bool,
119 },
120 BoxSelection {
121 initial_mouse_pos: Vector2<f32>,
123 min: Cell<Vector2<f32>>,
124 max: Cell<Vector2<f32>>,
125 },
126}
127
128#[derive(Clone)]
129enum Selection {
130 Keys { keys: FxHashSet<Uuid> },
131 LeftTangent { key: usize },
134 RightTangent { key: usize },
135}
136
137#[derive(Copy, Clone)]
138enum PickResult {
139 Key(usize),
140 LeftTangent(usize),
141 RightTangent(usize),
142}
143
144impl Selection {
145 fn single_key(key: Uuid) -> Self {
146 let mut keys = FxHashSet::default();
147 keys.insert(key);
148 Self::Keys { keys }
149 }
150}
151
152impl Control for CurveEditor {
153 fn query_component(&self, type_id: TypeId) -> Option<&dyn Any> {
154 if type_id == TypeId::of::<Self>() {
155 Some(self)
156 } else {
157 None
158 }
159 }
160
161 fn draw(&self, ctx: &mut DrawingContext) {
162 self.update_matrices();
163 self.draw_background(ctx);
164 self.draw_grid(ctx);
165 self.draw_curve(ctx);
166 self.draw_keys(ctx);
167 self.draw_operation(ctx);
168 }
169
170 fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
171 self.widget.handle_routed_message(ui, message);
172
173 if message.destination() == self.handle {
174 if let Some(msg) = message.data::<WidgetMessage>() {
175 match msg {
176 WidgetMessage::KeyUp(KeyCode::Delete) => {
177 self.remove_selection(ui);
178 }
179 WidgetMessage::MouseMove { pos, state } => {
180 let local_mouse_pos = self.point_to_local_space(*pos);
181 if let Some(operation_context) = self.operation_context.as_ref() {
182 match operation_context {
183 OperationContext::DragKeys {
184 entries,
185 initial_mouse_pos,
186 } => {
187 let local_delta = local_mouse_pos - initial_mouse_pos;
188 for entry in entries {
189 let key = self.key_container.key_mut(entry.key).unwrap();
190 key.position = entry.initial_position + local_delta;
191 }
192 self.sort_keys();
193 }
194 OperationContext::MoveView {
195 initial_mouse_pos,
196 initial_view_pos,
197 } => {
198 let delta = (pos - initial_mouse_pos).scale(1.0 / self.zoom);
199 self.view_position = initial_view_pos + delta;
200 }
201 OperationContext::DragTangent { key, left } => {
202 let key_pos =
203 self.key_container.key_index_ref(*key).unwrap().position;
204 let screen_key_pos = self.point_to_screen_space(key_pos);
205 let key = self.key_container.key_index_mut(*key).unwrap();
206 if let CurveKeyKind::Cubic {
207 left_tangent,
208 right_tangent,
209 } = &mut key.kind
210 {
211 let mut local_delta = pos - screen_key_pos;
212 if *left {
213 local_delta.x = local_delta.x.min(f32::EPSILON);
214 } else {
215 local_delta.x = local_delta.x.max(f32::EPSILON);
216 }
217 let tangent =
218 (local_delta.y / local_delta.x).clamp(-10e6, 10e6);
219
220 if *left {
221 *left_tangent = tangent;
222 } else {
223 *right_tangent = tangent;
224 }
225 } else {
226 unreachable!(
227 "attempt to edit tangents of non-cubic curve key!"
228 )
229 }
230 }
231 OperationContext::BoxSelection {
232 initial_mouse_pos,
233 min,
234 max,
235 ..
236 } => {
237 min.set(local_mouse_pos.inf(initial_mouse_pos));
238 max.set(local_mouse_pos.sup(initial_mouse_pos));
239 }
240 }
241 } else if state.left == ButtonState::Pressed {
242 if let Some(selection) = self.selection.as_ref() {
243 match selection {
244 Selection::Keys { keys } => {
245 self.operation_context = Some(OperationContext::DragKeys {
246 entries: keys
247 .iter()
248 .map(|k| DragEntry {
249 key: *k,
250 initial_position: self
251 .key_container
252 .key_ref(*k)
253 .unwrap()
254 .position,
255 })
256 .collect::<Vec<_>>(),
257 initial_mouse_pos: local_mouse_pos,
258 });
259 }
260 Selection::LeftTangent { key } => {
261 self.operation_context =
262 Some(OperationContext::DragTangent {
263 key: *key,
264 left: true,
265 })
266 }
267 Selection::RightTangent { key } => {
268 self.operation_context =
269 Some(OperationContext::DragTangent {
270 key: *key,
271 left: false,
272 })
273 }
274 }
275
276 if self.operation_context.is_some() {
277 ui.capture_mouse(self.handle);
278 }
279 } else {
280 self.operation_context = Some(OperationContext::BoxSelection {
281 initial_mouse_pos: local_mouse_pos,
282 min: Default::default(),
283 max: Default::default(),
284 })
285 }
286 }
287 }
288
289 WidgetMessage::MouseUp { .. } => {
290 if let Some(context) = self.operation_context.take() {
291 ui.release_mouse_capture();
292
293 match context {
295 OperationContext::DragKeys { .. }
296 | OperationContext::DragTangent { .. } => {
297 self.sort_keys();
299
300 self.send_curve(ui);
301 }
302 OperationContext::BoxSelection { min, max, .. } => {
303 let min = min.get();
304 let max = max.get();
305
306 let rect =
307 Rect::new(min.x, min.y, max.x - min.x, max.y - min.y);
308
309 let mut selection = FxHashSet::default();
310 for key in self.key_container.keys() {
311 if rect.contains(key.position) {
312 selection.insert(key.id);
313 }
314 }
315
316 if !selection.is_empty() {
317 self.set_selection(
318 Some(Selection::Keys { keys: selection }),
319 ui,
320 );
321 }
322 }
323 _ => {}
324 }
325 }
326 }
327 WidgetMessage::MouseDown { pos, button } => match button {
328 MouseButton::Left => {
329 let pick_result = self.pick(*pos);
330
331 if let Some(picked) = pick_result {
332 match picked {
333 PickResult::Key(picked_key) => {
334 let picked_key_id = self
335 .key_container
336 .key_index_ref(picked_key)
337 .unwrap()
338 .id;
339 if let Some(selection) = self.selection.as_mut() {
340 match selection {
341 Selection::Keys { keys } => {
342 if ui.keyboard_modifiers().control {
343 keys.insert(picked_key_id);
344 }
345 if !keys.contains(&picked_key_id) {
346 self.set_selection(
347 Some(Selection::single_key(
348 picked_key_id,
349 )),
350 ui,
351 );
352 }
353 }
354 Selection::LeftTangent { .. }
355 | Selection::RightTangent { .. } => self
356 .set_selection(
357 Some(Selection::single_key(picked_key_id)),
358 ui,
359 ),
360 }
361 } else {
362 self.set_selection(
363 Some(Selection::single_key(picked_key_id)),
364 ui,
365 );
366 }
367 }
368 PickResult::LeftTangent(picked_key) => {
369 self.set_selection(
370 Some(Selection::LeftTangent { key: picked_key }),
371 ui,
372 );
373 }
374 PickResult::RightTangent(picked_key) => {
375 self.set_selection(
376 Some(Selection::RightTangent { key: picked_key }),
377 ui,
378 );
379 }
380 }
381 } else {
382 self.set_selection(None, ui);
383 }
384 }
385 MouseButton::Middle => {
386 ui.capture_mouse(self.handle);
387 self.operation_context = Some(OperationContext::MoveView {
388 initial_mouse_pos: *pos,
389 initial_view_pos: self.view_position,
390 });
391 }
392 _ => (),
393 },
394 WidgetMessage::MouseWheel { amount, .. } => {
395 let k = if *amount < 0.0 { 0.9 } else { 1.1 };
396 ui.send_message(CurveEditorMessage::zoom(
397 self.handle,
398 MessageDirection::ToWidget,
399 self.zoom * k,
400 ));
401 }
402 _ => {}
403 }
404 } else if let Some(msg) = message.data::<CurveEditorMessage>() {
405 if message.destination() == self.handle
406 && message.direction() == MessageDirection::ToWidget
407 {
408 match msg {
409 CurveEditorMessage::Sync(curve) => {
410 self.key_container = KeyContainer::from(curve);
411 }
412 CurveEditorMessage::ViewPosition(view_position) => {
413 self.view_position = *view_position;
414 }
415 CurveEditorMessage::Zoom(zoom) => {
416 self.zoom = *zoom;
417 }
418 CurveEditorMessage::RemoveSelection => {
419 self.remove_selection(ui);
420 }
421 CurveEditorMessage::ChangeSelectedKeysKind(kind) => {
422 self.change_selected_keys_kind(kind.clone(), ui);
423 }
424 CurveEditorMessage::AddKey(screen_pos) => {
425 let local_pos = self.point_to_local_space(*screen_pos);
426 self.key_container.add(CurveKeyView {
427 position: local_pos,
428 kind: CurveKeyKind::Linear,
429 id: Uuid::new_v4(),
430 });
431 self.set_selection(None, ui);
432 self.sort_keys();
433 }
434 CurveEditorMessage::ZoomToFit => {
435 let mut max_y = -f32::MAX;
436 let mut min_y = f32::MAX;
437 let mut max_x = -f32::MAX;
438 let mut min_x = f32::MAX;
439
440 let mut push = |x: f32, y: f32| {
441 if x > max_x {
442 max_x = x;
443 }
444 if x < min_x {
445 min_x = x;
446 }
447 if y > max_y {
448 max_y = y;
449 }
450 if y < min_y {
451 min_y = y;
452 }
453 };
454
455 for keys in self.key_container.keys().windows(2) {
456 let left = &keys[0];
457 let right = &keys[1];
458 match (&left.kind, &right.kind) {
459 (
461 CurveKeyKind::Cubic {
462 right_tangent: left_tangent,
463 ..
464 },
465 CurveKeyKind::Constant,
466 )
467 | (
468 CurveKeyKind::Cubic {
469 right_tangent: left_tangent,
470 ..
471 },
472 CurveKeyKind::Linear,
473 ) => {
474 let (y0, y1) = inf_sup_cubicf(
475 left.position.y,
476 right.position.y,
477 *left_tangent,
478 0.0,
479 );
480 push(left.position.x, y0);
481 push(right.position.x, y1);
482 }
483
484 (
486 CurveKeyKind::Cubic {
487 right_tangent: left_tangent,
488 ..
489 },
490 CurveKeyKind::Cubic {
491 left_tangent: right_tangent,
492 ..
493 },
494 ) => {
495 let (y0, y1) = inf_sup_cubicf(
496 left.position.y,
497 right.position.y,
498 *left_tangent,
499 *right_tangent,
500 );
501 push(left.position.x, y0);
502 push(right.position.x, y1);
503 }
504 _ => {
505 push(left.position.x, left.position.y);
506 push(right.position.x, right.position.y);
507 }
508 }
509 }
510
511 let min = Vector2::new(min_x, min_y);
512 let max = Vector2::new(max_x, max_y);
513 let center = (min + max).scale(0.5);
514
515 let mut offset = self.actual_size().scale(0.5 * self.zoom);
516 offset.y *= -1.0;
517 self.view_position = center + offset;
518 }
519 }
520 }
521 }
522 }
523 }
524
525 fn preview_message(&self, ui: &UserInterface, message: &mut UiMessage) {
526 if let Some(MenuItemMessage::Click) = message.data::<MenuItemMessage>() {
527 if message.destination() == self.context_menu.remove {
528 ui.send_message(CurveEditorMessage::remove_selection(
529 self.handle,
530 MessageDirection::ToWidget,
531 ));
532 } else if message.destination() == self.context_menu.make_constant {
533 ui.send_message(CurveEditorMessage::change_selected_keys_kind(
534 self.handle,
535 MessageDirection::ToWidget,
536 CurveKeyKind::Constant,
537 ));
538 } else if message.destination() == self.context_menu.make_linear {
539 ui.send_message(CurveEditorMessage::change_selected_keys_kind(
540 self.handle,
541 MessageDirection::ToWidget,
542 CurveKeyKind::Linear,
543 ));
544 } else if message.destination() == self.context_menu.make_cubic {
545 ui.send_message(CurveEditorMessage::change_selected_keys_kind(
546 self.handle,
547 MessageDirection::ToWidget,
548 CurveKeyKind::Cubic {
549 left_tangent: 0.0,
550 right_tangent: 0.0,
551 },
552 ));
553 } else if message.destination() == self.context_menu.add_key {
554 let screen_pos = ui.node(self.context_menu.widget).screen_position();
555 ui.send_message(CurveEditorMessage::add_key(
556 self.handle,
557 MessageDirection::ToWidget,
558 screen_pos,
559 ));
560 } else if message.destination() == self.context_menu.zoom_to_fit {
561 ui.send_message(CurveEditorMessage::zoom_to_fit(
562 self.handle,
563 MessageDirection::ToWidget,
564 ));
565 }
566 }
567 }
568}
569
570fn draw_cubic(
571 left_pos: Vector2<f32>,
572 left_tangent: f32,
573 right_pos: Vector2<f32>,
574 right_tangent: f32,
575 steps: usize,
576 ctx: &mut DrawingContext,
577) {
578 let mut prev = left_pos;
579 for i in 0..steps {
580 let t = i as f32 / (steps - 1) as f32;
581 let middle_x = lerpf(left_pos.x, right_pos.x, t);
582 let middle_y = cubicf(left_pos.y, right_pos.y, t, left_tangent, right_tangent);
583 let pt = Vector2::new(middle_x, middle_y);
584 ctx.push_line(prev, pt, 1.0);
585 prev = pt;
586 }
587}
588
589fn round_to_step(x: f32, step: f32) -> f32 {
590 x - x % step
591}
592
593impl CurveEditor {
594 fn update_matrices(&self) {
595 let vp = Vector2::new(self.view_position.x, -self.view_position.y);
596 self.view_matrix.set(
597 Matrix3::new_nonuniform_scaling_wrt_point(
598 &Vector2::new(self.zoom, self.zoom),
599 &Point2::from(self.actual_size().scale(0.5)),
600 ) * Matrix3::new_translation(&vp),
601 );
602
603 let screen_bounds = self.screen_bounds();
604 self.screen_matrix.set(
605 Matrix3::new_translation(&screen_bounds.position)
606 * Matrix3::new_translation(&Vector2::new(0.0, screen_bounds.h()))
608 * Matrix3::new_nonuniform_scaling(&Vector2::new(1.0, -1.0))
609 * self.view_matrix.get(),
610 );
611
612 self.inv_screen_matrix
613 .set(self.view_matrix.get().try_inverse().unwrap_or_default());
614 }
615
616 pub fn point_to_view_space(&self, point: Vector2<f32>) -> Vector2<f32> {
618 self.view_matrix
619 .get()
620 .transform_point(&Point2::from(point))
621 .coords
622 }
623
624 pub fn point_to_screen_space(&self, point: Vector2<f32>) -> Vector2<f32> {
626 self.screen_matrix
627 .get()
628 .transform_point(&Point2::from(point))
629 .coords
630 }
631
632 pub fn vector_to_screen_space(&self, vector: Vector2<f32>) -> Vector2<f32> {
634 (self.screen_matrix.get() * Vector3::new(vector.x, vector.y, 0.0)).xy()
635 }
636
637 pub fn point_to_local_space(&self, point: Vector2<f32>) -> Vector2<f32> {
639 let mut p = point - self.screen_position();
640 p.y = self.actual_size().y - p.y;
641 self.view_matrix
642 .get()
643 .try_inverse()
644 .unwrap_or_default()
645 .transform_point(&Point2::from(p))
646 .coords
647 }
648
649 fn sort_keys(&mut self) {
650 self.key_container.sort_keys();
651 }
652
653 fn set_selection(&mut self, selection: Option<Selection>, ui: &UserInterface) {
654 self.selection = selection;
655
656 ui.send_message(WidgetMessage::enabled(
657 self.context_menu.remove,
658 MessageDirection::ToWidget,
659 self.selection.is_some(),
660 ));
661
662 ui.send_message(WidgetMessage::enabled(
663 self.context_menu.key,
664 MessageDirection::ToWidget,
665 self.selection.is_some(),
666 ));
667 }
668
669 fn remove_selection(&mut self, ui: &mut UserInterface) {
670 if let Some(Selection::Keys { keys }) = self.selection.as_ref() {
671 for &id in keys {
672 self.key_container.remove(id);
673 }
674
675 self.set_selection(None, ui);
676
677 self.send_curve(ui);
679 }
680 }
681
682 fn change_selected_keys_kind(&mut self, kind: CurveKeyKind, ui: &mut UserInterface) {
683 if let Some(Selection::Keys { keys }) = self.selection.as_ref() {
684 for key in keys {
685 self.key_container.key_mut(*key).unwrap().kind = kind.clone();
686 }
687
688 self.send_curve(ui);
689 }
690 }
691
692 fn pick(&self, pos: Vector2<f32>) -> Option<PickResult> {
694 for (i, key) in self.key_container.keys().iter().enumerate() {
697 let screen_pos = self.point_to_screen_space(key.position);
698 let bounds = Rect::new(
699 screen_pos.x - self.key_size * 0.5,
700 screen_pos.y - self.key_size * 0.5,
701 self.key_size,
702 self.key_size,
703 );
704 if bounds.contains(pos) {
705 return Some(PickResult::Key(i));
706 }
707
708 if let CurveKeyKind::Cubic {
710 left_tangent,
711 right_tangent,
712 } = key.kind
713 {
714 let left_handle_pos = self.tangent_screen_position(
715 wrap_angle(left_tangent.atan()) + std::f32::consts::PI,
716 key.position,
717 );
718
719 if (left_handle_pos - pos).norm() <= self.key_size * 0.5 {
720 return Some(PickResult::LeftTangent(i));
721 }
722
723 let right_handle_pos =
724 self.tangent_screen_position(wrap_angle(right_tangent.atan()), key.position);
725
726 if (right_handle_pos - pos).norm() <= self.key_size * 0.5 {
727 return Some(PickResult::RightTangent(i));
728 }
729 }
730 }
731 None
732 }
733
734 fn tangent_screen_position(&self, angle: f32, key_position: Vector2<f32>) -> Vector2<f32> {
735 self.point_to_screen_space(key_position)
736 + Vector2::new(angle.cos(), angle.sin()).scale(self.handle_radius)
737 }
738
739 fn send_curve(&self, ui: &UserInterface) {
740 ui.send_message(CurveEditorMessage::sync(
741 self.handle,
742 MessageDirection::FromWidget,
743 self.key_container.curve(),
744 ));
745 }
746
747 fn draw_background(&self, ctx: &mut DrawingContext) {
748 let screen_bounds = self.screen_bounds();
749 ctx.push_rect_filled(&screen_bounds, None);
751 ctx.commit(screen_bounds, self.background(), CommandTexture::None, None);
752 }
753
754 fn draw_grid(&self, ctx: &mut DrawingContext) {
755 let screen_bounds = self.screen_bounds();
756
757 let step_size = 50.0 / self.zoom.clamp(0.001, 1000.0);
758
759 let mut local_left_bottom = self.point_to_local_space(screen_bounds.left_top_corner());
760 let local_left_bottom_n = local_left_bottom;
761 local_left_bottom.x = round_to_step(local_left_bottom.x, step_size);
762 local_left_bottom.y = round_to_step(local_left_bottom.y, step_size);
763
764 let mut local_right_top = self.point_to_local_space(screen_bounds.right_bottom_corner());
765 local_right_top.x = round_to_step(local_right_top.x, step_size);
766 local_right_top.y = round_to_step(local_right_top.y, step_size);
767
768 let w = (local_right_top.x - local_left_bottom.x).abs();
769 let h = (local_right_top.y - local_left_bottom.y).abs();
770
771 let nw = ((w / step_size).ceil()) as usize;
772 let nh = ((h / step_size).ceil()) as usize;
773
774 for ny in 0..=nh {
775 let k = ny as f32 / (nh) as f32;
776 let y = local_left_bottom.y - k * h;
777 ctx.push_line(
778 self.point_to_screen_space(Vector2::new(local_left_bottom.x - step_size, y)),
779 self.point_to_screen_space(Vector2::new(local_right_top.x + step_size, y)),
780 1.0,
781 );
782 }
783
784 for nx in 0..=nw {
785 let k = nx as f32 / (nw) as f32;
786 let x = local_left_bottom.x + k * w;
787 ctx.push_line(
788 self.point_to_screen_space(Vector2::new(x, local_left_bottom.y + step_size)),
789 self.point_to_screen_space(Vector2::new(x, local_right_top.y - step_size)),
790 1.0,
791 );
792 }
793
794 let vb = self.point_to_screen_space(Vector2::new(0.0, -10e6));
796 let ve = self.point_to_screen_space(Vector2::new(0.0, 10e6));
797 ctx.push_line(vb, ve, 2.0);
798
799 let hb = self.point_to_screen_space(Vector2::new(-10e6, 0.0));
800 let he = self.point_to_screen_space(Vector2::new(10e6, 0.0));
801 ctx.push_line(hb, he, 2.0);
802
803 ctx.commit(
804 screen_bounds,
805 self.grid_brush.clone(),
806 CommandTexture::None,
807 None,
808 );
809
810 let mut text = self.text.borrow_mut();
812 for ny in 0..=nh {
813 let k = ny as f32 / (nh) as f32;
814 let y = local_left_bottom.y - k * h;
815 text.set_text(format!("{:.1}", y)).build();
816 ctx.draw_text(
817 screen_bounds,
818 self.point_to_screen_space(Vector2::new(local_left_bottom_n.x, y)),
819 &text,
820 );
821 }
822
823 for nx in 0..=nw {
824 let k = nx as f32 / (nw) as f32;
825 let x = local_left_bottom.x + k * w;
826 text.set_text(format!("{:.1}", x)).build();
827 ctx.draw_text(
828 screen_bounds,
829 self.point_to_screen_space(Vector2::new(x, 0.0)),
830 &text,
831 );
832 }
833 }
834
835 fn draw_curve(&self, ctx: &mut DrawingContext) {
836 let screen_bounds = self.screen_bounds();
837 let draw_keys = self.key_container.keys();
838
839 if let Some(first) = draw_keys.first() {
840 let screen_pos = self.point_to_screen_space(first.position);
841 ctx.push_line(Vector2::new(0.0, screen_pos.y), screen_pos, 1.0);
842 }
843 if let Some(last) = draw_keys.last() {
844 let screen_pos = self.point_to_screen_space(last.position);
845 ctx.push_line(
846 screen_pos,
847 Vector2::new(screen_bounds.x() + screen_bounds.w(), screen_pos.y),
848 1.0,
849 );
850 }
851
852 for pair in draw_keys.windows(2) {
853 let left = &pair[0];
854 let right = &pair[1];
855
856 let left_pos = self.point_to_screen_space(left.position);
857 let right_pos = self.point_to_screen_space(right.position);
858
859 let steps = ((right_pos.x - left_pos.x).abs() / 2.0) as usize;
860
861 match (&left.kind, &right.kind) {
862 (CurveKeyKind::Constant, CurveKeyKind::Constant)
864 | (CurveKeyKind::Constant, CurveKeyKind::Linear)
865 | (CurveKeyKind::Constant, CurveKeyKind::Cubic { .. }) => {
866 ctx.push_line(left_pos, Vector2::new(right_pos.x, left_pos.y), 1.0);
867 ctx.push_line(Vector2::new(right_pos.x, left_pos.y), right_pos, 1.0);
868 }
869
870 (CurveKeyKind::Linear, CurveKeyKind::Constant)
872 | (CurveKeyKind::Linear, CurveKeyKind::Linear)
873 | (CurveKeyKind::Linear, CurveKeyKind::Cubic { .. }) => {
874 ctx.push_line(left_pos, right_pos, 1.0)
875 }
876
877 (
879 CurveKeyKind::Cubic {
880 right_tangent: left_tangent,
881 ..
882 },
883 CurveKeyKind::Constant,
884 )
885 | (
886 CurveKeyKind::Cubic {
887 right_tangent: left_tangent,
888 ..
889 },
890 CurveKeyKind::Linear,
891 ) => draw_cubic(left_pos, *left_tangent, right_pos, 0.0, steps, ctx),
892
893 (
895 CurveKeyKind::Cubic {
896 right_tangent: left_tangent,
897 ..
898 },
899 CurveKeyKind::Cubic {
900 left_tangent: right_tangent,
901 ..
902 },
903 ) => draw_cubic(
904 left_pos,
905 *left_tangent,
906 right_pos,
907 *right_tangent,
908 steps,
909 ctx,
910 ),
911 }
912 }
913 ctx.commit(screen_bounds, self.foreground(), CommandTexture::None, None);
914 }
915
916 fn draw_keys(&self, ctx: &mut DrawingContext) {
917 let screen_bounds = self.screen_bounds();
918 let keys_to_draw = self.key_container.keys();
919
920 for (i, key) in keys_to_draw.iter().enumerate() {
921 let origin = self.point_to_screen_space(key.position);
922 let size = Vector2::new(self.key_size, self.key_size);
923 let half_size = size.scale(0.5);
924
925 ctx.push_rect_filled(
926 &Rect::new(
927 origin.x - half_size.x,
928 origin.y - half_size.x,
929 size.x,
930 size.y,
931 ),
932 None,
933 );
934
935 let mut selected = false;
936 if let Some(selection) = self.selection.as_ref() {
937 match selection {
938 Selection::Keys { keys } => {
939 selected = keys.contains(&key.id);
940 }
941 Selection::LeftTangent { key } | Selection::RightTangent { key } => {
942 selected = i == *key;
943 }
944 }
945 }
946
947 if selected {
949 let (show_left, show_right) = match keys_to_draw.get(i.wrapping_sub(1)) {
950 Some(left) => match (&left.kind, &key.kind) {
951 (CurveKeyKind::Cubic { .. }, CurveKeyKind::Cubic { .. }) => (true, true),
952 (CurveKeyKind::Linear, CurveKeyKind::Cubic { .. })
953 | (CurveKeyKind::Constant, CurveKeyKind::Cubic { .. }) => (false, true),
954 _ => (false, false),
955 },
956 None => match key.kind {
957 CurveKeyKind::Cubic { .. } => (false, true),
958 _ => (false, false),
959 },
960 };
961
962 if let CurveKeyKind::Cubic {
963 left_tangent,
964 right_tangent,
965 } = key.kind
966 {
967 if show_left {
968 let left_handle_pos = self.tangent_screen_position(
969 wrap_angle(left_tangent.atan()) + std::f32::consts::PI,
970 key.position,
971 );
972 ctx.push_line(origin, left_handle_pos, 1.0);
973 ctx.push_circle(
974 left_handle_pos,
975 self.key_size * 0.5,
976 6,
977 Default::default(),
978 );
979 }
980
981 if show_right {
982 let right_handle_pos = self.tangent_screen_position(
983 wrap_angle(right_tangent.atan()),
984 key.position,
985 );
986 ctx.push_line(origin, right_handle_pos, 1.0);
987 ctx.push_circle(
988 right_handle_pos,
989 self.key_size * 0.5,
990 6,
991 Default::default(),
992 );
993 }
994 }
995 }
996
997 ctx.commit(
998 screen_bounds,
999 if selected {
1000 self.selected_key_brush.clone()
1001 } else {
1002 self.key_brush.clone()
1003 },
1004 CommandTexture::None,
1005 None,
1006 );
1007 }
1008 }
1009
1010 fn draw_operation(&self, ctx: &mut DrawingContext) {
1011 if let Some(OperationContext::BoxSelection { min, max, .. }) =
1012 self.operation_context.as_ref()
1013 {
1014 let min = self.point_to_screen_space(min.get());
1015 let max = self.point_to_screen_space(max.get());
1016 let rect = Rect::new(min.x, min.y, max.x - min.x, max.y - min.y);
1017
1018 ctx.push_rect(&rect, 1.0);
1019 ctx.commit(
1020 self.screen_bounds(),
1021 Brush::Solid(Color::WHITE),
1022 CommandTexture::None,
1023 None,
1024 );
1025 }
1026 }
1027}
1028
1029pub struct CurveEditorBuilder {
1030 widget_builder: WidgetBuilder,
1031 curve: Curve,
1032 view_position: Vector2<f32>,
1033 zoom: f32,
1034}
1035
1036impl CurveEditorBuilder {
1037 pub fn new(widget_builder: WidgetBuilder) -> Self {
1038 Self {
1039 widget_builder,
1040 curve: Default::default(),
1041 view_position: Default::default(),
1042 zoom: 1.0,
1043 }
1044 }
1045
1046 pub fn with_curve(mut self, curve: Curve) -> Self {
1047 self.curve = curve;
1048 self
1049 }
1050
1051 pub fn with_zoom(mut self, zoom: f32) -> Self {
1052 self.zoom = zoom;
1053 self
1054 }
1055
1056 pub fn with_view_position(mut self, view_position: Vector2<f32>) -> Self {
1057 self.view_position = view_position;
1058 self
1059 }
1060
1061 pub fn build(mut self, ctx: &mut BuildContext) -> Handle<UiNode> {
1062 let keys = KeyContainer::from(&self.curve);
1063
1064 let add_key;
1065 let remove;
1066 let make_constant;
1067 let make_linear;
1068 let make_cubic;
1069 let key;
1070 let zoom_to_fit;
1071 let context_menu = PopupBuilder::new(WidgetBuilder::new())
1072 .with_content(
1073 StackPanelBuilder::new(
1074 WidgetBuilder::new()
1075 .with_child({
1076 add_key = MenuItemBuilder::new(WidgetBuilder::new())
1077 .with_content(MenuItemContent::text("Add Key"))
1078 .build(ctx);
1079 add_key
1080 })
1081 .with_child({
1082 remove = MenuItemBuilder::new(WidgetBuilder::new().with_enabled(false))
1083 .with_content(MenuItemContent::text("Remove"))
1084 .build(ctx);
1085 remove
1086 })
1087 .with_child({
1088 key = MenuItemBuilder::new(WidgetBuilder::new().with_enabled(false))
1089 .with_content(MenuItemContent::text("Key..."))
1090 .with_items(vec![
1091 {
1092 make_constant = MenuItemBuilder::new(WidgetBuilder::new())
1093 .with_content(MenuItemContent::text("Constant"))
1094 .build(ctx);
1095 make_constant
1096 },
1097 {
1098 make_linear = MenuItemBuilder::new(WidgetBuilder::new())
1099 .with_content(MenuItemContent::text("Linear"))
1100 .build(ctx);
1101 make_linear
1102 },
1103 {
1104 make_cubic = MenuItemBuilder::new(WidgetBuilder::new())
1105 .with_content(MenuItemContent::text("Cubic"))
1106 .build(ctx);
1107 make_cubic
1108 },
1109 ])
1110 .build(ctx);
1111 key
1112 })
1113 .with_child({
1114 zoom_to_fit = MenuItemBuilder::new(WidgetBuilder::new())
1115 .with_content(MenuItemContent::text("Zoom To Fit"))
1116 .build(ctx);
1117 zoom_to_fit
1118 }),
1119 )
1120 .build(ctx),
1121 )
1122 .build(ctx);
1123
1124 if self.widget_builder.foreground.is_none() {
1125 self.widget_builder.foreground = Some(Brush::Solid(Color::opaque(130, 130, 130)))
1126 }
1127
1128 let editor = CurveEditor {
1129 widget: self
1130 .widget_builder
1131 .with_context_menu(context_menu)
1132 .with_preview_messages(true)
1133 .build(),
1134 key_container: keys,
1135 zoom: 1.0,
1136 view_position: Default::default(),
1137 view_matrix: Default::default(),
1138 screen_matrix: Default::default(),
1139 inv_screen_matrix: Default::default(),
1140 key_brush: Brush::Solid(Color::opaque(140, 140, 140)),
1141 selected_key_brush: Brush::Solid(Color::opaque(220, 220, 220)),
1142 key_size: 8.0,
1143 handle_radius: 36.0,
1144 operation_context: None,
1145 grid_brush: Brush::Solid(Color::from_rgba(110, 110, 110, 50)),
1146 selection: None,
1147 text: RefCell::new(
1148 FormattedTextBuilder::new()
1149 .with_brush(Brush::Solid(Color::opaque(100, 100, 100)))
1150 .with_font(crate::DEFAULT_FONT.clone())
1151 .build(),
1152 ),
1153 context_menu: ContextMenu {
1154 widget: context_menu,
1155 add_key,
1156 remove,
1157 make_constant,
1158 make_linear,
1159 make_cubic,
1160 key,
1161 zoom_to_fit,
1162 },
1163 };
1164
1165 ctx.add_node(UiNode::new(editor))
1166 }
1167}