1use 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#[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 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#[derive(Copy, Clone, Debug, Default, PartialEq, WidgetStyle_)]
37pub struct Style {
38 #[conrod(default = "theme.shape_color")]
40 pub color: Option<Color>,
41 #[conrod(default = "theme.border_width")]
43 pub border: Option<f64>,
44 #[conrod(default = "theme.border_color")]
46 pub border_color: Option<Color>,
47 #[conrod(default = "theme.label_color")]
49 pub label_color: Option<Color>,
50 #[conrod(default = "theme.font_size_medium")]
52 pub label_font_size: Option<FontSize>,
53 #[conrod(default = "14")]
55 pub value_font_size: Option<FontSize>,
56 #[conrod(default = "6.0")]
58 pub point_radius: Option<Scalar>,
59 #[conrod(default = "2.0")]
61 pub line_thickness: Option<Scalar>,
62 #[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
77pub struct State {
79 pressed_point: Option<usize>,
80 ids: Ids,
81}
82
83pub trait EnvelopePoint: Clone + PartialEq {
86 type X: Float + ToString;
88 type Y: Float + ToString;
90 fn get_x(&self) -> Self::X;
92 fn get_y(&self) -> Self::Y;
94 fn set_x(&mut self, _x: Self::X);
96 fn set_y(&mut self, _y: Self::Y);
98 fn get_curve(&self) -> f32 {
100 1.0
101 }
102 fn set_curve(&mut self, _curve: f32) {}
104 fn new(_x: Self::X, _y: Self::Y) -> Self;
106}
107
108impl EnvelopePoint for Point {
109 type X = Scalar;
110 type Y = Scalar;
111 fn get_x(&self) -> Scalar {
113 self[0]
114 }
115 fn get_y(&self) -> Scalar {
117 self[1]
118 }
119 fn set_x(&mut self, x: Scalar) {
121 self[0] = x
122 }
123 fn set_y(&mut self, y: Scalar) {
125 self[1] = y
126 }
127 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 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, 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 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#[derive(Copy, Clone, Debug)]
170pub enum Event<E>
171where
172 E: EnvelopePoint,
173{
174 AddPoint {
176 i: usize,
178 point: E,
180 },
181 RemovePoint {
183 i: usize,
185 },
186 MovePoint {
188 i: usize,
190 x: E::X,
192 y: E::Y,
194 },
195}
196
197impl<E> Event<E>
198where
199 E: EnvelopePoint,
200{
201 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 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 let map_x_to = |x: E::X, start: Scalar, end: Scalar| -> Scalar {
297 map_range(x, min_x, max_x, start, end)
298 };
299 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 let map_to_x = |value: Scalar, start: Scalar, end: Scalar| -> E::X {
307 map_range(value, start, end, min_x, max_x)
308 };
309 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 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 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 let mut pressed_point = state.pressed_point;
348
349 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 event::Widget::Press(press) => match press.button {
367 event::Button::Mouse(MouseButton::Left, xy) => {
375 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 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 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 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 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 event::Widget::Release(release) => {
460 if let event::Button::Mouse(MouseButton::Left, _) = release.button {
461 pressed_point = None;
462 }
463 }
464
465 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 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 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 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; 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}