Skip to main content

maolan_widgets/
adsr.rs

1//! Interactive attack, decay, sustain, and release envelope editor.
2use 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/// Edits emitted by [`adsr_envelope`]. Stage indices follow attack, decay,
12/// sustain, release order.
13#[derive(Debug, Clone, Copy, PartialEq)]
14pub enum AdsrMessage {
15    Change(usize, f32),
16    Release(usize),
17    Reset(usize),
18    Assign(usize),
19}
20
21/// Draw an interactive ADSR envelope.
22///
23/// `ranges` and `steps` use attack, decay, sustain, release order.
24/// Time stages are spaced logarithmically. Right-clicking a handle resets its
25/// stage; stage labels emit `Assign` for hosts that support modulation routing.
26pub 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}