1use iced::{
3 Color, Element, Event, Length, Point, Rectangle, Renderer, Theme, mouse,
4 widget::{
5 button, canvas,
6 canvas::{Action, Frame, Geometry, Path, Program, Stroke, Text},
7 column, row, text,
8 },
9};
10
11#[derive(Debug, Clone, Copy, PartialEq)]
14pub enum AdsrMessage {
15 Change(usize, f32),
16 Release(usize),
17 Reset(usize),
18 Assign(usize),
19}
20
21pub fn adsr_envelope<'a>(
27 values: [f32; 4],
28 ranges: [(f32, f32); 4],
29 steps: [f32; 4],
30 assigned: [bool; 4],
31) -> Element<'a, AdsrMessage> {
32 let graph = Envelope {
33 values,
34 ranges,
35 steps,
36 assigned,
37 };
38 let labels = row((0..4).map(|i| {
39 let value = if i == 2 {
40 format!("{:.0}%", values[i] * 100.0)
41 } else {
42 format!("{:.2}s", values[i])
43 };
44 button(column![
45 text(["Attack", "Decay", "Sustain", "Release"][i]).size(10),
46 text(value).size(10)
47 ])
48 .padding(2)
49 .width(Length::Fill)
50 .style(button::text)
51 .on_press(AdsrMessage::Assign(i))
52 .into()
53 }))
54 .width(210);
55 column![canvas(graph).width(210).height(115), labels]
56 .spacing(3)
57 .into()
58}
59
60struct Envelope {
61 values: [f32; 4],
62 ranges: [(f32, f32); 4],
63 steps: [f32; 4],
64 assigned: [bool; 4],
65}
66
67#[derive(Default)]
68struct DragState {
69 drag: Option<(usize, Point, f32)>,
70}
71
72impl Envelope {
73 fn time_position(value: f32, range: (f32, f32)) -> f32 {
74 let norm = ((value - range.0) / (range.1 - range.0).max(f32::EPSILON)).clamp(0.0, 1.0);
75 (1.0 + norm * 100.0).ln() / 101.0_f32.ln()
76 }
77 fn time_value(position: f32, range: (f32, f32)) -> f32 {
78 let norm = (101.0_f32.powf(position.clamp(0.0, 1.0)) - 1.0) / 100.0;
79 range.0 + norm * (range.1 - range.0)
80 }
81 fn points(&self) -> [Point; 5] {
82 let attack = 12.0 + 28.0 * Self::time_position(self.values[0], self.ranges[0]);
83 let decay = attack + 12.0 + 28.0 * Self::time_position(self.values[1], self.ranges[1]);
84 let sustain_range = self.ranges[2];
85 let sustain_norm = ((self.values[2] - sustain_range.0)
86 / (sustain_range.1 - sustain_range.0).max(f32::EPSILON))
87 .clamp(0.0, 1.0);
88 let sustain = 94.0 - sustain_norm * 72.0;
89 [
90 Point::new(12.0, 94.0),
91 Point::new(attack, 22.0),
92 Point::new(decay, sustain),
93 Point::new(decay + 36.0, sustain),
94 Point::new(
95 decay + 48.0 + 28.0 * Self::time_position(self.values[3], self.ranges[3]),
96 94.0,
97 ),
98 ]
99 }
100 fn hit(&self, point: Point) -> Option<usize> {
101 self.points()[1..]
102 .iter()
103 .position(|p| p.distance(point) <= 9.0)
104 }
105}
106
107impl Program<AdsrMessage> for Envelope {
108 type State = DragState;
109 fn update(
110 &self,
111 state: &mut DragState,
112 event: &Event,
113 bounds: Rectangle,
114 cursor: mouse::Cursor,
115 ) -> Option<Action<AdsrMessage>> {
116 match event {
117 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
118 let point = cursor.position_in(bounds)?;
119 let i = self.hit(point)?;
120 state.drag = Some((i, point, self.values[i]));
121 Some(Action::capture())
122 }
123 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right)) => {
124 let i = self.hit(cursor.position_in(bounds)?)?;
125 Some(Action::publish(AdsrMessage::Reset(i)).and_capture())
126 }
127 Event::Mouse(mouse::Event::CursorMoved { position }) => {
128 let (i, start, initial) = state.drag?;
129 let point = Point::new(position.x - bounds.x, position.y - bounds.y);
130 let range = self.ranges[i];
131 let value = if i == 2 {
132 let norm = (initial - range.0) / (range.1 - range.0).max(f32::EPSILON)
133 - (point.y - start.y) / 72.0;
134 range.0 + norm * (range.1 - range.0)
135 } else {
136 Self::time_value(
137 Self::time_position(initial, range) + (point.x - start.x) / 28.0,
138 range,
139 )
140 };
141 let step = self.steps[i].max(f32::EPSILON);
142 let value = ((value / step).round() * step).clamp(range.0, range.1);
143 Some(Action::publish(AdsrMessage::Change(i, value)).and_capture())
144 }
145 Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
146 | Event::Window(iced::window::Event::Unfocused) => {
147 let (i, _, _) = state.drag.take()?;
148 Some(Action::publish(AdsrMessage::Release(i)).and_capture())
149 }
150 _ => None,
151 }
152 }
153 fn draw(
154 &self,
155 _state: &DragState,
156 renderer: &Renderer,
157 theme: &Theme,
158 bounds: Rectangle,
159 _cursor: mouse::Cursor,
160 ) -> Vec<Geometry> {
161 let mut frame = Frame::new(renderer, bounds.size());
162 let palette = theme.extended_palette();
163 let ink = palette.background.base.text;
164 let accent = palette.primary.base.color;
165 let points = self.points();
166 let shape = Path::new(|p| {
167 p.move_to(points[0]);
168 for point in &points[1..] {
169 p.line_to(*point);
170 }
171 p.close();
172 });
173 frame.fill(&shape, Color { a: 0.18, ..accent });
174 let curve = Path::new(|p| {
175 p.move_to(points[0]);
176 for point in &points[1..] {
177 p.line_to(*point);
178 }
179 });
180 frame.stroke(&curve, Stroke::default().with_color(accent).with_width(2.0));
181 frame.stroke(
182 &Path::new(|p| {
183 p.move_to(Point::new(12.0, 16.0));
184 p.line_to(points[0]);
185 p.line_to(Point::new(205.0, 94.0));
186 }),
187 Stroke::default().with_color(ink),
188 );
189 for (i, point) in points[1..].iter().enumerate() {
190 let mut y = point.y + 7.0;
191 while y < 101.0 {
192 frame.stroke(
193 &Path::line(
194 Point::new(point.x, y),
195 Point::new(point.x, (y + 2.0).min(101.0)),
196 ),
197 Stroke::default().with_color(Color { a: 0.4, ..ink }),
198 );
199 y += 5.0;
200 }
201 frame.fill(
202 &Path::circle(*point, 4.0),
203 if self.assigned[i] {
204 Color::from_rgb(1.0, 0.83, 0.10)
205 } else {
206 accent
207 },
208 );
209 }
210 for (label, position) in [
211 ("level", Point::new(0.0, 0.0)),
212 ("time", Point::new(183.0, 102.0)),
213 ] {
214 frame.fill_text(Text {
215 content: label.into(),
216 position,
217 color: ink,
218 size: 10.0.into(),
219 ..Text::default()
220 });
221 }
222 vec![frame.into_geometry()]
223 }
224 fn mouse_interaction(
225 &self,
226 state: &DragState,
227 bounds: Rectangle,
228 cursor: mouse::Cursor,
229 ) -> mouse::Interaction {
230 if state.drag.is_some() {
231 mouse::Interaction::Grabbing
232 } else if cursor
233 .position_in(bounds)
234 .and_then(|p| self.hit(p))
235 .is_some()
236 {
237 mouse::Interaction::Grab
238 } else {
239 mouse::Interaction::default()
240 }
241 }
242}