Skip to main content

fyrox_ui/curve/
mod.rs

1// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
2//
3// Permission is hereby granted, free of charge, to any person obtaining a copy
4// of this software and associated documentation files (the "Software"), to deal
5// in the Software without restriction, including without limitation the rights
6// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7// copies of the Software, and to permit persons to whom the Software is
8// furnished to do so, subject to the following conditions:
9//
10// The above copyright notice and this permission notice shall be included in all
11// copies or substantial portions of the Software.
12//
13// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19// SOFTWARE.
20
21use crate::message::{CursorIcon, MessageData};
22use crate::style::resource::StyleResourceExt;
23use crate::style::Style;
24use crate::{
25    brush::Brush,
26    core::{
27        algebra::{Matrix3, Point2, SimdPartialOrd, Vector2, Vector3},
28        color::Color,
29        math::{
30            cubicf,
31            curve::{Curve, CurveKeyKind},
32            lerpf, wrap_angle, Rect,
33        },
34        parking_lot::{MappedMutexGuard, Mutex, MutexGuard},
35        pool::Handle,
36        reflect::prelude::*,
37        type_traits::prelude::*,
38        uuid_provider,
39        visitor::prelude::*,
40        SafeLock,
41    },
42    curve::key::{CurveKeyView, CurveKeyViewContainer},
43    draw::{CommandTexture, Draw, DrawingContext},
44    formatted_text::{FormattedText, FormattedTextBuilder},
45    grid::{Column, GridBuilder, Row},
46    menu::{ContextMenuBuilder, MenuItemBuilder, MenuItemContent, MenuItemMessage},
47    message::{ButtonState, KeyCode, MessageDirection, MouseButton, UiMessage},
48    numeric::{NumericUpDownBuilder, NumericUpDownMessage},
49    popup::PopupBuilder,
50    stack_panel::StackPanelBuilder,
51    text::TextBuilder,
52    widget::{Widget, WidgetBuilder, WidgetMessage},
53    BuildContext, Control, RcUiNodeHandle, Thickness, UiNode, UserInterface, VerticalAlignment,
54};
55use fxhash::FxHashSet;
56
57use crate::grid::Grid;
58use crate::menu::MenuItem;
59use crate::numeric::NumericUpDown;
60use fyrox_graph::constructor::{ConstructorProvider, GraphNodeConstructor};
61use fyrox_graph::SceneGraph;
62use std::{
63    cell::{Cell, RefCell},
64    sync::mpsc::Sender,
65};
66
67pub mod key;
68
69#[derive(Debug, Clone, PartialEq)]
70pub enum CurveEditorMessage {
71    SyncBackground(Vec<Curve>),
72    Sync(Vec<Curve>),
73    Colorize(Vec<(Uuid, Brush)>),
74    ViewPosition(Vector2<f32>),
75    Zoom(Vector2<f32>),
76    ZoomToFit {
77        /// Should the zoom to fit be performed on some of the next update cycle (up to 10 frames delay), or immediately when
78        /// processing the message.
79        after_layout: bool,
80    },
81    HighlightZones(Vec<HighlightZone>),
82
83    // Internal messages. Use only when you know what you're doing.
84    // These are internal because you must use Sync message to request changes
85    // in the curve editor.
86    ChangeSelectedKeysKind(CurveKeyKind),
87    ChangeSelectedKeysValue(f32),
88    ChangeSelectedKeysLocation(f32),
89    RemoveSelection,
90    CopySelection,
91    PasteSelection,
92    // Position in screen coordinates.
93    AddKey(Vector2<f32>),
94    ShowBackgroundCurves(bool),
95}
96
97impl MessageData for CurveEditorMessage {
98    fn need_perform_layout(&self) -> bool {
99        matches!(self, Self::ZoomToFit { .. })
100    }
101}
102
103/// Highlight zone in values space.
104#[derive(Clone, Debug, PartialEq, Visit, Reflect, Default)]
105pub struct HighlightZone {
106    pub rect: Rect<f32>,
107    pub brush: Brush,
108}
109
110#[derive(Debug, Default)]
111pub struct CurveTransformCell(Mutex<CurveTransform>);
112
113impl Clone for CurveTransformCell {
114    fn clone(&self) -> Self {
115        Self(Mutex::new(self.0.safe_lock().clone()))
116    }
117}
118
119impl CurveTransformCell {
120    /// Position of the center of the curve editor in the curve coordinate space.
121    pub fn position(&self) -> Vector2<f32> {
122        self.0.safe_lock().position
123    }
124    /// Scape of the curve editor: multiply curve space units by this to get screen space units.
125    pub fn scale(&self) -> Vector2<f32> {
126        self.0.safe_lock().scale
127    }
128    /// Location of the curve editor on the screen, in screen space units.
129    pub fn bounds(&self) -> Rect<f32> {
130        self.0.safe_lock().bounds
131    }
132    /// Modify the current position. Call this when the center of the curve view should change.
133    pub fn set_position(&self, position: Vector2<f32>) {
134        self.0.safe_lock().position = position
135    }
136    /// Modify the current zoom of the curve view.
137    pub fn set_scale(&self, scale: Vector2<f32>) {
138        self.0.safe_lock().scale = scale;
139    }
140    /// Update the bounds of the curve view. Call this to ensure the CurveTransform accurately
141    /// reflects the actual size of the widget being drawn.
142    pub fn set_bounds(&self, bounds: Rect<f32>) {
143        self.0.safe_lock().bounds = bounds;
144    }
145    /// Just like [CurveTransformCell::y_step_iter] but for x-coordinates.
146    /// Iterate through a list of x-coordinates across the width of the bounds.
147    /// The x-coordinates are in curve-space, but their distance apart should be
148    /// at least `grid_size` in screen-space.
149    ///
150    /// This iterator indates where grid lines should be drawn to make a curve easier
151    /// to read for the user.
152    pub fn x_step_iter(&self, grid_size: f32) -> StepIterator {
153        self.0.safe_lock().x_step_iter(grid_size)
154    }
155    /// Just like [CurveTransformCell::x_step_iter] but for y-coordinates.
156    /// Iterate through a list of y-coordinates across the width of the bounds.
157    /// The y-coordinates are in curve-space, but their distance apart should be
158    /// at least `grid_size` in screen-space.
159    ///
160    /// This iterator indates where grid lines should be drawn to make a curve easier
161    /// to read for the user.
162    pub fn y_step_iter(&self, grid_size: f32) -> StepIterator {
163        self.0.safe_lock().y_step_iter(grid_size)
164    }
165    /// Construct the transformation matrices to reflect the current position, scale, and bounds.
166    pub fn update_transform(&self) {
167        self.0.safe_lock().update_transform();
168    }
169    /// Transform a point on the curve into a point in the local coordinate space of the widget.
170    pub fn curve_to_local(&self) -> MappedMutexGuard<Matrix3<f32>> {
171        MutexGuard::map(self.0.safe_lock(), |t| &mut t.curve_to_local)
172    }
173    /// Transform a point on the curve into a point on the screen.
174    pub fn curve_to_screen(&self) -> MappedMutexGuard<Matrix3<f32>> {
175        MutexGuard::map(self.0.safe_lock(), |t| &mut t.curve_to_screen)
176    }
177    /// Transform a point in the local coordinate space of the widget into a point in the coordinate space of the curve.
178    /// After the transformation, the x-coordinate could be a key location and the y-coordinate could be a key value.
179    /// Y-coordinates are flipped so that positive-y becomes the up direction.
180    pub fn local_to_curve(&self) -> MappedMutexGuard<Matrix3<f32>> {
181        MutexGuard::map(self.0.safe_lock(), |t| &mut t.local_to_curve)
182    }
183    /// Transform a point on the screen into a point in the coordinate space of the curve.
184    /// After the transformation, the x-coordinate could be a key location and the y-coordinate could be a key value.
185    /// Y-coordinates are flipped so that positive-y becomes the up direction.
186    pub fn screen_to_curve(&self) -> MappedMutexGuard<Matrix3<f32>> {
187        MutexGuard::map(self.0.safe_lock(), |t| &mut t.screen_to_curve)
188    }
189}
190
191/// Default grid size when not otherwise specified
192pub const STANDARD_GRID_SIZE: f32 = 50.0;
193/// Step sizes are more readable when they are easily recognizable.
194/// This is a list of step sizes to round up to when choosing a step size.
195/// The values in this list are meaningless; they are just intended to be convenient and round and easy to read.
196const STANDARD_STEP_SIZES: [f32; 10] = [0.1, 0.2, 0.5, 1.0, 2.0, 5.0, 10.0, 25.0, 50.0, 100.0];
197
198/// Round the given step size up to the next standard step size, if possible.
199fn standardize_step(step: f32) -> f32 {
200    STANDARD_STEP_SIZES
201        .iter()
202        .copied()
203        .find(|x| step <= *x)
204        .unwrap_or(step)
205}
206
207/// This object represents the transformation curve coordinates into
208/// the local coordinates of the curve editor widget and into the coordinates
209/// of the screen. Using this object allows other widgets to align themselves
210/// perfectly with the coordinates of a curve editor.
211///
212/// Since widgets are not mutable during layout and rendering, a CurveTransform
213/// is intended to be used within a [CurveTransformCell] which provides interior mutability.
214#[derive(Clone, Debug)]
215pub struct CurveTransform {
216    /// Position of the center of the curve editor in the curve coordinate space.
217    pub position: Vector2<f32>,
218    /// Scape of the curve editor: multiply curve space units by this to get screen space units.
219    pub scale: Vector2<f32>,
220    /// Location of the curve editor on the screen, in screen space units.
221    pub bounds: Rect<f32>,
222    /// Transform a point on a curve into a point in the local space
223    /// of the curve editor. Before applying this transformation, (0,0) is
224    /// the origin of the curve, and positive-y is up.
225    /// After the transform, (0,0) is the top-left corner of the editor,
226    /// and positive-y is down.
227    pub curve_to_local: Matrix3<f32>,
228    /// The inverse of `curve_to_local`, transforming a point in the local space
229    /// of the editor widget into a point in the space of the curve.
230    pub local_to_curve: Matrix3<f32>,
231    /// Transform a point on the screen into a point in the space of the curve.
232    /// For example, a mouse click position would be transformed into the corresponding
233    /// (x,y) coordinates of a cure key that would result from the click.
234    pub screen_to_curve: Matrix3<f32>,
235    /// Transform a point on the curve into a point on the screen.
236    pub curve_to_screen: Matrix3<f32>,
237}
238
239impl Default for CurveTransform {
240    fn default() -> Self {
241        Self {
242            position: Vector2::default(),
243            scale: Vector2::new(1.0, 1.0),
244            bounds: Rect::default(),
245            curve_to_local: Matrix3::identity(),
246            local_to_curve: Matrix3::identity(),
247            screen_to_curve: Matrix3::identity(),
248            curve_to_screen: Matrix3::identity(),
249        }
250    }
251}
252
253impl CurveTransform {
254    /// Construct the transformations matrices for the current position, scale, and bounds.
255    pub fn update_transform(&mut self) {
256        let bounds = self.bounds;
257        let local_center = bounds.size.scale(0.5);
258        let mut curve_to_local = Matrix3::<f32>::identity();
259        // Translate the view position to be at (0,0) in the curve space.
260        curve_to_local.append_translation_mut(&-self.position);
261        // Scale from curve units into local space units.
262        curve_to_local.append_nonuniform_scaling_mut(&self.scale);
263        // Flip y-positive from pointing up to pointing down
264        curve_to_local.append_nonuniform_scaling_mut(&Vector2::new(1.0, -1.0));
265        // Translate (0,0) to the center of the widget in local space.
266        curve_to_local.append_translation_mut(&local_center);
267        // Find the inverse transform matrix automatically.
268        let local_to_curve = curve_to_local.try_inverse().unwrap_or_default();
269        let mut curve_to_screen = curve_to_local;
270        // Translate the local (0,0) to the top-left corner of the widget in screen coordinates.
271        curve_to_screen.append_translation_mut(&bounds.position);
272        // Find the screen-to-curve matrix automatically from the curve-to-screen matrix.
273        let screen_to_curve = curve_to_screen.try_inverse().unwrap_or_default();
274        *self = CurveTransform {
275            curve_to_local,
276            local_to_curve,
277            screen_to_curve,
278            curve_to_screen,
279            ..*self
280        };
281    }
282    /// Just like [CurveTransform::y_step_iter] but for x-coordinates.
283    /// Iterate through a list of x-coordinates across the width of the bounds.
284    /// The x-coordinates are in curve-space, but their distance apart should be
285    /// at least `grid_size` in screen-space.
286    ///
287    /// This iterator indates where grid lines should be drawn to make a curve easier
288    /// to read for the user.
289    pub fn x_step_iter(&self, grid_size: f32) -> StepIterator {
290        let zoom = self.scale;
291        let step_size = grid_size / zoom.x.clamp(0.001, 1000.0);
292        let screen_left = self.bounds.position.x;
293        let screen_right = self.bounds.position.x + self.bounds.size.x;
294        let left = self
295            .screen_to_curve
296            .transform_point(&Point2::new(screen_left, 0.0))
297            .x;
298        let right = self
299            .screen_to_curve
300            .transform_point(&Point2::new(screen_right, 0.0))
301            .x;
302        StepIterator::new(standardize_step(step_size), left, right)
303    }
304    /// Just like [CurveTransform::x_step_iter] but for y-coordinates.
305    /// Iterate through a list of y-coordinates across the width of the bounds.
306    /// The y-coordinates are in curve-space, but their distance apart should be
307    /// at least `grid_size` in screen-space.
308    ///
309    /// This iterator indates where grid lines should be drawn to make a curve easier
310    /// to read for the user.
311    pub fn y_step_iter(&self, grid_size: f32) -> StepIterator {
312        let zoom = self.scale;
313        let step_size = grid_size / zoom.y.clamp(0.001, 1000.0);
314        let screen_top = self.bounds.position.y;
315        let screen_bottom = self.bounds.position.y + self.bounds.size.y;
316        let start = self
317            .screen_to_curve
318            .transform_point(&Point2::new(0.0, screen_bottom))
319            .y;
320        let end = self
321            .screen_to_curve
322            .transform_point(&Point2::new(0.0, screen_top))
323            .y;
324        StepIterator::new(standardize_step(step_size), start, end)
325    }
326}
327
328/// Iterate through f32 values stepping by `step_size`. Each value is a multiple of `step_size`.
329#[derive(Debug, Clone)]
330pub struct StepIterator {
331    pub step_size: f32,
332    index: isize,
333    end: isize,
334}
335
336impl StepIterator {
337    /// Construct an iterator that starts at or before `start` and ends at or after `end`.
338    /// The intention is to cover the whole range of `start` to `end` at least.
339    pub fn new(step: f32, start: f32, end: f32) -> Self {
340        Self {
341            step_size: step,
342            index: (start / step).floor() as isize,
343            end: ((end / step).ceil() as isize).saturating_add(1),
344        }
345    }
346}
347
348impl Iterator for StepIterator {
349    type Item = f32;
350
351    fn next(&mut self) -> Option<Self::Item> {
352        if self.index >= self.end {
353            return None;
354        }
355        let value = (self.index as f32) * self.step_size;
356        self.index += 1;
357        Some(value)
358    }
359}
360
361#[derive(Default, Clone, Visit, Reflect, Debug)]
362pub struct CurvesContainer {
363    curves: Vec<CurveKeyViewContainer>,
364}
365
366impl CurvesContainer {
367    pub fn from_native(brush: Brush, curves: &[Curve]) -> Self {
368        Self {
369            curves: curves
370                .iter()
371                .map(|curve| CurveKeyViewContainer::new(curve, brush.clone()))
372                .collect::<Vec<_>>(),
373        }
374    }
375
376    pub fn to_native(&self) -> Vec<Curve> {
377        self.curves
378            .iter()
379            .map(|view| view.curve())
380            .collect::<Vec<_>>()
381    }
382
383    pub fn container_of(&self, key_id: Uuid) -> Option<&CurveKeyViewContainer> {
384        self.curves
385            .iter()
386            .find(|curve| curve.key_ref(key_id).is_some())
387    }
388
389    pub fn key_ref(&self, uuid: Uuid) -> Option<&CurveKeyView> {
390        // TODO: This will be slow for curves with lots of keys.
391        self.curves.iter().find_map(|keys| keys.key_ref(uuid))
392    }
393
394    pub fn key_mut(&mut self, uuid: Uuid) -> Option<&mut CurveKeyView> {
395        // TODO: This will be slow for curves with lots of keys.
396        self.curves.iter_mut().find_map(|keys| keys.key_mut(uuid))
397    }
398
399    pub fn remove(&mut self, uuid: Uuid) {
400        for curve in self.curves.iter_mut() {
401            curve.remove(uuid);
402        }
403    }
404
405    pub fn iter(&self) -> impl Iterator<Item = &CurveKeyViewContainer> {
406        self.curves.iter()
407    }
408
409    pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut CurveKeyViewContainer> {
410        self.curves.iter_mut()
411    }
412}
413
414#[derive(Default, Clone, Visit, Reflect, Debug, ComponentProvider)]
415#[reflect(derived_type = "UiNode")]
416pub struct CurveEditor {
417    widget: Widget,
418    background_curves: CurvesContainer,
419    curves: CurvesContainer,
420    #[visit(skip)]
421    #[reflect(hidden)]
422    curve_transform: CurveTransformCell,
423    key_brush: Brush,
424    selected_key_brush: Brush,
425    key_size: f32,
426    grid_brush: Brush,
427    background_curve_brush: Brush,
428    #[visit(skip)]
429    #[reflect(hidden)]
430    operation_context: Option<OperationContext>,
431    #[visit(skip)]
432    #[reflect(hidden)]
433    selection: Option<Selection>,
434    handle_radius: f32,
435    context_menu: ContextMenu,
436    #[visit(skip)]
437    #[reflect(hidden)]
438    text: RefCell<FormattedText>,
439    view_bounds: Option<Rect<f32>>,
440    show_x_values: bool,
441    show_y_values: bool,
442    grid_size: Vector2<f32>,
443    min_zoom: Vector2<f32>,
444    max_zoom: Vector2<f32>,
445    highlight_zones: Vec<HighlightZone>,
446    #[visit(skip)]
447    #[reflect(hidden)]
448    zoom_to_fit_timer: Option<usize>,
449    #[visit(skip)]
450    #[reflect(hidden)]
451    clipboard: Vec<(Vector2<f32>, CurveKeyKind)>,
452    show_background_curves: bool,
453}
454
455impl ConstructorProvider<UiNode, UserInterface> for CurveEditor {
456    fn constructor() -> GraphNodeConstructor<UiNode, UserInterface> {
457        GraphNodeConstructor::new::<Self>()
458            .with_variant("Curve Editor", |ui| {
459                CurveEditorBuilder::new(WidgetBuilder::new().with_name("Curve Editor"))
460                    .build(&mut ui.build_ctx())
461                    .to_base()
462                    .into()
463            })
464            .with_group("Input")
465    }
466}
467
468crate::define_widget_deref!(CurveEditor);
469
470#[derive(Default, Clone, Visit, Reflect, Debug)]
471struct ContextMenu {
472    widget: RcUiNodeHandle,
473    add_key: Handle<MenuItem>,
474    remove: Handle<MenuItem>,
475    key: Handle<MenuItem>,
476    make_constant: Handle<MenuItem>,
477    make_linear: Handle<MenuItem>,
478    make_cubic: Handle<MenuItem>,
479    zoom_to_fit: Handle<MenuItem>,
480    key_properties: Handle<Grid>,
481    key_value: Handle<NumericUpDown<f32>>,
482    key_location: Handle<NumericUpDown<f32>>,
483    copy_keys: Handle<MenuItem>,
484    paste_keys: Handle<MenuItem>,
485}
486
487#[derive(Clone, Debug)]
488struct DragEntry {
489    key_id: Uuid,
490    initial_position: Vector2<f32>,
491}
492
493#[derive(Clone, Debug)]
494enum OperationContext {
495    DragKeys {
496        // In local coordinates.
497        initial_mouse_pos: Vector2<f32>,
498        entries: Vec<DragEntry>,
499    },
500    MoveView {
501        initial_mouse_pos: Vector2<f32>,
502        initial_view_pos: Vector2<f32>,
503    },
504    DragTangent {
505        key_id: Uuid,
506        left: bool,
507    },
508    BoxSelection {
509        // In local coordinates.
510        initial_mouse_pos: Vector2<f32>,
511        min: Cell<Vector2<f32>>,
512        max: Cell<Vector2<f32>>,
513    },
514}
515
516impl OperationContext {
517    fn is_dragging(&self) -> bool {
518        matches!(
519            self,
520            OperationContext::DragKeys { .. } | OperationContext::DragTangent { .. }
521        )
522    }
523}
524
525#[derive(Clone, Debug)]
526enum Selection {
527    Keys { keys: FxHashSet<Uuid> },
528    LeftTangent { key_id: Uuid },
529    RightTangent { key_id: Uuid },
530}
531
532#[derive(Copy, Clone)]
533enum PickResult {
534    Key(Uuid),
535    LeftTangent(Uuid),
536    RightTangent(Uuid),
537}
538
539impl Selection {
540    fn single_key(key: Uuid) -> Self {
541        let mut keys = FxHashSet::default();
542        keys.insert(key);
543        Self::Keys { keys }
544    }
545}
546
547uuid_provider!(CurveEditor = "5c7b087e-871e-498d-b064-187b604a37d8");
548
549impl Control for CurveEditor {
550    fn draw(&self, ctx: &mut DrawingContext) {
551        self.curve_transform.set_bounds(self.screen_bounds());
552        self.curve_transform.update_transform();
553        self.draw_background(ctx);
554        self.draw_highlight_zones(ctx);
555        self.draw_grid(ctx);
556        if self.show_background_curves {
557            self.draw_curves(&self.background_curves, ctx);
558            self.draw_keys(&self.background_curves, &self.background_curve_brush, ctx);
559        }
560        self.draw_curves(&self.curves, ctx);
561        self.draw_keys(&self.curves, &self.key_brush, ctx);
562        self.draw_operation(ctx);
563    }
564
565    fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
566        self.widget.handle_routed_message(ui, message);
567
568        if message.destination() == self.handle {
569            if let Some(msg) = message.data::<WidgetMessage>() {
570                match msg {
571                    WidgetMessage::KeyUp(code) => match code {
572                        KeyCode::Delete => self.remove_selection(ui),
573                        KeyCode::KeyF => self.zoom_to_fit(&ui.sender()),
574                        _ => (),
575                    },
576                    WidgetMessage::MouseMove { pos, state } => {
577                        let is_dragging = self
578                            .operation_context
579                            .as_ref()
580                            .is_some_and(|ctx| ctx.is_dragging());
581                        if self.pick(*pos).is_some() || is_dragging {
582                            if self.cursor.is_none() {
583                                ui.send(
584                                    self.handle,
585                                    WidgetMessage::Cursor(Some(if is_dragging {
586                                        CursorIcon::Grabbing
587                                    } else {
588                                        CursorIcon::Grab
589                                    })),
590                                );
591                            }
592                        } else if self.cursor.is_some() {
593                            ui.send(self.handle, WidgetMessage::Cursor(None));
594                        }
595
596                        let curve_mouse_pos = self.screen_to_curve_space(*pos);
597                        if let Some(operation_context) = self.operation_context.as_ref() {
598                            match operation_context {
599                                OperationContext::DragKeys {
600                                    entries,
601                                    initial_mouse_pos,
602                                } => {
603                                    let local_delta = curve_mouse_pos - initial_mouse_pos;
604                                    for entry in entries {
605                                        if let Some(key) = self.curves.key_mut(entry.key_id) {
606                                            key.position = entry.initial_position + local_delta;
607                                        }
608                                    }
609                                    self.sort_keys();
610                                    self.invalidate_visual();
611                                }
612                                OperationContext::MoveView {
613                                    initial_mouse_pos,
614                                    initial_view_pos,
615                                } => {
616                                    let d = *pos - initial_mouse_pos;
617                                    let zoom = self.curve_transform.scale();
618                                    // Dragging left moves the position right. Dragging up moves the position down.
619                                    // Remember: up is negative-y in screen space, and up is positive-y in curve space.
620                                    let delta = Vector2::<f32>::new(-d.x / zoom.x, d.y / zoom.y);
621                                    ui.send(
622                                        self.handle,
623                                        CurveEditorMessage::ViewPosition(initial_view_pos + delta),
624                                    );
625                                }
626                                OperationContext::DragTangent { key_id: key, left } => {
627                                    if let Some(key) = self.curves.key_mut(*key) {
628                                        let key_pos = key.position;
629
630                                        let screen_key_pos = self
631                                            .curve_transform
632                                            .curve_to_screen()
633                                            .transform_point(&Point2::from(key_pos))
634                                            .coords;
635
636                                        if let CurveKeyKind::Cubic {
637                                            left_tangent,
638                                            right_tangent,
639                                        } = &mut key.kind
640                                        {
641                                            let mut local_delta = pos - screen_key_pos;
642                                            if *left {
643                                                local_delta.x = local_delta.x.min(f32::EPSILON);
644                                            } else {
645                                                local_delta.x = local_delta.x.max(f32::EPSILON);
646                                            }
647                                            let tangent =
648                                                (local_delta.y / local_delta.x).clamp(-10e6, 10e6);
649
650                                            if *left {
651                                                *left_tangent = tangent;
652                                            } else {
653                                                *right_tangent = tangent;
654                                            }
655
656                                            self.invalidate_visual();
657                                        } else {
658                                            unreachable!(
659                                                "attempt to edit tangents of non-cubic curve key!"
660                                            )
661                                        }
662                                    }
663                                }
664                                OperationContext::BoxSelection {
665                                    initial_mouse_pos,
666                                    min,
667                                    max,
668                                    ..
669                                } => {
670                                    min.set(curve_mouse_pos.inf(initial_mouse_pos));
671                                    max.set(curve_mouse_pos.sup(initial_mouse_pos));
672                                    self.invalidate_visual();
673                                }
674                            }
675                        } else if state.left == ButtonState::Pressed {
676                            if let Some(selection) = self.selection.as_ref() {
677                                match selection {
678                                    Selection::Keys { keys } => {
679                                        self.operation_context = Some(OperationContext::DragKeys {
680                                            entries: keys
681                                                .iter()
682                                                .map(|k| DragEntry {
683                                                    key_id: *k,
684                                                    initial_position: self
685                                                        .curves
686                                                        .key_ref(*k)
687                                                        .map(|k| k.position)
688                                                        .unwrap_or_default(),
689                                                })
690                                                .collect::<Vec<_>>(),
691                                            initial_mouse_pos: curve_mouse_pos,
692                                        });
693                                    }
694                                    Selection::LeftTangent { key_id: key } => {
695                                        self.operation_context =
696                                            Some(OperationContext::DragTangent {
697                                                key_id: *key,
698                                                left: true,
699                                            })
700                                    }
701                                    Selection::RightTangent { key_id: key } => {
702                                        self.operation_context =
703                                            Some(OperationContext::DragTangent {
704                                                key_id: *key,
705                                                left: false,
706                                            })
707                                    }
708                                }
709                            } else {
710                                self.operation_context = Some(OperationContext::BoxSelection {
711                                    initial_mouse_pos: curve_mouse_pos,
712                                    min: Default::default(),
713                                    max: Default::default(),
714                                })
715                            }
716
717                            if self.operation_context.is_some() {
718                                ui.capture_mouse(self.handle);
719                            }
720                        }
721                    }
722
723                    WidgetMessage::MouseUp { .. } => {
724                        if let Some(context) = self.operation_context.take() {
725                            ui.release_mouse_capture();
726
727                            // Send modified curve back to user.
728                            match context {
729                                OperationContext::DragKeys { .. }
730                                | OperationContext::DragTangent { .. } => {
731                                    // Ensure that the order of keys is correct.
732                                    self.sort_keys();
733
734                                    self.send_curves(ui);
735                                    self.invalidate_visual();
736                                }
737                                OperationContext::BoxSelection { min, max, .. } => {
738                                    let min = min.get();
739                                    let max = max.get();
740
741                                    let rect =
742                                        Rect::new(min.x, min.y, max.x - min.x, max.y - min.y);
743
744                                    let mut selection = FxHashSet::default();
745                                    for curve in self.curves.iter() {
746                                        for key in curve.keys() {
747                                            if rect.contains(key.position) {
748                                                selection.insert(key.id);
749                                            }
750                                        }
751                                    }
752
753                                    if !selection.is_empty() {
754                                        self.set_selection(
755                                            Some(Selection::Keys { keys: selection }),
756                                            ui,
757                                        );
758                                    }
759                                    self.invalidate_visual();
760                                }
761                                _ => {}
762                            }
763                        }
764                    }
765                    WidgetMessage::MouseDown { pos, button } => match button {
766                        MouseButton::Left => {
767                            let pick_result = self.pick(*pos);
768
769                            if let Some(picked) = pick_result {
770                                match picked {
771                                    PickResult::Key(picked_key) => {
772                                        if let Some(picked_key_id) =
773                                            self.curves.key_ref(picked_key).map(|key| key.id)
774                                        {
775                                            if let Some(selection) = self.selection.as_mut() {
776                                                match selection {
777                                                    Selection::Keys { keys } => {
778                                                        if ui.keyboard_modifiers().control {
779                                                            keys.insert(picked_key_id);
780                                                        }
781                                                        if !keys.contains(&picked_key_id) {
782                                                            self.set_selection(
783                                                                Some(Selection::single_key(
784                                                                    picked_key_id,
785                                                                )),
786                                                                ui,
787                                                            );
788                                                        }
789                                                    }
790                                                    Selection::LeftTangent { .. }
791                                                    | Selection::RightTangent { .. } => self
792                                                        .set_selection(
793                                                            Some(Selection::single_key(
794                                                                picked_key_id,
795                                                            )),
796                                                            ui,
797                                                        ),
798                                                }
799                                            } else {
800                                                self.set_selection(
801                                                    Some(Selection::single_key(picked_key_id)),
802                                                    ui,
803                                                );
804                                            }
805                                        }
806                                    }
807                                    PickResult::LeftTangent(picked_key) => {
808                                        self.set_selection(
809                                            Some(Selection::LeftTangent { key_id: picked_key }),
810                                            ui,
811                                        );
812                                    }
813                                    PickResult::RightTangent(picked_key) => {
814                                        self.set_selection(
815                                            Some(Selection::RightTangent { key_id: picked_key }),
816                                            ui,
817                                        );
818                                    }
819                                }
820                            } else {
821                                self.set_selection(None, ui);
822                            }
823                        }
824                        MouseButton::Middle => {
825                            ui.capture_mouse(self.handle);
826                            self.operation_context = Some(OperationContext::MoveView {
827                                initial_mouse_pos: *pos,
828                                initial_view_pos: self.curve_transform.position(),
829                            });
830                        }
831                        _ => (),
832                    },
833                    WidgetMessage::MouseWheel { amount, .. } => {
834                        let k = if *amount < 0.0 { 0.9 } else { 1.1 };
835
836                        let zoom = self.curve_transform.scale();
837                        let new_zoom = if ui.keyboard_modifiers().shift {
838                            Vector2::new(zoom.x * k, zoom.y)
839                        } else if ui.keyboard_modifiers.control {
840                            Vector2::new(zoom.x, zoom.y * k)
841                        } else {
842                            zoom * k
843                        };
844
845                        ui.send(self.handle, CurveEditorMessage::Zoom(new_zoom));
846
847                        message.set_handled(true);
848                    }
849                    _ => {}
850                }
851            } else if let Some(msg) = message.data_for::<CurveEditorMessage>(self.handle) {
852                match msg {
853                    CurveEditorMessage::SyncBackground(curves) => {
854                        self.background_curves =
855                            CurvesContainer::from_native(self.key_brush.clone(), curves);
856
857                        for curve in self.background_curves.iter_mut() {
858                            curve.brush = self.background_curve_brush.clone();
859                        }
860
861                        self.invalidate_visual();
862                    }
863                    CurveEditorMessage::Sync(curves) => {
864                        let color_map = self
865                            .curves
866                            .iter()
867                            .map(|curve| (curve.id(), curve.brush.clone()))
868                            .collect::<Vec<_>>();
869
870                        self.curves = CurvesContainer::from_native(self.key_brush.clone(), curves);
871
872                        self.colorize(&color_map);
873
874                        self.invalidate_visual();
875                    }
876                    CurveEditorMessage::Colorize(color_map) => {
877                        self.colorize(color_map);
878
879                        self.invalidate_visual();
880                    }
881                    CurveEditorMessage::ViewPosition(view_position) => {
882                        self.set_view_position(*view_position);
883                        ui.try_send_response(message);
884
885                        self.invalidate_visual();
886                    }
887                    CurveEditorMessage::Zoom(zoom) => {
888                        self.curve_transform
889                            .set_scale(zoom.simd_clamp(self.min_zoom, self.max_zoom));
890                        ui.try_send_response(message);
891
892                        self.invalidate_visual();
893                    }
894                    CurveEditorMessage::RemoveSelection => {
895                        self.remove_selection(ui);
896
897                        self.invalidate_visual();
898                    }
899                    CurveEditorMessage::ChangeSelectedKeysKind(kind) => {
900                        self.change_selected_keys_kind(kind.clone(), ui);
901
902                        self.invalidate_visual();
903                    }
904                    CurveEditorMessage::AddKey(screen_pos) => {
905                        let local_pos = self.screen_to_curve_space(*screen_pos);
906
907                        let mut curves = Vec::new();
908                        if let Some(selection) = self.selection.as_ref() {
909                            if let Selection::Keys { keys } = selection {
910                                curves.extend(self.curves.iter_mut().filter(|curve| {
911                                    for key in curve.keys() {
912                                        if keys.contains(&key.id) {
913                                            return true;
914                                        }
915                                    }
916                                    false
917                                }));
918                            }
919                        } else {
920                            curves.extend(self.curves.curves.iter_mut());
921                        };
922
923                        let mut added_keys = FxHashSet::default();
924                        for curve in curves {
925                            let id = Uuid::new_v4();
926                            curve.add(CurveKeyView {
927                                position: local_pos,
928                                kind: CurveKeyKind::Linear,
929                                id,
930                            });
931                            added_keys.insert(id);
932                        }
933
934                        self.set_selection(Some(Selection::Keys { keys: added_keys }), ui);
935                        self.sort_keys();
936                        self.send_curves(ui);
937
938                        self.invalidate_visual();
939                    }
940                    CurveEditorMessage::ZoomToFit { after_layout } => {
941                        if *after_layout {
942                            // TODO: Layout system could take up to 10 frames in worst cases. This is super hackish solution
943                            // but when it works, who cares.
944                            self.zoom_to_fit_timer = Some(10);
945                        } else {
946                            self.zoom_to_fit(&ui.sender);
947                        }
948
949                        self.invalidate_visual();
950                    }
951                    CurveEditorMessage::ChangeSelectedKeysValue(value) => {
952                        self.change_selected_keys_value(*value, ui);
953
954                        self.invalidate_visual();
955                    }
956                    CurveEditorMessage::ChangeSelectedKeysLocation(location) => {
957                        self.change_selected_keys_location(*location, ui);
958
959                        self.invalidate_visual();
960                    }
961                    CurveEditorMessage::HighlightZones(zones) => {
962                        self.highlight_zones.clone_from(zones);
963
964                        self.invalidate_visual();
965                    }
966                    CurveEditorMessage::CopySelection => {
967                        if let Some(Selection::Keys { keys }) = self.selection.as_ref() {
968                            let menu_pos =
969                                ui.node(self.context_menu.widget.handle()).screen_position();
970                            let local_menu_pos = self.screen_to_curve_space(menu_pos);
971
972                            self.clipboard.clear();
973                            for key in keys {
974                                for curve in self.curves.iter() {
975                                    if let Some(key) = curve.key_ref(*key) {
976                                        self.clipboard.push((
977                                            key.position - local_menu_pos,
978                                            key.kind.clone(),
979                                        ));
980                                    }
981                                }
982                            }
983                        }
984                    }
985                    CurveEditorMessage::PasteSelection => {
986                        if !self.clipboard.is_empty() {
987                            let menu_pos =
988                                ui.node(self.context_menu.widget.handle()).screen_position();
989                            let local_menu_pos = self.screen_to_curve_space(menu_pos);
990
991                            let mut selection = FxHashSet::default();
992                            for (offset, kind) in self.clipboard.iter().cloned() {
993                                for curve in self.curves.iter_mut() {
994                                    let id = Uuid::new_v4();
995
996                                    selection.insert(id);
997
998                                    curve.add(CurveKeyView {
999                                        position: local_menu_pos + offset,
1000                                        kind: kind.clone(),
1001                                        id,
1002                                    });
1003                                }
1004                            }
1005
1006                            self.set_selection(Some(Selection::Keys { keys: selection }), ui);
1007                            self.sort_keys();
1008                            self.send_curves(ui);
1009
1010                            self.invalidate_visual();
1011                        }
1012                    }
1013                    CurveEditorMessage::ShowBackgroundCurves(show) => {
1014                        if self.show_background_curves != *show {
1015                            self.show_background_curves = *show;
1016                            self.invalidate_layout();
1017                        }
1018                    }
1019                }
1020            }
1021        }
1022    }
1023
1024    fn preview_message(&self, ui: &UserInterface, message: &mut UiMessage) {
1025        if let Some(MenuItemMessage::Click) = message.data::<MenuItemMessage>() {
1026            if message.destination() == self.context_menu.remove {
1027                ui.send(self.handle, CurveEditorMessage::RemoveSelection);
1028            } else if message.destination() == self.context_menu.make_constant {
1029                ui.send(
1030                    self.handle,
1031                    CurveEditorMessage::ChangeSelectedKeysKind(CurveKeyKind::Constant),
1032                );
1033            } else if message.destination() == self.context_menu.make_linear {
1034                ui.send(
1035                    self.handle,
1036                    CurveEditorMessage::ChangeSelectedKeysKind(CurveKeyKind::Linear),
1037                );
1038            } else if message.destination() == self.context_menu.make_cubic {
1039                ui.send(
1040                    self.handle,
1041                    CurveEditorMessage::ChangeSelectedKeysKind(CurveKeyKind::Cubic {
1042                        left_tangent: 0.0,
1043                        right_tangent: 0.0,
1044                    }),
1045                );
1046            } else if message.destination() == self.context_menu.add_key {
1047                let screen_pos = ui.node(self.context_menu.widget.handle()).screen_position();
1048                ui.send(self.handle, CurveEditorMessage::AddKey(screen_pos));
1049            } else if message.destination() == self.context_menu.zoom_to_fit {
1050                ui.send(
1051                    self.handle,
1052                    CurveEditorMessage::ZoomToFit {
1053                        after_layout: false,
1054                    },
1055                );
1056            } else if message.destination() == self.context_menu.copy_keys {
1057                ui.send(self.handle, CurveEditorMessage::CopySelection);
1058            } else if message.destination() == self.context_menu.paste_keys {
1059                ui.send(self.handle, CurveEditorMessage::PasteSelection);
1060            }
1061        } else if let Some(NumericUpDownMessage::<f32>::Value(value)) = message.data() {
1062            if message.direction() == MessageDirection::FromWidget && !message.handled() {
1063                if message.destination() == self.context_menu.key_value {
1064                    ui.send(
1065                        self.handle,
1066                        CurveEditorMessage::ChangeSelectedKeysValue(*value),
1067                    );
1068                } else if message.destination() == self.context_menu.key_location {
1069                    ui.send(
1070                        self.handle,
1071                        CurveEditorMessage::ChangeSelectedKeysLocation(*value),
1072                    );
1073                }
1074            }
1075        }
1076    }
1077
1078    fn update(&mut self, _dt: f32, ui: &mut UserInterface) {
1079        if let Some(timer) = self.zoom_to_fit_timer.as_mut() {
1080            *timer = timer.saturating_sub(1);
1081            if *timer == 0 {
1082                self.zoom_to_fit(&ui.sender);
1083                self.zoom_to_fit_timer = None;
1084            }
1085        }
1086    }
1087}
1088
1089fn draw_cubic(
1090    left_pos: Vector2<f32>,
1091    left_tangent: f32,
1092    right_pos: Vector2<f32>,
1093    right_tangent: f32,
1094    steps: usize,
1095    ctx: &mut DrawingContext,
1096) {
1097    let mut prev = left_pos;
1098    for i in 0..steps {
1099        let t = i as f32 / (steps - 1) as f32;
1100        let middle_x = lerpf(left_pos.x, right_pos.x, t);
1101        let middle_y = cubicf(left_pos.y, right_pos.y, t, left_tangent, right_tangent);
1102        let pt = Vector2::new(middle_x, middle_y);
1103        ctx.push_line(prev, pt, 1.0);
1104        prev = pt;
1105    }
1106}
1107
1108fn round_to_step(x: f32, step: f32) -> f32 {
1109    x - x % step
1110}
1111
1112impl CurveEditor {
1113    #[allow(clippy::let_and_return)] // Improves readability
1114    fn set_view_position(&mut self, position: Vector2<f32>) {
1115        self.curve_transform
1116            .set_position(self.view_bounds.map_or(position, |bounds| {
1117                let local_space_position = -position;
1118                let clamped_local_space_position = Vector2::new(
1119                    local_space_position
1120                        .x
1121                        .clamp(bounds.position.x, bounds.position.x + 2.0 * bounds.size.x),
1122                    local_space_position
1123                        .y
1124                        .clamp(bounds.position.y, bounds.position.y + 2.0 * bounds.size.y),
1125                );
1126                let clamped_view_space = -clamped_local_space_position;
1127                clamped_view_space
1128            }));
1129    }
1130
1131    fn colorize(&mut self, color_map: &[(Uuid, Brush)]) {
1132        for (curve_id, brush) in color_map.iter() {
1133            if let Some(curve) = self.curves.iter_mut().find(|curve| &curve.id() == curve_id) {
1134                curve.brush = brush.clone();
1135            }
1136        }
1137    }
1138
1139    fn zoom_to_fit(&mut self, sender: &Sender<UiMessage>) {
1140        let mut min = Vector2::repeat(f32::MAX);
1141        let mut max = Vector2::repeat(-f32::MAX);
1142
1143        for curve in self.curves.iter() {
1144            let bounds = curve.curve().bounds();
1145            if bounds.position.x < min.x {
1146                min.x = bounds.position.x;
1147            }
1148            if bounds.position.y < min.y {
1149                min.y = bounds.position.y;
1150            }
1151            let local_max = bounds.position + bounds.size;
1152            if local_max.x > max.x {
1153                max.x = local_max.x;
1154            }
1155            if local_max.y > max.y {
1156                max.y = local_max.y;
1157            }
1158        }
1159
1160        let mut bounds = Rect {
1161            position: min,
1162            size: max - min,
1163        };
1164
1165        // Prevent division by zero.
1166        if bounds.size.x < 0.001 {
1167            bounds.size.x = 0.001;
1168        }
1169        if bounds.size.y < 0.001 {
1170            bounds.size.y = 0.001;
1171        }
1172
1173        let center = bounds.center();
1174
1175        sender
1176            .send(UiMessage::for_widget(
1177                self.handle,
1178                CurveEditorMessage::Zoom(Vector2::new(
1179                    self.actual_local_size().x / bounds.w(),
1180                    self.actual_local_size().y / bounds.h(),
1181                )),
1182            ))
1183            .unwrap();
1184
1185        sender
1186            .send(UiMessage::for_widget(
1187                self.handle,
1188                CurveEditorMessage::ViewPosition(center),
1189            ))
1190            .unwrap();
1191    }
1192
1193    /// Transforms a point to view space.
1194    pub fn point_to_view_space(&self, point: Vector2<f32>) -> Vector2<f32> {
1195        self.curve_transform
1196            .local_to_curve()
1197            .transform_point(&Point2::from(point))
1198            .coords
1199    }
1200
1201    /// Transforms a point to screen space.
1202    pub fn point_to_screen_space(&self, point: Vector2<f32>) -> Vector2<f32> {
1203        self.curve_transform
1204            .curve_to_screen()
1205            .transform_point(&Point2::from(point))
1206            .coords
1207    }
1208
1209    /// Transforms a point to widget's local space.
1210    pub fn point_to_local_space(&self, point: Vector2<f32>) -> Vector2<f32> {
1211        self.curve_transform
1212            .curve_to_local()
1213            .transform_point(&Point2::from(point))
1214            .coords
1215    }
1216
1217    /// Transforms a vector to screen space.
1218    pub fn vector_to_screen_space(&self, vector: Vector2<f32>) -> Vector2<f32> {
1219        (*self.curve_transform.curve_to_screen() * Vector3::new(vector.x, vector.y, 0.0)).xy()
1220    }
1221
1222    /// Transforms a screen position to a curve position.
1223    pub fn screen_to_curve_space(&self, point: Vector2<f32>) -> Vector2<f32> {
1224        self.curve_transform
1225            .screen_to_curve()
1226            .transform_point(&Point2::from(point))
1227            .coords
1228    }
1229
1230    fn sort_keys(&mut self) {
1231        for curve in self.curves.iter_mut() {
1232            curve.sort_keys();
1233        }
1234    }
1235
1236    fn set_selection(&mut self, selection: Option<Selection>, ui: &UserInterface) {
1237        self.selection = selection;
1238
1239        ui.send(
1240            self.context_menu.remove,
1241            WidgetMessage::Enabled(self.selection.is_some()),
1242        );
1243        ui.send(
1244            self.context_menu.key,
1245            WidgetMessage::Enabled(self.selection.is_some()),
1246        );
1247        ui.send(
1248            self.context_menu.key_properties,
1249            WidgetMessage::Enabled(self.selection.is_some()),
1250        );
1251
1252        if let Some(Selection::Keys { keys }) = self.selection.as_ref() {
1253            if let Some(first) = keys.iter().next() {
1254                if let Some(key) = self.curves.key_ref(*first) {
1255                    ui.send_handled(
1256                        self.context_menu.key_location,
1257                        NumericUpDownMessage::Value(key.position.x),
1258                    );
1259                    ui.send_handled(
1260                        self.context_menu.key_value,
1261                        NumericUpDownMessage::Value(key.position.y),
1262                    );
1263                }
1264            }
1265        }
1266    }
1267
1268    fn remove_selection(&mut self, ui: &mut UserInterface) {
1269        if let Some(Selection::Keys { keys }) = self.selection.as_ref() {
1270            for &id in keys {
1271                self.curves.remove(id);
1272            }
1273
1274            self.set_selection(None, ui);
1275
1276            // Send modified curve back to user.
1277            self.send_curves(ui);
1278        }
1279    }
1280
1281    fn change_selected_keys_kind(&mut self, kind: CurveKeyKind, ui: &mut UserInterface) {
1282        if let Some(Selection::Keys { keys }) = self.selection.as_ref() {
1283            for key in keys {
1284                if let Some(key) = self.curves.key_mut(*key) {
1285                    key.kind = kind.clone();
1286                }
1287            }
1288
1289            self.send_curves(ui);
1290        }
1291    }
1292
1293    fn change_selected_keys_value(&mut self, value: f32, ui: &mut UserInterface) {
1294        if let Some(Selection::Keys { keys }) = self.selection.as_ref() {
1295            let mut modified = false;
1296            for key in keys {
1297                if let Some(key) = self.curves.key_mut(*key) {
1298                    let key_value = &mut key.position.y;
1299                    if (*key_value).ne(&value) {
1300                        *key_value = value;
1301                        modified = true;
1302                    }
1303                }
1304            }
1305
1306            if modified {
1307                self.send_curves(ui);
1308            }
1309        }
1310    }
1311
1312    fn change_selected_keys_location(&mut self, location: f32, ui: &mut UserInterface) {
1313        if let Some(Selection::Keys { keys }) = self.selection.as_ref() {
1314            let mut modified = false;
1315            for key in keys {
1316                if let Some(key) = self.curves.key_mut(*key) {
1317                    let key_location = &mut key.position.x;
1318                    if (*key_location).ne(&location) {
1319                        *key_location = location;
1320                        modified = true;
1321                    }
1322                }
1323            }
1324
1325            if modified {
1326                self.send_curves(ui);
1327            }
1328        }
1329    }
1330
1331    /// `pos` must be in screen space.
1332    fn pick(&self, pos: Vector2<f32>) -> Option<PickResult> {
1333        for curve in self.curves.iter() {
1334            // Linear search is fine here, having a curve with thousands of
1335            // points is insane anyway.
1336            for key in curve.keys().iter() {
1337                let screen_pos = self.point_to_screen_space(key.position);
1338                let bounds = Rect::new(
1339                    screen_pos.x - self.key_size * 0.5,
1340                    screen_pos.y - self.key_size * 0.5,
1341                    self.key_size,
1342                    self.key_size,
1343                );
1344                if bounds.contains(pos) {
1345                    return Some(PickResult::Key(key.id));
1346                }
1347
1348                // Check tangents.
1349                if let CurveKeyKind::Cubic {
1350                    left_tangent,
1351                    right_tangent,
1352                } = key.kind
1353                {
1354                    let left_handle_pos = self.tangent_screen_position(
1355                        wrap_angle(left_tangent.atan()) + std::f32::consts::PI,
1356                        key.position,
1357                    );
1358
1359                    if (left_handle_pos - pos).norm() <= self.key_size * 0.5 {
1360                        return Some(PickResult::LeftTangent(key.id));
1361                    }
1362
1363                    let right_handle_pos = self
1364                        .tangent_screen_position(wrap_angle(right_tangent.atan()), key.position);
1365
1366                    if (right_handle_pos - pos).norm() <= self.key_size * 0.5 {
1367                        return Some(PickResult::RightTangent(key.id));
1368                    }
1369                }
1370            }
1371        }
1372        None
1373    }
1374
1375    fn tangent_screen_position(&self, angle: f32, key_position: Vector2<f32>) -> Vector2<f32> {
1376        self.point_to_screen_space(key_position)
1377            + Vector2::new(angle.cos(), angle.sin()).scale(self.handle_radius)
1378    }
1379
1380    fn tangent_local_position(&self, angle: f32, key_position: Vector2<f32>) -> Vector2<f32> {
1381        self.point_to_local_space(key_position)
1382            + Vector2::new(angle.cos(), angle.sin()).scale(self.handle_radius)
1383    }
1384
1385    fn send_curves(&self, ui: &UserInterface) {
1386        ui.post(
1387            self.handle,
1388            CurveEditorMessage::Sync(self.curves.to_native()),
1389        );
1390    }
1391
1392    fn draw_background(&self, ctx: &mut DrawingContext) {
1393        let bounds = self.bounding_rect();
1394        // Draw background.
1395        ctx.push_rect_filled(&bounds, None);
1396        ctx.commit(
1397            self.clip_bounds(),
1398            self.background(),
1399            CommandTexture::None,
1400            &self.material,
1401            None,
1402        );
1403    }
1404
1405    fn draw_highlight_zones(&self, ctx: &mut DrawingContext) {
1406        for zone in self.highlight_zones.iter() {
1407            let left_top_corner = self.point_to_local_space(zone.rect.left_top_corner());
1408            let bottom_right_corner = self.point_to_local_space(zone.rect.right_bottom_corner());
1409            ctx.push_rect_filled(
1410                &Rect::new(
1411                    left_top_corner.x,
1412                    left_top_corner.y,
1413                    bottom_right_corner.x - left_top_corner.x,
1414                    bottom_right_corner.y - left_top_corner.y,
1415                ),
1416                None,
1417            );
1418            ctx.commit(
1419                self.clip_bounds(),
1420                zone.brush.clone(),
1421                CommandTexture::None,
1422                &self.material,
1423                None,
1424            );
1425        }
1426    }
1427
1428    fn draw_grid(&self, ctx: &mut DrawingContext) {
1429        let screen_bounds = self.screen_bounds();
1430
1431        let zoom = self.curve_transform.scale();
1432        let step_size_x = self.grid_size.x / zoom.x;
1433        let step_size_y = self.grid_size.y / zoom.y;
1434
1435        let mut local_left_bottom = self.screen_to_curve_space(screen_bounds.left_top_corner());
1436        let local_left_bottom_n = local_left_bottom;
1437        local_left_bottom.x = round_to_step(local_left_bottom.x, step_size_x);
1438        local_left_bottom.y = round_to_step(local_left_bottom.y, step_size_y);
1439
1440        let mut local_right_top = self.screen_to_curve_space(screen_bounds.right_bottom_corner());
1441        local_right_top.x = round_to_step(local_right_top.x, step_size_x);
1442        local_right_top.y = round_to_step(local_right_top.y, step_size_y);
1443
1444        for y in self.curve_transform.y_step_iter(self.grid_size.y) {
1445            ctx.push_line(
1446                self.point_to_local_space(Vector2::new(local_left_bottom.x - step_size_x, y)),
1447                self.point_to_local_space(Vector2::new(local_right_top.x + step_size_x, y)),
1448                1.0,
1449            );
1450        }
1451
1452        for x in self.curve_transform.x_step_iter(self.grid_size.x) {
1453            ctx.push_line(
1454                self.point_to_local_space(Vector2::new(x, local_left_bottom.y + step_size_y)),
1455                self.point_to_local_space(Vector2::new(x, local_right_top.y - step_size_y)),
1456                1.0,
1457            );
1458        }
1459
1460        // Draw main axes.
1461        let vb = self.point_to_local_space(Vector2::new(0.0, -10e6));
1462        let ve = self.point_to_local_space(Vector2::new(0.0, 10e6));
1463        ctx.push_line(vb, ve, 2.0);
1464
1465        let hb = self.point_to_local_space(Vector2::new(-10e6, 0.0));
1466        let he = self.point_to_local_space(Vector2::new(10e6, 0.0));
1467        ctx.push_line(hb, he, 2.0);
1468
1469        ctx.commit(
1470            self.clip_bounds(),
1471            self.grid_brush.clone(),
1472            CommandTexture::None,
1473            &self.material,
1474            None,
1475        );
1476
1477        // Draw values.
1478        let mut text = self.text.borrow_mut();
1479
1480        if self.show_y_values {
1481            for y in self.curve_transform.y_step_iter(self.grid_size.y) {
1482                text.set_text(format!("{y:.1}")).measure_and_arrange();
1483                ctx.draw_text(
1484                    self.clip_bounds(),
1485                    self.point_to_local_space(Vector2::new(local_left_bottom_n.x, y)),
1486                    &self.material,
1487                    &text,
1488                );
1489            }
1490        }
1491
1492        if self.show_x_values {
1493            for x in self.curve_transform.x_step_iter(self.grid_size.x) {
1494                text.set_text(format!("{x:.1}")).measure_and_arrange();
1495                ctx.draw_text(
1496                    self.clip_bounds(),
1497                    self.point_to_local_space(Vector2::new(x, local_left_bottom_n.y)),
1498                    &self.material,
1499                    &text,
1500                );
1501            }
1502        }
1503    }
1504
1505    fn draw_curves(&self, curves: &CurvesContainer, ctx: &mut DrawingContext) {
1506        let screen_bounds = self.screen_bounds();
1507
1508        for curve in curves.iter() {
1509            let draw_keys = curve.keys();
1510
1511            if let Some(first) = draw_keys.first() {
1512                let screen_pos = self.point_to_local_space(first.position);
1513                ctx.push_line(Vector2::new(0.0, screen_pos.y), screen_pos, 1.0);
1514            }
1515            if let Some(last) = draw_keys.last() {
1516                let screen_pos = self.point_to_local_space(last.position);
1517                ctx.push_line(
1518                    screen_pos,
1519                    Vector2::new(screen_bounds.x() + screen_bounds.w(), screen_pos.y),
1520                    1.0,
1521                );
1522            }
1523
1524            for pair in draw_keys.windows(2) {
1525                let left = &pair[0];
1526                let right = &pair[1];
1527
1528                let left_pos = self.point_to_local_space(left.position);
1529                let right_pos = self.point_to_local_space(right.position);
1530
1531                let steps = ((right_pos.x - left_pos.x).abs() / 2.0) as usize;
1532
1533                match (&left.kind, &right.kind) {
1534                    // Constant-to-any is depicted as two straight lines.
1535                    (CurveKeyKind::Constant, CurveKeyKind::Constant)
1536                    | (CurveKeyKind::Constant, CurveKeyKind::Linear)
1537                    | (CurveKeyKind::Constant, CurveKeyKind::Cubic { .. }) => {
1538                        ctx.push_line(left_pos, Vector2::new(right_pos.x, left_pos.y), 1.0);
1539                        ctx.push_line(Vector2::new(right_pos.x, left_pos.y), right_pos, 1.0);
1540                    }
1541
1542                    // Linear-to-any is depicted as a straight line.
1543                    (CurveKeyKind::Linear, CurveKeyKind::Constant)
1544                    | (CurveKeyKind::Linear, CurveKeyKind::Linear)
1545                    | (CurveKeyKind::Linear, CurveKeyKind::Cubic { .. }) => {
1546                        ctx.push_line(left_pos, right_pos, 1.0)
1547                    }
1548
1549                    // Cubic-to-constant and cubic-to-linear is depicted as Hermite spline with right tangent == 0.0.
1550                    (
1551                        CurveKeyKind::Cubic {
1552                            right_tangent: left_tangent,
1553                            ..
1554                        },
1555                        CurveKeyKind::Constant,
1556                    )
1557                    | (
1558                        CurveKeyKind::Cubic {
1559                            right_tangent: left_tangent,
1560                            ..
1561                        },
1562                        CurveKeyKind::Linear,
1563                    ) => draw_cubic(left_pos, *left_tangent, right_pos, 0.0, steps, ctx),
1564
1565                    // Cubic-to-cubic is depicted as Hermite spline.
1566                    (
1567                        CurveKeyKind::Cubic {
1568                            right_tangent: left_tangent,
1569                            ..
1570                        },
1571                        CurveKeyKind::Cubic {
1572                            left_tangent: right_tangent,
1573                            ..
1574                        },
1575                    ) => draw_cubic(
1576                        left_pos,
1577                        *left_tangent,
1578                        right_pos,
1579                        *right_tangent,
1580                        steps,
1581                        ctx,
1582                    ),
1583                }
1584            }
1585            ctx.commit(
1586                self.clip_bounds(),
1587                curve.brush.clone(),
1588                CommandTexture::None,
1589                &self.material,
1590                None,
1591            );
1592        }
1593    }
1594
1595    fn draw_keys(&self, curves: &CurvesContainer, brush: &Brush, ctx: &mut DrawingContext) {
1596        for curve in curves.iter() {
1597            let keys_to_draw = curve.keys();
1598
1599            for key in keys_to_draw.iter() {
1600                let origin = self.point_to_local_space(key.position);
1601                let size = Vector2::new(self.key_size, self.key_size);
1602                let half_size = size.scale(0.5);
1603
1604                ctx.push_rect_filled(
1605                    &Rect::new(
1606                        origin.x - half_size.x,
1607                        origin.y - half_size.x,
1608                        size.x,
1609                        size.y,
1610                    ),
1611                    None,
1612                );
1613
1614                let mut selected = false;
1615                if let Some(selection) = self.selection.as_ref() {
1616                    match selection {
1617                        Selection::Keys { keys } => {
1618                            selected = keys.contains(&key.id);
1619                        }
1620                        Selection::LeftTangent { key_id } | Selection::RightTangent { key_id } => {
1621                            selected = key.id == *key_id;
1622                        }
1623                    }
1624                }
1625
1626                // Show tangents for Cubic keys.
1627                if selected {
1628                    let (show_left, show_right) =
1629                        match self.curves.container_of(key.id).and_then(|container| {
1630                            container
1631                                .key_position(key.id)
1632                                .and_then(|i| keys_to_draw.get(i.wrapping_sub(1)))
1633                        }) {
1634                            Some(left) => match (&left.kind, &key.kind) {
1635                                (CurveKeyKind::Cubic { .. }, CurveKeyKind::Cubic { .. }) => {
1636                                    (true, true)
1637                                }
1638                                (CurveKeyKind::Linear, CurveKeyKind::Cubic { .. })
1639                                | (CurveKeyKind::Constant, CurveKeyKind::Cubic { .. }) => {
1640                                    (false, true)
1641                                }
1642                                _ => (false, false),
1643                            },
1644                            None => match key.kind {
1645                                CurveKeyKind::Cubic { .. } => (false, true),
1646                                _ => (false, false),
1647                            },
1648                        };
1649
1650                    if let CurveKeyKind::Cubic {
1651                        left_tangent,
1652                        right_tangent,
1653                    } = key.kind
1654                    {
1655                        if show_left {
1656                            let left_handle_pos = self.tangent_local_position(
1657                                wrap_angle(left_tangent.atan()) + std::f32::consts::PI,
1658                                key.position,
1659                            );
1660                            ctx.push_line(origin, left_handle_pos, 1.0);
1661                            ctx.push_circle_filled(
1662                                left_handle_pos,
1663                                self.key_size * 0.5,
1664                                6,
1665                                Default::default(),
1666                            );
1667                        }
1668
1669                        if show_right {
1670                            let right_handle_pos = self.tangent_local_position(
1671                                wrap_angle(right_tangent.atan()),
1672                                key.position,
1673                            );
1674                            ctx.push_line(origin, right_handle_pos, 1.0);
1675                            ctx.push_circle_filled(
1676                                right_handle_pos,
1677                                self.key_size * 0.5,
1678                                6,
1679                                Default::default(),
1680                            );
1681                        }
1682                    }
1683                }
1684
1685                ctx.commit(
1686                    self.clip_bounds(),
1687                    if selected {
1688                        self.selected_key_brush.clone()
1689                    } else {
1690                        brush.clone()
1691                    },
1692                    CommandTexture::None,
1693                    &self.material,
1694                    None,
1695                );
1696            }
1697        }
1698    }
1699
1700    fn draw_operation(&self, ctx: &mut DrawingContext) {
1701        if let Some(OperationContext::BoxSelection { min, max, .. }) =
1702            self.operation_context.as_ref()
1703        {
1704            let min = self.point_to_local_space(min.get());
1705            let max = self.point_to_local_space(max.get());
1706            let rect = Rect::new(min.x, min.y, max.x - min.x, max.y - min.y);
1707
1708            ctx.push_rect(&rect, 1.0);
1709            ctx.commit(
1710                self.clip_bounds(),
1711                Brush::Solid(Color::WHITE),
1712                CommandTexture::None,
1713                &self.material,
1714                None,
1715            );
1716        }
1717    }
1718}
1719
1720pub struct CurveEditorBuilder {
1721    widget_builder: WidgetBuilder,
1722    background_curves: Vec<Curve>,
1723    curves: Vec<Curve>,
1724    view_position: Vector2<f32>,
1725    zoom: f32,
1726    view_bounds: Option<Rect<f32>>,
1727    show_x_values: bool,
1728    show_y_values: bool,
1729    grid_size: Vector2<f32>,
1730    min_zoom: Vector2<f32>,
1731    max_zoom: Vector2<f32>,
1732    highlight_zones: Vec<HighlightZone>,
1733    show_background_curves: bool,
1734}
1735
1736impl CurveEditorBuilder {
1737    pub fn new(widget_builder: WidgetBuilder) -> Self {
1738        Self {
1739            widget_builder,
1740            background_curves: Default::default(),
1741            curves: Default::default(),
1742            view_position: Default::default(),
1743            zoom: 1.0,
1744            view_bounds: None,
1745            show_x_values: true,
1746            show_y_values: true,
1747            grid_size: Vector2::new(STANDARD_GRID_SIZE, STANDARD_GRID_SIZE),
1748            min_zoom: Vector2::new(0.001, 0.001),
1749            max_zoom: Vector2::new(1000.0, 1000.0),
1750            highlight_zones: Default::default(),
1751            show_background_curves: true,
1752        }
1753    }
1754
1755    pub fn with_background_curves(mut self, curves: Vec<Curve>) -> Self {
1756        self.background_curves = curves;
1757        self
1758    }
1759
1760    pub fn with_curves(mut self, curves: Vec<Curve>) -> Self {
1761        self.curves = curves;
1762        self
1763    }
1764
1765    pub fn with_zoom(mut self, zoom: f32) -> Self {
1766        self.zoom = zoom;
1767        self
1768    }
1769
1770    pub fn with_view_position(mut self, view_position: Vector2<f32>) -> Self {
1771        self.view_position = view_position;
1772        self
1773    }
1774
1775    pub fn with_show_x_values(mut self, show_x_values: bool) -> Self {
1776        self.show_x_values = show_x_values;
1777        self
1778    }
1779
1780    pub fn with_show_y_values(mut self, show_y_values: bool) -> Self {
1781        self.show_y_values = show_y_values;
1782        self
1783    }
1784
1785    /// View bounds in value-space.
1786    pub fn with_view_bounds(mut self, bounds: Rect<f32>) -> Self {
1787        self.view_bounds = Some(bounds);
1788        self
1789    }
1790
1791    pub fn with_grid_size(mut self, size: Vector2<f32>) -> Self {
1792        self.grid_size = size;
1793        self
1794    }
1795
1796    pub fn with_min_zoom(mut self, min_zoom: Vector2<f32>) -> Self {
1797        self.min_zoom = min_zoom;
1798        self
1799    }
1800
1801    pub fn with_max_zoom(mut self, max_zoom: Vector2<f32>) -> Self {
1802        self.max_zoom = max_zoom;
1803        self
1804    }
1805
1806    pub fn with_highlight_zone(mut self, zones: Vec<HighlightZone>) -> Self {
1807        self.highlight_zones = zones;
1808        self
1809    }
1810
1811    pub fn with_show_background_curves(mut self, show: bool) -> Self {
1812        self.show_background_curves = show;
1813        self
1814    }
1815
1816    pub fn build(mut self, ctx: &mut BuildContext) -> Handle<CurveEditor> {
1817        let background_curve_brush = ctx.style.get_or_default::<Brush>(Style::BRUSH_LIGHT);
1818        let key_brush = Brush::Solid(Color::opaque(140, 140, 140));
1819
1820        let mut background_curves = CurvesContainer::from_native(key_brush.clone(), &self.curves);
1821        for curve in background_curves.iter_mut() {
1822            curve.brush = background_curve_brush.clone();
1823        }
1824
1825        let curves = CurvesContainer::from_native(key_brush.clone(), &self.curves);
1826
1827        let add_key;
1828        let remove;
1829        let make_constant;
1830        let make_linear;
1831        let make_cubic;
1832        let key;
1833        let zoom_to_fit;
1834        let key_properties;
1835        let key_value;
1836        let key_location;
1837        let copy_keys;
1838        let paste_keys;
1839        let context_menu =
1840            ContextMenuBuilder::new(
1841                PopupBuilder::new(WidgetBuilder::new())
1842                    .with_content(
1843                        StackPanelBuilder::new(
1844                            WidgetBuilder::new()
1845                                .with_child({
1846                                    key_properties = GridBuilder::new(
1847                                        WidgetBuilder::new()
1848                                            .with_enabled(false)
1849                                            .with_child(
1850                                                TextBuilder::new(
1851                                                    WidgetBuilder::new()
1852                                                        .with_vertical_alignment(
1853                                                            VerticalAlignment::Center,
1854                                                        )
1855                                                        .with_margin(Thickness::uniform(1.0))
1856                                                        .on_row(0)
1857                                                        .on_column(0),
1858                                                )
1859                                                .with_text("Location")
1860                                                .build(ctx),
1861                                            )
1862                                            .with_child({
1863                                                key_location = NumericUpDownBuilder::<f32>::new(
1864                                                    WidgetBuilder::new()
1865                                                        .with_margin(Thickness::uniform(1.0))
1866                                                        .on_row(0)
1867                                                        .on_column(1),
1868                                                )
1869                                                .build(ctx);
1870                                                key_location
1871                                            })
1872                                            .with_child(
1873                                                TextBuilder::new(
1874                                                    WidgetBuilder::new()
1875                                                        .with_vertical_alignment(
1876                                                            VerticalAlignment::Center,
1877                                                        )
1878                                                        .with_margin(Thickness::uniform(1.0))
1879                                                        .on_row(1)
1880                                                        .on_column(0),
1881                                                )
1882                                                .with_text("Value")
1883                                                .build(ctx),
1884                                            )
1885                                            .with_child({
1886                                                key_value = NumericUpDownBuilder::<f32>::new(
1887                                                    WidgetBuilder::new()
1888                                                        .with_margin(Thickness::uniform(1.0))
1889                                                        .on_row(1)
1890                                                        .on_column(1),
1891                                                )
1892                                                .build(ctx);
1893                                                key_value
1894                                            }),
1895                                    )
1896                                    .add_column(Column::auto())
1897                                    .add_column(Column::stretch())
1898                                    .add_row(Row::strict(22.0))
1899                                    .add_row(Row::strict(22.0))
1900                                    .build(ctx);
1901                                    key_properties
1902                                })
1903                                .with_child({
1904                                    add_key = MenuItemBuilder::new(WidgetBuilder::new())
1905                                        .with_content(MenuItemContent::text("Add Key"))
1906                                        .build(ctx);
1907                                    add_key
1908                                })
1909                                .with_child({
1910                                    remove = MenuItemBuilder::new(
1911                                        WidgetBuilder::new().with_enabled(false),
1912                                    )
1913                                    .with_content(MenuItemContent::text("Remove"))
1914                                    .build(ctx);
1915                                    remove
1916                                })
1917                                .with_child({
1918                                    key = MenuItemBuilder::new(
1919                                        WidgetBuilder::new().with_enabled(false),
1920                                    )
1921                                    .with_content(MenuItemContent::text("Key..."))
1922                                    .with_items(vec![
1923                                        {
1924                                            make_constant =
1925                                                MenuItemBuilder::new(WidgetBuilder::new())
1926                                                    .with_content(MenuItemContent::text("Constant"))
1927                                                    .build(ctx);
1928                                            make_constant
1929                                        },
1930                                        {
1931                                            make_linear =
1932                                                MenuItemBuilder::new(WidgetBuilder::new())
1933                                                    .with_content(MenuItemContent::text("Linear"))
1934                                                    .build(ctx);
1935                                            make_linear
1936                                        },
1937                                        {
1938                                            make_cubic = MenuItemBuilder::new(WidgetBuilder::new())
1939                                                .with_content(MenuItemContent::text("Cubic"))
1940                                                .build(ctx);
1941                                            make_cubic
1942                                        },
1943                                    ])
1944                                    .build(ctx);
1945                                    key
1946                                })
1947                                .with_child({
1948                                    zoom_to_fit = MenuItemBuilder::new(WidgetBuilder::new())
1949                                        .with_content(MenuItemContent::text("Zoom To Fit"))
1950                                        .build(ctx);
1951                                    zoom_to_fit
1952                                })
1953                                .with_child({
1954                                    copy_keys = MenuItemBuilder::new(WidgetBuilder::new())
1955                                        .with_content(MenuItemContent::text("Copy Selected Keys"))
1956                                        .build(ctx);
1957                                    copy_keys
1958                                })
1959                                .with_child({
1960                                    paste_keys = MenuItemBuilder::new(WidgetBuilder::new())
1961                                        .with_content(MenuItemContent::text("Paste Keys"))
1962                                        .build(ctx);
1963                                    paste_keys
1964                                }),
1965                        )
1966                        .build(ctx),
1967                    )
1968                    .with_restrict_picking(false),
1969            )
1970            .build(ctx);
1971        let context_menu = RcUiNodeHandle::new(context_menu, ctx.sender());
1972
1973        if self.widget_builder.foreground.is_none() {
1974            self.widget_builder.foreground = Some(ctx.style.property(Style::BRUSH_BRIGHT))
1975        }
1976
1977        let editor = CurveEditor {
1978            widget: self
1979                .widget_builder
1980                .with_context_menu(context_menu.clone())
1981                .with_preview_messages(true)
1982                .with_need_update(true)
1983                .build(ctx),
1984            background_curves,
1985            curves,
1986            curve_transform: Default::default(),
1987            key_brush,
1988            selected_key_brush: Brush::Solid(Color::opaque(220, 220, 220)),
1989            key_size: 8.0,
1990            handle_radius: 36.0,
1991            operation_context: None,
1992            grid_brush: Brush::Solid(Color::from_rgba(110, 110, 110, 50)),
1993            selection: None,
1994            text: RefCell::new(
1995                FormattedTextBuilder::new(ctx.default_font())
1996                    .with_brush(Brush::Solid(Color::opaque(100, 100, 100)))
1997                    .build(),
1998            ),
1999            context_menu: ContextMenu {
2000                widget: context_menu,
2001                add_key,
2002                remove,
2003                make_constant,
2004                make_linear,
2005                make_cubic,
2006                key,
2007                zoom_to_fit,
2008                key_properties,
2009                key_value,
2010                key_location,
2011                copy_keys,
2012                paste_keys,
2013            },
2014            view_bounds: self.view_bounds,
2015            show_x_values: self.show_x_values,
2016            show_y_values: self.show_y_values,
2017            grid_size: self.grid_size,
2018            min_zoom: self.min_zoom,
2019            max_zoom: self.max_zoom,
2020            highlight_zones: self.highlight_zones,
2021            zoom_to_fit_timer: None,
2022            clipboard: Default::default(),
2023            background_curve_brush,
2024            show_background_curves: self.show_background_curves,
2025        };
2026
2027        ctx.add(editor)
2028    }
2029}
2030
2031#[cfg(test)]
2032mod test {
2033    use crate::{curve::CurveEditorBuilder, test::test_widget_deletion, widget::WidgetBuilder};
2034    #[test]
2035    fn test_curve_editor_deletion() {
2036        test_widget_deletion(|ctx| CurveEditorBuilder::new(WidgetBuilder::new()).build(ctx));
2037    }
2038}