conrod_core/widget/
envelope_editor.rs

1//! The `EnvelopeEditor` widget and related items.
2
3use num::Float;
4use position::{Direction, Edge, Point, Rect, Scalar};
5use std;
6use text;
7use utils::{clamp, map_range, percentage, val_to_string};
8use widget;
9use {Borderable, Color, Colorable, FontSize, Labelable, Positionable, Sizeable, Widget};
10
11/// Used for editing a series of 2D Points on a cartesian (X, Y) plane within some given range.
12///
13/// Useful for things such as oscillator/automation envelopes or any value series represented
14/// periodically.
15#[derive(WidgetCommon_)]
16pub struct EnvelopeEditor<'a, E>
17where
18    E: EnvelopePoint + 'a,
19{
20    #[conrod(common_builder)]
21    common: widget::CommonBuilder,
22    env: &'a [E],
23    /// The value skewing for the envelope's y-axis. This is useful for displaying exponential
24    /// ranges such as frequency.
25    pub skew_y_range: f32,
26    min_x: E::X,
27    max_x: E::X,
28    min_y: E::Y,
29    max_y: E::Y,
30    maybe_label: Option<&'a str>,
31    style: Style,
32    enabled: bool,
33}
34
35/// Styling for the EnvelopeEditor, necessary for constructing its renderable Element.
36#[derive(Copy, Clone, Debug, Default, PartialEq, WidgetStyle_)]
37pub struct Style {
38    /// Coloring for the EnvelopeEditor's **BorderedRectangle**.
39    #[conrod(default = "theme.shape_color")]
40    pub color: Option<Color>,
41    /// Thickness of the **BorderedRectangle**'s border.
42    #[conrod(default = "theme.border_width")]
43    pub border: Option<f64>,
44    /// Color of the border.
45    #[conrod(default = "theme.border_color")]
46    pub border_color: Option<Color>,
47    /// Color of the label.
48    #[conrod(default = "theme.label_color")]
49    pub label_color: Option<Color>,
50    /// The font size of the **EnvelopeEditor**'s label if one was given.
51    #[conrod(default = "theme.font_size_medium")]
52    pub label_font_size: Option<FontSize>,
53    /// The font size of the value label.
54    #[conrod(default = "14")]
55    pub value_font_size: Option<FontSize>,
56    /// The radius of the envelope points.
57    #[conrod(default = "6.0")]
58    pub point_radius: Option<Scalar>,
59    /// The thickness of the envelope lines.
60    #[conrod(default = "2.0")]
61    pub line_thickness: Option<Scalar>,
62    /// The ID of the font used to display the label.
63    #[conrod(default = "theme.font_id")]
64    pub label_font_id: Option<Option<text::font::Id>>,
65}
66
67widget_ids! {
68    struct Ids {
69        rectangle,
70        label,
71        value_label,
72        point_path,
73        points[],
74    }
75}
76
77/// Represents the state of the EnvelopeEditor widget.
78pub struct State {
79    pressed_point: Option<usize>,
80    ids: Ids,
81}
82
83/// `EnvPoint` must be implemented for any type that is used as a 2D point within the
84/// EnvelopeEditor.
85pub trait EnvelopePoint: Clone + PartialEq {
86    /// A value on the X-axis of the envelope.
87    type X: Float + ToString;
88    /// A value on the Y-axis of the envelope.
89    type Y: Float + ToString;
90    /// Return the X value.
91    fn get_x(&self) -> Self::X;
92    /// Return the Y value.
93    fn get_y(&self) -> Self::Y;
94    /// Set the X value.
95    fn set_x(&mut self, _x: Self::X);
96    /// Set the Y value.
97    fn set_y(&mut self, _y: Self::Y);
98    /// Return the bezier curve depth (-1. to 1.) for the next interpolation.
99    fn get_curve(&self) -> f32 {
100        1.0
101    }
102    /// Set the bezier curve depth (-1. to 1.) for the next interpolation.
103    fn set_curve(&mut self, _curve: f32) {}
104    /// Create a new EnvPoint.
105    fn new(_x: Self::X, _y: Self::Y) -> Self;
106}
107
108impl EnvelopePoint for Point {
109    type X = Scalar;
110    type Y = Scalar;
111    /// Return the X value.
112    fn get_x(&self) -> Scalar {
113        self[0]
114    }
115    /// Return the Y value.
116    fn get_y(&self) -> Scalar {
117        self[1]
118    }
119    /// Return the X value.
120    fn set_x(&mut self, x: Scalar) {
121        self[0] = x
122    }
123    /// Return the Y value.
124    fn set_y(&mut self, y: Scalar) {
125        self[1] = y
126    }
127    /// Create a new Envelope Point.
128    fn new(x: Scalar, y: Scalar) -> Point {
129        [x, y]
130    }
131}
132
133impl<'a, E> EnvelopeEditor<'a, E>
134where
135    E: EnvelopePoint,
136{
137    /// Construct an EnvelopeEditor widget.
138    pub fn new(env: &'a [E], min_x: E::X, max_x: E::X, min_y: E::Y, max_y: E::Y) -> Self {
139        EnvelopeEditor {
140            common: widget::CommonBuilder::default(),
141            style: Style::default(),
142            env: env,
143            skew_y_range: 1.0, // Default skew amount (no skew).
144            min_x: min_x,
145            max_x: max_x,
146            min_y: min_y,
147            max_y: max_y,
148            maybe_label: None,
149            enabled: true,
150        }
151    }
152
153    /// Specify the font used for displaying the label.
154    pub fn label_font_id(mut self, font_id: text::font::Id) -> Self {
155        self.style.label_font_id = Some(Some(font_id));
156        self
157    }
158
159    builder_methods! {
160        pub point_radius { style.point_radius = Some(Scalar) }
161        pub line_thickness { style.line_thickness = Some(Scalar) }
162        pub value_font_size { style.value_font_size = Some(FontSize) }
163        pub skew_y { skew_y_range = f32 }
164        pub enabled { enabled = bool }
165    }
166}
167
168/// The kinds of events that may be yielded by the `EnvelopeEditor`.
169#[derive(Copy, Clone, Debug)]
170pub enum Event<E>
171where
172    E: EnvelopePoint,
173{
174    /// Insert a new point.
175    AddPoint {
176        /// The index at which the point should be inserted.
177        i: usize,
178        /// The new point.
179        point: E,
180    },
181    /// Remove a point.
182    RemovePoint {
183        /// The index of the point that should be removed.
184        i: usize,
185    },
186    /// Move a point.
187    MovePoint {
188        /// The index of the point that should be moved.
189        i: usize,
190        /// The point's new *x* value.
191        x: E::X,
192        /// The point's new *y* value.
193        y: E::Y,
194    },
195}
196
197impl<E> Event<E>
198where
199    E: EnvelopePoint,
200{
201    /// Update the given `envelope` in accordance with the `Event`.
202    pub fn update(self, envelope: &mut Vec<E>) {
203        match self {
204            Event::AddPoint { i, point } => {
205                if i <= envelope.len() {
206                    envelope.insert(i, point);
207                }
208            }
209
210            Event::RemovePoint { i } => {
211                if i < envelope.len() {
212                    envelope.remove(i);
213                }
214            }
215
216            Event::MovePoint { i, x, y } => {
217                let maybe_left = if i == 0 {
218                    None
219                } else {
220                    envelope.get(i - 1).map(|p| p.get_x())
221                };
222                let maybe_right = envelope.get(i + 1).map(|p| p.get_x());
223                if let Some(p) = envelope.get_mut(i) {
224                    let mut set_clamped = |min_x, max_x| {
225                        let x = if x < min_x {
226                            min_x
227                        } else if x > max_x {
228                            max_x
229                        } else {
230                            x
231                        };
232                        p.set_x(x);
233                        p.set_y(y);
234                    };
235                    match (maybe_left, maybe_right) {
236                        (None, None) => set_clamped(x, x),
237                        (Some(min), None) => set_clamped(min, x),
238                        (None, Some(max)) => set_clamped(x, max),
239                        (Some(min), Some(max)) => set_clamped(min, max),
240                    }
241                }
242            }
243        }
244    }
245}
246
247impl<'a, E> Widget for EnvelopeEditor<'a, E>
248where
249    E: EnvelopePoint,
250{
251    type State = State;
252    type Style = Style;
253    type Event = Vec<Event<E>>;
254
255    fn init_state(&self, id_gen: widget::id::Generator) -> Self::State {
256        State {
257            pressed_point: None,
258            ids: Ids::new(id_gen),
259        }
260    }
261
262    fn style(&self) -> Style {
263        self.style.clone()
264    }
265
266    /// Update the `EnvelopeEditor` in accordance to the latest input and call the given `react`
267    /// function if necessary.
268    fn update(self, args: widget::UpdateArgs<Self>) -> Self::Event {
269        let widget::UpdateArgs {
270            id,
271            state,
272            rect,
273            style,
274            mut ui,
275            ..
276        } = args;
277        let EnvelopeEditor {
278            env,
279            skew_y_range,
280            min_x,
281            max_x,
282            min_y,
283            max_y,
284            maybe_label,
285            ..
286        } = self;
287
288        let mut env = std::borrow::Cow::Borrowed(env);
289
290        let point_radius = style.point_radius(ui.theme());
291        let border = style.border(ui.theme());
292        let rel_rect = Rect::from_xy_dim([0.0, 0.0], rect.dim());
293        let inner_rel_rect = rel_rect.pad(border);
294
295        // Converts some envelope point's `x` value to a value in the given `Scalar` range.
296        let map_x_to = |x: E::X, start: Scalar, end: Scalar| -> Scalar {
297            map_range(x, min_x, max_x, start, end)
298        };
299        // Converts some envelope point's `y` value to a value in the given `Scalar` range.
300        let map_y_to = |y: E::Y, start: Scalar, end: Scalar| -> Scalar {
301            let skewed_perc = percentage(y, min_y, max_y).powf(1.0 / skew_y_range);
302            map_range(skewed_perc, 0.0, 1.0, start, end)
303        };
304
305        // Converts some `Scalar` value in the given range to an `x` value for an envelope point.
306        let map_to_x = |value: Scalar, start: Scalar, end: Scalar| -> E::X {
307            map_range(value, start, end, min_x, max_x)
308        };
309        // Converts some `Scalar` value in the given range to an `y` value for an envelope point.
310        let map_to_y = |value: Scalar, start: Scalar, end: Scalar| -> E::Y {
311            let unskewed_perc = percentage(value, start, end).powf(skew_y_range);
312            map_range(unskewed_perc, 0.0, 1.0, min_y, max_y)
313        };
314
315        // Determine the left and right X bounds for a point.
316        let get_x_bounds = |env: &[E], idx: usize| -> (E::X, E::X) {
317            let len = env.len();
318            let right_bound = if len > 0 && len - 1 > idx {
319                env[idx + 1].get_x()
320            } else {
321                max_x
322            };
323            let left_bound = if len > 0 && idx > 0 {
324                env[idx - 1].get_x()
325            } else {
326                min_x
327            };
328            (left_bound, right_bound)
329        };
330
331        // The index of the point that is under the given relative xy position.
332        let point_under_rel_xy = |env: &[E], xy: Point| -> Option<usize> {
333            for i in 0..env.len() {
334                let px = env[i].get_x();
335                let py = env[i].get_y();
336                let x = map_x_to(px, inner_rel_rect.left(), inner_rel_rect.right());
337                let y = map_y_to(py, inner_rel_rect.bottom(), inner_rel_rect.top());
338                let distance = (xy[0] - x).powf(2.0) + (xy[1] - y).powf(2.0);
339                if distance <= point_radius.powf(2.0) {
340                    return Some(i);
341                }
342            }
343            None
344        };
345
346        // Track the currently pressed point if any.
347        let mut pressed_point = state.pressed_point;
348
349        // Handle all events that have occurred to the EnvelopeEditor since the last update.
350        //
351        // Check for:
352        // - New points via left `Click`.
353        // - Remove points via right `Click`.
354        // - Dragging points via left `Drag`.
355        let mut events = Vec::new();
356        'events: for widget_event in ui.widget_input(id).events() {
357            use event;
358            use input::{self, MouseButton};
359
360            match widget_event {
361                // Upon mouse press, check for:
362                //
363                // - Point insertion
364                // - Point removal
365                // - The beggining of a point drag
366                event::Widget::Press(press) => match press.button {
367                    // Left mouse press:
368                    //
369                    // If the mouse is currently over an existing point, we want to begin
370                    // dragging it.
371                    //
372                    // Otherwise, if the mouse is not over an existing point, we want to insert
373                    // a new point and begin dragging it.
374                    event::Button::Mouse(MouseButton::Left, xy) => {
375                        // In this loop, we find the points on either side of the mouse to
376                        // determine the insertion index, while checking if we need to break early
377                        // in the case that the mouse is already over a point.
378                        let mut maybe_left = None;
379                        let mut maybe_right = None;
380                        for (i, p) in env.iter().enumerate() {
381                            let px = p.get_x();
382                            let py = p.get_y();
383                            let x = map_x_to(px, inner_rel_rect.left(), inner_rel_rect.right());
384                            let y = map_y_to(py, inner_rel_rect.bottom(), inner_rel_rect.top());
385                            let distance = (xy[0] - x).powf(2.0) + (xy[1] - y).powf(2.0);
386
387                            // If the press was over a point, begin dragging it and we're done.
388                            if distance <= point_radius.powf(2.0) {
389                                pressed_point = Some(i);
390                                continue 'events;
391                            }
392
393                            if x <= xy[0] {
394                                maybe_left = Some(i);
395                            } else if maybe_right.is_none() {
396                                maybe_right = Some(i);
397                                break;
398                            }
399                        }
400
401                        // We only want to insert a point if the mouse is in the inner rectangle
402                        // and not on the border.
403                        if !inner_rel_rect.is_over(xy) {
404                            continue 'events;
405                        }
406
407                        let new_x = map_to_x(xy[0], inner_rel_rect.left(), inner_rel_rect.right());
408                        let new_y = map_to_y(xy[1], inner_rel_rect.bottom(), inner_rel_rect.top());
409                        let new_point = EnvelopePoint::new(new_x, new_y);
410
411                        // Insert the point and push an `AddPoint` event.
412                        match (maybe_left, maybe_right) {
413                            (Some(_), None) | (None, None) => {
414                                let idx = env.len();
415                                let event = Event::AddPoint {
416                                    i: idx,
417                                    point: new_point,
418                                };
419                                pressed_point = Some(idx);
420                                events.push(event);
421                            }
422                            (None, Some(_)) => {
423                                let idx = 0;
424                                let event = Event::AddPoint {
425                                    i: idx,
426                                    point: new_point,
427                                };
428                                pressed_point = Some(idx);
429                                events.push(event);
430                            }
431                            (Some(_), Some(idx)) => {
432                                let event = Event::AddPoint {
433                                    i: idx,
434                                    point: new_point,
435                                };
436                                pressed_point = Some(idx);
437                                events.push(event);
438                            }
439                        }
440                    }
441
442                    // If the right mouse button was pressed over a point that is not currently
443                    // being dragged, remove the point.
444                    event::Button::Mouse(MouseButton::Right, xy) => {
445                        if pressed_point.is_some() || !inner_rel_rect.is_over(xy) {
446                            continue 'events;
447                        }
448
449                        if let Some(idx) = point_under_rel_xy(&env, xy) {
450                            let event = Event::RemovePoint { i: idx };
451                            events.push(event);
452                        }
453                    }
454
455                    _ => (),
456                },
457
458                // Check to see if a point was released in case it is later dragged.
459                event::Widget::Release(release) => {
460                    if let event::Button::Mouse(MouseButton::Left, _) = release.button {
461                        pressed_point = None;
462                    }
463                }
464
465                // A left `Drag` moves the `pressed_point` if there is one.
466                event::Widget::Drag(drag) if drag.button == input::MouseButton::Left => {
467                    if let Some(idx) = pressed_point {
468                        let drag_to_x_clamped = inner_rel_rect.x.clamp_value(drag.to[0]);
469                        let drag_to_y_clamped = inner_rel_rect.y.clamp_value(drag.to[1]);
470                        let unbounded_x = map_to_x(
471                            drag_to_x_clamped,
472                            inner_rel_rect.left(),
473                            inner_rel_rect.right(),
474                        );
475                        let (left_bound, right_bound) = get_x_bounds(&env, idx);
476                        let new_x = clamp(unbounded_x, left_bound, right_bound);
477                        let new_y = map_to_y(
478                            drag_to_y_clamped,
479                            inner_rel_rect.bottom(),
480                            inner_rel_rect.top(),
481                        );
482                        let event = Event::MovePoint {
483                            i: idx,
484                            x: new_x,
485                            y: new_y,
486                        };
487                        events.push(event);
488                    }
489                }
490
491                _ => (),
492            }
493        }
494
495        if state.pressed_point != pressed_point {
496            state.update(|state| state.pressed_point = pressed_point);
497        }
498
499        // Ensure that the local version of the `env` is up to date for drawing.
500        for event in &events {
501            event.clone().update(env.to_mut());
502        }
503
504        let inner_rect = rect.pad(border);
505        let dim = rect.dim();
506        let border = style.border(ui.theme());
507        let color = style.color(ui.theme());
508        let color = ui
509            .widget_input(id)
510            .mouse()
511            .and_then(|m| {
512                if inner_rect.is_over(m.abs_xy()) {
513                    Some(color.highlighted())
514                } else {
515                    None
516                }
517            })
518            .unwrap_or(color);
519        let border_color = style.border_color(ui.theme());
520        widget::BorderedRectangle::new(dim)
521            .middle_of(id)
522            .graphics_for(id)
523            .color(color)
524            .border(border)
525            .border_color(border_color)
526            .set(state.ids.rectangle, ui);
527
528        let font_id = style.label_font_id(&ui.theme).or(ui.fonts.ids().next());
529        let label_color = style.label_color(&ui.theme);
530        if let Some(label) = maybe_label {
531            let font_size = style.label_font_size(&ui.theme);
532            widget::Text::new(label)
533                .and_then(font_id, widget::Text::font_id)
534                .middle_of(state.ids.rectangle)
535                .graphics_for(id)
536                .color(label_color)
537                .font_size(font_size)
538                .set(state.ids.label, ui);
539        }
540
541        let line_color = label_color.with_alpha(1.0);
542        {
543            let thickness = style.line_thickness(ui.theme());
544            let points = env.iter().map(|point| {
545                let x = map_x_to(point.get_x(), inner_rect.left(), inner_rect.right());
546                let y = map_y_to(point.get_y(), inner_rect.bottom(), inner_rect.top());
547                [x, y]
548            });
549            widget::PointPath::new(points)
550                .wh(inner_rect.dim())
551                .xy(inner_rect.xy())
552                .graphics_for(id)
553                .parent(id)
554                .color(line_color)
555                .thickness(thickness)
556                .set(state.ids.point_path, ui);
557        }
558
559        // Ensure we have at least as many point widgets as there are points in the env.
560        if state.ids.points.len() < env.len() {
561            state.update(|state| {
562                state
563                    .ids
564                    .points
565                    .resize(env.len(), &mut ui.widget_id_generator())
566            });
567        }
568
569        let iter = state.ids.points.iter().zip(env.iter()).enumerate();
570        for (i, (&point_id, point)) in iter {
571            let x = map_x_to(point.get_x(), inner_rect.left(), inner_rect.right());
572            let y = map_y_to(point.get_y(), inner_rect.bottom(), inner_rect.top());
573            let point_color = if state.pressed_point == Some(i) {
574                line_color.clicked()
575            } else {
576                ui.widget_input(id)
577                    .mouse()
578                    .and_then(|mouse| {
579                        let mouse_abs_xy = mouse.abs_xy();
580                        let distance =
581                            (mouse_abs_xy[0] - x).powf(2.0) + (mouse_abs_xy[1] - y).powf(2.0);
582                        if distance <= point_radius.powf(2.0) {
583                            Some(line_color.highlighted())
584                        } else {
585                            None
586                        }
587                    })
588                    .unwrap_or(line_color)
589            };
590            widget::Circle::fill(point_radius)
591                .color(point_color)
592                .x_y(x, y)
593                .graphics_for(id)
594                .parent(id)
595                .set(point_id, &mut ui);
596        }
597
598        // Find the closest point to the mouse.
599        let maybe_closest_point = ui.widget_input(id).mouse().and_then(|mouse| {
600            let mut closest_distance = ::std::f64::MAX;
601            let mut closest_point = None;
602            for (i, p) in env.iter().enumerate() {
603                let px = p.get_x();
604                let py = p.get_y();
605                let x = map_x_to(px, inner_rect.left(), inner_rect.right());
606                let y = map_y_to(py, inner_rect.bottom(), inner_rect.top());
607                let mouse_abs_xy = mouse.abs_xy();
608                let distance = (mouse_abs_xy[0] - x).powf(2.0) + (mouse_abs_xy[1] - y).powf(2.0);
609                if distance < closest_distance {
610                    closest_distance = distance;
611                    closest_point = Some((i, (x, y)));
612                }
613            }
614            closest_point
615        });
616
617        if let Some((closest_idx, (x, y))) = maybe_closest_point {
618            let x_range = max_x - min_x;
619            let y_range = max_y - min_y;
620            let x_px_range = inner_rect.w() as usize;
621            let y_px_range = inner_rect.h() as usize;
622            let x_string = val_to_string(env[closest_idx].get_x(), max_x, x_range, x_px_range);
623            let y_string = val_to_string(env[closest_idx].get_y(), max_y, y_range, y_px_range);
624            let xy_string = format!("{}, {}", x_string, y_string);
625            let x_direction = match inner_rect.x.closest_edge(x) {
626                Edge::End => Direction::Backwards,
627                Edge::Start => Direction::Forwards,
628            };
629            let y_direction = match inner_rect.y.closest_edge(y) {
630                Edge::End => Direction::Backwards,
631                Edge::Start => Direction::Forwards,
632            };
633            let value_font_size = style.value_font_size(ui.theme());
634            let closest_point_id = state.ids.points[closest_idx];
635            const VALUE_TEXT_PAD: f64 = 5.0; // Slight padding between the point and the text.
636            widget::Text::new(&xy_string)
637                .and_then(font_id, widget::Text::font_id)
638                .x_direction_from(closest_point_id, x_direction, VALUE_TEXT_PAD)
639                .y_direction_from(closest_point_id, y_direction, VALUE_TEXT_PAD)
640                .color(line_color)
641                .graphics_for(id)
642                .parent(id)
643                .font_size(value_font_size)
644                .set(state.ids.value_label, ui);
645        }
646
647        events
648    }
649}
650
651impl<'a, E> Colorable for EnvelopeEditor<'a, E>
652where
653    E: EnvelopePoint,
654{
655    builder_method!(color { style.color = Some(Color) });
656}
657
658impl<'a, E> Borderable for EnvelopeEditor<'a, E>
659where
660    E: EnvelopePoint,
661{
662    builder_methods! {
663        border { style.border = Some(Scalar) }
664        border_color { style.border_color = Some(Color) }
665    }
666}
667
668impl<'a, E> Labelable<'a> for EnvelopeEditor<'a, E>
669where
670    E: EnvelopePoint,
671{
672    builder_methods! {
673        label { maybe_label = Some(&'a str) }
674        label_color { style.label_color = Some(Color) }
675        label_font_size { style.label_font_size = Some(FontSize) }
676    }
677}